AI & Machine Learning Engineering

Model Context Protocol (MCP): Building MCP Servers from Scratch

MatterAI
MatterAI
16 min read·

Model Context Protocol (MCP): Building MCP Servers from Scratch

The Model Context Protocol (MCP) standardizes how AI applications talk to external tools and data sources. Instead of every agent framework inventing its own tool-calling convention, MCP defines a client-server protocol: the AI host (an IDE, agent, or chat app) connects to MCP servers that expose tools (actions), resources (readable data), and prompts (reusable templates). This guide builds MCP servers from scratch and covers the decisions that matter in production.

The MCP Architecture

MCP has three roles:

  • Host: the application the user interacts with (Orbital, an IDE, a chat app). It owns the LLM and the conversation.
  • Client: one connection from the host to a server. A host can hold many clients.
  • Server: exposes capabilities to the host. Servers are stateless by design; state lives in the host or in external systems.
┌─────────────┐   MCP Client   ┌──────────────┐
│    Host     │◄──────────────►│ MCP Server   │
│  (Orbital)  │                │  (your tool) │
└─────────────┘                └──────────────┘

The protocol is JSON-RPC 2.0 over one of two transports: stdio (local, spawned as a child process) or HTTP with Server-Sent Events (remote, over the network).

Transport Choice: stdio vs HTTP

TransportUse CaseAuthLatency
stdioLocal tools, IDE integrationsNone (local)Lowest
HTTP/SSERemote services, multi-tenantOAuth 2.0, keysNetwork

Start with stdio for anything that runs on the developer's machine. Move to HTTP when the server must be shared, deployed, or accessed by many hosts.

Building a Server with the TypeScript SDK

Install the SDK and scaffold a minimal server:

npm install @modelcontextprotocol/sdk zod
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "github-ops",
  version: "1.0.0",
});

// A tool: an action the LLM can invoke
server.tool(
  "get_issue",
  "Fetch a GitHub issue by number",
  {
    owner: z.string(),
    repo: z.string(),
    issueNumber: z.number().int().positive(),
  },
  async ({ owner, repo, issueNumber }) => {
    const issue = await fetchIssue(owner, repo, issueNumber);
    return {
      content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
    };
  },
);

// A resource: data the host can read (e.g. for context injection)
server.resource(
  "repo-readme",
  "README of the current repository",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: await readReadme(),
      },
    ],
  }),
);

// A prompt: a reusable template the host can insert into the conversation
server.prompt(
  "review-pr",
  "Generate a code review checklist for a PR",
  { prNumber: z.number() },
  ({ prNumber }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Review PR #${prNumber}. Focus on correctness, security, and test coverage.`,
        },
      },
    ],
  }),
);

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

The zod schemas do double duty: they validate arguments and generate the JSON Schema the host sends to the LLM, which is what lets the model call your tool with correct arguments.

The Python Equivalent

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("github-ops")

@mcp.tool()
def get_issue(owner: str, repo: str, issue_number: int) -> str:
    """Fetch a GitHub issue by number."""
    issue = fetch_issue(owner, repo, issue_number)
    return json.dumps(issue, indent=2)

@mcp.resource("repo://readme")
def repo_readme(uri: str) -> str:
    return read_readme()

if __name__ == "__main__":
    mcp.run()  # stdio by default; use mcp.run(transport="sse") for HTTP

Tools vs Resources vs Prompts

  • Tools are for actions with side effects: create an issue, deploy a service, run a query. The LLM decides when to call them.
  • Resources are read-only data the host can pull into context: a file, a schema, a config. They answer "what does the model need to know?"
  • Prompts are templates that package a workflow: "review this PR", "explain this error". They give users a one-click way to start a task.

A common mistake is exposing everything as a tool. If the model needs a file's contents to answer, that is a resource, not a tool. Tools should be verbs; resources should be nouns.

Error Handling and Tool Design

Tools fail. Return structured errors instead of throwing, so the LLM can recover:

server.tool(
  "deploy_service",
  "Deploy a service to staging",
  { service: z.string() },
  async ({ service }) => {
    try {
      const result = await deploy(service);
      return { content: [{ type: "text", text: `Deployed: ${result.url}` }] };
    } catch (err) {
      return {
        content: [
          { type: "text", text: `Deploy failed: ${(err as Error).message}` },
        ],
        isError: true,
      };
    }
  },
);

Design rules that keep tools safe and useful:

  1. Narrow scopes: one tool, one job. get_issue and create_issue are separate tools.
  2. Validate aggressively: zod schemas reject bad arguments before your code runs.
  3. Never trust the model's input: treat every argument as attacker-controlled. No shell interpolation, no raw SQL.
  4. Return enough context: include IDs, URLs, and status so the model can act on the result without guessing.
  5. Cap expensive operations: add timeouts and result limits to every tool that touches external systems.

Security Model

MCP servers are a new attack surface: the LLM is now a user of your APIs. The OWASP LLM guidance applies directly:

  • Authorization: the server must enforce its own permissions. The host's user may not have rights to every tool. Check identity and scope per call.
  • Confirmation for destructive actions: tools that delete, overwrite, or spend money should require explicit confirmation from the human, not just the model.
  • Input validation: validate every argument against an allowlist where possible. A tool that accepts a URL should validate the scheme and host.
  • Secrets: never return secrets in tool output. Redact tokens, keys, and internal URLs before they reach the model.
  • Prompt injection: tool output flows back into the model's context. Treat tool results as untrusted data and instruct the model to ignore instructions found in them.

Connecting a Host

Register the server in your host's config. For Orbital and most IDE-based hosts, a local config file points at the command:

{
  "mcpServers": {
    "github-ops": {
      "command": "node",
      "args": ["dist/server.js"],
      "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
    }
  }
}

For remote servers, use the URL form:

{
  "mcpServers": {
    "prod-db": {
      "url": "https://mcp.example.com/db",
      "headers": { "Authorization": "Bearer ${MCP_TOKEN}" }
    }
  }
}

Implementation Checklist

  • Choose stdio for local tools, HTTP/SSE for shared services
  • Define tools as narrow verbs with zod-validated arguments
  • Expose read-only data as resources, not tools
  • Return structured errors with isError so the LLM can recover
  • Enforce authorization inside the server, per tool
  • Require human confirmation for destructive actions
  • Redact secrets from all tool output
  • Treat tool results as untrusted input (prompt injection defense)
  • Add timeouts and result caps to every external call
  • Test with a real host before shipping

MatterAI builds frontier AI infrastructure for engineering teams — from inference-optimized models to autonomous coding agents and agentic code reviews.

Explore what we're building:

  • Orbital IDE — Autonomous AI coding agent with background agents and deep codebase memory
  • AI Code Reviews — Agentic pre-commit reviews across GitHub, GitLab, and Bitbucket
  • Axon Models — Frontier-grade reasoning models at 70% lower inference cost

Get started free - https://app.matterai.so


Follow us on X · LinkedIn · GitHub

Share this Guide:

Ship Faster. Ship Safer.

Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.

No credit card requiredSOC 2 Type IISetup in 2 min