Skip to content
guide

10,000 MCP servers exist: here is what separates the good ones

| 9 min read

The MCP ecosystem crossed 10,000 public servers in April 2026. The TypeScript, Python, Java, Kotlin, C#, and Swift SDKs hit 97 million monthly downloads combined. OAuth 2.1 and Streamable HTTP transport are stable. The Linux Foundation oversees governance. Claude Desktop, Cursor, Windsurf, Zed, Continue, Sourcegraph Cody, and Taskade Genesis all ship with MCP client support.

Most of those 10,000 servers are abandoned experiments. Vague tool names, no input validation, dumped API surfaces, and generic error messages. The handful that developers keep coming back to share seven practices.

This post covers those seven practices with production code from Botoi's MCP server, which exposes 49 curated tools from 150+ API endpoints. Every code example is from the running server at api.botoi.com/mcp.

Server room with network cables and rack-mounted hardware representing MCP server infrastructure
10,000 MCP servers exist. The good ones share seven traits. Photo by Jordan Harrison on Unsplash

1. Curate tools; don't dump your entire API

The single biggest mistake: registering every API endpoint as an MCP tool. Botoi has 150+ endpoints. If all of them were MCP tools, the tool manifest would consume 30,000+ tokens before a conversation starts. That leaves less room for the user's actual question, increases the chance the model picks the wrong tool, and slows down every request because the model has to read through hundreds of tool definitions.

Botoi registers 49. The selection criteria: tools developers reach for during coding sessions (DNS lookup, JWT decode, hash generation), tools that produce structured output models can reason about (JSON validation, PII detection), and tools that save a context switch (Base64 encode, URL metadata extraction).

// curated-tools.ts - 49 tools from 150+ endpoints
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 curated tools
};

The tools that didn't make the cut: batch operations with large payloads (PDF generation from HTML), tools that return binary data (QR code image, screenshot capture), and tools with overlapping functionality (three different hash endpoints collapsed into one). Every tool you add costs tokens. Treat your MCP manifest like a product, not a mirror of your API docs.

Rule of thumb: if you have more than 50 tools, the model spends more time reading tool definitions than doing useful work. Audit your manifest quarterly and remove tools with zero invocations.

2. Write tool descriptions that help the model pick correctly

The model reads your tool description to decide whether to call it. A vague description like "Does DNS stuff" forces the model to guess. A description stuffed with implementation details wastes tokens on information the model doesn't need.

The pattern that works: one sentence starting with a verb that states what the tool does, followed by a second sentence starting with "Use when" that describes the trigger condition.

Bad descriptions

// Bad: vague, no trigger condition
{
  name: "dns",
  description: "Does DNS stuff"
}

// Bad: too long, includes implementation details
{
  name: "dns_lookup_tool",
  description: "This tool uses the DNS-over-HTTPS protocol to query Cloudflare's 1.1.1.1 resolver via a GET request to https://cloudflare-dns.com/dns-query with an Accept header of application/dns-json. It supports A, AAAA, MX, TXT, CNAME, and NS record types and returns the raw DNS response parsed into JSON format."
}

Good descriptions

// Good: verb-first, one sentence what + one sentence when
{
  name: "lookup_dns",
  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."
}

// Good: specific about capabilities and trigger condition
{
  name: "security_pii_detect",
  description:
    "Scan text for personal identifiable information (emails, phones, SSNs, credit cards). " +
    "Use when you need to audit user input or log output for sensitive data before storage."
}

The first sentence is for tool selection: it tells the model what the tool does and what inputs it accepts. The second sentence is for disambiguation: when two tools could match, the "Use when" clause helps the model pick the right one.

All 49 tools in Botoi's MCP server follow this pattern. The result: models consistently pick the correct tool on the first try, even when multiple tools have overlapping capabilities (e.g., lookup_email vs. lookup_dns for checking MX records).

3. Use tool annotations

The MCP spec defines four annotation hints: readOnlyHint, destructiveHint, idempotentHint, and openWorldHint. Most servers ignore them. Models and clients that respect these hints make safer decisions: they won't call destructive tools without confirmation, they'll retry idempotent tools on failure, and they'll warn users before tools that contact external services.

// Annotations from Botoi's curated-tools.ts
lookup_dns: {
  path: "/v1/dns/lookup",
  annotations: {
    readOnlyHint: true,    // reads data, changes nothing
    openWorldHint: true,   // contacts external DNS servers
  },
},

storage_paste_create: {
  path: "/v1/paste/create",
  annotations: {
    destructiveHint: true,   // creates new data
    idempotentHint: false,   // each call creates a new paste
  },
},

dev_hash: {
  path: "/v1/hash",
  annotations: {
    readOnlyHint: true,      // pure computation
    idempotentHint: true,    // same input = same output
  },
},
Annotation Meaning Example
readOnlyHint: true Reads data, changes nothing DNS lookup, SSL check, IP geolocation
destructiveHint: true Creates, updates, or deletes data Create paste, generate short URL
idempotentHint: true Safe to call multiple times with same input Hash generation, JSON formatting
openWorldHint: true Contacts external services DNS over HTTPS, WHOIS, URL metadata

Annotations cost almost nothing in terms of manifest size, but they give clients and models the metadata they need to plan multi-step workflows safely. Set them on every tool.

4. Go stateless with Streamable HTTP

The SSE (Server-Sent Events) transport is deprecated in the MCP spec. It required persistent connections, session management, and reconnection logic. Streamable HTTP replaces it with standard HTTP POST requests carrying JSON-RPC 2.0 payloads. No persistent connection. No session state. Works with every HTTP infrastructure: CDNs, load balancers, edge runtimes, serverless platforms.

Botoi's MCP server runs on Cloudflare Workers. Each request creates a fresh McpServer instance, registers all 49 tools, handles the request, and returns. No session ID, no reconnection handler, no state to manage between requests.

// MCP server on Cloudflare Workers with Streamable HTTP
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
  WebStandardStreamableHTTPServerTransport
} from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";

app.all("/mcp", async (c) => {
  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);
});

The WebStandardStreamableHTTPServerTransport from the MCP SDK works with any runtime that supports the Web Standards Request/Response API: Cloudflare Workers, Deno Deploy, Bun, Vercel Edge Functions, and Node.js 18+. If you're starting a new MCP server in 2026, use Streamable HTTP. If you're running an SSE server, migrate before clients drop support.

Stateless servers scale horizontally without coordination. Botoi's MCP endpoint runs on Cloudflare's edge network; requests route to the nearest data center. Computation-only tools (hashing, Base64, JSON formatting) respond in under 50ms.

5. Validate inputs with schemas

MCP tools receive arbitrary JSON from models. A model can send {"domain": 42} where you expect a string, include unknown fields, or omit required parameters. Validating inputs before execution catches these errors early and returns structured feedback the model can use to self-correct.

Botoi's approach: the schema-builder.ts module reads OpenAPI path definitions at registration time and converts each property to a Zod type. The MCP SDK validates inputs against the Zod schema before the tool handler runs.

// schema-builder.ts - OpenAPI to Zod conversion
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;
      default:
        schema = z.string(); break;
    }
  }

  if (!isRequired) schema = schema.optional();
  return schema;
}

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

  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;
}

The registration loop passes the generated schema as inputSchema for each tool:

// server.ts - register tools with Zod schemas
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<string, unknown>) => {
    return callApi(tool.path, tool.method, args, apiKey, env);
  });
}

This means every tool gets input validation for free. If the model sends an integer where a string is expected, the MCP SDK returns a structured error before your handler code runs. The model sees the error, corrects the input, and retries.

6. Return structured errors the model can reason about

When a tool fails, the model needs enough information to recover. "Something went wrong" is useless. A structured error with the field name, what was expected, and what was received gives the model a clear path to retry with corrected input.

Bad error response

// Bad: the model can't fix this
{
  "content": [{ "type": "text", "text": "Something went wrong" }],
  "isError": true
}

Good error response

// Structured error the model can reason about
{
  "content": [
    {
      "type": "text",
      "text": "{\n  \"error\": \"validation_error\",\n  \"message\": \"Invalid record type\",\n  \"field\": \"type\",\n  \"expected\": [\"A\", \"AAAA\", \"MX\", \"TXT\", \"CNAME\", \"NS\"],\n  \"received\": \"INVALID\"\n}"
    }
  ],
  "isError": true
}

Botoi's callApi wrapper captures the structured error from the underlying REST API and passes it through to the MCP response with isError: true:

// server.ts - API call wrapper with structured errors
async function callApi(
  path: string,
  method: string,
  body: unknown,
  apiKey: string | undefined,
  env: Env
) {
  const headers: Record<string, string> = {
    "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 = await appFetcher(req, env);
  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),
    }],
  };
}

The key detail: the isError: true flag tells the model that the tool invocation failed. The structured JSON in the text field tells it why. Models trained on tool-use patterns will read the error, identify the problem field, and retry with a corrected value. Generic error strings force the model to guess or give up.

7. Add auth without breaking the developer experience

MCP servers need authentication. The spec supports OAuth 2.1 for full authorization flows, but most developer tools work fine with API key forwarding. The developer adds their key to the MCP client config once; the server extracts it from the request headers and passes it through to every API call.

