pi-mcp-adapter 2.16.0 → 2.17.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 +11 -0
- package/README.md +3 -0
- package/init.ts +25 -6
- package/mcp-auth-flow.ts +18 -2
- package/mcp-auth.ts +72 -2
- package/mcp-oauth-provider.ts +25 -1
- package/package.json +1 -1
- package/tool-result-renderer.ts +62 -10
- package/types.ts +5 -0
- package/utils.ts +2 -1
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.17.0] - 2026-07-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added `settings.mcpFooterStatus` to compact or hide the persistent MCP footer status. Thanks @jwintz for issue #5.
|
|
14
|
+
- Added per-server OAuth `authorizationParams` for provider-specific authorization URL parameters such as Google's `access_type=offline`, while rejecting OAuth flow-owned parameter overrides. Thanks @hank-warren for issue #238.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Added a best-effort absolute-path fallback for loading the `@napi-rs/keyring` native binding when compiled Pi/Bun cannot resolve the package loader. Thanks @sgiath for issue #230.
|
|
18
|
+
- Bound collapsed MCP tool result rendering by character count as well as line count, preventing huge single-line results from slowing long TUI sessions. Thanks @Whisperfall for issue #249.
|
|
19
|
+
- Let configured `oauth.scope` override OAuth discovery scopes during authorization flows. Thanks @viggy28 for issue #225 and @adity982 for PR #226.
|
|
20
|
+
|
|
10
21
|
## [2.16.0] - 2026-07-30
|
|
11
22
|
|
|
12
23
|
### Added
|
package/README.md
CHANGED
|
@@ -258,6 +258,7 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
258
258
|
"idleTimeout": 10,
|
|
259
259
|
"requestTimeoutMs": 30000,
|
|
260
260
|
"showStatusIcon": true,
|
|
261
|
+
"mcpFooterStatus": "full",
|
|
261
262
|
"hostConfigDiscovery": "off",
|
|
262
263
|
"oauthDir": ".pi/mcp-oauth",
|
|
263
264
|
"trace": {
|
|
@@ -277,8 +278,10 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
277
278
|
| `idleTimeout` | Global idle timeout in minutes (default: 10, 0 to disable) |
|
|
278
279
|
| `requestTimeoutMs` | Global request timeout in milliseconds for live MCP calls (if omitted or `<= 0`, the MCP SDK default timeout is used) |
|
|
279
280
|
| `showStatusIcon` | Show the plug icon in MCP status and connection text (default: `true`). Set to `false` for plain `MCP: ...` text. |
|
|
281
|
+
| `mcpFooterStatus` | MCP footer verbosity: `"full"` (default), `"compact"` for `MCP connected/enabled`, or `"off"` to clear the persistent footer status. `/mcp status` remains available. |
|
|
280
282
|
| `hostConfigDiscovery` | Host-specific config policy: `"off"` (default), `"prompt"` (detect/report only), or `"on"` (explicitly load detected host configs as the lowest-precedence fallback) |
|
|
281
283
|
| `oauthDir` | Legacy OAuth `tokens.json` import directory for this MCP config. Relative paths resolve from the active project cwd. `MCP_OAUTH_DIR` still wins when set. Persistent OAuth credentials are stored in the OS credential store, not this directory. |
|
|
284
|
+
| `mcpServers.<name>.oauth.authorizationParams` | Extra authorization URL parameters for provider-specific OAuth extensions. Flow-owned parameters such as `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `response_type`, and `resource` cannot be overridden. |
|
|
282
285
|
| `directTools` | Global default for all servers (default: false). Per-server overrides this. |
|
|
283
286
|
| `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
|
|
284
287
|
| `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
|
package/init.ts
CHANGED
|
@@ -261,7 +261,8 @@ export async function initializeMcp(
|
|
|
261
261
|
});
|
|
262
262
|
|
|
263
263
|
if (ui && startupServers.length > 0) {
|
|
264
|
-
|
|
264
|
+
const status = formatMcpStatus(state.config, `connecting to ${startupServers.length} servers...`);
|
|
265
|
+
ui.setStatus("mcp", status);
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
const results = await parallelLimit(startupServers, 10, async ([name, definition]) => {
|
|
@@ -399,6 +400,9 @@ export async function initializeMcp(
|
|
|
399
400
|
|
|
400
401
|
owner.throwIfInactive();
|
|
401
402
|
lifecycle.startHealthChecks(runtimeSignal);
|
|
403
|
+
if (config.settings?.mcpFooterStatus === "off") {
|
|
404
|
+
ui?.setStatus("mcp", undefined);
|
|
405
|
+
}
|
|
402
406
|
publishMcpStatusSnapshot(state);
|
|
403
407
|
|
|
404
408
|
return state;
|
|
@@ -524,10 +528,24 @@ export function updateStatusBar(state: McpExtensionState): void {
|
|
|
524
528
|
const definition = state.config.mcpServers[name];
|
|
525
529
|
return connection.status === "connected" && definition !== undefined && !isServerDisabled(definition);
|
|
526
530
|
}).length;
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
531
|
+
const footerStatus = state.config.settings?.mcpFooterStatus ?? "full";
|
|
532
|
+
if (footerStatus === "off") {
|
|
533
|
+
ui.setStatus("mcp", undefined);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
let status = footerStatus === "compact"
|
|
538
|
+
? `MCP ${connectedCount}/${enabledCount}`
|
|
539
|
+
: `${enabledCount} ${enabledCount === 1 ? "server" : "servers"} enabled`;
|
|
540
|
+
if (footerStatus === "full") {
|
|
541
|
+
if (connectedCount > 0) status += ` (${connectedCount} connected)`;
|
|
542
|
+
if (disabledCount > 0) status += ` (${disabledCount} disabled)`;
|
|
543
|
+
}
|
|
544
|
+
const formattedStatus = footerStatus === "compact" ? status : formatMcpStatus(state.config, status);
|
|
545
|
+
if (formattedStatus === undefined) {
|
|
546
|
+
ui.setStatus("mcp", undefined);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
531
549
|
ui.setStatus("mcp", ui.theme ? ui.theme.fg("accent", formattedStatus) : formattedStatus);
|
|
532
550
|
}
|
|
533
551
|
|
|
@@ -565,7 +583,8 @@ export async function lazyConnect(state: McpExtensionState, serverName: string,
|
|
|
565
583
|
|
|
566
584
|
try {
|
|
567
585
|
if (state.ui) {
|
|
568
|
-
|
|
586
|
+
const status = formatMcpStatus(state.config, `connecting to ${serverName}...`);
|
|
587
|
+
state.ui.setStatus("mcp", status);
|
|
569
588
|
}
|
|
570
589
|
const newConnection = await state.manager.connect(serverName, definition, ownedSignal);
|
|
571
590
|
if (newConnection.status === "needs-auth") {
|
package/mcp-auth-flow.ts
CHANGED
|
@@ -57,6 +57,10 @@ type AuthDiscovery = {
|
|
|
57
57
|
scope?: string
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function applyConfiguredScope(discovery: AuthDiscovery, config: McpOAuthConfig): AuthDiscovery {
|
|
61
|
+
return config.scope !== undefined ? { ...discovery, scope: config.scope } : discovery
|
|
62
|
+
}
|
|
63
|
+
|
|
60
64
|
type PendingAuth = {
|
|
61
65
|
serverName: string
|
|
62
66
|
authProvider: McpOAuthProvider
|
|
@@ -162,6 +166,18 @@ export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
|
|
|
162
166
|
if (typeof definition.oauth.scope !== "string") throw new Error("OAuth scope must be a string")
|
|
163
167
|
config.scope = interpolateEnvVars(definition.oauth.scope)
|
|
164
168
|
}
|
|
169
|
+
if (definition.oauth?.authorizationParams !== undefined) {
|
|
170
|
+
const params = definition.oauth.authorizationParams
|
|
171
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
172
|
+
throw new Error("OAuth authorizationParams must be an object")
|
|
173
|
+
}
|
|
174
|
+
config.authorizationParams = {}
|
|
175
|
+
for (const [key, value] of Object.entries(params)) {
|
|
176
|
+
if (!key) throw new Error("OAuth authorizationParams keys must not be empty")
|
|
177
|
+
if (typeof value !== "string") throw new Error(`OAuth authorizationParams.${key} must be a string`)
|
|
178
|
+
config.authorizationParams[key] = interpolateEnvVars(value)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
165
181
|
if (definition.oauth?.redirectUri !== undefined) {
|
|
166
182
|
if (typeof definition.oauth.redirectUri !== "string") {
|
|
167
183
|
throw new Error("OAuth redirectUri must be a string")
|
|
@@ -304,7 +320,7 @@ export async function startAuth(
|
|
|
304
320
|
},
|
|
305
321
|
}, authStorageOptions, runtime.signal)
|
|
306
322
|
try {
|
|
307
|
-
const discovery = await probeAuthDiscovery(serverUrl, definition, signal)
|
|
323
|
+
const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
|
|
308
324
|
throwIfAborted(signal)
|
|
309
325
|
const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
|
|
310
326
|
throwIfAborted(signal)
|
|
@@ -370,7 +386,7 @@ export async function startAuth(
|
|
|
370
386
|
|
|
371
387
|
throwIfAborted(signal)
|
|
372
388
|
|
|
373
|
-
const discovery = await probeAuthDiscovery(serverUrl, definition, signal)
|
|
389
|
+
const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
|
|
374
390
|
throwIfAborted(signal)
|
|
375
391
|
const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
|
|
376
392
|
throwIfAborted(signal)
|
package/mcp-auth.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { createHash } from 'crypto';
|
|
14
14
|
import { createRequire } from 'module';
|
|
15
15
|
import { readFileSync, existsSync, rmSync } from 'fs';
|
|
16
|
-
import { join } from 'path';
|
|
16
|
+
import { dirname, join } from 'path';
|
|
17
17
|
import { getAgentPath } from './agent-dir.ts';
|
|
18
18
|
import { resolveConfiguredOAuthDir } from './config.ts';
|
|
19
19
|
|
|
@@ -113,6 +113,8 @@ interface KeyringEntry {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
type KeyringEntryConstructor = new (service: string, account: string) => KeyringEntry;
|
|
116
|
+
type KeyringModule = { Entry: KeyringEntryConstructor };
|
|
117
|
+
type KeyringRequire = ((id: string) => unknown) & { resolve(id: string): string };
|
|
116
118
|
|
|
117
119
|
interface AuthSecretStore {
|
|
118
120
|
read(account: string): string | undefined;
|
|
@@ -185,13 +187,81 @@ function getAuthSecretStore(): AuthSecretStore {
|
|
|
185
187
|
|
|
186
188
|
function getKeyringEntry(account: string): KeyringEntry {
|
|
187
189
|
try {
|
|
188
|
-
KeyringEntryClass ??= (
|
|
190
|
+
KeyringEntryClass ??= loadKeyringEntryClass();
|
|
189
191
|
return new KeyringEntryClass(AUTH_SECRET_SERVICE, account);
|
|
190
192
|
} catch (error) {
|
|
191
193
|
throw new Error('OAuth secure credential storage is unavailable. Configure the OS credential store and retry authentication.', { cause: error });
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
function loadKeyringEntryClass(keyringRequire: KeyringRequire = require, platform: NodeJS.Platform = process.platform, arch: NodeJS.Architecture = process.arch): KeyringEntryConstructor {
|
|
198
|
+
try {
|
|
199
|
+
return (keyringRequire('@napi-rs/keyring') as KeyringModule).Entry;
|
|
200
|
+
} catch (loaderError) {
|
|
201
|
+
try {
|
|
202
|
+
return loadKeyringNativeBindingFallback(keyringRequire, platform, arch).Entry;
|
|
203
|
+
} catch (fallbackError) {
|
|
204
|
+
throw new Error(`Failed to load @napi-rs/keyring; absolute-path native binding fallback also failed: ${formatErrorMessage(fallbackError)}`, {
|
|
205
|
+
cause: loaderError,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function loadKeyringNativeBindingFallback(keyringRequire: KeyringRequire, platform: NodeJS.Platform, arch: NodeJS.Architecture): KeyringModule {
|
|
212
|
+
const targets = getKeyringNativeBindingTargets(platform, arch);
|
|
213
|
+
if (targets.length === 0) {
|
|
214
|
+
throw new Error(`Unsupported @napi-rs/keyring native binding target: ${platform}-${arch}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let lastError: unknown;
|
|
218
|
+
for (const target of targets) {
|
|
219
|
+
try {
|
|
220
|
+
const packageJsonPath = keyringRequire.resolve(`${target.packageName}/package.json`);
|
|
221
|
+
return keyringRequire(join(dirname(packageJsonPath), target.bindingFile)) as KeyringModule;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
lastError = error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getKeyringNativeBindingTargets(platform: NodeJS.Platform, arch: NodeJS.Architecture): { packageName: string; bindingFile: string }[] {
|
|
231
|
+
return getKeyringNativeBindingSuffixes(platform, arch).map(suffix => ({
|
|
232
|
+
packageName: `@napi-rs/keyring-${suffix}`,
|
|
233
|
+
bindingFile: `keyring.${suffix}.node`,
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getKeyringNativeBindingSuffixes(platform: NodeJS.Platform, arch: NodeJS.Architecture): string[] {
|
|
238
|
+
if (platform === 'darwin') {
|
|
239
|
+
if (arch === 'arm64') return ['darwin-arm64'];
|
|
240
|
+
if (arch === 'x64') return ['darwin-x64'];
|
|
241
|
+
}
|
|
242
|
+
if (platform === 'win32') {
|
|
243
|
+
if (arch === 'arm64') return ['win32-arm64-msvc'];
|
|
244
|
+
if (arch === 'x64') return ['win32-x64-msvc'];
|
|
245
|
+
if (arch === 'ia32') return ['win32-ia32-msvc'];
|
|
246
|
+
}
|
|
247
|
+
if (platform === 'linux') {
|
|
248
|
+
if (arch === 'arm64') return ['linux-arm64-gnu', 'linux-arm64-musl'];
|
|
249
|
+
if (arch === 'arm') return ['linux-arm-gnueabihf'];
|
|
250
|
+
if (arch === 'riscv64') return ['linux-riscv64-gnu'];
|
|
251
|
+
if (arch === 'x64') return ['linux-x64-gnu', 'linux-x64-musl'];
|
|
252
|
+
}
|
|
253
|
+
if (platform === 'freebsd' && arch === 'x64') return ['freebsd-x64'];
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatErrorMessage(error: unknown): string {
|
|
258
|
+
return error instanceof Error ? error.message : String(error);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function loadTestKeyringEntryClass(keyringRequire: KeyringRequire, platform: NodeJS.Platform, arch: NodeJS.Architecture): KeyringEntryConstructor {
|
|
262
|
+
return loadKeyringEntryClass(keyringRequire, platform, arch);
|
|
263
|
+
}
|
|
264
|
+
|
|
195
265
|
export function getAuthStorageOptions(oauthDir: unknown, cwd = process.cwd()): AuthStorageOptions {
|
|
196
266
|
const baseDir = resolveConfiguredOAuthDir(oauthDir, cwd);
|
|
197
267
|
return baseDir ? { baseDir } : {};
|
package/mcp-oauth-provider.ts
CHANGED
|
@@ -82,11 +82,35 @@ export interface McpOAuthConfig {
|
|
|
82
82
|
clientId?: string
|
|
83
83
|
clientSecret?: string
|
|
84
84
|
scope?: string
|
|
85
|
+
authorizationParams?: Record<string, string>
|
|
85
86
|
redirectUri?: string
|
|
86
87
|
clientName?: string
|
|
87
88
|
clientUri?: string
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
const reservedAuthorizationParams = new Set([
|
|
92
|
+
"client_id",
|
|
93
|
+
"code_challenge",
|
|
94
|
+
"code_challenge_method",
|
|
95
|
+
"redirect_uri",
|
|
96
|
+
"resource",
|
|
97
|
+
"response_type",
|
|
98
|
+
"scope",
|
|
99
|
+
"state",
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
function addAuthorizationParams(authorizationUrl: URL, params: Record<string, string> | undefined): URL {
|
|
103
|
+
if (!params) return authorizationUrl
|
|
104
|
+
const nextUrl = new URL(authorizationUrl.toString())
|
|
105
|
+
for (const [key, value] of Object.entries(params)) {
|
|
106
|
+
if (reservedAuthorizationParams.has(key) || nextUrl.searchParams.has(key)) {
|
|
107
|
+
throw new Error(`OAuth authorizationParams.${key} cannot override an authorization flow parameter`)
|
|
108
|
+
}
|
|
109
|
+
nextUrl.searchParams.set(key, value)
|
|
110
|
+
}
|
|
111
|
+
return nextUrl
|
|
112
|
+
}
|
|
113
|
+
|
|
90
114
|
/** Callbacks for OAuth flow interactions */
|
|
91
115
|
export interface McpOAuthCallbacks {
|
|
92
116
|
onRedirect: (url: URL) => void | Promise<void>
|
|
@@ -386,7 +410,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
386
410
|
)
|
|
387
411
|
}
|
|
388
412
|
// URL is passed to callback, not logged (may contain sensitive params)
|
|
389
|
-
await this.callbacks.onRedirect(authorizationUrl)
|
|
413
|
+
await this.callbacks.onRedirect(addAuthorizationParams(authorizationUrl, this.config.authorizationParams))
|
|
390
414
|
}
|
|
391
415
|
|
|
392
416
|
/**
|
package/package.json
CHANGED
package/tool-result-renderer.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface McpToolResultDisplay {
|
|
|
34
34
|
|
|
35
35
|
const DEFAULT_MAX_CALL_INPUT_CHARS = 1500;
|
|
36
36
|
const DEFAULT_MAX_COLLAPSED_LINES = 3;
|
|
37
|
+
const DEFAULT_MAX_COLLAPSED_CHARS = 8000;
|
|
37
38
|
const COLLAPSED_RENDER_CHAR_SLACK = 8;
|
|
38
39
|
|
|
39
40
|
class CollapsibleText implements Component {
|
|
@@ -47,6 +48,7 @@ class CollapsibleText implements Component {
|
|
|
47
48
|
private readonly maxCollapsedLines: number,
|
|
48
49
|
private readonly ellipsis: string,
|
|
49
50
|
private readonly expandHint: string,
|
|
51
|
+
private readonly preTruncated = false,
|
|
50
52
|
) {
|
|
51
53
|
this.fullText = new Text(text, 0, 0);
|
|
52
54
|
this.footerText = new Text(`${ellipsis}\n${expandHint}`, 0, 0);
|
|
@@ -71,7 +73,7 @@ class CollapsibleText implements Component {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
const lines = this.collapsedText.text.render(width);
|
|
74
|
-
if (this.collapsedText.fullyIncluded && lines.length <= this.maxCollapsedLines) return lines;
|
|
76
|
+
if (!this.preTruncated && this.collapsedText.fullyIncluded && lines.length <= this.maxCollapsedLines) return lines;
|
|
75
77
|
|
|
76
78
|
return [
|
|
77
79
|
...lines.slice(0, this.maxCollapsedLines),
|
|
@@ -171,6 +173,58 @@ function blockToLines(block: McpToolContentBlock): string[] {
|
|
|
171
173
|
return [`[image: ${block.mimeType}]`];
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
function collectCollapsedResultLines(
|
|
177
|
+
content: AgentToolResult<McpToolResultDetails>["content"],
|
|
178
|
+
maxLines: number,
|
|
179
|
+
maxChars: number,
|
|
180
|
+
): McpToolResultDisplay {
|
|
181
|
+
if (content.length === 0) return { lines: ["(empty result)"], truncated: false };
|
|
182
|
+
|
|
183
|
+
const lines: string[] = [];
|
|
184
|
+
let remainingChars = maxChars;
|
|
185
|
+
let truncated = false;
|
|
186
|
+
|
|
187
|
+
const appendLine = (line: string) => {
|
|
188
|
+
if (lines.length >= maxLines || remainingChars <= 0) {
|
|
189
|
+
truncated = true;
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (line.length > remainingChars) {
|
|
194
|
+
lines.push(line.slice(0, remainingChars));
|
|
195
|
+
truncated = true;
|
|
196
|
+
remainingChars = 0;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lines.push(line);
|
|
201
|
+
remainingChars -= line.length + 1;
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
for (const block of content) {
|
|
206
|
+
if (block.type !== "text") {
|
|
207
|
+
if (!appendLine(`[image: ${block.mimeType}]`)) break;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let start = 0;
|
|
212
|
+
while (start <= block.text.length) {
|
|
213
|
+
const newline = block.text.indexOf("\n", start);
|
|
214
|
+
const line = newline === -1 ? block.text.slice(start) : block.text.slice(start, newline);
|
|
215
|
+
if (!appendLine(line)) break;
|
|
216
|
+
if (newline === -1) break;
|
|
217
|
+
start = newline + 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (truncated) break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (lines.length === 0) lines.push("");
|
|
224
|
+
if (truncated && lines.length >= maxLines) lines.push("…");
|
|
225
|
+
return { lines, truncated };
|
|
226
|
+
}
|
|
227
|
+
|
|
174
228
|
export function formatMcpToolResultIdentity(details: McpToolResultDetails | undefined): string | null {
|
|
175
229
|
if (details?.mode !== "call") return null;
|
|
176
230
|
const server = typeof details.server === "string"
|
|
@@ -189,18 +243,15 @@ export function formatMcpToolResultLines(
|
|
|
189
243
|
result: Pick<AgentToolResult<McpToolResultDetails>, "content">,
|
|
190
244
|
expanded: boolean,
|
|
191
245
|
maxCollapsedLines = 3,
|
|
246
|
+
maxCollapsedChars = DEFAULT_MAX_COLLAPSED_CHARS,
|
|
192
247
|
): McpToolResultDisplay {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (expanded || lines.length <= maxCollapsedLines) {
|
|
197
|
-
return { lines, truncated: false };
|
|
248
|
+
if (!expanded) {
|
|
249
|
+
return collectCollapsedResultLines(result.content, maxCollapsedLines, maxCollapsedChars);
|
|
198
250
|
}
|
|
199
251
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
};
|
|
252
|
+
const allLines = result.content.flatMap(blockToLines);
|
|
253
|
+
const lines = allLines.length > 0 ? allLines : ["(empty result)"];
|
|
254
|
+
return { lines, truncated: false };
|
|
204
255
|
}
|
|
205
256
|
|
|
206
257
|
export function renderMcpToolResult(
|
|
@@ -229,5 +280,6 @@ export function renderMcpToolResult(
|
|
|
229
280
|
DEFAULT_MAX_COLLAPSED_LINES + (identity ? 1 : 0),
|
|
230
281
|
activeTheme.fg("muted", "…"),
|
|
231
282
|
activeTheme.fg("muted", "(Ctrl+O to expand)"),
|
|
283
|
+
display.truncated,
|
|
232
284
|
);
|
|
233
285
|
}
|
package/types.ts
CHANGED
|
@@ -307,6 +307,8 @@ export interface OAuthConfig {
|
|
|
307
307
|
clientSecret?: string;
|
|
308
308
|
/** Requested OAuth scopes */
|
|
309
309
|
scope?: string;
|
|
310
|
+
/** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
|
|
311
|
+
authorizationParams?: Record<string, string>;
|
|
310
312
|
/** Exact authorization-code redirect URI for pre-registered clients */
|
|
311
313
|
redirectUri?: string;
|
|
312
314
|
/** Client display name for dynamic registration */
|
|
@@ -380,6 +382,7 @@ export interface McpOutputGuardSettings {
|
|
|
380
382
|
// Settings
|
|
381
383
|
export type ToolPrefix = "server" | "none" | "short" | "mcp";
|
|
382
384
|
export type HostConfigDiscovery = "off" | "prompt" | "on";
|
|
385
|
+
export type McpFooterStatus = "full" | "compact" | "off";
|
|
383
386
|
|
|
384
387
|
export interface McpTraceSettings {
|
|
385
388
|
/** Enable tracing for all servers unless a server sets trace to false. */
|
|
@@ -396,6 +399,8 @@ export interface McpSettings {
|
|
|
396
399
|
toolPrefix?: ToolPrefix;
|
|
397
400
|
/** Show the plug prefix in MCP status and connection text (default: true). Set to false to disable it. */
|
|
398
401
|
showStatusIcon?: boolean;
|
|
402
|
+
/** Footer status verbosity: full details, compact connected/enabled count, or no footer status. Defaults to full. */
|
|
403
|
+
mcpFooterStatus?: McpFooterStatus;
|
|
399
404
|
/** Discover detected host-specific MCP configs only when explicitly enabled. */
|
|
400
405
|
hostConfigDiscovery?: HostConfigDiscovery;
|
|
401
406
|
idleTimeout?: number; // minutes, default 10, 0 to disable
|
package/utils.ts
CHANGED
|
@@ -277,7 +277,8 @@ export function formatAuthRequiredMessage(
|
|
|
277
277
|
return template ? template.replaceAll("${server}", serverName) : defaultMessage;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
export function formatMcpStatus(config: Pick<McpConfig, "settings">, message: string): string {
|
|
280
|
+
export function formatMcpStatus(config: Pick<McpConfig, "settings">, message: string): string | undefined {
|
|
281
|
+
if (config.settings?.mcpFooterStatus === "off") return undefined;
|
|
281
282
|
return `${config.settings?.showStatusIcon === false ? "MCP: " : "🔌 MCP: "}${message}`;
|
|
282
283
|
}
|
|
283
284
|
|