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 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, initiate OAuth, and toggle tools between direct and proxy — all from one overlay.
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-auth <server>` | OAuth setup |
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
- If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`, and direct tool calls will automatically run OAuth when needed and retry once. In non-interactive sessions, browser-based OAuth still requires running `/mcp-auth <server>` manually.
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 "@mariozechner/pi-coding-agent";
2
- import type { McpExtensionState } from "./state.js";
3
- import type { McpConfig, ServerEntry, McpPanelCallbacks, McpPanelResult, ImportKind } from "./types.js";
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.js";
15
- import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.js";
16
- import { loadMetadataCache } from "./metadata-cache.js";
17
- import { buildToolMetadata } from "./tool-metadata.js";
18
- import { supportsOAuth, authenticate } from "./mcp-auth-flow.js";
19
- import { hasStoredTokens } from "./mcp-auth.js";
20
- import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.js";
21
- import { openPath } from "./utils.js";
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<void> {
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
- ctx.ui.notify(`Server "${serverName}" not found in config`, "error");
149
- return;
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
- ctx.ui.notify(
163
- `Server "${serverName}" has no URL configured (OAuth requires HTTP transport)`,
164
- "error"
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
- "success"
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?: string): { lines: string[]; fingerprint: string | null } {
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.js");
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
- export async function openMcpPanel(
300
+ function buildMcpPanelCallbacks(
278
301
  state: McpExtensionState,
279
- pi: ExtensionAPI,
302
+ config: McpConfig,
280
303
  ctx: ExtensionContext,
281
- configOverridePath?: string,
282
- ): Promise<PanelFlowResult> {
283
- if (Object.keys(state.config.mcpServers).length === 0) {
284
- return openMcpSetup(state, pi, ctx, configOverridePath, "empty");
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
- && !hasStoredTokens(serverName)
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 { createMcpPanel } = await import("./mcp-panel.js");
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.js";
6
- import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.js";
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 {
@@ -1,5 +1,5 @@
1
- import { ConsentError } from "./errors.js";
2
- import { logger } from "./logger.js";
1
+ import { ConsentError } from "./errors.ts";
2
+ import { logger } from "./logger.ts";
3
3
 
4
4
  export type ToolConsentMode = "never" | "once-per-server" | "always";
5
5
 
package/direct-tools.ts CHANGED
@@ -1,16 +1,16 @@
1
- import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
2
- import type { McpExtensionState } from "./state.js";
3
- import type { DirectToolSpec, McpConfig, McpContent } from "./types.js";
4
- import type { MetadataCache } from "./metadata-cache.js";
5
- import { lazyConnect, getFailureAgeSeconds } from "./init.js";
6
- import { isServerCacheValid } from "./metadata-cache.js";
7
- import { formatSchema } from "./tool-metadata.js";
8
- import { transformMcpContent } from "./tool-registrar.js";
9
- import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.js";
10
- import { formatToolName, isToolExcluded } from "./types.js";
11
- import { resourceNameToToolName } from "./resource-tools.js";
12
- import { authenticate, supportsOAuth } from "./mcp-auth-flow.js";
13
- import { formatAuthRequiredMessage } from "./utils.js";
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?.grantType ?? "authorization_code";
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 = ToolDefinition["execute"];
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,
@@ -1,4 +1,4 @@
1
- import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.js";
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 "@mariozechner/pi-coding-agent";
2
- import type { McpExtensionState } from "./state.js";
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.js";
5
- import { loadMcpConfig } from "./config.js";
6
- import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.js";
7
- import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.js";
8
- import { loadMetadataCache } from "./metadata-cache.js";
9
- import { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.js";
10
- import { getConfigPathFromArgv, truncateAtWord } from "./utils.js";
11
- import { initializeOAuth, shutdownOAuth } from "./mcp-auth-flow.js";
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<Record<string, unknown>>(spec.inputSchema || { type: "object", properties: {} }),
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;