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-09-28 10:46:02
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/mcp-bugzilla (Old)
 and      /work/SRC/openSUSE:Factory/.mcp-bugzilla.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "mcp-bugzilla"

Mon Sep 28 10:46:02 2026 rev:4 rq:1380832 version:0.21.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/mcp-bugzilla/mcp-bugzilla.changes        
2026-08-18 16:38:23.001010767 +0200
+++ /work/SRC/openSUSE:Factory/.mcp-bugzilla.new.383539/mcp-bugzilla.changes    
2026-09-28 10:46:38.871443304 +0200
@@ -1,0 +2,19 @@
+Tue Sep 22 13:49:54 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to version 0.21.0:
+  * Surface Bugzilla's own error message (for example an invalid
+    field name) to the caller instead of the generic httpx status
+    text, so a client can correct its own query
+  * Stop leaking the request URL, which carries the API key, into
+    error messages and tracebacks
+  * Rename the bug id parameter to bug_id in bug_history,
+    bug_comments and summarize_bug_prompt
+  * bug_info now takes bug_ids as a list instead of a set, for
+    better JSON schema compatibility
+  * Expose the is_private field in bug_comments output
+  * add_comment is no longer annotated as destructive, matching
+    the other append-only write tools
+- Keep fastmcp as a versioned floor instead of upstream's exact
+  pin on 4.0.3; the fastmcp API the server uses is unchanged
+
+-------------------------------------------------------------------

Old:
----
  mcp_bugzilla-0.20.0.tar.gz

New:
----
  mcp_bugzilla-0.21.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ mcp-bugzilla.spec ++++++
--- /var/tmp/diff_new_pack.QHwd0q/_old  2026-09-28 10:46:39.573472717 +0200
+++ /var/tmp/diff_new_pack.QHwd0q/_new  2026-09-28 10:46:39.575472801 +0200
@@ -21,7 +21,7 @@
 # not a multi-flavour module.
 %define pythons %{primary_python}
 Name:           mcp-bugzilla
-Version:        0.20.0
+Version:        0.21.0
 Release:        0
 Summary:        Model Context Protocol server for Bugzilla
 License:        Apache-2.0
@@ -48,6 +48,9 @@
 
 %prep
 %autosetup -p1 -n mcp_bugzilla-%{version}
+# upstream pins fastmcp exactly (dependabot); the API surface it uses is
+# unchanged, so keep a floor instead of the pin
+sed -i -E 's/"fastmcp==[0-9.]+"/"fastmcp>=3.4.7"/' pyproject.toml
 
 %build
 %pyproject_wheel

++++++ mcp_bugzilla-0.20.0.tar.gz -> mcp_bugzilla-0.21.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_bugzilla-0.20.0/PKG-INFO 
new/mcp_bugzilla-0.21.0/PKG-INFO
--- old/mcp_bugzilla-0.20.0/PKG-INFO    1970-01-01 01:00:00.000000000 +0100
+++ new/mcp_bugzilla-0.21.0/PKG-INFO    1970-01-01 01:00:00.000000000 +0100
@@ -1,8 +1,8 @@
 Metadata-Version: 2.3
 Name: mcp-bugzilla
-Version: 0.20.0
+Version: 0.21.0
 Summary: MCP server for Bugzilla
-Requires-Dist: fastmcp==3.4.7
+Requires-Dist: fastmcp==4.0.3
 Requires-Dist: httpx-retries>=0.5.0
 Requires-Dist: pytest ; extra == 'dev'
 Requires-Dist: pytest-asyncio ; extra == 'dev'
@@ -38,18 +38,18 @@
 
 #### Bug Information
 
-- **`bug_info(bug_ids: set[int], include_fields: Optional[str] = None, 
exclude_fields: Optional[str] = None)`**: Retrieves comprehensive details for 
specified Bugzilla bug IDs. By default every field is returned; use Bugzilla's 
native field selection to trim large or bulk fetches.
+- **`bug_info(bug_ids: list[int], include_fields: Optional[str] = None, 
exclude_fields: Optional[str] = None)`**: Retrieves comprehensive details for 
specified Bugzilla bug IDs. By default every field is returned; use Bugzilla's 
native field selection to trim large or bulk fetches.
   - **Parameters**:
-    - `bug_ids`: A set of bug IDs to fetch details for
+    - `bug_ids`: A list of bug IDs to fetch details for
     - `include_fields`: Comma-separated field names to return (Bugzilla's 
native `Bug.get` parameter; supports field groups and 
`_default`/`_all`/`_extra`). For the leanest response request only scalar 
fields, e.g. `"id,status,resolution,summary"`. Requesting a user field 
(`assigned_to`, `creator`, `cc`, `qa_contact`) also returns its verbose 
`*_detail` object. Defaults to all fields
     - `exclude_fields`: Comma-separated field names to drop. To remove a 
user-object expansion, exclude both the base field and its `*_detail` together, 
e.g. `"cc,cc_detail"` — excluding the `*_detail` alone has no effect, as 
Bugzilla re-attaches it while the base field is present
   - **Returns**: A dictionary containing the array `bugs` which lists the 
