Skip to content

Inspect behaviour

  • Lesson 10
  • 50 min
  • Module 3: Build and inspect a local server

What you will be able to do

Look at the server the way any other application will see it. You use MCP Inspector, the MCP project’s developer tool, to discover the server’s capabilities. You call a tool with valid and invalid inputs, and read each result and error. Then you prove the module’s outcome: discovery, one valid call and one controlled failure.

Before you startWhat to know and to have readyAbout 50 minutes

What you need to know

Your project

  • ~/projects/support-mcp with the server of the last lesson: python -m pytest -q prints 40 passed.
  • Node.js 22.19 or later, only for Inspector. Check with node --version; the course used 24.18.0. If you do not have it, install the LTS version from nodejs.org.
  • An internet connection the first time: npx downloads Inspector, about 97 MB. No account and no key.

When a car shows a warning light, a mechanic plugs a small reader into a standard port under the dashboard. The reader does not drive the car. It asks each part for its state, shows the raw answers, and can send a test command: “turn on the fan”.

The reader is useful because it is not the car’s own dashboard. The dashboard shows what its designers chose to show. The reader shows what the car really says, in the standard form that every garage understands.

The problem: the tests pass, but what do other applications see?

Section titled “The problem: the tests pass, but what do other applications see?”

Your 40 tests pass. They test the server through the SDK’s client, which the server’s own developers wrote. A different host, such as a partner’s app, has its own client. It reads the same messages, but it shows them in its own way, and it gives the text to its own model.

The course team found a real example of this. In the first version of support-mcp, each tool’s docstring had two lines. The tests passed. Then the team listed the tools with Inspector, and saw this description:

"description": "Search this organization's policy documents. Returns at most 5 short matches, best first,\n each with the URI of the full policy. Use it before you answer a policy question."

The line break and eight spaces of the docstring’s indentation went to every client, and so to every model. Nothing failed, but the text that a model reads was not what Amira meant. No test looked at it, because no test read the description as a client shows it. The team changed the docstrings to one line each. Your server has the fixed text.

The idea: an independent client that shows everything

Section titled “The idea: an independent client that shows everything”

MCP Inspector is a developer tool from the MCP project. It is a host with an MCP client, but it has no model and no opinions. It connects to a server and lists what the server offers. You can call each tool, read each resource and fill each prompt by hand, and it shows every message. This course uses Inspector 2.10.1, which needs Node.js. You start it with npx, which downloads and runs a Node.js package.

Inspector has two ways to work:

  • The web UI runs a small web server on your computer and opens a page in your browser. You click through the server’s tools, resources and prompts. Use it to explore.
  • The CLI (command-line interface) sends one request, prints the result as JSON and exits with a code. Use it for checks that you repeat, and in scripts.
Two panels at the top. Web UI: npx @modelcontextprotocol/inspector@2.10.1. Your browser at http://127.0.0.1:6274, with a token in the address, talks to the Inspector's local web server (Node.js), which holds the MCP client. Notes: you click Connect, Tools, a tool, Execute Tool; the Messages panel lists every request; use it to explore and to see what a model would see: names, descriptions, schemas, annotations. CLI: inspector@2.10.1 --cli. One command is one request; it prints the result as JSON, then exits; exit code 0 OK, 5 tool error, 1 error. Notes: use it in scripts and checks; the target first, then -- and the Inspector's options. Both panels start python -m support_mcp as a child process on stdio: the same server, the same messages. An amber note at the bottom: default era legacy, initialize with revision 2025-11-25; add --protocol-era modern for revision 2026-07-28, server/discover and _meta; the server answers both, so test both.
Inspector 2.10.1 in its two modes. Both start the server as a child process on stdio, as any host does.
Text description of the diagram

Web UI (npx @modelcontextprotocol/inspector@2.10.1): your browser opens http://127.0.0.1:6274, with a token in the address. It talks to Inspector’s local web server, which holds the MCP client. You click Connect, Tools, a tool and Execute Tool, and the Messages panel lists every request. You see what a model could see: names, descriptions, schemas and annotations.

CLI (… --cli): one command is one request. It prints the result as JSON and exits with code 0 (OK), 5 (a tool error) or 1 (an error). Write the target first, then --, then Inspector’s own options.

Both start python -m support_mcp as a child process on stdio, so they see the same server and the same messages as any host.

