📢
Admissions Open for October 2026 Batch | Free Career Counselling | Limited Scholarships
Register Now →

AI & Careers

What Is MCP? Build Your First Model Context Protocol Server

Quick answer: The Model Context Protocol (MCP) is an open standard for connecting AI applications to external systems. The official docs describe it as "a USB-C port for AI applications": instead of writing a bespoke integration for every assistant, you write one MCP server and any MCP-compatible client can use it. A server exposes three things: tools (functions the model can call), resources (data it can read) and prompts (reusable templates). The current protocol version is 2026-07-28, and the Python SDK is mcp 2.2.0, which needs Python 3.10 or newer. This guide builds a working server that exposes a company's order database to Claude, and covers the parts that matter when the database belongs to a customer rather than to you.

Why this is on the Forward Deployed Engineer job description

If you have read Anthropic's Forward Deployed Engineer posting, you will have seen MCP named directly: the role is expected to deliver technical artifacts "like MCP servers and agents for production workflows" inside customer systems. That is not a coincidence of wording. MCP is becoming the standard answer to the question an FDE faces on day one of every engagement: the customer has systems, the model needs to reach them, what do I build?

Before MCP, the answer was a custom integration per assistant per system. Now it is one server per system, usable by any client that speaks the protocol. That is a meaningful change in how much work a single engineer can deliver, which is exactly why the role and the protocol are growing together. For the wider picture of the role itself, see our Forward Deployed Engineer career guide.

What MCP actually is

MCP is an open-source standard for connecting AI applications to external systems: data sources such as files and databases, tools such as search and calculators, and workflows such as specialised prompts (modelcontextprotocol.io).

Three participants matter, and people mix up the words constantly:

ParticipantWhat it isExample
MCP HostThe AI application coordinating everythingClaude Desktop, Claude Code, VS Code
MCP ClientA connector inside the host, one per serverCreated by the host automatically
MCP ServerThe program that provides context and actionsWhat you are about to write

A host creates one client per server, and each client holds a dedicated connection. "Server" here says nothing about where the code runs: a local server on the stdio transport runs on the same machine as the host, while a remote server on the Streamable HTTP transport runs elsewhere and typically serves many clients.

Underneath, MCP has a data layer (JSON-RPC 2.0 messages defining the primitives) and a transport layer (stdio for local processes, Streamable HTTP for remote). The SDK hides most of this, which is why you can build something useful without reading the specification end to end.

The three things a server can expose

  • Tools: executable functions the AI can invoke to do something, such as querying a database or calling an API. Discovered with tools/list, executed with tools/call.
  • Resources: data sources that provide context, such as a file's contents or a database schema. Read, not executed.
  • Prompts: reusable templates that structure an interaction, such as a system prompt or a set of few-shot examples.

A good mental model: tools are verbs, resources are nouns, prompts are recipes. Our example server uses tools and one resource.

What changed in the 2026-07-28 spec

Worth knowing, because a great deal of MCP tutorial content online still describes the older design and will mislead you.

  • MCP is now a stateless protocol. Every request carries the protocol version and capabilities in a _meta field, so the server infers nothing from previous requests.
  • Discovery happens through server/discover, a request every server must implement, which returns supported versions and capabilities. Calling it is optional for clients, and its response is cacheable.
  • Sampling is deprecated as of 2026-07-28. It previously let a server ask the client's application for a model completion. New implementations are told to integrate with LLM provider APIs directly.
  • Logging is deprecated too. New servers should log to stderr on stdio, or use OpenTelemetry.
  • Elicitation is now the client primitive that matters: it lets a server ask the user for more information or confirm an action, via elicitation/create.
  • Change notifications are opt-in. The client opens a long-lived subscriptions/listen stream naming the notification types it wants.

If you follow a tutorial that has your server requesting completions through sampling, you are reading something written against an older version of the protocol.

Build a server: exposing an order database

Most MCP tutorials build a weather client. We are going to build something closer to real forward deployment work: a server that lets Claude answer questions about a company's orders, backed by a SQLite database, without letting it touch anything it should not.

Everything below was run before publishing. You need Python 3.10 or newer, because mcp 2.2.0 requires it.

1. Set up the project

uv init orders
cd orders
uv venv
source .venv/bin/activate
uv add "mcp[cli]"

If you do not use uv, a plain virtual environment and pip install "mcp[cli]" works the same way.

2. Create a database to stand in for the customer's system

In a real engagement this is the customer's existing database and you would be connecting to it read-only. For the tutorial, seed a local one with seed.py:

import sqlite3

