Skip to main content

Three channels

Let the Agent call your Mini App: managed MCP

Each MCP server uses exactly one lifecycle:
  • Ordinary plugin MCP: runs independently (for example over stdio) without the Mini App’s Node service. Don’t list it in mcpEndpoints.
  • Mini App-managed MCP: implemented by the Mini App’s Node service and available only while it runs. Use it when tools should share business state with the page.
For managed MCP, first declare a Streamable HTTP server in the plugin’s *.mcp.json, using this fixed placeholder url that the host rewrites after publishing:
servers.mcp.json
Then mount it on a Node service path in miniapp.json, and reference servers.mcp.json from mcpServers in plugin.json:
miniapp/miniapp.json
  • Before start returns, the Node entry must implement Streamable HTTP MCP at that path on context.listen. The host rewrites only this server’s address; tool discovery, ordering, calls, and errors stay with the plugin MCP runtime.
  • When the user sends a message that references the Mini App, the host starts the service, connects MCP, and confirms the tools are available before calling the model. If a required managed MCP is unavailable, the request fails instead of letting the model continue without its tools.
  • Keep tools/list light: return tool definitions only, without importing data, scanning documents, or calling remote services. Put business initialization in the tool calls that need it.
  • A broken MCP handler doesn’t affect the page, but the Agent can’t use those tools until you fix it and publish again.

Hand off to the message input

Use this when you prepare context and let the user decide whether to send it; see message.append. If the Agent should change tables, cards, or other business objects, give it MCP tools and use stable business IDs in requests and tools. Users can edit or delete the text in the input, so it can’t serve as a correlation ID between a request and a turn.

Agent API

A Mini App can start Agent turns directly from the page (window.mcode.miniapp.agent) or from Node (context.agent). The Agent keeps its full capabilities: files, shell, Skills, and MCP, including this Mini App’s own MCP.See Agent API reference for full signatures, model and questionnaire types, session fields, and event structures, and Error reference for error handling.

Enable it

miniapp/miniapp.json
  • Without agentAccess, access is denied. Declare "session.run" to run turns, "session.read" to read and observe user conversations, or both. Neither permission grants the other.
  • agentConcurrency is optional, from 1 to 32, default 4: how many turns this Mini App runs at once. Extra calls wait in order, in a queue up to four times the concurrency. Each session runs one turn at a time.
  • Grants take effect with the process: changing agentAccess and publishing again restarts the service.
  • Users can switch running and reading permissions off separately in plugin details without a restart. Switching running off aborts this Mini App’s in-flight turns; switching reading off ends the affected observers. The API object stays present, so handle permission errors at the call site.

Who owns a session

These sessions don’t appear in the user’s conversation list. Each Mini App can continue only sessions it created, and a continued session keeps its agent and directory. Declaring session.read separately lets it read and observe user conversations, but does not let it continue them. run and answer can pass model to switch models; it must be a model the user can see in models(). See Mini Apps and sessions for how these sessions relate to chats.

Methods

Follow up while a session is busy

When continuing a session, pass whenBusy together with sessionId:
  • queue: the message runs as its own turn after the current turn finishes and returns that turn’s result.
  • steer: hands the message to the running turn to adjust its work. Once read, the follow-up resolves with status: 'steered' and no separate text; the original turn’s call receives the reply. If that turn finishes before reading the message, the message runs as the next turn instead.
  • On an idle session, both start a turn immediately. Without whenBusy, a busy session rejects with busy on the page or BUSY in Node.
  • After the user stops a session, its existing queue may pause and follow-ups wait for the user to resume it. Waiting keeps a run slot; cancel the call to give it up. A steered message already handed to the running turn cannot be withdrawn.

Read and observe user conversations

miniapp/miniapp.json
session.read exposes user conversations and their final replies. Declare it only when the product needs that content; it does not grant permission to run turns.
  • sessions() returns { sessions }, with up to 50 recently updated user conversations. Mini App, scheduled, and sub-Agent sessions are excluded.
  • session(sessionId) returns the conversation’s state, project directory, model, and latest assistant reply when available. On the page, session() without an ID reads the chat it is open in, or returns undefined when it is not in a chat. Node has no current chat and requires an explicit ID.
  • observe({ all: true }, { onEvent }) receives turn.started, the final reply in message.committed, and turn.finished from every user conversation. Passing sessionId observes that conversation’s progress instead. Observers never receive approvals or questionnaires.
  • Observation is not a run and takes no run slot; each Mini App has up to four subscriptions. The page ends one with the requestId passed to observe and cancel(requestId); Node uses signal in the second argument. Closing the page, ending the service, or switching reading off also ends the affected subscriptions.
  • Reading or observing this Mini App’s own sessions can also be authorized by session.run. Other Mini Apps’ sessions and sub-Agent sessions cannot be read.
Check host support with capabilities?.has('agent.observe') and the corresponding feature names before calling. Support does not mean the manifest declared the permission or the user still allows it.

Approvals and questionnaires

Who can answer an approval

The host lets an approval wait only when someone can answer it; otherwise it is denied right away.

  • permission.requested → reply: a tool call waits for approval. Only “allow once” and “deny” exist; there is no “always allow”, because a Mini App never writes the user’s global permission rules.
  • question.requested → answer: the turn paused on a questionnaire (ask_user, a feature enable, or a Plan review). Call answer after the paused run returns.
  • An approval waits only while someone can answer it: the call passed onEvent, or a page has the session open in the sidebar through open. Otherwise it is denied, and the model is told that nobody can approve it and to continue without the tool.
  • There is no timeout. A waiting call is released by cancellation, process exit, or republishing.
  • When the Agent delegates to a child session, the child’s approvals also arrive through onEvent, with the child’s own sessionId. Don’t filter events by the root turn, or the call waits forever.

Page example

Node example

Call context.agent from a request handler, never in start. A service restart cancels turns in flight.

Events

The table summarizes common events. See Agent event reference for complete fields, busy-session follow-up events, and the difference between run and observation events.Ignore event types you don’t recognize; more may be added.

Design guidance

  • Put the task, data scope, required interaction, success criteria, and stop conditions in text. Page labels, form state, and the host chat’s context aren’t carried over automatically.
  • deny rejects only that one tool request; it doesn’t put the file off-limits from then on. Enforce boundaries that must hold across calls with host permissions or Hook rules.
  • A root turn’s finished status doesn’t mean background child tasks are done. If you need a child’s result, use foreground delegation in the business request.
  • AGENT_OUTCOME_UNKNOWN means the turn may already have run. Reconcile the session state first; don’t retry automatically.