Index Ai Academy
Phase 9 · Model Context Protocol · Production MCP

Testing, designing and shipping servers

Conformance checks, tool design for models, SDKs and deployment.

About 40 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

A server that works with one host on your laptop isn't done. It has to work with every host, keep working as the protocol evolves, and actually help models get tasks done. That last part is a design problem as much as a coding one. This lesson covers how professionals test servers, the tool design habits that separate useful servers from frustrating ones, and how servers get packaged, found and run.

What you'll be able to do

  • Test a server at three levels: handlers, full JSON-RPC exchanges, and conformance probes
  • Design tools for models: task-shaped, well described, concise and recoverable
  • Package and distribute a server, and know the official SDKs and the MCP Registry
  • Deploy a stateless remote server that scales, and observe it in production

What it covers

  1. Three levels of testing
  2. Designing tools for models
  3. Two tool designs, compared
  4. Packaging and finding servers
  5. Two probes against the practice server
  6. Renames break things

Three levels of testing

A car maker tests each part, then the assembled engine, then runs a standard road test that every car must pass. Servers get the same three levels:

  1. Handler tests. Each tool is a function, so test it like one: inputs in, result out, with fakes for the APIs it calls. Fast, and most of your tests.
  2. Protocol tests. Feed whole JSON-RPC messages to the server through an in-memory transport and check the replies: ids echoed, errors coded correctly, notifications unanswered. This catches the mistakes that only appear when a real client connects.
  3. Conformance probes. A fixed battery of checks any MCP server should pass, regardless of its tools: does server/discover work, does an unknown method give -32601, does a missing _meta give -32602? Run them in CI, so a refactor can't silently break compatibility.

For poking at a server by hand, the MCP Inspector is a developer tool that connects to a server and lets you browse and call its tools, resources and prompts in a browser:

npx @modelcontextprotocol/inspector uv run shop_server.py

Finally, take it for a test drive: a real host, with a real model, on real tasks. That's where you learn whether the tools are well designed.

Quick check
What does this print?
def handle(msg):
    if msg.get("method") == "ping":
        return {"id": msg["id"], "result": {}}
    return {"id": msg["id"], "error": {"code": -32601}}
print(handle({"id": 9, "method": "nope"})["error"]["code"])
  1. 9
  2. nope
  3. None
  4. -32601
Show the answer

-32601

The method isn't "ping", so the reply carries the error, and ["error"]["code"] reads -32601. A protocol test feeds in a whole message and checks the reply's error code.

Try it now

Each level catches mistakes the one before it can't see.

Quick check
Which level of testing catches a server that wrongly replies to notifications?
  1. Protocol tests: whole JSON-RPC messages through an in-memory transport
  2. Handler tests of each tool function
  3. A spell-check of the tool descriptions
  4. Load tests
Show the answer

Protocol tests: whole JSON-RPC messages through an in-memory transport

Handler tests call tool functions directly, so they never see notifications. Protocol tests check the message handling itself.

Designing tools for models

A model only knows your tools through their names, descriptions and schemas, and it pays in tokens for every result. Ask a waiter what's good tonight and you want a recommendation, not the supplier list. The habits that work:

  • Shape tools around tasks, not endpoints. Don't mirror an API's 40 endpoints as 40 tools. A model asked "how's the spring campaign doing?" does better with one campaign_report(name) tool than with a chain of list_campaigns, get_campaign and get_metrics calls.
  • Name and describe for the model. Say what it does, when to use it, and what comes back. Describe each parameter's format with an example: "ISO date, e.g. 2026-10-30".
  • Constrain inputs. Use enums, minimums and required fields, so wrong calls fail fast with a clear message instead of doing something odd.
  • Return what's useful, concisely. Long raw JSON wastes context and buries the answer. Return the fields that matter, paginate long lists, and consider a detail parameter ("summary" or "full"). Include ids the model will need for follow-up calls.
  • Make errors teach. "No campaign named 'sprng'. Did you mean 'Spring sale'?" gets fixed on the next call. Use isError results for these, not protocol errors.
  • Keep the list small and stable. Fewer, distinct tools are easier for a model to choose between. A fixed order helps caching.
  • Test with a model. Write a few realistic tasks, run them, and read the transcripts. The model's confusion shows you which description to fix. That's an eval, as in Phase 1.

In plain words: design tools for what people ask, and read how the model uses them.

Worked example

Two tool designs, compared

Tools that mirror your API make the model do the joining. Count what it takes to answer "where's Ann's order?" with each design.

CUSTOMERS = {"ann@shop.example": "c1"}
ORDERS = {"c1": ["o9"]}
STATUS = {"o9": "shipped"}

calls = []
def find_customer(email):
    calls.append("find_customer"); return CUSTOMERS[email]
def list_orders(customer):
    calls.append("list_orders"); return ORDERS[customer]
def get_order(order):
    calls.append("get_order"); return STATUS[order]

status = get_order(list_orders(find_customer("ann@shop.example"))[0])
print("API-shaped:", status, "after", len(calls), "tool calls")

calls.clear()
def latest_order_status(email):
    calls.append("latest_order_status")
    return STATUS[ORDERS[CUSTOMERS[email]][0]]

print("Task-shaped:", latest_order_status("ann@shop.example"), "after", len(calls), "tool call")
It runs right here in your browser, with nothing to install.

Each extra call is another trip to the model, with more waiting, more tokens and another chance to go wrong. Design tools around what people ask for, not around your database tables.

Quick check
Your server wraps a CRM API with 35 endpoints. What's usually the better design for an agent?
  1. A handful of task-shaped tools (find_contact, log_call, deal_summary) with clear descriptions
  2. No tools; only resources
  3. One tool that takes a raw HTTP request
  4. 35 tools, one per endpoint, so nothing is missing
Show the answer

A handful of task-shaped tools (find_contact, log_call, deal_summary) with clear descriptions

Fewer, higher-level tools mean fewer choices, fewer calls per task, and less context spent on definitions.

Packaging and finding servers

  • Python servers are usually published to PyPI and run with uvx shop-mcp, which installs and runs them in an isolated environment. Node servers are published to npm and run with npx. Many are also published as container images.
  • The MCP Registry is the community's central directory of public servers, a phone book that hosts and people can search. Publishing there gives yours a stable name and metadata.
  • Version your server with semantic versioning, numbers like 1.4.2 whose first part goes up when something breaks. Changing a tool's name, removing a parameter or changing what it returns is a breaking change for every host and every prompt that relies on it.
  • The official SDKs (Python and TypeScript are the most used, and there are others, such as C#, Java, Kotlin, Go and Rust) handle framing, transports, schemas and protocol versions. They're how most servers are built. Knowing what they do inside, which is what this phase has taught, is how you debug them.

Running a remote server

Modern MCP is stateless, which makes deployment ordinary web engineering:

  • Run several copies behind a load balancer, with no sticky sessions. Any copy can answer any request.
  • Put OAuth in front (last lesson), plus rate limits per user and per tool.
  • Use ttlMs on list results, so clients don't hammer tools/list.
  • Log to stderr (or your platform's logs) with a request id. Clients can pass OpenTelemetry trace context (traceparent) in _meta, so one trace can follow a request from the host through your server to the APIs it calls.
  • Watch per-tool error rates and latency. A tool that fails 30% of the time is a design bug.
Worked example

Two probes against the practice server

Conformance probes are ordinary requests with expected answers. Here are two, run against the live practice server.

import requests

URL = "https://api.gradient.test/mcp"
GOOD = {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}

def post(method, meta, id=1, version="2026-07-28"):
    body = {"jsonrpc": "2.0", "id": id, "method": method, "params": {"_meta": meta}}
    headers = {"MCP-Protocol-Version": version, "Mcp-Method": method}
    return requests.post(URL, json=body, headers=headers, timeout=10)

r = post("resources/unknown", GOOD)
print("unknown method:", r.status_code, r.json()["error"]["code"], "(want 404, -32601)")

r = post("tools/list", {**GOOD, "io.modelcontextprotocol/protocolVersion": "1999-01-01"}, version="1999-01-01")
err = r.json()["error"]
print("bad version:", r.status_code, err["code"], err["data"]["supported"], "(want 400, -32022)")
In the lesson you can change this and run it.

Each probe sends one request and compares the answer with what the spec requires. A conformance suite is a few dozen of these, run against any server. The exercise builds one that works on any in-memory server.

Quick check
What does a conformance suite check that your handler tests don't?
  1. How fast the server starts
  2. Protocol behavior every client relies on: error codes, ids, notifications, discovery and versions
  3. Whether the model likes the tools
  4. Whether each tool's business logic is right
Show the answer

Protocol behavior every client relies on: error codes, ids, notifications, discovery and versions

Handler tests check your tools; conformance checks your server speaks MCP correctly, whatever its tools are.

Renames break things

Rename a street, and every map and delivery route that used the old name goes wrong. A tool's name is a public interface. Models, prompts and saved approvals all depend on it, so treat a rename as a breaking change.

Practice what you just read

  • 2 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

What is the MCP Inspector for?

  1. Connecting to a server and browsing or calling its tools, resources and prompts by hand while developing
  2. Scanning servers for malware and known security holes, before you connect them to a host that has your data
  3. Deploying servers to the cloud, by packaging each one and checking it answers once it's running there
  4. Training models to use tools, by recording each call you make by hand and replaying it as an example

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson