diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc1157..08f0b30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Security + +- **`mcp` 2.0.0 → 2.2.0** (GHSA-84m7-p3x7-pcfv, CVE-2026-59951: unbounded session accumulation in the StreamableHTTP + transport). (Dependabot #356) + +### Fixed + +- **Tool error messages survive `mcp` 2.2.0.** The SDK now replaces the message of any exception that is not a + `ToolError` with a generic "Error executing tool ". MCPg's own errors (`MCPgError` subclasses, e.g. an + unconfigured NL→SQL provider that names the env var to set) are re-raised as `ToolError` by the call wrapper, and the + audit log records their text; the heavy-diagnostics gate raises `ToolError`. Unexpected exceptions keep the SDK's + generic message. + ## [0.8.3] - 2026-10-14 A feature release by content (new tools and opt-in flags, nothing removed or renamed), shipped as a patch per the maintainer's call: the default tool surface only gains five read-only tools, and every new write or admin tool needs an explicit `MCPG_ALLOW_*` flag. See [release notes](docs/release-notes-0.8.3.md). diff --git a/src/mcpg/server.py b/src/mcpg/server.py index c68b1c7..c3f9512 100644 --- a/src/mcpg/server.py +++ b/src/mcpg/server.py @@ -25,6 +25,7 @@ from mcpg.context import AppContext from mcpg.cursors import CursorManager from mcpg.database import Database +from mcpg.errors import MCPgError from mcpg.listen import ListenManager from mcpg.middleware.rate_limit import RateLimiter from mcpg.observability import get_metrics @@ -204,6 +205,18 @@ async def call_tool( ) metrics.record_call(name, "error", duration, bucket=bucket) raise friendly_exc from exc + # Since mcp 2.2.0 the SDK wraps any exception that is not a + # ToolError in UnexpectedToolError("Error executing tool ") + # and drops its message (unexpected errors can leak internals). + # MCPgError messages are written for the client ("provider + # 'openai' is not configured ...; set OPENAI_API_KEY"), so they + # go back as a ToolError, and the audit log gets the real text. + domain_error = exc if isinstance(exc, MCPgError) else exc.__cause__ + if isinstance(domain_error, MCPgError): + tool_exc = ToolError(str(domain_error)) + audit.record(audit.AuditEvent(tool=name, arguments=arguments, status="error", error=str(tool_exc))) + metrics.record_call(name, "error", duration, bucket=bucket) + raise tool_exc from domain_error audit.record(audit.AuditEvent(tool=name, arguments=arguments, status="error", error=str(exc))) metrics.record_call(name, "error", duration, bucket=bucket) raise diff --git a/src/mcpg/tools.py b/src/mcpg/tools.py index ff3b51e..ac93828 100644 --- a/src/mcpg/tools.py +++ b/src/mcpg/tools.py @@ -12,6 +12,7 @@ from typing import Annotated, Any, TypeVar, cast from mcp.server.mcpserver import Context, MCPServer +from mcp.server.mcpserver.exceptions import ToolError from mcp.types import ToolAnnotations from pydantic import Field @@ -245,10 +246,14 @@ async def _cached_call( # noqa: UP047 def _check_heavy_diagnostics(ctx: _Ctx, tool_name: str) -> None: - """Raise a RuntimeError if heavy diagnostics are disabled by settings.""" + """Raise a ToolError if heavy diagnostics are disabled by settings. + + A ToolError, not a RuntimeError: since mcp 2.2.0 the SDK replaces the message + of any other exception with a generic "Error executing tool ..." text, and this + message is meant for the client.""" settings = ctx.request_context.lifespan_context.settings if not settings.enable_heavy_diagnostics: - raise RuntimeError( + raise ToolError( f"The tool {tool_name!r} has been disabled by the server administrator " "(MCPG_ENABLE_HEAVY_DIAGNOSTICS is set to false)." ) diff --git a/uv.lock b/uv.lock index 32ba1d8..eed4b9b 100644 --- a/uv.lock +++ b/uv.lock @@ -1307,7 +1307,7 @@ wheels = [ [[package]] name = "mcp" -version = "2.0.0" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1325,9 +1325,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/76/31/ac54fb0fdd5b37de704486e288bba4fbbb463f24cfcfedbede407b854513/mcp-2.2.0.tar.gz", hash = "sha256:2dc37ecb1974becdcebdbf7561e7c15a07dbbf20ba21ba16c3593b3038b3afbd", size = 4084129, upload-time = "2026-09-07T16:06:23.439Z" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/1b/ff/8e7eade68b8a28f7da0ed1085544341b51f9c935dbf6b95c76b7edfea6a0/mcp-2.2.0-py3-none-any.whl", hash = "sha256:bde982589473a060ae145e3406e9a5333fe538c97229ba841f5a7f92be004f81", size = 365656, upload-time = "2026-09-07T16:06:19.711Z" }, ] [package.optional-dependencies] @@ -1338,15 +1338,15 @@ cli = [ [[package]] name = "mcp-types" -version = "2.0.0" +version = "2.2.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" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/91/762d7755d971aff8a28d75f7961656148edf27875c8026e6385aaab08ae7/mcp_types-2.2.0.tar.gz", hash = "sha256:d3ed53703ddd10d9c6399f29d322bb66f3f67ab41348ac8556ba23e07fedefad", size = 65892, upload-time = "2026-09-07T16:06:25.187Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/8f/d7/6ffba5d8cd5dd9b8a19478875c50e04945314ba5074e84d749283f27f62d/mcp_types-2.2.0-py3-none-any.whl", hash = "sha256:ea476b73ee86709ab5abc9452385ed36cc05907e582355622e294595c9a04f13", size = 69106, upload-time = "2026-09-07T16:06:21.461Z" }, ] [[package]]