requested information about the bugs (status, assignee, summary, description, 
extensions, etc.)
   - **Note on Bugzilla API parity**: both `include_fields` and 
`exclude_fields` are native Bugzilla `Bug.get` parameters, forwarded to the API 
unchanged; the underlying request is standard Bugzilla
-  - **Example**: `bug_info({12345, 67890}, 
include_fields="id,status,resolution,summary")` fetches two bugs with just the 
essential scalar fields
+  - **Example**: `bug_info([12345, 67890], 
include_fields="id,status,resolution,summary")` fetches two bugs with just the 
essential scalar fields
 
-- **`bug_history(id: int, new_since: Optional[datetime] = None, 
changed_fields: Optional[str] = None, exclude_authors: Optional[str] = None, 
limit: Optional[int] = None)`**: Fetches the change history of a given bug ID, 
with SQL-like controls to keep only the change events that matter for triage.
+- **`bug_history(bug_id: int, new_since: Optional[datetime] = None, 
changed_fields: Optional[str] = None, exclude_authors: Optional[str] = None, 
limit: Optional[int] = None)`**: Fetches the change history of a given bug ID, 
with SQL-like controls to keep only the change events that matter for triage.
   - **Parameters**:
-    - `id`: The bug ID to fetch history for
+    - `bug_id`: The bug ID to fetch history for
     - `new_since`: Optional datetime object to only return history newer than 
this time
     - `changed_fields`: Comma-separated Bugzilla field names; keep only 
changes to these fields, dropping events left with no matching change (e.g. 
`"status,resolution,assigned_to"` to see only lifecycle changes and hide the cc 
/ flagtypes.name / summary churn that dominates most histories)
     - `exclude_authors`: Comma-separated substrings; drop events whose author 
matches any (e.g. `"upstream-release-monitoring"` to hide release-monitoring 
bot edits)
@@ -58,7 +58,7 @@
   - **Note on Bugzilla API parity**: `new_since` mirrors Bugzilla's own API 
parameter. `changed_fields`, `exclude_authors`, and `limit` have no server-side 
Bugzilla equivalent — this server applies them client-side, as post-processing 
over the standard `Bug.history` response; the underlying Bugzilla request is 
unmodified
   - **Example**: `bug_history(2504555, changed_fields="status,resolution")` 
returns just the event where the bug was closed as RAWHIDE, filtering out the 
surrounding cc and needinfo-flag churn
 
-- **`bug_comments(id: int, include_private_comments: bool = False, new_since: 
Optional[datetime] = None, include_fields: Optional[str] = 
"count,id,creator,creation_time,text,attachment_id", exclude_creators: 
Optional[str] = None, limit: Optional[int] = None)`**: Fetches comments 
associated with a given bug ID, with SQL-like controls to keep only what is 
needed and avoid flooding the context on large threads.
+- **`bug_comments(bug_id: int, include_private_comments: bool = False, 
new_since: Optional[datetime] = None, include_fields: Optional[str] = 
"count,id,creator,creation_time,text,attachment_id,is_private", 
exclude_creators: Optional[str] = None, limit: Optional[int] = None)`**: 
Fetches comments associated with a given bug ID, with SQL-like controls to keep 
only what is needed and avoid flooding the context on large threads.
   - **Parameters**:
     - `id`: The bug ID to fetch comments for
     - `include_private_comments`: Whether to include private comments 
(default: `False`)
@@ -205,7 +205,7 @@
 - **`quicksearch_syntax_resource()`**: Returns documentation on Bugzilla's 
quicksearch syntax.
   - **Returns**: A string containing HTML documentation.
 
-- **`summarize_bug_prompt(id: int)`**: Returns a detailed summary prompt for 
all comments of a given bug ID.
+- **`summarize_bug_prompt(bug_id: int)`**: Returns a detailed summary prompt 
for all comments of a given bug ID.
   - **Returns**: A well-structured summary of the bug's comments including 
usernames (bold italic) and dates (bold).
 
 #### Utility Tools