Here's the client config for Claude Desktop with an API key:

// Client config: Claude Desktop
{
  "mcpServers": {
    "botoi": {
      "type": "streamable-http",
      "url": "https://api.botoi.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_KEY"
      }
    }
  }
}

The server extracts the key from either the Authorization header or the X-API-Key header, then forwards it to every internal API call:

// server.ts - extract API key from MCP request headers
app.all("/mcp", async (c) => {
  const apiKey =
    c.req.header("X-API-Key") ||
    c.req.header("Authorization")?.replace("Bearer ", "");

  // Pass the key through to every API call
  const server = createMcpServer(apiKey, c.env);
  const transport = new WebStandardStreamableHTTPServerTransport();
  await server.connect(transport);
  return transport.handleRequest(c.req.raw);
});

Without a key, the server still works. Botoi allows anonymous access at 5 requests per minute and 100 per day, enough for casual use during a coding session. With a key, limits scale to the developer's plan tier. This approach means zero-friction onboarding (no signup required to try it) with a clear upgrade path when usage grows.

The developer's API key never leaves the MCP server. It goes from the client config to the MCP request header to the internal API call. The model never sees it in the conversation context.

Good MCP server vs. bad MCP server

Trait Bad server Good server
Tool count Dumps every endpoint (100+) Curates high-value tools (under 50)
Tool descriptions "Does DNS stuff" or 200-word walls Verb + what it does + "Use when" trigger
Annotations Missing on all tools Set on every tool: readOnly, destructive, idempotent, openWorld
Transport SSE with session management Stateless Streamable HTTP
Input validation None; crashes on bad input Zod schemas generated from OpenAPI spec
Error responses "Something went wrong" Structured JSON with field, expected, and received
Authentication None, or blocks anonymous use entirely API key forwarding with anonymous fallback and rate limits

The 10,000-server landscape

The growth from 1,000 to 10,000 MCP servers in six months happened because the protocol stabilized. OAuth 2.1 replaced the ad-hoc auth patterns. Streamable HTTP replaced SSE. The Linux Foundation governance gave enterprises the confidence to build production servers. SDKs in six languages lowered the barrier to entry.

But quantity didn't bring quality. Most of those servers were weekend experiments that never got past the "register all my endpoints and see what happens" stage. The servers that developers use daily invested in curation, descriptions, schemas, error handling, and auth. Those five areas are where 90% of the usability gap lives.

If you're building an MCP server in 2026, the bar isn't "does it work." The bar is "does the model pick the right tool, understand the error when it picks wrong, and recover without human intervention." The seven practices above get you there.

Botoi's MCP server is open for testing. Connect from Claude Desktop, Claude Code, Cursor, VS Code, or Windsurf using the configs on the MCP setup page. The full tool manifest is at api.botoi.com/v1/mcp/tools.json, and the API docs cover every endpoint behind the tools.

Frequently asked questions

How many MCP servers exist in 2026?
The MCP ecosystem crossed 10,000 public servers in April 2026, up from around 1,000 in late 2025. The growth is driven by stable OAuth 2.1 support, Streamable HTTP transport replacing SSE, and client integration in Claude Desktop, Cursor, Windsurf, Zed, Continue, Sourcegraph Cody, and Taskade Genesis.
How many tools should an MCP server expose?
There is no hard limit, but fewer is better. Each tool definition consumes tokens in the model context window. A 150-tool manifest can cost 30,000+ tokens before the conversation starts. Botoi curates 49 tools from 150+ API endpoints. Aim for under 50 well-described tools unless your use case demands more.
What MCP transport should I use in 2026?
Streamable HTTP is the stable, recommended transport. SSE (Server-Sent Events) transport is deprecated in the MCP spec. Streamable HTTP uses standard HTTP POST with JSON-RPC 2.0 payloads, works with any HTTP infrastructure (CDNs, load balancers, edge runtimes), and requires no persistent connection.
What are MCP tool annotations and why do they matter?
Tool annotations are metadata hints defined in the MCP spec: readOnlyHint, destructiveHint, idempotentHint, and openWorldHint. They tell AI models whether a tool reads data, writes data, is safe to retry, or contacts external services. Models use these hints to plan safer multi-step workflows and avoid destructive calls without user confirmation.
Do MCP servers need authentication?
Yes. The MCP spec now includes OAuth 2.1 for authentication. Even for simpler setups, require a Bearer token or API key on every request. Anonymous access should have strict rate limits. Botoi allows anonymous access at 5 requests per minute and 100 per day, with higher limits for authenticated keys.

Try this API

IP Geolocation API — interactive playground and code examples

More guide posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.