Skip to content

Commit 91b8614

Browse files
gxklclaude
andcommitted
refactor(controller): host-agnostic MCP register via McpRouter boundary
The MCP controller register mixed record collection with egg-specific transport (routes on app.router, ctxStorage, middleware, SSE/stream state), and the egg host threaded its Application into the module inner-object DI graph as a PRIVATE `eggApp` provided object so the register could reach it — a host-boundary leak that undermines the host-agnostic module-plugin goal. Extract a `McpRouter` transport boundary (controller-core): the shared `MCPControllerRegister` now only COLLECTS tool/resource/prompt records and delegates transport to an injected router via `registerServer()`. The egg node-HTTP transport moves to `EggMcpRouter` (constructed imperatively in app.ts, which already holds `app`), and the service worker fetch transport to `ServiceWorkerMcpRouter`; both provide the `mcpRouter` DI name (the two host plugins are never used together, so no qualifier is needed). MCPServerHelper was already host-agnostic and is unchanged. With the register no longer needing `app`, the `eggApp` provided inner object is removed from ModuleHandler and EggControllerRegisterFactory drops its host generic/injection — Egg Application leaves the DI graph. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ade8dd9 commit 91b8614

16 files changed

Lines changed: 956 additions & 962 deletions

File tree

tegg/plugin/controller/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
"./lib/impl/http/HTTPMethodRegister": "./src/lib/impl/http/HTTPMethodRegister.ts",
5252
"./lib/impl/http/HTTPMethodRegisterBase": "./src/lib/impl/http/HTTPMethodRegisterBase.ts",
5353
"./lib/impl/http/Req": "./src/lib/impl/http/Req.ts",
54+
"./lib/impl/mcp/EggMcpRouter": "./src/lib/impl/mcp/EggMcpRouter.ts",
55+
"./lib/impl/mcp/McpRouter": "./src/lib/impl/mcp/McpRouter.ts",
5456
"./lib/impl/mcp/MCPConfig": "./src/lib/impl/mcp/MCPConfig.ts",
5557
"./lib/impl/mcp/MCPControllerRegister": "./src/lib/impl/mcp/MCPControllerRegister.ts",
5658
"./lib/impl/mcp/MCPServerHelper": "./src/lib/impl/mcp/MCPServerHelper.ts",

tegg/plugin/controller/src/app.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'node:assert';
22

3-
import { ControllerMetaBuilderFactory, ControllerType } from '@eggjs/controller-decorator';
3+
import { ControllerMetaBuilderFactory, ControllerType, type MCPControllerMeta } from '@eggjs/controller-decorator';
44
import { EggPrototypeFactory, GlobalGraph, type LoadUnitLifecycleContext } from '@eggjs/metadata';
55
import {
66
EggContainerFactory,
@@ -15,13 +15,14 @@ import { AgentControllerProto } from './lib/AgentControllerProto.ts';
1515
import { CONTROLLER_LOAD_UNIT, ControllerLoadUnit } from './lib/ControllerLoadUnit.ts';
1616
import { ControllerLoadUnitHandler } from './lib/ControllerLoadUnitHandler.ts';
1717
import { ControllerMetadataManager } from './lib/ControllerMetadataManager.ts';
18+
import { ControllerRegisterDefaults } from './lib/ControllerRegisterDefaults.ts';
1819
import type { ControllerRegisterFactory } from './lib/ControllerRegisterFactory.ts';
1920
import { EggControllerLoader } from './lib/EggControllerLoader.ts';
2021
import { HTTPControllerRegister } from './lib/impl/http/HTTPControllerRegister.ts';
22+
import { EggMcpRouter } from './lib/impl/mcp/EggMcpRouter.ts';
2123
import { MCPControllerRegister } from './lib/impl/mcp/MCPControllerRegister.ts';
2224
import { middlewareGraphHook } from './lib/MiddlewareGraphHook.ts';
2325
import type { RootProtoManager } from './lib/RootProtoManager.ts';
24-
import { ControllerRegisterDefaults } from './lib/ControllerRegisterDefaults.ts';
2526

2627
// Load Controller process
2728
// 1. await add load unit is ready, controller may depend other load unit
@@ -60,8 +61,13 @@ export default class ControllerAppBootHook implements ILifecycleBoot {
6061
return new EggControllerLoader(unitPath);
6162
});
6263
// Drained by the module factory proto when the InnerObjectLoadUnit
63-
// materializes (before any business load unit).
64-
ControllerRegisterDefaults.enqueue(ControllerType.HTTP, HTTPControllerRegister.create);
64+
// materializes (before any business load unit). The creators close over
65+
// `this.app` (rather than the DI-threaded host) so the egg app never has to
66+
// be provided as an inner object — HTTP registers mount on `app.router`,
67+
// the MCP router captures the app imperatively.
68+
ControllerRegisterDefaults.enqueue(ControllerType.HTTP, (proto, meta) =>
69+
HTTPControllerRegister.create(proto, meta, this.app),
70+
);
6571
this.app.loadUnitFactory.registerLoadUnitCreator(
6672
CONTROLLER_LOAD_UNIT,
6773
(ctx: LoadUnitLifecycleContext): ControllerLoadUnit => {
@@ -97,7 +103,18 @@ export default class ControllerAppBootHook implements ILifecycleBoot {
97103
// init http root proto middleware
98104
this.prepareMiddleware(this.app.config.coreMiddleware);
99105
if (this.mcpEnable()) {
100-
ControllerRegisterDefaults.enqueue(ControllerType.MCP, MCPControllerRegister.create);
106+
// One EggMcpRouter + one collect-only MCPControllerRegister per app,
107+
// created lazily on the first MCP controller proto (during didLoad, when
108+
// app.router/app.config.mcp are ready). The router owns all egg transport;
109+
// the register only collects records and delegates to the router.
110+
let eggMcpRouter: EggMcpRouter | undefined;
111+
let mcpRegister: MCPControllerRegister | undefined;
112+
ControllerRegisterDefaults.enqueue(ControllerType.MCP, (proto, meta) => {
113+
eggMcpRouter ??= new EggMcpRouter(this.app);
114+
mcpRegister ??= new MCPControllerRegister(meta as MCPControllerMeta, eggMcpRouter);
115+
mcpRegister.addControllerProto(proto);
116+
return mcpRegister;
117+
});
101118
// Don't let the mcp's body be consumed
102119
this.app.config.coreMiddleware.unshift('mcpBodyMiddleware');
103120

@@ -168,7 +185,7 @@ export default class ControllerAppBootHook implements ILifecycleBoot {
168185
// and register methods after collect is done.
169186
HTTPControllerRegister.instance?.doRegister(this.app.rootProtoManager);
170187

171-
this.app.config.mcp.hooks = MCPControllerRegister.hooks;
188+
this.app.config.mcp.hooks = EggMcpRouter.hooks;
172189
});
173190
}
174191

@@ -190,7 +207,8 @@ export default class ControllerAppBootHook implements ILifecycleBoot {
190207
// InnerObjectLoadUnit teardown.
191208
ControllerMetadataManager.instance.clear();
192209
HTTPControllerRegister.clean();
193-
MCPControllerRegister.clean();
210+
// The MCP register/router are per-boot closures (no static instance to
211+
// clean); the scope-backed hook list is torn down with the app bag.
194212
});
195213
}
196214
}

