Claude Code Hands-On (4): MCP Servers, or How Claude Talks to Anything

MCP is the plug-in protocol that lets Claude Code reach beyond your filesystem. Install one (Playwright), see it work end-to-end, and learn the permission model that keeps it from going feral.

If you only learn one extension mechanism in Claude Code, learn MCP. It is the difference between an autocomplete and a platform.

The 60-second pitch

MCP is Model Context Protocol — a small open spec that lets Claude Code call tools and read resources from external servers. The “server” is any process that speaks MCP over stdio or HTTP. Claude treats MCP tools the same as its built-in tools: the model decides when to call them, you confirm or auto-approve, results come back as text.

This means Claude can:

  • Drive a browser (Playwright MCP)
  • Query a Postgres database (Postgres MCP)
  • Read your Notion or Obsidian vault
  • Talk to your internal API
  • Anything you wrap in a 200-line Node script

MCP servers are mostly community-built. There’s a catalog at mcp.so and a growing one inside Anthropic’s docs. The good ones are short, focused, and honest about what they do.

Install your first MCP server

Playwright is the canonical “first MCP” because it makes the value obvious. From your shell — not inside Claude Code:

1
claude mcp add playwright npx @playwright/mcp@latest

Three things happened:

  1. The command registered an MCP server named playwright.
  2. The “command to run it” is npx @playwright/mcp@latest — npx will fetch and run it on demand.
  3. The server is now in your machine-level config (~/.claude/settings.json).

Verify:

1
2
claude mcp list
# playwright    npx @playwright/mcp@latest    enabled

Open Claude Code:

1
2
claude
> can you open https://news.ycombinator.com and tell me the top 3 story titles?

The first time it runs you’ll get a permission prompt — “Claude wants to use the playwright tool.” Approve. The agent will fire up a headless browser, load the page, extract the titles, and return them. Total round-trip: a few seconds.

What just happened, technically

Claude Code  --(stdio)--> @playwright/mcp process
       |                        |
       |                        v
       |                   real browser
       |                        |
       <----- text -------------+

The MCP server is its own process. Claude Code talks to it over stdio (or HTTP, for remote servers). Tools are discovered at handshake — Claude asks “what can you do?” and the server replies with a list of tool schemas. From then on, the model can call those tools as if they were built-in.

The protocol is small enough that you can read the spec in 30 minutes. Building your own is a half-day project for a focused use case.

Permissions — the part you must not skip

Every MCP tool has a permission level. The first time the model calls one, you get a confirmation dialog. You have three options:

  • Allow once — for this call only
  • Allow for this session — until you /clear or quit
  • Always allow — written into .claude/settings.json

I do not recommend “always allow” for any MCP that mutates state. Read-only tools (search, fetch, query) — sure. Anything that writes files, sends messages, or hits an API with side effects — confirm every time, or scope the permission narrowly.

The right pattern in .claude/settings.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "permissions": {
    "allow": [
      "mcp__playwright__navigate",
      "mcp__playwright__extract_text"
    ],
    "deny": [
      "mcp__playwright__execute_javascript"
    ]
  }
}

This auto-allows the safe Playwright tools and outright blocks the one that runs arbitrary JS in the browser. Per-tool granularity.

Useful MCP servers I actually use

After trying ~30, the ones that earned a permanent spot:

ServerWhat it doesWhy it’s worth it
playwrightBrowser automationReplaces a dozen hand-rolled scrapers
postgresRead-only Postgres queries“Show me the rows” without leaving Claude
filesystemSandboxed file access outside the project rootRead configs in ~/.config/ etc.
slackSearch Slack history, post to channelsStandup summaries write themselves
githubIssue and PR access via GitHub APICleaner than CLI for some queries

The pattern: each does one thing well, ships as a single npm or PyPI package, and exposes 3-10 tools.

What MCP isn’t

A few things to be clear about:

  • Not a sandbox. An MCP server runs with your user permissions. A malicious server can do whatever you can do. Vet what you install.
  • Not a fast path. Each MCP tool call is a process roundtrip. Latency is real. Don’t use MCP for things you’d put in a hot loop.
  • Not the only way. For one-off integrations, a slash command that runs a shell script is sometimes the right answer. MCP is for things you’ll call repeatedly across sessions.

Building one (sketch)

If you want to build your own, the SDK is @modelcontextprotocol/sdk for Node or mcp for Python. The minimal server is ~50 lines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const server = new Server({ name: 'my-tools', version: '0.1' }, {
  capabilities: { tools: {} }
});

server.setRequestHandler('tools/list', async () => ({
  tools: [{
    name: 'echo',
    description: 'Echo back the input',
    inputSchema: { type: 'object', properties: { text: { type: 'string' } } }
  }]
}));

server.setRequestHandler('tools/call', async (req) => {
  if (req.params.name === 'echo') {
    return { content: [{ type: 'text', text: req.params.arguments.text }] };
  }
});

await server.connect(new StdioServerTransport());

That’s a working MCP server. Save it, register it (claude mcp add my-echo node my-server.js), restart, and Claude has a new tool. The hard part is choosing what tools to expose; the protocol gets out of the way.

Next piece: hooks — code that runs before and after every tool call. The defensive layer.

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub