bugzilla-mcp 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bugzilla_mcp/__init__.py +65 -0
- bugzilla_mcp/cli.py +94 -0
- bugzilla_mcp/middleware/__init__.py +6 -0
- bugzilla_mcp/middleware/read_only.py +55 -0
- bugzilla_mcp/middleware/validate_headers.py +48 -0
- bugzilla_mcp/tools/__init__.py +22 -0
- bugzilla_mcp/tools/bugzilla.py +676 -0
- bugzilla_mcp/utils/__init__.py +11 -0
- bugzilla_mcp/utils/bugzilla.py +675 -0
- bugzilla_mcp-0.6.0.dist-info/METADATA +467 -0
- bugzilla_mcp-0.6.0.dist-info/RECORD +14 -0
- bugzilla_mcp-0.6.0.dist-info/WHEEL +4 -0
- bugzilla_mcp-0.6.0.dist-info/entry_points.txt +3 -0
- bugzilla_mcp-0.6.0.dist-info/licenses/LICENSE +192 -0
bugzilla_mcp/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""MCP package for Bugzilla server"""
|
|
2
|
+
|
|
3
|
+
from .tools.bugzilla import (
|
|
4
|
+
bug_info,
|
|
5
|
+
bug_comments,
|
|
6
|
+
add_comment,
|
|
7
|
+
bugs_quicksearch,
|
|
8
|
+
learn_quicksearch_syntax,
|
|
9
|
+
server_url,
|
|
10
|
+
bug_url,
|
|
11
|
+
download_attachments,
|
|
12
|
+
download_attachment,
|
|
13
|
+
bugs_info,
|
|
14
|
+
bugs_comments,
|
|
15
|
+
bugs_analysis_context,
|
|
16
|
+
classify_bugs_heuristics,
|
|
17
|
+
analyze_bugs_statistics,
|
|
18
|
+
create_bug,
|
|
19
|
+
update_bug,
|
|
20
|
+
bug_history,
|
|
21
|
+
bugs_advanced_search,
|
|
22
|
+
bug_dependencies,
|
|
23
|
+
duplicate_chain,
|
|
24
|
+
get_user,
|
|
25
|
+
search_users,
|
|
26
|
+
list_products,
|
|
27
|
+
get_product_components,
|
|
28
|
+
upload_attachment,
|
|
29
|
+
tag_comment,
|
|
30
|
+
bug_attachments,
|
|
31
|
+
get_attachment,
|
|
32
|
+
register_tools,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"bug_info",
|
|
37
|
+
"bug_comments",
|
|
38
|
+
"add_comment",
|
|
39
|
+
"bugs_quicksearch",
|
|
40
|
+
"learn_quicksearch_syntax",
|
|
41
|
+
"server_url",
|
|
42
|
+
"bug_url",
|
|
43
|
+
"download_attachments",
|
|
44
|
+
"download_attachment",
|
|
45
|
+
"bugs_info",
|
|
46
|
+
"bugs_comments",
|
|
47
|
+
"bugs_analysis_context",
|
|
48
|
+
"classify_bugs_heuristics",
|
|
49
|
+
"analyze_bugs_statistics",
|
|
50
|
+
"create_bug",
|
|
51
|
+
"update_bug",
|
|
52
|
+
"bug_history",
|
|
53
|
+
"bugs_advanced_search",
|
|
54
|
+
"bug_dependencies",
|
|
55
|
+
"duplicate_chain",
|
|
56
|
+
"get_user",
|
|
57
|
+
"search_users",
|
|
58
|
+
"list_products",
|
|
59
|
+
"get_product_components",
|
|
60
|
+
"upload_attachment",
|
|
61
|
+
"tag_comment",
|
|
62
|
+
"bug_attachments",
|
|
63
|
+
"get_attachment",
|
|
64
|
+
"register_tools",
|
|
65
|
+
]
|
bugzilla_mcp/cli.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""`bugzilla-mcp` command and the server builders shared by server.py / server_local.py"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
import bugzilla_mcp.utils as utils
|
|
11
|
+
from bugzilla_mcp.middleware import ValidateHeaders
|
|
12
|
+
from bugzilla_mcp.middleware.read_only import ReadOnlyMode, read_only_default
|
|
13
|
+
from bugzilla_mcp.tools.bugzilla import WRITE_TOOL_NAMES, register_tools
|
|
14
|
+
from bugzilla_mcp.utils import Bugzilla
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_local_server(url: str, api_key: str, read_only: bool | None = None) -> FastMCP:
|
|
18
|
+
"""Single-user server: one Bugzilla client for the whole process.
|
|
19
|
+
|
|
20
|
+
It may read and write files on this machine (attachment downloads, uploads
|
|
21
|
+
from a path), because the only user is whoever started it.
|
|
22
|
+
"""
|
|
23
|
+
# asyncio.run copies this context into the server's tasks, so every tool sees it
|
|
24
|
+
utils.current_bz.set(Bugzilla(url=url, api_key=api_key, allow_local_files=True))
|
|
25
|
+
|
|
26
|
+
mcp = FastMCP("Bugzilla")
|
|
27
|
+
mcp.add_middleware(ReadOnlyMode(
|
|
28
|
+
WRITE_TOOL_NAMES,
|
|
29
|
+
read_only_default() if read_only is None else read_only,
|
|
30
|
+
allow_header_override=False,
|
|
31
|
+
))
|
|
32
|
+
register_tools(mcp, local_files=True)
|
|
33
|
+
return mcp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_http_server(read_only: bool | None = None) -> FastMCP:
|
|
37
|
+
"""Multi-user server: each request brings its own `api_key` / `bugzilla_url` headers.
|
|
38
|
+
|
|
39
|
+
Never touches this machine's disk, and clients may send `read_only: false`.
|
|
40
|
+
"""
|
|
41
|
+
mcp = FastMCP("Bugzilla")
|
|
42
|
+
mcp.add_middleware(ValidateHeaders())
|
|
43
|
+
mcp.add_middleware(ReadOnlyMode(
|
|
44
|
+
WRITE_TOOL_NAMES,
|
|
45
|
+
read_only_default() if read_only is None else read_only,
|
|
46
|
+
allow_header_override=True,
|
|
47
|
+
))
|
|
48
|
+
register_tools(mcp, local_files=False)
|
|
49
|
+
return mcp
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _version() -> str:
|
|
53
|
+
try:
|
|
54
|
+
return version("bugzilla-mcp")
|
|
55
|
+
except PackageNotFoundError: # running from a checkout that isn't installed
|
|
56
|
+
return "unknown"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str] | None = None) -> None:
|
|
60
|
+
parser = argparse.ArgumentParser(
|
|
61
|
+
prog="bugzilla-mcp",
|
|
62
|
+
description=(
|
|
63
|
+
"Bugzilla MCP server. stdio (default) reads BUGZILLA_URL and BUGZILLA_API_KEY "
|
|
64
|
+
"from the environment or a .env file; http takes them from each request's headers."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument("--transport", choices=["stdio", "http"], default="stdio")
|
|
68
|
+
parser.add_argument("--host", default="127.0.0.1", help="http only (default: 127.0.0.1)")
|
|
69
|
+
parser.add_argument("--port", type=int, default=8000, help="http only (default: 8000)")
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--allow-writes",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="enable tools that change Bugzilla (same as BUGZILLA_READ_ONLY=false)",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {_version()}")
|
|
76
|
+
args = parser.parse_args(argv)
|
|
77
|
+
|
|
78
|
+
load_dotenv() # optional .env in the current directory
|
|
79
|
+
read_only = False if args.allow_writes else None # None: BUGZILLA_READ_ONLY decides
|
|
80
|
+
|
|
81
|
+
if args.transport == "stdio":
|
|
82
|
+
# The API key only comes from the environment: command-line arguments
|
|
83
|
+
# show up in process lists and shell history.
|
|
84
|
+
url = os.environ.get("BUGZILLA_URL", "")
|
|
85
|
+
api_key = os.environ.get("BUGZILLA_API_KEY", "")
|
|
86
|
+
if not url or not api_key:
|
|
87
|
+
parser.error("set BUGZILLA_URL and BUGZILLA_API_KEY (environment or .env)")
|
|
88
|
+
build_local_server(url, api_key, read_only).run(transport="stdio", show_banner=False)
|
|
89
|
+
else:
|
|
90
|
+
build_http_server(read_only).run(transport="http", host=args.host, port=args.port)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Middleware that hides and blocks write tools unless writes are enabled"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from fastmcp.exceptions import ToolError
|
|
5
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
6
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
7
|
+
|
|
8
|
+
READ_ONLY_ENV = "BUGZILLA_READ_ONLY"
|
|
9
|
+
READ_ONLY_HEADER = "read_only"
|
|
10
|
+
|
|
11
|
+
_FALSE = {"0", "false", "no", "off"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_only_default() -> bool:
|
|
15
|
+
"""Read-only unless BUGZILLA_READ_ONLY is explicitly false/0/no/off"""
|
|
16
|
+
return os.environ.get(READ_ONLY_ENV, "true").strip().lower() not in _FALSE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReadOnlyMode(Middleware):
|
|
20
|
+
"""Hide write tools from tools/list and refuse calls to them in read-only mode.
|
|
21
|
+
|
|
22
|
+
The mode defaults to `default` (from BUGZILLA_READ_ONLY). On the HTTP server a
|
|
23
|
+
client can override it per request with a `read_only: false` header.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, write_tools: set[str], default: bool, allow_header_override: bool):
|
|
27
|
+
self.write_tools = write_tools
|
|
28
|
+
self.default = default
|
|
29
|
+
self.allow_header_override = allow_header_override
|
|
30
|
+
|
|
31
|
+
def _read_only(self) -> bool:
|
|
32
|
+
if self.allow_header_override:
|
|
33
|
+
value = (get_http_headers() or {}).get(READ_ONLY_HEADER)
|
|
34
|
+
if value is not None:
|
|
35
|
+
return value.strip().lower() not in _FALSE
|
|
36
|
+
return self.default
|
|
37
|
+
|
|
38
|
+
async def on_list_tools(self, context: MiddlewareContext, call_next):
|
|
39
|
+
tools = await call_next(context)
|
|
40
|
+
if not self._read_only():
|
|
41
|
+
return tools
|
|
42
|
+
return [t for t in tools if t.name not in self.write_tools]
|
|
43
|
+
|
|
44
|
+
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
|
45
|
+
if context.message.name in self.write_tools and self._read_only():
|
|
46
|
+
how = (
|
|
47
|
+
f"send the `{READ_ONLY_HEADER}: false` header"
|
|
48
|
+
if self.allow_header_override
|
|
49
|
+
else f"set {READ_ONLY_ENV}=false"
|
|
50
|
+
)
|
|
51
|
+
raise ToolError(
|
|
52
|
+
f"`{context.message.name}` changes data, and this server is in read-only mode. "
|
|
53
|
+
f"To enable write tools, {how}."
|
|
54
|
+
)
|
|
55
|
+
return await call_next(context)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Middleware to validate required HTTP headers"""
|
|
2
|
+
|
|
3
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
4
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
5
|
+
from fastmcp.exceptions import ValidationError
|
|
6
|
+
from bugzilla_mcp.utils import Bugzilla
|
|
7
|
+
import bugzilla_mcp.utils as utils
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValidateHeaders(Middleware):
|
|
11
|
+
"""Validate incoming HTTP headers
|
|
12
|
+
|
|
13
|
+
Requires both `api_key` and `bugzilla_url` headers to be present.
|
|
14
|
+
Creates a Bugzilla client scoped to the current request (utils.current_bz)
|
|
15
|
+
and closes it once the request is handled.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# on_request, not on_message: since fastmcp 4, on_message also sees notifications
|
|
19
|
+
# (initialized, cancelled, progress), which never need a Bugzilla client.
|
|
20
|
+
async def on_request(self, middleware_context: MiddlewareContext, call_next):
|
|
21
|
+
headers = get_http_headers()
|
|
22
|
+
|
|
23
|
+
# During inspection or when headers are not available, skip validation
|
|
24
|
+
# and use a dummy Bugzilla instance so `fastmcp inspect` works without credentials
|
|
25
|
+
if not headers:
|
|
26
|
+
bz = Bugzilla(url="https://bugzilla.example.com", api_key="inspection-placeholder")
|
|
27
|
+
else:
|
|
28
|
+
if "api_key" not in headers:
|
|
29
|
+
raise ValidationError("`api_key` header is required")
|
|
30
|
+
|
|
31
|
+
if "bugzilla_url" not in headers:
|
|
32
|
+
raise ValidationError("`bugzilla_url` header is required")
|
|
33
|
+
|
|
34
|
+
bugzilla_url = headers["bugzilla_url"]
|
|
35
|
+
|
|
36
|
+
# Normalize URL: add https:// if no protocol is specified
|
|
37
|
+
if bugzilla_url and not bugzilla_url.startswith(("http://", "https://")):
|
|
38
|
+
bugzilla_url = f"https://{bugzilla_url}"
|
|
39
|
+
|
|
40
|
+
bz = Bugzilla(url=bugzilla_url, api_key=headers["api_key"])
|
|
41
|
+
|
|
42
|
+
# all the tools & prompts read this for making api calls
|
|
43
|
+
token = utils.current_bz.set(bz)
|
|
44
|
+
try:
|
|
45
|
+
return await call_next(middleware_context)
|
|
46
|
+
finally:
|
|
47
|
+
utils.current_bz.reset(token)
|
|
48
|
+
await bz.close()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Tools package for Bugzilla MCP server"""
|
|
2
|
+
|
|
3
|
+
from .bugzilla import (
|
|
4
|
+
bug_info,
|
|
5
|
+
bug_comments,
|
|
6
|
+
add_comment,
|
|
7
|
+
bugs_quicksearch,
|
|
8
|
+
learn_quicksearch_syntax,
|
|
9
|
+
server_url,
|
|
10
|
+
bug_url,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"bug_info",
|
|
15
|
+
"bug_comments",
|
|
16
|
+
"add_comment",
|
|
17
|
+
"bugs_quicksearch",
|
|
18
|
+
"learn_quicksearch_syntax",
|
|
19
|
+
"server_url",
|
|
20
|
+
"bug_url",
|
|
21
|
+
]
|
|
22
|
+
|