DEV Community

Programming Central
Programming Central

Posted on

The Standardization of Agent Context: Why MCP is the New REST

Remember the Wild West of web development? Back before REST and HTTP standardized how systems talk to one another, building a modern web application meant navigating a labyrinth of proprietary remote procedure calls (RPC), CORBA implementations, and custom XML-over-TCP sockets. If you wanted to hook up a client application to three different backend services—say, a user management ledger, an inventory database, and a billing endpoint—you had to write three entirely different networking stacks. You had to learn three different serialization formats, wrestle with vendor-specific connection lifecycles, and maintain brittle error-handling wrappers.

It was absolute chaos.

Today, agentic AI engineering finds itself trapped in that exact same pre-REST era. If you are building AI agents today, you already know the pain. You write custom integration scaffolding just to get a Large Language Model to talk to your enterprise database or fetch live monitoring logs. You craft ad-hoc system prompts, parse erratic model outputs, manually map out tool-calling schemas tailored to a single vendor’s API, and write fragile JSON-parsing loops that break the moment the model decides to format its response with an extra markdown tag.

If you hook up a brilliant tool to query a Jira instance for an OpenAI-based agent, that tool is completely incompatible with an Anthropic-based agent or a local open-weights model running via Ollama.

Enter the Model Context Protocol (MCP). This is not just another incremental framework update; it is the fundamental architectural standardization that agent engineering has been starving for. MCP is the "REST for AI context." It decouples the data consumer (the LLM client) from the data producer (the MCP server), bringing sanity, security, and scalability to the world of generative AI agents.


The Theoretical Foundation: Tokens, Context Windows, and Architectural Fragmentation

To understand why MCP is shaking up the AI engineering world, we have to look under the hood at how LLMs interact with the outside world.

An LLM does not inherently know about your corporate database, your company’s internal Slack channels, or the file structure sitting on your local disk. It only knows what is injected directly into its active Context Window. To give an agent access to external data, every piece of information—whether it is a database schema, an API documentation file, or the return value of a terminal command—must be serialized into text, tokenized, and shoved straight into the model's memory.

This brings us to the harsh economic and technical reality of the Token. A token is the fundamental unit of text processing for LLMs, roughly equal to four characters in English text. Because models process text token-by-token, and because context windows are strictly bounded by both financial costs and quadratic attention-scaling penalties (the infamous "lost in the middle" phenomenon), every single byte of context matters.

In the pre-MCP era, developers regularly threw bloated, unstructured JSON payloads, messy markdown tables, and massive system instructions directly at the model, crossing their fingers that the LLM could parse the noise. This ad-hoc approach did two terrible things:

  1. It burned through expensive tokens on verbose, inconsistent system instructions.
  2. It introduced massive security vulnerabilities, such as prompt injection attacks hidden inside untrusted tool outputs.

MCP solves this fragmentation by establishing a universal contract between LLM hosts (clients) and capability providers (servers). Just as your web browser doesn't care whether an Apache or Nginx server written in C, Go, or Rust is powering a website—so long as they both speak HTTP—an LLM application client running MCP does not need to know the internal implementation details of an MCP server. Whether the server is querying a local SQLite database, executing commands inside an isolated Docker container, or scraping a live webpage, the communication happens over a standardized, decoupled wire protocol built on top of JSON-RPC 2.0.


The Architectural Anatomy of MCP: Clients, Servers, and Transport Layers

To build reliable agentic systems, you need to understand the three distinct pillars that make up the Model Context Protocol: Hosts/Clients, Servers, and Transports.

1. MCP Hosts and Clients

The Host is your overarching AI application. This could be an AI-powered IDE extension like Cursor or VS Code, a desktop agent application, or a custom TypeScript orchestrator built with advanced control flow graphs. The Host is responsible for managing user intent, enforcing security boundaries, and rendering the user interface.

The Client lives inside the Host. Its sole job is to maintain a 1:1 session connection with an MCP Server. Crucially, the client does not execute tools itself. Instead, it discovers what tools, resources, and prompts are available from the server, translates those capabilities into the native schema format expected by the current LLM (such as OpenAI's function-calling definitions or Anthropic's tool-use blocks), intercepts the model's tool calls, and routes them back to the server for execution.

2. MCP Servers

