pi-mcp-adapter 2.5.4 → 2.6.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 +23 -0
- package/README.md +10 -3
- package/banner.png +0 -0
- package/commands.ts +130 -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 +32 -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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.6.1] - 2026-05-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added `/mcp logout <server>` to clear stored OAuth credentials and disconnect the server. Thanks @mattzcarey for PR #96.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Cancel pending OAuth callbacks when logging out of an MCP server.
|
|
17
|
+
|
|
18
|
+
## [2.6.0] - 2026-05-10
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Added a no-argument `/mcp-auth` OAuth picker and in-panel auth shortcut for OAuth-capable MCP servers.
|
|
22
|
+
- Added compact collapsed rendering for MCP proxy and direct-tool result rows while keeping full tool results available when expanded.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Migrated Pi runtime dependencies and imports from deprecated `@mariozechner/*` packages to `@earendil-works/*` packages.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Re-register dynamic OAuth clients during fresh auth when cached DCR client info exists without tokens, avoiding dead authorization URLs after server-side client invalidation.
|
|
29
|
+
- Kept OAuth tokens, dynamic client info, PKCE verifiers, and OAuth state bound to the server URL so stale credentials cannot be reused after a server URL changes.
|
|
30
|
+
- Kept the `/mcp-auth` OAuth picker search focused on OAuth server rows and prevented hidden panel shortcuts from unexpectedly launching auth.
|
|
31
|
+
- Kept long MCP error results expanded in compact tool result rendering.
|
|
32
|
+
|
|
10
33
|
## [2.5.4] - 2026-05-04
|
|
11
34
|
|
|
12
35
|
### Changed
|
package/README.md
CHANGED
|
@@ -236,7 +236,7 @@ Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/
|
|
|
236
236
|
|
|
237
237
|
When you change direct-tool toggles in `/mcp` or write new config through `/mcp setup`, the extension triggers Pi's normal reload flow automatically. That refreshes extensions, prompts, skills, and MCP tool registration in one shot, so newly configured direct tools can appear without a manual restart.
|
|
238
238
|
|
|
239
|
-
**Interactive configuration:** Run `/mcp` to open an interactive panel showing all servers with connection status, tools, and direct/proxy toggles. You can reconnect servers
|
|
239
|
+
**Interactive configuration:** Run `/mcp` to open an interactive panel showing all servers with connection status, tools, and direct/proxy toggles. You can reconnect servers and toggle tools between direct and proxy from the same overlay. For OAuth, press Enter on a server that needs auth or `ctrl+a` on any OAuth server.
|
|
240
240
|
|
|
241
241
|
**Guided first-run setup:** Run `/mcp setup` to inspect detected shared MCP files, adopt compatibility imports from other hosts, open discovered config paths, preview exact before/after file diffs for writes, scaffold a minimal project `.mcp.json`, or quick-add RepoPrompt into a standard/shared MCP file.
|
|
242
242
|
|
|
@@ -332,6 +332,8 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only
|
|
|
332
332
|
| Connect | `mcp({ connect: "server-name" })` |
|
|
333
333
|
| UI messages | `mcp({ action: "ui-messages" })` |
|
|
334
334
|
|
|
335
|
+
MCP proxy and direct-tool results render compactly by default: long text shows the first three lines plus a `Ctrl+O to expand` hint, while the full result remains available when expanded and is still returned unchanged to the model.
|
|
336
|
+
|
|
335
337
|
Search includes both MCP tools and Pi tools (from extensions). Pi tools appear first with `[pi tool]` prefix. Space-separated words are OR'd.
|
|
336
338
|
|
|
337
339
|
Tool names are fuzzy-matched on hyphens and underscores — `context7_resolve_library_id` finds `context7_resolve-library-id`.
|
|
@@ -345,9 +347,13 @@ Tool names are fuzzy-matched on hyphens and underscores — `context7_resolve_li
|
|
|
345
347
|
| `/mcp tools` | List all tools |
|
|
346
348
|
| `/mcp reconnect` | Reconnect all servers |
|
|
347
349
|
| `/mcp reconnect <server>` | Connect or reconnect a single server |
|
|
348
|
-
| `/mcp
|
|
350
|
+
| `/mcp logout <server>` | Clear stored OAuth credentials for a server and disconnect it |
|
|
351
|
+
| `/mcp-auth` | Open an OAuth server picker in interactive UI sessions |
|
|
352
|
+
| `/mcp-auth <server>` | OAuth setup for a specific server |
|
|
353
|
+
|
|
354
|
+
If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`, and direct tool calls automatically run OAuth when needed and retry once.
|
|
349
355
|
|
|
350
|
-
|
|
356
|
+
In interactive sessions, you can also authenticate from `/mcp` with `ctrl+a` or Enter on a server that needs auth. In non-interactive sessions, browser-based OAuth still requires `/mcp-auth <server>`. `/mcp-auth` without a server only opens a picker in the interactive UI.
|
|
351
357
|
|
|
352
358
|
## How It Works
|
|
353
359
|
|
|
@@ -363,4 +369,5 @@ If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`,
|
|
|
363
369
|
## Limitations
|
|
364
370
|
|
|
365
371
|
- Cross-session server sharing not yet implemented (each Pi session runs its own server processes)
|
|
372
|
+
- Compact MCP result rendering summarizes text, but inline images are still controlled by Pi's image display settings and may render below the compact text summary.
|
|
366
373
|
- MCP sampling support is text-only; context inclusion, tools, stop sequences, audio, and image content are rejected with explicit errors.
|
package/banner.png
ADDED
|
Binary file
|
package/commands.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
2
|
-
import type { McpExtensionState } from "./state.
|
|
3
|
-
import type { McpConfig, ServerEntry, McpPanelCallbacks, McpPanelResult, ImportKind } from "./types.
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.ts";
|
|
3
|
+
import type { McpAuthResult, McpConfig, ServerEntry, McpPanelCallbacks, McpPanelResult, ImportKind } from "./types.ts";
|
|
4
4
|
import {
|
|
5
5
|
ensureCompatibilityImports,
|
|
6
6
|
getMcpDiscoverySummary,
|
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
writeDirectToolsConfig,
|
|
12
12
|
writeSharedServerEntry,
|
|
13
13
|
writeStarterProjectConfig,
|
|
14
|
-
} from "./config.
|
|
15
|
-
import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.
|
|
16
|
-
import { loadMetadataCache } from "./metadata-cache.
|
|
17
|
-
import { buildToolMetadata } from "./tool-metadata.
|
|
18
|
-
import { supportsOAuth, authenticate } from "./mcp-auth-flow.
|
|
19
|
-
import {
|
|
20
|
-
import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.
|
|
21
|
-
import { openPath } from "./utils.
|
|
14
|
+
} from "./config.ts";
|
|
15
|
+
import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.ts";
|
|
16
|
+
import { loadMetadataCache } from "./metadata-cache.ts";
|
|
17
|
+
import { buildToolMetadata } from "./tool-metadata.ts";
|
|
18
|
+
import { supportsOAuth, authenticate, removeAuth } from "./mcp-auth-flow.ts";
|
|
19
|
+
import { getAuthForUrl } from "./mcp-auth.ts";
|
|
20
|
+
import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.ts";
|
|
21
|
+
import { openPath } from "./utils.ts";
|
|
22
22
|
|
|
23
23
|
export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
|
|
24
24
|
if (!ctx.hasUI) return;
|
|
@@ -140,30 +140,30 @@ export async function authenticateServer(
|
|
|
140
140
|
serverName: string,
|
|
141
141
|
config: McpConfig,
|
|
142
142
|
ctx: ExtensionContext
|
|
143
|
-
): Promise<
|
|
144
|
-
if (!ctx.hasUI) return;
|
|
143
|
+
): Promise<McpAuthResult> {
|
|
144
|
+
if (!ctx.hasUI) return { ok: false, message: "OAuth authentication requires an interactive session." };
|
|
145
145
|
|
|
146
146
|
const definition = config.mcpServers[serverName];
|
|
147
147
|
if (!definition) {
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const message = `Server "${serverName}" not found in config`;
|
|
149
|
+
ctx.ui.notify(message, "error");
|
|
150
|
+
return { ok: false, message };
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
if (!supportsOAuth(definition)) {
|
|
154
|
+
const message = `Server "${serverName}" does not use OAuth authentication. Set "auth": "oauth" or omit auth for auto-detection.`;
|
|
153
155
|
ctx.ui.notify(
|
|
154
156
|
`Server "${serverName}" does not use OAuth authentication.\n` +
|
|
155
157
|
`Set "auth": "oauth" or omit auth for auto-detection.`,
|
|
156
158
|
"error"
|
|
157
159
|
);
|
|
158
|
-
return;
|
|
160
|
+
return { ok: false, message };
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
if (!definition.url) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
);
|
|
166
|
-
return;
|
|
164
|
+
const message = `Server "${serverName}" has no URL configured (OAuth requires HTTP transport)`;
|
|
165
|
+
ctx.ui.notify(message, "error");
|
|
166
|
+
return { ok: false, message };
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
try {
|
|
@@ -171,31 +171,54 @@ export async function authenticateServer(
|
|
|
171
171
|
const status = await authenticate(serverName, definition.url, definition);
|
|
172
172
|
|
|
173
173
|
if (status === "authenticated") {
|
|
174
|
+
const message = `OAuth authentication successful for "${serverName}"! Run /mcp reconnect ${serverName} to connect with the new token.`;
|
|
174
175
|
ctx.ui.notify(
|
|
175
176
|
`OAuth authentication successful for "${serverName}"!\n` +
|
|
176
177
|
`Run /mcp reconnect ${serverName} to connect with the new token.`,
|
|
177
|
-
"
|
|
178
|
-
);
|
|
179
|
-
} else {
|
|
180
|
-
ctx.ui.notify(
|
|
181
|
-
`OAuth authentication failed for "${serverName}".`,
|
|
182
|
-
"error"
|
|
178
|
+
"info"
|
|
183
179
|
);
|
|
180
|
+
return { ok: true, message };
|
|
184
181
|
}
|
|
182
|
+
|
|
183
|
+
const message = `OAuth authentication failed for "${serverName}".`;
|
|
184
|
+
ctx.ui.notify(message, "error");
|
|
185
|
+
return { ok: false, message };
|
|
185
186
|
} catch (error) {
|
|
186
187
|
const message = error instanceof Error ? error.message : String(error);
|
|
187
188
|
ctx.ui.notify(`Failed to authenticate "${serverName}": ${message}`, "error");
|
|
189
|
+
return { ok: false, message };
|
|
188
190
|
} finally {
|
|
189
191
|
ctx.ui.setStatus("mcp-auth", undefined);
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
194
|
|
|
195
|
+
export async function logoutServer(
|
|
196
|
+
serverName: string,
|
|
197
|
+
state: McpExtensionState,
|
|
198
|
+
ctx: ExtensionContext
|
|
199
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
200
|
+
const definition = state.config.mcpServers[serverName];
|
|
201
|
+
if (!definition) {
|
|
202
|
+
const message = `Server "${serverName}" not found in config`;
|
|
203
|
+
if (ctx.hasUI) ctx.ui.notify(message, "error");
|
|
204
|
+
return { ok: false, message };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await removeAuth(serverName);
|
|
208
|
+
await state.manager.close(serverName);
|
|
209
|
+
updateStatusBar(state);
|
|
210
|
+
|
|
211
|
+
const message = `OAuth credentials cleared for "${serverName}". Run /mcp-auth ${serverName} to authenticate again.`;
|
|
212
|
+
if (ctx.hasUI) ctx.ui.notify(message, "info");
|
|
213
|
+
return { ok: true, message };
|
|
214
|
+
}
|
|
215
|
+
|
|
193
216
|
export interface PanelFlowResult {
|
|
194
217
|
configChanged: boolean;
|
|
195
218
|
}
|
|
196
219
|
|
|
197
|
-
function buildSharedConfigNoticeLines(configOverridePath
|
|
198
|
-
const discovery = getMcpDiscoverySummary(configOverridePath);
|
|
220
|
+
function buildSharedConfigNoticeLines(configOverridePath: string | undefined, cwd: string): { lines: string[]; fingerprint: string | null } {
|
|
221
|
+
const discovery = getMcpDiscoverySummary(configOverridePath, cwd);
|
|
199
222
|
const onboardingState = loadOnboardingState();
|
|
200
223
|
if (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {
|
|
201
224
|
return { lines: [], fingerprint: null };
|
|
@@ -221,16 +244,16 @@ export async function openMcpSetup(
|
|
|
221
244
|
): Promise<PanelFlowResult> {
|
|
222
245
|
if (!ctx.hasUI) return { configChanged: false };
|
|
223
246
|
|
|
224
|
-
const discovery = getMcpDiscoverySummary(configOverridePath);
|
|
247
|
+
const discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd);
|
|
225
248
|
const onboardingState = loadOnboardingState();
|
|
226
|
-
const { createMcpSetupPanel } = await import("./mcp-setup-panel.
|
|
249
|
+
const { createMcpSetupPanel } = await import("./mcp-setup-panel.ts");
|
|
227
250
|
let configChanged = false;
|
|
228
251
|
|
|
229
252
|
const callbacks = {
|
|
230
253
|
previewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),
|
|
231
|
-
previewStarterProject: () => previewStarterProjectConfig(),
|
|
254
|
+
previewStarterProject: () => previewStarterProjectConfig(ctx.cwd),
|
|
232
255
|
previewRepoPrompt: () => {
|
|
233
|
-
const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
|
|
256
|
+
const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd).repoPrompt;
|
|
234
257
|
if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;
|
|
235
258
|
return previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
|
|
236
259
|
},
|
|
@@ -240,12 +263,12 @@ export async function openMcpSetup(
|
|
|
240
263
|
return result;
|
|
241
264
|
},
|
|
242
265
|
scaffoldProjectConfig: async () => {
|
|
243
|
-
const path = writeStarterProjectConfig();
|
|
266
|
+
const path = writeStarterProjectConfig(ctx.cwd);
|
|
244
267
|
configChanged = true;
|
|
245
268
|
return { path };
|
|
246
269
|
},
|
|
247
270
|
addRepoPrompt: async () => {
|
|
248
|
-
const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
|
|
271
|
+
const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd).repoPrompt;
|
|
249
272
|
if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {
|
|
250
273
|
throw new Error("RepoPrompt is not available to add from this setup screen.");
|
|
251
274
|
}
|
|
@@ -265,7 +288,7 @@ export async function openMcpSetup(
|
|
|
265
288
|
ctx.ui.custom(
|
|
266
289
|
(tui, _theme, _keybindings, done) => {
|
|
267
290
|
return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState }, tui, () => {
|
|
268
|
-
done();
|
|
291
|
+
done(undefined);
|
|
269
292
|
resolve({ configChanged });
|
|
270
293
|
});
|
|
271
294
|
},
|
|
@@ -274,25 +297,18 @@ export async function openMcpSetup(
|
|
|
274
297
|
});
|
|
275
298
|
}
|
|
276
299
|
|
|
277
|
-
|
|
300
|
+
function buildMcpPanelCallbacks(
|
|
278
301
|
state: McpExtensionState,
|
|
279
|
-
|
|
302
|
+
config: McpConfig,
|
|
280
303
|
ctx: ExtensionContext,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const config = state.config;
|
|
288
|
-
const cache = loadMetadataCache();
|
|
289
|
-
const provenanceMap = getServerProvenance(pi.getFlag("mcp-config") as string | undefined ?? configOverridePath);
|
|
290
|
-
const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(pi.getFlag("mcp-config") as string | undefined ?? configOverridePath);
|
|
291
|
-
|
|
292
|
-
const callbacks: McpPanelCallbacks = {
|
|
293
|
-
reconnect: async (serverName: string) => {
|
|
294
|
-
return lazyConnect(state, serverName);
|
|
304
|
+
): McpPanelCallbacks {
|
|
305
|
+
return {
|
|
306
|
+
reconnect: (serverName: string) => lazyConnect(state, serverName),
|
|
307
|
+
canAuthenticate: (serverName: string) => {
|
|
308
|
+
const definition = config.mcpServers[serverName];
|
|
309
|
+
return definition ? supportsOAuth(definition) : false;
|
|
295
310
|
},
|
|
311
|
+
authenticate: (serverName: string) => authenticateServer(serverName, config, ctx),
|
|
296
312
|
getConnectionStatus: (serverName: string) => {
|
|
297
313
|
const definition = config.mcpServers[serverName];
|
|
298
314
|
const connection = state.manager.getConnection(serverName);
|
|
@@ -301,9 +317,10 @@ export async function openMcpPanel(
|
|
|
301
317
|
}
|
|
302
318
|
if (
|
|
303
319
|
definition?.auth === "oauth"
|
|
320
|
+
&& definition.url
|
|
304
321
|
&& definition.oauth !== false
|
|
305
322
|
&& definition.oauth?.grantType !== "client_credentials"
|
|
306
|
-
&& !
|
|
323
|
+
&& !getAuthForUrl(serverName, definition.url)?.tokens
|
|
307
324
|
) {
|
|
308
325
|
return "needs-auth";
|
|
309
326
|
}
|
|
@@ -316,8 +333,27 @@ export async function openMcpPanel(
|
|
|
316
333
|
return freshCache?.servers?.[serverName] ?? null;
|
|
317
334
|
},
|
|
318
335
|
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function openMcpPanel(
|
|
339
|
+
state: McpExtensionState,
|
|
340
|
+
pi: ExtensionAPI,
|
|
341
|
+
ctx: ExtensionContext,
|
|
342
|
+
configOverridePath?: string,
|
|
343
|
+
): Promise<PanelFlowResult> {
|
|
344
|
+
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
345
|
+
return openMcpSetup(state, pi, ctx, configOverridePath, "empty");
|
|
346
|
+
}
|
|
319
347
|
|
|
320
|
-
const
|
|
348
|
+
const config = state.config;
|
|
349
|
+
const cache = loadMetadataCache();
|
|
350
|
+
const configPath = pi.getFlag("mcp-config") as string | undefined ?? configOverridePath;
|
|
351
|
+
const provenanceMap = getServerProvenance(configPath, ctx.cwd);
|
|
352
|
+
const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);
|
|
353
|
+
|
|
354
|
+
const callbacks = buildMcpPanelCallbacks(state, config, ctx);
|
|
355
|
+
|
|
356
|
+
const { createMcpPanel } = await import("./mcp-panel.ts");
|
|
321
357
|
let configChanged = false;
|
|
322
358
|
|
|
323
359
|
await new Promise<void>((resolve) => {
|
|
@@ -329,7 +365,7 @@ export async function openMcpPanel(
|
|
|
329
365
|
configChanged = true;
|
|
330
366
|
ctx.ui.notify("Direct tools updated. Pi will reload after this panel closes.", "info");
|
|
331
367
|
}
|
|
332
|
-
done();
|
|
368
|
+
done(undefined);
|
|
333
369
|
resolve();
|
|
334
370
|
}, { noticeLines });
|
|
335
371
|
},
|
|
@@ -343,3 +379,42 @@ export async function openMcpPanel(
|
|
|
343
379
|
|
|
344
380
|
return { configChanged };
|
|
345
381
|
}
|
|
382
|
+
|
|
383
|
+
export async function openMcpAuthPanel(
|
|
384
|
+
state: McpExtensionState,
|
|
385
|
+
pi: ExtensionAPI,
|
|
386
|
+
ctx: ExtensionContext,
|
|
387
|
+
configOverridePath?: string,
|
|
388
|
+
): Promise<PanelFlowResult> {
|
|
389
|
+
if (!ctx.hasUI) return { configChanged: false };
|
|
390
|
+
|
|
391
|
+
const config = state.config;
|
|
392
|
+
const oauthServers = Object.entries(config.mcpServers).filter(([, definition]) => supportsOAuth(definition));
|
|
393
|
+
if (oauthServers.length === 0) {
|
|
394
|
+
ctx.ui.notify("No OAuth-capable MCP servers are configured.", "warning");
|
|
395
|
+
return { configChanged: false };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const cache = loadMetadataCache();
|
|
399
|
+
const configPath = pi.getFlag("mcp-config") as string | undefined ?? configOverridePath;
|
|
400
|
+
const provenanceMap = getServerProvenance(configPath, ctx.cwd);
|
|
401
|
+
const callbacks = buildMcpPanelCallbacks(state, config, ctx);
|
|
402
|
+
const { createMcpPanel } = await import("./mcp-panel.ts");
|
|
403
|
+
|
|
404
|
+
await new Promise<void>((resolve) => {
|
|
405
|
+
ctx.ui.custom(
|
|
406
|
+
(tui, _theme, _keybindings, done) => {
|
|
407
|
+
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, () => {
|
|
408
|
+
done(undefined);
|
|
409
|
+
resolve();
|
|
410
|
+
}, {
|
|
411
|
+
authOnly: true,
|
|
412
|
+
noticeLines: ["Select an OAuth MCP server and press Enter or ctrl+a to authenticate."],
|
|
413
|
+
});
|
|
414
|
+
},
|
|
415
|
+
{ overlay: true, overlayOptions: { anchor: "center", width: 82 } },
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
return { configChanged: false };
|
|
420
|
+
}
|
package/config.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { getAgentPath } from "./agent-dir.
|
|
6
|
-
import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.
|
|
5
|
+
import { getAgentPath } from "./agent-dir.ts";
|
|
6
|
+
import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
9
9
|
const PROJECT_CONFIG_NAME = ".mcp.json";
|
|
@@ -105,8 +105,8 @@ export function getProjectPiConfigPath(cwd = process.cwd()): string {
|
|
|
105
105
|
return resolve(cwd, PROJECT_PI_CONFIG_NAME);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function getConfigDiscoveryPaths(overridePath?: string): ConfigDiscoveryPath[] {
|
|
109
|
-
return getConfigSources(overridePath).map((source) => ({
|
|
108
|
+
export function getConfigDiscoveryPaths(overridePath?: string, cwd = process.cwd()): ConfigDiscoveryPath[] {
|
|
109
|
+
return getConfigSources(overridePath, cwd).map((source) => ({
|
|
110
110
|
label: source.label,
|
|
111
111
|
path: source.readPath,
|
|
112
112
|
exists: existsSync(source.readPath),
|
|
@@ -180,13 +180,13 @@ export function getMcpDiscoverySummary(overridePath?: string, cwd = process.cwd(
|
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
export function loadMcpConfig(overridePath?: string): McpConfig {
|
|
183
|
+
export function loadMcpConfig(overridePath?: string, cwd = process.cwd()): McpConfig {
|
|
184
184
|
let config: McpConfig = { mcpServers: {} };
|
|
185
185
|
|
|
186
|
-
for (const source of getConfigSources(overridePath)) {
|
|
186
|
+
for (const source of getConfigSources(overridePath, cwd)) {
|
|
187
187
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
188
188
|
if (!loaded) continue;
|
|
189
|
-
config = mergeConfigs(config, expandImports(loaded));
|
|
189
|
+
config = mergeConfigs(config, expandImports(loaded, cwd));
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
return config;
|
|
@@ -262,12 +262,12 @@ function mergeImports(left: ImportKind[] | undefined, right: ImportKind[] | unde
|
|
|
262
262
|
return [...new Set(merged)];
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
function expandImports(config: McpConfig): McpConfig {
|
|
265
|
+
function expandImports(config: McpConfig, cwd = process.cwd()): McpConfig {
|
|
266
266
|
if (!config.imports?.length) return config;
|
|
267
267
|
|
|
268
268
|
const importedServers: Record<string, ServerEntry> = {};
|
|
269
269
|
for (const importKind of config.imports) {
|
|
270
|
-
const importPath = resolveImportPath(importKind);
|
|
270
|
+
const importPath = resolveImportPath(importKind, cwd);
|
|
271
271
|
if (!importPath) continue;
|
|
272
272
|
|
|
273
273
|
try {
|
|
@@ -591,17 +591,17 @@ export function writeSharedServerEntry(filePath: string, serverName: string, ent
|
|
|
591
591
|
return filePath;
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
export function getServerProvenance(overridePath?: string): Map<string, ServerProvenance> {
|
|
594
|
+
export function getServerProvenance(overridePath?: string, cwd = process.cwd()): Map<string, ServerProvenance> {
|
|
595
595
|
const provenance = new Map<string, ServerProvenance>();
|
|
596
596
|
const userPath = getPiGlobalConfigPath(overridePath);
|
|
597
597
|
|
|
598
|
-
for (const source of getConfigSources(overridePath)) {
|
|
598
|
+
for (const source of getConfigSources(overridePath, cwd)) {
|
|
599
599
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
600
600
|
if (!loaded) continue;
|
|
601
601
|
|
|
602
602
|
if (loaded.imports?.length) {
|
|
603
603
|
for (const importKind of loaded.imports) {
|
|
604
|
-
const importPath = resolveImportPath(importKind);
|
|
604
|
+
const importPath = resolveImportPath(importKind, cwd);
|
|
605
605
|
if (!importPath) continue;
|
|
606
606
|
|
|
607
607
|
try {
|
package/consent-manager.ts
CHANGED
package/direct-tools.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { McpExtensionState } from "./state.
|
|
3
|
-
import type { DirectToolSpec, McpConfig, McpContent } from "./types.
|
|
4
|
-
import type { MetadataCache } from "./metadata-cache.
|
|
5
|
-
import { lazyConnect, getFailureAgeSeconds } from "./init.
|
|
6
|
-
import { isServerCacheValid } from "./metadata-cache.
|
|
7
|
-
import { formatSchema } from "./tool-metadata.
|
|
8
|
-
import { transformMcpContent } from "./tool-registrar.
|
|
9
|
-
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.
|
|
10
|
-
import { formatToolName, isToolExcluded } from "./types.
|
|
11
|
-
import { resourceNameToToolName } from "./resource-tools.
|
|
12
|
-
import { authenticate, supportsOAuth } from "./mcp-auth-flow.
|
|
13
|
-
import { formatAuthRequiredMessage } from "./utils.
|
|
1
|
+
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.ts";
|
|
3
|
+
import type { DirectToolSpec, McpConfig, McpContent } from "./types.ts";
|
|
4
|
+
import type { MetadataCache } from "./metadata-cache.ts";
|
|
5
|
+
import { lazyConnect, getFailureAgeSeconds } from "./init.ts";
|
|
6
|
+
import { isServerCacheValid } from "./metadata-cache.ts";
|
|
7
|
+
import { formatSchema } from "./tool-metadata.ts";
|
|
8
|
+
import { transformMcpContent } from "./tool-registrar.ts";
|
|
9
|
+
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.ts";
|
|
10
|
+
import { formatToolName, isToolExcluded } from "./types.ts";
|
|
11
|
+
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
12
|
+
import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
13
|
+
import { formatAuthRequiredMessage } from "./utils.ts";
|
|
14
14
|
|
|
15
15
|
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
16
16
|
|
|
@@ -48,7 +48,7 @@ async function attemptDirectAutoAuth(
|
|
|
48
48
|
return { status: "skipped" };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const grantType = definition.oauth
|
|
51
|
+
const grantType = definition.oauth ? definition.oauth.grantType ?? "authorization_code" : "authorization_code";
|
|
52
52
|
if (!state.ui && grantType !== "client_credentials") {
|
|
53
53
|
return {
|
|
54
54
|
status: "failed",
|
|
@@ -260,7 +260,13 @@ export function buildProxyDescription(
|
|
|
260
260
|
return desc;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
type DirectToolExecute =
|
|
263
|
+
type DirectToolExecute = (
|
|
264
|
+
toolCallId: string,
|
|
265
|
+
params: Record<string, unknown>,
|
|
266
|
+
signal: AbortSignal | undefined,
|
|
267
|
+
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
268
|
+
ctx: ExtensionContext,
|
|
269
|
+
) => Promise<AgentToolResult<Record<string, unknown>>>;
|
|
264
270
|
|
|
265
271
|
export function createDirectToolExecutor(
|
|
266
272
|
getState: () => McpExtensionState | null,
|
package/host-html-template.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.
|
|
1
|
+
import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.ts";
|
|
2
2
|
|
|
3
3
|
// Use locally bundled AppBridge to avoid CDN Zod bundling issues
|
|
4
4
|
const DEFAULT_APP_BRIDGE_MODULE_URL = "/app-bridge.bundle.js";
|
package/index.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import type { ExtensionAPI, ToolInfo } from "@
|
|
2
|
-
import type { McpExtensionState } from "./state.
|
|
1
|
+
import type { ExtensionAPI, ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.ts";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import { showStatus, showTools, reconnectServers, authenticateServer, openMcpPanel, openMcpSetup } from "./commands.
|
|
5
|
-
import { loadMcpConfig } from "./config.
|
|
6
|
-
import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.
|
|
7
|
-
import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.
|
|
8
|
-
import { loadMetadataCache } from "./metadata-cache.
|
|
9
|
-
import { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.
|
|
10
|
-
import { getConfigPathFromArgv, truncateAtWord } from "./utils.
|
|
11
|
-
import { initializeOAuth, shutdownOAuth } from "./mcp-auth-flow.
|
|
4
|
+
import { showStatus, showTools, reconnectServers, authenticateServer, logoutServer, openMcpAuthPanel, openMcpPanel, openMcpSetup } from "./commands.ts";
|
|
5
|
+
import { loadMcpConfig } from "./config.ts";
|
|
6
|
+
import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.ts";
|
|
7
|
+
import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.ts";
|
|
8
|
+
import { loadMetadataCache } from "./metadata-cache.ts";
|
|
9
|
+
import { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.ts";
|
|
10
|
+
import { getConfigPathFromArgv, truncateAtWord } from "./utils.ts";
|
|
11
|
+
import { initializeOAuth, shutdownOAuth } from "./mcp-auth-flow.ts";
|
|
12
|
+
import { renderMcpToolResult } from "./tool-result-renderer.ts";
|
|
12
13
|
|
|
13
14
|
export default function mcpAdapter(pi: ExtensionAPI) {
|
|
14
15
|
let state: McpExtensionState | null = null;
|
|
@@ -66,13 +67,14 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
66
67
|
|| missingConfiguredDirectToolServers.length > 0;
|
|
67
68
|
|
|
68
69
|
for (const spec of directSpecs) {
|
|
69
|
-
pi.registerTool({
|
|
70
|
+
(pi.registerTool as (tool: unknown) => unknown)({
|
|
70
71
|
name: spec.prefixedName,
|
|
71
72
|
label: `MCP: ${spec.originalName}`,
|
|
72
73
|
description: spec.description || "(no description)",
|
|
73
74
|
promptSnippet: truncateAtWord(spec.description, 100) || `MCP tool from ${spec.serverName}`,
|
|
74
|
-
parameters: Type.Unsafe
|
|
75
|
+
parameters: Type.Unsafe((spec.inputSchema || { type: "object", properties: {} }) as never),
|
|
75
76
|
execute: createDirectToolExecutor(() => state, () => initPromise, spec),
|
|
77
|
+
renderResult: renderMcpToolResult,
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -170,6 +172,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
170
172
|
const parts = args?.trim()?.split(/\s+/) ?? [];
|
|
171
173
|
const subcommand = parts[0] ?? "";
|
|
172
174
|
const targetServer = parts[1];
|
|
175
|
+
const rest = parts.slice(1).join(" ");
|
|
173
176
|
|
|
174
177
|
switch (subcommand) {
|
|
175
178
|
case "reconnect":
|
|
@@ -186,6 +189,15 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
186
189
|
}
|
|
187
190
|
break;
|
|
188
191
|
}
|
|
192
|
+
case "logout": {
|
|
193
|
+
const serverName = rest;
|
|
194
|
+
if (!serverName) {
|
|
195
|
+
if (ctx.hasUI) ctx.ui.notify("Usage: /mcp logout <server>", "error");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await logoutServer(serverName, state, ctx);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
189
201
|
case "status":
|
|
190
202
|
case "":
|
|
191
203
|
default:
|
|
@@ -207,8 +219,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
207
219
|
description: "Authenticate with an MCP server (OAuth)",
|
|
208
220
|
handler: async (args, ctx) => {
|
|
209
221
|
const serverName = args?.trim();
|
|
210
|
-
if (!serverName) {
|
|
211
|
-
if (ctx.hasUI) ctx.ui.notify("Usage: /mcp-auth <server-name>", "error");
|
|
222
|
+
if (!serverName && !ctx.hasUI) {
|
|
212
223
|
return;
|
|
213
224
|
}
|
|
214
225
|
|
|
@@ -226,12 +237,17 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
226
237
|
return;
|
|
227
238
|
}
|
|
228
239
|
|
|
240
|
+
if (!serverName) {
|
|
241
|
+
await openMcpAuthPanel(state, pi, ctx, earlyConfigPath);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
229
245
|
await authenticateServer(serverName, state.config, ctx);
|
|
230
246
|
},
|
|
231
247
|
});
|
|
232
248
|
|
|
233
249
|
if (shouldRegisterProxyTool) {
|
|
234
|
-
pi.registerTool({
|
|
250
|
+
(pi.registerTool as (tool: unknown) => unknown)({
|
|
235
251
|
name: "mcp",
|
|
236
252
|
label: "MCP",
|
|
237
253
|
description: buildProxyDescription(earlyConfig, earlyCache, directSpecs),
|
|
@@ -247,6 +263,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
247
263
|
server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
|
|
248
264
|
action: Type.Optional(Type.String({ description: "Action: 'ui-messages' to retrieve prompts/intents from UI sessions" })),
|
|
249
265
|
}),
|
|
266
|
+
renderResult: renderMcpToolResult,
|
|
250
267
|
async execute(_toolCallId, params: {
|
|
251
268
|
tool?: string;
|
|
252
269
|
args?: string;
|