CodeGym /Cursos /ChatGPT Apps /Implementação de ferramentas no servidor: da chamada à re...

Implementação de ferramentas no servidor: da chamada à resposta

ChatGPT Apps
Nível 4 , Lição 2
Disponível

1. Visão geral: caminho da chamada de ferramenta pelo servidor

Antes de escrever código, vamos fixar a arquitetura. Isso ajuda a não se afogar nos detalhes.

Na terminologia de Apps SDK + MCP tudo funciona assim: temos um servidor MCP (neste curso é o Route Handler app/mcp/route.ts no Next.js), que registra ferramentas e recursos e implementa os handlers dessas ferramentas.

Esquema de alto nível:

sequenceDiagram
    participant User as Usuário
    participant Chat as ChatGPT (modelo)
    participant App as ChatGPT App
    participant MCP as MCP-servidor / backend
    participant DB as Catálogo/APIs externas

    User->>Chat: "Sugira um presente..."
    Chat->>App: decide chamar a ferramenta `suggest_gifts`
    App->>MCP: JSON-RPC call_tool (nome + argumentos)
    MCP->>MCP: Validação, autorização
    MCP->>DB: Consulta ao catálogo/filtragem
    DB-->>MCP: Lista de candidatos
    MCP-->>App: structuredContent + content + _meta
    App-->>Chat: Entrega o resultado à modelo + ao widget
    Chat-->>User: Explica a escolha, exibe o widget

A ideia principal: o servidor não sabe nada sobre a “mágica” do modelo. Ele vê uma requisição comum: nome da ferramenta + argumentos, e deve retornar uma resposta estruturada. O modelo, por sua vez, não vê seu código; ele vê apenas:

  • quais ferramentas existem e seus esquemas;
  • os argumentos que ele mesmo formulou;
  • o JSON de resposta que você retornou.

Portanto, nossa tarefa nesta aula é implementar com cuidado a parte do meio: o servidor MCP e os handlers das ferramentas.

Insight: limite de mcp-tools

No servidor MCP, a quantidade de ferramentas é uma métrica tão limitada quanto memória ou tokens de contexto. Formalmente, você pode registrar dezenas ou até centenas de ferramentas, mas a plataforma e o modelo não trabalham com elas de forma linear: cada nova ferramenta aumenta o “ruído” no roteamento.

Na prática, os parâmetros de referência são:

  • teto rígido para o ChatGPT ≈ até 128 MCP-tools por servidor;
  • faixa operacionalaté 50 ferramentas. Passando disso, a qualidade cai visivelmente: o modelo começa a confundir ferramentas com descrições parecidas, lembra-se menos das raras e escolhe a errada com mais frequência.

Na Anthropic é semelhante: limite na ordem de 100 tools no máximo, e eles próprios recomendam ficar por volta de até 50.

2. Onde vive a lógica de servidor no template Next.js + Apps SDK

No módulo 2, já levantamos o template oficial do Next.js para ChatGPT App e passamos rapidamente por sua estrutura. Agora veremos onde vive o servidor MCP e como ele se conecta ao widget.

Se você usa esse template, o servidor MCP geralmente é implementado no arquivo app/mcp/route.ts (App Router). É exatamente para lá que chegam as chamadas JSON‑RPC do ChatGPT: tools/call, resources/list, handshake etc.

Estrutura típica do projeto:

my-chatgpt-app/
├─ app/
│  ├─ mcp/
│  │  └─ route.ts          # MCP-servidor + registro de ferramentas
│  ├─ page.tsx             # React widget (UI)
│  ├─ layout.tsx           # Root layout, Bootstrap SDK
│  └─ globals.css          # Estilos globais
│
├─ proxy.ts                # CORS e afins
├─ next.config.ts
├─ package.json
├─ tsconfig.json
└─ .env

No route.ts nós:

  1. criamos uma instância do servidor MCP (via @modelcontextprotocol/sdk);
  2. registramos as ferramentas (server.registerTool(...));
  3. definimos o handler HTTP que recebe as requisições do ChatGPT e as repassa para o servidor MCP.

