Skip to main content
A bundle is a single document that describes everything a working setup needs: the MCP servers, the skills, the agents wired to both, the channels that reach them, the schedules that run them, the policies that constrain them, and the values a user has to supply. Installing one turns that document into real workspace resources.

The problem

A useful agent is never one object. It is an agent plus two MCP instances plus three skills plus a cron trigger plus a spend cap — and the agent references the MCP instances by id, and the trigger references the agent by id, and every id is workspace-local. Sharing that setup by writing instructions produces a twenty-step guide where step fourteen silently depends on step three. Sharing it as a database export produces something that cannot be installed anywhere else, because every reference is a UUID that means nothing in another workspace. And in both cases the person installing it finds out what credentials it needs by hitting an error part-way through, after resources have already been created.

How AgentArea approaches it

One fully-inlined document, referenced by key

Schema version 0.1.0. Every entity has a key — a stable in-document identifier matching [a-zA-Z][a-zA-Z0-9_]* — and entities reference each other by key, never by database id. An agent lists mcps: [github] and skills: [triage]; an automation names agent: triage_bot. The installer resolves keys to real ids at install time, which is what makes the document portable.

Secrets never appear in the document

A SetupField declares a key, a label, a type, and whether it is required. Everything else in the bundle references it as ${setup.<key>}:
The installer marks the resulting env_schema entry as a secret based on SetupField.type, rather than relying on the MCP service’s name-based heuristic. The bundle already declared which inputs are credentials, so guessing from the variable name is unnecessary and less accurate. A URL MCP that verifies the signed call context (tool call context) takes its signing key the same way. context_secret is a ${setup.<key>} reference to a setup field of type secret, never the key itself:
The value goes to the instance’s context_secret, which stores it in the secret manager; the instance then shows context_signing: true. A literal value fails parsing, a reference on a non-URL MCP or to a non-secret field blocks the bundle, and a value shorter than 32 or longer than 512 characters refuses the install with a message that names the field, not the value. Exporting a workspace turns a signed instance’s key into a fresh secret setup field.

A workspace exports as a bundle

GET /v1/workspaces/{workspace}/export (admin only; Settings → Workspace → Export workspace in the webapp) writes the workspace’s agents, MCP connections, skills, cron automations and supported webhook channels as one bundle. Each agent’s MCP reference, an instance id, resolves to the exported MCP key. An agent that references an MCP or skill the export cannot represent fails the export with a 422 naming it, so tools are never silently dropped. A GitHub-imported skill exports as source_type: github with its source_url, so the import fetches the whole package again. Exporting only its SKILL.md would drop the files the skill references. An exported bundle is meant to be shared, so no credential travels in it. Secret-backed values are never read; each becomes a secret setup field. Plain MCP configuration is screened as well. These values are replaced by a ${setup.<key>} reference to a secret setup field:
  • an environment variable or header whose name looks like a credential (token, secret, password, api_key and similar), or whose value is a credentialed URL or an Authorization-style value (Bearer …, Basic …);
  • an args entry that is a credentialed URL, the value of a credential flag (--api-key=x, --token x, TOKEN=x), or a credential header argument (Authorization: Bearer x);
  • an endpoint_url that is a credentialed URL.
A URL counts as credentialed when it has a password in its userinfo, a query parameter named like a credential, or an opaque path segment of 24 or more letters and digits, which is how per-user secret URLs look. Everything else in environment and headers is exported as written. Setup keys are derived from the MCP key and the position, so the same workspace always exports the same document. On install, references in args and endpoint_url are resolved into the values the runtime reads, because an MCP server takes its arguments verbatim. An export that refused every MCP with a credential in its arguments would refuse most real workspaces: a Postgres MCP has nowhere else to take its connection string. Templating keeps the export usable, and the person installing the bundle supplies their own values.

Agent settings that are policies

Per-agent behaviour that governance owns travels as a policy bound to the agent’s key. The final-answer check, for example (see answer check):

Analyze, then install

POST /v1/workspaces/{workspace}/bundles/analyze parses the source and returns an ImportPreview — every entity tagged CREATE, REUSE, or SKIPPED with a reason, plus a list of issues. It writes nothing. This is where a missing setup field, an agent pointing at a nonexistent MCP key, or a command runtime the platform cannot run is surfaced. POST /v1/workspaces/{workspace}/bundles/install takes the canonical bundle and the setup values. Missing required setup blocks the whole install before anything is written. Inline skill content is parsed with the same SKILL.md parser the skill service uses, so a frontmatter that is not valid YAML (an unquoted value containing : is the usual cause) is a blocking issue in the preview, naming the skill and the line. Install answers 422 when the bundle or a setup value is invalid (anything analyze blocks, missing required setup, a refused context_secret, a skill the skill service refuses, an MCP the MCP service refuses) and 400 when an install step fails for another reason, such as a GitHub skill that cannot be fetched. Both are problem+json: the message in detail, code bundle_invalid or bundle_install_failed, and the blocking issues. Analyze accepts exactly one of source (pasted text) or source_url (fetched server-side behind an SSRF guard, capped at 5 MB).

Install order is dependency order

MCPs, then skills, then agents, then channels, then automations, then policies. Each step returns the key-to-id map the next one needs. An agent whose MCP was skipped is still created; an automation whose agent was not created is skipped with that reason recorded, rather than failing the install.

Idempotency follows what the last install recorded

An InstalledBundle row is upserted by bundle name, recording the canonical document and the full install result: each entity’s key, action and id. The next install of the same bundle reuses an MCP instance, skill or agent only by the id that result recorded for its key, and only while it still exists (an archived agent is installed again). It is marked REUSED and not duplicated. Triggers are named {bundle_name}:{key}; one is reused only when it carries that name and runs the agent this install bound. An entity that merely carries a name from the bundle is never reused. Names are not unique and any member picks them, so an agent called Triage Bot that the bundle did not install is someone else’s: reusing it would wire the bundle’s triggers, channels and policies to it. The install creates its own alongside. An agent’s MCP tools name the bundle’s MCP instances by id. Re-running an install is therefore close to a no-op for what the bundle installed before.

A failed install leaves nothing behind

Each domain service commits on its own, so install is made all-or-nothing by compensation. Every entity the install creates is recorded with how to delete it; when a later step fails, those entities are deleted again, newest first, before the error is returned: policies, triggers and their stored channel credentials, agents, skills, MCP instances (with their stored secrets) and the MCP server specs created for them. Entities that already existed and were REUSED are never touched. A retry after a failure therefore creates everything fresh instead of reusing a half-configured MCP instance from the failed attempt.

Automations and channels install disabled

enabled defaults to false on both, and the installer explicitly disables a cron trigger after creating it — trigger creation schedules unconditionally, so without that step an imported automation would start running before the user had confirmed its credentials. Connect, verify, then activate.

Bundles in the catalog

A registry of type bundles holds catalog items whose spec is the bundle document, keyed by the bundle’s name. Nothing is materialized on sync; the bundle is provisioned into a workspace on demand through the same install path.

Why not a repository or an archive

A directory layout or a tarball is the familiar packaging shape, and it gives up the property that matters most here: a bundle must be analyzable before anything is written. A single inlined document can be pasted into a form, fetched in one request, diffed in a pull request, reviewed by a human, and previewed against a live workspace. An archive means fetch, unpack, and then trust — and the fetch itself becomes an attack surface with a much larger blast radius than a size-capped text fetch behind an SSRF guard.

Why not reference resources by id

Ids are workspace-local. A bundle that names them can be exported from the workspace that built it and installed nowhere else, including back into the same workspace after those resources were recreated. Keys cost an indirection at install time and buy portability, which is the entire point of the format.

Why not install first and report failures after

The preview exists because a half-finished install is worse than a refused one. An MCP instance created with a credential, orphaned because the agent step then failed, is a live secret nobody is tracking. Blocking on missing required setup before the first write, and reporting unsupported entities as SKIPPED in a preview, moves those failures to a point where nothing has been created yet.

Limits

  • Only canonical YAML or JSON is parsed. parse_bundle loads the document and validates it against the Bundle schema. There is no adapter that converts a Claude plugin manifest, a repository layout, or any other external format — despite what the format module’s own docstring implies.
  • Rollback is compensation, not a database transaction. If deleting a created entity also fails (the database is down, say), that failure is logged and the rest are still removed; the original error is what the caller sees.
  • Command MCPs are restricted to eight runtimes. npx, uvx, uv, python, python3, node, bunx, deno. A command that is a local path or references ${CLAUDE_PLUGIN_ROOT} is skipped with a reason recorded, not failed. Docker and URL MCPs are always accepted.
  • Channels are Telegram only and automations are cron only in 0.1.0. The schema is a closed literal in both cases, so anything else fails validation.
  • Skills support inline content and GitHub only. Zip and S3 import are not in 0.1.0.
  • Export screening is heuristic. A credential in plain MCP configuration that matches none of the rules above (a short token in an unnamed positional argument, for example) is exported as written. Store credentials as secret values (isSecret in the spec’s env_schema) and they never leave the workspace.
  • There is no uninstall. InstalledBundle records what was installed, and removing those resources is manual. The API surface is analyze and install.
  • A reused entity is not reconciled. An agent the bundle installed before is reused as it is now, edits included; its configuration is not brought back to the document.
  • extra="forbid" everywhere. An unrecognized field anywhere in the document is a validation error, not a warning. A bundle authored against a newer schema version is rejected outright rather than partially understood.

Registry and catalog

How a bundle reaches a workspace as a catalog item

MCP

What the mcps section provisions
Last modified on November 1, 2026