@@ -571,17 +571,17 @@
 # Get the history of changes for a bug
 history = client.call_tool(
     "bug_history",
-    {"id": 12345, "new_since": datetime.fromisoformat("2026-01-01T00:00:00")},
+    {"bug_id": 12345, "new_since": 
datetime.fromisoformat("2026-01-01T00:00:00")},
 )
 
 # Get all public comments
 public_comments = client.call_tool(
-    "bug_comments", {"id": 12345, "include_private_comments": False}
+    "bug_comments", {"bug_id": 12345, "include_private_comments": False}
 )
 
 # Get all comments including private ones
 all_comments = client.call_tool(
-    "bug_comments", {"id": 12345, "include_private_comments": True}
+    "bug_comments", {"bug_id": 12345, "include_private_comments": True}
 )
 ```
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_bugzilla-0.20.0/README.md 
new/mcp_bugzilla-0.21.0/README.md
--- old/mcp_bugzilla-0.20.0/README.md   1970-01-01 01:00:00.000000000 +0100
+++ new/mcp_bugzilla-0.21.0/README.md   1970-01-01 01:00:00.000000000 +0100
@@ -25,18 +25,18 @@
 
 #### Bug Information
 
-- **`bug_info(bug_ids: set[int], include_fields: Optional[str] = None, 
exclude_fields: Optional[str] = None)`**: Retrieves comprehensive details for 
specified Bugzilla bug IDs. By default every field is returned; use Bugzilla's 
native field selection to trim large or bulk fetches.
+- **`bug_info(bug_ids: list[int], include_fields: Optional[str] = None, 
exclude_fields: Optional[str] = None)`**: Retrieves comprehensive details for 
specified Bugzilla bug IDs. By default every field is returned; use Bugzilla's 
native field selection to trim large or bulk fetches.
   - **Parameters**:
-    - `bug_ids`: A set of bug IDs to fetch details for
+    - `bug_ids`: A list of bug IDs to fetch details for
     - `include_fields`: Comma-separated field names to return (Bugzilla's 
native `Bug.get` parameter; supports field groups and 
`_default`/`_all`/`_extra`). For the leanest response request only scalar 
fields, e.g. `"id,status,resolution,summary"`. Requesting a user field 
(`assigned_to`, `creator`, `cc`, `qa_contact`) also returns its verbose 
`*_detail` object. Defaults to all fields
     - `exclude_fields`: Comma-separated field names to drop. To remove a 
user-object expansion, exclude both the base field and its `*_detail` together, 
e.g. `"cc,cc_detail"` — excluding the `*_detail` alone has no effect, as 
Bugzilla re-attaches it while the base field is present
   - **Returns**: A dictionary containing the array `bugs` which lists the 
requested information about the bugs (status, assignee, summary, description, 
extensions, etc.)
   - **Note on Bugzilla API parity**: both `include_fields` and 
`exclude_fields` are native Bugzilla `Bug.get` parameters, forwarded to the API 
unchanged; the underlying request is standard Bugzilla
-  - **Example**: `bug_info({12345, 67890}, 
include_fields="id,status,resolution,summary")` fetches two bugs with just the 
essential scalar fields
+  - **Example**: `bug_info([12345, 67890], 
include_fields="id,status,resolution,summary")` fetches two bugs with just the 
essential scalar fields
 
-- **`bug_history(id: int, new_since: Optional[datetime] = None, 
changed_fields: Optional[str] = None, exclude_authors: Optional[str] = None, 
limit: Optional[int] = None)`**: Fetches the change history of a given bug ID, 
with SQL-like controls to keep only the change events that matter for triage.
+- **`bug_history(bug_id: int, new_since: Optional[datetime] = None, 
changed_fields: Optional[str] = None, exclude_authors: Optional[str] = None, 
limit: Optional[int] = None)`**: Fetches the change history of a given bug ID, 
with SQL-like controls to keep only the change events that matter for triage.
   - **Parameters**:
-    - `id`: The bug ID to fetch history for
+    - `bug_id`: The bug ID to fetch history for
     - `new_since`: Optional datetime object to only return history newer than 
this time
     - `changed_fields`: Comma-separated Bugzilla field names; keep only 
changes to these fields, dropping events left with no matching change (e.g. 
`"status,resolution,assigned_to"` to see only lifecycle changes and hide the cc 
/ flagtypes.name / summary churn that dominates most histories)
     - `exclude_authors`: Comma-separated substrings; drop events whose author 
matches any (e.g. `"upstream-release-monitoring"` to hide release-monitoring 
bot edits)
@@ -45,7 +45,7 @@
   - **Note on Bugzilla API parity**: `new_since` mirrors Bugzilla's own API 
parameter. `changed_fields`, `exclude_authors`, and `limit` have no server-side 
Bugzilla equivalent — this server applies them client-side, as post-processing 
over the standard `Bug.history` response; the underlying Bugzilla request is 
unmodified
   - **Example**: `bug_history(2504555, changed_fields="status,resolution")` 
returns just the event where the bug was closed as RAWHIDE, filtering out the 
surrounding cc and needinfo-flag churn
 
-- **`bug_comments(id: int, include_private_comments: bool = False, new_since: 
Optional[datetime] = None, include_fields: Optional[str] = 
"count,id,creator,creation_time,text,attachment_id", exclude_creators: 
Optional[str] = None, limit: Optional[int] = None)`**: Fetches comments 
associated with a given bug ID, with SQL-like controls to keep only what is 
needed and avoid flooding the context on large threads.
+- **`bug_comments(bug_id: int, include_private_comments: bool = False, 
new_since: Optional[datetime] = None, include_fields: Optional[str] = 
"count,id,creator,creation_time,text,attachment_id,is_private", 
exclude_creators: Optional[str] = None, limit: Optional[int] = None)`**: 
Fetches comments associated with a given bug ID, with SQL-like controls to keep 
only what is needed and avoid flooding the context on large threads.
   - **Parameters**:
     - `id`: The bug ID to fetch comments for
     - `include_private_comments`: Whether to include private comments 
(default: `False`)
@@ -192,7 +192,7 @@
 - **`quicksearch_syntax_resource()`**: Returns documentation on Bugzilla's 
quicksearch syntax.
   - **Returns**: A string containing HTML documentation.
 
-- **`summarize_bug_prompt(id: int)`**: Returns a detailed summary prompt for 
all comments of a given bug ID.
+- **`summarize_bug_prompt(bug_id: int)`**: Returns a detailed summary prompt 
for all comments of a given bug ID.
   - **Returns**: A well-structured summary of the bug's comments including 
usernames (bold italic) and dates (bold).
 
 #### Utility Tools
@@ -558,17 +558,17 @@
 # Get the history of changes for a bug
 history = client.call_tool(
     "bug_history",
-    {"id": 12345, "new_since": datetime.fromisoformat("2026-01-01T00:00:00")},
+    {"bug_id": 12345, "new_since": 
datetime.fromisoformat("2026-01-01T00:00:00")},
 )
 
 # Get all public comments
 public_comments = client.call_tool(
-    "bug_comments", {"id": 12345, "include_private_comments": False}
+    "bug_comments", {"bug_id": 12345, "include_private_comments": False}
 )
 
 # Get all comments including private ones
 all_comments = client.call_tool(
-    "bug_comments", {"id": 12345, "include_private_comments": True}
+    "bug_comments", {"bug_id": 12345, "include_private_comments": True}
 )
 ```
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_bugzilla-0.20.0/pyproject.toml 
new/mcp_bugzilla-0.21.0/pyproject.toml
--- old/mcp_bugzilla-0.20.0/pyproject.toml      1970-01-01 01:00:00.000000000 
+0100
+++ new/mcp_bugzilla-0.21.0/pyproject.toml      1970-01-01 01:00:00.000000000 
+0100
@@ -1,11 +1,11 @@
 [project]
 name = "mcp-bugzilla"
-version = "0.20.0"
+version = "0.21.0"
 description = "MCP server for Bugzilla"
 readme = "README.md"
 requires-python = ">=3.13"
 dependencies = [
-    "fastmcp==3.4.7",
+    "fastmcp==4.0.3",
     "httpx-retries>=0.5.0",
 ]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_bugzilla-0.20.0/pyproject.toml.orig 
new/mcp_bugzilla-0.21.0/pyproject.toml.orig
--- old/mcp_bugzilla-0.20.0/pyproject.toml.orig 1970-01-01 01:00:00.000000000 
+0100
+++ new/mcp_bugzilla-0.21.0/pyproject.toml.orig 1970-01-01 01:00:00.000000000 
+0100
@@ -1,12 +1,12 @@
 [project]
 name = "mcp-bugzilla"
-version = "0.20.0"
+version = "0.21.0"
 description = "MCP server for Bugzilla"
 readme = "README.md"
 requires-python = ">=3.13"
 
 dependencies = [
-    "fastmcp==3.4.7",
+    "fastmcp==4.0.3",
     "httpx-retries>=0.5.0",
 ]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_bugzilla-0.20.0/src/mcp_bugzilla/lib_bugzilla.py 
new/mcp_bugzilla-0.21.0/src/mcp_bugzilla/lib_bugzilla.py
--- old/mcp_bugzilla-0.20.0/src/mcp_bugzilla/lib_bugzilla.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/mcp_bugzilla-0.21.0/src/mcp_bugzilla/lib_bugzilla.py    1970-01-01 
01:00:00.000000000 +0100
@@ -92,25 +92,36 @@
             transport=RetryTransport(),
         )
 
-    async def close(self):
-        await self.client.aclose()
-
-    async def server_version(self) -> str:
-        """Fetch bugzilla server version"""
-        try:
-            r = await self.client.get("/version")
-            r.raise_for_status()
-            return _json_or_raise(r)["version"]
-
-        except httpx.HTTPStatusError as e:
+    async def _request(self, method: str, url: str, **kwargs) -> 
httpx.Response:
+        """Issue a request. Surface Bugzilla's own error on HTTP errors;
+        re-raise network errors untouched."""
+        try:
+            r = await self.client.request(method, url, **kwargs)
+            r.raise_for_status()
+        except httpx.HTTPStatusError as e:  # caller-fixable: surface detail
+            body = _bugzilla_error_body(e.response)
+            if body is not None:
+                mcp_log.error(
+                    f"[BZ-RES] Failed: {e.response.status_code} "
+                    f"code={body.get('code')} {body.get('message')}"
+                )
+                raise BugzillaAPIError(e.response.status_code, body) from None
             mcp_log.error(
                 f"[BZ-RES] Failed: {e.response.status_code} {e.response.text}"
             )
-            raise
-
-        except httpx.RequestError as e:
+            raise BugzillaResponseError(e.response) from None
+        except httpx.RequestError as e:  # infra fault: stay loud
             mcp_log.error(f"[BZ-RES] Network Error: {e}")
             raise
