The Model Context Protocol (MCP) is how AI clients like Claude Desktop discover and call your tools. The spec is good. The auth story is not. MCP 2025-03-26 requires OAuth 2.1 with PKCE, protected resource metadata at /.well-known/oauth-protected-resource, authorization server metadata at /.well-known/oauth-authorization-server, and dynamic client registration. Rolling that yourself takes a week. This tutorial takes an afternoon.
We will build a real MCP tool server on Cloudflare Workers with Hono, add kavachOS for the OAuth layer, test it with the MCP inspector locally, deploy it to a custom domain, and connect it to Claude Desktop. Every file is shown in full.
01
Project setup on Cloudflare Workers and Hono
Hono runs everywhere but it is particularly good on Cloudflare Workers. The bundle is tiny, the routing is fast, and the context API gives you typed bindings to KV, Durable Objects, and D1 without boilerplate.
npm create cloudflare@latest mcp-tool-server -- --template hono
cd mcp-tool-server
npm install kavachos @hono/zod-validator zodOpen wrangler.toml and add your API key as a secret binding. Never hardcode it:
name = "mcp-tool-server"
main = "src/index.ts"
compatibility_date = "2025-01-01"
[vars]
MCP_SERVER_NAME = "my-tools"
MCP_SERVER_VERSION = "1.0.0"
# Add your secret via: npx wrangler secret put KAVACHOS_API_KEYimport { createKavach } from 'kavachos';
export function getKavach(apiKey: string) {
return createKavach({ apiKey });
}02
Defining your tools
Tools in MCP are JSON Schema objects with a name, description, and input schema. When Claude (or any MCP client) decides to call your tool, it sends the input as a JSON object matching that schema. You validate it and return a result.
Start with two tools: one that searches a knowledge base and one that creates a document. We will define the schemas first, then wire them to handlers.
import { z } from 'zod';
export const TOOLS = [
{
name: 'search_knowledge_base',
description: 'Search the team knowledge base for relevant articles and documents.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query',
},
limit: {
type: 'number',
description: 'Maximum number of results to return (default 5)',
default: 5,
},
},
required: ['query'],
},
},
{
name: 'create_document',
description: 'Create a new document in the team knowledge base.',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string' },
content: { type: 'string' },
tags: {
type: 'array',
items: { type: 'string' },
},
},
required: ['title', 'content'],
},
},
] as const;
export const SearchInput = z.object({
query: z.string().min(1),
limit: z.number().int().min(1).max(20).default(5),
});
export const CreateDocInput = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
tags: z.array(z.string()).default([]),
});The TOOLS array gets served at the tools/list endpoint. The Zod schemas validate runtime input so you never pass garbage to your business logic.
03
Adding OAuth 2.1 with kavachOS
This is the hard part without a library. MCP requires four spec-compliant endpoints and PKCE on every authorization code exchange. With kavachOS, you mount one adapter and the endpoints appear automatically.
Here is what you are trading off against doing it yourself:
- Write authorization server metadata endpoint (RFC 8414)
- Write protected resource metadata endpoint (RFC 9728)
- Implement PKCE code challenge verification (RFC 7636)
- Build dynamic client registration (RFC 7591)
- Manage token storage, rotation, and revocation
- Handle audience binding (RFC 8707) for each resource
kavach.mcp.hono(app)mounts all four required endpoints- PKCE enforced by default, cannot be disabled
- Dynamic registration enabled automatically
- Tokens stored in kavachOS Cloud, not your Worker
- Revocation and rotation handled for you
The adapter call sits in your main src/index.ts:
import { Hono } from 'hono';
import { getKavach } from './kavach';
import { TOOLS, SearchInput, CreateDocInput } from './tools';
import { handleSearch, handleCreateDoc } from './handlers';
type Bindings = {
KAVACHOS_API_KEY: string;
MCP_SERVER_NAME: string;
MCP_SERVER_VERSION: string;
};
const app = new Hono<{ Bindings: Bindings }>();
// Mount kavachOS MCP adapter -- provides all OAuth 2.1 endpoints
app.use('*', async (c, next) => {
const kavach = getKavach(c.env.KAVACHOS_API_KEY);
// Attaches /.well-known/*, /authorize, /token, /register
await kavach.mcp.hono(app, {
serverName: c.env.MCP_SERVER_NAME,
serverVersion: c.env.MCP_SERVER_VERSION,
// Scopes your tools require
requiredScopes: ['read:knowledge', 'write:knowledge'],
});
return next();
});
// MCP protocol endpoint
app.post('/mcp', async (c) => {
const kavach = getKavach(c.env.KAVACHOS_API_KEY);
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (!token) {
return c.json({ error: 'Unauthorized' }, 401);
}
const identity = await kavach.mcp.verifyToken(token);
if (!identity.valid) {
return c.json({ error: 'Invalid token' }, 401);
}
const body = await c.req.json();
const { method, params, id } = body;
if (method === 'tools/list') {
return c.json({
jsonrpc: '2.0',
id,
result: { tools: TOOLS },
});
}
if (method === 'tools/call') {
const { name, arguments: args } = params;
if (name === 'search_knowledge_base') {
const input = SearchInput.parse(args);
const results = await handleSearch(input, identity);
return c.json({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(results) }] } });
}
if (name === 'create_document') {
const input = CreateDocInput.parse(args);
// Check write scope before creating
if (!identity.scopes.includes('write:knowledge')) {
return c.json({ jsonrpc: '2.0', id, error: { code: -32603, message: 'Forbidden' } });
}
const doc = await handleCreateDoc(input, identity);
return c.json({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(doc) }] } });
}
return c.json({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
}
return c.json({ jsonrpc: '2.0', id, error: { code: -32600, message: 'Invalid request' } });
});
export default app;The full spec compliance lives inside kavach.mcp.hono(). You only write the tool dispatch logic. See the MCP OAuth guide for the full list of endpoints it registers and how to customize the authorization UI.
Dynamic client registration deserves a note. MCP clients register themselves on first contact: they POST their metadata to your /register endpoint and get a client ID back. You do not pre-configure clients. They self-register. kavachOS stores these registrations and enforces redirect URI matching on every subsequent flow. The MCP OAuth guide explains how to whitelist redirect URI patterns and revoke a rogue client.
04
Testing locally with the MCP inspector
Anthropic ships a CLI tool called the MCP inspector. It connects to any MCP server, runs the OAuth flow in a browser window, and lets you call tools interactively. This is the fastest way to catch issues before deploying.
# Start your Worker locally
npx wrangler dev
# In another terminal, point the inspector at your local server
npx @modelcontextprotocol/inspector http://localhost:8787/mcpThe inspector opens a browser tab. It hits your /.well-known/oauth-protected-resource endpoint, discovers the authorization server, registers itself, and starts the PKCE flow. If any of those steps fail, the inspector shows you the exact HTTP exchange that went wrong.
Once the inspector is connected you should see your tool list on the left and a call panel on the right. Type a query into search_knowledge_base and watch the JSON response come back. If it does, your OAuth layer is working.
05
Deploying to a custom domain
Deploying to Cloudflare takes one command. The custom domain step requires a zone you control in Cloudflare DNS, which takes about two minutes to configure.
# Deploy the Worker
npx wrangler deploy
# Add your API key as a secret (run once)
npx wrangler secret put KAVACHOS_API_KEY
# Add a custom domain route in the Cloudflare dashboard, or via wrangler:
npx wrangler routes put tools.yourdomain.com/mcpUpdate your kavachOS project settings with the production URL so the OAuth metadata endpoints advertise the right issuer. In the dashboard, go to your project settings and set MCP_RESOURCE_URL to https://tools.yourdomain.com. The issuer in your token responses must match this value exactly or clients will reject them.
06
Connecting Claude Desktop
Claude Desktop reads MCP server config from a JSON file. On macOS it lives at ~/Library/Application Support/Claude/claude_desktop_config.json. Add your server under the mcpServers key:
{
"mcpServers": {
"my-tools": {
"url": "https://tools.yourdomain.com/mcp",
"transport": "http"
}
}
}Restart Claude Desktop. It will hit your /.well-known/oauth-protected-resource endpoint, register itself, and open a browser window for the OAuth flow on first use. After you authorize, Claude caches the token and all future calls go straight to your tool handler. You should see your tools listed in the Claude Desktop sidebar under “Tools.”
From here you can extend the server with more tools, add webhook support via Durable Objects for long-running tasks, or set up per-user data isolation using kavachOS agent identities. The scaffold you just built handles all of those without touching the OAuth layer again.
Topics
- #MCP tool server
- #MCP OAuth tutorial
- #Model Context Protocol
- #agent auth tutorial
- #Cloudflare Workers MCP
Keep going in the docs
MCP
MCP OAuth 2.1
Full OAuth 2.1 authorization server for MCP, compliant with RFC 9728, 8414, and 7591.
Reference
Hono adapter
Deploy kavachOS at the edge with Hono on Cloudflare Workers, Deno Deploy, or Bun.
Agents
Agent identity
Cryptographic bearer tokens, wildcard permissions, and per-agent budgets. The core primitive.
Read next
- Engineering
MCP OAuth 2.1 explained: PKCE, metadata, and dynamic registration
A close reading of the MCP auth spec: what each RFC covers, where clients can go wrong, and how to test compliance.
10 min read - Tutorial
How to add auth to a Next.js AI agent in ten minutes
A working app with user login, agent identity, and MCP OAuth, in ten minutes flat. Copy the code, ship it.
8 min read
Share this post
Get started
Try kavachOS Cloud free
Free up to 1,000 MAU. Full MCP OAuth 2.1 on every plan.