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 operacional — até 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:
- criamos uma instância do servidor MCP (via @modelcontextprotocol/sdk);
- registramos as ferramentas (server.registerTool(...));
- 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.
- O usuário escreve no ChatGPT:
“Sugira um presente para um amigo, ele gosta de jogos de tabuleiro, orçamento até 50 dólares”. - 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 } } - A plataforma envia esse JSON‑RPC ao nosso servidor MCP (POST /app/mcp), e o Next.js passa o corpo para server.handle(...).
- 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".
- O ChatGPT recebe a resposta, coloca o structuredContent no contexto, carrega o recurso HTML do widget gifts.html e passa o toolOutput para lá.
- 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.
- 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.
GO TO FULL VERSION