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.
Files changed (104) hide show
  1. package/ask/index.ts +12 -11
  2. package/ask/picker.ts +356 -76
  3. package/ask/prompt.ts +6 -2
  4. package/ask/tests/picker.test.ts +273 -82
  5. package/codemap.ts +276 -0
  6. package/hotreload.ts +239 -0
  7. package/init.ts +302 -0
  8. package/mcp/CHANGELOG.md +384 -0
  9. package/mcp/LICENSE +21 -0
  10. package/mcp/OAUTH.md +355 -0
  11. package/mcp/README.md +410 -0
  12. package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
  13. package/mcp/__tests__/cli.upstream-test.ts +97 -0
  14. package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
  15. package/mcp/__tests__/config.upstream-test.ts +303 -0
  16. package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
  17. package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
  18. package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
  19. package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
  20. package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
  21. package/mcp/__tests__/errors.upstream-test.ts +218 -0
  22. package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
  23. package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
  24. package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
  25. package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
  26. package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
  27. package/mcp/__tests__/logger.upstream-test.ts +175 -0
  28. package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
  29. package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
  30. package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
  31. package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
  32. package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
  33. package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
  34. package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
  35. package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
  36. package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
  37. package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
  38. package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
  39. package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
  40. package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
  41. package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
  42. package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
  43. package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
  44. package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
  45. package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
  46. package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
  47. package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
  48. package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
  49. package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
  50. package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
  51. package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
  52. package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
  53. package/mcp/agent-dir.ts +20 -0
  54. package/mcp/app-bridge.bundle.js +67 -0
  55. package/mcp/cli.js +184 -0
  56. package/mcp/commands.ts +422 -0
  57. package/mcp/config.ts +666 -0
  58. package/mcp/consent-manager.ts +64 -0
  59. package/mcp/direct-tools.ts +439 -0
  60. package/mcp/elicitation-handler.ts +347 -0
  61. package/mcp/errors.ts +219 -0
  62. package/mcp/glimpse-ui.ts +80 -0
  63. package/mcp/host-html-template.ts +427 -0
  64. package/mcp/index.ts +362 -0
  65. package/mcp/init.ts +362 -0
  66. package/mcp/lifecycle.ts +93 -0
  67. package/mcp/logger.ts +169 -0
  68. package/mcp/mcp-auth-flow.ts +559 -0
  69. package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
  70. package/mcp/mcp-auth.ts +302 -0
  71. package/mcp/mcp-auth.upstream-test.ts +373 -0
  72. package/mcp/mcp-callback-server.ts +372 -0
  73. package/mcp/mcp-callback-server.upstream-test.ts +416 -0
  74. package/mcp/mcp-oauth-provider.ts +369 -0
  75. package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
  76. package/mcp/mcp-panel.ts +829 -0
  77. package/mcp/mcp-setup-panel.ts +580 -0
  78. package/mcp/metadata-cache.ts +201 -0
  79. package/mcp/notify.ts +111 -0
  80. package/mcp/npx-resolver.ts +424 -0
  81. package/mcp/oauth-handler.ts +57 -0
  82. package/mcp/onboarding-state.ts +68 -0
  83. package/mcp/package.json +106 -0
  84. package/mcp/panel-keys.ts +37 -0
  85. package/mcp/proxy-modes.ts +949 -0
  86. package/mcp/resource-tools.ts +17 -0
  87. package/mcp/sampling-handler.ts +268 -0
  88. package/mcp/server-manager.ts +545 -0
  89. package/mcp/state.ts +41 -0
  90. package/mcp/tool-metadata.ts +216 -0
  91. package/mcp/tool-registrar.ts +46 -0
  92. package/mcp/tool-result-renderer.ts +161 -0
  93. package/mcp/types.ts +448 -0
  94. package/mcp/ui-resource-handler.ts +146 -0
  95. package/mcp/ui-server.ts +623 -0
  96. package/mcp/ui-session.ts +386 -0
  97. package/mcp/ui-stream-types.ts +89 -0
  98. package/mcp/utils.ts +129 -0
  99. package/mcp/vitest.config.ts +14 -0
  100. package/migrate.ts +258 -0
  101. package/notification.ts +218 -0
  102. package/notifications-log.ts +83 -0
  103. package/package.json +20 -3
  104. package/status.ts +140 -0
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Tests for mcp-callback-server.ts - OAuth callback server
3
+ */
4
+
5
+ import { describe, it, beforeEach, afterEach } from "node:test"
6
+ import assert from "node:assert"
7
+ import { createServer } from "node:http"
8
+ import {
9
+ ensureCallbackServer,
10
+ waitForCallback,
11
+ cancelPendingCallback,
12
+ stopCallbackServer,
13
+ isCallbackServerRunning,
14
+ getPendingAuthCount,
15
+ releaseCallbackServer,
16
+ } from "./mcp-callback-server.ts"
17
+ import { getConfiguredOAuthCallbackPort, getOAuthCallbackPath, getOAuthCallbackPort } from "./mcp-oauth-provider.ts"
18
+
19
+ async function getFreePort(): Promise<number> {
20
+ const probe = createServer()
21
+ await new Promise<void>((resolve, reject) => {
22
+ probe.once("error", reject)
23
+ probe.listen(0, "localhost", resolve)
24
+ })
25
+ const address = probe.address()
26
+ await new Promise<void>((resolve) => probe.close(() => resolve()))
27
+ if (!address || typeof address === "string") {
28
+ throw new Error("Failed to reserve a free test port")
29
+ }
30
+ return address.port
31
+ }
32
+
33
+ describe("mcp-callback-server", () => {
34
+ beforeEach(async () => {
35
+ // Stop any running server before each test
36
+ await stopCallbackServer().catch(() => {})
37
+ })
38
+
39
+ afterEach(async () => {
40
+ // Stop server after each test
41
+ await stopCallbackServer().catch(() => {})
42
+ })
43
+
44
+ describe("ensureCallbackServer", () => {
45
+ it("should start the callback server", async () => {
46
+ await ensureCallbackServer()
47
+ assert.strictEqual(isCallbackServerRunning(), true)
48
+ })
49
+
50
+ it("should be idempotent", async () => {
51
+ await ensureCallbackServer()
52
+ await ensureCallbackServer()
53
+ await ensureCallbackServer()
54
+ assert.strictEqual(isCallbackServerRunning(), true)
55
+ })
56
+
57
+ it("should reserve callback state atomically with the initial bind", async () => {
58
+ await ensureCallbackServer({ oauthState: "reserved-initial-state", reserveState: true })
59
+
60
+ await assert.rejects(
61
+ async () => await ensureCallbackServer({ callbackHost: "127.0.0.1" }),
62
+ /cannot be switched while authorizations are pending/
63
+ )
64
+
65
+ releaseCallbackServer("reserved-initial-state")
66
+ })
67
+
68
+ it("should not switch callback hosts while callback state is reserved", async () => {
69
+ await ensureCallbackServer({ oauthState: "reserved-host-state", reserveState: true })
70
+
71
+ await assert.rejects(
72
+ async () => await ensureCallbackServer({ callbackHost: "127.0.0.1" }),
73
+ /cannot be switched while authorizations are pending/
74
+ )
75
+
76
+ releaseCallbackServer("reserved-host-state")
77
+ })
78
+
79
+ it("should not switch callback paths while callback state is reserved", async () => {
80
+ await ensureCallbackServer({ callbackPath: "/first/callback", oauthState: "reserved-path-state", reserveState: true })
81
+
82
+ await assert.rejects(
83
+ async () => await ensureCallbackServer({ callbackPath: "/second/callback" }),
84
+ /cannot be switched while authorizations are pending/
85
+ )
86
+ assert.strictEqual(getOAuthCallbackPath(), "/first/callback")
87
+
88
+ releaseCallbackServer("reserved-path-state")
89
+ })
90
+
91
+ it("should release reserved callback state when strict binding fails", async () => {
92
+ const port = await getFreePort()
93
+ const blocker = createServer((_req, res) => {
94
+ res.writeHead(200)
95
+ res.end("blocked")
96
+ })
97
+
98
+ await new Promise<void>((resolve, reject) => {
99
+ blocker.once("error", reject)
100
+ blocker.listen(port, "localhost", resolve)
101
+ })
102
+
103
+ try {
104
+ await assert.rejects(
105
+ async () => await ensureCallbackServer({ strictPort: true, port, oauthState: "failed-bind-state", reserveState: true }),
106
+ /already in use/
107
+ )
108
+ } finally {
109
+ await new Promise<void>((resolve) => blocker.close(() => resolve()))
110
+ }
111
+
112
+ await ensureCallbackServer({ callbackPath: "/after-failure" })
113
+ await ensureCallbackServer({ callbackPath: "/after-failure-switch" })
114
+ assert.strictEqual(getOAuthCallbackPath(), "/after-failure-switch")
115
+ })
116
+
117
+ it("should bind an explicit strict host, port, and custom callback path", async () => {
118
+ const port = await getFreePort()
119
+
120
+ await ensureCallbackServer({ strictPort: true, port, callbackHost: "127.0.0.1", callbackPath: "/custom/callback" })
121
+
122
+ assert.strictEqual(getOAuthCallbackPort(), port)
123
+ assert.strictEqual(getOAuthCallbackPath(), "/custom/callback")
124
+ assert.strictEqual((await fetch(`http://127.0.0.1:${port}/callback?code=nope&state=custom-state`)).status, 404)
125
+
126
+ const callbackPromise = waitForCallback("custom-state")
127
+ const response = await fetch(`http://127.0.0.1:${port}/custom/callback?code=ok&state=custom-state`)
128
+ assert.strictEqual(response.status, 200)
129
+ assert.strictEqual(await callbackPromise, "ok")
130
+ })
131
+
132
+ it("should reject an occupied explicit strict port", async () => {
133
+ const port = await getFreePort()
134
+ const blocker = createServer((_req, res) => {
135
+ res.writeHead(200)
136
+ res.end("blocked")
137
+ })
138
+
139
+ await new Promise<void>((resolve, reject) => {
140
+ blocker.once("error", reject)
141
+ blocker.listen(port, "localhost", resolve)
142
+ })
143
+
144
+ try {
145
+ await assert.rejects(
146
+ async () => await ensureCallbackServer({ strictPort: true, port }),
147
+ /already in use/
148
+ )
149
+ } finally {
150
+ await new Promise<void>((resolve) => blocker.close(() => resolve()))
151
+ }
152
+ })
153
+
154
+ it("should use an OS-assigned port when the configured non-strict port is occupied", async () => {
155
+ const configuredPort = getConfiguredOAuthCallbackPort()
156
+ const blocker = createServer((_req, res) => {
157
+ res.writeHead(200)
158
+ res.end("blocked")
159
+ })
160
+
161
+ try {
162
+ await new Promise<void>((resolve, reject) => {
163
+ blocker.once("error", reject)
164
+ blocker.listen(configuredPort, "localhost", resolve)
165
+ })
166
+ } catch (error) {
167
+ if ((error as NodeJS.ErrnoException).code === "EADDRINUSE") return
168
+ throw error
169
+ }
170
+
171
+ try {
172
+ await ensureCallbackServer()
173
+ const callbackPort = getOAuthCallbackPort()
174
+ assert.notStrictEqual(callbackPort, configuredPort)
175
+
176
+ const state = "occupied-port-state"
177
+ const callbackPromise = waitForCallback(state)
178
+ const response = await fetch(`http://localhost:${callbackPort}/callback?code=ok&state=${state}`)
179
+ assert.strictEqual(response.status, 200)
180
+ assert.strictEqual(await callbackPromise, "ok")
181
+
182
+ await assert.rejects(
183
+ async () => await ensureCallbackServer({ strictPort: true }),
184
+ /already in use/
185
+ )
186
+ } finally {
187
+ await new Promise<void>((resolve) => blocker.close(() => resolve()))
188
+ }
189
+ })
190
+ })
191
+
192
+ describe("waitForCallback / callback handling", () => {
193
+ it("should resolve with code on successful callback", async () => {
194
+ await ensureCallbackServer()
195
+
196
+ const state = "test-state-123"
197
+ const expectedCode = "auth-code-abc"
198
+
199
+ // Start waiting for callback
200
+ const callbackPromise = waitForCallback(state)
201
+
202
+ // Simulate callback by making HTTP request
203
+ const callbackPort = getOAuthCallbackPort()
204
+ const response = await fetch(
205
+ `http://localhost:${callbackPort}/callback?code=${expectedCode}&state=${state}`
206
+ )
207
+
208
+ // Should get HTML success response
209
+ assert.strictEqual(response.status, 200)
210
+ const html = await response.text()
211
+ assert.ok(html.includes("Authorization Successful"))
212
+
213
+ // Callback promise should resolve
214
+ const code = await callbackPromise
215
+ assert.strictEqual(code, expectedCode)
216
+ })
217
+
218
+ it("should reject on error parameter", async () => {
219
+ await ensureCallbackServer()
220
+
221
+ const state = "test-state-error"
222
+ const errorMsg = "access_denied"
223
+
224
+ const callbackPromise = waitForCallback(state)
225
+
226
+ // Simulate error callback
227
+ const callbackPort = getOAuthCallbackPort()
228
+ const response = await fetch(
229
+ `http://localhost:${callbackPort}/callback?error=${errorMsg}&state=${state}`
230
+ )
231
+
232
+ assert.strictEqual(response.status, 200)
233
+ const html = await response.text()
234
+ assert.ok(html.includes("Authorization Failed"))
235
+
236
+ // Callback promise should reject
237
+ await assert.rejects(callbackPromise, /access_denied/)
238
+ })
239
+
240
+ it("should escape provider-controlled OAuth error details", async () => {
241
+ await ensureCallbackServer()
242
+
243
+ const state = "test-state-error-escaping"
244
+ const callbackPromise = waitForCallback(state)
245
+ const callbackPort = getOAuthCallbackPort()
246
+ const description = `<script>alert("x")</script>&reason=bad`
247
+ const response = await fetch(
248
+ `http://localhost:${callbackPort}/callback?error=access_denied&error_description=${encodeURIComponent(description)}&state=${state}`
249
+ )
250
+
251
+ assert.strictEqual(response.status, 200)
252
+ const html = await response.text()
253
+ assert.ok(!html.includes("<script>"))
254
+ assert.ok(html.includes("&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;&amp;reason=bad"))
255
+ await assert.rejects(callbackPromise, /<script>alert\("x"\)<\/script>&reason=bad/)
256
+ })
257
+
258
+ it("should not reflect OAuth error details for invalid state", async () => {
259
+ await ensureCallbackServer()
260
+
261
+ const callbackPort = getOAuthCallbackPort()
262
+ const response = await fetch(
263
+ `http://localhost:${callbackPort}/callback?error=access_denied&error_description=${encodeURIComponent("<script>bad()</script>")}&state=invalid-state`
264
+ )
265
+
266
+ assert.strictEqual(response.status, 400)
267
+ const html = await response.text()
268
+ assert.ok(html.includes("Invalid or expired state parameter"))
269
+ assert.ok(!html.includes("<script>"))
270
+ assert.ok(!html.includes("bad()"))
271
+ })
272
+
273
+ it("should return 400 for missing state", async () => {
274
+ await ensureCallbackServer()
275
+
276
+ const callbackPort = getOAuthCallbackPort()
277
+ const response = await fetch(
278
+ `http://localhost:${callbackPort}/callback?code=abc123`
279
+ )
280
+
281
+ assert.strictEqual(response.status, 400)
282
+ const html = await response.text()
283
+ assert.ok(html.includes("Missing required state parameter"))
284
+ })
285
+
286
+ it("should return 400 for invalid state", async () => {
287
+ await ensureCallbackServer()
288
+
289
+ // Register a different state
290
+ const pendingCallback = waitForCallback("valid-state")
291
+
292
+ const callbackPort = getOAuthCallbackPort()
293
+ const response = await fetch(
294
+ `http://localhost:${callbackPort}/callback?code=abc123&state=invalid-state`
295
+ )
296
+
297
+ assert.strictEqual(response.status, 400)
298
+ const html = await response.text()
299
+ assert.ok(html.includes("Invalid or expired state parameter"))
300
+
301
+ cancelPendingCallback("valid-state")
302
+ await assert.rejects(pendingCallback, /Authorization cancelled/)
303
+ })
304
+
305
+ it("should return 400 for missing code", async () => {
306
+ await ensureCallbackServer()
307
+
308
+ const state = "test-state-no-code"
309
+ const pendingCallback = waitForCallback(state)
310
+
311
+ const callbackPort = getOAuthCallbackPort()
312
+ const response = await fetch(
313
+ `http://localhost:${callbackPort}/callback?state=${state}`
314
+ )
315
+
316
+ assert.strictEqual(response.status, 400)
317
+ const html = await response.text()
318
+ assert.ok(html.includes("No authorization code provided"))
319
+
320
+ cancelPendingCallback(state)
321
+ await assert.rejects(pendingCallback, /Authorization cancelled/)
322
+ })
323
+
324
+ it("should not switch callback paths while callbacks are pending", async () => {
325
+ await ensureCallbackServer({ callbackPath: "/first/callback" })
326
+
327
+ const state = "pending-path-state"
328
+ const callbackPromise = waitForCallback(state)
329
+
330
+ await assert.rejects(
331
+ async () => await ensureCallbackServer({ callbackPath: "/second/callback" }),
332
+ /cannot be switched while authorizations are pending/
333
+ )
334
+ assert.strictEqual(getOAuthCallbackPath(), "/first/callback")
335
+
336
+ cancelPendingCallback(state)
337
+ await assert.rejects(callbackPromise, /Authorization cancelled/)
338
+ })
339
+
340
+ it("should return 404 for wrong path", async () => {
341
+ await ensureCallbackServer()
342
+
343
+ const callbackPort = getOAuthCallbackPort()
344
+ const response = await fetch(
345
+ `http://localhost:${callbackPort}/wrong/path`
346
+ )
347
+
348
+ assert.strictEqual(response.status, 404)
349
+ })
350
+ })
351
+
352
+ describe("cancelPendingCallback", () => {
353
+ it("should reject pending callback", async () => {
354
+ await ensureCallbackServer()
355
+
356
+ const state = "test-state-cancel"
357
+ const callbackPromise = waitForCallback(state)
358
+
359
+ cancelPendingCallback(state)
360
+
361
+ await assert.rejects(callbackPromise, /Authorization cancelled/)
362
+ })
363
+ })
364
+
365
+ describe("stopCallbackServer", () => {
366
+ it("should stop the server", async () => {
367
+ await ensureCallbackServer()
368
+ assert.strictEqual(isCallbackServerRunning(), true)
369
+
370
+ await stopCallbackServer()
371
+ assert.strictEqual(isCallbackServerRunning(), false)
372
+ })
373
+
374
+ it("should reject all pending callbacks", async () => {
375
+ await ensureCallbackServer()
376
+
377
+ const state1 = "state-1"
378
+ const state2 = "state-2"
379
+
380
+ const promise1 = waitForCallback(state1)
381
+ const promise2 = waitForCallback(state2)
382
+
383
+ await stopCallbackServer()
384
+
385
+ await assert.rejects(promise1, /OAuth callback server stopped/)
386
+ await assert.rejects(promise2, /OAuth callback server stopped/)
387
+ })
388
+ })
389
+
390
+ describe("getPendingAuthCount", () => {
391
+ it("should return 0 when no pending auths", async () => {
392
+ await ensureCallbackServer()
393
+ assert.strictEqual(getPendingAuthCount(), 0)
394
+ })
395
+
396
+ it("should return count of pending auths", async () => {
397
+ await ensureCallbackServer()
398
+
399
+ const promise1 = waitForCallback("state-1")
400
+ assert.strictEqual(getPendingAuthCount(), 1)
401
+
402
+ const promise2 = waitForCallback("state-2")
403
+ assert.strictEqual(getPendingAuthCount(), 2)
404
+
405
+ const promise3 = waitForCallback("state-3")
406
+ assert.strictEqual(getPendingAuthCount(), 3)
407
+
408
+ cancelPendingCallback("state-1")
409
+ cancelPendingCallback("state-2")
410
+ cancelPendingCallback("state-3")
411
+ await assert.rejects(promise1, /Authorization cancelled/)
412
+ await assert.rejects(promise2, /Authorization cancelled/)
413
+ await assert.rejects(promise3, /Authorization cancelled/)
414
+ })
415
+ })
416
+ })