Em seguida, vamos escrever código em TypeScript com base nessa estrutura.

3. Servidor MCP mínimo e handler de ferramenta

Comecemos pelo mais simples: vamos criar o servidor e adicionar nossa ferramenta didática suggest_gifts, que retornará um stub.

Suponha que o MCP‑SDK já esteja instalado:

pnpm add @modelcontextprotocol/sdk

E criaremos um app/mcp/route.ts simples:

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

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

// Registro da ferramenta com o esquema mínimo
server.registerTool(
  "suggest_gifts",
  {
    title: "Seleção de presentes",
    description: "Seleciona presentes por interesses e orçamento.",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Breve descrição do destinatário." },
      },
      required: ["query"],
    },
  },
  async ({ input }) => {
    // Aqui ficará a lógica de negócio
    return {
      content: [
        {
          type: "text",
          text: `Simulação: presentes para "${input.query}".`,
        },
      ],
      structuredContent: {},
    };
  }
);

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

Isso já é funcional: o ChatGPT poderá chamar suggest_gifts, e o servidor retornará um stub textual.

É importante notar que server.registerTool recebe:

  • o nome da ferramenta;
  • metadados e o JSON Schema de entrada;
  • o handler — uma função assíncrona para a qual chegam os argumentos input.

Mas por enquanto não há validação, nem um structured output decente, nem autorização. É exatamente isso que vamos fazer agora.

4. Validação de entrada e separação de camadas

Por que o JSON Schema sozinho não basta

Sim, a plataforma valida automaticamente o básico conforme o esquema: tipos de campos, propriedades obrigatórias etc. Mas:

  • o modelo pode enviar dados logicamente incorretos (por exemplo, orçamento −100 ou uma lista de interesses com 1000 itens);
  • você tem restrições de negócio (orçamento máximo, moedas suportadas etc.);
  • às vezes, o ChatGPT ou outro cliente pode se comportar de forma estranha e mandar algo totalmente inesperado.

Portanto, dentro do handler ainda é necessária uma validação adicional.

Vamos separar o código: handler ↔ lógica de negócio

Para o código do servidor não virar “espaguete”, é útil manter a lógica de negócio separada. Por exemplo, vamos criar 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;
};

// "Base" simples de presentes
const CATALOG: GiftItem[] = [
  {
    id: "board-game-1",
    title: "Jogo de tabuleiro \"Estratégia espacial\"",
    price: 39,
    currency: "USD",
    score: 0.93,
    tags: ["board_games", "strategy", "2-4_players"],
    shortDescription: "Ótimo presente para quem curte jogos de tabuleiro.",
  },
  // ...
];

export function suggestGifts(input: SuggestGiftsInput): GiftItem[] {
  if (input.maxBudget <= 0) {
    throw new Error("O orçamento deve ser um número positivo.");
  }

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

  // Simplificando: ordenamos por score e pegamos o top-3
  return filtered.sort((a, b) => b.score - a.score).slice(0, 3);
}

Agora, no handler da ferramenta MCP, faremos:

  • o parsing de input;
  • o mapeamento para o tipo SuggestGiftsInput;
  • a chamada segura de suggestGifts;
  • o empacotamento do resultado em um formato compreensível pelo ChatGPT e pelo nosso UI.

5. Implementação do handler: do input ao structuredContent

Vamos reescrever o registerTool em route.ts, usando nossa lógica de negócio:

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

server.registerTool(
  "suggest_gifts",
  {
    title: "Seleção de presentes",
    description:
      "Use quando precisar selecionar presentes por interesses, orçamento e tipo de relacionamento.",
    inputSchema: {
      type: "object",
      properties: {
        age: {
          type: "integer",
          minimum: 0,
          maximum: 120,
          description: "Idade do destinatário, se conhecida.",
        },
        relationship: {
          type: "string",
          enum: ["friend", "partner", "colleague"],
          description: "Tipo de relacionamento com o destinatário.",
        },
        maxBudget: {
          type: "number",
          minimum: 1,
          description: "Orçamento máximo em dólares americanos.",
        },
        interests: {
          type: "array",
          items: { type: "string" },
          description: "Interesses do destinatário (por exemplo, board games, hiking).",
        },
      },
      required: ["relationship", "maxBudget", "interests"],
    },
  },
  async ({ input }) => {
    // Validação lógica básica
    if (!Array.isArray(input.interests) || input.interests.length === 0) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: "É preciso informar pelo menos um interesse do destinatário.",
          },
        ],
        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:
              "Não encontrei presentes adequados dentro do orçamento informado. Tente aumentar o orçamento ou ajustar os interesses.",
          },
        ],
        structuredContent: {
          items: [],
          emptyReason: "NO_MATCHES",
        },
      };
    }

    return {
      content: [
        {
          type: "text",
          text: `Encontrei ${items.length} opções de presente adequadas.`,
        },
      ],
      structuredContent: {
        items: items.map((item) => ({
          id: item.id,
          title: item.title,
          price: item.price,
          currency: item.currency,
          shortDescription: item.shortDescription,
          tags: item.tags,
        })),
      },
    };
  }
);

Há alguns pontos importantes aqui.

Primeiro, verificamos explicitamente que interests não é uma lista vazia. Mesmo que o JSON Schema permita formalmente um array vazio, para nós tal chamada não faz sentido. É melhor retornar um erro claro do que tentar montar uma lista aleatória.

Segundo, retornamos dois conjuntos de dados:

  • content — para o modelo. É um breve resumo textual: “encontrei N opções”. O modelo usará isso na resposta ao usuário.
  • structuredContent — para o modelo e para o UI. É um JSON estruturado com a lista de presentes, que nosso widget pode renderizar em forma de cards.

Erro comum — colocar o JSON inteiro em content. Não faça isso: o modelo gasta tokens e pode se confundir. Mantenha content curto e coloque os detalhes em structuredContent.

6. Adicionando o template de UI e _meta/openai/outputTemplate

No nível do Apps SDK, o servidor também informa ao ChatGPT qual template de UI usar para visualizar o resultado da ferramenta. Isso é feito via recursos e _meta["openai/outputTemplate"]: o servidor registra um recurso HTML com mimeType: "text/html+skybridge", e a ferramenta referencia esse recurso na resposta.

No template do Next.js isso geralmente fica encapsulado em uma camada de conveniência, mas, simplificando, fica assim:

// em algum ponto da inicialização do servidor MCP
server.registerResource("ui://widget/gifts.html", {
  name: "Gift suggestions widget",
  mimeType: "text/html+skybridge",
  // em seguida: como servir o HTML (template embutido ou arquivo)
});

E na resposta da ferramenta:

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

Assim, o ChatGPT não apenas entende a estrutura do resultado, mas também carrega o HTML/JS do widget apropriado, e nosso componente React dentro do iframe lê window.openai.toolOutput e renderiza a lista de presentes.

Falaremos em mais detalhes sobre a parte de UI nas aulas sobre o fluxo ToolOutput → UI (neste mesmo módulo). Por ora, note apenas o vínculo: o handler da ferramenta responde não apenas pelos dados de negócio, mas também por qual template de UI associar ao resultado. Aqui olhamos para esse vínculo pelos olhos do servidor MCP: qual template indicar e o que colocar em structuredContent.

Insight

Os criadores do ChatGPT idealizaram o widget como um template para exibição de JSON. Por isso usam o nome outputTemplate. A ideia original é: o ChatGPT chama um mcp‑tool, e o mcp‑tool retorna JSON e, às vezes, retorna um widget. Se não houver widget, o ChatGPT decide por conta própria como exibir o JSON.

E, se o widget for indicado, o ChatGPT mostra o widget, passa o JSON para o widget como toolOutput e o widget deve exibir esse JSON. O widget é um template para exibir JSON. É por isso que ele é cacheado já na fase de registro do aplicativo na Store.

Você pode usar o widget como preferir: nele é possível chamar fetch(). Mas, entendendo a proposta original dos desenvolvedores do ChatGPT, fica mais fácil aceitar algumas limitações existentes e, provavelmente, futuras mudanças.

7. Autorização e acesso no handler

Até agora, fingimos que tudo no mundo são dados públicos. Na prática, parte das ferramentas exige autorização: acesso à conta do usuário, seus pedidos, pagamentos, documentos etc.

Na terminologia de Apps SDK / MCP, você pode definir securitySchemes em uma ferramenta e, depois, no handler, verificar tokens e contexto.

Exemplo simples:

server.registerTool(
  "list_user_orders",
  {
    title: "Lista de pedidos do usuário",
    description: "Retorna os pedidos mais recentes do usuário autenticado.",
    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: "É preciso entrar na conta para ver os pedidos.",
          },
        ],
        _meta: {
          // Pedimos ao ChatGPT para iniciar o UI de OAuth
          "mcp/www_authenticate": [
            'Bearer resource_metadata="https://your-mcp.example.com/.well-known/oauth-protected-resource", error="insufficient_scope", error_description="Autentique-se para continuar."',
          ],
        },
      };
    }

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

    return {
      content: [
        {
          type: "text",
          text: `Encontrei ${orders.length} últimos pedidos.`,
        },
      ],
      structuredContent: { orders },
    };
  }
);

É importante entender que:

  • O ChatGPT não “adivinha” suas verificações. Ele apenas passa tokens e contexto, e você deve realizar a autorização adequadamente.
  • O campo especial _meta["mcp/www_authenticate"] informa à plataforma: “é preciso mostrar ao usuário um UI para login/atualização de token”. Sem isso, o ChatGPT verá apenas um erro.

Falaremos sobre as dificuldades da autorização separadamente no módulo 10; por enquanto, basta a ideia básica: verifique o token no handler e não confie cegamente no modelo.

8. Integração com APIs externas e BD: camadas e práticas

É grande a tentação de “fazer tudo no handler”: parsing de argumentos, consulta à base, filtragem, mapeamento para structuredContent, logging e um pouco de filosofia — tudo em uma função de 150 linhas. É como escrever todo o app em pages/index.tsx — dá, mas dói.

É muito melhor separar em camadas:

// 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 (atualizado)
import { fetchGiftsFromApi } from "./gifts-repository";

export async function suggestGifts(input: SuggestGiftsInput): Promise<GiftItem[]> {
  if (input.maxBudget <= 0) {
    throw new Error("O orçamento deve ser um número positivo.");
  }

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

  return items.sort((a, b) => b.score - a.score).slice(0, 3);
}
// route.ts (trecho do 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: "Ocorreu um erro ao selecionar presentes. Tente novamente mais tarde.",
          },
        ],
        structuredContent: {
          errorCode: "INTERNAL_ERROR",
        },
      };
    }
  }

Essa abordagem traz várias vantagens.

  • Testabilidade: você pode escrever testes unitários para suggestGifts e fetchGiftsFromApi sem subir o servidor MCP.
  • Legibilidade: o handler permanece um adaptador fino entre o protocolo (MCP) e sua lógica.
  • Reuso: se depois você precisar da mesma seleção de presentes em outro lugar (por exemplo, em uma API REST), não será preciso “arrancar” a lógica do MCP.

9. Logging e observabilidade básica

A implementação de ferramentas no servidor é um ótimo lugar para já cuidar de observabilidade mínima. Em produção, você vai querer saber:

  • quais ferramentas são chamadas;
  • com quais argumentos (sem PII, claro);
  • quanto tempo leva o processamento;
  • quantos erros e de quais tipos.

Como estamos entendendo o ChatGPT App, deixaremos o uso de loggers profissionais para depois. Um logger simples em volta dos handlers pode ser assim:

// 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(),
      // Nunca registre PII em produção!
      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 do 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 {
      // ... lógica principal ...
      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;
    }
  }
);

Depois, nos módulos sobre métricas, SLO e monitoramento, você poderá usar esses logs para construir gráficos e alertas. Mas é bom criar o hábito de logar desde já.

10. Como o resultado do servidor chega ao widget (e volta)

Na seção 6, já vinculamos o resultado da ferramenta ao template de UI via _meta["openai/outputTemplate"]. Agora veremos o mesmo caminho do outro lado — como esse structuredContent vai parar dentro do widget React e o que fazer com ele no UI.