+        return r
+
+    async def close(self):
+        await self.client.aclose()
+
+    async def server_version(self) -> str:
+        """Fetch bugzilla server version"""
+        r = await self._request("GET", "/version")
+        return _json_or_raise(r)["version"]
 
     async def bugzilla_info(self) -> dict[str, Any]:
         """Fetch comprehensive bugzilla server information:
@@ -179,17 +190,7 @@
 
         mcp_log.info(f"[BZ-REQ] GET {self.api_url}{url} params={params}")
 
-        try:
-            r = await self.client.get(url, params=params)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("GET", url, params=params)
 
         envelope = _json_or_raise(r)
         bugs = envelope.get("bugs", [])
@@ -212,24 +213,7 @@
             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
+        r = await self._request("GET", "/product", params=params)
 
         envelope = _json_or_raise(r)
         products = envelope.get("products", [])
@@ -247,17 +231,7 @@
         params = {"include_fields": "id,flags"}
         mcp_log.info(f"[BZ-REQ] GET {self.api_url}{url} params={params}")
 
-        try:
-            r = await self.client.get(url, params=params)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("GET", url, params=params)
 
         bugs = _json_or_raise(r).get("bugs", [])
         flags = bugs[0].get("flags", []) if bugs else []
@@ -275,17 +249,7 @@
 
         mcp_log.info(f"[BZ-REQ] GET {self.api_url}{url} params={params}")
 
-        try:
-            r = await self.client.get(url, params=params)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("GET", url, params=params)
 
         data = _json_or_raise(r).get("bugs", [])
         history = data[0].get("history", []) if data else []
@@ -304,17 +268,7 @@
 
         mcp_log.info(f"[BZ-REQ] GET {self.api_url}{url} params={params}")
 
-        try:
-            r = await self.client.get(url, params=params)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("GET", url, params=params)
 
         # The response structure is {"bugs": {"<id>": {"comments": [...]}}}
         data = (
@@ -332,17 +286,7 @@
         url = f"/bug/{bug_id}/comment"
         mcp_log.info(f"[BZ-REQ] POST {self.api_url}{url} json={payload}")
 
-        try:
-            r = await self.client.post(url, json=payload)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("POST", url, json=payload)
 
         data = _json_or_raise(r)
         mcp_log.info("[BZ-RES] Comment added successfully")
@@ -365,17 +309,7 @@
 
         mcp_log.info(f"[BZ-REQ] GET {self.api_url}/bug params={params}")
 
-        try:
-            r = await self.client.get("/bug", params=params)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("GET", "/bug", params=params)
 
         envelope = _json_or_raise(r)
         bugs = envelope.get("bugs", [])
@@ -393,23 +327,7 @@
         url = f"/bug/{bug_id}"
         mcp_log.info(f"[BZ-REQ] PUT {self.api_url}{url} json={payload}")
 
-        try:
-            r = await self.client.put(url, json=payload)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as e:
-            if (bz_error := _bugzilla_error_body(e.response)) is not None:
-                # Surface structured Bugzilla error (e.g., validation 
rejection)
-                mcp_log.error(
-                    f"[BZ-RES] Failed: {e.response.status_code} 
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
+        r = await self._request("PUT", url, json=payload)
 
         data = _json_or_raise(r)
         mcp_log.info("[BZ-RES] Bug updated successfully")
@@ -426,17 +344,7 @@
         url = "/bug"
         mcp_log.info(f"[BZ-REQ] POST {self.api_url}{url} json={fields}")
 
-        try:
-            r = await self.client.post(url, json=fields)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("POST", url, json=fields)
 
         data = _json_or_raise(r)
         mcp_log.info(f"[BZ-RES] Created bug {data.get('id')}")
@@ -453,17 +361,7 @@
             f"[BZ-REQ] POST {self.api_url}{url} 
file_name={payload.get('file_name')!r}"
         )
 
-        try:
-            r = await self.client.post(url, json=payload)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("POST", url, json=payload)
 
         data = _json_or_raise(r)
         mcp_log.info(f"[BZ-RES] Attachment(s) {data.get('ids')} added to bug 
{bug_id}")
@@ -475,17 +373,7 @@
         url = f"/bug/{bug_id}/attachment"
         mcp_log.info(f"[BZ-REQ] GET {self.api_url}{url} exclude_fields=data")
 
-        try:
-            r = await self.client.get(url, params={"exclude_fields": "data"})
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("GET", url, params={"exclude_fields": "data"})
 
         # /bug/{id}/attachment returns {"bugs": {"<bug_id>": [ {att}, ... ]}}
         attachments = _json_or_raise(r).get("bugs", {}).get(str(bug_id), [])
@@ -498,17 +386,7 @@
         # Don't log the (possibly large / binary) base64 blob in the response.
         mcp_log.info(f"[BZ-REQ] GET {self.api_url}{url}")
 
-        try:
-            r = await self.client.get(url)
-            r.raise_for_status()
-        except httpx.HTTPStatusError as 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
+        r = await self._request("GET", url)
 
         # /bug/attachment/{id} returns {"attachments": {"<attachment_id>": 
{att}}}
         attachment = _json_or_raise(r).get("attachments", 
{}).get(str(attachment_id))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_bugzilla-0.20.0/src/mcp_bugzilla/mcp_utils.py 
new/mcp_bugzilla-0.21.0/src/mcp_bugzilla/mcp_utils.py
--- old/mcp_bugzilla-0.20.0/src/mcp_bugzilla/mcp_utils.py       1970-01-01 
01:00:00.000000000 +0100
+++ new/mcp_bugzilla-0.21.0/src/mcp_bugzilla/mcp_utils.py       1970-01-01 
01:00:00.000000000 +0100
@@ -11,6 +11,7 @@
 import logging
 import os
 import re
+from typing import Any
 
 # Re-export Bugzilla and BugzillaAPIError for backward compatibility.
 # Lazily imported to avoid circular dependencies — lib_bugzilla imports
@@ -98,3 +99,58 @@
     base = os.path.basename(name or "").strip()
     base = re.sub(r"[^A-Za-z0-9._-]", "_", base).strip("._")
     return base or f"attachment-{attachment_id}"
+
+
+def filter_limit(
+    items: list[dict[str, Any]], limit: int | None
+) -> list[dict[str, Any]]:
+    """Keep only the most recent N items (preserves chronological order)."""
+    if limit and limit > 0:
+        return items[-limit:]
+    return items
+
+
+def filter_include_fields(
+    items: list[dict[str, Any]], fields: str | None
+) -> list[dict[str, Any]]:
+    """Keep only the specified fields for each item in the list."""
+    if fields is None:
+        return items
+    wanted = [f.strip() for f in fields.split(",") if f.strip()]
+    return [{k: item[k] for k in wanted if k in item} for item in items]
+
+
+def filter_exclude_substrings(
+    items: list[dict[str, Any]], field: str, patterns_str: str | None
+) -> list[dict[str, Any]]:
+    """Exclude items where the specified field contains any of the 
comma-separated substrings."""
+    if not patterns_str:
+        return items
+    patterns = [p.strip() for p in patterns_str.split(",") if p.strip()]
+    return [
+        item
+        for item in items
+        if not any(p in (item.get(field) or "") for p in patterns)
+    ]
+
+
+def filter_history_changes(
+    history: list[dict[str, Any]], changed_fields: str | None
+) -> list[dict[str, Any]]:
+    """Filter bug history to keep only events that modified the specified 
fields."""
+    if not changed_fields:
+        return history
+    wanted = {f.strip() for f in changed_fields.split(",") if f.strip()}
+    pruned = []
+    for h in history:
+        kept = [c for c in h.get("changes", []) if c.get("field_name") in 
wanted]
+        if kept:
+            pruned.append({**h, "changes": kept})
+    return pruned
+
+
+def filter_by_flag(
+    items: list[dict[str, Any]], field: str, required_value: bool
+) -> list[dict[str, Any]]:
+    """Keep items where the boolean value of the specified field matches 
required_value."""
+    return [item for item in items if bool(item.get(field)) is required_value]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_bugzilla-0.20.0/src/mcp_bugzilla/server.py 
new/mcp_bugzilla-0.21.0/src/mcp_bugzilla/server.py
--- old/mcp_bugzilla-0.20.0/src/mcp_bugzilla/server.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/mcp_bugzilla-0.21.0/src/mcp_bugzilla/server.py  1970-01-01 
01:00:00.000000000 +0100
@@ -18,9 +18,18 @@
 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 (
+    filter_by_flag,
+    filter_exclude_substrings,
+    filter_history_changes,
+    filter_include_fields,
+    filter_limit,
+    is_textual,
+    mcp_log,
+    safe_filename,
+)
 
 # The FastMCP instance
 mcp = FastMCP("Bugzilla")
@@ -103,12 +112,12 @@
 
 @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True}, 
tags={"read"})
 async def bug_info(
-    bug_ids: set[int],
+    bug_ids: list[int],
     include_fields: str | None = None,
     exclude_fields: str | None = None,
     bz: Bugzilla = _get_bz,
 ) -> dict[str, Any]:
-    """Returns information for one or more bugzilla bug ids.
+    """Returns information for one or more Bugzilla bug IDs.
 
     By default every field is returned. For large or bulk fetches, use
     Bugzilla's native field selection (both are forwarded to Bug.get):
@@ -247,14 +256,14 @@
 
 @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True}, 
tags={"read"})
 async def bug_history(
-    id: int,
+    bug_id: int,
     new_since: datetime | None = None,
     changed_fields: str | None = None,
     exclude_authors: str | None = None,
     limit: int | None = None,
     bz: Bugzilla = _get_bz,
 ) -> list[dict[str, Any]]:
-    """Returns the history of given bug id.
+    """Returns the history of a given bug.
 
     A bug's history is often dominated by low-signal churn (repeated bot
     edits, cc-list changes, flag flips). These SQL-like controls keep only
@@ -273,38 +282,17 @@
     """
 
     mcp_log.info(
-        f"[LLM-REQ] bug_history(id={id}, new_since={new_since}, "
+        f"[LLM-REQ] bug_history(bug_id={bug_id}, new_since={new_since}, "
         f"changed_fields={changed_fields}, exclude_authors={exclude_authors}, "
         f"limit={limit})"
     )
 
     try:
