Repository navigation
Compile filesystem routes into the Agent Bundle meta-framework #93
Description
Summary
Agent Bundle already owns configuration normalization, host artifact generation,
Rslib builds, Rsbuild-based MCP App compilation, generated lifecycle shells,
virtual modules, Workbench development, and Rstest verification. The missing
framework feature is the application compiler between project source and those
existing build/runtime layers.
Today, framework mode discovers package-level entries such as src/cli.ts and
src/mcp/<server>.ts, but individual MCP tools, resources, prompts, CLI
commands, and rendered operations are assembled manually at runtime. Projects
must import every operation, concatenate registries, repeat server identifiers,
and write thin MCP/CLI entry modules.
Add a convention-driven route compiler. The framework supplies standard source
roots and infers servers and route kinds from paths. Root configuration is only
for project-wide policy and explicit overrides. A route is one module:
its filesystem path supplies stable identity, its optional named config export
adds protocol metadata, and its remaining exports provide schemas, execution,
and React rendering. The compiler validates the complete route graph, generates
types and virtual registries, and feeds the existing Rslib/Rsbuild build paths.
This deliberately supersedes the prior 2026-08-26 design decision that listed
a filesystem router as a non-goal.
Existing capabilities to preserve
This issue does not propose rebuilding capabilities that already exist:
agent-bundleandcreate-agent-bundleare built with Rslib.- Workbench and browser-facing MCP Apps are built with Rsbuild.
- The repository already uses Rstest for unit, integration, browser, packed,
and native-smoke coverage. - Explicit config already overrides conventional package entries.
- Generated lifecycle shells already own stdio startup, stdout protection,
cancellation, shutdown, and heartbeat behavior. - Generated modules already ride the Rslib build through protected virtual
module entries. - MCP Apps already compile into self-contained HTML and a generated registry.
- Project snapshots and the development watcher already detect source additions,
edits, and deletions.
The compiler should extend these mechanisms rather than create a parallel build
system or expose Rspack as a product API.
Problem
The current Audiobook Curator illustrates the gap:
agent-bundle.config.tsdeclares a server namedcurator.src/application.tsmanually imports five operation families and joins their
arrays.- each operation repeats its MCP server assignment and listing metadata;
src/mcp/curator.tsrepeats the server name and calls
createRscMcpServer;src/cli.tsmanually callsrunRscCli;- MCP Apps still require explicit resource registration in a custom server.
This is convention-driven packaging around a runtime registry, not a complete
meta-framework authoring model.
Goals
- Make the project tree, route-module exports, and
agent-bundle.config.tsthe
complete application declaration. - Remove hand-authored operation arrays, application registries, server
selector strings, and framework lifecycle entry modules. - Generate one validated route graph used by build, dev, type generation,
Workbench, CLI, MCP, scripts, tests, and provenance. - Keep public metadata in a statically discoverable route-module export rather
than hiding it inside runtime registration calls. - Fail at build time for missing implementations, duplicate routes, unsafe
names, ambiguous custom-server/route-server combinations, or unsupported
target capabilities. - Preserve custom MCP server entries as an explicit escape hatch.
Non-goals
- A bundler-neutral compiler or adapter ecosystem.
- A direct Rspack authoring surface.
- Replacing Rslib, Rsbuild, Rstest, host adapters, or the current artifact
model. - Requiring a sidecar config file for every route.
- Dynamic web-style URL parameters in the first filesystem-routing release.
Proposed source structure
src/
mcp/
curator/
tools/
inspect.tsx
inventory.tsx
convert.tsx
resources/
catalog.ts
book.ts
prompts/
curate.tsx
apps/
dashboard.tsx
cli/
doctor.tsx
scripts/
rebuild-index.ts
summarize-library.tsx
events/
file/
saved.tsx
providers/
git-worktree.ts
These are built-in framework conventions, not paths that every project must
repeat in config:
src/mcp/<server>/tools/*
src/mcp/<server>/resources/*
src/mcp/<server>/prompts/*
src/mcp/<server>/apps/*
src/events/*
src/providers/*
src/cli/*
src/scripts/*
For example, src/mcp/curator/tools/inspect.tsx is enough to declare the
inspect tool on the generated curator MCP server. Adding that file is the
declaration. No mcp.servers.curator.root, hooks.root, cli.root, or
scripts.root setting is required.
The conventional root config stays small:
import { defineConfig } from 'agent-bundle/config';
export default defineConfig({
plugin: {
description: 'Evidence-backed audiobook curation.',
name: 'audiobook-curator',
},
targets: ['claude', 'codex'],
});Root/path configuration exists only as an escape hatch for a nonstandard source
tree, a remote or prebuilt MCP server, a custom transport, bounded discovery,
target restrictions, disabling a convention, or another explicit override.
Route-module contract
Each route is a single module. Its path supplies kind, owning surface, and
identity. An optional named config export supplies additional protocol
metadata and overrides. There is no tool.config.ts, resource.config.ts,
prompt.config.ts, app.config.ts, or hook.config.ts sidecar convention.
This follows the route-module style used by React Router: recognized exports
live beside the route implementation, while the root config remains
project-wide. The compiler extracts recognized exports through a controlled
build transform. V1 accepts only statically analyzable literal/config-helper
forms; it never executes a route handler to discover metadata. Unsupported
dynamic config produces a build diagnostic rather than an incomplete manifest.
// src/mcp/curator/tools/inspect.tsx
export const config = {
description: 'Inspect audiobook sources without changing them.',
annotations: { readOnlyHint: true },
title: 'Inspect audiobook sources',
} satisfies ToolConfig;Root config owns project-wide concerns:
- plugin identity and convention overrides;
- server transports and host targets;
- shared defaults, policies, and bounded overrides;
- build outputs and framework plugins.
The route module's optional config export owns route-specific static details,
such as tool descriptions and annotations, resource URIs, prompt descriptions,
App resource identity, hook filters, and timeouts. When config is absent, the
compiler uses only honest path-derived defaults; it does not fabricate a
description or capability claim.
Other recognized route exports own executable behavior:
- input and result schemas;
- loading or execution;
- React rendering;
- explicit capabilities injected by the framework;
- route-local error mapping where needed.
// src/mcp/curator/tools/inspect.tsx
import { Agent } from '@agent-bundle/runtime';
import { z } from 'zod';
export const config = {
description: 'Inspect audiobook sources without changing them.',
annotations: { readOnlyHint: true },
title: 'Inspect audiobook sources',
} satisfies ToolConfig;
export const inputSchema = z.object({
maxFiles: z.number().int().min(1).max(256).optional(),
root: z.string(),
});
export default async function Inspect({ input, signal }: ToolRouteProps<typeof inputSchema>) {
const receipt = await inspectSources(input, { signal });
return (
<Agent.Result value={receipt}>
<Agent.Markdown>
Inspected **{receipt.files.length}** audiobook files.
</Agent.Markdown>
</Agent.Result>
);
}No defineOperation, operation array, application registry, manual
registerTool, createRscMcpServer, or server selector is required.
Declaration and discovery rules
- A recognized file beneath a standard framework root is an application
declaration; its path declares route kind, server, and identity. Root config
is unnecessary for the standard layout. - A route may optionally export
config. The compiler validates it according
to the route kind and includes it in the semantic manifest. - Required executable exports are route-kind-specific and validated at build
time. The compiler never imports a route merely to discover whether it is a
tool, resource, prompt, App, event, CLI command, script, or provider. include/excludeglobs, private filename conventions, and ignored paths
may bound discovery. Files outside those rules do not ship.- An explicit per-route override in root config may redirect or disable a
conventional route and produces a shadowing diagnostic when applicable. - A server is in exactly one mode:
- generated routes;
- custom local entry;
- command/prebuilt entry;
- remote URL.
- Generated routes and a custom server entry may not silently coexist.
- Apps-only servers may be generated automatically; authors should not write
an otherwise-empty server merely to register compiled resources. - Route discovery uses the repository's existing sorted globbing, ignore,
provenance, and safe-path machinery. - Existing
src/mcp/<server>.tsconflicts with route-mode
src/mcp/<server>/; existingsrc/cli.tsconflicts with routedsrc/cli/.
Migration requires an explicit mode/override, and the compiler never
silently chooses one.
Compiler output
The compiler produces an immutable route graph containing:
interface CompiledAgentRoute {
kind: 'tool' | 'resource' | 'prompt' | 'app' | 'event-route' | 'cli' | 'script';
id: string;
source: string;
serverId?: string;
inputTypeModule?: string;
config: Readonly<Record<string, unknown>>;
provenance: RouteProvenance;
}The same compiler IR owns one capability-state contract used by route
validation, projectors, events, and host components:
type CapabilityState =
| { state: 'supported'; evidence: CapabilityEvidence }
| { state: 'degraded'; reason: string; evidence?: CapabilityEvidence }
| { state: 'unavailable'; reason: string }
| { state: 'prohibited'; reason: string };This evolves the existing adapter capability lookup; it is not a second
capability registry. A required route fails before packaging unless all
selected targets support it or the route is explicitly target-restricted.
From this graph it generates:
- route-specific TypeScript types;
- one virtual server entry per generated MCP server;
- CLI and script registries;
- semantic event and context-provider manifests;
- Workbench capability/catalog data;
- build and artifact provenance inputs;
- exact host manifests through the existing adapters;
- compiler-aware Rstest fixtures.
The generated MCP server entry:
- imports the discovered route implementations;
- constructs the real MCP server using config/plugin identity;
- registers tools, resources, prompts, and compiled Apps;
- binds the Agent renderer and transport projector;
- exports a server factory consumed by the existing lifecycle shell.
Development behavior
- Adding, removing, or renaming a discovered route invalidates the route graph.
- Editing a route recompiles only its owning entry and affected generated
registries. - Workbench receives the generated capability catalog rather than inferring
capabilities from unrelated runtime endpoints. - Invalid source retains the last good active build, matching current project
and runtime-generation behavior. - Generated types are deterministic and derived from the same graph used to
compile artifacts.
Migration
- Keep explicit handwritten entries available as a documented escape hatch;
compatibility duration and deprecation policy are decided separately. - Add route mode to one server at a time.
- Move each operation into one route module with recognized exports and an
optional namedconfigexport. - Generate server and CLI entries after the final manual registration leaves.
- Convert Audiobook Curator into the reference framework-mode application.
- Update
create-agent-bundletemplates to generate single-file route modules. - Deprecate manual operation registries after route mode proves parity.
Acceptance criteria
- A project can declare tools, resources, prompts, and Apps by adding route
modules beneath conventional roots and ship them without a hand-authored MCP
server factory. - A project can delete source after building and the packed artifact still
lists, calls, reads, and retrieves prompts through a real MCP client. - Generated route types, Workbench catalogs, host manifests, and runtime
registration all derive from the same immutable route graph. - Route
configmetadata is semantically equal after the selected protocol
SDK's documented wire serialization and omission rules. - Route changes invalidate dev output and provenance deterministically.
- Custom server mode continues to support applications outside generated-route
conventions. - Audiobook Curator no longer contains
src/application.ts, operation arrays,
repeatedmcp.serverstrings, or a one-linesrc/mcp/curator.tsadapter. - No generated-route project needs per-route sidecar config files or duplicate
per-tool declarations inagent-bundle.config.ts. - A conventional project does not configure the standard MCP, event, provider,
CLI, or script roots; adding a recognized source file is sufficient.
Design references
- Current Agent Bundle framework-mode boundary
- Current deterministic discovery and shadowing machinery
- Current entry convention normalization
- Current virtual entry generation
- Current adapter capability and evidence model
- Audiobook Curator's current manual application registry
- React Router route modules and filesystem-route convention
- Rsbuild Plugin React Router framework-mode compiler precedent
- Rslib and Rsbuild share the same plugin system
Stack position
- Foundational issue; no other issue in this stack is a prerequisite.
- Unlocks: #95 request context, #96 RSC rendering, #97 semantic events, #102 CLI/scripts, #100 host components, and #103 Rstest.
- Existing handwritten entries remain supported as explicit escape hatches.
Full meta-framework stack
- Foundations: #93 routes - #94 identity - #95 context - #96 renderer
- Runtime: #97 events - #98 state - #99 notices
- Host: #100 components - #101 lifecycle
- Surfaces and proof: #102 CLI/scripts - #103 Rstest - #104 proximity example - #105 Workbench
Shared generated runtime topology
Generated routes should not create an unrelated execution stack per surface.
The route graph also produces a shared server-runtime plan:
host MCP client ── tools/call ─┐
├─ generated Agent runtime
thin hook client ─ render RPC ┘ ├─ request/context providers
├─ optional process state
└─ RSC/Flight render dispatcher
The generated MCP entry may host both the public MCP server and the internal
Flight render dispatcher. A tool invocation validates MCP input, supplies it as
typed route props, performs one Flight render, decodes the final Agent Document,
and projects it into a standards-valid CallToolResult. A generated hook
wrapper may reuse the same runtime through a compiler-owned local transport,
or through a native MCP-tool hook when the host exposes an equivalent contract.
The compiler owns runtime discovery, handshake, artifact/protocol identity,
cancellation, deadlines, and shutdown. Applications do not construct query
strings, local sockets, or MCP clients manually. If a host cannot connect a
hook to the shared runtime, inspect reports the degraded execution mode and the
compiler either emits an explicit standalone fallback or rejects a route that
requires shared process state.
MCP and Flight remain different layers. MCP is the public host protocol and may
also supply the host-managed process lifecycle. Flight is the internal React
render protocol. The final edge remains a legal MCP result or native hook
response; raw Flight bytes are never presented to a host that did not negotiate
them.
Activity
feat(routes): port the config-claim rule from #116 and serve the prep…
refactor(routes): use the shared cycle-safe deepFreeze and fold the d…
feat: compile route-graph IR and inspect focus (#93) (#119)
ScriptedAlchemy commentedon Sep 1, 2026
Wave 1 Lane B PR-1 (route-compiler substrate) has landed via #119, squash-merged as 7857add.
Reconciliation with the closed duplicate #116 is complete: the config-claim rule (explicit
scripts/hooks/bin/lib/mcpdeclarations claim their modules, so they never become conventional routes) was ported into #119'sgraph.tswithpnpm examples:checkas proof thatexamples/hooks-and-scriptsandexamples/mcp-appbuild unchanged; the AB480x diagnostic codes are reconciled and documented indocs/diagnostics.md; the inspect--routesfocus reuses the graph compiled during preparation instead of recompiling (Codex finding); hostile config getters fail closed to the AB7001 validation path instead of aborting discovery.Per owner policy, the merge bar was scoped local tests (1811 unit + integration api/public-api all green at the merged head); hosted CI runs post-merge with one comprehensive pass at end of Wave 1.
Next: PR-2, the static
config-export extractor (TS/TSX parse, documented accepted-expression grammar, named dynamic-config diagnostics, fixtures, inspect surfacing), branching from post-merge main and building on the shippedCompiledRouteGraphtypes.feat: statically extract route config exports into the route-graph IR (…
ScriptedAlchemy commentedon Sep 1, 2026
Wave 1 Lane B PR-2 (static
config-export extractor) has landed via #135, squash-merged as a7d42cf.Every discovered route module's
export const config = <expression>declaration is now statically extracted into the route-graph IR (real TS/TSX parse, module never executed), with the accepted expression grammar documented indocs/diagnostics.mdand named diagnostics for everything outside it:AB4805for rejected declaration shapes,AB4806for dynamic initializers naming the offending construct and position. The graph digest covers extracted configs andinspect --routessurfaces them per route. Still consumer-invisible.That completes the #93 Wave 1 substrate scope for Lane B: route discovery + immutable IR + collision/mode diagnostics (PR-1, #119) and the config extractor (PR-2, #135). Remaining #93 work (generated server entries, public authoring surface) is out of Wave 1 scope per the plan.
ScriptedAlchemy commentedon Sep 3, 2026
Closed-issue re-audit (G3) — VERIFIED. Closed at the #150 merge without a closing ledger, so recording one: route discovery/IR/collision diagnostics (#119), static
configextractor (#135), generated servers through Flight (#147), audiobook migration (#150) are all onorigin/main. Conventional roots arepackages/agent-bundle/src/routes/graph.ts:48-57;AB4800–AB4825documented;examples/audiobook-curatorhas nosrc/application.ts, nosrc/mcp/curator.ts, nodefineOperation/createRscMcpServer/runRscCli, andtests/application.test.tsderives the full 16-tool server + 30-command graph from route modules alone (run green: 40 + 31 route-unit); themcp-serverscaffold template is a single-file route module (src/mcp/status/tools/report-status.tsx). Packed deleted-source proof (packed-stdio-projection) green.Only migration step 7 ("deprecate manual operation registries") is not done:
defineOperation/defineRscApplication/createRscMcpServer/runRscClicarry no@deprecatedanddocs/framework-mode.mdcalls them supported escape hatches — consistent with this issue's own step 1 ("compatibility duration and deprecation policy are decided separately") and the #107 ledger. Left as a policy decision, not a defect.