Embora esta aula foque no servidor, é importante entender que você projeta não só um “API para o modelo”, mas também um “API para o UI”. O servidor retorna:

  • structuredContent — dados que são vistos pelo modelo e pelo widget (via toolOutput);
  • content — uma descrição “condensada” do resultado para o modelo;
  • _meta — campos privados para o widget: openai/outputTemplate, openai/widgetCSP, openai/widgetDomain etc.

Dentro do widget React, você faz algo como:

// app/page.tsx (trecho)
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>Aguardando resultados da seleção de presentes…</div>;
  }

  if (!output.items || output.items.length === 0) {
    return <div>Não há presentes adequados. Tente alterar os critérios.</div>;
  }

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

É por isso que é tão importante que o structuredContent tenha um contrato estável e seja amigável ao UI: campos separados, sem um inferno de aninhamento em 10 níveis.

Falamos em detalhes sobre esse caminho em outra aula do módulo 4; aqui, apenas reforçamos: o servidor e o widget se baseiam na mesma estrutura structuredContent.

11. Tratamento de erros no servidor: formato e estratégia

Nas seções 8–9 já tocamos em erros e logging dentro do handler. Agora vamos juntar tudo em um formato unificado: como exatamente retornar erros das ferramentas para que o modelo e o UI possam lidar com eles.

Erros nos handlers são inevitáveis: uma hora a API externa cai, outra chegam dados ruins, ou você mesmo digita algo errado. O principal é não transformá-los em “500 Internal Server Error sem explicação” para o modelo e o usuário.

Uma boa implementação de ferramenta no servidor:

  • diferencia erros de validação do usuário/modelo e erros internos;
  • retorna um campo claro isError e um errorCode compreensível em structuredContent;
  • fornece ao humano, em content, uma mensagem amigável.

Exemplo (suponha que os metadados da ferramenta — title, description, inputSchema etc. — já estejam extraídos para a variável meta, para não duplicá-los aqui):

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(
          "Orçamento muito alto. Ajuste a solicitação (até 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:
                "Não encontrei presentes para esse orçamento. Tente alterar os interesses ou aumentar o orçamento.",
            },
          ],
          structuredContent: {
            items: [],
            emptyReason: "NO_MATCHES",
          },
        };
      }

      return {/* resultado normal */};
    } catch (err) {
      console.error(err);
      return makeErrorResult(
        "Erro interno do servidor ao selecionar presentes.",
        "INTERNAL_ERROR"
      );
    }
  }
);

Esse formato ajuda tanto o modelo (que pode tentar ajustar os argumentos) quanto o UI (o widget pode exibir mensagens específicas para diferentes errorCode).

Falaremos em breve sobre resiliência, idempotência e design seguro de ferramentas, mas já agora é útil acostumar-se: é melhor retornar um erro explícito do que fazer algo estranho silenciosamente.

No final da aula, ainda reuniremos esses e outros pontos em uma lista de erros comuns na implementação de ferramentas no servidor, para servir como um checklist.

12. Exemplo rápido end‑to‑end: da requisição à resposta

Vamos juntar tudo o que fizemos em uma cadeia lógica no nosso aplicativo GiftGenius.

  1. O usuário escreve no ChatGPT:
    “Sugira um presente para um amigo, ele gosta de jogos de tabuleiro, orçamento até 50 dólares”.
  2. O modelo, conhecendo a ferramenta suggest_gifts e seu esquema, decide chamá-la e monta o tool_call:
    {
      "tool": "suggest_gifts",
      "arguments": {
        "relationship": "friend",
        "maxBudget": 50,
        "interests": ["board games"],
        "age": null
      }
    }
    
  3. A plataforma envia esse JSON‑RPC ao nosso servidor MCP (POST /app/mcp), e o Next.js passa o corpo para server.handle(...).
  4. Nosso handler suggest_gifts:
    • valida que interests não está vazio;
    • chama suggestGifts(payload);
    • obtém um array GiftItem[] (top‑3 por score);
    • empacota em structuredContent.items e adiciona _meta["openai/outputTemplate"] = "ui://widget/gifts.html".
  5. O ChatGPT recebe a resposta, coloca o structuredContent no contexto, carrega o recurso HTML do widget gifts.html e passa o toolOutput para lá.
  6. Nosso widget React lê window.openai.toolOutput.items e renderiza a lista de presentes; com base em content e structuredContent, o modelo escreve ao usuário uma explicação de por que esses presentes são adequados.
  7. O usuário clica, por exemplo, em “Mostrar mais” no widget — o widget chama callTool via SDK → volta ao nosso handler, mas agora com outros argumentos (por exemplo, orçamento aumentado).

