Dietro le quinte dell'MCP di CaptainDNS

Di CaptainDNS
Pubblicato il 27 novembre 2025

  • #MCP
  • #Architettura
  • #DNS
  • #Email
  • #Integrazioni IA
TL;DR

TL;DR - 🔨 Prima di collegare CaptainDNS a ChatGPT via MCP serviva un'architettura chiara: un server MCP dedicato, posto tra gli host (ChatGPT, tool interni) e la API esistente di CaptainDNS.

  • Il server MCP non contiene logica DNS o email: delega tutto al backend CaptainDNS tramite una API interna sicura.
  • Il trasporto MCP si basa su HTTP + JSON-RPC, con un solo punto di ingresso /stream allineato al modello HTTP+SSE moderno.
  • Gli strumenti esposti (dns_lookup, dns_propagation, email_auth_audit) sono tipizzati così l'IA può scoprirli e chiamarli in autonomia.
  • Le prime prove hanno fatto emergere timeout, errori 424 e sottigliezze di protocollo (notifications, tools/list, tools/call) che hanno imposto di irrobustire il contratto.
  • Questo articolo spiega come abbiamo strutturato l'architettura, messo in sicurezza gli scambi e fatto debugging end-to-end.

Perché un server MCP dedicato per CaptainDNS?

Dal punto di vista prodotto, CaptainDNS resta un SaaS classico: interfaccia web Next.js (frontend) e API Go (services/api) che gestisce la logica di business (DNS, email, resolver, logging, scoring).

Aggiungere MCP non significa esporre direttamente la API al modello: abbiamo scelto di inserire un server MCP dedicato (services/mcp-server) che funge da adattatore.

In pratica:

  • services/api resta l'unica fonte di verità (DNS, propagazione, email).
  • services/mcp-server è un client forte di questa API:
    • si presenta con un token di servizio per provare che proviene dall'infrastruttura CaptainDNS;
    • propaga un eventuale token Auth0 utente per rispettare quote, logging e permessi.
  • Gli host (ChatGPT, tool interni, agent) vedono solo il server MCP e parlano MCP/JSON-RPC, mai la API grezza.

Questa separazione consente di:

  • disaccoppiare l'evoluzione della API da quella del contratto MCP;
  • aggiungere guardrail specifici per il mondo IA (rate limiting, controlli di formato, timeout aggressivi); e
  • mantenere un'architettura pulita.

Panoramica dell'architettura

A livello alto, l'architettura appare così:

  • Host MCP: ChatGPT (Connectors), tool interni, altri client MCP.
  • Server MCP CaptainDNS:
    • espone gli strumenti MCP (dns_lookup, dns_propagation, email_auth_audit, ecc.) via JSON-RPC;
    • valida e normalizza gli input (domini, tipi di record, selector DKIM);
    • applica timeout, quote e classificazione errori.
  • API CaptainDNS (services/api):
    • endpoint /resolve, /resolve/propagation, mail/domain-check;
    • database e resolver;
    • log, scoring, profilo utente.

Il server MCP è un ponte di protocollo: traduce le chiamate MCP (tools/list, tools/call) in chiamate HTTP interne, poi riconfeziona la risposta in un formato utilizzabile dal client MCP e dal modello.

Contratto MCP: initialize, tools/list, tools/call

Il protocollo poggia su JSON-RPC 2.0 e tre metodi principali lato server:

  • initialize: negoziazione della versione di protocollo e delle capacità.
  • tools/list: scoperta degli strumenti disponibili.
  • tools/call: esecuzione di uno strumento nominato con argomenti tipizzati.

initialize: dire chi sei e cosa supporti

Quando un host (ad esempio ChatGPT) apre una sessione con il server MCP CaptainDNS, parte inviando:

  • method: "initialize";
  • params.protocolVersion: una versione di protocollo (es. "2025-06-18");
  • params.clientInfo: nome e versione del client (openai-mcp, 1.0.0, ecc.).

Il server MCP risponde con:

  • protocolVersion: la versione che accetta (spesso uguale a quella del client);
  • capabilities: in particolare tools: { listChanged: false } per indicare una lista di strumenti stabile;
  • serverInfo: nome ("captaindns-mcp-server") e versione ("0.1.0") del server.

