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
6 changes: 6 additions & 0 deletions .changeset/remove-legacy-entry-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"agent-bundle": minor
"create-agent-bundle": patch
---

Require every local stdio MCP entry to default-export a server factory: self-connecting entries no longer build and AB4730 is now an error instead of an informational nudge; retire AB4736, so documents left in the top-level `skills/`, `commands/`, and `rules/` locations are ignored rather than reported (#839)
60 changes: 30 additions & 30 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ even when no error diagnostic was reported.
| `AB470x` | Package build `bin` configuration (`AB4700`–`AB4705`; `AB4706`: artifact output overlaps `dist`; `AB4707`: `output` shape plus `output.distPath` string and `output.sourceMap` boolean types; `AB4708`–`AB4709`: `output.distPath` root escape and reserved namespace); see below. |
| `AB471x` | Package build `lib` configuration (`AB4710`–`AB4715`) and declaration generation (`AB4716`); see below. |
| `AB472x` | The `tools.rsbuild` / `tools.rspack` escape hatch (`AB4720`–`AB4723`: shape; `AB4724`: a framework-owned Rsbuild plugin re-added through `tools.rsbuild.plugins`; `AB4725`: `tools` externalizes a non-built-in; `AB4726`: a deprecated Rsbuild v2 configuration key; see below). |
| `AB473x` | Migration nudges (informational; see below). |
| `AB473x` | Entry conventions: `AB4730` and `AB4737`–`AB4738` are errors, `AB4731`–`AB4735` are informational shadowing nudges, and `AB4736` is retired (see below). |
| `AB4740`–`AB4751` | Prebuilt payloads and prebuilt entries (see below). |
| `AB4760` | The published `agent-bundle/meta` identity module evaluated outside every compiled surface and outside the Rstest presets (see below). |
| `AB4765`–`AB4768` | Artifact-hosted routed CLI and npm lifecycle paths: a target without the `cli` capability omits `bin/<name>.mjs`; a host-emitted file collides with it; an npm root cannot select a routed CLI absent from the manifest; or a consumer lifecycle names an unsupported or absent Node path (see below). |
Expand Down Expand Up @@ -617,18 +617,19 @@ development-only fallback can never produce a release artifact, so
| `AB4014` | error | A `plugin.metadata` field is not the shape the shared descriptive layer accepts, or the block declares a field beyond `author`, `homepage`, `keywords`, `license`, and `repository`. The config declared it, so it is an error rather than a withheld value, a blank string or empty array included, where `null` is how a field is opted out. |
| `AB4015` | warning | A `package.json` descriptive field cannot be shared with any host manifest, a `homepage`, `repository`, or `author.url` the pinned host schemas' `uri` format refuses, an `author.email` their `email` format refuses, or a `repository` in a form this compiler will not convert (`owner/repo` and `github:` shorthands, `git@`/`git://`/`git+ssh`/`git+http` URLs; only `http(s)` and the `git+https://…` URL npm writes, with or without a trailing `.git`, are read). An `author` with any malformed part is withheld whole. The field is withheld rather than guessed at; declare `plugin.metadata.<field>` to share an explicit value. A field the config already overrides is not reported. |

## Migration nudges and convention claims (`AB4730`–`AB4738`)
## Entry conventions and convention claims (`AB4730`–`AB4738`)

The entry conventions and the framework-owned stdio lifecycle shell (RFC #50)
replaced patterns consumers previously wrote by hand. When `validate`,
`inspect`, `build`, or `dev` prepares project source and finds one of those
pre-convention patterns, it reports a migration diagnostic. `AB4730`–`AB4735`
are **informational** nudges and never block anything. `AB4736`–`AB4738` are
errors: the removed top-level authored-document locations are no longer
discovered, and a conventional script whose `bin` entry would run an export
the artifact script ignores cannot ship on both surfaces, so the compiler
refuses to omit or misbuild them silently. The CLI prints these in
human `validate` output and includes them in every `--json` diagnostics array.
define how a project's modules reach an artifact. When `validate`, `inspect`,
`build`, or `dev` prepares project source, it reports what the conventions
refuse and what they silently shadow. `AB4730`, `AB4737`, and `AB4738` are
**errors**: an entry the framework cannot wrap, and a conventional script
whose `bin` entry would run an export the artifact script ignores, cannot
ship, so the compiler refuses to misbuild them silently. `AB4731`–`AB4735`
are **informational** nudges for a confusable state where explicit
configuration shadows a conventional file on disk, and never block anything.
`AB4736` is retired. The CLI prints these in human `validate` output and
includes them in every `--json` diagnostics array.

Which explicit config keys *claim* a conventional module out of discovery is
tabulated in `docs/entry-conventions.md` ("Which config keys claim a
Expand All @@ -639,17 +640,19 @@ keeps shipping as an artifact script beside the bin because the two outputs
are disjoint and both envelopes run the same `main`. That dual-surface shape
is intentional and raises no diagnostic.

### `AB4730` self-connecting stdio MCP entry
### `AB4730` stdio MCP entry without a server factory

A local MCP server entry module (explicit `entry:` or the conventional
`src/mcp/<server-id>.ts`) has no default export, so the build bundles it
byte-for-byte instead of wrapping it in the framework stdio lifecycle shell
(console-to-stderr guard, SIGINT/SIGTERM, stdin-EOF exit, bounded shutdown,
heartbeat). The detection is the same static default-export scan the build
uses, so the nudge and the build always agree.
`src/mcp/<server-id>.ts`) has no default export. Every local entry is wrapped
in the framework stdio lifecycle shell (console-to-stderr guard,
SIGINT/SIGTERM, stdin-EOF exit, bounded shutdown, heartbeat), and the shell
calls the module's default export to build the server, so a module without
one cannot be built. The detection is the same static default-export scan the
build uses, so the diagnostic and the build always agree.

Adopt: default-export a server factory from the entry module. Silence: keep
the self-connecting entry, its behavior is preserved exactly.
Recover: default-export the server factory from the entry module, or declare
a prebuilt server with `command` or `url`, which the framework launches
as-is and never wraps.

### `AB4731` `src/cli.ts` shadowed by explicit `bin` config

Expand Down Expand Up @@ -696,18 +699,15 @@ document beats a generated one, so the component module never compiles.
Adopt: remove `SKILL.md` so the rendered skill compiles at build. Silence:
remove the component module.

### `AB4736` legacy top-level authored document location
### `AB4736` retired

A document still matches a removed top-level convention:
`skills/<name>/SKILL.md` (or rendered `SKILL.tsx`/`SKILL.ts`),
`commands/*.md`, or `rules/*.mdc`. These locations are no longer discovered,
and every unignored legacy document is reported as an error. A top-level
skill covered by explicit `skills` configuration is claimed and stays valid;
commands and rules have no equivalent override.

Recover: move the document under `src/skills/`, `src/commands/`, or
`src/rules/`. Explicit `skills` paths remain valid anywhere. Published
artifact paths remain `skills/`, `commands/`, and `rules/`.
The report of documents left in the removed top-level locations
(`skills/<name>/SKILL.md`, `commands/*.md`, `rules/*.mdc`). Discovery reads
`src/skills/<name>/SKILL.md`, `src/commands/*.md`, and `src/rules/*.mdc`
only, so a document at the top level is now ignored without a diagnostic. An
explicit `skills` path still names a skill directory anywhere in the project,
including the top level. Published artifact paths remain `skills/`,
`commands/`, and `rules/`. The code is never reused.

### `AB4737` rendered script claimed as a package bin entry lacks `main` or the component

Expand Down
43 changes: 22 additions & 21 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -709,15 +709,16 @@ forwards the real server's `none`, and a plain script's `main` receives the
real child process's probe (two pipes). A test that wants other values injects
`context.terminal` through the same seam as every identity axis.

### Migration nudges
### Convention diagnostics

Source validation reports **informational** nudges (never errors, migrations
stay optional) when a project exhibits a pre-convention pattern: `AB4730` for
a self-connecting stdio entry that a default-exported factory would upgrade
to the framework lifecycle shell, and `AB4731`/`AB4732`/`AB4733` when
`src/cli.ts`, `src/index.ts`, or `src/mcp/<server-id>.ts` exists but explicit
configuration shadows it. `bin: false` / `lib: false` opt-outs stay silent.
See `docs/diagnostics.md` for each trigger and how to adopt or silence it.
Source validation reports `AB4730` as an **error** when a local stdio MCP
entry has no default export: the framework lifecycle shell calls that export
to build the server, so such a module cannot be built. It reports
**informational** nudges (never errors) when `src/cli.ts`, `src/index.ts`, or
`src/mcp/<server-id>.ts` exists but explicit configuration shadows it
(`AB4731`/`AB4732`/`AB4733`). `bin: false` / `lib: false` opt-outs stay
silent. See `docs/diagnostics.md` for each trigger and how to recover from or
silence it.

## Generated entry shells

Expand Down Expand Up @@ -1051,8 +1052,8 @@ server still passes `kind: 'tool'`.

### The stdio MCP lifecycle shell

An MCP server entry that **default-exports a server factory** is served under
the framework lifecycle:
Every local MCP server entry **default-exports a server factory** and is
served under the framework lifecycle:

```ts
// src/mcp/curator.ts — the whole stdio entry a consumer writes
Expand All @@ -1071,15 +1072,15 @@ race against wedged transports, and heartbeat/activity logging on stderr
(5-minute interval, 60-second activity throttle, labeled with the server
name).

Self-connecting entries, modules that construct and connect a transport at
top level without a default export, keep today's behavior byte for byte: no
lifecycle shell and no operator `.env` layer (#469); an entry that wants the
layer calls `applyOperatorEnv` from `agent-bundle/launch-env` itself,
passing its own declared `env` block as `manifestEnv` if a passed-through
manifest default should yield to the file as it does in the generated shell.
That module is aliased into every stdio entry, shell or not, so the import is
inlined from this package rather than resolved through the plugin's own
`node_modules`, and a `tools` hatch can never externalize it.
A module that constructs and connects a transport at top level without a
default export cannot be built: source validation reports `AB4730` as an
error. A server the framework should launch as-is instead of compiling is
declared with `command` or `url`, or as a `{ prebuilt: ... }` entry the
consumer's own build produced. The operator `.env` layer (#469) comes from
`agent-bundle/launch-env`, which the shell's prelude applies; that module is
aliased into every stdio entry, so the import is inlined from this package
rather than resolved through the plugin's own `node_modules`, and a `tools`
hatch can never externalize it.

Every served tool call is one ordinary `tools/call`: optional
`notifications/progress` while the caller's progress token is live, then one
Expand Down Expand Up @@ -1456,8 +1457,8 @@ canonical precedence order (highest wins):
| 1 (lowest) | Manifest env | Entries declared in the server config plus the injected plugin-root anchor, path tokens expanded. |

Installed packs get the same layer and the same order without `mcp run`
(#469): every artifact shell that runs plugin code, the stdio MCP entry of a
factory-exporting server (a self-connecting entry has no shell), the hook
(#469): every artifact shell that runs plugin code, the stdio MCP entry of
every local server, the hook
wrappers that execute handlers or render standalone, and the artifact CLI
`bin/<name>.mjs`, applies `agent-bundle/launch-env` (`src/launch-env.ts`,
plain Node, inlined into the bundle) at startup. It reads `<plugin
Expand Down
6 changes: 3 additions & 3 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,9 @@ alias a custom runner must add; it never reports a fabricated identity.
`src/skills/<name>/SKILL.md` ships with no declaration. Config wins,
conventions fill: declaring `skills:` replaces the directory convention
entirely, and validation reports `AB4734` for any conventional skill directory
the explicit list leaves uncovered. Skills at the removed top-level
`skills/<name>/` location are an `AB4736` error unless explicit `skills`
config claims them.
the explicit list leaves uncovered. Discovery reads `src/skills/` only, so a
directory at the removed top-level `skills/<name>/` location is ignored
unless an explicit `skills` path names it.

A skill whose document is generated (power tier, never required) puts
`SKILL.tsx` (or `SKILL.ts`) in the skill directory instead of `SKILL.md`. The
Expand Down
49 changes: 25 additions & 24 deletions fixtures/integration/comprehensive/src/mcp-server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';

import apps from 'agent-bundle/mcp-apps';

Expand All @@ -8,31 +7,33 @@ import { localMcpMessage } from './mcp-local.ts';
const app = apps[0];
if (app === undefined) throw new Error('Expected an integration MCP App.');

const server = new McpServer({ name: 'integration-mcp', version: '1.0.0' });
export default function createIntegrationServer(): McpServer {
const server = new McpServer({ name: 'integration-mcp', version: '1.0.0' });

server.registerResource(app.name, app.resourceUri, {
_meta: {
ui: {
...(typeof app._meta?.ui === 'object' && app._meta.ui !== null ? app._meta.ui : {}),
resourceUri: app.resourceUri,
server.registerResource(app.name, app.resourceUri, {
_meta: {
ui: {
...(typeof app._meta?.ui === 'object' && app._meta.ui !== null ? app._meta.ui : {}),
resourceUri: app.resourceUri,
},
},
},
mimeType: app.mimeType,
}, async (uri) => ({
contents: [{
mimeType: app.mimeType,
text: app.html,
uri: uri.href,
}],
}));
}, async (uri) => ({
contents: [{
mimeType: app.mimeType,
text: app.html,
uri: uri.href,
}],
}));

server.registerTool('show-dashboard', {
_meta: { ui: { resourceUri: app.resourceUri } },
description: 'Returns the integration MCP App.',
}, async () => ({
_meta: { ui: { resourceUri: app.resourceUri } },
content: [{ text: `dashboard ready: ${localMcpMessage}`, type: 'text' }],
structuredContent: { resourceUri: app.resourceUri, view: app.name },
}));
server.registerTool('show-dashboard', {
_meta: { ui: { resourceUri: app.resourceUri } },
description: 'Returns the integration MCP App.',
}, async () => ({
_meta: { ui: { resourceUri: app.resourceUri } },
content: [{ text: `dashboard ready: ${localMcpMessage}`, type: 'text' }],
structuredContent: { resourceUri: app.resourceUri, view: app.name },
}));

await server.connect(new StdioServerTransport());
return server;
}
35 changes: 18 additions & 17 deletions fixtures/integration/packed-release/src/mcp-server.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';

import apps from 'agent-bundle/mcp-apps';

const app = apps[0];
if (app === undefined) throw new Error('Expected the packed-release MCP App.');

const server = new McpServer({ name: 'packed-release-mcp', version: '1.0.0' });
export default function createPackedReleaseServer(): McpServer {
const server = new McpServer({ name: 'packed-release-mcp', version: '1.0.0' });

server.registerResource(app.name, app.resourceUri, {
_meta: { ui: { resourceUri: app.resourceUri } },
mimeType: app.mimeType,
}, async (uri) => ({
contents: [{ mimeType: app.mimeType, text: app.html, uri: uri.href }],
}));
server.registerResource(app.name, app.resourceUri, {
_meta: { ui: { resourceUri: app.resourceUri } },
mimeType: app.mimeType,
}, async (uri) => ({
contents: [{ mimeType: app.mimeType, text: app.html, uri: uri.href }],
}));

server.registerTool('show-dashboard', {
_meta: { ui: { resourceUri: app.resourceUri } },
description: 'Returns the packed-release MCP App.',
}, async () => ({
_meta: { ui: { resourceUri: app.resourceUri } },
content: [{ text: 'packed dashboard ready', type: 'text' }],
structuredContent: { resourceUri: app.resourceUri, view: app.name },
}));
server.registerTool('show-dashboard', {
_meta: { ui: { resourceUri: app.resourceUri } },
description: 'Returns the packed-release MCP App.',
}, async () => ({
_meta: { ui: { resourceUri: app.resourceUri } },
content: [{ text: 'packed dashboard ready', type: 'text' }],
structuredContent: { resourceUri: app.resourceUri, view: app.name },
}));

await server.connect(new StdioServerTransport());
return server;
}
Loading
Loading