Runtime MCP
Each Session process can reach a Session-bound MCP server. Its tools create and control other Sessions with the current Session’s runtime authority. For when to use child or independent work, see Delegation.
ctx.mcp.getConnectionOptions(policy?)
// Available on InitContext and AgentContext.
const options: RuntimeMcpConnectionOptions = await ctx.mcp.getConnectionOptions()
// Request a narrower connection when injecting MCP into a native client.
const observation = await ctx.mcp.getConnectionOptions({
allow: [{ operation: "inspect_turn", agents: ["helper"] }],
})
interface RuntimeMcpConnectionOptions {
readonly url: string
readonly headers: Readonly<{ Authorization: string }>
}
Returns the connection for the current Session process: a local
http://127.0.0.1:<port>/mcp URL and a bearer Authorization header. Omitting
policy requests the Agent’s published runtimeMcp maximum. If that Agent field
is omitted, publication resolves a finite maximum using the exact Agent keys in
that Deployment and new Computer placement.
A supplied { allow: [...] } must be a subset of the maximum; { allow: [] }
grants no remote operations. There is no explicit unrestricted argument.
Equivalent policies return the same URL and header values in that process while
rechecking current authority. Distinct scopes have distinct credentials. A process
can retain its maximum and at most 15 distinct proper subsets; exceeding that limit
rejects without replacing existing descriptors. The server is provisioned lazily
on the first successful call. The accessor is available on
ctx.mcp in both init and turn, including Agents without init; calls
after process shutdown reject.
Normal hibernation preserves the connection options. Reconstruction starts a
new process and reruns init; obtain options from the new context. The return
types RuntimeMcpConnectionOptions, RuntimeMcpPolicy, and RuntimeMcpRule
are exported from @helmr/sdk. See Execution policies
for rule syntax and trust boundaries.
Pass the URL and headers to any MCP client that supports the Streamable HTTP transport, for example a model harness running inside the Computer:
import { agent } from "@helmr/sdk"
// `repoComputer` is an existing template() definition.
export const coordinator = agent({
id: "coordinator",
template: repoComputer,
async init(ctx) {
const { url, headers } = await ctx.mcp.getConnectionOptions()
return { helmrMcp: { url, headers } }
},
async turn(turn, ctx) {
// Configure the harness that runs this Turn with ctx.initResult.helmrMcp
// as a Streamable HTTP MCP server.
return { ok: true }
},
})
The header is a secret for this process. Do not log it or send it off the Computer.
Server behavior
| Property | Value |
|---|---|
| Transport | MCP Streamable HTTP, stateless, JSON responses |
| Server name | helmr-runtime |
| Request body | At most 1 MiB |
| Request timeout | 60 seconds |
| Concurrent requests | At most 64 per Session process; excess requests receive HTTP 503 with Retry-After: 1 |
Tool calls are not tied to a Turn. Closing the MCP connection, an HTTP
DELETE, or client disconnect never closes or cancels a Session. A
disconnect during a mutation does not undo it; reconcile with the same
idempotencyKey.
Successful tool results are a single text content item containing a JSON
object. Failed calls return an error whose text is code: message (see
Errors).
Runtime skill
The server’s initialization instructions point to helmr-runtime, a skill
that explains how to delegate work, observe outcomes, and control Sessions.
Agents can discover and read it with the skills tool over the same MCP
connection. There is no file to install in the harness’s working directory.
Clients decide whether to show server instructions to the model; the skills
tool description also explains how to retrieve the guidance.
The skill is bundled with the MCP server. Its content and the exposed tools reflect the connection’s scope. It explains workflows without granting permissions or replacing the user’s instructions. Reading it is subject to the same authentication and Session admission checks as other MCP requests, including holds and shutdown.
Authority
| Rule | Tools |
|---|---|
| Any admitted Session, including children | skills |
| Root Sessions only | list_agents, spawn, start |
| Owned descendants only | send_turn, interrupt_session, resume_session, close_session, cancel_session |
| Eligible Sessions in this Environment | enqueue |
| Sessions accessible to the caller | inspect_session, inspect_turn, wait_turn, list_sessions |
Every remote tool also requires the connection’s operation and exact target Agent
grant. list_agents derives its targets from start or spawn; list_sessions
uses inspect_session, and wait_turn uses inspect_turn. list_computers needs
its own targetless rule. Current Role, origin, relationship, lifecycle and Computer
checks still apply. Creation grants add no implicit observation or control grants.
The receiver enforces these grants during admission and before returning a retained
receipt. Retrying creation with omitted computerId still requests new placement,
even if its original receipt already identifies an allocated Computer. Admitted
work retains its fixed target and can continue after the caller process is rebuilt.
A narrower connection scopes requests made with that credential. It does not isolate native clients or shell code sharing the same Computer: code able to access the Session’s runtime channels may obtain the Agent maximum. Use separate Computers for separate trust domains.
Child Sessions cannot create work or ask questions; they report those needs to their owner.
Tools
Text-part arrays (input, message) are [{ "type": "text", "text": "..." }]
with at most 64 parts. Mutating tools require an idempotencyKey; reuse the
same key with identical arguments when an outcome is uncertain.
skills
List the available runtime skills or read one in full. This read-only tool runs no Session operation and needs no active Turn or idempotency key.
| Input | Type | Description |
|---|---|---|
name |
string | Optional. Omit to list skills; provide an exact listed name to read its SKILL.md. |
skills({}) returns:
{
"skills": [
{ "name": "helmr-runtime", "description": "…" }
]
}
skills({ "name": "helmr-runtime" }) returns:
{
"name": "helmr-runtime",
"description": "…",
"content": "---\nname: helmr-runtime\n…"
}
content contains the connection’s Markdown guidance. Unrestricted connections
receive the complete bundled document; finite connections receive only guidance
for their granted tools.
An unknown name returns isError: true with an unknown_skill error. Names
are catalog identifiers, not paths or URLs. The catalog currently has one
skill; listing and reading do not require pagination or additional resources.
list_agents
List Agent definitions that spawn or start can target.
| Input | Type | Description |
|---|---|---|
operation |
"spawn" | "start" |
Required. Both list the caller’s pinned Deployment. |
cursor |
string | Optional continuation cursor. |
Result: { "deploymentId": string, "agents": [{ "agent": string }], "nextCursor"?: string }.
Pages hold up to 50 definitions, filtered by the connection’s creation grants
before pagination. A denied creation family has no candidates.
list_computers
List Computers visible to this Session.
| Input | Type | Description |
|---|---|---|
cursor |
string | Optional continuation cursor. |
Result: { "computers": [Computer], "next_cursor"?: string }. Each Computer has
id, key?, template, deployment_id, status, residency, error?,
delete_after_unused_ms, unused_since, created_at and
updated_at. A finite creation rule with computer: "caller" permits only the
caller’s own logical Computer ID. Other Computers cannot be selected for runtime
creation. Discovery is limited to the caller’s workload and permitted relations.
spawn
Create child work owned by the caller, using the caller’s pinned Deployment.
start
Create independent work, using the caller’s pinned Deployment. The new Session records the caller as its requester but is not owned by it.
spawn and start take the same input:
| Input | Type | Description |
|---|---|---|
agent |
string | Required. Agent name from list_agents. |
input |
text-part array | Required. Initial Turn input, delivered unchanged. |
idempotencyKey |
string | Required. |
computerId |
string | Optional. Omit for new placement. Under a finite scope, an explicit ID must be the caller’s Computer and requires a computer: "caller" rule. Unrestricted scopes retain ordinary existing-Computer authorization. |
Result: { "sessionId", "turnId", "sequence" }.
Runtime creation has no Session key. The new Session’s idle periods come from
the target Agent definition.
enqueue
Admit another Turn in an existing Session in this Environment. Existing holds remain in force.
| Input | Type | Description |
|---|---|---|
sessionId |
string | Required. |
input |
text-part array | Required. |
idempotencyKey |
string | Required. |
Result: { "sessionId", "turnId", "sequence" }.
send_turn
Send a message to an owned descendant’s exact active Turn. It is never enqueued as a fallback. A result confirms admission, not delivery to the handler.
| Input | Type | Description |
|---|---|---|
sessionId |
string | Required. |
turnId |
string | Required. |
message |
text-part array | Required. |
idempotencyKey |
string | Required. |
Result: { "id", "turnId" }, where id is the message ID.
interrupt_turn
Interrupt exactly one running Turn in an owned descendant Session. Takes required
sessionId, turnId, and idempotencyKey. Returns
{ id, sessionId, turnId, status: "accepted" }. It creates no hold and does not
interrupt the target Session’s children. Queued and settled Turns
return turn_not_interruptible, with the target status and rejection reason.
Acceptance does not prove the handler has stopped; dispatch waits for stop
acknowledgement. Failure to stop isolates the Session for explicit recovery.
interrupt_session, close_session, cancel_session
Control an owned descendant and its subtree.
| Tool | Effect |
|---|---|
interrupt_session |
Places a hold and requests interruption. A result is not proof that execution has stopped. |
close_session |
Closes admission while work that can run drains. Held queued work and pending questions are cancelled. |
cancel_session |
Cancels pending work, requests that active work stop and closes the subtree. |
| Input | Type | Description |
|---|---|---|
sessionId |
string | Required. |
idempotencyKey |
string | Required. |
interrupt_session returns { "id", "sessionId", "status": "accepted", "holdId" }.
close_session and cancel_session return { "sessionId", "status" } with the
Session status after the call: closing or closed.
resume_session
Release a hold on an owned descendant. Post the hold’s id to its
session_id; inspect_session reports can_release for each hold. Of
interrupts, only those this Session placed can be released this way; storage
pressure holds cannot. Interrupted input is not replayed.
| Input | Type | Description |
|---|---|---|
sessionId |
string | Required. The held Session, the hold’s session_id. |
holdId |
string | Required. The exact hold to release. |
idempotencyKey |
string | Required. |
Result: { "id", "sessionId", "status": "accepted", "holdId" }.
inspect_session
| Input | Type | Description |
|---|---|---|
sessionId |
string | Required. |
Result: one Session with id, agent, template, deployment_id, computer_id,
root_session_id, parent_session_id, requester_session_id, initial_turn
({ id, status } or null), key?, slack_channel_id?, status,
close_after_idle_ms, idle_since, holds and created_at. Each hold is
{ id, session_id, kind, created_at, recovery_gap?, can_release }. Inspection
does not grant access to the Session’s history.
list_sessions
Recover owned or requested Sessions, including terminal ones, with their initial Turn identities.
| Input | Type | Description |
|---|---|---|
relation |
"owned" | "requested" |
Required. owned lists direct children; requested also includes independent work this Session started. |
status |
string array | Optional filter: open, closing, closed. Omit for all. |
limit |
integer | Optional, 1–100. Default 50. |
cursor |
string | Optional continuation cursor. |
Result: { "sessions": [Session], "next_cursor"?: string }, with Sessions in
the inspect_session shape.
inspect_turn
| Input | Type | Description |
|---|---|---|
sessionId |
string | Required. |
turnId |
string | Required. |
Result: { "id", "sessionId", "status", "waitBlocked"?, "result"?, "response"?, "error"?, "payloadExpiredAt"? }.
status is one of queued, running, completed, failed,
interrupted, cancelled. error is { code, message? }.
wait_turn
Wait a bounded time for a Turn to settle. Repeatable; a timeout does not cancel the Turn.
| Input | Type | Description |
|---|---|---|
sessionId |
string | Required. |
turnId |
string | Required. |
timeoutMs |
integer | Required, 1–30000. |
Result: { "status": "timeout" }, or
{ "status": "settled", "outcome": { "status", "result"?, "response"?, "error"?, "payloadExpiredAt"? } }.
If the Turn is queued only because it needs the Computer allocation the caller
is holding, wait_turn fails with capacity_wait_blocked instead of waiting.
Errors
Validation failures name the missing or invalid argument. Admission failures carry one of these codes:
| Code | Meaning |
|---|---|
invalid_arguments |
Arguments are malformed or reference an invalid identity. |
invalid_runtime_mcp_policy |
The getter policy is malformed or names a key outside the caller’s pinned manifest. |
runtime_mcp_denied |
The requested scope exceeds the maximum, or the current descriptor does not authorize the operation, Agent or placement. |
runtime_mcp_scope_limit |
This process already retains its maximum and 15 distinct proper subsets. |
root_session_required |
The operation requires a root Session. |
target_not_published |
The target Agent is not in the selected Deployment. |
idempotency_conflict |
The key was reused with different arguments. |
authority_changed |
The caller no longer has authority for the target. |
admission_unavailable |
Admission is temporarily unavailable. |
message_closed |
The target Turn no longer accepts messages. |
content_kind_unsupported, content_limit_exceeded |
Input contains an unsupported part or exceeds a limit. |
channel_unavailable, start_message_too_large, source_conversation_pending, source_conversation_unavailable, conversation_changed, target_workspace_mismatch |
Slack-related admission failures. See Slack. |
When an outcome is uncertain, retry with the original arguments and
idempotencyKey, then inspect the target.