In questa fase nessuna chiamata di business è ancora partita verso la API CaptainDNS: ci si allinea solo su versione di protocollo e capacità generali.

tools/list: annunciare gli strumenti CaptainDNS

Dopo initialize, il client invia tools/list. L'MCP CaptainDNS risponde con un array di definizioni di strumenti, ciascuna con:

  • name: identificativo dello strumento, ad esempio dns_lookup, dns_propagation, email_auth_audit;
  • description: cosa fa lo strumento, in linguaggio naturale;
  • inputSchema: uno schema JSON che descrive con precisione gli argomenti attesi;
  • annotations: metadati (tag, scope raccomandati come captaindns:dns:read).

Gli strumenti CaptainDNS sono volutamente read-only: interrogano DNS o configurazioni email, ma non modificano nulla.

Esempi di parametri tipizzati:

  • dns_lookup:
    • domain (obbligatorio);
    • record_type (enum A, AAAA, TXT, MX, ecc.);
    • resolver_preset (opzionale);
    • trace (booleano) per richiedere una traccia iterativa.
  • dns_propagation:
    • stessa logica, applicata a più resolver.
  • email_auth_audit:
    • domain (obbligatorio);
    • rp_domain (opzionale, per report/policy);
    • dkim_selectors (lista opzionale di selector da sondare).

tools/call: eseguire uno strumento di business

Quando il modello decide di usare uno strumento, non chiama direttamente dns_lookup: invia un tools/call con:

  • params.name: nome dello strumento ("dns_lookup", "dns_propagation", "email_auth_audit");
  • params.arguments: un oggetto JSON conforme a inputSchema.

Il server MCP:

  1. valida e normalizza gli argomenti (domini, tipi DNS, ecc.);
  2. chiama l'endpoint backend corretto (/resolve, /resolve/propagation, /mail/domain-check) tramite un client interno;
  3. riconfeziona la risposta in un CallToolResult con:
    • structuredContent: la risposta strutturata CaptainDNS (risposte DNS, punteggio email, dettagli SPF/DKIM/DMARC/BIMI, ecc.);
    • eventualmente content con un riassunto testuale per l'IA;
    • isError: false se tutto è andato bene, true se lo strumento è stato eseguito ma ha restituito un errore di business (es. timeout DNS).

Gli errori strutturali (tool sconosciuto, parametri invalidi, JSON malformato) restano errori JSON-RPC classici (-32601, -32602, ecc.), così il client MCP distingue chiaramente problemi di protocollo da problemi di business.

Trasporto MCP: HTTP + JSON-RPC e SSE

Per la prima versione abbiamo scelto il trasporto moderno basato su HTTP + JSON-RPC, con supporto SSE opzionale.

Diagramma del trasporto MCP di CaptainDNS

Punto di ingresso: POST /stream

Il punto di ingresso principale del server MCP è un endpoint HTTP:

  • POST /stream con un body JSON-RPC 2.0.

I client moderni lo usano per:

  • aprire la sessione;
  • inviare initialize;
  • concatenare tools/list e tools/call.

Il server MCP:

  • legge la richiesta JSON-RPC (jsonrpc, id, method, params);
  • instrada verso l'handler appropriato (initialize, tools/list, tools/call);
  • restituisce una risposta JSON-RPC strutturata:
    • result (successo);
    • oppure error (errore di protocollo).

SSE: compatibilità e introspezione

Per client più datati o casi specifici, il server espone anche:

  • GET /stream con Accept: text/event-stream;

Questo flusso SSE:

  • annuncia le metadati di base (endpoint HTTP per le richieste);
  • può pubblicare una lista semplificata degli strumenti;
  • invia ping regolari per mantenere le connessioni sotto controllo.

In pratica, l'integrazione con ChatGPT si basa soprattutto su POST /stream JSON-RPC. I primi malfunzionamenti (timeout, 424, ecc.) derivavano da un handshake SSE incompleto; convergere su un unico flusso JSON-RPC ben definito li ha risolti.

Interazioni server MCP ↔ API CaptainDNS

Per ogni tools/call, il server MCP agisce come client autenticato della API CaptainDNS.

