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
- MCP Architecture Overview
- Protocol Specifications
- Transport Layer Implementation
- Tool Definition & Registration
- Browser Automation Integration
- Building Custom MCP Servers
- Error Handling & Resilience
- Security Considerations
- Performance Optimization
- Advanced Patterns
- Frequently Asked Questions
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
- AI Request: User asks AI assistant for action requiring external tool
- Tool Selection: AI determines which MCP tool to invoke
- Protocol Message: Client sends structured request to MCP server
- Tool Execution: Server processes request and calls external service
- Response: Results flow back through protocol to AI
- 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:
- Name: Unique identifier
- Description: What the tool does (AI uses this)
- Input Schema: JSON Schema for parameters
- 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
- MCP Specification - Official protocol documentation
- MCP TypeScript SDK - Reference implementation
- MCP Inspector - Testing and debugging tool
Community Projects
- Onpiste MCP Server - Browser automation reference implementation
- Awesome MCP - Curated list of MCP servers
- MCP Examples - Official example servers
Related Reading
- MCP IDE Integration Guide - Set up MCP in Cursor, VS Code, Claude Desktop
- MCP Use Cases Collection - 15 practical automation examples
- Multi-Agent Architecture - How agents leverage MCP tools
- Natural Language Automation - AI-powered browser control through MCP
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.
