Start the server the way an agent would, send the initialize request, then tools/list, and inspect what comes back. A probe script can check that every stdout line is valid JSON-RPC, that the server answers in time, that each tool and parameter has a useful description, and how large the tool list is, since tool definitions are sent to the model as context.

Key takeaways

  • Over stdio, stdout belongs to the protocol. The MCP specification says a server must not write anything to stdout that is not a valid MCP message; logs go to stderr.
  • The lifecycle is fixed. The client sends initialize, the server replies, the client sends an initialized notification, and only then do requests such as tools/list work.
  • Descriptions are the interface. The model chooses tools from their name, description and parameter descriptions, so a tool described as 'Search' is a tool that gets misused or skipped.
  • Tool definitions cost context before anything is called. The real four-tool server measured here sends about 3,000 characters, roughly 750 tokens by a rough four-characters-per-token rule.
  • Every connected server adds to that. Ten servers with ten tools each is a different cost from one small server, and a probe makes the number visible.

Why test first

A tool-using client hides most failures. A server that crashes on startup, writes a banner to stdout, or takes too long to answer the handshake usually shows up as "no tools available", with nothing to say which of those happened. Talking to the server directly turns a vague symptom into a specific line of output. If you are still building one, the mechanics are laid out in building an MCP server from scratch, and what the protocol does is in what MCP actually does.

What the stdio transport and lifecycle require, from the MCP specification.
RuleStatus
Messages are JSON-RPC, one per line, over stdin and stdoutDocumented
The server must not write non-protocol output to stdout; stderr may carry logsDocumented
Client sends initialize, then an initialized notification, before other requestsDocumented
Tools are described by a name, a description and a JSON Schema for inputDocumented
A description of at least 40 characters, and every parameter describedMy threshold

Source: the Model Context Protocol specification.

The probe

Give it the command that starts your server. It performs the handshake, lists the tools, and prints one line per finding. It uses a timeout, so a server that never answers is reported instead of hanging the check:

import json, select, subprocess, sys, time

# usage: mcp_probe.py <command> [args...]   (a stdio MCP server)
proc = subprocess.Popen(sys.argv[1:], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)

def send(msg):
    proc.stdin.write(json.dumps(msg) + '\n'); proc.stdin.flush()

def read(want_id, timeout=10):
    end = time.time() + timeout
    while time.time() < end:
        if not select.select([proc.stdout], [], [], max(0, end - time.time()))[0]:
            return None          # nothing arrived before the deadline
        line = proc.stdout.readline()
        if not line:
            return None
        line = line.strip()
        if not line:
            continue
        try:
            msg = json.loads(line)
        except ValueError:
            print('FAIL  stdout line is not JSON (breaks the protocol): %r' % line[:80]); continue
        if msg.get('id') == want_id:
            return msg
    return None

t0 = time.time()
send({'jsonrpc': '2.0', 'id': 1, 'method': 'initialize', 'params': {'protocolVersion': '2025-06-18', 'capabilities': {}, 'clientInfo': {'name': 'probe', 'version': '0'}}})
init = read(1)
if not init or 'result' not in init:
    print('FAIL  no valid initialize response within 10 s'); proc.terminate(); sys.exit(1)
print('ok    initialize in %d ms; server %s %s; protocol %s' % ((time.time() - t0) * 1000, init['result']['serverInfo']['name'], init['result']['serverInfo'].get('version', '?'), init['result']['protocolVersion']))
print('      capabilities: %s' % ', '.join(init['result'].get('capabilities', {}).keys()))
send({'jsonrpc': '2.0', 'method': 'notifications/initialized'})
send({'jsonrpc': '2.0', 'id': 2, 'method': 'tools/list'})
tools = read(2)
if not tools or 'result' not in tools:
    print('FAIL  tools/list gave no result'); proc.terminate(); sys.exit(1)
lst = tools['result']['tools']
payload = json.dumps(tools['result'])
print('ok    %d tools, tools/list payload %d characters (about %d tokens at 4 chars/token, approximate)' % (len(lst), len(payload), len(payload) // 4))
problems = 0
for t in lst:
    size = len(json.dumps(t))
    issues = []
    if not t.get('description'): issues.append('no description')
    elif len(t['description']) < 40: issues.append('description under 40 chars')
    schema = t.get('inputSchema', {})
    if schema.get('type') != 'object': issues.append('inputSchema.type is not object')
    for name, p in schema.get('properties', {}).items():
        if not p.get('description'): issues.append('param %s has no description' % name)
    problems += bool(issues)
    print('%s %-28s %5d chars%s' % ('WARN ' if issues else 'ok   ', t['name'], size, ('  - ' + '; '.join(issues)) if issues else ''))
proc.terminate()
print('\n%d of %d tools have issues' % (problems, len(lst)))

Tested on real and broken servers

The real server is the Search Console server from the companion guide. The others are small fakes I wrote to fail on purpose.
ServerWhat the probe reportedStatus
Real server, four toolsHandshake ok, 4 tools, no description problemsMeasured
Prints a banner to stdoutFlagged the non-JSON line as breaking the protocolMeasured
Tool described only as 'Search', parameter undocumentedFlagged bothMeasured
Tool with no description at allFlaggedMeasured
Process that never answersReported no initialize response within 10 secondsMeasured

The hang case failed to be caught at first: my first version used a blocking read, so the timeout never fired, and the check itself hung. It now waits on the stream with a deadline. That is the sort of bug a probe is for, and it is why I tested it against a server that fails, not only one that works.

What the tool list costs

Clients send tool definitions to the model along with your prompt, so they count as input tokens whether or not a tool is called (Anthropic's tool use documentation describes tool definitions as part of the input). For the real server, the tools/list payload was 2,989 characters:

Size of each tool's definition as sent by the real server.
ToolCharacters
list_sites302
list_sitemaps573
inspect_url898
search_analytics_query1,197

At about four characters per token that is roughly 750 tokens. I use that rule only to give a sense of scale: it is an approximation for English text, not the tokenizer any particular model uses. The useful reading is relative: one tool is 40% of the total, and a server with fifty tools would be a different decision. This is the same trade-off as other context costs in how coding agents choose what to read and prompt caching, where a stable block that is sent every turn can be cached.

What to fix

  • Stdout noise: send logs to stderr, and check dependencies that print on import.
  • Vague descriptions: say what the tool does, what it returns and when to use it instead of its neighbour.
  • Undocumented parameters: add a description, with units and formats (a date range, a URL form).
  • Heavy tools: trim schema text that repeats what the description already says, and split rarely used tools into a separate server.

What this does not cover

The probe tests the protocol, not the server's behaviour. It checks that the server speaks the protocol and describes its tools; it does not call them, check authentication, check that results are correct or safe, or test a server over HTTP. The real server is one I wrote, so it is not a sample of servers in general. Startup time depended on whether Node was warm (about 4 seconds on the first run, under 1 second on the next, one run each). The MCP inspector, which the real server's project exposes as an npm script, is an interactive alternative; I did not run it for this write-up. Token figures are approximations.

Badri Dutta

Software engineer · 8 years technical SEO

Written after building and probing the Search Console server in the companion guide, including finding a bug in the probe's own timeout by testing it against a server that hangs.

Full background →