Skip to content

[MCP] Allineamento dei 13 server MCP alla spec 2026-07-28 + SDK v2 e hardening (tracking) #250

Description

@gzileni

Contesto

La revisione 2026-07-28 della
specifica MCP è la più invasiva dalla nascita del protocollo: MCP diventa stateless (via
l'handshake initialize e le sessioni), arriva server/discover obbligatorio, gli header
Mcp-Method/Mcp-Name diventano requisiti di conformità, e Roots/Sampling/Logging entrano in
deprecazione. In parallelo l'SDK Python mcp 2.0.0 (rilasciato lo stesso giorno) implementa la
revisione e rinomina FastMCPMCPServer, spostando la configurazione di trasporto dal
costruttore a run().

Questa issue è il tracking dell'allineamento dei 13 server MCP del repo, più una serie di
miglioramenti di qualità dei tool che oggi sono a metà strada.

Tipo: tracking / piano realizzativo. Deliverable: 5 PR indipendenti (Fase 0–3 + 5) con
sotto-issue collegate. La Fase 4 è bloccata da upstream.


Stato attuale (fotografia del repo)

13 pacchetti MCP, ~80 tool, tutti sullo stesso stampo:

pacchetto tool ToolAnnotations tipo di ritorno
ckan-mcp-server 12 dict
istat-mcp-server 10 dict
opencoesione-mcp-server 8 dict
openpnrr-mcp-server 6 dict
maturity-mcp-server 5 dict
opendata-mcp-server 5 dict
centriditalia-mcp-server 5 dict
ods-mcp-server 3 dict
socrata-mcp-server 3 dict
web-mcp 2 dict
bdap-mcp-server 1 dict
ispra-mcp-server 1 dict
osm-mcp 19 str (JSON serializzato a mano)

Punti strutturali:

  • Pin identico in 14 pyproject.toml: mcp>=1.2.0,<2.0 (mcp[cli] per osm-mcp).
  • server.py duplicato 13 volte: il diff fra ckan-mcp-server/src/ckan_mcp/server.py e
    ods-mcp-server/src/ods_mcp/server.py differisce solo per stringhe di log/nome/istruzioni.
    Stesso build_server(), stesso /healthz, stesso dispatch stdio|streamable-http|sse.
  • Nessun server espone resources o prompts → capability annunciate = solo tools.
  • Nessuna autenticazione su nessun endpoint MCP HTTP; HOST=0.0.0.0, porte pubblicate su tutte
    le interfacce in docker-compose.yml, e nessun TransportSecuritySettings (allowlist
    Host/Origin) configurato da nessuna parte.
  • Ramo TRANSPORT=sse ancora presente in tutti i server.py (transport deprecato dal
    2025-03-26, ora classificato Deprecated sotto la nuova feature-lifecycle policy).
  • Gli smoke test make mcp-stdio-* inviano {"jsonrpc":"2.0","id":1,"method":"tools/list"}
    senza initialize e senza _meta → non validi in era moderna.
  • osm-mcp usa MCP_TRANSPORT mentre gli altri 12 usano TRANSPORT.
  • Zero test di conformità protocollo: i test coprono i client HTTP (test_ckan_client.py,
    test_sdmx_client.py), non il livello MCP.

⚠️ Problema di sicurezza indipendente dalla spec

Il floor mcp>=1.2.0 lascia installabili versioni con 6 advisory HIGH già pubblicate. Nulla nel
repo impedisce a un build di risolvere mcp==1.2.0:

range vulnerabile patch advisory
< 1.9.4 1.9.4 DoS via validation error in FastMCP
< 1.10.0 1.10.0 unhandled exception in Streamable HTTP → DoS
< 1.23.0 1.23.0 DNS rebinding protection non attiva per default
>= 1.23.0, <= 1.27.1 1.27.2 task handler: un client accede/cancella i task di altri
<= 1.27.1 1.27.2 HTTP transport serve richieste di sessione senza verificare il principal autenticato
< 1.28.1 1.28.1 WebSocket transport senza validazione Host/Origin

Va chiuso anche se il resto del piano slitta (→ Fase 0).


Cosa cambia con 2026-07-28

Le parti che ci toccano:

  1. MCP diventa stateless. Spariscono initialize / notifications/initialized e l'header
    Mcp-Session-Id. Ogni richiesta porta in _meta la propria
    io.modelcontextprotocol/protocolVersion + clientCapabilities; il server timbra serverInfo
    in ogni risultato.
  2. server/discover è obbligatorio (MUST): versioni supportate, capability, identità,
    instructions.
  3. Ogni result ha resultType ("complete" | "input_required").
  4. Header obbligatori sui POST: MCP-Protocol-Version, Mcp-Method, e Mcp-Name per
    tools/call/resources/read/prompts/get. Il server MUST validare header vs body →
    altrimenti 400 + -32020 HeaderMismatch.
  5. Rimossi: endpoint GET, resources/subscribe, ping, logging/setLevel, resumability SSE
    (Last-Event-ID). Le notifiche di cambiamento passano da subscriptions/listen.
  6. MRTR sostituisce ogni richiesta server→client (sampling, elicitation, roots): il server
    ritorna InputRequiredResult, il client ritenta con inputResponses.
  7. ttlMs + cacheScope obbligatori su tools/list, prompts/list, resources/list,
    resources/read, resources/templates/list.
  8. Deprecati: Roots, Sampling, Logging MCP-level, transport HTTP+SSE, OAuth DCR (→ Client ID
    Metadata Documents).
  9. tools/list SHOULD avere ordine deterministico (prompt cache); inputSchema/outputSchema
    accettano qualsiasi keyword JSON Schema 2020-12.

Breaking changes dell'SDK Python v2 che ci colpiscono

  • FastMCPMCPServer, mcp.server.fastmcpmcp.server.mcpserver (import path
    rimosso, non deprecato).
  • Config trasporto spostata dal costruttore a run(): host, port,
    streamable_http_path, stateless_http, json_response, transport_security non sono più
    argomenti di MCPServer(...)MCPServer("x", port=9000) è un TypeError.
  • Tutti i campi wire in snake_case (tool.input_schema, result.is_error).
  • McpErrorMCPError; sollevarlo dentro un tool ora è un errore di protocollo, non
    is_error=True.
  • httpxhttpx2, con TLS verificato via truststore (trust store del sistema) invece di
    certifi. mcp-types nuova dipendenza pin-esatta, opentelemetry-api obbligatoria.
  • Le funzioni def sincrone girano su worker thread; il lifespan Streamable HTTP gira una volta
    all'avvio (non più per sessione/richiesta).
  • @mcp.tool() / @mcp.resource() / @mcp.prompt() restano invariati come firma → per server
    decorator-based come i nostri il porting è per il 90% i due punti sopra.

Il vincolo che decide la strategia

agent-framework-core[all] 1.12.1 → mcp<2,>=1.24.0

opendata-backend usa agent_framework.MCPStreamableHTTPTool (factory.py:30,324) come client
MCP: non può installare mcp 2.x finché agent-framework non alza il cap.

Questo non blocca i server. L'SDK v2 serve entrambe le ere sulla stessa app:
streamable_http_app() risponde sia all'initialize di un client 2025 sia alle richieste
per-request-_meta di un client 2026, senza flag né deployment separati. Quindi:

Server → mcp 2.x subito. Client (backend) → resta legacy e continua a funzionare.

Le due migrazioni sono disaccoppiate: è il cardine del piano.


Fase 0 — Hardening del pin e della rete

~0.5 gg · nessuna dipendenza · nessun cambio di comportamento. Chiude le 6 advisory.

  • 14 pyproject.toml: mcp>=1.28.1,<2.0 (mcp[cli]>=1.28.1,<2.0 per osm-mcp) — compatibile
    con il floor >=1.24.0 di agent-framework.
  • Attivare la DNS rebinding protection esplicitamente:
    TransportSecuritySettings(allowed_hosts=..., allowed_origins=...) da
    mcp.server.transport_security, valorizzata da env (MCP_ALLOWED_HOSTS/MCP_ALLOWED_ORIGINS).
    Con HOST=0.0.0.0 la protezione automatica non scatta.
  • docker-compose.yml: porte MCP su 127.0.0.1:1808x:808x (oggi 13 endpoint MCP non
    autenticati sono esposti su tutte le interfacce in dev). In prod l'overlay aes-infra le
    tiene interne, ma il compose locale no.
  • pip-audit (o Dependabot) nel job ci.yml, per non riaprire il buco.

Fase 1 — Runtime MCP condiviso

~2 gg · prerequisito per fare le Fasi 2–3 una volta invece di tredici.

Nuovo pacchetto opendata-mcp-runtime/non in opendata_core/: l'invariante di CLAUDE.md
vieta FastMCP/FastAPI lì.

  • build_mcp_server(*, name, title, instructions, version, register, log_name) -> MCPServer
  • run_from_env(server): legge TRANSPORT/HOST/PORT/MCP_PATH, monta /healthz, applica
    transport_security.
  • Riscrivere i 13 server.py sopra il runtime (~15 righe ciascuno).
  • Allineare MCP_TRANSPORT (osm) → TRANSPORT, aggiornando Makefile:225.
  • Rimuovere il ramo sse (un solo punto).
  • osm-mcp: l'istanza FastMCP a livello di modulo (osm-mcp/src/osm_mcp/server.py:12) passa a
    build_server() come gli altri — necessario per la Fase 3, dove host/port/
    streamable_http_path non sono più argomenti del costruttore.
  • Dockerfile: copiare opendata-mcp-runtime/ accanto a opendata_core/, context = root (R1).

Fase 2 — Qualità dei tool

~2–3 gg · dopo la Fase 1. Paga subito sull'agente e prepara la spec nuova.

  • ToolAnnotations su tutti i ~80 tool. Nessun tool del repo muta stato (nessun
    create_*/update_*/delete_*): readOnlyHint=True, idempotentHint=True, openWorldHint=True (leggono portali esterni). Oggi ce l'hanno 3 server su 13.
  • title leggibile per tool e server (nuovo parametro posizionale in MCPServer), più
    icons / website_url.
  • osm-mcp: -> str-> dict. 19 tool su 19 ritornano JSON dentro una stringa: il client
    non riceve né outputSchemastructuredContent e il modello deve fare un parse. È l'unico
    server fuori standard nel repo.
  • Ordine deterministico di tools/list (nuovo SHOULD): migliora l'hit rate della prompt
    cache Anthropic, rilevante dato che resolve_provider di default è claude (R11).
  • outputSchema espliciti → budget di contesto. Il commento in
    istat-mcp-server/src/istat_mcp/tools.py:28 documenta che FastMCP duplica il payload in
    content e structuredContent, raddoppiando il prompt, e per questo _MAX_PAYLOAD_CHARS
    è tenuto a 80 KB. Con outputSchema dichiarato e structured_output controllato si serve solo
    la forma strutturata e si alza il budget utile — guadagno diretto sui codelist tipo
    CL_ITTER107.
  • Paginazione a cursore dove oggi c'è solo limit (ckan_search_datasets,
    istat_list_dataflows, …): la spec ha nextCursor, noi tronchiamo.

Fase 3 — Migrazione SDK v2 / protocollo 2026-07-28

~3–4 gg · dopo la Fase 1. Con il runtime condiviso, il lavoro è concentrato in un punto.

Nel runtime (una volta):

  • from mcp.server import MCPServer; args di trasporto → run(transport=..., host=..., port=..., streamable_http_path=..., transport_security=...).
  • cache_hints= per popolare ttlMs/cacheScope, mappati sui TTL che già gestiamo (catalogo
    SDMX 24h via SDMX_PERSISTENT_CACHE_TTL_SECONDS, classify 24h): cacheScope="public" per i
    cataloghi, "private" per tutto ciò che dipende dall'utente.
  • server/discover e subscriptions/listen: serviti out-of-the-box da MCPServer, nulla da
    scrivere — solo da verificare in test.

Nei 13 pacchetti (meccanico):

  • pyproject.toml: mcp>=2,<3; sse-starlette>=3 dove importato; starlette/uvicorn
    restano.
  • httpxhttpx2 solo dove l'oggetto viene passato all'SDK. opendata_core è condiviso
    col backend e usa httpx: non toccarlo; i due pacchetti si installano affiancati.
  • Rinomina snake_case sui campi wire; McpErrorMCPError.
  • ⚠️ Verificare ca-certificates nelle immagini (python:3.x-slim): httpx2 valida il TLS
    contro il trust store di sistema via truststore, non contro certifi. Senza CA store usabile
    tutte le chiamate a ISTAT/CKAN/Overpass fallirebbero l'handshake. Fallback:
    SSL_CERT_FILE/SSL_CERT_DIR o verify=ssl_context esplicito. Rischio operativo n.1.
  • Audit di except httpx. e isinstance(..., httpx...): se httpx resta installato, un
    except httpx.ConnectError importa ancora ma non matcha più nulla (fallimento silenzioso).

