How to Build and Secure an MCP Server for AI Agents
Build and secure an MCP server so AI agents can use your tools: HTTP transport, authentication, and rate limiting, step by step.
What MCP gives you
The Model Context Protocol (MCP) turns your app’s functions into tools an agent can call — listing data, creating records, running actions — over a standardized interface. Build it once and Claude, local models, and other MCP clients can all use it.
Step 1 — Install the SDK
npm install @modelcontextprotocol/server zod
Step 2 — Define a tool
src/server.ts:
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.registerTool(
"get_weather",
{ description: "Get the current weather for a city.", inputSchema: { city: z.string() } },
async ({ city }) => ({
content: [{ type: "text", text: `Sunny, 22°C in ${city}.` }],
})
);
The Zod schema becomes the agent-visible contract — write good descriptions; the model reads them to decide when to call the tool.
Step 3 — Expose it over HTTP
Wrap the server in a Streamable HTTP handler and require auth:
import { createMcpHandler, requireBearerAuth } from "@modelcontextprotocol/server";
const gate = requireBearerAuth({
verifier: {
async verifyAccessToken(token) {
if (token !== process.env.ADMIN_PASSWORD) {
throw new Error("Invalid token");
}
return { token, clientId: "devnotes", scopes: [] };
},
},
});
const handler = createMcpHandler(() => server);
export default {
async fetch(request: Request) {
const auth = await gate(request);
if (auth instanceof Response) return auth;
return handler.fetch(request, { authInfo: auth });
},
};
Step 4 — Add rate limiting
Cap failed auth attempts and overall requests per IP:
const buckets = new Map<string, number>();
function rateLimit(key: string, max: number, windowMs: number): boolean {
const now = Date.now();
const count = buckets.get(key) || 0;
if (count >= max) return false;
buckets.set(key, count + 1);
setTimeout(() => buckets.delete(key), windowMs);
return true;
}
In production, prefer Cloudflare’s rate-limit rules over in-memory maps, which reset across edge isolates.
Step 5 — Prefer OAuth over long-lived tokens
A static bearer token shared in config files is the weakest link in any MCP deployment: it can’t be rotated without breaking every client, and it lives in plaintext wherever the client config lives.
If your server is reachable over the internet, implement the OAuth 2.1 authorization code flow with PKCE instead — MCP has a well-defined spec for it. The client opens a browser, the user approves once, and the server issues short-lived access tokens (plus a refresh token). A leaked access token dies in days instead of living forever, and revoking access means rejecting the refresh, not redeploying.
The protocol-level details live in the MCP spec’s authorization section; the principle that matters here is: short-lived, per-user, revocable beats shared-and-permanent.
Step 6 — Connect Claude Code
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "https://example.com/mcp",
"headers": { "Authorization": "Bearer <ADMIN_PASSWORD>" }
}
}
}
Restart Claude Code and run /mcp to confirm the tools appear.
Security checklist
- Authenticate every request — never expose tools anonymously.
- Compare secrets in constant time — avoid timing side channels.
- Validate all inputs — the Zod schema is the minimum; enforce invariants server-side too.
- Allowlist redirect URIs if you add OAuth, or a malicious client can phish a code.
- Rate-limit at the edge, not just in-process — in-memory maps reset across serverless isolates. Cloudflare’s edge rules and KV-backed counters survive restarts; see cloudflare-waf-rate-limiting for the full picture.
- Return drafts, don’t auto-commit — let the human/agent decide what to save.
- Audit your tool list — every registered tool is an attack surface. If a tool can write or delete, it needs the same scrutiny as a database admin account; read-only tools should be the default.
Summary
You have a secured, HTTP-served MCP server whose tools agents can discover and call. From here, add OAuth for hosted connectors or a stdio transport for local models.
Comments
One comment per thread every 30 minutes · edits are unlimited.