Headless Targets

Headless targets let a package expose tools, actions, workflows, or background behavior without mounting a browser surface.

Each target has its own runtime assumptions. Code shared with a webview target must avoid browser APIs unless it is behind a target-specific adapter.

Host Bootstrap

The host installs the supported SDK capability session before evaluating the selected expose. Your module imports the public SDK normally:

import { sdk } from '@theaiplatform/miniapp-sdk/sdk';

Do not read or install the bootstrap value yourself. Its storage and transport are host implementation details, not a package API.

If reading an sdk property reports that the environment is unsupported, the expose is running outside its host bootstrap or was assigned to the wrong target runtime.

QuickJS

QuickJS exposes do not receive browser or server globals by default. Avoid assumptions about window, document, process, Buffer, fetch, sockets, and timers. Use SDK capabilities for supported host effects.

src/tools/summarize.ts
import { sdk } from '@theaiplatform/miniapp-sdk/sdk';

export async function execute(input: { channelId: string }) {
  const result = await sdk.channels.getTimeline({
    channelId: input.channelId,
  });
  return {
    messageCount: result.timeline.messages.length,
    sequence: result.timeline.sequence,
  };
}

Build it as a distinct target graph:

const quickjs = tapLib({
  manifest: './manifest.tap.json',
  packageTarget: 'quickjs',
  packageOutputRoot: '.tap-build/quickjs',
  federation: {
    name: 'example_tools_quickjs',
    manifest: true,
    library: { type: 'module' },
    dts: false,
    exposes: {
      './tools/summarize': './src/tools/summarize.ts',
    },
  },
});

The build rejects ambient globals that are unavailable in the QuickJS contract.

Packaged MCP Servers

A package-runtime mcp.server contribution uses one server-specific Module Federation expose on every target that implements it. The expose name is derived from the signed contribution ID:

const quickjs = tapLib({
  manifest: './manifest.tap.json',
  packageTarget: 'quickjs',
  packageOutputRoot: '.tap-build/quickjs',
  federation: {
    name: 'example_mcp_quickjs',
    exposes: {
      './mcp/catalog': './src/mcp/catalog.ts',
    },
  },
});

The expose exports one typed server definition. The MVP runtime supports MCP tools; the host derives server and consumer identity from the verified package graph, so neither value is supplied to defineMcpServer:

src/mcp/catalog.ts
import { defineMcpServer } from '@theaiplatform/miniapp-sdk/mcp';

export const mcpServer = defineMcpServer({
  tools: {
    search: {
      description: 'Search the package catalog',
      inputSchema: {
        type: 'object',
        properties: { query: { type: 'string' } },
        required: ['query'],
        additionalProperties: false,
      },
      execute({ query }: { query: string }) {
        return { matches: findMatches(query) };
      },
    },
  },
});

Use the named mcpServer export. A package MCP expose is not a normal miniapp default export and its tools are never registered as ordinary package tools. The tools-only MVP accepts API version 1 and pure QuickJS execution only: package MCP server/tool authorization actions or effects fail activation, and the miniapp host-action SDK is unavailable while an MCP tool runs. Use an ordinary package tool contribution when work needs reviewed host actions.

For a server contribution whose ID is catalog, the descriptor target and build configuration must both select ./mcp/catalog. A package with several servers gives each server its own ./mcp/<server-contribution-id> expose; do not combine them behind ./tap/mcp.

The descriptor declares both the server's maximum audience and every runtime tool that may appear in its live catalog:

[
  {
    "kind": "mcp.server",
    "id": "catalog",
    "apiVersion": 1,
    "targets": {
      "quickjs": { "expose": "./mcp/catalog", "runtime": "quickjs" }
    },
    "options": {
      "protocolVersion": "2026-01-26",
      "implementation": { "type": "package-runtime" },
      "consumerPolicy": {
        "externalConsumers": ["selected-specialists"]
      }
    }
  },
  {
    "kind": "mcp.tool",
    "id": "catalog-search",
    "apiVersion": 1,
    "options": {
      "serverContributionId": "catalog",
      "toolName": "search"
    }
  }
]

The runtime tools keys and the owning server's mcp.tool names must match one-to-one. Missing, extra, or renamed tools fail package activation.

contributionIds names exact eligible contributions in the same installed package. External entries are closed consumer classes, not wildcard grants. The host must narrow every declared class to selected specialist, chat, workflow, miniapp, or platform identities before use. Both lists default empty, which denies every consumer.

The current desktop MVP wires only exact selected-specialist consumers. In Settings → Miniapps → Installed, open the owning package details and grant the server to a canonical specialist slug such as chloe (not a versioned manifest ID such as chloe@1.0.0). The package detail establishes which installation registered the server, and an empty grant set remains deny-all. The other external consumer classes are signed schema maxima for later trusted call sites; declaring them does not make chat, workflows, miniapps, or platform services live consumers in this MVP.

TAP persists that narrowing as an audited selection on the server package's installation record: the exact server contribution, exact consumer target, granting human, and grant time. A package consumer is stored only as an installation ID plus contribution ID. It cannot persist its own miniapp, specialist, or workflow classification; the host re-derives that closed class from the referenced installation's verified descriptor.

This expose packages the server implementation into the verified target graph. It does not register a server from package code. During verified desktop package reconciliation, the host now derives an immutable, deny-by-default registration with package, installation, release, host-local delivery generation, target, integrity, server, and child-catalog provenance. It prepares a complete desired snapshot and atomically commits, replaces, or removes that snapshot in the canonical MCP owner. Failed candidate validation leaves the prior release intact, and restart reconstruction comes from the package installation database rather than the user MCP configuration file.

Reconciliation loads grants across the complete enabled, account-visible installation graph, then re-resolves package consumers against the exact subset that passed the final QuickJS, checkpoint, UI, entitlement, and provenance readiness barrier. Missing, disabled, unusable, or failed contributions and installations, cross-scope package references, duplicates, invalid identities, and selections outside the signed maximum fail closed instead of producing a partial audience. With no persisted selections, the projected audience is DenyAll.

For a workspace-scoped installation, grant replacement must name the authoritative active workspace and pass the platform mcp:manage policy check; an authenticated account or renderer-provided workspace id alone is not authority. User-scoped replacement is instead bound to the exact active account and canonical granting human. Account and workspace transitions run the same guarded reconciliation, withdrawing projections outside the newly active scope.

The verified QuickJS host loads each expected mcpServer, validates its live tool names against the exact descriptor-declared mcp.tool children, and rejects a missing, extra, duplicated, or renamed tool before the package MCP generation becomes visible. Runtime descriptions and input schemas are bounded and validated at the isolate boundary. The descriptor remains authority for server ownership, contribution identity, timeout, target, and release.

The canonical registry issues a provisional access lease for an exact consumer and exact descriptor-declared tool. An authorized specialist receives only those package MCP tools in its private per-turn catalog; they are never copied into the global tool registry or the user's MCP server store. Immediately before QuickJS dispatch, the host reacquires the package projection gate and revalidates the activation, audience, and child membership. Audience narrowing therefore revokes an already-discovered tool even when the server generation is unchanged, and an A-to-B-to-A replacement cannot revive an old lease. Existing specialist-authored tool policy and delegation scope remain additional upper bounds: an MCP grant supplies authority, but does not widen a specialist that was explicitly configured with a narrower tool set.

This projection is desktop-only in the current host. Mobile and other non-desktop hosts accept an authoritative empty MCP snapshot, but fail package activation closed when the snapshot contains any MCP declaration; declarations are never silently ignored.

The production MVP is intentionally tools-only and package-runtime-only. Chat turns use the exact specialist identity that performs the turn; there is no second broad "chat" grant on a specialist call. The same consumer-scoped host API can be adopted by direct chat, workflows, miniapps, and platform services when those owners have a trusted identity-bearing call site, but those call sites are not implied by descriptor acceptance.

Model-facing package tool names are host-generated, provider-safe aliases. The host preserves ordinary tools and fails the package tool closed if an exact or provider-sanitized name would collide; package authority is always checked by the exact registration and contribution lease, never by a name prefix.

Stdio and Streamable HTTP server declarations are host-declarative and do not select a Module Federation expose. They enter the same non-live immutable registration projection, but the host does not connect them until command review and credential-binding authority exist. mcp.prompt, mcp.resource, mcp.resource-template, and mcp.app also remain declaration-only until their canonical read/render paths preserve the same lease and provenance contract.

Worker, Node, and Workflow Host

  • Use worker for code compatible with the supported worker runtime.
  • Use node for contributions that require the supported Node.js module profile.
  • Use workflow-host for contributions evaluated by the workflow runtime.

Declare a separate expose for each runtime. Do not point several incompatible target entries at a bundle compiled for one environment.

Multi-Target Packages

Compile every target into its own staging root, then call assembleTapPackage once with an exact target map. The assembler rejects missing, extra, overlapping, or invalid target roots and preserves the previous complete output if assembly fails.

See Build and Module Federation for a complete multi-target configuration.