MCP Inspector is a client you can point at any
MCP server to list its capabilities
and call them by hand. You've used its --cli mode for single calls in earlier lessons.
The web client is the fuller surface, and it's what this lesson uses: you'll work through its
panels, then use them to find and fix a bug that reports itself as a success.
Connecting on the modern protocol
Inspector connects on the legacy protocol era by default. To get the modern era
(2026-07-28), give your server an entry in an Inspector catalog file with
protocolEra set, then launch against that file:
{
"mcpServers": {
"demo-server": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "src/stdio-server.ts"],
"protocolEra": "modern"
}
}
} npx @modelcontextprotocol/inspector --catalog inspector-catalog.json
Keep that file in your project root, not .vscode/. It belongs to Inspector alone,
unrelated to .vscode/mcp.json or Claude Code's .mcp.json.
Launch it
- Create
inspector-catalog.jsonin your project root with the entry above. - Run the launch command. Open the URL it prints exactly as printed: the token in it authenticates your browser to Inspector's backend, which can spawn processes on your machine.
- From the Servers tab, connect to
demo-server. -
Open Connection Info and confirm Protocol reads
2026-07-28and Era reads Modern.
The panels
Once connected, the tab bar across the top carries one panel per capability your server declared:
| Panel | What you see in it |
|---|---|
| Servers | Your server list, connection state, and per-server settings |
| Tools | Every registered tool, its arguments as a generated form, and the result of a call |
| Prompts | Every registered prompt, its arguments, and the messages it renders |
| Resources | Every registered resource by URI, and the content a read returns |
The monitoring panels sit in a separate column down the right-hand side rather than in that tab bar:
| Panel | What you see in it |
|---|---|
| Protocol | The JSON-RPC transcript: each request paired with its response |
| Console | Your server process's stderr, which is where its own logging goes |
Keeping them in their own column is the point: you can watch the traffic while you work in Tools or Resources, instead of switching away from what you're doing.
Note: a server with a logging capability, task support, or an HTTP
transport also gets Logs, Tasks, or Network panels alongside those two. Yours has none of those, so
they don't appear.
Where to look when a call goes wrong
- Is
contentpopulated? Emptycontenton a successful call means the result shape is wrong, almost always a misspelled or missing key. - Is
isErrorset? If so, the message text is your error, and the fix is usually in your handler. - Did the request carry the arguments you expected? Check the request half of the Protocol entry. A tool behaving as though it got nothing may have received nothing.
- Is there a matching response at all? A request with no paired response means the server didn't answer, so look for a crash in the Console panel rather than a logic bug.
Break it and debug it
Introduce a bug in add-task and track it down. Restart the server entry in Inspector
after each edit so it picks up the change.
Bug 1. Change your handler's return to misspell the result key:
return { message: [{ type: "text", text: `Added: ${title}` }] }; - Call
add-taskfrom the Tools panel. Does it report an error? - Open Protocol and read the result. Where did your text go, and what is
content? - Why did neither the SDK nor Inspector reject this?
Bug 2. Now fix the key but return content as a single object instead
of an array:
return { content: { type: "text", text: `Added: ${title}` } }; - Call it again. How does this failure differ from Bug 1, and where does Inspector show it?
- Fix the tool. Which of the two bugs would you rather ship, and what does that tell you about which failures to look for?
What you should see
Bug 1 reports success, and the Tools panel shows an empty result:
{
"content": [],
"message": [{ "type": "text", "text": "Added: Buy milk" }],
"resultType": "complete"
}
Nothing is invalid. content is absent so the SDK defaults it to empty, and
message is an unrecognized key that gets passed through untouched. Both halves are
individually acceptable, so there's nothing to reject. A model calling this tool gets an empty
result with no explanation.
Bug 2 fails loudly, and the Tools panel shows the error directly:
Invalid tools/call result: expected array, received object
TypeScript also catches it before you ever run the server. Bug 2 is the one you'd rather ship every
time: it names the field and what was wrong with it, and it fails the moment you call the tool. Bug
1 is the dangerous class, a successful-looking call that delivers nothing, invisible until someone
notices the model has no idea what your tool returned. That's why the first question in the list
above is whether content is populated, and why calling every tool once and asserting
content is non-empty catches this whole family of bugs.
Check your understanding
Why wasn't the misspelled key rejected?
Your handler returns `{ message: [...] }` instead of `{ content: [...] }`. The call reports success and the result shows `"content": []`. Why did neither the SDK nor Inspector catch it?