---
title: "I was wrong about MCPs"
description: "I treated MCPs as service connectors. The useful shift is a capability layer: search, execute, and reusable primitives an agent can compose on its own."
author: "adam"
author_name: "Adam Gospodarczyk"
tags: ["agents","mcp"]
published_at: "2026-08-20T16:10:47.444Z"
canonical: "https://mega.dev/wrong-about-mcps"
---

MCP servers often focus on **integrating a single service**, giving the agent new tools and/or context. But what if we shift our perspective and break a few familiar patterns, turning MCP into a set of new capabilities the agent can use however it needs?

<Plate src="mcp-layer" alt="MCP as a connector reaching one service, next to MCP as a capability layer offering memory, tasks, email, storage, apps and packages." fig="FIG.01" width="wide" />

And to be clear, MCPs and CLIs built around a single service work perfectly well for many use cases. What follows is a different approach, one that reaches far beyond direct integrations. Whether or not you find it useful, it may change how you think about the programmer’s role in the AI era.

## Concept behind Model Context Protocol

If you haven't come across it yet: the Model Context Protocol (MCP) standardizes how AI agents, like ChatGPT, Claude, OpenCode, or Amp, connect to external systems. The protocol does more, but its core use case is simple: **delivering JSON Schemas that describe which tools an agent can select, and what arguments to call them with.**

<Plate src="tools-unified" alt="A transcript where one shared tool schema turns a plain question into a checked campaign answer." fig="FIG.02" width="wide" />

In practice, an existing API, say Resend's, can be translated into a set of tools optimized for AI agents, letting them send emails, manage contacts, or launch campaigns, for example.

Compared to the raw API, MCP tools provide context LLMs lack by default: descriptions and hints that increase the odds an action succeeds, or that the agent recovers when it doesn't.

Below is a scenario where the agent gets stuck using the raw API to send an email, due to a missing property. In the MCP scenario, it gets a clear hint on how to recover from the error.

<Plate src="mcp-vs-api" alt="Two stacked rails: with the direct API call the agent hits a bare missing audience_id error and the user is left to look the id up by hand, while through the Resend MCP the same error carries a hint to call list_audiences, so the agent recovers and the mail is delivered." fig="FIG.03" width="wide" />

When an agent uses a well-known service like GitHub, it already knows the interface. There's little value in additional context here. But with less popular services, the agent either fails and learns from errors, or has to find the latest docs first to learn how to use it.

For coding agents, CLI tools are often more convenient than MCPs: they have native access to bash and, through it, to the entire device they control. This works well for a single user and simple integrations, but the advantage fades the moment you go beyond a single-user context or move remote. **So the whole "MCPs vs CLIs" debate doesn't really make sense, we're talking about different use cases.**

Knowing all this, let's broaden the perspective and think about MCPs beyond integrations alone.

## Moving beyond integrating services

To explore what MCPs can really do, let's look at [Kody](https://github.com/kentcdodds/kody) a personal assistant built by [Kent C. Dodds](https://x.com/kentcdodds) (& contributors), that focuses on **generalized capabilities** rather than direct integrations.

Running on Cloudflare, Kody can remotely connect to any agentic harness that supports MCP, or work in the background, on a schedule or in response to events. In practice, the assistant is available wherever the user is: OpenAI Codex, Anthropic Claude Code, or any custom UI.

<Plate src="kody" alt="Kody drawn as one remote server, kody-mcp, with its two tools search and execute above the capability surface they reach: memory, storage, values and secrets it holds; packages, jobs and workflows it runs; integrations, email, MCP servers and connectors it reaches." fig="FIG.04" width="wide" />

At its core, the assistant comes down to just two tools:

- **search**: finds and/or loads Kody's capabilities.
- **execute**: runs TypeScript code inside Kody's runtime, with direct access to all its features.

Together, they let the agent explore available capabilities, including ones created during previous interactions, and use them within scripts. Sounds powerful. But why?

Well, ask a typical agent to "prepare a weekly email report" and it has to perform a whole series of actions to get there. And since LLMs are non-deterministic, the risk of something going wrong compounds with every additional step!

<Plate src="agent-report" alt="A weekly email report broken into four agent steps, the error chance climbing from 5% to 50% while the odds of a correct run fall from 95% to 28%, ending in a roughly 72% failure." fig="FIG.05" width="wide" />

Kody takes a different path. **It writes and tests a script that fetches all messages once, then saves it as a Package.** From then on, it simply reuses the Package. Since code is mostly deterministic, the results remain consistent unless the script calls an LLM, for example, to summarize the messages, or contains other logic that introduces randomness or produces unpredictable results.

