MCP and Skill: How an Agent Grows Hands and Learns Routines
This is the 5th article in the Agent Full-Stack Development in Practice series. The series uses catbuddy to progressively deconstruct harness design. The previous article (04) thoroughly explained the core of 'hands and feet': how tools are registered and dispatched, and how file reads/writes avoid privilege escalation. But the dozen or so built-in tools are just a starting point—in real-world requirements, you'll always want to connect GitHub, Feishu, or some internal company service. This article discusses the other half of 'hands and feet': how to use a standard protocol (MCP) to connect the external tool ecosystem, and then use Skills to tell the Agent 'when to use these capabilities and how to combine them'. It's fine to read independently; knowing that 'Agents rely on tools to work' is enough to get started.
The tools are enough, but the Agent still 'can't do the job'
By the end of the previous article, catbuddy's Agent had grown hands and feet: it could read files, modify code, run commands, and search the web. Logically, everything should be smooth sailing.
But you'll quickly hit two walls.
The first wall: built-in tools are never enough. Today, the user wants to operate a GitHub repository; tomorrow, they want to write to a Feishu document; the day after, they want to query a database on the company intranet. You can't write a bunch of glue code in the harness, modify the core, and release a new version every time you connect an external service. This wall is dismantled by MCP—giving the Agent a standard socket where external capabilities can be plugged in and used immediately.
The second wall: knowing how to use tools ≠ knowing how to do a job. You give the Agent write_file, exec, web_search, and tell it to 'make a PPT'. It knows how to call each tool, but the entire script—'first understand the requirements, then search for materials, then generate page by page, and finally preview'—it has to figure out on its own. Figuring it out means instability. This wall is dismantled by Skill—giving the Agent a work manual that clearly states when, in what steps, and how to combine these tools.
In a nutshell, and the main thread of this article:
MCP provides the hands and feet (connecting capabilities), and Skill teaches the routines (orchestrating capabilities).
Let's break it down into two halves.
MCP: Don't Write Custom Glue for Every External Capability
1.1 A USB Analogy Suffices
MCP stands for Model Context Protocol, an open protocol proposed by Anthropic. The name is intimidating, but the core idea can be summed up in one sentence:
Don't write custom glue for every external capability; use a standard protocol for unified access.
Think about the world before USB: mice plugged into PS/2 ports, printers into parallel ports, USB drives into serial ports. Every new device required a different interface and a dedicated driver. After USB appeared, no matter what you plugged in, the physical interface and communication protocol were the same—plug and play.
MCP does the same thing. Whether you connect a file system, Shell, browser, or database to the Agent, the communication format is the same standard. The Agent side doesn't need to write a dedicated driver for each peripheral, and the external service side doesn't need to adapt to each Agent client.
The protocol itself defines only three core actions, simple enough that there's little to explain:
list_tools(): The Agent asks the server, 'What tools do you have?' The server returns the tool name + parameter definition (JSON Schema).call_tool(): The Agent says, 'Help me execute this tool with these parameters,' and the server executes and returns the result.tool_result: The server sends back the result, and the Agent feeds it back to the LLM to continue reasoning.
The complexity is all in the JSON Schema definition of the parameters; the interaction model is just these three steps.
We also hesitated initially: why not define our own tool interface? interface Tool { name, schema, execute } could be written in half a day. But 'defining our own' means tool developers would have to learn catbuddy's private specification, the hundreds of existing open-source MCP services would be unusable, and there would be no interoperability with other AI tools. Choosing MCP is essentially giving up the temptation to 'reinvent the wheel'—the value of accessing the entire ecosystem far exceeds those half-day labor costs.
1.2 McpManager: 'Translating' External Tools into Internal Tools
The protocol is dead; someone needs to run it inside the harness. That someone is McpManager. It does three things: manages the connection lifecycle, normalizes formats, and injects external tools into the registry.
First, let's see how it turns an external MCP service's tools into something that looks exactly like a built-in tool in the Agent's eyes:
In code, the key is two steps, both inside createMcpToolWrapper():
// tools/mcp.ts —— Wrapping an external MCP tool into an internal Tool
const name = sanitizeMcpName(`mcp_${serverName}_${toolDef.name}`) // ① Add mcp_ prefix + sanitize name
const parameters = normalizeSchemaForOpenai(toolDef.inputSchema) // ② JSON Schema normalization
return {
name,
definition: { type: 'function', function: { name, description, parameters } },
execute: (call) => executeMcpTool(client, originalName, name, call.arguments, timeout),
}
Two details are worth pausing on:
① mcp_{server}_{tool} prefix + sanitizeMcpName. The prefix ensures namespaces don't collide—if you connect two services both named search, they become mcp_brave_search and mcp_github_search, not fighting each other. sanitizeMcpName replaces illegal characters in the name (model APIs have character restrictions on tool names) uniformly with _, preventing an external service with a weird name from crashing the entire call round.
② normalizeSchemaForOpenai does format normalization. This is the most easily overlooked yet most practical dirty work in MCP integration. MCP's schema and the schema expected by various model APIs are not exactly the same—for example, 'nullable types' common in MCP might be written as type: ["string", "null"] or stuffed inside anyOf, which some providers don't recognize. This function flattens them into a unified form (extracting null into nullable: true, recursively processing nested properties and array items). The external tool's schema is normalized from MCP format to the internal format—after this step, the external tool truly looks the same as a built-in tool in the LLM's eyes.
After wrapping, a single registry.register() injects it into the ToolRegistry (the registration mechanism was covered in Article 04, reused directly here). External tools and built-in tools are then completely equal: when the Agent schedules, it doesn't care at all whether a tool is built into catbuddy or provided by some external MCP process—it only looks at name and description. This is the essence of the USB analogy: after unifying the interface, the upper layer is completely unaware of the underlying heterogeneity.
1.3 Connection Lifecycle: connect / reload / disconnect
External services aren't 'always there' like built-in tools—they are independent child processes that can fail to start, disconnect, or need hot reconnection. McpManager manages this lifecycle, exposing just three actions:
registerTools(registry): Called at startup. First registers a special toolmcp_reload(allowing the Agent itself to trigger a reconnection), then iterates through each MCP service in the config, starting child processes one by one,client.connect(),listTools(), wrapping and registering. If one service fails to connect, it doesn't drag down the whole system—failure info is collected intolastFailures, and other services work normally.reload(): When the config changes or a service crashes, no need to restart the entire App. It firstunregisterByPrefix('mcp_')to clean all old MCP tools from the registry, then reconnects everything. The returned message tells you3/4 server(s) connected, 27 tool(s) registered, and for those that couldn't connect, it attaches human-readable failure reasons (e.g., 'npx not in PATH', 'package name 404').disconnect(): Callsclient.close()one by one, exiting cleanly.
The beauty of this design is consistent with the main thread from Article 01: 'organs only recognize interfaces'. All the complexity of MCP (child processes, stdio, disconnection retries, schema differences) is locked inside the single file McpManager. The registry doesn't know, the Agent Loop doesn't know, the LLM doesn't know even more. Adding an external service requires zero changes to the core code—only the config changes.
1.4 Security: External Tools Follow the Same Boundaries
Giving AI hands and feet is scary enough; now you want to connect external tools of unknown origin—what about security?
The answer is reassuring: External tools enjoy no special privileges; they follow the same security boundaries as built-in tools. The several gates discussed in Article 04 apply equally to MCP tools. Here, we just point out where they are and what they block:
- File operations go through PathGuard. Any tool wanting to read or write files must first pass its path through
ctx.resolvePath()—directly rejected if it goes outside the workspace. Details were covered in Article 04, not repeated here. web-fetchblocks private IP ranges to prevent SSRF.web-fetch.tscallsvalidateUrlTarget()before fetching a URL, blocking all private addresses (10.x / 172.16.x / 192.168.x), loopback (127.x), and cloud metadata addresses (169.254.169.254, the credential endpoint for AWS/cloud servers). SSRF (Server-Side Request Forgery) is tricking the server into accessing internal network addresses it shouldn't—this gate is specifically to block that. Another detail: it re-validates on every redirect hop (callingvalidateUrlTargetin a loop insidefetchWithSafeRedirects), preventing DNS rebinding-style bypasses like 'return a legitimate address first, then 302 redirect to the intranet'.execblocks dangerous commands.exec.tsscans the command string with a regex before actual execution:rm -rf/format/dd/mkfs/:()(fork bomb) /chmod 777triggers an immediateError: dangerous command blocked, pluscontainsInternalUrlblocks intranet URLs. This isn't a perfect sandbox—an experienced person can always bypass string filtering—but it's the first defensive gate: preventing the LLM from casually executing destructive operations without knowing (you ask 'how to clear node_modules', and it might just spit outrm -rf /).
Just remember this principle: MCP expands the capability boundary, but not the permission boundary. External tools connected are still locked inside PathGuard and these several gates.
Skill: Tools Are Equipment, Skill Is the Instruction Manual
The hands and feet are ready, and the external ecosystem is connected. But the second wall mentioned at the beginning still stands: The Agent knowing how to use each tool doesn't mean it can combine them to complete a complex task.
2.1 The Gym Analogy
You go to the gym for the first time, and the coach gives you a bunch of equipment: barbells, dumbbells, cable machines, treadmills. You know how to use each one. But 'how to train biceps'—what to do first, what next, how many reps per set, how long to rest between sets? This requires an instruction manual.
- Tools (MCP) = Gym equipment. Answers 'what can be done': can read files, run commands, search the web.
- Skill = Instruction manual / Training plan. Answers 'when and how to combine this equipment': Need to make a PPT? First understand requirements → then search for materials → then generate page by page → finally preview and adjust.
In catbuddy, each Skill is a Markdown file (SKILL.md), containing three things:
- When to use—what scenario this skill solves;
- Tool combination steps—which tools to call in what order;
- Judgment rules—how to decide at a fork in the road.
Take the 'PPT generation' example, SKILL.md looks roughly like this:
# PPT Generation Skill
## Steps
1. Understand user requirements (topic, audience, number of slides)
2. Use web_search to collect relevant materials
3. Organize an outline, generate content slide by slide
4. Use python-pptx to generate the .pptx file
## Judgment Rules
- User says 'make a PPT' but doesn't specify slide count → ask first
- Content is a technical topic → default to adding an architecture diagram slide
With this manual, the Agent doesn't need to figure out 'how to make a PPT' on its own—every step, what to do if stuck, how to judge, is written clearly. The division of labor between Tools and Skills, summed up in a table:
| MCP Tools | Skill | |
|---|---|---|
| Granularity | Atomic operations | Complete workflows |
| Content | Function signature + Parameter Schema | When to use + Steps + Judgment rules |
| Defined by | Tool provider (catbuddy / 3rd party) | Skill author (community / you) |
| Analogy | Screwdriver, wrench | IKEA assembly instructions |
2.2 Four-Level Progressive Loading: 10 Skills Occupy Only 300 Tokens on Standby
This is the design point in the Skill system I most want to talk about.
You might think: Skills are so useful, why not just stuff the full text of all Skills into the system prompt? The Agent would know everything from the start, even saving the step of 'reading the manual'.
Don't. Do the math and you'll understand. Suppose you install 10 Skills, each SKILL.md averaging 2000 tokens. Dumping them all in is 20,000 tokens—occupying 20% of the standard context window. You haven't done anything yet, and one-fifth of the 'attention' is already eaten up by manuals that are completely useless at this moment. The more Skills, the worse it gets: 50 Skills is 100,000 tokens, directly blowing the window.
catbuddy's approach is called Four-Level Progressive Loading (Progressive Disclosure)—the core is one sentence: Summaries are resident, full text is on-demand. This logic is handled by SkillLoader (context/skill-loader.ts) within the ContextBuilder system:
Let's look level by level:
① Summary Layer (Resident). SkillLoader.buildSkillsSummary() condenses each available Skill into a single line - name: description, assembled into a list injected into the system prompt. 10 Skills is about ~300 tokens. The Agent thus knows what skills are available, but doesn't know the specifics of how to use each one—enough for it to judge.
② Always-on Full Text (Resident). Two skills are special: memory (cross-session memory) and my (self-check status/config). Their code names are hardcoded as ALWAYS_LOAD_SKILLS = ['memory', 'my']. These two are so fundamental, needed almost every round, so loadAlwaysSkills() keeps their complete full text resident too. If even these two required 'read the manual first', the user's first message would need an extra round trip.
③ Normal Skill Full Text (On-Demand). This is the cleverest layer. The full text of normal Skills is not in the system prompt—the Agent only knows it 'exists'. When the LLM judges 'this task needs a certain skill', it uses read_file itself to read that SKILL.md (corresponding to SkillLoader.readSkill()). For example, you ask 'What's the weather in Beijing tomorrow?', the Agent's reasoning chain is: sees weather in the summary → 'should use weather' → read_file to read its SKILL.md → follow the manual to fetch data → format output. Use one, read one, absolutely no prepayment.
④ Bundled Resources (On-Demand). A Skill folder isn't just SKILL.md; it can also contain scripts/ (executable scripts, the Agent runs directly with exec), references/ (reference docs, read when needed), assets/ (output templates). This makes a Skill not just 'a piece of prompt', but a true executable package.
The saved cost is very real: Standby cost is compressed from the full 20,000 tokens to about 300 tokens, and a single use only reads one extra 2000-token full text, saving about 90% overall. The Agent won't 'not know what to use' because of this—the summary is enough for it to judge; it just glances at the manual once more before actually starting work.
Here we only discuss the 'four-level progressive on-demand loading' branch of Skill. The complete five-layer assembly pipeline of ContextBuilder (how personality, guide files, layered memory, skill summaries, and tool lists are assembled layer by layer into the LLM's 'field of vision') is the main event of the next article, Article 06, so we'll leave it here for now.
2.3 Workspace-Level Skills: Customize the Agent per Project Without Touching Harness Code
Four-level loading solves 'token saving', but there's an even more brilliant capability: allowing each project to customize the Agent's working method without changing any harness code.
The mechanism is this: Skills have two sources—built-in (shipped with catbuddy), and workspace-level (placed in the project's .catbuddy/skills/ directory). When scanning, the workspace directory is placed before the built-in directory, and a seen set is used for deduplication:
In the logic of skill.ts's listDiscoverableSkills(), it's: first scanSkillsRoot(workspace/skills, ...), then scanSkillsRoot(builtinDir, ...), relying on seen.has(dir) to make a workspace Skill with the same name automatically override the built-in Skill. When readSkill() reads the full text, the workspace path is also first in line.
What does this mean? Your Project A and Project B can each place a code-review skill with the same name but different content in .catbuddy/skills/—Project A follows its standards, Project B follows its own, without interfering with each other. To customize a set of exclusive Agent working methods for a project, all you need to do is drop a few Markdown files into this project directory—no release, no core changes, not a single line of harness code touched. This is another concrete realization of the main thread from Article 01: 'changes locked within boundaries'.
2.4 Hot Reload: Takes Effect on the Next Message After Installation
By the way, a very satisfying point to use. Skill installation/enable/disable requires no restart, no new session, no 'refresh skill list' button—usable on the next message.
The secret is in SkillLoader.fingerprint(): it calculates the mtime (file modification time) of the Skill directory into a fingerprint, used as part of the key for the system prompt cache. You install a new Skill, the directory mtime changes → fingerprint changes → system prompt cache automatically invalidates → when the next message reassembles the context, the new Skill's summary is in. Disabling is the same: disabledSkills changes, fingerprint changes, cache invalidates. (How this mtime-based fingerprint caching makes the hot path zero I/O is also left for Article 06 to detail.)
Wrapping Up: Hands and Feet + Routines = An Agent That Can Work
Connecting these two articles (04 + 05), the 'hands and feet' organ is complete:
| MCP | Skill | |
|---|---|---|
| In a Nutshell | Provides hands and feet—connects external capabilities | Teaches routines—orchestrates capabilities into scripts |
| Problem Solved | Built-in tools are insufficient, don't want to write glue | Knowing how to use tools ≠ knowing how to do a job |
| Core Mechanism | Standard protocol + mcp_ prefix normalized injection | Four-level progressive loading, summaries resident, full text on-demand |
| Landing File | tools/mcp.ts's McpManager | context/skill-loader.ts's SkillLoader |
| Keyword | USB: Plug and play, upper layer unaware of heterogeneity | Gym: Equipment alone isn't enough; you need an instruction manual |
The model is responsible for thinking, the tools for execution, and the Skill tells the model 'after this step of thinking, which tool to call next'. Only when all three are assembled does the Agent grow from 'can chat, can do single operations' to 'can complete a complex task end-to-end'.
What did this article cover?
- MCP provides hands and feet: A standard protocol (Model Context Protocol) unifies access to external tools, plug-and-play like USB.
McpManagermanages connect/reload/disconnect, normalizes external schemas, addsmcp_prefix and injects intoToolRegistry, making built-in and external tools look exactly the same in the LLM's eyes—and still goes through PathGuard / SSRF / dangerous command interception, expanding capability but not permission. - Skill teaches routines: Tools answer 'what can be done', Skills answer 'when and how to combine'. Each Skill is a Markdown file containing 'when to use + steps + judgment rules'. Four-level progressive loading keeps summaries resident and full text on-demand—10 Skills occupy only about 300 tokens on standby, not 20,000.
- Workspace-level Skills (
.catbuddy/skills/) can automatically override built-in Skills with the same name, customizing exclusive Agent working methods for each project, without touching any harness code throughout the process.
Next Article Preview: The hands and feet are ready, but what exactly does the Agent 'see' each round? How is this field of vision—personality, memory, skill summaries, tool lists—assembled layer by layer, and how is the context prevented from exploding as the conversation grows longer? Article 06 discusses the 'eyes': the five-layer assembly + three-layer defense + precise token counting and self-healing of ContextBuilder.