Autenticazione e identità

Il MCP invia sempre:

  • un token di servizio affinché la API riconosca un'origine interna fidata;
  • un token utente, quando il host MCP lo fornisce, così la API può:
    • collegare le richieste a un profilo (per log e cronologia);
    • applicare regole di quota o permessi.

Il server MCP non conserva dati utente a lungo termine: propaga semplicemente l'identità verso la API, che resta l'autorità su profilo e registri.

Client interno e normalizzazione

Tutte le richieste in uscita passano da un unico client interno che:

  • normalizza i domini (example.com, senza punto finale, in minuscolo);
  • valida i tipi di record (A, AAAA, TXT, ecc.);
  • restringe il perimetro degli strumenti (selector DKIM, preset dei resolver);
  • applica timeout calibrati per ogni strumento (dns_lookup più corto di email_auth_audit o dns_propagation).

Il server MCP non effettua mai chiamate HTTP grezze fuori da questo client: facilita manutenzione e sicurezza.

Classificazione degli errori

Gli errori rientrano in tre famiglie:

  • input: parametri invalidi o mancanti (dominio malformato, tipo di record non supportato, token mancante, ecc.);
  • business: problema di business (timeout DNS, resolver irraggiungibile, upstream email lento, ecc.);
  • internal: malfunzionamento interno (bug, configurazione mancante, API in errore 5xx).

Su /stream (JSON-RPC), gli errori sono riflessi come codici standard (-32602, -32001, -32603) con dettagli in error.data. Nei risultati degli strumenti, un errore di business può apparire come isError: true con un structuredContent dedicato.

Field notes: timeout, 424 e altre sorprese

La teoria MCP è elegante; la pratica ha portato bug e sorprese. Ecco i principali problemi incontrati e come li abbiamo risolti.

1. Il timeout silenzioso quando si aggiunge il server

Primo passo: dichiarare il server MCP in ChatGPT. All'inizio l'URL configurata puntava a:

  • un server in ascolto solo su 127.0.0.1, oppure
  • un endpoint HTTP che non implementava davvero MCP.

Risultato:

  • nessun log initialize lato MCP;
  • ChatGPT finiva per mostrare un semplice timeout: "unable to connect to the MCP server".

Cause:

  • server in ascolto solo su 127.0.0.1 invece che su 0.0.0.0 dietro reverse proxy;
  • uso di localhost/IP private nella configurazione (irraggiungibili dal cloud ChatGPT).

Lezione: prima del protocollo, verificare le basi: DNS pubblico, HTTPS, ascolto su un indirizzo raggiungibile e tracce visibili lato MCP non appena si tenta di connettersi.

2. SSE senza handshake completo: la connessione che resta aperta... e poi muore

Quando l'URL MCP era raggiungibile, i primi log mostravano:

  • un POST /stream restituito in 405 (method not allowed);
  • un fallback a GET /stream con Accept: text/event-stream;
  • una connessione SSE accettata e chiusa dopo ~2 minuti.

Sulla carta "funzionava" (esisteva una connessione SSE), ma ChatGPT non riusciva mai a inizializzare la sessione MCP. Motivo:

  • il flusso SSE inviava ping ma non l'handshake atteso (niente evento endpoint, nessuna informazione sull'URL JSON-RPC).

Soluzione: semplificare il trasporto con un punto di ingresso chiaro:

  • POST /stream per tutto ciò che è JSON-RPC (initialize, tools/list, tools/call);
  • GET /stream SSE come canale di introspezione secondario, ma non indispensabile per ChatGPT.

Convergere su un unico punto di ingresso JSON-RPC robusto ha eliminato la maggior parte dei timeout misteriosi.

3. initialize incompleto: quando il client si aspetta più metadati

Versione successiva: la connessione si stabiliva, ma il client MCP si bloccava su initialize. I log mostravano:

  • un initialize in ingresso corretto (con protocolVersion e clientInfo);
  • una risposta che conteneva già la lista degli strumenti e talvolta un capabilities.tools tipizzato male.

Problemi individuati:

  • mancanza di protocolVersion in result;
  • mancanza di serverInfo;
  • capabilities.tools restituito come booleano (true) invece che come oggetto ({listChanged:false});
  • inclusione di campi non standard (lista tools, endpoint custom) nella risposta initialize.

Soluzione:

  • normalizzare la risposta initialize:
    • eco di protocolVersion;
    • capabilities.tools = { listChanged: false };
    • serverInfo minimo (name, version);
  • spostare la lista degli strumenti in tools/list.

Una volta rispettato il contratto, il client ha potuto concatenare notifications/initialized e poi tools/list senza errori.

4. Rispondere a una notification: un buon modo per mandare in crash il client

Altra sorpresa: notifications/initialized è una notifica JSON-RPC:

  • niente id;
  • il client non si aspetta risposta.

Una prima versione del server rispondeva comunque con:

  • una pseudo risposta JSON-RPC contenente id: null.

Per un client rigoroso sembra una risposta a una richiesta inesistente, da cui errori tipo "unhandled errors in a TaskGroup".

Soluzione: applicare la regola semplice:

  • se la richiesta non ha id (notification):
    • si logga;
    • eventualmente si aggiorna lo stato interno;
    • ma non si invia nulla sul flusso.

Questo dettaglio ha eliminato un'intera classe di errori lato client.

5. tools/list e inputSchema vs input_schema

Nelle prime prove, ChatGPT arrivava fino a tools/list ma falliva nel costruire gli strumenti interni. Motivo:

  • la risposta tools/list usava la proprietà input_schema in snake_case invece di inputSchema in camelCase.

Anche con lo schema JSON corretto, un client tipizzato si aspetta rigorosamente inputSchema. Con input_schema, il campo risultava assente: impossibile generare form di input o validare gli argomenti.

Soluzione: rinominare sistematicamente la chiave in inputSchema nelle definizioni degli strumenti.

6. tools/call: lo strumento sconosciuto che non lo è

Passo successivo: stabilizzato tools/list, le chiamate tentavano tools/call... e ricevevano sistematicamente:

  • un JSON-RPC method not found;
  • un codice interno ERR_UNKNOWN_TOOL.

Il server cercava ancora un metodo corrispondente a dns_lookup o dns_propagation, mentre il protocollo MCP impone un unico metodo tools/call con parametro name.

Soluzione:

  • aggiungere un handler dedicato tools/call;
  • instradarlo verso gli strumenti corretti in base a params.name;
  • validare i parametri con il inputSchema corrispondente;
  • restituire un CallToolResult strutturato.

Da lì, gli strumenti CaptainDNS hanno iniziato finalmente a essere eseguiti via MCP.

7. content type "json" vs structuredContent: i dettagli che rompono le integrazioni

In una versione intermedia, il server MCP restituiva i risultati così:

  • result.content[0].type = "json";
  • result.content[0].json = { ... }.

Comodo lato server, ma non corrisponde a un tipo di blocco standard del protocollo (che definisce soprattutto text, image, resource, ecc.). Alcuni client tolleranti lo interpretano, altri no.

Su ChatGPT poteva tradursi in errori interni incapsulati come:

  • http_error 424;
  • unhandled errors in a TaskGroup (1 sub-exception).

Soluzione:

  • spostare il payload strutturato in structuredContent;
  • riservare content a un riassunto testuale opzionale (facile da mostrare all'utente);
  • usare isError per segnalare chiaramente se il risultato di business è un successo o un errore.

Con questo schema sono spariti i 424 legati al mapping del contenuto.

FAQ: timeout, 424 e buone pratiche MCP

Domande frequenti sul MCP di CaptainDNS

Quali prerequisiti controllare se aggiungere il server MCP fallisce?

Parti dalle basi:

  • L'URL punta a un endpoint HTTPS pubblico, non a localhost o a un IP privato.
  • POST /stream risponde (niente 405) e vedi un log initialize lato server.
  • Il server è in ascolto su 0.0.0.0 dietro il reverse proxy, non solo su 127.0.0.1.
  • SSE (GET /stream) è opzionale: non bloccarti se il JSON-RPC funziona.

Se initialize non compare mai nei log, è un problema di rete (DNS, TLS, firewall), non di protocollo.

Come gestire un `http_error 424` o `unhandled errors in a TaskGroup`?

Di solito indica una risposta fuori contratto MCP:

  • metti la payload strutturata in structuredContent e usa content solo per un riepilogo di testo;
  • non rispondere alle notification (notifications/initialized non ha id);
  • restituisci un solo CallToolResult con isError esplicito invece di blocchi personalizzati type: \"json\".

Se i log mostrano un tools/call riuscito ma il client fallisce, verifica prima questi punti.

Perché instradare tutti gli strumenti via `tools/call`?

Il MCP impone un solo metodo di business (tools/call) con params.name:

  • lato server, implementa tools/call e instrada verso dns_lookup, dns_propagation, email_auth_audit, validando con l'inputSchema corrispondente;
  • lato client, assicurati che params.name corrisponda esattamente a un name restituito da tools/list.

Metodi separati o nomi errati portano a method not found o ERR_UNKNOWN_TOOL.

Come diagnosticare un timeout o una latenza insolita?

I timeout arrivano spesso dal percorso di rete più che dal protocollo:

  • controlla i timeout per strumento lato MCP (dns_lookup più breve di dns_propagation);
  • riduci un sweep di resolver per i test, poi allargalo;
  • guarda i log del backend (DNS lenti, upstream mail) e la presenza del service token.

Un timeout senza initialize loggato resta un problema di raggiungibilità di rete.

Basta HTTP JSON-RPC o serve anche SSE?

Di default basta un solo punto di ingresso JSON-RPC (POST /stream):

  • è il percorso più stabile per ChatGPT e i client moderni;
  • la discovery e le chiamate degli strumenti passano di lì.

Aggiungi SSE (GET /stream) solo se ti serve introspezione; in quel caso il flusso deve fornire l'endpoint e ping regolari.

Glossario MCP e CaptainDNS

MCP (Model Context Protocol)

Protocollo aperto che standardizza il modo in cui un modello di IA dialoga con strumenti esterni: database, API, servizi come CaptainDNS. Definisce concetti come initialize, tools/list, tools/call e formati di risposta tipizzati (CallToolResult, ContentBlock, structuredContent).

Host

Applicazione che incorpora un modello di IA e un client MCP: ChatGPT, un IDE, un agent interno. È l'host che decide di chiamare uno strumento CaptainDNS via MCP in risposta alle istruzioni dell'utente.

Server MCP

Servizio che espone strumenti MCP utilizzabili dagli host. Nel nostro caso: captaindns-mcp-server, un servizio Go che conosce la superficie API di CaptainDNS e la traduce nel formato MCP.

JSON-RPC 2.0

Protocollo leggero basato su JSON utilizzato da MCP per descrivere richieste (method, params, id) e risposte (result o error). MCP ne fa un uso circoscritto (initialize, tools/list, tools/call, notifications).

tools/list

Metodo MCP che restituisce la lista degli strumenti disponibili su un server MCP, con il loro inputSchema e i metadati. È il punto di partenza perché un modello sappia cosa può fare con CaptainDNS.

tools/call

Metodo MCP che un host utilizza per eseguire uno strumento specifico. Il server MCP legge params.name, valida params.arguments, chiama la API backend e restituisce un CallToolResult che rappresenta il risultato strutturato (o l'errore di business).

structuredContent

Campo opzionale del CallToolResult dove un server MCP può collocare dati strutturati (JSON) prodotti da uno strumento. CaptainDNS vi inserisce, ad esempio, risposte DNS, punteggi email, dettagli SPF/DKIM/DMARC/BIMI.

TaskGroup

Concetto dei runtime asincroni (Python, ecc.): un gruppo di task eseguiti in parallelo. Un messaggio come "unhandled errors in a TaskGroup" indica che si è verificata un'eccezione non gestita in uno o più di questi task, spesso a causa di una sottile incompatibilità di formato o di un bug nella catena.

Articoli simili

CaptainDNS · 21 novembre 2025

Un MCP per CaptainDNS?

Prima di collegare CaptainDNS alle IA, bisogna capire cos'è il Model Context Protocol (MCP) e cosa permette davvero. Un piccolo ABC del MCP e le prime piste per CaptainDNS.

  • #MCP
  • #IA
  • #DNS
  • #E-mail
  • #Architettura