Either way, this approach is far more reliable and efficient.

<Plate src="kody-tools" alt="A five step transcript of one weekly report request: the user asks in plain language, the agent calls search for summarize weekly emails, Kody returns a ranked shortlist with email_message_search at the top ahead of integrations, jobs, values and saved packages, the agent executes one TypeScript script that calls kody.email_message_search and summarizes the result, and the user gets a summary from a script now saved as a package for the next search to find." fig="FIG.06" width="wide" />

Code execution is just the tip of the iceberg. What makes this MCP special are its native components, and they're generalized in a way that goes well beyond a typical integration.

<Callout kind="insight" label="Beyond MCP">
What follows matters not just for MCP, but for working with AI agents in general.
</Callout>

So instead of thinking in terms of **every single feature**, we should elevate our thinking toward **system design and product engineering**. That shifts our focus away from details AI already handles with ease, toward the high-level concepts that shape the environment agents operate in. This won't come out of thin air, though: it requires a deep understanding of user context and possible use cases, far beyond a simple requirements doc describing a given feature.

Let’s consider a few examples here, by looking closer at Kody’s architecture.

### Packages

As mentioned, an assistant can either perform every action step by step each time, or write a script to save and reuse later.

In an example below, the user asks the agent for a podcast that highlights the most important news. The logic for it is then designed, tested and stored. It may be even shared with others! And from now on, whenever the users asks for the podcast, the agent simply picks the right package and executes it.

<Plate src="kody-packages" alt="A vertical timeline: a user asks for a daily podcast, the agent's search finds only raw web fetch and text to speech capabilities, so it prototypes the workflow and saves the working code as the versioned package @kent/daily-podcast; the next day the same request is answered by a single packages.invokeChecked call against that package." fig="FIG.07" width="wide" />

From a technical perspective:

- D1 Database keeps track of the package and its details.
- Cloudflare Artifacts holds the original, versioned source code.
- Cloudflare KV stores the package’s ready-to-run bundles.
- Cloudflare Workers provide an isolated environment in which the code runs.
- Cloudflare Durable Objects preserve data and support realtime sessions or long-running services when needed.

Packages are a clever way to leverage how good AI has become at programming, letting agents work smart instead of hard. I don’t need to mention this saves tons of tokens, meaning your money.

### Integrations

The way of connecting agents with external apps and services is another of Kody's components, and another example of how we can shift our programming mindset. Instead of building dedicated integrations with communicators, email, calendars, issue trackers, maps, or various devices, Kody provides generalized primitives for OAuth, API credentials, OpenAPI specifications, external MCP servers, and private-network connectors. Then it exposes their functionality to the "execute" tool's runtime.

This makes the agent independent enough that when it faces a task requiring an integration it doesn't have, it simply creates one, asking the user to log in or securely provide an API key.

The example below shows two scenarios:

- one where a missing integration stops the agent cold,
- and one where the agent configures a connection and builds reusable behavior around it, simply asking the user for help with authorization.

<Plate src="kody-integrations" alt="Two transcripts side by side: a dedicated-integration agent stops at I don't have a Linear integration and the user leaves the chat, while an agent that builds its own sends a secure connect link, verifies the connection, closes the stale issues with createAuthenticatedFetch, and saves the run as the linear-cleanup package." fig="FIG.08" width="wide" />

From a technical perspective, Kody owns the reusable authentication machinery, so the agent doesn't have to rebuild everything from scratch every time. Instead, it focuses on supplying the provider-specific configuration and deciding what to do with the resulting connection. The diagram below shows this in action.

<Plate src="kody-oauth" alt="A five-step transcript of Kody's OAuth handshake: the agent resolves the provider's URLs and scopes into a hosted connect link, the user enters the Client ID and Secret outside the chat, Kody runs state and PKCE and exchanges the returned code, stores config as a Value and tokens as encrypted host-scoped Secrets, and the agent proves the connection with one read-only GraphQL call." fig="FIG.09" width="wide" />

This is the reality we live in. Many of us remember when building an OAuth integration took hours. Now agents generate them on the fly, needing only a little steering. Realizing this is what moved Kent's assistant from one that needs integrations built by hand to one that builds them on its own!

And there's a bonus here: **the assistant generally improves automatically with every upgrade of the LLM it runs on**. And "the acceleration is accelerating", as you've probably noticed.

### Jobs and Workflows

Purely reactive agents, ones that only act when prompted, still have their uses, but they're starting to feel like a relic. Today, an agent shouldn't just respond to what we say - it should also act when we're not around.

With the Packages Kody can build, background tasks naturally come to mind. After all, once the code is written, it can run automatically, without involving the LLM at all. And since the Jobs and Workflows are Kody’s native capabilities, it can manage them on its own.

<Plate src="kody-scheduling" alt="A five-stage vertical pipeline: the agent supplies a generated inline module or a reused package export, Kody validates it into one bundle at definition time, a single control layer takes over with a Job's schedule, timezone, alarm and repeat or a Workflow's durable instance, runAt, retry and idempotency, the same isolated Worker executes that bundle with no model in the loop, and output, logs, saved state and run history land in the same places either way." fig="FIG.10" width="wide" />

From a technical perspective:

- **D1** tracks Job schedules, Workflow statuses, and executions
- **Artifacts** keep the source code for Jobs and Packages
- **JobManager Durable Objects** wake Kody up for scheduled Jobs
- **Workflows** manage durable runs, including waits and retries
- **Workers** execute Job packages and inline Workflow code
- **StorageRunner Durable Objects** preserve Job data between runs
- **After execution, D1** records the outcome and, for recurring Jobs, the next run time

Here's how Jobs and Workflows differ:

<Plate src="kody-workflows" alt="Two lifecycle columns: a JOB is defined with code and a one-time, interval or cron schedule, saved with an alarm set, then waits, wakes when the alarm fires, runs the stored code exactly once in its own Worker, reschedules the next alarm, and repeats until deleted or paused; a WORKFLOW is requested once with an idempotency key, validated for ownership and limits before anything starts, runs a single attempt whose status outlives the request, retries the same run automatically on a temporary failure, and completes by succeeding or failing for good." fig="FIG.11" width="wide" />

Jobs, in a nutshell, pair a one-time or recurring schedule with code to execute, either a standalone snippet or a package-owned job entry. They're great for automations like sending follow-ups or generating reports.

Workflows, in turn, rely on Cloudflare Workflows to manage individual durable executions. They come in handy when work needs to wait, continue beyond the original request, avoid duplicate runs, or retry after a failure, which makes them a natural fit for multi-step tasks.

### Memory

Agents need memory so they can act without being handed every detail each time. And while agentic memory remains an open problem, some promising strategies are already out there, like the [Quarq Agent](https://x.com/quarqlabs/status/2061571757488972153) and [Observational Memory](https://mastra.ai/blog/observational-memory) which can be combined and achieve impressive results.

Kody keeps it simpler: **a search tool** with **hybrid search** underneath, combining lexical matching and vector search, plus some filtering and re-ranking on top.

In practice, you can ask the agent to remember something, and it prepares a new entry candidate and compares it against existing records. Based on that, the agent decides whether to store a new memory or update an existing one.

<Plate src="kody-memory" alt="Two labelled pipelines side by side: REMEMBER runs candidate, verify against existing memories with a create, skip, update, replace or merge decision table, write into D1 and Vectorize, stored; RECALL runs memoryContext, hybrid search across both stores filtered by userId, rank, suppress by conversationId, surface." fig="FIG.12" width="wide" />

And since memory is just another capability, it combines easily with other actions, including those performed through Packages. Say we tell the assistant about our favorite places, later, we can simply ask for directions to that restaurant we enjoyed last time.

<Plate src="kody-recall" alt="A favorite ramen spot saved weeks earlier as a verified record in D1 and Vectorize, then matched by meaning in a fresh conversation and fed into a Maps package that returns the route." fig="FIG.13" width="wide" />

It’s worth noting that "agentic memory" doesn’t necessarily work like human memory. Depending on the implementation, an agent may not realize it knows something and, even when asked about it directly, may fail to retrieve the relevant memories. This can lead to confabulations or degraded performance.

### Values

Some code snippets, Package apps, Jobs, or Workflows need stable configuration that can be read again later. Kody provides the Values capability for storing small, readable settings within the context of a session, app, or user.

Unlike Memory, Values are retrieved deterministically by name rather than contextually by meaning or pattern matching. They are intended primarily for non-sensitive configuration, such as a time zone, selected workspace, report format, or default project.

For example, if a Package includes a UI, an app-scoped Value can preserve the user’s settings between sessions.

<Plate src="kody-values" alt="A five step vertical rail for Values: an agent calls value_set with a name, value, scope and description record, a Worker authorizes and applies userId filtering, D1 stores value_buckets and value_entries with no Vectorize or KV involved, value_get reads back by name and checks session first, then app, then user, and the most specific accessible Value returns, with session Values expiring after an hour." fig="FIG.14" width="wide" />

This simple primitive can support many useful scenarios. For example, a background Job could periodically update a user-scoped **currentCity** Value. A Package or agent could then retrieve it explicitly when answering a question such as, "Where can I find good food nearby?"

<Plate src="kody-snippets" alt="Two numbered steps: value_set stores a user-scoped reportTimezone, and a package later reads it back by name with a UTC fallback." fig="FIG.15" width="wide" />

Values are best suited to the latest small configuration value. Location history or a larger collection of records would belong in Storage instead.

### Storage

A Package, Job, service, app, or execution **may need its own private database for runtime state and accumulated records.**

Instead of representing this information as individual Values in Kody’s central D1 database, Kody assigns the runtime a StorageRunner Durable Object.

Technically, Kody Storage is a private, durable database tied to a single runtime identity. The Worker executing the code is temporary; the data it writes persists.

<Plate src="kody-storage" alt="Three runs of one Job calling storage.get and storage.set on one bucket: run one sets runCount to 1 and its Worker ends, run two reads 1 and sets 2, run three reads 2 and sets 3, all feeding one StorageRunner Durable Object that holds runCount and a report_history table, so tomorrow's run reads 3 from the same bucket while another user gets another object." fig="FIG.16" width="wide" />

### Remote connectors and external MCP servers

As mentioned, Kody is an MCP server that exposes two tools, **search and execute**, along with a set of capabilities. Agents acting as MCP clients can connect to Kody and interact with these tools. But Kody can also extend its capability registry with tools provided by external systems.

It supports two connection models:

- Kody can act as MCP Client itself and access external MCP Server tools through kody.mcp interface
- Local processes can reach Kody and expose their tools through kody.remote interface

<Plate src="kody-connectors" alt="Two columns comparing Kody's connection models: outbound, where Kody is the MCP client calling kody.mcp over HTTP to a public HTTPS server it authorizes with MCP OAuth and manages in the McpClientHub Durable Object; and inbound, where an external process dials in over WebSocket from behind NAT with an instance id and shared secret, runs its tools locally, and is held by a Remote Connector Session Durable Object. Both end with their tools joining the same search index." fig="FIG.17" width="wide" />

In both cases, the additional tools become part of the user’s isolated Kody environment. The agent can discover them through **search** and invoke them from **execute**, just like Kody’s built-in capabilities.

## Takeaway

Shifting the perspective from **a connector** to **a capability layer** makes the concept of an MCP server far more flexible and, in many ways, truly AI-native. Rather than viewing an agent’s capabilities through the lens of old programming habits, where everything had to be built by hand, Kent draws on nearly everything AI can do today to create an assistant that overcomes many of the limitations typical agents face. Most importantly, his assistant improves as the underlying models improve—something worth keeping in mind whenever we design for AI.

Here are a few points worth taking away:

- **Code mode:** Reasoning models can generate useful code. Rather than forcing every task through predefined tools, give agents a sandboxed runtime where appropriate. Dedicated tools still matter when stable interfaces, permissions, and auditability are required.
- **Determinism:** LLM behavior can vary, especially when sampling is used. Instead of trying to remove that variability entirely, use it during generation and evaluation. An agent can generate an implementation, test it, and revise it. Once validated, running that implementation is usually faster, cheaper, and more reliable than repeating the full reasoning process each time.
- **Generalization:** Instead of implementing every action ourselves, we can shape environments where agents create and execute task specific actions, while code enforces constraints and verifies outcomes deterministically.
- **Primitives:** Components designed around one narrow use case limit agents. Prefer small capabilities with clear contracts that can support several use cases without becoming vague or unsafe.
- **Composition:** Agents can compose primitives when their interfaces, permissions, and failure modes are explicit. Instead of building each workflow ourselves, we need to give agents reusable components they can safely assemble into new workflows.
- **Control:** Agents generating their own capabilities creates legitimate engineering risks. The answer is not to ignore them or stop exploring, but to constrain execution through sandboxing, least privilege, validation, observability, approval gates, and rollback.
- **Verification:** Deterministic execution does not guarantee a correct outcome. Verification must test explicit invariants, permissions, and expected effects. Where success cannot be formalized, use evaluation, human review, or bounded uncertainty.

That’s it. I was wrong about MCPs as I thought about them as connectors with some additional features that comes from the protocol itself, and that all the capability layer has to be tight directly to the Host.

I’m curious what you think about all this, so feel free to leave a comment or repost it if you find it useful.