-        history = await bz.bug_history(id, new_since=new_since)
+        history = await bz.bug_history(bug_id, new_since=new_since)
 
-        # WHERE who NOT LIKE any(exclude_authors)
-        if exclude_authors:
-            patterns = [p.strip() for p in exclude_authors.split(",") if 
p.strip()]
-            history = [
-                h
-                for h in history
-                if not any(p in (h.get("who") or "") for p in patterns)
-            ]
-
-        # WHERE field_name IN (changed_fields); drop events left with no match
-        if changed_fields:
-            wanted = {f.strip() for f in changed_fields.split(",") if 
f.strip()}
-            pruned = []
-            for h in history:
-                kept = [
-                    c for c in h.get("changes", []) if c.get("field_name") in 
wanted
-                ]
-                if kept:
-                    pruned.append({**h, "changes": kept})
-            history = pruned
-
-        # LIMIT to the most recent N (chronological order preserved)
-        if limit and limit > 0:
-            history = history[-limit:]
+        history = filter_exclude_substrings(history, "who", exclude_authors)
+        history = filter_history_changes(history, changed_fields)
+        history = filter_limit(history, limit)
 
         mcp_log.info(f"[LLM-RES] Returning {len(history)} history items")
         return history
@@ -314,22 +302,23 @@
 
 @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True}, 
