Skip to content

Migrate to MCP Python SDK v2 (dual-era protocol support) - #89

Merged
ChiragAgg5k merged 1 commit into
mainfrom
feat/mcp-sdk-v2-migration
Jul 29, 2026
Merged

Migrate to MCP Python SDK v2 (dual-era protocol support)#89
ChiragAgg5k merged 1 commit into
mainfrom
feat/mcp-sdk-v2-migration

Conversation

@ChiragAgg5k

Copy link
Copy Markdown
Member

Summary

Ports mcp-server-appwrite from MCP Python SDK v1.28 to v2.0.0, which brings free support for the 2026-07-28 (stateless / modern) protocol while keeping the existing 2025-era handshake path working for every client shipping today.

The HTTP/OAuth surface (StreamableHTTPSessionManager(stateless=True), bearer auth, discovery routes) is unchanged. The cost is in the low-level Server rewrite: decorators → on_* constructor handlers, full result types, snake_case mcp.types, and explicit CallToolResult(is_error=True) so tool failures stay model-visible.

Also keeps mcp.handshake metrics alive for modern clients (no initialize), and fixes integration smokes for the Appwrite SDK’s required variable_id on create_variable.

Why this migration

v2’s StreamableHTTPSessionManager routes on MCP-Protocol-Version:

  • handshake versions (2024-11-052025-11-25) → existing legacy/stateless path
  • anything else (including 2026-07-28) → modern per-request handler

So one hosted app serves both eras with no new ASGI wiring.

flowchart LR
  req["POST / or /mcp"] --> bearer["RequireBearer + AuthContext"]
  bearer --> mgr["StreamableHTTPSessionManager"]
  mgr --> check{"Handshake protocol version?"}
  check -->|yes| legacy["legacy / initialize path"]
  check -->|no| modern["handle_modern_request"]
  legacy --> srv["low-level Server on_* handlers"]
  modern --> srv
Loading

Dependencies

pyproject.toml / uv.lock:

  • mcp[cli]>=1.12.0,<2mcp[cli]>=2,<3
  • opentelemetry-{api,sdk,exporter-otlp-proto-http} floor → >=1.28 (v2 hard-depends on this)
  • keep explicit httpx (v2 swaps its internal client to httpx2; we still use httpx for uploads / OAuth proxy / discovery)

Core: rewrite build_mcp_server

Decorators are gone. Handlers take ServerRequestContext + typed params and return full result objects:

