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:
Sven Scharmentke
2026-08-22 16:31:13 +02:00
committed by GitHub
parent 30c7dd9d9f
commit 356a6bdfe1
6 changed files with 220 additions and 126 deletions
+51
View File
@@ -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
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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
+12 -10
View File
@@ -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:
Generated
+66 -51
View File
@@ -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"