Skip to content

Commit 490fc36

Browse files
committed
refactor: lazy-load agentic inside the devframe/adapters/mcp functions
Replaces the alias's top-level await with a per-function lazy import: importing the entry is now side-effect-free (DF0079 moves to call time), and without TLA the entry rejoins the main server build graph, dropping the isolated tsdown config. createMcpFetchHandler and mountMcpHttp become async through this entry; the migration guide covers the added await.
1 parent b0880ec commit 490fc36

7 files changed

Lines changed: 60 additions & 37 deletions

File tree

‎docs/content/6.errors/DF0079.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: 'The mcp option is enabled, but the optional peer "@devframes/agent
99
1010
## Cause
1111

12-
An explicit `mcp` setting (`true`, a route options object, the `--mcp` flag, or the `mcp` CLI subcommand) asked for an MCP surface - or `devframe/adapters/mcp` was imported directly - but the implementation could not be loaded from the optional `@devframes/agentic` peer, typically because it is not installed. Unlike the omitted `'auto'` default (which degrades to a one-time [DF0078](/errors/DF0078) warning), an explicit opt-in fails fast rather than silently running without MCP.
12+
An explicit `mcp` setting (`true`, a route options object, the `--mcp` flag, or the `mcp` CLI subcommand) asked for an MCP surface - or a `devframe/adapters/mcp` function was called - but the implementation could not be loaded from the optional `@devframes/agentic` peer, typically because it is not installed. Unlike the omitted `'auto'` default (which degrades to a one-time [DF0078](/errors/DF0078) warning), an explicit opt-in fails fast rather than silently running without MCP.
1313

1414
## Fix
1515

‎docs/content/7.migrations/1.migration-0.10.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@ Every surface reacts to the missing peer the same way:
2121
| `true` / route options object | always mounts | throws [DF0079](/errors/DF0079) |
2222
| `false` | never mounts, never probes | same |
2323

24-
Importing `devframe/adapters/mcp` itself without the peer also throws [DF0079](/errors/DF0079). This applies everywhere the `mcp` setting exists: `createCac` / `--mcp`, `createDevServer`, `initDevframe`, `initHub`'s aggregate endpoint, and the framework kits.
24+
Calling a `devframe/adapters/mcp` function without the peer also throws [DF0079](/errors/DF0079); the import itself stays side-effect-free and loads no MCP code. This applies everywhere the `mcp` setting exists: `createCac` / `--mcp`, `createDevServer`, `initDevframe`, `initHub`'s aggregate endpoint, and the framework kits.
2525

26-
The exports are unchanged: `createMcpServer`, `createMcpFetchHandler`, `mountMcpHttp`, and their option types (the option and handle types are also importable from `devframe/types`). The `<your-app> mcp` stdio subcommand keeps working with the peer installed.
26+
The exports keep their names and option types (also importable from `devframe/types`): `createMcpServer`, `createMcpFetchHandler`, `mountMcpHttp`. The latter two are now **async** - they await the lazy peer load - so add an `await` where you called them synchronously:
27+
28+
```diff
29+
- const handler = createMcpFetchHandler(ctx, options)
30+
+ const handler = await createMcpFetchHandler(ctx, options)
31+
```
32+
33+
The `<your-app> mcp` stdio subcommand keeps working with the peer installed.
2734

2835
## `devframe connect` requires `@devframes/agentic`
2936

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
// The user-facing MCP adapter entry. The implementation (and the MCP SDK)
22
// lives in the optional `@devframes/agentic` peer, which is never imported
3-
// directly: this entry lazy-loads it (throwing a coded DF0079 when the peer
4-
// is not installed) and re-exports the surface, typed against devframe's own
5-
// contract in `types/mcp.ts`.
3+
// directly: each function lazy-loads it on first call (throwing a coded
4+
// DF0079 when the peer is not installed), so importing this entry stays
5+
// side-effect-free and loads no MCP code. Signatures are typed against
6+
// devframe's own contract in `types/mcp.ts`; `createMcpFetchHandler` and
7+
// `mountMcpHttp` are async here (they await the lazy load), unlike the
8+
// synchronous implementations behind them.
9+
import type { H3 } from 'h3'
10+
import type { MountedMcpHttp, MountMcpHttpOptions } from '../node/agentic'
11+
import type { DevframeNodeContext } from '../types/context'
12+
import type { DevframeDefinition } from '../types/devframe'
13+
import type { CreateMcpFetchHandlerOptions, CreateMcpServerOptions, McpFetchHandler, McpServerHandle } from '../types/mcp'
614
import { importAgenticMcp } from '../node/agentic'
715

816
export type { MountedMcpHttp, MountMcpHttpOptions } from '../node/agentic'
@@ -14,12 +22,31 @@ export type {
1422
McpServerHandle,
1523
} from '../types/mcp'
1624

17-
// The alias must resolve its exports at module evaluation (they are consumed
18-
// as plain named imports), so the lazy load is a deliberate top-level await;
19-
// it builds in its own graph (see tsdown.config.ts) to keep TLA contained.
20-
// eslint-disable-next-line antfu/no-top-level-await
21-
const mcp = await importAgenticMcp()
25+
/** Build an MCP server over the agent surface of a devframe definition (stdio). */
26+
export async function createMcpServer(
27+
definition: DevframeDefinition,
28+
options?: CreateMcpServerOptions,
29+
): Promise<McpServerHandle> {
30+
const mcp = await importAgenticMcp()
31+
return mcp.createMcpServer(definition, options)
32+
}
2233

23-
export const createMcpServer = mcp.createMcpServer
24-
export const createMcpFetchHandler = mcp.createMcpFetchHandler
25-
export const mountMcpHttp = mcp.mountMcpHttp
34+
/** Build a framework-agnostic `Request → Response` MCP endpoint over a devframe context. */
35+
export async function createMcpFetchHandler(
36+
ctx: DevframeNodeContext,
37+
options: CreateMcpFetchHandlerOptions,
38+
): Promise<McpFetchHandler> {
39+
const mcp = await importAgenticMcp()
40+
return mcp.createMcpFetchHandler(ctx, options)
41+
}
42+
43+
/** Mount a stateless MCP endpoint on an h3 app at `path`. */
44+
export async function mountMcpHttp(
45+
app: H3,
46+
ctx: DevframeNodeContext,
47+
path: string,
48+
options: MountMcpHttpOptions,
49+
): Promise<MountedMcpHttp> {
50+
const mcp = await importAgenticMcp()
51+
return mcp.mountMcpHttp(app, ctx, path, options)
52+
}

‎packages/devframe/tsdown.config.ts‎

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,14 @@ const serverEntries = {
116116
'adapters/build': 'src/adapters/build.ts',
117117
'adapters/embedded': 'src/adapters/embedded.ts',
118118
'adapters/initiate': 'src/adapters/initiate.ts',
119+
'adapters/mcp': 'src/adapters/mcp.ts',
119120
'cli/main': 'src/cli/main.ts',
120121
'recipes/common-rpc-functions': 'src/recipes/common-rpc-functions.ts',
121122
'recipes/interactive-auth': 'src/recipes/interactive-auth.ts',
122123
}
123124

124125
/**
125-
* Four configs:
126+
* Three configs:
126127
*
127128
* 1. Runtime client/agnostic build (`dts: false`). Independent rolldown
128129
* chunk graph so server-only imports like `devframe/rpc/transports/ws-server`
@@ -136,11 +137,6 @@ const serverEntries = {
136137
* `src/types/rpc-augments.ts`, produce exactly one declaration site.
137138
* This is what lets consumer `declare module 'devframe'` augmentations
138139
* propagate across every import chain.
139-
* 4. The `adapters/mcp` alias, alone. Its top-level `await` (lazy-loading
140-
* `@devframes/agentic/mcp` at module evaluation) would otherwise reshape
141-
* the server graph's chunking - every sibling entry turns into async-safe
142-
* re-export facades. An isolated graph keeps the TLA contained; the few
143-
* helpers it inlines are duplicated only here.
144140
*/
145141
export default defineConfig([
146142
{
@@ -195,14 +191,6 @@ export default defineConfig([
195191
deps,
196192
dts: { emitDtsOnly: true },
197193
outExtensions: () => ({ dts: '.d.mts' }),
198-
entry: { ...clientEntries, ...serverEntries, 'adapters/mcp': 'src/adapters/mcp.ts' },
199-
},
200-
{
201-
clean: false,
202-
platform: 'node',
203-
tsconfig,
204-
deps: nodeDeps,
205-
dts: false,
206-
entry: { 'adapters/mcp': 'src/adapters/mcp.ts' },
194+
entry: { ...clientEntries, ...serverEntries },
207195
},
208196
])

‎tests/__snapshots__/tsnapi/devframe/adapters/mcp.snapshot.d.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
22
* Generated by tsnapi — public API snapshot of `devframe/adapters/mcp`
33
*/
4-
// #region Variables
5-
export declare const createMcpFetchHandler: (ctx: DevframeNodeContext, options: CreateMcpFetchHandlerOptions) => McpFetchHandler;
6-
export declare const createMcpServer: (definition: DevframeDefinition, options?: CreateMcpServerOptions) => Promise<McpServerHandle>;
7-
export declare const mountMcpHttp: (app: import("h3").H3, ctx: DevframeNodeContext, path: string, options: MountMcpHttpOptions) => MountedMcpHttp;
4+
// #region Functions
5+
export declare function createMcpFetchHandler(_: DevframeNodeContext, _: CreateMcpFetchHandlerOptions): Promise<McpFetchHandler>;
6+
export declare function createMcpServer(_: DevframeDefinition, _?: CreateMcpServerOptions): Promise<McpServerHandle>;
7+
export declare function mountMcpHttp(_: H3, _: DevframeNodeContext, _: string, _: MountMcpHttpOptions): Promise<MountedMcpHttp>;
88
// #endregion
99

1010
// #region Other
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
22
* Generated by tsnapi — public API snapshot of `devframe/adapters/mcp`
33
*/
4-
// #region Variables
5-
export var createMcpFetchHandler /* const */
6-
export var createMcpServer /* const */
7-
export var mountMcpHttp /* const */
4+
// #region Functions
5+
export async function createMcpFetchHandler(_, _) {}
6+
export async function createMcpServer(_, _) {}
7+
export async function mountMcpHttp(_, _, _, _) {}
88
// #endregion

‎tests/optional-mcp-bundles.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const root = fileURLToPath(new URL('..', import.meta.url))
99
const entries = [
1010
'packages/devframe/dist/adapters/cac.mjs',
1111
'packages/devframe/dist/adapters/initiate.mjs',
12+
'packages/devframe/dist/adapters/mcp.mjs',
1213
'packages/hub/dist/node/initiate.mjs',
1314
'packages/next/dist/hub.mjs',
1415
]

0 commit comments

Comments
 (0)