CodeGym /Corsi /ChatGPT Apps /Implementazione lato server degli strumenti: dalla chiama...

Implementazione lato server degli strumenti: dalla chiamata alla risposta

ChatGPT Apps
Livello 4 , Lezione 2
Disponibile

1. Il quadro completo: il percorso di invocazione dello strumento tramite server

Prima di scrivere codice, fissiamo l’architettura. Questo aiuterà a non perdersi nei dettagli.

Nella terminologia Apps SDK + MCP tutto appare così: abbiamo un server MCP (nel nostro corso è il Route Handler app/mcp/route.ts in Next.js) che registra strumenti e risorse e implementa i handler per questi strumenti.

Schema ad alto livello:

sequenceDiagram
    participant User as Utente
    participant Chat as ChatGPT (modello)
    participant App as ChatGPT App
    participant MCP as Server MCP / backend
    participant DB as Catalogo/API esterni

    User->>Chat: "Scegli un regalo..."
    Chat->>App: decide di chiamare il tool `suggest_gifts`
    App->>MCP: JSON-RPC call_tool (nome + argomenti)
    MCP->>MCP: Validazione, autorizzazione
    MCP->>DB: Query al catalogo/filtraggio
    DB-->>MCP: Elenco dei candidati
    MCP-->>App: structuredContent + content + _meta
    App-->>Chat: Passa il risultato al modello + al widget
    Chat-->>User: Spiega la scelta, mostra il widget

L’idea principale: il server non sa nulla della “magia” del modello. Vede una normale richiesta: nome dello strumento + argomenti, e deve restituire una risposta strutturata. Il modello non vede il vostro codice, vede solo:

  • quali strumenti esistono e i loro schemi;
  • gli argomenti che esso stesso ha formato;
  • la risposta JSON che avete restituito.

Quindi il nostro compito in questa lezione è implementare con cura la parte centrale: il server MCP e i handler dei tools.

Insight: mcp-tools limit

Nel server MCP la quantità di strumenti è una metrica limitata esattamente come la memoria o i token di contesto. Formalmente potete registrare decine o persino centinaia di tools, ma la piattaforma e il modello non lavorano con essi in modo lineare: ogni nuovo strumento aumenta il “rumore” durante l’instradamento.

La pratica suggerisce alcuni riferimenti:

  • tetto rigido per ChatGPT ≈ fino a 128 MCP-tools per server;
  • intervallo consigliatofino a 50 strumenti. Oltre, la qualità cala visibilmente: il modello inizia a confondere strumenti con descrizioni simili, ricorda meno spesso quelli rari e sceglie più spesso quello sbagliato.

Con Anthropic il quadro è simile: limite di ordine 100 tools massimo, e loro stessi raccomandano di restare intorno a fino a 50.

2. Dove vive la logica server nel template Next.js + Apps SDK

Nel modulo 2 abbiamo già avviato il template ufficiale Next.js per ChatGPT App e dato un’occhiata alla sua struttura. Ora vediamo dove vive il server MCP e come è collegato al widget.

Se usate questo template, il server MCP di solito è implementato nel file app/mcp/route.ts (App Router). È lì che arrivano le chiamate JSON‑RPC di ChatGPT: tools/call, resources/list, handshake, ecc.

Struttura tipica del progetto:

my-chatgpt-app/
├─ app/
│  ├─ mcp/
│  │  └─ route.ts          # Server MCP + registrazione degli strumenti
│  ├─ page.tsx             # React widget (UI)
│  ├─ layout.tsx           # Root layout, Bootstrap SDK
│  └─ globals.css          # Stili globali
│
├─ proxy.ts                # CORS e altro
├─ next.config.ts
├─ package.json
├─ tsconfig.json
└─ .env

In route.ts noi:

  1. creiamo un’istanza del server MCP (tramite @modelcontextprotocol/sdk);
  2. registriamo gli strumenti (server.registerTool(...));
  3. definiamo l’handler HTTP che riceve le richieste da ChatGPT e le inoltra al server MCP.

Poi scriveremo il codice in TypeScript, basandoci su questa struttura.

3. Server MCP minimo e handler di uno strumento

Partiamo dal più semplice: creiamo il server e aggiungiamo il nostro strumento didattico suggest_gifts, che restituirà un segnaposto.

Supponiamo che il MCP‑SDK sia già installato:

pnpm add @modelcontextprotocol/sdk

E creiamo un semplice app/mcp/route.ts:

// app/mcp/route.ts
import { NextRequest } from "next/server";
import { McpServer } from "@modelcontextprotocol/sdk/server";

const server = new McpServer({ name: "giftgenius-mcp" });

// Registrazione di uno strumento con schema minimo
server.registerTool(
  "suggest_gifts",
  {
    title: "Suggerimento regali",
    description: "Suggerisce regali in base a interessi e budget.",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Breve descrizione del destinatario." },
      },
      required: ["query"],
    },
  },
  async ({ input }) => {
    // Qui andrà la business logic
    return {
      content: [
        {
          type: "text",
          text: `Segnaposto: regali per "${input.query}".`,
        },
      ],
      structuredContent: {},
    };
  }
);

// Handler HTTP Next.js
export async function POST(req: NextRequest) {
  const body = await req.text(); // Stringa JSON-RPC
  const response = await server.handle(body);
  return new Response(response, {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
}

È già una versione funzionante: ChatGPT può chiamare suggest_gifts e il server restituirà un testo segnaposto.

È importante che server.registerTool accetti:

  • il nome dello strumento;
  • i metadati e lo schema JSON dell’input;
  • il handler — una funzione asincrona alla quale arrivano gli argomenti input.

Ma per ora non ci sono validazione, output strutturato decente né autorizzazione. Proprio ora ce ne occuperemo.

4. Validazione degli input e separazione dei livelli

Perché una sola JSON Schema non basta

Sì, la piattaforma valida automaticamente le cose di base secondo lo schema: tipi dei campi, proprietà obbligatorie, ecc. Ma:

  • il modello può inviare dati logicamente scorretti (per esempio, budget −100 o un elenco di interessi con 1000 elementi);
  • avete vincoli di business (budget massimo, valute supportate, ecc.);
  • a volte ChatGPT o un altro client può comportarsi in modo strano e inviare qualcosa di del tutto inatteso.

Pertanto, all’interno dell’handler serve comunque una validazione aggiuntiva.

Separiamo il codice: handler ↔ business logic

Per evitare spaghetti code sul server, conviene tenere la business logic separata. Ad esempio, creiamo app/mcp/gifts.ts:

// app/mcp/gifts.ts
export type SuggestGiftsInput = {
  age?: number | null;
  relationship: "friend" | "partner" | "colleague";
  maxBudget: number;
  interests: string[];
};

export type GiftItem = {
  id: string;
  title: string;
  price: number;
  currency: "USD";
  score: number;
  tags: string[];
  shortDescription: string;
};

// Semplice "catalogo" di regali
const CATALOG: GiftItem[] = [
  {
    id: "board-game-1",
    title: "Gioco da tavolo 'Strategia spaziale'",
    price: 39,
    currency: "USD",
    score: 0.93,
    tags: ["board_games", "strategy", "2-4_players"],
    shortDescription: "Ottimo regalo per gli amanti dei giochi da tavolo.",
  },
  // ...
];

export function suggestGifts(input: SuggestGiftsInput): GiftItem[] {
  if (input.maxBudget <= 0) {
    throw new Error("Il budget deve essere un numero positivo.");
  }

  const filtered = CATALOG.filter(
    (item) => item.price <= input.maxBudget
  );

  // In modo semplice: ordiniamo per score e prendiamo i primi 3
  return filtered.sort((a, b) => b.score - a.score).slice(0, 3);
}

Ora nell’handler dello strumento MCP ci occupiamo di:

  • parsing di input;
  • mapping al tipo SuggestGiftsInput;
  • invocazione sicura di suggestGifts;
  • impacchettamento del risultato in un formato comprensibile a ChatGPT e alla nostra UI.

5. Implementazione dell’handler: da input a structuredContent

Riscriviamo registerTool in route.ts usando la nostra business logic:

// app/mcp/route.ts (frammento)
import { suggestGifts, SuggestGiftsInput } from "./gifts";

server.registerTool(
  "suggest_gifts",
  {
    title: "Suggerimento regali",
    description:
      "Usalo quando serve suggerire regali in base a interessi, budget e tipo di relazione.",
    inputSchema: {
      type: "object",
      properties: {
        age: {
          type: "integer",
          minimum: 0,
          maximum: 120,
          description: "Età del destinatario, se nota.",
        },
        relationship: {
          type: "string",
          enum: ["friend", "partner", "colleague"],
          description: "Tipo di relazione con il destinatario.",
        },
        maxBudget: {
          type: "number",
          minimum: 1,
          description: "Budget massimo in dollari statunitensi.",
        },
        interests: {
          type: "array",
          items: { type: "string" },
          description: "Interessi del destinatario (ad es., board games, hiking).",
        },
      },
      required: ["relationship", "maxBudget", "interests"],
    },
  },
  async ({ input }) => {
    // Validazione logica di base
    if (!Array.isArray(input.interests) || input.interests.length === 0) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: "È necessario specificare almeno un interesse del destinatario.",
          },
        ],
        structuredContent: { errorCode: "NO_INTERESTS" },
      };
    }

    const payload: SuggestGiftsInput = {
      age: input.age ?? null,
      relationship: input.relationship,
      maxBudget: input.maxBudget,
      interests: input.interests,
    };

    const items = suggestGifts(payload);

    if (items.length === 0) {
      return {
        content: [
          {
            type: "text",
            text:
              "Non ho trovato regali adatti con il budget indicato. Prova ad aumentare il budget o a modificare gli interessi.",
          },
        ],
        structuredContent: {
          items: [],
          emptyReason: "NO_MATCHES",
        },
      };
    }

    return {
      content: [
        {
          type: "text",
          text: `Ho trovato ${items.length} opzioni di regalo adatte.`,
        },
      ],
      structuredContent: {
        items: items.map((item) => ({
          id: item.id,
          title: item.title,
          price: item.price,
          currency: item.currency,
          shortDescription: item.shortDescription,
          tags: item.tags,
        })),
      },
    };
  }
);

