ayjnt
All examples
mcpllm-tools

MCP tools agent

Build an MCP server for LLM tool-calling. ayjnt detects `McpAgent` as a base class and dispatches through `McpAgent.serve()` automatically — streamable HTTP and SSE transports handled for you.

What you'll learn
  • How `extends McpAgent` changes the dispatch path
  • How to register tools with `server.tool(name, schema, handler)`
  • URL shape trade-offs for MCP agents (no `:instanceId` — sessions live in headers)
01 step

Start from the blank scaffold

Every ayjnt example starts here. `bunx ayjnt new` drops a one-agent project with a single `alive` agent that responds "I'm alive" to any request — enough to prove the pipeline works before you replace it with the real thing.

~/my-agent-app
my-app/ (blank scaffold)
agent.ts
package.json
tsconfig.json
.gitignore
README.md
02 step

Add the MCP dependency

The Agents SDK's `McpAgent` uses the reference MCP SDK for schema validation + transport. Add it along with zod for schema definitions.

~/my-agent-app
03 step

Extend McpAgent, register tools

ayjnt detects the `extends McpAgent` clause source-level (a regex — don't alias the import) and routes `/tools` through `Tools.serve("/tools", { binding: "TOOLS" }).fetch(...)` instead of the normal Agent dispatch. The MCP transport layer (streamable-http, SSE, session management) is handled for you.

agents/tools/agent.ts ts
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { GeneratedEnv } from "@ayjnt/env";

type State = { invocations: number };

export default class Tools extends McpAgent<GeneratedEnv, State> {
  override initialState: State = { invocations: 0 };

  server = new McpServer({ name: "my-tools", version: "0.1.0" });

  async init() {
    this.server.tool("echo", "Echo the input.", { text: z.string() },
      async ({ text }) => {
        this.setState({ invocations: this.state.invocations + 1 });
        return { content: [{ type: "text", text }] };
      });

    this.server.tool("add", "Add two numbers.",
      { a: z.number(), b: z.number() },
      async ({ a, b }) => {
        this.setState({ invocations: this.state.invocations + 1 });
        return { content: [{ type: "text", text: String(a + b) }] };
      });
  }
}
04 step

Plug into Claude Desktop

Once deployed, point any MCP client at your worker's URL. Streamable-http and SSE transports are served from the same path. Session id lives in the `Mcp-Session-Id` header — one DO instance per session, created automatically.

~/Library/Application Support/Claude/claude_desktop_config.json jsonc
{
  "mcpServers": {
    "my-tools": {
      "url": "https://my-app.<account>.workers.dev/tools"
    }
  }
}
05 step

What it looks like

Open Claude Desktop — the tools show up in the attachment menu. Ask it to add two numbers and it calls `add(a, b)`; ask it to repeat text and it calls `echo`.

Claude Desktop — your MCP server result
  ┌──────────────── Claude Desktop ────────────────┐
  │ ≡  my-tools  ✓ connected                        │
  │ ─────────────────────────────────────────────── │
  │                                                 │
  │  User: can you add 17 and 25?                   │
  │                                                 │
  │  Claude: I'll use the add tool.                 │
  │    ▸ tool call: add({ a: 17, b: 25 })   ◀──── HTTPS POST /tools
  │    ▸ result: 42                          ◀──── McpAgent DO
  │                                                 │
  │  The answer is 42.                              │
  │                                                 │
  │  [📎 my-tools ▾]   ask anything…   [send]      │
  └─────────────────────────────────────────────────┘
06 step

Deploy

`ayjnt deploy` checks your git tree is clean + synced with origin, regenerates the wrangler config from scratch, then shells out to `wrangler deploy`. The committed migrations.json file is the source of truth for what's in production.

~/my-agent-app