pi-mcp-adapter 2.5.4 → 2.6.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 +15 -0
- package/README.md +9 -3
- package/banner.png +0 -0
- package/commands.ts +109 -55
- package/config.ts +12 -12
- package/consent-manager.ts +2 -2
- package/direct-tools.ts +21 -15
- package/host-html-template.ts +1 -1
- package/index.ts +22 -15
- package/init.ts +15 -15
- package/lifecycle.ts +3 -3
- package/mcp-auth-flow.ts +14 -5
- package/mcp-auth.ts +25 -5
- package/mcp-callback-server.ts +1 -1
- package/mcp-oauth-provider.ts +6 -7
- package/mcp-panel.ts +114 -38
- package/mcp-setup-panel.ts +4 -4
- package/metadata-cache.ts +5 -5
- package/npx-resolver.ts +1 -1
- package/oauth-handler.ts +1 -1
- package/onboarding-state.ts +1 -1
- package/package.json +7 -3
- package/proxy-modes.ts +11 -11
- package/sampling-handler.ts +7 -6
- package/server-manager.ts +8 -8
- package/state.ts +8 -8
- package/tool-metadata.ts +5 -5
- package/tool-registrar.ts +1 -1
- package/tool-result-renderer.ts +65 -0
- package/types.ts +11 -4
- package/ui-resource-handler.ts +4 -4
- package/ui-server.ts +6 -6
- package/ui-session.ts +5 -5
- package/utils.ts +2 -2
package/mcp-auth-flow.ts
CHANGED
|
@@ -10,24 +10,26 @@ import {
|
|
|
10
10
|
} from "@modelcontextprotocol/sdk/client/auth.js"
|
|
11
11
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
|
12
12
|
import open from "open"
|
|
13
|
-
import { McpOAuthProvider, type McpOAuthConfig } from "./mcp-oauth-provider.
|
|
13
|
+
import { McpOAuthProvider, type McpOAuthConfig } from "./mcp-oauth-provider.ts"
|
|
14
14
|
import {
|
|
15
15
|
ensureCallbackServer,
|
|
16
16
|
waitForCallback,
|
|
17
17
|
cancelPendingCallback,
|
|
18
18
|
stopCallbackServer,
|
|
19
|
-
} from "./mcp-callback-server.
|
|
19
|
+
} from "./mcp-callback-server.ts"
|
|
20
20
|
import {
|
|
21
21
|
getAuthForUrl,
|
|
22
22
|
isTokenExpired,
|
|
23
23
|
hasStoredTokens,
|
|
24
24
|
clearAllCredentials,
|
|
25
|
+
clearClientInfo,
|
|
26
|
+
clearCodeVerifier,
|
|
25
27
|
updateOAuthState,
|
|
26
28
|
getOAuthState,
|
|
27
29
|
clearOAuthState,
|
|
28
30
|
type StoredTokens,
|
|
29
|
-
} from "./mcp-auth.
|
|
30
|
-
import type { ServerEntry } from "./types.
|
|
31
|
+
} from "./mcp-auth.ts"
|
|
32
|
+
import type { ServerEntry } from "./types.ts"
|
|
31
33
|
|
|
32
34
|
/** Auth status for a server */
|
|
33
35
|
export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
|
|
@@ -74,6 +76,13 @@ export async function startAuth(
|
|
|
74
76
|
): Promise<{ authorizationUrl: string }> {
|
|
75
77
|
const config = definition ? extractOAuthConfig(definition) : {}
|
|
76
78
|
|
|
79
|
+
const storedAuth = await getAuthForUrl(serverName, serverUrl)
|
|
80
|
+
if (storedAuth?.clientInfo && !storedAuth.tokens && !config.clientId) {
|
|
81
|
+
clearClientInfo(serverName)
|
|
82
|
+
clearCodeVerifier(serverName)
|
|
83
|
+
await clearOAuthState(serverName)
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
if (config.grantType === "client_credentials") {
|
|
78
87
|
const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
|
|
79
88
|
onRedirect: async () => {
|
|
@@ -92,7 +101,7 @@ export async function startAuth(
|
|
|
92
101
|
await ensureCallbackServer({ strictPort: Boolean(config.clientId) })
|
|
93
102
|
|
|
94
103
|
const oauthState = generateState()
|
|
95
|
-
await updateOAuthState(serverName, oauthState)
|
|
104
|
+
await updateOAuthState(serverName, oauthState, serverUrl)
|
|
96
105
|
|
|
97
106
|
let capturedUrl: URL | undefined
|
|
98
107
|
const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
|
package/mcp-auth.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
|
|
13
13
|
import { join } from 'path';
|
|
14
|
-
import { getAgentPath } from './agent-dir.
|
|
14
|
+
import { getAgentPath } from './agent-dir.ts';
|
|
15
15
|
|
|
16
16
|
/** OAuth token storage format */
|
|
17
17
|
export interface StoredTokens {
|
|
@@ -163,6 +163,11 @@ export function updateTokens(
|
|
|
163
163
|
serverUrl?: string
|
|
164
164
|
): void {
|
|
165
165
|
const entry = getAuthEntry(serverName) ?? {};
|
|
166
|
+
if (serverUrl && entry.serverUrl !== serverUrl) {
|
|
167
|
+
delete entry.clientInfo;
|
|
168
|
+
delete entry.codeVerifier;
|
|
169
|
+
delete entry.oauthState;
|
|
170
|
+
}
|
|
166
171
|
entry.tokens = tokens;
|
|
167
172
|
saveAuthEntry(serverName, entry, serverUrl);
|
|
168
173
|
}
|
|
@@ -176,6 +181,11 @@ export function updateClientInfo(
|
|
|
176
181
|
serverUrl?: string
|
|
177
182
|
): void {
|
|
178
183
|
const entry = getAuthEntry(serverName) ?? {};
|
|
184
|
+
if (serverUrl && entry.serverUrl !== serverUrl) {
|
|
185
|
+
delete entry.tokens;
|
|
186
|
+
delete entry.codeVerifier;
|
|
187
|
+
delete entry.oauthState;
|
|
188
|
+
}
|
|
179
189
|
entry.clientInfo = clientInfo;
|
|
180
190
|
saveAuthEntry(serverName, entry, serverUrl);
|
|
181
191
|
}
|
|
@@ -183,10 +193,15 @@ export function updateClientInfo(
|
|
|
183
193
|
/**
|
|
184
194
|
* Update code verifier for a server.
|
|
185
195
|
*/
|
|
186
|
-
export function updateCodeVerifier(serverName: string, codeVerifier: string): void {
|
|
196
|
+
export function updateCodeVerifier(serverName: string, codeVerifier: string, serverUrl?: string): void {
|
|
187
197
|
const entry = getAuthEntry(serverName) ?? {};
|
|
198
|
+
if (serverUrl && entry.serverUrl !== serverUrl) {
|
|
199
|
+
delete entry.tokens;
|
|
200
|
+
delete entry.clientInfo;
|
|
201
|
+
delete entry.oauthState;
|
|
202
|
+
}
|
|
188
203
|
entry.codeVerifier = codeVerifier;
|
|
189
|
-
saveAuthEntry(serverName, entry);
|
|
204
|
+
saveAuthEntry(serverName, entry, serverUrl);
|
|
190
205
|
}
|
|
191
206
|
|
|
192
207
|
/**
|
|
@@ -203,10 +218,15 @@ export function clearCodeVerifier(serverName: string): void {
|
|
|
203
218
|
/**
|
|
204
219
|
* Update OAuth state for a server.
|
|
205
220
|
*/
|
|
206
|
-
export function updateOAuthState(serverName: string, state: string): void {
|
|
221
|
+
export function updateOAuthState(serverName: string, state: string, serverUrl?: string): void {
|
|
207
222
|
const entry = getAuthEntry(serverName) ?? {};
|
|
223
|
+
if (serverUrl && entry.serverUrl !== serverUrl) {
|
|
224
|
+
delete entry.tokens;
|
|
225
|
+
delete entry.clientInfo;
|
|
226
|
+
delete entry.codeVerifier;
|
|
227
|
+
}
|
|
208
228
|
entry.oauthState = state;
|
|
209
|
-
saveAuthEntry(serverName, entry);
|
|
229
|
+
saveAuthEntry(serverName, entry, serverUrl);
|
|
210
230
|
}
|
|
211
231
|
|
|
212
232
|
/**
|
package/mcp-callback-server.ts
CHANGED
package/mcp-oauth-provider.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type {
|
|
|
14
14
|
OAuthClientInformationFull,
|
|
15
15
|
} from "@modelcontextprotocol/sdk/shared/auth.js"
|
|
16
16
|
import {
|
|
17
|
-
getAuthEntry,
|
|
18
17
|
getAuthForUrl,
|
|
19
18
|
updateTokens,
|
|
20
19
|
updateClientInfo,
|
|
@@ -25,7 +24,7 @@ import {
|
|
|
25
24
|
clearTokens,
|
|
26
25
|
type StoredTokens,
|
|
27
26
|
type StoredClientInfo,
|
|
28
|
-
} from "./mcp-auth.
|
|
27
|
+
} from "./mcp-auth.ts"
|
|
29
28
|
|
|
30
29
|
// Callback server configuration
|
|
31
30
|
const DEFAULT_OAUTH_CALLBACK_PORT = 19876
|
|
@@ -212,7 +211,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
212
211
|
throw new Error("redirectToAuthorization is not used for client_credentials flow")
|
|
213
212
|
}
|
|
214
213
|
// No saved oauthState means we're on the post-refresh authorize fallback.
|
|
215
|
-
const entry = await
|
|
214
|
+
const entry = await getAuthForUrl(this.serverName, this.serverUrl)
|
|
216
215
|
if (!entry?.oauthState) {
|
|
217
216
|
throw new UnauthorizedError(
|
|
218
217
|
`Re-authentication required for MCP server: ${this.serverName}`,
|
|
@@ -226,7 +225,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
226
225
|
* Save the PKCE code verifier.
|
|
227
226
|
*/
|
|
228
227
|
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
|
229
|
-
updateCodeVerifier(this.serverName, codeVerifier)
|
|
228
|
+
updateCodeVerifier(this.serverName, codeVerifier, this.serverUrl)
|
|
230
229
|
}
|
|
231
230
|
|
|
232
231
|
/**
|
|
@@ -237,7 +236,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
237
236
|
if (this.usesClientCredentials) {
|
|
238
237
|
throw new Error("codeVerifier is not used for client_credentials flow")
|
|
239
238
|
}
|
|
240
|
-
const entry = await
|
|
239
|
+
const entry = await getAuthForUrl(this.serverName, this.serverUrl)
|
|
241
240
|
if (!entry?.codeVerifier) {
|
|
242
241
|
throw new Error(`No code verifier saved for MCP server: ${this.serverName}`)
|
|
243
242
|
}
|
|
@@ -248,7 +247,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
248
247
|
* Save the OAuth state parameter for CSRF protection.
|
|
249
248
|
*/
|
|
250
249
|
async saveState(state: string): Promise<void> {
|
|
251
|
-
updateOAuthState(this.serverName, state)
|
|
250
|
+
updateOAuthState(this.serverName, state, this.serverUrl)
|
|
252
251
|
}
|
|
253
252
|
|
|
254
253
|
/**
|
|
@@ -259,7 +258,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
259
258
|
if (this.usesClientCredentials) {
|
|
260
259
|
throw new Error("state is not used for client_credentials flow")
|
|
261
260
|
}
|
|
262
|
-
const entry = await
|
|
261
|
+
const entry = await getAuthForUrl(this.serverName, this.serverUrl)
|
|
263
262
|
if (!entry?.oauthState) {
|
|
264
263
|
throw new UnauthorizedError(
|
|
265
264
|
`Re-authentication required for MCP server: ${this.serverName}`,
|
package/mcp-panel.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { matchesKey, truncateToWidth, visibleWidth } from "@
|
|
2
|
-
import { isToolExcluded } from "./types.
|
|
3
|
-
import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance } from "./types.
|
|
4
|
-
import { resourceNameToToolName } from "./resource-tools.
|
|
5
|
-
import type { MetadataCache, ServerCacheEntry, CachedTool } from "./metadata-cache.
|
|
1
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { isToolExcluded } from "./types.ts";
|
|
3
|
+
import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance } from "./types.ts";
|
|
4
|
+
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
5
|
+
import type { MetadataCache, ServerCacheEntry, CachedTool } from "./metadata-cache.ts";
|
|
6
6
|
|
|
7
7
|
interface PanelTheme {
|
|
8
8
|
border: string;
|
|
@@ -120,10 +120,12 @@ class McpPanel {
|
|
|
120
120
|
private discardSelected = 1;
|
|
121
121
|
private importNotice: string | null = null;
|
|
122
122
|
private authNotice: string | null = null;
|
|
123
|
+
private authInFlight: string | null = null;
|
|
123
124
|
private inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
124
125
|
private visibleItems: VisibleItem[] = [];
|
|
125
126
|
private tui: { requestRender(): void };
|
|
126
127
|
private t = DEFAULT_THEME;
|
|
128
|
+
private authOnly: boolean;
|
|
127
129
|
|
|
128
130
|
private static readonly MAX_VISIBLE = 12;
|
|
129
131
|
private static readonly INACTIVITY_MS = 60_000;
|
|
@@ -135,13 +137,15 @@ class McpPanel {
|
|
|
135
137
|
private callbacks: McpPanelCallbacks,
|
|
136
138
|
tui: { requestRender(): void },
|
|
137
139
|
private done: (result: McpPanelResult) => void,
|
|
138
|
-
|
|
140
|
+
options: { noticeLines?: string[]; authOnly?: boolean } = {},
|
|
139
141
|
) {
|
|
140
142
|
this.tui = tui;
|
|
141
|
-
this.noticeLines = noticeLines;
|
|
143
|
+
this.noticeLines = options.noticeLines ?? [];
|
|
144
|
+
this.authOnly = options.authOnly === true;
|
|
142
145
|
this.prefix = config.settings?.toolPrefix ?? "server";
|
|
143
146
|
|
|
144
147
|
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
148
|
+
if (this.authOnly && !callbacks.canAuthenticate(serverName)) continue;
|
|
145
149
|
const prov = provenance.get(serverName);
|
|
146
150
|
const serverCache = cache?.servers?.[serverName];
|
|
147
151
|
|
|
@@ -154,7 +158,7 @@ class McpPanel {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
const tools: ToolState[] = [];
|
|
157
|
-
if (serverCache) {
|
|
161
|
+
if (serverCache && !this.authOnly) {
|
|
158
162
|
for (const tool of serverCache.tools ?? []) {
|
|
159
163
|
if (isToolExcluded(tool.name, serverName, this.prefix, definition.excludeTools)) {
|
|
160
164
|
continue;
|
|
@@ -230,6 +234,14 @@ class McpPanel {
|
|
|
230
234
|
this.visibleItems = [];
|
|
231
235
|
for (let si = 0; si < this.servers.length; si++) {
|
|
232
236
|
const server = this.servers[si];
|
|
237
|
+
if (query && this.authOnly) {
|
|
238
|
+
const score = mode === "name" ? fuzzyScore(query, server.name) : 0;
|
|
239
|
+
if (score > 0) {
|
|
240
|
+
this.visibleItems.push({ type: "server", serverIndex: si });
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
233
245
|
this.visibleItems.push({ type: "server", serverIndex: si });
|
|
234
246
|
if (server.expanded || query) {
|
|
235
247
|
for (let ti = 0; ti < server.tools.length; ti++) {
|
|
@@ -248,7 +260,7 @@ class McpPanel {
|
|
|
248
260
|
}
|
|
249
261
|
}
|
|
250
262
|
|
|
251
|
-
if (query) {
|
|
263
|
+
if (query && !this.authOnly) {
|
|
252
264
|
this.visibleItems = this.visibleItems.filter((item) => {
|
|
253
265
|
if (item.type === "server") {
|
|
254
266
|
return this.visibleItems.some(
|
|
@@ -284,7 +296,7 @@ class McpPanel {
|
|
|
284
296
|
handleInput(data: string): void {
|
|
285
297
|
this.resetInactivityTimeout();
|
|
286
298
|
this.importNotice = null;
|
|
287
|
-
this.authNotice = null;
|
|
299
|
+
if (!this.authInFlight) this.authNotice = null;
|
|
288
300
|
|
|
289
301
|
if (this.confirmingDiscard) {
|
|
290
302
|
this.handleDiscardInput(data);
|
|
@@ -360,7 +372,7 @@ class McpPanel {
|
|
|
360
372
|
|
|
361
373
|
if (matchesKey(data, "space")) {
|
|
362
374
|
const item = this.visibleItems[this.cursorIndex];
|
|
363
|
-
if (item) this.toggleItem(item);
|
|
375
|
+
if (item && !this.authOnly) this.toggleItem(item);
|
|
364
376
|
return;
|
|
365
377
|
}
|
|
366
378
|
|
|
@@ -369,8 +381,8 @@ class McpPanel {
|
|
|
369
381
|
if (!item) return;
|
|
370
382
|
const server = this.servers[item.serverIndex];
|
|
371
383
|
if (item.type === "server") {
|
|
372
|
-
if (server.connectionStatus === "needs-auth") {
|
|
373
|
-
this.
|
|
384
|
+
if (this.authOnly || server.connectionStatus === "needs-auth") {
|
|
385
|
+
this.authenticateServer(server);
|
|
374
386
|
return;
|
|
375
387
|
}
|
|
376
388
|
server.expanded = !server.expanded;
|
|
@@ -387,6 +399,12 @@ class McpPanel {
|
|
|
387
399
|
return;
|
|
388
400
|
}
|
|
389
401
|
|
|
402
|
+
if (matchesKey(data, "ctrl+a")) {
|
|
403
|
+
const item = this.visibleItems[this.cursorIndex];
|
|
404
|
+
if (item) this.authenticateSelectedServer(item);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
390
408
|
if (matchesKey(data, "ctrl+r")) {
|
|
391
409
|
const item = this.visibleItems[this.cursorIndex];
|
|
392
410
|
if (!item) return;
|
|
@@ -413,6 +431,7 @@ class McpPanel {
|
|
|
413
431
|
}
|
|
414
432
|
|
|
415
433
|
if (data === "?") {
|
|
434
|
+
if (this.authOnly) return;
|
|
416
435
|
this.descSearchActive = true;
|
|
417
436
|
this.descQuery = "";
|
|
418
437
|
this.rebuildVisibleItems();
|
|
@@ -439,7 +458,39 @@ class McpPanel {
|
|
|
439
458
|
}
|
|
440
459
|
}
|
|
441
460
|
|
|
461
|
+
private authenticateSelectedServer(item: VisibleItem): void {
|
|
462
|
+
this.authenticateServer(this.servers[item.serverIndex]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private authenticateServer(server: ServerState): void {
|
|
466
|
+
if (this.authInFlight) return;
|
|
467
|
+
if (!this.callbacks.canAuthenticate(server.name)) {
|
|
468
|
+
this.authNotice = `${server.name} does not use OAuth authentication.`;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this.authInFlight = server.name;
|
|
473
|
+
this.authNotice = `Authenticating ${server.name}...`;
|
|
474
|
+
this.tui.requestRender();
|
|
475
|
+
|
|
476
|
+
this.callbacks.authenticate(server.name).then((result) => {
|
|
477
|
+
server.connectionStatus = this.callbacks.getConnectionStatus(server.name);
|
|
478
|
+
this.authNotice = result.ok
|
|
479
|
+
? `OAuth finished for ${server.name}. Run reconnect if it is still idle.`
|
|
480
|
+
: `OAuth failed for ${server.name}${result.message ? `: ${result.message}` : ". Check the notification for details."}`;
|
|
481
|
+
this.authInFlight = null;
|
|
482
|
+
this.tui.requestRender();
|
|
483
|
+
}).catch((error) => {
|
|
484
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
485
|
+
server.connectionStatus = this.callbacks.getConnectionStatus(server.name);
|
|
486
|
+
this.authNotice = `OAuth failed for ${server.name}: ${message}`;
|
|
487
|
+
this.authInFlight = null;
|
|
488
|
+
this.tui.requestRender();
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
442
492
|
private toggleItem(item: VisibleItem): void {
|
|
493
|
+
if (this.authOnly) return;
|
|
443
494
|
const server = this.servers[item.serverIndex];
|
|
444
495
|
if (item.type === "server") {
|
|
445
496
|
const newState = !server.tools.every((t) => t.isDirect);
|
|
@@ -550,7 +601,7 @@ class McpPanel {
|
|
|
550
601
|
const emptyRow = () => fg(t.border, "│") + " ".repeat(innerW) + fg(t.border, "│");
|
|
551
602
|
const divider = () => fg(t.border, "├" + "─".repeat(innerW) + "┤");
|
|
552
603
|
|
|
553
|
-
const titleText = " MCP Servers ";
|
|
604
|
+
const titleText = this.authOnly ? " MCP OAuth " : " MCP Servers ";
|
|
554
605
|
const borderLen = innerW - visibleWidth(titleText);
|
|
555
606
|
const leftB = Math.floor(borderLen / 2);
|
|
556
607
|
const rightB = borderLen - leftB;
|
|
@@ -579,7 +630,7 @@ class McpPanel {
|
|
|
579
630
|
|
|
580
631
|
if (this.servers.length === 0) {
|
|
581
632
|
lines.push(emptyRow());
|
|
582
|
-
lines.push(row(fg(t.hint, italic("No MCP servers configured."))));
|
|
633
|
+
lines.push(row(fg(t.hint, italic(this.authOnly ? "No OAuth-capable MCP servers configured." : "No MCP servers configured."))));
|
|
583
634
|
lines.push(emptyRow());
|
|
584
635
|
} else {
|
|
585
636
|
const maxVis = McpPanel.MAX_VISIBLE;
|
|
@@ -631,27 +682,40 @@ class McpPanel {
|
|
|
631
682
|
: fg(t.hint, " Keep ");
|
|
632
683
|
lines.push(row(`Discard unsaved changes? ${discardBtn} ${keepBtn}`));
|
|
633
684
|
} else {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
0
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
685
|
+
if (this.authOnly) {
|
|
686
|
+
lines.push(row(fg(t.description, "select a server to authenticate")));
|
|
687
|
+
} else {
|
|
688
|
+
const directCount = this.servers.reduce((sum, s) => sum + s.tools.filter((t) => t.isDirect).length, 0);
|
|
689
|
+
const totalTokens = this.servers.reduce(
|
|
690
|
+
(sum, s) => sum + s.tools.filter((t) => t.isDirect).reduce((ts, t) => ts + t.estimatedTokens, 0),
|
|
691
|
+
0,
|
|
692
|
+
);
|
|
693
|
+
const stats =
|
|
694
|
+
directCount > 0 ? `${directCount} direct ~${totalTokens.toLocaleString()} tokens` : "no direct tools";
|
|
695
|
+
lines.push(row(fg(t.description, stats + (this.dirty ? fg(t.needsAuth, " (unsaved)") : ""))));
|
|
696
|
+
}
|
|
642
697
|
}
|
|
643
698
|
|
|
644
699
|
lines.push(emptyRow());
|
|
645
|
-
const hints =
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
700
|
+
const hints = this.authOnly
|
|
701
|
+
? [
|
|
702
|
+
italic("↑↓") + " navigate",
|
|
703
|
+
italic("⏎") + " auth",
|
|
704
|
+
italic("ctrl+a") + " auth",
|
|
705
|
+
italic("esc") + " clear/close",
|
|
706
|
+
italic("ctrl+c") + " quit",
|
|
707
|
+
]
|
|
708
|
+
: [
|
|
709
|
+
italic("↑↓") + " navigate",
|
|
710
|
+
italic("space") + " toggle",
|
|
711
|
+
italic("⏎") + " expand/auth",
|
|
712
|
+
italic("ctrl+a") + " auth",
|
|
713
|
+
italic("ctrl+r") + " reconnect",
|
|
714
|
+
italic("?") + " desc search",
|
|
715
|
+
italic("ctrl+s") + " save",
|
|
716
|
+
italic("esc") + " clear/close",
|
|
717
|
+
italic("ctrl+c") + " quit",
|
|
718
|
+
];
|
|
655
719
|
const gap = " ";
|
|
656
720
|
const gapW = 2;
|
|
657
721
|
const maxW = innerW - 2;
|
|
@@ -685,9 +749,10 @@ class McpPanel {
|
|
|
685
749
|
|
|
686
750
|
const nameStr = isCursor ? bold(fg(t.selected, server.name)) : server.name;
|
|
687
751
|
const importLabel = server.source === "import" ? fg(t.description, ` (${server.importKind ?? "import"})`) : "";
|
|
752
|
+
const statusLabel = this.renderConnectionStatus(server);
|
|
688
753
|
|
|
689
|
-
if (!server.hasCachedData) {
|
|
690
|
-
return `${prefix} ${nameStr}${importLabel} ${fg(t.description, "(not cached)")}`;
|
|
754
|
+
if (!server.hasCachedData && !this.authOnly) {
|
|
755
|
+
return `${prefix} ${nameStr}${importLabel} ${fg(t.description, "(not cached)")}${statusLabel}`;
|
|
691
756
|
}
|
|
692
757
|
|
|
693
758
|
const directCount = server.tools.filter((t) => t.isDirect).length;
|
|
@@ -709,7 +774,18 @@ class McpPanel {
|
|
|
709
774
|
toolInfo = fg(t.description, toolInfo);
|
|
710
775
|
}
|
|
711
776
|
|
|
712
|
-
return `${prefix} ${toggleIcon} ${nameStr}${importLabel} ${toolInfo}`;
|
|
777
|
+
return `${prefix} ${toggleIcon} ${nameStr}${importLabel} ${toolInfo}${statusLabel}`;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private renderConnectionStatus(server: ServerState): string {
|
|
781
|
+
const t = this.t;
|
|
782
|
+
if (this.authInFlight === server.name) return ` ${fg(t.needsAuth, "authenticating")}`;
|
|
783
|
+
if (server.connectionStatus === "needs-auth") return ` ${fg(t.needsAuth, "needs auth")}`;
|
|
784
|
+
if (server.connectionStatus === "connecting") return ` ${fg(t.needsAuth, "connecting")}`;
|
|
785
|
+
if (server.connectionStatus === "failed") return ` ${fg(t.cancel, "failed")}`;
|
|
786
|
+
if (this.authOnly && server.connectionStatus === "connected") return ` ${fg(t.direct, "connected")}`;
|
|
787
|
+
if (this.authOnly) return ` ${fg(t.description, "idle")}`;
|
|
788
|
+
return "";
|
|
713
789
|
}
|
|
714
790
|
|
|
715
791
|
private renderToolRow(tool: ToolState, isCursor: boolean, innerW: number): string {
|
|
@@ -744,7 +820,7 @@ export function createMcpPanel(
|
|
|
744
820
|
callbacks: McpPanelCallbacks,
|
|
745
821
|
tui: { requestRender(): void },
|
|
746
822
|
done: (result: McpPanelResult) => void,
|
|
747
|
-
options?: { noticeLines?: string[] },
|
|
823
|
+
options?: { noticeLines?: string[]; authOnly?: boolean },
|
|
748
824
|
): McpPanel & { dispose(): void } {
|
|
749
|
-
return new McpPanel(config, cache, provenance, callbacks, tui, done, options
|
|
825
|
+
return new McpPanel(config, cache, provenance, callbacks, tui, done, options ?? {});
|
|
750
826
|
}
|
package/mcp-setup-panel.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { matchesKey, truncateToWidth, visibleWidth } from "@
|
|
2
|
-
import type { ImportKind } from "./types.
|
|
3
|
-
import type { ConfigWritePreview, McpDiscoverySummary } from "./config.
|
|
4
|
-
import type { McpOnboardingState } from "./onboarding-state.
|
|
1
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { ImportKind } from "./types.ts";
|
|
3
|
+
import type { ConfigWritePreview, McpDiscoverySummary } from "./config.ts";
|
|
4
|
+
import type { McpOnboardingState } from "./onboarding-state.ts";
|
|
5
5
|
|
|
6
6
|
interface SetupTheme {
|
|
7
7
|
border: string;
|
package/metadata-cache.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// metadata-cache.ts - Persistent MCP metadata cache
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
-
import { getAgentPath } from "./agent-dir.
|
|
4
|
+
import { getAgentPath } from "./agent-dir.ts";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
7
|
-
import type { McpTool, McpResource, ServerEntry, ToolMetadata } from "./types.
|
|
8
|
-
import { formatToolName, isToolExcluded } from "./types.
|
|
9
|
-
import { resourceNameToToolName } from "./resource-tools.
|
|
10
|
-
import { extractToolUiStreamMode, interpolateEnvRecord, resolveBearerToken, resolveConfigPath } from "./utils.
|
|
7
|
+
import type { McpTool, McpResource, ServerEntry, ToolMetadata } from "./types.ts";
|
|
8
|
+
import { formatToolName, isToolExcluded } from "./types.ts";
|
|
9
|
+
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
10
|
+
import { extractToolUiStreamMode, interpolateEnvRecord, resolveBearerToken, resolveConfigPath } from "./utils.ts";
|
|
11
11
|
|
|
12
12
|
const CACHE_VERSION = 1;
|
|
13
13
|
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
package/npx-resolver.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// npx-resolver.ts - Resolve npx/npm exec binaries to avoid npm parent processes
|
|
2
2
|
import { existsSync, readFileSync, realpathSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync, openSync, readSync, closeSync } from "node:fs";
|
|
3
3
|
import { join, dirname, extname, resolve, sep } from "node:path";
|
|
4
|
-
import { getAgentPath } from "./agent-dir.
|
|
4
|
+
import { getAgentPath } from "./agent-dir.ts";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
const CACHE_VERSION = 1;
|
package/oauth-handler.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// oauth-handler.ts - OAuth token management for MCP servers
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { getAgentPath } from "./agent-dir.
|
|
4
|
+
import { getAgentPath } from "./agent-dir.ts";
|
|
5
5
|
import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
6
6
|
|
|
7
7
|
// Token storage path for a server
|
package/onboarding-state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { getAgentPath } from "./agent-dir.
|
|
3
|
+
import { getAgentPath } from "./agent-dir.ts";
|
|
4
4
|
|
|
5
5
|
export interface McpOnboardingState {
|
|
6
6
|
version: 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-adapter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"server-manager.ts",
|
|
56
56
|
"sampling-handler.ts",
|
|
57
57
|
"tool-registrar.ts",
|
|
58
|
+
"tool-result-renderer.ts",
|
|
58
59
|
"resource-tools.ts",
|
|
59
60
|
"lifecycle.ts",
|
|
60
61
|
"metadata-cache.ts",
|
|
@@ -73,22 +74,25 @@
|
|
|
73
74
|
"logger.ts",
|
|
74
75
|
"errors.ts",
|
|
75
76
|
"app-bridge.bundle.js",
|
|
77
|
+
"banner.png",
|
|
76
78
|
"README.md",
|
|
77
79
|
"CHANGELOG.md",
|
|
78
80
|
"LICENSE"
|
|
79
81
|
],
|
|
80
82
|
"dependencies": {
|
|
83
|
+
"@earendil-works/pi-ai": "^0.74.0",
|
|
84
|
+
"@earendil-works/pi-tui": "^0.74.0",
|
|
81
85
|
"@modelcontextprotocol/ext-apps": "^1.2.2",
|
|
82
86
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
83
|
-
"@mariozechner/pi-ai": "^0.70.2",
|
|
84
|
-
"typebox": "^1.1.24",
|
|
85
87
|
"open": "^10.2.0",
|
|
88
|
+
"typebox": "^1.1.24",
|
|
86
89
|
"zod": "^3.25.0 || ^4.0.0"
|
|
87
90
|
},
|
|
88
91
|
"peerDependencies": {
|
|
89
92
|
"zod": "^3.25.0 || ^4.0.0"
|
|
90
93
|
},
|
|
91
94
|
"devDependencies": {
|
|
95
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
92
96
|
"@types/bun": "^1.0.0",
|
|
93
97
|
"@types/node": "^20.0.0",
|
|
94
98
|
"@types/open": "^6.2.1",
|
package/proxy-modes.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { AgentToolResult, ToolInfo } from "@
|
|
2
|
-
import type { McpExtensionState } from "./state.
|
|
3
|
-
import type { ToolMetadata, McpContent } from "./types.
|
|
4
|
-
import { getServerPrefix, parseUiPromptHandoff } from "./types.
|
|
5
|
-
import { lazyConnect, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar } from "./init.
|
|
6
|
-
import { buildToolMetadata, getToolNames, findToolByName, formatSchema } from "./tool-metadata.
|
|
7
|
-
import { transformMcpContent } from "./tool-registrar.
|
|
8
|
-
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.
|
|
9
|
-
import { formatAuthRequiredMessage, truncateAtWord } from "./utils.
|
|
10
|
-
import { authenticate, supportsOAuth } from "./mcp-auth-flow.
|
|
1
|
+
import type { AgentToolResult, ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.ts";
|
|
3
|
+
import type { ToolMetadata, McpContent } from "./types.ts";
|
|
4
|
+
import { getServerPrefix, parseUiPromptHandoff } from "./types.ts";
|
|
5
|
+
import { lazyConnect, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar } from "./init.ts";
|
|
6
|
+
import { buildToolMetadata, getToolNames, findToolByName, formatSchema } from "./tool-metadata.ts";
|
|
7
|
+
import { transformMcpContent } from "./tool-registrar.ts";
|
|
8
|
+
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.ts";
|
|
9
|
+
import { formatAuthRequiredMessage, truncateAtWord } from "./utils.ts";
|
|
10
|
+
import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
11
11
|
|
|
12
12
|
type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
|
|
13
13
|
|
|
@@ -45,7 +45,7 @@ async function attemptAutoAuth(
|
|
|
45
45
|
return { status: "skipped" };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const grantType = definition.oauth
|
|
48
|
+
const grantType = definition.oauth ? definition.oauth.grantType ?? "authorization_code" : "authorization_code";
|
|
49
49
|
if (!state.ui && grantType !== "client_credentials") {
|
|
50
50
|
return {
|
|
51
51
|
status: "failed",
|
package/sampling-handler.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { complete, type Api, type AssistantMessage, type Message, type Model, type TextContent } from "@
|
|
2
|
-
import { truncateAtWord } from "./utils.
|
|
3
|
-
import type { ExtensionUIContext, ModelRegistry } from "@
|
|
1
|
+
import { complete, type Api, type AssistantMessage, type Message, type Model, type TextContent } from "@earendil-works/pi-ai";
|
|
2
|
+
import { truncateAtWord } from "./utils.ts";
|
|
3
|
+
import type { ExtensionUIContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
5
|
import {
|
|
6
6
|
CreateMessageRequestSchema,
|
|
@@ -147,10 +147,11 @@ async function resolveSamplingModel(
|
|
|
147
147
|
const errors: string[] = [];
|
|
148
148
|
for (const model of candidates) {
|
|
149
149
|
const auth = await options.modelRegistry.getApiKeyAndHeaders(model);
|
|
150
|
-
if (auth.ok) {
|
|
151
|
-
|
|
150
|
+
if (auth.ok === false) {
|
|
151
|
+
errors.push(`${model.provider}/${model.id}: ${auth.error}`);
|
|
152
|
+
continue;
|
|
152
153
|
}
|
|
153
|
-
|
|
154
|
+
return { model, apiKey: auth.apiKey, headers: auth.headers };
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
if (errors.length > 0) {
|