CodeGym /Corsi /ChatGPT Apps /Primo server MCP: dall’SDK a tools/resources/prompts funz...

Primo server MCP: dall’SDK a tools/resources/prompts funzionanti

ChatGPT Apps
Livello 6 , Lezione 3
Disponibile

1. Cosa costruiremo oggi e come si inserisce nell’applicazione

Ricordiamo la nostra app didattica: stiamo creando un assistente per la scelta dei regali. Nei moduli precedenti avevamo già:

  • un widget in ChatGPT (Next.js 16 + Apps SDK) che mostra l’UI, lo stato e sa invocare callTool;
  • un backend semplice (tramite Apps SDK / route di Next.js) che restituiva stub di regali.

Ora vogliamo spostare la logica del nostro assistente in un server MCP separato. Alla fine lo schema sarà così:

flowchart TD
  subgraph ChatGPT
    U[Utente
in chat] W["Widget dell’App
(Apps SDK)"] end subgraph MCP Client C[ChatGPT MCP client] end subgraph OurServer[Il nostro server MCP] T1[Tool: suggest_gifts] R1[Resource: gift_catalog] P1[Prompt: birthday_template] end U --> W W -- callTool --> C C <-- JSON-RPC / HTTP --> OurServer OurServer --> C C --> W

Quindi ora:

  • il modello dentro ChatGPT vede il nostro MCP server come un set standard di tools/resources/prompts;
  • callTool dal widget diventa logicamente una chiamata MCP interna;
  • il nostro server descrive i contratti (schemi, descrizioni) e implementa la logica di business.

Alla fine di questa lezione dovreste avere un progetto Node/TypeScript separato con un MCP server che:

  • si avvia in locale con un solo comando;
  • registra almeno uno strumento e una risorsa;
  • restituisce dati sensati (anche se con mock semplici);
  • è strutturato in modo da poter essere esteso.

Nel frattempo non riscriviamo l’attuale backend basato su Apps SDK/Next.js: resta com’è, e il MCP server lo avviamo come servizio separato accanto. Più avanti potrete collegarlo alla ChatGPT App e spostare gradualmente lì la logica dei regali al posto degli stub precedenti.

2. Stack: TypeScript + MCP SDK + trasporto HTTP

Scriveremo il server MCP in TypeScript su Node.js. L’SDK JS/TS ufficiale per MCP è nel pacchetto @modelcontextprotocol/sdk. Si occupa della routine di JSON‑RPC, validazione e conversione degli schemi: voi descrivete gli argomenti tramite schemi Zod, e l’SDK li converte in JSON Schema comprensibile al modello.

Per il trasporto ci serve la variante HTTP: ChatGPT parla con server MCP remoti via rete, non via stdio/locale. La specifica MCP descrive un formato standard di “HTTP in streaming” — di fatto l’evoluzione del vecchio schema HTTP+SSE. In pratica è un unico endpoint HTTP che gestisce la richiesta (POST/GET) e, se necessario, invia la risposta in streaming. Nell’SDK TypeScript per MCP di solito c’è già un trasporto pronto per questo formato, collegabile a Express o Hono.

Per non disperderci, daremo per scontato di avere:

  • un oggetto server McpServer da @modelcontextprotocol/sdk;
  • un trasporto HTTP (ad esempio StreamableHttpServerTransport o analogo), integrabile con Express.

I nomi esatti delle classi possono cambiare leggermente tra versioni dell’SDK, ma l’architettura è sempre:

  1. create un oggetto server MCP;
  2. registrate su di esso tools/resources/prompts;
  3. collegate il trasporto all’applicazione HTTP.

3. Struttura del progetto e preparazione

Creiamo una cartella separata per il MCP server. È comodo tenerla accanto all’app frontend, ma come progetto Node separato:

chatgpt-gift-app/
  app/              ← Next.js + Apps SDK (widget)
  mcp-server/       ← il nostro server MCP

Dentro mcp-server:

mcp-server/
  src/
    server.ts       ← entrypoint del server MCP
    gifts.ts        ← logica di business per la selezione dei regali
  package.json
  tsconfig.json

Un semplice esempio di gifts.ts lo faremo tra poco; per ora concentriamoci su server.ts.

Supponiamo che abbiate già inizializzato il progetto:

mkdir mcp-server
cd mcp-server
npm init -y
npm install typescript ts-node-dev zod express @modelcontextprotocol/sdk

tsconfig.json — assolutamente standard (esnext modules, target node, strict). Potete prenderlo da un qualsiasi vostro progetto TS.

4. Estrarre la logica di business in un modulo separato

È forte la tentazione di scrivere subito server.registerTool(..., async () => {...}) e infilarci dentro tutta la logica. Ma è meglio separare fin dall’inizio:

  • un modulo che non sa nulla di MCP, JSON‑RPC e simili;
  • un modulo che conosce solo MCP, ma poco della logica di business.

In src/gifts.ts descriviamo una semplice funzione per suggerire regali:

// src/gifts.ts

export type GiftIdea = {
  id: string;
  title: string;
  price: number;
  occasion: string;
};

export type SuggestGiftsInput = {
  age: number;
  relationship: "friend" | "partner" | "child" | "coworker";
  budget: number;
};

export function suggestGifts(input: SuggestGiftsInput): GiftIdea[] {
  // per ora solo mock
  return [
    {
      id: "book-1",
      title: "Un libro sul suo hobby preferito",
      price: Math.min(input.budget, 30),
      occasion: "generic",
    },
    {
      id: "game-1",
      title: "Un gioco da tavolo per il gruppo",
      price: Math.min(input.budget, 50),
      occasion: "party",
    },
  ];
}

Questa funzione è pura: in ingresso parametri, in uscita un array di idee. Potete testarla con unit test, riutilizzarla altrove, e non dipende in alcun modo da MCP. È proprio così che si consiglia di fare: l’involucro server da una parte, le funzioni di business dall’altra.

5. Creare il MCP server e collegare il trasporto HTTP

Ora il punto d’ingresso src/server.ts. A grandi linee dobbiamo:

  1. creare un’istanza del server MCP;
  2. registrare strumenti, risorse e prompt;
  3. alzare un server HTTP (ad esempio Express) e collegargli il trasporto MCP.

Partiamo dalla bozza:

// src/server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server";
import { StreamableHttpServerTransport } from "@modelcontextprotocol/sdk/transport/streamable-http";

const app = express();

// 1. Creiamo il server MCP
const mcpServer = new McpServer({
  name: "gift-assistant-mcp",
  version: "0.1.0",
});

// 2. Qui registreremo in seguito tools/resources/prompts

// 3. Configuriamo il trasporto su HTTP
const transport = new StreamableHttpServerTransport({
  path: "/mcp", // endpoint unico MCP
  app,          // integriamo nell’app Express
});

transport.attach(mcpServer);

const PORT = process.env.PORT ?? 4000;
app.listen(PORT, () => {
  console.log(`MCP server listening on http://localhost:${PORT}/mcp`);
});

I nomi concreti delle classi di trasporto possono variare, ma il pattern è uno: create l’endpoint HTTP e collegatevi il server MCP come handler di JSON‑RPC sopra HTTP/stream.

A questo punto il server non fa ancora nulla di utile, ma sa già:

  • completare l’handshake MCP;
  • rispondere alle richieste di discovery di base (l’elenco di tools/resources/prompts — per ora vuoto).

Passo successivo — registrare il primo strumento.

6. Registrare lo strumento suggest_gifts tramite MCP SDK

L’Apps SDK ufficiale e la documentazione MCP mostrano lo stesso pattern per registrare uno strumento: il metodo registerTool, a cui passate nome, descrittore (titolo, descrizione, schema degli argomenti) e handler.

Abbiamo già descritto il tipo SuggestGiftsInput in gifts.ts. Ora aggiungiamo uno schema Zod, così il server può validare gli argomenti in ingresso e fornire automaticamente alla LLM un JSON Schema corretto.

// src/server.ts (estratto)
import { z } from "zod";
import { suggestGifts } from "./gifts";

const suggestGiftsInputSchema = z.object({
  age: z.number().int().min(0).max(120),
  relationship: z.enum(["friend", "partner", "child", "coworker"]),
  budget: z.number().min(0),
});

Ora registriamo lo strumento:

// sempre in server.ts

mcpServer.registerTool(
  "suggest_gifts",
  {
    title: "Suggest gift ideas",
    description:
      "Suggerisce idee regalo in base all’età, al tipo di relazione e al budget.",
    // L’SDK convertirà lo schema Zod in JSON Schema per il modello
    inputSchema: suggestGiftsInputSchema,
  },
  async ({ input }) => {
    const ideas = suggestGifts(input);

    const text = ideas
      .map(
        (g) =>
          `• ${g.title} — ~${g.price} USD (occasion: ${g.occasion}, id: ${g.id})`
      )
      .join("\n");

    return {
      content: [
        {
          type: "text",
          text,
        },
      ],
      // structuredContent può essere usato nel widget
      structuredContent: {
        ideas,
      },
    };
  }
);

Punti chiave:

  • inputSchema — schema Zod. L’SDK TS sa trasformarlo in JSON Schema e così descrive automaticamente lo strumento per il modello.
  • L’handler riceve un oggetto con input (il cui tipo deriva dallo schema). All’interno potete chiamare la vostra funzione di business.
  • Nel result restituite il content — il testo che il modello vedrà come risultato — e, se volete, lo structuredContent con una struttura JSON, che poi il vostro widget può consumare.

Se nei moduli precedenti avete già creato uno strumento tramite Apps SDK, questo codice dovrebbe risultare familiare: lo stesso pattern, solo che ora vive in un MCP server separato.

7. Aggiungere la risorsa gift_catalog per i dati

Gli strumenti sono azioni. A volte è utile anche fornire dati come risorsa, in modo che il modello possa leggerli, cercarvi oppure il vostro widget possa caricare template, componenti e così via. MCP descrive separatamente il concetto di risorse con URI, MIME type e contenuto.

Creiamo una risorsa semplice gift_catalog, che restituisce l’elenco dei regali disponibili. Per ora saranno ancora mock, ma nella realtà potrebbe essere un export dal database o un product feed.

Prima il catalogo stesso:

// src/gifts.ts (aggiunta)
export const giftCatalog: GiftIdea[] = [
  {
    id: "book-1",
    title: "Libro di programmazione",
    price: 25,
    occasion: "learning",
  },
  {
    id: "lego-1",
    title: "Set LEGO",
    price: 60,
    occasion: "fun",
  },
];

Ora registriamo la risorsa sul server:

// src/server.ts (estratto)
import { giftCatalog } from "./gifts";

mcpServer.registerResource(
  "gift_catalog",
  {
    title: "Gift catalog",
    description: "Un semplice catalogo di regali per demo e debug.",
    mimeType: "application/json",
  },
  async () => {
    return {
      contents: [
        {
          uri: "mcp://gift-catalog",
          mimeType: "application/json",
          text: JSON.stringify(giftCatalog, null, 2),
        },
      ],
    };
  }
);

Cosa succede qui a livello logico:

  • il nome della risorsa gift_catalog sarà visibile al client in discovery (poi lo vedrete nella lista delle risorse nell’MCP inspector);
  • il descrittore contiene una descrizione leggibile e il MIME type;
  • l’handler restituisce un array di contents con URI e testo — è il formato standard della risorsa in MCP.

Successivamente potrete:

  • leggere questa risorsa dal client (ad esempio un agente o un inspector);
  • usarla come template/dati per l’UI;
  • fare esperimenti: come il modello usa il catalogo pronto per spiegare all’utente le opzioni.

8. Registrare un prompt semplice

La terza entità di MCP — i prompt, suggerimenti predefiniti. Permettono di non ripetere prompt di sistema o utente lunghi, ma di conservarli sul server con dei nomi.

Facciamo un mini‑esempio: il prompt birthday_gift, richiamabile come “template precompilato di conversazione su un regalo di compleanno”.

// src/server.ts (estratto)

mcpServer.registerPrompt("birthday_gift", {
  title: "Birthday gift helper",
  description: "Template di richiesta per scegliere un regalo di compleanno.",
  messages: [
    {
      role: "system",
      content:
        "Sei un assistente per la ricerca di regali. Fai domande di chiarimento e proponi diverse opzioni.",
    },
    {
      role: "user",
      content:
        "Mi serve un regalo di compleanno. Fai le domande necessarie e aiutami a scegliere.",
    },
  ],
});

Sotto il cofano MCP permette ai client di:

  • ottenere l’elenco dei prompt (nell’inspector vedrete birthday_gift);
  • richiederne il contenuto e usarlo come suggerimento di base per il modello.

A parte, nel modulo su system prompt e istruzioni, analizziamo in dettaglio come questi prompt si combinano con le istruzioni globali dell’applicazione. Qui ci interessa solo “vederli” come parte del server MCP.

9. Come funziona tutto a runtime

Mettiamo insieme il quadro completo.

Quando un client (ad esempio l’MCP Inspector o ChatGPT) si collega al nostro endpoint HTTP /mcp:

  1. avviene l’handshake: client e server si scambiano le capacità supportate (tools/resources/prompts ecc.);
  2. il client invoca i metodi di discovery: ottiene l’elenco di strumenti, risorse, prompt con descrizioni e schemi;
  3. quando il modello decide di invocare uno strumento, forma una richiesta JSON‑RPC con un metodo tipo tools/call o simile — l’SDK lato server lo trasforma in una chiamata interna all’handler registrato con registerTool;
  4. l’handler esegue la logica di business (nel nostro caso suggestGifts o la consegna di giftCatalog) e restituisce il risultato in un formato standardizzato;
  5. l’SDK serializza la risposta di nuovo in JSON‑RPC e la invia al client attraverso lo stesso trasporto HTTP/stream.

Tutti i dettagli di JSON‑RPC, della formazione dell’id, del routing dei metodi ecc. restano dentro @modelcontextprotocol/sdk. Per voi l’interfaccia è molto simile all’Apps SDK: lavorate con registerTool/registerResource/registerPrompt e relativi handler, senza preoccuparvi del protocollo.

10. Avvio locale e primo test semplice

Supponiamo che abbiate aggiunto tutto quanto sopra. Rimane da avviare.

In package.json potete aggiungere lo script:

{
  "scripts": {
    "dev": "ts-node-dev src/server.ts"
  }
}

Avvio:

npm run dev

In console dovrebbe apparire qualcosa del tipo:

MCP server listening on http://localhost:4000/mcp

L’ispezione completa e le chiamate manuali degli strumenti le faremo nella prossima lezione tramite MCP Inspector / MCP Jam. Ma già ora potete fare uno smoke test super semplice con curl:

curl -X POST http://localhost:4000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Questo curl è uno smoke test puramente facoltativo per chi ama guardare le risposte JSON “grezze”. Nello sviluppo reale quasi sempre comunicate con il server MCP tramite SDK, non componendo a mano richieste JSON‑RPC.

Il nome esatto del metodo dipende dalla versione del protocollo e dell’SDK, ma l’idea è che otterrete una lista JSON, in cui tra i tools sarà visibile suggest_gifts. Se il metodo non coincide, non è un problema: lo scopo di questa lezione non è ricordare a memoria tutti i nomi, ma far sì che non abbiate paura di guardare le risposte JSON e di capirne la struttura, grazie alle lezioni precedenti.

11. Integrazione con la nostra ChatGPT App e sviluppi futuri

Per ora il server MCP vive per conto suo. Nei prossimi moduli:

  • lo collegherete all’MCP Inspector e imparerete a fare debug di tools/resources/prompts separatamente, senza toccare ChatGPT;
  • configurerete la ChatGPT App in modo che veda questo MCP server come fonte di strumenti;
  • sposterete parte della logica, prima realizzata dentro l’Apps SDK (ad esempio tramite tools integrati), nello strato MCP;
  • aggiungerete autenticazione, logging, scenari in streaming — sopra lo scheletro già pronto.

Per ora è importante che:

  • abbiate un servizio separato responsabile delle “abilità” e dei “dati” dell’applicazione;
  • questo servizio parli con i client tramite lo standard MCP, non tramite un REST personalizzato;
  • siate già in grado di registrare a mano strumenti, risorse e prompt, senza timore del protocollo.

12. Qualche nota sulla struttura del codice e best practice

Anche in un esempio così piccolo si possono impostare buone abitudini.

Primo, mantenete la configurazione del server separata. Tutto ciò che riguarda nome, versione, logging, impostazioni del trasporto (porta, path /mcp) si può estrarre in un piccolo modulo config.ts. Più avanti, quando effettuerete il deploy su Vercel o dietro un MCP gateway, dovrete aggiungere variabili d’ambiente e vi ringrazierete.

Secondo, cercate di mantenere i metodi registerTool/registerResource/registerPrompt il più possibile “sottili”. La descrizione di schemi, testi e logica di business sono cose che stanno bene in file separati:

  • gifts.ts — funzioni di selezione dei regali;
  • catalog.ts — lavoro con il catalogo prodotti;
  • prompts.ts — set di prompt.

Il file server.ts diventa così una sorta di “provider MCP” che incolla tutto insieme.

Terzo, ricordate che il server MCP è per sua natura reattivo: attende connessioni e richieste dei client. Questo significa che qualsiasi operazione bloccante o troppo lunga dentro gli strumenti impatterà direttamente la UX in ChatGPT. Nei prossimi moduli parleremo di timeout, operazioni asincrone e risposte in streaming, ma già ora conviene pensare a quali operazioni spostare in background e quali devono rispondere velocemente.

Insight: ChatGPT supporta solo una parte di MCP

È importante capire: le ChatGPT Apps usano MCP come trasporto e formato, ma non sono client MCP completi. Se si legge solo il protocollo, è facile costruire aspettative sbagliate su come funzionerà tutto a runtime.

Cosa promette il “puro” MCP:

  • le risorse (resources) possono essere lette dinamicamente, su richiesta del client, non una volta per tutte;
  • il server può inviare notifiche resourceChanged/toolChanged e quindi “spingere” aggiornamenti senza riavviare il client;
  • si può costruire un sistema piuttosto flessibile, in cui il set di tools/resources/prompts è governato da config o stato esterno.

Nel contesto delle ChatGPT Apps non è così. Per l’applicazione lo scenario è molto più statico:

  • al momento della registrazione, l’App ChatGPT legge una volta la descrizione di tutti i tools e resources;
  • poi questa configurazione viene di fatto messa in cache come parte della versione dell’app;
  • aggiornamenti dinamici tramite notifiche MCP non sono supportati — la piattaforma semplicemente li ignora.

13. Errori tipici nella scrittura del primo MCP server

Errore n. 1: riversare tutta la logica di business direttamente in registerTool.
La tentazione di “scrivere tutto velocemente nell’handler dello strumento” è grande, soprattutto in un esempio didattico. Ma poi diventa un mostro illeggibile, convalidazione, accesso al DB e formattazione della risposta mescolati. Meglio estrarre subito le funzioni di business (suggestGifts, lavoro con il catalogo) in moduli separati, e nell’handler fare solo il “collante”.

Errore n. 2: legarsi in modo rigido a nomi specifici dei metodi JSON di MCP.
A volte gli studenti iniziano a scrivere if (method === "tools/list") e a parsare JSON a mano. Non serve: è lavoro dell’SDK. La spec MCP e i nomi dei metodi possono evolvere, e l’SDK se ne fa carico. Usate registerTool, registerResource, registerPrompt e lasciate che la libreria decida come appaia in JSON‑RPC.

Errore n. 3: non considerare il trasporto e provare a far usare a ChatGPT un server stdio.
Il trasporto stdio è perfetto per client locali come ambienti desktop, dove il client può avviare il server come sottoprocesso. Ma ChatGPT comunica via HTTPS e ha bisogno di un endpoint HTTP/stream. Tentare di “tunnelizzare” uno stdio finisce in dolore. Per la ChatGPT App usate subito il trasporto HTTP (Streamable HTTP).

Errore n. 4: ignorare i MIME type e la struttura delle risorse.
Per le risorse contano non solo i contenuti, ma anche il tipo (mimeType) e l’URI. Se ovunque scrivete text/plain e buttate stringhe JSON a caso, per i client (e gli inspector) sarà più difficile capire che dati siano. Indicate MIME type corretti (application/json, text/html per template UI ecc.) e URI stabili.

Errore n. 5: usare il server MCP come “random HTTP API”.
A volte viene l’idea: “Già che ho Express, appendo anche /api/whatever e ci picchio direttamente”. Mescolare l’endpoint MCP con REST arbitrario non conviene: complica configurazione, routing e sicurezza. Meglio avere un contratto chiaro: /mcp per MCP, percorsi separati per altre esigenze, o addirittura un altro servizio. In produzione è particolarmente importante per configurare gateway e autorizzazione. In breve, non trasformate il server MCP in un “random HTTP API” — una collezione di endpoint casuali non legati al contratto MCP.

Errore n. 6: non loggare i messaggi in ingresso e in uscita MCP.
Senza log, il server MCP diventa una scatola nera: “qualcosa non funziona, ma non so cosa”. Già sul primo server ha senso scrivere almeno su stderr log strutturati compatti: metodo dello strumento, stato, tempo di esecuzione. L’importante è non loggare dati sensibili e token; ne parleremo più avanti quando tratteremo la sicurezza.

Errore n. 7: tentare il debug di tutto direttamente via ChatGPT, senza avere un inspector.
Scenario comune: uno studente scrive il server MCP, lo collega subito alla ChatGPT App e “tutto si rompe in modo poco chiaro”. Nel frattempo l’inspector non è mai stato avviato. Risulta difficile capire dov’è il problema: nel protocollo, nel server, nell’Apps SDK o nel comportamento del modello. La strada giusta è assicurarsi prima che il server MCP funzioni correttamente in isolamento (tramite MCP Jam / Inspector), e solo dopo collegarlo all’applicazione.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION