Project structure
An Agent Bundle project is an ordinary Node package with agent-bundle.config.ts and a
conventional source tree. Start with the files your plugin needs. A static Skill does not need
an MCP server; a tool does not need a browser App; an App does not need its own server registry.
Conventions discover the application. Configuration supplies identity, policy, and explicit exceptions. Conflicting declarations are diagnosed rather than universally resolved by "config always wins."
The layout
This is an illustrative composite plugin, not a minimum template. The opening paths are ordinary authored source; host manifests and executable wrappers belong in generated output.
There is no authored application.ts registry, generated MCP entry, or native hook JSON in this
layout. The compiler derives them where needed. domain/ and components/ are organizational
choices, not new framework conventions. Use an explicit payload declaration for prebuilt
non-TypeScript assets or native executables; do not assume an arbitrary source directory ships.
What each root means
Private path segments beginning with _ or . and declaration files are excluded from route
discovery. Event files use .ts for lightweight results and .tsx for JSX; call ctx.render()
from a .ts handler to load its separate .view.tsx sibling. See
hooks for execution and provider access.
Route metadata uses a bounded static grammar. Tools put it in defineTool's inline object;
resources, prompts, apps, and compatibility routes use config exports. Keep runtime work in
the handler or domain module, and keep schemas authoritative rather than reconstructing
protocol types independently in the CLI and browser.
Adding an interactive App
A tool's Agent.* JSX describes the agent-facing document. An MCP App entry runs in the
browser and owns its DOM or React UI. Do not import a server route into the browser: that can
pull Node APIs, domain services, and server-only dependencies into the wrong runtime.
Connect the two through the existing App resource binding. For a browser entry at
src/mcp/ops/apps/dashboard.tsx, declare its resource URI in that entry:
This is metadata for the browser entry, not a complete UI. The default HTML provides #root;
use an explicit template when your bootstrap needs a different shell. The tool can reference
the App without repeating the URI string:
Use createAppClient from agent-bundle/app in the browser. Its opening-input/result/error
listeners and tool calls are the integration boundary; do not recreate postMessage transport,
request-ID maps, or an MCP client. Register opening listeners before connecting. Render the
opening result supplied by the host instead of automatically calling a mutation again on mount.
Dispose the client and subscriptions when the owning UI is retired.
Generated route declarations supply App call types. They must be current and included in the browser's TypeScript project; they do not replace validation of untrusted data. Pure presentation components and validated data can be shared where their imports are browser-safe. A Node-rendered document component is not automatically a DOM component just because both use JSX.
To expose the same App through the optional production browser surface, configure web.apps
with its existing server/app identity and opening tool. There is no second src/web/ registry.
See MCP servers and Apps,
the working App example, and
web configuration for complete examples and policy.
Browser presentation and the host projection that launches its server are separate choices.
Config versus conventions
The conventional starting point remains:
Add explicit configuration only for something the convention cannot express: a target-scoped plain hook, prebuilt payload, remote server, nonstandard path, package-only bin, or policy. Required capabilities still need a supported host or an explicit restriction.
A handwritten src/mcp/<server>.ts entry is an interoperability path for a declared server.
Combining it with generated routes for the same server needs an explicit mode decision; do
not rely on filesystem order. Likewise, src/cli.ts and src/cli/ are alternative CLI modes.
src/index.ts is an optional library entry, not application registration. bin: false and
lib: false opt out of those package conventions.
For declarations that intentionally differ from conventions, inspect the resulting graph rather than guessing which file won:
The compiler must already be installed in this project. Details of source selection and migration diagnostics are in configuration and package entries.
Where output lands
Keep these responsibilities separate:
The composite plugin root
A schematic output is:
Only relevant declared surfaces are emitted. Exact native documents and additional runtime
files are recorded by the manifest; this tree is not an inventory to reconstruct in application
code. There is no artifact/<host>/ partition. Amp uses its nested native directory, while the
framework's manifest-aware installation entry accepts the composite root and selects the
correct host surface.
output.distPath or CLI --output moves the composite root without changing its internal
layout. CLI precedence is --output, then configuration, then artifact/. The programmatic
build() API has separate package-output/default semantics; use its reference rather than
copying a CLI default into a custom build harness.
The manifest records file digests and canonical executable identities. Reordering targets does not create another application. Conflicting bytes or unintended cross-host discovery are validation problems, not a reason to hand-edit the emitted manifests. See targets and artifacts.
The npm package build
dist/ is the complete generated npm package root, not merely a folder of JavaScript:
A routed CLI is not rebuilt into a competing .js implementation for npm. The generated
package.json points at the artifact executable. Explicit package-only bins and library entries
are different outputs and keep their own compilation.
The composite and npm outputs must not overlap; putting the artifact at dist/ when package
output also occupies it is invalid. Build outputs are not source inputs. Pack the generated
npm root, not the source repository or a guessed nested dist/artifact/ directory. Follow
shipping a bundle for validation and installation.
Writable state is not installed code
Inside a request, use the framework-provided plugin/context binding. Standalone code can use
the documented public resolvePluginRoot runtime resolver, which distinguishes root from
stateRoot; AGENT_BUNDLE_STATE_ROOT is independent of the code-root override.
Do not place runtime caches, refreshed credentials, or databases under the installed code tree just because their loader lives there. Application-owned data keeps its own schema and explicit migration/override policy, and must not collide with framework storage. The framework does not implicitly adopt or purge arbitrary plugin data. See configuration and state and installation lifecycle.
Next steps
Quick start exercises a real conventional route. Skills, hooks, MCP, and scripts/assets specify each surface. Read compiler architecture for implementation details, not as another setup step.