CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-03-29Intermediate

Claude MCP Hybrid Architecture — Design Patterns for Combining Deterministic Tools with AI Reasoning

Learn how to build reliable AI agents using Claude MCP hybrid architecture. Combine deterministic tools with AI reasoning using patterns inspired by Andrew Ng's Tool Use framework.

MCP45hybrid-architecture2Tool Use8design-patterns3agents7

Premium Article

What Is Claude MCP Hybrid Architecture?

When building AI agents, relying entirely on LLMs for every task is both costly and unpredictable. On the other hand, traditional deterministic code alone can't handle nuanced reasoning. Claude MCP hybrid architecture bridges these two worlds by combining the strengths of both approaches.

In this design pattern, MCP servers provide deterministic tools — database queries, calculations, file operations — while Claude handles probabilistic reasoning — intent interpretation, decision-making, and natural language generation. This separation echoes what Stanford professor Andrew Ng has advocated in his agentic design framework: LLMs should function as orchestrators, delegating precise operations to deterministic tool calls rather than attempting everything through generation.

This article walks you through the core design patterns for building hybrid architectures with Claude MCP, complete with working code examples. If you're new to MCP, MCP Practical Guide covers the fundamentals.

Implementation Foundation: Building an MCP Server From Scratch

To realize the "deterministic tool layer" in hybrid architecture, you need an actual MCP server. When the existing MCP servers (GitHub, Slack, Notion, etc.) don't fit your use case, building one from scratch in Node.js + TypeScript is the right path. The following sections walk through the structure, steps, and operational patterns required for a self-built MCP server.

MCP Server Architecture

Core Concepts

An MCP server can expose three types of capabilities:

  1. Tools: Functions Claude can invoke (data retrieval, computations, external API calls)
  2. Resources: Data sources Claude can read (files, DB records)
  3. Prompts: Reusable prompt templates

This tutorial focuses on Tools — the most practical capability for building a database search server.

Project Structure

src/
├── index.ts          # Entry point (server startup)
├── server.ts         # MCP server definition
├── tools/
│   ├── search.ts     # Search tool
│   └── stats.ts      # Statistics tool
└── lib/
    └── database.ts   # Database connection layer

Step 1: Create the Server Skeleton

Define the MCP server instance in src/server.ts:

// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 
export function createServer() {
  const server = new McpServer({
    name: "my-data-server",
    version: "1.0.0",
  });
 
  return server;
}

The entry point src/index.ts starts the server using the stdio transport:

// src/index.ts
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server.js";
import { registerSearchTool } from "./tools/search.js";
import { registerStatsTool } from "./tools/stats.js";
 
const server = createServer();
 
// Register tools
registerSearchTool(server);
registerStatsTool(server);
 
// Connect via stdio transport (used by Claude Desktop / Claude Code)
const transport = new StdioServerTransport();
await server.connect(transport);
 
// Expected behavior:
// Server starts and listens for MCP protocol messages via stdin/stdout

Step 2: Define Your Tools

Search Tool Implementation

Create src/tools/search.ts with Zod-validated parameters for type safety:

// src/tools/search.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { searchDatabase } from "../lib/database.js";
 
export function registerSearchTool(server: McpServer) {
  server.tool(
    "search_records",
    "Search database records by keyword. Results are sorted by relevance.",
    {
      query: z.string().describe("Search keyword"),
      limit: z.number().min(1).max(50).default(10).describe("Maximum number of results"),
      category: z.enum(["all", "articles", "users", "logs"]).default("all").describe("Category to search"),
    },
    async ({ query, limit, category }) => {
      try {
        const results = await searchDatabase(query, { limit, category });
 
        if (results.length === 0) {
          return {
            content: [
              {
                type: "text" as const,
                text: `No records found matching "${query}".`,
              },
            ],
          };
        }
 
        const formatted = results
          .map((r, i) => `${i + 1}. [${r.category}] ${r.title}\n   ${r.summary}\n   ID: ${r.id}`)
          .join("\n\n");
 
        return {
          content: [
            {
              type: "text" as const,
              text: `${results.length} results:\n\n${formatted}`,
            },
          ],
        };
      } catch (error) {
        const message = error instanceof Error ? error.message : "Unknown error";
        return {
          content: [{ type: "text" as const, text: `Search error: ${message}` }],
          isError: true,
        };
      }
    }
  );
}

Statistics Tool Implementation

// src/tools/stats.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { getStats } from "../lib/database.js";
 
export function registerStatsTool(server: McpServer) {
  server.tool(
    "get_statistics",
    "Retrieve database statistics including record count, category distribution, and last update time.",
    {
      category: z.enum(["all", "articles", "users", "logs"]).default("all"),
    },
    async ({ category }) => {
      const stats = await getStats(category);
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(stats, null, 2),
          },
        ],
      };
    }
  );
}
 
// Expected output when Claude calls this tool:
// {
//   "totalRecords": 1523,
//   "categories": { "articles": 890, "users": 412, "logs": 221 },
//   "lastUpdated": "2026-03-14T10:30:00Z"
// }

Step 3: Database Connection Layer

src/lib/database.ts abstracts data access. Replace with PostgreSQL or SQLite for production:

// src/lib/database.ts
interface Record {
  id: string;
  title: string;
  summary: string;
  category: string;
  score: number;
}
 
interface SearchOptions {
  limit: number;
  category: string;
}
 
// In-memory demo data (replace with actual DB in production)
const DEMO_DATA: Record[] = [
  { id: "001", title: "MCP Protocol Spec", summary: "Model Context Protocol technical specification", category: "articles", score: 0.95 },
  { id: "002", title: "Claude API Reference", summary: "Complete reference for the Anthropic Claude API", category: "articles", score: 0.88 },
  { id: "003", title: "Agent Design Patterns", summary: "Collection of AI agent design patterns", category: "articles", score: 0.82 },
];
 
