Skip to content

Compile filesystem routes into the Agent Bundle meta-framework #93

Description

@ScriptedAlchemy

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-bundle and create-agent-bundle are 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.ts declares a server named curator.
  • src/application.ts manually imports five operation families and joins their
    arrays.
  • each operation repeats its MCP server assignment and listing metadata;
  • src/mcp/curator.ts repeats the server name and calls
    createRscMcpServer;
  • src/cli.ts manually calls runRscCli;
  • 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

  1. Make the project tree, route-module exports, and agent-bundle.config.ts the
    complete application declaration.
  2. Remove hand-authored operation arrays, application registries, server
    selector strings, and framework lifecycle entry modules.
  3. Generate one validated route graph used by build, dev, type generation,
    Workbench, CLI, MCP, scripts, tests, and provenance.
  4. Keep public metadata in a statically discoverable route-module export rather
    than hiding it inside runtime registration calls.
  5. Fail at build time for missing implementations, duplicate routes, unsafe
    names, ambiguous custom-server/route-server combinations, or unsupported
    target capabilities.
  6. 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

  1. 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.
  2. A route may optionally export config. The compiler validates it according
    to the route kind and includes it in the semantic manifest.
  3. 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.
  4. include/exclude globs, private filename conventions, and ignored paths
    may bound discovery. Files outside those rules do not ship.
  5. An explicit per-route override in root config may redirect or disable a
    conventional route and produces a shadowing diagnostic when applicable.
  6. A server is in exactly one mode:
    • generated routes;
    • custom local entry;
    • command/prebuilt entry;
    • remote URL.
  7. Generated routes and a custom server entry may not silently coexist.
  8. Apps-only servers may be generated automatically; authors should not write
    an otherwise-empty server merely to register compiled resources.
  9. Route discovery uses the repository's existing sorted globbing, ignore,
    provenance, and safe-path machinery.
  10. Existing src/mcp/<server>.ts conflicts with route-mode
    src/mcp/<server>/; existing src/cli.ts conflicts with routed src/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:

  1. imports the discovered route implementations;
  2. constructs the real MCP server using config/plugin identity;
  3. registers tools, resources, prompts, and compiled Apps;
  4. binds the Agent renderer and transport projector;
  5. 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

  1. Keep explicit handwritten entries available as a documented escape hatch;
    compatibility duration and deprecation policy are decided separately.
  2. Add route mode to one server at a time.
  3. Move each operation into one route module with recognized exports and an
    optional named config export.
  4. Generate server and CLI entries after the final manual registration leaves.
  5. Convert Audiobook Curator into the reference framework-mode application.
  6. Update create-agent-bundle templates to generate single-file route modules.
  7. 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 config metadata 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,
    repeated mcp.server strings, or a one-line src/mcp/curator.ts adapter.
  • No generated-route project needs per-route sidecar config files or duplicate
    per-tool declarations in agent-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

Stack position

Full meta-framework stack

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestmeta-frameworkAgent Bundle compiler-coupled meta-framework

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions