Skip to main content
Glama

mcpkit — Build and Test MCP Servers

CI Python 3.10+ MCP 2026-07-28 Zero dependencies

A Model Context Protocol server framework with the thing MCP tooling mostly lacks: a way to test the server.

uv venv && uv pip install -e ".[dev]" && pytest    # 42 tests, zero deps
python examples/filesystem_server.py               # conformance report
from mcpkit import Server, TestClient

server = Server("my-server")

@server.tool(description="Add two numbers.", read_only=True)
def add(a: int, b: int) -> int:
    return a + b

client = TestClient(server)
assert client.call_text("add", a=2, b=3) == "5"

Testing MCP servers

The usual approach is launching the server as a subprocess and driving it with a real client: slow, awkward in CI, terrible failure messages.

TestClient speaks the protocol in-process — real JSON-RPC messages, real handshake sequencing, real error shapes — but as ordinary function calls with ordinary assertions. Every message round-trips through json.dumps/loads, so anything unserialisable fails in a test rather than against a real client.

Related MCP server: MCP Python Server

The conformance checker

conformance_report() checks what actually breaks interoperability — the things server authors never test because their one client happens to tolerate them:

✓ initialize returns a protocolVersion
✓ initialize returns serverInfo.name
✓ initialize returns capabilities
✓ notifications receive no response
✓ requests before initialize are rejected
✓ unknown methods return -32601
✓ tools/list returns a list
✓ every tool has name, description and inputSchema
✓ every inputSchema is an object schema
✓ no tool has an empty description
✓ calling an unknown tool returns -32602
✓ every response carries jsonrpc 2.0
✓ every response echoes its request id
✓ no response has both result and error

14/14 conformance checks passed

"Notifications receive no response" is the one that bites hardest. notifications/initialized has no id, so it must not be answered — reply anyway and a strict client hangs up, with no useful error on either side.

"No tool has an empty description" catches something valid-but-useless: a tool a model cannot decide when to call.

Failing tools are results, not protocol errors

result = client.call_tool("divide", a=1, b=0)
result["isError"]                    # True
result["content"][0]["text"]         # "ZeroDivisionError: division by zero"

A tool that raises returns isError: true with the message, so the model sees what went wrong and can correct. A JSON-RPC error would make the call vanish from the model's view entirely.

An unknown tool is a protocol error — that's a client bug, not something the model can fix.

Schemas come from signatures

@server.tool(description="Greet someone.", name="who to greet")
def greet(name: str, formal: bool = False) -> str: ...
{"type": "object",
 "properties": {"name": {"type": "string", "description": "who to greet"},
                "formal": {"type": "boolean"}},
 "required": ["name"]}

Uses inspect.signature(fn, eval_str=True). Without eval_str, code with from __future__ import annotations gets annotations as strings, every parameter silently degrades to "string", and the client sends the wrong types with no error anywhere.

A bug worth keeping

call_text(name, **arguments) collided with a tool argument called name — and name is one of the most common tool arguments there is:

TypeError: TestClient.call_text() got multiple values for argument 'name'

Fixed with positional-only parameters, pinned by TestArgumentCollisions. (The same trap caught me in prompt-registry; it's worth knowing by name.)

Example server

examples/filesystem_server.py is a read-only filesystem server: list_files, read_file, count, one resource, one prompt — and a sandbox that resolves paths before checking containment, so ../../../etc/passwd and symlinks both get rejected.

python examples/filesystem_server.py --serve   # over stdio, for a real client

Transport

Line-delimited JSON over stdio. Anything the server logs must go to stderr — stdout is the transport, and a stray print() there corrupts the stream in a way that is genuinely painful to diagnose.

License

MIT — see LICENSE.


Built by Mehul Jariwala · LinkedIn

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A Python-based implementation of the Model Context Protocol that enables communication between a model context management server and client through a request-response architecture.
    -
  • A
    license
    B
    quality
    D
    maintenance
    A Model Context Protocol (MCP) server based on OpenRPC, providing JSON-RPC function invocation and method discovery services.
    2
    1
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    A Python framework for building Model Context Protocol servers with decorator-based tools, zero-config deployment, and high performance.
    10
    Apache 2.0