Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package mcp-bugzilla for openSUSE:Factory checked in at 2026-08-18 16:37:37 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/mcp-bugzilla (Old) and /work/SRC/openSUSE:Factory/.mcp-bugzilla.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "mcp-bugzilla" Tue Aug 18 16:37:37 2026 rev:3 rq:1371682 version:0.20.0 Changes: -------- --- /work/SRC/openSUSE:Factory/mcp-bugzilla/mcp-bugzilla.changes 2026-08-12 16:12:02.933309713 +0200 +++ /work/SRC/openSUSE:Factory/.mcp-bugzilla.new.1258/mcp-bugzilla.changes 2026-08-18 16:38:23.001010767 +0200 @@ -1,0 +2,15 @@ +Tue Aug 18 07:40:07 UTC 2026 - Martin Pluskal <[email protected]> + +- Update to version 0.20.0: + * Add get_component_defaults tool to look up a component's + default assignee and QA contact, either by product/component + or resolved from a bug id + * update_bug_fields gained reset_qa_contact and + reset_assigned_to to reset those fields to the component's + defaults + * Add --tool-search flag (MCP_TOOL_SEARCH env) to collapse all + tools into search_tools + call_tool meta-tools using BM25 + relevance ranking + * Bump fastmcp requirement accordingly + +------------------------------------------------------------------- Old: ---- mcp_bugzilla-0.19.0.tar.gz New: ---- mcp_bugzilla-0.20.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ mcp-bugzilla.spec ++++++ --- /var/tmp/diff_new_pack.54Kz57/_old 2026-08-18 16:38:23.944044512 +0200 +++ /var/tmp/diff_new_pack.54Kz57/_new 2026-08-18 16:38:23.945044548 +0200 @@ -21,20 +21,20 @@ # not a multi-flavour module. %define pythons %{primary_python} Name: mcp-bugzilla -Version: 0.19.0 +Version: 0.20.0 Release: 0 Summary: Model Context Protocol server for Bugzilla License: Apache-2.0 URL: https://github.com/openSUSE/mcp-bugzilla Source: https://files.pythonhosted.org/packages/source/m/mcp_bugzilla/mcp_bugzilla-%{version}.tar.gz -BuildRequires: %{primary_python}-fastmcp >= 3.4.6 +BuildRequires: %{primary_python}-fastmcp >= 3.4.7 BuildRequires: %{primary_python}-httpx-retries >= 0.5.0 BuildRequires: %{primary_python}-pip BuildRequires: %{primary_python}-uv-build BuildRequires: %{primary_python}-wheel BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: %{primary_python}-fastmcp >= 3.4.6 +Requires: %{primary_python}-fastmcp >= 3.4.7 Requires: %{primary_python}-httpx-retries >= 0.5.0 Provides: bugzilla-mcp = %{version}-%{release} Obsoletes: bugzilla-mcp < %{version} ++++++ mcp_bugzilla-0.19.0.tar.gz -> mcp_bugzilla-0.20.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mcp_bugzilla-0.19.0/PKG-INFO new/mcp_bugzilla-0.20.0/PKG-INFO --- old/mcp_bugzilla-0.19.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/mcp_bugzilla-0.20.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,8 +1,8 @@ Metadata-Version: 2.3 Name: mcp-bugzilla -Version: 0.19.0 +Version: 0.20.0 Summary: MCP server for Bugzilla -Requires-Dist: fastmcp==3.4.6 +Requires-Dist: fastmcp==3.4.7 Requires-Dist: httpx-retries>=0.5.0 Requires-Dist: pytest ; extra == 'dev' Requires-Dist: pytest-asyncio ; extra == 'dev' @@ -70,6 +70,14 @@ - **Note on Bugzilla API parity**: `new_since` and `include_fields` mirror Bugzilla's own API parameters of the same names (`include_fields` is applied client-side here). `exclude_creators` and `limit` have no server-side Bugzilla equivalent — this server applies them client-side, as post-processing over the standard `Bug.comments` response; the underlying Bugzilla request is unmodified. - **Example**: `bug_comments(12345, exclude_creators="upstream-release-monitoring", limit=5)` returns the 5 most recent comments that aren't from the release-monitoring bot +- **`get_component_defaults(component: str = None, product: str = None, bug_id: int = None)`**: Looks up a component's default assignee and QA contact, read from the parent product since Bugzilla has no component endpoint. + - **Parameters**: + - `component`: Component name (resolved from `bug_id` if omitted) + - `product`: Product the component belongs to (resolved from `bug_id` if omitted) + - `bug_id`: Resolve product/component from this bug instead of passing them + - **Returns**: A dictionary with `product`, `component`, `default_assignee`, `default_qa_contact`, `is_active` + - **Example**: `get_component_defaults(bug_id=12345)` returns the defaults for that bug's component. To reset a bug *to* these defaults, use `update_bug_fields(..., reset_qa_contact=True)`. + - **`add_comment(bug_id: int, comment: str, is_private: bool = False)`**: Adds a new comment to a specified bug. - **Parameters**: @@ -126,16 +134,20 @@ + **Returns**: A dictionary containing the updated bug fields + **Example**: `assign_bug(12345, "[email protected]", comment="You're the expert on this component")` -* **`update_bug_fields(bug_id: int, priority: str = None, severity: str = None, resolution: str = None, comment: str = "")`**: Updates various bug fields. +* **`update_bug_fields(bug_id: int, priority: str = None, severity: str = None, resolution: str = None, custom_fields: dict = None, reset_qa_contact: bool = False, reset_assigned_to: bool = False, comment: str = "")`**: Updates various bug fields. + **Parameters**: - `bug_id`: The bug ID to update - `priority`: Priority level (e.g., `urgent`, `high`, `medium`, `low`, `unspecified`) - `severity`: Severity level (e.g., `urgent`, `high`, `medium`, `low`, `unspecified`) - `resolution`: Resolution (only for closed bugs) + - `custom_fields`: Dict of custom fields, e.g. `{"cf_fixed_in": "1.2.3"}` + - `reset_qa_contact`: If `True`, reset the QA contact to the component's default + - `reset_assigned_to`: If `True`, reset the assignee to the component's default - `comment`: Optional comment explaining the changes + **Returns**: A dictionary containing the updated bug fields + **Example**: `update_bug_fields(12345, priority="high", severity="urgent", comment="Escalating due to customer impact")` + + **Example**: `update_bug_fields(12345, reset_qa_contact=True)` resets the QA contact to the component default * **`add_cc_to_bug(bug_id: int, cc_email: str)`**: Adds an email address to the CC list of a bug. @@ -330,6 +342,7 @@ | `--bugzilla-api-key <KEY>` | `BUGZILLA_API_KEY` | *none* | Static Bugzilla API key. Optional: if omitted and not provided per-request via `--mcp-auth-header` (http), access is **anonymous**. For `--transport stdio` this is the only source of the key | | `--bugzilla-auth-mode {query,bearer}` | `BUGZILLA_AUTH_MODE` | `query` | How to authenticate with Bugzilla: `query` sends `?api_key=<KEY>` (default, works with most instances); `bearer` sends `Authorization: Bearer <KEY>` header (required for Red Hat Bugzilla and similar) | | `--read-only` | `MCP_READ_ONLY` | `False` | Disables all tools which can modify a bug. Works well in conjunction with `MCP_BUGZILLA_DISABLED_METHODS` | +| `--tool-search` | `MCP_TOOL_SEARCH` | `False` | Collapse all tools into `search_tools` + `call_tool` meta-tools using BM25 relevance ranking. The LLM searches for tools by natural language query and calls them directly. Reduces context overhead when many tools are available | | `--download-dir <DIR>` | `BUGZILLA_DOWNLOAD_DIR` | `<tmpdir>/mcp-bugzilla` | Directory where `download_attachment` writes binary/oversized attachments. The default directory is created on first use and restricted to the owner (`0o700`); an explicit `output_dir` keeps its own permissions | **Note**: `--host` and `--port` are rejected with an error when used together with `--transport stdio`. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mcp_bugzilla-0.19.0/README.md new/mcp_bugzilla-0.20.0/README.md --- old/mcp_bugzilla-0.19.0/README.md 1970-01-01 01:00:00.000000000 +0100 +++ new/mcp_bugzilla-0.20.0/README.md 1970-01-01 01:00:00.000000000 +0100 @@ -57,6 +57,14 @@ - **Note on Bugzilla API parity**: `new_since` and `include_fields` mirror Bugzilla's own API parameters of the same names (`include_fields` is applied client-side here). `exclude_creators` and `limit` have no server-side Bugzilla equivalent — this server applies them client-side, as post-processing over the standard `Bug.comments` response; the underlying Bugzilla request is unmodified. - **Example**: `bug_comments(12345, exclude_creators="upstream-release-monitoring", limit=5)` returns the 5 most recent comments that aren't from the release-monitoring bot +- **`get_component_defaults(component: str = None, product: str = None, bug_id: int = None)`**: Looks up a component's default assignee and QA contact, read from the parent product since Bugzilla has no component endpoint. + - **Parameters**: + - `component`: Component name (resolved from `bug_id` if omitted) + - `product`: Product the component belongs to (resolved from `bug_id` if omitted) + - `bug_id`: Resolve product/component from this bug instead of passing them + - **Returns**: A dictionary with `product`, `component`, `default_assignee`, `default_qa_contact`, `is_active` + - **Example**: `get_component_defaults(bug_id=12345)` returns the defaults for that bug's component. To reset a bug *to* these defaults, use `update_bug_fields(..., reset_qa_contact=True)`. + - **`add_comment(bug_id: int, comment: str, is_private: bool = False)`**: Adds a new comment to a specified bug. - **Parameters**: @@ -113,16 +121,20 @@ + **Returns**: A dictionary containing the updated bug fields + **Example**: `assign_bug(12345, "[email protected]", comment="You're the expert on this component")` -* **`update_bug_fields(bug_id: int, priority: str = None, severity: str = None, resolution: str = None, comment: str = "")`**: Updates various bug fields. +* **`update_bug_fields(bug_id: int, priority: str = None, severity: str = None, resolution: str = None, custom_fields: dict = None, reset_qa_contact: bool = False, reset_assigned_to: bool = False, comment: str = "")`**: Updates various bug fields. + **Parameters**: - `bug_id`: The bug ID to update - `priority`: Priority level (e.g., `urgent`, `high`, `medium`, `low`, `unspecified`) - `severity`: Severity level (e.g., `urgent`, `high`, `medium`, `low`, `unspecified`) - `resolution`: Resolution (only for closed bugs) + - `custom_fields`: Dict of custom fields, e.g. `{"cf_fixed_in": "1.2.3"}` + - `reset_qa_contact`: If `True`, reset the QA contact to the component's default + - `reset_assigned_to`: If `True`, reset the assignee to the component's default - `comment`: Optional comment explaining the changes + **Returns**: A dictionary containing the updated bug fields + **Example**: `update_bug_fields(12345, priority="high", severity="urgent", comment="Escalating due to customer impact")` + + **Example**: `update_bug_fields(12345, reset_qa_contact=True)` resets the QA contact to the component default * **`add_cc_to_bug(bug_id: int, cc_email: str)`**: Adds an email address to the CC list of a bug. @@ -317,6 +329,7 @@ | `--bugzilla-api-key <KEY>` | `BUGZILLA_API_KEY` | *none* | Static Bugzilla API key. Optional: if omitted and not provided per-request via `--mcp-auth-header` (http), access is **anonymous**. For `--transport stdio` this is the only source of the key | | `--bugzilla-auth-mode {query,bearer}` | `BUGZILLA_AUTH_MODE` | `query` | How to authenticate with Bugzilla: `query` sends `?api_key=<KEY>` (default, works with most instances); `bearer` sends `Authorization: Bearer <KEY>` header (required for Red Hat Bugzilla and similar) | | `--read-only` | `MCP_READ_ONLY` | `False` | Disables all tools which can modify a bug. Works well in conjunction with `MCP_BUGZILLA_DISABLED_METHODS` | +| `--tool-search` | `MCP_TOOL_SEARCH` | `False` | Collapse all tools into `search_tools` + `call_tool` meta-tools using BM25 relevance ranking. The LLM searches for tools by natural language query and calls them directly. Reduces context overhead when many tools are available | | `--download-dir <DIR>` | `BUGZILLA_DOWNLOAD_DIR` | `<tmpdir>/mcp-bugzilla` | Directory where `download_attachment` writes binary/oversized attachments. The default directory is created on first use and restricted to the owner (`0o700`); an explicit `output_dir` keeps its own permissions | **Note**: `--host` and `--port` are rejected with an error when used together with `--transport stdio`. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mcp_bugzilla-0.19.0/pyproject.toml new/mcp_bugzilla-0.20.0/pyproject.toml --- old/mcp_bugzilla-0.19.0/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/mcp_bugzilla-0.20.0/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 @@ -1,11 +1,11 @@ [project] name = "mcp-bugzilla" -version = "0.19.0" +version = "0.20.0" description = "MCP server for Bugzilla" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "fastmcp==3.4.6", + "fastmcp==3.4.7", "httpx-retries>=0.5.0", ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mcp_bugzilla-0.19.0/pyproject.toml.orig new/mcp_bugzilla-0.20.0/pyproject.toml.orig --- old/mcp_bugzilla-0.19.0/pyproject.toml.orig 1970-01-01 01:00:00.000000000 +0100 +++ new/mcp_bugzilla-0.20.0/pyproject.toml.orig 1970-01-01 01:00:00.000000000 +0100 @@ -1,12 +1,12 @@ [project] name = "mcp-bugzilla" -version = "0.19.0" +version = "0.20.0" description = "MCP server for Bugzilla" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "fastmcp==3.4.6", + "fastmcp==3.4.7", "httpx-retries>=0.5.0", ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mcp_bugzilla-0.19.0/src/mcp_bugzilla/__init__.py new/mcp_bugzilla-0.20.0/src/mcp_bugzilla/__init__.py --- old/mcp_bugzilla-0.19.0/src/mcp_bugzilla/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/mcp_bugzilla-0.20.0/src/mcp_bugzilla/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -82,6 +82,13 @@ ) parser.add_argument( + "--tool-search", + action="store_true", + default=os.getenv("MCP_TOOL_SEARCH", "false").lower() == "true", + help="Collapse all tools into search_tools + call_tool meta-tools using BM25 relevance ranking. Environment variable MCP_TOOL_SEARCH=true can also be used.", + ) + + parser.add_argument( "--transport", type=str, choices=["http", "stdio"], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mcp_bugzilla-0.19.0/src/mcp_bugzilla/lib_bugzilla.py new/mcp_bugzilla-0.20.0/src/mcp_bugzilla/lib_bugzilla.py --- old/mcp_bugzilla-0.19.0/src/mcp_bugzilla/lib_bugzilla.py 1970-01-01 01:00:00.000000000 +0100 +++ new/mcp_bugzilla-0.20.0/src/mcp_bugzilla/lib_bugzilla.py 1970-01-01 01:00:00.000000000 +0100 @@ -197,6 +197,46 @@ mcp_log.debug(f"[BZ-RES] {envelope}") return envelope + async def get_product( + self, name: str, include_fields: str | None = None + ) -> dict[str, Any]: + """Fetch a product by name, with its components and their defaults. + + Bugzilla has no component endpoint: component defaults (assignee, QA + contact) are only exposed nested under the parent product. The full + payload is large, so pass ``include_fields`` (dotted paths work, e.g. + ``components.default_assigned_to``) to keep only what is needed. + """ + params = {"names": name} + if include_fields: + params["include_fields"] = include_fields + mcp_log.info(f"[BZ-REQ] GET {self.api_url}/product params={params}") + + try: + r = await self.client.get("/product", params=params) + r.raise_for_status() + except httpx.HTTPStatusError as e: + bz_error = _bugzilla_error_body(e.response) + if bz_error is not None: + mcp_log.error( + f"[BZ-RES] Failed: {e.response.status_code} " + f"code={bz_error.get('code')} {bz_error['message']}" + ) + raise BugzillaAPIError(e.response.status_code, bz_error) from e + mcp_log.error( + f"[BZ-RES] Failed: {e.response.status_code} {e.response.text}" + ) + raise + except httpx.RequestError as e: + mcp_log.error(f"[BZ-RES] Network Error: {e}") + raise + + envelope = _json_or_raise(r) + products = envelope.get("products", []) + mcp_log.info(f"[BZ-RES] Retrieved {len(products)} products") + mcp_log.debug(f"[BZ-RES] {envelope}") + return envelope + async def bug_flags(self, bug_id: int) -> list[dict[str, Any]]: """Return the flags currently set on a bug, with their instance ids. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mcp_bugzilla-0.19.0/src/mcp_bugzilla/server.py new/mcp_bugzilla-0.20.0/src/mcp_bugzilla/server.py --- old/mcp_bugzilla-0.19.0/src/mcp_bugzilla/server.py 1970-01-01 01:00:00.000000000 +0100 +++ new/mcp_bugzilla-0.20.0/src/mcp_bugzilla/server.py 1970-01-01 01:00:00.000000000 +0100 @@ -18,9 +18,9 @@ from fastmcp import FastMCP from fastmcp.dependencies import CurrentHeaders, Depends from fastmcp.exceptions import PromptError, ResourceError, ToolError +from .mcp_utils import is_textual, mcp_log, safe_filename from .lib_bugzilla import Bugzilla -from .mcp_utils import is_textual, mcp_log, safe_filename # The FastMCP instance mcp = FastMCP("Bugzilla") @@ -144,6 +144,107 @@ raise ToolError(f"Failed to fetch bug info\nReason: {e}") +class ComponentDefaults(TypedDict): + """A component's default assignee and QA contact. + + ``default_assignee`` / ``default_qa_contact`` are ``None`` both when the + component has none set and when the API key cannot see them. Bugzilla + reports an unset contact as an empty string; that is normalised to ``None`` + so the two cases read alike. + """ + + product: str + component: str + default_assignee: str | None + default_qa_contact: str | None + is_active: bool | None + + [email protected](annotations={"readOnlyHint": True, "openWorldHint": True}, tags={"read"}) +async def get_component_defaults( + component: str | None = None, + product: str | None = None, + bug_id: int | None = None, + bz: Bugzilla = _get_bz, +) -> ComponentDefaults: + """Look up a component's default assignee and QA contact. + + Bugzilla has no component endpoint, so the defaults come from the parent + product. Pass ``product`` and ``component``, or a ``bug_id`` to resolve them + from that bug; explicit values win. To reset a bug *to* these defaults, use + ``update_bug_fields`` with ``reset_qa_contact`` / ``reset_assigned_to``. + + Args: + component: Component name (resolved from bug_id if omitted) + product: Product the component belongs to (resolved from bug_id if omitted) + bug_id: Resolve product/component from this bug instead of passing them + """ + mcp_log.info( + f"[LLM-REQ] get_component_defaults(component={component!r}, " + f"product={product!r}, bug_id={bug_id})" + ) + + try: + resolved_from_bug = False + if bug_id is not None and (not product or not component): + envelope = await bz.bug_info({bug_id}, include_fields="product,component") + bugs = envelope.get("bugs", []) + if not bugs: + raise ToolError(f"Bug {bug_id} not found") + product = product or bugs[0].get("product") + component = component or bugs[0].get("component") + resolved_from_bug = True + + if not product: + raise ToolError( + f"Bug {bug_id} returned no product field" + if resolved_from_bug + else "Either `product` or `bug_id` must be supplied" + ) + if not component: + raise ToolError( + f"Bug {bug_id} returned no component field; pass `component` explicitly" + if resolved_from_bug + else "Either `component` or `bug_id` must be supplied" + ) + + # The full product payload is ~10x larger and carries flag_types, + # milestones and versions this tool never reads. + envelope = await bz.get_product( + product, + include_fields=( + "name,components.name,components.default_assigned_to," + "components.default_qa_contact,components.is_active" + ), + ) + products = envelope.get("products", []) + if not products: + raise ToolError(f"Product {product!r} not found") + + components = products[0].get("components", []) + for comp in components: + if comp.get("name") == component: + return ComponentDefaults( + product=product, + component=component, + # Bugzilla's key is default_assigned_to, not default_assignee, + # and an unset contact comes back as "" rather than null. + default_assignee=comp.get("default_assigned_to") or None, + default_qa_contact=comp.get("default_qa_contact") or None, + is_active=comp.get("is_active"), + ) + + available = ", ".join(c.get("name", "?") for c in components) + raise ToolError( + f"Component {component!r} not found in product {product!r}. " + f"Available components: {available}" + ) + except ToolError: + raise + except Exception as e: # noqa: BLE001 + raise ToolError(f"Failed to fetch component defaults\n{e}") + + @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True}, tags={"read"}) async def bug_history( id: int, @@ -520,6 +621,8 @@ severity: str | None = None, resolution: str | None = None, custom_fields: dict[str, Any] | None = None, + reset_qa_contact: bool = False, + reset_assigned_to: bool = False, comment: str = "", bz: Bugzilla = _get_bz, ) -> dict[str, Any]: @@ -531,10 +634,13 @@ severity: Severity (e.g., urgent, high, medium, low, unspecified) resolution: Resolution (e.g., FIXED, WONTFIX, NOTABUG, DUPLICATE) - only for closed bugs custom_fields: Dict of custom fields e.g. {"cf_fixed_in": "1.2.3"} + reset_qa_contact: Reset the QA contact to the component's default + reset_assigned_to: Reset the assignee to the component's default comment: Optional comment explaining the changes """ mcp_log.info( - f"[LLM-REQ] update_bug_fields(bug_id={bug_id}, priority={priority}, severity={severity}, resolution={resolution})" + f"[LLM-REQ] update_bug_fields(bug_id={bug_id}, priority={priority}, severity={severity}, " + f"resolution={resolution}, reset_qa_contact={reset_qa_contact}, reset_assigned_to={reset_assigned_to})" ) updates = {} @@ -546,6 +652,10 @@ updates["resolution"] = resolution if custom_fields: updates.update(custom_fields) + if reset_qa_contact: + updates["reset_qa_contact"] = True + if reset_assigned_to: + updates["reset_assigned_to"] = True if not updates: raise ToolError("At least one field must be specified") @@ -1153,6 +1263,15 @@ # Ensure base_url doesn't have trailing slash for consistency base_url = base_url.removesuffix("/") + # Optionally enable BM25 search transform + if getattr(cli_args, "tool_search", False): + from fastmcp.server.transforms.search import BM25SearchTransform + + mcp.add_transform(BM25SearchTransform()) + mcp_log.info( + "BM25 search transform enabled — tools collapsed into search_tools and call_tool" + ) + # Seletively disable components before running the server disable_components_selectively() disable_write_components()
