ealeng
Menu

Episode 03 of 7

Explore a real server with MCP Inspector

Connect MCP Inspector to the running acme-crm-mcp server: follow the 401 to the metadata, get and read a token, watch the handshake, inspect the tools, and check that a successful call returned the right tenant's data.

MCP Server · Hands-on MCP, Ep. 16 min read
Video transcript3:51

0:00Intro

0:01

Hands-on MCP, episode one: meet the MCP Inspector.

0:06

In earlier videos we drew a multi-tenant MCP server. Now it’s real, and running on this machine.

0:13Becky wants to see it work

0:14

Becky has seen the diagrams. Now she wants to see the real thing work.Becky: Diagrams are nice… but does it actually work? 🤨

0:19

Her team gave her the Acme CRM server, running locally: an authorization server that issues tokens, and the MCP server itself.

0:28

And the MCP Inspector, the official tool for poking at MCP servers. It runs in the browser, protected by its own local token.

0:3801 · Add the server

0:39

First, Becky adds the server. Add servers, then add manually.

0:44

She gives it a name, acme CRM, and picks the transport: streamable HTTP, the way remote MCP servers talk.

0:52

Then the server’s address: localhost:8787/mcp.

0:5902 · The first try

1:01

Becky flips the switch to connect, and it fails.Becky: Failed?! 😤

1:04

That’s not a bug. The server answered 401: no token, no entry.

1:09

And look at the network log. After the 401, the Inspector followed the server’s hint to its protected resource metadata, and from there found the authorization server.

1:19

The Inspector wanted to sign Becky in there, but found no sign-in endpoint. This demo’s token service only issues tokens to registered clients. So Becky gets one herself.

1:3103 · Getting a token

1:32

The authorization server hands out tokens to registered clients. Becky’s team registered one for her: acme agent.

1:40

One command, and she has an access token.

1:43

Decode it, and you can read the claims inside. Org ID acme: that’s her tenant. Her scopes: read and write. The audience: this exact server. And it expires in an hour.

1:54

Anyone can read a token. What matters is the signature: the server checks it, plus issuer, audience and expiry, on every single call.

2:0404 · Adding the header

2:05

Back in the Inspector, she opens the server’s settings, and finds custom headers.

2:10

One header: Authorization, then Bearer, then the token.

2:13

That’s exactly how any MCP client presents its token: the same header, on every request.

2:2005 · Connected

2:22

Now she connects again. Connected.Becky: Connected! 🎉

2:24

The protocol log shows the handshake. Initialize: client and server agree on a protocol version and capabilities. Then initialized. Then the first real question: tools list.

2:36

One more detail: the Inspector also asks for a notification stream, and gets 405. This server is stateless on purpose, so any instance can answer any request.

2:4806 · The tools

2:49

In the tools tab, the Inspector lists everything the server offers: exactly what an AI model would see.

2:56

Search contacts is marked read-only. Its description tells models to search before creating. And it has one required field: the query.

3:06

Delete contact is marked destructive, and its description says to confirm with the user first. Hints like these are how clients decide when to ask before acting.

3:1607 · First calls

3:18

Time for a real call. Search contacts, query Priya, execute.

3:22

Priya Shah, from Initech, straight out of Acme’s database.

3:26

Now Becky searches for Dana Kim, whom she knows is in the system.

3:30

Zero matches. Dana belongs to Globex, a different tenant. With Acme’s token, Globex’s data simply doesn’t exist.Becky: Wait… where’s Dana? 🤔

3:39Summary

3:40

Connect. Authenticate. Discover. Call.

3:43

Next time, Becky switches tokens, and sees tenant isolation, scopes, and the audit log in action.Becky: See you in episode 2! 👋

Key takeaways

  • ✓A first connection that fails with 401 is the server working: the WWW-Authenticate header points to the protected-resource metadata, which names the authorization server.
  • ✓The demo's authorization server has no authorization_endpoint and only the client_credentials grant, so the Inspector can't offer a browser sign-in: get a token for a registered client yourself.
  • ✓Anyone can decode a token. Read its claims to understand it (org_id, scope, aud, exp); trust comes from the server checking the signature, issuer, audience and expiry on every call.
  • ✓The tools tab shows exactly what a model sees: names, descriptions that say when to use a tool, input schemas, and hints like readOnly and destructive.
  • ✓A green connection isn't evidence of isolation. Check the records a call returns, and repeat with another tenant's token: Dana is 0 matches for acme and c_201 for globex.

MCP Inspector is the tool for exploring an MCP server by hand: connect, list its tools, call them, and watch every message. Against a server with real authorization, the first connection fails with a 401, and that's where understanding the server starts. This chapter connects Inspector to the running acme-crm-mcp demo: following the 401 to a token, reading the token, the handshake, the tools tab, and checking that a successful call returned the right tenant's data.

Try it · Trace the Inspector sessionThe demo's actual requests and responses

01 · Configure

Add server · Streamable HTTP · http://localhost:8787/mcp

transport: streamable-http
url:       http://localhost:8787/mcp

# the Inspector itself runs in the browser, behind its own local session token

Streamable HTTP is for remote servers; stdio is for local processes. Every MCP request goes to this one URL.

Run it yourself

The acme-crm-mcp demo needs Docker and Node 20+:

npm install
npm run db:up          # Postgres on :5433 with the schema and demo data
npm run dev            # authorization server on :8788, MCP server on :8787/mcp
npm run inspect        # the MCP Inspector web UI (a pinned version)

In the Inspector, add a server manually: any name, transport Streamable HTTP (stdio is for local processes), URL http://localhost:8787/mcp. The Inspector itself runs in your browser behind its own local session token.

Demo data: tenant acme has Priya Shah, Marcus Lee and Sofia Alvarez; tenant globex has Dana Kim (c_201) and Tom Becker. npm run db:reset puts it back at any time.

Following the 401

The first connection fails, and that's the server working. Three requests happen before you can do anything, all visible in the Inspector's network log:

Request Answer What it tells the client
POST :8787/mcp 401 · WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource" No token, and where to look next
GET :8787/.well-known/oauth-protected-resource authorization_servers: ["http://localhost:8788"], scopes_supported: ["crm:read", "crm:write"] Which authorization server issues tokens for this resource
GET :8788/.well-known/oauth-authorization-server token_endpoint, jwks_uri, grant_types_supported: ["client_credentials"], response_types_supported: [] How to get a token, and the public keys that verify it

Why there's no sign-in. A browser sign-in needs an authorization_endpoint and the code response type. The demo's authorization server has neither: it's a small mock that issues tokens only to registered clients, with the client-credentials grant. So the Inspector stops there, and you get a token yourself. A production authorization server (Auth0, Okta, Keycloak) adds the authorization-code flow with PKCE, and the Inspector would open a sign-in page instead.

Getting and reading a token

export TOKEN=$(npm run -s token)       # registered client acme-agent
npm run decode -- $TOKEN
Claim Value Meaning
org_id acme The tenant: the server takes it from here, never from tool arguments
scope crm:read crm:write What the token may do; each tool needs one
aud http://localhost:8787/mcp This exact server, nothing else
iss http://localhost:8788 Who issued it
exp issued-at + 3600 It expires in an hour
header alg, kid RS256, demo-key-1 Which key signed it, to look up in jwks.json

Decoding isn't verifying. The claims are base64url, readable by anyone, and a forged token decodes just as well. What matters is the signature, which the server checks on every call along with the issuer, audience and expiry.

In the Inspector, open the server's settings and add one custom header, Authorization: Bearer <token>, then reconnect. That's exactly how any MCP client presents its token: the same header, on every request.

Other tokens to try, for the next chapters:

npm run -s token -- acme-readonly                         # crm:read only
npm run -s token -- globex-agent                          # the other tenant
npm run -s token -- acme-agent --expired                  # test-only: expired an hour ago
npm run -s token -- acme-agent --audience https://other   # test-only: issued for another server

The two test-only tokens are minted locally with the demo's signing key, not by the authorization server, to show the MCP server rejecting them.

The handshake, and the protocol version

Once connected, the Inspector's protocol log shows:

Message Answer
initialize Protocol version and capabilities agreed
notifications/initialized Accepted (202)
tools/list The three tools
GET /mcp, for a notification stream 405, Allow: POST

