Skip to content

Commit a9a57b4

Browse files
authored
feat: add mcp (#307)
<!-- Thank you for your pull request. Please review below requirements. Bug fixes and new features should include tests and possibly benchmarks. Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md 感谢您贡献代码。请确认下列 checklist 的完成情况。 Bug 修复和新功能必须包含测试,必要时请附上性能测试。 Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md --> ##### Checklist <!-- Remove items that do not apply. For completed items, change [ ] to [x]. --> - [ ] `npm test` passes - [ ] tests and/or benchmarks are included - [ ] documentation is changed or added - [ ] commit message follows commit guidelines ##### Affected core subsystem(s) <!-- Provide affected core subsystem(s). --> ##### Description of change <!-- Provide a description of the change below this comment. --> <!-- - any feature? - close https://github.com/eggjs/egg/ISSUE_URL --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced comprehensive support for the Model Context Protocol (MCP) with new decorators for controllers, prompts, tools, resources, and extra parameters, alongside detailed metadata classes and utilities. - Added MCP controller registration with lifecycle hooks, integrated into Egg.js routing, container factories, and middleware for MCP request management. - Enabled MCP message handling over SSE and streamable HTTP transports, including session management and proxy capabilities. - Integrated an MCP proxy plugin supporting multiplexed communication, client registration, message forwarding, and cluster-aware HTTP server handling. - Provided Egg.js application extensions exposing MCP proxy clients for seamless integration. - Added in-memory event store and transport implementations supporting MCP communication, event replay, and notification streaming. - Added tools supporting notification streams, resource listing, and prompt handling with schema validation and Zod integration. - **Bug Fixes** - Ensured correct middleware ordering to prevent premature request body consumption on MCP endpoints. - **Documentation** - Added changelogs and README files for new MCP and proxy plugins. - **Tests** - Added extensive test suites and fixtures validating MCP controllers, proxy functionality, and client-server interactions over multiple transport protocols. - **Chores** - Updated dependencies and configuration files to support MCP and proxy features. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent ce2d38c commit a9a57b4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+4006
-1
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"rules": {
1111
"@typescript-eslint/ban-types": "off",
12+
"import/no-unresolved": "off",
1213
"import/no-relative-packages": "error"
1314
},
1415
"overrides": [

core/controller-decorator/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import './src/impl/http/HTTPControllerMetaBuilder';
2+
import './src/impl/mcp/MCPControllerMetaBuilder';
23

34
export * from '@eggjs/tegg-types/controller-decorator';
45
export * from './src/model';
@@ -9,8 +10,14 @@ export * from './src/decorator/http/HTTPController';
910
export * from './src/decorator/http/HTTPMethod';
1011
export * from './src/decorator/http/HTTPParam';
1112
export * from './src/decorator/http/Host';
13+
export * from './src/decorator/mcp/MCPController';
14+
export * from './src/decorator/mcp/MCPPrompt';
15+
export * from './src/decorator/mcp/MCPResource';
16+
export * from './src/decorator/mcp/MCPTool';
17+
export * from './src/decorator/mcp/Extra';
1218
export * from './src/builder/ControllerMetaBuilderFactory';
1319
export * from './src/util/ControllerMetadataUtil';
20+
export * from './src/util/MCPInfoUtil';
1421
export * from './src/util/HTTPPriorityUtil';
1522

1623
export { default as ControllerInfoUtil } from './src/util/ControllerInfoUtil';

core/controller-decorator/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@eggjs/tegg-common-util": "^3.53.0",
4444
"@eggjs/tegg-metadata": "^3.53.0",
4545
"@eggjs/tegg-types": "^3.53.0",
46+
"@modelcontextprotocol/sdk": "^1.10.0",
4647
"is-type-of": "^1.2.1",
4748
"path-to-regexp": "^1.8.0",
4849
"reflect-metadata": "^0.1.13",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {
2+
EggProtoImplClass,
3+
} from '@eggjs/tegg-types';
4+
import { MCPInfoUtil } from '../../../src/util/MCPInfoUtil';
5+
6+
export function Extra() {
7+
return function(
8+
target: any,
9+
propertyKey: PropertyKey,
10+
parameterIndex: number,
11+
) {
12+
const controllerClazz = target.constructor as EggProtoImplClass;
13+
const methodName = propertyKey as string;
14+
MCPInfoUtil.setMCPExtra(parameterIndex, controllerClazz, methodName);
15+
};
16+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ControllerType, MCPControllerParams, AccessLevel, EggProtoImplClass } from '@eggjs/tegg-types';
2+
import { PrototypeUtil, SingletonProto } from '@eggjs/core-decorator';
3+
import ControllerInfoUtil from '../../util/ControllerInfoUtil';
4+
import { StackUtil } from '@eggjs/tegg-common-util';
5+
import { MCPInfoUtil } from '../../../src/util/MCPInfoUtil';
6+
7+
export function MCPController(param?: MCPControllerParams) {
8+
return function(constructor: EggProtoImplClass) {
9+
const func = SingletonProto({
10+
accessLevel: AccessLevel.PUBLIC,
11+
name: param?.protoName,
12+
});
13+
func(constructor);
14+
ControllerInfoUtil.setControllerType(constructor, ControllerType.MCP);
15+
if (param?.controllerName) {
16+
ControllerInfoUtil.setControllerName(constructor, param?.controllerName);
17+
}
18+
// './tegg/core/common-util/src/StackUtil.ts',
19+
// './tegg/core/core-decorator/src/decorator/Prototype.ts',
20+
// './tegg/core/controller-decorator/src/decorator/tr/TRController.ts',
21+
// './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
22+
// './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
23+
// './tegg/core/controller-decorator/test/fixtures/TRFooController.ts',
24+
PrototypeUtil.setFilePath(constructor, StackUtil.getCalleeFromStack(false, 5));
25+
26+
if (param?.name) {
27+
MCPInfoUtil.setMCPName(param.name, constructor);
28+
}
29+
if (param?.version) {
30+
MCPInfoUtil.setMCPVersion(param.version, constructor);
31+
}
32+
};
33+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
ControllerType,
3+
EggProtoImplClass,
4+
MCPPromptParams,
5+
} from '@eggjs/tegg-types';
6+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7+
import { MCPInfoUtil } from '../../../src/util/MCPInfoUtil';
8+
import MethodInfoUtil from '../../../src/util/MethodInfoUtil';
9+
10+
export function MCPPrompt(params?: MCPPromptParams) {
11+
return function(
12+
target: any,
13+
propertyKey: PropertyKey,
14+
) {
15+
const controllerClazz = target.constructor as EggProtoImplClass;
16+
const methodName = propertyKey as string;
17+
MethodInfoUtil.setMethodControllerType(
18+
controllerClazz,
19+
methodName,
20+
ControllerType.MCP,
21+
);
22+
MCPInfoUtil.setMCPPromptParams(
23+
{
24+
...params,
25+
mcpName: params?.name,
26+
name: methodName,
27+
},
28+
controllerClazz,
29+
methodName,
30+
);
31+
MCPInfoUtil.setMCPPrompt(controllerClazz, methodName);
32+
};
33+
}
34+
35+
export function PromptArgsSchema(argsSchema: Parameters<McpServer['prompt']>['2']) {
36+
return function(
37+
target: any,
38+
propertyKey: PropertyKey,
39+
parameterIndex: number,
40+
) {
41+
const controllerClazz = target.constructor as EggProtoImplClass;
42+
const methodName = propertyKey as string;
43+
MCPInfoUtil.setMCPPromptArgsInArgs(
44+
{
45+
argsSchema,
46+
index: parameterIndex,
47+
},
48+
controllerClazz,
49+
methodName,
50+
);
51+
};
52+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {
2+
ControllerType,
3+
EggProtoImplClass,
4+
MCPResourceParams,
5+
} from '@eggjs/tegg-types';
6+
import { MCPInfoUtil } from '../../../src/util/MCPInfoUtil';
7+
import MethodInfoUtil from '../../../src/util/MethodInfoUtil';
8+
9+
export function MCPResource(params: MCPResourceParams) {
10+
return function(
11+
target: any,
12+
propertyKey: PropertyKey,
13+
) {
14+
const controllerClazz = target.constructor as EggProtoImplClass;
15+
const methodName = propertyKey as string;
16+
MethodInfoUtil.setMethodControllerType(
17+
controllerClazz,
18+
methodName,
19+
ControllerType.MCP,
20+
);
21+
MCPInfoUtil.setMCPResourceParams(
22+
{
23+
...params,
24+
mcpName: params.name,
25+
name: methodName,
26+
},
27+
controllerClazz,
28+
methodName,
29+
);
30+
MCPInfoUtil.setMCPResource(controllerClazz, methodName);
31+
};
32+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
ControllerType,
3+
EggProtoImplClass,
4+
MCPToolParams,
5+
} from '@eggjs/tegg-types';
6+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7+
import { MCPInfoUtil } from '../../../src/util/MCPInfoUtil';
8+
import MethodInfoUtil from '../../../src/util/MethodInfoUtil';
9+
10+
export function MCPTool(params?: MCPToolParams) {
11+
return function(
12+
target: any,
13+
propertyKey: PropertyKey,
14+
) {
15+
const controllerClazz = target.constructor as EggProtoImplClass;
16+
const methodName = propertyKey as string;
17+
MethodInfoUtil.setMethodControllerType(
18+
controllerClazz,
19+
methodName,
20+
ControllerType.MCP,
21+
);
22+
MCPInfoUtil.setMCPToolParams(
23+
{
24+
...params,
25+
mcpName: params?.name,
26+
name: methodName,
27+
},
28+
controllerClazz,
29+
methodName,
30+
);
31+
MCPInfoUtil.setMCPTool(controllerClazz, methodName);
32+
};
33+
}
34+
35+
export function ToolArgsSchema(argsSchema: Parameters<McpServer['tool']>['2']) {
36+
return function(
37+
target: any,
38+
propertyKey: PropertyKey,
39+
parameterIndex: number,
40+
) {
41+
const controllerClazz = target.constructor as EggProtoImplClass;
42+
const methodName = propertyKey as string;
43+
MCPInfoUtil.setMCPToolArgsInArgs(
44+
{
45+
argsSchema,
46+
index: parameterIndex,
47+
},
48+
controllerClazz,
49+
methodName,
50+
);
51+
};
52+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import assert from 'assert';
2+
import { PrototypeUtil } from '@eggjs/core-decorator';
3+
import { MCPPromptMeta, MCPResourceMeta, MCPToolMeta } from '../../model';
4+
import { ControllerValidator } from '../../util/validator/ControllerValidator';
5+
import ControllerInfoUtil from '../../util/ControllerInfoUtil';
6+
import { MCPControllerPromptMetaBuilder } from './MCPControllerPromptMetaBuilder';
7+
import { MCPControllerResourceMetaBuilder } from './MCPControllerResourceMetaBuilder';
8+
import { MCPControllerToolMetaBuilder } from './MCPControllerToolMetaBuilder';
9+
import { MCPInfoUtil } from '../../../src/util/MCPInfoUtil';
10+
import { ControllerType, EggProtoImplClass } from '@eggjs/tegg-types';
11+
import { MCPControllerMeta } from '../../../src/model/MCPControllerMeta';
12+
import { ControllerMetaBuilderFactory } from '../../builder/ControllerMetaBuilderFactory';
13+
14+
export class MCPControllerMetaBuilder {
15+
private readonly clazz: EggProtoImplClass;
16+
17+
constructor(clazz: EggProtoImplClass) {
18+
this.clazz = clazz;
19+
}
20+
21+
private buildResource(): MCPResourceMeta[] {
22+
const methodNames = MCPInfoUtil.getMCPResource(this.clazz);
23+
const methods: MCPResourceMeta[] = [];
24+
for (const methodName of methodNames) {
25+
const builder = new MCPControllerResourceMetaBuilder(this.clazz, methodName);
26+
const methodMeta = builder.build();
27+
if (methodMeta) {
28+
methods.push(methodMeta);
29+
}
30+
}
31+
return methods;
32+
}
33+
34+
private buildPrompt(): MCPPromptMeta[] {
35+
const methodNames = MCPInfoUtil.getMCPPrompt(this.clazz);
36+
const methods: MCPPromptMeta[] = [];
37+
for (const methodName of methodNames) {
38+
const builder = new MCPControllerPromptMetaBuilder(this.clazz, methodName);
39+
const methodMeta = builder.build();
40+
if (methodMeta) {
41+
methods.push(methodMeta);
42+
}
43+
}
44+
return methods;
45+
}
46+
47+
private buildTool(): MCPToolMeta[] {
48+
const methodNames = MCPInfoUtil.getMCPTool(this.clazz);
49+
const methods: MCPToolMeta[] = [];
50+
for (const methodName of methodNames) {
51+
const builder = new MCPControllerToolMetaBuilder(this.clazz, methodName);
52+
const methodMeta = builder.build();
53+
if (methodMeta) {
54+
methods.push(methodMeta);
55+
}
56+
}
57+
return methods;
58+
}
59+
60+
build(): MCPControllerMeta {
61+
ControllerValidator.validate(this.clazz);
62+
const controllerType = ControllerInfoUtil.getControllerType(this.clazz);
63+
assert(controllerType === ControllerType.MCP, 'invalidate controller type');
64+
const mcpMiddlewares = ControllerInfoUtil.getControllerMiddlewares(this.clazz);
65+
const resources = this.buildResource();
66+
const prompts = this.buildPrompt();
67+
const tools = this.buildTool();
68+
const property = PrototypeUtil.getProperty(this.clazz);
69+
const protoName = property!.name as string;
70+
const clazzName = this.clazz.name;
71+
const controllerName = ControllerInfoUtil.getControllerName(this.clazz) || clazzName;
72+
const name = MCPInfoUtil.getMCPName(this.clazz) || controllerName;
73+
const version = MCPInfoUtil.getMCPVersion(this.clazz) || '1.0.0';
74+
const needAcl = ControllerInfoUtil.hasControllerAcl(this.clazz);
75+
const aclCode = ControllerInfoUtil.getControllerAcl(this.clazz);
76+
return new MCPControllerMeta(
77+
clazzName, protoName, controllerName, name, version, tools, resources,
78+
prompts, mcpMiddlewares, needAcl, aclCode,
79+
);
80+
}
81+
82+
static create(clazz: EggProtoImplClass) {
83+
return new MCPControllerMetaBuilder(clazz);
84+
}
85+
}
86+
87+
ControllerMetaBuilderFactory.registerControllerMetaBuilder(ControllerType.MCP, MCPControllerMetaBuilder.create);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { EggProtoImplClass } from '@eggjs/tegg-types';
2+
import { MCPPromptMeta } from '../../model';
3+
import { MethodValidator } from '../../util/validator/MethodValidator';
4+
import MethodInfoUtil from '../../util/MethodInfoUtil';
5+
import { MCPInfoUtil } from '../../util/MCPInfoUtil';
6+
7+
export class MCPControllerPromptMetaBuilder {
8+
private readonly clazz: EggProtoImplClass;
9+
private readonly methodName: string;
10+
11+
constructor(clazz: EggProtoImplClass, methodName: string) {
12+
this.clazz = clazz;
13+
this.methodName = methodName;
14+
}
15+
16+
build(): MCPPromptMeta | undefined {
17+
MethodValidator.validate(this.clazz, this.methodName);
18+
const controllerType = MethodInfoUtil.getMethodControllerType(this.clazz, this.methodName);
19+
if (!controllerType) {
20+
return undefined;
21+
}
22+
const middlewares = MethodInfoUtil.getMethodMiddlewares(this.clazz, this.methodName);
23+
const needAcl = MethodInfoUtil.hasMethodAcl(this.clazz, this.methodName);
24+
const aclCode = MethodInfoUtil.getMethodAcl(this.clazz, this.methodName);
25+
const params = MCPInfoUtil.getMCPPromptParams(this.clazz, this.methodName);
26+
const detail = MCPInfoUtil.getMCPPromptArgsIndex(this.clazz, this.methodName);
27+
const extra = MCPInfoUtil.getMCPExtra(this.clazz, this.methodName);
28+
29+
return new MCPPromptMeta({
30+
name: this.methodName,
31+
middlewares,
32+
needAcl,
33+
aclCode,
34+
detail,
35+
extra,
36+
...params,
37+
});
38+
}
39+
}

0 commit comments

Comments
 (0)