KavachOS is open source. Cloud launching soon.
kavachOS

02/TUTORIAL

Building an MCP tool server with authend to end

Scaffold the server, wire up OAuth, write tools, test with Claude. Complete code, no skipped steps.

GD

Gagan Deep Singh

Founder, GLINCKER

Published

April 4, 202615 min read

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.

bash
npm create cloudflare@latest mcp-tool-server -- --template hono
cd mcp-tool-server
npm install kavachos @hono/zod-validator zod

Open wrangler.toml and add your API key as a secret binding. Never hardcode it:

tomlwrangler.toml
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_KEY
typescriptsrc/kavach.ts
import { 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.

typescriptsrc/tools.ts
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:

OAuth from scratch
  • 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
With kavachOS
  • 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:

typescriptsrc/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.

bash
# Start your Worker locally
npx wrangler dev

# In another terminal, point the inspector at your local server
npx @modelcontextprotocol/inspector http://localhost:8787/mcp

The 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.

bash
# 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/mcp

Update 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:

jsonclaude_desktop_config.json
{
  "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

Read next

Share this post

Get started

Try kavachOS Cloud free

Free up to 1,000 MAU. Full MCP OAuth 2.1 on every plan.