Aditya Sharma

What Is an MCP Server? I Ran One and Logged Every Message

On this page, 10 sections

An MCP server is a program that gives an AI app tools, data and prompt templates through one standard protocol. The AI app (the host) starts one client per server, and the two exchange JSON-RPC 2.0 messages over standard input and output for a local server, or over Streamable HTTP for a remote one.

It is not a model, and it does not decide when it gets called. The MCP architecture page says the protocol covers context exchange only and does not dictate how an AI application uses an LLM or manages the context it receives.

I did not want to explain this from a diagram alone. On 11 October 2026 I ran a 13-line server built from the Python SDK tutorial (version 2.3.0) over stdio, sent it three raw requests, and pasted the replies below, reindented but otherwise unchanged.

Host, client, server

The architecture page names three participants. The host is the AI application, such as Claude Code or Claude Desktop. The client is a component inside the host that keeps one dedicated connection to one server. The server is the program that provides context.

So a host talking to five servers runs five clients. The page gives Visual Studio Code as an example: it connects to the Sentry server and the local filesystem server, and creates a separate client object for each.

“Server” says nothing about where it runs. The same page calls a server launched as a subprocess a local server, and one that lives on a vendor’s platform, like Sentry’s, a remote server. Both are MCP servers.

What a server can offer

The spec revision I read, 2026-07-28, defines three things a server can expose. Each has a list method so the client can discover what exists, and the list can change over time.

  • Tools: functions the AI app can call, such as a file operation, an API call or a database query. Called with tools/call.
  • Resources: data that gives context, such as file contents, database records or API responses.
  • Prompts: reusable templates, such as a system prompt or few-shot examples.

The tools page of the spec describes tools as model-controlled, meaning the language model can discover and call them on its own, and it describes a human in the loop who can deny a tool invocation. Claude Code’s permission prompts are one way a host does that.

Local or remote

Two transports are defined. The stdio page says the client launches the server as a subprocess, and the rules it sets are short, and these are the ones that matter:

  • The server reads JSON-RPC messages from stdin and writes them to stdout.
  • Messages are delimited by newlines and must not contain embedded newlines.
  • The server must not write anything to stdout that is not a valid MCP message.
  • The server may write logs to stderr, and the client should not treat that as an error.

The practical consequence follows from the third rule, and it is my reading, not a line from the page: a stray debug print in a stdio server writes to the channel the client parses, so log to stderr instead.

The other transport is Streamable HTTP, which uses HTTP POST for client-to-server messages with optional Server-Sent Events for streaming, and supports bearer tokens, API keys and custom headers. The architecture page says MCP recommends OAuth for the tokens.

In Claude Code the command for a remote server is claude mcp add --transport http <name> <url>, per the Claude Code MCP page I read on 11 October 2026. The same page says a JSON entry with a url but no type is read as a stdio server and skipped with an error, which is a common way to lose twenty minutes.

The server I ran

This is the whole server. It is the tool half of the Python SDK tutorial, plus a run call so it speaks stdio. I syntax-checked it with python -m py_compile and ran it on Python 3.14 with mcp 2.3.0.

from mcp.server import MCPServer

mcp = MCPServer("Demo")


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


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

The probe starts that file as a subprocess and writes one JSON-RPC request per line, which is the stdio framing described above. Every request carries the protocol version, client info and client capabilities in _meta, because this revision of the protocol expects them on each request.