Qui ci sono alcuni punti importanti.

Per prima cosa, verifichiamo esplicitamente che interests non sia una lista vuota. Anche se la JSON Schema formalmente consente un array vuoto, per noi una richiesta del genere non ha senso. Meglio restituire subito un errore comprensibile che provare a costruire un elenco casuale.

In secondo luogo, restituiamo due set di dati:

  • content — per il modello. È un breve riassunto testuale: “trovate N opzioni”. Il modello lo userà nella risposta all’utente.
  • structuredContent — per il modello e per la UI. È un JSON strutturato con l’elenco dei regali, che il nostro widget può renderizzare come schede.

Errore frequente — caricare nel content tutto il JSON chilometrico. Non serve: il modello consuma token e può confondersi. Meglio tenere content conciso e mettere i dettagli in structuredContent.

6. Aggiunta del template UI e _meta/openai/outputTemplate

A livello di Apps SDK il server dice anche a ChatGPT quale template UI usare per visualizzare il risultato dello strumento. Si fa tramite le risorse e _meta["openai/outputTemplate"]: il server registra una risorsa HTML con mimeType: "text/html+skybridge", e lo strumento nel risultato ci fa riferimento.

Nel template Next.js questo di solito è incapsulato in una comoda astrazione, ma in forma semplificata appare così:

// da qualche parte durante l'inizializzazione del server MCP
server.registerResource("ui://widget/gifts.html", {
  name: "Gift suggestions widget",
  mimeType: "text/html+skybridge",
  // poi: modo di servire l'HTML (template incorporato o file)
});

E nella risposta dello strumento:

return {
  content: [{ type: "text", text: `Ho trovato ${items.length} regali.` }],
  structuredContent: { items: /* ... */ },
  _meta: {
    "openai/outputTemplate": "ui://widget/gifts.html",
  },
};

In tal modo, ChatGPT non solo capisce la struttura del risultato, ma carica anche l’HTML/JS necessario per il widget, e il nostro componente React dentro l’iframe leggerà window.openai.toolOutput e renderizzerà l’elenco dei regali.

Parleremo in modo più dettagliato della parte UI nelle lezioni sulla gestione di ToolOutput → UI (in questo stesso modulo), quindi qui ci concentriamo solo sul legame: l’handler dello strumento risponde non solo con i dati di business, ma anche con il template UI a cui agganciare il risultato. Qui guardiamo a questo collegamento con gli occhi del server MCP: quale template indicare e cosa mettere in structuredContent.

Insight

I creatori di ChatGPT hanno concepito il widget come un template per visualizzare il JSON. Per questo usano il nome outputTemplate. L’idea originaria è questa: ChatGPT invoca un mcp‑tool, e il mcp‑tool restituisce JSON e talvolta un widget. Se il widget non c’è, ChatGPT decide da solo come visualizzare il JSON.

Se invece il widget è indicato, ChatGPT mostra il widget, passa al widget il JSON come toolOutput e il widget deve visualizzare il JSON. Il widget è un template per la visualizzazione del JSON. Ecco perché viene messo in cache già in fase di registrazione dell’app nello Store.

Potete usare il widget come preferite: al suo interno si può chiamare fetch(). Ma se comprendete l’idea originale degli sviluppatori di ChatGPT, vi sarà più facile accettare alcuni limiti e, probabilmente, i futuri cambiamenti.

7. Autorizzazione e accesso nell’handler

Finora abbiamo finto che tutto nel mondo fosse pubblico. In pratica, parte degli strumenti richiede autorizzazione: accesso all’account dell’utente, ai suoi ordini, pagamenti, documenti, ecc.

Nella terminologia Apps SDK / MCP a uno strumento si possono definire securitySchemes e poi, nell’handler, verificare token e contesto.

Un esempio semplice:

server.registerTool(
  "list_user_orders",
  {
    title: "Elenco ordini dell'utente",
    description: "Restituisce gli ultimi ordini dell'utente autenticato.",
    inputSchema: { type: "object", properties: {}, additionalProperties: false },
    _meta: {
        securitySchemes: [{ type: "oauth2", scopes: ["orders.read"] }],        
    }  
  },
  async ({ auth }) => {
    if (!auth?.accessToken) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: "Devi effettuare l'accesso per visualizzare gli ordini.",
          },
        ],
        _meta: {
          // Chiediamo a ChatGPT di avviare l'UI OAuth
          "mcp/www_authenticate": [
            'Bearer resource_metadata="https://your-mcp.example.com/.well-known/oauth-protected-resource", error="insufficient_scope", error_description="Autenticati per continuare."',
          ],
        },
      };
    }

    // Qui verifichiamo token, issuer, audience, scope...
    const orders = await fetchUserOrders(auth.accessToken);

    return {
      content: [
        {
          type: "text",
          text: `Ho trovato ${orders.length} ordini recenti.`,
        },
      ],
      structuredContent: { orders },
    };
  }
);

È importante capire che:

  • ChatGPT non “indovina da solo” i vostri controlli. Passa solo token e contesto, e voi dovete implementare un’autorizzazione corretta.
  • Il campo speciale _meta["mcp/www_authenticate"] dice alla piattaforma: “bisogna mostrare all’utente l’UI per login/aggiornamento token”. Senza questo, ChatGPT vedrà semplicemente un errore.

Parleremo delle complessità dell’autorizzazione nel modulo 10; per ora basta la nozione di base: verifichiamo il token nell’handler, non ci fidiamo ciecamente del modello.

8. Integrazione con API esterne e DB: livelli e pratiche

La tentazione di “fare tutto nell’handler” è forte: parsing degli argomenti, query al database, filtraggio, mapping in structuredContent, logging e un po’ di filosofia — tutto in un’unica funzione da 150 righe. È come scrivere tutta l’app in pages/index.tsx: si può, ma fa male.

Molto meglio separare i livelli:

// gifts-repository.ts
import type { GiftItem } from "./gifts";

export async function fetchGiftsFromApi(
  maxBudget: number,
  interests: string[]
): Promise<GiftItem[]> {
  const resp = await fetch("https://example.com/api/gifts", {
    method: "POST",
    body: JSON.stringify({ maxBudget, interests }),
    headers: { "Content-Type": "application/json" },
  });

  if (!resp.ok) {
    throw new Error(`Gift API error: ${resp.status}`);
  }

  const data = (await resp.json()) as GiftItem[];
  return data;
}
// gifts.ts (aggiornato)
import { fetchGiftsFromApi } from "./gifts-repository";

