Back to blog

Model Context Protocol Deep Dive: Complete Technical Guide

Keywords: Model Context Protocol architecture, MCP server development, browser automation protocol, MCP implementation, AI tool integration

Model Context Protocol (MCP) is changing how AI applications interact with external tools. But how does it actually work under the hood? This technical guide explores MCP architecture, protocol specifications, and real-world implementation patterns for browser automation integration.

Whether you're building custom MCP servers, extending existing tools, or just want to understand the technology, this guide provides the technical depth you need.

Table of Contents

Reading Time: ~25 minutes | Difficulty: Advanced | Last Updated: January 16, 2026


MCP Architecture Overview

The Core Components

Model Context Protocol consists of four fundamental components:

┌─────────────────────────────────────────────┐
│           AI Application (Client)            │
│   (Cursor, VS Code, Claude Desktop, etc.)   │
└──────────────────┬──────────────────────────┘
                   │ MCP Protocol (stdio/HTTP)
┌──────────────────▼──────────────────────────┐
│             MCP Server                       │
│  ┌────────────────────────────────────────┐ │
│  │        Tool Registry                    │ │
│  │  - Tool definitions                     │ │
│  │  - Input schemas                        │ │
│  │  - Handler functions                    │ │
│  └────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────┐ │
│  │     Resource Manager                    │ │
│  │  - Context data                         │ │
│  │  - State management                     │ │
│  └────────────────────────────────────────┘ │
└──────────────────┬──────────────────────────┘
                   │ Native API / WebSocket
┌──────────────────▼──────────────────────────┐
│        External Service/API                  │
│    (Browser, Database, File System, etc.)   │
└─────────────────────────────────────────────┘

Communication Flow

  1. AI Request: User asks AI assistant for action requiring external tool
  2. Tool Selection: AI determines which MCP tool to invoke
  3. Protocol Message: Client sends structured request to MCP server
  4. Tool Execution: Server processes request and calls external service
  5. Response: Results flow back through protocol to AI
  6. Context Update: AI incorporates results into conversation

Protocol Specifications

Message Format

MCP uses JSON-RPC 2.0 for message structure:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "browser_navigate",
    "arguments": {
      "url": "https://example.com"
    }
  }
}

Response Format

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Successfully navigated to https://example.com"
      }
    ],
    "isError": false
  }
}

Error Response

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32603,
    "message": "Navigation failed",
    "data": {
      "details": "ERR_CONNECTION_REFUSED",
      "url": "https://example.com"
    }
  }
}

Transport Layer Implementation

MCP supports multiple transport mechanisms. For browser automation with Onpiste, we use stdio transport for IDE integration.

Stdio Transport

Benefits:

  • Simple implementation
  • Works with all IDEs supporting MCP
  • Low latency (local process communication)
  • No network configuration required

Implementation:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// Create MCP server instance
const server = new Server(
  {
    name: "onpiste-browser-automation",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);

// Initialize stdio transport
const transport = new StdioServerTransport();

// Connect server to transport
await server.connect(transport);

console.error("Onpiste MCP server running on stdio");

HTTP Transport (Alternative)

For web-based integrations:

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();

app.post("/mcp", async (req, res) => {
  const transport = new SSEServerTransport("/mcp/messages", res);
  await server.connect(transport);
});

app.listen(3000, () => {
  console.log("MCP server listening on http://localhost:3000");
});

WebSocket Transport

For real-time bidirectional communication:

import WebSocket from "ws";
import { WebSocketServerTransport } from "@modelcontextprotocol/sdk/server/websocket.js";

const wss = new WebSocket.Server({ port: 8080 });

wss.on("connection", async (ws) => {
  const transport = new WebSocketServerTransport(ws);
  await server.connect(transport);
});

Tool Definition & Registration

Tool Schema Structure

Every MCP tool requires:

  1. Name: Unique identifier
  2. Description: What the tool does (AI uses this)
  3. Input Schema: JSON Schema for parameters
  4. Handler: Function that executes the tool

Basic Tool Registration

import { z } from "zod";

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "browser_navigate",
        description: "Navigate browser to a specified URL",
        inputSchema: {
          type: "object",
          properties: {
            url: {
              type: "string",
              format: "uri",
              description: "URL to navigate to",
            },
          },
          required: ["url"],
        },
      },
    ],
  };
});

Tool Execution Handler

import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "browser_navigate": {
      const { url } = args as { url: string };

      try {
        // Execute navigation (details in next section)
        await browserContext.navigate(url);

        return {
          content: [
            {
              type: "text",
              text: `Successfully navigated to ${url}`,
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Navigation failed: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    }

    default:
      throw new Error(`Unknown tool: ${name}`);
  }
});

Input Validation with Zod

For type-safe parameter handling:

import { z } from "zod";

// Define schema
const NavigateSchema = z.object({
  url: z.string().url(),
  waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).optional(),
  timeout: z.number().min(0).max(60000).optional(),
});

// In tool handler
case "browser_navigate": {
  const params = NavigateSchema.parse(args);
  await browserContext.navigate(
    params.url,
    params.waitUntil,
    params.timeout
  );
  // ...
}

Browser Automation Integration

Architecture Pattern

MCP Server (Node.js)
Native Messaging Protocol
Chrome Extension (Onpiste)
Chrome DevTools Protocol / Extension APIs
Browser Actions

Native Messaging Setup

The MCP server communicates with Chrome extension via native messaging:

manifest.json (Chrome Extension):

{
  "name": "onpiste",
  "permissions": [
    "nativeMessaging",
    "tabs",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ]
}

Native Messaging Host (MCP Server):

import { NativeMessagingHost } from "./native-messaging.js";

class BrowserContext {
  private messaging: NativeMessagingHost;

  constructor() {
    this.messaging = new NativeMessagingHost({
      extensionId: "hmojfgaobpbggbfcaijjghjimbbjfnei",
    });
  }

  async navigate(url: string): Promise<void> {
    const response = await this.messaging.sendMessage({
      action: "navigate",
      params: { url },
    });

    if (!response.success) {
      throw new Error(response.error);
    }
  }

  async click(selector: string): Promise<void> {
    const response = await this.messaging.sendMessage({
      action: "click",
      params: { selector },
    });

    if (!response.success) {
      throw new Error(response.error);
    }
  }

  async screenshot(): Promise<string> {
    const response = await this.messaging.sendMessage({
      action: "screenshot",
      params: {},
    });

    return response.data; // Base64 image
  }
}

Chrome Extension Message Handling

// background/service-worker.ts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.action) {
    case "navigate": {
      chrome.tabs.update({ url: message.params.url })
        .then(() => sendResponse({ success: true }))
        .catch((error) => sendResponse({
          success: false,
          error: error.message
        }));
      return true; // Async response
    }

    case "click": {
      chrome.tabs.query({ active: true }, ([tab]) => {
        chrome.scripting.executeScript({
          target: { tabId: tab.id },
          func: (selector) => {
            const element = document.querySelector(selector);
            if (element) {
              element.click();
              return true;
            }
            throw new Error(`Element not found: ${selector}`);
          },
          args: [message.params.selector],
        })
        .then(() => sendResponse({ success: true }))
        .catch((error) => sendResponse({
          success: false,
          error: error.message
        }));
      });
      return true;
    }

    case "screenshot": {
      chrome.tabs.captureVisibleTab(null, { format: "png" }, (dataUrl) => {
        sendResponse({ success: true, data: dataUrl });
      });
      return true;
    }
  }
});

This pattern enables natural language browser automation through the MCP interface.


Building Custom MCP Servers

Project Structure

my-mcp-server/
├── src/
│   ├── index.ts           # Server entry point
│   ├── tools/
│   │   ├── navigation.ts  # Navigation tools
│   │   ├── interaction.ts # Click, type, etc.
│   │   └── extraction.ts  # Data extraction
│   ├── browser/
│   │   ├── context.ts     # Browser abstraction
│   │   └── messaging.ts   # Native messaging
│   └── types.ts           # TypeScript types
├── package.json
└── tsconfig.json

Complete Server Example

// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { BrowserContext } from "./browser/context.js";

// Initialize browser context
const browser = new BrowserContext();

// Create MCP server
const server = new Server(
  {
    name: "custom-browser-automation",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Register tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "navigate_to",
        description: "Navigate to a URL",
        inputSchema: {
          type: "object",
          properties: {
            url: { type: "string", format: "uri" },
          },
          required: ["url"],
        },
      },
      {
        name: "click_element",
        description: "Click an element on the page",
        inputSchema: {
          type: "object",
          properties: {
            selector: { type: "string" },
          },
          required: ["selector"],
        },
      },
      {
        name: "extract_text",
        description: "Extract text from elements matching selector",
        inputSchema: {
          type: "object",
          properties: {
            selector: { type: "string" },
          },
          required: ["selector"],
        },
      },
    ],
  };
});

// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case "navigate_to": {
        await browser.navigate(args.url);
        return {
          content: [
            { type: "text", text: `Navigated to ${args.url}` },
          ],
        };
      }

      case "click_element": {
        await browser.click(args.selector);
        return {
          content: [
            { type: "text", text: `Clicked element: ${args.selector}` },
          ],
        };
      }

      case "extract_text": {
        const text = await browser.extractText(args.selector);
        return {
          content: [
            { type: "text", text: `Extracted text: ${text}` },
          ],
        };
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    return {
      content: [
        { type: "text", text: `Error: ${error.message}` },
      ],
      isError: true,
    };
  }
});

// Start server
const transport = new StdioServerTransport();
await server.connect(transport);

console.error("Custom MCP server started");

Advanced Tool: Multi-Step Workflow

{
  name: "test_login_flow",
  description: "Test complete login flow with credentials",
  inputSchema: {
    type: "object",
    properties: {
      loginUrl: { type: "string", format: "uri" },
      email: { type: "string", format: "email" },
      password: { type: "string" },
      expectedRedirect: { type: "string" },
    },
    required: ["loginUrl", "email", "password"],
  },
}

// Handler
case "test_login_flow": {
  const { loginUrl, email, password, expectedRedirect } = args;

  // Navigate to login page
  await browser.navigate(loginUrl);

  // Fill credentials
  await browser.type("#email", email);
  await browser.type("#password", password);

  // Submit
  await browser.click("button[type='submit']");

  // Wait for navigation
  await browser.waitForNavigation();

  // Verify redirect
  const currentUrl = await browser.getCurrentUrl();
  const success = expectedRedirect
    ? currentUrl.includes(expectedRedirect)
    : true;

  return {
    content: [
      {
        type: "text",
        text: success
          ? "✅ Login flow test passed"
          : `❌ Login flow test failed: expected ${expectedRedirect}, got ${currentUrl}`,
      },
    ],
    isError: !success,
  };
}

Error Handling & Resilience

Error Categories

enum ErrorCode {
  CONNECTION_FAILED = 1000,
  TIMEOUT = 1001,
  ELEMENT_NOT_FOUND = 1002,
  NAVIGATION_FAILED = 1003,
  EXTENSION_NOT_CONNECTED = 1004,
}

class MCPError extends Error {
  constructor(
    public code: ErrorCode,
    message: string,
    public details?: any
  ) {
    super(message);
    this.name = "MCPError";
  }
}

Retry Logic

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  delay = 1000
): Promise<T> {
  let lastError: Error;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (attempt < maxRetries) {
        await new Promise((resolve) => setTimeout(resolve, delay));
        continue;
      }
    }
  }

  throw new MCPError(
    ErrorCode.TIMEOUT,
    `Operation failed after ${maxRetries} attempts`,
    { lastError: lastError.message }
  );
}

// Usage
case "browser_navigate": {
  await withRetry(() => browser.navigate(args.url), 3, 2000);
  return { content: [{ type: "text", text: "Navigated successfully" }] };
}

Timeout Handling

async function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number
): Promise<T> {
  return Promise.race([
    promise,
    new Promise<T>((_, reject) =>
      setTimeout(
        () => reject(new MCPError(
          ErrorCode.TIMEOUT,
          `Operation timed out after ${timeoutMs}ms`
        )),
        timeoutMs
      )
    ),
  ]);
}

// Usage
case "browser_click": {
  await withTimeout(
    browser.click(args.selector),
    5000 // 5 second timeout
  );
  return { content: [{ type: "text", text: "Element clicked" }] };
}

Graceful Degradation

async function safeExecute<T>(
  operation: () => Promise<T>,
  fallback: T
): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    console.error("Operation failed, using fallback:", error);
    return fallback;
  }
}

// Usage
case "browser_screenshot": {
  const screenshot = await safeExecute(
    () => browser.screenshot(),
    null // Fallback if screenshot fails
  );

  if (!screenshot) {
    return {
      content: [{ type: "text", text: "Screenshot not available" }],
      isError: true,
    };
  }

  return {
    content: [
      { type: "image", data: screenshot, mimeType: "image/png" },
    ],
  };
}

These patterns ensure reliable browser automation even under adverse conditions.


Security Considerations

Input Validation

import { z } from "zod";

// Validate all URLs
const UrlSchema = z.string().url().refine(
  (url) => {
    // Block file:// protocol
    if (url.startsWith("file://")) return false;

    // Block internal IPs in production
    if (process.env.NODE_ENV === "production") {
      const hostname = new URL(url).hostname;
      if (hostname === "localhost" || hostname.startsWith("127.")) {
        return false;
      }
    }

    return true;
  },
  { message: "Invalid URL for security reasons" }
);

case "browser_navigate": {
  const url = UrlSchema.parse(args.url);
  await browser.navigate(url);
  // ...
}

Sandboxing Tool Execution

class SandboxedBrowserContext {
  private allowedDomains: string[];

  constructor(allowedDomains: string[]) {
    this.allowedDomains = allowedDomains;
  }

  async navigate(url: string): Promise<void> {
    const hostname = new URL(url).hostname;

    if (!this.isDomainAllowed(hostname)) {
      throw new MCPError(
        ErrorCode.PERMISSION_DENIED,
        `Domain not allowed: ${hostname}`
      );
    }

    // Proceed with navigation
    await this.browser.navigate(url);
  }

  private isDomainAllowed(hostname: string): boolean {
    return this.allowedDomains.some(
      (allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`)
    );
  }
}

// Initialize with whitelist
const browser = new SandboxedBrowserContext([
  "localhost",
  "example.com",
  "staging.myapp.com",
]);

Rate Limiting

import rateLimit from "express-rate-limit";

// For HTTP transport
const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // 100 requests per minute
  message: "Too many requests, please try again later",
});

app.use("/mcp", limiter);

Audit Logging

import winston from "winston";

const logger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: "mcp-audit.log" }),
  ],
});

// Log all tool executions
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  logger.info("Tool execution", {
    tool: name,
    params: sanitizeParams(args),
    timestamp: new Date().toISOString(),
    client: request.context?.clientName,
  });

  // Execute tool...
});

function sanitizeParams(params: any): any {
  const sanitized = { ...params };

  // Remove sensitive data
  if (sanitized.password) sanitized.password = "***";
  if (sanitized.apiKey) sanitized.apiKey = "***";

  return sanitized;
}

For comprehensive security practices, see our enterprise automation security guide.


Performance Optimization

Connection Pooling

class BrowserConnectionPool {
  private connections: BrowserContext[] = [];
  private maxConnections = 5;

  async getConnection(): Promise<BrowserContext> {
    // Reuse existing connection if available
    for (const conn of this.connections) {
      if (!conn.isBusy()) {
        return conn;
      }
    }

    // Create new connection if under limit
    if (this.connections.length < this.maxConnections) {
      const conn = new BrowserContext();
      await conn.connect();
      this.connections.push(conn);
      return conn;
    }

    // Wait for available connection
    return this.waitForConnection();
  }

  private async waitForConnection(): Promise<BrowserContext> {
    return new Promise((resolve) => {
      const checkInterval = setInterval(() => {
        for (const conn of this.connections) {
          if (!conn.isBusy()) {
            clearInterval(checkInterval);
            resolve(conn);
            return;
          }
        }
      }, 100);
    });
  }
}

Response Caching

import NodeCache from "node-cache";

const cache = new NodeCache({
  stdTTL: 300, // 5 minutes
  checkperiod: 60,
});

case "browser_screenshot": {
  const cacheKey = `screenshot:${args.url}`;

  // Check cache
  const cached = cache.get<string>(cacheKey);
  if (cached) {
    return {
      content: [
        { type: "image", data: cached, mimeType: "image/png" },
      ],
    };
  }

  // Generate screenshot
  const screenshot = await browser.screenshot();

  // Cache result
  cache.set(cacheKey, screenshot);

  return {
    content: [
      { type: "image", data: screenshot, mimeType: "image/png" },
    ],
  };
}

Parallel Execution

case "check_multiple_urls": {
  const { urls } = args as { urls: string[] };

  // Execute checks in parallel
  const results = await Promise.allSettled(
    urls.map(async (url) => {
      await browser.navigate(url);
      const status = await browser.getPageStatus();
      return { url, status };
    })
  );

  const formatted = results.map((result, index) => {
    if (result.status === "fulfilled") {
      return `${urls[index]}: ${result.value.status}`;
    } else {
      return `${urls[index]}: ${result.reason}`;
    }
  });

  return {
    content: [
      { type: "text", text: formatted.join("\n") },
    ],
  };
}

Advanced Patterns

Stateful Workflows

class WorkflowContext {
  private state: Map<string, any> = new Map();

  set(key: string, value: any): void {
    this.state.set(key, value);
  }

  get(key: string): any {
    return this.state.get(key);
  }

  clear(): void {
    this.state.clear();
  }
}

const workflowContext = new WorkflowContext();

// Multi-step workflow
case "start_workflow": {
  workflowContext.set("startTime", Date.now());
  workflowContext.set("currentStep", "initialized");
  return { content: [{ type: "text", text: "Workflow started" }] };
}

case "continue_workflow": {
  const step = workflowContext.get("currentStep");
  // Continue from where we left off...
}

Event Streaming

import { EventEmitter } from "events";

class MCPEventEmitter extends EventEmitter {
  emitProgress(tool: string, progress: number): void {
    this.emit("progress", { tool, progress });
  }

  emitResult(tool: string, result: any): void {
    this.emit("result", { tool, result });
  }
}

const events = new MCPEventEmitter();

// In tool handler
case "long_running_task": {
  events.emitProgress("long_running_task", 0);

  await step1();
  events.emitProgress("long_running_task", 33);

  await step2();
  events.emitProgress("long_running_task", 66);

  await step3();
  events.emitProgress("long_running_task", 100);

  events.emitResult("long_running_task", { success: true });

  return { content: [{ type: "text", text: "Task completed" }] };
}

Resource Management

import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";

// Register resources (context data)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: "browser://current-url",
        name: "Current Browser URL",
        mimeType: "text/plain",
      },
      {
        uri: "browser://page-title",
        name: "Current Page Title",
        mimeType: "text/plain",
      },
    ],
  };
});

// Provide resource data
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  switch (uri) {
    case "browser://current-url":
      const url = await browser.getCurrentUrl();
      return {
        contents: [
          { uri, mimeType: "text/plain", text: url },
        ],
      };

    case "browser://page-title":
      const title = await browser.getTitle();
      return {
        contents: [
          { uri, mimeType: "text/plain", text: title },
        ],
      };

    default:
      throw new Error(`Unknown resource: ${uri}`);
  }
});

Testing MCP Servers

Unit Testing

import { describe, it, expect, beforeEach } from "vitest";
import { createMockBrowserContext } from "./mocks.js";

describe("Browser Navigation Tool", () => {
  let browser: MockBrowserContext;

  beforeEach(() => {
    browser = createMockBrowserContext();
  });

  it("should navigate to valid URL", async () => {
    await browser.navigate("https://example.com");
    expect(browser.currentUrl).toBe("https://example.com");
  });

  it("should throw error for invalid URL", async () => {
    await expect(
      browser.navigate("not-a-url")
    ).rejects.toThrow("Invalid URL");
  });

  it("should handle connection errors", async () => {
    browser.simulateOffline();
    await expect(
      browser.navigate("https://example.com")
    ).rejects.toThrow("Connection failed");
  });
});

Integration Testing with MCP Inspector

# Install MCP Inspector
npm install -g @modelcontextprotocol/inspector

# Test your server
mcp-inspector npx tsx src/index.ts

# Opens web interface for testing tools

End-to-End Testing

import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

describe("MCP Server E2E", () => {
  it("should handle full workflow", async () => {
    // Start MCP server
    const serverProcess = exec("npx tsx src/index.ts");

    // Wait for startup
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // Send test request via stdio
    const result = await sendMCPRequest({
      method: "tools/call",
      params: {
        name: "browser_navigate",
        arguments: { url: "https://example.com" },
      },
    });

    expect(result.content[0].text).toContain("Successfully navigated");

    // Cleanup
    serverProcess.kill();
  });
});

Deployment Strategies

Packaging for Distribution

{
  "name": "@yourorg/mcp-browser",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "mcp-browser": "./dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc",
    "prepublish": "npm run build"
  }
}

NPM Distribution

# Build
npm run build

# Publish
npm publish --access public

# Users install with
npx @yourorg/mcp-browser@latest

Docker Deployment

FROM node:20-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY dist ./dist

CMD ["node", "dist/index.js"]

System Service (systemd)

[Unit]
Description=MCP Browser Automation Server
After=network.target

[Service]
Type=simple
User=mcpserver
WorkingDirectory=/opt/mcp-server
ExecStart=/usr/bin/node /opt/mcp-server/dist/index.js
Restart=on-failure

[Install]
WantedBy=multi-user.target

Monitoring & Observability

Metrics Collection

import { Counter, Histogram, Registry } from "prom-client";

const registry = new Registry();

const toolCallsCounter = new Counter({
  name: "mcp_tool_calls_total",
  help: "Total number of tool calls",
  labelNames: ["tool", "status"],
  registers: [registry],
});

const toolDurationHistogram = new Histogram({
  name: "mcp_tool_duration_seconds",
  help: "Tool execution duration",
  labelNames: ["tool"],
  registers: [registry],
});

// In tool handler
const start = Date.now();
try {
  await executeTool();
  toolCallsCounter.inc({ tool: name, status: "success" });
} catch (error) {
  toolCallsCounter.inc({ tool: name, status: "error" });
} finally {
  const duration = (Date.now() - start) / 1000;
  toolDurationHistogram.observe({ tool: name }, duration);
}

// Expose metrics endpoint (HTTP transport)
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", registry.contentType);
  res.end(await registry.metrics());
});

Structured Logging

import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  transport: {
    target: "pino-pretty",
    options: {
      colorize: true,
    },
  },
});

// In tool handler
logger.info({
  msg: "Tool execution started",
  tool: name,
  params: sanitizeParams(args),
  requestId: generateRequestId(),
});

Frequently Asked Questions

Q: Can I use MCP with languages other than TypeScript/JavaScript? A: Yes! MCP is language-agnostic. The protocol uses JSON-RPC over stdio/HTTP, so you can implement servers in Python, Go, Rust, or any language that can read/write JSON.

Q: How do I debug MCP server issues? A: Use the MCP Inspector tool to test your server interactively. Add logging to stderr (stdout is reserved for protocol messages).

Q: Can multiple clients connect to one MCP server? A: With stdio transport, it's one-to-one. For multiple clients, use HTTP or WebSocket transport with proper session management.

Q: How do I handle long-running operations? A: Implement async task patterns with status checking. Return immediately with a task ID, then provide a separate tool to check task status.

Q: What's the maximum message size for MCP? A: The protocol doesn't enforce limits, but practical limits depend on your transport. For stdio, large messages work fine. Consider streaming for very large data.

Q: How do I version my MCP tools? A: Include version in tool names (browser_navigate_v2) or use semantic versioning in your server metadata. AI applications can select appropriate versions.

Q: Can MCP servers call other MCP servers? A: Yes! You can create composite MCP servers that delegate to other servers, building complex tool ecosystems.

Q: How do I handle authentication for external APIs? A: Store credentials securely (environment variables, secret management) and validate them before making API calls. Never pass credentials through MCP messages.

Q: Can I use MCP for production automation? A: Yes, with proper error handling, monitoring, rate limiting, and security measures. Many teams use MCP for CI/CD, monitoring, and internal tools.

Q: What's the performance overhead of MCP? A: Minimal. JSON serialization and IPC (stdio/HTTP) add microseconds to milliseconds. The bottleneck is typically the tool execution itself, not the protocol.


Additional Resources

Official Documentation

Community Projects


Conclusion

Model Context Protocol is more than a specification—it's a new paradigm for AI-tool integration. By understanding MCP's architecture, you can build powerful integrations that extend AI capabilities into any domain.

Key takeaways:

Simple Protocol: JSON-RPC over stdio/HTTP makes implementation straightforward ✅ Flexible Architecture: Support for tools, resources, and prompts covers most use cases ✅ Production Ready: With proper error handling and security, MCP works in production ✅ Extensible: Build custom servers for any domain (databases, APIs, hardware, etc.) ✅ Growing Ecosystem: Increasing IDE support and community projects

Whether you're building custom automation, integrating with existing systems, or exploring new AI application patterns, MCP provides the foundation for seamless AI-tool interaction.


Ready to build with MCP? Try Onpiste browser automation or start developing your own MCP server today.

Share this article