The debate over MCP versus skills keeps resurfacing, and David’s post making the rounds on Hacker News captured 350+ points and nearly 300 comments. Having built a Discord bot that runs an MCP server in production, I have opinions about why MCP wins, and they are not the ones usually cited first.
Most advocates lead with portability. Write one MCP server, deploy it across Claude Code, Cursor, Windsurf, Zed, VS Code Copilot. That is a real advantage, and it compounds over time as more clients adopt the open protocol. But portability is a secondary benefit. The primary reason MCP beats prompt-injected skills for any non-trivial tool is simpler: the failure mode.
The Failure Mode Is the Design
Prompt-injected skills come in two common forms. The first is Markdown command files, like Claude Code’s .claude/commands/ system, which expand to prompt templates. The second is XML-tag parsing, where the model emits something like <skill name="remind" userId="123" text="ship it" /> inside its response, and the host application parses it with a regex.
Both approaches share a critical property: when the model gets the format wrong, the failure is silent. With XML-tag skills, a malformed tag is either silently dropped or breaks the parser. The model gets no feedback. It cannot retry with corrected arguments because it never learns that anything went wrong. From the model’s perspective, the skill was invoked; from the host’s perspective, it was not.
MCP is structurally different. Tools are called at inference time via tools/call, arguments are validated against a JSON Schema before reaching the handler, and the server returns an explicit error result on failure. The model sees that error and can revise. This converts an invisible failure into a recoverable one.
server.tool(
"remind",
{
userId: z.string().describe("Discord user ID to remind"),
text: z.string().describe("Reminder message text"),
delayMs: z.number().int().positive().describe("Delay in milliseconds"),
},
async ({ userId, text, delayMs }) => {
// Handler only executes if all three arguments pass validation
await scheduleReminder(userId, text, delayMs);
return { content: [{ type: "text", text: `Reminder set for ${delayMs}ms` }] };
}
);
The Zod schema above converts automatically to JSON Schema via the TypeScript MCP SDK. The client sends that schema to the model at the start of each session, so the model learns what valid arguments look like from the descriptions. If it calls the tool with a negative delayMs, validation fails, the error returns to the model, and it retries with a corrected value. None of that loop is possible with XML-tag parsing.
Inference-Time Execution Changes What Is Possible
The other structural difference is when execution happens. Markdown skills are prompt templates: any shell commands run before the model sees anything, all output is pre-staged. The model cannot branch on intermediate values because all branching decisions were made when the template was written.
MCP tools execute during inference. The model reasons about the available tools, decides to call one, receives structured output, and continues reasoning with that output in context. This makes chaining natural rather than pre-scripted:
- Call
github_prto fetch a pull request - Notice a failing check in the returned data
- Call
logsto retrieve the relevant CI output - Call
errorsto cross-reference known error patterns - Return a diagnostic summary
In a skills-based system, you would need to anticipate this exact sequence at template-write time and hardcode all four steps. With MCP, the model handles the branching. The template author does not have to be prescient about every possible path through a workflow.
Resources: The Primitive Nobody Uses
MCP defines three primitives: Tools, Resources, and Prompts. The community has largely collapsed everything into Tools and ignored the other two, which loses something meaningful.
Resources are readable data streams addressed by URI. A Resource can be subscribed to, and the server notifies the client when the underlying data changes. The semantic distinction matters: Resources carry no side effects by design. Tools are actions that may have real-world consequences.
For a Discord bot, the difference is concrete. A tool that reads the current server configuration and a tool that updates it should not look the same to the client. Resources make that distinction explicit in the protocol layer. A cautious client can surface a Resource to the user without risking unintended writes.
The subscription model also means live context without polling. An MCP-connected database can notify the client when the schema changes; a CLAUDE.md entry about the schema goes stale silently, with no signal to the model that its assumptions are now wrong.
Where Skills Genuinely Win
There is a narrower category where skills remain the right choice, and it is worth being precise about it.
Behavioral configuration, things like persona, output format, domain knowledge, and communication style, operates at a different layer than tool calls. A prompt-injected skill that says “always format code with explicit error types” or “this server is used for incident response, be terse” shapes model reasoning before any tool call happens. MCP tools cannot do this. They add capability; they do not shape the prior.
Simple personal shortcuts are also fine as skills. A Markdown command that generates a commit message or reformats a changelog does not need a running server process, schema validation, or error handling. The pre-scripted, one-shot nature is a feature. The operational cost of standing up an MCP server for something that simple is real: the server can crash, needs reconnection logic, needs versioning, needs logging.
For a solo project, that overhead is manageable. For teams wanting zero-maintenance tooling, it deserves honest accounting.
The Context Problem
MCP has one genuinely unglamorous problem: context passing. An out-of-process MCP server has no implicit access to request context. If your bot needs to know which Discord guild, channel, or user triggered a tool call, you have to inject that at spawn time.
The current idiom is environment variables:
{
"mcpServers": {
"ralph-skills": {
"command": "node",
"args": ["dist/mcp/server.js"],
"env": {
"RALPH_GUILD_ID": "...",
"RALPH_CHANNEL_ID": "...",
"RALPH_USER_ID": "..."
}
}
}
}
This works, but testing requires manually setting those variables. Standalone debugging is awkward. The server has to trust that the client populated the environment correctly, with no protocol-level validation that the values are present or sane. Skills advocates who cite context access as a reason to stay in-process have a point here.
For operations that genuinely require live platform objects, like setting a Discord nickname or managing reaction roles, in-process handlers remain necessary. An out-of-process MCP server cannot reach a live Guild object without a full additional IPC layer. The boundary is real, and designing around it adds complexity that does not always pay off.
The Hybrid Architecture
Most production agentic systems end up using both. MCP for capability tools that need schema validation, error recovery, and portability. Prompt-injected skills for behavioral configuration and persona. A small set of in-process handlers for platform-native operations that the protocol boundary cannot reach.
David’s post is arguing against a false choice, and the Hacker News thread has 300 comments largely because people are talking past each other on exactly this distinction. MCP and skills are not competing for the same slot in the architecture. MCP handles everything where the failure mode matters, where you need error recovery, where independent testability via the MCP Inspector is worth having, where portability across clients compounds over time. Skills handle the behavioral layer that tool calls cannot reach.
Once you separate capability from configuration, the preference stops being a preference. It becomes a question of which layer you are working in.