The protocol era: by default, Inspector 2.10.1 uses the legacy era: initialize with revision 2025-11-25. Add --protocol-era modern to use revision 2026-07-28, with server/discover and _meta. The server answers both, so test both.

The last box of the picture is a real finding of this course. Inspector 2.10.1 starts in the legacy era by default. The course team put a recorder between Inspector and the server, and the first message was:

{"method": "initialize", "params": {"protocolVersion": "2025-11-25", "capabilities": {…}, "clientInfo": {"name": "inspector-cli", …}}, "jsonrpc": "2.0", "id": 0}

With --protocol-era modern, the first message became server/discover with _meta for 2026-07-28. This matters when you write down what you tested. “Inspector works” is true for both eras, but you should say which one you used.

A tiny example: list the tools from the command line

Section titled “A tiny example: list the tools from the command line”

Start with one command, from your project folder, with the virtual environment active. Inspector starts the server with the command after --cli, so python must be the environment’s Python.

Terminal window
npx @modelcontextprotocol/inspector@2.10.1 --cli python -m support_mcp -- --protocol-era modern --method tools/list

The first time, npx asks whether it may install the package: type y and press Enter. The output is the answer to tools/list, as JSON. Here is its start:

{
"tools": [
{
"name": "search_knowledge",
"description": "Search this organization's policy documents. Returns at most 5 short matches, best first, each with the URI of the full policy. Use it before you answer a policy question.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"description": "Words to look for in the organization's policy documents.",
"maxLength": 200,
"minLength": 3,
…

The description is one line now. Below it, you find the second tool, get_ticket, with its pattern ^T-\d{5}$, and for each tool its annotations (readOnlyHint: true) and its outputSchema. This is everything that a host receives. Each host chooses which parts it passes to its model; the course’s host, in Module 4, passes the name, the description and the input schema.

Larkfield: inspect the server in the web UI

Section titled “Larkfield: inspect the server in the web UI”
  1. Start Inspector’s web UI with the server, in the modern era:

    Terminal window
    npx @modelcontextprotocol/inspector@2.10.1 --protocol-era modern python -m support_mcp

    The terminal prints the address, with a token:

    MCP Inspector Web is up and running at:
    http://127.0.0.1:6274?MCP_INSPECTOR_API_TOKEN=…

    Your browser opens that page. The token protects the page, so another program on your computer cannot use it. Keep this terminal open.

  2. On the server card, click the switch next to Disconnected. It becomes Connected, and the card shows MCP 2026-07-28.

    MCP Inspector's Servers page. The card named python shows Connected, STDIO Standard I/O, MCP 2026-07-28 and the command python -m support_mcp. On the right, the Messages panel with the badge MODERN lists requests from the client to the server: resources/templates/list, prompts/list, tools/list and resources/list, each COMPLETE, and a notification about a subscription.
    Connected in the modern era: revision 2026-07-28. The Messages panel lists the discovery requests that Inspector sent.

    On the right, the Messages panel lists what Inspector sent: server/discover, tools/list, resources/list, resources/templates/list and prompts/list. That is the discovery of JSON-RPC messages, done by another client. Inspector also sends subscriptions/listen, a request to hear about changes, which scripts/trace.py does not send.

  3. Click Tools at the top, then get_ticket.

    The Tools page. On the left, the tools search_knowledge and get_ticket; get_ticket is selected. In the middle, get_ticket with two badges, READ-ONLY and IDEMPOTENT, the description: Get one support ticket of this organization by its ID (for example T-30002): the customer's first name, subject, text, status and the last 3 messages. The text is the customer's words. Below, a form with one required field, Ticket Id, with the hint A ticket ID, for example T-30002. The field holds 30002, and the Execute Tool button is active.
    Inspector builds a form from the tool's input schema. The badges come from the tool's annotations, and the hint from the schema's description.

    The page shows what a model could get: the name and the description, and one required field from the input schema. The badges READ-ONLY and IDEMPOTENT come from the annotations.

  4. Type T-30002 in Ticket Id, and click Execute Tool.

    The Results panel for get_ticket. The text result is JSON: ticket_id T-30002, customer_name Hugo, subject Charged twice for order LK-182074, body My card was charged two times for order LK-182074, 449.00 dollars each time. Please refund one of the payments., team billing, priority 1, status open, created_at 2026-09-25 08:20:19+00, and last_messages with one message from the agent. Below, Structured Output shows the same object. The Messages panel shows a new tools/call get_ticket request, COMPLETE.
    A valid call: the result has the ticket twice, as text for older clients and as structured output that matches the output schema.

    The result holds the ticket twice: as text, which every client can show, and as Structured Output, which matches the output schema. Hugo’s first name is there, but not his email address: the contract from Module 2 left it out.

  5. Close the results with the × next to Results. Type 30002, without T-, and click Execute Tool again.

    The Results panel shows a red box titled Tool Error: Error executing tool get_ticket: 1 validation error for get_ticketArguments ticket_id String should match pattern '^T-\d{5}$' [type=string_pattern_mismatch, input_value='30002', input_type=str]. The Messages panel shows a second tools/call get_ticket, COMPLETE.
    A controlled validation failure: the server refused 30002 because it does not match the pattern. Inspector shows it as a Tool Error.

    Inspector sent the request: it did not check the pattern itself. The server’s schema refused it. The answer is a tool error, a result with isError: true, so the Messages panel shows the request as complete. A model would read this text, and could try again with T-30002.

  6. Click Resources. The list shows 9 URIs, among them Manual refunds and goodwill credits, a staff-only document: Sam is staff. Click one to read it.

  7. To stop Inspector, go back to the terminal and press Ctrl+C. It stops the web server and the child process.

Try one more thing on your own: stop Inspector, and start it again without --protocol-era modern. The server card then shows MCP 2025-11-25, and the Messages panel shows LEGACY and an initialize request:

The Servers page in the default era. The card shows Connected and MCP 2025-11-25. The Messages panel has the badge LEGACY and lists resources/templates/list, prompts/list, tools/list, resources/list, notifications/initialized and initialize.
Inspector's default: the legacy era, with the initialize handshake. The same server answers it.

The web UI is for exploring. For checks that you repeat, use the CLI. The same request gives the same output, and the exit code says what happened. Run these from the project folder. The course did not test these commands in PowerShell, which quotes 'ticket_id="30002"' differently.

  1. A valid call:

    Terminal window
    npx @modelcontextprotocol/inspector@2.10.1 --cli python -m support_mcp -- --protocol-era modern --method tools/call --tool-name get_ticket --tool-arg ticket_id=T-30002

    The output is the result as JSON, with "isError": false at the end. The exit code is 0: check it with echo $? (PowerShell: $LASTEXITCODE).

  2. The same call with ticket_id=30002:

    Terminal window
    npx @modelcontextprotocol/inspector@2.10.1 --cli python -m support_mcp -- --protocol-era modern --method tools/call --tool-name get_ticket --tool-arg ticket_id=30002
    "text": "Error executing tool get_ticket: 1 validation error for get_ticketArguments\nticket_id\n Input should be a valid string [type=string_type, input_value=30002, input_type=int]\n …"
    …
    "isError": true

    On stderr: {"error":{"code":"tool_is_error","message":"Tool 'get_ticket' returned isError:true."}}, and the exit code is 5. Read the message closely: “Input should be a valid string”, input_type=int. The CLI turned 30002 into the number 30002, because it looks like a number. The web UI sent a string. Same input, two different messages.

  3. Send the string on purpose, with JSON quotes inside the argument:

    Terminal window
    npx @modelcontextprotocol/inspector@2.10.1 --cli python -m support_mcp -- --protocol-era modern --method tools/call --tool-name get_ticket --tool-arg 'ticket_id="30002"'

    Now the message is the one from the web UI, String should match pattern '^T-\d{5}$', with input_value='30002', input_type=str. Exit code 5.

  4. Read another organization’s policy:

    Terminal window
    npx @modelcontextprotocol/inspector@2.10.1 --cli python -m support_mcp -- --protocol-era modern --method resources/read --uri policy://bramble/returns-policy
    {"error":{"code":"error","message":"Resource not found: policy://bramble/returns-policy"}}

    Exit code 1: a protocol error, not a tool error. The CLI does not print the JSON-RPC code. You saw it in the recording of JSON-RPC messages: −32602, with the URI in data.

  5. Fill the prompt:

    Terminal window
    npx @modelcontextprotocol/inspector@2.10.1 --cli python -m support_mcp -- --protocol-era modern --method prompts/get --prompt-name draft_reply --prompt-args ticket_id=T-30002

    The output holds one user message that starts with Draft a reply to support ticket T-30002 for larkfield. The organization comes from the server’s environment, not from the arguments.

You have now seen every kind of failure that this server sends. The picture sorts them by the two shapes from JSON-RPC messages:

Two panels. Left, red: protocol error, error instead of result; the request itself failed; the client and the host's code handle it; the model usually never sees it. Four examples: -32601 Method not found, tools/delete; -32602 Invalid params, no _meta, or the resource policy://bramble/… on Larkfield; -32003 forbidden or insufficient_scope, this server's own code, no membership or no scope; -32022 Unsupported protocol version, server/discover with 2099-01-01. Right, amber: tool error, a result with isError true; the tool was called and refused the call; the text goes back to the model, which can read it and try again. Three examples: validation, get_ticket with 30002, String should match pattern; not_found, get_ticket T-40003 on Larkfield, no ticket in this organization; unknown tool, delete_ticket, Unknown tool, which SDK 2.3.0 sends although the specification asks for -32602. A note: Inspector CLI exit code 5 for a tool error, exit code 1 for a protocol error.
Every failure of support-mcp in this module, sorted by its shape. The shape decides who reacts: the host's code, or the model.
Text description of the diagram

Protocol error: error instead of result. The request itself failed. The client and the host’s code handle it; the model usually never sees it.

  • −32601 Method not found: tools/delete.
  • −32602 Invalid params: no _meta, or the resource policy://bramble/… on a Larkfield connection.
  • −32003 forbidden or insufficient_scope: this server’s own code, for no membership or no scope.
  • −32022 Unsupported protocol version: server/discover with 2099-01-01.

Tool error: a result with isError: true. The tool was called and refused the call. The text goes back to the model, which can read it and try again.

  • validation: get_ticket with “30002”: “String should match pattern”.
  • not_found: get_ticket T-40003 on Larkfield: no ticket in this organization.
  • unknown tool: delete_ticket: “Unknown tool”. SDK 2.3.0 sends a tool error; the specification asks for −32602.

The Inspector CLI exits with code 5 for a tool error and code 1 for a protocol error.

The team keeps a short list of what the inspection found, because each item becomes a check later:

  • Descriptions are content. A model reads them. The first run showed the docstring’s indentation. The fix was one-line docstrings. In Module 7, a contract test compares the descriptions with a file. Then a change cannot slip through again.
  • Clients send different types. The CLI sent 30002 as a number, the web UI as a string. The server must check every input itself, and it does: its schema refused both.
  • The era is part of the result. Inspector’s default is the legacy era. A compatibility note says “Inspector 2.10.1, both eras”, not just “Inspector”.
  • Tool errors look like success to the protocol. The Messages panel marks the failed call as complete. Only isError: true says that the tool refused.

Use Inspector whenever you change what a client sees: a new tool, a new description, a new error message. Use it before you connect a real host, so that you know the server’s side is right. Use the CLI in scripts, because the exit codes make a quick check easy.

It also has limits and costs:

  • It needs Node.js, which a Python project does not otherwise need, and about 97 MB for the package.
  • It is a developer tool, not a test suite. It shows what happens today. Turn each finding into a test, so that it stays fixed.
  • It runs a local web server with a token. It listens only on 127.0.0.1. Do not share the address with its token, and stop Inspector when you finish.
  • It stores secrets. For a server that needs a token, Inspector keeps it in your system’s keychain by default. This server needs none. To keep secrets only in memory, set MCP_INSPECTOR_SECRET_STORE=memory before you start it; the course did this for every run.
  • It is not your host. It shows what the server sends, not what a given AI application does with it. Module 4 builds that application.

Common mistake

No servers found in config file

Symptom: the CLI prints {"error":{"code":"error","message":"No servers found in config file"}} and does nothing.

Cause: the options came before the server command, in the order of Inspector version 1. Version 2 needs the target first. The failing command was … --cli --format json python -m support_mcp --method tools/list

Fix: write the server command first, then --, then Inspector’s options: … --cli python -m support_mcp -- --method tools/list. The -- is needed here because the server command has its own option, -m. It tells Inspector where that command ends.

Common mistake

Input should be a valid string

Symptom: a CLI call with --tool-arg ticket_id=30002 fails with Input should be a valid string [type=string_type, input_value=30002, input_type=int], and exit code 5.

Cause: the CLI reads values that look like numbers as numbers. The server expected a string, so its schema refused the call.

Fix: if you meant to test the pattern, send a string with JSON quotes: --tool-arg 'ticket_id="30002"'. If you meant a valid call, use the real ID: ticket_id=T-30002.

Module practice: build and inspect a small server

Section titled “Module practice: build and inspect a small server”

Guided example. This module’s worked example is your own project. You chose the SDK and built a tested environment. Then you read and sent JSON-RPC by hand. You ran support-mcp over stdio, with scripts/trace.py as a host. Last, you inspected it with Inspector: discovery, a valid call and a controlled failure.

Your turn. Harbour Cycles, the made-up bike rental shop of Module 1, wants a small read-only server. Write harbour_server.py, a new file outside support_mcp/, with synthetic records in a Python dictionary:

  1. Five bikes, each with an ID such as B-101, a size (S, M or L), a kind, a dock and whether it is free now.
  2. A read-only tool get_bike(bike_id) whose input must match ^B-\d{3}$, and which returns one bike or a not_found tool error.
  3. A read-only tool list_available(size, limit): the size is S, M or L, the limit from 1 to 5, and the result never has more than 5 bikes.
  4. One resource, terms://harbour/rental-terms, with the rental terms.
  5. Logs on stderr only.

Then inspect it with Inspector’s CLI: list the tools, make one valid call and one call that fails validation.

Show a worked solution
harbour_server.py
"""Harbour Cycles: a small read-only MCP server over in-memory records."""
import json
import sys
from typing import Annotated, Literal
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations
from pydantic import BaseModel, Field
BIKES = {
"B-101": {"size": "M", "kind": "city", "available": True, "dock": "Harbour Street"},
"B-102": {"size": "L", "kind": "city", "available": False, "dock": "Harbour Street"},
"B-103": {"size": "S", "kind": "child", "available": True, "dock": "Old Pier"},
"B-104": {"size": "M", "kind": "electric", "available": True, "dock": "Old Pier"},
"B-105": {"size": "M", "kind": "electric", "available": False, "dock": "Station Road"},
}
TERMS = """# Rental terms
Rent a bike for 1 to 7 days. The deposit is 50 dollars and comes back when the bike is returned undamaged.
Wear a helmet: we lend one with every bike."""
READ_ONLY = ToolAnnotations(read_only_hint=True, destructive_hint=False, idempotent_hint=True, open_world_hint=False)
BikeId = Annotated[str, Field(pattern=r"^B-\d{3}$", description="A bike ID, for example B-101.")]
class Bike(BaseModel):
bike_id: str
size: Literal["S", "M", "L"]
kind: str
available: bool
dock: str
class Available(BaseModel):
size: str
bikes: list[Bike] = Field(max_length=5)
mcp = MCPServer("harbour-bikes", version="0.1.0")
def log(**fields) -> None:
print(json.dumps(fields), file=sys.stderr, flush=True)
@mcp.tool(annotations=READ_ONLY)
def get_bike(bike_id: BikeId) -> Bike:
"""Get one rental bike by its ID (for example B-101): its size, kind, dock and whether it is free now."""
if bike_id not in BIKES:
log(event="tool_call", tool="get_bike", outcome="not_found")
raise ToolError(f"not_found: there is no bike {bike_id}")
log(event="tool_call", tool="get_bike", outcome="ok")
return Bike(bike_id=bike_id, **BIKES[bike_id])
@mcp.tool(annotations=READ_ONLY)
def list_available(
size: Literal["S", "M", "L"], limit: Annotated[int, Field(ge=1, le=5, description="1 to 5 bikes.")] = 3
) -> Available:
"""List free bikes of one size (S, M or L), at most 5."""
bikes = [Bike(bike_id=k, **v) for k, v in BIKES.items() if v["size"] == size and v["available"]][:limit]
log(event="tool_call", tool="list_available", outcome="ok", results=len(bikes))
return Available(size=size, bikes=bikes)
@mcp.resource("terms://harbour/rental-terms", name="rental-terms", title="Rental terms", mime_type="text/markdown")
def rental_terms() -> str:
return TERMS
if __name__ == "__main__":
mcp.run("stdio")

The inspection, from the folder of the file, with the project’s environment active (each command after npx @modelcontextprotocol/inspector@2.10.1 --cli python harbour_server.py --):

Options Exit What came back
--protocol-era modern --method tools/list 0 2 tools: get_bike (pattern ^B-\d{3}$) and list_available (size in S, M, L; limit 1 to 5)
… --method tools/call --tool-name list_available --tool-arg size=M 0 B-101 (city, Harbour Street) and B-104 (electric, Old Pier): the two free M bikes
… --method tools/call --tool-name list_available --tool-arg size=XL 5 tool error: Input should be 'S', 'M' or 'L', input_value='XL'
… --method tools/call --tool-name get_bike --tool-arg bike_id=B-9 5 tool error: String should match pattern '^B-\d{3}$'
… --method resources/read --uri terms://harbour/rental-terms 0 the rental terms

On stderr, each successful call wrote one line, for example {"event": "tool_call", "tool": "list_available", "outcome": "ok", "results": 2}.

Your server can differ. Check four things. Every input has a bound: a pattern, a list of values or a range. Every result has a maximum size, nothing writes to stdout, and the tools only read. A tool such as run_sql(query) or edit_bike(...) fails the task: it is not read-only, or it has no bound.

Check your understanding

The Messages panel marks a tools/call as complete, but the Results panel shows a red Tool Error. Explain how both can be true, and say who should react to the error.

Show the answer

The JSON-RPC request succeeded: the server answered with a result, so the protocol sees a complete request. Inside the result, isError: true says that the tool refused the call, here because of the input pattern. This is a tool error, not a protocol error. The model should react: it reads the text and can correct its arguments, for example to T-30002. The host’s code reacts to protocol errors, such as −32601 or −32602.

Check your understanding

Predict: you run the Inspector CLI with --tool-arg ticket_id=T-40003 against support-mcp, started as Sam at Larkfield. What is the exit code, what does the result say, and is it a protocol error or a tool error?

Show the answer

Exit code 5, a tool error: Error executing tool get_ticket: not_found: there is no ticket T-40003 in this organization, with isError: true. T-40003 is a Bramble Books ticket. The ID matches the pattern, so the call reaches the tool. The tool looks for the ticket inside Larkfield only, and answers not_found, the same as for a ticket that does not exist anywhere.

Check your understanding

Practical check: with Inspector, show the descriptions of support-mcp's two tools. Are they one line each, and which annotation does Inspector show as a badge?

Show the answer

Yes. In the web UI, the description of get_ticket is one paragraph with no line break: “Get one support ticket of this organization by its ID (for example T-30002): …”. With the CLI, tools/list shows "description" as one string without \n. Inspector shows READ-ONLY and IDEMPOTENT as badges, from readOnlyHint: true and idempotentHint: true. If you see \n followed by spaces, your server still has two-line docstrings: copy server.py from end-of-m03 again.

Check your understanding

Module completion check: demonstrate successful discovery, one valid call and one controlled validation failure for support-mcp. For each, give the command or the clicks, and the evidence that shows the outcome.

Show the answer

A complete answer has three pieces of evidence, each from a real run of your project.

  1. Discovery. python -m scripts.trace discovery shows five requests (server/discover, tools/list, resources/list, resources/templates/list, prompts/list) and five results, ids 1 to 5. Or, in Inspector: Connected, MCP 2026-07-28, two tools, 9 resources, 1 template and 1 prompt.
  2. One valid call. get_ticket with T-30002, from Inspector or python -m scripts.trace valid-call: a result with isError: false, the subject “Charged twice for order LK-182074”, and one log line on stderr with "outcome": "ok".
  3. One controlled validation failure. get_ticket with the string "30002": a result with isError: true and String should match pattern '^T-\d{5}$'. With the CLI, the exit code is 5.

Explain each in a sentence. Discovery lists what any client can use. The valid call returns bounded, structured data for the caller’s organization. The failure is a tool error that the server’s schema caught, not a crash. Demonstrated needs all three, with outputs from your own run. A screenshot of the valid call alone is partially demonstrated.

Recap

  • MCP Inspector 2.10.1 is an independent client. Use the web UI to explore, and the CLI for repeatable checks. Its exit codes: 0 OK, 5 a tool error, 1 an error.
  • Its default era is legacy; add --protocol-era modern for revision 2026-07-28, and say which era you tested.
  • Look at what the client receives. Descriptions are content for the model; Inspector showed a real defect that the tests missed.
  • A tool error is a complete request with isError: true. A protocol error is an error response.

Module recap. You chose the official SDK with four questions, and built a fresh, tested environment. You read JSON-RPC by hand: requests, results, errors, notifications, and the silence after a broken line. You ran support-mcp over stdio, with protocol on stdout, logs on stderr, and the caller from the environment. And you inspected the server like any other client would. Your project equals the snapshot end-of-m03: 40 passed.

Next, you build an AI application of your own that connects to this server, with a mock model first.

Next: Client integration