Toda essa cadeia se sustenta no fato de que a implementação da ferramenta no servidor:

  • recebe um input estruturado conforme o JSON Schema acordado;
  • valida os dados cuidadosamente;
  • chama lógica de negócio isolada;
  • retorna um structured output estável;
  • indica, quando necessário, o template de UI e metadados.

13. Erros típicos na implementação de ferramentas no servidor

Erro nº 1: “Tudo em um lugar só” — handler gigante.
Quando toda a lógica e o acesso a APIs externas vivem dentro de server.registerTool(..., async () => { ... }), o código cresce rápido e vira um monólito ilegível. Ao menor ajuste, tudo quebra de uma vez. Prefira extrair a lógica de negócio para funções/módulos separados e manter o handler como um adaptador fino.

Erro nº 2: Fé cega no JSON Schema.
Desenvolvedores muitas vezes pensam: “Se existe esquema — a entrada é sempre válida”. Mas o modelo pode enviar valores estranhos, e clientes externos, mais ainda. Não dá para depender apenas de tipos e JSON Schema — é necessária validação lógica (limites de orçamento, tamanho de arrays, valores permitidos etc.).

Erro nº 3: Colocar tudo em content e ignorar structuredContent.
Às vezes colocam um JSON enorme em content como string “por via das dúvidas”. Isso torna as dicas do modelo ruidosas e caras em tokens, e o UI sofre porque precisa decodificar uma string em vez de receber uma estrutura normal. É muito melhor manter content curto e colocar os detalhes em structuredContent.

Erro nº 4: Formato instável do structured output.
Hoje items é um array de objetos com campos id, title, price, e amanhã você renomeia price para amount, e o widget quebra. Ou adiciona um novo nível de aninhamento. Dá para fazer, mas é preciso versionar o contrato ou evoluir o esquema em passos menores. Caso contrário, o UI e os testes quebram o tempo todo.

Erro nº 5: Falta de tratamento de erros significativo.
Lançar uma exceção e esperar que a plataforma “se vire” não é uma boa estratégia. O modelo verá um erro obscuro de JSON‑RPC, o usuário — um banner vermelho, e você perderá o contexto do problema. É muito melhor retornar isError, errorCode e uma mensagem legível, registrando os detalhes no servidor.

Erro nº 6: Ignorar autorização e confiar no modelo.
Às vezes, desenvolvedores pensam: “O modelo é inteligente, não vai chamar essa ferramenta se o usuário não estiver autenticado”. Na realidade, o modelo não conhece seus ACLs e limites; ele vê apenas as descrições dos tools. Todas as verificações de permissão devem estar no handler do servidor, independentemente de como a ferramenta é descrita.

Erro nº 7: Logar tudo, inclusive PII.
É fácil, por hábito, logar o input inteiro. No caso de ChatGPT App, isso pode incluir PII (nomes, e‑mail, endereços etc.), o que viola a política da OpenAI e o bom senso. Prefira logar apenas informações agregadas/anonimizadas: tipo de relacionamento, faixa de orçamento, quantidade de interesses.

Erro nº 8: Ausência de timeouts e retries ao trabalhar com APIs externas.
Se a ferramenta, dentro do handler, faz fetch para uma API externa sem timeouts e repetição, qualquer lentidão dessa API parecerá “o ChatGPT travou”. O usuário pensará que o app quebrou. No servidor, defina limites de tempo, trate timeouts e retorne um erro significativo.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION