Model Context Protocol (MCP) explained for developers

Model Context Protocol (MCP) explained: how servers expose tools, resources and prompts to AI agents, how stdio and HTTP transports work, and what to secure.

By Hamza Ghouri

Every AI agent needs to reach real systems: your database, your ticket tracker, your docs, your internal APIs. Without a standard, each agent gets its own custom integration for each system, and you rewrite the same glue over and over. The Model Context Protocol (MCP) is an open standard that fixes this. You expose a system once as an MCP server, and any MCP-compatible client can discover and use it.

This post explains MCP the way I'd explain it to a backend developer: what the pieces are, what goes over the wire, and where the security risks sit.

Hosts, clients and servers

MCP has three roles.

  • Host: the application the user works in. An editor with a coding agent, a desktop chat app, or your own agent service.
  • Client: a connection the host opens to one server. A host with three servers configured runs three clients.
  • Server: a program that exposes capabilities from some system. A Postgres server, a GitHub server, a server for your company's internal API.

Messages between client and server are JSON-RPC 2.0. When a connection opens, the two sides run an initialize handshake to agree on a protocol version and declare what each supports. After that, the client can list and call whatever the server offers.

If you've written a tool-calling agent by hand, as in building a tool-using AI agent in TypeScript, MCP will feel familiar. It moves the tool registry out of your agent and behind a protocol, so tools can be shared and discovered at runtime.

The three things a server exposes

Tools

Tools are actions the model can choose to call: search_tickets, run_query, create_branch. Each has a name, a description and an inputSchema in JSON Schema. The client fetches them with tools/list and invokes one with tools/call.

// Client -> server, after the model decides to use a tool
const request = {
  jsonrpc: "2.0",
  id: 7,
  method: "tools/call",
  params: {
    name: "search_tickets",
    arguments: { query: "login timeout", status: "open" },
  },
};

// Server -> client
const response = {
  jsonrpc: "2.0",
  id: 7,
  result: {
    content: [{ type: "text", text: "3 open tickets match: #812 Login times out on slow networks ..." }],
    isError: false,
  },
};

A failed tool call comes back as a normal result with isError: true, so the model can read what went wrong and adjust.

Resources

Resources are data the host application can read and put into context: a file, a database schema, a config document. Each has a URI and is read with resources/read. The application decides when to include them. A good rule of thumb: tools are for doing, resources are for reading.

Prompts

Prompts are reusable templates the server offers to the user, like "review this pull request" or "summarise this incident". They show up in the host as commands the user can pick. They're the least used of the three, but handy for packaging a team's standard workflows.

Transports: stdio and HTTP

MCP defines how messages travel, and two transports cover most cases.

stdio is for local servers. The host launches the server as a child process and they exchange messages over stdin and stdout. There's no network port and no auth handshake; the server runs with the user's local permissions. This is how most developer tools ship MCP servers.

HTTP is for remote servers. The server runs as a web service, clients connect over HTTP, and the server can stream responses back. This is the one to use when a server is shared by a team, needs central credentials, or sits next to a production system. It also means the usual web concerns apply: authentication, TLS, rate limits and logging.

My default: stdio for personal tooling on a laptop, HTTP for anything shared or anything that touches production data.

When an MCP server is worth writing

If one agent in one codebase needs a tool, a plain function in your agent loop is simpler. MCP pays off when:

  • Several agents or tools need the same integration. Write it once, use it from an editor agent, a chat app and a CI job.
  • The integration belongs to a different team. The team that owns the billing API can own the billing MCP server and its permissions.
  • You want to serve shared context. Team conventions are a good example. I wrote about making engineering conventions machine-readable for AI agents, and an MCP server is a natural way to serve them to every engineer's agent.

Keep servers small and focused. A server with five well-described tools gets used correctly far more often than one with fifty.

Security: treat every server as code you're running

MCP makes it easy to give a model access to things, which is exactly why it needs care.

  • Servers act with your credentials. A server connected to your database or email can do whatever those credentials allow. Scope tokens to the minimum, and prefer read-only access until you need writes.
  • Tool output is untrusted input. A ticket, a web page or a document can contain text written to steer the model. That's prompt injection, and I'd read up on securing LLM features against prompt injection before wiring a model to anything with write access.
  • Tool descriptions are input too. The model reads them. A third-party server with a malicious description can influence how the model uses your other tools.
  • Local servers run code on your machine. Installing a stdio server means running someone else's program. Install from sources you trust, pin versions, and read what it does.
  • Approve the dangerous calls. Hosts can ask the user before running a tool. Keep that on for anything that writes, sends or deletes.

Where I'd start

Wrap one internal system you already query by hand, expose two or three read-only tools with sharp descriptions, and run it over stdio from your editor. Once it's useful and you trust it, move it to HTTP with proper auth so the rest of the team can connect.

Want this kind of work on your product?

Get in touch