Servidor MCP NestJS
npm i @nestjs/mcp @modelcontextprotocol/server zod
npm i -D @modelcontextprotocol/client # cliente de teste em memória
Zod 4.2+ e ArkType funcionam diretamente; Valibot através de toStandardJsonSchema(); um JSON Schema existente via fromJsonSchema(). Esquemas do Zod 3 são recusados na inicialização.
McpModule.forRoot({
name: 'cat-store', version: '1.0.0',
instructions: 'Consulte pedidos com find-order antes de alterá-los.', // informa ao modelo como usar o servidor
}),
app.listen(3000) então expõe o endpoint em /mcp. Inspecione-o com npx @modelcontextprotocol/inspector. Na inicialização, o módulo escaneia os provedores (todos os módulos, ou apenas include) em busca de métodos decorados. Ele falha rapidamente em nomes duplicados, esquemas inválidos ou scopes declarados sem auth.
Primitivas: métodos em provedores
| Primitiva | Decorador | Quem usa | Chamada com |
|---|---|---|---|
| Ferramenta (uma ação) | @Tool({ name, description, inputSchema, outputSchema?, annotations?, scopes? }) | o modelo | entrada validada, ctx |
| Recurso (dados) | @Resource({ uri, mimeType }) / @ResourceTemplate({ uriTemplate: 'orders://{orderId}' }) | o cliente, muitas vezes anexado pelo usuário | {} ou as variáveis do template, ctx (ctx.uri) |
| Prompt (um modelo) | @Prompt({ name, description, argsSchema }) | o usuário escolhe um | argumentos validados, ctx |
@Injectable() // um provedor comum, listado em providers de um módulo
export class OrdersTools {
constructor(private readonly orders: OrdersService) {}
@Tool({
name: 'find-order',
description: 'Encontre um pedido pelo seu id, ou os pedidos mais recentes de um cliente pelo email',
inputSchema: z.object({
orderId: z.string().optional().describe('O id do pedido, como ord_123'),
email: z.email().optional().describe('O endereço de email do cliente'),
}),
annotations: { readOnlyHint: true }, // o cliente pode executá-lo sem perguntar ao usuário
})
async find({ orderId, email }: { orderId?: string; email?: string }) {
const found = orderId ? [await this.orders.findById(orderId)] : await this.orders.latestFor(email!);
if (!found[0]) throw new NotFoundException('Nenhum pedido encontrado'); // → resultado da ferramenta com isError: true
return found;
}
@Tool({
name: 'change-address',
description: 'Altere o endereço de entrega de um pedido que ainda não foi enviado',
inputSchema: z.object({ orderId: z.string(), address: z.string().min(10) }),
outputSchema: z.object({ orderId: z.string(), address: z.string(), status: z.string() }),
scopes: ['orders:write'],
})
async changeAddress({ orderId, address }: { orderId: string; address: string }, ctx: McpContext) {
const o = await this.orders.changeAddress(orderId, address, ctx.authInfo?.clientId);
return { orderId: o.id, address: o.address, status: o.status }; // → structuredContent + texto JSON
}
}
Valores de retorno: uma string de ferramenta se torna um bloco de texto. Um objeto se torna structuredContent se houver um outputSchema, e texto JSON caso contrário. Retorne um CallToolResult você mesmo para imagens, áudio ou recursos incorporados; undefined dá um resultado vazio. A string de um recurso é enviada como texto, um Buffer/Uint8Array como blob, um objeto como JSON. A string de um prompt se torna uma mensagem de usuário; um array retorna PromptMessage[]. Nomes de ferramentas têm 1–128 caracteres de A-Z a-z 0-9 _ - . e, por padrão, são o nome do método.
Escreva nomes, descrições e textos .describe() para o modelo. São tudo o que ele conhece: diga o que a ferramenta faz, quando usá-la, se ela altera algo, e dê um valor de exemplo para cada argumento. Use recursos para conteúdo carregado como está; use ferramentas para consultas que recebem argumentos.
Contexto de chamada
ctx: McpContext tem estas propriedades:
kind,name,uriauthInfo(clientId,scopes,expiresAt,extra)request(a requisição da plataforma;undefinedsobre stdio)requestInfotransport('http' | 'stdio')era('modern'2026-07-28 /'legacy'2025)signal: passe parafetche chamadas de banco de dados; chamethrowIfAborted()em loopsreportProgress(progress, total?, message?)server(contexto do SDK)
Para perguntar ao usuário (confirmações, valores ausentes), retorne inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message, requestedSchema }) } }). Na repetição, leia a resposta com acceptedContent(ctx.server.mcpReq.inputResponses, 'confirm', Schema). Sobre HTTP, isso só funciona para clientes 2026-07-28, porque o endpoint é sem estado. Sobre stdio, funciona para ambas as eras.
Provedores permanecem singletons. Leia dados por chamada de ctx, não de provedores com escopo de requisição.
Aprimoradores e erros
- Guards, interceptors, pipes e filtros funcionam como em rotas, com
context.getType() === 'mcp'.McpExecutionContext.create(context)fornecegetInput(),getContext(),getKind(),getName(),getRequest()egetAuthInfo(). - Aprimoradores globais executam em chamadas MCP e na requisição HTTP que as transporta. Aprimoradores exclusivos HTTP devem verificar
context.getType() === 'http'antes de chamarswitchToHttp(). - Um
ValidationPipe({ transform: true })global valida a entrada tipada por DTO. Decoradores personalizados de parâmetro vêm depois de(input, ctx, @CurrentUser() user). - Erros lançados por ferramentas se tornam resultados
isError: truedos quais o modelo pode se recuperar. UmaHttpExceptionmantém sua mensagem; qualquer outro erro se tornaInternal server errore é registrado. - Recursos e prompts respondem com erros JSON-RPC:
NotFoundException→ recurso não encontrado,BadRequestException→ parâmetros inválidos. - Filtros: verifique
host.getType<McpContextType>() === 'mcp'. O que o filtro retornar se torna o resultado; para enviar um erro JSON-RPC, lance umProtocolError.
Autorização (servidores acessíveis por rede)
McpModule.forRootAsync({
imports: [ConfigModule, AuthModule],
inject: [ConfigService, IdentityProviderTokenVerifier],
useFactory: (config: ConfigService, verifier: IdentityProviderTokenVerifier) => ({
name: 'cat-store', version: config.get('VERSION'),
auth: {
verifier, // implementa McpTokenVerifier.verifyAccessToken(token) → AuthInfo | null
resource: config.get('MCP_RESOURCE'), // URL pública canônica = público-alvo do token
authorizationServers: [config.get('IDENTITY_PROVIDER_URL')],
scopesSupported: ['orders:read', 'orders:write'],
},
}),
}),
- O servidor é um servidor de recursos OAuth 2.1: ele verifica tokens e nunca os emite.
- Ele serve metadados RFC 9728 em
/.well-known/oauth-protected-resource/mcp. Requisições sem token recebem um 401 com um desafioWWW-Authenticate: Bearer, que inicia o login do cliente. - Verificadores:
- JWTs: use
JwtVerifierdo@nestjs/authentication(verifica assinatura, emissor, público e expiração). - Tokens opacos: chame o endpoint de introspecção RFC 7662.
- O
AuthInforetornado deve carregarresource === auth.resourcee umexpiresAtfuturo, caso contrário o token é recusado com 401. - Lance um erro 401 para recusar um token; qualquer outro erro se torna 500.
- JWTs: use
- Ausência de
scopesem uma primitiva → 403 com desafio de step-up.auth.requiredScopesse aplica a todas as requisições. - Com
@nestjs/authentication≥ 0.1.3 e@nestjs/authorization≥ 0.1.1:- Seus guards globais autenticam cada chamada, e
@CurrentUser()e@Can(Policy, 'action')funcionam em ferramentas. - Defina
decorators: [Public()]no endpoint para queauthlide com o desafio. - Uma ferramenta sem
@Can()é negada.
- Seus guards globais autenticam cada chamada, e
- Servidores exclusivamente internos podem pular OAuth e usar
decorators: [UseGuards(ApiKeyGuard)]no endpoint.
Transportes
- HTTP Streamable é servido por padrão;
path: falsedesabilita. O endpoint é um controlador normal, então prefixo global, versionamento (endpointVersion), middleware e CORS se aplicam. É sem estado: não são necessárias sessões adesivas. Ambas as eras do protocolo são servidas (http.legacy: 'reject'serve apenas 2026-07-28).http.responseModepode ser'auto','json'ou'sse'. - stdio, para assistentes que iniciam um processo local: crie
stdio.tscomNestFactory.createApplicationContext(AppModule, { logger: new McpStdioLogger() }), depoisawait app.get(McpStdioServer).listen(); await app.close();. Qualquer outra coisa escrita no stdout quebra o protocolo. Credenciais vêm deprocess.env; escopos não são verificados, mas guards executam.
Consulte references/mcp-reference.md para a tabela completa de opções, configurações de produção, configuração do cliente stdio, padrões de teste e mapa de migração.
Fluxo de trabalho
- 1. Instalar; verificar Zod 4.2+;
McpModule.forRootcomname,version,instructions. - 2. Adicionar ferramentas, recursos e prompts em provedores, com descrições orientadas ao modelo,
.describe()em cada argumento,readOnlyHintem ferramentas somente leitura, eoutputSchemaquando o resultado é estruturado. - 3. Garantir que aprimoradores globais exclusivos HTTP verifiquem
getType(); adicionar guards e filtros MCP conforme necessário. - 4. Servidores acessíveis por rede: adicionar
auth(verificador, recurso, servidores de autorização, escopos),allowedHosts/allowedOrigins, cabeçalhos CORS e limite de corpo. Servidores locais: adicionarstdio.tscomMcpStdioLogger. - 5. Testes: testar métodos unitariamente; executar e2e com
connectMcpClient(moduleRef, { token })contra um verificador de teste; testar a rota HTTP sobre um socket. Compilar, lintar, executar testes, iniciar o app e listar ferramentas com o Inspector.
Arquivos de referência
references/mcp-reference.md: opções do módulo, opções do decorador, endurecimento HTTP (Origin, Host, cabeçalhos CORS, tamanho do corpo), configuração do cliente stdio, nomenclatura NestJS Observe, checklist de produção, testes (tokens e escopos, eras do protocolo, verificações do McpRegistry, testes HTTP e stdio), e migração de@rekog/mcp-nest1.x/2.x. Leia ao configurar produção, escrever testes ou migrar.
Fontes: Visão geral · Primitivas · Transportes · Testes · Migração