pi-mcp-adapter 2.3.5 → 2.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.3.5",
3
+ "version": "2.4.1",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,8 @@
45
45
  "proxy-modes.ts",
46
46
  "direct-tools.ts",
47
47
  "commands.ts",
48
+ "onboarding-state.ts",
49
+ "mcp-setup-panel.ts",
48
50
  "types.ts",
49
51
  "ui-stream-types.ts",
50
52
  "config.ts",
package/proxy-modes.ts CHANGED
@@ -7,9 +7,52 @@ import { buildToolMetadata, getToolNames, findToolByName, formatSchema } from ".
7
7
  import { transformMcpContent } from "./tool-registrar.js";
8
8
  import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.js";
9
9
  import { truncateAtWord } from "./utils.js";
10
+ import { authenticate, supportsOAuth } from "./mcp-auth-flow.js";
10
11
 
11
12
  type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
12
13
 
14
+ type AutoAuthResult =
15
+ | { status: "skipped" }
16
+ | { status: "success" }
17
+ | { status: "failed"; message: string };
18
+
19
+ function getAuthRequiredMessage(serverName: string): string {
20
+ return `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} first.`;
21
+ }
22
+
23
+ async function attemptAutoAuth(
24
+ state: McpExtensionState,
25
+ serverName: string,
26
+ ): Promise<AutoAuthResult> {
27
+ if (state.config.settings?.autoAuth !== true) {
28
+ return { status: "skipped" };
29
+ }
30
+
31
+ const definition = state.config.mcpServers[serverName];
32
+ if (!definition || !supportsOAuth(definition) || !definition.url) {
33
+ return { status: "skipped" };
34
+ }
35
+
36
+ const grantType = definition.oauth?.grantType ?? "authorization_code";
37
+ if (!state.ui && grantType !== "client_credentials") {
38
+ return {
39
+ status: "failed",
40
+ message: `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} in an interactive session.`,
41
+ };
42
+ }
43
+
44
+ try {
45
+ await authenticate(serverName, definition.url, definition);
46
+ return { status: "success" };
47
+ } catch (error) {
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ return {
50
+ status: "failed",
51
+ message: `OAuth authentication failed for "${serverName}": ${message}. Run /mcp-auth ${serverName} first.`,
52
+ };
53
+ }
54
+ }
55
+
13
56
  export function executeUiMessages(state: McpExtensionState): ProxyToolResult {
14
57
  const sessions = state.completedUiSessions;
15
58
 
@@ -376,12 +419,26 @@ export async function executeConnect(state: McpExtensionState, serverName: strin
376
419
  if (state.ui) {
377
420
  state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
378
421
  }
379
- const connection = await state.manager.connect(serverName, definition);
422
+ let connection = await state.manager.connect(serverName, definition);
380
423
  if (connection.status === "needs-auth") {
381
- return {
382
- content: [{ type: "text" as const, text: `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} first.` }],
383
- details: { mode: "connect", error: "auth_required", server: serverName },
384
- };
424
+ const autoAuth = await attemptAutoAuth(state, serverName);
425
+ if (autoAuth.status === "failed") {
426
+ return {
427
+ content: [{ type: "text" as const, text: autoAuth.message }],
428
+ details: { mode: "connect", error: "auth_required", server: serverName, message: autoAuth.message },
429
+ };
430
+ }
431
+ if (autoAuth.status === "success") {
432
+ await state.manager.close(serverName);
433
+ connection = await state.manager.connect(serverName, definition);
434
+ }
435
+ if (connection.status === "needs-auth") {
436
+ const message = getAuthRequiredMessage(serverName);
437
+ return {
438
+ content: [{ type: "text" as const, text: message }],
439
+ details: { mode: "connect", error: "auth_required", server: serverName, message },
440
+ };
441
+ }
385
442
  }
386
443
  const prefix = state.config.settings?.toolPrefix ?? "server";
387
444
  const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
@@ -409,6 +466,7 @@ export async function executeCall(
409
466
  ): Promise<ProxyToolResult> {
410
467
  let serverName: string | undefined = serverOverride;
411
468
  let toolMeta: ToolMetadata | undefined;
469
+ let autoAuthAttempted = false;
412
470
  const prefixMode = state.config.settings?.toolPrefix ?? "server";
413
471
 
414
472
  if (serverName && !state.config.mcpServers[serverName]) {
@@ -438,17 +496,48 @@ export async function executeCall(
438
496
  } else {
439
497
  const needsAuthConnection = state.manager.getConnection(serverName);
440
498
  if (needsAuthConnection?.status === "needs-auth") {
441
- return {
442
- content: [{ type: "text" as const, text: `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} first.` }],
443
- details: { mode: "call", error: "auth_required", server: serverName },
444
- };
499
+ if (!autoAuthAttempted) {
500
+ autoAuthAttempted = true;
501
+ const autoAuth = await attemptAutoAuth(state, serverName);
502
+ if (autoAuth.status === "failed") {
503
+ return {
504
+ content: [{ type: "text" as const, text: autoAuth.message }],
505
+ details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
506
+ };
507
+ }
508
+ if (autoAuth.status === "success") {
509
+ await state.manager.close(serverName);
510
+ state.failureTracker.delete(serverName);
511
+ const connectedAfterAuth = await lazyConnect(state, serverName);
512
+ if (connectedAfterAuth) {
513
+ toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
514
+ if (!toolMeta) {
515
+ return {
516
+ content: [{ type: "text" as const, text: `Tool "${toolName}" not found on "${serverName}" after reconnect.` }],
517
+ details: { mode: "call", error: "tool_not_found_after_reconnect", requestedTool: toolName },
518
+ };
519
+ }
520
+ }
521
+ }
522
+ }
523
+
524
+ if (!toolMeta && state.manager.getConnection(serverName)?.status === "needs-auth") {
525
+ const message = getAuthRequiredMessage(serverName);
526
+ return {
527
+ content: [{ type: "text" as const, text: message }],
528
+ details: { mode: "call", error: "auth_required", server: serverName, message },
529
+ };
530
+ }
445
531
  }
446
- const failedAgo = getFailureAgeSeconds(state, serverName);
447
- if (failedAgo !== null) {
448
- return {
449
- content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
450
- details: { mode: "call", error: "server_backoff", server: serverName },
451
- };
532
+
533
+ if (!toolMeta) {
534
+ const failedAgo = getFailureAgeSeconds(state, serverName);
535
+ if (failedAgo !== null) {
536
+ return {
537
+ content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
538
+ details: { mode: "call", error: "server_backoff", server: serverName },
539
+ };
540
+ }
452
541
  }
453
542
  }
454
543
  }
@@ -462,9 +551,27 @@ export async function executeCall(
462
551
  .sort((a, b) => b.prefix.length - a.prefix.length);
463
552
 
464
553
  for (const { name: configuredServer } of candidates) {
554
+ const existingConnection = state.manager.getConnection(configuredServer);
465
555
  const failedAgo = getFailureAgeSeconds(state, configuredServer);
466
- if (failedAgo !== null) continue;
467
- const connected = await lazyConnect(state, configuredServer);
556
+ if (failedAgo !== null && existingConnection?.status !== "needs-auth") continue;
557
+
558
+ let connected = await lazyConnect(state, configuredServer);
559
+ if (!connected && state.manager.getConnection(configuredServer)?.status === "needs-auth" && !autoAuthAttempted) {
560
+ autoAuthAttempted = true;
561
+ const autoAuth = await attemptAutoAuth(state, configuredServer);
562
+ if (autoAuth.status === "failed") {
563
+ return {
564
+ content: [{ type: "text" as const, text: autoAuth.message }],
565
+ details: { mode: "call", error: "auth_required", server: configuredServer, message: autoAuth.message },
566
+ };
567
+ }
568
+ if (autoAuth.status === "success") {
569
+ await state.manager.close(configuredServer);
570
+ state.failureTracker.delete(configuredServer);
571
+ connected = await lazyConnect(state, configuredServer);
572
+ }
573
+ }
574
+
468
575
  if (!connected) continue;
469
576
  if (!prefixMatchedServer) prefixMatchedServer = configuredServer;
470
577
  toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
@@ -492,10 +599,29 @@ export async function executeCall(
492
599
 
493
600
  let connection = state.manager.getConnection(serverName);
494
601
  if (connection?.status === "needs-auth") {
495
- return {
496
- content: [{ type: "text" as const, text: `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} first.` }],
497
- details: { mode: "call", error: "auth_required", server: serverName },
498
- };
602
+ if (!autoAuthAttempted) {
603
+ autoAuthAttempted = true;
604
+ const autoAuth = await attemptAutoAuth(state, serverName);
605
+ if (autoAuth.status === "failed") {
606
+ return {
607
+ content: [{ type: "text" as const, text: autoAuth.message }],
608
+ details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
609
+ };
610
+ }
611
+ if (autoAuth.status === "success") {
612
+ await state.manager.close(serverName);
613
+ state.failureTracker.delete(serverName);
614
+ connection = state.manager.getConnection(serverName);
615
+ }
616
+ }
617
+
618
+ if (connection?.status === "needs-auth") {
619
+ const message = getAuthRequiredMessage(serverName);
620
+ return {
621
+ content: [{ type: "text" as const, text: message }],
622
+ details: { mode: "call", error: "auth_required", server: serverName, message },
623
+ };
624
+ }
499
625
  }
500
626
  if (!connection || connection.status !== "connected") {
501
627
  const failedAgo = getFailureAgeSeconds(state, serverName);
@@ -520,10 +646,28 @@ export async function executeCall(
520
646
  }
521
647
  connection = await state.manager.connect(serverName, definition);
522
648
  if (connection.status === "needs-auth") {
523
- return {
524
- content: [{ type: "text" as const, text: `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} first.` }],
525
- details: { mode: "call", error: "auth_required", server: serverName },
526
- };
649
+ if (!autoAuthAttempted) {
650
+ autoAuthAttempted = true;
651
+ const autoAuth = await attemptAutoAuth(state, serverName);
652
+ if (autoAuth.status === "failed") {
653
+ return {
654
+ content: [{ type: "text" as const, text: autoAuth.message }],
655
+ details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
656
+ };
657
+ }
658
+ if (autoAuth.status === "success") {
659
+ await state.manager.close(serverName);
660
+ connection = await state.manager.connect(serverName, definition);
661
+ }
662
+ }
663
+
664
+ if (connection.status === "needs-auth") {
665
+ const message = getAuthRequiredMessage(serverName);
666
+ return {
667
+ content: [{ type: "text" as const, text: message }],
668
+ details: { mode: "call", error: "auth_required", server: serverName, message },
669
+ };
670
+ }
527
671
  }
528
672
  state.failureTracker.delete(serverName);
529
673
  updateServerMetadata(state, serverName);
package/tool-metadata.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
2
2
  import type { McpExtensionState } from "./state.js";
3
3
  import type { ToolMetadata, McpTool, McpResource, ServerEntry } from "./types.js";
4
- import { formatToolName } from "./types.js";
4
+ import { formatToolName, isToolExcluded } from "./types.js";
5
5
  import { resourceNameToToolName } from "./resource-tools.js";
6
6
  import { extractToolUiStreamMode } from "./utils.js";
7
7
 
@@ -20,6 +20,10 @@ export function buildToolMetadata(
20
20
  failedTools.push("(unnamed)");
21
21
  continue;
22
22
  }
23
+ if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) {
24
+ continue;
25
+ }
26
+
23
27
  let uiResourceUri: string | undefined;
24
28
  try {
25
29
  uiResourceUri = getToolUiResourceUri({ _meta: tool._meta });
@@ -39,6 +43,10 @@ export function buildToolMetadata(
39
43
  if (definition.exposeResources !== false) {
40
44
  for (const resource of resources) {
41
45
  const baseName = `get_${resourceNameToToolName(resource.name)}`;
46
+ if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) {
47
+ continue;
48
+ }
49
+
42
50
  metadata.push({
43
51
  name: formatToolName(baseName, serverName, prefix),
44
52
  originalName: baseName,
package/types.ts CHANGED
@@ -305,6 +305,8 @@ export interface ServerEntry {
305
305
  exposeResources?: boolean;
306
306
  // Direct tool registration
307
307
  directTools?: boolean | string[];
308
+ // Exclude specific MCP tools/resources by original or prefixed name
309
+ excludeTools?: string[];
308
310
  // Debug
309
311
  debug?: boolean; // Show server stderr (default: false)
310
312
  }
@@ -314,6 +316,8 @@ export interface McpSettings {
314
316
  toolPrefix?: "server" | "none" | "short";
315
317
  idleTimeout?: number; // minutes, default 10, 0 to disable
316
318
  directTools?: boolean;
319
+ disableProxyTool?: boolean;
320
+ autoAuth?: boolean;
317
321
  }
318
322
 
319
323
  // Root config
@@ -391,3 +395,32 @@ export function formatToolName(
391
395
  const p = getServerPrefix(serverName, prefix);
392
396
  return p ? `${p}_${toolName}` : toolName;
393
397
  }
398
+
399
+ function normalizeToolName(value: string): string {
400
+ return value.replace(/-/g, "_");
401
+ }
402
+
403
+ export function isToolExcluded(
404
+ toolName: string,
405
+ serverName: string,
406
+ prefix: "server" | "none" | "short",
407
+ excludeTools?: unknown
408
+ ): boolean {
409
+ if (!Array.isArray(excludeTools) || excludeTools.length === 0) return false;
410
+
411
+ const candidates = new Set<string>([
412
+ normalizeToolName(toolName),
413
+ normalizeToolName(formatToolName(toolName, serverName, prefix)),
414
+ normalizeToolName(formatToolName(toolName, serverName, "server")),
415
+ normalizeToolName(formatToolName(toolName, serverName, "short")),
416
+ ]);
417
+
418
+ for (const excluded of excludeTools) {
419
+ if (typeof excluded !== "string") continue;
420
+ if (candidates.has(normalizeToolName(excluded))) {
421
+ return true;
422
+ }
423
+ }
424
+
425
+ return false;
426
+ }
package/utils.ts CHANGED
@@ -1,25 +1,34 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { platform } from "node:os";
3
3
 
4
- export async function openUrl(pi: ExtensionAPI, url: string, browser?: string): Promise<void> {
4
+ async function execOpen(pi: ExtensionAPI, target: string, browser?: string) {
5
5
  const os = platform();
6
- let result;
7
6
 
8
7
  if (os === "darwin") {
9
- result = browser ? await pi.exec("open", ["-a", browser, url]) : await pi.exec("open", [url]);
10
- } else if (os === "win32") {
11
- result = browser
12
- ? await pi.exec("cmd", ["/c", "start", "", browser, url])
13
- : await pi.exec("cmd", ["/c", "start", "", url]);
14
- } else {
15
- result = browser ? await pi.exec(browser, [url]) : await pi.exec("xdg-open", [url]);
8
+ return browser ? pi.exec("open", ["-a", browser, target]) : pi.exec("open", [target]);
9
+ }
10
+ if (os === "win32") {
11
+ return browser
12
+ ? pi.exec("cmd", ["/c", "start", "", browser, target])
13
+ : pi.exec("cmd", ["/c", "start", "", target]);
16
14
  }
15
+ return browser ? pi.exec(browser, [target]) : pi.exec("xdg-open", [target]);
16
+ }
17
17
 
18
+ export async function openUrl(pi: ExtensionAPI, url: string, browser?: string): Promise<void> {
19
+ const result = await execOpen(pi, url, browser);
18
20
  if (result.code !== 0) {
19
21
  throw new Error(result.stderr || `Failed to open browser (exit code ${result.code})`);
20
22
  }
21
23
  }
22
24
 
25
+ export async function openPath(pi: ExtensionAPI, targetPath: string): Promise<void> {
26
+ const result = await execOpen(pi, targetPath);
27
+ if (result.code !== 0) {
28
+ throw new Error(result.stderr || `Failed to open path (exit code ${result.code})`);
29
+ }
30
+ }
31
+
23
32
  export async function parallelLimit<T, R>(
24
33
  items: T[],
25
34
  limit: number,