tegg/plugin/controller/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export * from './lib/RootProtoManager.ts';
1212
export * from './lib/errors.ts';
1313
export * from './lib/impl/http/HTTPControllerRegisterBase.ts';
1414
export * from './lib/impl/http/HTTPMethodRegisterBase.ts';
15+
export * from './lib/impl/mcp/McpRouter.ts';
1516
export * from './lib/impl/mcp/MCPServerHelper.ts';

tegg/plugin/controller/src/lib/ControllerModule.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import { LifecyclePostInject } from '@eggjs/lifecycle';
2-
import {
3-
EggPrototypeLifecycleProto,
4-
Inject,
5-
InjectOptional,
6-
InnerObjectProto,
7-
LoadUnitLifecycleProto,
8-
} from '@eggjs/tegg';
2+
import { EggPrototypeLifecycleProto, Inject, InnerObjectProto, LoadUnitLifecycleProto } from '@eggjs/tegg';
93
import { AccessLevel } from '@eggjs/tegg-types';
10-
import type { Application } from 'egg';
114

125
import { ControllerLoadUnitHook } from './ControllerLoadUnitHook.ts';
136
import { ControllerPrototypeHook } from './ControllerPrototypeHook.ts';
@@ -28,13 +21,11 @@ import { RootProtoManager } from './RootProtoManager.ts';
2821
export class EggRootProtoManager extends RootProtoManager {}
2922

3023
@InnerObjectProto({ name: 'controllerRegisterFactory', accessLevel: AccessLevel.PUBLIC })
31-
export class EggControllerRegisterFactory extends ControllerRegisterFactory<Application | undefined> {
32-
// The egg host provides `eggApp` as a PRIVATE inner object (transport
33-
// registers mount routes on app.router); standalone provides none and the
34-
// fetch creators ignore the host argument.
35-
constructor(@InjectOptional() eggApp?: Application) {
36-
super(eggApp);
37-
}
24+
export class EggControllerRegisterFactory extends ControllerRegisterFactory {
25+
// No host is threaded through the DI graph: egg's transport creators close
26+
// over `app` imperatively (see app.ts), the fetch creators are container
27+
// citizens. Both register through ControllerRegisterDefaults / the injected
28+
// factory directly.
3829

3930
/**
4031
* Apply the transport creators the host enqueued imperatively before this

0 commit comments

Comments
 (0)