The 405 isn't a failure: the server is stateless on purpose, builds a fresh MCP server for every request, and only accepts POST, so any instance can answer any request.

This is the handshake-era flow. The demo uses the MCP TypeScript SDK 1.32. The 2026-07-28 specification retired the initialize / initialized exchange and the Mcp-Session-Id header: each request now carries its own protocol version, client identity and capabilities in _meta, and an optional server/discover call covers capability discovery. Check which version your client and server speak before comparing a trace with this one (release notes).

What a model sees: the tools tab

Tool Hints Input Description
search_contacts readOnlyHint query, required Find contacts by name, email, or company. Call this before create_contact to avoid duplicates.
create_contact not read-only, not destructive, not idempotent name required; email (must be an email), company Create a contact. Only call this after search_contacts finds no match; it does not merge duplicates.
delete_contact destructiveHint, idempotentHint id, required Permanently delete a contact by id. Confirm with the user first.

Read the descriptions as instructions to a model: they say when to use a tool, not just what it does. The hints are how clients decide when to ask before acting. Neither is enforcement: scopes and row-level security are (Chapter 1).

Verify the result, not just the connection

Call (acme token) Result
search_contacts {"query": "Priya"} 1 match: Priya Shah, Initech (c_101), straight out of acme's rows
search_contacts {"query": "Dana"} 0 matches: Dana belongs to globex, and with acme's token globex's data doesn't exist

A green connection proves the token works. It doesn't prove the data is right. For every call, look at the records that came back and check they belong to the token's tenant, then repeat with another tenant's token: with globex-agent, the same Dana search returns c_201. If the result doesn't change with identity, something is wrong.

Beyond clicking

Every check above can run without the browser: npm run smoke uses the Inspector's CLI mode for five real calls, checked by exit code (0 ok, 3 authentication required, 5 tool error). Chapter 6 builds on that.

Practice: what the Inspector did for you

Do the Inspector's discovery and token reading in code: pull the metadata URL out of a 401, decide from the authorization-server metadata how a client can get a token, decode a token's claims (and see that a forged one decodes just as well), and list why a token won't work for a given call. The tests use the demo's real responses.

Try it · Practice in PythonRuns in your browser · ⌘/Ctrl + Enter to run
inspector_practice.py55 lines

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.The Inspector's first connection to localhost:8787/mcp fails with 401. What does that tell you?

  2. 2.Why did the Inspector find no way to sign in?

  3. 3.You decode a token and see org_id: acme. What have you proven?

  4. 4.After connecting, the Inspector's GET for a notification stream gets 405. Is something wrong?

  5. 5.search_contacts for "Dana" with acme's token returns 0 matches, though Dana Kim is in the database. What should you do?

  6. 6.Recordings and current traces disagree: one shows initialize / initialized, the other doesn't. Why?

Check your understanding

Try each question before opening its answer.

What does the first 401 help the MCP client discover?

The WWW-Authenticate header points to the protected-resource metadata, which names the authorization server; that server's metadata gives the token endpoint, the grant types and the signing keys.

Why can't the Inspector sign you in to the demo?

Its authorization server has no authorization_endpoint and supports only client_credentials: it issues tokens to registered clients, not browser users. Get a token with npm run token and add it as a header.

What can you learn by decoding a token, and what can't you?

You can read the tenant (org_id), scopes, audience, issuer and expiry. You can't learn whether it's genuine: a forged token decodes the same. The server's signature check decides that.

Is the 405 on GET /mcp a problem?

No. The server is stateless and only accepts POST. The handshake and tools/list succeeded; the 405 is only the optional notification stream.

Why don't current MCP traces show initialize / initialized?

The 2026-07-28 specification retired that exchange and protocol-level sessions; each request carries its protocol version and client info, with an optional server/discover. The demo still uses the handshake-era SDK.

What should you verify after a tool call succeeds?

The records themselves: they belong to the token's tenant. Then repeat with another tenant's token and check the result changes: Dana is 0 matches for acme and c_201 for globex.

Next up: Chapter 4 switches tokens on purpose: a read-only token on a write, another tenant's token, an expired one, and what each must return.

Learning something useful?

New labs, articles, and videos on evaluating and testing AI systems come out regularly. Follow along so you don't miss the next one.