@nestjs-mcp/server 0.1.0-alpha.10
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/.copilotignore +38 -0
- package/.devcontainer/Dockerfile.dev +28 -0
- package/.devcontainer/devcontainer.json +56 -0
- package/.devcontainer/docker-compose.yml +15 -0
- package/.dockerignore +37 -0
- package/.prettierrc +4 -0
- package/LICENSE +21 -0
- package/README.md +540 -0
- package/dist/controllers/sse/index.d.ts +2 -0
- package/dist/controllers/sse/index.js +19 -0
- package/dist/controllers/sse/index.js.map +1 -0
- package/dist/controllers/sse/sse.controller.d.ts +10 -0
- package/dist/controllers/sse/sse.controller.js +57 -0
- package/dist/controllers/sse/sse.controller.js.map +1 -0
- package/dist/controllers/sse/sse.service.d.ts +16 -0
- package/dist/controllers/sse/sse.service.js +78 -0
- package/dist/controllers/sse/sse.service.js.map +1 -0
- package/dist/controllers/streamable/index.d.ts +2 -0
- package/dist/controllers/streamable/index.js +19 -0
- package/dist/controllers/streamable/index.js.map +1 -0
- package/dist/controllers/streamable/streamable.controller.d.ts +9 -0
- package/dist/controllers/streamable/streamable.controller.js +62 -0
- package/dist/controllers/streamable/streamable.controller.js.map +1 -0
- package/dist/controllers/streamable/streamable.service.d.ts +24 -0
- package/dist/controllers/streamable/streamable.service.js +118 -0
- package/dist/controllers/streamable/streamable.service.js.map +1 -0
- package/dist/decorators/capabilities.constants.d.ts +4 -0
- package/dist/decorators/capabilities.constants.js +8 -0
- package/dist/decorators/capabilities.constants.js.map +1 -0
- package/dist/decorators/capabilities.decorators.d.ts +8 -0
- package/dist/decorators/capabilities.decorators.js +49 -0
- package/dist/decorators/capabilities.decorators.js.map +1 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +19 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors/message.interceptor.d.ts +10 -0
- package/dist/interceptors/message.interceptor.js +61 -0
- package/dist/interceptors/message.interceptor.js.map +1 -0
- package/dist/interfaces/capabilities.interface.d.ts +52 -0
- package/dist/interfaces/capabilities.interface.js +3 -0
- package/dist/interfaces/capabilities.interface.js.map +1 -0
- package/dist/interfaces/context.interface.d.ts +6 -0
- package/dist/interfaces/context.interface.js +3 -0
- package/dist/interfaces/context.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +2 -0
- package/dist/interfaces/index.js +19 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/interfaces/mcp-server-options.interface.d.ts +42 -0
- package/dist/interfaces/mcp-server-options.interface.js +3 -0
- package/dist/interfaces/mcp-server-options.interface.js.map +1 -0
- package/dist/interfaces/message.types.d.ts +8 -0
- package/dist/interfaces/message.types.js +3 -0
- package/dist/interfaces/message.types.js.map +1 -0
- package/dist/mcp.module.d.ts +13 -0
- package/dist/mcp.module.js +193 -0
- package/dist/mcp.module.js.map +1 -0
- package/dist/registry/discovery.service.d.ts +16 -0
- package/dist/registry/discovery.service.js +85 -0
- package/dist/registry/discovery.service.js.map +1 -0
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.js +19 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/logger.service.d.ts +16 -0
- package/dist/registry/logger.service.js +97 -0
- package/dist/registry/logger.service.js.map +1 -0
- package/dist/registry/registry.service.d.ts +16 -0
- package/dist/registry/registry.service.js +170 -0
- package/dist/registry/registry.service.js.map +1 -0
- package/dist/services/message.service.d.ts +7 -0
- package/dist/services/message.service.js +25 -0
- package/dist/services/message.service.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/eslint.config.mjs +40 -0
- package/package.json +109 -0
- package/src/controllers/sse/index.ts +2 -0
- package/src/controllers/sse/sse.controller.ts +25 -0
- package/src/controllers/sse/sse.service.ts +90 -0
- package/src/controllers/streamable/index.ts +2 -0
- package/src/controllers/streamable/streamable.controller.ts +24 -0
- package/src/controllers/streamable/streamable.service.ts +169 -0
- package/src/decorators/capabilities.constants.ts +7 -0
- package/src/decorators/capabilities.decorators.ts +150 -0
- package/src/decorators/index.ts +2 -0
- package/src/index.ts +11 -0
- package/src/interceptors/message.interceptor.ts +70 -0
- package/src/interfaces/capabilities.interface.ts +95 -0
- package/src/interfaces/context.interface.ts +18 -0
- package/src/interfaces/index.ts +2 -0
- package/src/interfaces/mcp-server-options.interface.ts +105 -0
- package/src/interfaces/message.types.ts +13 -0
- package/src/mcp.module.ts +250 -0
- package/src/mcp.service.spec.ts +28 -0
- package/src/registry/discovery.service.ts +116 -0
- package/src/registry/index.ts +2 -0
- package/src/registry/logger.service.ts +143 -0
- package/src/registry/registry.service.ts +282 -0
- package/src/services/message.service.ts +18 -0
package/package.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nestjs-mcp/server",
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
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
|
+
"author": "Adrián Darío Hidalgo Flores",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22",
|
|
9
|
+
"pnpm": ">=10"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/adrian-d-hidalgo/nestjs-mcp-server.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/adrian-d-hidalgo/nestjs-mcp-server#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/adrian-d-hidalgo/nestjs-mcp-server/issues"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"decorators",
|
|
26
|
+
"integration",
|
|
27
|
+
"large-language-models",
|
|
28
|
+
"llm",
|
|
29
|
+
"mcp",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"module",
|
|
32
|
+
"nestjs",
|
|
33
|
+
"sdk",
|
|
34
|
+
"server",
|
|
35
|
+
"typescript"
|
|
36
|
+
],
|
|
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
|
+
"peerDependencies": {
|
|
53
|
+
"@nestjs/common": "^11.0.1",
|
|
54
|
+
"@nestjs/core": "^11.0.1",
|
|
55
|
+
"@nestjs/platform-express": "^11.0.1",
|
|
56
|
+
"reflect-metadata": "^0.2.2",
|
|
57
|
+
"rxjs": "^7.8.1"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@modelcontextprotocol/sdk": "^1.10.2",
|
|
61
|
+
"zod": "^3.24.3"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
65
|
+
"@eslint/js": "^9.18.0",
|
|
66
|
+
"@nestjs/cli": "^11.0.0",
|
|
67
|
+
"@nestjs/config": "^4.0.2",
|
|
68
|
+
"@nestjs/schematics": "^11.0.0",
|
|
69
|
+
"@nestjs/testing": "^11.0.1",
|
|
70
|
+
"@swc/cli": "^0.6.0",
|
|
71
|
+
"@swc/core": "^1.10.7",
|
|
72
|
+
"@types/express": "^5.0.0",
|
|
73
|
+
"@types/jest": "^29.5.14",
|
|
74
|
+
"@types/node": "^22.10.7",
|
|
75
|
+
"@types/supertest": "^6.0.2",
|
|
76
|
+
"eslint": "^9.18.0",
|
|
77
|
+
"eslint-config-prettier": "^10.0.1",
|
|
78
|
+
"eslint-plugin-prettier": "^5.2.2",
|
|
79
|
+
"globals": "^16.0.0",
|
|
80
|
+
"husky": "^9.1.7",
|
|
81
|
+
"jest": "^29.7.0",
|
|
82
|
+
"prettier": "^3.4.2",
|
|
83
|
+
"source-map-support": "^0.5.21",
|
|
84
|
+
"supertest": "^7.0.0",
|
|
85
|
+
"ts-jest": "^29.2.5",
|
|
86
|
+
"ts-loader": "^9.5.2",
|
|
87
|
+
"ts-node": "^10.9.2",
|
|
88
|
+
"tsconfig-paths": "^4.2.0",
|
|
89
|
+
"typescript": "^5.7.3",
|
|
90
|
+
"typescript-eslint": "^8.20.0"
|
|
91
|
+
},
|
|
92
|
+
"jest": {
|
|
93
|
+
"moduleFileExtensions": [
|
|
94
|
+
"js",
|
|
95
|
+
"json",
|
|
96
|
+
"ts"
|
|
97
|
+
],
|
|
98
|
+
"rootDir": "src",
|
|
99
|
+
"testRegex": ".*\\.spec\\.ts$",
|
|
100
|
+
"transform": {
|
|
101
|
+
"^.+\\.(t|j)s$": "ts-jest"
|
|
102
|
+
},
|
|
103
|
+
"collectCoverageFrom": [
|
|
104
|
+
"**/*.(t|j)s"
|
|
105
|
+
],
|
|
106
|
+
"coverageDirectory": "../coverage",
|
|
107
|
+
"testEnvironment": "node"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Controller, Get, Post, Req, Res } from '@nestjs/common';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { McpLoggerService } from '../../registry/logger.service';
|
|
5
|
+
import { SseService } from './sse.service';
|
|
6
|
+
|
|
7
|
+
@Controller()
|
|
8
|
+
export class SseController {
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly logger: McpLoggerService,
|
|
11
|
+
private readonly service: SseService,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
@Get('sse')
|
|
15
|
+
async handleSse(@Req() req: Request, @Res() res: Response) {
|
|
16
|
+
this.logger.log('[SSE] Connection established');
|
|
17
|
+
await this.service.handleSse(req, res);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Post('messages')
|
|
21
|
+
async handleMessages(@Req() req: Request, @Res() res: Response) {
|
|
22
|
+
this.logger.log('[SSE] Message received');
|
|
23
|
+
await this.service.handleMessage(req, res);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
3
|
+
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
|
4
|
+
import { Request, Response } from 'express';
|
|
5
|
+
|
|
6
|
+
import { McpServerOptions } from '../../interfaces/mcp-server-options.interface';
|
|
7
|
+
import { McpLoggerService } from '../../registry/logger.service';
|
|
8
|
+
import { RegistryService } from '../../registry/registry.service';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class SseService implements OnModuleInit {
|
|
12
|
+
private server: McpServer;
|
|
13
|
+
|
|
14
|
+
private transports = {} as Record<string, SSEServerTransport>;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
@Inject('MCP_SERVER_OPTIONS')
|
|
18
|
+
private readonly options: McpServerOptions,
|
|
19
|
+
private readonly registry: RegistryService,
|
|
20
|
+
private readonly logger: McpLoggerService,
|
|
21
|
+
) {
|
|
22
|
+
this.server = new McpServer(this.options.serverInfo, this.options.options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async onModuleInit() {
|
|
26
|
+
this.logger.log('Starting MCP controller registration', 'MCP_SERVER');
|
|
27
|
+
await this.registry.registerAll(this.server);
|
|
28
|
+
this.logger.log('MCP initialization completed', 'MCP_SERVER');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Handle an SSE request for server-sent events
|
|
33
|
+
*
|
|
34
|
+
* This establishes a connection for server-to-client notifications
|
|
35
|
+
*/
|
|
36
|
+
async handleSse(req: Request, res: Response) {
|
|
37
|
+
// Create SSE transport for legacy clients
|
|
38
|
+
const transport = new SSEServerTransport('/messages', res);
|
|
39
|
+
this.transports[transport.sessionId] = transport;
|
|
40
|
+
|
|
41
|
+
this.logger.debug(
|
|
42
|
+
`Starting SSE for sessionId: ${transport.sessionId}`,
|
|
43
|
+
'api',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
res.on('close', () => {
|
|
47
|
+
delete this.transports[transport.sessionId];
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await this.server.connect(transport);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handle SSE messages sent from client to server
|
|
55
|
+
*/
|
|
56
|
+
async handleMessage(req: Request, res: Response) {
|
|
57
|
+
const sessionId = req.query.sessionId as string;
|
|
58
|
+
const transport = this.transports[sessionId];
|
|
59
|
+
|
|
60
|
+
this.logger.debug(
|
|
61
|
+
`Receiving SSE message for sessionId: ${sessionId}`,
|
|
62
|
+
'api',
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
this.logger.debug(`SSE message: ${JSON.stringify(req.body)}`, 'MCP_SERVER');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (transport) {
|
|
69
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
70
|
+
} else {
|
|
71
|
+
res.status(400).send('No transport found for sessionId');
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const errorMessage =
|
|
75
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
76
|
+
|
|
77
|
+
this.logger.error(
|
|
78
|
+
'Error al manejar mensaje SSE',
|
|
79
|
+
errorMessage,
|
|
80
|
+
'MCP_SERVER',
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
res.status(500).send({
|
|
84
|
+
statusCode: 500,
|
|
85
|
+
error: 'Internal Server Error',
|
|
86
|
+
message: errorMessage,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Controller, Delete, Get, Post, Req, Res } from '@nestjs/common';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { StreamableService } from './streamable.service';
|
|
5
|
+
|
|
6
|
+
@Controller()
|
|
7
|
+
export class StreamableController {
|
|
8
|
+
constructor(private readonly service: StreamableService) {}
|
|
9
|
+
|
|
10
|
+
@Post('mcp')
|
|
11
|
+
async handleMcpPost(@Req() req: Request, @Res() res: Response) {
|
|
12
|
+
await this.service.handlePostRequest(req, res);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Get('mcp')
|
|
16
|
+
async handleMcpGet(@Req() req: Request, @Res() res: Response) {
|
|
17
|
+
await this.service.handleGetRequest(req, res);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Delete('mcp')
|
|
21
|
+
async handleMcpDelete(@Req() req: Request, @Res() res: Response) {
|
|
22
|
+
await this.service.handleDeleteRequest(req, res);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import { Request, Response } from 'express';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
McpModuleTransportOptions,
|
|
10
|
+
McpServerOptions,
|
|
11
|
+
} from '../../interfaces/mcp-server-options.interface';
|
|
12
|
+
import { McpLoggerService } from '../../registry/logger.service';
|
|
13
|
+
import { RegistryService } from '../../registry/registry.service';
|
|
14
|
+
// TODO: Stateless mode should be handled here or in another service
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class StreamableService implements OnModuleInit {
|
|
18
|
+
private server: McpServer;
|
|
19
|
+
|
|
20
|
+
private transports = {} as Record<string, StreamableHTTPServerTransport>;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
@Inject('MCP_SERVER_OPTIONS')
|
|
24
|
+
private readonly options: McpServerOptions,
|
|
25
|
+
@Inject('MCP_TRANSPORT_OPTIONS')
|
|
26
|
+
private readonly transportOptions: McpModuleTransportOptions,
|
|
27
|
+
private readonly registry: RegistryService,
|
|
28
|
+
private readonly logger: McpLoggerService,
|
|
29
|
+
) {
|
|
30
|
+
this.server = new McpServer(this.options.serverInfo, this.options.options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async onModuleInit() {
|
|
34
|
+
await this.registry.registerAll(this.server);
|
|
35
|
+
|
|
36
|
+
this.logger.log('MCP STREAMEABLE initialization completed');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handle a streamable HTTP POST request from a client
|
|
41
|
+
*
|
|
42
|
+
* - Uses sessionId from query or generates a new one if missing
|
|
43
|
+
* - Stores the transport by sessionId for later retrieval
|
|
44
|
+
* - Cleans up transport on connection close
|
|
45
|
+
*
|
|
46
|
+
* @param req Express Request object (expects sessionId in query)
|
|
47
|
+
* @param res Express Response object
|
|
48
|
+
*/
|
|
49
|
+
async handlePostRequest(
|
|
50
|
+
req: Request<any, any, any, { sessionId?: string }>,
|
|
51
|
+
res: Response,
|
|
52
|
+
) {
|
|
53
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
54
|
+
let transport: StreamableHTTPServerTransport;
|
|
55
|
+
|
|
56
|
+
const { options } = this.transportOptions?.streamable || {};
|
|
57
|
+
|
|
58
|
+
if (sessionId && this.transports[sessionId]) {
|
|
59
|
+
transport = this.transports[sessionId];
|
|
60
|
+
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
61
|
+
transport = new StreamableHTTPServerTransport({
|
|
62
|
+
sessionIdGenerator: () =>
|
|
63
|
+
options?.sessionIdGenerator?.() || randomUUID(),
|
|
64
|
+
onsessioninitialized: (sessionId) => {
|
|
65
|
+
this.transports[sessionId] = transport;
|
|
66
|
+
},
|
|
67
|
+
enableJsonResponse: options?.enableJsonResponse,
|
|
68
|
+
eventStore: options?.eventStore,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
transport.onclose = () => {
|
|
72
|
+
if (transport.sessionId) {
|
|
73
|
+
delete this.transports[transport.sessionId];
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
await this.server.connect(transport);
|
|
78
|
+
} else {
|
|
79
|
+
// Invalid request
|
|
80
|
+
res.status(400).json({
|
|
81
|
+
jsonrpc: '2.0',
|
|
82
|
+
error: {
|
|
83
|
+
code: -32000,
|
|
84
|
+
message: 'Bad Request: No valid session ID provided',
|
|
85
|
+
},
|
|
86
|
+
id: null,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await transport.handleRequest(req, res, req.body);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle a streamable HTTP GET request from a client
|
|
97
|
+
*
|
|
98
|
+
* This method retrieves the existing streamable transport for the session and delegates the request.
|
|
99
|
+
*
|
|
100
|
+
* @param req Express Request object (expects sessionId in query)
|
|
101
|
+
* @param res Express Response object
|
|
102
|
+
*/
|
|
103
|
+
async handleGetRequest(
|
|
104
|
+
req: Request<any, any, any, { sessionId?: string }>,
|
|
105
|
+
res: Response,
|
|
106
|
+
) {
|
|
107
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
108
|
+
|
|
109
|
+
if (!sessionId || !this.transports[sessionId]) {
|
|
110
|
+
res.status(400).send('Invalid or missing session ID');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const transport = this.transports[sessionId];
|
|
115
|
+
|
|
116
|
+
await transport.handleRequest(req, res);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle a streamable HTTP DELETE request to clean up a session
|
|
121
|
+
*
|
|
122
|
+
* - Accepts sessionId from query or x-mcp-session-id header
|
|
123
|
+
* - Closes and removes the transport if found
|
|
124
|
+
* - Always sends a response
|
|
125
|
+
*
|
|
126
|
+
* @param req Express Request object
|
|
127
|
+
* @param res Express Response object
|
|
128
|
+
*/
|
|
129
|
+
async handleDeleteRequest(
|
|
130
|
+
req: Request<any, any, any, { sessionId?: string }>,
|
|
131
|
+
res: Response,
|
|
132
|
+
) {
|
|
133
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
134
|
+
|
|
135
|
+
if (!sessionId) {
|
|
136
|
+
res.status(400).json({ error: 'Missing sessionId' });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const transport = this.transports[sessionId];
|
|
141
|
+
|
|
142
|
+
if (transport) {
|
|
143
|
+
this.logger.debug(
|
|
144
|
+
`Closing streamable transport for sessionId: ${sessionId}`,
|
|
145
|
+
'STREAMABLE',
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await transport.close();
|
|
149
|
+
|
|
150
|
+
const uuidV4Regex =
|
|
151
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
152
|
+
|
|
153
|
+
if (!uuidV4Regex.test(sessionId)) {
|
|
154
|
+
res.status(400).json({ error: 'Invalid sessionId format' });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
delete this.transports[sessionId];
|
|
159
|
+
|
|
160
|
+
res.status(200).json({ success: true, sessionId });
|
|
161
|
+
} else {
|
|
162
|
+
this.logger.debug(
|
|
163
|
+
`No streamable transport found for sessionId: ${sessionId}`,
|
|
164
|
+
'STREAMABLE',
|
|
165
|
+
);
|
|
166
|
+
res.status(404).json({ error: 'Transport not found', sessionId });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Prefijo para todas las constantes de metadatos
|
|
2
|
+
export const PREFIX = 'MCP';
|
|
3
|
+
|
|
4
|
+
// Constantes de metadatos para decoradores de método
|
|
5
|
+
export const MCP_TOOL = `${PREFIX}:tool`;
|
|
6
|
+
export const MCP_PROMPT = `${PREFIX}:prompt`;
|
|
7
|
+
export const MCP_RESOURCE = `${PREFIX}:resource`;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Injectable, SetMetadata } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PromptOptions,
|
|
5
|
+
ResourceOptions,
|
|
6
|
+
ToolOptions,
|
|
7
|
+
} from '../interfaces/capabilities.interface';
|
|
8
|
+
import { MCP_PROMPT, MCP_RESOURCE, MCP_TOOL } from './capabilities.constants';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Metadata key to mark a class as an MCP Resolver.
|
|
12
|
+
*/
|
|
13
|
+
export const MCP_RESOLVER = '__mcp_resolver__';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Metadata key to attach guards to a Resolver class or method.
|
|
17
|
+
*/
|
|
18
|
+
export const MCP_GUARDS = '__mcp_guards__';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decorator for marking a class as an MCP Resolver.
|
|
22
|
+
* Enables dependency injection and workspace grouping for MCP capabilities.
|
|
23
|
+
*
|
|
24
|
+
* @param workspace Optional workspace/namespace for grouping capabilities
|
|
25
|
+
* @example
|
|
26
|
+
* @Resolver('my-workspace')
|
|
27
|
+
* export class MyResolver { ... }
|
|
28
|
+
*/
|
|
29
|
+
export function Resolver(workspace?: string): ClassDecorator {
|
|
30
|
+
return function (target: any) {
|
|
31
|
+
Injectable()(target);
|
|
32
|
+
SetMetadata(MCP_RESOLVER, workspace || true)(target);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decorator to attach one or more guards to a Resolver class or method.
|
|
38
|
+
* Accepts guard classes or instances implementing CanActivate.
|
|
39
|
+
*
|
|
40
|
+
* @param guards One or more guard classes or instances
|
|
41
|
+
* @example
|
|
42
|
+
* @UseGuards(MyGuard)
|
|
43
|
+
* @Resolver('workspace')
|
|
44
|
+
* export class MyResolver { ... }
|
|
45
|
+
*/
|
|
46
|
+
export function UseGuards(...guards: any[]): ClassDecorator & MethodDecorator {
|
|
47
|
+
return SetMetadata(MCP_GUARDS, guards);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decorator for marking a method as an MCP Tool.
|
|
52
|
+
* Use with @McpProvider.
|
|
53
|
+
*
|
|
54
|
+
* La herramienta debe devolver un objeto con el formato:
|
|
55
|
+
* {
|
|
56
|
+
* content: [
|
|
57
|
+
* {
|
|
58
|
+
* type: 'text', // Puede ser 'text', 'image', 'video', 'audio', etc.
|
|
59
|
+
* text: 'Texto de la respuesta',
|
|
60
|
+
* }
|
|
61
|
+
* ]
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* @param options Tool configuration
|
|
65
|
+
*/
|
|
66
|
+
export function Tool(options: ToolOptions) {
|
|
67
|
+
return function (
|
|
68
|
+
target: object,
|
|
69
|
+
propertyKey: string,
|
|
70
|
+
descriptor: PropertyDescriptor,
|
|
71
|
+
) {
|
|
72
|
+
SetMetadata(MCP_TOOL, {
|
|
73
|
+
...options,
|
|
74
|
+
methodName: propertyKey,
|
|
75
|
+
})(target, propertyKey, descriptor);
|
|
76
|
+
|
|
77
|
+
return descriptor;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Decorator for marking a method as an MCP Prompt.
|
|
83
|
+
* Use with @McpProvider.
|
|
84
|
+
*
|
|
85
|
+
* El prompt debe devolver un objeto con el formato:
|
|
86
|
+
* {
|
|
87
|
+
* messages: [
|
|
88
|
+
* {
|
|
89
|
+
* role: 'assistant',
|
|
90
|
+
* content: {
|
|
91
|
+
* type: 'text',
|
|
92
|
+
* text: 'Texto del mensaje'
|
|
93
|
+
* }
|
|
94
|
+
* }
|
|
95
|
+
* ]
|
|
96
|
+
* }
|
|
97
|
+
*
|
|
98
|
+
* @param options Prompt configuration
|
|
99
|
+
*/
|
|
100
|
+
export function Prompt(options: PromptOptions) {
|
|
101
|
+
return function (
|
|
102
|
+
target: object,
|
|
103
|
+
propertyKey: string,
|
|
104
|
+
descriptor: PropertyDescriptor,
|
|
105
|
+
) {
|
|
106
|
+
SetMetadata(MCP_PROMPT, {
|
|
107
|
+
...options,
|
|
108
|
+
methodName: propertyKey,
|
|
109
|
+
})(target, propertyKey, descriptor);
|
|
110
|
+
|
|
111
|
+
return descriptor;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Decorator for marking a method as an MCP Resource provider.
|
|
117
|
+
* Use with @McpProvider.
|
|
118
|
+
*
|
|
119
|
+
* Hay dos modos de uso para los recursos:
|
|
120
|
+
*
|
|
121
|
+
* 1. Recurso con URI fija:
|
|
122
|
+
* @Resource({
|
|
123
|
+
* name: 'nombreRecurso',
|
|
124
|
+
* uri: 'resource://midominio/recurso'
|
|
125
|
+
* })
|
|
126
|
+
*
|
|
127
|
+
* 2. Recurso con plantilla (para parámetros dinámicos):
|
|
128
|
+
* @Resource({
|
|
129
|
+
* name: 'nombreRecurso',
|
|
130
|
+
* template: 'resource://midominio/recurso/{parametro}'
|
|
131
|
+
* })
|
|
132
|
+
*
|
|
133
|
+
* También se puede proporcionar solo el nombre como string:
|
|
134
|
+
* @Resource('nombreRecurso')
|
|
135
|
+
*
|
|
136
|
+
* @param options Resource configuration or just the name as a string
|
|
137
|
+
*/
|
|
138
|
+
export function Resource(options: ResourceOptions) {
|
|
139
|
+
return function (
|
|
140
|
+
target: object,
|
|
141
|
+
propertyKey: string,
|
|
142
|
+
descriptor: PropertyDescriptor,
|
|
143
|
+
) {
|
|
144
|
+
SetMetadata(MCP_RESOURCE, {
|
|
145
|
+
...options,
|
|
146
|
+
methodName: propertyKey,
|
|
147
|
+
})(target, propertyKey, descriptor);
|
|
148
|
+
return descriptor;
|
|
149
|
+
};
|
|
150
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CallHandler,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
Injectable,
|
|
5
|
+
NestInterceptor,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { Request, Response } from 'express';
|
|
8
|
+
import { Observable } from 'rxjs';
|
|
9
|
+
|
|
10
|
+
import { McpMessage } from '../interfaces/message.types';
|
|
11
|
+
import { McpLoggerService } from '../registry/logger.service';
|
|
12
|
+
import { MessageService } from '../services/message.service';
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class RequestContextInterceptor implements NestInterceptor {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly logger: McpLoggerService,
|
|
17
|
+
private readonly messageService: MessageService,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
21
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
22
|
+
const response = context.switchToHttp().getResponse<Response>();
|
|
23
|
+
let message: McpMessage | undefined;
|
|
24
|
+
|
|
25
|
+
this.logger.log(
|
|
26
|
+
`Request path: ${request.path}`,
|
|
27
|
+
'[RequestContextInterceptor]',
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
switch (request.path) {
|
|
31
|
+
case '/sse':
|
|
32
|
+
this.logger.log(
|
|
33
|
+
'SSE connection detected',
|
|
34
|
+
'[RequestContextInterceptor]',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
message = {
|
|
38
|
+
req: request,
|
|
39
|
+
res: response,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.messageService.set(message);
|
|
43
|
+
|
|
44
|
+
break;
|
|
45
|
+
case '/messages':
|
|
46
|
+
this.logger.log('SSE message received', '[RequestContextInterceptor]');
|
|
47
|
+
|
|
48
|
+
message = {
|
|
49
|
+
req: request,
|
|
50
|
+
res: response,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.messageService.set(message);
|
|
54
|
+
|
|
55
|
+
break;
|
|
56
|
+
case '/mcp':
|
|
57
|
+
this.logger.log('MCP request detected', '[RequestContextInterceptor]');
|
|
58
|
+
// TODO: Handle MCP request
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
this.logger.log(
|
|
62
|
+
`Regular request: ${request.method} ${request.url}`,
|
|
63
|
+
'[RequestContextInterceptor]',
|
|
64
|
+
);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return next.handle();
|
|
69
|
+
}
|
|
70
|
+
}
|