conn = sqlite3.connect("orders.db")
conn.executescript("""
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
    order_id   TEXT PRIMARY KEY,
    customer   TEXT NOT NULL,
    city       TEXT NOT NULL,
    amount_inr INTEGER NOT NULL,
    status     TEXT NOT NULL,
    placed_on  TEXT NOT NULL
);
INSERT INTO orders VALUES
 ('ORD-1001','Sharda Traders','Nagpur',48500,'delivered','2026-09-02'),
 ('ORD-1002','Deshmukh Agro','Amravati',127000,'delivered','2026-09-04'),
 ('ORD-1003','Vidarbha Foods','Nagpur',19250,'pending','2026-09-11'),
 ('ORD-1004','Kalbande & Sons','Wardha',64300,'shipped','2026-09-15'),
 ('ORD-1005','Orange City Mills','Nagpur',210400,'delivered','2026-09-19'),
 ('ORD-1006','Shree Distributors','Pune',88000,'cancelled','2026-09-21'),
 ('ORD-1007','Deshmukh Agro','Amravati',73500,'delivered','2026-09-26');
""")
conn.commit()
conn.close()

3. Write the server

Create orders.py. Note the import: the server class is MCPServer, imported from mcp.server.

import sqlite3
from pathlib import Path

from mcp.server import MCPServer

mcp = MCPServer("orders")

DB_PATH = Path(__file__).parent / "orders.db"
MAX_ROWS = 50


def query(sql: str, params: tuple = ()) -> list[dict]:
    """Run a read-only query and return rows as dictionaries."""
    conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
    conn.row_factory = sqlite3.Row
    try:
        rows = conn.execute(sql, params).fetchmany(MAX_ROWS)
        return [dict(r) for r in rows]
    finally:
        conn.close()

Three decisions in that helper are the whole point of this article, and we come back to them below: the connection is opened mode=ro, queries are parameterised, and results are capped at MAX_ROWS.

Now the tools. The SDK reads your type hints and docstring to generate the tool definition, so the docstring is not decoration: it is what the model sees when deciding whether to call your tool.

@mcp.tool()
async def find_order(order_id: str) -> str:
    """Look up a single customer order by its ID.

    Args:
        order_id: The order reference, for example ORD-1004
    """
    rows = query(
        "SELECT order_id, customer, city, amount_inr, status, placed_on "
        "FROM orders WHERE order_id = ?",
        (order_id,),
    )
    if not rows:
        return f"No order found with ID {order_id}."

    o = rows[0]
    return (
        f"Order {o['order_id']}\n"
        f"Customer: {o['customer']} ({o['city']})\n"
        f"Amount: INR {o['amount_inr']:,}\n"
        f"Status: {o['status']}\n"
        f"Placed on: {o['placed_on']}"
    )


@mcp.tool()
async def orders_by_status(status: str) -> str:
    """List orders with a given status, most recent first.

    Args:
        status: One of pending, shipped, delivered or cancelled
    """
    allowed = {"pending", "shipped", "delivered", "cancelled"}
    if status.lower() not in allowed:
        return f"Unknown status '{status}'. Use one of: {', '.join(sorted(allowed))}."

    rows = query(
        "SELECT order_id, customer, amount_inr, placed_on FROM orders "
        "WHERE status = ? ORDER BY placed_on DESC",
        (status.lower(),),
    )
    if not rows:
        return f"No orders with status '{status}'."

    lines = [
        f"{r['order_id']}  {r['customer']:<18} INR {r['amount_inr']:>8,}  {r['placed_on']}"
        for r in rows
    ]
    return f"{len(rows)} order(s) with status '{status}':\n" + "\n".join(lines)


@mcp.tool()
async def revenue_by_city(min_amount: int = 0) -> str:
    """Total delivered revenue grouped by city.

    Args:
        min_amount: Only include cities whose total is at least this amount in INR
    """
    rows = query(
        "SELECT city, SUM(amount_inr) AS total, COUNT(*) AS orders FROM orders "
        "WHERE status = 'delivered' GROUP BY city HAVING total >= ? ORDER BY total DESC",
        (min_amount,),
    )
    if not rows:
        return "No delivered orders matched that threshold."

    lines = [f"{r['city']:<12} INR {r['total']:>9,}  ({r['orders']} orders)" for r in rows]
    return "Delivered revenue by city:\n" + "\n".join(lines)

Then a resource, so the model can read the schema rather than guess at column names, and finally the entry point:

@mcp.resource("schema://orders")
def orders_schema() -> str:
    """The orders table schema, so the model knows what it can ask for."""
    rows = query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'orders'")
    return rows[0]["sql"] if rows else "orders table not found"


if __name__ == "__main__":
    mcp.run(transport="stdio")

4. Connect it to Claude Desktop

Open your config file, creating it if it does not exist:

# macOS
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
# Linux
code ~/.config/Claude/claude_desktop_config.json
# Windows (PowerShell)
code $env:AppData\Claude\claude_desktop_config.json

