●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 /subtask●LIMITS — 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 own●MCPBG — 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_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — 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 $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●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 /subtask●LIMITS — 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 own●MCPBG — 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_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — 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 $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
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.
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:
Tools: Functions Claude can invoke (data retrieval, computations, external API calls)
Resources: Data sources Claude can read (files, DB records)
Prompts: Reusable prompt templates
This tutorial focuses on Tools — the most practical capability for building a database search server.
// src/server.tsimport { 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.tsimport { 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 toolsregisterSearchTool(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.tsimport { 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.tsimport { 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.tsinterface 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.
After publishing, users can run it with npx my-mcp-server.
Docker Distribution
FROM node:20-slimWORKDIR /appCOPY package*.json ./RUN npm ci --productionCOPY 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.
Dividing Responsibilities Between Deterministic and AI Layers
The foundation of hybrid architecture lies in a clear division of labor. Here's how to decide what goes where.
Assign to deterministic tools (MCP servers):
Database CRUD operations
Numerical calculations and aggregations
File system reads and writes
External API calls (REST/GraphQL)
Input validation and format conversion
Assign to Claude (AI reasoning):
Interpreting user intent and selecting appropriate tools
Summarizing and explaining tool results
Synthesizing conclusions from multiple tool outputs
Deciding recovery strategies when errors occur
Generating natural language responses
By maintaining this separation, you significantly improve the predictability, debuggability, and cost-efficiency of your entire system.
Designing the Three-Layer Hybrid Architecture
A production-ready hybrid architecture consists of three layers working together.
Layer 1: Deterministic Tool Layer (MCP Server)
Tools implemented as MCP servers should be pure functions — given the same input, they always return the same output.
// mcp-server/tools/database-query.ts// Database query tool — a deterministic operationimport { z } from "zod";const QueryParamsSchema = z.object({ table: z.string().describe("Target table name"), filters: z.record(z.string()).describe("Filter conditions (key=column, value=condition)"), limit: z.number().default(10).describe("Maximum number of records to return"),});export const databaseQueryTool = { name: "query_database", description: "Retrieve records from a table matching the specified filter conditions", inputSchema: QueryParamsSchema, async execute(params: z.infer<typeof QueryParamsSchema>) { // Execute query deterministically with validated parameters const { table, filters, limit } = params; const whereClause = Object.entries(filters) .map(([key, value]) => `${key} = '${value}'`) .join(" AND "); const query = `SELECT * FROM ${table} WHERE ${whereClause} LIMIT ${limit}`; // Results are deterministic for the same input and data state const results = await db.execute(query); return { success: true, data: results, query }; },};
Layer 2: Orchestration Layer (Claude API)
Claude analyzes user requests and determines which tools to call and in what order.
// orchestrator/hybrid-agent.ts// Hybrid agent using Claude as the orchestratorimport Anthropic from "@anthropic-ai/sdk";const client = new Anthropic();async function runHybridAgent(userMessage: string) { const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 4096, system: `You are a data analysis assistant.Analyze the user's request and call the appropriate tools.Key design principles:1. Always use tools for calculations and data retrieval (never compute yourself)2. Trust tool results as ground truth (never "imagine" results)3. When multiple tools are needed, determine execution order based on dependencies4. If an error occurs, consider alternatives before reporting to the user`, tools: mcpTools, // Tool definitions fetched from MCP server messages: [{ role: "user", content: userMessage }], }); return response;}
Layer 3: Result Synthesis Layer
Claude integrates results from multiple tool executions and presents them in the most useful format.
// orchestrator/result-synthesizer.ts// Logic for synthesizing tool execution resultsasync function synthesizeResults( toolResults: ToolResult[], originalQuery: string) { const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 2048, messages: [ { role: "user", content: `Synthesize the following tool results to answer the user's question.User question: ${originalQuery}Tool execution results:${toolResults .map( (r, i) => `[Tool ${i + 1}: ${r.toolName}]\n${JSON.stringify(r.output, null, 2)}` ) .join("\n\n")}Response rules:- Use numerical data exactly as returned by tools- Clearly state when you are inferring or extrapolating- Report any contradictions between tool results`, }, ], }); return response;}
Implementing Andrew Ng's Tool Use Patterns with Claude MCP
The agentic design best practices advocated by Professor Andrew Ng align naturally with MCP hybrid architecture. Here are three key patterns and how to implement them.
Pattern 1: Plan → Execute → Verify Loop
The LLM creates an execution plan, deterministic tools carry it out, and the LLM verifies the results — repeating until the task is complete.
// patterns/plan-execute-verify.ts// Plan-Execute-Verify loop patternconst systemPrompt = `You are a task execution agent.Follow these three steps for every request:Step 1 - Plan: Analyze the request and output an execution plan as a JSON array of tool calls.Step 2 - Execute: Call tools in the planned sequence.Step 3 - Verify: Review all results. If they match expectations, provide the final answer. If not, revise the plan and retry.`;async function planExecuteVerify(userRequest: string) { let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { // Step 1: Claude creates a plan (probabilistic) const plan = await generatePlan(userRequest); // Step 2: Execute the plan via tools (deterministic) const results = await executePlan(plan); // Step 3: Claude verifies results (probabilistic) const verification = await verifyResults(results, userRequest); if (verification.isSuccess) { return verification.finalResponse; } // Verification failed — refine and retry userRequest = `Previous results: ${JSON.stringify(results)}Feedback: ${verification.feedback}Original request: ${userRequest}`; attempts++; } return "Maximum retry attempts reached. Please review manually.";}
Pattern 2: Progressive Tool Narrowing
When you have many available tools, use a lightweight model to first classify the request category, then pass only the relevant tools to the main model. This dramatically reduces token consumption.
// patterns/tool-narrowing.ts// Progressive tool narrowing patternconst toolCategories = { database: ["query_database", "insert_record", "update_record"], file: ["read_file", "write_file", "list_directory"], api: ["http_get", "http_post", "graphql_query"], calculation: ["statistics", "aggregate", "transform"],};async function narrowAndExecute(userMessage: string) { // Phase 1: Category classification (minimal tokens) const categoryResponse = await client.messages.create({ model: "claude-haiku-4-5", // Lightweight model for classification max_tokens: 100, messages: [ { role: "user", content: `Select the tool category needed for this request.Categories: database, file, api, calculationRequest: ${userMessage}Category name only:`, }, ], }); const category = categoryResponse.content[0].text.trim(); // Phase 2: Execute with narrowed tool set (saves tokens) const relevantTools = toolCategories[category] || []; const fullTools = await getMCPTools(relevantTools); return client.messages.create({ model: "claude-sonnet-4-6", // Full model for execution max_tokens: 4096, tools: fullTools, messages: [{ role: "user", content: userMessage }], });}
Pattern 3: Deterministic Guardrails
Insert deterministic checks before and after AI decisions. This pattern is especially valuable when security or compliance requirements are involved.
Practical Example: Building a Sales Analysis Agent
Let's combine these patterns to build a hybrid agent that analyzes sales data.
// example/sales-analysis-agent.ts// Sales analysis hybrid agent implementationimport Anthropic from "@anthropic-ai/sdk";const client = new Anthropic();// Deterministic tool definitionsconst salesTools = [ { name: "get_sales_data", description: "Retrieve sales data for a specified date range", input_schema: { type: "object", properties: { start_date: { type: "string", description: "Start date (YYYY-MM-DD)" }, end_date: { type: "string", description: "End date (YYYY-MM-DD)" }, product_category: { type: "string", description: "Product category (omit for all categories)", }, }, required: ["start_date", "end_date"], }, }, { name: "calculate_statistics", description: "Calculate statistical values (mean, median, standard deviation) for a number array", input_schema: { type: "object", properties: { values: { type: "array", items: { type: "number" }, description: "Array of numbers to analyze", }, }, required: ["values"], }, },];async function analyzeSales(question: string) { const messages: Anthropic.MessageParam[] = [ { role: "user", content: question }, ]; // Agent loop: keep running until tool calls are complete let continueLoop = true; while (continueLoop) { const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 4096, system: `You are a sales analysis expert.Always use tools to retrieve and calculate data for the user's questions.Never estimate numbers yourself — rely solely on tool results as evidence.`, tools: salesTools, messages, }); if (response.stop_reason === "tool_use") { // Execute tool calls (deterministic processing) const toolResults = []; for (const block of response.content) { if (block.type === "tool_use") { const result = await executeWithGuardrails(block); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result), }); } } // Add to conversation history for next iteration messages.push({ role: "assistant", content: response.content }); messages.push({ role: "user", content: toolResults }); } else { // Final answer received continueLoop = false; return response.content; } }}// Usage example// Expected behavior:// 1. Claude calls get_sales_data (deterministic)// 2. Claude calls calculate_statistics on the retrieved data (deterministic)// 3. Claude interprets the statistics and generates a report (probabilistic)const report = await analyzeSales( "Compare last month's sales with this month and analyze the trend");console.log(report);
Operational Lessons the Docs Don't Cover
Hybrid architecture looks elegant as a diagram, but once you put it into production you run into small, undocumented snags. Here are the lessons that paid off the most, drawn from running Dolice Labs' automated publishing pipeline and an app business with over 50 million cumulative downloads.
Cost and Latency vs. an All-Claude Approach
Let me start with numbers. I measured a classification task — sorting incoming wallpaper-app reviews into "bug report / feature request / praise," then cross-referencing the bug reports against Crashlytics crash context — built two ways:
Sending everything to Claude: ~¥2.4 average token cost per request, 4.2s p95 latency
Hybrid (narrow candidates with deterministic preprocessing and rules, hand only the final judgment to Claude): ~¥0.7, 1.1s p95 latency
That's roughly a 71% cost reduction and a 74% latency reduction. The more you let the deterministic layer settle what can be settled mechanically, the smaller the context you hand to Claude — and both your bill and your latency fall. When reviews reach tens of thousands per month, that gap shows up directly in operating cost.
Five Production Pitfalls and How I Avoided Them
Most of what failed in practice came from the deterministic tool layer, not the AI reasoning. In the order I hit them:
Bloated tool definitions — Handing Claude 30+ tools in a single call hurt selection accuracy and wasted tokens. Switching to the progressive narrowing pattern covered earlier all but eliminated misfired calls.
Timeout propagation — When a database stalls inside a deterministic tool, the whole agent loop freezes. Wrap every tool call in its own timeout (I default to 8s) and return a structured "this failed" response so Claude can choose an alternative.
Runaway retries — I watched Claude call the same failing tool over and over. I cap retries of the same tool-plus-arguments at two, with a deterministic guardrail that breaks the loop on the third.
JSON parsing drift — Passing tool results as prose occasionally got them summarized mid-stream. Always return results as JSON with fixed key names so numeric values survive intact.
Context bloat driving cost — Keeping every past tool result in the conversation makes each loop iteration more expensive. Replace stale results with summaries, or keep only an ID reference.
My Criteria for Splitting Responsibilities
When I'm unsure whether a step belongs to a deterministic tool or to Claude, I ask:
Should identical input always produce identical output? If yes, deterministic tool. Aggregation, filtering, and validation live here.
Does it require interpreting ambiguous natural language? If yes, Claude. Sentiment classification and extracting a spec from a free-form request belong here.
Is the cost of being wrong high (billing, deletion, external sends)? If so, verify with a deterministic guardrail right before the final action.
In my experience, when in doubt, lean toward the deterministic side and leave Claude only the one place that genuinely needs language understanding. Resisting the urge to over-delegate to the AI is what keeps a hybrid setup both cheap and reproducible.
Looking back
Claude MCP hybrid architecture provides a proven set of design patterns for building reliable agent systems by optimally combining deterministic tools with AI reasoning. Inspired by Andrew Ng's Tool Use framework, this article covered three essential patterns: the Plan-Execute-Verify loop, progressive tool narrowing, and deterministic guardrails.
For a refresher on Claude's tool use fundamentals, see the Type-Safe Tool Use Guide. Start by adding a single deterministic tool to an MCP server and connecting it to Claude. Small, intentional steps compound into a robust hybrid architecture over time.
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.