◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
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.

MCP54hybrid-architectureTool Use9design-patterns3agents8

✦ 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-09-21
The morning a deprecation notice appeared, I checked versions before editing config
Claude Desktop and Cowork stop accepting older managed-configuration spellings on October 7. Here is how I sorted mine into what to fix now, what to fix after upgrading, and what changes meaning on its own.
⬡ API & SDK2026-09-02
The 400 that rejects a thinking block, and finding the edit that caused it
On Claude Fable 5.1, editing the system prompt, tools, or earlier messages before a thinking block makes the request fail with a 400. Here is how to check whether your own agent loop does that in a single request, and how to move a rebuilt system prompt and client-side trimming to patterns that leave the prefix untouched.
⬡ API & SDK2026-08-19
The listen Stream Kept Reopening Against My Serverless MCP Server
No errors anywhere, yet connections to the MCP server kept piling up overnight. Here is what happens when a fixed platform timeout meets a resubscribe loop, reproduced in a few dozen lines.