Chapter 7 of 9. An MCP server and a skill can both hand Claude a capability it did not have. Only one of them can be run at 07:00 by a launchd job with nobody watching. Prerequisite: Chapter 5, so you already know that a skill's
descriptionis its entire trigger surface and that firing it is a judgement the model makes.
Ask the question and you get the definition back. MCP connects Claude to external systems. Skills give Claude instructions and procedures. Both statements are true, and neither one helps the moment the capability you want is a Google API, because a Google API is unambiguously an external system and also perfectly reachable from a shell.
The estate this course documents currently holds 29 skills in ~/.claude/skills/ (20 of them symlinked in from the repos that own them) and exactly one MCP server added by hand at user scope. Four more arrive bundled inside plugins. That ratio was never a position anyone argued for. It fell out of two questions, asked in order, every time a new capability came up.
Everything below was checked against Claude Code 2.1.209 on 2026-07-14. This is a fast-moving surface and several of the claims are version-sensitive; where one is, I say so.
What an MCP server and a skill actually are, at the level that decides between them
The MCP specification defines the protocol as "an open protocol that enables seamless integration between LLM applications and external data sources and tools". Underneath, it is JSON-RPC 2.0 with capability negotiation and a lifecycle. The transport spec defines the two ways that traffic moves: stdio, where "the client launches the MCP server as a subprocess", and Streamable HTTP, where the server is a remote endpoint. The architecture overview names three participants. A host is the AI application. A server is the program that provides the context. Inside the host sits one client per connected server. An MCP server is a running program that Claude talks to over a wire.
A skill has no wire and no process. The Claude Code skills doc puts it flatly: "Create a SKILL.md file with instructions, and Claude adds it to its toolkit." The body loads into the calling agent's context when the model judges the description matches the task. It then runs at that agent's model with that agent's tools.
What muddies this is that a skill can carry a program too. The DVLAW skill in this estate ships a 90KB Python CLI and a 3.8MB SQLite database alongside its SKILL.md, and the roadmap skill ships another. So both mechanisms can do things. The difference is who launches the program and from where. SKILL.md states the arrangement itself:
python3 ~/.claude/skills/dvlaw/dvlaw.py <subcommand> [args]
The file's own gloss on that line: "The Python prints JSON. Claude reads that JSON and presents it to the user in a readable form." Determinism lives in the Python. Judgement lives in the markdown. Claude reaches the Python by running a shell command, exactly as you would.
An MCP server inverts that. The program is already running. Claude speaks to it as mcp__<server>__<tool> and no shell is involved at any point. Both questions below turn on that absent shell.
Question one: does anything other than Claude need to run this?
Four skills in this estate wrap a Google HTTP API. Search Console indexation, GA4 traffic, AdSense state, YouTube upload. Every one of them is a remote service with an OAuth flow and a JSON response, which is the exact silhouette of an MCP server. None of them is one.
Here is the layout of the Search Console one, with the directory name genericised:
~/.claude/skills/<search-console-telemetry>/
SKILL.md # when to reach for it, what it can answer
cli.py # the entry point
_oauth.py # the OAuth dance
_keychain.py # macOS Keychain wrapper
registry.json # which repos are wired up
plist-template.xml # a launchd job that runs cli.py on a schedule
Its SKILL.md frontmatter says where the credentials live: "OAuth refresh tokens live in macOS Keychain (no secrets in repo)". Seven subcommands, and one of them decides the architecture. install-cron writes a launchd plist so the daily digest arrives whether or not anybody is at the machine. This is the operative part of the template it writes:
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>__SKILL_DIR__/cli.py</string>
<string>report</string>
</array>
launchd fires that at a fixed hour. There is no terminal, no session, no model. Now try to imagine the same schedule against an MCP server. You cannot. The architecture doc says why: a tool is only callable through a client, and a client only exists inside the host. The host is the AI application. Remove the AI application and there is nothing left to make the call. Had that telemetry been built as an MCP server, the 07:00 digest would have required a Claude Code session to be open every morning at 07:00.
The bot registry from the two-bots-one-token article is the same shape, arrived at for a different reason. That registry exists because two services polled Telegram with the same token for weeks and collected 409 Conflict errors nobody read. The fix was a registry that made accidental token sharing require actively working around the system. The constitution's rule for reaching it is a Bash convention, not a tool:
# 1. Bot exists? → use it; fetch token via `cli.py token-get <name> --raw`
# 2. Bot doesn't exist? → `cli.py register --logical-name X --display-name "X"
# --prefix bbb --purpose "..."` then BotFather → `cli.py token-set X`
Its own summary of the read/write split is "The panel reads; the CLI writes." Launchd-scheduled pollers read the registry. So does the dashboard. Claude reads it too, through the same CLI, because there is no reason to give the model a private entrance to state that three non-model consumers already share.
Question two: will the thing on the other end survive being a subprocess?
Sometimes the answer is no, and then MCP is correct.
The constitution names chrome-devtools-mcp → all frontend debugging and context7 → before any library work, and both are MCP servers arriving via plugins. Browser debugging is the clean case. A debugging session is one live page held across dozens of calls: navigate, click, read the console, snapshot the DOM. The architecture overview gives the protocol-level version of that requirement: "MCP is a stateful protocol that requires lifecycle management." One caveat hangs off the phrase in the doc itself. A subset of MCP can be made stateless over the Streamable HTTP transport. Nothing on the shell side offers you either arrangement. A CLI subprocess exits after each invocation and takes the page with it, and call number two starts at a blank tab.
The single MCP server added by hand at user scope here is a stdio language-server bridge for one codebase, and it qualifies on the same test. A language server holds an indexed project in memory. Respawning it per query throws away the only thing it is for. Library documentation is the third pattern: a remote service, no meaningful local CLI, nothing on this machine to shell out to.
So the boundary emerged without anybody drawing it in advance. MCP won where the far end was a live process or a remote service with no adequate command-line front door. The skill won everywhere the capability was already a program that could stand up on its own.
What moving a capability into MCP costs you: your hooks stop seeing it
This is the part the tutorials skip, and it matters far more in a governed estate than a bare one.
Every blocking gate Claude Code itself enforces is a PreToolUse hook matched on a tool name. There are exactly two matchers registered under PreToolUse in ~/.claude/settings.json: Bash, carrying two hooks, and Write, carrying one. Nothing else. (Other gates in this estate run outside Claude Code entirely. A separate git-hooks repo, wired in through core.hooksPath, blocks commits at the pre-commit stage. Those gates fire on git commit whoever typed it. They know nothing about tool names.)
One of the two Bash hooks exists because of an incident on 2026-07-12. The first two paragraphs of its docstring, verbatim:
"""PreToolUse(Bash) — BLOCK synthetic keystrokes while the Mac is locked.
Why this is a hook and not a rule in a doc: on 2026-07-12 a keystroke drive ran while
the screen locked mid-session and typed a filename, a path and Return into the LOGIN
PASSWORD FIELD. The obvious check ("is Final Cut frontmost?") does NOT detect the lock —
FCP still reports frontmost underneath the login window. Only IOKit's
CGSSessionScreenIsLocked tells the truth.
That guard fires on Bash commands matching \bkeystroke\b|\bkey code\b|\bclick at\b|\bcliclick\b. It checks whether the screen is locked and denies the call if it is. Now repackage the same keystroke capability as an MCP server exposing a send_keystroke tool. The call arrives as mcp__<server>__send_keystroke. That name never reaches the Bash matcher. The lock check never runs, and the keystroke goes to whatever window has focus, including a login field.
Hooks can match MCP tools. The hooks reference and the MCP page both give the callable form mcp__<server>__<tool>, and that name is usable in a hook matcher exactly as Bash is. It also works in a subagent's tools list and in a permission rule. Nobody in this estate has written one. Every gate here assumes the dangerous thing arrives as a shell command, because until now it always has.
The hooks chapters of this course rest on a line the author wrote after a bad session: Agents are voluntary. Hooks are not. Make the non-negotiables hooks. Adding an MCP server puts a small crack in that. A hook written for Bash is not a hook for MCP, and the estate's enforcement layer currently has a shape that assumes every risky action passes through a shell.
The protocol is candid about where responsibility sits. The spec says "Tools represent arbitrary code execution and must be treated with appropriate caution", and that hosts "must obtain explicit user consent before invoking any tool". Then it concedes the limit of its own reach: "MCP itself cannot enforce these security principles at the protocol level." Claude Code's own MCP page adds that "Servers that fetch external content can expose you to prompt injection risk." Enforcement is the host's job. The host is Claude Code, and Claude Code's policy is whatever you put in your matchers.
The cost argument Anthropic makes against its own protocol
The skills doc states the economics of a skill body: "Unlike CLAUDE.md content, a skill's body loads only when it's used, so long reference material costs almost nothing until you need it." Nothing enters context until the skill fires.
An MCP server is not free in the same way. Every server you connect feeds a registry that the model can see; the architecture overview describes the AI application as combining the tools from all connected servers into one unified registry, and a registry has to be described to the model in tokens. Claude Code 2.1.209 defers MCP tool definitions by default, so only tool names enter context until a tool is actually used.
That default is where the version stamp starts earning its keep. The MCP page lists the places where the deferral switches itself off: on Google Cloud's Agent Platform it is disabled by default, and it stays disabled when ANTHROPIC_BASE_URL points at a non-first-party host. Tool search also needs a model that supports tool_reference blocks. Haiku models do not. Budget your context on the assumption that schemas are deferred, then run through a proxy or on Haiku, and every schema you had discounted is back in the window on every turn. ENABLE_TOOL_SEARCH is the switch that settles it rather than leaving you to assume.
Then the costs page says the quiet part in Anthropic's own voice: "Prefer CLI tools when available: Tools like gh, aws, gcloud, and sentry-cli are still more context-efficient than MCP servers because they don't add any per-tool listing."
The two questions, in the order to ask them
- Does anything other than Claude need this capability?
A cron, a CI job, a shell script, or you with the model switched off. If yes, build the CLI. The skill then becomes the paragraph that tells Claude when to reach for it and how to read the JSON that comes back. Four Google APIs in this estate went this way. The deciding factor each time was a launchd plist.
- Will the far end survive being a subprocess?
A live browser page, an indexed language server, a remote docs service with no local binary. If the state on the far side is the product, a per-call subprocess destroys it, and MCP is the right answer. This is where the browser-debugging server and the one hand-added language-server bridge earned their place.
- If neither question forces your hand, default to the skill.
It costs less context and it runs under the permission rules you already wrote. Every
PreToolUsegate you have built still sees the shell command underneath it. Anthropic's own cost guidance points the same way.
The transferable rule
Write the command line first, before you decide what wraps it. If the capability still stands up as a program you can run from a terminal with no model in the loop, it is a skill. The skill is then just the prose that tells Claude when to run it. Reach for MCP when the thing on the other end will not survive being a subprocess, and take the decision knowingly rather than because MCP is the word you had heard.
Then do the part almost nobody does. Open your hook matchers and check that they can still see the new surface. A capability that moves from Bash to mcp__server__tool has not become safer. It has moved out from under every gate you wrote for the old shape. Nothing announced that. It happened on the day you added the server.
The next chapter puts numbers on all of this. What a skill body costs, what an MCP tool listing costs, what a subagent's isolated context costs, and where the money in a long agentic session actually goes.
Package your next capability with the two questions
Take one capability you are about to add, or an MCP server you already run, and decide its packaging the way this estate did: write the command line first, ask the two questions in order, then open your hook matchers and check whether your gates can still see the result.
Expected behaviour
- A written verdict answering question one by naming every non-Claude consumer, such as a scheduler or a CI job, and question two by naming what state lives on the far end
- If question one said yes, the capability invoked from a terminal with no Claude session open, with output captured
- An inventory comparing your registered PreToolUse matchers against every mcp__ tool name reachable in your session, marking each call that no gate can see
- For any plugin-bundled server you gate, a matcher written against the full mcp__plugin_ namespaced form rather than the bare server key
PROVE IT Invoke one MCP tool your new matcher covers and paste the denial it produces. A matcher that has only ever appeared in the /hooks listing has never been seen to fire.
A keystroke capability moves from a Bash command into an MCP server exposing a send_keystroke tool. What happens to the PreToolUse lock-screen guard?
Which environment variable settles whether MCP tool schemas are deferred rather than loaded into context, instead of leaving you to assume?
Four skills in the estate wrap remote Google APIs, the exact silhouette of an MCP server, yet none of them is one. Explain what forced the skill-plus-CLI shape.
Show answer
The deciding consumer was launchd: a plist fires cli.py at a fixed hour with no session open and no model present. An MCP tool is only callable through a client, and a client only exists inside the host, so removing the AI application removes the caller entirely. Once the capability is a program on PATH for the scheduler, the skill is just the prose telling Claude when to run it and how to read the output.
↺ re-read: “Question one: does anything other than Claude need to run this?”