An MCP Server is a lightweight, independent process designed to expose three specific primitives to the client:

  • Resources: Passive data sources that the client can read. Resources act like files or database records. They possess unique URIs (e.g., postgres://users/schema or file:///logs/error.log) and can be read by the client to inject context directly into the LLM prompt window.
  • Tools: Active, executable functions that the model can invoke to perform actions or mutate state. Unlike passive resources, tools require explicit model intent and user authorization. Examples include execute_sql_query, send_slack_message, or restart_server.
  • Prompts: Templates or pre-packaged workflows provided by the server to the client. This allows server maintainers to bundle domain-specific expert prompting strategies directly within the server binary. For instance, a database MCP server might expose a prompt template called optimize_slow_query that guides the LLM to inspect database execution plans in a standardized way.

3. Transport Layers

The transport layer governs how the MCP Client and MCP Server exchange JSON-RPC 2.0 messages. MCP is completely transport-agnostic, but two primary transports dominate real-world architectures:

  • Stdio (Standard Input/Output): The client spawns the MCP server as a local child process (such as a Node.js process, a Python script, or a compiled Go binary). Communication occurs via standard input and output streams using newline-delimited JSON-RPC messages. This transport is exceptionally secure, requires zero network configuration, and is ideal for local desktop tools and single-tenant environments.
  • SSE (Server-Sent Events): For distributed, remote, or multi-tenant cloud architectures, MCP supports communication over HTTP using Server-Sent Events for server-to-client streaming, paired with standard HTTP POST requests for client-to-server commands.

Deep Dive: Solving Tight Coupling and Optimizing the Token Economy

In traditional custom integrations, an agent's orchestration logic is inextricably bound to the data sources it touches. If you write a TypeScript function that queries your internal employee database and injects that data into an LLM prompt, that function contains hardcoded database drivers, connection string environment variables, custom error handling for network timeouts, and specific data-formatting logic designed to compress output to fit the token budget.

If you later decide to switch your primary LLM vendor—say, migrating from an older model to a newer model with a completely different native function-calling schema—you have to refactor your prompt generation code, your tool definitions, and your parsing wrappers. Worse, if a security vulnerability is discovered in how your database query tool sanitizes inputs, you have to patch every single agent application across your entire enterprise that implements that custom query logic.

MCP inverts this relationship through strict interface segregation:

  • Security and Sandboxing: The MCP Server runs in its own process space. If connected via stdio, the host application controls its environment variables, file system access, and network permissions. The LLM never touches the database directly; it merely suggests a tool call to the client, which validates the request against policy engines before passing it to the server.
  • Reusability and Composability: Because an MCP server speaks a universal protocol, a single MCP server for GitHub can be written once by the community and consumed simultaneously by a VS Code extension, a terminal-based coding agent, a Discord bot, and an enterprise workflow orchestrator.

Demand-Paging Your Context Window

A core theoretical challenge in agent design is managing the trade-off between context richness and token expenditure. In naive implementations, developers dump entire database schemas or massive log files straight into the system prompt at startup. This wastes thousands of tokens on irrelevant tables or historical logs that the agent may never touch.

With MCP, context injection is lazy, dynamic, and pull-based:

  1. Discovery Phase: When an MCP client initializes a connection, it queries the server’s capabilities. The server returns a lightweight manifest listing available resources and tools, consuming a negligible number of tokens.
  2. Listing and URI Resolution: The LLM inspects the tool and resource descriptions. If it determines it needs information from a specific resource (such as the schema for an orders table), it issues a read request for that exact URI (postgres://schema/orders).
  3. Targeted Injection: The MCP server fetches only that specific resource, formats it, and returns it to the client for injection into the context window precisely when needed.

This mirrors virtual memory pagination in operating systems: rather than loading an entire multi-gigabyte program into physical RAM all at once, the operating system pages in memory blocks strictly on demand. MCP brings demand-paged memory management directly to the LLM context window.


Building a Production-Ready SaaS MCP Server in TypeScript

To truly appreciate how MCP standardizes data access and tool integration, let us examine a concrete, self-contained TypeScript implementation of an MCP Server built for a SaaS customer support environment.

This server exposes a database tool to fetch user subscription details and account metrics, allowing any MCP-compliant client to securely query customer data without writing custom API client wrappers for every new model release.

/**
 * @file mcp-saas-server.ts
 * @description A foundational, self-contained Model Context Protocol (MCP) server 
 * built in TypeScript for a SaaS customer support environment. It exposes standardized 
 * tools and resource schemas over standard input/output (stdio) transport.
 */

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ErrorCode,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";

// ==========================================
// MOCK DATABASE & DATA LAYER
// ==========================================

/**
 * Interface representing a SaaS Customer Profile.
 */
interface CustomerProfile {
  id: string;
  email: string;
  plan: "starter" | "pro" | "enterprise";
  mrr: number;
  status: "active" | "churned" | "trial";
  supportTier: "standard" | "priority" | "dedicated";
}

/**
 * In-memory data store acting as our SaaS backend database.
 */
const MOCK_SAAS_DATABASE: Record<string, CustomerProfile> = {
  "usr_101": {
    id: "usr_101",
    email: "alice@acme-corp.io",
    plan: "enterprise",
    mrr: 1250.00,
    status: "active",
    supportTier: "dedicated",
  },
  "usr_102": {
    id: "usr_102",
    email: "bob@startup-labs.com",
    plan: "pro",
    mrr: 150.00,
    status: "active",
    supportTier: "priority",
  },
};

// ==========================================
// MCP SERVER INITIALIZATION
// ==========================================

/**
 * Initialize the MCP Server instance with metadata describing the service.
 * This metadata is shared with the client during the capability negotiation handshake.
 */
const server = new Server(
  {
    name: "saas-support-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},      // Indicates this server provides executable functions
      resources: {},  // Indicates this server provides readable data URIs
    },
  }
);

// ==========================================
// TOOL REGISTRATION (Executable Capabilities)
// ==========================================

/**
 * Handler for listing available tools. 
 * When an LLM client connects, it calls this endpoint to discover what actions it can execute.
 */
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "get_customer_profile",
        description: "Retrieves billing, plan, and support tier details for a specific SaaS customer by their unique user ID.",
        inputSchema: {
          type: "object",
          properties: {
            userId: {
              type: "string",
              description: "The unique user identifier, e.g., usr_101",
            },
          },
          required: ["userId"],
        },
      },
    ],
  };
});

