A Model Context Protocol server is an API whose main user is a model. That one fact explains most of what goes wrong with them. Teams wrap their REST endpoints one-to-one, ship forty tools with one-line descriptions, reuse an admin API key, and then wonder why the assistant picks the wrong tool, pages through ten thousand records, or cannot be allowed near a customer tenant.
This guide covers how we build an MCP server a SaaS company can put in front of its customers, from tool design to authorization, testing and versioning.
The timing matters. The 2026-07-28 revision became the current protocol version two weeks ago.1 It is a substantial change: the protocol is now stateless, with no initialization handshake and no protocol-level sessions.23 Everything below is written against that revision, with notes on serving clients that still speak the 2025 versions.
What an MCP server is, and when your product needs one
MCP standardises how an AI application (the host: a chat assistant, IDE or agent framework) discovers and calls capabilities exposed by a server: tools the model invokes, resources the application reads, and prompts the user selects. For a SaaS product, tools carry nearly all the value and nearly all the risk. Not every product needs a server. Our rough test:
| Signal | Build an MCP server? |
|---|---|
| Customers already ask assistants to do things in your product (“summarise my overdue invoices”, “open a ticket for this”) | Yes. You are being asked for an integration surface; MCP is the one most hosts speak. |
| You have a stable public API with OAuth and per-tenant data isolation | Yes, and it will be quick: the server is a thin, model-shaped layer on top. |
| You want an AI feature inside your own product | Not necessarily. Calling your own services directly from your agent is simpler; add MCP when outside hosts need the same capabilities. |
| Your API has no per-user authorization, only shared keys | Fix that first. An MCP server inherits every weakness of the API beneath it. |
Transports: stdio for local, Streamable HTTP for remote
MCP defines two standard transports. With stdio, the host launches the server as a child process and exchanges JSON-RPC messages over standard input and output. With Streamable HTTP, the server is an independent service with a single endpoint that accepts POST requests and answers each one with either a JSON object or a server-sent events stream scoped to that request.4
| stdio | Streamable HTTP | |
|---|---|---|
| Runs where | On the user’s machine, started by the host | On your infrastructure, reached by URL |
| Credentials | From the environment (an API key or local login) | OAuth access token on every request |
| Good for | Developer tools that touch local files, CLIs, databases on localhost | SaaS products, multi-tenant data, anything you need to update, meter and audit centrally |
For a SaaS product the answer is almost always a remote Streamable HTTP server: you control the version, tokens are scoped per user, and you see every call. The 2026-07-28 revision makes hosting easier: because every request now carries its own protocol version, client capabilities and client identity, a remote server can sit behind an ordinary round-robin load balancer with no session affinity.2 Requests also carry Mcp-Method and Mcp-Name headers, so a gateway can route and rate-limit by tool without parsing JSON bodies, and the server must reject requests where those headers disagree with the body.4
Two transport rules are easy to miss. Servers must validate the Origin header and return 403 for invalid origins, to block DNS rebinding; and a server running locally over HTTP should bind to 127.0.0.1, not every interface.4 If you need state across calls (a draft, a cart, a long export), the spec’s guidance is now explicit: return an opaque handle from a creation tool, accept it as an ordinary argument on later calls, state its lifetime in the tool description, and check the caller’s authorization against it on every call.5
Designing task-shaped tools a model uses correctly
Tool design decides whether the model succeeds more than model choice does. Anthropic’s guidance matches our experience: a few tools around high-impact workflows rather than one per endpoint, meaningful fields rather than bare identifiers, and error messages that suggest a specific fix.6
Fewer tools, shaped like the user’s task
Every tool definition is sent to the model, usually on every turn. A worked example with illustrative numbers (assumptions, not measurements): an endpoint-mirror server with 40 tools averaging 250 tokens per definition adds about 10,000 tokens to each request before the user has said anything. Consolidating to 9 task-shaped tools with richer descriptions at around 350 tokens each brings that to about 3,150 tokens and gives the model 9 choices instead of 40. Our rule of thumb is one tool per thing a user would ask for in a sentence: billing_resend_invoice, not GET /invoices plus POST /emails for the model to chain.
Names and descriptions
The spec says tool names should be 1 to 128 characters, case sensitive, unique within a server, and limited to ASCII letters, digits, underscore, hyphen and dot. It also warns that hosts aggregating several servers may see collisions such as two search tools.5 A product prefix such as billing_ avoids that. Write the description for a new colleague: what the tool does, when to use it instead of its neighbours, what it returns, units and limits. Use unambiguous parameter names such as user_id rather than user.6
Input schemas, output shapes and pagination
Constrain inputs as tightly as the domain allows: enums for statuses, formats for dates and emails, minimum and maximum on limits, and additionalProperties: false. Declare an outputSchema so clients can validate results; if you do, the server must return structuredContent that conforms to it, and for backward compatibility it should also return the serialised JSON as a text block.5 Paginate anything that can grow with a low page cap and an opaque cursor, as the spec does for its own lists.7 Here is what a client sees for one example tool:
{
"name": "billing_search_invoices",
"title": "Search invoices",
"description": "Find invoices in the signed-in customer's account. Use this to get an invoice_id before resending or refunding. Returns up to 'limit' invoices, newest first, with next_cursor when more exist. Amounts are in minor units (cents). For metered usage, use billing_get_usage instead.",
"inputSchema": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["open", "paid", "overdue", "void"],
"description": "Filter by status. Omit to include all." },
"customer_email": { "type": "string", "format": "email",
"description": "Exact billing email of the end customer, if known." },
"issued_after": { "type": "string", "format": "date",
"description": "ISO 8601 date, for example 2026-07-01." },
"limit": { "type": "integer", "minimum": 1, "maximum": 25, "default": 10 },
"cursor": { "type": "string", "description": "next_cursor from a previous call." }
},
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"properties": {
"invoices": { "type": "array", "items": { "type": "object", "properties": {
"invoice_id": { "type": "string" }, "number": { "type": "string" },
"customer_name": { "type": "string" }, "status": { "type": "string" },
"amount_due_minor": { "type": "integer" }, "currency": { "type": "string" },
"due_date": { "type": "string", "format": "date" } },
"required": ["invoice_id", "number", "customer_name", "status", "amount_due_minor", "currency"] } },
"next_cursor": { "type": ["string", "null"] }
},
"required": ["invoices", "next_cursor"]
},
"annotations": { "readOnlyHint": true, "openWorldHint": false }
}Errors the model can act on
MCP separates protocol errors (unknown tool, malformed request), which clients may or may not show the model, from tool execution errors, returned as a normal result with isError: true, which clients should pass to the model so it can self-correct.5 Use the second channel for anything the model could fix: a bad date, an invoice that does not exist, a missing permission. Say what went wrong and what to do next: “No invoice INV-2041 in this account. Call billing_search_invoices with customer_email to find the right number.” A bare 404 or stack trace teaches the model nothing.
A minimal server with the official TypeScript SDK
Version 2 of the TypeScript SDK shipped on 27 July 2026 alongside the new spec, as @modelcontextprotocol/server; the v1 @modelcontextprotocol/sdk line gets fixes for at least six months.8 Tools are registered with registerTool and a Zod v4 schema; the SDK rejects invalid arguments with an isError result before your handler runs and turns thrown exceptions into tool errors.9 createMcpHandler builds a fresh server per request and returns a web-standard fetch handler; it does not check tokens or headers itself, so authentication goes in front of it.10 The sketch below uses the SDK’s bearer-token gate and protected resource metadata helper,11 with jose for JWT verification:
import {
createMcpHandler, getOAuthProtectedResourceMetadataUrl, McpServer,
OAuthError, OAuthErrorCode, oauthMetadataResponse, requireBearerAuth,
} from '@modelcontextprotocol/server';
import type { AuthInfo } from '@modelcontextprotocol/server';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import * as z from 'zod/v4';
const MCP_URL = new URL('https://mcp.example.com/mcp');
const ISSUER = 'https://auth.example.com';
const JWKS = createRemoteJWKSet(new URL(ISSUER + '/.well-known/jwks.json'));
const oauthMetadata = await fetch(ISSUER + '/.well-known/oauth-authorization-server')
.then((r) => r.json());
async function verifyAccessToken(token: string): Promise<AuthInfo> {
try {
// audience = this server: tokens minted for any other resource are rejected
const { payload } = await jwtVerify(token, JWKS, { issuer: ISSUER, audience: MCP_URL.href });
return {
token,
clientId: String(payload.client_id ?? payload.sub),
scopes: String(payload.scope ?? '').split(' ').filter(Boolean),
expiresAt: payload.exp,
extra: { tenantId: payload.tenant_id, userId: payload.sub },
};
} catch {
throw new OAuthError(OAuthErrorCode.InvalidToken, 'token rejected');
}
}
const Invoice = z.object({
invoice_id: z.string(), number: z.string(), customer_name: z.string(),
status: z.string(), amount_due_minor: z.number().int(), currency: z.string(),
});
function buildServer() {
const server = new McpServer({ name: 'acme-billing', version: '2.3.0' });
server.registerTool(
'billing_search_invoices',
{
title: 'Search invoices',
description: 'Find invoices in the signed-in customer account. Use this to get an ' +
'invoice_id before resending or refunding. Newest first; amounts in cents.',
inputSchema: z.object({
status: z.enum(['open', 'paid', 'overdue', 'void']).optional()
.describe('Filter by status. Omit to include all.'),
customer_email: z.email().optional().describe('Exact billing email, if known.'),
limit: z.number().int().min(1).max(25).default(10),
cursor: z.string().optional().describe('next_cursor from a previous call.'),
}),
outputSchema: z.object({ invoices: z.array(Invoice), next_cursor: z.string().nullable() }),
annotations: { readOnlyHint: true, openWorldHint: false },
},
async (args, ctx) => {
const caller = ctx.http?.authInfo;
if (!caller?.scopes.includes('invoices:read')) {
return { isError: true, content: [{ type: 'text',
text: 'Missing permission invoices:read. Ask the user to reconnect and grant invoice access.' }] };
}
// tenant comes from the verified token, never from tool arguments
const page = await invoiceStore.search(String(caller.extra?.tenantId), args);
const output = { invoices: page.items, next_cursor: page.nextCursor ?? null };
return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
},
);
return server;
}
const handler = createMcpHandler(buildServer);
const gate = requireBearerAuth({
verifier: { verifyAccessToken },
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(MCP_URL),
});
export default {
async fetch(request: Request): Promise<Response> {
const doc = oauthMetadataResponse(request, {
oauthMetadata, resourceServerUrl: MCP_URL, scopesSupported: ['invoices:read'],
});
if (doc) return doc; // RFC 9728 protected resource metadata
const auth = await gate(request);
if (auth instanceof Response) return auth; // 401 + WWW-Authenticate: resource_metadata=...
return handler.fetch(request, { authInfo: auth });
},
};In production, also validate Origin in front of the handler and emit an audit event from every tool.
Authorization under the 2026-07-28 spec
Authorization is optional in MCP, but an HTTP server should implement it as specified; a stdio server takes credentials from its environment instead.12 The model is OAuth 2.1: your MCP server is a resource server that validates tokens, and an authorization server (usually your existing identity provider) issues them.
The discovery flow
- The client calls your endpoint without a token. You return
401with aWWW-Authenticate: Bearerheader carryingresource_metadata(the URL of your protected resource metadata) and, ideally, thescopeneeded. - The client fetches your OAuth 2.0 Protected Resource Metadata (RFC 9728), which MCP servers must publish. It names your authorization servers.
- The client discovers the authorization server through RFC 8414 or OpenID Connect Discovery, registers (Client ID Metadata Documents are preferred; Dynamic Client Registration is now deprecated but still allowed; pre-registration also works), and runs the authorization code flow with PKCE and a
resourceparameter naming your server’s canonical URI. - Every subsequent request carries
Authorization: Bearer, never a token in the query string.12
Token validation and no passthrough
Your server must check that each token was issued for it as the audience, return 401 for invalid or expired tokens, and must not accept or forward any other token.12 The security guide calls forwarding a client’s token to a downstream API “token passthrough” and explains why it is forbidden: it bypasses downstream controls and destroys the audit trail.13 To call your internal APIs, exchange the user’s token for a downstream credential or use a service identity that carries user and tenant as verified claims.
Scopes and step-up
Keep scopes_supported to the minimal set needed for basic use, and ask for more only when a privileged operation is first attempted. When a token lacks a scope, the spec says to return 403 with error="insufficient_scope" and all the scopes the operation needs in a single challenge.12 The security guide lists the common mistakes: publishing every scope, wildcard scopes such as * or full-access, and bundling unrelated privileges to avoid future prompts.13 A practical scope set for our billing example: invoices:read by default, invoices:send and refunds:write requested on first use. In SDK v2.0.0, a scope that only some tools need is checked inside the handler, as in the sketch above; responding 403 at the HTTP layer instead triggers the client’s step-up flow.11
Per-user consent when you proxy a third-party API
If your server fronts someone else’s API with a single static OAuth client ID, a malicious client can ride an existing consent cookie and receive an authorization code meant for the user (a confused deputy). MCP’s security guidance requires such proxies to record approved client IDs per user, show their own consent page naming the client, scopes and redirect URI, and match redirect URIs exactly.13
Least privilege and confirmations for write actions
Tool annotations tell the host how risky a tool is. Their defaults assume the worst: a tool with no annotations is treated as not read-only, destructive, not idempotent and open-world.7 Set them honestly, but do not rely on them for safety: clients must treat annotations as untrusted unless the server is trusted.5 Hosts are asked to prompt for confirmation on sensitive operations, but servers are the ones required to validate inputs, enforce access control and rate-limit.5 So we tier tools and attach controls on the server:
| Tier | Examples | Annotations | Server-side controls |
|---|---|---|---|
| Read | Search invoices, get usage | readOnlyHint: true | Tenant scoping from the token, result caps, base scope only |
| Reversible write | Add a note, resend an invoice | destructiveHint: false, idempotentHint where true | Separate scope, idempotency key, audit event |
| Irreversible | Issue a refund, delete a contact | destructiveHint: true | Separate scope, business-rule limits, explicit user confirmation |
For confirmations, do not depend only on the host showing a dialog. For irreversible actions we use a two-step pattern: billing_prepare_refund validates the request and returns a human-readable preview plus a short-lived confirmation ID bound to the user and the exact arguments; billing_issue_refund requires that ID. Above a threshold, the commit step asks the user directly. Under 2026-07-28 a server does this by returning an input_required result carrying an elicitation request, which the client answers by retrying the call.3 Where the client does not support elicitation, fall back to approval inside your own product.
Rate limits, audit logging and multi-tenancy
A confused agent can call the same tool hundreds of times. Rate-limit per user and per tenant, not just per IP. A sizing example with assumed numbers: if a normal agent task makes about 15 tool calls over two minutes, a per-user limit of 60 calls a minute with a burst of 20 leaves plenty of headroom while capping a runaway loop at 3,600 calls an hour. Return rate-limit failures as tool errors that say when to retry. The Mcp-Method and Mcp-Name headers let you enforce per-tool limits at the gateway.4
Log every call as an audit event: tenant, user, OAuth client ID, tool, argument hash, result, latency and records touched. When a customer asks what the assistant did in their account last Tuesday, you should be able to answer from one query.
For multi-tenancy, derive the tenant from the verified token on every call and never accept it as a tool argument. The spec allows tools/list to vary by the authorization on the request, for example hiding write tools from a read-only token, but not by connection.5 State handles need the same care: the security guide says to generate them randomly, bind them server-side to the authenticated user, and never treat possession of a handle as authentication.13
Testing and evals for tool selection and correctness
Test two things separately. Correctness (does each tool behave for valid and invalid inputs, per tenant and scope?) is ordinary integration testing; SDK v2 lets you drive the real handler in process by passing handler.fetch as the client transport’s fetch.14 Selection (does a model pick the right tool with the right arguments, and leave write tools alone when it should?) needs an eval run several times against the models your customers use.
// evals/tool-selection.ts — run nightly and on every change to tool definitions
const cases = [
{ prompt: "Which of Globex's invoices are overdue?",
expect: 'billing_search_invoices', args: { status: 'overdue' } },
{ prompt: 'Send invoice INV-2041 to the customer again',
expect: 'billing_resend_invoice', args: { invoice_number: 'INV-2041' } },
{ prompt: 'How many API calls did we bill in July?', expect: 'billing_get_usage' },
{ prompt: 'Refund everything we charged last year', // should clarify, not act
expect: null, forbid: ['billing_issue_refund', 'billing_prepare_refund'] },
];
const { tools } = await client.listTools(); // real definitions from the real server
const TRIALS = 5;
for (const c of cases) {
const trials = [];
for (let i = 0; i < TRIALS; i++) {
const call = await firstToolCall(model, tools, c.prompt); // your provider's tool-use API
trials.push({
selected: (call?.name ?? null) === c.expect,
argsOk: !c.args || isSubset(c.args, call?.arguments ?? {}),
forbidden: (c.forbid ?? []).includes(call?.name ?? ''),
});
}
report(c, {
passAll: trials.every((t) => t.selected && t.argsOk && !t.forbidden), // pass^k
selectionRate: trials.filter((t) => t.selected).length / TRIALS,
forbiddenCalls: trials.filter((t) => t.forbidden).length, // must be 0
});
}Start with 40 to 60 cases from real support tickets and demos, including near-misses between similar tools and requests that should trigger no write. At five trials each that is 200 to 300 model calls per model; at an assumed few cents per call, a full run costs a few dollars, cheap enough for every description change. Fix confused tool pairs in the descriptions, not the prompt. Anthropic also recommends tracking tool-call counts and tool errors per task, and keeping a held-out set so you do not overfit descriptions to your evals.6 Our guide to retrofitting evals covers building the case set from production traces.
Versioning and backward compatibility
Protocol revisions
Protocol versions are dates that mark the last backward-incompatible change. Under 2026-07-28 each request declares its version; a server that does not support it returns UnsupportedProtocolVersionError listing what it does support, and every server must implement server/discover so clients can check up front.1 Two weeks after the release, many deployed hosts will still speak the handshake-based 2025 revisions, so plan to serve both for a while. SDK v2’s handler serves 2025-era clients statelessly from the same factory by default, and legacy: 'reject' makes an endpoint modern-only once your traffic says you can.15 If your server relied on Mcp-Session-Id for state, move it into explicit handles first; the announcement names this as the main migration cost.2 Deprecated features (now including roots, sampling, logging, the old HTTP+SSE transport and Dynamic Client Registration) normally remain for at least twelve months.3
Your tool contract
Tool names and schemas are an API consumed by models, by hosts that cache your tool list, and by customers’ saved workflows:
- Additive changes are safe: new optional parameters, output fields and tools.
- Breaking changes get a new name: removing a parameter, tightening a type or changing semantics means
billing_search_invoices_v2alongside the old tool. Mark the old one deprecated in its description and remove it when its call volume is negligible. - Keep listings stable: return tools in a deterministic order, which the spec recommends for client caching and prompt-cache hits, and set sensible
ttlMsvalues, now required on list results.3 - Re-run selection evals on every description change. A reworded description is a behaviour change.
Distribution: getting your server in front of users
When the Linux Foundation formed the Agentic AI Foundation as MCP’s new home in December 2025, it listed Claude, Cursor, Microsoft Copilot, Gemini, VS Code and ChatGPT among MCP’s adopters.16 For a remote server, distribution means three things:
- A URL and a docs page listing the endpoint, scopes, tools and an example prompt per tool.
- Directories. Anthropic launched a directory of Claude connectors in July 2025,17 and OpenAI opened app submissions with an in-product app directory in December 2025; ChatGPT apps are built on MCP, and a submission includes MCP connection details and testing guidelines.18 Expect a review process for any curated directory, so have a demo account and a security answer sheet ready.
- The MCP Registry. The project’s own registry, an open catalogue and API for publicly available servers, launched in preview in September 2025.19
Failure modes we see in MCP servers
| Failure | Symptom | Fix |
|---|---|---|
| Endpoint mirroring | Model chains five calls for one request, or picks the wrong near-duplicate tool | Consolidate into task-shaped tools; selection evals on confused pairs |
| Context flooding | One call returns thousands of rows; the model loses the instructions | Small default page size, cursors, filters, concise output fields |
| Token passthrough or admin keys | Server works for every tenant with one credential; audit trail shows only the server | Audience-checked tokens, per-user downstream credentials |
| Tenant from arguments | A crafted prompt makes the model pass another tenant’s ID | Tenant and user only from verified token claims |
| Silent contract changes | Customer workflows break after a deploy; selection quality drops | Additive changes only, versioned tool names, evals in CI |
| Session-bound state | Multi-step flows break behind a load balancer or after the 2026-07-28 upgrade | Explicit, user-bound state handles with stated lifetimes |
Production MCP server checklist
- Remote Streamable HTTP server,
Originvalidated, stateless behind the load balancer. - Fewer than a dozen task-shaped tools with prefixed names and descriptions written for a new colleague.
- Tight input schemas, declared output schemas, small pages with opaque cursors.
- Tool errors returned with
isErrorand a concrete next step. - Protected resource metadata published; tokens audience-checked; no passthrough.
- Minimal default scopes, step-up for writes, single complete scope challenges.
- Honest annotations, plus server-side tiers: separate scopes, limits and confirmation for irreversible actions.
- Per-user and per-tenant rate limits, and one audit event per call.
- Tenant and user taken only from the token; state handles bound to the user.
- Integration tests through the real handler and a selection eval on every definition change.
- 2025-era clients still served; tool contract changed additively, with new names for breaking changes.
Most of this is ordinary API discipline applied to an unusual consumer. If you want a second pair of hands designing or hardening an MCP server for your product, that is what our Agent-Ready Integration work covers.
Sources
- Model Context Protocol specification, “Versioning” (current revision 2026-07-28) — modelcontextprotocol.io/specification/versioning
- Model Context Protocol Blog, “The 2026-07-28 Specification,” 28 Jul 2026 — blog.modelcontextprotocol.io/posts/2026-07-28/
- Model Context Protocol specification 2026-07-28, “Key Changes” — modelcontextprotocol.io/specification/2026-07-28/changelog
- Model Context Protocol specification 2026-07-28, “Streamable HTTP” — modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
- Model Context Protocol specification 2026-07-28, “Tools” — modelcontextprotocol.io/specification/2026-07-28/server/tools
- Anthropic Engineering, “Writing effective tools for AI agents — with agents,” 11 Sep 2025 — www.anthropic.com/engineering/writing-tools-for-agents
- Model Context Protocol specification 2026-07-28, “Schema Reference” (ToolAnnotations, Cursor) — modelcontextprotocol.io/specification/2026-07-28/schema
- MCP TypeScript SDK v2.0.0 (released 27 Jul 2026), README — github.com/modelcontextprotocol/typescript-sdk/blob/%40modelcontextprotocol%2Fserver%402.0.0/README.md
- MCP TypeScript SDK v2.0.0 docs, “Tools” and “Errors” — github.com/modelcontextprotocol/typescript-sdk/blob/%40modelcontextprotocol%2Fserver%402.0.0/docs/servers/tools.md
- MCP TypeScript SDK v2.0.0 docs, “Serve over HTTP” — github.com/modelcontextprotocol/typescript-sdk/blob/%40modelcontextprotocol%2Fserver%402.0.0/docs/serving/http.md
- MCP TypeScript SDK v2.0.0 docs, “Require authorization” — github.com/modelcontextprotocol/typescript-sdk/blob/%40modelcontextprotocol%2Fserver%402.0.0/docs/serving/authorization.md
- Model Context Protocol specification 2026-07-28, “Authorization” — modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- Model Context Protocol, “Security Best Practices” (2026-07-28 docs) — modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices
- MCP TypeScript SDK v2.0.0 docs, “Test a server” — github.com/modelcontextprotocol/typescript-sdk/blob/%40modelcontextprotocol%2Fserver%402.0.0/docs/testing.md
- MCP TypeScript SDK v2.0.0 docs, “Support legacy clients” — github.com/modelcontextprotocol/typescript-sdk/blob/%40modelcontextprotocol%2Fserver%402.0.0/docs/serving/legacy-clients.md
- Linux Foundation, “Linux Foundation Announces the Formation of the Agentic AI Foundation,” 9 Dec 2025 — www.linuxfoundation.org/press/linux-foundation-announces-the-formation-of-the-agentic-ai-foundation
- Anthropic, “Discover tools that work with Claude,” 14 Jul 2025 — claude.com/blog/connectors-directory
- OpenAI, “Developers can now submit apps to ChatGPT,” 17 Dec 2025 — openai.com/index/developers-can-now-submit-apps-to-chatgpt/
- Model Context Protocol Blog, “Introducing the MCP Registry,” 8 Sep 2025 — blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/
Specification and SDK details reflect the 2026-07-28 MCP revision and TypeScript SDK v2.0.0 as of August 2026. Token counts, rate limits and eval costs are illustrative assumptions, not measurements. Code samples are sketches rather than drop-in implementations.
