The Harness Within the Harness: A Portable Application Boundary for Coding Agents
Keep the workflow, swap the coding harness: using MCP as a portable boundary around bounded, durable agent applications
Feeling the pain
Are you getting coding harness fatigue? Wondering how you’re going to port those skills and scripts you spent so long crafting to the next big thing?
Yeah, me too.
Moving between harnesses has a way of making the differences painfully obvious. A skill may carry over. An agent definition may not. Plugins, hooks, commands, subagents and approval flows all have their own ideas about how they should work. Even when two harnesses support the same model, the behaviour surrounding that model can be completely different.
I had already run straight into this while moving between coding harnesses. Moving the files was the easy part. Preserving how the work actually ran was not.
The design goal became simple: Keep the workflow. Swap the harness.
The recent push toward open coding harnesses is a welcome part of the answer. An open, provider-neutral host makes the conversational layer more inspectable and replaceable. But it does not make an execution contract portable when that contract still lives in host-specific skills, plugins, hooks and approval flows. Switching open harnesses can still mean rebuilding how the work runs.
So I spent some time experimenting with a portable application boundary beneath the conversational host. To my surprise, I ended up with an architecture that offered more than portability:
- A portable behavioural core: One implementation behind an interface that multiple harnesses already understand.
- Cross-provider capabilities and cost control: Choose which model, if any, runs each part of a task.
- Bounded execution: Move along a spectrum from pure model-driven work to entirely deterministic software.
- Context control: Decide exactly what each inner model sees rather than handing it the entire parent conversation by default.
- Durable execution: Let long-running work survive a disconnected or restarted parent harness.
- Local execution: Reuse the authenticated tools already on my machine instead of copying long-lived credentials into a hosted runner.
Here comes the surprise: This isn’t some shiny new protocol that enables all of the above. It’s one that’s been here all along: Good old MCP.
Well, sort of.
MCP doesn’t magically provide model routing, durable state, deterministic workflows or sensible approval boundaries. What it provides is the seam. It gave me a narrow, typed interface between the agent talking to me and software executing on my behalf. The interesting part came from what I could put behind that seam.
One harness, two very different jobs
A coding harness is great at conversation. It has the broad context. It can ask follow-up questions, understand corrections, present options and carry opaque details between tool calls without making me think about them.
Execution has a different set of needs. It may require fixed ordering, bounded retries, model selection, durable state, cancellation, recovery and guarantees around side effects. Those concerns are usually easier to express and test in software than in a prompt.
The architectural shift was to stop treating those as one job.
In the approach I took, the coding agent still owns the conversation and the top-level tool-selection loop. An inner, portable harness owns the repeatable execution contract for the capability the coding agent invokes.
This is the practical extension of that question about control-loop ownership.
The single-layer harness
This is the most common approach today: The coding harness owns the conversation, interprets the skill, chooses the tools, orders the work and decides when it’s done. The tools may be deterministic, but the model owns the execution path. That’s exactly what I want for exploratory work and a fantastic way to get an idea working quickly.
The problems start when the prose becomes an operating contract.
If a skill specifies ordering, retries, approval and model selection, the model is acting as the runtime. Moving the skill moves the words, but not necessarily the behaviour. A script helps only with the part inside it; if the model still decides when to call it and what happens next, the larger contract remains in the conversation.
The multi-layer harness
So what if we keep the conversation exactly where it belongs, but move the repeatable execution contract into software?
That is what the proof of concept became. The parent coding harness connects to a local MCP server over stdio. From the parent’s perspective it gets a set of ordinary, typed tools. Behind those tools, the inner harness can call regular Python functions, run a checkpointed graph, invoke one or more models, connect to downstream MCP servers or call an API directly.
The parent does not need to know which mechanism a capability uses. More importantly, switching the parent does not require rebuilding that mechanism in the new harness’s native plugin or agent format. The new parent needs MCP configuration and, at most, a thin skill explaining how to use the tools well.
This isn’t a nested replacement for the coding agent. The parent still decides which capability to invoke and owns the interaction with the user. MCP is the boundary between that conversation and a substantial implementation with its own execution guarantees.
Local by design
I made one other choice deliberately: The inner harness runs locally over stdio. Remote execution isn’t a future milestone here. Local is the point.
That means the harness can use the same authenticated tooling I already use: Source-control, cloud and cluster CLIs, an SSH agent and whatever else is available in my local environment. I don’t have to copy long-lived credentials into a hosted runner or build a second credential-distribution system just to move execution somewhere else.
That authority cuts both ways. Local MCP is not a sandbox. The server runs with whatever permissions its process has, so the inner harness exposes narrow, allowlisted capabilities rather than handing a model unrestricted access to my environment. It also doesn’t mean all data stays on my machine. Model calls and downstream APIs still receive whatever I send them.
This is separate from the portability claim. MCP makes the conversational host replaceable; local execution decides where the application and its trust boundary live. When local authenticated tooling isn’t enough, the harness owns the downstream authentication itself. Either way, credentials belong to the local application rather than the parent conversation.
The operation lifecycle is the concrete seam
Pull-request review made the boundary tangible. A review graph fans out across reviewer models and perspectives, gives them bounded read-only source tools, deliberates over findings and aggregates the result. It can take long enough that holding one MCP request open is a bad idea, so the server exposes lifecycle tools instead.
From the parent harness, a representative invocation looks like this. The operation ID and outputs below are shortened to focus on the lifecycle:
User: Review this PR with independent quality and security perspectives.
Agent → start_pr_review(
pr_url="https://github.com/example-org/example-repo/pull/42",
models={
"quality": ["provider-a/reviewer"],
"security": ["provider-b/reviewer"]
}
)
← {"operation_id": "84d1…", "status": "queued"}
Agent → get_pr_review(operation_id="84d1…")
← {"status": "ready", "preview_ready": true, "commit_ready": true}
Agent → preview_pr_review(operation_id="84d1…")
← {"revision": 1, "payload_hash": "b12f…", "summary": "…", "comments": ["…"]}
Agent: Here is the exact review that is ready to publish. Post it?
User: Yes.
Agent → commit_pr_review(
operation_id="84d1…", revision=1, payload_hash="b12f…"
)
← {"status": "completed"}
That lifecycle is the application boundary in miniature. The parent carries an opaque operation ID and owns the approval conversation. The inner harness owns the fan-out, repository snapshot, budgets, checkpoints and exact payload that is eventually published. The operation survives independently of the chat session that started it.
The preview and commit split matters just as much as start and get. The
parent can show the user exactly what will change without gaining the authority
to quietly change it. A commit is accepted only for the previewed revision and
payload hash. Conversation stays flexible; the side-effect contract does not.
Keep the workflow, swap the harness
The proof of portability was deliberately boring: I moved the parent role from Claude Code to OpenCode without changing the review graph or lifecycle tools. Both hosts launched the same local server over stdio. Only their configuration shape and a thin layer of usage instructions changed.
Stripped of project-specific paths and options, the two host entries reduce to
this. Claude Code uses a project-level
.mcp.json:
{
"mcpServers": {
"portable-operations": {
"command": "portable-harness",
"args": ["serve"]
}
}
}
OpenCode expresses the same local server in
opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servers": {
"portable-operations": {
"type": "local",
"command": ["portable-harness", "serve"]
}
}
}
}
After the second host connected, it discovered the same tools and used the same
start, get, preview and commit lifecycle. The operation state did not
move into the new conversation. The models, checkpoints, validation and
side-effect policy did not move into host-specific files. The migration changed
the conversational shell, not the application.
That is the useful portability claim. It is not parity.
What MCP does not make portable
MCP gives both hosts a common way to discover typed capabilities, invoke them and receive results. It does not standardize everything around that exchange:
| Concern | What remains host- or application-specific |
|---|---|
| Context semantics | The host decides how it assembles conversation and workspace context. The inner application sees only what crosses the boundary or what it gathers through its own tools. |
| Approvals | Each host presents and remembers tool approvals differently. The application must still enforce its own preview, authorization and mutation rules. |
| Sandboxing | MCP does not define the containment, filesystem access, credentials or network authority of the code behind a tool. |
| Event models | Hosts differ in how they represent progress, cancellation, reconnects and long-running work, and in which protocol extensions they support. |
| Execution guarantees | Durability, retries, idempotency, recovery and side-effect guarantees belong to the implementation behind the seam. |
Installation, UI and surrounding context still feel different in each host. A thin host-specific skill can improve tool use, but it cannot be the source of the invariants I am trying to preserve. “Write once, works everywhere” would be overselling it. The narrower claim is: Preserve the execution contract while swapping the conversational shell.
How I got here
I didn’t sit down and design the final architecture in one shot. The useful parts emerged as I added use cases to an experimental harness and found new ways for my original assumptions to be wrong. It put the argument from Stop Asking LLMs to Be Deterministic behind an MCP boundary: Let software own the predictable parts and reserve model judgment for the places that need it.
Start simple: Drafting a work item
The first meaningful workflow drafted a work item. One model turns an informal request into structured fields. The calling agent presents them, gathers what’s missing and lets the user edit them through normal conversation. Separate tools then find valid parent items and owners, preview the complete work item without writing and revalidate it immediately before creation.
This was the first useful separation: The parent owns the interview; the inner harness owns the data contract and side effect. A workflow did not need to be one giant tool call. Small capabilities let the caller own the conversation without giving it ownership of the invariants.
Then it got long
The pull-request review introduced the lifecycle shown above. Fan-out and deliberation made a single long-held request increasingly fragile, while the possibility of publishing comments made a precise preview-and-commit contract more important. That was the point where the inner harness stopped feeling like a collection of tools and started feeling like an application.
Durable does not mean agentic
A long-running provisioning workflow broke another assumption. The external operation can take tens of minutes and needs persistence, cancellation and restart recovery—including the awkward point where a process may have started before its receipt was recorded.
It uses no model at all.
That distinction turned out to be important: Durability and agency are different dimensions. A deterministic job may need a checkpointed graph. An agentic task may be short enough to run as a normal request. Choosing a workflow engine does not mean every node suddenly needs an LLM.
A later two-stage data lookup reinforced the point: It is a durable search with deterministic query construction and result grouping. The conversational entrypoint does not make the work itself agentic.
Finally, put an agent inside the boundaries
An operational investigation tested the other end of the spectrum. This one really does need agentic behaviour: An investigator has to form hypotheses, choose evidence and adapt to what it finds. It follows the same broad division I use for runtime QA: Give the agent the investigative loop, while software and people retain the consequential decisions.
But “agentic” doesn’t have to mean “unbounded.”
The graph fixes the broad topology: investigate, validate, revise if necessary, then finish or return an inconclusive result. The investigator and validator have separate model configurations and explicit tool-call budgets, the number of revision rounds is capped and the entire operation has a deadline. The tools are allowlisted and read-only.
Even the final team-chat post sits outside the agentic loop. The server renders the exact message, hashes it and waits. Only a commit matching that revision and hash can publish it.
The output is still probabilistic. The operating envelope is not.
The execution spectrum
I originally split the project into tools, workflows and agents. That worked well enough for organizing code, but it wasn’t a particularly good mental model. The categories overlap:
- An MCP tool is an interface boundary, not an execution strategy.
- A workflow can contain deterministic functions, agentic nodes or both.
- An agent can be one bounded part of a larger deterministic graph.
- A deterministic workflow can be durable even though it never calls a model.
What emerged was a set of independent decisions:
| Question | One end | The other end |
|---|---|---|
| How much judgment is required? | Deterministic functions | Model-driven tool loops |
| How long does it live? | One request | Durable, recoverable operation |
| Can it change the outside world? | Read-only | Previewed and committed mutation |
| Who chooses the path? | Fixed topology | Agent-selected next action |
| What context does it need? | Explicit typed input | Bounded tools and model context |
This lets each problem use the least agentic mechanism that fits. It also means I can test the topology, state transitions, validation and side-effect policy with normal software tests, while keeping evals focused on the nodes where model judgment actually matters.
What the boundary buys
With the implementation in place, the benefits ended up being a little more specific than my original claims.
Cross-provider model routing and cost control
Because the inner harness creates the model calls, model selection becomes ordinary configuration. A task can use one model for investigation, another for independent validation and no model at all for deterministic work. A PR review can deliberately use models from different families instead of whatever the parent harness happens to provide.
MCP doesn’t provide this. It merely lets the parent call an implementation that does. The cost control comes from choosing where model judgment adds value, setting explicit limits there and keeping everything else out of the token meter.
Bounded execution
“Bounded” does not mean “deterministic.” A model can choose evidence and form hypotheses while software controls its tools, budget, runtime and authority. That makes the task governable, not predictable.
At the other end, a capability can be plain Python. A conversational entrypoint doesn’t make deterministic work agentic.
Full context control
The parent model has broad conversational and workspace context. An inner model does not inherit that context unless I explicitly pass it across the boundary. It receives the exact tools chosen for its role and deliberately constructed system and user prompts.
That control falls into two related disciplines: Prompt engineering defines how a model should behave during an invocation. Graph engineering decides when that invocation happens, which state becomes its context, which tools it receives and what happens next.
Prompt engineering: What the model knows
The system prompt describes how to operate: Role, safety boundaries, evidence standards and tool policy. The user prompt describes what to work on: The dossier, environment, time window, previous candidate and validator feedback. That separation keeps chat messages and logs as untrusted data below the harness-owned operating contract. It doesn’t eliminate prompt injection, but it avoids mixing evidence into the highest-authority instructions.
The system prompt doesn’t have to be static, either. The investigator combines a stable role, safety rules and observability guidance with the current round, remaining rounds and tool-call budget. The graph separately enforces the exploration window. The user prompt evolves independently, adding the prior candidate and validator objections when a revision is required.
Graph engineering: When and how the model runs
Dynamic system prompts become supremely useful because a long investigation is not in the same state after a validator rejection and a round of tool calls as it was at the start. The graph selects the investigator or validator, attaches the appropriate tools and budgets and decides whether another revision is possible. On the final round, it changes the operating contract. When exploration time runs out, it removes the tools and requires structured synthesis from the evidence already collected.
The graph also uses a purpose-built compaction prompt to preserve exact queries, results, failures, contradictions and unresolved checks as a durable investigation notebook. The model decides how to investigate; software decides when it runs, what it knows and which constraints apply right now. Those prompts and transitions can be generated from validated state and tested like the rest of the execution contract.
Durable execution through ordinary tools
The distinction between ephemeral and durable work was one of the more useful learnings from the experiment. A quick draft can return directly. A review, provisioning request or investigation needs an operation identity, persisted state and lifecycle calls.
I deliberately kept ordinary MCP tools as the baseline: start, get,
cancel, and, where a side effect is involved, preview and commit.
MCP Tasks can
provide a protocol-native lifecycle where both sides support the extension, but
the workflow does not depend on that support—or on keeping one request or one
conversation alive.
The part that isn’t free
Of course, moving the execution contract into software means owning that software.
The inner harness needs packaging, configuration, tests, observability, authentication, state storage, schema evolution and lifecycle management. It doesn’t automatically inherit the parent’s filesystem tools, model providers, credentials or MCP connections. If it needs those things, they have to be passed in or implemented behind its own boundary.
Beyond the authenticated local tools it can invoke directly, some capabilities need team-chat and observability systems exposed through downstream MCP servers. Their credentials aren’t passed through by the parent. The inner harness establishes its own OAuth sessions, stores and refreshes its own tokens and reports when an interactive browser authorization is required. Portability didn’t remove that complexity. It moved it into a component I could control and reuse.
This would be ridiculous for every one-off task. A short skill is still the right answer for fast-moving, low-consequence work. The multi-layer approach starts earning its keep when a capability is shared across harnesses, runs for a long time, has meaningful side effects, needs provider-specific routing or has an execution contract worth testing.
So, what did I actually build?
I started out trying to make my agent capabilities more portable. What I ended up building looked less like a collection of MCP tools and more like a local application with MCP as its front door.
The coding harness still does the part I want it to do: Talk to me, understand what I’m trying to accomplish, gather broad context and choose a capability. The inner harness does the part I want to be boring and repeatable: Enforce the contract, call models deliberately, persist state and control side effects.
MCP isn’t the application. It isn’t the control loop. It’s the narrow seam that lets both layers do the job they’re best suited for.
And when the next big harness pops up? I still expect to write some config and probably a thin skill. But I shouldn’t have to rebuild the application hiding behind it.
I’ll take that trade.
Comments