Receber por email
← Voltar para o catálogo

mcp-best-practices

Construa servidores MCP de produção com o SDK TypeScript. Esta skill aborda a especificação 2025-11-25, versões do SDK, seleção de transporte, design de ferramentas, tratamento de erros, segurança, desempenho, bugs conhecidos, extensões MCP, UIs interativas, autorização e o Registro MCP. É essencial para construir servidores, projetar ferramentas, escolher transportes e lidar com erros MCP.

29estrelas
Atualizado há 4 meses

Ver no GitHub ↗ Licença: MIT

Como adicionar

/plugin marketplace add tenequm/skills

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/mcp-best-practices/svg)](https://www.skillteca.com.br/skills/mcp-best-practices)

Alerta por categoria

Receba novas skills de Design e Frontend toda segunda

1 email curto, só com as skills novas de Design e Frontend. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

MCP Best Practices

Decision reference for building production MCP servers with the TypeScript SDK. Not a tutorial - assumes you already have a working server and need to make it correct, fast, and secure.

Quick Reference

| Component | Current | Next | |-----------|---------|------| | Spec | 2025-11-25 (spec.modelcontextprotocol.io) | - | | TS SDK (stable) | v1.29.0 (@modelcontextprotocol/sdk) | v2 alpha published | | TS SDK (v2) | Alpha (2.0.0-alpha.2 on npm, Apr 2026): /server, /client, /core, /hono, /express, /node, /fastify | Stable pending; only alpha published | | JSON Schema | 2020-12 default (explicit $schema supported) | - | | Transport | Streamable HTTP (remote), stdio (local) | SSE + WebSocket removed in v2 | | Extensions | MCP Apps (Stable, SEP-1865), Auth Extensions (official) | Domain-specific WGs | | Registry | Preview with v0.1 API freeze since 2025-10-24 (registry) | GA pending |

v1 imports (production today):

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

v2 imports (when stable):

import { McpServer } from "@modelcontextprotocol/server";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server";

Server Setup

Transport Decision

| Scenario | Transport | Key Config | |----------|-----------|------------| | Remote, stateless (K8s, CF Workers) | WebStandardStreamableHTTPServerTransport | sessionIdGenerator: undefined, enableJsonResponse: true | | Remote, stateful (long tasks, SSE) | WebStandardStreamableHTTPServerTransport | sessionIdGenerator: () => randomUUID() | | Local CLI / Claude Desktop | StdioServerTransport | Default | | Legacy SSE clients | SSE removed in v2 - migrate to Streamable HTTP | - |

Stateless Pattern (recommended for remote deployment)

Per-request server+transport creation is the canonical pattern. Maintainer @ihrpr confirms: "each transport should have an instance of MCPServer" (#343). Sharing instances leaks cross-client data (GHSA-345p-7cg4-v4c7).

app.post("/mcp", async (c) => {
  const server = new McpServer({ name: "my-server", version: "1.0.0" });
  // Register tools, resources, prompts...
  registerTools(server);

  const transport = new WebStandardStreamableHTTPServerTransport({
    sessionIdGenerator: undefined,   // stateless - no session tracking
    enableJsonResponse: true,        // JSON responses, no SSE streaming
  });

  // All tools/resources must be registered before connect() (#893)
  try {
    await server.connect(transport);
    return transport.handleRequest(c.req.raw);
  } finally {
    await transport.close();
    await server.close();
  }
});

What to hoist to module level (don't recreate per request):

  • Zod schemas (they never change)
  • Annotation objects ({ readOnlyHint: true, ... })
  • Tool description strings
  • Payment configs, upstream API clients

The McpServer itself must be per-request, but its constant inputs should not be.

For deep dive on transports, sessions, HTTP/2 gotchas, and K8s deployment: see references/transport-patterns.md

Framework Integration

Hono (web-standard):

import { Hono } from "hono";
const app = new Hono();
app.post("/mcp", handleMcpRequest);  // WebStandardStreamableHTTPServerTransport
app.get("/mcp", handleMcpSse);       // Optional: SSE for server notifications
app.delete("/mcp", handleMcpDelete); // Optional: session termination

Cloudflare Workers: Same pattern - WebStandardStreamableHTTPServerTransport works natively in Workers runtime.

Express/Node (v2): Use @modelcontextprotocol/express middleware with NodeStreamableHTTPServerTransport (wraps the Web Standard transport for IncomingMessage/ServerResponse).

Tool Design

Registration API

v1 (current stable) - server.tool() works but has ambiguous overloads. Prefer the config-object form when possible:

server.tool("search_docs", "Search documents", {
  query: z.string().describe("Search query"),
  max_results: z.number().optional().describe("Max results (default 20)"),
}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
  async ({ query, max_results }) => { /* handler */ }
);

v2 (migration target) - registerTool() with config object:

server.registerTool("search_docs", {
  title: "Document Search",
  description: "Search documents by keyword or phrase",
  inputSchema: z.object({
    query: z.string().describe("Search query"),
    max_results: z.number().optional().describe("Max results (default 20)"),
  }),
  outputSchema: z.object({
    results: z.array(z.object({ id: z.string(), text: z.string() })),
    has_more: z.boolean(),
  }),
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
}, async ({ query, max_results }) => {
  const result = await fetchDocs(query, max_results);
  return {
    structuredContent: result,
    content: [{ type: "text", text: JSON.stringify(result) }],
  };
});

Naming

Spec (2025-11-25): 1-128 chars, case-sensitive. Allowed: A-Za-z0-9_-.

DO: search_docs, get_user_profile, admin.tools.list DON'T: search (too generic, collides across servers), Search Docs (spaces not allowed)

Service-prefix your tools (github_*, jira_*) when multiple servers are active - LLMs confuse generic names across servers.

Schema Rules

.describe() on every field - this is what LLMs use for argument generation.

For complete Zod-to-JSON-Schema conversion rules, what breaks silently, outputSchema/structuredContent patterns: see references/tool-schema-guide.md

Critical bugs:

  • z.union() / z.discriminatedUnion() silently produce empty schemas on v1.x (#1643). The fix landed in the v2 line (PR #1796); the v1.x backport (PR #2017) is still open, so the bug is present on every released v1 version. Use flat z.object() with z.enum() discriminator field instead.
  • Plain JSON Schema objects silently dropped before v1.28.0. Fixed in v1.28 - now throws at registration (#1596).
  • z.transform() stripped during conversion - JSON Schema can't represent transforms (#702).
  • Client-side AJV strict-mode rejection: Zod v4 z.object() produces JSON Schema with additionalProperties: false. The SDK's client validates structuredContent against outputSchema with AJV strict mode and rejects extra fields. Server-side .parse() strips extras silently, but the original structuredContent is sent to the client unchanged - so the server thinks it's fine and the client errors. Fix: explicitly .parse() upstream data before assigning to structuredContent, or use .passthrough() on schemas that intentionally pass through extra fields.

Annotations

All are optional hints (untrusted from untrusted servers per spec):

| Annotation | Default | Meaning | |------------|---------|---------| | readOnlyHint | false | Tool doesn't modify its environment | | destructiveHint | true | May perform destructive updates (only when readOnly=false) | | idempotentHint | false | Repeated calls with same args have no additional effect | | openWorldHint | true | Inte

Compartilhar X LinkedIn