Come abbiamo costruito un server MCP in Deno su Supabase Edge Functions

Architettura, doppio transport HTTP e stdio, Dockerfile per i check automatici delle directory, e le insidie vere che abbiamo incontrato: timeout, commit sbagliati e punteggi di qualità dei tool.

Redazione Zornade
5 min di lettura
Immagine di copertina per: Come abbiamo costruito un server MCP in Deno su Supabase Edge Functions

Questo articolo è il dietro le quinte del server MCP di Zornade, quello che porta il catasto italiano dentro Claude, Cursor e Copilot. Se vuoi solo usarlo, la guida pratica è nell'articolo di annuncio. Qui raccontiamo come è fatto e, soprattutto, le cose che abbiamo sbagliato e corretto in corsa.

Perché Deno e Supabase Edge Functions

Un server MCP remoto è un piccolo servizio HTTP che parla JSON-RPC. Le alternative erano tante, ma due vincoli hanno deciso per noi: il codice doveva vivere nello stesso progetto Supabase delle Edge Function esistenti, e volevamo deploy con un comando solo, senza container da gestire. Deno gira nativamente sulle Edge Function di Supabase, TypeScript senza build step, e il deploy è supabase functions deploy mcp. Per un server di consultazione, dove il traffico è interattivo e non continuo, i cold start delle Edge Function sono irrilevanti.

Sopra Deno abbiamo usato mcp-lite, una libreria leggera che implementa il protocollo con Hono per il routing HTTP e Zod per gli schemi dei tool. La scelta di Zod è la parte che ripaga di più: ogni tool dichiara lo schema dei parametri una sola volta, e da lì derivano sia la validazione in ingresso sia la documentazione che i client mostrano al modello.

Due transport per due mondi

Il server di produzione parla streamable HTTP sull'endpoint pubblico. Ma i check automatici delle directory MCP, come quelli di Glama, vogliono un'altra cosa: costruiscono il server in un container e lo interrogano su stdio, leggendo e scrivendo JSON-RPC su standard input e output. Sono due mondi diversi, e la soluzione che abbiamo adottato è un modulo condiviso: le definizioni dei sei tool vivono in un file unico, tools.ts, e ci sono due entrypoint, uno HTTP e uno stdio, che registrano esattamente gli stessi strumenti. Così i due transport non possono divergere.

Lo stdio l'abbiamo implementato a mano, una sessantina di righe che leggono righe JSON e rispondono a initialize, tools/list, tools/call e ping. Non serve una libreria per questo: il protocollo è semplice, e avere il controllo diretto aiuta quando qualcosa non torna.

Le insidie vere, con i numeri

Ecco le quattro che ci hanno fatto perdere più tempo.

La prima: i check di Glama avvolgono il comando di avvio in mcp-proxy, un proxy pensato per server stdio. Il nostro server era solo HTTP, quindi il proxy aspettava un handshake su stdio che non arrivava mai: timeout dopo 60 secondi esatti e "could not start the proxy". La correzione è stato l'entrypoint stdio, che ha risolto il problema alla radice.

La seconda: la Dockerfile dei check non è un file che incolli, ma un form che la genera. Base image fissa tra Debian slim, build steps come array JSON, comando di avvio come array JSON. Per Deno significa installarlo dentro i build steps, perché non esiste un'immagine predefinita. E il form rifiuta in partenza le soluzioni di comodo: proxyare l'endpoint esterno con mcp-remote viene bocciato con "the Dockerfile must build and run the server locally". Ha senso: i check devono verificare il tuo codice, non un ponte.

La terza: il build clona il repository al commit indicato nella pagina admin. Se il commit è rimasto quello vecchio, il build scarica il codice vecchio e fallisce con file non trovato, senza che nessun errore dica che il problema è il commit. Noi avevamo pushato il codice nuovo ma la pagina era ferma al commit precedente: serviva un sync manuale, o lo SHA pieno nel campo pinned commit.

La quarta, innocua ma rumorosa: un client SDK moderno, dopo l'initialize, prova ad aprire uno stream SSE persistente, e il nostro server in modalità stateless risponde 400. La richiesta e risposta funzionano comunque, ma i log si riempiono di errori che sembrano gravi e non lo sono. Lo segnalo perché se lo incontrate non è il vostro server ad essere rotto.

La qualità dei tool paga

Le directory come Glama assegnano un punteggio alla qualità delle definizioni dei tool, non solo al fatto che il server risponda. Descrizioni lunghe, parametri con esempi concreti e limiti espliciti fanno la differenza: i nostri sei tool sono stati valutati tutti A. Vale anche per chi usa MCP senza directory: un modello sceglie meglio gli argomenti se lo schema glieli spiega.

Osservabilità

Il server scrive una riga di log per ogni evento, initialize incluso con il nome del client, in una tabella Postgres dedicata, in fire-and-forget per non toccare la latenza. Da lì un cruscotto Grafana mostra chiamate per tool, errori, p95 e client unici. Per un servizio che parla con i modelli di mezza internet, sapere chi lo usa e se risponde è la differenza tra un prodotto e un esperimento.

Il codice è open source, licenza MIT, su github.com/zornade/zornade-mcp, e la chiave gratuita per provarlo si crea su app.zornade.com/api.

Foto di copertina: Marcel Gierschick su Pexels

Stai usando questi dati per un progetto aziendale?

Possiamo costruire la soluzione completa per te - backend, frontend, data engineering, deploy e supporto. Prima valutazione tecnica gratuita.

Richiedi un preventivo gratuito