Test e tooling:

  • Aggiornare i payload make mcp-stdio-*: tools/list nudo non è più valido in era moderna
    (serve _meta con protocolVersion + clientCapabilities).
  • Nuovo target make mcp-discover-* che chiama server/discover.
  • Test di conformità per pacchetto usando il Client in-memory di v2 (Client(server),
    nessun transport): asserisce server/discover, resultType, presenza di ttlMs/cacheScope,
    annotations, ordine di tools/list.
  • CI: doppia era verificata — un client mode="legacy" e uno moderno contro la stessa app.

Fase 4 — Client-side (backend) — 🔒 BLOCCATA

~1–2 gg · bloccata da agent-framework-core[all] → mcp<2.

  • Aprire/seguire una issue upstream su agent-framework-core per il cap mcp<2.
  • Quando si sblocca: MCPStreamableHTTPTool → nuovo Client dell'SDK v2.
  • Benefici attesi: niente sticky session (13 server scalabili dietro round-robin), cache
    client-side che onora ttlMs, e Mcp-Method/Mcp-Name come header ⇒ Traefik può fare
    rate-limit e routing per-tool senza leggere il body
    .
  • Alternativa se serve prima: client MCP diretto su mcp 2.x per i path che non passano da
    agent-framework (es. osm_map.py:109, che chiama osm-mcp).

