pi-mcp-adapter 2.5.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.6.0] - 2026-05-10
11
+
12
+ ### Added
13
+ - Added a no-argument `/mcp-auth` OAuth picker and in-panel auth shortcut for OAuth-capable MCP servers.
14
+ - Added compact collapsed rendering for MCP proxy and direct-tool result rows while keeping full tool results available when expanded.
15
+
16
+ ### Changed
17
+ - Migrated Pi runtime dependencies and imports from deprecated `@mariozechner/*` packages to `@earendil-works/*` packages.
18
+
19
+ ### Fixed
20
+ - 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.
21
+ - 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.
22
+ - Kept the `/mcp-auth` OAuth picker search focused on OAuth server rows and prevented hidden panel shortcuts from unexpectedly launching auth.
23
+ - Kept long MCP error results expanded in compact tool result rendering.
24
+
25
+ ## [2.5.4] - 2026-05-04
26
+
27
+ ### Changed
28
+ - Ignored npm lockfiles and removed checked-in `package-lock.json` files.
29
+
30
+ ### Fixed
31
+ - Resolved `${VAR}` and `$env:VAR` placeholders in HTTP bearer tokens.
32
+ - Honored MCP sampling `modelPreferences.hints` before falling back to the current/default model.
33
+
10
34
  ## [2.5.3] - 2026-05-01
11
35
 
12
36
  ### Added
package/README.md CHANGED
@@ -126,7 +126,7 @@ Pi-specific files are the write targets for imported or shared global servers wh
126
126
  | `headers` | HTTP headers; supports `${VAR}` and `$env:VAR` interpolation |
127
127
  | `auth` | `"bearer"` or `"oauth"` |
128
128
  | `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
129
- | `bearerToken` / `bearerTokenEnv` | Token or env var name |
129
+ | `bearerToken` / `bearerTokenEnv` | Token or env var name; `bearerToken` supports `${VAR}` and `$env:VAR` interpolation |
130
130
  | `lifecycle` | `"lazy"` (default), `"eager"`, or `"keep-alive"` |
131
131
  | `idleTimeout` | Minutes before idle disconnect (overrides global) |
132
132
  | `exposeResources` | Expose MCP resources as tools (default: true) |
@@ -159,7 +159,7 @@ Pi-specific files are the write targets for imported or shared global servers wh
159
159
  | `directTools` | Global default for all servers (default: false). Per-server overrides this. |
160
160
  | `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
161
161
  | `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
162
- | `sampling` | Allow MCP servers to request LLM sampling through Pi's current/default model (default: true when UI approval is available). |
162
+ | `sampling` | Allow MCP servers to sample through Pi models, honoring `modelPreferences.hints` before current/default fallback (default: true when UI approval is available). |
163
163
  | `samplingAutoApprove` | Skip sampling confirmation prompts. Required for sampling in non-UI sessions (default: false). |
164
164
 
165
165
  Per-server `idleTimeout` overrides the global setting.
@@ -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,12 @@ 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-auth` | Open an OAuth server picker in interactive UI sessions |
351
+ | `/mcp-auth <server>` | OAuth setup for a specific server |
352
+
353
+ If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`, and direct tool calls automatically run OAuth when needed and retry once.
349
354
 
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.
355
+ 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
356
 
352
357
  ## How It Works
353
358
 
@@ -363,4 +368,5 @@ If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`,
363
368
  ## Limitations
364
369
 
365
370
  - Cross-session server sharing not yet implemented (each Pi session runs its own server processes)
371
+ - 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
372
  - 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 } 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,20 +171,22 @@ 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
  }
@@ -194,8 +196,8 @@ export interface PanelFlowResult {
194
196
  configChanged: boolean;
195
197
  }
196
198
 
