Summary
normalizeMcpServers (packages/agent-bundle/src/config/normalize.ts) explicitly merges a config-side declaration onto a route-generated server — const declaration = configured[name] ?? {} feeds env, args, and targets, and normalizeMcpApps reads rawServer.apps for any server with a source (generated servers have one) — but validateMcpServer / validateMcpApps (packages/agent-bundle/src/config/validate.ts) never consult the route graph. Any mcp.servers.<generated-server> declaration is therefore rejected before normalization runs:
- a declaration with only
env (or args/targets) → AB4304 "must define exactly one of entry, command, or url, or provide the conventional stdio entry src/mcp/.ts"
- a declaration with
apps → AB4304 plus AB4322 "can declare Apps only with a local entry"
So there is no supported way to attach extra env/args/targets to a generated server, and the config-side apps record documented in docs/framework-mode.md / the mcp-app example is unusable for generated servers (the src/mcp/<server>/apps/*.ts route is the only working path). Neither docs/framework-mode.md, docs/entry-conventions.md, nor docs/diagnostics.md mention this restriction, and the AB4304 recovery text points at the handwritten-entry convention — which would then trigger AB4800 (both an entry and route modules).
Observed on framework main af1c185 (agent-bundle@0.0.0-preview-af1c185).
Minimal repro
repro/
package.json { "name": "repro", "private": true, "type": "module", "version": "0.0.1" }
agent-bundle.config.ts
src/mcp/demo/tools/status.tsx
agent-bundle.config.ts:
import { defineConfig } from 'agent-bundle/config';
export default defineConfig({
mcp: {
servers: {
// route-generated server (src/mcp/demo/tools/*.tsx) that only wants extra env
demo: { env: { DEMO_MODE: 'strict' } },
},
},
plugin: { description: 'AB4304 repro', name: 'repro', version: '0.0.1' },
targets: ['portable'],
});
src/mcp/demo/tools/status.tsx:
import { Agent } from '@agent-bundle/runtime';
import type { ToolConfig, ToolRouteProps } from 'agent-bundle';
import React from 'react';
import { z } from 'zod';
export const config = { annotations: { readOnlyHint: true }, description: 'Read status.' } satisfies ToolConfig;
export const inputSchema = z.object({}).strict();
export const resultSchema = z.object({ status: z.literal('ready') }).strict();
export default async function Status(_props: ToolRouteProps<typeof inputSchema>) {
return <Agent.Result value={{ status: 'ready' }}><Agent.Text>ready</Agent.Text></Agent.Result>;
}
$ agent-bundle validate --json
[{"code":"AB4304","message":"MCP server \"demo\" must define exactly one of entry, command, or url, or provide the conventional stdio entry src/mcp/demo.ts.", ...}]
Delete the mcp block → Validation succeeded (and the server builds). Replace env with apps: { panel: { entry: './views/panel.ts', resourceUri: 'ui://repro/panel.html' } } → AB4304 + AB4322.
Expected
Either
validateMcpServer/validateMcpApps accept a declaration for a server that the route graph compiles in generated mode when it carries only the keys normalization honors (env, args, targets, apps), and reject entry/command/url there via the existing AB4800; or
- the restriction is deliberate, in which case
AB4304's message/recovery should say so for generated servers, and docs/framework-mode.md should document that generated servers take no config declaration (env/args/targets/apps only through routes).
Context
Hit while re-porting ScriptedAlchemy/movie-library onto af1c185: the widget MCP App was declared under mcp.servers['movie-library-library'].apps and had to move to the src/mcp/movie-library-library/apps/widget.ts route convention (which works fine).
Summary
normalizeMcpServers(packages/agent-bundle/src/config/normalize.ts) explicitly merges a config-side declaration onto a route-generated server —const declaration = configured[name] ?? {}feedsenv,args, andtargets, andnormalizeMcpAppsreadsrawServer.appsfor any server with asource(generated servers have one) — butvalidateMcpServer/validateMcpApps(packages/agent-bundle/src/config/validate.ts) never consult the route graph. Anymcp.servers.<generated-server>declaration is therefore rejected before normalization runs:env(orargs/targets) →AB4304"must define exactly one of entry, command, or url, or provide the conventional stdio entry src/mcp/.ts"apps→AB4304plusAB4322"can declare Apps only with a local entry"So there is no supported way to attach extra
env/args/targetsto a generated server, and the config-sideappsrecord documented indocs/framework-mode.md/ themcp-appexample is unusable for generated servers (thesrc/mcp/<server>/apps/*.tsroute is the only working path). Neitherdocs/framework-mode.md,docs/entry-conventions.md, nordocs/diagnostics.mdmention this restriction, and theAB4304recovery text points at the handwritten-entry convention — which would then triggerAB4800(both an entry and route modules).Observed on framework main
af1c185(agent-bundle@0.0.0-preview-af1c185).Minimal repro
agent-bundle.config.ts:src/mcp/demo/tools/status.tsx:Delete the
mcpblock →Validation succeeded(and the server builds). Replaceenvwithapps: { panel: { entry: './views/panel.ts', resourceUri: 'ui://repro/panel.html' } }→AB4304+AB4322.Expected
Either
validateMcpServer/validateMcpAppsaccept a declaration for a server that the route graph compiles ingeneratedmode when it carries only the keys normalization honors (env,args,targets,apps), and rejectentry/command/urlthere via the existingAB4800; orAB4304's message/recovery should say so for generated servers, anddocs/framework-mode.mdshould document that generated servers take no config declaration (env/args/targets/apps only through routes).Context
Hit while re-porting
ScriptedAlchemy/movie-libraryontoaf1c185: the widget MCP App was declared undermcp.servers['movie-library-library'].appsand had to move to thesrc/mcp/movie-library-library/apps/widget.tsroute convention (which works fine).