export async function searchDatabase(query: string, options: SearchOptions): Promise<Record[]> {
  const lowerQuery = query.toLowerCase();
  let results = DEMO_DATA.filter(
    (r) =>
      r.title.toLowerCase().includes(lowerQuery) ||
      r.summary.toLowerCase().includes(lowerQuery)
  );
 
  if (options.category !== "all") {
    results = results.filter((r) => r.category === options.category);
  }
 
  return results.slice(0, options.limit);
}
 
export async function getStats(category: string) {
  const data = category === "all" ? DEMO_DATA : DEMO_DATA.filter((r) => r.category === category);
  const categories: { [key: string]: number } = {};
  data.forEach((r) => {
    categories[r.category] = (categories[r.category] || 0) + 1;
  });
 
  return {
    totalRecords: data.length,
    categories,
    lastUpdated: new Date().toISOString(),
  };
}

Step 4: Connecting to Claude Desktop

Configuration

To use your MCP server with Claude Desktop, edit the config file.

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

{
  "mcpServers": {
    "my-data-server": {
      "command": "node",
      "args": ["/path/to/my-mcp-server/dist/index.js"],
      "env": {
        "DATABASE_URL": "postgresql://localhost:5432/mydb"
      }
    }
  }
}

For development, use tsx to run TypeScript directly:

{
  "mcpServers": {
    "my-data-server": {
      "command": "npx",
      "args": ["tsx", "/path/to/my-mcp-server/src/index.ts"]
    }
  }
}

Claude Code Integration

For Claude Code, place a .mcp.json at the project root:

{
  "mcpServers": {
    "my-data-server": {
      "command": "node",
      "args": ["./dist/index.js"]
    }
  }
}

Step 5: Error Handling and Logging

In production, robust error handling is essential. MCP servers can use stderr for logging since stdout is reserved for MCP protocol communication:

// src/lib/logger.ts
export function log(level: "info" | "warn" | "error", message: string, data?: unknown) {
  const timestamp = new Date().toISOString();
  const entry = { timestamp, level, message, ...(data ? { data } : {}) };
  console.error(JSON.stringify(entry));
}
 
// Usage:
// log("info", "Search executed", { query: "MCP", results: 3 });
// Expected output (stderr):
// {"timestamp":"2026-03-14T10:30:00Z","level":"info","message":"Search executed","data":{"query":"MCP","results":3}}

Best practices for error handling within tools:

server.tool("risky_operation", "Call an external API endpoint", {
  endpoint: z.string().url(),
}, async ({ endpoint }) => {
  try {
    const response = await fetch(endpoint, {
      signal: AbortSignal.timeout(10000), // 10-second timeout
    });
 
    if (!response.ok) {
      return {
        content: [{ type: "text" as const, text: `API error: ${response.status} ${response.statusText}` }],
        isError: true,
      };
    }
 
    const data = await response.json();
    return {
      content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
    };
  } catch (error) {
    log("error", "External API call failed", { endpoint, error: String(error) });
    return {
      content: [{ type: "text" as const, text: `Connection error: ${error instanceof Error ? error.message : "Unknown"}` }],
      isError: true,
    };
  }
});

Step 6: Testing and Debugging

MCP Inspector

Use the built-in inspector to test your server interactively:

npx @modelcontextprotocol/inspector node dist/index.js

This opens http://localhost:5173 where you can view registered tools and invoke them manually.

Unit Testing

// tests/search.test.ts
import { searchDatabase } from "../src/lib/database.js";
 
const results = await searchDatabase("MCP", { limit: 10, category: "all" });
console.assert(results.length > 0, "Expected non-empty results");
console.assert(results[0].title.includes("MCP"), "Expected MCP in title");
console.log("All tests passed");
 
// Expected output:
// All tests passed

Production Deployment Best Practices

Publishing as an npm Package

npm run build
npm publish

After publishing, users can run it with npx my-mcp-server.

Docker Distribution

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist/ ./dist/
ENTRYPOINT ["node", "dist/index.js"]

Security Considerations

  • Environment variables for secrets: Never hardcode API keys or DB credentials
  • Input validation: Zod validates all parameters (already implemented)
  • Rate limiting: Throttle external API calls to prevent abuse
  • Least privilege: Use read-only database users where possible

That covers the implementation foundation. The next sections focus on the design decisions for integrating your server into a hybrid architecture.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Measured results from a hybrid setup that cut token cost by ~71% and p95 latency by ~74% versus an all-LLM approach
Five production pitfalls I hit (bloated tool definitions, timeout propagation, runaway retries) and the fixes that actually worked
The decision criteria I use to split work between the deterministic layer and the AI layer, drawn from a wallpaper-app review pipeline wired to Crashlytics
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

API & SDK2026-07-08
Contract-Test Every Tool Before You Submit or Automate an MCP Connector
A connector that works once in a chat can still break silently in an unattended job through misread response shapes or double-fired writes. Here is a small harness that machine-checks tool descriptions, response contracts, idempotency, and latency, with measured numbers.
API & SDK2026-06-30
When a Tool Result Is Too Big and Melts Your Context Window: Designing Cursor-Based Pagination
When a list tool returns hundreds of rows at once, an agent's context can collapse in a single call. Here is a cursor-based pagination design that keeps tool output small and protects your token budget, with working code.
API & SDK2026-06-28
Did That Post Actually Go Through? Safely Retrying an Interrupted MCP Write Without Double-Executing
When an MCP write tool call is interrupted by a dropped connection, you can't tell whether the server ran it. Here's why naive retries cause double-execution, and a working wrapper that uses idempotency keys and a reconcile read to retry safely — with examples from an unattended pipeline.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →