Tre primitive, e quante ne vede davvero chi ti consuma

Un server MCP può esporre tool, resource e prompt: dopo la connessione il client chiede le capability con tools/list, prompts/list e resources/list. Prima di scrivere una riga, però, decidi chi consumerà il server, perché il supporto non è uniforme e questa è la scelta che ti costa di più se la fai dopo.

Se il consumatore è il connettore MCP della Messages API, il perimetro è stretto: «Of the feature set of the MCP specification, only tool calls are currently supported.» Il server deve essere esposto pubblicamente via HTTP, con Streamable HTTP o SSE, e «Local STDIO servers cannot be connected directly.» Resource e prompt, in quello scenario, semplicemente non arrivano.

Se ti servono davvero, gestisci tu la connessione MCP e usa gli helper degli SDK — mcpTools, mcpMessages, mcpResourceToContent, mcpResourceToFile — che convertono i tipi MCP in tipi dell’API Claude. La regola d’uso è dichiarata: «Use the client-side helpers when you need local servers, prompts, resources, or more control over the connection with the base SDK.»

Un’avvertenza di navigazione: Claude Code e il Claude Agent SDK sono documentati su code.claude.com, non su platform.claude.com. Cercare agent-sdk nel posto sbagliato è mezz’ora buttata.

Scegliere il canale

Quattro trasporti, con una gerarchia chiara. Stdio per i processi locali: «Stdio servers run as local processes on your machine. They’re ideal for tools that need direct system access or custom scripts.» Per il remoto, «HTTP servers are the recommended option for connecting to remote MCP servers». SSE esiste ancora ma non è la strada: «The SSE (Server-Sent Events) transport is deprecated. Use HTTP servers instead, where available.»

Il quarto è la risposta alla domanda sui socket. WebSocket tiene una connessione bidirezionale persistente e serve ai server che spingono eventi verso Claude senza essere interrogati; ma paga un prezzo esplicito, perché HTTP supporta OAuth e il flag claude mcp add --transport, e WebSocket nessuno dei due. Se il tuo server risponde soltanto a richieste, resta su HTTP.

C’è poi un quinto caso che spesso è il migliore per chi costruisce un’app con l’Agent SDK: il server in-process. Con createSdkMcpServer (o create_sdk_mcp_server) il server «runs in-process inside your application, not as a separate process». Niente processo da avviare, niente serializzazione fra processi, e a differenza di uno stdio o di un HTTP senza tool list in cache non ritarda mai il primo turno, dove gli altri aspettano fino al timeout di MCP_TIMEOUT, 30 secondi di default.

Scrivere il server: risultati, errori, dimensione

Un tool ha quattro parti: nome, descrizione, schema di input (Zod in TypeScript; in Python un dict di tipi, oppure JSON Schema completo quando servono enum, range o campi opzionali) e handler. L’handler restituisce un content con blocchi text, image, audio, resource o resource_link, più opzionalmente structuredContent e isError.

@tool("fetch_data", "Fetch data from an API", {"endpoint": str})
async def fetch_data(args):
    resp = await client.get(args["endpoint"])
    if resp.status_code != 200:
        return {
            "content": [{"type": "text",
                         "text": f"API error: {resp.status_code} {resp.reason_phrase}"}],
            "is_error": True,
        }
    return {"content": [{"type": "text", "text": resp.text}]}

Sugli errori il comportamento è indulgente ma la scelta è tua: «A handler error doesn’t stop the agent loop.» Un’eccezione non catturata diventa comunque un risultato di errore con il messaggio grezzo; catturarla e tornare isError serve a comporre il testo che Claude legge davvero, aggiungendo quale richiesta è fallita e che cosa provare.

Le annotation sono l’altra leva: readOnlyHint a true è l’unica che cambia il comportamento, perché abilita la chiamata in parallelo con altri tool di sola lettura. Le altre sono informative, e vale la pena ricordare che «Annotations are metadata, not enforcement»: se l’handler scrive, marcarlo read-only non lo rende innocuo.

Infine la dimensione dell’output, che è il difetto più comune nei server scritti in fretta. Claude Code avvisa oltre i 10.000 token e taglia a 25.000 di default: superata la soglia, l’output completo finisce in un file e il tool result viene sostituito da un messaggio di errore che ne nomina il percorso. Puoi alzare il tetto con MAX_MCP_OUTPUT_TOKENS, ma la risposta giusta quasi sempre è paginare, filtrare e troncare nel server.

Integrazione e deployment

I tool MCP arrivano al modello con il nome mcp__nomeserver__nometool, e sono le liste di tool permessi a decidere se girano senza chiedere: il wildcard vale dopo il prefisso, quindi mcp__github__* è valido mentre una voce non ancorata viene ignorata con un warning all’avvio.

Per le credenziali: env per i server stdio, header per HTTP e SSE, e headersHelper per gli schemi che non sono OAuth, come Kerberos o un SSO interno. In .mcp.json puoi espandere variabili d’ambiente in command, args, env, url e headers, così il file entra nel version control senza segreti:

{
  "mcpServers": {
    "api-server": {
      "type": "http",
      "url": "${API_BASE_URL:-https://api.example.com}/mcp",
      "headers": { "Authorization": "Bearer ${API_KEY}" }
    }
  }
}

Due cose da progettare, non da subire. Se il tuo server espone un tool pericoloso, dichiaralo: con _meta e la chiave anthropic/requiresUserInteraction impostata al booleano true, Claude Code chiede conferma a ogni chiamata, anche in bypassPermissions. E ricorda la riconnessione: i server remoti vengono ritentati con backoff esponenziale fino a cinque volte, mentre «Stdio servers are local processes, and Claude Code doesn’t reconnect them automatically». Un server stdio che muore resta morto per tutta la sessione.