Nel frattempo: nessuna azione richiesta — i server v2 servono il client legacy.

Fase 5 — Capability nuove (opzionale, alto valore prodotto)

~3–5 gg · dopo la Fase 3. Oggi il progetto usa solo tools.

  • resources: cataloghi e report come risorse indirizzabili (opendata://portals,
    opendata://territory/{istat}/report, opendata://maturity/{entity}/scorecard), con
    ttlMs/cacheScope e subscriptions/listen per notificare i cambi rilevati da
    opendata-monitor — incastro esatto con l'agente di monitoraggio (Agente di monitoraggio schedulato (cron): freshness / qualità / link + notifica #88, Maturità: avvisi nel tempo (trend, freshness, link rotti) #103).
  • prompts: i workflow che oggi vivono come istruzioni hardcoded in config.py
    (CKAN_INSTRUCTIONS, ISTAT_INSTRUCTIONS, …) sono di fatto prompt template. Esporli come
    prompts renderebbe opendata-mcp-server usabile da Claude Desktop con i percorsi
    Esplora/Territorio/Maturità/Qualità pronti, e ridurrebbe la duplicazione che R5 ci
    costringe a mantenere a mano su 5 template.
  • MRTR / elicitation per i tool che oggi indovinano: territory_resolve con comune ambiguo
    può chiedere disambiguazione invece di scegliere.
  • ⏸️ Estensione ufficiale Tasks (io.modelcontextprotocol/tasks) per harvest CKAN e batch
    lunghi: l'SDK Python v2 non la implementa ancora → da rinviare.

Ordine e stima

# Fase Effort Blocca / bloccata da
0 Pin + hardening rete 0.5 gg
1 Runtime MCP condiviso 2 gg abilita 2, 3
2 Qualità tool 2–3 gg dopo 1
3 SDK v2 + 2026-07-28 3–4 gg dopo 1 · rischio TLS/httpx2
4 Client (backend) 1–2 gg 🔒 agent-framework mcp<2
5 resources / prompts 3–5 gg dopo 3

Le Fasi 0, 1 e 2 hanno valore anche se la 3 slitta, e nessuna delle tre richiede coordinamento col
backend.

Rischi

  1. TLS via truststore (Fase 3): il rischio più concreto. Da validare su un solo server prima di
    propagare — web-mcp (2 tool) è il candidato per il PoC.
  2. httpx/httpx2 non interscambiabili a runtime: un httpx.AsyncClient passato come
    http_client degrada in modo silenzioso invece di sollevare.
  3. Doppia era in produzione: fino alla Fase 4 gireranno server 2026 con client 2025. Copertura
    in CI obbligatoria (Fase 3).
  4. Validazione più severa in v2: i risultati sono validati in uscita, quindi uno schema
    {} scritto a mano fallirebbe tools/list. I nostri tool sono tutti decorator-based → basso
    rischio, ma va verificato.

Vincoli di processo (regole del repo)

  • R1 — Docker build context = root del repo: i Dockerfile della Fase 1 devono copiare
    opendata-mcp-runtime/ accanto a opendata_core/.
  • R3 — test via /tmp/oda-venv, per pacchetto durante l'iterazione.
  • R11 — la Fase 2 (ordine deterministico) interagisce con la prompt cache del provider risolto.
  • R12make lint && make test prima di ogni commit; un PR per fase.
  • R14 — le Fasi 2 e 5 cambiano capability visibili → aggiornare nello stesso PR la sezione
    README e la copy della pagina UI interessata.
  • Invariante CLAUDE.md: no FastMCP in opendata_core/ → da cui il pacchetto runtime separato.

Riferimenti

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestroadmapPunto della roadmap di sviluppo

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions