pi-mcp-adapter 2.15.0 → 2.16.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 +16 -0
- package/README.md +3 -2
- package/commands.ts +44 -10
- package/direct-tools.ts +18 -10
- package/elicitation-handler.ts +11 -10
- package/init.ts +3 -3
- package/json-schema-validator.ts +23 -9
- package/mcp-auth-flow.ts +18 -15
- package/mcp-auth.ts +209 -11
- package/mcp-oauth-provider.ts +92 -34
- package/metadata-cache.ts +10 -7
- package/oauth-handler.ts +1 -1
- package/package.json +4 -4
- package/prompts.ts +2 -2
- package/proxy-modes.ts +4 -4
- package/sampling-handler.ts +11 -10
- package/server-manager.ts +22 -30
- package/session-recovery.ts +12 -18
- package/tool-metadata.ts +6 -5
- package/tool-result-renderer.ts +30 -5
- package/types.ts +10 -1
- package/ui-resource-handler.ts +1 -1
- package/ui-server.ts +3 -3
- package/ui-session.ts +1 -1
- package/unix-socket-transport.ts +3 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.16.0] - 2026-07-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added an optional per-server `toolPrefix` override for direct MCP tools, prompts, and proxy summaries, falling back to the global prefix when unset. Thanks @FurryWolfX for issue #229.
|
|
14
|
+
- Added an advisory warning when resolved direct tools pass the documented 75-tool threshold, with no cap or enforcement. Thanks @JasonLandbridge for issue #240.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Restored the MCP SDK v1 client for compatibility with deployed MCP servers and OAuth providers. This rollback temporarily removes SDK v2-only protocol negotiation while retaining OAuth issuer binding and callback issuer validation. Thanks @hyknerf for PR #237 and issue #236, @leonfox28 for issue #227, and @JorelLatraille for issue #241.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Avoided a double-close race when disconnecting MCP connections by letting the SDK client own transport shutdown. Thanks Szymon Wiszczuk (@golota60) for PR #235.
|
|
21
|
+
- Prevented `/mcp` and `/mcp-auth` from crashing when the OS OAuth credential store is unavailable.
|
|
22
|
+
- Stored oversized OAuth credential payloads as secure-store manifests plus chunks, so Windows Credential Manager value limits no longer block OAuth completion for large token records. Thanks @LysanderdeJong for issue #223.
|
|
23
|
+
- Stored OAuth credential payloads as compact JSON, so multiline secrets no longer corrupt gnome-keyring plaintext keyrings. Thanks @hank-warren for issue #239.
|
|
24
|
+
- Kept collapsed MCP tool results from re-wrapping full multi-10KB payloads on repeated TUI renders, avoiding composer keystroke lag after large MCP dumps. Thanks @hyknerf for PR #233 and @chapmanb for the held-Text follow-up fix.
|
|
25
|
+
|
|
10
26
|
## [2.15.0] - 2026-07-25
|
|
11
27
|
|
|
12
28
|
### Added
|
package/README.md
CHANGED
|
@@ -193,6 +193,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
193
193
|
| `requestTimeoutMs` | Request timeout in milliseconds for live MCP calls (overrides global; if omitted or `<= 0`, the MCP SDK default timeout is used) |
|
|
194
194
|
| `exposeResources` | Expose MCP resources as tools (default: true) |
|
|
195
195
|
| `directTools` | `true`, `string[]`, or `false` — register tools individually instead of through proxy |
|
|
196
|
+
| `toolPrefix` | Override global `settings.toolPrefix` for this server (`"server"`, `"short"`, `"none"`, or `"mcp"`) |
|
|
196
197
|
| `includeTools` | `string[]` of tool names or glob patterns to expose (matches original names like `get_screenshot`, generated resource names like `read_figjam`, and prefixed names like `figma_get_screenshot`) |
|
|
197
198
|
| `excludeTools` | `string[]` of tool names or glob patterns to hide (applied after `includeTools`) |
|
|
198
199
|
| `debug` | Show server stderr (default: false) |
|
|
@@ -272,7 +273,7 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
272
273
|
|
|
273
274
|
| Setting | Description |
|
|
274
275
|
|---------|-------------|
|
|
275
|
-
| `toolPrefix` | `"server"` (default), `"short"` (strips `-mcp` suffix), `"none"`, or `"mcp"` (prefixes with `mcp__`, using server-mode normalization) |
|
|
276
|
+
| `toolPrefix` | `"server"` (default), `"short"` (strips `-mcp` suffix), `"none"`, or `"mcp"` (prefixes with `mcp__`, using server-mode normalization). Per-server `toolPrefix` overrides this for that server. |
|
|
276
277
|
| `idleTimeout` | Global idle timeout in minutes (default: 10, 0 to disable) |
|
|
277
278
|
| `requestTimeoutMs` | Global request timeout in milliseconds for live MCP calls (if omitted or `<= 0`, the MCP SDK default timeout is used) |
|
|
278
279
|
| `showStatusIcon` | Show the plug icon in MCP status and connection text (default: `true`). Set to `false` for plain `MCP: ...` text. |
|
|
@@ -407,7 +408,7 @@ To hide specific tools while still using `directTools: true`, add `excludeTools`
|
|
|
407
408
|
|
|
408
409
|
`includeTools` and `excludeTools` filter direct tools, proxy search/list/describe, and the `/mcp` panel view.
|
|
409
410
|
|
|
410
|
-
Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`.
|
|
411
|
+
Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`. If 75+ direct tools resolve, the adapter prints a warning but still registers the tools you configured.
|
|
411
412
|
|
|
412
413
|
Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/mcp-cache.json` when set), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only while the cache populates, then the extension hot-loads the refreshed direct tools into the current session. Servers that advertise MCP list-change notifications refresh the current session when their tool or resource list changes. On Pi versions that expose `pi.unregisterTool()`, stale direct tools are removed from the registry during refresh; older Pi versions still deactivate them from the active tool set. To force a refresh: `/mcp reconnect <server>`.
|
|
413
414
|
|
package/commands.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateMetadataCac
|
|
|
17
17
|
import { loadMetadataCache, reconstructPromptMetadata } from "./metadata-cache.ts";
|
|
18
18
|
import { buildToolMetadata } from "./tool-metadata.ts";
|
|
19
19
|
import { supportsOAuth, authenticate, removeAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { getAuthStorageOptions, inspectAuthForUrl } from "./mcp-auth.ts";
|
|
21
21
|
import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.ts";
|
|
22
22
|
import { openPath, resolveServerUrl, sanitizeTerminalText } from "./utils.ts";
|
|
23
23
|
import { isAbortError } from "./runtime-owner.ts";
|
|
@@ -166,7 +166,7 @@ export async function reconnectServer(
|
|
|
166
166
|
const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
|
|
167
167
|
state.toolMetadata.set(name, metadata);
|
|
168
168
|
if (!connection.promptDiscoveryFailed) {
|
|
169
|
-
state.promptMetadata?.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix));
|
|
169
|
+
state.promptMetadata?.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));
|
|
170
170
|
state.promptMetadataLive?.add(name);
|
|
171
171
|
}
|
|
172
172
|
if (connection.instructions) {
|
|
@@ -312,9 +312,33 @@ export async function logoutServer(
|
|
|
312
312
|
return { ok: false, message };
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
|
|
315
|
+
const signal = state.owner?.signal;
|
|
316
|
+
try {
|
|
317
|
+
await removeAuth(serverName, { authStorageOptions: state.authStorageOptions, signal, runtime: state.oauthRuntime });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (isAbortError(error, signal)) throw error;
|
|
320
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
321
|
+
if (ui) {
|
|
322
|
+
ui.notify(`Failed to clear OAuth credentials for "${serverName}": ${sanitizeTerminalText(message)}`, "error");
|
|
323
|
+
}
|
|
324
|
+
return { ok: false, message };
|
|
325
|
+
}
|
|
326
|
+
|
|
316
327
|
state.owner?.throwIfInactive();
|
|
317
|
-
|
|
328
|
+
try {
|
|
329
|
+
await state.manager.close(serverName);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (isAbortError(error, signal)) throw error;
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
333
|
+
if (ui) {
|
|
334
|
+
ui.notify(
|
|
335
|
+
`OAuth credentials were cleared for "${serverName}", but its connection could not be closed: ${sanitizeTerminalText(message)}`,
|
|
336
|
+
"error",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return { ok: false, message };
|
|
340
|
+
}
|
|
341
|
+
|
|
318
342
|
state.owner?.throwIfInactive();
|
|
319
343
|
updateStatusBar(state);
|
|
320
344
|
|
|
@@ -416,6 +440,11 @@ function buildMcpPanelCallbacks(
|
|
|
416
440
|
config: McpConfig,
|
|
417
441
|
ctx: ExtensionContext,
|
|
418
442
|
): McpPanelCallbacks {
|
|
443
|
+
// Panel-only diagnostics keep status inspection from mutating connection
|
|
444
|
+
// failure state while allowing the existing panel failure UI to show why the
|
|
445
|
+
// credential store could not be inspected.
|
|
446
|
+
const authStatusFailures = new Map<string, string>();
|
|
447
|
+
|
|
419
448
|
return {
|
|
420
449
|
reconnect: (serverName: string) => reconnectServer(state, ctx, serverName),
|
|
421
450
|
canAuthenticate: (serverName: string) => {
|
|
@@ -424,12 +453,10 @@ function buildMcpPanelCallbacks(
|
|
|
424
453
|
},
|
|
425
454
|
authenticate: (serverName: string) => authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime),
|
|
426
455
|
getConnectionStatus: (serverName: string) => {
|
|
456
|
+
authStatusFailures.delete(serverName);
|
|
427
457
|
const definition = config.mcpServers[serverName];
|
|
428
458
|
if (isServerDisabled(definition)) return "disabled";
|
|
429
459
|
const connection = state.manager.getConnection(serverName);
|
|
430
|
-
if (connection?.status === "needs-auth") {
|
|
431
|
-
return "needs-auth";
|
|
432
|
-
}
|
|
433
460
|
let serverUrl: string | undefined;
|
|
434
461
|
try {
|
|
435
462
|
serverUrl = definition ? resolveServerUrl(definition) : undefined;
|
|
@@ -441,15 +468,22 @@ function buildMcpPanelCallbacks(
|
|
|
441
468
|
&& serverUrl
|
|
442
469
|
&& definition.oauth !== false
|
|
443
470
|
&& definition.oauth?.grantType !== "client_credentials"
|
|
444
|
-
&& !getAuthForUrl(serverName, serverUrl, state.authStorageOptions)?.tokens
|
|
445
471
|
) {
|
|
446
|
-
|
|
472
|
+
const authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);
|
|
473
|
+
if (authStatus.status === "unavailable") {
|
|
474
|
+
authStatusFailures.set(serverName, authStatus.message);
|
|
475
|
+
return "failed";
|
|
476
|
+
}
|
|
477
|
+
if (authStatus.status === "absent" || !authStatus.entry.tokens) {
|
|
478
|
+
return "needs-auth";
|
|
479
|
+
}
|
|
447
480
|
}
|
|
481
|
+
if (connection?.status === "needs-auth") return "needs-auth";
|
|
448
482
|
if (connection?.status === "connected") return "connected";
|
|
449
483
|
if (getFailureAgeSeconds(state, serverName) !== null) return "failed";
|
|
450
484
|
return "idle";
|
|
451
485
|
},
|
|
452
|
-
getFailureMessage: (serverName: string) => getFailureMessage(state, serverName),
|
|
486
|
+
getFailureMessage: (serverName: string) => authStatusFailures.get(serverName) ?? getFailureMessage(state, serverName),
|
|
453
487
|
refreshCacheAfterReconnect: (serverName: string) => {
|
|
454
488
|
const freshCache = loadMetadataCache();
|
|
455
489
|
return freshCache?.servers?.[serverName] ?? null;
|
package/direct-tools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { UrlElicitationRequiredError } from "@modelcontextprotocol/
|
|
2
|
+
import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import type { McpExtensionState } from "./state.ts";
|
|
4
4
|
import type { DirectToolSpec, McpConfig, McpContent, ToolPrefix } from "./types.ts";
|
|
5
5
|
import type { MetadataCache } from "./metadata-cache.ts";
|
|
@@ -11,7 +11,7 @@ import { formatSchema } from "./tool-metadata.ts";
|
|
|
11
11
|
import { resolveMcpResultContent, transformMcpContent } from "./tool-registrar.ts";
|
|
12
12
|
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
|
|
13
13
|
import { maybeStartUiSession, summarizeUiSessionResult, type UiSessionRuntime } from "./ui-session.ts";
|
|
14
|
-
import { formatToolName, isServerDisabled, isToolAllowed } from "./types.ts";
|
|
14
|
+
import { formatToolName, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
15
15
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
16
16
|
import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
17
17
|
import { formatAuthRequiredMessage, resolveServerUrl, truncateAtWord } from "./utils.ts";
|
|
@@ -20,6 +20,7 @@ import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
|
|
|
20
20
|
|
|
21
21
|
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
22
22
|
const INSTRUCTIONS_SNIPPET_LENGTH = 150;
|
|
23
|
+
export const DIRECT_TOOLS_ADVISORY_THRESHOLD = 75;
|
|
23
24
|
|
|
24
25
|
type DirectAutoAuthResult =
|
|
25
26
|
| { status: "skipped" }
|
|
@@ -140,10 +141,12 @@ export function resolveDirectTools(
|
|
|
140
141
|
|
|
141
142
|
if (!toolFilter) continue;
|
|
142
143
|
|
|
144
|
+
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
145
|
+
|
|
143
146
|
for (const tool of serverCache.tools ?? []) {
|
|
144
147
|
if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
|
|
145
|
-
if (!isToolAllowed(tool.name, serverName,
|
|
146
|
-
const prefixedName = formatToolName(tool.name, serverName,
|
|
148
|
+
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) continue;
|
|
149
|
+
const prefixedName = formatToolName(tool.name, serverName, effectivePrefix);
|
|
147
150
|
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
148
151
|
console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
|
|
149
152
|
continue;
|
|
@@ -168,8 +171,8 @@ export function resolveDirectTools(
|
|
|
168
171
|
for (const resource of serverCache.resources ?? []) {
|
|
169
172
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
170
173
|
if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
|
|
171
|
-
if (!isToolAllowed(baseName, serverName,
|
|
172
|
-
const prefixedName = formatToolName(baseName, serverName,
|
|
174
|
+
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) continue;
|
|
175
|
+
const prefixedName = formatToolName(baseName, serverName, effectivePrefix);
|
|
173
176
|
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
174
177
|
console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
|
|
175
178
|
continue;
|
|
@@ -190,6 +193,10 @@ export function resolveDirectTools(
|
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
if (specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
|
|
197
|
+
console.warn(`MCP: ${specs.length} direct tools resolved. Each direct tool adds prompt context; README guidance recommends targeted sets of 5-20 tools and using the proxy or an explicit string[] when 75+ direct tools would be registered.`);
|
|
198
|
+
}
|
|
199
|
+
|
|
193
200
|
return specs;
|
|
194
201
|
}
|
|
195
202
|
|
|
@@ -217,13 +224,14 @@ export function buildProxyDescription(
|
|
|
217
224
|
const definition = config.mcpServers[serverName];
|
|
218
225
|
if (isServerDisabled(definition)) continue;
|
|
219
226
|
const entry = cache?.servers?.[serverName];
|
|
227
|
+
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
220
228
|
const toolCount = (entry?.tools ?? []).filter(
|
|
221
|
-
(tool) => isToolAllowed(tool.name, serverName,
|
|
229
|
+
(tool) => isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools),
|
|
222
230
|
).length;
|
|
223
231
|
const resourceCount = definition?.exposeResources !== false
|
|
224
232
|
? (entry?.resources ?? []).filter((resource) => {
|
|
225
233
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
226
|
-
return isToolAllowed(baseName, serverName,
|
|
234
|
+
return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools);
|
|
227
235
|
}).length
|
|
228
236
|
: 0;
|
|
229
237
|
const totalItems = toolCount + resourceCount;
|
|
@@ -433,9 +441,9 @@ export function createDirectToolExecutor(
|
|
|
433
441
|
name: spec.originalName,
|
|
434
442
|
arguments: params ?? {},
|
|
435
443
|
_meta: uiSession?.requestMeta,
|
|
436
|
-
}, requestOptions), ownedSignal),
|
|
444
|
+
}, undefined, requestOptions), ownedSignal),
|
|
437
445
|
);
|
|
438
|
-
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/
|
|
446
|
+
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/sdk/types.js").CallToolResult);
|
|
439
447
|
|
|
440
448
|
if (result.isError) {
|
|
441
449
|
const mcpContent = (result.content ?? []) as McpContent[];
|
package/elicitation-handler.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
ElicitRequestSchema,
|
|
5
|
+
ErrorCode,
|
|
6
|
+
McpError,
|
|
6
7
|
type ElicitRequest,
|
|
7
8
|
type ElicitRequestFormParams,
|
|
8
9
|
type ElicitRequestURLParams,
|
|
9
10
|
type ElicitResult,
|
|
10
|
-
|
|
11
|
-
} from "@modelcontextprotocol/
|
|
12
|
-
import {
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
|
|
13
|
+
import type { JsonSchemaType } from "@modelcontextprotocol/sdk/validation/types.js";
|
|
13
14
|
import open from "open";
|
|
14
15
|
|
|
15
16
|
export type ElicitationValue = string | number | boolean | string[] | undefined;
|
|
@@ -27,7 +28,7 @@ export interface ElicitationHandlerOptions {
|
|
|
27
28
|
export type ServerElicitationConfig = Omit<ElicitationHandlerOptions, "serverName" | "onUrlAccepted">;
|
|
28
29
|
|
|
29
30
|
export function registerElicitationHandler(client: Client, options: ElicitationHandlerOptions): void {
|
|
30
|
-
client.setRequestHandler(
|
|
31
|
+
client.setRequestHandler(ElicitRequestSchema, request =>
|
|
31
32
|
handleElicitationRequest(options, request));
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -304,16 +305,16 @@ export async function handleUrlElicitation(
|
|
|
304
305
|
options: ElicitationHandlerOptions,
|
|
305
306
|
params: ElicitRequestURLParams,
|
|
306
307
|
): Promise<ElicitResult> {
|
|
307
|
-
if (!options.allowUrl) throw new
|
|
308
|
+
if (!options.allowUrl) throw new McpError(ErrorCode.InvalidParams, "URL elicitation is not supported");
|
|
308
309
|
|
|
309
310
|
let parsed: URL;
|
|
310
311
|
try {
|
|
311
312
|
parsed = new URL(params.url);
|
|
312
313
|
} catch {
|
|
313
|
-
throw new
|
|
314
|
+
throw new McpError(ErrorCode.InvalidParams, "URL elicitation supplied an invalid URL");
|
|
314
315
|
}
|
|
315
316
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
316
|
-
throw new
|
|
317
|
+
throw new McpError(ErrorCode.InvalidParams, "URL elicitation only supports HTTP and HTTPS URLs");
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
const decision = await options.ui.select([
|
package/init.ts
CHANGED
|
@@ -245,7 +245,7 @@ export async function initializeMcp(
|
|
|
245
245
|
resourceCounts.set(name, cachedEntry.resources.length);
|
|
246
246
|
}
|
|
247
247
|
if (cachedEntry.prompts?.length) {
|
|
248
|
-
promptMetadata.set(name, reconstructPromptMetadata(name, cachedEntry.prompts ?? [], prefix));
|
|
248
|
+
promptMetadata.set(name, reconstructPromptMetadata(name, cachedEntry.prompts ?? [], prefix, definition));
|
|
249
249
|
}
|
|
250
250
|
if (cachedEntry.instructions) {
|
|
251
251
|
serverInstructions.set(name, cachedEntry.instructions);
|
|
@@ -301,7 +301,7 @@ export async function initializeMcp(
|
|
|
301
301
|
toolMetadata.set(name, metadata);
|
|
302
302
|
resourceCounts.set(name, connection.resources.length);
|
|
303
303
|
if (!connection.promptDiscoveryFailed) {
|
|
304
|
-
promptMetadata.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix));
|
|
304
|
+
promptMetadata.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));
|
|
305
305
|
promptMetadataLive.add(name);
|
|
306
306
|
}
|
|
307
307
|
if (connection.instructions) {
|
|
@@ -433,7 +433,7 @@ export function updateServerMetadata(state: McpExtensionState, serverName: strin
|
|
|
433
433
|
state.toolMetadata.set(serverName, metadata);
|
|
434
434
|
state.resourceCounts?.set(serverName, connection.resources.length);
|
|
435
435
|
if (!connection.promptDiscoveryFailed) {
|
|
436
|
-
state.promptMetadata?.set(serverName, reconstructPromptMetadata(serverName, connection.prompts ?? [], prefix));
|
|
436
|
+
state.promptMetadata?.set(serverName, reconstructPromptMetadata(serverName, connection.prompts ?? [], prefix, definition));
|
|
437
437
|
state.promptMetadataLive?.add(serverName);
|
|
438
438
|
}
|
|
439
439
|
if (connection.instructions) {
|
package/json-schema-validator.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "@modelcontextprotocol/client/validators/ajv";
|
|
1
|
+
import { Ajv } from "ajv";
|
|
2
|
+
import Ajv2020Import from "ajv/dist/2020.js";
|
|
3
|
+
import addFormatsImport from "ajv-formats";
|
|
4
|
+
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
|
|
6
5
|
import type {
|
|
7
6
|
JsonSchemaType,
|
|
8
7
|
JsonSchemaValidator,
|
|
9
8
|
jsonSchemaValidator as JsonSchemaValidatorProvider,
|
|
10
|
-
} from "@modelcontextprotocol/
|
|
9
|
+
} from "@modelcontextprotocol/sdk/validation/types.js";
|
|
10
|
+
|
|
11
|
+
// ajv-formats types target its bundled ajv; the runtime accepts both instances.
|
|
12
|
+
const addFormats = addFormatsImport as unknown as (instance: Ajv) => void;
|
|
11
13
|
|
|
12
14
|
type SchemaDialect =
|
|
13
15
|
| { status: "unstamped" }
|
|
@@ -17,6 +19,9 @@ const DRAFT_07_SCHEMA_URIS: ReadonlySet<string> = new Set([
|
|
|
17
19
|
"http://json-schema.org/draft-07/schema",
|
|
18
20
|
"https://json-schema.org/draft-07/schema",
|
|
19
21
|
]);
|
|
22
|
+
const DRAFT_2020_12_SCHEMA_URIS: ReadonlySet<string> = new Set([
|
|
23
|
+
"https://json-schema.org/draft/2020-12/schema",
|
|
24
|
+
]);
|
|
20
25
|
|
|
21
26
|
function schemaDialect(schema: JsonSchemaType): SchemaDialect {
|
|
22
27
|
if (!("$schema" in schema) || typeof schema.$schema !== "string") {
|
|
@@ -29,14 +34,23 @@ function schemaDialect(schema: JsonSchemaType): SchemaDialect {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export function createJsonSchemaValidator(): JsonSchemaValidatorProvider {
|
|
32
|
-
const defaultValidator = new AjvJsonSchemaValidator();
|
|
33
37
|
let draft07Validator: AjvJsonSchemaValidator | undefined;
|
|
38
|
+
let draft2020Validator: AjvJsonSchemaValidator | undefined;
|
|
34
39
|
|
|
35
40
|
return {
|
|
36
41
|
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
|
|
37
42
|
const dialect = schemaDialect(schema);
|
|
38
|
-
if (dialect.status
|
|
39
|
-
|
|
43
|
+
if (dialect.status === "unstamped" || DRAFT_2020_12_SCHEMA_URIS.has(dialect.uri)) {
|
|
44
|
+
draft2020Validator ??= (() => {
|
|
45
|
+
const Ajv2020 = Ajv2020Import as unknown as typeof Ajv;
|
|
46
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
47
|
+
addFormats(ajv);
|
|
48
|
+
return new AjvJsonSchemaValidator(ajv);
|
|
49
|
+
})();
|
|
50
|
+
return draft2020Validator.getValidator<T>(schema);
|
|
51
|
+
}
|
|
52
|
+
if (!DRAFT_07_SCHEMA_URIS.has(dialect.uri)) {
|
|
53
|
+
throw new Error(`Unsupported JSON Schema dialect: ${dialect.uri}`);
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
draft07Validator ??= (() => {
|
package/mcp-auth-flow.ts
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
import {
|
|
8
8
|
auth as runSdkAuth,
|
|
9
9
|
extractWWWAuthenticateParams,
|
|
10
|
-
IssuerMismatchError,
|
|
11
10
|
UnauthorizedError,
|
|
12
|
-
|
|
13
|
-
} from "@modelcontextprotocol/
|
|
11
|
+
} from "@modelcontextprotocol/sdk/client/auth.js"
|
|
12
|
+
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js"
|
|
14
13
|
import open from "open"
|
|
15
14
|
import { McpOAuthProvider, type McpOAuthConfig } from "./mcp-oauth-provider.ts"
|
|
16
15
|
import {
|
|
@@ -572,10 +571,25 @@ export async function completeAuth(
|
|
|
572
571
|
let keepPendingForRetry = false
|
|
573
572
|
let caughtError: unknown
|
|
574
573
|
try {
|
|
574
|
+
const discoveryState = await pendingAuth.authProvider.discoveryState()
|
|
575
|
+
const metadata = discoveryState?.authorizationServerMetadata
|
|
576
|
+
const expectedIssuer = metadata?.issuer ?? discoveryState?.authorizationServerUrl
|
|
577
|
+
const requiresIssuer = (metadata as { authorization_response_iss_parameter_supported?: unknown } | undefined)
|
|
578
|
+
?.authorization_response_iss_parameter_supported === true
|
|
579
|
+
if (expectedIssuer !== undefined && iss === undefined && requiresIssuer) {
|
|
580
|
+
keepPendingForRetry = true
|
|
581
|
+
throw new Error(
|
|
582
|
+
`The authorization server for ${serverName} requires the RFC 9207 "iss" parameter. ` +
|
|
583
|
+
"Paste the full redirect URL from the browser address bar (not just the authorization code).",
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
if (expectedIssuer !== undefined && iss !== undefined && iss !== expectedIssuer) {
|
|
587
|
+
throw new Error(`The OAuth authorization response issuer does not match the discovered issuer for ${serverName}.`)
|
|
588
|
+
}
|
|
589
|
+
|
|
575
590
|
const result = await abortable(runSdkAuth(pendingAuth.authProvider, {
|
|
576
591
|
serverUrl: pendingAuth.serverUrl,
|
|
577
592
|
authorizationCode: code,
|
|
578
|
-
...(iss !== undefined ? { iss } : {}),
|
|
579
593
|
...pendingAuth.discovery,
|
|
580
594
|
}), signal)
|
|
581
595
|
throwIfAborted(signal)
|
|
@@ -585,17 +599,6 @@ export async function completeAuth(
|
|
|
585
599
|
return "authenticated"
|
|
586
600
|
} catch (error) {
|
|
587
601
|
caughtError = error
|
|
588
|
-
// RFC 9207: the AS advertises authorization_response_iss_parameter_supported
|
|
589
|
-
// but no `iss` accompanied the pasted code (e.g. the user pasted only the
|
|
590
|
-
// raw code). Keep the pending flow alive so the user can re-paste the full
|
|
591
|
-
// redirect URL instead of restarting authentication from scratch.
|
|
592
|
-
if (iss === undefined && error instanceof IssuerMismatchError && error.kind === "authorization_response") {
|
|
593
|
-
keepPendingForRetry = true
|
|
594
|
-
throw new Error(
|
|
595
|
-
`The authorization server for ${serverName} requires the RFC 9207 "iss" parameter. ` +
|
|
596
|
-
"Paste the full redirect URL from the browser address bar (not just the authorization code).",
|
|
597
|
-
)
|
|
598
|
-
}
|
|
599
602
|
throw error
|
|
600
603
|
} finally {
|
|
601
604
|
if (!keepPendingForRetry) {
|