Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flagship-route-authoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-agent-bundle": minor
---

Generate single-file MCP route modules with matching runtime dependencies.
11 changes: 10 additions & 1 deletion docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ entries carry `provenance.kind: 'conventional'` in the normalized model.
| `src/cli.ts` | Package bin named after `plugin.name` (skipped when the name is not a safe output name). | `bin: false` |
| `src/index.ts` | Library output with declarations. | `lib: false` |
| `src/mcp/<server-id>.ts` | Stdio entry for the declared MCP server `<server-id>` that names no `entry`, `command`, or `url`. | Declare `entry` explicitly |
| `src/mcp/<server>/{tools,resources,prompts}/*.{ts,tsx}` | Generated MCP server routes; path supplies identity and each executable module supplies static `config`, schemas, and one async default Server Component. | Set `routes.servers.<server>` to `custom`, `command`, or `remote` |
| `src/mcp/<server>/apps/*.{ts,tsx}` | Browser MCP App entry compiled to self-contained HTML and registered on the generated server; static `config.resourceUri` is required. | Use a custom server or prefix the file with `_` |
| `src/scripts/<name>.ts` | Plain script compiled to `scripts/<name>.mjs` in every selected target artifact — the same pipeline explicit `scripts` entries use. A `scripts` entry that references the file claims it. Rendered (`.tsx`) and nested modules are hard errors until later #102 stages (`AB4807`/`AB4808`). | Prefix a path segment with `_`, or claim the file with an explicit `scripts` entry |

Conventions match `.ts` and `.tsx` files exactly.
Expand All @@ -73,7 +75,14 @@ See `docs/diagnostics.md` for each trigger and how to adopt or silence it.

## Generated entry shells

The framework provides the entry files consumers used to write by hand
A route-mode MCP surface emits a public lifecycle entry plus one warm internal
Flight worker. The entry owns `runAgentRequest`, session/actor binding, the
final Agent Document dispatcher, legal MCP projection, resource/prompt
registration, and compiled App resources. The worker exists solely to isolate
React's `react-server` condition and is reused until that MCP process closes;
raw Flight bytes never cross the public MCP wire.

The framework also provides the entry files consumers used to write by hand
(react-router's provided-entry trick). Every generated shell imports the
consumer module by absolute path and is bundled through the same Rslib
synthesis and invariant assertions as all generated executables.
Expand Down
227 changes: 49 additions & 178 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
@@ -1,194 +1,65 @@
# Framework mode

Structure lives in `agent-bundle.config.ts` and file conventions. JSX renders.
That is the whole model (RFC #63); RFC #50's entry conventions are the sibling
contract for `bin`/`lib`/MCP entries.

## What a newcomer must learn

Three things:

1. **One directory convention.** Every `skills/<name>/SKILL.md` ships as a
Skill. Add a folder and it ships — no declaration anywhere.
2. **One flat config file.** `agent-bundle.config.ts` declares the plugin
identity, targets, and anything a file cannot say for itself:
Agent Bundle has one newcomer model:

1. **Files under conventional `src/` roots are the app.** For MCP, put one
module at `src/mcp/<server>/{tools,resources,prompts}/<name>.tsx`; its path
is its identity. `skills/<name>/SKILL.md`, `src/scripts/<name>.ts`,
`src/cli.ts`, and `src/index.ts` keep their existing conventions.
2. **One small flat config.** `agent-bundle.config.ts` holds project identity,
targets, and policy that no route file can own.
3. **JSX = rendering.** An executable route is one async default Server
Component. It does the work and returns `Agent.*`; there is no public
`execute`/`render` split.
4. **Opt in to context.** Call `await agent()` inside that component only when
host, session, actor, workspace, capability, or state context is needed.

The complete conventional config is usually:

```ts
import { defineConfig } from 'agent-bundle';
import { defineConfig } from 'agent-bundle/config';

export default defineConfig({
plugin: { description: '', name: 'my-plugin', version: '0.1.0' },
plugin: { description: 'Evidence-backed project tools.', name: 'my-plugin', version: '0.1.0' },
targets: ['portable', 'codex', 'claude'],
});
```

3. **JSX = rendering.** React elements appear only where something is
rendered: MCP/hook results at runtime (`Mcp.Result`, `Hook.Text`), and
skill documents at build time (below). There are no structural JSX
elements — no `<AgentBundle>`, `<Skill>`, or `<McpServer>`.

Entry files follow the same convention-with-fallback trick: `src/cli.ts` is
the package bin, `src/index.ts` the library, `src/mcp/<server-id>.ts` a
declared server's stdio entry — each applies when the file exists, and
explicit config always wins over a convention (`AB473x` nudges flag the
confusable shadowed states). See `docs/entry-conventions.md`.

## Applications with operations (when you have a CLI or MCP server)

`defineRscApplication` declares the runtime identity plus one typed operation
catalog; the conventional entries consume it:

```ts
// src/application.ts
export const application = defineRscApplication({
name: 'my-plugin',
operations: [status],
version: '0.1.0',
});

// src/cli.ts
export const main = (argv: readonly string[]) => runRscCli(application, argv);

// src/mcp/runtime.ts
export default () => createRscMcpServer(application, 'runtime');
```

The server's structural declaration (`mcp.servers.runtime: {}`) lives in the
config; the name passed to `createRscMcpServer` only selects which operations
to serve.

## One operation, end to end

An **operation** is a host-neutral use-case definition: one named unit of
work with a validated input, an implementation, a validated result, and a
result renderer. It is *not* a CLI command — the CLI command and the MCP
tool are optional projections declared alongside the shared core, and
either (or both) may be present. The `status` operation used above looks
like this in full, including the JSX:
A tool is one file:

```tsx
// src/operations/status.tsx
import { defineOperation } from '@agent-bundle/runtime/plugin';
import { Mcp } from '@agent-bundle/runtime';
// src/mcp/runtime/tools/status.tsx
import React from 'react';
import type { ToolConfig, ToolRouteProps } from 'agent-bundle';
import { Agent, agent } from '@agent-bundle/runtime';
import { z } from 'zod';

export const status = defineOperation({
// Shared core — both projections funnel through these four fields.
id: 'status',
inputSchema: z.object({ verbose: z.boolean().optional() }).strict(),
execute: async () => ({ status: 'ready' as const }),
resultSchema: z.object({ status: z.literal('ready') }).strict(),

// CLI projection — argv parsing, help text, exit codes. No JSX: the CLI
// prints the validated result as one line of JSON.
cli: {
name: 'status',
parse: (args) => (args.includes('--verbose') ? { verbose: true } : {}),
summary: 'Read runtime status.',
usage: 'status [--verbose]',
},

// MCP projection — tool metadata plus the result renderer. Only MCP
// consumes `render`, but it is a required field: a CLI-only operation
// still has to declare one.
mcp: {
description: 'Read runtime status.',
name: 'runtime_status',
readOnly: true,
server: 'runtime',
},
render: (result) => (
<Mcp.Result structuredContent={result}>
<Mcp.Text>{`Runtime is ${result.status}.`}</Mcp.Text>
</Mcp.Result>
),
});
export const config = {
annotations: { readOnlyHint: true },
description: 'Read runtime status.',
} satisfies ToolConfig;
export const inputSchema = z.object({ verbose: z.boolean().optional() }).strict();
export const resultSchema = z.object({ status: z.literal('ready') }).strict();

export default async function Status({ input, signal }: ToolRouteProps<typeof inputSchema>) {
if (signal.aborted) throw new DOMException('aborted', 'AbortError');
if (input.verbose) await agent();
const result = { status: 'ready' as const };
return <Agent.Result value={result}><Agent.Text>Runtime is ready.</Agent.Text></Agent.Result>;
}
```

Both projections run the identical pipeline —
`inputSchema.parse(input)` → `execute(input, { signal })` →
`resultSchema.parse(result)` — so inputs, implementation, and output
validation cannot drift between surfaces. Only the last step differs:

- **CLI** (`runRscCli`): `cli.parse` receives the arguments after the
command name and produces the input; the validated result is written to
stdout as one line of JSON (`JSON.stringify`), and `cli.exitCode(result)`
(default `0`) is returned to the entry, which sets it as the process exit
code. The CLI never touches `render` and never renders JSX.
- **MCP** (`createRscMcpServer`): the tool handler calls `render(result)`
and `lowerMcpResult` synchronously lowers the returned React element tree
(`Mcp.Result`, `Mcp.Text`, `Mcp.Image`, `Mcp.Audio`, `Mcp.ResourceLink`,
`Mcp.EmbeddedResource`) into a plain MCP `CallToolResult` object. The
lowering is strict — `Mcp.Text` takes exactly one string child, hence the
template literal above.

## Why `.tsx`, and current renderer status

The operation model shown above still uses the **synchronous MCP result DSL**:
`render` returns ordinary React elements and `lowerMcpResult` walks that tree
to produce the `CallToolResult` the MCP SDK sends. That compatibility path does
not involve Flight and remains the operative MCP projection.

Separately, `@agent-bundle/runtime` now exposes a final-only React-owned Flight
dispatcher for generated routes. An execution host supplies Flight bytes, the
dispatcher decodes intrinsic `Agent.*` elements into one immutable
`AgentDocument`, and cancellation follows the request `AbortSignal`. Streaming
Suspense replacement and public filesystem-route authoring are later stages.
Operations receive no implicit storage: persistent application state exists
only through the opt-in `@agent-bundle/runtime/state` kernel, which stateless
projects never import.

Operation modules are `.tsx` for exactly one reason: the `render` callback
returns JSX. Everything else in an operation — schemas, argv parsing, MCP
metadata — is plain TypeScript, and modules with no runtime JSX (such as an
application module that only composes operation arrays) stay `.ts`.

For a new reader, in one breath:

1. **What is an operation?** A host-neutral use-case definition — id, input
schema, `execute`, result schema, `render` — with optional CLI and MCP
projections.
2. **Which parts are shared by CLI and MCP?** The core four: `id`,
`inputSchema`, `execute`, `resultSchema` (plus the validation pipeline
around them).
3. **Which projection consumes `render`?** Only MCP, though every operation
must declare one. The CLI serializes the validated result as JSON.
4. **Is Flight involved in this operation projection?** No.
`lowerMcpResult` remains synchronous. The separate generated-route path uses
the final-only `AgentRenderDispatcher` described above.
5. **Why are operation modules `.tsx`?** Only because `render` returns JSX.

## Rendered skills (power tier, never required)

A skill whose document is generated: put `SKILL.tsx` (or `SKILL.ts`) in the
skill directory instead of `SKILL.md`. The module default-exports a component
and exports a `frontmatter` record; the build renders the tree to Markdown
and emits the `SKILL.md` every host consumes.

```tsx
// skills/deploy-checklist/SKILL.tsx
export const frontmatter = {
description: 'Deployment checklist.',
name: 'deploy-checklist',
};

export default () => (
<>
<h1>Deploy checklist</h1>
<p>Verify each step <strong>in order</strong>.</p>
</>
);
```

The renderer supports a documented element subset (`h1`–`h6`, `p`,
`ul`/`ol`/`li`, `strong`, `em`, `code`, `pre`, `blockquote`, `a`, `hr`,
`br`, fragments) and rejects anything outside it by name — never a silent
approximation. Components may be async, and may import project code, so the
document can be computed from the same sources the plugin ships. A
hand-authored `SKILL.md` in the same directory always wins (`AB4735`).

## Precedence, said once

Config wins, conventions fill. Declaring `skills:` in config replaces the
directory convention entirely (`AB4734` flags any directory left uncovered);
the same rule governs `bin`, `lib`, and MCP server entries.
The compiler statically reads `config`, imports schemas and implementations
only into generated entries, installs `runAgentRequest`, and derives the real
MCP server from the route graph. Each call renders through a warm internal
Flight dispatcher and lowers the final Agent Document to legal MCP output.
Flight is an implementation transport inside the generated runtime, never a
public host wire protocol.

Everything else is power-tier reference: custom/remote server modes and
collision recovery are in [Entry conventions](entry-conventions.md); accepted
static metadata, generated `.agent-bundle/routes.d.ts`, and diagnostics are in
[Diagnostics](diagnostics.md). Handwritten `src/mcp/<server>.ts`,
`defineOperation`, and `createRscMcpServer` remain supported escape hatches.
The handwritten CLI compatibility path still serializes validated results and
never renders JSX; routed CLI rendering belongs to #102 stage 3.
72 changes: 23 additions & 49 deletions examples/audiobook-curator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ pnpm example:audiobook

A complete TypeScript recreation of the original `audiobook-curator`, built in
framework mode: `agent-bundle.config.ts` plus file conventions declare the
structure, and one typed operation catalog produces a globally installable
CLI, one stdio MCP server, one Skill, and native Claude Code and Codex plugin
structure, and filesystem route modules produce one generated stdio MCP server, while a compatibility CLI remains globally installable, one Skill, and native Claude Code and Codex plugin
artifacts. JSX appears only where something is rendered — the MCP result
receipts. It has no hooks and does not call the old Python curator.

Expand Down Expand Up @@ -48,57 +47,32 @@ script, and lifecycle-wrapped MCP server) plus the npm package build beneath
`agent-bundle` and `@agent-bundle/runtime` exports with `workspace:*`
dependencies.

## Operation model

Every command is one `defineOperation` definition: a host-neutral use case —
`id`, input schema, `execute`, result schema, `render` — with two
projections declared beside it. The shared core runs identically on both
surfaces (`inputSchema.parse` → `execute` → `resultSchema.parse`); `cli`
adds argv parsing and exit codes, and `mcp` adds tool metadata. `render` is
a sibling of both, required on every operation but consumed only by the MCP
projection: the CLI prints each validated receipt as one line of JSON and
never renders JSX.

The runtime JSX for every operation is `<CuratorResult>` in
[`src/result.tsx`](src/result.tsx), which wraps the receipt in the MCP
result DSL:

```tsx
export const CuratorResult = ({ receipt }: { readonly receipt: CuratorReceipt }) => (
<Mcp.Result structuredContent={receipt}>
<Mcp.Text>{summary(receipt)}</Mcp.Text>
</Mcp.Result>
);
```
## Route model

The MCP application is the route tree under `src/mcp/curator/`: fifteen tool
modules plus one resource and one prompt. Every executable route exports static
`config`, `inputSchema`, `resultSchema`, and one async default Server Component
that executes the domain operation and renders `Agent.*`. The compiler derives
the `curator` server, lifecycle entry, warm Flight worker, and MCP registrations;
there is no `src/application.ts`, operation-array registry, handwritten
`src/mcp/curator.ts`, or per-operation server selector.

`lowerMcpResult` lowers that element tree synchronously into an MCP
`CallToolResult`. No React Server Components renderer or Flight transport is
involved anywhere in this example; operation modules are `.tsx` only because
`render` returns JSX, and `src/application.ts` stays `.ts` because it merely
composes the operation arrays. The end-to-end walkthrough is in
[Framework mode](../../docs/framework-mode.md).
The existing handwritten CLI remains a compatibility escape hatch until routed
CLI rendering in #102 stage 3. It uses the same domain helpers but still prints
validated JSON directly and never renders JSX.

## Source layout

- `agent-bundle.config.ts` — the structure: plugin identity, targets, the CLI
script, and the MCP server (whose entry is the `src/mcp/curator.ts`
convention). The Skill needs no declaration at all:
`skills/curate-audiobooks/SKILL.md` ships by convention.
- `src/application.ts` — composition only: merges the feature modules'
defaults into one `defineRscApplication` operation catalog.
- `src/operations/` — the operation catalog, grouped by workflow stage:
`discovery` (inspect/inventory/library-audit/select), `audible`
(search/select/cache), `evidence` (acoustic/whisper), `media-mutation`
(apply-metadata/apply-chapters), and `output` (convert/prepare/audit), with
shared `cli-arguments.ts` and `schemas.ts`.
- Domain logic lives beside them in `src/` (`library.ts`, `audible.ts`,
`evidence.ts`, `conversion.ts`, `media-mutation.ts`, `integrity-audit.ts`,
`curator-core.ts`) over the shared `foundation.ts` and `media-process.ts`
primitives; `result.tsx` renders every receipt for MCP.
- `src/cli.ts` exports `main`; the framework's generated process envelope
turns it into both the bundled script artifact entry and the npm bin.
`src/mcp/curator.ts` default-exports a server factory served under the
framework's stdio lifecycle shell. No hand-written entry shims remain.
- `agent-bundle.config.ts` — plugin identity, selected targets, and the bundled
CLI script; MCP needs no declaration.
- `src/mcp/curator/tools/` — one single-file route per MCP tool.
- `src/mcp/curator/resources/catalog.tsx` and `prompts/curate.tsx` — the routed
resource and prompt proofs.
- `src/operations/` — CLI-only compatibility command data and shared schemas; MCP
metadata and server strings do not live here.
- Domain logic remains in `src/` over `foundation.ts` and `media-process.ts`;
`result.tsx` renders route receipts as Agent Documents.
- `src/cli.ts` and `src/index.ts` keep the package bin/library conventions.

## Complete workflow

Expand Down
8 changes: 0 additions & 8 deletions examples/audiobook-curator/agent-bundle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ import { defineConfig } from 'agent-bundle/config';

export default defineConfig({
marketplace: true,
mcp: {
servers: {
// No `entry` needed: the conventional stdio entry `src/mcp/curator.ts`
// supplies it, and its default-exported factory runs under the
// framework lifecycle shell.
curator: {},
},
},
plugin: {
description:
'Complete plan-first audiobook inventory, matching, conversion, repair, and integrity audit.',
Expand Down
2 changes: 1 addition & 1 deletion examples/audiobook-curator/docs/parity-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ adapter coverage.
| Sources are immutable; mutation is plan-only without explicit `apply` | mutation foundation | real/synthetic before-and-after hashes |
| No shell execution; bounded output; caller cancellation; no local media deadline | capability/process foundation | child-process tests |
| Natural ordering, Unicode-safe identity, safe filenames without apostrophes | domain text foundation | ported pure tests |
| Claude and Codex derive Skill, script, and MCP from one config plus conventions | `agent-bundle.config.ts`, `src/application.ts` | artifact and installed-host tests |
| Claude and Codex derive Skill, script, and MCP from one config plus conventions | `agent-bundle.config.ts`, `src/mcp/curator/` route tree | artifact and installed-host tests |

## Operations

Expand Down
Loading
Loading