Pular para o conteúdo
Integration

Servidor OpenAPI para MCP: 150 endpoints, 49 ferramentas de IA

| 9 min read

Como convertemos uma especificação OpenAPI em um servidor MCP selecionado com 49 ferramentas. Conversão de esquema, descrições de ferramentas, anotações e transporte HTTP sem estado.

Laptop showing code in a dark development environment
Photo by Clement Helardot on Unsplash

A API REST do Botoi possui mais de 150 endpoints. Quando construímos o servidor MCP, registramos 49 deles como ferramentas. Não porque o resto não funciona. Porque fornecer 150 ferramentas a um modelo de IA é como entregar a alguém um menu de 200 páginas; eles escolherão algo, mas não será a coisa certa.

Esta postagem aborda todo o processo: curadoria da lista de ferramentas, conversão de esquemas OpenAPI em objetos Zod, escrita de descrições que os modelos de IA analisam bem, adição de anotações MCP e execução de tudo como um Cloudflare Worker sem estado. Se você mantém uma API e deseja construir um servidor MCP a partir dela, este é o manual.

Por que 49 ferramentas, não 150

Cada ferramenta registrada em um servidor MCP é serializada na janela de contexto do modelo. O nome da ferramenta, a descrição e o esquema de entrada completo consomem tokens. Um manifesto de 150 ferramentas pode queimar mais de 30.000 tokens antes que o usuário digite uma única palavra.

Isso cria dois problemas:

  • Restam menos tokens para a conversa em si
  • O modelo escolhe a ferramenta errada com mais frequência quando a lista é longa

Nós testamos isso. Com todos os mais de 150 pontos de extremidade registrados, Claude escolheu a ferramenta correta na primeira tentativa em cerca de 72% das vezes. Com 49 ferramentas selecionadas, esse número saltou para 94%. A lista menor e focada tornou o modelo melhor em seu trabalho.

Os critérios de curadoria eram simples:

  • Um agente de IA precisa disso no meio da conversa? (Pesquisa de DNS: sim. Geração de PDF: raramente.)
  • A ferramenta retorna dados estruturados sobre os quais o modelo pode raciocinar? (JSON: sim. Imagem binária: não.)
  • O modelo pode preencher os parâmetros exigidos em linguagem natural? (Nome de domínio: sim. Objetos de configuração aninhados complexos: não.)

A estrutura do manifesto da ferramenta

Cada ferramenta selecionada mapeia um nome de ferramenta MCP para um caminho de API, método HTTP, descrição e anotações. Aqui está a interface TypeScript:

export interface CuratedTool {
  path: string;
  method: 'get' | 'post';
  title: string;
  description: string;
  annotations: {
    readOnlyHint?: boolean;
    destructiveHint?: boolean;
    idempotentHint?: boolean;
    openWorldHint?: boolean;
  };
}

E aqui está a aparência de duas entradas na prática:

// curated-tools.ts
export const CURATED_TOOLS: Record<string, CuratedTool> = {
  lookup_dns: {
    path: '/v1/dns/lookup',
    method: 'post',
    title: 'DNS Lookup',
    description:
      'Query DNS records (A, AAAA, MX, TXT, CNAME, NS) for a domain. ' +
      'Use when you need to check DNS configuration or troubleshoot domain resolution.',
    annotations: { readOnlyHint: true, openWorldHint: true },
  },
  dev_hash: {
    path: '/v1/hash',
    method: 'post',
    title: 'Hash Text',
    description:
      'Generate a hash (MD5, SHA-1, SHA-256, SHA-512) of input text. ' +
      'Use for checksums, data integrity, or fingerprinting.',
    annotations: { readOnlyHint: true },
  },
  // ... 47 more tools
};

O path e method campos apontam para o endpoint REST existente. O description informa ao modelo quando usar a ferramenta. O annotations diga ao modelo como a ferramenta se comporta.

Convertendo esquemas OpenAPI para Zod

O SDK do MCP espera esquemas de entrada de ferramentas como objetos Zod. Nossa API já possui uma especificação OpenAPI 3.1 com definições completas do corpo da solicitação para cada endpoint. O construtor de esquema lê essas definições e gera tipos Zod na inicialização do servidor.

A função principal de conversão mapeia cada tipo de propriedade OpenAPI para seu equivalente Zod:

// schema-builder.ts
import { z } from 'zod';
import { paths } from '../../openapi-paths';

function mapPropertyToZod(
  prop: OpenApiProperty,
  isRequired: boolean
): z.ZodTypeAny {
  let schema: z.ZodTypeAny;

  if (prop.enum && prop.enum.length > 0) {
    schema = z.enum(prop.enum as [string, ...string[]]);
  } else {
    switch (prop.type) {
      case 'number':
      case 'integer':
        schema = z.number();
        break;
      case 'boolean':
        schema = z.boolean();
        break;
      case 'array':
        schema = z.array(z.string());
        break;
      case 'object':
        schema = z.record(z.unknown());
        break;
      default:
        schema = z.string();
        break;
    }
  }

  if (prop.description) {
    schema = schema.describe(prop.description);
  }
  if (prop.default !== undefined) {
    schema = schema.default(prop.default);
  }
  if (!isRequired) {
    schema = schema.optional();
  }

  return schema;
}

As principais decisões nesta função:

  • enum os valores se tornam z.enum(), dando ao modelo um conjunto fixo de opções válidas
  • Os campos obrigatórios permanecem obrigatórios; campos opcionais obter .optional()
  • A OpenAPI description transporta através .describe(), que o SDK do MCP inclui no manifesto da ferramenta
  • Os valores padrão se propagam através .default()

O buildZodSchema A função lida com endpoints POST (corpo da solicitação) e GET (parâmetros de consulta):

export function buildZodSchema(
  apiPath: string,
  method: 'get' | 'post'
): Record<string, z.ZodTypeAny> {
  const operation = getOperation(apiPath, method);
  if (!operation) return {};

  // POST: read from requestBody schema
  if (method === 'post') {
    const schema = operation.requestBody
      ?.content?.['application/json']?.schema;
    if (!schema?.properties) return {};

    const required = new Set(schema.required ?? []);
    const result: Record<string, z.ZodTypeAny> = {};

    for (const [key, prop] of Object.entries(schema.properties)) {
      result[key] = mapPropertyToZod(prop, required.has(key));
    }
    return result;
  }

  // GET: read from query parameters
  const params = operation.parameters;
  if (!params || params.length === 0) return {};

  const result: Record<string, z.ZodTypeAny> = {};
  for (const param of params) {
    if (param.in !== 'query') continue;
    const prop: OpenApiProperty = {
      type: param.schema?.type ?? 'string',
      description: param.description ?? param.schema?.description,
      default: param.schema?.default,
      enum: param.schema?.enum,
    };
    result[param.name] = mapPropertyToZod(prop, param.required === true);
  }
  return result;
}

Esta função é executada uma vez por ferramenta durante a criação do servidor. Ele retorna um apartamento Record<string, z.ZodTypeAny> que o SDK do MCP serialize no esquema JSON para o manifesto da ferramenta.

Escrever descrições de ferramentas Os modelos de IA analisam bem

A descrição da ferramenta é o campo mais importante para a seleção correta da ferramenta. Os modelos leem para decidir se uma ferramenta corresponde à intenção do usuário. Descrições vagas levam a escolhas erradas de ferramentas.

Estabelecemos um padrão de duas frases:

  1. Primeira frase: o que a ferramenta faz, começando com um verbo. Inclua os tipos ou formatos de dados específicos que ele manipula.
  2. Segunda frase: quando usá-lo, começando com "Usar quando". Isso dá ao modelo uma condição de gatilho.

Compare estas duas descrições para a mesma ferramenta de pesquisa de DNS:

Versão Descrição Problema
Ruim "Ferramenta DNS para pesquisar coisas" Nenhum tipo de registro listado, nenhuma condição de acionamento, vago
Boa "Consulte registros DNS (A, AAAA, MX, TXT, CNAME, NS) para um domínio. Use quando precisar verificar a configuração de DNS ou solucionar problemas de resolução de domínio." Nenhum

A versão boa informa ao modelo os tipos exatos de registros que ele pode consultar (para que ele saiba que esta ferramenta lida com pesquisas MX) e as situações que devem acioná-las (verificações de configuração de DNS, solução de problemas). O modelo compara a intenção do usuário com essas palavras-chave.

Anotações MCP: informando aos modelos como as ferramentas se comportam

As anotações são sinalizadores de metadados em cada ferramenta. Eles não afetam a execução. Eles dizem ao modelo que tipo de efeitos colaterais esperar.

// Read-only tool that hits an external service
lookup_dns: {
  annotations: { readOnlyHint: true, openWorldHint: true },
}

// Encryption tool: no external calls, same input = same output
security_encrypt: {
  annotations: { idempotentHint: true },
}

As quatro anotações e o que elas sinalizam:

Anotação Sinal Exemplo
readOnlyHint Esta ferramenta lê dados, mas nunca modifica nada Pesquisa de DNS, WHOIS, verificação de SSL
destructiveHint Esta ferramenta exclui ou substitui dados Exclusão da caixa de entrada do Webhook (não em nosso conjunto selecionado)
idempotentHint Chamar esta ferramenta duas vezes com a mesma entrada produz o mesmo resultado Criptografar AES, descriptografar AES
openWorldHint Esta ferramenta faz solicitações de rede externa Pesquisa de IP, metadados de URL, detecção de tecnologia

Das nossas 49 ferramentas, 44 transportam readOnlyHint: true. As 12 ferramentas de pesquisa também carregam openWorldHint: true porque eles chamam servidores DNS externos, registros WHOIS ou buscam páginas da web ativas. As ferramentas de criptografia/descriptografia carregam idempotentHint: true porque são transformações determinísticas.

Nenhuma de nossas ferramentas selecionadas carrega destructiveHint. Essa foi uma escolha deliberada. Excluímos ferramentas como exclusão de caixa de entrada de webhook e exclusão de pasta do conjunto selecionado porque os modelos de IA não devem excluir dados do usuário sem proteções fortes.

Registrando ferramentas no servidor MCP

O ciclo de registro une tudo. Ele itera na lista de ferramentas selecionadas, cria o esquema Zod a partir da especificação OpenAPI e registra cada ferramenta com sua descrição e anotações:

// server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { CURATED_TOOLS } from './curated-tools';
import { buildZodSchema } from './schema-builder';

function createMcpServer(apiKey: string | undefined, env: Env) {
  const server = new McpServer(
    { name: 'botoi', version: '1.0.0' },
    { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() }
  );

  for (const [toolName, tool] of Object.entries(CURATED_TOOLS)) {
    const zodSchema = buildZodSchema(tool.path, tool.method);

    server.registerTool(toolName, {
      title: tool.title,
      description: tool.description,
      inputSchema: zodSchema,
      annotations: tool.annotations,
    }, async (args: Record&lt;string, unknown&gt;) =&gt; {
      return callApi(tool.path, tool.method, args, apiKey, env);
    });
  }

  return server;
}

Quando o modelo chama uma ferramenta, a função manipuladora recebe os argumentos analisados ​​e os encaminha para a rota interna da API. O callApi A função cria uma solicitação HTTP interna e retorna a resposta como conteúdo formatado em MCP:

async function callApi(
  path: string,
  method: string,
  body: unknown,
  apiKey: string | undefined,
  env: Env
) {
  const headers: Record&lt;string, string&gt; = {
    'Content-Type': 'application/json',
  };
  if (apiKey) headers['X-API-Key'] = apiKey;

  const req = new Request(\`http://internal\$\{path}\`, {
    method: method.toUpperCase(),
    headers,
    body: method === 'post' ? JSON.stringify(body) : undefined,
  });

  const res = appFetcher
    ? await appFetcher(req, env)
    : await fetch(req);

  const json = await res.json();
  if (!json.success) {
    return {
      content: [{ type: 'text', text: JSON.stringify(json.error, null, 2) }],
      isError: true,
    };
  }
  return {
    content: [{ type: 'text', text: JSON.stringify(json.data, null, 2) }],
  };
}

O appFetcher O padrão permite que o servidor MCP chame as rotas da API por meio de uma referência de função interna em vez de fazer uma solicitação HTTP externa. Isso evita viagens de ida e volta na rede. O manipulador MCP e as rotas da API são executados no mesmo Cloudflare Worker, portanto, o roteamento interno é uma chamada de função, não um salto HTTP.

Transporte HTTP sem estado em Cloudflare Workers

O MCP suporta dois transportes: stdio (para processos locais) e Streamable HTTP (para servidores remotos). Escolhemos Streamable HTTP porque o servidor é executado em Cloudflare Workers, que não oferece suporte a processos de longa execução.

// Hono route handler
import { WebStandardStreamableHTTPServerTransport }
  from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';

app.all('/mcp', async (c) =&gt; {
  const apiKey =
    c.req.header('X-API-Key') ||
    c.req.header('Authorization')?.replace('Bearer ', '');

  const server = createMcpServer(apiKey, c.env);
  const transport = new WebStandardStreamableHTTPServerTransport();
  await server.connect(transport);
  return transport.handleRequest(c.req.raw);
});

Cada solicitação cria um novo McpServer exemplo. Nenhum estado de sessão persiste entre solicitações. Isso é bom porque cada chamada de ferramenta é independente; o modelo envia o nome da ferramenta e os argumentos e o servidor retorna o resultado. Sem transações em várias etapas.

O design sem estado tem três vantagens:

  • Não é necessário armazenamento de sessão (sem Redis, sem KV, sem banco de dados)
  • Escala para zero quando ocioso, escala horizontalmente sob carga
  • Implanta em mais de 300 pontos de presença da Cloudflare sem configuração

O manuseio da chave API acontece na camada MCP. O cliente envia a chave via X-API-Key ou Authorization: Bearer cabeçalho. A rota MCP o extrai e o passa para a chamada de API interna. Nenhum middleware de autenticação separado na própria rota MCP.

O manual para sua própria API

Se você possui uma especificação OpenAPI e deseja construir um servidor MCP, aqui está a versão condensada:

  1. Organize sua lista de ferramentas. Escolha entre 20 e 80 endpoints que retornam dados estruturados e aceitam entradas simples. Ignore endpoints que retornam dados binários, exigem uploads de arquivos ou possuem esquemas de entrada profundamente aninhados.
  2. Escreva um conversor de esquema. Mapeie seus tipos de propriedade OpenAPI para Zod. Transfira descrições, padrões e valores de enumeração. Manipule os padrões de corpo de solicitação (POST) e parâmetro de consulta (GET).
  3. Escreva descrições de duas frases. Frase um: o que a ferramenta faz, começando com um verbo. Frase dois: "Usar quando" + condição de gatilho. Seja específico sobre tipos e formatos de dados.
  4. Adicione anotações. Marcar ferramentas somente leitura. Ferramentas de sinalização que fazem chamadas de rede externa. Identifique operações idempotentes. Exclua ferramentas destrutivas, a menos que você tenha fluxos de confirmação.
  5. Escolha seu transporte. Use HTTP Streamable para servidores remotos, stdio para ferramentas CLI locais. O SDK do MCP fornece ambos.
  6. Encaminhe chamadas de ferramenta para sua API existente. Não reescreva a lógica de negócios. Chame suas próprias rotas internamente. O servidor MCP é uma camada adaptadora fina.

O servidor MCP do Botoi tem 4 arquivos: curated-tools.ts (49 definições de ferramentas), schema-builder.ts (conversor OpenAPI para Zod), server.ts (registro e roteamento), e tools.ts (ponto final do manifesto público). A coisa toda adiciona cerca de 400 linhas de TypeScript a uma API existente.

Experimente

O servidor Botoi MCP está ativo em https://api.botoi.com/mcp. Conecte-o ao Claude Desktop, Claude Code, Cursor ou VS Code em menos de um minuto. Veja o Documentos de configuração do MCP para trechos de configuração para cada cliente compatível.

Navegue pelo manifesto completo da ferramenta para ver todas as 49 definições de ferramentas com seus esquemas e anotações. O Documentos da API cobrem o conjunto completo de mais de 150 endpoints REST atrás do servidor MCP.

FAQ

Como você constrói um servidor MCP a partir de uma especificação OpenAPI?
Analise suas definições de caminho OpenAPI, extraia o esquema do corpo da solicitação (para POST) ou parâmetros de consulta (para GET), converta cada propriedade em um tipo Zod e registre cada ferramenta em uma instância McpServer com o esquema Zod como inputSchema. O MCP SDK lida com transporte JSON-RPC e descoberta de ferramentas.
Por que não expor todos os endpoints da API como ferramentas MCP?
Os modelos de IA têm um limite de janela de contexto. Cada definição de ferramenta consome tokens. Um manifesto de 150 ferramentas pode consumir mais de 30.000 tokens antes do início da conversa. A curadoria de até 49 ferramentas de alto valor mantém o manifesto abaixo de 8.000 tokens e melhora a precisão da seleção de ferramentas.
O que são anotações da ferramenta MCP e por que são importantes?
As anotações são dicas de metadados como readOnlyHint, destructiveHint, idempotentHint e openWorldHint. Eles informam aos modelos de IA se uma ferramenta lê ou grava dados, se entra em contato com serviços externos e se é seguro tentar novamente. Os modelos usam essas dicas para planejar fluxos de trabalho em várias etapas e evitar ações destrutivas sem confirmação.
Um servidor MCP pode ser executado no Cloudflare Workers?
Sim. Use o WebStandardStreamableHTTPServerTransport do SDK do MCP. Ele funciona com qualquer tempo de execução que suporte a API de solicitação/resposta de padrões da Web. Cloudflare Workers, Deno Deploy e Vercel Edge Functions se qualificam. Cada solicitação cria uma nova instância do McpServer, portanto, nenhum estado de sessão é necessário.
Como devo escrever descrições de ferramentas MCP para modelos de IA?
Comece com um verbo. Indique o que a ferramenta faz em uma frase. Adicione uma segunda frase começando com "Usar quando" que descreva a condição do gatilho. Ignore os detalhes da implementação. Exemplo: "Consulte registros DNS (A, AAAA, MX, TXT, CNAME, NS) para um domínio. Use quando precisar verificar a configuração de DNS ou solucionar problemas de resolução de domínio."

Comece a construir com botoi

150+ endpoints de API para consultas, processamento de texto, geração de imagens e utilitários para desenvolvedores. Plano gratuito, sem cartão de crédito.