Claude Code Mastery — Direct Track
CC9 — Integrations Module 10 of 16
40 minIntermediate
← CC8: Tool Use Deep Dive 🏠 Home CC10: Claude Features →

CC9: MCP — Model Context Protocol

Wire GitHub, Postgres, Slack, Jira, Playwright, and your own custom tools into Claude Code as first-class capabilities. The protocol is open, the ecosystem is large, and the security model is least-privilege by default.

Learning Objectives

  • Explain MCP in one sentence and why it matters compared to baked-in tools.
  • Add an MCP server to a project via .mcp.json with stdio / http / sse transports.
  • Pick the right scope (project / user / managed) and approve servers without auto-trusting everything.
  • Decide between writing a skill vs adding an MCP server for a given capability.
  • Apply the four-point security checklist before connecting an MCP server in production.
  • Build your own MCP server in Python using FastMCP: define tools, resources, and prompts.
  • Inspect a server with the MCP Inspector and understand the JSON-RPC protocol underneath.

What MCP Is

Everyday Analogy

Think of how USB-C ended a war. Before USB-C, every device had its own connector: micro-USB for Android, Lightning for Apple, weird trapezoid for old cameras. Buying a new phone meant rebuying chargers. Now: one cable, every device. The cable doesn't know what it's charging; the device tells it.

MCP is USB-C for AI tools. Before MCP, integrating a new data source meant building one-off plugins for each AI tool: a Cursor plugin, a Copilot plugin, a Claude plugin. Now: build one MCP server, every AI tool that speaks the protocol can use it. Claude Code, Cursor, ChatGPT — same server.

Technical Definition

MCP (Model Context Protocol) is an open standard from Anthropic for connecting AI assistants to external tools and data. An MCP server exposes tools (functions Claude can call), resources (data Claude can read), and prompts (templates Claude can invoke). An MCP client (Claude Code, in our case) discovers what each server offers, surfaces it to the model, and routes calls.

Servers run as separate processes or HTTP endpoints. Three transports: stdio (subprocess via stdin/stdout, the default), http (POST to URL), sse (Server-Sent Events). Most local servers use stdio; remote servers use http or sse.

Adding Your First Server — GitHub in 30 Seconds

The GitHub MCP server gives Claude Code first-class access to issues, PRs, branches, file content, comments — anything gh can do but as structured tool calls, not shell parsing.

# 1. Get a GitHub token with the right scopes
$ gh auth refresh -s repo,read:org
$ gh auth token  # copy the token to a secure place

# 2. Drop a .mcp.json in your project root
$ cat > .mcp.json <<'EOF'
{
  "mcpServers": {
    "github": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_TOKEN" }
    }
  }
}
EOF

# 3. Export the token and start a session
$ export GITHUB_TOKEN=ghp_...
$ claude

Claude Code prompts the first time it sees a project-level .mcp.json: "Project wants to start GitHub MCP server — allow / always-allow / deny?". Pick "always" for trusted servers; "allow once" for testing. Now you can ask: "List my open PRs and summarize each in one line." Claude calls mcp__github__list_pull_requests — no shell needed.

Where the config lives

  • Project: .mcp.json at repo root — checked into git, team-shared.
  • User: under mcpServers in ~/.claude.json — personal, all your projects.
  • Managed: managed-mcp.json in admin-only directory — org-wide.

The Production Ecosystem — What's Out There

Anthropic and the community ship dozens of MCP servers. The ones you'll hit first:

ServerWhat it addsCommon use
githubIssues, PRs, branches, file content, comments"Find the bug from issue #1234, fix it, open PR linked to the issue"
filesystemSandboxed file ops outside the working dir"Read my Notes folder for related ideas"
postgresRead-only SQL via a connection string"What's the avg order value in the last 30 days?"
slackRead channels, post messages, search history"Summarize today's #incidents channel"
jiraRead/comment/transition tickets"Move ENG-1234 to In Progress and comment with the PR link"
playwrightDrive a real browser (click, type, screenshot, evaluate JS)"Take a screenshot of staging signup page; describe layout"
memoryLong-lived knowledge graph (entities + relations)"Remember that AcmeCorp uses our SSO via SAML"
gdriveGoogle Drive search and read"Find the Q3 planning doc and summarize it"
fetchHTTP fetch with HTML→markdown conversion"Read the docs at example.com/api and explain"
(your own)Whatever your team needsInternal API access, custom DB, deploy controls

Inline servers in subagent frontmatter

From CC6: subagents can declare their own scoped MCP servers. The Playwright pattern:

---
name: browser-tester
description: Test features in a real browser
mcpServers:
  - playwright:
      type: stdio
      command: npx
      args: ["-y", "@playwright/mcp@latest"]
---
Use Playwright tools to navigate, take screenshots, and verify UI behavior.

The Playwright server only exists while this subagent is alive — main session never sees it, doesn't pay context cost for tool descriptions.

Server Scoping — Project / User / Managed

MCP scoping is a separate question from where the .mcp.json file is. Three settings control it:

SettingWhat it doesUse when
enableAllProjectMcpServersAuto-approve every server in .mcp.json at startupPersonal trust in a project — never recommended for shared repos
enabledMcpjsonServersApprove only this list from .mcp.jsonSelectively trust specific servers in a repo
disabledMcpjsonServersBlock these from .mcp.jsonOverride a server you don't want despite team trusting it
allowedMcpServers (managed)Org-wide allowlist of approved server namesEnterprise — restrict what teams can run
deniedMcpServers (managed)Org-wide blocklist (takes precedence)Block a server everywhere, even if team adds it
allowManagedMcpServersOnly (managed)Only respect the managed allowlist; ignore project / user listsMost-locked-down enterprise config
First-launch trust prompt

When you start a session in a project that has a .mcp.json you haven't seen before, Claude Code prompts: "Project defines N MCP servers. Approve?" Pick servers individually — "approve all" is rarely the right answer for an unfamiliar repo. The settings keys above let you persist your decisions.

Skills vs MCP — The Decision

Skills (CC5) and MCP servers both add capability. They're not interchangeable. The decision tree:

QuestionSkillMCP
Mostly a prompt with shell substitution?YES — perfect for skillno
Needs persistent connection (DB, browser session)?noYES — MCP server
Reusable across many AI tools (Cursor, Copilot, Claude Code)?no — tool-specificYES — standard protocol
Should be in your repo, version-controlled?YES — checks into .claude/skills/config-only (server lives elsewhere)
Light wrapper around gh / git / npm?YES — skill with !\`gh ...\` shell injectionoverkill
Stateful long-running connection (Postgres, Redis)?noYES — MCP keeps the connection
Calling third-party REST API once-off?maybe (!\`curl ...\`)maybe (existing MCP server?)
Rule of thumb

If you're tempted to write an MCP server but it's a thin wrapper around shell commands you already have — write a skill instead. Skills are simpler, version with the repo, and don't add a process to manage. Reserve MCP for: stateful connections (DBs, browsers), capabilities other AI tools should also use (publish once, consume everywhere), and anything that genuinely needs the protocol's structured tool/resource/prompt model.

Security Checklist — Before You Connect

Four checks before adding any MCP server

1. Read the server's source. NPM-installable servers are arbitrary code running with your privileges. npm view @whatever/server, check the publisher, look at the code if it touches anything sensitive (credentials, network, fs).

2. Use the narrowest possible token. GitHub MCP wants a PAT? Generate one with only the repos and scopes it needs. Don't reuse your everyday token. Set expiration. Rotate.

3. Restrict tool surface with permission rules. An MCP server might expose 20 tools; you might only need 3. Add specific allows in permissions.allow and a broad deny: "deny": ["mcp__<server>__*"] first, then "allow": ["mcp__<server>__list_*", "mcp__<server>__read_*"] for the safe subset.

4. Audit via hooks. Add a PreToolUse hook with matcher: "mcp__.*" that logs every MCP call to your audit pipeline. Lets you verify what's actually being called and detect drift from expected behavior.

Common minefields

  • Postgres MCP with admin connection string. If a tool exists called execute_sql and your token can DROP TABLE … it can DROP TABLE. Use a read-only DB role.
  • Slack MCP with workspace-wide post permission. Either narrow the bot's channel access, or set permission rules to allow only list_* / read_*.
  • Filesystem MCP rooted at ~/. Now Claude can read your whole home folder — SSH keys, browser cookies, everything. Always root at a specific safe directory.
  • "Just trust it" trust prompts. Picking "always allow" on an unfamiliar project's .mcp.json means future commits to that file silently expand Claude's powers. Audit changes to .mcp.json in PRs.
What just happened?

You learned to plug Claude Code into the wider tool ecosystem without giving it the keys to the kingdom. The pattern: narrow tokens, narrow permissions, narrow scopes, audit via hooks. The same model holds for any future MCP server: trust through least privilege, not blanket allow.

Build Your Own MCP Server

Consuming MCP servers is half the story. The other half is writing one. The good news: with FastMCP (the Python convenience layer in the official MCP SDK), a useful server is ~30 lines.

Three things an MCP server can expose
  • Tools — functions Claude can call. Same shape as built-in CLI tools (CC8). Most servers are mostly tools.
  • Resources — read-only data Claude can fetch by URI (e.g. db://orders/123). Like a tool, but for stable lookups; Claude can pre-load them.
  • Prompts — reusable prompt templates the user can invoke (surfaced as slash-command-like options). Useful for "summarize ticket" kind of operations.

Minimal server — tools only

# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("ucc_lookup")

@mcp.tool()
def lookup_filing(po_number: str) -> dict:
    """Look up a UCC filing by purchase order number.

    Use whenever a user mentions a PO# or asks about filing status.
    Returns: {po_number, debtor, status, filed_at, lien_count}.
    """
    # Real impl would query Postgres; mock data here:
    return {
        "po_number": po_number, "debtor": "Acme LLC",
        "status": "active", "filed_at": "2024-03-12", "lien_count": 2,
    }

@mcp.tool()
def search_debtor(name: str, limit: int = 10) -> list[dict]:
    """Search filings by debtor name (case-insensitive substring)."""
    return [{"po_number": "PO-100001", "debtor": "Acme LLC"}]

if __name__ == "__main__":
    mcp.run()

That's it. Run with python server.py; the server speaks MCP over stdio by default. Register with Claude Code:

$ claude mcp add ucc-lookup --scope project -- python "$PWD/server.py"

Adding resources — URI-addressable data

Resources are content Claude can pull by URI. They don't take parameters; they're identified by their URI pattern.

@mcp.resource("filing://{po_number}")
def filing_resource(po_number: str) -> str:
    """Return the filing as JSON, addressable by URI."""
    f = lookup_filing(po_number)
    return json.dumps(f, indent=2)

@mcp.resource("schema://ucc")
def schema_resource() -> str:
    """The full UCC schema, returned as markdown."""
    return open("schema.md").read()

Use resources for content that's looked up rather than computed: schemas, glossaries, fixed configs, doc files. Tools call them via the URI; Claude Code surfaces them under /mcp in the CLI.

Adding prompts — reusable prompt templates

@mcp.prompt()
def filing_summary(po_number: str) -> str:
    """Generate a one-paragraph summary of a UCC filing."""
    return f"""Summarize filing {po_number} in 3 sentences:
1) who filed against whom, 2) when, 3) current status.
Tone: factual, no opinions. Cite the filing number."""

@mcp.prompt()
def risk_review(debtor: str) -> str:
    """Review a debtor's UCC risk profile."""
    return f"Pull all filings for <debtor>{debtor}</debtor>. Score lien risk 0-10. Justify."

Prompts appear in Claude Code's /mcp menu as named, parameterized templates. Useful for codifying recurring workflows so users don't re-type them.

Tools vs resources vs prompts — quick decision
  • Tools: compute, query, mutate. Has parameters. Claude decides when to call.
  • Resources: stable read-only content. URI-addressable. User pulls; Claude can pre-load.
  • Prompts: templates the user picks from a menu. Best for "do X with this data" operations.

Inspecting your server with the MCP Inspector

Before plugging into Claude Code, sanity-check with the official Inspector — a browser tool that connects to your server and lets you call tools, fetch resources, and use prompts in isolation.

$ npx @modelcontextprotocol/inspector python server.py
# opens http://localhost:5173 with a UI to your server
# — list tools, run them, inspect the JSON-RPC traffic.

If a tool misbehaves in Claude Code but works in the Inspector, the issue is in Claude's tool-selection (prompt the description better) not the server. If it fails in the Inspector too, it's your server logic.

What just happened?

You wrote an MCP server in < 50 lines that exposes tools, resources, and prompts. Anything you can write in Python can now become a Claude Code capability available across all your sessions. The MCP Inspector is to MCP what curl is to HTTP — debug everything there first.

Protocol Anatomy — What's Underneath FastMCP

FastMCP hides the wire format. For debugging, knowing what's actually flowing helps. MCP is JSON-RPC 2.0 over a transport (stdio, HTTP, or SSE). Three message kinds:

MethodPurposeDirection
initializeHandshake: protocol version, capabilitiesclient → server
tools/listList available tools (name, description, schema)client → server
tools/callInvoke a tool with argumentsclient → server
resources/list, resources/readList/read resourcesclient → server
prompts/list, prompts/getList prompts and instantiate oneclient → server
notifications/*Push messages from server (e.g. log entries)server → client

What a tool call looks like on the wire

// Claude Code -> server
{
  "jsonrpc": "2.0",
  "id": 17,
  "method": "tools/call",
  "params": {
    "name": "lookup_filing",
    "arguments": {"po_number": "PO-100001"}
  }
}

// server -> Claude Code
{
  "jsonrpc": "2.0",
  "id": 17,
  "result": {
    "content": [
      {"type": "text", "text": "{\"po_number\": \"PO-100001\", ...}"}
    ],
    "isError": false
  }
}

Implementing a custom client (when you'd want to)

You won't usually write an MCP client — Claude Code is one. But if you're building your own agent that should consume MCP servers, the SDK gives you a client too:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([t.name for t in tools.tools])
            r = await session.call_tool("lookup_filing", {"po_number": "PO-100001"})
            print(r.content)
Why care about the protocol?

Two reasons. First, debugging: when a tool misfires, you can trace the actual JSON-RPC and pinpoint whether Claude sent bad args or your server returned bad shape. Second, portability: an MCP server built for Claude Code works for any MCP-aware client — Claude Desktop, custom agents, third-party IDEs. You write once, the ecosystem benefits.

Lab 1 — Connect a Postgres MCP to the UCC Schema

Working in the PublicRecords API. The starter uses H2 in-memory; production uses Postgres. This lab spins up a local Postgres seeded with the same UCC data, registers the official Postgres MCP server with Claude Code, and asks Claude natural-language questions that get answered with real SQL. About 15 minutes (mostly waiting on Docker).

Prerequisites

  • Docker Desktop (or any Docker daemon) running.
  • Node 20+ (for npx, used to run the MCP server).
  • The data.sql file from the starter repo at src/main/resources/data.sql.

Step 1 — Spin up Postgres with the UCC schema

How it works

Production runs on Postgres; H2 was a dev convenience. We spin up a containerized Postgres on port 5433 (so it doesn't collide with anything you already have on 5432), then create the same filings table the JPA entity maps to. The Postgres MCP server will connect to this DB and let Claude query it via natural language.

Start the container:

docker run --name ucc-pg -e POSTGRES_PASSWORD=dev -p 5433:5432 -d postgres:16

Wait ~5 seconds for Postgres to accept connections, then create the schema by piping this SQL into psql:

SQL Schema
paste into: docker exec -i ucc-pg psql -U postgres
CREATE TABLE filings (
  id BIGSERIAL PRIMARY KEY,
  state CHAR(2) NOT NULL,
  debtor_name TEXT NOT NULL,
  secured_party TEXT NOT NULL,
  collateral_description TEXT,
  filed_at TIMESTAMPTZ NOT NULL,
  expires_at TIMESTAMPTZ
);

Then load the seed data from CC0:

docker exec -i ucc-pg psql -U postgres < src/main/resources/data.sql

Verify (should print 8):

docker exec -it ucc-pg psql -U postgres -c "SELECT count(*) FROM filings"

Step 2 — Register the Postgres MCP server (project scope)

Project scope means the config commits with the repo so every teammate gets the same connection automatically:

$ cd /path/to/PublicRecordsAPI
$ claude mcp add postgres-ucc --scope project \
    -- npx -y @modelcontextprotocol/server-postgres \
    "postgresql://postgres:dev@localhost:5433/postgres"

This writes to .mcp.json in the project root. Inspect it and notice the connection string is in plaintext — swap it for an env-var reference before committing (see Step 6).

Step 3 — Verify the server is up

> /mcp

The list should show postgres-ucc with status connected. If status is "failed", the most likely cause is the port (5433 vs 5432) or that npx couldn't reach npm. Run npx -y @modelcontextprotocol/server-postgres ... manually in a terminal to see the actual error.

Step 4 — Ask Claude in plain English

> Top 5 states by number of UCC filings, sorted descending.

> Which secured parties have filings expiring before 2028?
   List them with the debtor and expiration date.

> Find any filing where the collateral description mentions
   "equipment" but not a VIN or schedule reference.

Watch the tool ribbon — you'll see postgres-ucc.query calls with the SQL Claude generated. Read the SQL. If it does something you didn't intend (e.g., joins a table you'd rather it not touch), the MCP server has read access to everything; you'll want a read-only role next.

Step 5 — Lock down to a read-only role

The default postgres superuser is overkill. Create a read-only role and reconnect:

$ docker exec -i ucc-pg psql -U postgres <<'SQL'
CREATE ROLE claude_ro WITH LOGIN PASSWORD 'ro-only-pw';
GRANT CONNECT ON DATABASE postgres TO claude_ro;
GRANT USAGE ON SCHEMA public TO claude_ro;
GRANT SELECT ON filings TO claude_ro;
SQL

$ claude mcp remove postgres-ucc --scope project
$ claude mcp add postgres-ucc --scope project \
    -- npx -y @modelcontextprotocol/server-postgres \
    "postgresql://claude_ro:ro-only-pw@localhost:5433/postgres"

Now ask Claude to "insert a fake filing for testing" — the MCP query tool will fail with a permission error. That's the right outcome. Read-only by default, write access only when explicitly required.

Step 6 — Swap the password for an env-var reference

Plaintext credentials in .mcp.json get committed to git — bad. The ${VAR} syntax tells Claude Code to interpolate from your shell environment at startup. Replace the file contents with this:

JSON MCP Config
.mcp.json
{
  "mcpServers": {
    "postgres-ucc": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "${UCC_DB_URL}"]
    }
  }
}

Then export the variable in your shell (add to ~/.zshrc / ~/.bashrc / Windows env vars to persist):

export UCC_DB_URL=postgresql://claude_ro:ro-only-pw@localhost:5433/postgres

Restart Claude Code and verify /mcp still shows connected. Now you can commit .mcp.json safely — teammates supply their own credentials.

Step 7 — Cross-tool query: combine UCC data with code

The real value of MCP is asking questions that span data and code:

> Compare the columns in the filings table (via postgres-ucc) with
   the fields in @src/main/java/com/publicrecords/api/filing/Filing.java.
   Are they in sync? List any divergence.

Claude calls the MCP query tool to introspect the schema, reads the entity, and reports drift. This is the workflow CC14's CI integration extends.

Stretch — Add a second MCP for GitHub

Register the official GitHub MCP, scoped to read-only on this single repo, then ask Claude: "Find any open issues mentioning UCC schema changes." The principle is the same — narrow scope, narrow tokens, audit via the tool ribbon.

Lab complete — what you should have

A local Postgres seeded with UCC data, the Postgres MCP server connected via a read-only role, env-var-referenced credentials so .mcp.json is safe to commit, and a working pattern for asking Claude data questions in plain English while keeping the blast radius small. CC14 ships all of this through CI/CD.

Lab 2 — Debugging MCP Servers with the Inspector

You met the Inspector earlier — one terminal command and a paragraph. That's enough to know it exists; this lab is enough to make it part of your daily MCP-development workflow. About 20 minutes.

How it works

The MCP Inspector is the official, browser-based client for any MCP server. It speaks the same JSON-RPC protocol Claude Code does, so whatever the Inspector can talk to, Claude Code can too — and vice versa. The UI gives you what stdout-based debugging can't: tool-schema introspection, click-to-invoke forms, a live JSON-RPC traffic log, server-side notifications, and resource browsing. Think of it as Postman for MCP — the visual debugger the protocol always needed.

Prerequisites

  • The server.py FastMCP server you wrote earlier in this module (the one with lookup_filing, the filings://stats resource, and the summarize_filing prompt). If you skipped that, the code is in the “Build Your Own MCP Server” section above.
  • Node 20+ (the Inspector ships as an npm package).
  • A modern browser. The Inspector UI runs on http://localhost:6274 by default.

Step 1 — Launch the Inspector pointed at your server

One command. The Inspector spawns your MCP server as a subprocess over stdio, then opens a UI session in your browser.

npx @modelcontextprotocol/inspector python server.py

Output should look like:

Starting MCP inspector...
✓  Proxy server listening on 127.0.0.1:6277
🔗 Open inspector with token pre-filled:
   http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=abc123...
🔌 Connecting to server: python server.py

Open the pre-filled URL (it auto-opens in most environments). You'll land on the connection panel; pick stdio transport and click Connect. The header should turn green: “Connected to python server.py”.

If it doesn't connect

99% of failures are the subprocess crashing on startup — usually a Python ImportError or a stray print() that writes to stdout (which corrupts the JSON-RPC stream). The Inspector logs subprocess stderr in the Server Info tab. Run python server.py by itself once and confirm it produces zero stdout output before retrying.

Step 2 — Tour the five panels

The Inspector has five tabs across the top. Each maps directly to a JSON-RPC method group from the Protocol Anatomy section above:

TabMaps toWhat it's for
Toolstools/list · tools/callSee every tool's schema; click to invoke with form-filled args.
Resourcesresources/list · resources/readBrowse URI-addressable data your server exposes (e.g. filings://stats).
Promptsprompts/list · prompts/getInstantiate prompt templates with arg substitution.
Notificationsnotifications/*Server-pushed messages — log lines, progress updates.
Server InfoinitializeProtocol version, server name/version, declared capabilities, subprocess stderr.

Click Tools → you should see lookup_filing with its description and a form derived from the function's type hints. The same form Claude sees when it picks tools, rendered for human eyes.

Step 3 — Invoke a tool from the UI and read the wire trace

In the Tools tab, click lookup_filing. The form has one field, po_number. Enter PO-100001 and click Run Tool.

Two things appear:

  1. Tool Result panel: the structured response from your function.
  2. History / JSON-RPC pane at the bottom: the raw wire-format exchange.

Click on the request in the history pane to expand it:

// 12:42:18  → SEND
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "lookup_filing",
    "arguments": { "po_number": "PO-100001" }
  }
}

// 12:42:18  ← RECV
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "content": [
      { "type": "text", "text": "{\"po_number\": \"PO-100001\", \"state\": \"TX\", ...}" }
    ],
    "isError": false
  }
}

This is exactly what Claude Code sends and receives. If something looks off in Claude's behavior, this trace is the ground truth.

Step 4 — Trigger a deliberate failure and read the two error shapes

Now invoke lookup_filing with po_number = NOPE. The tool returns whatever your server does for “not found” — probably an empty object or an exception.

Look at the JSON-RPC response. Two failure shapes are common, and they mean different things to Claude:

ShapeJSON-RPC envelopeWhat Claude sees
Tool-level error "isError": true, content with error text “Tool ran and returned an error” — Claude can react, retry, or ask the user.
Protocol-level error "error": { "code": -32603, "message": "..." } “Tool is broken” — Claude treats this as fatal; no graceful recovery.

You want tool-level errors for expected failures (not found, invalid arg, rate limit). Reserve protocol-level errors for unrecoverable bugs. In FastMCP: return a structured dict with an error key → tool-level. raise Exception(...) → protocol-level. Knowing the difference is one of the easiest production wins you can ship.

Step 5 — Read resources and instantiate prompts

Switch to the Resources tab. Click filings://statsRead Resource. The response surfaces as raw text plus mime type. This is how Claude pulls in static-ish data via @filings://stats mentions inside Claude Code.

Switch to Prompts. Click summarize_filing; the Inspector renders a form for the template's parameters. Fill in filing_id=PO-100001 and click Get Prompt. The response is the substituted prompt text Claude would receive — useful when prompts misbehave in Claude Code and you need to verify what's actually being injected.

Step 6 — Watch server-side notifications (your observability channel)

Add a notification when a lookup runs. Modify the tool in server.py:

Python Server — add to lookup_filing
server.py
from fastmcp import Context

@mcp.tool
async def lookup_filing(ctx: Context, po_number: str) -> dict:
    await ctx.info(f"Looking up {po_number}")  # <-- server->client notification
    # ... existing lookup logic ...
    return {...}

Hit the toolbar's Restart Server button (or Cmd/Ctrl + R). Run the tool again. Switch to Notifications: you'll see notifications/message events with your log line. This is your production observability channel — everything you emit via ctx.info / ctx.warning / ctx.error flows to any MCP client. Claude Code surfaces these in /mcp logs; the Inspector surfaces them live.

Step 7 — Inspect the Postgres MCP server from Lab 1

The Inspector isn't just for servers you wrote. Point it at the official Postgres MCP you registered in Lab 1:

npx @modelcontextprotocol/inspector npx -y @modelcontextprotocol/server-postgres "postgresql://claude_ro:ro-only-pw@localhost:5433/postgres"

You'll see query in the Tools tab. Run SELECT state, count(*) FROM filings GROUP BY state through the form. The same SQL Claude would generate when you ask plain-English questions — now you can verify the tool works before wiring up Claude, isolating “my server is broken” from “Claude picked wrong args.”

Stretch — Drive the Inspector from CI

The Inspector ships a --cli mode that runs without a browser. Use it in CI to smoke-test MCP servers as part of your build:

$ npx @modelcontextprotocol/inspector --cli python server.py \
    --method tools/call \
    --tool-name lookup_filing \
    --tool-arg po_number=PO-100001
# exits 0 on tool-success, non-zero on protocol or tool errors — perfect for CI gates

Drop a step like this into the .github/workflows/mcp-smoke.yml alongside the patterns from CC14, and every PR is gated on a working MCP server contract.

Lab complete — what you can now do

Invoke any MCP server's tools, resources, and prompts from a browser UI. Read the raw JSON-RPC traffic to ground-truth what Claude sees. Distinguish tool-level from protocol-level errors and design your server to use them correctly. Surface server-side logs via notifications. Smoke-test MCP servers in CI without a browser. The Inspector is the missing visual debugger for the MCP development loop — treat any server failure as “did I check the Inspector first?” before chasing it through Claude Code logs.

Knowledge Check

1. Your team needs Claude Code to read live PostgreSQL data and post Slack messages. Skill or MCP?

A
Two skills — one calling psql, one calling Slack curl.
B
MCP — both need stateful connections; postgres-mcp + slack-mcp.
C
One skill calling both via shell.
D
Subagent only, no MCP.
Correct. Both need stateful connections (DB session, OAuth-aware Slack client). MCP is built for that. Skills suit thin shell wrappers, not persistent connections.
Look again. Stateful + reusable across tools = MCP. Skills are for prompt-shaped workflows.

2. You add a third-party MCP server. It exposes 30 tools but you only need 3 (read-only ones). How do you restrict the surface?

A
Edit the server's source to delete the other 27.
B
permissions.deny: ["mcp__server__*"] + allow: ["mcp__server__list_*", "mcp__server__read_*", "mcp__server__get_*"]
C
Set disabledMcpjsonServers: ["server"]
D
You can't — MCP is all-or-nothing.
Correct. Default-deny on the server prefix, then explicit allow for the safe subset. deny > allow precedence (CC4) means anything not in the allow list is blocked.
Look again. Use permissions.deny for default-deny on mcp__server__*, then allow the specific safe tools. Permission rules apply to MCP tools the same way they do to built-in tools.

3. .mcp.json at project root: who can read it?

A
Only you — gitignored by default.
B
The whole team — checked into git like any project file. New members get a trust prompt.
C
Only managed admins.
D
Anyone with read access on Anthropic's servers.
Correct. .mcp.json is a project file checked into git. Teammates get the trust prompt the first time. Don't put secrets in it — reference env vars ("$GITHUB_TOKEN").
Look again. .mcp.json lives in your repo and is shared via git. Treat it like any config file: no secrets, env-var references only.

4. Your subagent needs Playwright but you don't want Playwright tools cluttering your main session's context. What's the right pattern?

A
Add it to project .mcp.json.
B
Add it to user ~/.claude.json.
C
Inline in the subagent's frontmatter under mcpServers — scoped to that subagent only.
D
Spawn it as a child process from the subagent's prompt.
Correct. Inline server defs in subagent frontmatter exist only while that subagent runs. Main session never sees the tool descriptions, doesn't pay context cost.
Look again. The pattern from CC6: inline mcpServers in subagent frontmatter scopes the connection to that subagent only.

5. A teammate sets enableAllProjectMcpServers: true in their user-level settings. What's the risk?

A
None — trust prompts are annoying.
B
Any project they open auto-runs every server in its .mcp.json — including ones added in commits they haven't reviewed.
C
Only servers they've seen before run.
D
Just slows down session start.
Correct. Auto-approving everything bypasses the trust prompt — including future commits to .mcp.json. Stick with explicit per-project approval.
Look again. The trust prompt protects against silent expansion of Claude's powers via future .mcp.json commits. Auto-approving disables that protection across every project.

Module Summary

  • MCP = open protocol; same server works across Claude Code, Cursor, ChatGPT, custom tools.
  • Configure: project .mcp.json · user ~/.claude.json · managed managed-mcp.json.
  • Transports: stdio (subprocess, default), http, sse.
  • Production ecosystem: GitHub, Postgres, Slack, Jira, Filesystem, Playwright, Memory, Drive, Fetch.
  • Skill vs MCP: skill for prompt-shaped workflows + shell wrappers; MCP for stateful connections + cross-tool reuse.
  • Trust prompt on first project visit; enableAllProjectMcpServers bypasses it (don't).
  • Security: narrow tokens, restrict tool surface via permissions, audit via PreToolUse hooks, root filesystem servers carefully.
  • Inline mcpServers in subagent frontmatter for tools that should NOT pollute main context.