Blog

Give your model tools: your first Model Context Protocol (MCP) server

Agents and tools get the attention; the Model Context Protocol is the plumbing underneath. You’ll build the smallest possible MCP server (one tool, one file), connect a client, and watch a model call your code. By the end the abstraction is no longer mysterious, and neither are its sharp edges.

1. The problem before MCP: the M×N mess

Before there was a standard, every application that wanted to give a model tools wired them up its own way. Your IDE talked to your database with one bespoke integration; your chat app talked to the same database with a different one; a third app reinvented both. With M applications and N tools, you were on the hook for something close to M×N custom connectors, none of them reusable. Every new tool meant re-integrating it everywhere, and every new app meant re-integrating everything. It was integration glue all the way down, and none of it composed. Worse, the glue was brittle: an API change on either side broke a hand-written connector that usually only one engineer fully understood.

2. What MCP is: a USB-C port for AI

MCP (Model Context Protocol) is an open standard introduced by Anthropic in November 2024 for connecting AI applications to tools, data, and context. Through 2025 it saw broad, cross-vendor adoption and is now widely described as a de-facto standard for AI tool use (Thoughtworks traces that arc). The framing that stuck: MCP is a “USB-C port for AI.” Standardize the connector once, and any compliant tool plugs into any compliant application. Write your tool as an MCP server a single time, and every MCP-aware host (an IDE, a desktop assistant, an agent framework) can use it without a custom adapter. The M×N mess collapses to M+N.

3. The anatomy: hosts, clients, servers

Three roles, and it’s worth getting them straight before you write code:

  • Host: the application the user actually interacts with (an AI-enabled IDE, a desktop assistant). It runs the model and decides when to reach for a tool.
  • Client: the connector living inside the host that speaks MCP. One client maintains one connection to one server.
  • Server: what you build. It advertises capabilities and executes them when asked.

A server can expose three kinds of capability, and the distinction matters because it changes who is in control:

  • Tools are model-callable functions that do something and may have side effects: send_email, run_query, create_ticket. The model decides when to invoke them.
  • Resources are read-only data the host loads into context: a file’s contents, a database row, a query result. The application, not the model, decides what to surface.
  • Prompts are reusable, parameterized message templates a user invokes deliberately, like a slash-command that pre-fills a well-tested instruction.

Today we’ll build a tool, because tools are where the power — and the risk — live.

4. Build it: a one-tool server

Anthropic ships official SDKs (Python and TypeScript among them). With the Python SDK, a complete, working MCP server is this short:

# pip install "mcp[cli]==1.28.1"    # pin the version; the SDK moves quickly, and old pins carry advisories
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("demo")

@mcp.tool()
def add(a: float, b: float) -> float:
    """Add two numbers and return the sum."""
    return a + b

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

That’s the whole server. The @mcp.tool() decorator does the load-bearing work: it introspects the function’s type hints and docstring and turns them into the schema the model reads to decide when and how to call add. This is why the docstring isn’t a nicety: it’s the tool’s user manual, written for the model. A vague docstring is a tool the model calls at the wrong moment.

5. Connect a client and watch the call

The SDK’s CLI (installed by the [cli] extra) includes a development inspector. Point it at your file:

mcp dev server.py

This launches the MCP Inspector, a client that connects to your server, lists the tools it found, and lets you invoke add by hand, the same handshake a real host performs. You’ll see your tool’s schema appear, call it with arguments, and get the result back through the protocol. In a real session the loop is identical, just automated: the host lists your server’s tools, hands their schemas to the model, and when the model emits a call, the client routes it to your server, runs add, and feeds the result back into the conversation. Nothing about your server changes between the inspector and production. That is the entire payoff of a standard. To wire the server into an actual assistant instead, mcp install server.py registers it with a compatible host. Either way, the moment the tool shows up in a client and returns a value, MCP stops being an acronym and becomes a thing you built.

Two things mcp dev does not tell you, though. It needs Node.js and npm on your PATH, because the inspector is a JavaScript app; without npx the command stops at “npx not found. Please ensure Node.js and npm are properly installed and added to your system PATH.” And it shells out to npx @modelcontextprotocol/inspector with no version constraint, so every run fetches and executes whatever npm resolves that day. If that bothers you, and on a machine holding credentials it should, drive the inspector yourself at a pinned version: npx @modelcontextprotocol/inspector@1.0.0 python server.py, which wants Node 22.7.5 or newer.

6. Security

This is the part the demos skip, and it’s the part that matters most. A tool is executable capability. When the model decides to call your function, your code runs, so every tool is an attack surface, and a careless one is a liability.

  • Validate every input. The arguments arrive from a model that can be wrong, or steered into being wrong. Treat them like untrusted user input, because that’s exactly what they are.
  • Least privilege. Scope each server to the narrowest capability that does the job. A tool that needs to read one directory should not be able to write your whole filesystem, and nothing should hand an agent a general shell.
  • Never expose secrets. Assume anything a resource can read or a tool can return may end up in the model’s context, and from there, out. Keep credentials, keys, and private data out of tool inputs and outputs.
  • Prompt injection is real. A document a resource loads, or a web page a tool fetches, can carry instructions that hijack the model into misusing your other tools. Treat all returned content as untrusted data, not as commands to follow.
  • Keep a human in the loop for anything destructive or irreversible. Automate the reversible; confirm the rest.
  • Log every invocation. You cannot secure what you cannot see, so record which tool ran, with which arguments, and what it returned. When something goes wrong, that trail turns a mystery into a five-minute fix.

None of this is optional hardening for “later.” It’s the difference between a tool and a foothold.

From here, try exposing something useful: for example, turning the retrieval function from the RAG-from-scratch tutorial into a search_docs tool, so any MCP host can ground its answers in your documents. And when those tools start touching real systems (production databases, internal APIs, customer data), the security section above stops being a checklist and becomes an architecture problem, which is exactly the kind of review CloudSignal does before an agent ships.

Sources / further reading

Written by Ashwin Rajendraprasad for CloudSignal AI. The code above is free to reuse.