pi-soly 1.9.3 → 1.11.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/ask/index.ts +12 -11
- package/ask/picker.ts +356 -76
- package/ask/prompt.ts +6 -2
- package/ask/tests/picker.test.ts +273 -82
- package/codemap.ts +276 -0
- package/hotreload.ts +239 -0
- package/init.ts +302 -0
- package/mcp/CHANGELOG.md +384 -0
- package/mcp/LICENSE +21 -0
- package/mcp/OAUTH.md +355 -0
- package/mcp/README.md +410 -0
- package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
- package/mcp/__tests__/cli.upstream-test.ts +97 -0
- package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
- package/mcp/__tests__/config.upstream-test.ts +303 -0
- package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
- package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
- package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
- package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
- package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
- package/mcp/__tests__/errors.upstream-test.ts +218 -0
- package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
- package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
- package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
- package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
- package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
- package/mcp/__tests__/logger.upstream-test.ts +175 -0
- package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
- package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
- package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
- package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
- package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
- package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
- package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
- package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
- package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
- package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
- package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
- package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
- package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
- package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
- package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
- package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
- package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
- package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
- package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
- package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
- package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
- package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
- package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
- package/mcp/agent-dir.ts +20 -0
- package/mcp/app-bridge.bundle.js +67 -0
- package/mcp/cli.js +184 -0
- package/mcp/commands.ts +422 -0
- package/mcp/config.ts +666 -0
- package/mcp/consent-manager.ts +64 -0
- package/mcp/direct-tools.ts +439 -0
- package/mcp/elicitation-handler.ts +347 -0
- package/mcp/errors.ts +219 -0
- package/mcp/glimpse-ui.ts +80 -0
- package/mcp/host-html-template.ts +427 -0
- package/mcp/index.ts +362 -0
- package/mcp/init.ts +362 -0
- package/mcp/lifecycle.ts +93 -0
- package/mcp/logger.ts +169 -0
- package/mcp/mcp-auth-flow.ts +559 -0
- package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
- package/mcp/mcp-auth.ts +302 -0
- package/mcp/mcp-auth.upstream-test.ts +373 -0
- package/mcp/mcp-callback-server.ts +372 -0
- package/mcp/mcp-callback-server.upstream-test.ts +416 -0
- package/mcp/mcp-oauth-provider.ts +369 -0
- package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
- package/mcp/mcp-panel.ts +829 -0
- package/mcp/mcp-setup-panel.ts +580 -0
- package/mcp/metadata-cache.ts +201 -0
- package/mcp/notify.ts +111 -0
- package/mcp/npx-resolver.ts +424 -0
- package/mcp/oauth-handler.ts +57 -0
- package/mcp/onboarding-state.ts +68 -0
- package/mcp/package.json +106 -0
- package/mcp/panel-keys.ts +37 -0
- package/mcp/proxy-modes.ts +949 -0
- package/mcp/resource-tools.ts +17 -0
- package/mcp/sampling-handler.ts +268 -0
- package/mcp/server-manager.ts +545 -0
- package/mcp/state.ts +41 -0
- package/mcp/tool-metadata.ts +216 -0
- package/mcp/tool-registrar.ts +46 -0
- package/mcp/tool-result-renderer.ts +161 -0
- package/mcp/types.ts +448 -0
- package/mcp/ui-resource-handler.ts +146 -0
- package/mcp/ui-server.ts +623 -0
- package/mcp/ui-session.ts +386 -0
- package/mcp/ui-stream-types.ts +89 -0
- package/mcp/utils.ts +129 -0
- package/mcp/vitest.config.ts +14 -0
- package/migrate.ts +258 -0
- package/notification.ts +218 -0
- package/notifications-log.ts +83 -0
- package/package.json +20 -3
- package/status.ts +140 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for mcp-oauth-provider.ts - OAuth provider implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, before, after } from "node:test"
|
|
6
|
+
import assert from "node:assert"
|
|
7
|
+
import { existsSync, rmSync, mkdirSync } from "fs"
|
|
8
|
+
import { join } from "path"
|
|
9
|
+
import { tmpdir } from "os"
|
|
10
|
+
import { randomBytes } from "crypto"
|
|
11
|
+
|
|
12
|
+
// Set up isolated temp directory for tests
|
|
13
|
+
const TEST_DIR = join(tmpdir(), `mcp-oauth-test-${randomBytes(4).toString('hex')}`)
|
|
14
|
+
process.env.MCP_OAUTH_DIR = TEST_DIR
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
getOAuthCallbackPath,
|
|
18
|
+
getOAuthCallbackPort,
|
|
19
|
+
McpOAuthProvider,
|
|
20
|
+
setOAuthCallbackPath,
|
|
21
|
+
setOAuthCallbackPort,
|
|
22
|
+
type McpOAuthConfig,
|
|
23
|
+
} from "./mcp-oauth-provider.ts"
|
|
24
|
+
import { getAuthForUrl, saveAuthEntry, updateOAuthState } from "./mcp-auth.ts"
|
|
25
|
+
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
|
26
|
+
import type { OAuthClientInformationFull, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"
|
|
27
|
+
|
|
28
|
+
describe("McpOAuthProvider", () => {
|
|
29
|
+
const serverName = "test-server"
|
|
30
|
+
const serverUrl = "https://api.example.com"
|
|
31
|
+
let redirectCaptured: URL | undefined
|
|
32
|
+
|
|
33
|
+
before(() => {
|
|
34
|
+
// Ensure clean state
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(TEST_DIR)) {
|
|
37
|
+
rmSync(TEST_DIR, { recursive: true, force: true })
|
|
38
|
+
}
|
|
39
|
+
mkdirSync(TEST_DIR, { recursive: true })
|
|
40
|
+
} catch {
|
|
41
|
+
// Ignore cleanup errors
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
after(() => {
|
|
46
|
+
// Clean up temp directory
|
|
47
|
+
try {
|
|
48
|
+
if (existsSync(TEST_DIR)) {
|
|
49
|
+
rmSync(TEST_DIR, { recursive: true, force: true })
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore cleanup errors
|
|
53
|
+
}
|
|
54
|
+
redirectCaptured = undefined
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function createProvider(config: McpOAuthConfig = {}) {
|
|
58
|
+
return new McpOAuthProvider(serverName, serverUrl, config, {
|
|
59
|
+
onRedirect: async (url) => {
|
|
60
|
+
redirectCaptured = url
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("redirectUrl", () => {
|
|
66
|
+
it("should return the correct redirect URL", () => {
|
|
67
|
+
const provider = createProvider()
|
|
68
|
+
assert.strictEqual(
|
|
69
|
+
provider.redirectUrl,
|
|
70
|
+
"http://localhost:19876/callback"
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("should use a configured redirect URI", () => {
|
|
75
|
+
const provider = createProvider({ redirectUri: "http://localhost:3118/slack/callback" })
|
|
76
|
+
assert.strictEqual(provider.redirectUrl, "http://localhost:3118/slack/callback")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("should snapshot generated redirect URI at construction", () => {
|
|
80
|
+
const originalPort = getOAuthCallbackPort()
|
|
81
|
+
const originalPath = getOAuthCallbackPath()
|
|
82
|
+
setOAuthCallbackPort(41234)
|
|
83
|
+
setOAuthCallbackPath("/snapshot/callback")
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const provider = createProvider()
|
|
87
|
+
setOAuthCallbackPort(52345)
|
|
88
|
+
setOAuthCallbackPath("/changed/callback")
|
|
89
|
+
|
|
90
|
+
assert.strictEqual(provider.redirectUrl, "http://localhost:41234/snapshot/callback")
|
|
91
|
+
assert.deepStrictEqual(provider.clientMetadata.redirect_uris, ["http://localhost:41234/snapshot/callback"])
|
|
92
|
+
} finally {
|
|
93
|
+
setOAuthCallbackPort(originalPort)
|
|
94
|
+
setOAuthCallbackPath(originalPath)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("clientMetadata", () => {
|
|
100
|
+
it("should return correct metadata for public client", () => {
|
|
101
|
+
const provider = createProvider()
|
|
102
|
+
const metadata = provider.clientMetadata
|
|
103
|
+
|
|
104
|
+
assert.deepStrictEqual(metadata.redirect_uris, ["http://localhost:19876/callback"])
|
|
105
|
+
assert.strictEqual(metadata.client_name, "Pi Coding Agent")
|
|
106
|
+
assert.strictEqual(metadata.client_uri, "https://github.com/nicobailon/pi-mcp-adapter")
|
|
107
|
+
assert.deepStrictEqual(metadata.grant_types, ["authorization_code", "refresh_token"])
|
|
108
|
+
assert.deepStrictEqual(metadata.response_types, ["code"])
|
|
109
|
+
assert.strictEqual(metadata.token_endpoint_auth_method, "none")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("should return correct metadata for confidential client", () => {
|
|
113
|
+
const provider = createProvider({ clientSecret: "secret" })
|
|
114
|
+
const metadata = provider.clientMetadata
|
|
115
|
+
|
|
116
|
+
assert.strictEqual(metadata.token_endpoint_auth_method, "client_secret_post")
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("should use configured redirect URI and client metadata", () => {
|
|
120
|
+
const provider = createProvider({
|
|
121
|
+
redirectUri: "http://localhost:3118/slack/callback",
|
|
122
|
+
clientName: "Slack MCP",
|
|
123
|
+
clientUri: "https://example.com/slack-mcp",
|
|
124
|
+
})
|
|
125
|
+
const metadata = provider.clientMetadata
|
|
126
|
+
|
|
127
|
+
assert.deepStrictEqual(metadata.redirect_uris, ["http://localhost:3118/slack/callback"])
|
|
128
|
+
assert.strictEqual(metadata.client_name, "Slack MCP")
|
|
129
|
+
assert.strictEqual(metadata.client_uri, "https://example.com/slack-mcp")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("should use configured client name for client_credentials", () => {
|
|
133
|
+
const provider = createProvider({
|
|
134
|
+
grantType: "client_credentials",
|
|
135
|
+
clientName: "Service MCP",
|
|
136
|
+
})
|
|
137
|
+
const metadata = provider.clientMetadata
|
|
138
|
+
|
|
139
|
+
assert.strictEqual(metadata.client_name, "Service MCP")
|
|
140
|
+
assert.deepStrictEqual(metadata.redirect_uris, [])
|
|
141
|
+
assert.deepStrictEqual(metadata.grant_types, ["client_credentials"])
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe("clientInformation", () => {
|
|
146
|
+
it("should return config clientId when provided", async () => {
|
|
147
|
+
const provider = createProvider({ clientId: "config-client", clientSecret: "config-secret" })
|
|
148
|
+
const info = await provider.clientInformation()
|
|
149
|
+
|
|
150
|
+
assert.strictEqual(info?.client_id, "config-client")
|
|
151
|
+
assert.strictEqual(info?.client_secret, "config-secret")
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("should return stored client info when no config", async () => {
|
|
155
|
+
const provider = createProvider()
|
|
156
|
+
|
|
157
|
+
// Save client info directly
|
|
158
|
+
saveAuthEntry(serverName, {
|
|
159
|
+
clientInfo: {
|
|
160
|
+
clientId: "stored-client",
|
|
161
|
+
clientSecret: "stored-secret",
|
|
162
|
+
clientIdIssuedAt: Math.floor(Date.now() / 1000),
|
|
163
|
+
clientSecretExpiresAt: Math.floor(Date.now() / 1000) + 3600,
|
|
164
|
+
},
|
|
165
|
+
serverUrl,
|
|
166
|
+
}, serverUrl)
|
|
167
|
+
|
|
168
|
+
const info = await provider.clientInformation()
|
|
169
|
+
assert.strictEqual(info?.client_id, "stored-client")
|
|
170
|
+
assert.strictEqual(info?.client_secret, "stored-secret")
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("should return undefined when URL doesn't match", async () => {
|
|
174
|
+
const provider = createProvider()
|
|
175
|
+
|
|
176
|
+
// Save client info with different URL
|
|
177
|
+
saveAuthEntry(serverName, {
|
|
178
|
+
clientInfo: {
|
|
179
|
+
clientId: "stored-client",
|
|
180
|
+
clientSecret: "stored-secret",
|
|
181
|
+
},
|
|
182
|
+
serverUrl: "https://different.com",
|
|
183
|
+
}, "https://different.com")
|
|
184
|
+
|
|
185
|
+
const info = await provider.clientInformation()
|
|
186
|
+
assert.strictEqual(info, undefined)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("should return undefined when client secret expired", async () => {
|
|
190
|
+
const provider = createProvider()
|
|
191
|
+
|
|
192
|
+
// Save client info with expired secret
|
|
193
|
+
saveAuthEntry(serverName, {
|
|
194
|
+
clientInfo: {
|
|
195
|
+
clientId: "stored-client",
|
|
196
|
+
clientSecret: "stored-secret",
|
|
197
|
+
clientSecretExpiresAt: 1, // Expired in 1970
|
|
198
|
+
},
|
|
199
|
+
serverUrl,
|
|
200
|
+
}, serverUrl)
|
|
201
|
+
|
|
202
|
+
const info = await provider.clientInformation()
|
|
203
|
+
assert.strictEqual(info, undefined)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("should prefer config over stored", async () => {
|
|
207
|
+
const provider = createProvider({ clientId: "config-client" })
|
|
208
|
+
|
|
209
|
+
// Save different client info
|
|
210
|
+
saveAuthEntry(serverName, {
|
|
211
|
+
clientInfo: {
|
|
212
|
+
clientId: "stored-client",
|
|
213
|
+
clientSecret: "stored-secret",
|
|
214
|
+
},
|
|
215
|
+
serverUrl,
|
|
216
|
+
}, serverUrl)
|
|
217
|
+
|
|
218
|
+
const info = await provider.clientInformation()
|
|
219
|
+
assert.strictEqual(info?.client_id, "config-client")
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe("saveClientInformation", () => {
|
|
224
|
+
it("should save client information", async () => {
|
|
225
|
+
const provider = createProvider()
|
|
226
|
+
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
|
227
|
+
const info: OAuthClientInformationFull = {
|
|
228
|
+
client_id: "new-client",
|
|
229
|
+
client_secret: "new-secret",
|
|
230
|
+
redirect_uris: ["http://localhost:3118/callback"],
|
|
231
|
+
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
232
|
+
client_secret_expires_at: futureTime,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await provider.saveClientInformation(info)
|
|
236
|
+
|
|
237
|
+
const storedInfo = await provider.clientInformation()
|
|
238
|
+
assert.strictEqual(storedInfo?.client_id, "new-client")
|
|
239
|
+
assert.strictEqual(storedInfo?.client_secret, "new-secret")
|
|
240
|
+
assert.deepStrictEqual(getAuthForUrl(serverName, serverUrl)?.clientInfo?.redirectUris, ["http://localhost:3118/callback"])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("should save the current redirect URL when registration omits redirect_uris", async () => {
|
|
244
|
+
const provider = new McpOAuthProvider("redirect-fallback", serverUrl, { redirectUri: "http://localhost:3118/custom" }, {
|
|
245
|
+
onRedirect: async () => {},
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
await provider.saveClientInformation({
|
|
249
|
+
client_id: "fallback-client",
|
|
250
|
+
client_secret: "fallback-secret",
|
|
251
|
+
} as OAuthClientInformationFull)
|
|
252
|
+
|
|
253
|
+
assert.deepStrictEqual(getAuthForUrl("redirect-fallback", serverUrl)?.clientInfo?.redirectUris, ["http://localhost:3118/custom"])
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it("should return stored dynamic client info even when redirect URIs are stale", async () => {
|
|
257
|
+
const provider = new McpOAuthProvider("stale-redirect-client", serverUrl, { redirectUri: "http://localhost:3118/current" }, {
|
|
258
|
+
onRedirect: async () => {},
|
|
259
|
+
})
|
|
260
|
+
saveAuthEntry("stale-redirect-client", {
|
|
261
|
+
clientInfo: {
|
|
262
|
+
clientId: "stored-client",
|
|
263
|
+
clientSecret: "stored-secret",
|
|
264
|
+
redirectUris: ["http://localhost:19876/callback"],
|
|
265
|
+
},
|
|
266
|
+
serverUrl,
|
|
267
|
+
}, serverUrl)
|
|
268
|
+
|
|
269
|
+
const info = await provider.clientInformation()
|
|
270
|
+
assert.strictEqual(info?.client_id, "stored-client")
|
|
271
|
+
assert.strictEqual(info?.client_secret, "stored-secret")
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
describe("tokens / saveTokens", () => {
|
|
276
|
+
it("should save and retrieve tokens", async () => {
|
|
277
|
+
const provider = createProvider()
|
|
278
|
+
const tokens: OAuthTokens = {
|
|
279
|
+
access_token: "access-123",
|
|
280
|
+
token_type: "Bearer",
|
|
281
|
+
refresh_token: "refresh-456",
|
|
282
|
+
expires_in: 3600,
|
|
283
|
+
scope: "read write",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await provider.saveTokens(tokens)
|
|
287
|
+
const stored = await provider.tokens()
|
|
288
|
+
|
|
289
|
+
assert.strictEqual(stored?.access_token, "access-123")
|
|
290
|
+
assert.strictEqual(stored?.refresh_token, "refresh-456")
|
|
291
|
+
assert.strictEqual(stored?.scope, "read write")
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it("should calculate expires_in from stored expiresAt", async () => {
|
|
295
|
+
const provider = createProvider()
|
|
296
|
+
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
|
297
|
+
|
|
298
|
+
await provider.saveTokens({
|
|
299
|
+
access_token: "access",
|
|
300
|
+
token_type: "Bearer",
|
|
301
|
+
expires_in: 3600,
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const stored = await provider.tokens()
|
|
305
|
+
assert.ok(stored?.expires_in !== undefined)
|
|
306
|
+
assert.ok(stored!.expires_in! > 0)
|
|
307
|
+
assert.ok(stored!.expires_in! <= 3600)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it("should return undefined when URL doesn't match", async () => {
|
|
311
|
+
const provider = createProvider()
|
|
312
|
+
|
|
313
|
+
// Save tokens with different URL
|
|
314
|
+
saveAuthEntry(serverName, {
|
|
315
|
+
tokens: {
|
|
316
|
+
accessToken: "token",
|
|
317
|
+
},
|
|
318
|
+
serverUrl: "https://different.com",
|
|
319
|
+
}, "https://different.com")
|
|
320
|
+
|
|
321
|
+
const stored = await provider.tokens()
|
|
322
|
+
assert.strictEqual(stored, undefined)
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe("redirectToAuthorization", () => {
|
|
327
|
+
it("should call onRedirect with URL when a flow is in progress", async () => {
|
|
328
|
+
const provider = new McpOAuthProvider("redirect-with-state", serverUrl, {}, {
|
|
329
|
+
onRedirect: async (url) => {
|
|
330
|
+
redirectCaptured = url
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
await updateOAuthState("redirect-with-state", "state-abc", serverUrl)
|
|
334
|
+
const testUrl = new URL("https://example.com/auth")
|
|
335
|
+
|
|
336
|
+
await provider.redirectToAuthorization(testUrl)
|
|
337
|
+
|
|
338
|
+
assert.strictEqual(redirectCaptured, testUrl)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it("should throw UnauthorizedError when no flow is in progress", async () => {
|
|
342
|
+
const provider = new McpOAuthProvider("redirect-no-state", serverUrl, {}, {
|
|
343
|
+
onRedirect: async () => {},
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
await assert.rejects(
|
|
347
|
+
async () => provider.redirectToAuthorization(new URL("https://example.com/auth")),
|
|
348
|
+
(err: unknown) => err instanceof UnauthorizedError && /Re-authentication required/.test((err as Error).message),
|
|
349
|
+
)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it("should ignore OAuth state saved for a different server URL before redirecting", async () => {
|
|
353
|
+
let redirected = false
|
|
354
|
+
const provider = new McpOAuthProvider("redirect-url-bound", serverUrl, {}, {
|
|
355
|
+
onRedirect: async () => {
|
|
356
|
+
redirected = true
|
|
357
|
+
},
|
|
358
|
+
})
|
|
359
|
+
saveAuthEntry("redirect-url-bound", {
|
|
360
|
+
oauthState: "stale-state",
|
|
361
|
+
serverUrl: "https://different.example.com",
|
|
362
|
+
}, "https://different.example.com")
|
|
363
|
+
|
|
364
|
+
await assert.rejects(
|
|
365
|
+
async () => provider.redirectToAuthorization(new URL("https://example.com/auth")),
|
|
366
|
+
(err: unknown) => err instanceof UnauthorizedError && /Re-authentication required/.test((err as Error).message),
|
|
367
|
+
)
|
|
368
|
+
assert.strictEqual(redirected, false)
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
describe("codeVerifier / saveCodeVerifier", () => {
|
|
373
|
+
it("should save and retrieve code verifier", async () => {
|
|
374
|
+
const provider = new McpOAuthProvider("code-verifier-test", serverUrl, {}, {
|
|
375
|
+
onRedirect: async () => {},
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
await provider.saveCodeVerifier("verifier-abc-123")
|
|
379
|
+
|
|
380
|
+
const verifier = await provider.codeVerifier()
|
|
381
|
+
assert.strictEqual(verifier, "verifier-abc-123")
|
|
382
|
+
assert.strictEqual(getAuthForUrl("code-verifier-test", serverUrl)?.codeVerifier, "verifier-abc-123")
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it("should throw when no code verifier", async () => {
|
|
386
|
+
const provider = new McpOAuthProvider("code-verifier-throw", serverUrl, {}, {
|
|
387
|
+
onRedirect: async () => {},
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
await assert.rejects(
|
|
391
|
+
async () => provider.codeVerifier(),
|
|
392
|
+
/No code verifier saved/
|
|
393
|
+
)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it("should ignore code verifiers saved for a different server URL", async () => {
|
|
397
|
+
const provider = new McpOAuthProvider("code-verifier-url-bound", serverUrl, {}, {
|
|
398
|
+
onRedirect: async () => {},
|
|
399
|
+
})
|
|
400
|
+
saveAuthEntry("code-verifier-url-bound", {
|
|
401
|
+
codeVerifier: "stale-verifier",
|
|
402
|
+
serverUrl: "https://different.example.com",
|
|
403
|
+
}, "https://different.example.com")
|
|
404
|
+
|
|
405
|
+
await assert.rejects(
|
|
406
|
+
async () => provider.codeVerifier(),
|
|
407
|
+
/No code verifier saved/
|
|
408
|
+
)
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
describe("state / saveState", () => {
|
|
413
|
+
it("should save and retrieve state", async () => {
|
|
414
|
+
const provider = new McpOAuthProvider("state-test-save", serverUrl, {}, {
|
|
415
|
+
onRedirect: async () => {},
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
await provider.saveState("state-xyz-789")
|
|
419
|
+
|
|
420
|
+
const state = await provider.state()
|
|
421
|
+
assert.strictEqual(state, "state-xyz-789")
|
|
422
|
+
assert.strictEqual(getAuthForUrl("state-test-save", serverUrl)?.oauthState, "state-xyz-789")
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it("should throw UnauthorizedError when no state is saved", async () => {
|
|
426
|
+
const provider = new McpOAuthProvider("state-test-throw", serverUrl, {}, {
|
|
427
|
+
onRedirect: async () => {},
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
await assert.rejects(
|
|
431
|
+
async () => provider.state(),
|
|
432
|
+
(err: unknown) => err instanceof UnauthorizedError && /Re-authentication required/.test((err as Error).message),
|
|
433
|
+
)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it("should ignore OAuth state saved for a different server URL", async () => {
|
|
437
|
+
const provider = new McpOAuthProvider("state-url-bound", serverUrl, {}, {
|
|
438
|
+
onRedirect: async () => {},
|
|
439
|
+
})
|
|
440
|
+
saveAuthEntry("state-url-bound", {
|
|
441
|
+
oauthState: "stale-state",
|
|
442
|
+
serverUrl: "https://different.example.com",
|
|
443
|
+
}, "https://different.example.com")
|
|
444
|
+
|
|
445
|
+
await assert.rejects(
|
|
446
|
+
async () => provider.state(),
|
|
447
|
+
(err: unknown) => err instanceof UnauthorizedError && /Re-authentication required/.test((err as Error).message),
|
|
448
|
+
)
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
describe("invalidateCredentials", () => {
|
|
453
|
+
it("should remove all credentials when type is 'all'", async () => {
|
|
454
|
+
const provider = createProvider()
|
|
455
|
+
|
|
456
|
+
await provider.saveTokens({
|
|
457
|
+
access_token: "token",
|
|
458
|
+
token_type: "Bearer",
|
|
459
|
+
})
|
|
460
|
+
await provider.saveClientInformation({
|
|
461
|
+
client_id: "client",
|
|
462
|
+
client_secret: "secret",
|
|
463
|
+
redirect_uris: ["http://localhost/callback"],
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
await provider.invalidateCredentials("all")
|
|
467
|
+
|
|
468
|
+
assert.strictEqual(await provider.tokens(), undefined)
|
|
469
|
+
assert.strictEqual(await provider.clientInformation(), undefined)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it("should only remove tokens when type is 'tokens'", async () => {
|
|
473
|
+
const provider = createProvider()
|
|
474
|
+
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
|
475
|
+
|
|
476
|
+
await provider.saveTokens({
|
|
477
|
+
access_token: "token",
|
|
478
|
+
token_type: "Bearer",
|
|
479
|
+
})
|
|
480
|
+
await provider.saveClientInformation({
|
|
481
|
+
client_id: "client",
|
|
482
|
+
client_secret: "secret",
|
|
483
|
+
redirect_uris: ["http://localhost/callback"],
|
|
484
|
+
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
485
|
+
client_secret_expires_at: futureTime,
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
await provider.invalidateCredentials("tokens")
|
|
489
|
+
|
|
490
|
+
assert.strictEqual(await provider.tokens(), undefined)
|
|
491
|
+
const clientInfo = await provider.clientInformation()
|
|
492
|
+
assert.strictEqual(clientInfo?.client_id, "client")
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it("should only remove client info when type is 'client'", async () => {
|
|
496
|
+
const provider = createProvider()
|
|
497
|
+
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
|
498
|
+
|
|
499
|
+
await provider.saveTokens({
|
|
500
|
+
access_token: "token",
|
|
501
|
+
token_type: "Bearer",
|
|
502
|
+
})
|
|
503
|
+
await provider.saveClientInformation({
|
|
504
|
+
client_id: "client",
|
|
505
|
+
client_secret: "secret",
|
|
506
|
+
redirect_uris: ["http://localhost/callback"],
|
|
507
|
+
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
508
|
+
client_secret_expires_at: futureTime,
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
await provider.invalidateCredentials("client")
|
|
512
|
+
|
|
513
|
+
const tokens = await provider.tokens()
|
|
514
|
+
assert.strictEqual(tokens?.access_token, "token")
|
|
515
|
+
assert.strictEqual(await provider.clientInformation(), undefined)
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
})
|