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

# Node runtime

> The entry module exports start(context). The host launches the process, assigns the port, guards access, and cleans up.

See [Node API reference](https://agent.minimax.io/docs/miniapp/reference/node-api.md) for the entry, `context`, Connector, and tool contracts, and [Agent API reference](https://agent.minimax.io/docs/miniapp/reference/agent-api.md) for session methods.

## Entry contract

* The entry is an ES module that exports `start(context)`. Return from `start` only after the HTTP listener accepts connections and every declared route is installed. That is the readiness signal; you don't write a health-check route.
* In `start`, only register routes and start listening. Don't fetch business data synchronously; load it on demand in request handlers.
* Return `{ dispose }`. `dispose` closes the listener, SSE / WebSocket connections, child processes, timers, subscriptions, and persistence handles. It is not a per-request completion callback.
* The entry process is the one the host manages directly. It must stay alive, holding its assigned port, for as long as the Mini App is available.
* No Mini App SDK is installed at runtime. The `miniapp-api.ts` file from the Skill is for type checking only.

## context fields

| Field | Description |
| - | - |
| `pluginId` / `pluginRoot` | The adopted plugin identity and the directory of its runtime copy |
| `dataDir` | A data directory the host isolates for this plugin. Keep runtime state here, not in the workspace or the payload |
| `listen` | The assigned loopback address `{ host: '127.0.0.1', port }`. Don't choose or probe a port yourself |
| `signal` | The runtime lifecycle signal, aborted on stop or failed startup; pass it explicitly to request options or register cleanup for background work. It does not automatically cancel requests or close plugin resources |
| `logger` | Structured logging; pass stable event codes and non-sensitive fields |
| `hostConnector?` | Present when `hostConnectorAccess.providers` declares at least one provider; see [Data access](https://agent.minimax.io/docs/miniapp/authors/data.md#host-connector) |
| `agent?` | Present when `agentAccess` declares `session.run` or `session.read`; see [Agent API](https://agent.minimax.io/docs/miniapp/authors/agent.md#agent-api) |
| `tool?` | Present when `toolAccess: ["call"]` is declared; calls tools mounted by the host. See [Unified tool entry](https://agent.minimax.io/docs/miniapp/authors/data.md#tools) |
| `capabilities` | `has(name)` synchronously checks whether the host implements an interface; see [Versions and capability detection](https://agent.minimax.io/docs/miniapp/reference/manifest.md#compat) |

The manifest determines whether `hostConnector`, `agent`, and `tool` are injected. Switching a permission off in the permission panel keeps the object but rejects the corresponding calls, so object presence alone does not establish authorization. `capabilities.has(name)` reports host implementation support, not a grant to this plugin. Use optional chaining for older hosts, such as `context.capabilities?.has('tool')`.

## Minimal entry

```javascript miniapp/node/server.mjs theme={null}
// @ts-check
import { readFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { join } from 'node:path';

/** @typedef {import('./miniapp-api.js').MiniAppContext} MiniAppContext */
/** @typedef {import('./miniapp-api.js').MiniAppLifecycle} MiniAppLifecycle */

/**
 * @param {MiniAppContext} context
 * @returns {Promise<MiniAppLifecycle>}
 */
export async function start(context) {
  const clientEntry = await readFile(join(context.pluginRoot, 'miniapp/client/index.html'));
  const server = createServer((request, response) => {
    const url = new URL(request.url ?? '/', 'http://miniapp.local');
    if (request.method === 'GET' && url.pathname === '/dashboard') {
      response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
      response.end(clientEntry);
      return;
    }
    response.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
    response.end(JSON.stringify({ error: 'not_found' }));
  });

  await listen(server, context.listen.host, context.listen.port);
  context.logger.info('miniapp.runtime.listening');

  let disposed = false;
  const dispose = async () => {
    if (disposed) return;
    disposed = true;
    context.signal.removeEventListener('abort', onAbort);
    await close(server);
  };
  const onAbort = () => void dispose();
  context.signal.addEventListener('abort', onAbort, { once: true });
  if (context.signal.aborted) await dispose();

  return { dispose };
}

function listen(server, host, port) {
  return new Promise((resolve, reject) => {
    const onError = (error) => reject(error);
    server.once('error', onError);
    server.listen(port, host, () => {
      server.off('error', onError);
      resolve();
    });
  });
}

function close(server) {
  return new Promise((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()));
  });
}
```

## When it runs

* **On demand**: it starts when a Mini App is selected with `@`, its page is opened, or on `publish` or `restart`. Enabling the plugin doesn't start it.
* **Shared service:** each Mini App has one Node process shared by all conversations, and each conversation gets its own page. See [Mini Apps and sessions](https://agent.minimax.io/docs/miniapp/sessions.md).
* **Capacity:** at most 3 different Mini App services run at once. A fourth returns `BUSY`; updating a running Mini App doesn't take an extra slot.
* **Stop**: `stop` only stops the service and keeps the plugin and its other capabilities. Closing a running Mini App's tab and confirming stops its shared service and closes that plugin's pages in every conversation.
* **Update and restart:** the old process stops before the new one starts. A Mini App never has more than one process, so don't start a second entry yourself.

When a marketplace update still provides a Mini App definition compatible with this host and requires a new process, the host keeps the running version until you apply the update from the toolbar or plugin details, or the service stops and the new definition is adopted. If the new package removes the definition or the host withholds it because of its version requirement, the old service is retired. See [Marketplace updates](https://agent.minimax.io/docs/miniapp/host/lifecycle.md#marketplace-updates).

## Other languages and child processes

Anything can sit behind the entry. For a Python, Go, or other backend, have the Node entry launch it on its own internal port and reverse-proxy to it from the assigned port.

> [!WARNING]
>
> The host manages only the entry Node process. Close any child processes the plugin starts in `dispose`; the host doesn't promise to find and end them.

## Port authentication

Before loading the entry, the host installs an authentication guard on the `node:http` servers in the process. Only the Mini App page and the managed MCP client carry the current run's credential; every other request gets `401` before it reaches plugin code. Plugins need no auth logic and never see the credential. See [Ports and credentials](https://agent.minimax.io/docs/miniapp/host/security.md#guard) for how it works.

* Serve the assigned port with `node:http`, either `createServer` or a framework built on it. If `https`, `http2`, a raw `net` server, or a child process binds the port directly, the guard can't protect it, the service never becomes ready, and startup fails with `READINESS_TIMEOUT`.
* The `/__mavis/` path prefix belongs to the host. Don't declare or handle routes under it.
* Build page requests from relative or same-origin URLs. Service Workers, pop-ups, external browsers, and ordinary browser tabs don't carry the credential and get `401`.
* The guard covers every `node:http` server in the process. To receive third-party callbacks such as OAuth redirects, route them through the assigned port instead of opening another HTTP server in the process.
* A restart issues a new credential. An already open tab gets `401` until the user refreshes or reopens it; this is expected.

## Logging

Use `context.logger` for diagnostics. The host collects only bounded stdout / stderr samples, so keep credentials and business content out of logs and don't rely on them being retained in full.

The current log bridge truncates messages and sends only the keys of `fields`, not their values. See the [logger reference](https://agent.minimax.io/docs/miniapp/reference/node-api.md#logger) for methods and limits.
