@rekog/mcp-nest 1.2.0 → 1.3.0

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.
Files changed (64) hide show
  1. package/.github/workflows/pipeline.yml +26 -0
  2. package/.prettierrc +4 -0
  3. package/README.md +51 -190
  4. package/dist/controllers/sse.controller.factory.d.ts +4 -6
  5. package/dist/controllers/sse.controller.factory.d.ts.map +1 -1
  6. package/dist/controllers/sse.controller.factory.js +13 -6
  7. package/dist/controllers/sse.controller.factory.js.map +1 -1
  8. package/dist/decorators/constants.d.ts +1 -0
  9. package/dist/decorators/constants.d.ts.map +1 -1
  10. package/dist/decorators/constants.js +2 -1
  11. package/dist/decorators/constants.js.map +1 -1
  12. package/dist/decorators/index.d.ts +1 -0
  13. package/dist/decorators/index.d.ts.map +1 -1
  14. package/dist/decorators/index.js +1 -0
  15. package/dist/decorators/index.js.map +1 -1
  16. package/dist/decorators/resource.decorator.d.ts +9 -0
  17. package/dist/decorators/resource.decorator.d.ts.map +1 -0
  18. package/dist/decorators/resource.decorator.js +10 -0
  19. package/dist/decorators/resource.decorator.js.map +1 -0
  20. package/dist/decorators/tool.decorator.d.ts +4 -3
  21. package/dist/decorators/tool.decorator.d.ts.map +1 -1
  22. package/dist/decorators/tool.decorator.js +2 -6
  23. package/dist/decorators/tool.decorator.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/interfaces/index.d.ts +1 -0
  29. package/dist/interfaces/index.d.ts.map +1 -1
  30. package/dist/interfaces/index.js +1 -0
  31. package/dist/interfaces/index.js.map +1 -1
  32. package/dist/interfaces/mcp-tool.interface.d.ts +15 -0
  33. package/dist/interfaces/mcp-tool.interface.d.ts.map +1 -0
  34. package/dist/interfaces/mcp-tool.interface.js +3 -0
  35. package/dist/interfaces/mcp-tool.interface.js.map +1 -0
  36. package/dist/mcp.module.d.ts.map +1 -1
  37. package/dist/mcp.module.js +8 -25
  38. package/dist/mcp.module.js.map +1 -1
  39. package/dist/services/mcp-executor.service.d.ts +15 -0
  40. package/dist/services/mcp-executor.service.d.ts.map +1 -0
  41. package/dist/services/mcp-executor.service.js +172 -0
  42. package/dist/services/mcp-executor.service.js.map +1 -0
  43. package/dist/services/mcp-registry.service.d.ts +31 -0
  44. package/dist/services/mcp-registry.service.d.ts.map +1 -0
  45. package/dist/services/mcp-registry.service.js +119 -0
  46. package/dist/services/mcp-registry.service.js.map +1 -0
  47. package/dist/services/mcp-registry.service.spec.d.ts +2 -0
  48. package/dist/services/mcp-registry.service.spec.d.ts.map +1 -0
  49. package/dist/services/mcp-registry.service.spec.js +61 -0
  50. package/dist/services/mcp-registry.service.spec.js.map +1 -0
  51. package/eslint.config.mjs +38 -0
  52. package/image.png +0 -0
  53. package/package.json +22 -5
  54. package/playground/README.md +17 -0
  55. package/playground/greeting.resource.ts +25 -0
  56. package/playground/greeting.tool.ts +36 -0
  57. package/playground/server.ts +25 -0
  58. package/tests/mcp-resource.e2e.spec.ts +179 -0
  59. package/tests/{mcp-auth.e2e.spec.ts → mcp-tool-auth.e2e.spec.ts} +72 -83
  60. package/tests/mcp-tool.e2e.spec.ts +235 -0
  61. package/tests/utils.ts +42 -0
  62. package/tsconfig.build.json +11 -0
  63. package/tsconfig.build.tsbuildinfo +1 -0
  64. package/tests/mcp.e2e.spec.ts +0 -115
@@ -0,0 +1,235 @@
1
+ import { Progress } from '@modelcontextprotocol/sdk/types.js';
2
+ import { INestApplication, Inject, Injectable, Scope } from '@nestjs/common';
3
+ import { Test, TestingModule } from '@nestjs/testing';
4
+ import { z } from 'zod';
5
+ import { Context, Tool } from '../src';
6
+ import { McpModule } from '../src/mcp.module';
7
+ import { createMCPClient } from './utils';
8
+ import { REQUEST } from '@nestjs/core';
9
+
10
+ // Mock user repository
11
+ @Injectable()
12
+ class MockUserRepository {
13
+ async findOne(id: string) {
14
+ return Promise.resolve({
15
+ id,
16
+ name: 'Repository User',
17
+ orgMemberships: [
18
+ {
19
+ orgId: 'org123',
20
+ organization: {
21
+ name: 'Repository Org',
22
+ },
23
+ },
24
+ ],
25
+ });
26
+ }
27
+ }
28
+
29
+ // Greeting tool that uses the authentication context
30
+ @Injectable()
31
+ export class GreetingTool {
32
+ constructor(private readonly userRepository: MockUserRepository) {}
33
+
34
+ @Tool({
35
+ name: 'hello-world',
36
+ description: 'A sample tool that get the user by id',
37
+ parameters: z.object({
38
+ name: z.string().default('World'),
39
+ }),
40
+ })
41
+ async sayHello({ id }, context: Context) {
42
+ const user = await this.userRepository.findOne(id);
43
+
44
+ // Report progress for demonstration
45
+ for (let i = 0; i < 5; i++) {
46
+ await new Promise((resolve) => setTimeout(resolve, 50));
47
+ await context.reportProgress({
48
+ progress: (i + 1) * 20,
49
+ total: 100,
50
+ } as Progress);
51
+ }
52
+
53
+ return {
54
+ content: [
55
+ {
56
+ type: 'text',
57
+ text: `Hello, ${user.name}!`,
58
+ },
59
+ ],
60
+ };
61
+ }
62
+
63
+ @Tool({
64
+ name: 'hello-world-error',
65
+ description: 'A sample tool that get the user by id',
66
+ parameters: z.object({}),
67
+ })
68
+ async sayHelloError() {
69
+ throw new Error('any error');
70
+ }
71
+ }
72
+
73
+ @Injectable({ scope: Scope.REQUEST })
74
+ export class GreetingToolRequestScoped {
75
+ constructor(private readonly userRepository: MockUserRepository) {}
76
+
77
+ @Tool({
78
+ name: 'hello-world-scoped',
79
+ description: 'A sample tool that get the user by id',
80
+ parameters: z.object({
81
+ name: z.string().default('World'),
82
+ }),
83
+ })
84
+ async sayHello({ id }, context: Context) {
85
+ const user = await this.userRepository.findOne(id);
86
+
87
+ // Report progress for demonstration
88
+ for (let i = 0; i < 5; i++) {
89
+ await new Promise((resolve) => setTimeout(resolve, 50));
90
+ await context.reportProgress({
91
+ progress: (i + 1) * 20,
92
+ total: 100,
93
+ } as Progress);
94
+ }
95
+
96
+ return {
97
+ content: [
98
+ {
99
+ type: 'text',
100
+ text: `Hello, ${user.name}!`,
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ }
106
+
107
+ @Injectable({ scope: Scope.REQUEST })
108
+ export class ToolRequestScoped {
109
+ constructor(@Inject(REQUEST) private request: Request) {}
110
+
111
+ @Tool({
112
+ name: 'get-request-scoped',
113
+ description: 'A sample tool that get the request',
114
+ parameters: z.object({}),
115
+ })
116
+ async getRequest() {
117
+ return {
118
+ content: [
119
+ {
120
+ type: 'text',
121
+ text: this.request.headers['any-header'] ?? 'No header found',
122
+ },
123
+ ],
124
+ };
125
+ }
126
+ }
127
+
128
+ describe('E2E: MCP ToolServer', () => {
129
+ let app: INestApplication;
130
+ let testPort: number;
131
+
132
+ beforeAll(async () => {
133
+ const moduleFixture: TestingModule = await Test.createTestingModule({
134
+ imports: [
135
+ McpModule.forRoot({
136
+ name: 'test-mcp-server',
137
+ version: '0.0.1',
138
+ guards: [],
139
+ }),
140
+ ],
141
+ providers: [
142
+ GreetingTool,
143
+ GreetingToolRequestScoped,
144
+ MockUserRepository,
145
+ ToolRequestScoped,
146
+ ],
147
+ }).compile();
148
+
149
+ app = moduleFixture.createNestApplication();
150
+ await app.listen(0);
151
+
152
+ const server = app.getHttpServer();
153
+ testPort = server.address().port;
154
+ });
155
+
156
+ afterAll(async () => {
157
+ await app.close();
158
+ });
159
+
160
+ it('should list tools', async () => {
161
+ const client = await createMCPClient(testPort);
162
+ const tools = await client.listTools();
163
+
164
+ // Verify that the authenticated tool is available
165
+ expect(tools.tools.length).toBeGreaterThan(0);
166
+ expect(tools.tools.find((t) => t.name === 'hello-world')).toBeDefined();
167
+
168
+ await client.close();
169
+ });
170
+
171
+ it.each([{ tool: 'hello-world' }, { tool: 'hello-world-scoped' }])(
172
+ 'should call the tool and receive progress notifications for $tool',
173
+ async ({ tool }) => {
174
+ const client = await createMCPClient(testPort);
175
+
176
+ let progressCount = 1;
177
+ const result: any = await client.callTool(
178
+ {
179
+ name: tool,
180
+ arguments: { id: 'userRepo123' },
181
+ },
182
+ undefined,
183
+ {
184
+ onprogress: () => {
185
+ progressCount++;
186
+ },
187
+ },
188
+ );
189
+
190
+ // Verify that progress notifications were received
191
+ expect(progressCount).toBe(5);
192
+
193
+ // Verify that authentication context was available to the tool
194
+ expect(result.content[0].type).toBe('text');
195
+ expect(result.content[0].text).toContain('Hello, Repository User!');
196
+
197
+ await client.close();
198
+ },
199
+ );
200
+
201
+ it('should call the tool and receive progress notifications for get-request-scoped', async () => {
202
+ const client = await createMCPClient(testPort, {
203
+ requestInit: {
204
+ headers: {
205
+ 'any-header': 'any-value',
206
+ },
207
+ },
208
+ });
209
+
210
+ const result: any = await client.callTool({
211
+ name: 'get-request-scoped',
212
+ arguments: {},
213
+ });
214
+
215
+ expect(result.content[0].type).toBe('text');
216
+ expect(result.content[0].text).toContain('any-value');
217
+
218
+ await client.close();
219
+ });
220
+
221
+ it('should call the tool and receive an error', async () => {
222
+ const client = await createMCPClient(testPort);
223
+ const result: any = await client.callTool({
224
+ name: 'hello-world-error',
225
+ arguments: {},
226
+ });
227
+
228
+ expect(result).toEqual({
229
+ content: [{ type: 'text', text: 'any error' }],
230
+ isError: true,
231
+ });
232
+
233
+ await client.close();
234
+ });
235
+ });
package/tests/utils.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3
+
4
+ /**
5
+ * Creates and connects a new MCP (Model Context Protocol) client for testing
6
+ *
7
+ * @param port - The port number to connect to on localhost
8
+ * @param sseArgs - Optional configuration for the SSE transport connection. Can include eventSourceInit and requestInit options.
9
+ * @returns A connected MCP Client instance
10
+ * @example
11
+ * ```ts
12
+ * const client = await createMCPClient(3000, {
13
+ * requestInit: {
14
+ * headers: {
15
+ * Authorization: 'Bearer token'
16
+ * }
17
+ * }
18
+ * });
19
+ * ```
20
+ */
21
+ export async function createMCPClient(
22
+ port: number,
23
+ sseArgs: {
24
+ eventSourceInit?: EventSourceInit;
25
+ requestInit?: RequestInit;
26
+ } = {},
27
+ ): Promise<Client> {
28
+ const client = new Client(
29
+ { name: 'example-client', version: '1.0.0' },
30
+ {
31
+ capabilities: {
32
+ tools: {},
33
+ resources: {},
34
+ resourceTemplates: {},
35
+ },
36
+ },
37
+ );
38
+ const sseUrl = new URL(`http://localhost:${port}/sse`);
39
+ const transport = new SSEClientTransport(sseUrl, sseArgs);
40
+ await client.connect(transport);
41
+ return client;
42
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist",
6
+ "noEmitOnError": false,
7
+ "declaration": true
8
+ },
9
+ "include": ["src"],
10
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
11
+ }