· 5 min read ·

MCP Wins on Contracts, But Skills Win on Context

Source: hackernews

There is a post making the rounds on Hacker News where the author argues, again, that MCP is the better foundation for giving AI models access to capabilities. The “still” in the title matters: this is not a first impression. The author has lived with both approaches and keeps arriving at the same conclusion.

I have been building on top of this exact tension for months now, and I broadly agree. But the framing of “MCP vs. skills” obscures something important: the two things are not doing the same job, and the systems that treat them as substitutes for each other tend to make a mess of both.

What Each Thing Actually Is

MCP, the Model Context Protocol, is a standardized protocol for exposing tools to an AI model. The canonical transport is JSON-RPC 2.0 over stdio. You write a server, register tools with explicit schemas (via Zod in the TypeScript SDK, which converts to JSON Schema), and the client, whether that is Claude Desktop, Claude’s CLI, Cursor, or Cline, spawns your server as a child process and presents the tool list to the model at runtime. The model sees structured, machine-readable contracts. When it calls a tool, the arguments are validated against the schema before your handler ever touches them. The response is a structured result object, not free-form text.

Skills, as the term is used in Claude Code and in a lot of Discord bot codebases, covers two different things that often get conflated. The first is prompt injection: markdown or XML files that get prepended to the system prompt to shape the model’s behavior, persona, or knowledge. The second is a pattern where the model encodes intent as XML tags inside its response text, and a separate parsing layer in the host application reads those tags and actuates something. Both get called “skills.” They are quite different under the hood.

The Schema Argument Is Correct

The strongest argument for MCP is the schema argument, and it holds up in practice. When you expose a capability as an MCP tool, the model knows, before it decides to call the tool, exactly what arguments are required, what types they must be, and what the tool does. You write something like:

server.tool(
  "remind",
  "Schedule a reminder message for a user",
  {
    userId: z.string().describe("Discord user ID"),
    message: z.string().describe("Reminder content"),
    delayMinutes: z.number().int().positive().describe("Minutes from now"),
  },
  async (params) => {
    // params.userId, params.message, params.delayMinutes are typed and validated
    return { content: [{ type: "text", text: `Reminder set for ${params.delayMinutes} minutes.` }] };
  }
);

The XML-tag skill equivalent would be the model emitting something like <skill name="remind" userId="123" message="standup" delayMinutes="30"/> somewhere in its response text, and your application running a regex over the output to extract those attributes. When the model formats it slightly differently, or includes the tag mid-sentence in a way that confuses the regex, the action silently drops. There is no error for the model to see and retry. The failure mode is invisible.

MCP’s error path is explicit: the tool returns an error result, the model sees it, and it can attempt a corrected call. That loop is the difference between a system that degrades gracefully and one that fails silently.

The Portability Argument Is Also Correct

The other strong point in favor of MCP is portability. An MCP server you write for Claude’s CLI works without modification in Claude Desktop, in Cursor, in Windsurf, in any client that speaks the protocol. The ecosystem is growing fast. XML-tag skills, by contrast, are completely proprietary to the parsing layer you wrote in your specific application. They do not transfer anywhere.

If you are building something that might outlive the current host application, or something that other people should be able to integrate with their own tooling, MCP is the obvious choice. Writing a schema-defined tool once and having it available across the whole MCP ecosystem is a much better investment than encoding the same capability into bespoke regex parsing.

Where Skills Still Win

Here is where I diverge a bit from the pure MCP position. Prompt-injected skills, the behavioral and contextual kind, do something MCP cannot. They shape how the model thinks and speaks before any tool call happens. When you inject a CONTEXT.md that establishes a persona, defines output constraints, sets communication style, and provides domain knowledge, you are not adding a capability. You are configuring the model’s reasoning. No tool schema does that.

For Discord bots specifically, there is a second case where XML-tag skills remain necessary: anything that requires live access to the Discord runtime. An MCP server is a separate process. It does not have access to the Guild object, the GuildMember, or the live Client. If you need to set a user’s nickname, send an embed to a specific channel, or manage reaction roles, you cannot do that from an out-of-process MCP server without building an entire additional IPC layer back to the bot process. The practical answer is to keep a narrow set of XML-tag skills for Discord-native side effects and let MCP handle everything else.

This is not a failure of MCP’s design. It is a boundary question. MCP is designed for capabilities that can run in a separate process with access to external APIs and persistent storage. Discord-native operations that require the live runtime object are not that. The honest architectural response is a hybrid, not a replacement.

Context Passing Is the Unglamorous Problem

One thing neither the article nor most MCP discussions spend much time on is context passing. When Claude’s CLI spawns your MCP server as a child process, the server has no implicit access to the request context: which user triggered the invocation, which channel it came from, what guild it belongs to. You have to pass that context explicitly, and the current idiomatic answer is environment variables injected at spawn time.

const mcpEnv = {
  RALPH_GUILD_ID: message.guild?.id ?? "",
  RALPH_CHANNEL_ID: message.channelId,
  RALPH_USER_ID: message.author.id,
};

The server reads process.env.RALPH_GUILD_ID to scope every data operation to the correct context. It works, but it is inelegant, and it means your MCP server’s tools are implicitly stateful via environment variables that only exist inside a properly spawned invocation. Testing requires care. Running the server standalone for debugging requires manually setting those variables. This is not a dealbreaker, but it is the kind of friction that proponents of XML-tag skills point to when they argue for simpler approaches.

The Actual Trade-Off

MCP is the right foundation for any capability you want schema-validated, model-discoverable, independently testable, and portable across clients. The contract between the model and the tool is explicit, and the failure modes are visible. For anything that touches stateful data, external APIs, or persistent storage, MCP is simply better-engineered than regex parsing over model output.

Skills, in the prompt-injection sense, remain the right tool for behavioral configuration. They are not in competition with MCP tools; they operate on a different layer entirely. And for a narrow class of operations that require synchronous access to a runtime object the MCP server cannot reach, some form of in-process invocation pattern remains necessary.

The author of the original post is right that MCP wins on the merits for capability exposure. The caveat is that most real agentic systems end up with both: MCP for tools, prompt injection for behavior, and a small residual set of in-process handlers for whatever the architecture cannot cleanly delegate. Acknowledging that hybrid is more useful than arguing for a clean replacement.

Was this interesting?