@rekog/mcp-nest 1.1.4 → 1.2.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/.rinorism/index.d.ts +1 -0
- package/.vscode/settings.json +1 -0
- package/README.md +157 -15
- package/coverage/clover.xml +135 -121
- package/coverage/coverage-final.json +8 -8
- package/coverage/lcov-report/index.html +39 -39
- package/coverage/lcov.info +208 -207
- package/dist/controllers/sse.controller.factory.d.ts +13 -5
- package/dist/controllers/sse.controller.factory.d.ts.map +1 -1
- package/dist/controllers/sse.controller.factory.js +19 -6
- package/dist/controllers/sse.controller.factory.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/mcp-options.interface.d.ts +2 -0
- package/dist/interfaces/mcp-options.interface.d.ts.map +1 -1
- package/dist/mcp.module.d.ts.map +1 -1
- package/dist/mcp.module.js +10 -6
- package/dist/mcp.module.js.map +1 -1
- package/dist/services/mcp-tool-registry.service.d.ts +19 -0
- package/dist/services/mcp-tool-registry.service.d.ts.map +1 -0
- package/dist/services/mcp-tool-registry.service.js +66 -0
- package/dist/services/mcp-tool-registry.service.js.map +1 -0
- package/dist/services/mcp-tools-executor.service.d.ts +84 -0
- package/dist/services/mcp-tools-executor.service.d.ts.map +1 -0
- package/dist/services/mcp-tools-executor.service.js +154 -0
- package/dist/services/mcp-tools-executor.service.js.map +1 -0
- package/dist/services/mcp-tools.discovery.d.ts +19 -15
- package/dist/services/mcp-tools.discovery.d.ts.map +1 -1
- package/dist/services/mcp-tools.discovery.js +66 -127
- package/dist/services/mcp-tools.discovery.js.map +1 -1
- package/package.json +1 -1
- package/tests/mcp-auth.e2e.spec.ts +231 -0
- package/tests/mcp.e2e.spec.ts +30 -73
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -8,174 +8,113 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
exports.McpToolsDiscovery = void 0;
|
|
13
16
|
const common_1 = require("@nestjs/common");
|
|
14
17
|
const core_1 = require("@nestjs/core");
|
|
15
|
-
const decorators_1 = require("../decorators");
|
|
16
18
|
const zod_1 = require("zod");
|
|
17
19
|
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
18
20
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
const ContentZodSchema = zod_1.z.discriminatedUnion("type", [
|
|
26
|
-
|
|
27
|
-
]);
|
|
28
|
-
const ContentResultZodSchema = zod_1.z
|
|
29
|
-
.object({
|
|
30
|
-
content: ContentZodSchema.array(),
|
|
31
|
-
isError: zod_1.z.boolean().optional(),
|
|
32
|
-
})
|
|
33
|
-
.strict();
|
|
21
|
+
const mcp_tool_registry_service_1 = require("./mcp-tool-registry.service");
|
|
22
|
+
const TextContentZodSchema = zod_1.z.object({ type: zod_1.z.literal("text"), text: zod_1.z.string() }).strict();
|
|
23
|
+
const ImageContentZodSchema = zod_1.z.object({ type: zod_1.z.literal("image"), data: zod_1.z.string(), mimeType: zod_1.z.string() }).strict();
|
|
24
|
+
const AudioContentZodSchema = zod_1.z.object({ type: zod_1.z.literal("audio"), data: zod_1.z.string(), mimeType: zod_1.z.string() }).strict();
|
|
25
|
+
const ResourceZodSchema = zod_1.z.object({ uri: zod_1.z.string(), mimeType: zod_1.z.string().optional(), text: zod_1.z.string().optional(), blob: zod_1.z.string().optional(), name: zod_1.z.string().optional(), description: zod_1.z.string().optional(), size: zod_1.z.number().optional() }).refine(data => data.text !== undefined || data.blob !== undefined, { message: "Resource must have either text or blob content" });
|
|
26
|
+
const EmbeddedResourceZodSchema = zod_1.z.object({ type: zod_1.z.literal("resource"), resource: ResourceZodSchema }).strict();
|
|
27
|
+
const ContentZodSchema = zod_1.z.discriminatedUnion("type", [TextContentZodSchema, ImageContentZodSchema, AudioContentZodSchema, EmbeddedResourceZodSchema]);
|
|
28
|
+
const ContentResultZodSchema = zod_1.z.object({ content: ContentZodSchema.array().min(1), isError: zod_1.z.boolean().optional() }).strict();
|
|
34
29
|
class UserError extends Error {
|
|
35
|
-
constructor(message) {
|
|
36
|
-
super(message);
|
|
37
|
-
this.name = 'UserError';
|
|
38
|
-
}
|
|
39
30
|
}
|
|
40
31
|
let McpToolsDiscovery = class McpToolsDiscovery {
|
|
41
|
-
constructor(
|
|
42
|
-
this.
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
45
|
-
}
|
|
46
|
-
onApplicationBootstrap() {
|
|
47
|
-
this.collectTools();
|
|
32
|
+
constructor(registry, moduleRef, request) {
|
|
33
|
+
this.registry = registry;
|
|
34
|
+
this.moduleRef = moduleRef;
|
|
35
|
+
this.request = request;
|
|
48
36
|
}
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const controllers = this.discovery.getControllers();
|
|
52
|
-
const allInstances = [...providers, ...controllers]
|
|
53
|
-
.filter((wrapper) => wrapper.instance)
|
|
54
|
-
.map((wrapper) => wrapper.instance);
|
|
55
|
-
allInstances.forEach((instance) => {
|
|
56
|
-
if (!instance || typeof instance !== 'object') {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
this.metadataScanner.getAllMethodNames(instance).forEach((methodName) => {
|
|
60
|
-
const methodRef = instance[methodName];
|
|
61
|
-
const methodMetaKeys = Reflect.getOwnMetadataKeys(methodRef);
|
|
62
|
-
if (!methodMetaKeys.includes(decorators_1.MCP_TOOL_METADATA_KEY)) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const metadata = Reflect.getMetadata(decorators_1.MCP_TOOL_METADATA_KEY, methodRef);
|
|
66
|
-
this.tools.push({
|
|
67
|
-
metadata,
|
|
68
|
-
instance,
|
|
69
|
-
methodName,
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
registerTools(mcpServer) {
|
|
37
|
+
registerRequestHandlers(mcpServer) {
|
|
38
|
+
const tools = this.registry.getTools();
|
|
75
39
|
mcpServer.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
76
|
-
|
|
40
|
+
const toolList = tools.map((tool) => ({
|
|
77
41
|
name: tool.metadata.name,
|
|
78
42
|
description: tool.metadata.description,
|
|
79
43
|
inputSchema: tool.metadata.parameters
|
|
80
44
|
? (0, zod_to_json_schema_1.zodToJsonSchema)(tool.metadata.parameters)
|
|
81
45
|
: undefined,
|
|
82
46
|
}));
|
|
83
|
-
return {
|
|
84
|
-
tools
|
|
85
|
-
};
|
|
47
|
+
return { tools: toolList };
|
|
86
48
|
});
|
|
87
|
-
mcpServer.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
90
|
-
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${
|
|
91
|
-
}
|
|
92
|
-
const schema = tool.metadata.parameters;
|
|
93
|
-
let parsedParams = request.params.arguments;
|
|
94
|
-
if (schema && schema instanceof zod_1.z.ZodType) {
|
|
95
|
-
const result = schema.safeParse(request.params.arguments);
|
|
96
|
-
if (!result.success) {
|
|
97
|
-
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid ${request.params.name} parameters: ${JSON.stringify(result.error.format())}`);
|
|
98
|
-
}
|
|
99
|
-
parsedParams = result.data;
|
|
49
|
+
mcpServer.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (callRequest, baseContext) => {
|
|
50
|
+
const toolInfo = this.registry.findTool(callRequest.params.name);
|
|
51
|
+
if (!toolInfo) {
|
|
52
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${callRequest.params.name}`);
|
|
100
53
|
}
|
|
101
|
-
const
|
|
54
|
+
const schema = toolInfo.metadata.parameters;
|
|
55
|
+
let parsedParams = callRequest.params.arguments;
|
|
56
|
+
let toolExecutionResult;
|
|
102
57
|
try {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
content: [{ type: "text", text: result }],
|
|
108
|
-
});
|
|
58
|
+
const executionContext = this.createExecutionContext(mcpServer, callRequest.params?._meta?.progressToken, baseContext);
|
|
59
|
+
const toolInstance = await this.moduleRef.resolve(toolInfo.providerClass, undefined, { strict: false });
|
|
60
|
+
if (!toolInstance) {
|
|
61
|
+
throw new Error(`Failed to resolve instance for tool provider: ${toolInfo.providerClass.name}`);
|
|
109
62
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
63
|
+
const resultFromMethod = await toolInstance[toolInfo.methodName].call(toolInstance, parsedParams, executionContext);
|
|
64
|
+
if (typeof resultFromMethod === "string") {
|
|
65
|
+
toolExecutionResult = { content: [{ type: "text", text: resultFromMethod }] };
|
|
114
66
|
}
|
|
115
67
|
else {
|
|
116
|
-
|
|
68
|
+
const validation = ContentResultZodSchema.safeParse(resultFromMethod);
|
|
69
|
+
if (!validation.success) {
|
|
70
|
+
console.error(`Invalid result structure from tool ${toolInfo.metadata.name}:`, validation.error.format());
|
|
71
|
+
executionContext.log.error(`Invalid result structure from tool ${toolInfo.metadata.name}`, { error: validation.error.format() });
|
|
72
|
+
throw new Error(`Tool ${toolInfo.metadata.name} returned an invalid result structure.`);
|
|
73
|
+
}
|
|
74
|
+
toolExecutionResult = validation.data;
|
|
117
75
|
}
|
|
118
76
|
}
|
|
119
77
|
catch (error) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
78
|
+
const executionContextForLog = this.createExecutionContext(mcpServer, undefined, baseContext);
|
|
79
|
+
executionContextForLog.log.error(`Tool execution failed: ${toolInfo.metadata.name}`, { error: error.message });
|
|
80
|
+
if (error instanceof UserError || error instanceof types_js_1.McpError) {
|
|
81
|
+
toolExecutionResult = { content: [{ type: "text", text: error.message }], isError: true };
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
toolExecutionResult = { content: [{ type: "text", text: "An internal error occurred." }], isError: true };
|
|
125
85
|
}
|
|
126
|
-
return {
|
|
127
|
-
content: [{ type: "text", text: `Error: ${error}` }],
|
|
128
|
-
isError: true,
|
|
129
|
-
};
|
|
130
86
|
}
|
|
87
|
+
return { result: toolExecutionResult };
|
|
131
88
|
});
|
|
132
89
|
}
|
|
133
|
-
|
|
90
|
+
createExecutionContext(mcpServer, progressToken, baseContext) {
|
|
91
|
+
const user = this.request.user;
|
|
134
92
|
return {
|
|
93
|
+
user: user,
|
|
135
94
|
reportProgress: async (progress) => {
|
|
136
|
-
if (progressToken) {
|
|
137
|
-
|
|
138
|
-
method: "notifications/progress",
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
95
|
+
if (progressToken !== undefined) {
|
|
96
|
+
try {
|
|
97
|
+
await mcpServer.server.notification({ method: "notifications/progress", params: { ...progress, progressToken } });
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
console.error("Failed to send progress notification:", e);
|
|
101
|
+
}
|
|
144
102
|
}
|
|
145
103
|
},
|
|
146
104
|
log: {
|
|
147
|
-
debug: (message, context) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
});
|
|
152
|
-
},
|
|
153
|
-
error: (message, context) => {
|
|
154
|
-
mcpServer.server.sendLoggingMessage({
|
|
155
|
-
level: "error",
|
|
156
|
-
data: { message, context },
|
|
157
|
-
});
|
|
158
|
-
},
|
|
159
|
-
info: (message, context) => {
|
|
160
|
-
mcpServer.server.sendLoggingMessage({
|
|
161
|
-
level: "info",
|
|
162
|
-
data: { message, context },
|
|
163
|
-
});
|
|
164
|
-
},
|
|
165
|
-
warn: (message, context) => {
|
|
166
|
-
mcpServer.server.sendLoggingMessage({
|
|
167
|
-
level: "warning",
|
|
168
|
-
data: { message, context },
|
|
169
|
-
});
|
|
170
|
-
},
|
|
105
|
+
debug: (message, context) => { mcpServer.server.sendLoggingMessage({ level: "debug", data: { message, context, user: user?.userId } }); },
|
|
106
|
+
error: (message, context) => { mcpServer.server.sendLoggingMessage({ level: "error", data: { message, context, user: user?.userId } }); },
|
|
107
|
+
info: (message, context) => { mcpServer.server.sendLoggingMessage({ level: "info", data: { message, context, user: user?.userId } }); },
|
|
108
|
+
warn: (message, context) => { mcpServer.server.sendLoggingMessage({ level: "warning", data: { message, context, user: user?.userId } }); },
|
|
171
109
|
},
|
|
172
110
|
};
|
|
173
111
|
}
|
|
174
112
|
};
|
|
175
113
|
exports.McpToolsDiscovery = McpToolsDiscovery;
|
|
176
114
|
exports.McpToolsDiscovery = McpToolsDiscovery = __decorate([
|
|
177
|
-
(0, common_1.Injectable)(),
|
|
178
|
-
|
|
179
|
-
|
|
115
|
+
(0, common_1.Injectable)({ scope: common_1.Scope.REQUEST }),
|
|
116
|
+
__param(2, (0, common_1.Inject)(core_1.REQUEST)),
|
|
117
|
+
__metadata("design:paramtypes", [mcp_tool_registry_service_1.McpToolRegistryService,
|
|
118
|
+
core_1.ModuleRef, Object])
|
|
180
119
|
], McpToolsDiscovery);
|
|
181
120
|
//# sourceMappingURL=mcp-tools.discovery.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-tools.discovery.js","sourceRoot":"","sources":["../../src/services/mcp-tools.discovery.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mcp-tools.discovery.js","sourceRoot":"","sources":["../../src/services/mcp-tools.discovery.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,231 @@
|
|
|
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 { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { Progress } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { CanActivate, ExecutionContext } from "@nestjs/common";
|
|
10
|
+
|
|
11
|
+
// Mock authentication guard
|
|
12
|
+
class MockAuthGuard implements CanActivate {
|
|
13
|
+
canActivate(context: ExecutionContext): boolean {
|
|
14
|
+
const request = context.switchToHttp().getRequest();
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if (request.headers.authorization && request.headers.authorization.includes("token-xyz")) {
|
|
18
|
+
request.user = {
|
|
19
|
+
id: "user123",
|
|
20
|
+
name: "Test User",
|
|
21
|
+
orgMemberships: [
|
|
22
|
+
{
|
|
23
|
+
orgId: "org123",
|
|
24
|
+
organization: {
|
|
25
|
+
name: "Auth Test Org",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Mock user repository
|
|
38
|
+
@Injectable()
|
|
39
|
+
class MockUserRepository {
|
|
40
|
+
async findOne() {
|
|
41
|
+
return {
|
|
42
|
+
id: "userRepo123",
|
|
43
|
+
name: "Repository User",
|
|
44
|
+
orgMemberships: [
|
|
45
|
+
{
|
|
46
|
+
orgId: "org123",
|
|
47
|
+
organization: {
|
|
48
|
+
name: "Repository Org",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Greeting tool that uses the authentication context
|
|
57
|
+
@Injectable()
|
|
58
|
+
export class AuthGreetingTool {
|
|
59
|
+
constructor(private readonly userRepository: MockUserRepository) {}
|
|
60
|
+
|
|
61
|
+
@Tool({
|
|
62
|
+
name: "auth-hello-world",
|
|
63
|
+
description: "A sample tool that accesses the authenticated user",
|
|
64
|
+
parameters: z.object({
|
|
65
|
+
name: z.string().default("World"),
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
async sayHello({ name }, context: Context, request: Request & { user: any }) {
|
|
69
|
+
// Access both repository data and the authenticated user context
|
|
70
|
+
const repoUser = await this.userRepository.findOne();
|
|
71
|
+
const authUser = request.user; // Authenticated user from the request
|
|
72
|
+
|
|
73
|
+
// 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
|
+
|
|
78
|
+
// Report progress for demonstration
|
|
79
|
+
for (let i = 0; i < 5; i++) {
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
81
|
+
await context.reportProgress({
|
|
82
|
+
progress: (i+1) * 20,
|
|
83
|
+
total: 100,
|
|
84
|
+
} as Progress);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: greeting,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("E2E: MCP Server with Authentication", () => {
|
|
99
|
+
let app: INestApplication;
|
|
100
|
+
let testPort: number;
|
|
101
|
+
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
104
|
+
imports: [
|
|
105
|
+
McpModule.forRoot({
|
|
106
|
+
name: "test-auth-mcp-server",
|
|
107
|
+
version: "0.0.1",
|
|
108
|
+
// Specify the MockAuthGuard to protect the messages endpoint
|
|
109
|
+
guards: [MockAuthGuard],
|
|
110
|
+
capabilities: {
|
|
111
|
+
tools: {
|
|
112
|
+
"auth-hello-world": {
|
|
113
|
+
description: "A sample tool that accesses the authenticated user",
|
|
114
|
+
input: {
|
|
115
|
+
name: {
|
|
116
|
+
type: "string",
|
|
117
|
+
default: "World",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
125
|
+
providers: [AuthGreetingTool, MockUserRepository, MockAuthGuard],
|
|
126
|
+
}).compile();
|
|
127
|
+
|
|
128
|
+
app = moduleFixture.createNestApplication();
|
|
129
|
+
await app.listen(0);
|
|
130
|
+
|
|
131
|
+
const server = app.getHttpServer();
|
|
132
|
+
testPort = server.address().port;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
afterAll(async () => {
|
|
136
|
+
await app.close();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should list tools", async () => {
|
|
140
|
+
const client = new 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, {
|
|
146
|
+
requestInit: {
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: 'Bearer token-xyz'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
await client.connect(transport);
|
|
153
|
+
const tools = await client.listTools();
|
|
154
|
+
|
|
155
|
+
// Verify that the authenticated tool is available
|
|
156
|
+
expect(tools.tools.length).toBeGreaterThan(0);
|
|
157
|
+
expect(tools.tools.find((t) => t.name === "auth-hello-world")).toBeDefined();
|
|
158
|
+
|
|
159
|
+
await client.close();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should inject authentication context into the tool', async () => {
|
|
163
|
+
const client = new 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, {
|
|
169
|
+
requestInit: {
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: 'Bearer token-xyz'
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
await client.connect(transport);
|
|
176
|
+
|
|
177
|
+
let progressCount = 0;
|
|
178
|
+
const result: any = await client.callTool(
|
|
179
|
+
{
|
|
180
|
+
name: "auth-hello-world",
|
|
181
|
+
arguments: { name: "Authenticated User" },
|
|
182
|
+
},
|
|
183
|
+
undefined,
|
|
184
|
+
{
|
|
185
|
+
onprogress: (progress) => {
|
|
186
|
+
progressCount++;
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Verify that progress notifications were received
|
|
192
|
+
expect(progressCount).toBeGreaterThan(0);
|
|
193
|
+
|
|
194
|
+
// Verify that authentication context was available to the tool
|
|
195
|
+
expect(result.content[0].type).toBe("text");
|
|
196
|
+
expect(result.content[0].text).toContain("Auth Test Org");
|
|
197
|
+
expect(result.content[0].text).toContain("Test User");
|
|
198
|
+
expect(result.content[0].text).toContain("Repository user is Repository User");
|
|
199
|
+
|
|
200
|
+
await client.close();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
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
|
+
// Connection should be rejected
|
|
220
|
+
try {
|
|
221
|
+
await client.connect(transport);
|
|
222
|
+
// If we get here, the test should fail
|
|
223
|
+
fail('Connection should have been rejected');
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// We expect an error to be thrown when authentication fails
|
|
226
|
+
expect(error).toBeDefined();
|
|
227
|
+
} finally {
|
|
228
|
+
await client.close();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|