@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.
- package/.github/workflows/pipeline.yml +26 -0
- package/.prettierrc +4 -0
- package/README.md +51 -190
- package/dist/controllers/sse.controller.factory.d.ts +4 -6
- package/dist/controllers/sse.controller.factory.d.ts.map +1 -1
- package/dist/controllers/sse.controller.factory.js +13 -6
- package/dist/controllers/sse.controller.factory.js.map +1 -1
- package/dist/decorators/constants.d.ts +1 -0
- package/dist/decorators/constants.d.ts.map +1 -1
- package/dist/decorators/constants.js +2 -1
- package/dist/decorators/constants.js.map +1 -1
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/resource.decorator.d.ts +9 -0
- package/dist/decorators/resource.decorator.d.ts.map +1 -0
- package/dist/decorators/resource.decorator.js +10 -0
- package/dist/decorators/resource.decorator.js.map +1 -0
- package/dist/decorators/tool.decorator.d.ts +4 -3
- package/dist/decorators/tool.decorator.d.ts.map +1 -1
- package/dist/decorators/tool.decorator.js +2 -6
- package/dist/decorators/tool.decorator.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/interfaces/index.d.ts.map +1 -1
- package/dist/interfaces/index.js +1 -0
- package/dist/interfaces/index.js.map +1 -1
- package/dist/interfaces/mcp-tool.interface.d.ts +15 -0
- package/dist/interfaces/mcp-tool.interface.d.ts.map +1 -0
- package/dist/interfaces/mcp-tool.interface.js +3 -0
- package/dist/interfaces/mcp-tool.interface.js.map +1 -0
- package/dist/mcp.module.d.ts.map +1 -1
- package/dist/mcp.module.js +8 -25
- package/dist/mcp.module.js.map +1 -1
- package/dist/services/mcp-executor.service.d.ts +15 -0
- package/dist/services/mcp-executor.service.d.ts.map +1 -0
- package/dist/services/mcp-executor.service.js +172 -0
- package/dist/services/mcp-executor.service.js.map +1 -0
- package/dist/services/mcp-registry.service.d.ts +31 -0
- package/dist/services/mcp-registry.service.d.ts.map +1 -0
- package/dist/services/mcp-registry.service.js +119 -0
- package/dist/services/mcp-registry.service.js.map +1 -0
- package/dist/services/mcp-registry.service.spec.d.ts +2 -0
- package/dist/services/mcp-registry.service.spec.d.ts.map +1 -0
- package/dist/services/mcp-registry.service.spec.js +61 -0
- package/dist/services/mcp-registry.service.spec.js.map +1 -0
- package/eslint.config.mjs +38 -0
- package/image.png +0 -0
- package/package.json +22 -5
- package/playground/README.md +17 -0
- package/playground/greeting.resource.ts +25 -0
- package/playground/greeting.tool.ts +36 -0
- package/playground/server.ts +25 -0
- package/tests/mcp-resource.e2e.spec.ts +179 -0
- package/tests/{mcp-auth.e2e.spec.ts → mcp-tool-auth.e2e.spec.ts} +72 -83
- package/tests/mcp-tool.e2e.spec.ts +235 -0
- package/tests/utils.ts +42 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tests/mcp.e2e.spec.ts +0 -115
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Injectable, Scope } from '@nestjs/common';
|
|
2
|
+
import { Resource } from '../src';
|
|
3
|
+
|
|
4
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
5
|
+
export class GreetingResource {
|
|
6
|
+
constructor() {}
|
|
7
|
+
|
|
8
|
+
@Resource({
|
|
9
|
+
name: 'hello-world',
|
|
10
|
+
description: 'A simple greeting resource',
|
|
11
|
+
mimeType: 'text/plain',
|
|
12
|
+
uri: 'mcp://hello-world/{name}',
|
|
13
|
+
})
|
|
14
|
+
sayHello({ name }) {
|
|
15
|
+
return {
|
|
16
|
+
contents: [
|
|
17
|
+
{
|
|
18
|
+
uri: 'mcp://hello-world',
|
|
19
|
+
mimeType: 'text/plain',
|
|
20
|
+
text: `Hello ${name}`,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Context, Tool } from '../src';
|
|
3
|
+
import { Progress } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class GreetingTool {
|
|
8
|
+
constructor() {}
|
|
9
|
+
|
|
10
|
+
@Tool({
|
|
11
|
+
name: 'hello-world',
|
|
12
|
+
description:
|
|
13
|
+
'Returns a greeting and simulates a long operation with progress updates',
|
|
14
|
+
parameters: z.object({
|
|
15
|
+
name: z.string().default('World'),
|
|
16
|
+
}),
|
|
17
|
+
})
|
|
18
|
+
async sayHello({ name }, context: Context) {
|
|
19
|
+
const greeting = `Hello, ${name}!`;
|
|
20
|
+
|
|
21
|
+
const totalSteps = 5;
|
|
22
|
+
for (let i = 0; i < totalSteps; i++) {
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
24
|
+
|
|
25
|
+
// Send a progress update.
|
|
26
|
+
await context.reportProgress({
|
|
27
|
+
progress: (i + 1) * 20,
|
|
28
|
+
total: 100,
|
|
29
|
+
} as Progress);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: 'text', text: greeting }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NestFactory } from '@nestjs/core';
|
|
2
|
+
import { Module } from '@nestjs/common';
|
|
3
|
+
import { McpModule } from '../src';
|
|
4
|
+
import { GreetingResource } from './greeting.resource';
|
|
5
|
+
import { GreetingTool } from './greeting.tool';
|
|
6
|
+
|
|
7
|
+
@Module({
|
|
8
|
+
imports: [
|
|
9
|
+
McpModule.forRoot({
|
|
10
|
+
name: 'playground-mcp-server',
|
|
11
|
+
version: '0.0.1',
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
providers: [GreetingResource, GreetingTool],
|
|
15
|
+
})
|
|
16
|
+
class AppModule {}
|
|
17
|
+
|
|
18
|
+
async function bootstrap() {
|
|
19
|
+
const app = await NestFactory.create(AppModule);
|
|
20
|
+
await app.listen(3030);
|
|
21
|
+
|
|
22
|
+
console.log('MCP server started on port 3030');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
void bootstrap();
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { INestApplication } from '@nestjs/common';
|
|
2
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
3
|
+
import { Injectable } from '@nestjs/common';
|
|
4
|
+
import { McpModule } from '../src/mcp.module';
|
|
5
|
+
import { createMCPClient } from './utils';
|
|
6
|
+
import { Resource } from '../src';
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class GreetingToolResource {
|
|
10
|
+
constructor() {}
|
|
11
|
+
|
|
12
|
+
@Resource({
|
|
13
|
+
name: 'hello-world',
|
|
14
|
+
description: 'A simple greeting resource',
|
|
15
|
+
mimeType: 'text/plain',
|
|
16
|
+
uri: 'mcp://hello-world',
|
|
17
|
+
})
|
|
18
|
+
async sayHello({ uri }) {
|
|
19
|
+
return {
|
|
20
|
+
contents: [
|
|
21
|
+
{
|
|
22
|
+
uri,
|
|
23
|
+
mimeType: 'text/plain',
|
|
24
|
+
text: 'Hello World',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Resource({
|
|
31
|
+
name: 'hello-world-dynamic',
|
|
32
|
+
description: 'A simple greeting dynamic resource',
|
|
33
|
+
mimeType: 'text/plain',
|
|
34
|
+
uri: 'mcp://hello-world-dynamic/{userName}',
|
|
35
|
+
})
|
|
36
|
+
async sayHelloDynamic({ uri, userName }) {
|
|
37
|
+
return {
|
|
38
|
+
contents: [
|
|
39
|
+
{
|
|
40
|
+
uri: uri,
|
|
41
|
+
mimeType: 'text/plain',
|
|
42
|
+
text: `Hello ${userName}`,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Resource({
|
|
49
|
+
name: 'hello-world-dynamic-multiple-paths',
|
|
50
|
+
description: 'A simple greeting dynamic resource with multiple paths',
|
|
51
|
+
mimeType: 'text/plain',
|
|
52
|
+
uri: 'mcp://hello-world-dynamic-multiple-paths/{userId}/{userName}',
|
|
53
|
+
})
|
|
54
|
+
async sayHelloMultiplePathsDynamic({ uri, userId, userName }) {
|
|
55
|
+
return {
|
|
56
|
+
contents: [
|
|
57
|
+
{
|
|
58
|
+
uri: uri,
|
|
59
|
+
mimeType: 'text/plain',
|
|
60
|
+
text: `Hello ${userName} from ${userId}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Resource({
|
|
67
|
+
name: 'hello-world-dynamic-multiple-paths-error',
|
|
68
|
+
description: 'A simple greeting dynamic resource with multiple paths',
|
|
69
|
+
mimeType: 'text/plain',
|
|
70
|
+
uri: 'mcp://hello-world-dynamic-multiple-paths-error/{userId}/{userName}',
|
|
71
|
+
})
|
|
72
|
+
async sayHelloMultiplePathsDynamicError() {
|
|
73
|
+
throw new Error('any error');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('E2E: MCP Resource Server', () => {
|
|
78
|
+
let app: INestApplication;
|
|
79
|
+
let testPort: number;
|
|
80
|
+
|
|
81
|
+
beforeAll(async () => {
|
|
82
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
83
|
+
imports: [
|
|
84
|
+
McpModule.forRoot({
|
|
85
|
+
name: 'test-mcp-server',
|
|
86
|
+
version: '0.0.1',
|
|
87
|
+
guards: [],
|
|
88
|
+
}),
|
|
89
|
+
],
|
|
90
|
+
providers: [GreetingToolResource],
|
|
91
|
+
}).compile();
|
|
92
|
+
|
|
93
|
+
app = moduleFixture.createNestApplication();
|
|
94
|
+
await app.listen(0);
|
|
95
|
+
|
|
96
|
+
const server = app.getHttpServer();
|
|
97
|
+
testPort = server.address().port;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterAll(async () => {
|
|
101
|
+
await app.close();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should list resources', async () => {
|
|
105
|
+
const client = await createMCPClient(testPort);
|
|
106
|
+
const resources = await client.listResources();
|
|
107
|
+
|
|
108
|
+
expect(resources.resources.find((r) => r.name === 'hello-world')).toEqual({
|
|
109
|
+
name: 'hello-world',
|
|
110
|
+
uri: 'mcp://hello-world',
|
|
111
|
+
description: 'A simple greeting resource',
|
|
112
|
+
mimeType: 'text/plain',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
resources.resources.find((r) => r.name === 'hello-world-dynamic'),
|
|
117
|
+
).toEqual({
|
|
118
|
+
name: 'hello-world-dynamic',
|
|
119
|
+
uri: 'mcp://hello-world-dynamic/{userName}',
|
|
120
|
+
description: 'A simple greeting dynamic resource',
|
|
121
|
+
mimeType: 'text/plain',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await client.close();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should call the dynamic resource', async () => {
|
|
128
|
+
const client = await createMCPClient(testPort);
|
|
129
|
+
|
|
130
|
+
const result: any = await client.readResource({
|
|
131
|
+
uri: 'mcp://hello-world-dynamic/Raphael_John',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.contents[0].uri).toBe(
|
|
135
|
+
'mcp://hello-world-dynamic/Raphael_John',
|
|
136
|
+
);
|
|
137
|
+
expect(result.contents[0].mimeType).toBe('text/plain');
|
|
138
|
+
expect(result.contents[0].text).toBe('Hello Raphael_John');
|
|
139
|
+
|
|
140
|
+
await client.close();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should call the dynamic resource with multiple paths', async () => {
|
|
144
|
+
const client = await createMCPClient(testPort);
|
|
145
|
+
|
|
146
|
+
const result: any = await client.readResource({
|
|
147
|
+
uri: 'mcp://hello-world-dynamic-multiple-paths/123/Raphael_John',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(result.contents[0].uri).toBe(
|
|
151
|
+
'mcp://hello-world-dynamic-multiple-paths/123/Raphael_John',
|
|
152
|
+
);
|
|
153
|
+
expect(result.contents[0].mimeType).toBe('text/plain');
|
|
154
|
+
expect(result.contents[0].text).toBe('Hello Raphael_John from 123');
|
|
155
|
+
|
|
156
|
+
await client.close();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should return an error when the resource is not found', async () => {
|
|
160
|
+
const client = await createMCPClient(testPort);
|
|
161
|
+
|
|
162
|
+
const result: any = await client.readResource({
|
|
163
|
+
uri: 'mcp://hello-world-dynamic-multiple-paths-error/123/Raphael_John',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(result).toEqual({
|
|
167
|
+
contents: [
|
|
168
|
+
{
|
|
169
|
+
uri: 'mcp://hello-world-dynamic-multiple-paths-error/123/Raphael_John',
|
|
170
|
+
mimeType: 'text/plain',
|
|
171
|
+
text: 'any error',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
isError: true,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await client.close();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
import { INestApplication, Injectable } from
|
|
2
|
-
import { Test, TestingModule } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { Context, Tool } from
|
|
5
|
-
import { McpModule } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import { INestApplication, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { Context, Tool } from '../src';
|
|
5
|
+
import { McpModule } from '../src/mcp.module';
|
|
6
|
+
import { Progress } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
8
|
+
import { createMCPClient } from './utils';
|
|
9
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
10
10
|
|
|
11
11
|
// Mock authentication guard
|
|
12
12
|
class MockAuthGuard implements CanActivate {
|
|
13
13
|
canActivate(context: ExecutionContext): boolean {
|
|
14
14
|
const request = context.switchToHttp().getRequest();
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
if (
|
|
17
|
+
request.headers.authorization &&
|
|
18
|
+
request.headers.authorization.includes('token-xyz')
|
|
19
|
+
) {
|
|
18
20
|
request.user = {
|
|
19
|
-
id:
|
|
20
|
-
name:
|
|
21
|
+
id: 'user123',
|
|
22
|
+
name: 'Test User',
|
|
21
23
|
orgMemberships: [
|
|
22
24
|
{
|
|
23
|
-
orgId:
|
|
25
|
+
orgId: 'org123',
|
|
24
26
|
organization: {
|
|
25
|
-
name:
|
|
27
|
+
name: 'Auth Test Org',
|
|
26
28
|
},
|
|
27
29
|
},
|
|
28
30
|
],
|
|
@@ -30,6 +32,7 @@ class MockAuthGuard implements CanActivate {
|
|
|
30
32
|
|
|
31
33
|
return true;
|
|
32
34
|
}
|
|
35
|
+
|
|
33
36
|
return false;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
@@ -38,18 +41,18 @@ class MockAuthGuard implements CanActivate {
|
|
|
38
41
|
@Injectable()
|
|
39
42
|
class MockUserRepository {
|
|
40
43
|
async findOne() {
|
|
41
|
-
return {
|
|
42
|
-
id:
|
|
43
|
-
name:
|
|
44
|
+
return Promise.resolve({
|
|
45
|
+
id: 'userRepo123',
|
|
46
|
+
name: 'Repository User',
|
|
44
47
|
orgMemberships: [
|
|
45
48
|
{
|
|
46
|
-
orgId:
|
|
49
|
+
orgId: 'org123',
|
|
47
50
|
organization: {
|
|
48
|
-
name:
|
|
51
|
+
name: 'Repository Org',
|
|
49
52
|
},
|
|
50
53
|
},
|
|
51
54
|
],
|
|
52
|
-
};
|
|
55
|
+
});
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -59,10 +62,10 @@ export class AuthGreetingTool {
|
|
|
59
62
|
constructor(private readonly userRepository: MockUserRepository) {}
|
|
60
63
|
|
|
61
64
|
@Tool({
|
|
62
|
-
name:
|
|
63
|
-
description:
|
|
65
|
+
name: 'auth-hello-world',
|
|
66
|
+
description: 'A sample tool that accesses the authenticated user',
|
|
64
67
|
parameters: z.object({
|
|
65
|
-
name: z.string().default(
|
|
68
|
+
name: z.string().default('World'),
|
|
66
69
|
}),
|
|
67
70
|
})
|
|
68
71
|
async sayHello({ name }, context: Context, request: Request & { user: any }) {
|
|
@@ -71,15 +74,13 @@ export class AuthGreetingTool {
|
|
|
71
74
|
const authUser = request.user; // Authenticated user from the request
|
|
72
75
|
|
|
73
76
|
// Construct greeting using both data sources
|
|
74
|
-
const greeting = `Hello, ${name}! I'm ${authUser.name} from ${
|
|
75
|
-
authUser.orgMemberships[0].organization.name
|
|
76
|
-
}. Repository user is ${repoUser.name}.`;
|
|
77
|
+
const greeting = `Hello, ${name}! I'm ${authUser.name} from ${authUser.orgMemberships[0].organization.name}. Repository user is ${repoUser.name}.`;
|
|
77
78
|
|
|
78
79
|
// Report progress for demonstration
|
|
79
80
|
for (let i = 0; i < 5; i++) {
|
|
80
81
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
81
82
|
await context.reportProgress({
|
|
82
|
-
progress: (i+1) * 20,
|
|
83
|
+
progress: (i + 1) * 20,
|
|
83
84
|
total: 100,
|
|
84
85
|
} as Progress);
|
|
85
86
|
}
|
|
@@ -87,7 +88,7 @@ export class AuthGreetingTool {
|
|
|
87
88
|
return {
|
|
88
89
|
content: [
|
|
89
90
|
{
|
|
90
|
-
type:
|
|
91
|
+
type: 'text',
|
|
91
92
|
text: greeting,
|
|
92
93
|
},
|
|
93
94
|
],
|
|
@@ -95,7 +96,7 @@ export class AuthGreetingTool {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
describe(
|
|
99
|
+
describe('E2E: MCP Server Tool with Authentication', () => {
|
|
99
100
|
let app: INestApplication;
|
|
100
101
|
let testPort: number;
|
|
101
102
|
|
|
@@ -103,18 +104,21 @@ describe("E2E: MCP Server with Authentication", () => {
|
|
|
103
104
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
104
105
|
imports: [
|
|
105
106
|
McpModule.forRoot({
|
|
106
|
-
name:
|
|
107
|
-
version:
|
|
107
|
+
name: 'test-auth-mcp-server',
|
|
108
|
+
version: '0.0.1',
|
|
108
109
|
// Specify the MockAuthGuard to protect the messages endpoint
|
|
109
110
|
guards: [MockAuthGuard],
|
|
110
111
|
capabilities: {
|
|
112
|
+
resources: {},
|
|
113
|
+
resourceTemplates: {},
|
|
111
114
|
tools: {
|
|
112
|
-
|
|
113
|
-
description:
|
|
115
|
+
'auth-hello-world': {
|
|
116
|
+
description:
|
|
117
|
+
'A sample tool that accesses the authenticated user',
|
|
114
118
|
input: {
|
|
115
119
|
name: {
|
|
116
|
-
type:
|
|
117
|
-
default:
|
|
120
|
+
type: 'string',
|
|
121
|
+
default: 'World',
|
|
118
122
|
},
|
|
119
123
|
},
|
|
120
124
|
},
|
|
@@ -136,53 +140,43 @@ describe("E2E: MCP Server with Authentication", () => {
|
|
|
136
140
|
await app.close();
|
|
137
141
|
});
|
|
138
142
|
|
|
139
|
-
it(
|
|
140
|
-
const client =
|
|
141
|
-
{ name: "example-client", version: "1.0.0" },
|
|
142
|
-
{ capabilities: {} },
|
|
143
|
-
);
|
|
144
|
-
const sseUrl = new URL(`http://localhost:${testPort}/sse`);
|
|
145
|
-
const transport = new SSEClientTransport(sseUrl, {
|
|
143
|
+
it('should list tools', async () => {
|
|
144
|
+
const client = await createMCPClient(testPort, {
|
|
146
145
|
requestInit: {
|
|
147
146
|
headers: {
|
|
148
|
-
Authorization: 'Bearer token-xyz'
|
|
149
|
-
}
|
|
150
|
-
}
|
|
147
|
+
Authorization: 'Bearer token-xyz',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
151
150
|
});
|
|
152
|
-
await client.connect(transport);
|
|
153
151
|
const tools = await client.listTools();
|
|
154
152
|
|
|
155
153
|
// Verify that the authenticated tool is available
|
|
156
154
|
expect(tools.tools.length).toBeGreaterThan(0);
|
|
157
|
-
expect(
|
|
155
|
+
expect(
|
|
156
|
+
tools.tools.find((t) => t.name === 'auth-hello-world'),
|
|
157
|
+
).toBeDefined();
|
|
158
158
|
|
|
159
159
|
await client.close();
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
it('should inject authentication context into the tool', async () => {
|
|
163
|
-
const client =
|
|
164
|
-
{ name: "example-client", version: "1.0.0" },
|
|
165
|
-
{ capabilities: {} },
|
|
166
|
-
);
|
|
167
|
-
const sseUrl = new URL(`http://localhost:${testPort}/sse`);
|
|
168
|
-
const transport = new SSEClientTransport(sseUrl, {
|
|
163
|
+
const client = await createMCPClient(testPort, {
|
|
169
164
|
requestInit: {
|
|
170
165
|
headers: {
|
|
171
|
-
Authorization: 'Bearer token-xyz'
|
|
172
|
-
}
|
|
173
|
-
}
|
|
166
|
+
Authorization: 'Bearer token-xyz',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
174
169
|
});
|
|
175
|
-
await client.connect(transport);
|
|
176
170
|
|
|
177
171
|
let progressCount = 0;
|
|
178
172
|
const result: any = await client.callTool(
|
|
179
173
|
{
|
|
180
|
-
name:
|
|
181
|
-
arguments: { name:
|
|
174
|
+
name: 'auth-hello-world',
|
|
175
|
+
arguments: { name: 'Authenticated User' },
|
|
182
176
|
},
|
|
183
177
|
undefined,
|
|
184
178
|
{
|
|
185
|
-
onprogress: (
|
|
179
|
+
onprogress: () => {
|
|
186
180
|
progressCount++;
|
|
187
181
|
},
|
|
188
182
|
},
|
|
@@ -192,40 +186,35 @@ describe("E2E: MCP Server with Authentication", () => {
|
|
|
192
186
|
expect(progressCount).toBeGreaterThan(0);
|
|
193
187
|
|
|
194
188
|
// Verify that authentication context was available to the tool
|
|
195
|
-
expect(result.content[0].type).toBe(
|
|
196
|
-
expect(result.content[0].text).toContain(
|
|
197
|
-
expect(result.content[0].text).toContain(
|
|
198
|
-
expect(result.content[0].text).toContain(
|
|
189
|
+
expect(result.content[0].type).toBe('text');
|
|
190
|
+
expect(result.content[0].text).toContain('Auth Test Org');
|
|
191
|
+
expect(result.content[0].text).toContain('Test User');
|
|
192
|
+
expect(result.content[0].text).toContain(
|
|
193
|
+
'Repository user is Repository User',
|
|
194
|
+
);
|
|
199
195
|
|
|
200
196
|
await client.close();
|
|
201
197
|
});
|
|
202
198
|
|
|
203
199
|
it('should reject unauthenticated connections', async () => {
|
|
204
|
-
const client = new Client(
|
|
205
|
-
{ name: "example-client", version: "1.0.0" },
|
|
206
|
-
{ capabilities: {} },
|
|
207
|
-
);
|
|
208
|
-
const sseUrl = new URL(`http://localhost:${testPort}/sse`);
|
|
209
|
-
|
|
210
|
-
// Using invalid token
|
|
211
|
-
const transport = new SSEClientTransport(sseUrl, {
|
|
212
|
-
requestInit: {
|
|
213
|
-
headers: {
|
|
214
|
-
Authorization: 'Bearer invalid-token'
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
200
|
// Connection should be rejected
|
|
201
|
+
let client: Client | undefined;
|
|
220
202
|
try {
|
|
221
|
-
await
|
|
203
|
+
client = await createMCPClient(testPort, {
|
|
204
|
+
requestInit: {
|
|
205
|
+
headers: {
|
|
206
|
+
Authorization: 'Bearer invalid-token',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
222
211
|
// If we get here, the test should fail
|
|
223
212
|
fail('Connection should have been rejected');
|
|
224
213
|
} catch (error) {
|
|
225
214
|
// We expect an error to be thrown when authentication fails
|
|
226
215
|
expect(error).toBeDefined();
|
|
227
216
|
} finally {
|
|
228
|
-
await client
|
|
217
|
+
await client?.close();
|
|
229
218
|
}
|
|
230
219
|
});
|
|
231
|
-
});
|
|
220
|
+
});
|