export async function suggestGifts(input: SuggestGiftsInput): Promise<GiftItem[]> {
  if (input.maxBudget <= 0) {
    throw new Error("Il budget deve essere un numero positivo.");
  }

  const items = await fetchGiftsFromApi(input.maxBudget, input.interests);

  return items.sort((a, b) => b.score - a.score).slice(0, 3);
}
// route.ts (frammento dell'handler)
  async ({ input }) => {
    try {
      const payload: SuggestGiftsInput = {
        age: input.age ?? null,
        relationship: input.relationship,
        maxBudget: input.maxBudget,
        interests: input.interests,
      };

      const items = await suggestGifts(payload);

      // ...
    } catch (err) {
      console.error("suggest_gifts failed", err);
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: "Si è verificato un errore durante la selezione dei regali. Riprova più tardi.",
          },
        ],
        structuredContent: {
          errorCode: "INTERNAL_ERROR",
        },
      };
    }
  }

Questo approccio offre vari vantaggi.

  • Testabilità: si possono scrivere unit test per suggestGifts e fetchGiftsFromApi senza avviare il server MCP.
  • Leggibilità: l’handler resta un sottile adattatore tra il protocollo (MCP) e la vostra logica.
  • Riutilizzo: se in seguito servirà lo stesso suggerimento regali altrove (per esempio in una REST API separata), non dovrete “estrarre” la logica dal MCP.

9. Logging e osservabilità di base

L’implementazione lato server degli strumenti è un ottimo luogo per prendersi cura da subito di un minimo di osservabilità. In produzione vorrete sapere:

  • quali strumenti vengono chiamati;
  • con quali argomenti (senza PII, ovviamente);
  • quanto tempo richiede l’elaborazione;
  • quanti e quali errori si verificano.

Ora stiamo esplorando la ChatGPT App, quindi rimandiamo i logger professionali. Un logger di wrapping semplicissimo intorno agli handler potrebbe essere così:

// simple-logger.ts
export function logToolInvocationStart(tool: string, args: unknown) {
  console.log(
    JSON.stringify({
      level: "info",
      event: "tool_invocation_started",
      tool,
      timestamp: new Date().toISOString(),
      // Mai loggare PII in produzione!
      args,
    })
  );
}

export function logToolInvocationEnd(tool: string, ms: number, success: boolean) {
  console.log(
    JSON.stringify({
      level: "info",
      event: "tool_invocation_finished",
      tool,
      durationMs: ms,
      success,
      timestamp: new Date().toISOString(),
    })
  );
}
// route.ts (wrapper dell'handler)
import { logToolInvocationStart, logToolInvocationEnd } from "./simple-logger";

server.registerTool(
  "suggest_gifts",
  { /* ...meta... */ },
  async ({ input }) => {
    const startedAt = Date.now();
    logToolInvocationStart("suggest_gifts", {
      relationship: input.relationship,
      maxBudget: input.maxBudget,
      interestsCount: Array.isArray(input.interests)
        ? input.interests.length
        : 0,
    });

    try {
      // ... logica principale ...
      const duration = Date.now() - startedAt;
      logToolInvocationEnd("suggest_gifts", duration, true);
      return result;
    } catch (err) {
      const duration = Date.now() - startedAt;
      logToolInvocationEnd("suggest_gifts", duration, false);
      throw err;
    }
  }
);

In seguito, nei moduli su metriche, SLO e monitoraggio, potrete costruire grafici e alert su questi log. Ma l’abitudine a loggare è bene acquisirla già ora.

10. Come il risultato del server arriva al widget (e ritorno)

Nella sezione 6 abbiamo già collegato il risultato dello strumento al template UI tramite _meta["openai/outputTemplate"]. Ora vediamo lo stesso percorso dall’altro lato — come questo structuredContent finisce dentro il widget React e cosa farne in UI.

Anche se questa lezione si focalizza sul server, è importante capire che state progettando non solo un “API per il modello”, ma anche un “API per la UI”. Il server restituisce:

  • structuredContent — dati visibili sia al modello sia al widget (tramite toolOutput);
  • content — descrizione “compressa” del risultato per il modello;
  • _meta — campi privati per il widget: openai/outputTemplate, openai/widgetCSP, openai/widgetDomain, ecc.

Dentro il widget React poi farete qualcosa del genere:

// app/page.tsx (frammento)
type ToolOutput = {
  items?: {
    id: string;
    title: string;
    price: number;
    currency: string;
    shortDescription: string;
    tags: string[];
  }[];
  emptyReason?: string;
};

declare global {
  interface Window {
    openai?: {
      toolOutput?: ToolOutput;
    };
  }
}

export default function GiftWidget() {
  const output = typeof window !== "undefined"
    ? window.openai?.toolOutput
    : undefined;

  if (!output) {
    return <div>In attesa dei risultati della selezione dei regali…</div>;
  }

  if (!output.items || output.items.length === 0) {
    return <div>Nessun regalo adatto. Prova a modificare i criteri.</div>;
  }

  return (
    <ul>
      {output.items.map((item) => (
        <li key={item.id}>
          <strong>{item.title}</strong> — {item.price} {item.currency}
        </li>
      ))}
    </ul>
  );
}

Ecco perché è così importante che structuredContent abbia un contratto stabile ed è amichevole per la UI: campi separati, non un inferno annidato in 10 livelli.

Parleremo nel dettaglio di questo percorso in un’altra lezione del modulo 4; qui fissiamo che server e widget si appoggiano alla stessa struttura structuredContent.

11. Gestione degli errori sul server: formato e strategia

Nelle sezioni 8–9 abbiamo già accennato a errori e logging all’interno degli handler. Ora riuniamo il tutto in un formato unico: come restituire gli errori degli strumenti in modo che sia il modello sia la UI possano gestirli.

Gli errori negli handler sono inevitabili: un’API esterna cadrà, arriverà un input non valido, o farete un refuso. L’importante è non trasformarli in “500 Internal Server Error senza spiegazioni” per modello e utente.

Una buona implementazione lato server di uno strumento:

  • distingue tra errori di validazione dell’utente/modello ed errori interni;
  • restituisce un chiaro campo isError e un errorCode significativo in structuredContent;
  • fornisce all’utente in content un messaggio amichevole.

Esempio (supponiamo che i metadati dello strumento — title, description, inputSchema, ecc. — siano già estratti in una variabile meta, così da non duplicarli qui):

function makeErrorResult(message: string, code: string) {
  return {
    isError: true,
    content: [
      {
        type: "text",
        text: message,
      },
    ],
    structuredContent: {
      errorCode: code,
    },
  };
}

server.registerTool(
  "suggest_gifts",
  meta,
  async ({ input }) => {
    try {
      if (input.maxBudget > 10000) {
        return makeErrorResult(
          "Budget troppo alto. Specifica la richiesta (fino a 10000 USD).",
          "BUDGET_TOO_HIGH"
        );
      }

      const items = await suggestGifts({
        age: input.age ?? null,
        relationship: input.relationship,
        maxBudget: input.maxBudget,
        interests: input.interests,
      });

      if (!items.length) {
        return {
          content: [
            {
              type: "text",
              text:
                "Non ho trovato regali con questo budget. Prova a modificare gli interessi o ad aumentare il budget.",
            },
          ],
          structuredContent: {
            items: [],
            emptyReason: "NO_MATCHES",
          },
        };
      }

      return {/* risultato normale */};
    } catch (err) {
      console.error(err);
      return makeErrorResult(
        "Errore interno del server durante la selezione dei regali.",
        "INTERNAL_ERROR"
      );
    }
  }
);

Questo formato aiuta sia il modello (che può provare a modificare gli argomenti) sia la UI (il widget può mostrare messaggi specifici per diversi errorCode).

Parleremo più avanti di resilienza, idempotenza e design sicuro degli strumenti, ma è già utile abituarsi: meglio restituire un errore esplicito che fare qualcosa di ambiguo in silenzio.

Alla fine della lezione raccoglieremo questi e altri punti in un elenco di errori tipici nell’implementazione lato server degli strumenti, per usarlo come checklist.

12. Un breve esempio end‑to‑end: dalla richiesta alla risposta