tags={"read"})
 async def bug_comments(
-    id: int,
+    bug_id: int,
     include_private_comments: bool = False,
     new_since: datetime | None = None,
-    include_fields: str | None = 
"count,id,creator,creation_time,text,attachment_id",
+    include_fields: str
+    | None = "count,id,creator,creation_time,text,attachment_id,is_private",
     exclude_creators: str | None = None,
     limit: int | None = None,
     bz: Bugzilla = _get_bz,
 ) -> list[dict[str, Any]]:
-    """Returns the comments of given bug id.
+    """Returns the comments of a given bug.
 
     Comment threads on long-lived bugs can be very large (hundreds of automated
     comments), so this tool exposes SQL-like controls to return only what is
     needed and avoid flooding the context window:
 
       include_fields    Comma-separated field names to keep per comment
-                        (default: 
count,id,creator,creation_time,text,attachment_id).
+                        (default: 
count,id,creator,creation_time,text,attachment_id,is_private).
                         Pass None to return every field.
       exclude_creators  Comma-separated substrings; drop comments whose creator
                         matches any -- e.g. "upstream-release-monitoring" to 
hide
@@ -337,40 +326,25 @@
       limit             Return only the most recent N comments (chronological
                         order is preserved).
       new_since         Only comments newer than the given date.
-      include_private_comments  Include private comments (default: False).
+      include_private_comments  MUST be set to True to retrieve 
private/internal comments (default: False).
     """
 
     mcp_log.info(
-        f"[LLM-REQ] bug_comments(id={id}, "
+        f"[LLM-REQ] bug_comments(bug_id={bug_id}, "
         f"include_private_comments={include_private_comments}, 
new_since={new_since}, "
         f"include_fields={include_fields}, 
exclude_creators={exclude_creators}, "
         f"limit={limit})"
     )
 
     try:
-        comments = await bz.bug_comments(id, new_since=new_since)
+        comments = await bz.bug_comments(bug_id, new_since=new_since)
 
-        # WHERE is_private = false (unless explicitly requested)
         if not include_private_comments:
-            comments = [c for c in comments if not c.get("is_private", False)]
+            comments = filter_by_flag(comments, "is_private", False)
 
-        # WHERE creator NOT LIKE any(exclude_creators)
-        if exclude_creators:
-            patterns = [p.strip() for p in exclude_creators.split(",") if 
p.strip()]
-            comments = [
-                c
-                for c in comments
-                if not any(p in (c.get("creator") or "") for p in patterns)
-            ]
-
-        # LIMIT to the most recent N (chronological order preserved)
-        if limit and limit > 0:
-            comments = comments[-limit:]
-
-        # SELECT include_fields
-        if include_fields is not None:
-            wanted = [f.strip() for f in include_fields.split(",") if 
f.strip()]
-            comments = [{k: c[k] for k in wanted if k in c} for c in comments]
+        comments = filter_exclude_substrings(comments, "creator", 
exclude_creators)
+        comments = filter_limit(comments, limit)
+        comments = filter_include_fields(comments, include_fields)
 
         mcp_log.info(f"[LLM-RES] Returning {len(comments)} comments")
         return comments
@@ -380,13 +354,17 @@
 
 
 @mcp.tool(
-    annotations={"readOnlyHint": False, "destructiveHint": True, 
"openWorldHint": True},
+    annotations={
+        "readOnlyHint": False,
+        "destructiveHint": False,
+        "openWorldHint": True,
+    },
     tags={"write"},
 )
 async def add_comment(
     bug_id: int, comment: str, is_private: bool = False, bz: Bugzilla = _get_bz
 ) -> dict[str, int]:
-    """Add a comment to a bug. It can optionally be private. If success, 
returns the created comment id."""
+    """Add a comment to a bug. It can optionally be private. Returns the 
created comment id on success."""
     mcp_log.info(
         f"[LLM-REQ] add_comment(bug_id={bug_id}, comment='{comment}', 
is_private={is_private})"
     )
@@ -406,10 +384,10 @@
     offset: int | None = 0,
     bz: Bugzilla = _get_bz,
 ) -> dict[str, Any]:
-    """Search bugs using bugzilla's quicksearch syntax
+    """Search bugs using Bugzilla's Quicksearch syntax.
 
-    To reduce the token limit & response time, only returns a subset of fields 
for each bug
-    The user can query full details of each bug using the bug_info tool
+    To reduce the token limit & response time, this only returns a subset of 
fields for each bug.
+    The user can query full details of each bug using the `bug_info` tool.
     Returns the top-level bug data envelope containing the matched bugs.
     """
 
@@ -436,7 +414,7 @@
 
 @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True}, 
tags={"read"})
 async def quicksearch_syntax_resource(bz: Bugzilla = _get_bz) -> str:
-    """Access the documentation of the bugzilla quicksearch syntax. LLM can 
learn using this tool. Response is in HTML"""
+    """Access the documentation of the Bugzilla Quicksearch syntax. Use this 
to learn how to construct advanced search queries. Response is in HTML."""
 
     mcp_log.info("[LLM-REQ] quicksearch_syntax_resource()")
 
@@ -461,7 +439,7 @@
 
 @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True}, 
tags={"read"})
 async def bugzilla_server_info(bz: Bugzilla = _get_bz) -> dict[str, Any]:
-    """Returns comprehensive bugzilla server information (url, version, 
extensions, timezone, time, parameters)."""
+    """Returns comprehensive Bugzilla server information (url, version, 
extensions, timezone, time, parameters)."""
     mcp_log.info("[LLM-REQ] bugzilla_server_info()")
     try:
         return await bz.bugzilla_info()
@@ -471,14 +449,14 @@
 
 @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": False}, 
tags={"read"})
 def bug_url(bug_id: int) -> str:
-    """returns the bug url"""
+    """Returns the web URL for a given bug."""
     mcp_log.info(f"[LLM-REQ] bug_url(bug_id={bug_id})")
     return f"{base_url}/show_bug.cgi?id={bug_id}"
 
 
 @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True}, 
tags={"read"})
 async def mcp_server_info_resource(bz: Bugzilla = _get_bz) -> dict[str, Any]:
-    """Returns the args being used by the current server instance"""
+    """Returns the configuration arguments and version of the current MCP 
server instance."""
 
     mcp_log.info("[LLM-REQ] mcp_server_info_resource()")
 
@@ -503,13 +481,13 @@
 
 
 @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True}, 
tags={"read"})
-async def summarize_bug_prompt(id: int, bz: Bugzilla = _get_bz) -> str:
-    """Summarizes all the comments of a bug"""
+async def summarize_bug_prompt(bug_id: int, bz: Bugzilla = _get_bz) -> str:
+    """Returns a prompt containing all comments of a bug, which can be used to 
generate a summary."""
 
-    mcp_log.info(f"[LLM-REQ] summarize_bug_prompt(id={id})")
+    mcp_log.info(f"[LLM-REQ] summarize_bug_prompt(bug_id={bug_id})")
 
     try:
-        comments = await bz.bug_comments(id)
+        comments = await bz.bug_comments(bug_id)
 
         summary_prompt = f"""
     You are an expert in summarizing bugzilla comments.
@@ -872,7 +850,7 @@
         bug_id: Bug to attach the file to
         file_name: File name shown in Bugzilla
         summary: Short description of the attachment
-        data: The attachment content, **base64-encoded** (binary-safe)
+        data: The attachment content. MUST be base64-encoded (binary-safe)!
         content_type: MIME type (ignored by Bugzilla when is_patch=True)
         is_patch: Mark the attachment as a patch
         is_private: Restrict the attachment to the insider group
@@ -931,17 +909,13 @@
     try:
         attachments = await bz.list_attachments(bug_id)
 
-        # WHERE NOT is_obsolete
         if exclude_obsolete:
-            attachments = [a for a in attachments if not a.get("is_obsolete")]
+            attachments = filter_by_flag(attachments, "is_obsolete", False)
 
-        # WHERE is_patch
         if patches_only:
-            attachments = [a for a in attachments if a.get("is_patch")]
+            attachments = filter_by_flag(attachments, "is_patch", True)
 
-        # LIMIT to the most recent N (chronological order preserved)
-        if limit and limit > 0:
-            attachments = attachments[-limit:]
+        attachments = filter_limit(attachments, limit)
 
         mcp_log.info(f"[LLM-RES] Returning {len(attachments)} attachments")
         return attachments

Reply via email to