modelcontextprotocol / modelcontextprotocol/python-sdk
perf: JSONRPCMessage smart union scores all four branches on every inbound message
Personne n'a encore pris cette issue.
- Langage dominant
- Python
- Étoiles
- 24.3k
- Forks
- 4k
- Merge moyen
- 1 j 1 h
- PR mergées (30 j)
- 31
Description
Description
jsonrpc_message_adapter (src/mcp-types/mcp_types/jsonrpc.py) validates the
JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError union in
Pydantic's smart-union mode, which attempts/scores every branch for every message.
This adapter is the single decode choke point for every transport:
server/streamable_http.py(POST body),server/sse.py,server/stdio.pyclient/streamable_http.py(SSE data + JSON responses),client/sse.py,client/stdio.py
The four variants are trivially distinguishable by key presence per JSON-RPC 2.0
(method [+ valid id] → request/notification, error → error, result → response),
so a callable Discriminator + Tags (available at the repo's pydantic floor,
>=2.12.0) selects exactly one branch instead.
Measured impact
scripts/bench_jsonrpc_codec.py (proposed alongside the fix), darwin arm64 / CPython 3.14.2 / pydantic 2.12.5, best-of-5:
| payload | path | smart union | discriminator | speedup |
|---|---|---|---|---|
| request 109B | validate_json | 1.76µs | 1.05µs | 1.69x |
| request 109B | from_json + validate_python | 1.45µs | 0.90µs | 1.60x |
| notification 105B | validate_json | 1.88µs | 0.86µs | 2.18x |
| notification 105B | from_json + validate_python | 1.29µs | 0.80µs | 1.60x |
| result 22.3KB | validate_json | 51.7µs | 32.1µs | 1.61x |
| result 22.3KB | from_json + validate_python | 12.7µs | 12.2µs | 1.04x |
For high-throughput agent communication (many small requests/notifications per
second per session) this halves envelope-decode CPU on the hot path of every
transport.
Behavior compatibility
The fix keeps classification parity with today's smart-union outcomes for all
spec-valid messages, verified empirically and pinned by tests:
{"id": null | 1.5 | true, "method": ...}and absent-id →JSONRPCNotification(current downgrade behavior preserved; note the interaction with #2057 / PR #2075 - if that lands, the discriminator's id-guard becomes the single, clean place to implement the rejection)- degenerate
{method, id, result}→JSONRPCRequest;{id, result, error}→JSONRPCError JSONRPCMessagestays a plain union (soisinstance(x, JSONRPCMessage)keeps working); theAnnotated/Tagwrapping lives only inside theTypeAdapter- wire output (
dump_json,by_alias=True,exclude_none=True) is byte-identical - deliberate divergence on spec-invalid hybrids (surfaced by cubic's review on the PR):
{method, error}hybrids classify as a call instead ofJSONRPCError, and
{method: <non-str>, result}is rejected instead of falling through to
JSONRPCResponse- smart union picked those via field-count scoring, which let a
malformed frame masquerade as an error response to a pending request; pinned by tests - only visible change:
ValidationErrorfor unclassifiable input becomes a single
jsonrpc_message_invaliderror with a clear message instead of a 4-branch error dump
(no in-repo test asserts the old text; error codes and HTTP statuses unchanged)
Additional finding worth recording: with a callable discriminator,
validate_json must materialize the payload to call the discriminator, so the
server's existing two-phase parse (pydantic_core.from_json → validate_python)
is the faster path on large bodies (12.2µs vs 32.1µs on 22KB) - the fix adds a
comment pinning that so it doesn't get "cleaned up" into a regression later.
Proposed fix
PR ready: key-presence callable discriminator on the adapter, zero call-site
changes, new parity/branch tests (100% coverage maintained), standalone
micro-benchmark script.
Disclosure: this issue and the accompanying PR were developed with AI assistance
(Claude Code); all measurements and parity checks were run and verified locally.
References
- Draft PR with the implementation, measurements, and tests: #3135 (kept in draft pending this issue)
- Related: #2057 / #2075 (null-id classification - the discriminator id-guard is the natural locus for that change if it lands)
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Piste de recherche
Commencez dans src/mcp-types/mcp_types/jsonrpc.py, au niveau de jsonrpc_message_adapter, puis examinez les points d’appel du transport mentionnés dans l’issue afin de confirmer qu’aucune modification n’est requise. Exécutez scripts/bench_jsonrpc_codec.py et les tests de parité/branch proposés ; le travail est considéré comme terminé lorsque la validation d’une seule branch, le comportement wire et la parité de classification sont préservés, avec le comportement documenté pour les entrées invalides.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- python
- Domaine
- backend-api-design, performance
- Type d'issue
- Refactorisation
- Difficulté
- 3/5
- Temps estimé
- 1-2 jours
- Activité
- À l'abandon
- Clarté
- Clairement spécifiée
- Accessibilité débutants
- 25/100