/**
 * Handler for executing tool calls requested by the LLM client.
 * Validates inputs, interacts with the mock database, and returns a structured content response.
 */
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name !== "get_customer_profile") {
    throw new McpError(
      ErrorCode.MethodNotFound,
      `Unknown tool: ${request.params.name}`
    );
  }

  const args = request.params.arguments as { userId?: string };

  if (!args || typeof args.userId !== "string") {
    throw new McpError(
      ErrorCode.InvalidParams,
      "Invalid or missing 'userId' parameter in tool arguments."
    );
  }

  const customer = MOCK_SAAS_DATABASE[args.userId];

  if (!customer) {
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            error: `Customer with ID '${args.userId}' not found in the SaaS database.`,
          }),
        },
      ],
      isError: true,
    };
  }

  return {
    content: [
      {
        type: "text",
        text: JSON.stringify(customer, null, 2),
      },
    ],
  };
});

// ==========================================
// RESOURCE REGISTRATION (Data Context)
// ==========================================

/**
 * Handler for listing static or dynamic resources. 
 * Resources represent passive context (documents, system health, static policies) 
 * that can be read directly by the LLM context window.
 */
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: "saas://policies/sla",
        name: "SaaS Service Level Agreement (SLA)",
        mimeType: "text/markdown",
        description: "Defines uptime guarantees, response times, and credit refunds by support tier.",
      },
    ],
  };
});

/**
 * Handler for reading a specific resource content via its URI.
 */
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const uri = request.params.uri;

  if (uri === "saas://policies/sla") {
    const slaContent = `# SaaS Service Level Agreement (SLA)
- **Dedicated Support Tier**: 1-hour response time guarantee for critical incidents. 99.99% uptime.
- **Priority Support Tier**: 4-hour response time guarantee. 99.9% uptime.
- **Standard Support Tier**: 24-hour response time. 99.5% uptime.`;

    return {
      contents: [
        {
          uri,
          mimeType: "text/markdown",
          text: slaContent,
        },
      ],
    };
  }

  throw new McpError(
    ErrorCode.InvalidRequest,
    `Resource not found: ${uri}`
  );
});

// ==========================================
// TRANSPORT & SERVER STARTUP
// ==========================================

/**
 * Asynchronous function to establish the standard I/O transport layer 
 * and bind the MCP server instance to it.
 */
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);

  // Log startup to stderr so stdout remains pristine for MCP JSON-RPC protocol frames
  console.error("SaaS Support MCP Server running on stdio transport.");
}

