pi-mcp-adapter 2.8.0 → 2.10.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/CHANGELOG.md +20 -0
- package/README.md +30 -1
- package/commands.ts +6 -5
- package/direct-tools.ts +18 -4
- package/elicitation-handler.ts +347 -0
- package/index.ts +27 -2
- package/init.ts +11 -0
- package/mcp-auth-flow.ts +127 -30
- package/mcp-oauth-provider.ts +48 -1
- package/mcp-panel.ts +12 -9
- package/mcp-setup-panel.ts +13 -9
- package/package.json +4 -2
- package/panel-keys.ts +37 -0
- package/proxy-modes.ts +119 -4
- package/server-manager.ts +109 -36
- package/tool-metadata.ts +98 -34
- package/types.ts +1 -0
- package/ui-resource-handler.ts +2 -1
- package/ui-session.ts +2 -1
package/mcp-auth-flow.ts
CHANGED
|
@@ -38,10 +38,15 @@ export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
|
|
|
38
38
|
|
|
39
39
|
// Track pending transports for auth completion
|
|
40
40
|
const pendingTransports = new Map<string, StreamableHTTPClientTransport>()
|
|
41
|
+
const pendingAuthStates = new Map<string, string>()
|
|
42
|
+
const pendingAuthCleanupTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
41
43
|
|
|
42
44
|
// Deduplicate concurrent authenticate() calls per server.
|
|
43
45
|
const pendingAuthentications = new Map<string, Promise<AuthStatus>>()
|
|
44
46
|
|
|
47
|
+
/** Timeout for manual auth completion (5 minutes) */
|
|
48
|
+
const MANUAL_AUTH_TIMEOUT_MS = 5 * 60 * 1000
|
|
49
|
+
|
|
45
50
|
/**
|
|
46
51
|
* Generate a cryptographically secure random state parameter.
|
|
47
52
|
*/
|
|
@@ -214,18 +219,124 @@ export async function startAuth(
|
|
|
214
219
|
if (!capturedUrl) {
|
|
215
220
|
throw new UnauthorizedError("OAuth authorization URL was not provided")
|
|
216
221
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
new StreamableHTTPClientTransport(new URL(serverUrl), { authProvider }),
|
|
220
|
-
)
|
|
222
|
+
const pendingTransport = new StreamableHTTPClientTransport(new URL(serverUrl), { authProvider })
|
|
223
|
+
await setPendingTransport(serverName, pendingTransport, oauthState)
|
|
221
224
|
return { authorizationUrl: capturedUrl.toString() }
|
|
222
225
|
} catch (error) {
|
|
223
|
-
|
|
224
|
-
await clearOAuthState(serverName)
|
|
226
|
+
await clearPendingAuth(serverName, oauthState)
|
|
225
227
|
throw error
|
|
226
228
|
}
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
async function setPendingTransport(
|
|
232
|
+
serverName: string,
|
|
233
|
+
transport: StreamableHTTPClientTransport,
|
|
234
|
+
oauthState: string,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
await clearPendingAuth(serverName)
|
|
237
|
+
pendingTransports.set(serverName, transport)
|
|
238
|
+
pendingAuthStates.set(serverName, oauthState)
|
|
239
|
+
const cleanupTimer = setTimeout(() => {
|
|
240
|
+
void clearPendingAuth(serverName, oauthState)
|
|
241
|
+
}, MANUAL_AUTH_TIMEOUT_MS)
|
|
242
|
+
cleanupTimer.unref?.()
|
|
243
|
+
pendingAuthCleanupTimers.set(serverName, cleanupTimer)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function clearPendingAuth(serverName: string, oauthState?: string): Promise<void> {
|
|
247
|
+
const pendingState = pendingAuthStates.get(serverName)
|
|
248
|
+
if (oauthState && pendingState && pendingState !== oauthState) return
|
|
249
|
+
|
|
250
|
+
const timer = pendingAuthCleanupTimers.get(serverName)
|
|
251
|
+
if (timer) {
|
|
252
|
+
clearTimeout(timer)
|
|
253
|
+
pendingAuthCleanupTimers.delete(serverName)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const transport = pendingTransports.get(serverName)
|
|
257
|
+
pendingTransports.delete(serverName)
|
|
258
|
+
pendingAuthStates.delete(serverName)
|
|
259
|
+
const stateToRelease = pendingState ?? oauthState
|
|
260
|
+
if (stateToRelease) {
|
|
261
|
+
releaseCallbackServer(stateToRelease)
|
|
262
|
+
const storedState = await getOAuthState(serverName)
|
|
263
|
+
if (storedState === stateToRelease) {
|
|
264
|
+
await clearOAuthState(serverName)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (transport) {
|
|
268
|
+
await transport.close().catch(() => {})
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getSearchParamsFromInput(input: string): URLSearchParams | undefined {
|
|
273
|
+
try {
|
|
274
|
+
const url = new URL(input)
|
|
275
|
+
const params = new URLSearchParams(url.search)
|
|
276
|
+
if (url.hash) {
|
|
277
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash
|
|
278
|
+
const hashParams = new URLSearchParams(hash)
|
|
279
|
+
for (const [key, value] of hashParams) {
|
|
280
|
+
if (!params.has(key)) params.set(key, value)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return params
|
|
284
|
+
} catch {
|
|
285
|
+
const query = input.includes("?") ? input.slice(input.indexOf("?") + 1) : input
|
|
286
|
+
const params = new URLSearchParams(query.startsWith("#") ? query.slice(1) : query)
|
|
287
|
+
return params.has("code") || params.has("state") || params.has("error") ? params : undefined
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Extract an OAuth authorization code from either a raw code, a query string,
|
|
293
|
+
* or the full localhost redirect URL copied from the browser address bar.
|
|
294
|
+
*/
|
|
295
|
+
export function parseAuthorizationCodeInput(input: string, expectedState?: string): string {
|
|
296
|
+
const trimmed = input.trim()
|
|
297
|
+
if (!trimmed) {
|
|
298
|
+
throw new Error("Authorization code or redirect URL is required")
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const params = getSearchParamsFromInput(trimmed)
|
|
302
|
+
if (params) {
|
|
303
|
+
const error = params.get("error")
|
|
304
|
+
if (error) {
|
|
305
|
+
const description = params.get("error_description")
|
|
306
|
+
throw new Error(description ? `${error}: ${description}` : error)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const state = params.get("state")
|
|
310
|
+
if (expectedState && !state) {
|
|
311
|
+
throw new Error("OAuth state missing from redirect URL")
|
|
312
|
+
}
|
|
313
|
+
if (expectedState && state !== expectedState) {
|
|
314
|
+
throw new Error("OAuth state mismatch - potential CSRF attack")
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const code = params.get("code")
|
|
318
|
+
if (code) return code
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (/^[A-Za-z0-9._~+/=-]+$/.test(trimmed)) {
|
|
322
|
+
return trimmed
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
throw new Error("Could not find an OAuth authorization code in the provided input")
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Complete OAuth authentication from manual user input.
|
|
330
|
+
*/
|
|
331
|
+
export async function completeAuthFromInput(
|
|
332
|
+
serverName: string,
|
|
333
|
+
input: string,
|
|
334
|
+
): Promise<AuthStatus> {
|
|
335
|
+
const oauthState = await getOAuthState(serverName)
|
|
336
|
+
const code = parseAuthorizationCodeInput(input, oauthState)
|
|
337
|
+
return completeAuth(serverName, code)
|
|
338
|
+
}
|
|
339
|
+
|
|
229
340
|
/**
|
|
230
341
|
* Complete OAuth authentication with the authorization code.
|
|
231
342
|
*/
|
|
@@ -245,12 +356,7 @@ export async function completeAuth(
|
|
|
245
356
|
await transport.finishAuth(authorizationCode)
|
|
246
357
|
return "authenticated"
|
|
247
358
|
} finally {
|
|
248
|
-
|
|
249
|
-
releaseCallbackServer(oauthState)
|
|
250
|
-
}
|
|
251
|
-
await clearOAuthState(serverName)
|
|
252
|
-
pendingTransports.delete(serverName)
|
|
253
|
-
await transport.close().catch(() => {})
|
|
359
|
+
await clearPendingAuth(serverName, oauthState)
|
|
254
360
|
}
|
|
255
361
|
}
|
|
256
362
|
|
|
@@ -291,16 +397,13 @@ export async function authenticate(
|
|
|
291
397
|
const callbackPromise = waitForCallback(oauthState)
|
|
292
398
|
|
|
293
399
|
try {
|
|
294
|
-
// Open browser
|
|
295
|
-
|
|
400
|
+
// Open browser. Always print the URL first so remote/headless users can copy it
|
|
401
|
+
// even when the OS browser handoff is unavailable or invisible.
|
|
402
|
+
console.log(`MCP Auth: Open this URL to authenticate ${serverName}:\n${authorizationUrl}`)
|
|
296
403
|
try {
|
|
297
404
|
await open(authorizationUrl)
|
|
298
405
|
} catch (error) {
|
|
299
|
-
console.warn(`MCP Auth: Failed to open browser for ${serverName}`, { error })
|
|
300
|
-
throw new Error(
|
|
301
|
-
`Could not open browser. Please open this URL manually: ${authorizationUrl}`,
|
|
302
|
-
{ cause: error },
|
|
303
|
-
)
|
|
406
|
+
console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error })
|
|
304
407
|
}
|
|
305
408
|
|
|
306
409
|
// Wait for callback
|
|
@@ -318,12 +421,7 @@ export async function authenticate(
|
|
|
318
421
|
return await completeAuth(serverName, code)
|
|
319
422
|
} catch (error) {
|
|
320
423
|
cancelPendingCallback(oauthState)
|
|
321
|
-
await
|
|
322
|
-
const pendingTransport = pendingTransports.get(serverName)
|
|
323
|
-
if (pendingTransport) {
|
|
324
|
-
pendingTransports.delete(serverName)
|
|
325
|
-
await pendingTransport.close().catch(() => {})
|
|
326
|
-
}
|
|
424
|
+
await clearPendingAuth(serverName, oauthState)
|
|
327
425
|
throw error
|
|
328
426
|
}
|
|
329
427
|
})()
|
|
@@ -418,11 +516,7 @@ export async function removeAuth(serverName: string): Promise<void> {
|
|
|
418
516
|
if (oauthState) {
|
|
419
517
|
cancelPendingCallback(oauthState)
|
|
420
518
|
}
|
|
421
|
-
|
|
422
|
-
if (pendingTransport) {
|
|
423
|
-
pendingTransports.delete(serverName)
|
|
424
|
-
await pendingTransport.close().catch(() => {})
|
|
425
|
-
}
|
|
519
|
+
await clearPendingAuth(serverName, oauthState)
|
|
426
520
|
clearAllCredentials(serverName)
|
|
427
521
|
await clearOAuthState(serverName)
|
|
428
522
|
console.log(`MCP Auth: Removed credentials for ${serverName}`)
|
|
@@ -458,5 +552,8 @@ export async function initializeOAuth(): Promise<void> {}
|
|
|
458
552
|
* Stops the callback server and cancels pending auths.
|
|
459
553
|
*/
|
|
460
554
|
export async function shutdownOAuth(): Promise<void> {
|
|
555
|
+
for (const serverName of Array.from(pendingTransports.keys())) {
|
|
556
|
+
await clearPendingAuth(serverName)
|
|
557
|
+
}
|
|
461
558
|
await stopCallbackServer()
|
|
462
559
|
}
|
package/mcp-oauth-provider.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Handles OAuth client registration, token storage, and authorization redirection.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
|
8
|
+
import type { AddClientAuthentication, OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
|
9
9
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
|
10
10
|
import type {
|
|
11
11
|
OAuthClientMetadata,
|
|
@@ -135,6 +135,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
135
135
|
grant_types: ["authorization_code", "refresh_token"],
|
|
136
136
|
response_types: ["code"],
|
|
137
137
|
token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none",
|
|
138
|
+
...(this.config.scope !== undefined ? { scope: this.config.scope } : {}),
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -305,6 +306,52 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Adds configured authorization-code scope without replacing the SDK's
|
|
311
|
+
* default token endpoint authentication behavior.
|
|
312
|
+
*/
|
|
313
|
+
addClientAuthentication: AddClientAuthentication = async (headers, params, _url, metadata) => {
|
|
314
|
+
if (params.get("grant_type") === "authorization_code" && !params.has("scope") && this.config.scope) {
|
|
315
|
+
params.set("scope", this.config.scope)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const clientInfo = await this.clientInformation()
|
|
319
|
+
if (!clientInfo) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const supportedMethods = metadata?.token_endpoint_auth_methods_supported ?? []
|
|
324
|
+
const hasClientSecret = clientInfo.client_secret !== undefined
|
|
325
|
+
let authMethod: "client_secret_basic" | "client_secret_post" | "none"
|
|
326
|
+
|
|
327
|
+
if (supportedMethods.length === 0) {
|
|
328
|
+
authMethod = hasClientSecret ? "client_secret_post" : "none"
|
|
329
|
+
} else if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
|
|
330
|
+
authMethod = "client_secret_basic"
|
|
331
|
+
} else if (hasClientSecret && supportedMethods.includes("client_secret_post")) {
|
|
332
|
+
authMethod = "client_secret_post"
|
|
333
|
+
} else if (supportedMethods.includes("none")) {
|
|
334
|
+
authMethod = "none"
|
|
335
|
+
} else {
|
|
336
|
+
authMethod = hasClientSecret ? "client_secret_post" : "none"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (authMethod === "client_secret_basic") {
|
|
340
|
+
if (!clientInfo.client_secret) {
|
|
341
|
+
throw new Error("client_secret_basic authentication requires a client_secret")
|
|
342
|
+
}
|
|
343
|
+
headers.set("Authorization", `Basic ${Buffer.from(`${clientInfo.client_id}:${clientInfo.client_secret}`).toString("base64")}`)
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!params.has("client_id")) {
|
|
348
|
+
params.set("client_id", clientInfo.client_id)
|
|
349
|
+
}
|
|
350
|
+
if (authMethod === "client_secret_post" && clientInfo.client_secret && !params.has("client_secret")) {
|
|
351
|
+
params.set("client_secret", clientInfo.client_secret)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
308
355
|
prepareTokenRequest(scope?: string): URLSearchParams | undefined {
|
|
309
356
|
if (!this.usesClientCredentials) {
|
|
310
357
|
return undefined
|
package/mcp-panel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { createPanelKeys, type PanelKeybindings, type PanelKeys } from "./panel-keys.ts";
|
|
2
3
|
import { isToolExcluded } from "./types.ts";
|
|
3
4
|
import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance } from "./types.ts";
|
|
4
5
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
@@ -126,6 +127,7 @@ class McpPanel {
|
|
|
126
127
|
private tui: { requestRender(): void };
|
|
127
128
|
private t = DEFAULT_THEME;
|
|
128
129
|
private authOnly: boolean;
|
|
130
|
+
private keys: PanelKeys;
|
|
129
131
|
|
|
130
132
|
private static readonly MAX_VISIBLE = 12;
|
|
131
133
|
private static readonly INACTIVITY_MS = 60_000;
|
|
@@ -137,11 +139,12 @@ class McpPanel {
|
|
|
137
139
|
private callbacks: McpPanelCallbacks,
|
|
138
140
|
tui: { requestRender(): void },
|
|
139
141
|
private done: (result: McpPanelResult) => void,
|
|
140
|
-
options: { noticeLines?: string[]; authOnly?: boolean } = {},
|
|
142
|
+
options: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings } = {},
|
|
141
143
|
) {
|
|
142
144
|
this.tui = tui;
|
|
143
145
|
this.noticeLines = options.noticeLines ?? [];
|
|
144
146
|
this.authOnly = options.authOnly === true;
|
|
147
|
+
this.keys = createPanelKeys(options.keybindings);
|
|
145
148
|
this.prefix = config.settings?.toolPrefix ?? "server";
|
|
146
149
|
|
|
147
150
|
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
@@ -318,7 +321,7 @@ class McpPanel {
|
|
|
318
321
|
|
|
319
322
|
// Modal description search mode
|
|
320
323
|
if (this.descSearchActive) {
|
|
321
|
-
if (matchesKey(data, "escape") ||
|
|
324
|
+
if (matchesKey(data, "escape") || this.keys.selectConfirm(data)) {
|
|
322
325
|
this.descSearchActive = false;
|
|
323
326
|
this.descQuery = "";
|
|
324
327
|
this.rebuildVisibleItems();
|
|
@@ -333,8 +336,8 @@ class McpPanel {
|
|
|
333
336
|
}
|
|
334
337
|
return;
|
|
335
338
|
}
|
|
336
|
-
if (
|
|
337
|
-
if (
|
|
339
|
+
if (this.keys.selectUp(data)) { this.moveCursor(-1); return; }
|
|
340
|
+
if (this.keys.selectDown(data)) { this.moveCursor(1); return; }
|
|
338
341
|
if (matchesKey(data, "space")) {
|
|
339
342
|
// Toggle even while in desc search
|
|
340
343
|
const item = this.visibleItems[this.cursorIndex];
|
|
@@ -367,8 +370,8 @@ class McpPanel {
|
|
|
367
370
|
return;
|
|
368
371
|
}
|
|
369
372
|
|
|
370
|
-
if (
|
|
371
|
-
if (
|
|
373
|
+
if (this.keys.selectUp(data)) { this.moveCursor(-1); return; }
|
|
374
|
+
if (this.keys.selectDown(data)) { this.moveCursor(1); return; }
|
|
372
375
|
|
|
373
376
|
if (matchesKey(data, "space")) {
|
|
374
377
|
const item = this.visibleItems[this.cursorIndex];
|
|
@@ -376,7 +379,7 @@ class McpPanel {
|
|
|
376
379
|
return;
|
|
377
380
|
}
|
|
378
381
|
|
|
379
|
-
if (
|
|
382
|
+
if (this.keys.selectConfirm(data)) {
|
|
380
383
|
const item = this.visibleItems[this.cursorIndex];
|
|
381
384
|
if (!item) return;
|
|
382
385
|
const server = this.servers[item.serverIndex];
|
|
@@ -518,7 +521,7 @@ class McpPanel {
|
|
|
518
521
|
this.confirmingDiscard = false;
|
|
519
522
|
return;
|
|
520
523
|
}
|
|
521
|
-
if (
|
|
524
|
+
if (this.keys.selectConfirm(data)) {
|
|
522
525
|
if (this.discardSelected === 0) {
|
|
523
526
|
this.cleanup();
|
|
524
527
|
this.done({ cancelled: true, changes: new Map() });
|
|
@@ -820,7 +823,7 @@ export function createMcpPanel(
|
|
|
820
823
|
callbacks: McpPanelCallbacks,
|
|
821
824
|
tui: { requestRender(): void },
|
|
822
825
|
done: (result: McpPanelResult) => void,
|
|
823
|
-
options?: { noticeLines?: string[]; authOnly?: boolean },
|
|
826
|
+
options?: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings },
|
|
824
827
|
): McpPanel & { dispose(): void } {
|
|
825
828
|
return new McpPanel(config, cache, provenance, callbacks, tui, done, options ?? {});
|
|
826
829
|
}
|
package/mcp-setup-panel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { createPanelKeys, type PanelKeybindings, type PanelKeys } from "./panel-keys.ts";
|
|
2
3
|
import type { ImportKind } from "./types.ts";
|
|
3
4
|
import type { ConfigWritePreview, McpDiscoverySummary } from "./config.ts";
|
|
4
5
|
import type { McpOnboardingState } from "./onboarding-state.ts";
|
|
@@ -59,6 +60,7 @@ export interface SetupPanelCallbacks {
|
|
|
59
60
|
export interface SetupPanelOptions {
|
|
60
61
|
mode: "empty" | "setup";
|
|
61
62
|
onboardingState: McpOnboardingState;
|
|
63
|
+
keybindings?: PanelKeybindings;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
type Screen = "empty" | "setup" | "imports" | "paths";
|
|
@@ -89,6 +91,7 @@ export class McpSetupPanel {
|
|
|
89
91
|
private notice: { text: string; tone: "success" | "warning" | "muted" } | null = null;
|
|
90
92
|
private tui: { requestRender(): void };
|
|
91
93
|
private t = DEFAULT_THEME;
|
|
94
|
+
private keys: PanelKeys;
|
|
92
95
|
private inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
93
96
|
private static readonly INACTIVITY_MS = 60_000;
|
|
94
97
|
|
|
@@ -100,6 +103,7 @@ export class McpSetupPanel {
|
|
|
100
103
|
private done: () => void,
|
|
101
104
|
) {
|
|
102
105
|
this.tui = tui;
|
|
106
|
+
this.keys = createPanelKeys(options.keybindings);
|
|
103
107
|
this.screen = options.mode;
|
|
104
108
|
for (const entry of discovery.imports) {
|
|
105
109
|
this.selectedImports.add(entry.kind);
|
|
@@ -191,17 +195,17 @@ export class McpSetupPanel {
|
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
const actions = this.getActions();
|
|
194
|
-
if (
|
|
198
|
+
if (this.keys.selectUp(data)) {
|
|
195
199
|
this.actionCursor = Math.max(0, this.actionCursor - 1);
|
|
196
200
|
this.tui.requestRender();
|
|
197
201
|
return;
|
|
198
202
|
}
|
|
199
|
-
if (
|
|
203
|
+
if (this.keys.selectDown(data)) {
|
|
200
204
|
this.actionCursor = Math.min(actions.length - 1, this.actionCursor + 1);
|
|
201
205
|
this.tui.requestRender();
|
|
202
206
|
return;
|
|
203
207
|
}
|
|
204
|
-
if (
|
|
208
|
+
if (this.keys.selectConfirm(data)) {
|
|
205
209
|
const selected = this.getSelectedAction();
|
|
206
210
|
if (selected) void this.runAction(selected.id);
|
|
207
211
|
}
|
|
@@ -209,12 +213,12 @@ export class McpSetupPanel {
|
|
|
209
213
|
|
|
210
214
|
private handleImportsInput(data: string): void {
|
|
211
215
|
const imports = this.discovery.imports;
|
|
212
|
-
if (
|
|
216
|
+
if (this.keys.selectUp(data)) {
|
|
213
217
|
this.importCursor = Math.max(0, this.importCursor - 1);
|
|
214
218
|
this.tui.requestRender();
|
|
215
219
|
return;
|
|
216
220
|
}
|
|
217
|
-
if (
|
|
221
|
+
if (this.keys.selectDown(data)) {
|
|
218
222
|
this.importCursor = Math.min(imports.length - 1, this.importCursor + 1);
|
|
219
223
|
this.tui.requestRender();
|
|
220
224
|
return;
|
|
@@ -230,24 +234,24 @@ export class McpSetupPanel {
|
|
|
230
234
|
this.tui.requestRender();
|
|
231
235
|
return;
|
|
232
236
|
}
|
|
233
|
-
if (
|
|
237
|
+
if (this.keys.selectConfirm(data)) {
|
|
234
238
|
void this.applySelectedImports();
|
|
235
239
|
}
|
|
236
240
|
}
|
|
237
241
|
|
|
238
242
|
private handlePathsInput(data: string): void {
|
|
239
243
|
const paths = this.getDetectedPaths();
|
|
240
|
-
if (
|
|
244
|
+
if (this.keys.selectUp(data)) {
|
|
241
245
|
this.pathCursor = Math.max(0, this.pathCursor - 1);
|
|
242
246
|
this.tui.requestRender();
|
|
243
247
|
return;
|
|
244
248
|
}
|
|
245
|
-
if (
|
|
249
|
+
if (this.keys.selectDown(data)) {
|
|
246
250
|
this.pathCursor = Math.min(paths.length - 1, this.pathCursor + 1);
|
|
247
251
|
this.tui.requestRender();
|
|
248
252
|
return;
|
|
249
253
|
}
|
|
250
|
-
if (
|
|
254
|
+
if (this.keys.selectConfirm(data)) {
|
|
251
255
|
const selected = paths[this.pathCursor];
|
|
252
256
|
if (!selected) return;
|
|
253
257
|
void this.runBusy(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-adapter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"config.ts",
|
|
55
55
|
"server-manager.ts",
|
|
56
56
|
"sampling-handler.ts",
|
|
57
|
+
"elicitation-handler.ts",
|
|
57
58
|
"tool-registrar.ts",
|
|
58
59
|
"tool-result-renderer.ts",
|
|
59
60
|
"resource-tools.ts",
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
"mcp-callback-server.ts",
|
|
72
73
|
"mcp-auth-flow.ts",
|
|
73
74
|
"mcp-panel.ts",
|
|
75
|
+
"panel-keys.ts",
|
|
74
76
|
"logger.ts",
|
|
75
77
|
"errors.ts",
|
|
76
78
|
"app-bridge.bundle.js",
|
|
@@ -93,7 +95,7 @@
|
|
|
93
95
|
"zod": "^3.25.0 || ^4.0.0"
|
|
94
96
|
},
|
|
95
97
|
"devDependencies": {
|
|
96
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
98
|
+
"@earendil-works/pi-coding-agent": "^0.79.1",
|
|
97
99
|
"@types/bun": "^1.0.0",
|
|
98
100
|
"@types/node": "^20.0.0",
|
|
99
101
|
"@types/open": "^6.2.1",
|
package/panel-keys.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
/** The `tui.select.*` keybinding ids the adapter panels resolve. */
|
|
4
|
+
export type PanelSelectKeybinding =
|
|
5
|
+
| "tui.select.up"
|
|
6
|
+
| "tui.select.down"
|
|
7
|
+
| "tui.select.confirm";
|
|
8
|
+
|
|
9
|
+
/** Structural subset of pi-tui's `KeybindingsManager` (which satisfies it). */
|
|
10
|
+
export interface PanelKeybindings {
|
|
11
|
+
matches(data: string, keybinding: PanelSelectKeybinding): boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Key matchers for list navigation: user's `tui.select.*` bindings when a
|
|
16
|
+
* manager is provided, otherwise the previous hardcoded defaults.
|
|
17
|
+
*/
|
|
18
|
+
export interface PanelKeys {
|
|
19
|
+
selectUp(data: string): boolean;
|
|
20
|
+
selectDown(data: string): boolean;
|
|
21
|
+
selectConfirm(data: string): boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createPanelKeys(keybindings?: PanelKeybindings): PanelKeys {
|
|
25
|
+
if (keybindings) {
|
|
26
|
+
return {
|
|
27
|
+
selectUp: (data) => keybindings.matches(data, "tui.select.up"),
|
|
28
|
+
selectDown: (data) => keybindings.matches(data, "tui.select.down"),
|
|
29
|
+
selectConfirm: (data) => keybindings.matches(data, "tui.select.confirm"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
selectUp: (data) => matchesKey(data, "up"),
|
|
34
|
+
selectDown: (data) => matchesKey(data, "down"),
|
|
35
|
+
selectConfirm: (data) => matchesKey(data, "return"),
|
|
36
|
+
};
|
|
37
|
+
}
|