mirror of
https://github.com/svnscha/mcp-windbg.git
synced 2026-10-16 20:32:50 +08:00
Run on the mcp 2.x SDK (#81)
Closes #78. Lifts the `mcp<2.0.0` cap that 1.0.1 shipped as a stopgap. ## What actually breaks in 2.x The `McpError` -> `MCPError` rename is the half everyone hits first, but it was the easy half. 2.0.0 also removed the low-level `@server.list_tools()` / `@server.call_tool()` decorators that every tool and prompt was registered through. Handlers are now passed to `Server(...)` as constructor arguments, take `(ctx, params)`, and return the SDK's result types instead of bare lists: | 1.x | 2.x | | :--- | :--- | | `@server.list_tools()` -> `list[Tool]` | `on_list_tools=` -> `ListToolsResult` | | `@server.call_tool()` -> `list[TextContent]` | `on_call_tool=` -> `CallToolResult` | | `@server.list_prompts()` / `@server.get_prompt()` | `on_list_prompts=` / `on_get_prompt=` | | `McpError(ErrorData(code=, message=))` | `MCPError(code, message)` | Tool dispatch is split from result wrapping, so the dozen handler returns keep their `list[TextContent]` shape and exactly one place builds the `CallToolResult`. ## What did not change - **Clients see nothing.** Same nine tools, same schemas, both transports. - **Transports needed no work.** `stdio_server`, `StreamableHTTPSessionManager`, and `Server.run` are all signature-compatible. - **Python support.** The 2.x SDK requires 3.10+, exactly this project's existing floor. - **Errors still surface as errors.** Raising `MCPError` from a handler becomes a JSON-RPC error response; the harness already normalized raised errors and `isError` alike, so the scenario assertions were unaffected. ## The harness moved too It drives a real client from the same SDK, so it needed three changes: `MCPError`, `CallToolResult.is_error` (the wire format is still `isError` - only the Python attribute is snake_case), and `streamable_http_client` now yielding two values instead of three. Its note about the stdio teardown race is corrected rather than deleted: **2.0.0 fixed that race** - `stdout_reader` now catches `BrokenResourceError` as well as `ClosedResourceError` around its send. The guard stays for the other transports, and it only suppresses after every step has completed, so it cannot mask a real failure. ## Verification, all on mcp 2.0.0 - 89 hermetic tests - 10 `live` cdb scenarios against real `cdb.exe` - both kernel scenarios against the live KDNET target - coverage 91% against the 88% gate - `mkdocs build --strict` clean, `server.json` valid against the registry schema, CLI smoke-tested ## Note on releasing Left under `[Unreleased]`. Whenever you cut it, this plus #80 make it a 1.1.0: new `wait_for_break` tool, kernel fixes, and a dependency floor move that is breaking for anyone who pinned `mcp` 1.x themselves (nothing needs to, since installing mcp-windbg pulls the SDK it needs).
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
name: Dependency canary
|
||||
|
||||
# Resolves the runtime dependencies to the newest versions the pyproject ranges
|
||||
# allow - ignoring uv.lock - and runs the hermetic suite against them.
|
||||
#
|
||||
# Why this exists: CI installs from uv.lock, so it only ever proves the server
|
||||
# works against the exact versions we pinned. Users installing from PyPI get
|
||||
# whatever the ranges permit, which can be newer than anything CI has seen. That
|
||||
# gap is how a breaking SDK release reaches users first (#76). This job closes
|
||||
# it by testing tomorrow's resolution today.
|
||||
#
|
||||
# It runs on a schedule rather than on pull requests on purpose: an upstream
|
||||
# release breaking the build should page us, not block someone's unrelated PR.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 7 * * 1" # Mondays 07:00 UTC, ahead of Dependabot's 09:00 run
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
latest-allowed:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.13
|
||||
|
||||
- name: Resolve to the newest allowed versions
|
||||
# --upgrade ignores the pinned versions in uv.lock; --resolution highest
|
||||
# makes the intent explicit rather than relying on the default.
|
||||
run: uv sync --dev --upgrade --resolution highest
|
||||
|
||||
- name: Show what that resolved to
|
||||
run: |
|
||||
uv run python -c "import importlib.metadata as m; [print(f'{p:12} {m.version(p)}') for p in ('mcp','pydantic','starlette','uvicorn')]"
|
||||
|
||||
- name: Run the hermetic suite
|
||||
# The live and kernel scenarios need cdb.exe and a target, neither of which
|
||||
# this job has. The hermetic subset still drives a real MCP client against a
|
||||
# real server subprocess, so an SDK change lands here first.
|
||||
run: uv run pytest src/mcp_windbg/tests/ -v --tb=short -m "not live"
|
||||
|
||||
- name: Check the CLI still starts
|
||||
run: uv run python -m mcp_windbg --help
|
||||
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **Runs on the mcp 2.x SDK** - the requirement is now `mcp>=2.0.0`, up from the `<2.0.0` cap
|
||||
1.0.1 shipped as a stopgap. 2.0.0 removed the low-level `@server.list_tools()` /
|
||||
`@server.call_tool()` decorators this server registered its tools through, so the handlers are
|
||||
now passed to `Server(...)` directly and return the SDK's result types (`ListToolsResult`,
|
||||
`CallToolResult`) instead of bare lists. `McpError` is `MCPError`, and it takes a code and a
|
||||
message rather than an `ErrorData`. None of this is visible to clients: the same nine tools
|
||||
with the same schemas, over both stdio and streamable-http (#78).
|
||||
|
||||
Installing mcp-windbg pulls the SDK it needs, so there is nothing to do on upgrade. Python
|
||||
support is unchanged - the 2.x SDK requires 3.10+, exactly as this project already did.
|
||||
- **Every runtime dependency is capped at the next major** - `mcp<3.0.0`, `pydantic<3.0.0`,
|
||||
`starlette<2.0.0`, `uvicorn<1.0.0`. #76 happened because `mcp>=1.28.1` had no upper bound, so
|
||||
the 2.0.0 release landed in fresh installs and broke them at import; moving the floor to 2.x
|
||||
without a ceiling would have left the identical trap for 3.0.0. mcp-windbg is an application
|
||||
rather than a library, so an upper bound cannot cause a diamond conflict for anyone, and it
|
||||
turns the next breaking SDK release into a Dependabot PR that fails CI instead of an install
|
||||
everybody has to work around by hand.
|
||||
- **A weekly canary tests the versions users will actually get** - CI installs from `uv.lock`, so
|
||||
it only ever proved the server works against the exact versions pinned there, while a PyPI
|
||||
install resolves to whatever the ranges allow. `dependency-canary.yml` closes that gap: it
|
||||
resolves to the newest allowed versions, ignoring the lock, and runs the hermetic suite against
|
||||
them every Monday. It is scheduled rather than attached to pull requests on purpose - an
|
||||
upstream release breaking the build should page the maintainer, not block someone's unrelated PR.
|
||||
|
||||
### Added
|
||||
|
||||
- **`wait_for_break`** - block until a target you resumed stops again (bugcheck, breakpoint, or a
|
||||
|
||||
+11
-4
@@ -22,11 +22,18 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
]
|
||||
# Every runtime dependency is capped at the next major. mcp-windbg is an
|
||||
# application, not a library - nothing depends on it, so an upper bound cannot
|
||||
# cause a diamond conflict for anyone, and it is the only thing that stops a
|
||||
# breaking SDK release from landing in fresh installs unannounced. mcp 2.0.0
|
||||
# did exactly that to 1.0.0 (#76): an unbounded `mcp>=1.28.1` resolved to it and
|
||||
# died at import before the server started. A cap turns that into a Dependabot
|
||||
# PR that fails CI instead of an install everyone has to work around by hand.
|
||||
dependencies = [
|
||||
"mcp>=1.28.1,<2.0.0",
|
||||
"pydantic>=2.13.4",
|
||||
"starlette>=1.3.1",
|
||||
"uvicorn>=0.51.0",
|
||||
"mcp>=2.0.0,<3.0.0",
|
||||
"pydantic>=2.13.4,<3.0.0",
|
||||
"starlette>=1.3.1,<2.0.0",
|
||||
"uvicorn>=0.51.0,<1.0.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
+54
-61
@@ -14,18 +14,20 @@ from .kd_session import KDSession
|
||||
from .filter_script import FilterScript, load_filter_script
|
||||
from .prompts import load_prompt
|
||||
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
||||
from mcp.types import (
|
||||
ErrorData,
|
||||
TextContent,
|
||||
Tool,
|
||||
Prompt,
|
||||
PromptArgument,
|
||||
PromptMessage,
|
||||
GetPromptResult,
|
||||
ListToolsResult,
|
||||
ListPromptsResult,
|
||||
CallToolResult,
|
||||
INVALID_PARAMS,
|
||||
INTERNAL_ERROR,
|
||||
)
|
||||
@@ -75,29 +77,23 @@ def _register_session(session, kind: str, label: str) -> str:
|
||||
|
||||
|
||||
def _require_session(session_id: str, kind: str):
|
||||
"""Return the session for ``session_id``, or raise a helpful McpError.
|
||||
"""Return the session for ``session_id``, or raise a helpful MCPError.
|
||||
|
||||
Enforces that the session is of the expected kind so ``run_cdb_command`` on a
|
||||
kernel session (or vice versa) fails clearly instead of misbehaving.
|
||||
"""
|
||||
record = _sessions.get(session_id)
|
||||
if record is None:
|
||||
raise McpError(ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=(
|
||||
raise MCPError(INVALID_PARAMS, (
|
||||
f"Unknown session_id {session_id!r}. Open a session first - the "
|
||||
f"open_* tools return a session_id to use here."
|
||||
),
|
||||
))
|
||||
))
|
||||
if record["kind"] != kind:
|
||||
actual = record["kind"]
|
||||
raise McpError(ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=(
|
||||
raise MCPError(INVALID_PARAMS, (
|
||||
f"session_id {session_id!r} is a {actual} session, not {kind}. "
|
||||
f"Use run_{actual}_command / close_{actual}_session for it."
|
||||
),
|
||||
))
|
||||
))
|
||||
return record["session"]
|
||||
|
||||
|
||||
@@ -110,19 +106,13 @@ def _require_live_session(session_id: str, what: str):
|
||||
"""
|
||||
record = _sessions.get(session_id)
|
||||
if record is None:
|
||||
raise McpError(ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=f"Unknown session_id {session_id!r}. Open a session first.",
|
||||
))
|
||||
raise MCPError(INVALID_PARAMS, f"Unknown session_id {session_id!r}. Open a session first.")
|
||||
session = record["session"]
|
||||
if not getattr(session, "is_live_session", False):
|
||||
raise McpError(ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=(
|
||||
raise MCPError(INVALID_PARAMS, (
|
||||
f"session_id {session_id!r} is a dump session; there is no "
|
||||
f"running target to {what}."
|
||||
),
|
||||
))
|
||||
))
|
||||
return session
|
||||
|
||||
|
||||
@@ -344,8 +334,12 @@ def _create_server(
|
||||
transport: str = "stdio",
|
||||
auto_dump_dir_symbols: bool = True,
|
||||
) -> Server:
|
||||
"""Create and configure the MCP server with all tools and prompts."""
|
||||
server = Server("mcp-windbg")
|
||||
"""Create and configure the MCP server with all tools and prompts.
|
||||
|
||||
mcp 2.x takes its handlers as constructor arguments rather than through
|
||||
``@server.<method>()`` decorators, so the server itself is built at the end
|
||||
of this function, once the handlers below exist.
|
||||
"""
|
||||
|
||||
def filter_tool_arguments(tool_name: str, arguments: dict | None, call_id: str) -> dict:
|
||||
if arguments is None:
|
||||
@@ -359,9 +353,8 @@ def _create_server(
|
||||
return content
|
||||
return content_filter.process_output(tool_name, content, transport, call_id)
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[Tool]:
|
||||
return [
|
||||
async def on_list_tools(ctx, params) -> ListToolsResult:
|
||||
return ListToolsResult(tools=[
|
||||
Tool(
|
||||
name="list_dumps",
|
||||
description="""
|
||||
@@ -447,10 +440,19 @@ def _create_server(
|
||||
""",
|
||||
inputSchema=WaitForBreak.model_json_schema(),
|
||||
),
|
||||
]
|
||||
])
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name, arguments: dict) -> list[TextContent]:
|
||||
async def on_call_tool(ctx, params) -> CallToolResult:
|
||||
"""Dispatch a tool call and wrap its content in the 2.x result type.
|
||||
|
||||
A raised MCPError still reaches the caller as an error - the SDK turns
|
||||
it into a JSON-RPC error response - so the handlers below keep raising
|
||||
rather than hand-building error results.
|
||||
"""
|
||||
content = await _dispatch_tool(params.name, params.arguments or {})
|
||||
return CallToolResult(content=content)
|
||||
|
||||
async def _dispatch_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||
try:
|
||||
call_id = uuid.uuid4().hex
|
||||
arguments = filter_tool_arguments(name, arguments, call_id)
|
||||
@@ -499,16 +501,13 @@ def _create_server(
|
||||
if name == "wait_for_break":
|
||||
return filter_tool_content(name, await _handle_wait_for_break(WaitForBreak(**arguments)), call_id)
|
||||
|
||||
raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Unknown tool: {name}"))
|
||||
raise MCPError(INVALID_PARAMS, f"Unknown tool: {name}")
|
||||
|
||||
except McpError:
|
||||
except MCPError:
|
||||
raise
|
||||
except Exception as e:
|
||||
traceback_str = traceback.format_exc()
|
||||
raise McpError(ErrorData(
|
||||
code=INTERNAL_ERROR,
|
||||
message=f"Error executing tool {name}: {str(e)}\n{traceback_str}"
|
||||
))
|
||||
raise MCPError(INTERNAL_ERROR, f"Error executing tool {name}: {str(e)}\n{traceback_str}")
|
||||
|
||||
# -- Tool handlers --------------------------------------------------------
|
||||
|
||||
@@ -516,12 +515,9 @@ def _create_server(
|
||||
args = ListDumps(**arguments)
|
||||
directory = args.directory_path or get_local_dumps_path()
|
||||
if directory is None:
|
||||
raise McpError(ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message="No directory path specified and no default dump path found in registry."
|
||||
))
|
||||
raise MCPError(INVALID_PARAMS, "No directory path specified and no default dump path found in registry.")
|
||||
if not os.path.exists(directory) or not os.path.isdir(directory):
|
||||
raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Directory not found: {directory}"))
|
||||
raise MCPError(INVALID_PARAMS, f"Directory not found: {directory}")
|
||||
|
||||
pattern = os.path.join(directory, "**", "*.*dmp") if args.recursive else os.path.join(directory, "*.*dmp")
|
||||
dump_files = sorted(glob.glob(pattern, recursive=args.recursive))
|
||||
@@ -552,7 +548,7 @@ def _create_server(
|
||||
timeout=effective, verbose=verbose, auto_dump_dir_symbols=auto_dump_dir_symbols,
|
||||
)
|
||||
except Exception as e:
|
||||
raise McpError(ErrorData(code=INTERNAL_ERROR, message=f"Failed to open cdb dump session: {e}"))
|
||||
raise MCPError(INTERNAL_ERROR, f"Failed to open cdb dump session: {e}")
|
||||
|
||||
session_id = _register_session(session, "cdb", f"dump {args.dump_path}")
|
||||
results = [_session_header(session_id, "cdb", f"crash dump {args.dump_path}")]
|
||||
@@ -574,7 +570,7 @@ def _create_server(
|
||||
symbols_path=effective_symbols, timeout=effective, verbose=verbose,
|
||||
)
|
||||
except Exception as e:
|
||||
raise McpError(ErrorData(code=INTERNAL_ERROR, message=f"Failed to open cdb remote session: {e}"))
|
||||
raise MCPError(INTERNAL_ERROR, f"Failed to open cdb remote session: {e}")
|
||||
|
||||
session_id = _register_session(session, "cdb", f"remote {args.connection_string}")
|
||||
results = [_session_header(session_id, "cdb", f"remote target {args.connection_string}")]
|
||||
@@ -596,7 +592,7 @@ def _create_server(
|
||||
symbols_path=effective_symbols, timeout=effective, verbose=verbose,
|
||||
)
|
||||
except Exception as e:
|
||||
raise McpError(ErrorData(code=INTERNAL_ERROR, message=f"Failed to open kd session: {e}"))
|
||||
raise MCPError(INTERNAL_ERROR, f"Failed to open kd session: {e}")
|
||||
|
||||
session_id = _register_session(session, "kd", f"kernel {args.connection_string}")
|
||||
results = [_session_header(session_id, "kd", f"kernel target {args.connection_string}")]
|
||||
@@ -707,9 +703,8 @@ def _create_server(
|
||||
},
|
||||
}
|
||||
|
||||
@server.list_prompts()
|
||||
async def list_prompts() -> list[Prompt]:
|
||||
return [
|
||||
async def on_list_prompts(ctx, params) -> ListPromptsResult:
|
||||
return ListPromptsResult(prompts=[
|
||||
Prompt(
|
||||
name=name,
|
||||
title=spec["title"],
|
||||
@@ -723,27 +718,19 @@ def _create_server(
|
||||
],
|
||||
)
|
||||
for name, spec in PROMPT_SPECS.items()
|
||||
]
|
||||
])
|
||||
|
||||
@server.get_prompt()
|
||||
async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult:
|
||||
if arguments is None:
|
||||
arguments = {}
|
||||
async def on_get_prompt(ctx, params) -> GetPromptResult:
|
||||
name, arguments = params.name, params.arguments or {}
|
||||
|
||||
spec = PROMPT_SPECS.get(name)
|
||||
if spec is None:
|
||||
raise McpError(ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=f"Unknown prompt: {name}"
|
||||
))
|
||||
raise MCPError(INVALID_PARAMS, f"Unknown prompt: {name}")
|
||||
|
||||
try:
|
||||
prompt_content = load_prompt(name)
|
||||
except FileNotFoundError as e:
|
||||
raise McpError(ErrorData(
|
||||
code=INTERNAL_ERROR,
|
||||
message=f"Prompt file not found: {e}"
|
||||
))
|
||||
raise MCPError(INTERNAL_ERROR, f"Prompt file not found: {e}")
|
||||
|
||||
target = arguments.get(spec["argument"], "")
|
||||
if target:
|
||||
@@ -764,7 +751,13 @@ def _create_server(
|
||||
],
|
||||
)
|
||||
|
||||
return server
|
||||
return Server(
|
||||
"mcp-windbg",
|
||||
on_list_tools=on_list_tools,
|
||||
on_call_tool=on_call_tool,
|
||||
on_list_prompts=on_list_prompts,
|
||||
on_get_prompt=on_get_prompt,
|
||||
)
|
||||
|
||||
|
||||
# Clean up function to ensure all sessions are closed when the server exits
|
||||
|
||||
@@ -24,7 +24,7 @@ from anyio import BrokenResourceError, ClosedResourceError
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.stdio import StdioServerParameters, stdio_client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
|
||||
from . import harness
|
||||
|
||||
@@ -41,10 +41,11 @@ DEFAULT_TIMEOUT = 180
|
||||
# stdout_reader catches anyio.ClosedResourceError but not BrokenResourceError
|
||||
# around its read_stream_writer.send(), so a send into a just-closed receiver
|
||||
# escapes as a BrokenResourceError inside the task group's ExceptionGroup.
|
||||
# Confirmed still present in mcp 1.27.2 (the latest at the time of writing), so
|
||||
# bumping the dependency does not fix it. We suppress it only after every step
|
||||
# has completed, so a real failure (which raises before `completed` is set) is
|
||||
# never masked.
|
||||
# mcp 2.0.0 fixed that specific race - stdout_reader now catches
|
||||
# BrokenResourceError as well as ClosedResourceError around its send - but the
|
||||
# guard is kept for the other transports and for teardown noise from any other
|
||||
# source. It suppresses only after every step has completed, so a real failure
|
||||
# (which raises before `completed` is set) is never masked.
|
||||
_TEARDOWN_NOISE = (BrokenResourceError, ClosedResourceError)
|
||||
|
||||
|
||||
@@ -215,7 +216,7 @@ async def _run_http(scenario, server_args, remote_server, mapping, timeout) -> N
|
||||
url = f"http://127.0.0.1:{port}/mcp"
|
||||
try:
|
||||
with anyio.fail_after(timeout):
|
||||
async with streamable_http_client(url) as (read_stream, write_stream, _):
|
||||
async with streamable_http_client(url) as (read_stream, write_stream):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
await session.initialize()
|
||||
await _run_steps(scenario, session, remote_server, mapping)
|
||||
@@ -271,7 +272,7 @@ async def _run_step(
|
||||
name = step["get_prompt"]
|
||||
try:
|
||||
result = await session.get_prompt(name, arguments or None)
|
||||
except McpError as exc:
|
||||
except MCPError as exc:
|
||||
_check(scenario, index, f"get_prompt {name}", expect, str(exc), is_error=True)
|
||||
return
|
||||
text = "\n".join(
|
||||
@@ -336,13 +337,14 @@ def _apply_capture(
|
||||
|
||||
|
||||
async def _call_tool(session: ClientSession, tool: str, arguments: dict[str, Any]):
|
||||
"""Call a tool, normalizing both isError results and raised McpErrors."""
|
||||
"""Call a tool, normalizing both isError results and raised MCPErrors."""
|
||||
try:
|
||||
result = await session.call_tool(tool, arguments)
|
||||
except McpError as exc:
|
||||
except MCPError as exc:
|
||||
return True, str(exc)
|
||||
text = "\n".join(getattr(item, "text", "") for item in result.content)
|
||||
return bool(result.isError), text
|
||||
# mcp 2.x names the field is_error in Python; the wire format is still isError.
|
||||
return bool(result.is_error), text
|
||||
|
||||
|
||||
def _label(scenario: dict[str, Any], index: int, kind: str) -> str:
|
||||
|
||||
@@ -6,7 +6,8 @@ resolution-markers = [
|
||||
"python_full_version >= '3.14' and sys_platform != 'win32'",
|
||||
"python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version < '3.11' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'",
|
||||
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')",
|
||||
"python_full_version < '3.11' and sys_platform != 'win32'",
|
||||
]
|
||||
|
||||
@@ -469,40 +470,42 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
name = "httpcore2"
|
||||
version = "2.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
{ name = "truststore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
name = "httpx2"
|
||||
version = "2.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "httpcore2", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" },
|
||||
{ name = "idna" },
|
||||
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.3"
|
||||
name = "httpx2-jsfetch"
|
||||
version = "1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -652,15 +655,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.28.1"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "httpx2" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "mcp-types" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
@@ -670,9 +673,22 @@ dependencies = [
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-types"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -705,12 +721,12 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "coverage", extras = ["toml"], marker = "extra == 'test'", specifier = ">=7.6" },
|
||||
{ name = "mcp", specifier = ">=1.28.1,<2.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.13.4" },
|
||||
{ name = "mcp", specifier = ">=2.0.0,<3.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.13.4,<3.0.0" },
|
||||
{ name = "pytest", marker = "extra == 'test'", specifier = ">=9.1.1" },
|
||||
{ name = "pyyaml", marker = "extra == 'test'", specifier = ">=6.0.2" },
|
||||
{ name = "starlette", specifier = ">=1.3.1" },
|
||||
{ name = "uvicorn", specifier = ">=0.51.0" },
|
||||
{ name = "starlette", specifier = ">=1.3.1,<2.0.0" },
|
||||
{ name = "uvicorn", specifier = ">=0.51.0,<1.0.0" },
|
||||
]
|
||||
provides-extras = ["test"]
|
||||
|
||||
@@ -775,6 +791,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.44.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
@@ -933,20 +961,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.14.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
@@ -991,15 +1005,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.32"
|
||||
@@ -1319,7 +1324,8 @@ resolution-markers = [
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.14' and sys_platform != 'win32'",
|
||||
"python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'",
|
||||
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
|
||||
wheels = [
|
||||
@@ -1533,6 +1539,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truststore"
|
||||
version = "0.10.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twine"
|
||||
version = "6.2.0"
|
||||
|
||||
Reference in New Issue
Block a user