pi-mcp-adapter 2.9.0 → 2.10.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,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.10.0] - 2026-06-13
11
+
12
+ ### Added
13
+ - Added manual remote/headless OAuth proxy actions for copying authorization URLs and completing pasted redirect URLs or codes. Thanks @Gabrielgvl for PR #120.
14
+
15
+ ### Fixed
16
+ - Honored user `tui.select.*` keybindings in MCP management, setup, and auth panels. Thanks @owenniles for PR #138.
17
+ - Included configured OAuth scopes in authorization-code flows while preserving token endpoint authentication method selection. Thanks @carlosdagos for PR #140.
18
+ - Fixed MCP elicitation on stock Pi, including form dialogs with validation and review, consent-based URL handling, URL-required errors, completion notifications, and TUI-only browser navigation. Thanks @dmmulroy for PR #139.
19
+ - Expanded MCP schema formatting for nested `anyOf`/`oneOf` variants, `const` discriminators, nested object properties, and array items.
20
+
10
21
  ## [2.9.0] - 2026-06-04
11
22
 
12
23
  ### Added
package/README.md CHANGED
@@ -142,6 +142,26 @@ Pi-specific files are the write targets for imported or shared global servers wh
142
142
 
143
143
  For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact callback registered with the provider, for example `"http://localhost:3118/callback"`. Dynamic clients normally omit it and use a lazy OS-assigned localhost callback port.
144
144
 
145
+ ### Remote/headless OAuth
146
+
147
+ If Pi is running on a remote server and cannot open a local browser, start OAuth through the proxy tool:
148
+
149
+ ```js
150
+ mcp({ action: "auth-start", server: "linear-server" })
151
+ ```
152
+
153
+ Open the returned authorization URL in your local browser. After approval, your browser redirects to a localhost URL. On a remote server that local page may fail to load; copy the full URL from the browser address bar anyway and complete the flow in the same Pi session:
154
+
155
+ ```js
156
+ mcp({
157
+ action: "auth-complete",
158
+ server: "linear-server",
159
+ args: '{"redirectUrl":"http://localhost:19876/callback?code=...&state=..."}'
160
+ })
161
+ ```
162
+
163
+ You can also pass only the `code` query parameter with `args: '{"code":"..."}'`. Treat authorization URLs and codes as sensitive; they can grant access to the MCP server until the flow expires or completes.
164
+
145
165
  ### Lifecycle Modes
146
166
 
147
167
  - **`lazy`** (default) — Don't connect at startup. Connect on first tool call. Disconnect after idle timeout. Cached metadata keeps search/list working without connections.
@@ -169,14 +189,15 @@ For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact c
169
189
  | `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
170
190
  | `sampling` | Allow MCP servers to sample through Pi models, honoring `modelPreferences.hints` before current/default fallback (default: true when UI approval is available). |
171
191
  | `samplingAutoApprove` | Skip sampling confirmation prompts. Required for sampling in non-UI sessions (default: false). |
172
- | `elicitation` | Allow MCP servers to request user input through Pi UI forms/URL prompts (default: true when Pi UI form support is available). |
173
- | `elicitationAutoOpenUrls` | Automatically open URL elicitations without prompting first (default: false). |
192
+ | `elicitation` | Allow MCP servers to request user input through Pi dialogs (default: true when Pi UI is available). |
174
193
 
175
194
  Per-server `idleTimeout` overrides the global setting.
176
195
 
177
196
  ### MCP Elicitation
178
197
 
179
- When Pi exposes UI, the adapter advertises MCP elicitation support. Form elicitations are rendered with `ctx.ui.form()` and map Pi actions to MCP actions: submit → `accept`, secondary → `decline`, cancel → `cancel`. URL elicitations prompt before opening a browser unless `elicitationAutoOpenUrls` is enabled.
198
+ When Pi exposes dialog-capable UI, the adapter advertises form elicitation support. Forms use Pi's stock `select()` and `input()` dialogs, validate the response, and provide a review/edit step before submission. Explicit refusal maps to MCP `decline`; dismissing a dialog maps to `cancel`.
199
+
200
+ URL mode is advertised only in TUI mode. The adapter displays the requesting server, target host, and full URL, and always requires consent before opening the browser. It also handles URL-required tool errors (`-32042`) and completion notifications; after completing the browser interaction, retry the original tool call.
180
201
 
181
202
  ### Direct Tools
182
203
 
@@ -345,6 +366,8 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only
345
366
  | Call | `mcp({ tool: "...", args: '{"key": "value"}' })` |
346
367
  | Connect | `mcp({ connect: "server-name" })` |
347
368
  | UI messages | `mcp({ action: "ui-messages" })` |
369
+ | Auth start | `mcp({ action: "auth-start", server: "name" })` |
370
+ | Auth complete | `mcp({ action: "auth-complete", server: "name", args: '{"redirectUrl":"..."}' })` |
348
371
 
349
372
  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.
350
373
 
@@ -367,7 +390,7 @@ Tool names are fuzzy-matched on hyphens and underscores — `context7_resolve_li
367
390
 
368
391
  If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`, and direct tool calls automatically run OAuth when needed and retry once.
369
392
 
370
- 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.
393
+ In interactive sessions, you can also authenticate from `/mcp` with `ctrl+a` or Enter on a server that needs auth. In remote/headless sessions, use the proxy tool's `auth-start` and `auth-complete` actions to copy the authorization URL locally and paste the redirect URL back into Pi. `/mcp-auth` without a server only opens a picker in the interactive UI.
371
394
 
372
395
  ## How It Works
373
396
 
package/commands.ts CHANGED
@@ -286,8 +286,8 @@ export async function openMcpSetup(
286
286
 
287
287
  return new Promise<PanelFlowResult>((resolve) => {
288
288
  ctx.ui.custom(
289
- (tui, _theme, _keybindings, done) => {
290
- return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState }, tui, () => {
289
+ (tui, _theme, keybindings, done) => {
290
+ return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings }, tui, () => {
291
291
  done(undefined);
292
292
  resolve({ configChanged });
293
293
  });
@@ -358,7 +358,7 @@ export async function openMcpPanel(
358
358
 
359
359
  await new Promise<void>((resolve) => {
360
360
  ctx.ui.custom(
361
- (tui, _theme, _keybindings, done) => {
361
+ (tui, _theme, keybindings, done) => {
362
362
  return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result: McpPanelResult) => {
363
363
  if (!result.cancelled && result.changes.size > 0) {
364
364
  writeDirectToolsConfig(result.changes, provenanceMap, config);
@@ -367,7 +367,7 @@ export async function openMcpPanel(
367
367
  }
368
368
  done(undefined);
369
369
  resolve();
370
- }, { noticeLines });
370
+ }, { noticeLines, keybindings });
371
371
  },
372
372
  { overlay: true, overlayOptions: { anchor: "center", width: 82 } },
373
373
  );
@@ -403,12 +403,13 @@ export async function openMcpAuthPanel(
403
403
 
404
404
  await new Promise<void>((resolve) => {
405
405
  ctx.ui.custom(
406
- (tui, _theme, _keybindings, done) => {
406
+ (tui, _theme, keybindings, done) => {
407
407
  return createMcpPanel(config, cache, provenanceMap, callbacks, tui, () => {
408
408
  done(undefined);
409
409
  resolve();
410
410
  }, {
411
411
  authOnly: true,
412
+ keybindings,
412
413
  noticeLines: ["Select an OAuth MCP server and press Enter or ctrl+a to authenticate."],
413
414
  });
414
415
  },
package/direct-tools.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js";
2
3
  import type { McpExtensionState } from "./state.ts";
3
4
  import type { DirectToolSpec, McpConfig, McpContent } from "./types.ts";
4
5
  import type { MetadataCache } from "./metadata-cache.ts";
@@ -22,7 +23,7 @@ type DirectAutoAuthResult =
22
23
  function getDirectAuthRequiredMessage(
23
24
  state: McpExtensionState,
24
25
  serverName: string,
25
- defaultMessage = `MCP server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} first.`,
26
+ defaultMessage = `MCP server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`,
26
27
  ): string {
27
28
  return formatAuthRequiredMessage(state.config, serverName, defaultMessage);
28
29
  }
@@ -32,7 +33,7 @@ function getDirectAuthFailedMessage(state: McpExtensionState, serverName: string
32
33
  if (customGuidance) {
33
34
  return `OAuth authentication failed for "${serverName}": ${message}. ${getDirectAuthRequiredMessage(state, serverName)}`;
34
35
  }
35
- return `OAuth authentication failed for "${serverName}": ${message}. Run /mcp-auth ${serverName} first.`;
36
+ return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
36
37
  }
37
38
 
38
39
  async function attemptDirectAutoAuth(
@@ -55,7 +56,7 @@ async function attemptDirectAutoAuth(
55
56
  message: getDirectAuthRequiredMessage(
56
57
  state,
57
58
  serverName,
58
- `MCP server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} in an interactive session.`,
59
+ `MCP server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`,
59
60
  ),
60
61
  };
61
62
  }
@@ -255,7 +256,9 @@ export function buildProxyDescription(
255
256
  desc += ` mcp({ connect: "server-name" }) → Connect to a server and refresh metadata\n`;
256
257
  desc += ` mcp({ tool: "name", args: '{"key": "value"}' }) → Call a tool (args is JSON string)\n`;
257
258
  desc += ` mcp({ action: "ui-messages" }) → Retrieve accumulated messages from completed UI sessions\n`;
258
- desc += `\nMode: tool (call) > connect > describe > search > server (list) > action > nothing (status)`;
259
+ desc += ` mcp({ action: "auth-start", server: "name" }) → Start manual OAuth and get a browser URL\n`;
260
+ desc += ` mcp({ action: "auth-complete", server: "name", args: '{"redirectUrl":"..."}' }) → Complete manual OAuth\n`;
261
+ desc += `\nMode: action > tool (call) > connect > describe > search > server (list) > nothing (status)`;
259
262
 
260
263
  return desc;
261
264
  }
@@ -406,6 +409,17 @@ export function createDirectToolExecutor(
406
409
  details: { server: spec.serverName, tool: spec.originalName },
407
410
  };
408
411
  } catch (error) {
412
+ if (error instanceof UrlElicitationRequiredError) {
413
+ const action = await state.manager.handleUrlElicitationRequired(spec.serverName, error);
414
+ const message = action === "accept"
415
+ ? "The original MCP tool did not run. Complete the opened browser interaction, then retry the tool."
416
+ : `The URL interaction was ${action === "decline" ? "declined" : "cancelled"}.`;
417
+ uiSession?.sendToolCancelled(message);
418
+ return {
419
+ content: [{ type: "text" as const, text: message }],
420
+ details: { error: "url_elicitation_required", server: spec.serverName, action },
421
+ };
422
+ }
409
423
  const message = error instanceof Error ? error.message : String(error);
410
424
  uiSession?.sendToolCancelled(message);
411
425
  let errorText = `Failed to call tool: ${message}`;