async def handle_list_tools(
    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
    _emit_initialize(ctx)
    tools = operator.get_public_tools()
    return types.ListToolsResult(tools=tools)

server = Server(
    "Appwrite MCP Server",
    version=SERVER_VERSION,
    instructions=instructions,
    website_url=SERVER_WEBSITE_URL,
    icons=[types.Icon(src=SERVER_ICON_URL, mime_type="image/svg+xml")],
    on_list_tools=handle_list_tools,
    on_call_tool=handle_call_tool,
    on_list_resources=handle_list_resources,
    on_list_resource_templates=handle_list_resource_templates,
    on_read_resource=handle_read_resource,
)

Keep tool errors visible to the model

v1 wrapped raised exceptions into CallToolResult(isError=true). v2 turns uncaught exceptions into a sanitized -32603. We return an error result explicitly so confirm_write refusals and Appwrite API errors still reach the model:

except Exception as exc:
    # telemetry + Sentry unchanged …
    return types.CallToolResult(
        content=[types.TextContent(type="text", text=str(exc))],
        is_error=True,
    )

Unknown resource URIs raise MCPError(INVALID_PARAMS, …) so the message survives.

Dual-era client identity

_emit_initialize / _mcp_request_context now take ServerRequestContext. Prefer 2025-era ctx.session.client_params, fall back to 2026-era ctx.meta[CLIENT_INFO_META_KEY], and always take protocol version from ctx.protocol_version.

Snake_case type sweep

v2’s mcp.types attributes are snake_case. Constructor kwargs still accept camelCase, so reads are the dangerous half:

Before After
tool.inputSchema tool.input_schema
mimeType= / .mimeType mime_type= / .mime_type
uriTemplate= uri_template=
Resource(uri=AnyUrl(...)) plain str URI
item.model_dump(mode="json") …(mode="json", by_alias=True) so stored resources keep camelCase wire keys

Touched: server.py, operator.py, service.py, docs_search.py.

HTTP / CORS

constants.py — allow the SEP-2243 routing headers modern clients send:

"Access-Control-Allow-Headers": (
    "Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, "
    "Mcp-Method, Mcp-Name"
),

Telemetry: backwards-compatible handshake for modern clients

Modern clients never send initialize, so body-sniffing alone would silence mcp.handshake{status="success"}.

  • set_request_identity now returns True when it opens a new (client, subject) activity window
  • new record_stateless_connection emits one handshake success per new window (same instrument / attrs)
  • MCPIdentityMiddleware keeps the legacy initialize path, and for modern requests reads _meta clientInfo / protocolVersion (User-Agent / header fallback)

Legacy initialize counting is unchanged, so the timeseries stays comparable through the client migration.

Integration test fix (Appwrite SDK, not MCP)

Python SDK ≥18.1 requires client-supplied variable_id on functions.create_variable / sites.create_variable (same pattern as function_id / document_id). Our smokes still omitted it.

runner.call(
    "functions_create_variable",
    {
        "function_id": function_id,
        "variable_id": variable_id,  # new
        "key": "GREETING",
        "value": "hello",
    },
)

Same for sites. Unrelated to MCP v2; surfaced while E2Eing against Cloud on appwrite==22.2.0.

Files changed

Area Files
Deps pyproject.toml, uv.lock
Handlers / types server.py, operator.py, service.py, docs_search.py
HTTP / CORS / identity http_app.py, constants.py
Metrics telemetry.py
Unit tests test_server.py, test_http_app.py, test_operator.py, test_service.py, test_telemetry.py
Integration test_functions.py, test_sites.py

Verification

  • ruff check src tests
  • black --check src tests
  • pyright
  • Unit tests (193 passed, including is_error=True tool failures and modern handshake dedupe)
  • Dual-era in-process Client smoke (mode="legacy"2025-11-25, default/auto → 2026-07-28) against a live Cloud-backed operator: tools/list, search, users_list, confirm_write refusal, get_context, catalog resources/read, docs search, tables_db_list
  • Live integration suite against Cloud (nyc test project) — functions + sites smokes green after variable_id fix
  • Local HTTP: /healthz, protected-resource metadata, CORS allows Mcp-Method/Mcp-Name, unauth POST → 401 + WWW-Authenticate (legacy + modern headers)
  • docker build (Docker daemon unavailable in the agent environment — please run locally / CI)

Breaking changes for existing users?

No intentional user-facing breaks for current MCP clients.

  • 2025-era clients keep the initialize handshake path
  • Public tool names / confirm_write behavior unchanged
  • Hosted OAuth / stdio API-key auth unchanged
  • Stored MCP resources keep camelCase JSON via by_alias=True

What does change under the hood: modern (2026-07-28) clients work; handshake metrics for those clients are keyed off activity sessions instead of initialize; operators on Appwrite SDK ≥18.1 must pass variable_id when calling create-variable tools (API change, not MCP).

Test plan

  • CI green (lint, unit, docker, integration with secrets)
  • Spot-check hosted HTTP against Cloud with a current client (Cursor / Claude) — tools/list + a read call + a refused write
  • After deploy, confirm Grafana mcp_handshake / active-session panels still move for both legacy and modern traffic
  • Optional: Client(mode="legacy") and Client() against a staging MCP URL once the image is published

Made with Cursor

Port the low-level Server handlers to v2's on_* constructor API, preserve
model-visible tool errors, keep handshake metrics alive for 2026-07-28
clients, and fix integration tests for required variable_id.

Co-authored-by: Cursor <cursoragent@cursor.com>
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown

Greptile Summary

Migrates the server to MCP Python SDK v2 while preserving legacy and modern protocol support.

  • Replaces decorator-based MCP handlers with typed v2 request handlers and result objects.
  • Updates MCP model fields, resource serialization, and explicit tool-error responses.
  • Adds modern-client identity and stateless handshake telemetry.
  • Allows modern MCP routing headers through CORS.
  • Updates dependency resolution and Appwrite variable integration tests.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code failures identified.

The MCP v2 handler, model, HTTP identity, telemetry, and integration-test changes remain internally consistent, and the investigated dependency advisories did not originate from changed vulnerable versions or expose a reachable changed-code security path.

Important Files Changed

Filename Overview
src/mcp_server_appwrite/server.py Rewrites low-level MCP handlers for the v2 API, preserves model-visible tool errors, and adapts resource and request-context handling.
src/mcp_server_appwrite/http_app.py Adds modern-protocol detection and request metadata extraction for identity and handshake telemetry.
src/mcp_server_appwrite/telemetry.py Makes identity binding report newly opened activity windows and records deduplicated stateless handshakes.
src/mcp_server_appwrite/operator.py Migrates MCP model attributes to snake_case and preserves aliased wire serialization for stored content.
src/mcp_server_appwrite/service.py Updates generated tool definitions to use the v2 input_schema attribute.
pyproject.toml Moves the MCP dependency to v2 and raises the OpenTelemetry dependency floors.
uv.lock Re-resolves dependencies for MCP v2, including the new mcp-types and httpx2 dependency families.
tests/unit/test_server.py Updates v2 field expectations and tests explicit error results and dual-era request metadata.
tests/unit/test_http_app.py Tests modern request detection, metadata extraction, identity fallback, and handshake deduplication.
tests/unit/test_telemetry.py Tests activity-window creation and stateless handshake behavior.
tests/integration/test_functions.py Supplies the now-required variable_id when creating function variables.
tests/integration/test_sites.py Supplies the now-required variable_id when creating site variables.

Reviews (1): Last reviewed commit: "Migrate to MCP Python SDK v2 with dual-e..." | Re-trigger Greptile

@ChiragAgg5k
ChiragAgg5k merged commit f37d118 into main Jul 29, 2026
5 checks passed
@ChiragAgg5k
ChiragAgg5k deleted the feat/mcp-sdk-v2-migration branch July 29, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant