Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Implementar as 3 primitivas do MCP em um server TypeScript: tools, resources e prompts
Adicionar uma nova tool a um server existente seguindo o padrão de handlers duplos (ListTools + CallTool)
Estruturar um server MCP de produção com separação clara de responsabilidades
Testar um server MCP usando InMemoryTransport e client de linha de comando
Entender o ciclo completo de uma tool call: client → server → execução → resultado
Por que isso importa
As Unidades 02 e 03 mostraram como consumir tools via MCP. Esta unidade te ensina a criar o server — o outro lado da equação.
Um servidor MCP bem projetado é a peça que permite que qualquer equipe consuma seus dados e operações via qualquer cliente LLM. Você implementa uma vez e o Claude Desktop, Cursor, Windsurf, e seu próprio agente podem todos usar sem nenhuma mudança no server.
O template que você vai criar aqui — server com tools + resources + prompts — é o template de referência para todos os servers que você criar no futuro. As 3 primitivas cobrem 95% dos casos de uso de integração LLM.
Conceitos Fundamentais
Estrutura de um MCP Server TypeScript
Todo server MCP em TypeScript tem a mesma estrutura:
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import {
ListToolsRequestSchema, // GET /tools/list
CallToolRequestSchema, // POST /tools/call
ListResourcesRequestSchema, // GET /resources/list
ReadResourceRequestSchema, // GET /resources/read
ListPromptsRequestSchema, // GET /prompts/list
GetPromptRequestSchema, // GET /prompts/get
} from '@modelcontextprotocol/sdk/types.js'
// 1. Declara capabilities que o server suporta
const server = new Server(
{ name: 'meu-server', version: '1.0.0' },
{
capabilities: {
tools: {}, // declara suporte a tools
resources: {}, // declara suporte a resources
prompts: {}, // declara suporte a prompts
}
}
)
// 2. Registra handler para LISTAR tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [/* array de ToolDefinition */]
}))
// 3. Registra handler para EXECUTAR tools
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
// executa a tool e retorna resultado
return { content: [{ type: 'text', text: 'resultado' }] }
})
// Idem para resources e prompts...
// 4. Conecta ao transport
await server.connect(transport)Por que dois handlers por primitiva? O protocolo MCP separa “descoberta” (o que existe?) de “execução” (faça isso). Isso permite que clients façam discovery antes de decidir o que usar.
Tools: Ações com Schema
Uma tool definition tem 3 campos obrigatórios:
{
name: 'criar_tarefa', // identificador único no server
description: 'Cria uma nova tarefa. Use quando o usuário pedir para adicionar, criar ou registrar uma tarefa.', // para o LLM decidir quando usar
inputSchema: { // JSON Schema dos parâmetros
type: 'object',
properties: {
titulo: {
type: 'string',
description: 'Título curto e descritivo da tarefa',
},
prioridade: {
type: 'string',
enum: ['baixa', 'media', 'alta'],
description: 'Nível de prioridade da tarefa',
},
},
required: ['titulo', 'prioridade'], // campos obrigatórios
},
}O inputSchema segue o JSON Schema draft 7. Os campos mais usados: - type: ‘string’, ‘number’, ‘boolean’, ‘object’, ‘array’ - enum: lista de valores válidos - description: descrição do campo para o LLM - required: array de nomes de campos obrigatórios - minimum/maximum: limites numéricos - minLength/maxLength: limites de string
Resources: Dados para Leitura
Resources são URIs que o host pode buscar sob demanda. Úteis para documentação, configurações, schemas:
// Listar resources disponíveis
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'tarefas://lista', // URI identificador
name: 'Lista de tarefas',
description: 'Todas as tarefas cadastradas em JSON',
mimeType: 'application/json', // tipo do conteúdo
},
{
uri: 'tarefas://resumo',
name: 'Resumo de tarefas',
description: 'Contagem por status e prioridade',
mimeType: 'text/plain',
},
]
}))
// Ler o conteúdo de um resource
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
const uri = req.params.uri
if (uri === 'tarefas://lista') {
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(tarefas, null, 2),
}]
}
}
throw new Error(`Resource não encontrado: ${uri}`)
})Prompts: Templates Reutilizáveis
Prompts no MCP são templates que o host pode buscar e usar:
// Listar prompts disponíveis
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [
{
name: 'resumo_diario',
description: 'Gera um prompt para resumir as tarefas do dia',
arguments: [
{ name: 'data', description: 'Data no formato DD/MM/AAAA', required: false },
],
},
]
}))
// Retornar o conteúdo de um prompt específico
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
const { name, arguments: args } = req.params
if (name === 'resumo_diario') {
const data = args?.data ?? new Date().toLocaleDateString('pt-BR')
const tarefasPendentes = tarefas.filter(t => t.status === 'pendente')
return {
description: 'Prompt para resumo diário de tarefas',
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Resuma as tarefas pendentes para ${data}:\n\n${JSON.stringify(tarefasPendentes, null, 2)}`,
}
}
]
}
}
throw new Error(`Prompt não encontrado: ${name}`)
})Retorno de Erro em Tools
Quando uma tool falha, retorne com isError: true:
// Erro recuperável — o LLM pode tentar novamente ou adaptar
return {
content: [{ type: 'text', text: `Tarefa ${id} não encontrada` }],
isError: true,
}
// Sucesso
return {
content: [{ type: 'text', text: JSON.stringify(tarefa) }],
}Com isError: true, o LLM vê o erro como resultado da tool e pode ajustar sua próxima ação — por exemplo, listar as tarefas disponíveis antes de tentar buscar uma específica.
Aprofundamento Técnico
Adicionando Uma Nova Tool: O Padrão
Adicionar uma nova tool requer dois passos no mesmo server:
Passo 1: Declarar no ListTools handler
// No handler ListToolsRequestSchema, adicione à lista:
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
// ... tools existentes ...
{
name: 'atualizar_status', // NOVA TOOL
description: 'Atualiza o status de uma tarefa existente',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'ID da tarefa' },
status: {
type: 'string',
enum: ['pendente', 'em_progresso', 'concluida'],
description: 'Novo status da tarefa',
},
},
required: ['id', 'status'],
},
},
],
}))Passo 2: Implementar no CallTool handler
// No handler CallToolRequestSchema, adicione o case:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
// ... cases existentes ...
if (name === 'atualizar_status') { // NOVA IMPLEMENTAÇÃO
const { id, status } = args as { id: number; status: Tarefa['status'] }
const tarefa = tarefas.find(t => t.id === id)
if (!tarefa) {
return {
content: [{ type: 'text', text: `Tarefa ${id} não encontrada` }],
isError: true,
}
}
const statusAnterior = tarefa.status
tarefa.status = status
return {
content: [{
type: 'text',
text: JSON.stringify({
sucesso: true,
id,
status_anterior: statusAnterior,
status_novo: status,
}),
}],
}
}
throw new Error(`Tool desconhecida: ${name}`)
})A regra: se você declarou a tool no ListTools mas esqueceu de implementar no CallTool, o LLM tentará chamar e receberá throw new Error('Tool desconhecida') — que aparece como erro no tool_result.
Tipagem TypeScript para Args
O args em CallTool é Record<string, unknown> por padrão. Para typagem segura:
// UNSAFE: sem validação de tipo
const { id, status } = args as { id: number; status: string }
// SAFE com Zod:
import { z } from 'zod'
const AtualizarStatusSchema = z.object({
id: z.number().int().positive(),
status: z.enum(['pendente', 'em_progresso', 'concluida']),
})
// No handler:
const { id, status } = AtualizarStatusSchema.parse(args)
// Lança ZodError se args não bater com o schema — erro detalhadoUsar Zod é recomendado para servers de produção — garante que mesmo se o LLM enviar args malformados, você recebe um erro descritivo em vez de comportamento inesperado.
Estado em Memória vs Persistência
O server do starter usa arrays em memória (const tarefas: Tarefa[] = []). Isso é simples para aprender, mas tem limitações:
Estado em memória: - Reiniciar o server limpa todos os dados - Não compartilha estado entre múltiplas instâncias do server - Adequado para: demos, labs, state efêmero
Persistência real: para produção, use banco de dados:
// Substitua arrays por queries ao banco
import { db } from './db'
// Em vez de: tarefas.push(novaTarefa)
await db.query('INSERT INTO tarefas (titulo, prioridade) VALUES (?, ?)', [titulo, prioridade])
// Em vez de: tarefas.find(t => t.id === id)
const tarefa = await db.query('SELECT * FROM tarefas WHERE id = ?', [id])Exemplos Anotados
Exemplo 1: Server Completo de Tarefas
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import {
CallToolRequestSchema, GetPromptRequestSchema,
ListPromptsRequestSchema, ListResourcesRequestSchema,
ListToolsRequestSchema, ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
interface Tarefa {
id: number
titulo: string
prioridade: 'baixa' | 'media' | 'alta'
status: 'pendente' | 'em_progresso' | 'concluida'
}
let nextId = 1
const tarefas: Tarefa[] = []
function criarTarefasServer(): Server {
const server = new Server(
{ name: 'gerenciador-tarefas', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
)
// === TOOLS ===
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'criar_tarefa',
description: 'Cria uma nova tarefa na lista',
inputSchema: {
type: 'object',
properties: {
titulo: { type: 'string' },
prioridade: { type: 'string', enum: ['baixa', 'media', 'alta'] },
},
required: ['titulo', 'prioridade'],
},
},
{
name: 'listar_tarefas',
description: 'Lista tarefas, opcionalmente filtradas por status',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', enum: ['pendente', 'em_progresso', 'concluida', 'todas'] },
},
},
},
// TODO 1: Adicione a tool 'atualizar_status' aqui
// campos: id (number), status ('pendente'|'em_progresso'|'concluida')
],
}))
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params
if (name === 'criar_tarefa') {
const { titulo, prioridade } = args as { titulo: string; prioridade: Tarefa['prioridade'] }
const nova: Tarefa = { id: nextId++, titulo, prioridade, status: 'pendente' }
tarefas.push(nova)
return { content: [{ type: 'text', text: JSON.stringify(nova) }] }
}
if (name === 'listar_tarefas') {
const { status = 'todas' } = (args as { status?: string }) ?? {}
const filtradas = status === 'todas' ? tarefas : tarefas.filter(t => t.status === status)
return {
content: [{ type: 'text', text: JSON.stringify({ total: filtradas.length, tarefas: filtradas }) }]
}
}
// TODO 2: Implemente o handler para 'atualizar_status'
// - Encontra a tarefa pelo id em `tarefas`
// - Se não encontrar: retorna isError: true
// - Se encontrar: atualiza tarefa.status e retorna sucesso com status anterior e novo
throw new Error(`Tool desconhecida: ${name}`)
})
// === RESOURCES ===
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [{
uri: 'tarefas://todas',
name: 'Todas as tarefas',
mimeType: 'application/json',
}],
}))
server.setRequestHandler(ReadResourceRequestSchema, async (req) => ({
contents: [{
uri: req.params.uri,
mimeType: 'application/json',
text: JSON.stringify(tarefas, null, 2),
}]
}))
// === PROMPTS ===
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [{
name: 'planejar_dia',
description: 'Cria prompt para planejar tarefas do dia',
arguments: [],
}],
}))
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
if (req.params.name !== 'planejar_dia') throw new Error('Prompt não encontrado')
const pendentes = tarefas.filter(t => t.status === 'pendente')
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: `Tenho ${pendentes.length} tarefas pendentes. Quais devo priorizar hoje?\n\n${JSON.stringify(pendentes, null, 2)}`,
}
}]
}
})
return server
}
// Teste via client
async function testar() {
const server = criarTarefasServer()
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: {} })
const [ct, st] = InMemoryTransport.createLinkedPair()
await server.connect(st)
await client.connect(ct)
// Cria tarefas
await client.callTool({ name: 'criar_tarefa', arguments: { titulo: 'Estudar MCP', prioridade: 'alta' } })
await client.callTool({ name: 'criar_tarefa', arguments: { titulo: 'Revisar PR', prioridade: 'media' } })
// Lista todas
const lista = await client.callTool({ name: 'listar_tarefas', arguments: { status: 'todas' } })
console.log('Tarefas:', lista.content[0].text)
// TODO: Testa atualizar_status quando implementada
// await client.callTool({ name: 'atualizar_status', arguments: { id: 1, status: 'em_progresso' } })
// Resources
const resource = await client.readResource({ uri: 'tarefas://todas' })
console.log('Resource:', resource.contents[0].text)
// Prompts
const prompt = await client.getPrompt({ name: 'planejar_dia', arguments: {} })
console.log('Prompt:', prompt.messages[0].content.text)
}
testar().catch(console.error)Padrões e Armadilhas
Padrões
Padrão 1: Declare capability antes de registrar handler
// ERRADO: sem declarar resources na capability mas registrando handler
const server = new Server({ name: '...' }, { capabilities: { tools: {} } }) // sem resources!
server.setRequestHandler(ListResourcesRequestSchema, ...) // handler registrado mas client não sabe que existePadrão 2: Tools para mutação, Resources para leitura frequente sem args
Tool 'listar_tarefas' com args: quando você precisa filtrar/ordenar
Resource 'tarefas://todas': quando o client só quer todos os dados, sem filtrosSe a leitura precisa de parâmetros, use tool. Se é sempre o mesmo dado, use resource.
Padrão 3: Sempre implemente o case de erro em CallTool
// Ao final do handler, sempre lance erro para tools desconhecidas
// (não retorne silenciosamente)
throw new Error(`Tool desconhecida: ${name}`)Isso garante que erros de nome de tool são detectados imediatamente, não silenciados.
Armadilhas
⚠️ Armadilha 1: Declarar tool no ListTools mas não implementar no CallTool O LLM vai chamar a tool, o server vai lançar throw new Error('Tool desconhecida'), e o LLM vai receber um tool_result de erro. Diagnóstico: você declarou mas não implementou. Sem exceção, os dois handlers devem ficar em sincronia.
⚠️ Armadilha 2: args como any sem validação
// UNSAFE: se LLM enviar id como string, crash em runtime
const { id, status } = args as { id: number; status: string }
tarefas.find(t => t.id === id) // undefined se id for string "1" vs number 1
// SAFE: converta explicitamente
const id = Number(args.id)
if (isNaN(id)) return { content: [{ type: 'text', text: 'ID inválido' }], isError: true }Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
O starter tem 2 TODOs que completam a tool atualizar_status:
TODO 1 — No ListToolsRequestSchema handler: adicione a definição da tool atualizar_status à lista de tools. O inputSchema deve ter dois campos obrigatórios: id (number, “ID da tarefa a atualizar”) e status (string com enum dos 3 valores possíveis). Seção de referência: Conceitos Fundamentais → “Tools: Ações com Schema”.
TODO 2 — No CallToolRequestSchema handler: adicione o case if (name === 'atualizar_status'). Cast os args, encontre a tarefa com tarefas.find(t => t.id === id), retorne isError: true se não encontrar, caso contrário atualize tarefa.status e retorne JSON com {sucesso: true, id, status_anterior, status_novo}. Seção de referência: Aprofundamento Técnico → “Adicionando Uma Nova Tool: O Padrão”.
O starter já tem um comentário // TODO: Teste a tool atualizar_status quando implementada no main — descomente para testá-la após implementar.
Agora você está pronto para o lab.