@toolsdk.ai/registry 1.0.132 → 1.0.133

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 (38) hide show
  1. package/dist/api/index.js +4 -0
  2. package/dist/domains/executor/executor-types.d.ts +3 -1
  3. package/dist/domains/executor/local-executor.d.ts +1 -1
  4. package/dist/domains/executor/local-executor.js +3 -3
  5. package/dist/domains/executor/sandbox-executor.d.ts +1 -1
  6. package/dist/domains/executor/sandbox-executor.js +3 -3
  7. package/dist/domains/oauth/__tests__/oauth-session.test.d.ts +1 -0
  8. package/dist/domains/oauth/__tests__/oauth-session.test.js +272 -0
  9. package/dist/domains/oauth/__tests__/oauth-utils.test.d.ts +1 -0
  10. package/dist/domains/oauth/__tests__/oauth-utils.test.js +284 -0
  11. package/dist/domains/oauth/index.d.ts +9 -0
  12. package/dist/domains/oauth/index.js +9 -0
  13. package/dist/domains/oauth/oauth-handler.d.ts +65 -0
  14. package/dist/domains/oauth/oauth-handler.js +355 -0
  15. package/dist/domains/oauth/oauth-route.d.ts +11 -0
  16. package/dist/domains/oauth/oauth-route.js +138 -0
  17. package/dist/domains/oauth/oauth-schema.d.ts +257 -0
  18. package/dist/domains/oauth/oauth-schema.js +119 -0
  19. package/dist/domains/oauth/oauth-session.d.ts +54 -0
  20. package/dist/domains/oauth/oauth-session.js +116 -0
  21. package/dist/domains/oauth/oauth-types.d.ts +148 -0
  22. package/dist/domains/oauth/oauth-types.js +9 -0
  23. package/dist/domains/oauth/oauth-utils.d.ts +99 -0
  24. package/dist/domains/oauth/oauth-utils.js +267 -0
  25. package/dist/domains/package/package-handler.d.ts +2 -2
  26. package/dist/domains/package/package-handler.js +4 -4
  27. package/dist/domains/package/package-route.js +5 -5
  28. package/dist/domains/package/package-schema.d.ts +51 -0
  29. package/dist/domains/package/package-schema.js +17 -0
  30. package/dist/domains/package/package-so.d.ts +6 -2
  31. package/dist/domains/package/package-so.js +4 -3
  32. package/dist/shared/schemas/common-schema.d.ts +52 -0
  33. package/dist/shared/schemas/common-schema.js +7 -0
  34. package/dist/shared/scripts-helpers/index.d.ts +4 -0
  35. package/dist/shared/utils/mcp-client-util.d.ts +1 -1
  36. package/dist/shared/utils/mcp-client-util.js +13 -4
  37. package/package.json +1 -1
  38. package/packages/developer-tools/github-mcp.json +4 -1
package/dist/api/index.js CHANGED
@@ -4,6 +4,7 @@ import { serve } from "@hono/node-server";
4
4
  import { swaggerUI } from "@hono/swagger-ui";
5
5
  import { OpenAPIHono } from "@hono/zod-openapi";
6
6
  import { configRoutes } from "../domains/config/config-route";
7
+ import { oauthDemoRoutes, oauthRoutes } from "../domains/oauth/oauth-route";
7
8
  import { repository } from "../domains/package/package-handler";
8
9
  import { packageRoutes } from "../domains/package/package-route";
9
10
  import { initRegistryFactory } from "../domains/registry/registry-factory";
@@ -31,6 +32,9 @@ const app = new OpenAPIHono();
31
32
  // Domain routes
32
33
  app.route("/api/v1", packageRoutes);
33
34
  app.route("/api/v1/config", configRoutes);
35
+ app.route("/api/v1/oauth", oauthRoutes);
36
+ // Demo routes (serves demo-oauth.html and handles callbacks)
37
+ app.route("/demo", oauthDemoRoutes);
34
38
  if (isSearchEnabled()) {
35
39
  initializeSearchService().catch(console.error);
36
40
  app.route("/api/v1/search", searchRoutes);
@@ -6,6 +6,8 @@ export interface ToolExecuteRequest {
6
6
  inputData: Record<string, unknown>;
7
7
  envs?: Record<string, string>;
8
8
  sandboxProvider?: MCPSandboxProvider;
9
+ /** OAuth access token for MCP servers that require OAuth authentication */
10
+ accessToken?: string;
9
11
  }
10
12
  /**
11
13
  * Tool Executor Interface
@@ -13,5 +15,5 @@ export interface ToolExecuteRequest {
13
15
  */
14
16
  export interface ToolExecutor {
15
17
  executeTool(request: ToolExecuteRequest): Promise<unknown>;
16
- listTools(packageName: string, sandboxProvider?: MCPSandboxProvider): Promise<Tool[]>;
18
+ listTools(packageName: string, accessToken?: string): Promise<Tool[]>;
17
19
  }
@@ -8,5 +8,5 @@ export declare class LocalExecutor implements ToolExecutor {
8
8
  private readonly packageRepository;
9
9
  constructor();
10
10
  executeTool(request: ToolExecuteRequest): Promise<unknown>;
11
- listTools(packageName: string): Promise<Tool[]>;
11
+ listTools(packageName: string, accessToken?: string): Promise<Tool[]>;
12
12
  }
@@ -14,7 +14,7 @@ export class LocalExecutor {
14
14
  }
15
15
  async executeTool(request) {
16
16
  const mcpServerConfig = this.packageRepository.getPackageConfig(request.packageName);
17
- const { client, closeConnection } = await getMcpClient(mcpServerConfig, request.envs || {});
17
+ const { client, closeConnection } = await getMcpClient(mcpServerConfig, request.envs || {}, request.accessToken);
18
18
  try {
19
19
  const result = await client.callTool({
20
20
  name: request.toolKey,
@@ -27,7 +27,7 @@ export class LocalExecutor {
27
27
  await closeConnection();
28
28
  }
29
29
  }
30
- async listTools(packageName) {
30
+ async listTools(packageName, accessToken) {
31
31
  const mcpServerConfig = this.packageRepository.getPackageConfig(packageName);
32
32
  const mockEnvs = {};
33
33
  if (mcpServerConfig.env) {
@@ -35,7 +35,7 @@ export class LocalExecutor {
35
35
  mockEnvs[key] = "mock_value";
36
36
  });
37
37
  }
38
- const { client, closeConnection } = await getMcpClient(mcpServerConfig, mockEnvs);
38
+ const { client, closeConnection } = await getMcpClient(mcpServerConfig, mockEnvs, accessToken);
39
39
  try {
40
40
  const { tools } = await client.listTools();
41
41
  console.log(`[LocalExecutor] Tools list retrieved successfully for package ${packageName}`);
@@ -12,5 +12,5 @@ export declare class SandboxExecutor implements ToolExecutor {
12
12
  private readonly localExecutor;
13
13
  constructor(provider: MCPSandboxProvider);
14
14
  executeTool(request: ToolExecuteRequest): Promise<unknown>;
15
- listTools(packageName: string): Promise<Tool[]>;
15
+ listTools(packageName: string, accessToken?: string): Promise<Tool[]>;
16
16
  }
@@ -48,13 +48,13 @@ export class SandboxExecutor {
48
48
  await this.sandboxPool.release(runtime, this.provider);
49
49
  }
50
50
  }
51
- async listTools(packageName) {
51
+ async listTools(packageName, accessToken) {
52
52
  const mcpServerConfig = this.packageRepository.getPackageConfig(packageName);
53
53
  const runtime = mcpServerConfig.runtime || "python";
54
54
  // Sandbox only supports node runtime, fallback to LOCAL for other runtimes
55
55
  if (runtime !== "node") {
56
56
  console.log(`[SandboxExecutor] Runtime '${runtime}' is not supported in sandbox, using LOCAL execution`);
57
- return await this.localExecutor.listTools(packageName);
57
+ return await this.localExecutor.listTools(packageName, accessToken);
58
58
  }
59
59
  const sandboxClient = await this.sandboxPool.acquire(runtime, this.provider);
60
60
  try {
@@ -67,7 +67,7 @@ export class SandboxExecutor {
67
67
  console.warn(`[SandboxExecutor] sandbox list tools failed, falling back to LOCAL execution`);
68
68
  console.warn(`[SandboxExecutor] Error: ${error instanceof Error ? error.message : String(error)}`);
69
69
  try {
70
- const tools = await this.localExecutor.listTools(packageName);
70
+ const tools = await this.localExecutor.listTools(packageName, accessToken);
71
71
  console.log(`[SandboxExecutor] Tools list retrieved successfully with LOCAL fallback`);
72
72
  return tools;
73
73
  }
@@ -0,0 +1,272 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { OAuthSessionStore } from "../oauth-session";
3
+ // Create a new instance for testing (not the singleton)
4
+ function createTestStore() {
5
+ const store = new OAuthSessionStore();
6
+ // Stop the cleanup timer to avoid interference with tests
7
+ store.stopCleanup();
8
+ return store;
9
+ }
10
+ function createMockSession(overrides = {}) {
11
+ return Object.assign({ sessionId: "test-session-id", state: "test-state", codeVerifier: "test-code-verifier", codeChallenge: "test-code-challenge", clientInfo: {
12
+ client_id: "test-client-id",
13
+ }, callbackBaseUrl: "http://localhost:3003/callback", mcpServerUrl: "http://localhost:3001/mcp", packageName: "github-mcp", oauthMetadata: {
14
+ issuer: "http://localhost:3001",
15
+ authorization_endpoint: "http://localhost:3001/authorize",
16
+ token_endpoint: "http://localhost:3001/token",
17
+ }, createdAt: Date.now() }, overrides);
18
+ }
19
+ describe("OAuthSessionStore", () => {
20
+ let store;
21
+ beforeEach(() => {
22
+ store = createTestStore();
23
+ });
24
+ afterEach(() => {
25
+ store.stopCleanup();
26
+ store.clear();
27
+ });
28
+ describe("set", () => {
29
+ it("should store a session", () => {
30
+ // Arrange
31
+ const session = createMockSession();
32
+ // Act
33
+ store.set(session);
34
+ // Assert
35
+ expect(store.has(session.sessionId)).toBe(true);
36
+ expect(store.size()).toBe(1);
37
+ });
38
+ it("should allow retrieval by sessionId after set", () => {
39
+ // Arrange
40
+ const session = createMockSession();
41
+ // Act
42
+ store.set(session);
43
+ const retrieved = store.get(session.sessionId);
44
+ // Assert
45
+ expect(retrieved).toEqual(session);
46
+ });
47
+ it("should allow retrieval by state after set", () => {
48
+ // Arrange
49
+ const session = createMockSession();
50
+ // Act
51
+ store.set(session);
52
+ const retrieved = store.getByState(session.state);
53
+ // Assert
54
+ expect(retrieved).toEqual(session);
55
+ });
56
+ it("should overwrite session with same sessionId", () => {
57
+ // Arrange
58
+ const session1 = createMockSession({ packageName: "package1" });
59
+ const session2 = createMockSession({ packageName: "package2" });
60
+ // Act
61
+ store.set(session1);
62
+ store.set(session2);
63
+ // Assert
64
+ const retrieved = store.get(session1.sessionId);
65
+ expect(retrieved === null || retrieved === void 0 ? void 0 : retrieved.packageName).toBe("package2");
66
+ expect(store.size()).toBe(1);
67
+ });
68
+ });
69
+ describe("get", () => {
70
+ it("should return undefined for non-existent session", () => {
71
+ // Act
72
+ const result = store.get("non-existent-id");
73
+ // Assert
74
+ expect(result).toBeUndefined();
75
+ });
76
+ it("should return session when exists", () => {
77
+ // Arrange
78
+ const session = createMockSession();
79
+ store.set(session);
80
+ // Act
81
+ const result = store.get(session.sessionId);
82
+ // Assert
83
+ expect(result).toEqual(session);
84
+ });
85
+ it("should return undefined for expired session", () => {
86
+ // Arrange - create session that expired 11 minutes ago
87
+ const expiredSession = createMockSession({
88
+ sessionId: "expired-session",
89
+ createdAt: Date.now() - 11 * 60 * 1000,
90
+ });
91
+ store.set(expiredSession);
92
+ // Act
93
+ const result = store.get("expired-session");
94
+ // Assert
95
+ expect(result).toBeUndefined();
96
+ expect(store.has("expired-session")).toBe(false);
97
+ });
98
+ it("should return session that is still valid (9 minutes old)", () => {
99
+ // Arrange
100
+ const validSession = createMockSession({
101
+ sessionId: "valid-session",
102
+ createdAt: Date.now() - 9 * 60 * 1000,
103
+ });
104
+ store.set(validSession);
105
+ // Act
106
+ const result = store.get("valid-session");
107
+ // Assert
108
+ expect(result).toBeDefined();
109
+ expect(result === null || result === void 0 ? void 0 : result.sessionId).toBe("valid-session");
110
+ });
111
+ });
112
+ describe("getByState", () => {
113
+ it("should return undefined for non-existent state", () => {
114
+ // Act
115
+ const result = store.getByState("non-existent-state");
116
+ // Assert
117
+ expect(result).toBeUndefined();
118
+ });
119
+ it("should return session when state exists", () => {
120
+ // Arrange
121
+ const session = createMockSession({ state: "unique-state" });
122
+ store.set(session);
123
+ // Act
124
+ const result = store.getByState("unique-state");
125
+ // Assert
126
+ expect(result).toEqual(session);
127
+ });
128
+ it("should return undefined for expired session by state", () => {
129
+ // Arrange
130
+ const expiredSession = createMockSession({
131
+ state: "expired-state",
132
+ createdAt: Date.now() - 11 * 60 * 1000,
133
+ });
134
+ store.set(expiredSession);
135
+ // Act
136
+ const result = store.getByState("expired-state");
137
+ // Assert
138
+ expect(result).toBeUndefined();
139
+ });
140
+ });
141
+ describe("delete", () => {
142
+ it("should return false for non-existent session", () => {
143
+ // Act
144
+ const result = store.delete("non-existent-id");
145
+ // Assert
146
+ expect(result).toBe(false);
147
+ });
148
+ it("should delete existing session and return true", () => {
149
+ // Arrange
150
+ const session = createMockSession();
151
+ store.set(session);
152
+ // Act
153
+ const result = store.delete(session.sessionId);
154
+ // Assert
155
+ expect(result).toBe(true);
156
+ expect(store.has(session.sessionId)).toBe(false);
157
+ expect(store.size()).toBe(0);
158
+ });
159
+ it("should also remove state mapping when session is deleted", () => {
160
+ // Arrange
161
+ const session = createMockSession({ state: "state-to-delete" });
162
+ store.set(session);
163
+ // Act
164
+ store.delete(session.sessionId);
165
+ const byState = store.getByState("state-to-delete");
166
+ // Assert
167
+ expect(byState).toBeUndefined();
168
+ });
169
+ });
170
+ describe("has", () => {
171
+ it("should return false for non-existent session", () => {
172
+ // Act
173
+ const result = store.has("non-existent-id");
174
+ // Assert
175
+ expect(result).toBe(false);
176
+ });
177
+ it("should return true for existing session", () => {
178
+ // Arrange
179
+ const session = createMockSession();
180
+ store.set(session);
181
+ // Act
182
+ const result = store.has(session.sessionId);
183
+ // Assert
184
+ expect(result).toBe(true);
185
+ });
186
+ });
187
+ describe("size", () => {
188
+ it("should return 0 for empty store", () => {
189
+ // Act
190
+ const result = store.size();
191
+ // Assert
192
+ expect(result).toBe(0);
193
+ });
194
+ it("should return correct count after adding sessions", () => {
195
+ // Arrange
196
+ store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
197
+ store.set(createMockSession({ sessionId: "session-2", state: "state-2" }));
198
+ store.set(createMockSession({ sessionId: "session-3", state: "state-3" }));
199
+ // Act
200
+ const result = store.size();
201
+ // Assert
202
+ expect(result).toBe(3);
203
+ });
204
+ it("should return correct count after deleting sessions", () => {
205
+ // Arrange
206
+ store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
207
+ store.set(createMockSession({ sessionId: "session-2", state: "state-2" }));
208
+ store.delete("session-1");
209
+ // Act
210
+ const result = store.size();
211
+ // Assert
212
+ expect(result).toBe(1);
213
+ });
214
+ });
215
+ describe("clear", () => {
216
+ it("should remove all sessions", () => {
217
+ // Arrange
218
+ store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
219
+ store.set(createMockSession({ sessionId: "session-2", state: "state-2" }));
220
+ // Act
221
+ store.clear();
222
+ // Assert
223
+ expect(store.size()).toBe(0);
224
+ expect(store.has("session-1")).toBe(false);
225
+ expect(store.has("session-2")).toBe(false);
226
+ });
227
+ it("should also clear state mappings", () => {
228
+ // Arrange
229
+ store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
230
+ // Act
231
+ store.clear();
232
+ const byState = store.getByState("state-1");
233
+ // Assert
234
+ expect(byState).toBeUndefined();
235
+ });
236
+ });
237
+ describe("multiple sessions", () => {
238
+ it("should handle multiple sessions with different states", () => {
239
+ var _a, _b, _c, _d;
240
+ // Arrange
241
+ const session1 = createMockSession({
242
+ sessionId: "session-1",
243
+ state: "state-1",
244
+ packageName: "package-1",
245
+ });
246
+ const session2 = createMockSession({
247
+ sessionId: "session-2",
248
+ state: "state-2",
249
+ packageName: "package-2",
250
+ });
251
+ // Act
252
+ store.set(session1);
253
+ store.set(session2);
254
+ // Assert
255
+ expect((_a = store.get("session-1")) === null || _a === void 0 ? void 0 : _a.packageName).toBe("package-1");
256
+ expect((_b = store.get("session-2")) === null || _b === void 0 ? void 0 : _b.packageName).toBe("package-2");
257
+ expect((_c = store.getByState("state-1")) === null || _c === void 0 ? void 0 : _c.packageName).toBe("package-1");
258
+ expect((_d = store.getByState("state-2")) === null || _d === void 0 ? void 0 : _d.packageName).toBe("package-2");
259
+ });
260
+ it("should not affect other sessions when deleting one", () => {
261
+ // Arrange
262
+ store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
263
+ store.set(createMockSession({ sessionId: "session-2", state: "state-2" }));
264
+ // Act
265
+ store.delete("session-1");
266
+ // Assert
267
+ expect(store.has("session-1")).toBe(false);
268
+ expect(store.has("session-2")).toBe(true);
269
+ expect(store.getByState("state-2")).toBeDefined();
270
+ });
271
+ });
272
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,284 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildAuthorizationUrl, generatePKCE, generateSessionId, generateState, getCanonicalResourceUri, parseWWWAuthenticate, verifyPKCESupport, } from "../oauth-utils";
3
+ describe("oauth-utils", () => {
4
+ describe("generatePKCE", () => {
5
+ it("should generate valid PKCE parameters", () => {
6
+ // Act
7
+ const pkce = generatePKCE();
8
+ // Assert
9
+ expect(pkce).toHaveProperty("codeVerifier");
10
+ expect(pkce).toHaveProperty("codeChallenge");
11
+ expect(pkce).toHaveProperty("codeChallengeMethod");
12
+ expect(pkce.codeChallengeMethod).toBe("S256");
13
+ });
14
+ it("should generate code verifier with correct length (43 chars base64url)", () => {
15
+ // Act
16
+ const pkce = generatePKCE();
17
+ // Assert - 32 bytes base64url encoded = 43 characters
18
+ expect(pkce.codeVerifier.length).toBe(43);
19
+ });
20
+ it("should generate code challenge with correct length", () => {
21
+ // Act
22
+ const pkce = generatePKCE();
23
+ // Assert - SHA256 hash (32 bytes) base64url encoded = 43 characters
24
+ expect(pkce.codeChallenge.length).toBe(43);
25
+ });
26
+ it("should generate different values each time", () => {
27
+ // Act
28
+ const pkce1 = generatePKCE();
29
+ const pkce2 = generatePKCE();
30
+ // Assert
31
+ expect(pkce1.codeVerifier).not.toBe(pkce2.codeVerifier);
32
+ expect(pkce1.codeChallenge).not.toBe(pkce2.codeChallenge);
33
+ });
34
+ it("should generate base64url encoded values (no +, /, =)", () => {
35
+ // Act
36
+ const pkce = generatePKCE();
37
+ // Assert - base64url should not contain +, /, or =
38
+ expect(pkce.codeVerifier).not.toMatch(/[+/=]/);
39
+ expect(pkce.codeChallenge).not.toMatch(/[+/=]/);
40
+ });
41
+ });
42
+ describe("generateState", () => {
43
+ it("should generate a valid UUID", () => {
44
+ // Act
45
+ const state = generateState();
46
+ // Assert - UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
47
+ expect(state).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
48
+ });
49
+ it("should generate different values each time", () => {
50
+ // Act
51
+ const state1 = generateState();
52
+ const state2 = generateState();
53
+ // Assert
54
+ expect(state1).not.toBe(state2);
55
+ });
56
+ });
57
+ describe("generateSessionId", () => {
58
+ it("should generate a valid UUID", () => {
59
+ // Act
60
+ const sessionId = generateSessionId();
61
+ // Assert
62
+ expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
63
+ });
64
+ it("should generate different values each time", () => {
65
+ // Act
66
+ const id1 = generateSessionId();
67
+ const id2 = generateSessionId();
68
+ // Assert
69
+ expect(id1).not.toBe(id2);
70
+ });
71
+ });
72
+ describe("parseWWWAuthenticate", () => {
73
+ it("should parse complete WWW-Authenticate header", () => {
74
+ // Arrange
75
+ const header = 'Bearer realm="mcp", resource_metadata="http://localhost:3001/.well-known/oauth-protected-resource", scope="read write"';
76
+ // Act
77
+ const result = parseWWWAuthenticate(header);
78
+ // Assert
79
+ expect(result).toEqual({
80
+ realm: "mcp",
81
+ resourceMetadataUrl: "http://localhost:3001/.well-known/oauth-protected-resource",
82
+ scope: "read write",
83
+ });
84
+ });
85
+ it("should parse header with only realm", () => {
86
+ // Arrange
87
+ const header = 'Bearer realm="example"';
88
+ // Act
89
+ const result = parseWWWAuthenticate(header);
90
+ // Assert
91
+ expect(result).toEqual({
92
+ realm: "example",
93
+ });
94
+ });
95
+ it("should parse header with only resource_metadata", () => {
96
+ // Arrange
97
+ const header = 'Bearer resource_metadata="http://example.com/.well-known/oauth-protected-resource"';
98
+ // Act
99
+ const result = parseWWWAuthenticate(header);
100
+ // Assert
101
+ expect(result).toEqual({
102
+ resourceMetadataUrl: "http://example.com/.well-known/oauth-protected-resource",
103
+ });
104
+ });
105
+ it("should return empty object for empty header", () => {
106
+ // Arrange
107
+ const header = "Bearer";
108
+ // Act
109
+ const result = parseWWWAuthenticate(header);
110
+ // Assert
111
+ expect(result).toEqual({});
112
+ });
113
+ });
114
+ describe("verifyPKCESupport", () => {
115
+ it("should return supported=true when S256 is in supported methods", () => {
116
+ // Arrange
117
+ const metadata = {
118
+ issuer: "http://localhost:3001",
119
+ authorization_endpoint: "http://localhost:3001/authorize",
120
+ token_endpoint: "http://localhost:3001/token",
121
+ code_challenge_methods_supported: ["S256", "plain"],
122
+ };
123
+ // Act
124
+ const result = verifyPKCESupport(metadata);
125
+ // Assert
126
+ expect(result).toEqual({ supported: true, advertised: true });
127
+ });
128
+ it("should return supported=false when S256 is not in supported methods", () => {
129
+ // Arrange
130
+ const metadata = {
131
+ issuer: "http://localhost:3001",
132
+ authorization_endpoint: "http://localhost:3001/authorize",
133
+ token_endpoint: "http://localhost:3001/token",
134
+ code_challenge_methods_supported: ["plain"],
135
+ };
136
+ // Act
137
+ const result = verifyPKCESupport(metadata);
138
+ // Assert
139
+ expect(result).toEqual({ supported: false, advertised: true });
140
+ });
141
+ it("should return supported=true, advertised=false when code_challenge_methods_supported is missing", () => {
142
+ // Arrange
143
+ const metadata = {
144
+ issuer: "http://localhost:3001",
145
+ authorization_endpoint: "http://localhost:3001/authorize",
146
+ token_endpoint: "http://localhost:3001/token",
147
+ };
148
+ // Act
149
+ const result = verifyPKCESupport(metadata);
150
+ // Assert
151
+ expect(result).toEqual({ supported: true, advertised: false });
152
+ });
153
+ it("should return supported=true, advertised=false when code_challenge_methods_supported is empty", () => {
154
+ // Arrange
155
+ const metadata = {
156
+ issuer: "http://localhost:3001",
157
+ authorization_endpoint: "http://localhost:3001/authorize",
158
+ token_endpoint: "http://localhost:3001/token",
159
+ code_challenge_methods_supported: [],
160
+ };
161
+ // Act
162
+ const result = verifyPKCESupport(metadata);
163
+ // Assert
164
+ expect(result).toEqual({ supported: true, advertised: false });
165
+ });
166
+ });
167
+ describe("buildAuthorizationUrl", () => {
168
+ it("should build URL with all required parameters", () => {
169
+ // Arrange
170
+ const params = {
171
+ authorizationEndpoint: "http://localhost:3001/authorize",
172
+ clientId: "test-client",
173
+ redirectUri: "http://localhost:3003/callback",
174
+ state: "random-state",
175
+ codeChallenge: "test-challenge",
176
+ codeChallengeMethod: "S256",
177
+ };
178
+ // Act
179
+ const result = buildAuthorizationUrl(params);
180
+ // Assert
181
+ const url = new URL(result);
182
+ expect(url.origin).toBe("http://localhost:3001");
183
+ expect(url.pathname).toBe("/authorize");
184
+ expect(url.searchParams.get("response_type")).toBe("code");
185
+ expect(url.searchParams.get("client_id")).toBe("test-client");
186
+ expect(url.searchParams.get("redirect_uri")).toBe("http://localhost:3003/callback");
187
+ expect(url.searchParams.get("state")).toBe("random-state");
188
+ expect(url.searchParams.get("code_challenge")).toBe("test-challenge");
189
+ expect(url.searchParams.get("code_challenge_method")).toBe("S256");
190
+ });
191
+ it("should include scope when provided", () => {
192
+ // Arrange
193
+ const params = {
194
+ authorizationEndpoint: "http://localhost:3001/authorize",
195
+ clientId: "test-client",
196
+ redirectUri: "http://localhost:3003/callback",
197
+ state: "random-state",
198
+ codeChallenge: "test-challenge",
199
+ codeChallengeMethod: "S256",
200
+ scope: "read write",
201
+ };
202
+ // Act
203
+ const result = buildAuthorizationUrl(params);
204
+ // Assert
205
+ const url = new URL(result);
206
+ expect(url.searchParams.get("scope")).toBe("read write");
207
+ });
208
+ it("should include resource when provided", () => {
209
+ // Arrange
210
+ const params = {
211
+ authorizationEndpoint: "http://localhost:3001/authorize",
212
+ clientId: "test-client",
213
+ redirectUri: "http://localhost:3003/callback",
214
+ state: "random-state",
215
+ codeChallenge: "test-challenge",
216
+ codeChallengeMethod: "S256",
217
+ resource: "http://localhost:3001/mcp",
218
+ };
219
+ // Act
220
+ const result = buildAuthorizationUrl(params);
221
+ // Assert
222
+ const url = new URL(result);
223
+ expect(url.searchParams.get("resource")).toBe("http://localhost:3001/mcp");
224
+ });
225
+ it("should not include scope when not provided", () => {
226
+ // Arrange
227
+ const params = {
228
+ authorizationEndpoint: "http://localhost:3001/authorize",
229
+ clientId: "test-client",
230
+ redirectUri: "http://localhost:3003/callback",
231
+ state: "random-state",
232
+ codeChallenge: "test-challenge",
233
+ codeChallengeMethod: "S256",
234
+ };
235
+ // Act
236
+ const result = buildAuthorizationUrl(params);
237
+ // Assert
238
+ const url = new URL(result);
239
+ expect(url.searchParams.has("scope")).toBe(false);
240
+ });
241
+ });
242
+ describe("getCanonicalResourceUri", () => {
243
+ it("should return URL without trailing slash", () => {
244
+ // Arrange
245
+ const mcpServerUrl = "http://localhost:3001/mcp/";
246
+ // Act
247
+ const result = getCanonicalResourceUri(mcpServerUrl);
248
+ // Assert
249
+ expect(result).toBe("http://localhost:3001/mcp");
250
+ });
251
+ it("should keep URL without trailing slash unchanged", () => {
252
+ // Arrange
253
+ const mcpServerUrl = "http://localhost:3001/mcp";
254
+ // Act
255
+ const result = getCanonicalResourceUri(mcpServerUrl);
256
+ // Assert
257
+ expect(result).toBe("http://localhost:3001/mcp");
258
+ });
259
+ it("should handle root URL correctly", () => {
260
+ // Arrange
261
+ const mcpServerUrl = "http://localhost:3001/";
262
+ // Act
263
+ const result = getCanonicalResourceUri(mcpServerUrl);
264
+ // Assert
265
+ expect(result).toBe("http://localhost:3001/");
266
+ });
267
+ it("should strip query parameters", () => {
268
+ // Arrange
269
+ const mcpServerUrl = "http://localhost:3001/mcp?param=value";
270
+ // Act
271
+ const result = getCanonicalResourceUri(mcpServerUrl);
272
+ // Assert
273
+ expect(result).toBe("http://localhost:3001/mcp");
274
+ });
275
+ it("should strip fragment", () => {
276
+ // Arrange
277
+ const mcpServerUrl = "http://localhost:3001/mcp#section";
278
+ // Act
279
+ const result = getCanonicalResourceUri(mcpServerUrl);
280
+ // Assert
281
+ expect(result).toBe("http://localhost:3001/mcp");
282
+ });
283
+ });
284
+ });