@muzikanto/nestjs-mcp 1.3.0 → 1.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @muzikanto/nestjs-mcp
2
2
 
3
+ ## 1.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - custom mcp errors for filters
8
+
9
+ ## 1.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - add UseFilters support
14
+
15
+ ## 1.4.0
16
+
17
+ ### Minor Changes
18
+
19
+ - Dynamic tools, prompts, resources initialization
20
+
3
21
  ## 1.3.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @muzikanto/nestjs-mcp
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@muzikanto/nestjs-mcp)](https://www.npmjs.com/package/@muzikanto/nestjs-mcp)
4
- [![downloads](https://img.shields.io/npm/dt/@muzikanto/nestjs-mcp)]((https://www.npmjs.com/package/@muzikanto/nestjs-mcp))
4
+ [![downloads](https://img.shields.io/npm/dt/@muzikanto/nestjs-mcp)](<(https://www.npmjs.com/package/@muzikanto/nestjs-mcp)>)
5
5
  [![GitHub stars](https://img.shields.io/github/stars/Muzikanto/nestjs-mcp?style=social)](https://github.com/Muzikanto/nestjs-mcp)
6
6
  [![License](https://img.shields.io/npm/l/@muzikanto/nestjs-mcp)](https://github.com/Muzikanto/nestjs-mcp/blob/main/LICENSE)
7
7
 
@@ -10,6 +10,7 @@ NestJS MCP (Model Context Protocol) module — allows you to create “tools”,
10
10
  ---
11
11
 
12
12
  ## Contents
13
+
13
14
  - [Features](#features)
14
15
  - [Installation](#installation)
15
16
  - [Usage](#usage)
@@ -17,11 +18,18 @@ NestJS MCP (Model Context Protocol) module — allows you to create “tools”,
17
18
  - [Create MCP tool](#create-mcp-tool)
18
19
  - [Create MCP prompt](#create-mcp-prompt)
19
20
  - [Create MCP resource](#create-mcp-resource)
21
+ - [Dynamic creation](#dynamic-creation)
20
22
  - [Calling MCP tools via HTTP](#calling-mcp-tools-via-http)
21
23
  - [Calling MCP prompt via HTTP](#obtain-prompt)
22
24
  - [Obtaining all tools](#obtaining-all-tools)
23
25
  - [Obtaining all prompts](#obtaining-all-prompts)
24
- - [Auth guard](#auth-guard)
26
+ - [Guards](#guards)
27
+ - [For one](#for-one)
28
+ - [For all](#for-all)
29
+ - [Interceptors](#interceptors)
30
+ - [For one](#for-one-1)
31
+ - [For all](#for-all-1)
32
+ - [Filters](#filters)
25
33
  - [Integration with OpenAI Function Calls](#integration-with-openai-function-calls)
26
34
 
27
35
  ## Features
@@ -30,14 +38,15 @@ NestJS MCP (Model Context Protocol) module — allows you to create “tools”,
30
38
  - Register MCP prompts using the `@McpPrompt()` decorator
31
39
  - Automatic detection of all providers (tools) in the module
32
40
  - Input data validation
33
- - SSE endpoints for MCP (`GET /mcp/sse, POST /mcp/messages`)
34
- - Endpoint for calling tool (`POST /mcp/toos`)
35
- - Endpoint for a list of all tools (`GET /mcp/tools`)
41
+ - SSE endpoints for MCP (`GET /mcp/sse, POST /mcp/messages`)
42
+ - Endpoint for calling tool (`POST /mcp/toos`)
43
+ - Endpoint for a list of all tools (`GET /mcp/tools`)
36
44
  - Endpoint for calling prompt (`POST /mcp/prompts/:name`)
37
- - Endpoint for a list of all prompts (`GET /mcp/prompts`)
38
- - Easy integration with LLM (OpenAI Function Calls)
45
+ - Endpoint for a list of all prompts (`GET /mcp/prompts`)
46
+ - Easy integration with LLM (OpenAI Function Calls)
39
47
  - Support http adapters (default fastify)
40
- - Full TypeScript typing
48
+ - Full TypeScript typing
49
+ - Support `@UseGuards`, `@UseInterceptors`, `@UseFilters`
41
50
 
42
51
  ---
43
52
 
@@ -54,10 +63,10 @@ Peer dependencies: `@nestjs/common, @nestjs/core, @nestjs/swagger, reflect-metad
54
63
  ### Connecting the MCP module
55
64
 
56
65
  ```ts
57
- import { Module } from '@nestjs/common';
58
- import { McpModule } from '@muzikanto/nestjs-mcp';
59
- import { TelegramSendMessageTool } from './tools/telegram-send-message.tool';
60
- import { TelegramAutoReplyPrompt } from './prompts/telegram-auto-reply.prompt';
66
+ import { Module } from "@nestjs/common";
67
+ import { McpModule } from "@muzikanto/nestjs-mcp";
68
+ import { TelegramSendMessageTool } from "./tools/telegram-send-message.tool";
69
+ import { TelegramAutoReplyPrompt } from "./prompts/telegram-auto-reply.prompt";
61
70
 
62
71
  @Module({
63
72
  imports: [
@@ -72,13 +81,13 @@ export class AppModule {}
72
81
  ### Create MCP tool
73
82
 
74
83
  ```ts
75
- import { IMcpTool, McpTool } from '@muzikanto/nestjs-mcp';
76
- import { Telegraf } from 'telegraf';
77
- import z from 'zod';
84
+ import { IMcpTool, McpTool } from "@muzikanto/nestjs-mcp";
85
+ import { Telegraf } from "telegraf";
86
+ import z from "zod";
78
87
 
79
88
  const schema = {
80
- chatId: z.string().describe('Telegram chat id'), // строка с описанием
81
- text: z.string().describe('Message text'), // строка с описанием
89
+ chatId: z.string().describe("Telegram chat id"), // строка с описанием
90
+ text: z.string().describe("Message text"), // строка с описанием
82
91
  };
83
92
 
84
93
  @McpTool()
@@ -86,7 +95,7 @@ export class TelegramSendMessageTool implements IMcpTool<
86
95
  { chatId: string; text: string },
87
96
  { success: boolean }
88
97
  > {
89
- name = 'telegram.sendMessage';
98
+ name = "telegram.sendMessage";
90
99
 
91
100
  inputSchema = schema;
92
101
 
@@ -113,7 +122,8 @@ POST /mcp
113
122
  }
114
123
  ```
115
124
 
116
- Ответ
125
+ Ответ
126
+
117
127
  ```json
118
128
  {
119
129
  "data": {
@@ -134,7 +144,7 @@ GET /mcp/tool
134
144
  "inputSchema": {
135
145
  "type": "object",
136
146
  "properties": {
137
- "chatId": {
147
+ "chatId": {
138
148
  "type": "number",
139
149
  "description": "Telegram chat ID"
140
150
  },
@@ -143,10 +153,7 @@ GET /mcp/tool
143
153
  "description": "Message text"
144
154
  }
145
155
  },
146
- "required": [
147
- "chatId",
148
- "text"
149
- ]
156
+ "required": ["chatId", "text"]
150
157
  }
151
158
  }
152
159
  ]
@@ -155,29 +162,33 @@ GET /mcp/tool
155
162
  ### Create MCP prompt
156
163
 
157
164
  ```ts
158
- import { IMcpPrompt, McpPrompt } from '@muzikanto/nestjs-mcp';
159
- import z from 'zod';
165
+ import { IMcpPrompt, McpPrompt } from "@muzikanto/nestjs-mcp";
166
+ import z from "zod";
160
167
 
161
168
  const schema = {
162
- chatId: z.string().describe('Telegram chat id'), // строка с описанием
163
- text: z.string().describe('Message text'), // строка с описанием
169
+ chatId: z.string().describe("Telegram chat id"), // строка с описанием
170
+ text: z.string().describe("Message text"), // строка с описанием
164
171
  };
165
172
 
166
173
  @McpPrompt()
167
- export class TelegramAutoReplyPrompt implements IMcpPrompt<{ text: string; chatId: number; }> {
168
- name = 'telegram_auto_reply';
169
- description = 'Generate a short, fiendly reply to an incoming Telegram message and send it back to the same chat using teegram.sendMessage tool';
174
+ export class TelegramAutoReplyPrompt implements IMcpPrompt<{
175
+ text: string;
176
+ chatId: number;
177
+ }> {
178
+ name = "telegram_auto_reply";
179
+ description =
180
+ "Generate a short, fiendly reply to an incoming Telegram message and send it back to the same chat using teegram.sendMessage tool";
170
181
  schema = schema;
171
182
 
172
183
  async execute({ text, chatId }: { text: string; chatId: number }) {
173
184
  return [
174
185
  {
175
186
  role: "system",
176
- content: `You are a friendly Telegram bot. Reply briefly and to the point.`
187
+ content: `You are a friendly Telegram bot. Reply briefly and to the point.`,
177
188
  },
178
189
  {
179
190
  role: "user",
180
- content: text
191
+ content: text,
181
192
  },
182
193
  {
183
194
  role: "assistant",
@@ -185,10 +196,10 @@ export class TelegramAutoReplyPrompt implements IMcpPrompt<{ text: string; chatI
185
196
  name: "telegram.sendMessage",
186
197
  arguments: {
187
198
  chatId,
188
- text: "{{model_output}}"
189
- }
190
- }
191
- }
199
+ text: "{{model_output}}",
200
+ },
201
+ },
202
+ },
192
203
  ];
193
204
  }
194
205
  }
@@ -206,7 +217,7 @@ GET /mcp/prompts
206
217
  "inputSchema": {
207
218
  "type": "object",
208
219
  "properties": {
209
- "chatId": {
220
+ "chatId": {
210
221
  "type": "number",
211
222
  "description": "Telegram chat ID"
212
223
  },
@@ -215,10 +226,7 @@ GET /mcp/prompts
215
226
  "description": "Message text"
216
227
  }
217
228
  },
218
- "required": [
219
- "chatId",
220
- "text"
221
- ]
229
+ "required": ["chatId", "text"]
222
230
  }
223
231
  }
224
232
  ]
@@ -236,42 +244,43 @@ POST /mcp/prompts/telegram_auto_reply
236
244
  ```
237
245
 
238
246
  Response
247
+
239
248
  ```json
240
249
  {
241
250
  "messages": [
242
- {
243
- "role": "system",
244
- "content": "You are a friendly Telegram bot. Reply briefly and to the point."
245
- },
246
- {
247
- "role": "user",
248
- "content": "Hi. How are you ?"
249
- },
250
- {
251
- "role": "assistant",
252
- "tool_call": {
253
- "name": "telegram.sendMessage",
254
- "arguments": {
255
- "chatId": 100000000,
256
- "text": "{{model_output}}"
257
- }
251
+ {
252
+ "role": "system",
253
+ "content": "You are a friendly Telegram bot. Reply briefly and to the point."
254
+ },
255
+ {
256
+ "role": "user",
257
+ "content": "Hi. How are you ?"
258
+ },
259
+ {
260
+ "role": "assistant",
261
+ "tool_call": {
262
+ "name": "telegram.sendMessage",
263
+ "arguments": {
264
+ "chatId": 100000000,
265
+ "text": "{{model_output}}"
258
266
  }
259
267
  }
260
- ]
268
+ }
269
+ ]
261
270
  }
262
271
  ```
263
272
 
264
273
  ### Create MCP resource
265
274
 
266
275
  ```ts
267
- import { IMcpResource, McpResource } from '@muzikanto/nestjs-mcp';
276
+ import { IMcpResource, McpResource } from "@muzikanto/nestjs-mcp";
268
277
 
269
278
  @McpResource()
270
279
  export class TestResource implements IMcpResource<{ userId: string }> {
271
- name = 'users.get';
272
- uri = 'users://{userId}';
273
- title = 'Get test user';
274
- description = 'Get user by id';
280
+ name = "users.get";
281
+ uri = "users://{userId}";
282
+ title = "Get test user";
283
+ description = "Get user by id";
275
284
 
276
285
  async execute(url: URL, vars: { userId: string }) {
277
286
  return [{ uri: url.href, text: `Hello ${vars.userId}` }];
@@ -279,17 +288,64 @@ export class TestResource implements IMcpResource<{ userId: string }> {
279
288
  }
280
289
  ```
281
290
 
282
- ### Auth guard
291
+ ### Dynamic creation
292
+
283
293
  ```ts
284
- import { IMcpTool, McpTool } from '@muzikanto/nestjs-mcp';
285
- import { UseGuards } from '@nestjs/common';
286
- import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
287
- import { Observable } from 'rxjs';
294
+ import { McpDynamicService } from "@muzikanto/nestjs-mcp";
295
+ import { Injectable } from "@nestjs/common";
296
+ import { ExampleInterceptor } from "./example.interceptor";
297
+ import { ExampleGuard } from "./example.guard";
298
+
299
+ @Injectable()
300
+ export class McpDynamic {
301
+ constructor(protected readonly mcpDynamicService: McpDynamicService) {}
302
+
303
+ onModuleInit() {
304
+ this.mcpDynamicService.registerTool({
305
+ name: "dynamic_tool",
306
+ title: "Dynamic tool",
307
+ execute: () => Promise.resolve("test"),
308
+ guards: [ExampleGuard],
309
+ interceptors: [ExampleInterceptor],
310
+ });
311
+
312
+ this.mcpDynamicService.registerPrompt({
313
+ name: "dynamic_prompt",
314
+ title: "Dynamic prompt",
315
+ execute: () => Promise.resolve([{ role: "assistant", content: "test" }]),
316
+ guards: [ExampleGuard],
317
+ interceptors: [ExampleInterceptor],
318
+ });
319
+
320
+ this.mcpDynamicService.registerResource<{ testId: string }>({
321
+ name: "dynamic_resource",
322
+ title: "Dynamic resource",
323
+ uri: "dynamic://test/{testId}",
324
+ execute: (uri, input) =>
325
+ Promise.resolve([{ uri: uri.href, text: `ID: ${input.testId}` }]),
326
+ guards: [ExampleGuard],
327
+ interceptors: [ExampleInterceptor],
328
+ });
329
+ }
330
+ }
331
+ ```
332
+
333
+ ### Guards
334
+
335
+ ### For one
336
+
337
+ ```ts
338
+ import { IMcpTool, McpTool } from "@muzikanto/nestjs-mcp";
339
+ import { UseGuards } from "@nestjs/common";
340
+ import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
341
+ import { Observable } from "rxjs";
288
342
 
289
343
  @Injectable()
290
344
  class TestGuard implements CanActivate {
291
- canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
292
- console.log('TestGuard called');
345
+ canActivate(
346
+ context: ExecutionContext,
347
+ ): boolean | Promise<boolean> | Observable<boolean> {
348
+ console.log("TestGuard called");
293
349
  return true;
294
350
  }
295
351
  }
@@ -300,7 +356,75 @@ export class TelegramSendMessageTool implements IMcpTool<
300
356
  { chatId: string; text: string },
301
357
  { success: boolean }
302
358
  > {
303
- name = 'telegram.sendMessage';
359
+ name = "telegram.sendMessage";
360
+
361
+ async execute() {
362
+ await this.bot.telegram.sendMessage(input.chatId, input.text);
363
+ return { success: true };
364
+ }
365
+ }
366
+ ```
367
+
368
+ #### For all
369
+
370
+ ```ts
371
+ import { McpModule } from "@muzikanto/nestjs-mcp";
372
+ import { Module } from "@nestjs/common";
373
+ import { TestGuard } from "./other/test.guard";
374
+ import { ToolWithoutGuards } from "./test.tool";
375
+ import { APP_GUARD } from "@nestjs/core";
376
+
377
+ @Module({
378
+ imports: [
379
+ McpModule.forRoot({
380
+ providers: [
381
+ ToolWithoutGuards,
382
+ TestGuard,
383
+ { provide: APP_GUARD, useExisting: TestGuard },
384
+ ],
385
+ }),
386
+ ],
387
+ })
388
+ export class TestModule {}
389
+ ```
390
+
391
+ ### Interceptors
392
+
393
+ #### For one
394
+
395
+ ```ts
396
+ import { IMcpTool, McpTool } from "@muzikanto/nestjs-mcp";
397
+ import {
398
+ ExecutionContext,
399
+ NestInterceptor,
400
+ CallHandler,
401
+ Injectable,
402
+ UseInterceptors,
403
+ } from "@nestjs/common";
404
+ import { map } from "rxjs";
405
+
406
+ @Injectable()
407
+ export class ExampleInterceptor implements NestInterceptor {
408
+ intercept(context: ExecutionContext, next: CallHandler) {
409
+ console.log("Before execute");
410
+
411
+ return next.handle().pipe(
412
+ map((data: unknown) => {
413
+ console.log("After execute", data);
414
+
415
+ return data;
416
+ }),
417
+ );
418
+ }
419
+ }
420
+
421
+ @UseInterceptors(TestInterceptor)
422
+ @McpTool()
423
+ export class TelegramSendMessageTool implements IMcpTool<
424
+ { chatId: string; text: string },
425
+ { success: boolean }
426
+ > {
427
+ name = "telegram.sendMessage";
304
428
 
305
429
  async execute() {
306
430
  await this.bot.telegram.sendMessage(input.chatId, input.text);
@@ -309,6 +433,64 @@ export class TelegramSendMessageTool implements IMcpTool<
309
433
  }
310
434
  ```
311
435
 
436
+ #### For all
437
+
438
+ ```ts
439
+ import { McpModule } from "@muzikanto/nestjs-mcp";
440
+ import { Module } from "@nestjs/common";
441
+ import { TestInterceptor } from "./other/test.interceptor";
442
+ import { ToolWithoutInterceptors } from "./test.tool";
443
+ import { APP_INTERCEPTOR } from "@nestjs/core";
444
+
445
+ @Module({
446
+ imports: [
447
+ McpModule.forRoot({
448
+ providers: [
449
+ ToolWithoutInterceptors,
450
+ TestGuard,
451
+ { provide: APP_INTERCEPTOR, useExisting: TestInterceptor },
452
+ ],
453
+ }),
454
+ ],
455
+ })
456
+ export class TestModule {}
457
+ ```
458
+
459
+ ### Filters
460
+
461
+ ```ts
462
+ import { IMcpTool, McpTool, McpUnauthorizedException } from "@muzikanto/nestjs-mcp";
463
+ import {
464
+ ExceptionFilter,
465
+ Catch,
466
+ ArgumentsHost,
467
+ UseFilters,
468
+ } from "@nestjs/common";
469
+
470
+ @Catch(McpUnauthorizedException)
471
+ class ExampleFilter implements ExceptionFilter {
472
+ catch(exception: unknown, host: ArgumentsHost) {
473
+ return {
474
+ isError: true,
475
+ text: (exception as Error).message,
476
+ };
477
+ }
478
+ }
479
+
480
+ @UseFilters(ExampleFilter)
481
+ @McpTool()
482
+ export class TelegramSendMessageTool implements IMcpTool<
483
+ { chatId: string; text: string },
484
+ { success: boolean }
485
+ > {
486
+ name = "telegram.sendMessage";
487
+
488
+ async execute() {
489
+ throw new NotImplementedException();
490
+ }
491
+ }
492
+ ```
493
+
312
494
  ### Integration with OpenAI Function Calls
313
495
 
314
496
  ```ts
@@ -374,7 +556,9 @@ async function callMcpTool(toolName: string, payload: Record<string, any>) {
374
556
  ```
375
557
 
376
558
  ## Contributing
559
+
377
560
  Contributions are welcome! Please open issues or submit PRs.
378
561
 
379
562
  ## Changelog
563
+
380
564
  See [CHANGELOG](https://github.com/Muzikanto/nestjs-mcp/blob/main/CHANGELOG.md) for detailed version history and updates.
package/dist/index.d.ts CHANGED
@@ -4,3 +4,5 @@ export * from "./mcp-server/decorators/mcp-prompt.decorator";
4
4
  export * from "./mcp-server/decorators/mcp-resource.decorator";
5
5
  export * from "./mcp-client/mcp-client.module";
6
6
  export * from "./mcp-client/mcp-client.service";
7
+ export * from "./mcp-server/mcp-dynamic.service";
8
+ export * from "./mcp-server/exceptions";
package/dist/index.js CHANGED
@@ -20,3 +20,5 @@ __exportStar(require("./mcp-server/decorators/mcp-prompt.decorator"), exports);
20
20
  __exportStar(require("./mcp-server/decorators/mcp-resource.decorator"), exports);
21
21
  __exportStar(require("./mcp-client/mcp-client.module"), exports);
22
22
  __exportStar(require("./mcp-client/mcp-client.service"), exports);
23
+ __exportStar(require("./mcp-server/mcp-dynamic.service"), exports);
24
+ __exportStar(require("./mcp-server/exceptions"), exports);
@@ -0,0 +1,31 @@
1
+ import { HttpException, HttpStatus } from "@nestjs/common";
2
+ export declare class McpException extends HttpException {
3
+ constructor(message: string, status?: HttpStatus, options?: {
4
+ cause?: unknown;
5
+ });
6
+ }
7
+ export declare class McpBadRequestException extends McpException {
8
+ constructor(message: string, options?: {
9
+ cause?: unknown;
10
+ });
11
+ }
12
+ export declare class McpNotFoundException extends McpException {
13
+ constructor(message: string, options?: {
14
+ cause?: unknown;
15
+ });
16
+ }
17
+ export declare class McpInternalServerErrorException extends McpException {
18
+ constructor(message: string, options?: {
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ export declare class McpUnauthorizedException extends McpException {
23
+ constructor(message: string, options?: {
24
+ cause?: unknown;
25
+ });
26
+ }
27
+ export declare class McpNotImplementedException extends McpException {
28
+ constructor(message: string, options?: {
29
+ cause?: unknown;
30
+ });
31
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpNotImplementedException = exports.McpUnauthorizedException = exports.McpInternalServerErrorException = exports.McpNotFoundException = exports.McpBadRequestException = exports.McpException = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ class McpException extends common_1.HttpException {
6
+ constructor(message, status = common_1.HttpStatus.INTERNAL_SERVER_ERROR, options) {
7
+ super({ message, ...(options?.cause ? { cause: options.cause } : {}) }, status);
8
+ }
9
+ }
10
+ exports.McpException = McpException;
11
+ class McpBadRequestException extends McpException {
12
+ constructor(message, options) {
13
+ super(message, common_1.HttpStatus.BAD_REQUEST, options);
14
+ }
15
+ }
16
+ exports.McpBadRequestException = McpBadRequestException;
17
+ class McpNotFoundException extends McpException {
18
+ constructor(message, options) {
19
+ super(message, common_1.HttpStatus.NOT_FOUND, options);
20
+ }
21
+ }
22
+ exports.McpNotFoundException = McpNotFoundException;
23
+ class McpInternalServerErrorException extends McpException {
24
+ constructor(message, options) {
25
+ super(message, common_1.HttpStatus.INTERNAL_SERVER_ERROR, options);
26
+ }
27
+ }
28
+ exports.McpInternalServerErrorException = McpInternalServerErrorException;
29
+ class McpUnauthorizedException extends McpException {
30
+ constructor(message, options) {
31
+ super(message, common_1.HttpStatus.UNAUTHORIZED, options);
32
+ }
33
+ }
34
+ exports.McpUnauthorizedException = McpUnauthorizedException;
35
+ class McpNotImplementedException extends McpException {
36
+ constructor(message, options) {
37
+ super(message, common_1.HttpStatus.NOT_IMPLEMENTED, options);
38
+ }
39
+ }
40
+ exports.McpNotImplementedException = McpNotImplementedException;
@@ -0,0 +1,31 @@
1
+ import { CanActivate, NestInterceptor, Type } from "@nestjs/common";
2
+ import { ModuleRef } from "@nestjs/core";
3
+ import { McpService } from "./mcp.service";
4
+ import { IMcpTool } from "./decorators/mcp-tool.decorator";
5
+ import { IMcpResource } from "./decorators/mcp-resource.decorator";
6
+ import { IMcpPrompt } from "./decorators/mcp-prompt.decorator";
7
+ export type IMcpDynamicTool<Input, Result> = IMcpTool<Input, Result> & {
8
+ guards?: Type<CanActivate>[];
9
+ interceptors?: Type<NestInterceptor>[];
10
+ };
11
+ export type IMcpDynamicPrompt<Input> = IMcpPrompt<Input> & {
12
+ guards?: Type<CanActivate>[];
13
+ interceptors?: Type<NestInterceptor>[];
14
+ };
15
+ export type IMcpDynamicResource<Input> = IMcpResource<Input> & {
16
+ guards?: Type<CanActivate>[];
17
+ interceptors?: Type<NestInterceptor>[];
18
+ };
19
+ export declare class McpDynamicService {
20
+ private readonly moduleRef;
21
+ private readonly mcpService;
22
+ constructor(moduleRef: ModuleRef, mcpService: McpService);
23
+ /** Динамически регистрирует MCP tools */
24
+ registerTool<Input, Result>(tool: IMcpDynamicTool<Input, Result>): Promise<void>;
25
+ /** Динамически регистрирует MCP prompts */
26
+ registerPrompt<Input>(prompt: IMcpDynamicPrompt<Input>): Promise<void>;
27
+ /** Динамически регистрирует MCP resources */
28
+ registerResource<Input>(resource: IMcpDynamicResource<Input>): Promise<void>;
29
+ protected applyGuards(item: Function, guards: Type<CanActivate>[]): void;
30
+ protected applyInterceptors(item: Function, interceptors: Type<NestInterceptor>[]): void;
31
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.McpDynamicService = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const core_1 = require("@nestjs/core");
15
+ const mcp_service_1 = require("./mcp.service");
16
+ const constants_1 = require("@nestjs/common/constants");
17
+ let McpDynamicService = class McpDynamicService {
18
+ moduleRef;
19
+ mcpService;
20
+ constructor(moduleRef, mcpService) {
21
+ this.moduleRef = moduleRef;
22
+ this.mcpService = mcpService;
23
+ }
24
+ /** Динамически регистрирует MCP tools */
25
+ async registerTool(tool) {
26
+ const DynamicToolClass = class {
27
+ name = tool.name;
28
+ title = tool.title;
29
+ description = tool.description;
30
+ execute = tool.execute;
31
+ inputSchema = tool.inputSchema;
32
+ };
33
+ if (tool.guards) {
34
+ this.applyGuards(DynamicToolClass, tool.guards);
35
+ }
36
+ if (tool.interceptors) {
37
+ this.applyInterceptors(DynamicToolClass, tool.interceptors);
38
+ }
39
+ const instance = await this.moduleRef.create(DynamicToolClass);
40
+ this.mcpService.registerTool(instance.name, {
41
+ instance,
42
+ metatype: DynamicToolClass,
43
+ });
44
+ }
45
+ /** Динамически регистрирует MCP prompts */
46
+ async registerPrompt(prompt) {
47
+ const DynamicPromptClass = class {
48
+ name = prompt.name;
49
+ title = prompt.title;
50
+ description = prompt.description;
51
+ execute = prompt.execute;
52
+ inputSchema = prompt.inputSchema;
53
+ };
54
+ if (prompt.guards) {
55
+ this.applyGuards(DynamicPromptClass, prompt.guards);
56
+ }
57
+ if (prompt.interceptors) {
58
+ this.applyInterceptors(DynamicPromptClass, prompt.interceptors);
59
+ }
60
+ const instance = await this.moduleRef.create(DynamicPromptClass);
61
+ this.mcpService.registerPrompt(instance.name, {
62
+ instance,
63
+ metatype: DynamicPromptClass,
64
+ });
65
+ }
66
+ /** Динамически регистрирует MCP resources */
67
+ async registerResource(resource) {
68
+ const DynamicResourceClass = class {
69
+ name = resource.name;
70
+ title = resource.title;
71
+ uri = resource.uri;
72
+ description = resource.description;
73
+ execute = resource.execute;
74
+ list = resource.list;
75
+ };
76
+ if (resource.guards) {
77
+ this.applyGuards(DynamicResourceClass, resource.guards);
78
+ }
79
+ if (resource.interceptors) {
80
+ this.applyInterceptors(DynamicResourceClass, resource.interceptors);
81
+ }
82
+ const instance = await this.moduleRef.create(DynamicResourceClass);
83
+ this.mcpService.registerResource(instance.name, {
84
+ instance,
85
+ metatype: DynamicResourceClass,
86
+ });
87
+ }
88
+ applyGuards(item, guards) {
89
+ Reflect.defineMetadata(constants_1.GUARDS_METADATA, guards, item);
90
+ }
91
+ applyInterceptors(item, interceptors) {
92
+ Reflect.defineMetadata(constants_1.INTERCEPTORS_METADATA, interceptors, item);
93
+ }
94
+ };
95
+ exports.McpDynamicService = McpDynamicService;
96
+ exports.McpDynamicService = McpDynamicService = __decorate([
97
+ (0, common_1.Injectable)(),
98
+ __metadata("design:paramtypes", [core_1.ModuleRef,
99
+ mcp_service_1.McpService])
100
+ ], McpDynamicService);
@@ -16,6 +16,7 @@ const mcp_controller_1 = require("./mcp.controller");
16
16
  const inject_tokens_1 = require("./utils/inject-tokens");
17
17
  const config_1 = require("./config");
18
18
  const http_adapter_1 = require("./utils/http-adapter");
19
+ const mcp_dynamic_service_1 = require("./mcp-dynamic.service");
19
20
  const publicGuard = { canActivate: () => Promise.resolve(true) };
20
21
  let McpModule = McpModule_1 = class McpModule {
21
22
  static forRoot(metadata = {}) {
@@ -37,12 +38,13 @@ let McpModule = McpModule_1 = class McpModule {
37
38
  providers: [
38
39
  mcp_service_1.McpService,
39
40
  mcp_explorer_1.McpExplorer,
41
+ mcp_dynamic_service_1.McpDynamicService,
40
42
  configProviver,
41
43
  guardProvider,
42
44
  ...(metadata.guard ? [metadata.guard] : []),
43
45
  ...(metadata.providers || []),
44
46
  ],
45
- exports: [mcp_service_1.McpService, ...(metadata.exports || [])],
47
+ exports: [mcp_dynamic_service_1.McpDynamicService, ...(metadata.exports || [])],
46
48
  controllers: [mcp_controller_1.McpController],
47
49
  };
48
50
  }
@@ -19,6 +19,8 @@ const run_guards_1 = require("./utils/run-guards");
19
19
  const core_1 = require("@nestjs/core");
20
20
  const run_interceptors_1 = require("./utils/run-interceptors");
21
21
  const rxjs_1 = require("rxjs");
22
+ const run_fillters_1 = require("./utils/run-fillters");
23
+ const exceptions_1 = require("./exceptions");
22
24
  let McpService = class McpService {
23
25
  moduleRef;
24
26
  tools = new Map();
@@ -87,77 +89,89 @@ let McpService = class McpService {
87
89
  }
88
90
  async executePrompt(name, payload, context) {
89
91
  if (!this.prompts.has(name)) {
90
- throw new common_1.NotFoundException("Not found prompt");
91
- }
92
- const { instance: prompt, metatype } = this.prompts.get(name) || {};
93
- if (!prompt || !metatype) {
94
- throw new common_1.NotFoundException(`Unknown prompt: "${name}"`);
95
- }
96
- await (0, run_guards_1.runGuards)(this.moduleRef, metatype, context);
97
- // Валидация через AJV, если есть inputSchema
98
- if (prompt.inputSchema) {
99
- const zodSchema = (0, zod_1.zodToJsonSchema)(prompt.inputSchema);
100
- const validate = this.ajv.compile(zodSchema);
101
- const valid = validate(payload);
102
- if (!valid) {
103
- throw new common_1.NotFoundException("Invalid prompt arguments");
104
- }
92
+ throw new exceptions_1.McpNotFoundException("Not found prompt");
105
93
  }
94
+ const { instance: prompt, metatype } = this.prompts.get(name);
106
95
  try {
96
+ await (0, run_guards_1.runGuards)(this.moduleRef, metatype, context);
97
+ // Валидация через AJV, если есть inputSchema
98
+ if (prompt.inputSchema) {
99
+ const zodSchema = (0, zod_1.zodToJsonSchema)(prompt.inputSchema);
100
+ const validate = this.ajv.compile(zodSchema);
101
+ const valid = validate(payload);
102
+ if (!valid) {
103
+ throw new exceptions_1.McpBadRequestException("Invalid prompt arguments");
104
+ }
105
+ }
107
106
  // const result = await prompt.execute(payload);
108
- const result = await (0, run_interceptors_1.runInterceptors)(this.moduleRef, metatype, context, () => {
107
+ const stream = await (0, run_interceptors_1.runInterceptors)(this.moduleRef, metatype, context, () => {
109
108
  return (0, rxjs_1.from)(prompt.execute(payload));
110
109
  });
111
- return result;
110
+ return stream.pipe((0, rxjs_1.catchError)((err) => (0, rxjs_1.from)((0, run_fillters_1.runFilters)(this.moduleRef, metatype, err, context))));
112
111
  }
113
112
  catch (err) {
114
- throw new common_1.InternalServerErrorException("Failed to execute prompt");
113
+ const result = await (0, run_fillters_1.runFilters)(this.moduleRef, metatype, err, context);
114
+ if (result) {
115
+ return (0, rxjs_1.of)(result);
116
+ }
117
+ // Пробрасываем дальше, чтобы Observable корректно завершился
118
+ return (0, rxjs_1.throwError)(() => err);
115
119
  }
116
120
  }
117
121
  /**
118
122
  * Отправить сообщение в MCP "сервер"
119
123
  */
120
124
  async executeTool(msg, context) {
121
- const { instance: tool, metatype } = this.tools.get(msg.type) || {};
122
- if (!tool || !metatype) {
123
- throw new common_1.NotFoundException(`Unknown tool: "${msg.type}"`);
124
- }
125
- await (0, run_guards_1.runGuards)(this.moduleRef, metatype, context);
126
- // Валидация через AJV, если есть inputSchema
127
- if (tool.inputSchema) {
128
- const zodSchema = (0, zod_1.zodToJsonSchema)(tool.inputSchema);
129
- const validate = this.ajv.compile(zodSchema);
130
- const valid = validate(msg.payload);
131
- if (!valid) {
132
- throw new common_1.BadRequestException(this.ajv.errorsText(validate.errors));
133
- }
125
+ if (!this.tools.has(msg.type)) {
126
+ throw new exceptions_1.McpNotFoundException("Not found tool");
134
127
  }
128
+ const { instance: tool, metatype } = this.tools.get(msg.type);
135
129
  try {
130
+ await (0, run_guards_1.runGuards)(this.moduleRef, metatype, context);
131
+ // Валидация через AJV, если есть inputSchema
132
+ if (tool.inputSchema) {
133
+ const zodSchema = (0, zod_1.zodToJsonSchema)(tool.inputSchema);
134
+ const validate = this.ajv.compile(zodSchema);
135
+ const valid = validate(msg.payload);
136
+ if (!valid) {
137
+ throw new exceptions_1.McpBadRequestException(this.ajv.errorsText(validate.errors));
138
+ }
139
+ }
136
140
  // const result = await tool.execute(msg.payload);
137
- const result = await (0, run_interceptors_1.runInterceptors)(this.moduleRef, metatype, context, () => {
141
+ const stream = await (0, run_interceptors_1.runInterceptors)(this.moduleRef, metatype, context, () => {
138
142
  return (0, rxjs_1.from)(tool.execute(msg.payload));
139
143
  });
140
- return result;
144
+ return stream.pipe((0, rxjs_1.catchError)((err) => (0, rxjs_1.from)((0, run_fillters_1.runFilters)(this.moduleRef, metatype, err, context))));
141
145
  }
142
146
  catch (err) {
143
- throw new common_1.InternalServerErrorException("Failed to execute tool");
147
+ const result = await (0, run_fillters_1.runFilters)(this.moduleRef, metatype, err, context);
148
+ if (result) {
149
+ return (0, rxjs_1.of)(result);
150
+ }
151
+ // Пробрасываем дальше, чтобы Observable корректно завершился
152
+ return (0, rxjs_1.throwError)(() => err);
144
153
  }
145
154
  }
146
155
  async executeResource(name, uri, vars, context) {
147
- const { instance: resource, metatype } = this.resources.get(name) || {};
148
- if (!resource || !metatype) {
149
- throw new common_1.NotFoundException(`Unknown resource: "${name}"`);
156
+ if (!this.resources.has(name)) {
157
+ throw new exceptions_1.McpNotFoundException("Not found prompt");
150
158
  }
151
- await (0, run_guards_1.runGuards)(this.moduleRef, metatype, context);
159
+ const { instance: resource, metatype } = this.resources.get(name);
152
160
  try {
161
+ await (0, run_guards_1.runGuards)(this.moduleRef, metatype, context);
153
162
  // const result = await resource.execute(uri, vars);
154
- const result = await (0, run_interceptors_1.runInterceptors)(this.moduleRef, metatype, context, () => {
163
+ const stream = await (0, run_interceptors_1.runInterceptors)(this.moduleRef, metatype, context, () => {
155
164
  return (0, rxjs_1.from)(resource.execute(uri, vars));
156
165
  });
157
- return result;
166
+ return stream.pipe((0, rxjs_1.catchError)((err) => (0, rxjs_1.from)((0, run_fillters_1.runFilters)(this.moduleRef, metatype, err, context))));
158
167
  }
159
168
  catch (err) {
160
- throw new common_1.InternalServerErrorException("Failed to execute tool");
169
+ const result = await (0, run_fillters_1.runFilters)(this.moduleRef, metatype, err, context);
170
+ if (result) {
171
+ return (0, rxjs_1.of)(result);
172
+ }
173
+ // Пробрасываем дальше, чтобы Observable корректно завершился
174
+ return (0, rxjs_1.throwError)(() => err);
161
175
  }
162
176
  }
163
177
  createServer(context) {
@@ -181,8 +195,9 @@ let McpService = class McpService {
181
195
  };
182
196
  }
183
197
  catch (e) {
184
- console.error(e);
185
- throw new Error(`Faild to execute tool ${tool.name}`);
198
+ throw new exceptions_1.McpInternalServerErrorException(`Faild to execute tool ${tool.name}`, {
199
+ cause: e,
200
+ });
186
201
  }
187
202
  });
188
203
  }
@@ -208,7 +223,9 @@ let McpService = class McpService {
208
223
  return { messages: messages };
209
224
  }
210
225
  catch (e) {
211
- throw new Error(`Faild to execute tool ${prompt.name}`);
226
+ throw new exceptions_1.McpInternalServerErrorException(`Faild to execute tool ${prompt.name}`, {
227
+ cause: e,
228
+ });
212
229
  }
213
230
  });
214
231
  }
@@ -233,7 +250,9 @@ let McpService = class McpService {
233
250
  return { contents: result };
234
251
  }
235
252
  catch (e) {
236
- throw new Error(`Faild to execute tool ${prompt.name}`);
253
+ throw new exceptions_1.McpInternalServerErrorException(`Faild to execute tool ${prompt.name}`, {
254
+ cause: e,
255
+ });
237
256
  }
238
257
  });
239
258
  }
@@ -0,0 +1,3 @@
1
+ import { ArgumentsHost } from "@nestjs/common";
2
+ import { ModuleRef } from "@nestjs/core";
3
+ export declare function runFilters(moduleRef: ModuleRef, metatype: Function, exception: unknown, host: ArgumentsHost): Promise<any>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runFilters = runFilters;
4
+ const constants_1 = require("@nestjs/common/constants");
5
+ async function runFilters(moduleRef, metatype, exception, host) {
6
+ const resolveFilter = async (Filter) => {
7
+ if (typeof Filter !== "function") {
8
+ return Filter;
9
+ }
10
+ try {
11
+ return moduleRef.get(Filter, { strict: false });
12
+ }
13
+ catch {
14
+ try {
15
+ return await moduleRef.create(Filter);
16
+ }
17
+ catch {
18
+ return new Filter();
19
+ }
20
+ }
21
+ };
22
+ const filters = Reflect.getMetadata(constants_1.EXCEPTION_FILTERS_METADATA, metatype) || [];
23
+ for (const Filter of filters) {
24
+ const filterInstance = await resolveFilter(Filter);
25
+ const filterExceptions = (Reflect.getMetadata(constants_1.FILTER_CATCH_EXCEPTIONS, Filter) || []);
26
+ if (!filterExceptions ||
27
+ !filterExceptions.some((ex) => exception instanceof ex)) {
28
+ continue;
29
+ }
30
+ // ВАЖНО: filter сам обрабатывает exception
31
+ const result = await filterInstance.catch(exception, host);
32
+ // Nest прекращает выполнение после первого подходящего filter
33
+ return result;
34
+ }
35
+ // если фильтров нет — пробрасываем ошибку дальше
36
+ throw exception;
37
+ }
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runGuards = runGuards;
4
- const common_1 = require("@nestjs/common");
5
4
  const constants_1 = require("@nestjs/common/constants");
5
+ const exceptions_1 = require("../exceptions");
6
6
  /**
7
7
  * Универсальный helper для проверки массивов NestJS Guards
8
8
  */
@@ -28,7 +28,7 @@ async function runGuards(moduleRef, metatype, context) {
28
28
  const guardInstance = await resolveGuard(Guard);
29
29
  const can = await guardInstance.canActivate(context);
30
30
  if (!can) {
31
- throw new common_1.UnauthorizedException("Guard blocked execution");
31
+ throw new exceptions_1.McpUnauthorizedException("Guard blocked execution");
32
32
  }
33
33
  }
34
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muzikanto/nestjs-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.5.1",
4
4
  "description": "NestJS MCP (Model Context Protocol) module for creating tools for LLM or HTTP with validation, decorators, and OpenAI Function Calls integration.",
5
5
  "keywords": [
6
6
  "nestjs",
@@ -20,7 +20,7 @@
20
20
  "build": "tsc",
21
21
  "prepublishOnly": "npm run build",
22
22
  "lint": "prettier --check 'src/**/*.ts'",
23
- "format": "prettier --write 'src/**/*.ts'",
23
+ "format": "prettier --write 'src/**/*.ts' 'example/**/*.ts' 'README.md'",
24
24
  "ch": "changeset",
25
25
  "vers": "changeset version",
26
26
  "pub": "changeset publish"