Skip to main content
▲
AI DevKit
←Back to all articles
Agents•6 min read · 1,087 words

Building Production-Grade MCP Agents in TypeScript: Architecture, Memory & Security Sandboxing

A comprehensive engineering guide to building Model Context Protocol (MCP) servers and autonomous agents with TypeScript, Zod schema validation, SQLite persistent memory, and host integrations.

M
Mohan
AI Systems Engineer
📋Raw MD
Building Production-Grade MCP Agents in TypeScript: Architecture, Memory & Security Sandboxing

The proliferation of proprietary function-calling wrappers created a massive fragmentation problem across the AI agent ecosystem. Every LLM provider invented their own schema format, every developer wrote custom glue code, and agents remained trapped in isolated silos unable to inspect external file systems, databases, or developer environments without brittle scaffolding.

Anthropic’s open-source Model Context Protocol (MCP) standardizes this interaction layer. Rather than treating tool calling as an opaque API parameter, MCP establishes an open, bi-directional JSON-RPC 2.0 protocol between AI host clients (like Claude Desktop, Cursor IDE, or custom autonomous loops) and lightweight MCP servers.

Here is the production engineering architecture for building high-reliability, sandboxed TypeScript MCP servers with embedded SQLite transactional memory.


1. Protocol Architecture: Stdio vs. SSE Transports

MCP operates over two primary transport abstractions:

  1. Standard I/O (stdio): The host client spawns the MCP server as a child process. Requests and responses stream across standard input and output channels formatted as single-line JSON-RPC messages.
  2. Server-Sent Events (SSE): The host establishes an HTTP GET connection for persistent server-to-client streaming, while the client dispatches tool execution requests via HTTP POST payloads.
MCP Protocol Transport Overhead
Swipe table →
ModelMMLUGSM8KHumanEvalCost / 1M Input
DeepSeek-V3 (671B)88.5%89.3%82.6%$0.14
Claude 3.5 Sonnet88.7%92.4%93.7%$3.00
Llama 3.1 405B87.3%85.9%81.1%$1.20

2. Setting Up the TypeScript Project

Initialize a modern TypeScript project utilizing ESM modules and install the official MCP SDK along with Zod:

mkdir mcp-production-server
cd mcp-production-server
pnpm init
pnpm add @modelcontextprotocol/sdk zod better-sqlite3
pnpm add -D typescript @types/node @types/better-sqlite3 tsx

Configure your tsconfig.json for Node.js modern ESM target:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

3. Implementing the Sandboxed MCP Server

Let’s build a production server that provides:

  • Sandboxed file reading with directory containment protection.
  • Transactional key-value state persistence using SQLite.
  • Strict Zod parameter validation and structured error boundaries.

Create src/index.ts:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ErrorCode,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import Database from "better-sqlite3";
import * as path from "path";
import * as fs from "fs/promises";

// 1. Initialize SQLite Agent Memory Database
const db = new Database("agent_memory.sqlite");
db.pragma("journal_mode = WAL");
db.exec(`
  CREATE TABLE IF NOT EXISTS memory_store (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

const setMemory = db.prepare(
  "INSERT INTO memory_store (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP"
);
const getMemory = db.prepare("SELECT value FROM memory_store WHERE key = ?");

// 2. Initialize MCP Server Instance
const server = new Server(
  {
    name: "production-system-agent",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 3. Define Validation Schemas
const ReadFileSchema = z.object({
  relativePath: z.string().describe("Relative path inside the allowed project directory"),
});

const SetMemorySchema = z.object({
  key: z.string().min(1).describe("State key identifier"),
  value: z.string().describe("Arbitrary JSON string or context note to persist"),
});

const ALLOWED_ROOT = path.resolve(process.cwd(), "sandbox");

// Ensure sandbox directory exists
await fs.mkdir(ALLOWED_ROOT, { recursive: true });

// 4. Register Available Tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "read_sandbox_file",
        description: "Safely reads a text file strictly contained within the sandbox workspace.",
        inputSchema: {
          type: "object",
          properties: {
            relativePath: { type: "string", description: "Relative file path" },
          },
          required: ["relativePath"],
        },
      },
      {
        name: "persist_agent_memory",
        description: "Store durable task observations or state across long-running turns.",
        inputSchema: {
          type: "object",
          properties: {
            key: { type: "string", description: "Storage key identifier" },
            value: { type: "string", description: "Context value" },
          },
          required: ["key", "value"],
        },
      },
    ],
  };
});

// 5. Handle Tool Execution with Security Guardrails
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    if (name === "read_sandbox_file") {
      const parsed = ReadFileSchema.safeParse(args);
      if (!parsed.success) {
        throw new McpError(ErrorCode.InvalidParams, parsed.error.message);
      }

      // Security: Prevent path traversal attacks (e.g. "../../etc/passwd")
      const resolvedPath = path.resolve(ALLOWED_ROOT, parsed.data.relativePath);
      if (!resolvedPath.startsWith(ALLOWED_ROOT)) {
        throw new McpError(
          ErrorCode.InvalidParams,
          "Security Violation: Directory traversal outside sandbox is forbidden."
        );
      }

      const content = await fs.readFile(resolvedPath, "utf-8");
      return {
        content: [{ type: "text", text: content }],
      };
    }

    if (name === "persist_agent_memory") {
      const parsed = SetMemorySchema.safeParse(args);
      if (!parsed.success) {
        throw new McpError(ErrorCode.InvalidParams, parsed.error.message);
      }

      setMemory.run(parsed.data.key, parsed.data.value);
      return {
        content: [
          {
            type: "text",
            text: `Successfully persisted key "${parsed.data.key}" into SQLite memory.`,
          },
        ],
      };
    }

    throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
  } catch (error) {
    console.error(`[MCP Tool Error] [${name}]:`, error);
    return {
      content: [
        {
          type: "text",
          text: `Execution failed: ${error instanceof Error ? error.message : String(error)}`,
        },
      ],
      isError: true,
    };
  }
});

// 6. Connect Transport and Launch
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Production MCP Agent Server running over stdio transport.");
}

main().catch((err) => {
  console.error("Fatal startup error:", err);
  process.exit(1);
});

4. Host Configuration: Claude Desktop & Cursor Integration

Once compiled, you can plug this MCP server directly into your AI hosts without restarting or reinstalling applications.

Connecting to Claude Desktop

Open your Claude Desktop configuration file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add your server definition:

{
  "mcpServers": {
    "system-agent": {
      "command": "node",
      "args": ["C:/Users/Mohan-Pc/Desktop/mcp-production-server/dist/index.js"]
    }
  }
}

Connecting to Cursor IDE

In Cursor IDE, open Settings →\to Features →\to MCP Servers or add to ~/.cursor/mcp.json:

{
  "mcpServers": {
    "system-agent": {
      "command": "node",
      "args": ["C:/Users/Mohan-Pc/Desktop/mcp-production-server/dist/index.js"]
    }
  }
}

5. Security Architecture: The 4 Invariants of Production MCP

Deploying autonomous agents with local tool execution requires strict defensive design:

  1. Path Resolution Containment: Always run path.resolve() and verify with .startsWith(ALLOWED_ROOT). Never assume relative paths are safe, as LLMs frequently attempt ../ sequences when searching for configuration files.
  2. Standard Error Isolation: Direct all application logging, metrics, and trace telemetry to process.stderr. A single unexpected byte on stdout will break JSON-RPC deserialization.
  3. Execution Guardrails & Timeouts: Wrap asynchronous tool execution in an AbortController timeout (e.g. 15–30 seconds) to prevent frozen shell commands or slow network queries from hanging the agent reasoning loop.
  4. Structured Error Payloads: Always return { isError: true } alongside descriptive error text rather than terminating the process with an uncaught exception. This allows the LLM to inspect the failure and autonomously course-correct its next attempt.

By building on MCP’s open standard, your agents gain native portability across Claude Desktop, Cursor, and custom backend orchestrators while keeping system access locked down and reliable.

Recommended Next

View all articles →