197
- function buildSharedConfigNoticeLines(configOverridePath?: string): { lines: string[]; fingerprint: string | null } {
198
- const discovery = getMcpDiscoverySummary(configOverridePath);
199
+ function buildSharedConfigNoticeLines(configOverridePath: string | undefined, cwd: string): { lines: string[]; fingerprint: string | null } {
200
+ const discovery = getMcpDiscoverySummary(configOverridePath, cwd);
199
201
  const onboardingState = loadOnboardingState();
200
202
  if (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {
201
203
  return { lines: [], fingerprint: null };
@@ -221,16 +223,16 @@ export async function openMcpSetup(
221
223
  ): Promise<PanelFlowResult> {
222
224
  if (!ctx.hasUI) return { configChanged: false };
223
225
 
224
- const discovery = getMcpDiscoverySummary(configOverridePath);
226
+ const discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd);
225
227
  const onboardingState = loadOnboardingState();
226
- const { createMcpSetupPanel } = await import("./mcp-setup-panel.js");
228
+ const { createMcpSetupPanel } = await import("./mcp-setup-panel.ts");
227
229
  let configChanged = false;
228
230
 
229
231
  const callbacks = {
230
232
  previewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),
231
- previewStarterProject: () => previewStarterProjectConfig(),
233
+ previewStarterProject: () => previewStarterProjectConfig(ctx.cwd),
232
234
  previewRepoPrompt: () => {
233
- const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
235
+ const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd).repoPrompt;
234
236
  if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;
235
237
  return previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
236
238
  },
@@ -240,12 +242,12 @@ export async function openMcpSetup(
240
242
  return result;
241
243
  },
242
244
  scaffoldProjectConfig: async () => {
243
- const path = writeStarterProjectConfig();
245
+ const path = writeStarterProjectConfig(ctx.cwd);
244
246
  configChanged = true;
245
247
  return { path };
246
248
  },
247
249
  addRepoPrompt: async () => {
248
- const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
250
+ const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd).repoPrompt;
249
251
  if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {
250
252
  throw new Error("RepoPrompt is not available to add from this setup screen.");
251
253
  }
@@ -265,7 +267,7 @@ export async function openMcpSetup(
265
267
  ctx.ui.custom(
266
268
  (tui, _theme, _keybindings, done) => {
267
269
  return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState }, tui, () => {
268
- done();
270
+ done(undefined);
269
271
  resolve({ configChanged });
270
272
  });
271
273
  },
@@ -274,25 +276,18 @@ export async function openMcpSetup(
274
276
  });
275
277
  }
276
278
 
277
- export async function openMcpPanel(
279
+ function buildMcpPanelCallbacks(
278
280
  state: McpExtensionState,
279
- pi: ExtensionAPI,
281
+ config: McpConfig,
280
282
  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);
283
+ ): McpPanelCallbacks {
284
+ return {
285
+ reconnect: (serverName: string) => lazyConnect(state, serverName),
286
+ canAuthenticate: (serverName: string) => {
287
+ const definition = config.mcpServers[serverName];
288
+ return definition ? supportsOAuth(definition) : false;
295
289
  },
290
+ authenticate: (serverName: string) => authenticateServer(serverName, config, ctx),
296
291
  getConnectionStatus: (serverName: string) => {
297
292
  const definition = config.mcpServers[serverName];
298
293
  const connection = state.manager.getConnection(serverName);
@@ -301,9 +296,10 @@ export async function openMcpPanel(
301
296
  }
302
297
  if (
303
298
  definition?.auth === "oauth"
299
+ && definition.url
304
300
  && definition.oauth !== false
305
301
  && definition.oauth?.grantType !== "client_credentials"
306
- && !hasStoredTokens(serverName)
302
+ && !getAuthForUrl(serverName, definition.url)?.tokens
307
303
  ) {
308
304
  return "needs-auth";
309
305
  }
@@ -316,8 +312,27 @@ export async function openMcpPanel(
316
312
  return freshCache?.servers?.[serverName] ?? null;
317
313
  },
318
314
  };
315
+ }
316
+
317
+ export async function openMcpPanel(
318
+ state: McpExtensionState,
319
+ pi: ExtensionAPI,
320
+ ctx: ExtensionContext,
321
+ configOverridePath?: string,
322
+ ): Promise<PanelFlowResult> {
323
+ if (Object.keys(state.config.mcpServers).length === 0) {
324
+ return openMcpSetup(state, pi, ctx, configOverridePath, "empty");
325
+ }
326
+
327
+ const config = state.config;
328
+ const cache = loadMetadataCache();
329
+ const configPath = pi.getFlag("mcp-config") as string | undefined ?? configOverridePath;
330
+ const provenanceMap = getServerProvenance(configPath, ctx.cwd);
331
+ const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);
332
+
333
+ const callbacks = buildMcpPanelCallbacks(state, config, ctx);
319
334
 
320
- const { createMcpPanel } = await import("./mcp-panel.js");
335
+ const { createMcpPanel } = await import("./mcp-panel.ts");
321
336
  let configChanged = false;
322
337
 
323
338
  await new Promise<void>((resolve) => {
@@ -329,7 +344,7 @@ export async function openMcpPanel(
329
344
  configChanged = true;
330
345
  ctx.ui.notify("Direct tools updated. Pi will reload after this panel closes.", "info");
331
346
  }
332
- done();
347
+ done(undefined);
333
348
  resolve();
334
349
  }, { noticeLines });
335
350
  },
@@ -343,3 +358,42 @@ export async function openMcpPanel(
343
358
 
344
359
  return { configChanged };
345
360
  }
361
+
362
+ export async function openMcpAuthPanel(
363
+ state: McpExtensionState,
364
+ pi: ExtensionAPI,
365
+ ctx: ExtensionContext,
366
+ configOverridePath?: string,
367
+ ): Promise<PanelFlowResult> {
368
+ if (!ctx.hasUI) return { configChanged: false };
369
+
370
+ const config = state.config;
371
+ const oauthServers = Object.entries(config.mcpServers).filter(([, definition]) => supportsOAuth(definition));
372
+ if (oauthServers.length === 0) {
373
+ ctx.ui.notify("No OAuth-capable MCP servers are configured.", "warning");
374
+ return { configChanged: false };
375
+ }
376
+
377
+ const cache = loadMetadataCache();
378
+ const configPath = pi.getFlag("mcp-config") as string | undefined ?? configOverridePath;
379
+ const provenanceMap = getServerProvenance(configPath, ctx.cwd);
380
+ const callbacks = buildMcpPanelCallbacks(state, config, ctx);
381
+ const { createMcpPanel } = await import("./mcp-panel.ts");
382
+
383
+ await new Promise<void>((resolve) => {
384
+ ctx.ui.custom(
385
+ (tui, _theme, _keybindings, done) => {
386
+ return createMcpPanel(config, cache, provenanceMap, callbacks, tui, () => {
387
+ done(undefined);
388
+ resolve();
389
+ }, {
390
+ authOnly: true,
391
+ noticeLines: ["Select an OAuth MCP server and press Enter or ctrl+a to authenticate."],
392
+ });
393
+ },
394
+ { overlay: true, overlayOptions: { anchor: "center", width: 82 } },
395
+ );
396
+ });
397
+
398
+ return { configChanged: false };
399
+ }
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, 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
 
@@ -207,8 +209,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
207
209
  description: "Authenticate with an MCP server (OAuth)",
208
210
  handler: async (args, ctx) => {
209
211
  const serverName = args?.trim();
210
- if (!serverName) {
211
- if (ctx.hasUI) ctx.ui.notify("Usage: /mcp-auth <server-name>", "error");
212
+ if (!serverName && !ctx.hasUI) {
212
213
  return;
213
214
  }
214
215
 
@@ -226,12 +227,17 @@ export default function mcpAdapter(pi: ExtensionAPI) {
226
227
  return;
227
228
  }
228
229
 
230
+ if (!serverName) {
231
+ await openMcpAuthPanel(state, pi, ctx, earlyConfigPath);
232
+ return;
233
+ }
234
+
229
235
  await authenticateServer(serverName, state.config, ctx);
230
236
  },
231
237
  });
