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/CHANGELOG.md +26 -0
- package/README.md +74 -7
- package/cli.js +152 -86
- package/commands.ts +116 -9
- package/config.ts +566 -126
- package/direct-tools.ts +97 -6
- package/index.ts +97 -78
- package/init.ts +3 -12
- package/mcp-auth-flow.ts +61 -40
- package/mcp-panel.ts +51 -14
- package/mcp-setup-panel.ts +576 -0
- package/metadata-cache.ts +12 -3
- package/onboarding-state.ts +68 -0
- package/package.json +3 -1
- package/proxy-modes.ts +169 -25
- package/tool-metadata.ts +9 -1
- package/types.ts +33 -0
- package/utils.ts +18 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-adapter",
|
|
3
|
-
"version": "2.
|
|
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
|
-
|
|
422
|
+
let connection = await state.manager.connect(serverName, definition);
|
|
380
423
|
if (connection.status === "needs-auth") {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,
|