What you need to know before you start

  • MCP is the integration standard now. As of mid-2026 every major AI coding tool speaks it, and more than 5,000 community servers exist. Build once, plug into Claude Desktop, Claude Code, Cursor and the rest.
  • There are only three primitives to learn — tools, resources and prompts. If you understand those, you understand MCP.
  • Two transports matter: STDIO for local servers and Streamable HTTP for remote ones. Start local; it is far simpler.
  • The one pitfall that breaks every beginner is writing to stdout in a STDIO server. We will call it out loudly below.
  • A shipped MCP server is excellent proof-of-work. It is small, self-contained and demonstrably useful — exactly the kind of artefact that makes a Verified Builder profile credible.

If you have ever wired a model into a custom data source, you have probably written the same glue twice: once for one assistant, again for the next. The Model Context Protocol exists to end that. Think of it as a web API designed specifically for LLM interactions, with a published contract that every client already knows how to read. Anthropic introduced it, the specification is open at modelcontextprotocol.io, and the ecosystem has done the rest.

The mental model: tools, resources, prompts

Almost everything you build with MCP is one of three things. The clearest way to hold them in your head is to borrow the HTTP analogy the official Python SDK itself uses.

Primitive What it is HTTP analogy Who controls it
Tool A model-callable function that does something — a calculation, a lookup, a write. Like a POST — it executes code and may cause a side effect. The model decides when to call it.
Resource Readable context the client can load into the model — a file, a record, a config. Like a GET — it returns data, no side effects. The client or app chooses what to load.
Prompt A reusable, parameterised template the user can invoke. Like a saved snippet or slash command. The user picks it deliberately.

This separation matters more than it first appears. Resources are passive — they never surprise you. Tools are active — they can spend money, send an email or delete a row, so they carry all the security weight. Prompts are the friendliest surface for non-technical users. Most first servers expose two or three tools and a resource or two; you do not need all three primitives to be useful.

The two transports — and which to pick

An MCP server has to deliver its JSON-RPC messages somehow. The protocol defines two transports you will actually use. STDIO runs the server as a child process of the client and talks over standard input and output — perfect for a local tool. Streamable HTTP (which supersedes the older SSE transport for production) runs the server as a network service that multiple clients can reach.

  STDIO (local) Streamable HTTP (remote)
Where it runs As a child process on the same machine As a hosted network service
Setup cost Almost none — no ports, no auth Needs hosting, TLS and authentication
Auth Inherits the local user's trust OAuth 2.1 with PKCE, per the spec
Best for Your first server, personal tooling, IDE wiring Shared internal services, multi-user, always-on
The big gotcha Never write to stdout — it corrupts the stream An unauthenticated endpoint is an open door

Start with STDIO. You can build, run and wire a STDIO server into Claude Code or Claude Desktop in an afternoon, with nothing to deploy. Graduate to HTTP only when you genuinely need a server that lives somewhere other than the developer's laptop.

Build a minimal server in Python

We will build a small internal-docs server with two tools — one to search a document store, one to fetch a single document — plus a resource that exposes a service catalogue. Picture this running for an Indian fintech where engineers want their AI assistant to answer questions from internal runbooks without those runbooks ever leaving the company network.

Install the official SDK. The Python SDK ships the high-level FastMCP server interface; there is an equally official TypeScript SDK if your stack leans that way.

pip install "mcp[cli]"
# or, with uv:  uv add "mcp[cli]"

Now the server itself. The FastMCP decorators turn an ordinary typed Python function into a registered tool, and your type hints become the tool's input schema automatically.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("internal-docs")

# A small, in-memory stand-in for a real document store.
DOCS = {
    "kyc-runbook": "How to handle a failed KYC check: ...",
    "refund-policy": "Refunds are processed within 5 working days: ...",
    "oncall-escalation": "Sev-1 escalation path and contacts: ...",
}

@mcp.tool()
def search_docs(query: str, limit: int = 5) -> list[dict]:
    """Search internal runbooks by keyword. Returns matching doc ids and snippets."""
    query = query.strip().lower()
    if not query:
        raise ValueError("query must not be empty")
    limit = max(1, min(limit, 20))          # clamp — never trust the caller
    hits = [
        {"id": doc_id, "snippet": text[:160]}
        for doc_id, text in DOCS.items()
        if query in doc_id or query in text.lower()
    ]
    return hits[:limit]

@mcp.tool()
def get_doc(doc_id: str) -> str:
    """Fetch the full text of a single runbook by its id."""
    if doc_id not in DOCS:
        raise ValueError(f"unknown doc_id: {doc_id!r}")
    return DOCS[doc_id]

@mcp.resource("catalogue://docs")
def catalogue() -> str:
    """A readable list of every available runbook id."""
    return "\n".join(sorted(DOCS))

if __name__ == "__main__":
    mcp.run()       # defaults to the STDIO transport

That is a complete, working MCP server. The two @mcp.tool() functions become model-callable tools; the docstrings become the descriptions the model reads to decide when to call them, so write them as carefully as you would write an API doc. The @mcp.resource() function exposes the catalogue as readable context under a URI the client can load on demand. Notice we are already doing input validation — clamping limit, rejecting empty queries, refusing unknown ids — because a tool argument is untrusted input.

Watch out

In a STDIO server, never write to stdout. The protocol sends its JSON-RPC messages over standard output, so a stray print(), a noisy library, or a debug statement will interleave with the protocol stream, corrupt the JSON-RPC frames and silently break your server — usually with a baffling "failed to parse" error in the client. Send every log line to stderr instead (Python's logging defaults there; explicitly use file=sys.stderr if you reach for print). This is the single most common reason a first MCP server "doesn't work".

Wire it into a client

A server is only useful once a client launches it. For Claude Desktop or Claude Code, register the server in the client's MCP configuration. The client owns the process lifecycle — it starts your server as a child process and speaks STDIO to it.

{
  "mcpServers": {
    "internal-docs": {
      "command": "uv",
      "args": ["run", "python", "/srv/mcp/internal_docs.py"],
      "env": {
        "DOCS_DB_URL": "postgres://localhost/docs"
      }
    }
  }
}

Restart the client, and the two tools and the catalogue resource appear. From there the model can call search_docs, read what comes back, and call get_doc for the full text — all without your runbooks ever touching a third party. The same server binary works in any MCP-compatible IDE; that portability is the entire point of the standard.

Pro tip

Return structured results, not prose. A tool that returns a typed list of {"id", "snippet"} objects gives the model clean, machine-readable data to reason over and chain into the next call. A tool that returns a hand-formatted paragraph forces the model to re-parse your text and is far more error-prone. Let your return-type annotations do the work.

Every article here is written by a Verified Builder. Want your name on the next one?

AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.

Become a Verified Builder →

Security: the part that decides whether you ship

A demo server runs on your laptop and trusts everyone. A production server runs somewhere real and must trust no one by default. The gap between the two is almost entirely about how you treat your tools, because tools are the primitive that can act on the world. The 2026 reality is sobering: the first malicious MCP package appeared in late 2025, multiple CVEs have followed, and security reviews have found a worrying share of public servers vulnerable to command injection. Treat these practices as non-negotiable.

Least-privilege tool scopes

Give each tool the narrowest capability that does its job. A docs server should read documents — it has no business holding a database write connection "just in case". If a tool needs the filesystem, scope it to one directory. If it needs an API, give it a token with a single permission. The smaller the blast radius of a compromised tool, the smaller the incident.

Validate every tool argument

Tool arguments come from a model, which can be steered by an attacker through prompt injection. Treat them exactly as you would treat a web request body: validate types, clamp ranges, reject anything unexpected, and never interpolate an argument straight into a shell command or SQL string. Our search_docs clamps limit and rejects empty queries for precisely this reason. Parameterise database queries; never build them with string formatting.

Gate destructive actions behind a human

Anything irreversible — sending money, deleting records, emailing a customer, changing production config — should require explicit human approval before it executes, not after. Consider a UK retailer whose internal MCP server exposes a process_refund tool. Under the Consumer Rights Act a wrongly issued refund is a real liability, so the tool should return a confirmation request and only act once a human approves it. Make the dangerous path opt-in and visible.

Recommended

Split your tools into a read tier and a write tier, and mark the write tier explicitly so the client can require confirmation. A read-only docs server can be wired up freely; a server that can move money or mutate records should never auto-execute. This single distinction prevents most "the agent did something it shouldn't have" incidents.

Authenticate remote servers

The moment your server leaves the local machine, an unauthenticated endpoint is an open door to every data source it touches. The specification requires remote servers to implement OAuth 2.1 with PKCE; do not ship an HTTP transport without it. Put the server behind TLS, isolate it from other servers with its own credentials and process, and prevent cross-server data sharing so one breach cannot cascade.

Never leak secrets

Keep credentials out of tool descriptions, out of resource contents and out of return values — anything you return becomes model context and may be logged or surfaced. Read secrets from the environment (as the client config injects DOCS_DB_URL above), never hard-code them, and scrub error messages so a stack trace does not echo a connection string back to the model.

Taking it to production

The guidance the MCP maintainers keep repeating is the same advice that holds for any service: keep designs simple, observable and secure. A few concrete habits make the difference once a server is doing real work.

  • Log to stderr, structured. You already cannot use stdout on STDIO; lean into it and emit structured logs to stderr so you can trace which tool was called with what arguments and what it returned.
  • Version your server. Tool signatures are a contract. When you change a tool's arguments or behaviour, treat it like an API change — version it and communicate it, because clients have cached expectations.
  • Keep tools small and composable. Two narrow tools the model can chain beat one sprawling tool with a dozen modes. Narrow tools are easier to validate, easier to scope and easier to reason about.
  • Test the unhappy paths. Unknown ids, oversized inputs, malformed arguments — your tools should fail loudly and safely, never silently do the wrong thing.

None of this is exotic. It is the same discipline you would bring to a public HTTP API, applied to a new surface. That is the quiet strength of MCP: it lets you reuse everything you already know about building safe services, and points it at the models instead.

The one habit worth singling out is observability, because it is the thing first-time MCP authors skip and then regret. A tool call is a black box to the person debugging it unless you make it legible: log the tool name, a redacted view of the arguments, the outcome and the wall-clock duration on every invocation, and attach a correlation id so a single user request can be traced across several tool calls. When a model starts behaving oddly, that trace is usually the fastest way to see whether the fault is in your tool, in the arguments the model chose, or in the way you described the tool in the first place. A minimal structured log line over STDIO looks like this:

import sys, json, time

def log_call(tool, args, result, started):
    sys.stderr.write(json.dumps({
        "tool": tool,
        "args": {k: "***" if k in {"token", "api_key"} else v for k, v in args.items()},
        "ok": result is not None,
        "ms": round((time.monotonic() - started) * 1000),
    }) + "\n")
    sys.stderr.flush()

Notice it writes to sys.stderr, never sys.stdout — the same rule from earlier, now load-bearing in production. Pair these logs with a health check on remote servers and a simple alert when a tool's error rate climbs, and you have the observability floor a reviewer expects before they trust your server with anything that touches real data. For an Indian fintech exposing a KYC-lookup tool or a UK retailer wiring a refund tool to a payments backend, that audit trail is not optional polish — it is the evidence a compliance team will ask to see.

Why this is worth putting on your profile

A finished MCP server is an unusually good portfolio piece. It is small enough to read in one sitting, concrete enough to demo in thirty seconds, and current enough that shipping one in 2026 signals you are working at the frontier rather than reading about it. Whether it is an internal-docs server for an Indian fintech or a compliance-gated refund tool for a UK retailer, it is real proof-of-work — the kind of artefact that turns a CV line into something a hirer can actually inspect.

If you want to go deeper from here, three companion guides pick up where this one stops: agent design patterns for 2026 for the architectures your tools will plug into, structured-output prompting patterns for getting reliable typed results out of models, and Claude Code subagent and multi-agent orchestration for wiring several MCP servers into one workflow. When you have shipped your first server, add it to a proof-of-work portfolio and let it do the talking.

Primary references: the protocol specification and SDK docs at modelcontextprotocol.io, the official Python SDK, and Anthropic's MCP announcement at anthropic.com.