232
238
 
233
239
  if (shouldRegisterProxyTool) {
234
- pi.registerTool({
240
+ (pi.registerTool as (tool: unknown) => unknown)({
235
241
  name: "mcp",
236
242
  label: "MCP",
237
243
  description: buildProxyDescription(earlyConfig, earlyCache, directSpecs),
@@ -247,6 +253,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
247
253
  server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
248
254
  action: Type.Optional(Type.String({ description: "Action: 'ui-messages' to retrieve prompts/intents from UI sessions" })),
249
255
  }),
256
+ renderResult: renderMcpToolResult,
250
257
  async execute(_toolCallId, params: {
251
258
  tool?: string;
252
259
  args?: string;
package/init.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import type { McpExtensionState } from "./state.js";
3
- import type { ToolMetadata } from "./types.js";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { McpExtensionState } from "./state.ts";
3
+ import type { ToolMetadata } from "./types.ts";
4
4
  import { existsSync } from "node:fs";
5
- import { loadMcpConfig } from "./config.js";
6
- import { ConsentManager } from "./consent-manager.js";
7
- import { McpLifecycleManager } from "./lifecycle.js";
5
+ import { loadMcpConfig } from "./config.ts";
6
+ import { ConsentManager } from "./consent-manager.ts";
7
+ import { McpLifecycleManager } from "./lifecycle.ts";
8
8
  import {
9
9
  computeServerHash,
10
10
  getMetadataCachePath,
@@ -15,13 +15,13 @@ import {
15
15
  serializeResources,
16
16
  serializeTools,
17
17
  type ServerCacheEntry,
18
- } from "./metadata-cache.js";
19
- import { McpServerManager } from "./server-manager.js";
20
- import { buildToolMetadata, totalToolCount } from "./tool-metadata.js";
21
- import { UiResourceHandler } from "./ui-resource-handler.js";
22
- import { openUrl, parallelLimit } from "./utils.js";
23
- import { logger } from "./logger.js";
24
- import { getMissingConfiguredDirectToolServers } from "./direct-tools.js";
18
+ } from "./metadata-cache.ts";
19
+ import { McpServerManager } from "./server-manager.ts";
20
+ import { buildToolMetadata, totalToolCount } from "./tool-metadata.ts";
21
+ import { UiResourceHandler } from "./ui-resource-handler.ts";
22
+ import { openUrl, parallelLimit } from "./utils.ts";
23
+ import { logger } from "./logger.ts";
24
+ import { getMissingConfiguredDirectToolServers } from "./direct-tools.ts";
25
25
 
26
26
  const FAILURE_BACKOFF_MS = 60 * 1000;
27
27
 
@@ -30,7 +30,7 @@ export async function initializeMcp(
30
30
  ctx: ExtensionContext
31
31
  ): Promise<McpExtensionState> {
32
32
  const configPath = pi.getFlag("mcp-config") as string | undefined;
33
- const config = loadMcpConfig(configPath);
33
+ const config = loadMcpConfig(configPath, ctx.cwd);
34
34
 
35
35
  const manager = new McpServerManager();
36
36
  const samplingAutoApprove = config.settings?.samplingAutoApprove === true;
@@ -61,7 +61,7 @@ export async function initializeMcp(
61
61
  completedUiSessions: [],
62
62
  openBrowser: (url: string) => openUrl(pi, url, process.env.BROWSER),
63
63
  ui,
64
- sendMessage: (message, options) => pi.sendMessage(message, options),
64
+ sendMessage: (message, options) => pi.sendMessage(message as unknown as Parameters<typeof pi.sendMessage>[0], options),
65
65
  };
66
66
 
67
67
  const serverEntries = Object.entries(config.mcpServers);