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,372 @@
1
+ /**
2
+ * MCP OAuth Callback Server
3
+ *
4
+ * HTTP server that handles OAuth callbacks from the authorization server.
5
+ * Uses Node.js http module for compatibility.
6
+ */
7
+
8
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http"
9
+ import {
10
+ DEFAULT_OAUTH_CALLBACK_PATH,
11
+ getConfiguredOAuthCallbackPort,
12
+ getOAuthCallbackPath,
13
+ getOAuthCallbackPort,
14
+ setOAuthCallbackPath,
15
+ setOAuthCallbackPort,
16
+ } from "./mcp-oauth-provider.ts"
17
+
18
+ // HTML templates for callback responses
19
+ const HTML_SUCCESS = `<!DOCTYPE html>
20
+ <html>
21
+ <head>
22
+ <title>Pi - Authorization Successful</title>
23
+ <style>
24
+ body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
25
+ .container { text-align: center; padding: 2rem; }
26
+ h1 { color: #4ade80; margin-bottom: 1rem; }
27
+ p { color: #aaa; }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <div class="container">
32
+ <h1>Authorization Successful</h1>
33
+ <p>You can close this window and return to Pi.</p>
34
+ </div>
35
+ <script>setTimeout(() => window.close(), 2000);</script>
36
+ </body>
37
+ </html>`
38
+
39
+ function escapeHtml(value: string): string {
40
+ return value
41
+ .replace(/&/g, "&amp;")
42
+ .replace(/</g, "&lt;")
43
+ .replace(/>/g, "&gt;")
44
+ .replace(/"/g, "&quot;")
45
+ .replace(/'/g, "&#39;")
46
+ }
47
+
48
+ const HTML_ERROR = (error: string) => `<!DOCTYPE html>
49
+ <html>
50
+ <head>
51
+ <title>Pi - Authorization Failed</title>
52
+ <style>
53
+ body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
54
+ .container { text-align: center; padding: 2rem; }
55
+ h1 { color: #f87171; margin-bottom: 1rem; }
56
+ p { color: #aaa; }
57
+ .error { color: #fca5a5; font-family: monospace; margin-top: 1rem; padding: 1rem; background: rgba(248,113,113,0.1); border-radius: 0.5rem; }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <div class="container">
62
+ <h1>Authorization Failed</h1>
63
+ <p>An error occurred during authorization.</p>
64
+ <div class="error">${escapeHtml(error)}</div>
65
+ </div>
66
+ </body>
67
+ </html>`
68
+
69
+ /** Pending authorization request */
70
+ interface PendingAuth {
71
+ resolve: (code: string) => void
72
+ reject: (error: Error) => void
73
+ timeout: ReturnType<typeof setTimeout>
74
+ }
75
+
76
+ /** Server singleton state */
77
+ let server: Server | undefined
78
+ let bindingPromise: Promise<void> | undefined
79
+ const pendingAuths = new Map<string, PendingAuth>()
80
+ const reservedAuthStates = new Set<string>()
81
+
82
+ /** Timeout for callback completion (5 minutes) */
83
+ const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000
84
+
85
+ interface EnsureCallbackServerOptions {
86
+ strictPort?: boolean
87
+ port?: number
88
+ callbackHost?: string
89
+ callbackPath?: string
90
+ oauthState?: string
91
+ reserveState?: boolean
92
+ }
93
+
94
+ const DEFAULT_OAUTH_CALLBACK_HOST = "localhost"
95
+ let callbackServerHost = DEFAULT_OAUTH_CALLBACK_HOST
96
+
97
+ /**
98
+ * Handle incoming HTTP requests to the callback server.
99
+ */
100
+ function handleRequest(req: IncomingMessage, res: ServerResponse): void {
101
+ const url = new URL(req.url || "/", `http://${req.headers.host}`)
102
+
103
+ // Only handle the callback path
104
+ if (url.pathname !== getOAuthCallbackPath()) {
105
+ res.writeHead(404, { "Content-Type": "text/plain" })
106
+ res.end("Not found")
107
+ return
108
+ }
109
+
110
+ const code = url.searchParams.get("code")
111
+ const state = url.searchParams.get("state")
112
+ const error = url.searchParams.get("error")
113
+ const errorDescription = url.searchParams.get("error_description")
114
+
115
+ // Enforce state parameter presence for CSRF protection
116
+ if (!state) {
117
+ const errorMsg = "Missing required state parameter - potential CSRF attack"
118
+ res.writeHead(400, { "Content-Type": "text/html" })
119
+ res.end(HTML_ERROR(errorMsg))
120
+ return
121
+ }
122
+
123
+ const pending = pendingAuths.get(state)
124
+ const isReserved = reservedAuthStates.has(state)
125
+
126
+ // Handle OAuth errors only for a state that belongs to an active flow.
127
+ if (error) {
128
+ if (!pending && !isReserved) {
129
+ const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
130
+ res.writeHead(400, { "Content-Type": "text/html" })
131
+ res.end(HTML_ERROR(errorMsg))
132
+ return
133
+ }
134
+
135
+ const errorMsg = errorDescription || error
136
+ // Send HTTP response first before rejecting promise
137
+ res.writeHead(200, { "Content-Type": "text/html" })
138
+ res.end(HTML_ERROR(errorMsg))
139
+ reservedAuthStates.delete(state)
140
+ // Reject promise after response is sent (defer to allow test to attach handler)
141
+ if (pending) {
142
+ clearTimeout(pending.timeout)
143
+ pendingAuths.delete(state)
144
+ setTimeout(() => pending.reject(new Error(errorMsg)), 0)
145
+ }
146
+ return
147
+ }
148
+
149
+ // Validate state parameter
150
+ if (!pending) {
151
+ const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
152
+ res.writeHead(400, { "Content-Type": "text/html" })
153
+ res.end(HTML_ERROR(errorMsg))
154
+ return
155
+ }
156
+
157
+ // Require authorization code
158
+ if (!code) {
159
+ res.writeHead(400, { "Content-Type": "text/html" })
160
+ res.end(HTML_ERROR("No authorization code provided"))
161
+ return
162
+ }
163
+
164
+ // Clear timeout and resolve the pending promise
165
+ clearTimeout(pending.timeout)
166
+ pendingAuths.delete(state)
167
+ pending.resolve(code)
168
+
169
+ res.writeHead(200, { "Content-Type": "text/html" })
170
+ res.end(HTML_SUCCESS)
171
+ }
172
+
173
+ /**
174
+ * Ensure the callback server is running.
175
+ * If strictPort is true, requires binding on the configured callback port.
176
+ * If strictPort is false, asks the OS for an available local port.
177
+ */
178
+ export async function ensureCallbackServer(options: EnsureCallbackServerOptions = {}): Promise<void> {
179
+ while (bindingPromise) {
180
+ await bindingPromise
181
+ }
182
+
183
+ const operation = ensureCallbackServerLocked(options)
184
+ bindingPromise = operation
185
+ try {
186
+ await operation
187
+ } finally {
188
+ if (bindingPromise === operation) {
189
+ bindingPromise = undefined
190
+ }
191
+ }
192
+ }
193
+
194
+ async function ensureCallbackServerLocked(options: EnsureCallbackServerOptions = {}): Promise<void> {
195
+ const requiredPort = options.port ?? getConfiguredOAuthCallbackPort()
196
+ const strictPort = options.strictPort === true
197
+ const requestedHost = options.callbackHost ?? DEFAULT_OAUTH_CALLBACK_HOST
198
+ const rawRequestedPath = options.callbackPath ?? DEFAULT_OAUTH_CALLBACK_PATH
199
+ const requestedPath = rawRequestedPath.startsWith("/") ? rawRequestedPath : `/${rawRequestedPath}`
200
+ if (options.reserveState && !options.oauthState) {
201
+ throw new Error("OAuth callback reservation requires an oauthState")
202
+ }
203
+ let reservedState: string | undefined
204
+
205
+ const previousServer = server
206
+ const needsStrictRebind = Boolean(previousServer && strictPort && getOAuthCallbackPort() !== requiredPort)
207
+ const needsHostSwitch = Boolean(previousServer && callbackServerHost !== requestedHost)
208
+ const needsPathSwitch = Boolean(previousServer && getOAuthCallbackPath() !== requestedPath)
209
+
210
+ if (previousServer) {
211
+ if (!needsStrictRebind && !needsHostSwitch) {
212
+ if (needsPathSwitch) {
213
+ if (pendingAuths.size > 0 || reservedAuthStates.size > 0) {
214
+ throw new Error(
215
+ `OAuth callback server is using path ${getOAuthCallbackPath()}, but callback path ${requestedPath} is required and cannot be switched while authorizations are pending`
216
+ )
217
+ }
218
+ setOAuthCallbackPath(requestedPath)
219
+ }
220
+ if (options.reserveState && options.oauthState) {
221
+ reservedAuthStates.add(options.oauthState)
222
+ reservedState = options.oauthState
223
+ }
224
+ return
225
+ }
226
+
227
+ if (pendingAuths.size > 0 || reservedAuthStates.size > 0) {
228
+ throw new Error(
229
+ `OAuth callback server is running on ${callbackServerHost}:${getOAuthCallbackPort()}, but strict callback endpoint ${requestedHost}:${requiredPort} is required and cannot be switched while authorizations are pending`
230
+ )
231
+ }
232
+ }
233
+
234
+ const candidateServer = createServer(handleRequest)
235
+ const listenPort = strictPort ? requiredPort : 0
236
+
237
+ try {
238
+ await new Promise<void>((resolve, reject) => {
239
+ candidateServer.once("error", (err) => {
240
+ reject(err)
241
+ })
242
+
243
+ candidateServer.listen(listenPort, requestedHost, () => {
244
+ resolve()
245
+ })
246
+ })
247
+
248
+ if (strictPort) {
249
+ setOAuthCallbackPort(requiredPort)
250
+ } else {
251
+ const address = candidateServer.address()
252
+ if (!address || typeof address === "string" || typeof address.port !== "number") {
253
+ throw new Error("OAuth callback server did not report an assigned port")
254
+ }
255
+ setOAuthCallbackPort(address.port)
256
+ }
257
+
258
+ if (previousServer && (needsStrictRebind || needsHostSwitch)) {
259
+ await new Promise<void>((resolve) => {
260
+ previousServer.close(() => resolve())
261
+ })
262
+ }
263
+
264
+ callbackServerHost = requestedHost
265
+ setOAuthCallbackPath(requestedPath)
266
+ server = candidateServer
267
+ if (options.reserveState && options.oauthState) {
268
+ reservedAuthStates.add(options.oauthState)
269
+ reservedState = options.oauthState
270
+ }
271
+ server.unref()
272
+ } catch (error) {
273
+ if (reservedState) {
274
+ reservedAuthStates.delete(reservedState)
275
+ }
276
+ const nodeError = error as NodeJS.ErrnoException
277
+ await new Promise<void>((resolve) => {
278
+ candidateServer.close(() => resolve())
279
+ })
280
+
281
+ if (strictPort && nodeError.code === "EADDRINUSE") {
282
+ throw new Error(
283
+ `OAuth callback port ${requiredPort} is already in use. Pre-registered OAuth clients require an exact redirect URI; set MCP_OAUTH_CALLBACK_PORT to your registered port or free port ${requiredPort}`,
284
+ { cause: error }
285
+ )
286
+ }
287
+
288
+ throw error
289
+ }
290
+ }
291
+
292
+ export function reserveCallbackServer(oauthState: string): void {
293
+ reservedAuthStates.add(oauthState)
294
+ }
295
+
296
+ export function releaseCallbackServer(oauthState: string): void {
297
+ reservedAuthStates.delete(oauthState)
298
+ }
299
+
300
+ /**
301
+ * Wait for a callback with the given OAuth state.
302
+ * Returns a promise that resolves with the authorization code.
303
+ */
304
+ export function waitForCallback(oauthState: string): Promise<string> {
305
+ reservedAuthStates.delete(oauthState)
306
+ return new Promise((resolve, reject) => {
307
+ const timeout = setTimeout(() => {
308
+ if (pendingAuths.has(oauthState)) {
309
+ pendingAuths.delete(oauthState)
310
+ reject(new Error("OAuth callback timeout - authorization took too long"))
311
+ }
312
+ }, CALLBACK_TIMEOUT_MS)
313
+
314
+ pendingAuths.set(oauthState, { resolve, reject, timeout })
315
+ })
316
+ }
317
+
318
+ /**
319
+ * Cancel a pending authorization by state.
320
+ */
321
+ export function cancelPendingCallback(oauthState: string): void {
322
+ reservedAuthStates.delete(oauthState)
323
+ const pending = pendingAuths.get(oauthState)
324
+ if (pending) {
325
+ clearTimeout(pending.timeout)
326
+ pendingAuths.delete(oauthState)
327
+ pending.reject(new Error("Authorization cancelled"))
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Stop the callback server and reject all pending authorizations.
333
+ */
334
+ export async function stopCallbackServer(): Promise<void> {
335
+ if (server) {
336
+ await new Promise<void>((resolve) => {
337
+ server!.close(() => {
338
+ resolve()
339
+ })
340
+ })
341
+ server = undefined
342
+ }
343
+
344
+ setOAuthCallbackPort(getConfiguredOAuthCallbackPort())
345
+ callbackServerHost = DEFAULT_OAUTH_CALLBACK_HOST
346
+ setOAuthCallbackPath(DEFAULT_OAUTH_CALLBACK_PATH)
347
+
348
+ // Reject all pending auths (defer to allow any pending operations to complete)
349
+ const pendingList = Array.from(pendingAuths.entries())
350
+ pendingAuths.clear()
351
+ reservedAuthStates.clear()
352
+ setTimeout(() => {
353
+ for (const [, pending] of pendingList) {
354
+ clearTimeout(pending.timeout)
355
+ pending.reject(new Error("OAuth callback server stopped"))
356
+ }
357
+ }, 0)
358
+ }
359
+
360
+ /**
361
+ * Check if the callback server is running.
362
+ */
363
+ export function isCallbackServerRunning(): boolean {
364
+ return server !== undefined
365
+ }
366
+
367
+ /**
368
+ * Get the number of pending authorizations.
369
+ */
370
+ export function getPendingAuthCount(): number {
371
+ return pendingAuths.size
372
+ }