Construindo um Servidor MCP com o SDK TypeScript
O SDK oficial TypeScript MCP espelha a estrutura do SDK Python: registre manipuladores de ferramentas e recursos em um objeto de servidor e, em seguida, execute-o através de um transporte.
Se o seu serviço já existe em uma base de código Node, este SDK permite que você o exponha ao Claude sem introduzir uma segunda linguagem.
Resumo
Um servidor MCP TypeScript é construído em torno de McpServer, a classe de servidor de alto nível do SDK.
Você registra capacidades chamando server.tool(...) e server.resource(...) diretamente, em vez de usar decoradores.
Esquemas Zod definem a forma de entrada de cada ferramenta e servem como validação em tempo de execução.
O servidor se conecta a um transporte, mais comumente StdioServerTransport para desenvolvimento local, da mesma forma que o SDK Python faz.
Esta página espelha a profundidade do artigo do SDK Python: argumentos tipados, tratamento de erros e recursos parametrizados, expressos em TypeScript idiomático.
Receita
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "tickets-server", version: "1.0.0" });
server.tool(
"create_ticket",
{ title: z.string(), priority: z.enum(["low", "normal", "high"]).default("normal") },
async ({ title, priority }) => ({
content: [{ type: "text", text: `Created ticket for "${title}" (${priority})` }],
})
);
await server.connect(new StdioServerTransport());Quando usar isso:
- Seu serviço ou equipe existente já é baseado em TypeScript/Node e você deseja evitar um segundo runtime.
- Você deseja verificação de tipo em tempo de compilação nos argumentos da ferramenta, juntamente com validação em tempo de execução do mesmo esquema.
- Você está construindo para desenvolvimento local primeiro;
StdioServerTransporté a escolha certa antes de adicionar uma camada de rede. - Você deseja compartilhar tipos entre seu servidor MCP e uma superfície de API TypeScript existente.
Exemplo de Trabalho
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
interface Ticket {
id: string;
title: string;
priority: "low" | "normal" | "high";
status: "open" | "closed";
}
const tickets = new Map<string, Ticket>([
["T-1", { id: "T-1", title: "Printer offline", priority: "low", status: "open" }],
["T-2", { id: "T-2", title: "VPN outage", priority: "high", status: "closed" }],
]);
let nextId = 3;
const server = new McpServer({ name: "tickets-server", version: "1.0.0" });
server.tool(
"create_ticket",
{
title: z.string().min(1, "title is required"),
priority: z.enum(["low", "normal", "high"]).default("normal"),
},
async ({ title, priority }) => {
const id = `T-${nextId++}`;
tickets.set(id, { id, title, priority, status: "open" });
return { content: [{ type: "text", text: id }] };
}
);
server.tool(
"close_ticket",
{ ticketId: z.string() },
async ({ ticketId }) => {
const ticket = tickets.get(ticketId);
if (!ticket) {
throw new Error(`no ticket with id '${ticketId}'`);
}
ticket.status = "closed";
return { content: [{ type: "text", text: `${ticketId} closed` }] };
}
);
server.resource(
"ticket",
"tickets://{ticketId}",
async (uri, { ticketId }) => {
const ticket = tickets.get(ticketId as string);
if (!ticket) {
throw new Error(`no ticket with id '${ticketId}'`);
}
return {
contents: [
{
uri: uri.href,
text: `${ticket.id} [${ticket.priority}] ${ticket.title} - ${ticket.status}`,
},
],
};
}
);
server.resource("open-tickets", "tickets://open", async (uri) => {
const openIds = [...tickets.values()].filter((t) => t.status === "open").map((t) => t.id);
return { contents: [{ uri: uri.href, text: openIds.join("\n") || "no open tickets" }] };
});
await server.connect(new StdioServerTransport());O que isso demonstra:
- Duas ferramentas,
create_ticketeclose_ticket, com esquemas Zod que impõem tanto o tipo quanto os valores permitidos depriority. - Um recurso parametrizado,
tickets://{ticketId}, ao lado de um recurso fixo,tickets://open, espelhando os dois estilos de recurso do exemplo Python. - Objetos
Errorlançados usados para falhas de validação, convertidos pelo SDK em respostas de erro estruturadas. - A forma de retorno da ferramenta,
{ content: [...] }, que é o formato de resultado estruturado do SDK TypeScript.
Mergulho Profundo
Como Funciona
new McpServer({ name, version })cria uma instância de servidor e gerencia as etapas de conexão e negociação para você, de forma idêntica aoFastMCPem Python.server.tool(name, schema, handler)registra uma ferramenta sobname, usando o objetoschemaZod para construir o esquema JSON enviado aos clientes durante a negociação.server.resource(name, uriTemplate, handler)registra um recurso; um segmento{placeholder}no template URI é passado para o manipulador como um parâmetro.- O SDK valida os argumentos de entrada da ferramenta contra o esquema Zod antes de invocar seu manipulador, de modo que entradas malformadas nunca chegam ao seu código.
server.connect(transport)inicia o servidor contra um transporte escolhido,StdioServerTransportpara desenvolvimento local, um transporte baseado em HTTP para implantação remota.
Tratamento de Erros
Lançar um Error padrão dentro de um manipulador é a maneira correta de sinalizar falha.
server.tool("divide", { a: z.number(), b: z.number() }, async ({ a, b }) => {
if (b === 0) {
throw new Error("cannot divide by zero");
}
return { content: [{ type: "text", text: String(a / b) }] };
});O SDK captura o erro lançado e retorna um resultado de erro estruturado para o cliente em vez de travar o processo.
Evite retornar um resultado em formato de sucesso com uma mensagem de erro incorporada no texto; o cliente não tem como distinguir isso de um resultado real de forma confiável.
Ferramentas vs. Recursos em Resumo
| Capacidade | Registrado Com | Tem Efeitos Colaterais | Exemplo |
|---|---|---|---|
| Ferramenta | server.tool(name, schema, handler) | Frequentemente sim | create_ticket, close_ticket |
| Recurso | server.resource(name, uri, handler) | Não | tickets://open, tickets://{ticketId} |
Notas TypeScript
// Esquemas Zod servem como tipos em tempo de compilação via z.infer.
const CreateTicketInput = z.object({
title: z.string(),
priority: z.enum(["low", "normal", "high"]).default("normal"),
});
type CreateTicketInput = z.infer<typeof CreateTicketInput>;Definir o esquema uma vez com Zod e derivar o tipo TypeScript dele mantém a validação em tempo de execução e o tipo em tempo de compilação sincronizados, de modo que uma alteração de esquema não possa se desviar silenciosamente do tipo que seu manipulador espera.
Parâmetros e Valores de Retorno
| Método | Registra | Forma de Retorno do Manipulador |
|---|---|---|
server.tool(name, schema, handler) | Uma ação chamável | { content: [{ type: "text", text: string }] } |
server.resource(name, uri, handler) | Conteúdo somente leitura em um URI | { contents: [{ uri: string, text: string }] } |
Armadilhas
- Usar um tipo Zod genérico como
z.string()para um valor semelhante a enum. Isso permite que valores inválidos comopriority: "urgent"passem sem rejeição no nível do esquema. Correção: usez.enum([...])para qualquer argumento com um conjunto fixo de valores válidos. - Retornar uma string simples de um manipulador de ferramenta em vez da forma de conteúdo. O SDK espera
{ content: [...] }, não uma string bruta. Correção: sempre envolva o resultado no array de conteúdo estruturado, mesmo para uma resposta de texto simples. - Esquecer
awaitemserver.connect(...). O processo pode sair antes que o transporte termine de se conectar, fazendo com que o servidor pareça não fazer nada silenciosamente. Correção: sempre useawait server.connect(transport)no nível superior. - Mutar um
Mapem nível de módulo sem proteção para sessões concorrentes. Sob HTTP/SSE com vários clientes, chamadas de ferramentas sobrepostas podem competir pelo estado compartilhado na memória. Correção: use um banco de dados para o estado real assim que você passar da prototipagem local. - Nomear uma ferramenta de forma genérica como
runouexecute. Nomes vagos dão ao Claude pouco sinal sobre o que a ferramenta realmente faz, especialmente quando um servidor tem várias ferramentas. Correção: nomeie as ferramentas com base na ação específica que elas realizam,create_ticketem vez derun. - Pular
.min()ou validação de formato em entradas de string. Uma stringtitlevazia passa em uma verificaçãoz.string()bruta e pode criar registros malformados silenciosamente. Correção: adicione os refinamentos Zod específicos (.min(1),.email(), etc.) que o campo realmente precisa.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| SDK MCP Python | Sua equipe ou pilha existente é baseada em Python, ou você deseja reutilizar um código-fonte de dados/ML existente | Você deseja compartilhar tipos diretamente com um frontend ou API TypeScript existente |
Classes de protocolo MCP de baixo nível (sem McpServer) | Você precisa de controle granular que a API de alto nível não expõe | Ferramentas e recursos padrão cobrem suas necessidades, o que é a maioria das vezes |
| Uma API REST simples (sem MCP) | O consumidor não é um cliente LLM e não precisa de negociação de capacidade | Você deseja que o Claude descubra e chame sua funcionalidade dinamicamente |
FAQs
Como `McpServer` difere de `FastMCP` no SDK Python?
- Ambos são a classe de servidor de alto nível para seu respectivo SDK.
FastMCPusa decoradores (@mcp.tool());McpServerusa chamadas de método diretas (server.tool(...)).- Ambos lidam com a conexão e negociação de capacidade automaticamente e expõem os mesmos conceitos de ferramenta/recurso/prompt.
Por que este SDK usa Zod em vez de tipos TypeScript puros?
Os tipos TypeScript são apagados em tempo de execução, portanto, eles não podem validar dados de entrada por conta própria. Esquemas Zod fornecem tanto um validador em tempo de execução quanto, via z.infer, um tipo em tempo de compilação correspondente a partir da mesma definição.
O que acontece se um manipulador de ferramenta lançar um erro?
server.tool("risky", { n: z.number() }, async ({ n }) => {
if (n < 0) throw new Error("n must be non-negative");
return { content: [{ type: "text", text: String(n) }] };
});O SDK captura o erro lançado e retorna uma resposta de erro estruturada para o cliente em vez de travar o processo do servidor.
Um manipulador de recurso pode aceitar múltiplos parâmetros de seu URI?
Sim. Um template de URI pode incluir mais de um segmento {placeholder}, e cada um é passado para o manipulador, da mesma forma que os campos de esquema de uma ferramenta são passados como argumentos.
Os argumentos da ferramenta devem sempre ter padrões?
Não, apenas quando um padrão sensato realmente existe, como priority com padrão "normal". Campos obrigatórios, como title no exemplo do ticket, devem permanecer obrigatórios para que o esquema rejeite chamadas incompletas.
Qual transporte devo usar durante o desenvolvimento local?
StdioServerTransport. É o transporte local padrão; o cliente inicia seu servidor como um subprocesso e se comunica com ele via stdin/stdout, sem necessidade de configuração de rede.
Como testar um servidor MCP TypeScript sem um cliente real?
Escreva testes unitários que chamem suas funções manipuladoras diretamente com argumentos de exemplo e, em seguida, exercite-os separadamente através de um cliente simulado que fala o protocolo. Veja a página dedicada de testes para o padrão completo.
Posso registrar tanto uma ferramenta quanto um recurso com os mesmos dados subjacentes?
Sim, e é um padrão comum. Uma ferramenta que muta um registro (close_ticket) e um recurso que o lê (tickets://{ticketId}) podem compartilhar a mesma camada de armazenamento em memória ou banco de dados.
O SDK suporta manipuladores assíncronos?
Sim, os manipuladores são esperados para serem funções assíncronas por convenção, já que a maioria das ferramentas e recursos reais envolve I/O como uma chamada de banco de dados ou uma requisição de API.
Qual é a forma de retorno que um manipulador de ferramenta deve produzir?
Um objeto com um array content, cada entrada tipicamente { type: "text", text: string }. Retornar uma string bruta ou um objeto sem este wrapper não corresponderá ao que o cliente espera.
Como evito quebrar clientes existentes ao alterar o esquema de uma ferramenta?
Adicione novos campos como opcionais com .optional() ou um .default(...) sensato em vez de renomear ou remover campos existentes. Veja a página dedicada sobre versionamento de esquemas de ferramentas para a estratégia mais ampla.
O `McpServer` é específico para Node.js ou também roda em navegadores?
O SDK tem como alvo Node.js para uso no lado do servidor, já que os servidores MCP são tipicamente processos de longa duração com acesso a um transporte como stdio ou um socket de rede, nenhum dos quais está disponível em um contexto de navegador.
Relacionado
- Noções Básicas do Servidor MCP - o esqueleto mínimo no qual este artigo se baseia
- Construindo um Servidor MCP com o SDK Python - os mesmos padrões em Python
- Como Servidores MCP Lidam com Requisições - o ciclo de vida no qual esses manipuladores se conectam
- Testando Servidores MCP Antes da Implantação - exercitando esses manipuladores com um cliente simulado
- Comparação de Implantação de Servidor MCP stdio vs HTTP/SSE - escolhendo um transporte além do desenvolvimento local
Versões de Stack: Escrito contra a linha de modelos Claude atual em ~junho de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (o padrão), e Claude Haiku 4.5 - e os SDKs atuais do Model Context Protocol Python/TypeScript. Nomes de modelos, versões de SDK e a especificação MCP mudam rapidamente - verifique os detalhes atuais em platform.claude.com/docs e modelcontextprotocol.io antes de confiar neles.