main().catch((error) => {
  console.error("Fatal error running MCP server:", error);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Line-by-Line Breakdown of the Implementation

  1. Imports and SDK Setup: We import the core Server class from @modelcontextprotocol/sdk/server/index.js, the StdioServerTransport for standard input/output communication channels, and standard request schemas and error types from @modelcontextprotocol/sdk/types.js. This guarantees the type safety and protocol compliance required by the Model Context Protocol specification.
  2. Interface and Mock Data Structures: We define the CustomerProfile TypeScript interface to enforce strict typing on SaaS user attributes (id, email, plan, mrr, status, supportTier). We then instantiate MOCK_SAAS_DATABASE, acting as our simulated backend database store.
  3. Server Instance Configuration: We instantiate the Server class, supplying descriptive metadata (name: "saas-support-mcp-server", version: "1.0.0"). We declare our capabilities (tools and resources), informing connecting clients about the server's architectural features during the initial protocol handshake.
  4. Tool Discovery Handler (ListToolsRequestSchema): We register a request handler for listing tools. When an agent requests available tools, this handler returns an array containing metadata about get_customer_profile, including its human-readable description and a JSON Schema (inputSchema) specifying that a userId string parameter is strictly required.
  5. Tool Execution Handler (CallToolRequestSchema): We register the execution handler for tool calls. When the LLM decides to execute get_customer_profile, the MCP client forwards the arguments here. We validate that the requested tool name matches, verify that the userId argument exists and is a string, and query our mock database.
  6. Error Handling and Guard Clauses: Inside the tool execution handler, if a user ID is missing or malformed, we throw an McpError using standard error codes (ErrorCode.InvalidParams). If the user does not exist in the database, we return a structured JSON error response flagged with isError: true, allowing the agent to perform Tool Use Reflection and correct its trajectory in subsequent steps.
  7. Resource Discovery and Reading Handlers: We register handlers for listing and reading passive resources. Unlike tools (which execute code and cause side effects), resources represent static or contextual data assets. We expose a resource URI saas://policies/sla representing the corporate Service Level Agreement document so the agent can reference SLA commitments on the fly without cluttering the prompt window.
  8. Transport & Server Startup: We establish our communication channel via StdioServerTransport, binding our MCP server instance to it. Note that we log startup messages to stderr rather than stdout, ensuring that standard output remains completely pristine for JSON-RPC protocol frames.

Security, Governance, and Human-in-the-Loop Boundaries

As AI agents transition from passive chat interfaces to active execution engines capable of modifying files, executing shell commands, and triggering financial transactions, governance becomes your number one engineering priority.

In unstandardized agent architectures, security is usually an afterthought. Developers scatter ad-hoc if/else checks throughout custom tool functions to prevent destructive actions like dropping database tables or running dangerous shell commands. This inevitably leads to security gaps where a clever prompt injection attack can bypass your custom validation layers.

MCP centralizes the trust boundary. Because all tool execution requests must flow through the MCP Client, your host application can implement universal middleware, comprehensive audit logging, and strict authorization policies at the client layer:

  • Human-in-the-Loop Interception: Before an MCP client forwards a tool execution request to an MCP server, the host application can intercept the payload and trigger a UI prompt requiring explicit user confirmation (e.g., "Agent is attempting to execute DELETE FROM users WHERE active = false. Allow or Deny?").
  • Capability Scoping: During the initial handshake, the client can negotiate and restrict which tools the server is permitted to expose based on the user's role or the current security clearance of the session.
  • Immutable Audit Trails: Because all interactions adhere to the structured JSON-RPC 2.0 schema, every tool call, parameter passed, resource read, and execution result can be logged in a standardized format for compliance and post-mortem analysis.

Conclusion: The Future is Protocol-Driven

The Model Context Protocol represents a monumental shift in how we build generative AI applications. By moving away from brittle, custom integration scaffolding and embracing a universal, open standard for agent context, we are laying the foundational engineering bedrock required for enterprise-grade AI.

Just as the adoption of REST transformed web development from a fragmented wasteland into a cohesive, interoperable ecosystem, MCP is transforming agentic AI. Whether you are building customer support automation, autonomous coding assistants, or complex multi-agent orchestrations, adopting MCP ensures your tools are secure, reusable, and future-proof against the next wave of LLM releases.

It's time to retire the spaghetti integration code. Welcome to the era of protocol-driven AI agent engineering.

The concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the book **Model Context Protocol (MCP) & Computer Use. Standardizing Tool Integration, Vision-Driven Browser Automation, and Agent Governance in TypeScript, you can find it here. Check also the many other ebooks.

Top comments (0)