Add the server under mcpServers, using an absolute path:

{
  "mcpServers": {
    "orders": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/orders",
        "run",
        "orders.py"
      ]
    }
  }
}

If the server does not appear after restarting Claude Desktop, the usual cause is uv not being on the path that the desktop app sees. Replace "command": "uv" with the absolute path from which uv.

Restart, and you can ask things like "what is the status of ORD-1004" or "which city has the most delivered revenue", and the model will call your tools to answer.

What separates a demo server from a deployable one

Writing tools that work takes an afternoon. Writing tools you would let an AI assistant point at a customer's production database is a different exercise, and it is the part that actually earns the job title. Four things in the code above are doing that work.

The connection is read-only, enforced by the database

Opening with file:{path}?mode=ro means a write cannot happen even if a tool is wrong, or someone later adds a tool that tries one. Tested against the example database, a delete is rejected at the engine:

>>> query("DELETE FROM orders")
OperationalError: attempt to write a readonly database

Enforce safety at the lowest layer you can reach. A read-only database user is better still, because it survives your code being replaced.

Every query is parameterised

The model chooses the arguments to your tools, and those arguments come from text it was given, which may include text a user or document put there deliberately. Treat tool arguments as untrusted input, exactly as you would a web form. With ? placeholders, an injection attempt is just a string that matches nothing:

find_order("ORD-1001' OR '1'='1")
-> No order found with ID ORD-1001' OR '1'='1.

Never build SQL by interpolating a tool argument. This is the single most common serious bug in MCP servers written quickly.

Inputs are validated against an allowlist

orders_by_status checks the status against a known set and returns a helpful message rather than querying with whatever arrived. That gives the model a correction it can act on:

orders_by_status("banana")
-> Unknown status 'banana'. Use one of: cancelled, delivered, pending, shipped.

Output is bounded

fetchmany(MAX_ROWS) caps every result. An unbounded query against a real customer table will return far more than a context window can hold, and you will pay for every token of it. Cap rows, select only the columns you need, and summarise server-side rather than shipping raw tables to the model.

One risk you should not discover in production

An MCP server is a set of capabilities you are handing to a model that reads untrusted text. If a document, email or web page the model processes contains instructions, those instructions can influence which tools it calls and with what arguments. This is prompt injection, and giving a model real tools raises the stakes of it considerably.

The practical defences are unglamorous and they are the same ones the security literature keeps arriving at: keep each tool narrow rather than exposing one general run_sql, enforce permissions at the data layer rather than in the prompt, require human confirmation for consequential actions (elicitation/create exists precisely for this), log the tool calls and arguments so an incident is reconstructable, and prefer read-only access until a write genuinely cannot be avoided. The MCP documentation has a security best practices page worth reading before any customer deployment.

We covered a concrete, documented example of injected text moving an AI system's decision in our write-up of Jev and structured AI decisions.

When not to build an MCP server

A reasonable list, because the current enthusiasm suggests building one for everything:

  • Only your own application needs the data. If no external AI client will use it, a normal internal function is simpler and faster.
  • The task is one deterministic step. If the answer is always the same query with the same arguments, write a script and schedule it. A model in the loop adds cost and variance.
  • The data cannot leave its environment. Work out where the model runs and what the compliance position is before building, not after. This question kills more enterprise AI pilots than any technical problem.
  • The underlying API is already the hard part. MCP standardises access. It does not fix a slow, undocumented or unreliable upstream system, and wrapping one in a protocol will not make it dependable.

Where to go next

Three things worth building after this one, in order of how much they teach: a server over an HTTP API rather than a database, which forces you to handle timeouts, retries and rate limits; the same server on the Streamable HTTP transport with authentication, which is what a remote deployment actually requires; and a server with a write tool gated behind elicitation/create, which is where you learn how much confirmation design matters.

If you want this as part of a structured route into forward deployment work, our Applied AI Forward Deployment Engineering Program covers integration, RAG and agents, evaluation and deployment for working IT professionals with one to eight years of experience. If you are earlier on and want the AI engineering foundation first, start with the Artificial Intelligence course, or read how the role compares with others in FDE vs Solutions Engineer vs AI Engineer.

Sources and verification

  • Protocol definition, architecture, participants, primitives, transports, the 2026-07-28 protocol version and the sampling and logging deprecations: MCP architecture overview and introduction.
  • Server-building pattern, the MCPServer import, the @mcp.tool() decorator, mcp.run(transport="stdio") and the Claude Desktop configuration: Build an MCP server.
  • SDK version 2.2.0 and the Python 3.10 minimum: the mcp package metadata on PyPI, checked at the time of writing.
  • MCP named as an FDE deliverable: Anthropic Forward Deployed Engineer posting.
  • The database, tool logic, read-only enforcement, injection handling and allowlist behaviour in this article were executed before publishing, and the outputs shown are real. The MCP decorators themselves were checked against the official SDK documentation rather than run, because the machine used was on Python 3.9.
  • MCP is moving quickly. This article was written on 1 October 2026 against protocol version 2026-07-28; check the specification before relying on any detail here.

FAQ

Frequently Asked Questions

What is MCP (Model Context Protocol)?

MCP is an open standard for connecting AI applications to external systems such as databases, APIs and files. The official documentation compares it to a USB-C port for AI: you build one server, and any MCP-compatible client can use it instead of needing a custom integration per assistant.

What problem does MCP solve?

Before MCP, connecting an AI assistant to a system meant writing a bespoke integration for that specific assistant. MCP standardises the interface, so one server works with any compatible client. That significantly reduces integration work when several AI applications need the same systems.

What can an MCP server expose?

Three primitives. Tools are executable functions the model can call, such as a database query. Resources are data the model can read, such as a schema or file contents. Prompts are reusable templates that structure an interaction. Tools are verbs, resources are nouns, prompts are recipes.

What is the difference between an MCP host, client and server?

The host is the AI application, such as Claude Desktop or VS Code. The client is a connector inside the host that maintains one dedicated connection. The server is the program you write that provides context and actions. A host creates one client per server it connects to.

Which Python version and SDK do I need?

The mcp Python SDK is at version 2.2.0 and requires Python 3.10 or newer. Install it with uv add "mcp[cli]" or pip install "mcp[cli]". The server class is imported as: from mcp.server import MCPServer.

What is the current MCP protocol version?

2026-07-28 at the time of writing. That version made MCP a stateless protocol, introduced the mandatory server/discover request, and deprecated both sampling and logging. Tutorials describing server-side sampling are written against an older protocol version.

What transports does MCP support?

Two. The stdio transport runs the server as a local process communicating over standard input and output, which is what Claude Desktop uses for local servers. The Streamable HTTP transport uses HTTP POST with optional Server-Sent Events and is used for remote servers that serve many clients.

Is MCP only for Claude?

No. MCP is an open protocol with broad support. Claude, ChatGPT, VS Code, Cursor and other tools act as MCP hosts. The point of the standard is that you build a server once and it works across compatible clients.

How do I connect my MCP server to Claude Desktop?

Add it under the mcpServers key in claude_desktop_config.json, giving the command and an absolute path to your project directory, then restart the app. If the server does not appear, the usual cause is that uv is not on the path the desktop app sees, so use the absolute path from which uv.

Is it safe to connect an AI assistant to a production database?

Only with deliberate controls. Use a read-only connection or database user, parameterise every query, validate inputs against an allowlist, cap the number of rows returned, keep each tool narrow rather than exposing general SQL execution, log every tool call, and require human confirmation for consequential actions.

Can MCP tool arguments be used for SQL injection?

Yes, if you build SQL by string interpolation. The model chooses tool arguments from text it was given, which can include text placed there deliberately, so treat every argument as untrusted input. Parameterised queries with placeholders make an injection attempt just a string that matches nothing.

What is prompt injection in the context of MCP?

When a model processes untrusted text that contains instructions, those instructions can influence which tools it calls and with what arguments. Giving a model real tools raises the stakes. Defences include narrow tools, permissions enforced at the data layer rather than in the prompt, logging, and human confirmation for consequential actions.

What is elicitation in MCP?

Elicitation is the client primitive that lets a server ask the user for more information or confirm an action, using the elicitation/create method. It is the right mechanism for gating a write or any other consequential operation behind explicit human approval.

Why were sampling and logging deprecated?

Both were deprecated in protocol version 2026-07-28. Servers that previously requested model completions through sampling are now directed to integrate with LLM provider APIs directly, and servers that used protocol logging should log to stderr on stdio or use OpenTelemetry instead.

When should I not build an MCP server?

When only your own application needs the data and no external AI client will use it, when the task is a single deterministic step better served by a script, when compliance prevents the data leaving its environment, or when the underlying API is itself unreliable. MCP standardises access; it does not fix a bad upstream system.

Why do Forward Deployed Engineers need to know MCP?

Because it is now named directly on the job. Anthropic's Forward Deployed Engineer posting lists delivering technical artifacts such as MCP servers and agents for production workflows inside customer systems. MCP is becoming the standard way to connect a model to a customer's existing systems.

Not Sure Which Skill Gap Is Yours?

A free counselling session will map your current background against the programming, data, AI and deployment skills these roles need, and tell you honestly where to start.

Book Free Career Counselling

Keep Reading

Related Articles