1. O que vamos construir hoje e como isso se encaixa no aplicativo
Vamos relembrar nosso aplicativo de estudo: estamos fazendo um assistente para escolher presentes. Nos módulos anteriores já tínhamos:
- um widget no ChatGPT (Next.js 16 + Apps SDK), que mostra a UI, o estado e sabe acionar callTool;
- um backend simples (via Apps SDK / rotas do Next.js), que devolvia mocks de presentes.
Agora queremos “separar a inteligência” do nosso assistente em um servidor MCP separado. No fim, o diagrama ficará assim:
flowchart TD
subgraph ChatGPT
U[Usuário
no chat]
W["Widget do App
(Apps SDK)"]
end
subgraph Cliente MCP
C[ChatGPT MCP client]
end
subgraph OurServer[Nosso servidor 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
Ou seja, agora:
- o modelo dentro do ChatGPT enxerga nosso servidor MCP como um conjunto padrão de tools/resources/prompts;
- callTool a partir do widget vira, logicamente, uma chamada interna do MCP;
- nosso servidor descreve contratos (esquemas, descrições) e implementa a lógica de negócio.
Ao final desta aula, você deverá ter um projeto separado em Node/TypeScript com um servidor MCP que:
- sobe localmente com um único comando;
- registra ao menos uma ferramenta e um recurso;
- retorna dados significativos (mesmo que com mocks simples);
- está estruturado para poder evoluir depois.
Ao mesmo tempo, o backend existente via Apps SDK/Next.js não será reescrito agora: ele permanece como está, e o servidor MCP sobe como um serviço separado ao lado. Depois, você poderá “conectá-lo” ao ChatGPT App e gradualmente migrar para lá a lógica de presentes no lugar dos antigos mocks.
2. Stack: TypeScript + MCP SDK + transporte HTTP
Vamos escrever o servidor MCP em TypeScript no Node.js. O SDK oficial de JS/TS para MCP vive no pacote @modelcontextprotocol/sdk. Ele cuida da rotina de JSON‑RPC, validação e conversão de esquemas: você descreve os argumentos via esquemas Zod, e o SDK os converte para JSON Schema, que o modelo entende.
Para o transporte, precisamos de uma variante HTTP: o ChatGPT conversa com servidores MCP remotos pela rede, não por stdio/local. A especificação MCP descreve um formato padrão de “HTTP streaming” — na prática, uma evolução do esquema antigo HTTP+SSE. Na prática, é um único endpoint HTTP que processa a requisição (POST/GET) e, quando necessário, transmite o resultado em streaming. No SDK TypeScript de MCP geralmente já existe um transporte pronto para esse formato, que pode ser acoplado ao Express ou Hono.
Para focar, vamos assumir que temos:
- o objeto de servidor McpServer de @modelcontextprotocol/sdk;
- o transporte HTTP (por exemplo, StreamableHttpServerTransport ou similar), que pode ser integrado ao Express.
Os nomes exatos das classes podem variar um pouco entre versões do SDK, mas a arquitetura é sempre:
- criar um objeto servidor MCP;
- registrar nele tools/resources/prompts;
- conectar o transporte ao app HTTP.
3. Estrutura do projeto e preparação
Vamos criar uma pasta separada para o servidor MCP. É conveniente mantê-la ao lado do app frontend, mas como um projeto Node separado:
chatgpt-gift-app/
app/ ← Next.js + Apps SDK (widget)
mcp-server/ ← nosso servidor MCP
Dentro de mcp-server:
mcp-server/
src/
server.ts ← ponto de entrada do servidor MCP
gifts.ts ← lógica de negócio para seleção de presentes
package.json
tsconfig.json
Faremos um exemplo simples de gifts.ts um pouco mais adiante; por enquanto, vamos nos concentrar em server.ts.
Suponha que você já inicializou o projeto:
mkdir mcp-server
cd mcp-server
npm init -y
npm install typescript ts-node-dev zod express @modelcontextprotocol/sdk
tsconfig.json — bem comum (módulos esnext, target node, strict). Você pode reutilizar de qualquer projeto TS seu.
4. Extraindo a lógica de negócio para um módulo separado
A tentação é escrever logo server.registerTool(..., async () => {...}) e entuchar toda a lógica ali mesmo. Mas é melhor separar desde o início:
- um módulo que não sabe nada sobre MCP, JSON‑RPC e afins;
- um módulo que sabe apenas sobre MCP, mas pouco sabe da lógica de negócio.
Em src/gifts.ts, vamos descrever uma função simples de sugestão de presentes:
// 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[] {
// por enquanto, apenas mocks
return [
{
id: "book-1",
title: "Livro sobre o hobby favorito",
price: Math.min(input.budget, 30),
occasion: "generic",
},
{
id: "game-1",
title: "Jogo de tabuleiro para grupo",
price: Math.min(input.budget, 50),
occasion: "party",
},
];
}
Essa função é pura: recebe parâmetros de entrada e retorna um array de ideias. Você pode testá-la com unit tests, reutilizá-la em outro lugar, e ela não depende de MCP. É justamente a recomendação: a camada de servidor separada, as funções de negócio separadas.
5. Criando o servidor MCP e conectando o transporte HTTP
Agora, o ponto de entrada src/server.ts. Em linhas gerais, precisamos:
- criar uma instância do servidor MCP;
- registrar nele ferramentas, recursos e prompts;
- subir um servidor HTTP (por exemplo, Express) e acoplar o transporte MCP nele.
Comecemos com um esqueleto:
// 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. Criamos o servidor MCP
const mcpServer = new McpServer({
name: "gift-assistant-mcp",
version: "0.1.0",
});
// 2. Vamos registrar tools/resources/prompts aqui depois
// 3. Configuramos o transporte sobre HTTP
const transport = new StreamableHttpServerTransport({
path: "/mcp", // endpoint único do MCP
app, // integrando no app Express
});
transport.attach(mcpServer);
const PORT = process.env.PORT ?? 4000;
app.listen(PORT, () => {
console.log(`MCP server listening on http://localhost:${PORT}/mcp`);
});
Os nomes concretos da classe de transporte podem diferir, mas o padrão é um: você cria um endpoint HTTP e conecta a ele o servidor MCP como handler de JSON‑RPC sobre HTTP/stream.
Nesta etapa, o servidor ainda não faz nada útil, mas ele já consegue:
- passar o handshake do MCP;
- responder a requisições básicas de discovery (lista de tools/resources/prompts — por enquanto vazia).
O próximo passo é registrar a primeira ferramenta.
6. Registrando a tool suggest_gifts via o SDK do MCP
O Apps SDK oficial e a documentação do MCP mostram o mesmo padrão de registro de ferramenta: o método registerTool, no qual você passa o nome, o descritor (título, descrição, esquema de argumentos) e o handler.
Já descrevemos o tipo SuggestGiftsInput em gifts.ts. Agora vamos adicionar um esquema Zod para que o servidor valide os argumentos de entrada e gere automaticamente para a LLM um JSON Schema correto.
// src/server.ts (trecho)
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),
});
Agora registramos a ferramenta:
// ainda em server.ts
mcpServer.registerTool(
"suggest_gifts",
{
title: "Suggest gift ideas",
description:
"Seleciona ideias de presentes com base em idade, tipo de relação e orçamento.",
// O SDK converterá o esquema Zod em JSON Schema para o modelo
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 pode ser usado no widget
structuredContent: {
ideas,
},
};
}
);
Pontos-chave:
- inputSchema — esquema Zod. O SDK para TS sabe transformá-lo em JSON Schema e, assim, descreve automaticamente a ferramenta para o modelo.
- O handler recebe um objeto com input (cujo tipo você obtém do esquema). Dentro dele, você pode chamar sua função de negócio.
- Em result você retorna content — é o texto que o modelo verá como resultado e, se quiser, structuredContent com uma estrutura JSON que o seu widget pode consumir depois.
Se você já criou uma ferramenta via Apps SDK nos módulos anteriores, este código deve parecer muito familiar: o padrão é exatamente o mesmo, só que agora vive em um servidor MCP separado.
7. Adicionando o recurso gift_catalog para dados
Ferramentas são ações. Às vezes também queremos fornecer dados como recurso, para que o modelo possa lê-los, pesquisá-los ou para que seu widget carregue templates, componentes e assim por diante. O MCP descreve separadamente o conceito de recursos com URI, MIME types e conteúdo.
Vamos criar um recurso simples gift_catalog, que retorna uma lista de presentes disponíveis. Por enquanto serão os mesmos mocks, mas na prática poderia ser uma extração do banco ou de um product feed.
Primeiro, o catálogo em si:
// src/gifts.ts (complemento)
export const giftCatalog: GiftIdea[] = [
{
id: "book-1",
title: "Livro de programação",
price: 25,
occasion: "learning",
},
{
id: "lego-1",
title: "Conjunto LEGO",
price: 60,
occasion: "fun",
},
];
Agora registramos o recurso no servidor:
// src/server.ts (trecho)
import { giftCatalog } from "./gifts";
mcpServer.registerResource(
"gift_catalog",
{
title: "Gift catalog",
description: "Catálogo simples de presentes para demo e depuração.",
mimeType: "application/json",
},
async () => {
return {
contents: [
{
uri: "mcp://gift-catalog",
mimeType: "application/json",
text: JSON.stringify(giftCatalog, null, 2),
},
],
};
}
);
O que está acontecendo aqui, logicamente:
- o nome do recurso gift_catalog ficará visível ao cliente no discovery (no inspector do MCP você o verá na lista de recursos);
- o descritor contém a descrição legível e o MIME type;
- o handler retorna um array contents com URI e texto — é o formato padrão de recurso no MCP.
Depois você poderá:
- ler esse recurso a partir do cliente (por exemplo, um agente ou o inspector);
- usá-lo como templates/dados para a UI;
- fazer experimentos: como o modelo usa um catálogo pronto para explicar opções ao usuário.
8. Registrando um prompt simples
A terceira entidade do MCP são os prompts, instruções pré-criadas. Eles permitem não repetir prompts longos (do sistema ou do usuário), armazenando-os no servidor com nomes.
Vamos fazer um mini-exemplo: o prompt birthday_gift, que pode ser chamado como um “template pré-preenchido de conversa sobre presente de aniversário”.
// src/server.ts (trecho)
mcpServer.registerPrompt("birthday_gift", {
title: "Birthday gift helper",
description: "Template de solicitação para selecionar um presente de aniversário.",
messages: [
{
role: "system",
content:
"Você é um assistente de busca de presentes. Faça perguntas de esclarecimento e proponha algumas opções.",
},
{
role: "user",
content:
"Preciso de um presente de aniversário. Faça as perguntas necessárias e me ajude a escolher.",
},
],
});
Por baixo dos panos, o MCP permitirá aos clientes:
- obter a lista de prompts (no inspector você verá birthday_gift);
- pedir seu conteúdo e usá-lo como instrução base para o modelo.
Em outro módulo, sobre system prompt e instruções, analisaremos em detalhe como esses prompts se combinam com instruções globais do app. Aqui, o importante é “vê-los” como parte do servidor MCP.
9. Como tudo funciona em tempo de execução
Vamos juntar a figura inteira.
Quando um cliente (por exemplo, o MCP Inspector ou o ChatGPT) se conecta ao nosso endpoint HTTP /mcp:
- acontece o handshake: cliente e servidor trocam informações sobre capacidades suportadas (tools/resources/prompts etc.);
- o cliente chama métodos de discovery: obtém a lista de ferramentas, recursos e prompts com suas descrições e esquemas;
- quando o modelo decide chamar uma ferramenta, ele cria uma requisição JSON‑RPC com um método como tools/call ou similar — o SDK no servidor transforma isso em uma chamada interna do handler de registerTool;
- o handler executa a lógica de negócio (no nosso caso suggestGifts ou a entrega de giftCatalog) e retorna o resultado no formato padronizado;
- o SDK serializa a resposta de volta em JSON‑RPC e a envia ao cliente pelo mesmo transporte HTTP/stream.
Todos os detalhes de JSON‑RPC, formação de id, roteamento de métodos etc. ficam dentro de @modelcontextprotocol/sdk. Para você, a interface é muito parecida com a do Apps SDK: você trabalha com registerTool/registerResource/registerPrompt e seus handlers, sem se preocupar com o protocolo.
10. Execução local e primeiro teste simples
Suponha que você adicionou tudo o que vimos acima. Falta executar.
Em package.json, você pode adicionar o script:
{
"scripts": {
"dev": "ts-node-dev src/server.ts"
}
}
Executando:
npm run dev
No console deverá aparecer algo como:
MCP server listening on http://localhost:4000/mcp
A inspeção completa e as chamadas manuais de ferramentas faremos na próxima aula via MCP Inspector / MCP Jam. Mas, mesmo agora, dá para fazer um smoke test super simples via curl:
curl -X POST http://localhost:4000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Este curl é meramente um smoke test opcional para quem gosta de olhar as respostas JSON “cruas”. No desenvolvimento real, você quase sempre conversa com o servidor MCP via SDK, e não montando requisições JSON‑RPC na mão.
O nome exato do método depende da versão do protocolo e do SDK, mas a ideia é que você receba uma lista JSON onde, entre as tools, apareça suggest_gifts. Se o método não coincidir — sem drama: o objetivo da aula não é decorar todos os nomes, e sim que você não tenha medo de olhar para respostas JSON e entenda sua estrutura, graças às aulas anteriores.
11. Conexão com nosso ChatGPT App e próximos passos
Por enquanto, o servidor MCP vive por conta própria. Nos próximos módulos você vai:
- conectá-lo ao MCP Inspector e aprender a depurar tools/resources/prompts separadamente, sem mexer no ChatGPT;
- configurar o ChatGPT App para que ele enxergue este servidor MCP como fonte de ferramentas;
- migrar parte da lógica que antes ficava dentro do Apps SDK (por exemplo, via tools embutidos) para a camada MCP;
- adicionar autorização, logging, cenários de streaming — já sobre o esqueleto pronto.
Neste momento, o importante é que:
- você tenha um serviço separado, responsável pelas “habilidades” e “dados” do app;
- esse serviço fale com clientes via o padrão MCP, e não por um REST customizado;
- você já saiba registrar manualmente ferramentas, recursos e prompts, sem temer o protocolo.
12. Um pouco sobre estrutura de código e boas práticas
Mesmo em um exemplo pequeno, dá para criar bons hábitos.
Primeiro, mantenha a configuração do servidor separada. Tudo o que diz respeito a nome, versão, logging, ajustes de transporte (porta, caminho /mcp) pode ir para um módulo pequeno config.ts. Depois, quando for fazer deploy na Vercel ou atrás de um MCP gateway, você vai adicionar variáveis de ambiente e vai se agradecer por isso.
Segundo, tente manter os métodos registerTool/registerResource/registerPrompt o mais “finos” possível. Descrições de esquemas, textos e lógica de negócio — itens que ficam ótimos em arquivos separados:
- gifts.ts — funções de seleção de presentes;
- catalog.ts — trabalho com o catálogo de produtos;
- prompts.ts — conjunto de prompts.
O próprio server.ts vira uma espécie de “provedor MCP”, que apenas cola tudo.
Terceiro, lembre que o servidor MCP é, por natureza, reativo: ele espera conexões de clientes e suas requisições. Isso significa que operações bloqueantes ou longas demais dentro das ferramentas vão impactar diretamente o UX no ChatGPT. Nos próximos módulos vamos falar de timeouts, operações assíncronas e respostas em streaming, mas já agora pense em quais operações podem ir para background e quais devem responder rápido.
Insight: o ChatGPT suporta apenas parte do MCP
É importante entender: os ChatGPT Apps usam o MCP como transporte e formato, mas não são um cliente MCP completo. Se você ler apenas o protocolo, é fácil criar expectativas erradas sobre como tudo funcionará em runtime.
O que o MCP “puro” promete:
- recursos (resources) podem ser lidos de forma dinâmica, sob demanda do cliente, e não uma única vez para sempre;
- o servidor pode enviar notificações resourceChanged/toolChanged e assim “empurrar” atualizações sem reiniciar o cliente;
- é possível construir um sistema bastante flexível, em que o conjunto de tools/resources/prompts é gerenciado por configs ou estado externo.
No contexto dos ChatGPT Apps isso não ocorre. Para o aplicativo, a figura é bem mais estática:
- ao registrar o App, o ChatGPT lê uma vez a descrição de todas as tools e resources;
- depois, essa configuração basicamente é cacheada como parte da versão do app;
- atualizações dinâmicas via notificações do MCP não são suportadas — a plataforma simplesmente as ignora.
13. Erros comuns ao escrever o primeiro servidor MCP
Erro nº 1: Colocar toda a lógica de negócio direto no registerTool.
A tentação de “escrever rapidinho tudo no handler da ferramenta” é grande, especialmente em um exemplo didático. Mas depois isso vira um monstro ilegível, misturando validação, acesso a BD e formatação de resposta. É melhor extrair desde já as funções de negócio (suggestGifts, trabalho com catálogo) para módulos separados, e no handler fazer apenas a “orquestração”.
Erro nº 2: Prender-se rigidamente a nomes específicos de métodos JSON do MCP.
Às vezes estudantes começam a escrever if (method === "tools/list") e a fazer parsing do JSON na mão. Não precisa: esse é o trabalho do SDK. A especificação do MCP e os nomes de métodos podem evoluir, e o SDK cuida disso. Use registerTool, registerResource, registerPrompt e deixe a biblioteca decidir como isso se parece no JSON‑RPC.
Erro nº 3: Não considerar o transporte e tentar usar um servidor stdio com o ChatGPT.
O transporte stdio é perfeito para clientes locais, como ambientes desktop, onde o cliente pode iniciar o servidor como subprocesso. Mas o ChatGPT se comunica via HTTPS e precisa de um endpoint HTTP/stream. Tentar “dar um jeito” de passar stdio por um túnel dá dor de cabeça. Para o ChatGPT App, faça logo o transporte HTTP (Streamable HTTP).
Erro nº 4: Ignorar MIME types e a estrutura dos recursos.
Nos recursos, importa não apenas o conteúdo, mas também o tipo (mimeType) e o URI. Se você usar text/plain em tudo e jogar strings JSON sem pensar, os clientes (e inspectors) terão mais dificuldade para entender que dados são esses. Procure informar MIME types corretos (application/json, text/html para templates de UI etc.) e URIs estáveis.
Erro nº 5: Usar o servidor MCP como um “HTTP API aleatório”.
Às vezes bate a tentação: “Já que tenho o Express, vou pendurar também /api/whatever e bater direto lá”. Misturar o endpoint do MCP com um REST qualquer não é uma boa: complica configuração, roteamento e segurança. É melhor ter um contrato claro: /mcp para MCP, caminhos separados para outras necessidades, ou até outro serviço. Em produção isso é especialmente importante para configurar gateways e autorização. Ou seja, não transforme o servidor MCP em um “HTTP API aleatório” — um conjunto de rotas sem relação com o contrato do MCP.
Erro nº 6: Não fazer logging das mensagens de MCP de entrada e saída.
Sem logs, o servidor MCP vira uma caixa-preta: “algo não funciona, mas não sei o quê”. Já no primeiro servidor, vale a pena escrever pelo menos no stderr logs estruturados compactos: método da ferramenta, status, tempo de execução. O principal é não logar dados sensíveis e tokens — vamos falar disso mais adiante, quando chegarmos à segurança.
Erro nº 7: Tentar depurar tudo de uma vez via ChatGPT, sem um inspector.
Cena comum: o aluno escreve um servidor MCP, já conecta ao ChatGPT App e “tudo quebra misteriosamente”. Enquanto isso, o inspector nunca foi sequer aberto. Fica difícil entender se o problema está no protocolo, no servidor, no Apps SDK ou no comportamento do modelo. O caminho certo é primeiro garantir que o servidor MCP funciona corretamente em isolamento (via MCP Jam / Inspector), e só então conectá-lo ao app.
GO TO FULL VERSION