> ## Documentation Index
> Fetch the complete documentation index at: https://agent.minimax.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Two-way collaboration with the Agent

> The Agent can call a Mini App's tools, and a Mini App can hand work to the Agent. A person confirms at every key point.

<div className="code-docs">
  <h2 id="directions">
    Three channels
  </h2>

  | Direction | Mechanism | Status | Who confirms |
  | - | - | - | - |
  | Agent → Mini App | [Managed MCP](/docs/miniapp/authors/agent#mcp): the Mini App's Node service acts as an MCP server and exposes tools to the Agent | <Badge color="green" size="sm">Released</Badge> | The Agent's existing tool permissions |
  | Mini App → message input | [message.append](/docs/miniapp/authors/client-api#append): appends page content to the message input | <Badge color="green" size="sm">Released</Badge> | Host confirmation dialog; the user sends it |
  | Mini App → Agent session | [Agent API](/docs/miniapp/authors/agent#agent-api): starts Agent turns from the page or Node | <Badge color="green" size="sm">Released</Badge> | Tool approvals and questionnaires are rendered by the Mini App or handed to the host sidebar |

  <h2 id="mcp">
    Let the Agent call your Mini App: managed MCP
  </h2>

  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:

  ```json servers.mcp.json theme={null}
  {
    "schemaVersion": 1,
    "mcpServers": {
      "board-summary": {
        "type": "streamable-http",
        "url": "http://127.0.0.1:9/mcp/board-summary",
        "headers": {},
        "description": "Summarizes the current board data.",
        "timeout": 30000
      }
    }
  }
  ```

  Then mount it on a Node service path in `miniapp.json`, and reference `servers.mcp.json` from `mcpServers` in `plugin.json`:

  ```json miniapp/miniapp.json theme={null}
  {
    "mcpEndpoints": [
      { "server": "board-summary", "path": "/mcp/board-summary" }
    ]
  }
  ```

  * 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.

  <h2 id="append">
    Hand off to the message input
  </h2>

  Use this when you prepare context and let the user decide whether to send it; see [message.append](/docs/miniapp/authors/client-api#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.

  <h2 id="agent-api">
    Agent API
  </h2>

  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.

  <h3 id="agent-enable">
    Enable it
  </h3>

  ```json miniapp/miniapp.json theme={null}
  {
    "agentAccess": ["session.run"],
    "agentConcurrency": 4
  }
  ```

  * Without `agentAccess`, access is denied. The only valid value is `"session.run"`; any other value is rejected at publish time with `MINIAPP_AGENT_ACCESS_INVALID`.
  * `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.

  <h3 id="agent-session">
    Who owns a session
  </h3>

  | | Page-initiated | Node-initiated |
  | - | - | - |
  | Agent | The agent of the chat the Mini App is open in | The primary agent; list others with `agents()` and choose one at creation |
  | Working directory | That chat's project directory; without one, this Mini App's own Agent directory | This Mini App's own Agent directory |
  | Model | Inherits the model and thinking level of the chat it was created from | The runtime default model |

  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. `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](/docs/miniapp/sessions#agent-sessions) for how these sessions relate to chats.

  <h3 id="agent-methods">
    Methods
  </h3>

  | Method | Page | Node | Purpose |
  | - | - | - | - |
  | `run({ text, sessionId?, model?, agent? })` | ✓ | ✓ | Runs one turn and resolves with `{ sessionId, turnId, status, text?, error?, pendingQuestion? }`; `status` is `finished`, `error`, or `aborted` |
  | `reply({ sessionId, requestId, decision })` | ✓ | ✓ | Decides a tool approval; `decision` is `allowOnce` or `deny` |
  | `answer(...)` | ✓ | ✓ | Answers a paused questionnaire and observes the continuation turn. The page identifies the questionnaire with `questionId`; Node uses `requestId` |
  | `cancel(requestId)` | ✓ | — | Cancels a page-initiated call; Node cancels with `signal` |
  | `open(sessionId)` | ✓ | — | Opens the session in the host chat sidebar so the user can interact there |
  | `models()` / `agents()` | ✓ | ✓ | Lists available models and the user's configured agents |
  | `create({ agent? })` | ✓ | ✓ | Creates a session without running it and returns `{ sessionId }` |

  <h3 id="agent-interaction">
    Approvals and questionnaires
  </h3>

  <Frame caption="The host lets an approval wait only when someone can answer it; otherwise it is denied right away.">
    <img src="https://mintcdn.com/agent-en/mM6ovYl9sr4HdU6x/images/miniapp/approvals-en.svg?fit=max&auto=format&n=mM6ovYl9sr4HdU6x&q=85&s=934915e2d1442ab4a3bda5a89910c664" alt="Who can answer an approval" width="1200" height="460" data-path="images/miniapp/approvals-en.svg" />
  </Frame>

  * `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.

  <h3 id="agent-page">
    Page example
  </h3>

  ```javascript theme={null}
  const agent = window.mcode?.miniapp?.agent;
  if (agent) {
    const result = await agent.run(
      { text: 'Summarize this week\'s risks for the current filters and list what needs my confirmation.' },
      {
        onEvent(event) {
          if (event.type === 'text.delta') output.textContent += event.text;
          if (event.type === 'permission.requested') {
            // Render an approval card on the page; after the user decides, call:
            // agent.reply({ sessionId: event.sessionId, requestId: event.requestId, decision: 'allowOnce' })
            renderApproval(event);
          }
        },
      },
    );
    if (result.pendingQuestion) renderQuestionnaire(result.pendingQuestion.request);
    openInChatButton.onclick = () => agent.open(result.sessionId);
  }
  ```

  <h3 id="agent-node">
    Node example
  </h3>

  ```typescript theme={null}
  import type {
    MiniAppAgentClient,
    MiniAppAgentQuestionAnswer,
    MiniAppAgentQuestionnaire,
    MiniAppAgentRunEvent,
  } from './miniapp-api.js';

  // Your own UI round trips, for example an SSE push to the page and a POST back.
  declare function askUser(request: MiniAppAgentQuestionnaire): Promise<MiniAppAgentQuestionAnswer[]>;
  declare function confirmTool(toolName: string, reason: string): Promise<boolean>;

  async function runWithQuestions(agent: MiniAppAgentClient, text: string) {
    const onEvent = (event: MiniAppAgentRunEvent) => {
      if (event.type !== 'permission.requested') return;
      // The run waits for this decision; the event names the session even on its first run.
      void confirmTool(event.toolName, event.reason)
        .then((allowed) =>
          agent.reply({
            sessionId: event.sessionId,
            requestId: event.requestId,
            decision: allowed ? 'allowOnce' : 'deny',
          }),
        )
        .catch(() => {}); // REQUEST_NOT_FOUND: the run was cancelled meanwhile.
    };
    let result = await agent.run({ text }, { onEvent });
    // Answer only after the run resolves: until then it holds the session's single-run slot.
    while (result.pendingQuestion) {
      const answers = await askUser(result.pendingQuestion.request);
      result = await agent.answer(
        { sessionId: result.sessionId, requestId: result.pendingQuestion.requestId, answers },
        { onEvent },
      );
    }
    return result;
  }
  ```

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

  <h3 id="agent-events">
    Events
  </h3>

  | Event | Meaning |
  | - | - |
  | `run.queued` | All run slots are taken and the call is waiting; `position` is its place in line |
  | `turn.started` | The turn began; carries the `model` it was dispatched with |
  | `text.delta` | The next piece of the streamed reply |
  | `message.committed` | One complete assistant message; a turn can have several |
  | `tool.started` / `tool.finished` | A tool was called / returned; `ok` is `false` on failure |
  | `permission.requested` | A tool waits for approval; carries `requestId`, `reason`, and optional `toolInput` |
  | `question.requested` | The turn paused on a questionnaire; sent just before `turn.finished` |
  | `turn.finished` | Always last; carries the same `status`, `text`, and `error` as the result |

  Ignore event types you don't recognize; more may be added.

  <h2 id="principles">
    Design guidance
  </h2>

  * 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.
</div>