Mettiamo insieme tutto quello che abbiamo fatto in una catena logica nella nostra app GiftGenius.

  1. L’utente scrive a ChatGPT:
    «Scegli un regalo per un amico, ama i giochi da tavolo, budget fino a 50 dollari».
  2. Il modello, sapendo dello strumento suggest_gifts e del suo schema, decide di chiamarlo e forma il tool_call:
    {
      "tool": "suggest_gifts",
      "arguments": {
        "relationship": "friend",
        "maxBudget": 50,
        "interests": ["board games"],
        "age": null
      }
    }
    
  3. La piattaforma invia questo JSON‑RPC al nostro server MCP (POST /app/mcp), Next.js inoltra il body a server.handle(...).
  4. Il nostro handler suggest_gifts:
    • valida che interests non sia vuoto;
    • chiama suggestGifts(payload);
    • riceve un array GiftItem[] (top‑3 per score);
    • lo impacchetta in structuredContent.items e aggiunge _meta["openai/outputTemplate"] = "ui://widget/gifts.html".
  5. ChatGPT riceve la risposta, mette structuredContent nel contesto, carica la risorsa HTML del widget gifts.html, e passa lì toolOutput.
  6. Il nostro widget React legge window.openai.toolOutput.items e renderizza l’elenco dei regali; il modello, in base a content e structuredContent, scrive all’utente una spiegazione del perché questi regali siano adatti.
  7. L’utente preme, ad esempio, “Mostra altro” nel widget — il widget chiama callTool tramite SDK → si torna nel nostro handler, ma con altri argomenti (per esempio, budget aumentato).

Tutta questa catena si regge sul fatto che l’implementazione lato server dello strumento:

  • accetta un input strutturato secondo una JSON Schema concordata;
  • valida con cura i dati;
  • chiama una business logic isolata;
  • restituisce un output strutturato stabile;
  • all’occorrenza indica un template UI e i metadati.

13. Errori tipici nell’implementazione lato server degli strumenti

Errore n. 1: “Tutto in un unico posto” — handler gigantesco.
Quando tutta la logica e il lavoro con API esterne vivono dentro server.registerTool(..., async () => { ... }), il codice cresce rapidamente e diventa un monolite illeggibile. Al minimo cambiamento si rompe tutto. Meglio estrarre la business logic in funzioni/moduli separati e rendere l’handler un adattatore sottile.

Errore n. 2: Fiducia cieca nella JSON Schema.
Gli sviluppatori spesso pensano: “C’è lo schema — quindi l’input è sempre valido”. Ma il modello può inviare valori strani, e i client esterni ancora di più. Non ci si può affidare solo a tipi e JSON Schema — serve validazione logica (limiti di budget, lunghezza degli array, valori consentiti, ecc.).

Errore n. 3: Ammucchiare tutto in content e ignorare structuredContent.
A volte in content si inserisce un enorme JSON come stringa “per sicurezza”. Questo rende rumorosi e costosi in token i suggerimenti del modello, e la UI soffre perché deve decodificare la stringa invece di ricevere una struttura pulita. Molto meglio tenere content breve e mettere i dettagli in structuredContent.

Errore n. 4: Formato dell’output strutturato instabile.
Oggi items è un array di oggetti con campi id, title, price, e domani rinominate price in amount e il widget va in crash. O aggiungete un nuovo livello di annidamento. Si può fare, ma bisogna o versionare il contratto, o evolvere lo schema a piccoli passi. Altrimenti UI e test si rompono continuamente.

Errore n. 5: Mancanza di una gestione degli errori sensata.
Lanciare un’eccezione e sperare che la piattaforma “la gestisca in qualche modo” non è una buona strategia. Il modello vedrà un vago errore JSON‑RPC, l’utente — un banner rosso, e voi perderete il contesto del problema. Molto meglio restituire un isError, un errorCode e un messaggio comprensibile, loggando i dettagli sul server.

Errore n. 6: Ignorare l’autorizzazione e fidarsi del modello.
A volte gli sviluppatori pensano: “Il modello è intelligente, non chiamerà questo strumento se l’utente non è autorizzato”. In realtà il modello non conosce le vostre ACL e i limiti, vede solo le descrizioni dei tools. Tutti i controlli sui permessi devono stare nell’handler sul server, indipendentemente da come è descritto lo strumento.

Errore n. 7: Loggare tutto, incluse PII.
È molto facile per abitudine loggare l’intero input. Nel caso di ChatGPT App questo può includere PII (nomi, e‑mail, indirizzi, ecc.), violando sia la policy di OpenAI sia il buon senso. Meglio loggare solo informazioni aggregate/anonimizzate: tipo di relazione, intervallo di budget, numero di interessi.

Errore n. 8: Assenza di timeout e retry quando si lavora con API esterne.
Se lo strumento dentro l’handler fa un fetch a un’API esterna senza timeout e retry, qualsiasi ritardo di quell’API sembrerà “ChatGPT è bloccato”. L’utente penserà che l’intera app sia rotta. Sul lato server bisogna impostare limiti di tempo, gestire i timeout e restituire un errore sensato.

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