@nestjs-mcp/server 0.1.0-alpha.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +454 -136
- package/coverage/clover.xml +507 -0
- package/coverage/coverage-final.json +19 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +206 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/src/controllers/sse/index.html +146 -0
- package/coverage/lcov-report/src/controllers/sse/index.ts.html +91 -0
- package/coverage/lcov-report/src/controllers/sse/sse.controller.ts.html +160 -0
- package/coverage/lcov-report/src/controllers/sse/sse.service.ts.html +403 -0
- package/coverage/lcov-report/src/controllers/streamable/index.html +146 -0
- package/coverage/lcov-report/src/controllers/streamable/index.ts.html +91 -0
- package/coverage/lcov-report/src/controllers/streamable/streamable.controller.ts.html +157 -0
- package/coverage/lcov-report/src/controllers/streamable/streamable.service.ts.html +655 -0
- package/coverage/lcov-report/src/decorators/capabilities.constants.ts.html +106 -0
- package/coverage/lcov-report/src/decorators/capabilities.decorators.ts.html +535 -0
- package/coverage/lcov-report/src/decorators/index.html +146 -0
- package/coverage/lcov-report/src/decorators/index.ts.html +91 -0
- package/coverage/lcov-report/src/index.html +131 -0
- package/coverage/lcov-report/src/index.ts.html +118 -0
- package/coverage/lcov-report/src/interfaces/capabilities.interface.ts.html +703 -0
- package/coverage/lcov-report/src/interfaces/index.html +131 -0
- package/coverage/lcov-report/src/interfaces/index.ts.html +91 -0
- package/coverage/lcov-report/src/mcp.module.ts.html +817 -0
- package/coverage/lcov-report/src/registry/discovery.service.ts.html +433 -0
- package/coverage/lcov-report/src/registry/index.html +161 -0
- package/coverage/lcov-report/src/registry/index.ts.html +91 -0
- package/coverage/lcov-report/src/registry/logger.service.ts.html +514 -0
- package/coverage/lcov-report/src/registry/registry.service.ts.html +1183 -0
- package/coverage/lcov-report/src/services/index.html +116 -0
- package/coverage/lcov-report/src/services/session.manager.ts.html +163 -0
- package/coverage/lcov.info +912 -0
- package/dist/controllers/sse/sse.controller.d.ts +1 -3
- package/dist/controllers/sse/sse.controller.js +2 -8
- package/dist/controllers/sse/sse.controller.js.map +1 -1
- package/dist/interfaces/capabilities.interface.d.ts +1 -1
- package/dist/interfaces/{context.interface.d.ts → guards.interface.d.ts} +0 -2
- package/dist/interfaces/{message.types.js → guards.interface.js} +1 -1
- package/dist/interfaces/guards.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +1 -1
- package/dist/interfaces/index.js +1 -1
- package/dist/interfaces/index.js.map +1 -1
- package/dist/interfaces/mcp-server-options.interface.d.ts +2 -2
- package/dist/mcp.module.js +1 -18
- package/dist/mcp.module.js.map +1 -1
- package/dist/registry/registry.service.d.ts +1 -3
- package/dist/registry/registry.service.js +3 -8
- package/dist/registry/registry.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +20 -18
- package/src/controllers/sse/sse.service.ts +21 -5
- package/src/controllers/streamable/streamable.service.ts +45 -23
- package/src/interfaces/capabilities.interface.ts +112 -1
- package/src/interfaces/context.interface.ts +21 -6
- package/src/mcp.module.ts +4 -10
- package/src/registry/registry.service.ts +94 -10
- package/src/services/session.manager.ts +26 -0
- package/dist/interceptors/message.interceptor.d.ts +0 -10
- package/dist/interceptors/message.interceptor.js +0 -61
- package/dist/interceptors/message.interceptor.js.map +0 -1
- package/dist/interfaces/context.interface.js +0 -3
- package/dist/interfaces/context.interface.js.map +0 -1
- package/dist/interfaces/message.types.d.ts +0 -8
- package/dist/interfaces/message.types.js.map +0 -1
- package/dist/services/message.service.d.ts +0 -7
- package/dist/services/message.service.js +0 -25
- package/dist/services/message.service.js.map +0 -1
- package/src/interceptors/message.interceptor.ts +0 -70
- package/src/services/message.service.ts +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs-mcp/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Modular library for building scalable MCP servers with NestJS, providing decorators and integration patterns as a wrapper for the official MCP TypeScript SDK.",
|
|
5
5
|
"author": "Adrián Darío Hidalgo Flores",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,25 +30,13 @@
|
|
|
30
30
|
"model-context-protocol",
|
|
31
31
|
"module",
|
|
32
32
|
"nestjs",
|
|
33
|
+
"npm",
|
|
34
|
+
"pnpm",
|
|
33
35
|
"sdk",
|
|
34
36
|
"server",
|
|
35
|
-
"typescript"
|
|
37
|
+
"typescript",
|
|
38
|
+
"yarn"
|
|
36
39
|
],
|
|
37
|
-
"scripts": {
|
|
38
|
-
"build": "nest build",
|
|
39
|
-
"prepare": "husky install",
|
|
40
|
-
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
41
|
-
"start:example": "npx -y ts-node-dev --respawn examples/$EXAMPLE/main.ts",
|
|
42
|
-
"start:inspector": "npx -y @modelcontextprotocol/inspector",
|
|
43
|
-
"lint": "eslint \"{src,test,examples}/**/*.ts\" --fix",
|
|
44
|
-
"typecheck": "tsc --noEmit",
|
|
45
|
-
"test": "jest",
|
|
46
|
-
"test:watch": "jest --watch",
|
|
47
|
-
"test:cov": "jest --coverage",
|
|
48
|
-
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
49
|
-
"test:e2e": "jest --config ./test/jest-e2e.json",
|
|
50
|
-
"npm:publish": "node scripts/npm-publish.js"
|
|
51
|
-
},
|
|
52
40
|
"peerDependencies": {
|
|
53
41
|
"@nestjs/common": "^11.0.1",
|
|
54
42
|
"@nestjs/core": "^11.0.1",
|
|
@@ -105,5 +93,19 @@
|
|
|
105
93
|
],
|
|
106
94
|
"coverageDirectory": "../coverage",
|
|
107
95
|
"testEnvironment": "node"
|
|
96
|
+
},
|
|
97
|
+
"scripts": {
|
|
98
|
+
"build": "nest build",
|
|
99
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
100
|
+
"start:example": "npx -y ts-node-dev --respawn examples/$EXAMPLE/main.ts",
|
|
101
|
+
"start:inspector": "npx -y @modelcontextprotocol/inspector",
|
|
102
|
+
"lint": "eslint \"{src,test,examples}/**/*.ts\" --fix",
|
|
103
|
+
"typecheck": "tsc --noEmit",
|
|
104
|
+
"test": "jest",
|
|
105
|
+
"test:watch": "jest --watch",
|
|
106
|
+
"test:cov": "jest --coverage",
|
|
107
|
+
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
108
|
+
"test:e2e": "jest --config ./test/jest-e2e.json",
|
|
109
|
+
"npm:publish": "node scripts/npm-publish.js"
|
|
108
110
|
}
|
|
109
|
-
}
|
|
111
|
+
}
|
|
@@ -6,18 +6,18 @@ import { Request, Response } from 'express';
|
|
|
6
6
|
import { McpServerOptions } from '../../interfaces/mcp-server-options.interface';
|
|
7
7
|
import { McpLoggerService } from '../../registry/logger.service';
|
|
8
8
|
import { RegistryService } from '../../registry/registry.service';
|
|
9
|
+
import { SessionManager } from '../../services/session.manager';
|
|
9
10
|
|
|
10
11
|
@Injectable()
|
|
11
12
|
export class SseService implements OnModuleInit {
|
|
12
13
|
private server: McpServer;
|
|
13
14
|
|
|
14
|
-
private transports = {} as Record<string, SSEServerTransport>;
|
|
15
|
-
|
|
16
15
|
constructor(
|
|
17
16
|
@Inject('MCP_SERVER_OPTIONS')
|
|
18
17
|
private readonly options: McpServerOptions,
|
|
19
18
|
private readonly registry: RegistryService,
|
|
20
19
|
private readonly logger: McpLoggerService,
|
|
20
|
+
private readonly sessionManager: SessionManager,
|
|
21
21
|
) {
|
|
22
22
|
this.server = new McpServer(this.options.serverInfo, this.options.options);
|
|
23
23
|
}
|
|
@@ -36,7 +36,11 @@ export class SseService implements OnModuleInit {
|
|
|
36
36
|
async handleSse(req: Request, res: Response) {
|
|
37
37
|
// Create SSE transport for legacy clients
|
|
38
38
|
const transport = new SSEServerTransport('/messages', res);
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
this.sessionManager.setSession(transport.sessionId, {
|
|
41
|
+
transport,
|
|
42
|
+
request: req,
|
|
43
|
+
});
|
|
40
44
|
|
|
41
45
|
this.logger.debug(
|
|
42
46
|
`Starting SSE for sessionId: ${transport.sessionId}`,
|
|
@@ -44,7 +48,7 @@ export class SseService implements OnModuleInit {
|
|
|
44
48
|
);
|
|
45
49
|
|
|
46
50
|
res.on('close', () => {
|
|
47
|
-
|
|
51
|
+
this.sessionManager.deleteSession(transport.sessionId);
|
|
48
52
|
});
|
|
49
53
|
|
|
50
54
|
await this.server.connect(transport);
|
|
@@ -55,7 +59,12 @@ export class SseService implements OnModuleInit {
|
|
|
55
59
|
*/
|
|
56
60
|
async handleMessage(req: Request, res: Response) {
|
|
57
61
|
const sessionId = req.query.sessionId as string;
|
|
58
|
-
const
|
|
62
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
63
|
+
|
|
64
|
+
if (!session) {
|
|
65
|
+
res.status(400).send('Invalid or missing sessionId');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
59
68
|
|
|
60
69
|
this.logger.debug(
|
|
61
70
|
`Receiving SSE message for sessionId: ${sessionId}`,
|
|
@@ -64,6 +73,13 @@ export class SseService implements OnModuleInit {
|
|
|
64
73
|
|
|
65
74
|
this.logger.debug(`SSE message: ${JSON.stringify(req.body)}`, 'MCP_SERVER');
|
|
66
75
|
|
|
76
|
+
const transport = session.transport;
|
|
77
|
+
|
|
78
|
+
if (!(transport instanceof SSEServerTransport)) {
|
|
79
|
+
res.status(400).send('Invalid transport');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
try {
|
|
68
84
|
if (transport) {
|
|
69
85
|
await transport.handlePostMessage(req, res, req.body);
|
|
@@ -11,15 +11,13 @@ import {
|
|
|
11
11
|
} from '../../interfaces/mcp-server-options.interface';
|
|
12
12
|
import { McpLoggerService } from '../../registry/logger.service';
|
|
13
13
|
import { RegistryService } from '../../registry/registry.service';
|
|
14
|
-
|
|
14
|
+
import { SessionManager } from '../../services/session.manager';
|
|
15
15
|
// TODO: Stateless mode should be handled here or in another service
|
|
16
16
|
|
|
17
17
|
@Injectable()
|
|
18
18
|
export class StreamableService implements OnModuleInit {
|
|
19
19
|
private server: McpServer;
|
|
20
20
|
|
|
21
|
-
private transports = {} as Record<string, StreamableHTTPServerTransport>;
|
|
22
|
-
|
|
23
21
|
constructor(
|
|
24
22
|
@Inject('MCP_SERVER_OPTIONS')
|
|
25
23
|
private readonly options: McpServerOptions,
|
|
@@ -27,6 +25,7 @@ export class StreamableService implements OnModuleInit {
|
|
|
27
25
|
private readonly transportOptions: McpModuleTransportOptions,
|
|
28
26
|
private readonly registry: RegistryService,
|
|
29
27
|
private readonly logger: McpLoggerService,
|
|
28
|
+
private readonly sessionManager: SessionManager,
|
|
30
29
|
) {
|
|
31
30
|
this.server = new McpServer(this.options.serverInfo, this.options.options);
|
|
32
31
|
}
|
|
@@ -47,23 +46,34 @@ export class StreamableService implements OnModuleInit {
|
|
|
47
46
|
* @param req Express Request object (expects sessionId in query)
|
|
48
47
|
* @param res Express Response object
|
|
49
48
|
*/
|
|
50
|
-
async handlePostRequest(
|
|
51
|
-
req: Request<any, any, any, { sessionId?: string }>,
|
|
52
|
-
res: Response,
|
|
53
|
-
) {
|
|
49
|
+
async handlePostRequest(req: Request, res: Response) {
|
|
54
50
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
55
51
|
let transport: StreamableHTTPServerTransport;
|
|
56
52
|
|
|
57
53
|
const { options } = this.transportOptions?.streamable || {};
|
|
58
54
|
|
|
59
|
-
if (sessionId && this.
|
|
60
|
-
|
|
55
|
+
if (sessionId && this.sessionManager.getSession(sessionId)) {
|
|
56
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
57
|
+
|
|
58
|
+
if (!session) {
|
|
59
|
+
throw new Error('Session not found');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!(session.transport instanceof StreamableHTTPServerTransport)) {
|
|
63
|
+
throw new Error('Invalid transport');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
transport = session.transport;
|
|
61
67
|
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
68
|
+
// This is called only when method is initialize
|
|
62
69
|
transport = new StreamableHTTPServerTransport({
|
|
63
70
|
sessionIdGenerator: () =>
|
|
64
71
|
options?.sessionIdGenerator?.() || randomUUID(),
|
|
65
72
|
onsessioninitialized: (sessionId) => {
|
|
66
|
-
this.
|
|
73
|
+
this.sessionManager.setSession(sessionId, {
|
|
74
|
+
transport,
|
|
75
|
+
request: req,
|
|
76
|
+
});
|
|
67
77
|
},
|
|
68
78
|
enableJsonResponse: options?.enableJsonResponse,
|
|
69
79
|
eventStore: options?.eventStore,
|
|
@@ -71,9 +81,11 @@ export class StreamableService implements OnModuleInit {
|
|
|
71
81
|
|
|
72
82
|
transport.onclose = () => {
|
|
73
83
|
if (transport.sessionId) {
|
|
74
|
-
|
|
84
|
+
this.sessionManager.deleteSession(transport.sessionId);
|
|
75
85
|
}
|
|
76
86
|
};
|
|
87
|
+
|
|
88
|
+
await this.server.connect(transport);
|
|
77
89
|
} else {
|
|
78
90
|
// Invalid request
|
|
79
91
|
res.status(400).json({
|
|
@@ -99,18 +111,25 @@ export class StreamableService implements OnModuleInit {
|
|
|
99
111
|
* @param req Express Request object (expects sessionId in query)
|
|
100
112
|
* @param res Express Response object
|
|
101
113
|
*/
|
|
102
|
-
async handleGetRequest(
|
|
103
|
-
req: Request<any, any, any, { sessionId?: string }>,
|
|
104
|
-
res: Response,
|
|
105
|
-
) {
|
|
114
|
+
async handleGetRequest(req: Request, res: Response) {
|
|
106
115
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
107
116
|
|
|
108
|
-
if (!sessionId || !this.
|
|
117
|
+
if (!sessionId || !this.sessionManager.getSession(sessionId)) {
|
|
109
118
|
res.status(400).send('Invalid or missing session ID');
|
|
110
119
|
return;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
const
|
|
122
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
123
|
+
|
|
124
|
+
if (!session) {
|
|
125
|
+
throw new Error('Session not found');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { transport } = session;
|
|
129
|
+
|
|
130
|
+
if (!(transport instanceof StreamableHTTPServerTransport)) {
|
|
131
|
+
throw new Error('Invalid transport');
|
|
132
|
+
}
|
|
114
133
|
|
|
115
134
|
await transport.handleRequest(req, res);
|
|
116
135
|
}
|
|
@@ -125,10 +144,7 @@ export class StreamableService implements OnModuleInit {
|
|
|
125
144
|
* @param req Express Request object
|
|
126
145
|
* @param res Express Response object
|
|
127
146
|
*/
|
|
128
|
-
async handleDeleteRequest(
|
|
129
|
-
req: Request<any, any, any, { sessionId?: string }>,
|
|
130
|
-
res: Response,
|
|
131
|
-
) {
|
|
147
|
+
async handleDeleteRequest(req: Request, res: Response) {
|
|
132
148
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
133
149
|
|
|
134
150
|
if (!sessionId) {
|
|
@@ -136,7 +152,13 @@ export class StreamableService implements OnModuleInit {
|
|
|
136
152
|
return;
|
|
137
153
|
}
|
|
138
154
|
|
|
139
|
-
const
|
|
155
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
156
|
+
|
|
157
|
+
if (!session) {
|
|
158
|
+
throw new Error('Session not found');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { transport } = session;
|
|
140
162
|
|
|
141
163
|
if (transport) {
|
|
142
164
|
this.logger.debug(
|
|
@@ -154,7 +176,7 @@ export class StreamableService implements OnModuleInit {
|
|
|
154
176
|
return;
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
|
|
179
|
+
this.sessionManager.deleteSession(sessionId);
|
|
158
180
|
|
|
159
181
|
res.status(200).json({ success: true, sessionId });
|
|
160
182
|
} else {
|
|
@@ -2,7 +2,19 @@ import {
|
|
|
2
2
|
CompleteResourceTemplateCallback,
|
|
3
3
|
ListResourcesCallback,
|
|
4
4
|
} from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
-
import {
|
|
5
|
+
import { RequestHandlerExtra as SdkRequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
6
|
+
import {
|
|
7
|
+
ServerNotification,
|
|
8
|
+
ServerRequest,
|
|
9
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import {
|
|
11
|
+
z,
|
|
12
|
+
ZodOptional,
|
|
13
|
+
ZodRawShape,
|
|
14
|
+
ZodType,
|
|
15
|
+
ZodTypeAny,
|
|
16
|
+
ZodTypeDef,
|
|
17
|
+
} from 'zod';
|
|
6
18
|
|
|
7
19
|
export interface ResourceBaseOptions {
|
|
8
20
|
name: string;
|
|
@@ -93,3 +105,102 @@ export interface TemplateCallbacks {
|
|
|
93
105
|
[variable: string]: CompleteResourceTemplateCallback;
|
|
94
106
|
};
|
|
95
107
|
}
|
|
108
|
+
|
|
109
|
+
export type RequestHandlerExtra = SdkRequestHandlerExtra<
|
|
110
|
+
ServerRequest,
|
|
111
|
+
ServerNotification
|
|
112
|
+
>;
|
|
113
|
+
|
|
114
|
+
export class ResourceUriHandlerParams {
|
|
115
|
+
public readonly uri: URL;
|
|
116
|
+
public readonly extra: RequestHandlerExtra;
|
|
117
|
+
|
|
118
|
+
private constructor(uri: URL, extra: RequestHandlerExtra) {
|
|
119
|
+
this.uri = uri;
|
|
120
|
+
this.extra = extra;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static from(uri: URL, extra: RequestHandlerExtra): ResourceUriHandlerParams {
|
|
124
|
+
return new ResourceUriHandlerParams(uri, extra);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class ResourceTemplateHandlerParams {
|
|
129
|
+
public readonly uri: URL;
|
|
130
|
+
public readonly variables?: Record<string, string>;
|
|
131
|
+
public readonly extra: RequestHandlerExtra;
|
|
132
|
+
|
|
133
|
+
private constructor(
|
|
134
|
+
uri: URL,
|
|
135
|
+
extra: RequestHandlerExtra,
|
|
136
|
+
variables?: Record<string, string>,
|
|
137
|
+
) {
|
|
138
|
+
this.uri = uri;
|
|
139
|
+
this.extra = extra;
|
|
140
|
+
this.variables = variables;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static from(
|
|
144
|
+
uri: URL,
|
|
145
|
+
extra: RequestHandlerExtra,
|
|
146
|
+
variables?: Record<string, string>,
|
|
147
|
+
): ResourceTemplateHandlerParams {
|
|
148
|
+
return new ResourceTemplateHandlerParams(uri, extra, variables);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class PromptHandlerParams<
|
|
153
|
+
Args extends PromptArgsRawShape | undefined = undefined,
|
|
154
|
+
> {
|
|
155
|
+
public readonly args?: Args extends PromptArgsRawShape
|
|
156
|
+
? z.objectOutputType<Args, ZodTypeAny>
|
|
157
|
+
: undefined;
|
|
158
|
+
public readonly extra: RequestHandlerExtra;
|
|
159
|
+
|
|
160
|
+
private constructor(
|
|
161
|
+
extra: RequestHandlerExtra,
|
|
162
|
+
args?: Args extends PromptArgsRawShape
|
|
163
|
+
? z.objectOutputType<Args, ZodTypeAny>
|
|
164
|
+
: undefined,
|
|
165
|
+
) {
|
|
166
|
+
this.extra = extra;
|
|
167
|
+
this.args = args;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static from<Args extends PromptArgsRawShape | undefined>(
|
|
171
|
+
extra: RequestHandlerExtra,
|
|
172
|
+
args?: Args extends PromptArgsRawShape
|
|
173
|
+
? z.objectOutputType<Args, ZodTypeAny>
|
|
174
|
+
: undefined,
|
|
175
|
+
): PromptHandlerParams<Args> {
|
|
176
|
+
return new PromptHandlerParams<Args>(extra, args);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export class ToolHandlerParams<
|
|
181
|
+
Args extends ZodRawShape | undefined = undefined,
|
|
182
|
+
> {
|
|
183
|
+
public readonly args?: Args extends ZodRawShape
|
|
184
|
+
? z.objectOutputType<Args, ZodTypeAny>
|
|
185
|
+
: undefined;
|
|
186
|
+
public readonly extra: RequestHandlerExtra;
|
|
187
|
+
|
|
188
|
+
private constructor(
|
|
189
|
+
extra: RequestHandlerExtra,
|
|
190
|
+
args?: Args extends ZodRawShape
|
|
191
|
+
? z.objectOutputType<Args, ZodTypeAny>
|
|
192
|
+
: undefined,
|
|
193
|
+
) {
|
|
194
|
+
this.extra = extra;
|
|
195
|
+
this.args = args;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static from<Args extends ZodRawShape | undefined>(
|
|
199
|
+
extra: RequestHandlerExtra,
|
|
200
|
+
args?: Args extends ZodRawShape
|
|
201
|
+
? z.objectOutputType<Args, ZodTypeAny>
|
|
202
|
+
: undefined,
|
|
203
|
+
): ToolHandlerParams<Args> {
|
|
204
|
+
return new ToolHandlerParams<Args>(extra, args);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { ExecutionContext } from '@nestjs/common';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
PromptHandlerParams,
|
|
5
|
+
ResourceTemplateHandlerParams,
|
|
6
|
+
ResourceUriHandlerParams,
|
|
7
|
+
ToolHandlerParams,
|
|
8
|
+
} from './capabilities.interface';
|
|
4
9
|
/**
|
|
5
10
|
* Custom execution context for MCP guards.
|
|
6
11
|
* Extends NestJS ExecutionContext and adds args for MCP method arguments.
|
|
@@ -8,11 +13,21 @@ import { McpMessage } from './message.types';
|
|
|
8
13
|
* @property args - The arguments passed to the MCP method
|
|
9
14
|
* @property message - The current message from the request
|
|
10
15
|
*/
|
|
11
|
-
// TODO:
|
|
16
|
+
// TODO: Remove extends ExecutionContext we don't need it
|
|
12
17
|
export interface McpExecutionContext extends ExecutionContext {
|
|
13
|
-
|
|
14
|
-
args:
|
|
18
|
+
// TODO: Remove this once the getArgs method implementation is complete.
|
|
19
|
+
args:
|
|
20
|
+
| ResourceUriHandlerParams
|
|
21
|
+
| ResourceTemplateHandlerParams
|
|
22
|
+
| PromptHandlerParams
|
|
23
|
+
| ToolHandlerParams;
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
// TODO: Uncomment this once the getArgs type is fixed
|
|
26
|
+
// getArgs: () =>
|
|
27
|
+
// | ResourceUriHandlerParams
|
|
28
|
+
// | ResourceTemplateHandlerParams
|
|
29
|
+
// | PromptHandlerParams
|
|
30
|
+
// | ToolHandlerParams;
|
|
31
|
+
|
|
32
|
+
getSessionId: () => string;
|
|
18
33
|
}
|
package/src/mcp.module.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Implementation } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
|
|
3
|
-
import {
|
|
3
|
+
import { DiscoveryModule } from '@nestjs/core';
|
|
4
4
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
5
5
|
|
|
6
6
|
import { SseController, SseService } from './controllers/sse';
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
StreamableController,
|
|
9
9
|
StreamableService,
|
|
10
10
|
} from './controllers/streamable';
|
|
11
|
-
import { RequestContextInterceptor } from './interceptors/message.interceptor';
|
|
12
11
|
import {
|
|
13
12
|
McpFeatureOptions,
|
|
14
13
|
McpLoggingOptions,
|
|
@@ -19,8 +18,7 @@ import {
|
|
|
19
18
|
import { DiscoveryService } from './registry/discovery.service';
|
|
20
19
|
import { McpLoggerService } from './registry/logger.service';
|
|
21
20
|
import { RegistryService } from './registry/registry.service';
|
|
22
|
-
import {
|
|
23
|
-
|
|
21
|
+
import { SessionManager } from './services/session.manager';
|
|
24
22
|
@Module({
|
|
25
23
|
imports: [DiscoveryModule],
|
|
26
24
|
providers: [
|
|
@@ -31,13 +29,9 @@ import { MessageService } from './services/message.service';
|
|
|
31
29
|
useValue: new AsyncLocalStorage(),
|
|
32
30
|
},
|
|
33
31
|
McpLoggerService,
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
provide: APP_INTERCEPTOR,
|
|
37
|
-
useClass: RequestContextInterceptor,
|
|
38
|
-
},
|
|
32
|
+
SessionManager,
|
|
39
33
|
],
|
|
40
|
-
exports: [
|
|
34
|
+
exports: [SessionManager],
|
|
41
35
|
})
|
|
42
36
|
export class McpModule {
|
|
43
37
|
/**
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import type { CanActivate, Type } from '@nestjs/common';
|
|
6
6
|
import { Injectable } from '@nestjs/common';
|
|
7
|
+
import { Reflector } from '@nestjs/core';
|
|
7
8
|
|
|
8
9
|
import { MCP_RESOLVER } from '../decorators';
|
|
9
10
|
import {
|
|
@@ -13,20 +14,27 @@ import {
|
|
|
13
14
|
} from '../decorators/capabilities.constants';
|
|
14
15
|
import { MCP_GUARDS } from '../decorators/capabilities.decorators';
|
|
15
16
|
import {
|
|
17
|
+
PromptHandlerParams,
|
|
16
18
|
PromptOptions,
|
|
19
|
+
RequestHandlerExtra,
|
|
17
20
|
ResourceOptions,
|
|
21
|
+
ResourceTemplateHandlerParams,
|
|
22
|
+
ResourceUriHandlerParams,
|
|
23
|
+
ToolHandlerParams,
|
|
18
24
|
ToolOptions,
|
|
19
25
|
} from '../interfaces/capabilities.interface';
|
|
20
26
|
import { McpExecutionContext } from '../interfaces/context.interface';
|
|
21
|
-
import {
|
|
27
|
+
import { SessionManager } from '../services/session.manager';
|
|
22
28
|
import { DiscoveryService } from './discovery.service';
|
|
23
29
|
import { McpLoggerService } from './logger.service';
|
|
30
|
+
|
|
24
31
|
@Injectable()
|
|
25
32
|
export class RegistryService {
|
|
26
33
|
constructor(
|
|
27
34
|
private readonly discoveryService: DiscoveryService,
|
|
28
35
|
private readonly logger: McpLoggerService,
|
|
29
|
-
private readonly
|
|
36
|
+
private readonly reflector: Reflector,
|
|
37
|
+
private readonly sessionManager: SessionManager,
|
|
30
38
|
) {}
|
|
31
39
|
|
|
32
40
|
async registerAll(server: McpServer): Promise<void> {
|
|
@@ -42,6 +50,57 @@ export class RegistryService {
|
|
|
42
50
|
]);
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
private getDecoratorType(method: Type<any> | undefined): string | null {
|
|
54
|
+
if (!method) return null;
|
|
55
|
+
|
|
56
|
+
if (this.reflector.get(MCP_TOOL, method)) return 'TOOL';
|
|
57
|
+
if (this.reflector.get(MCP_PROMPT, method)) return 'PROMPT';
|
|
58
|
+
if (this.reflector.get(MCP_RESOURCE, method)) return 'RESOURCE';
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private getHandlerArgs(
|
|
64
|
+
method: Type<any> | undefined,
|
|
65
|
+
args: unknown[],
|
|
66
|
+
):
|
|
67
|
+
| ResourceUriHandlerParams
|
|
68
|
+
| ResourceTemplateHandlerParams
|
|
69
|
+
| PromptHandlerParams
|
|
70
|
+
| ToolHandlerParams {
|
|
71
|
+
if (!method) throw new Error('Method not found');
|
|
72
|
+
|
|
73
|
+
switch (this.getDecoratorType(method)) {
|
|
74
|
+
case 'RESOURCE':
|
|
75
|
+
return args[0] instanceof URL
|
|
76
|
+
? ResourceUriHandlerParams.from(
|
|
77
|
+
args[0],
|
|
78
|
+
args[1] as RequestHandlerExtra,
|
|
79
|
+
)
|
|
80
|
+
: ResourceTemplateHandlerParams.from(
|
|
81
|
+
args[0] as any,
|
|
82
|
+
args[2] as RequestHandlerExtra,
|
|
83
|
+
args[1] as Record<string, string>,
|
|
84
|
+
);
|
|
85
|
+
case 'PROMPT':
|
|
86
|
+
return args.length === 1
|
|
87
|
+
? PromptHandlerParams.from(args[0] as RequestHandlerExtra)
|
|
88
|
+
: PromptHandlerParams.from(
|
|
89
|
+
args[1] as RequestHandlerExtra,
|
|
90
|
+
args[0] as any,
|
|
91
|
+
);
|
|
92
|
+
case 'TOOL':
|
|
93
|
+
return args.length === 1
|
|
94
|
+
? ToolHandlerParams.from(args[0] as RequestHandlerExtra)
|
|
95
|
+
: ToolHandlerParams.from(
|
|
96
|
+
args[1] as RequestHandlerExtra,
|
|
97
|
+
args[0] as any,
|
|
98
|
+
);
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Unknown decorator type for method ${method.name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
45
104
|
/**
|
|
46
105
|
* Executes all guards attached to the resolver class and method.
|
|
47
106
|
* Throws an error if any guard denies access.
|
|
@@ -58,6 +117,7 @@ export class RegistryService {
|
|
|
58
117
|
): Promise<void> {
|
|
59
118
|
// Retrieve class-level guards
|
|
60
119
|
const classConstructor = instance.constructor;
|
|
120
|
+
|
|
61
121
|
const classGuards: (CanActivate | { new (): CanActivate })[] =
|
|
62
122
|
(Reflect.getMetadata(MCP_GUARDS, classConstructor) as (
|
|
63
123
|
| CanActivate
|
|
@@ -69,7 +129,8 @@ export class RegistryService {
|
|
|
69
129
|
string,
|
|
70
130
|
unknown
|
|
71
131
|
>;
|
|
72
|
-
|
|
132
|
+
|
|
133
|
+
const methodKey = prototype[methodName] as Type<any> | undefined;
|
|
73
134
|
|
|
74
135
|
const methodGuards: (CanActivate | { new (): CanActivate })[] =
|
|
75
136
|
(methodKey &&
|
|
@@ -83,16 +144,39 @@ export class RegistryService {
|
|
|
83
144
|
const allGuards = [...classGuards, ...methodGuards];
|
|
84
145
|
|
|
85
146
|
if (!allGuards.length) return Promise.resolve();
|
|
86
|
-
// Build a minimal context (customize as needed)
|
|
87
|
-
const context: McpExecutionContext = {
|
|
88
|
-
args,
|
|
89
|
-
message: this.messageService.get(),
|
|
90
147
|
|
|
91
|
-
|
|
92
|
-
|
|
148
|
+
const handlerArgs = this.getHandlerArgs(methodKey, args);
|
|
149
|
+
|
|
150
|
+
const { sessionId } = handlerArgs.extra;
|
|
151
|
+
|
|
152
|
+
if (!sessionId) {
|
|
153
|
+
throw new Error('Session not found');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
157
|
+
|
|
158
|
+
if (!session) {
|
|
159
|
+
throw new Error('Session not found');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const context: McpExecutionContext = {
|
|
163
|
+
args: handlerArgs,
|
|
164
|
+
// @ts-expect-error: Default types are 'http' | 'ws' | 'rpc' but in our case, we are using 'mcp'
|
|
93
165
|
getType: () => 'mcp',
|
|
94
166
|
getClass: () => instance.constructor as Type<any>,
|
|
95
|
-
getArgs: <T
|
|
167
|
+
getArgs: <T = any>() => args as T,
|
|
168
|
+
getArgByIndex: <T = any>(index: number) => args[index] as T,
|
|
169
|
+
getSessionId: () => sessionId,
|
|
170
|
+
getHandler: () => methodKey as unknown as Type<any>,
|
|
171
|
+
switchToHttp: () => ({
|
|
172
|
+
getRequest: <R = Request>() => session.request as R,
|
|
173
|
+
getResponse: () => {
|
|
174
|
+
throw new Error('Response not available in MCP context');
|
|
175
|
+
},
|
|
176
|
+
getNext: () => {
|
|
177
|
+
throw new Error('Next not available in MCP context');
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
96
180
|
};
|
|
97
181
|
|
|
98
182
|
return (async () => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { Injectable } from '@nestjs/common';
|
|
4
|
+
import { Request } from 'express';
|
|
5
|
+
|
|
6
|
+
type Session = {
|
|
7
|
+
transport: StreamableHTTPServerTransport | SSEServerTransport;
|
|
8
|
+
request: Request;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class SessionManager {
|
|
13
|
+
private sessions = new Map<string, Session>();
|
|
14
|
+
|
|
15
|
+
public getSession(sessionId: string): Session | undefined {
|
|
16
|
+
return this.sessions.get(sessionId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public setSession(sessionId: string, session: Session): void {
|
|
20
|
+
this.sessions.set(sessionId, session);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public deleteSession(sessionId: string): void {
|
|
24
|
+
this.sessions.delete(sessionId);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
|
2
|
-
import { Observable } from 'rxjs';
|
|
3
|
-
import { McpLoggerService } from '../registry/logger.service';
|
|
4
|
-
import { MessageService } from '../services/message.service';
|
|
5
|
-
export declare class RequestContextInterceptor implements NestInterceptor {
|
|
6
|
-
private readonly logger;
|
|
7
|
-
private readonly messageService;
|
|
8
|
-
constructor(logger: McpLoggerService, messageService: MessageService);
|
|
9
|
-
intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
|
|
10
|
-
}
|