import subprocess, json, sys
p = subprocess.Popen([sys.executable, "srv.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
meta = {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientInfo": {"name": "probe", "version": "0"},
        "io.modelcontextprotocol/clientCapabilities": {}}
reqs = [
 {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":meta}},
 {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"_meta":meta}},
 {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":1,"b":2},"_meta":meta}},
]
for r in reqs:
    p.stdin.write(json.dumps(r)+"\n"); p.stdin.flush()
    print(">>", json.dumps(r)[:160])
    print("<<", p.stdout.readline().strip()[:900])
p.stdin.close(); p.wait(timeout=10); print("exit", p.returncode)
print("stderr:", p.stderr.read()[:300])

Request 1: server/discover

Discovery asks the server what it supports. I reformatted the reply with indentation; the content is untouched.

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "cacheScope": "private",
    "capabilities": {
      "prompts": {
        "listChanged": true
      },
      "resources": {
        "listChanged": true,
        "subscribe": true
      },
      "tools": {
        "listChanged": true
      }
    },
    "resultType": "complete",
    "supportedVersions": [
      "2026-07-28"
    ],
    "ttlMs": 0,
    "_meta": {
      "io.modelcontextprotocol/serverInfo": {
        "name": "Demo",
        "version": ""
      }
    }
  }
}

Three things in that reply are worth reading. supportedVersions is ["2026-07-28"], the only revision it advertises through discovery. capabilities lists tools, resources and prompts, which the SDK enables by default. And serverInfo.version is an empty string because I never set one.

Request 2: tools/list

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "cacheScope": "private",
    "resultType": "complete",
    "tools": [
      {
        "description": "Add two numbers.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "a": {
              "title": "A",
              "type": "integer"
            },
            "b": {
              "title": "B",
              "type": "integer"
            }
          },
          "required": [
            "a",
            "b"
          ],
          "title": "addArguments"
        },
        "name": "add",
        "outputSchema": {
          "properties": {
            "result": {
              "title": "Result",
              "type": "integer"
            }
          },
          "required": [
            "result"
          ],
          "title": "addOutput",
          "type": "object"
        }
      }
    ],
    "ttlMs": 0,
    "_meta": {
      "io.modelcontextprotocol/serverInfo": {
        "name": "Demo",
        "version": ""
      }
    }
  }
}

I wrote no JSON Schema. The SDK built inputSchema from the type hints a: int, b: int, took the description from the docstring, and also generated an outputSchema from the return type.

Request 3: tools/call

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "text": "3",
        "type": "text"
      }
    ],
    "isError": false,
    "resultType": "complete",
    "structuredContent": {
      "result": 3
    },
    "_meta": {
      "io.modelcontextprotocol/serverInfo": {
        "name": "Demo",
        "version": ""
      }
    }
  }
}

The answer comes back twice: as text in content for the model to read, and as structuredContent for code that wants the number. isError is false. The process exited with code 0 when I closed its stdin.

Total: three requests, three replies, no handshake step before the first call that I sent. I did not test what happens if you skip _meta, so I make no claim about it.

What changed in the 2026-07-28 revision

The architecture page for this revision describes MCP as a stateless protocol: each request carries the version and the relevant capabilities, so the server can process it alone. Discovery through server/discover is described as optional, and a client may send any request directly and handle a version error if one comes back.

The same page marks two client features deprecated as of 2026-07-28: sampling (a server asking the host’s model for a completion) and logging (the page says to log to stderr on stdio or use OpenTelemetry). Elicitation, where a server asks the user for input, is delivered through what the spec calls Multi Round-Trip Requests.

I did not diff this against the previous spec text, so I will not say what was removed. If you learned MCP from an older tutorial, read the architecture page above before you copy its message examples.

What an MCP server is not

It is not retrieval. A server can sit in front of a document store and expose a search tool, but the protocol is the plumbing between the AI app and that tool, not the retrieval method. I compared the two in MCP server vs RAG.

It is also not tied to any one product. WordPress has its own set of servers, and what exists today is listed in MCP servers for WordPress. If you want to point Claude Code at a site, the full walkthrough is running Claude Code against WordPress.

What I could not verify

  • I ran the server over stdio only. I did not run a Streamable HTTP server, so everything above about HTTP comes from the docs.
  • My client was a raw script, not Claude Code. I did not connect this server to Claude Code in this session.
  • I tested one SDK, Python mcp 2.3.0. Other SDKs may behave differently on details like the empty version string.

Questions people ask

Does an MCP server run on my machine?

It can. A stdio server is launched by the host as a subprocess on the same machine. A remote server runs elsewhere and is reached over Streamable HTTP. The architecture page uses the filesystem server as the local example and Sentry’s as the remote one.

Do I have to write JSON Schema for my tools?

Not with the Python SDK. In the run above, type hints and a docstring were enough for the SDK to produce the input schema, the description and an output schema.

Does the model talk to the server directly?

No. The host coordinates the clients, and each client holds the connection to its server. The model sees the tool list the host gives it and asks the host to call a tool.

Do this today

Reproduce the run. Make a virtual environment, install the SDK with pip install mcp, save the server above as srv.py, save the probe as probe.py, and run python probe.py.

If your output shows a different supportedVersions, your SDK version is on a different revision of the spec, and that is the first thing to check when a tutorial’s messages do not match yours.

Resources

Tell me where I am wrong

Your email is not published and I do not add it to any list. Corrections with a source are the ones I act on fastest.