CLAUDE LABJP
PRICING — September 1 was the scheduled date for the Sonnet 5 price increase, and it did not happen. The introductory $2/$10 per MTok now stands as the regular pricePARTNER — Salesforce and Anthropic announced Claudeforce, an expanded partnership. The Salesforce in Claude plugin ships with 37 prebuilt sales skills, from meeting prep to pipeline managementTRUST — Claudeforce serves Claude through Amazon Bedrock inside the Salesforce Trust Boundary, so data and inference never leave the security perimeter — an answer aimed squarely at regulated industriesBETA — Salesforce in Claude is with select pilot customers for now, with an open beta expected during SeptemberLIMITS — The 50% weekly-limit boost runs through September 13. From September 14 the permanent level is 25% above the pre-promotion baseline, roughly a 17% cut from todayRELEASE — Claude Code has shipped nothing since v2.1.251 on August 28. Against a pace of one release every 0.8 days, a four-day gap is among the longest yetPRICING — September 1 was the scheduled date for the Sonnet 5 price increase, and it did not happen. The introductory $2/$10 per MTok now stands as the regular pricePARTNER — Salesforce and Anthropic announced Claudeforce, an expanded partnership. The Salesforce in Claude plugin ships with 37 prebuilt sales skills, from meeting prep to pipeline managementTRUST — Claudeforce serves Claude through Amazon Bedrock inside the Salesforce Trust Boundary, so data and inference never leave the security perimeter — an answer aimed squarely at regulated industriesBETA — Salesforce in Claude is with select pilot customers for now, with an open beta expected during SeptemberLIMITS — The 50% weekly-limit boost runs through September 13. From September 14 the permanent level is 25% above the pre-promotion baseline, roughly a 17% cut from todayRELEASE — Claude Code has shipped nothing since v2.1.251 on August 28. Against a pace of one release every 0.8 days, a four-day gap is among the longest yet
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.

MCP51hybrid-architecture2Tool Use9design-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 $15 for lifetime access
View Membership →

Related Articles

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.
API & SDK2026-08-01
Swapping Tools Mid-Conversation Without Losing Your Prompt Cache
Tools can now be added and removed between turns, but the tool block sits at the very front of the cache prefix. I measured 12 mutation patterns with fingerprints and built a stable-core plus volatile-tail registry with a guard that refuses unsafe changes.
API & SDK2026-07-31
The 400-Character Preview That Was Still Holding On to a Megabyte
Truncating tool output in your own MCP server does not free the original. Node's sliced strings keep the parent alive. Here is the measured 13-character boundary, which flattening tricks actually work, and a heap-snapshot audit script that counts the retained parents for you.
📚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 →