Como criar um servidor MCP local e conectá-lo ao Claude (com código funcional)
Crie um servidor MCP local a partir de uma pasta vazia: instale o SDK do TypeScript, registre duas ferramentas que funcionam, teste-as no MCP Inspector e depois conecte o servidor ao Claude Desktop e ao Claude Code. Inclui arquivos de configuração, correções de caminhos no Windows e uma lista de verificação para as falhas que quebram a maioria das configurações.
A maioria dos tutoriais de MCP para no "hello world" e deixa você olhando para um selo vermelho Disconnected. Este termina com um servidor rodando na sua própria máquina, o Claude chamando as ferramentas dele e uma lista de verificação curta para as falhas que atrapalham a maioria das pessoas. Você vai escrever cerca de 70 linhas de TypeScript, testá-las em um inspetor no navegador e conectar o resultado tanto ao Claude Desktop quanto ao Claude Code.
O servidor é uma pequena ferramenta de anotações: o Claude pode salvar uma nota em um arquivo JSON no seu disco e pesquisá-la depois. Ele é propositalmente simples, porque a infraestrutura é a mesma seja qual for o trabalho das suas ferramentas, seja ler um arquivo de notas, consultar um banco de dados ou chamar um modelo de imagem. Compilei e executei o arquivo do servidor abaixo com a versão 1.32 do SDK do TypeScript, então o código compila exatamente como mostrado.
O que você está realmente construindo
MCP em dois parágrafos
O Model Context Protocol (MCP) é um padrão aberto, apresentado pela Anthropic em novembro de 2024, que permite a um aplicativo de IA conversar com ferramentas externas de um jeito único e consistente. Em vez de cada aplicativo inventar seu próprio formato de plugin, um servidor MCP expõe capacidades, e um cliente MCP, como o Claude Desktop ou o Claude Code, as encontra e as chama. As mensagens são JSON-RPC 2.0 simples, então um servidor pode ser escrito em qualquer linguagem.
Um servidor pode oferecer três tipos de coisas:
Tools (ferramentas): funções que o modelo pode chamar, como "salvar uma nota" ou "executar uma consulta".
Resources (recursos): dados somente leitura que o aplicativo pode carregar como contexto, como um arquivo ou um registro de banco de dados.
Prompts: modelos de prompt reutilizáveis que o usuário aciona de propósito.
Este tutorial se atém a ferramentas, porque são as mais simples de testar e as mais úteis já no primeiro dia.
Por que rodar localmente
Um servidor local roda como um processo filho do cliente, na sua máquina, com seus arquivos e suas permissões. Nada fica exposto à internet, não há conta de hospedagem e a iteração é rápida: edite um arquivo, recompile, reinicie. O transporte usado é o stdio: o cliente inicia seu programa e se comunica com ele pela entrada e saída padrão.
stdio (local)
Streamable HTTP (remoto)
Onde roda
Processo filho no seu computador
Um servidor web que você ou outra pessoa hospeda
Quem pode acessar
Somente o aplicativo que o iniciou
Qualquer um com a URL e as credenciais
Autenticação
Nenhuma, ele herda sua conta de usuário
Obrigatória (OAuth ou tokens)
Ideal para
Ferramentas pessoais, acesso a arquivos, desenvolvimento
Ferramentas compartilhadas de equipe, integrações SaaS
💡 Bom saber: O Streamable HTTP substituiu o antigo transporte HTTP+SSE na revisão 2025-03-26 da especificação. E, como um navegador não consegue iniciar um processo no seu computador, um servidor stdio não pode ser adicionado ao claude.ai no navegador. Só servidores remotos funcionam ali.
Prepare o projeto
O que você precisa ter instalado
Ferramenta
Versão
Verifique com
Node.js
20 LTS ou mais recente
node --version
npm
Vem com o Node
npm --version
Claude Desktop
Mais recente, macOS ou Windows
Configurações, depois Developer
Claude Code (opcional)
Mais recente
claude --version
O Claude Desktop está disponível para macOS e Windows. No Linux, use a alternativa do Claude Code na seção de conexão abaixo; o servidor em si é idêntico.
⚠️ Atenção: O TypeScript 7, a versão que o npm instala hoje, não carrega mais pacotes @types sozinho. Sem a linha "types": ["node"], você recebe Cannot find name 'process' e erros semelhantes em todo import do Node.
Escreva suas primeiras duas ferramentas
O arquivo completo do servidor
Salve isto como src/index.ts. Ele expõe save_note e search_notes, e guarda tudo em um único arquivo JSON.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
const NOTES_DIR = process.env.NOTES_DIR ?? path.join(os.homedir(), "mcp-notes");
const NOTES_FILE = path.join(NOTES_DIR, "notes.json");
type Note = { id: number; title: string; body: string; createdAt: string };
async function readNotes(): Promise<Note[]> {
try {
return JSON.parse(await fs.readFile(NOTES_FILE, "utf8"));
} catch {
return [];
}
}
const server = new McpServer({ name: "local-notes", version: "1.0.0" });
server.registerTool(
"save_note",
{
title: "Save note",
description: "Save a short note with a title and a body to the local notes file.",
inputSchema: {
title: z.string().min(1).max(120).describe("Short title for the note"),
body: z.string().min(1).describe("The text of the note"),
},
},
async ({ title, body }) => {
const notes = await readNotes();
const note: Note = {
id: notes.length + 1,
title,
body,
createdAt: new Date().toISOString(),
};
await fs.mkdir(NOTES_DIR, { recursive: true });
await fs.writeFile(NOTES_FILE, JSON.stringify([...notes, note], null, 2));
return { content: [{ type: "text", text: `Saved note #${note.id}: ${title}` }] };
}
);
server.registerTool(
"search_notes",
{
title: "Search notes",
description: "Find saved notes whose title or body contains a word or phrase.",
inputSchema: {
query: z.string().min(1).describe("Word or phrase to look for"),
},
},
async ({ query }) => {
const q = query.toLowerCase();
const hits = (await readNotes()).filter((n) =>
`${n.title} ${n.body}`.toLowerCase().includes(q)
);
if (hits.length === 0) {
return { content: [{ type: "text", text: `No notes match "${query}".` }] };
}
const text = hits.map((n) => `#${n.id} ${n.title}\n${n.body}`).join("\n\n");
return { content: [{ type: "text", text }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("local-notes MCP server running on stdio");
O que cada parte faz
McpServer é a classe de alto nível. O name e a version que você passa aparecem na lista de servidores e nos logs do cliente.
registerTool recebe um nome de ferramenta, um objeto de configuração (title, description, inputSchema) e um handler assíncrono.
O schema Zod é validado antes de o seu handler rodar e depois convertido em JSON Schema para que o Claude possa lê-lo. Cada string .describe() chega ao modelo.
O valor de retorno é sempre { content: [...] }. Texto simples é o tipo de conteúdo mais simples; imagens e links de recursos também são aceitos.
StdioServerTransport lê as requisições do stdin e escreve as respostas no stdout.
💡 Dica: Escreva as descrições para o modelo, não para humanos. "Encontre notas salvas cujo título ou corpo contenha uma palavra ou frase" diz ao Claude quando chamar a ferramenta. "Pesquisar" não diz. O Claude escolhe as ferramentas principalmente pelos nomes e descrições.
Como search_notes nunca altera nada, marque isso na configuração dela: annotations: { readOnlyHint: true }. As dicas são apenas orientativas, e os clientes decidem por conta própria o quanto confiar nelas, mas elas permitem que clientes bem-comportados tratem ferramentas somente leitura com mais leveza.
Nunca imprima no stdout
Com stdio, o stdout é o canal do protocolo. Um único console.log("started") esquecido envia uma linha que não é JSON para o fluxo, e a maioria dos clientes vai derrubar a conexão ou mostrar o servidor como falho. Lembre-se desta regra e você evita a falha mais comum do primeiro dia:
Faça:console.error("message"), que escreve no stderr, onde os clientes coletam os logs.
Não faça:console.log(...) ou process.stdout.write(...) em qualquer parte do servidor, inclusive dentro de bibliotecas que você importa.
Teste antes que o Claude teste
Rode o MCP Inspector
O MCP Inspector é a ferramenta oficial de depuração. Ele inicia o seu servidor do mesmo jeito que um cliente faria e oferece botões no lugar de prompts.
npm run build
npx @modelcontextprotocol/inspector node build/index.js
Uma página abre no navegador. Clique em Connect, abra a aba Tools e pressione List Tools. Você deve ver save_note e search_notes com seus schemas. Execute save_note com um título e um corpo, depois execute search_notes com uma palavra desse corpo. A primeira chamada retorna Saved note #1: Standup, e o arquivo de notas aparece em uma pasta mcp-notes dentro do seu diretório pessoal (ou em NOTES_DIR, se você tiver definido outro local).
Envie mensagens JSON-RPC brutas
Se você quiser ver o próprio protocolo, o stdio usa uma mensagem JSON por linha. Coloque estas três linhas em requests.jsonl:
A segunda resposta lista as duas ferramentas com seus JSON Schemas. Essa troca é tudo o que o Claude faz ao se conectar: handshake, listar ferramentas, chamar ferramentas.
Conecte ao Claude
Edite a configuração do Claude Desktop
No Claude Desktop, abra Configurações, depois Developer, depois Edit Config. Isso revela claude_desktop_config.json:
Adicione o seu servidor em mcpServers. Use caminhos absolutos, porque o Claude Desktop inicia o processo a partir do próprio diretório de trabalho, não da pasta do projeto.
No macOS, o caminho fica parecido com /Users/you/projects/local-notes-mcp/build/index.js. Salve o arquivo, depois feche completamente o Claude Desktop (no Windows, pela bandeja do sistema, não apenas pelo botão de fechar da janela) e abra-o de novo. Suas ferramentas aparecem no menu de ferramentas do campo de entrada do chat, e o Claude pede permissão antes de executar qualquer uma delas.
Adicione ao Claude Code
O Claude Code não exige edição de arquivos. Um único comando registra o servidor:
Tudo depois do duplo hífen é o comando que inicia o seu servidor. Verifique com claude mcp list, ou digite /mcp dentro de uma sessão para ver o status. Uma flag de escopo decide quem recebe o servidor:
Escopo
Armazenado em
Quem vê
local (padrão)
Suas configurações privadas para este projeto
Somente você, neste projeto
project (--scope project)
.mcp.json no repositório
Todos que clonarem, depois de aprovarem
user (--scope user)
Sua configuração de usuário
Somente você, em todos os projetos
Experimente um prompt real
Peça ao Claude algo que force uma chamada de ferramenta:
Salve uma nota com o título "Standup" que diz "Publicar o post sobre MCP na sexta". Depois pesquise nas minhas notas por "sexta".
O Claude chama save_note, depois search_notes, e devolve o resultado citado. Abra notes.json para confirmar que os dados foram gravados no seu disco. Se foram, você tem um servidor MCP local funcionando.
Corrija falhas e proteja o servidor
Corrija as falhas comuns
Sintoma
Causa provável
Correção
Servidor aparece como falho ou desconectado
Saída no stdout, ou um erro ao iniciar
Execute node build/index.js manualmente e leia o stderr; remova todo console.log
Nenhuma ferramenta após editar a configuração
Cliente ainda aberto, ou JSON inválido
Feche completamente; procure vírgulas sobrando
spawn node ENOENT
O aplicativo não encontra node no seu PATH
Use o caminho absoluto para o binário node como command
Funciona no Inspector, falha no Claude
Caminhos relativos ou variáveis de ambiente ausentes
Caminhos absolutos, e coloque as variáveis em env
Alterações no código não têm efeito
Você não recompilou ou reiniciou
Execute npm run build e depois reinicie o cliente
Dois detalhes do Windows causam metade dos problemas restantes. Barras invertidas dentro de strings JSON precisam ser duplicadas (C:\\Users\\you\\...), ou você pode simplesmente usar barras normais, como no exemplo acima. E, no Windows nativo, servidores iniciados por npx geralmente precisam de um wrapper cmd /c em command; um comando node simples não funciona.
Quando algo ainda falhar, leia os logs. O Claude Desktop grava um log por servidor, em ~/Library/Logs/Claude no macOS e em %APPDATA%\Claude\logs no Windows. Suas próprias linhas console.error acabam ali.
Padrões seguros que vale manter
Um servidor stdio herda as suas permissões, então trate cada ferramenta como código que pode agir em seu nome.
Limite o raio de impacto. Mantenha o acesso a arquivos dentro de uma única pasta. Se uma ferramenta aceitar um caminho, resolva-o e rejeite qualquer coisa fora do diretório permitido.
Valide cada entrada. As regras min, max e enum do Zod não custam nada e bloqueiam chamadas malformadas antes de o seu handler rodar.
Mantenha segredos fora do código. Coloque os tokens no bloco env da sua configuração, e mantenha esse arquivo fora do controle de versão.
Leia antes de instalar. Adicione apenas servidores de terceiros cujo código-fonte você tenha examinado. Eles rodam com o acesso da sua conta.
Trate a saída das ferramentas como não confiável. Textos que sua ferramenta busca em páginas web ou e-mails podem conter instruções direcionadas ao modelo. Devolva-os como dados e mantenha as ações de escrita atrás de confirmação.
Migrando do stdio para HTTP
Quando colegas de equipe precisarem das mesmas ferramentas, troque o transporte. O SDK traz StreamableHTTPServerTransport, que serve as mesmas McpServer via HTTP atrás da sua própria autenticação. Suas chamadas registerTool não mudam. Só o transporte e o registro no cliente mudam, por exemplo claude mcp add --transport http notes https://your-host/mcp.
Você já pode ver esse padrão na prática. O conector PicassoIA no claude.ai é um servidor MCP remoto que lista geração de imagens, edição de imagens e geração de vídeo como ferramentas, e o Claude as chama sem nenhum processo local.
Rascunhe especificações de ferramentas no PicassoIA
Boas ferramentas começam com bons nomes e boas descrições, e um modelo de linguagem é uma forma rápida de rascunhá-las antes de escrever código. O PicassoIA hospeda 75 modelos de texto na categoria Large Language Models, incluindo o Claude Sonnet 5, que foi feito para tarefas de programação. Veja como usá-lo para desenhar ferramentas.
Descreva a ferramenta em linguagem simples: o que ela faz, o que recebe, o que devolve e se altera alguma coisa.
Peça um formato de saída fixo: um nome de ferramenta em snake_case, uma descrição de no máximo duas frases escrita para um modelo, um schema Zod com .describe() em cada campo e três casos-limite que devem falhar na validação.
Cole o resultado em uma chamada registerTool, recompile e teste no Inspector.
Ajuste a descrição, não o schema, quando o Claude escolher a ferramenta errada. Geralmente o problema é o texto.
Um prompt que funciona bem:
Estou criando uma ferramenta MCP chamada list_overdue_tasks. Ela lê o tasks.json, retorna as tarefas cujo dueDate é anterior a hoje e não altera nada. Escreva o nome da ferramenta, uma descrição de duas frases para um modelo de IA, um schema de entrada Zod com um describe() em cada campo e três entradas inválidas que ela deve rejeitar.
Para refatorações maiores, como dividir um servidor de 600 linhas em módulos, experimente o Claude Fable 5 ou o Claude Opus 4.7 com o seu arquivo inteiro colado.
Crie sua primeira imagem no PicassoIA
Seu servidor de notas é um modelo. Troque o arquivo JSON por uma chamada a um modelo de imagem, e o Claude pode renderizar imagens sob demanda. Você não precisa construir isso primeiro para ver o resultado. Cada foto deste artigo foi gerada com o P-Image, um dos modelos de texto para imagem no Picasso IA.
Abra o Picasso IA, digite uma frase descrevendo uma cena e gere. Depois teste três experimentos:
Mude a lente. Reescreva o mesmo prompt com "35mm" e depois com "85mm" e compare o enquadramento.
Mude a luz. Troque "morning window light" por "warm desk lamp" e veja o clima mudar.
Mude o ângulo. Peça uma vista de cima e depois uma vista em ângulo baixo do mesmo assunto.
Crie uma ferramenta que você gostaria que o Claude tivesse, conecte-a com os passos acima e depois passe dez minutos no Picasso IA criando as imagens do seu projeto. O servidor leva uma tarde. As imagens levam segundos.