@fitchmultz/pi-mcp-adapter 5.0.0 → 7.0.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 +50 -0
- package/OAUTH.md +3 -3
- package/README.md +160 -88
- package/dist/app-bridge.bundle.js +75 -33
- package/dist/commands.js +9 -6
- package/dist/config.js +41 -16
- package/dist/direct-tools.d.ts +2 -4
- package/dist/direct-tools.js +21 -319
- package/dist/gateway-arguments.d.ts +33 -0
- package/dist/gateway-arguments.js +63 -0
- package/dist/index.js +181 -276
- package/dist/init.js +6 -20
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +2 -2
- package/dist/mcp-auth-flow.js +2 -2
- package/dist/mcp-auth.d.ts +1 -1
- package/dist/mcp-auth.js +3 -3
- package/dist/mcp-code.d.ts +7 -6
- package/dist/mcp-code.js +98 -51
- package/dist/mcp-oauth-provider.d.ts +5 -5
- package/dist/mcp-oauth-provider.js +5 -5
- package/dist/mcp-output-guard.d.ts +57 -2
- package/dist/mcp-output-guard.js +129 -24
- package/dist/mcp-panel.d.ts +3 -3
- package/dist/mcp-panel.js +36 -99
- package/dist/mcp-script-worker.mjs +5 -2
- package/dist/mcp-setup-panel.d.ts +5 -4
- package/dist/mcp-setup-panel.js +15 -7
- package/dist/mcp-status.js +2 -1
- package/dist/mcp-trace.d.ts +1 -1
- package/dist/mcp-trace.js +1 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +25 -88
- package/dist/npx-resolver.js +8 -4
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +41 -7
- package/dist/proxy-modes.js +275 -490
- package/dist/resource-tools.d.ts +15 -0
- package/dist/resource-tools.js +26 -1
- package/dist/sampling-handler.js +5 -1
- package/dist/search-ranking.d.ts +0 -2
- package/dist/search-ranking.js +70 -137
- package/dist/server-manager.d.ts +2 -2
- package/dist/server-manager.js +44 -27
- package/dist/session-recovery.d.ts +7 -4
- package/dist/session-recovery.js +76 -10
- package/dist/state.d.ts +1 -1
- package/dist/tool-loader.d.ts +32 -0
- package/dist/tool-loader.js +152 -0
- package/dist/tool-metadata.d.ts +67 -0
- package/dist/tool-metadata.js +23 -17
- package/dist/tool-registrar.d.ts +4 -4
- package/dist/tool-registrar.js +18 -13
- package/dist/tool-result-renderer.d.ts +6 -3
- package/dist/tool-result-renderer.js +11 -6
- package/dist/types.d.ts +11 -36
- package/dist/ui-resource-handler.d.ts +2 -2
- package/dist/ui-resource-handler.js +2 -2
- package/dist/ui-server.js +4 -3
- package/dist/ui-stream-types.d.ts +8 -8
- package/dist/unix-socket-transport.d.ts +1 -1
- package/dist/unix-socket-transport.js +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +16 -4
- package/package.json +37 -96
- package/skills/mcp-scripting/SKILL.md +67 -20
- package/abort.ts +0 -34
- package/agent-dir.ts +0 -24
- package/app-bridge.bundle.js +0 -67
- package/banner.png +0 -0
- package/checkpoint.ts +0 -55
- package/commands.ts +0 -619
- package/config.ts +0 -1204
- package/consent-manager.ts +0 -25
- package/direct-tools.ts +0 -408
- package/dist/ts-shape.d.ts +0 -2
- package/dist/ts-shape.js +0 -147
- package/elicitation-handler.ts +0 -349
- package/error-signal.ts +0 -21
- package/glimpse-ui.ts +0 -80
- package/host-html-template.ts +0 -456
- package/index.ts +0 -900
- package/init.ts +0 -627
- package/legacy-migration.ts +0 -47
- package/lifecycle.ts +0 -165
- package/logger.ts +0 -127
- package/mcp-auth-flow.ts +0 -975
- package/mcp-auth.ts +0 -793
- package/mcp-callback-server.ts +0 -450
- package/mcp-code.ts +0 -386
- package/mcp-keyring-helper.cjs +0 -89
- package/mcp-oauth-provider.ts +0 -774
- package/mcp-output-guard.ts +0 -412
- package/mcp-panel.ts +0 -978
- package/mcp-script-worker.mjs +0 -128
- package/mcp-setup-panel.ts +0 -666
- package/mcp-status.ts +0 -109
- package/mcp-trace.ts +0 -295
- package/metadata-cache.ts +0 -333
- package/npx-resolver.ts +0 -488
- package/onboarding-state.ts +0 -58
- package/panel-keys.ts +0 -37
- package/prompts.ts +0 -337
- package/proxy-modes.ts +0 -1361
- package/resource-tools.ts +0 -17
- package/runtime-owner.ts +0 -123
- package/sampling-handler.ts +0 -273
- package/scripts/build.mjs +0 -131
- package/scripts/prepare.mjs +0 -102
- package/search-ranking.ts +0 -196
- package/server-manager.ts +0 -1320
- package/session-recovery.ts +0 -316
- package/state.ts +0 -64
- package/tool-approval.ts +0 -78
- package/tool-metadata.ts +0 -235
- package/tool-registrar.ts +0 -69
- package/tool-result-renderer.ts +0 -286
- package/ts-shape.ts +0 -145
- package/tsconfig.build.json +0 -13
- package/tsconfig.json +0 -16
- package/types.ts +0 -810
- package/ui-resource-handler.ts +0 -249
- package/ui-server.ts +0 -808
- package/ui-session.ts +0 -568
- package/ui-stream-types.ts +0 -89
- package/ui-tool-visibility.ts +0 -25
- package/unix-socket-transport.ts +0 -85
- package/utils.ts +0 -309
package/dist/session-recovery.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// - treat generic -32000/ConnectionClosed errors as session expiry
|
|
22
22
|
// - treat AbortError/cancellation as a session failure
|
|
23
23
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
24
|
-
import { SdkHttpError, SdkErrorCode, ProtocolError, InsufficientScopeError, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
24
|
+
import { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, InsufficientScopeError, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
25
25
|
import { logger } from "./logger.js";
|
|
26
26
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
27
27
|
import { isServerDisabled, isNonInteractiveOAuth } from "./types.js";
|
|
@@ -30,18 +30,62 @@ const toolTransportFailures = new WeakSet();
|
|
|
30
30
|
export function isToolTransportFailure(error) {
|
|
31
31
|
return error instanceof Error && toolTransportFailures.has(error);
|
|
32
32
|
}
|
|
33
|
+
const interruptedToolCalls = new WeakSet();
|
|
34
|
+
export function isInterruptedToolCall(error) {
|
|
35
|
+
return error instanceof Error && interruptedToolCalls.has(error);
|
|
36
|
+
}
|
|
37
|
+
const toolCallOutcome = new AsyncLocalStorage();
|
|
38
|
+
/** Interrupted outcome reporting is separate from the transport retry policy. */
|
|
39
|
+
export async function trackToolCallOutcome(fn, signal) {
|
|
40
|
+
const outcome = { pending: false, cleanup: new Set() };
|
|
41
|
+
return toolCallOutcome.run(outcome, async () => {
|
|
42
|
+
try {
|
|
43
|
+
return await fn();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (outcome.pending && error instanceof Error
|
|
47
|
+
&& (signal?.aborted || (error instanceof SdkError
|
|
48
|
+
&& (error.code === SdkErrorCode.RequestTimeout || error.code === SdkErrorCode.ConnectionClosed)))) {
|
|
49
|
+
// Abort reasons can be shared by calls that never reached dispatch.
|
|
50
|
+
const interrupted = new Error(error.message, { cause: error });
|
|
51
|
+
interruptedToolCalls.add(interrupted);
|
|
52
|
+
throw interrupted;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
for (const cleanup of outcome.cleanup)
|
|
58
|
+
cleanup();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
33
62
|
const toolSend = new AsyncLocalStorage();
|
|
34
|
-
/** Retain fetch/body-error origin without parsing or buffering the
|
|
63
|
+
/** Retain fetch/body-error origin without parsing or buffering the response. */
|
|
35
64
|
export const trackToolTransportFailure = async (input, init) => {
|
|
36
65
|
const pending = toolSend.getStore();
|
|
37
66
|
if (pending)
|
|
38
67
|
pending.bodyFailure = undefined;
|
|
68
|
+
let toolPost = false;
|
|
69
|
+
if (pending && init?.method === "POST" && typeof init.body === "string") {
|
|
70
|
+
try {
|
|
71
|
+
const request = JSON.parse(init.body);
|
|
72
|
+
toolPost = request.method === "tools/call" && request.id === pending.id;
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
if (toolPost && pending?.outcome)
|
|
77
|
+
pending.outcome.pending = true;
|
|
39
78
|
try {
|
|
40
79
|
const response = await fetch(input, init);
|
|
41
|
-
if (
|
|
42
|
-
|
|
80
|
+
if (toolPost && !response.ok && response.status < 500 && pending?.outcome)
|
|
81
|
+
pending.outcome.pending = false;
|
|
82
|
+
const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
83
|
+
const observeBody = response.ok
|
|
84
|
+
? mediaType === "text/event-stream" || (toolPost && mediaType === "application/json")
|
|
85
|
+
: toolPost && response.status >= 500;
|
|
86
|
+
if (!pending || !response.body || !observeBody)
|
|
43
87
|
return response;
|
|
44
|
-
pending.streaming =
|
|
88
|
+
pending.streaming = response.ok && mediaType === "text/event-stream";
|
|
45
89
|
// The native end hook has no error argument. Only a failed body read proves loss.
|
|
46
90
|
const reader = response.body.getReader();
|
|
47
91
|
return new Response(new ReadableStream({
|
|
@@ -59,8 +103,11 @@ export const trackToolTransportFailure = async (input, init) => {
|
|
|
59
103
|
// Native intentional closure suppresses onRequestStreamEnd.
|
|
60
104
|
if (init?.signal?.aborted)
|
|
61
105
|
pending.finishSend();
|
|
62
|
-
else if (error instanceof Error)
|
|
106
|
+
else if (error instanceof Error) {
|
|
63
107
|
pending.bodyFailure = error;
|
|
108
|
+
if (!pending.streaming)
|
|
109
|
+
interruptedToolCalls.add(error);
|
|
110
|
+
}
|
|
64
111
|
controller.error(error);
|
|
65
112
|
reader.releaseLock();
|
|
66
113
|
}
|
|
@@ -77,12 +124,16 @@ export const trackToolTransportFailure = async (input, init) => {
|
|
|
77
124
|
}
|
|
78
125
|
};
|
|
79
126
|
/** Observe only the originating tool send, not OAuth or catalog requests. */
|
|
80
|
-
export function
|
|
127
|
+
export function trackToolTransport(transport, http = false) {
|
|
128
|
+
const requests = new Map();
|
|
81
129
|
const onmessage = transport.onmessage;
|
|
82
130
|
transport.onmessage = (message, extra) => {
|
|
83
|
-
const pending = toolSend.getStore();
|
|
131
|
+
const pending = ("id" in message && message.id !== undefined ? requests.get(message.id) : undefined) ?? toolSend.getStore();
|
|
84
132
|
if (pending && "id" in message && message.id === pending.id && ("result" in message || "error" in message)) {
|
|
85
133
|
pending.responseReceived = true;
|
|
134
|
+
if (pending.outcome)
|
|
135
|
+
pending.outcome.pending = false;
|
|
136
|
+
requests.delete(pending.id);
|
|
86
137
|
pending.finishSend();
|
|
87
138
|
}
|
|
88
139
|
onmessage?.(message, extra);
|
|
@@ -98,15 +149,24 @@ export function trackToolHttpFailures(transport) {
|
|
|
98
149
|
const send = transport.send.bind(transport);
|
|
99
150
|
transport.send = async (message, options) => {
|
|
100
151
|
try {
|
|
101
|
-
if (!("method" in message && message.method === "tools/call" && "id" in message
|
|
152
|
+
if (!("method" in message && message.method === "tools/call" && "id" in message)) {
|
|
102
153
|
return await toolSend.run(undefined, () => send(message, options));
|
|
103
154
|
}
|
|
104
155
|
const pending = {
|
|
105
|
-
id: message.id, streaming: false, responseReceived: false,
|
|
156
|
+
id: message.id, outcome: toolCallOutcome.getStore(), streaming: false, responseReceived: false,
|
|
106
157
|
bodyFailure: undefined, otherFailure: undefined,
|
|
107
158
|
finishSend: () => { },
|
|
108
159
|
};
|
|
160
|
+
if (pending.outcome) {
|
|
161
|
+
requests.set(message.id, pending);
|
|
162
|
+
pending.outcome.cleanup.add(() => requests.delete(message.id));
|
|
163
|
+
if (!http)
|
|
164
|
+
pending.outcome.pending = true;
|
|
165
|
+
}
|
|
109
166
|
try {
|
|
167
|
+
if (!options?.requestSignal) {
|
|
168
|
+
return await toolSend.run(pending, () => send(message, options));
|
|
169
|
+
}
|
|
110
170
|
await abortable(new Promise((resolve, reject) => {
|
|
111
171
|
pending.finishSend = resolve;
|
|
112
172
|
void toolSend.run(pending, () => send(message, {
|
|
@@ -126,6 +186,12 @@ export function trackToolHttpFailures(transport) {
|
|
|
126
186
|
resolve(); }, reject);
|
|
127
187
|
}), options.requestSignal);
|
|
128
188
|
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// Local send failures are distinct from a sent request waiting for its reply.
|
|
191
|
+
if (!http && pending.outcome)
|
|
192
|
+
pending.outcome.pending = false;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
129
195
|
finally {
|
|
130
196
|
pending.finishSend = () => { };
|
|
131
197
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface McpExtensionState {
|
|
|
33
33
|
manager: McpServerManager;
|
|
34
34
|
lifecycle: McpLifecycleManager;
|
|
35
35
|
toolMetadata: Map<string, ToolMetadata[]>;
|
|
36
|
-
/**
|
|
36
|
+
/** Native resource counts, separate from callable tools. */
|
|
37
37
|
resourceCounts: Map<string, number>;
|
|
38
38
|
promptMetadata: Map<string, PromptMetadata[]>;
|
|
39
39
|
/** Servers whose prompt inventory came from successful live discovery. */
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type TSchema } from "typebox";
|
|
3
|
+
import { type MetadataCache } from "./metadata-cache.ts";
|
|
4
|
+
import type { McpExtensionState } from "./state.ts";
|
|
5
|
+
import { type DirectToolSpec, type McpAdapterOptions, type McpConfig, type ToolMetadata } from "./types.ts";
|
|
6
|
+
type Reference = {
|
|
7
|
+
name: string;
|
|
8
|
+
namespace?: string;
|
|
9
|
+
};
|
|
10
|
+
type SearchDefinition<TParams extends TSchema, TDetails> = Omit<ToolDefinition<TParams, TDetails>, "execute"> & {
|
|
11
|
+
execute: (...args: Parameters<ToolDefinition<TParams, TDetails>["execute"]>) => Promise<AgentToolResult<TDetails> & {
|
|
12
|
+
tools: Reference[];
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
export type SearchAPI = ExtensionAPI & {
|
|
16
|
+
registerToolSearch?: <TParams extends TSchema, TDetails>(definition: SearchDefinition<TParams, TDetails>) => void;
|
|
17
|
+
getActiveToolReferences?: () => Reference[];
|
|
18
|
+
setActiveToolReferences?: (tools: Reference[]) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function supportsNativeAsync(ctx: ExtensionContext): boolean;
|
|
21
|
+
export declare function createToolLoader(pi: SearchAPI, getState: () => McpExtensionState | null, getInitPromise: () => Promise<McpExtensionState> | null, beforeExecute?: McpAdapterOptions["beforeExecute"]): {
|
|
22
|
+
native: boolean;
|
|
23
|
+
restore: (ctx: ExtensionContext) => void;
|
|
24
|
+
sync: (config: McpConfig, cache: MetadataCache | null, env?: string[] | null, asyncSupported?: boolean, registerGateways?: (pins: DirectToolSpec[]) => void) => DirectToolSpec[];
|
|
25
|
+
activate: (matches: Array<{
|
|
26
|
+
server: string;
|
|
27
|
+
tool: ToolMetadata;
|
|
28
|
+
}>) => Reference[];
|
|
29
|
+
persist: () => void;
|
|
30
|
+
selectedServers: () => string[];
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { createDirectToolExecutor, directToolSelection } from "./direct-tools.js";
|
|
3
|
+
import { isServerCacheValid, reconstructToolMetadata } from "./metadata-cache.js";
|
|
4
|
+
import { createMcpDirectToolCallRenderer, renderMcpToolResult } from "./tool-result-renderer.js";
|
|
5
|
+
import { isServerDisabled } from "./types.js";
|
|
6
|
+
import { normalizeDirectToolInputSchema } from "./utils.js";
|
|
7
|
+
import { isToolCallApprovalRequired } from "./tool-approval.js";
|
|
8
|
+
const ENTRY = "mcp-tool-selection";
|
|
9
|
+
const key = (value) => JSON.stringify([value.server, value.tool]);
|
|
10
|
+
const refKey = (value) => JSON.stringify([value.namespace ?? null, value.name]);
|
|
11
|
+
const reserved = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls", "mcp", "mcp_search", "mcp_script"]);
|
|
12
|
+
export function supportsNativeAsync(ctx) {
|
|
13
|
+
const compat = ctx.model?.compat;
|
|
14
|
+
return "getPendingToolCalls" in ctx && typeof ctx.getPendingToolCalls === "function"
|
|
15
|
+
&& !!compat && "supportsAsyncTools" in compat && compat.supportsAsyncTools === true;
|
|
16
|
+
}
|
|
17
|
+
export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
18
|
+
const native = typeof pi.registerToolSearch === "function"
|
|
19
|
+
&& typeof pi.getActiveToolReferences === "function"
|
|
20
|
+
&& typeof pi.setActiveToolReferences === "function";
|
|
21
|
+
pi.registerEntryRenderer(ENTRY, () => undefined);
|
|
22
|
+
const registered = new Map();
|
|
23
|
+
let available = new Set();
|
|
24
|
+
let selected = new Map();
|
|
25
|
+
let inactive = new Map();
|
|
26
|
+
let restoring = true;
|
|
27
|
+
const active = () => native ? pi.getActiveToolReferences() : pi.getActiveTools().map(name => ({ name }));
|
|
28
|
+
const setActive = (refs) => {
|
|
29
|
+
const unique = [...new Map(refs.map(ref => [refKey(ref), ref])).values()];
|
|
30
|
+
if (JSON.stringify(active()) === JSON.stringify(unique))
|
|
31
|
+
return;
|
|
32
|
+
if (native)
|
|
33
|
+
pi.setActiveToolReferences(unique);
|
|
34
|
+
else
|
|
35
|
+
pi.setActiveTools(unique.map(ref => ref.name));
|
|
36
|
+
};
|
|
37
|
+
function capture() {
|
|
38
|
+
const current = new Set(active().map(refKey));
|
|
39
|
+
for (const [id, { selection, ref, pinned }] of registered) {
|
|
40
|
+
if (!available.has(id))
|
|
41
|
+
continue;
|
|
42
|
+
if (current.has(refKey(ref))) {
|
|
43
|
+
if (!pinned)
|
|
44
|
+
selected.set(id, selection);
|
|
45
|
+
inactive.delete(id);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
selected.delete(id);
|
|
49
|
+
if (pinned)
|
|
50
|
+
inactive.set(id, selection);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function persist() {
|
|
55
|
+
capture();
|
|
56
|
+
pi.appendEntry(ENTRY, { selected: [...selected.values()], inactive: [...inactive.values()] });
|
|
57
|
+
}
|
|
58
|
+
function restore(ctx) {
|
|
59
|
+
selected = new Map();
|
|
60
|
+
inactive = new Map();
|
|
61
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
62
|
+
if (entry.type !== "custom" || entry.customType !== ENTRY || !entry.data || typeof entry.data !== "object")
|
|
63
|
+
continue;
|
|
64
|
+
const data = entry.data;
|
|
65
|
+
for (const [field, target] of [[data.selected, selected], [data.inactive, inactive]]) {
|
|
66
|
+
target.clear();
|
|
67
|
+
for (const selection of Array.isArray(field) ? field : []) {
|
|
68
|
+
if (typeof selection?.server === "string" && typeof selection.tool === "string")
|
|
69
|
+
target.set(key(selection), selection);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
restoring = true;
|
|
74
|
+
}
|
|
75
|
+
function sync(config, cache, env, asyncSupported = false, registerGateways) {
|
|
76
|
+
if (!restoring)
|
|
77
|
+
capture();
|
|
78
|
+
const previousRefs = new Set([...registered.values()].map(item => refKey(item.ref)));
|
|
79
|
+
const existing = new Set(pi.getAllTools().map(tool => refKey(tool)));
|
|
80
|
+
const specs = [];
|
|
81
|
+
for (const [server, definition] of Object.entries(env === null ? {} : config.mcpServers)) {
|
|
82
|
+
if (isServerDisabled(definition))
|
|
83
|
+
continue;
|
|
84
|
+
const entry = cache?.servers[server];
|
|
85
|
+
const metadata = getState()?.toolMetadata.get(server) ?? (entry && isServerCacheValid(entry, definition)
|
|
86
|
+
? reconstructToolMetadata(server, entry, config.settings?.toolPrefix ?? "server", definition) : []);
|
|
87
|
+
const selection = directToolSelection(config, server, env ?? undefined);
|
|
88
|
+
for (const { name: prefixedName, ...tool } of metadata) {
|
|
89
|
+
if (tool.resourceUri)
|
|
90
|
+
continue;
|
|
91
|
+
const pinned = selection === true || (Array.isArray(selection) && selection.includes(tool.originalName));
|
|
92
|
+
const ref = native && !pinned ? { namespace: `mcp_${server}`, name: tool.originalName } : { name: prefixedName };
|
|
93
|
+
specs.push({ spec: { ...tool, prefixedName, serverName: server }, selection: { server, tool: tool.originalName }, ref, pinned });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const counts = new Map();
|
|
97
|
+
for (const { ref } of specs)
|
|
98
|
+
counts.set(refKey(ref), (counts.get(refKey(ref)) ?? 0) + 1);
|
|
99
|
+
available = new Set();
|
|
100
|
+
const enabled = [];
|
|
101
|
+
for (const { spec, selection, ref, pinned } of specs) {
|
|
102
|
+
const id = key(selection);
|
|
103
|
+
const refId = refKey(ref);
|
|
104
|
+
if ((!ref.namespace && reserved.has(ref.name)) || counts.get(refId) > 1 || (existing.has(refId) && !previousRefs.has(refId)))
|
|
105
|
+
continue;
|
|
106
|
+
available.add(id);
|
|
107
|
+
const asyncTool = asyncSupported && !beforeExecute && !spec.uiResourceUri && !isToolCallApprovalRequired(config, spec.serverName, spec);
|
|
108
|
+
const fingerprint = JSON.stringify({ spec, ref, asyncTool });
|
|
109
|
+
if (registered.get(id)?.fingerprint !== fingerprint) {
|
|
110
|
+
pi.registerTool({
|
|
111
|
+
...ref,
|
|
112
|
+
...(asyncTool ? { async: true } : {}),
|
|
113
|
+
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
114
|
+
label: spec.title ?? `MCP: ${spec.originalName}`,
|
|
115
|
+
description: spec.description || "(no description)",
|
|
116
|
+
parameters: Type.Unsafe(normalizeDirectToolInputSchema(spec.inputSchema)),
|
|
117
|
+
execute: createDirectToolExecutor(getState, getInitPromise, spec, beforeExecute),
|
|
118
|
+
renderCall: createMcpDirectToolCallRenderer(spec.prefixedName),
|
|
119
|
+
renderResult: renderMcpToolResult,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
registered.set(id, { selection, ref, fingerprint, pinned });
|
|
123
|
+
if (selected.has(id) || (pinned && !inactive.has(id)))
|
|
124
|
+
enabled.push(ref);
|
|
125
|
+
}
|
|
126
|
+
const permitted = new Set(pi.getAllTools().map(refKey));
|
|
127
|
+
const pins = specs.filter(item => item.pinned && available.has(key(item.selection)) && permitted.has(refKey(item.ref))).map(item => item.spec);
|
|
128
|
+
registerGateways?.(pins);
|
|
129
|
+
// Official 0.87 reactivates allowlisted definitions on registration. Apply our loadout last.
|
|
130
|
+
const ownedRefs = new Set([...previousRefs, ...[...registered.values()].map(item => refKey(item.ref))]);
|
|
131
|
+
setActive([...active().filter(ref => !ownedRefs.has(refKey(ref))), ...enabled]);
|
|
132
|
+
restoring = false;
|
|
133
|
+
return pins;
|
|
134
|
+
}
|
|
135
|
+
function activate(matches) {
|
|
136
|
+
const requested = matches.flatMap(({ server, tool }) => {
|
|
137
|
+
const id = key({ server, tool: tool.originalName });
|
|
138
|
+
const registration = available.has(id) ? registered.get(id) : undefined;
|
|
139
|
+
return registration ? [registration] : [];
|
|
140
|
+
});
|
|
141
|
+
// No await between reading and extending the current loadout: parallel searches union their selections.
|
|
142
|
+
setActive([...active(), ...requested.map(item => item.ref)]);
|
|
143
|
+
const current = new Set(active().map(refKey));
|
|
144
|
+
const tools = requested.filter(item => current.has(refKey(item.ref))).map(({ selection, ref }) => {
|
|
145
|
+
selected.set(key(selection), selection);
|
|
146
|
+
return ref;
|
|
147
|
+
});
|
|
148
|
+
persist();
|
|
149
|
+
return tools;
|
|
150
|
+
}
|
|
151
|
+
return { native, restore, sync, activate, persist, selectedServers: () => [...new Set([...selected.values()].map(item => item.server))] };
|
|
152
|
+
}
|
package/dist/tool-metadata.d.ts
CHANGED
|
@@ -5,5 +5,72 @@ export declare function buildToolMetadata(tools: McpTool[], resources: McpResour
|
|
|
5
5
|
failedTools: string[];
|
|
6
6
|
};
|
|
7
7
|
export declare function totalToolCount(state: McpExtensionState): number;
|
|
8
|
+
export declare function toToolDescriptor(server: string, tool: ToolMetadata): {
|
|
9
|
+
outputSchema?: {
|
|
10
|
+
[x: string]: unknown;
|
|
11
|
+
$schema?: string | undefined;
|
|
12
|
+
} | undefined;
|
|
13
|
+
annotations?: {
|
|
14
|
+
title?: string | undefined;
|
|
15
|
+
readOnlyHint?: boolean | undefined;
|
|
16
|
+
destructiveHint?: boolean | undefined;
|
|
17
|
+
idempotentHint?: boolean | undefined;
|
|
18
|
+
openWorldHint?: boolean | undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
execution?: {
|
|
21
|
+
taskSupport?: "forbidden" | "optional" | "required" | undefined;
|
|
22
|
+
} | undefined;
|
|
23
|
+
_meta?: {
|
|
24
|
+
[x: string]: unknown;
|
|
25
|
+
} | undefined;
|
|
26
|
+
icons?: {
|
|
27
|
+
src: string;
|
|
28
|
+
mimeType?: string | undefined;
|
|
29
|
+
sizes?: string[] | undefined;
|
|
30
|
+
theme?: "dark" | "light" | undefined;
|
|
31
|
+
}[] | undefined;
|
|
32
|
+
title?: string | undefined;
|
|
33
|
+
inputSchema?: {
|
|
34
|
+
[x: string]: unknown;
|
|
35
|
+
type: "object";
|
|
36
|
+
properties?: {
|
|
37
|
+
[x: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | {
|
|
38
|
+
[x: string]: string | number | boolean | /*elided*/ any | /*elided*/ any | null;
|
|
39
|
+
} | null)[] | {
|
|
40
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
41
|
+
} | null)[] | {
|
|
42
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
43
|
+
} | null)[] | {
|
|
44
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
45
|
+
} | null)[] | {
|
|
46
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
47
|
+
} | null)[] | {
|
|
48
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
49
|
+
} | null)[] | {
|
|
50
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
51
|
+
} | null)[] | {
|
|
52
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
53
|
+
} | null)[] | {
|
|
54
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
55
|
+
} | null)[] | {
|
|
56
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
57
|
+
} | null)[] | {
|
|
58
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
59
|
+
} | null)[] | {
|
|
60
|
+
[x: string]: string | number | boolean | any | any | null;
|
|
61
|
+
} | null;
|
|
62
|
+
} | undefined;
|
|
63
|
+
required?: string[] | undefined;
|
|
64
|
+
};
|
|
65
|
+
description: string;
|
|
66
|
+
server: string;
|
|
67
|
+
path: string;
|
|
68
|
+
name: string;
|
|
69
|
+
};
|
|
70
|
+
export declare function catalogCoverage(state: McpExtensionState, server?: string): {
|
|
71
|
+
complete: boolean;
|
|
72
|
+
knownServers: string[];
|
|
73
|
+
unknownServers: string[];
|
|
74
|
+
};
|
|
8
75
|
export declare function findToolByName(metadata: ToolMetadata[] | undefined, toolName: string): ToolMetadata | undefined;
|
|
9
76
|
export declare function formatSchema(schema: unknown, indent?: string): string;
|
package/dist/tool-metadata.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
2
|
-
import { formatToolName, isToolAllowed, resolveToolPrefix } from "./types.js";
|
|
2
|
+
import { formatToolName, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
|
|
3
3
|
import { resourceNameToToolName } from "./resource-tools.js";
|
|
4
4
|
import { extractToolUiStreamMode } from "./utils.js";
|
|
5
5
|
import { extractUiToolVisibility, isUiToolVisibleToModel } from "./ui-tool-visibility.js";
|
|
6
6
|
export function buildToolMetadata(tools, resources, definition, serverName, prefix) {
|
|
7
7
|
const metadata = [];
|
|
8
8
|
const failedTools = [];
|
|
9
|
-
|
|
9
|
+
if (isServerDisabled(definition))
|
|
10
|
+
return { metadata, failedTools };
|
|
10
11
|
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
11
12
|
for (const tool of tools) {
|
|
12
13
|
if (!tool?.name) {
|
|
@@ -17,14 +18,10 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
|
|
|
17
18
|
continue;
|
|
18
19
|
}
|
|
19
20
|
const name = formatToolName(tool.name, serverName, effectivePrefix);
|
|
20
|
-
if (seenNames.has(name)) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
21
|
const uiVisibility = extractUiToolVisibility(tool._meta);
|
|
24
22
|
if (!isUiToolVisibleToModel(uiVisibility)) {
|
|
25
23
|
continue;
|
|
26
24
|
}
|
|
27
|
-
seenNames.add(name);
|
|
28
25
|
let uiResourceUri;
|
|
29
26
|
try {
|
|
30
27
|
uiResourceUri = getToolUiResourceUri({ _meta: tool._meta });
|
|
@@ -34,11 +31,10 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
|
|
|
34
31
|
}
|
|
35
32
|
const uiStreamMode = extractToolUiStreamMode(tool._meta);
|
|
36
33
|
metadata.push({
|
|
34
|
+
...tool,
|
|
37
35
|
name,
|
|
38
36
|
originalName: tool.name,
|
|
39
37
|
description: tool.description ?? "",
|
|
40
|
-
...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
|
|
41
|
-
...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
|
|
42
38
|
...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
|
|
43
39
|
...(uiVisibility !== undefined ? { uiVisibility } : {}),
|
|
44
40
|
...(uiStreamMode !== undefined ? { uiStreamMode } : {}),
|
|
@@ -46,20 +42,19 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
|
|
|
46
42
|
}
|
|
47
43
|
if (definition.exposeResources !== false) {
|
|
48
44
|
for (const resource of resources) {
|
|
45
|
+
if (!resource?.name || !resource?.uri)
|
|
46
|
+
continue;
|
|
49
47
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
50
48
|
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) {
|
|
51
49
|
continue;
|
|
52
50
|
}
|
|
53
51
|
const name = formatToolName(baseName, serverName, effectivePrefix);
|
|
54
|
-
if (seenNames.has(name)) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
seenNames.add(name);
|
|
58
52
|
metadata.push({
|
|
59
53
|
name,
|
|
60
54
|
originalName: baseName,
|
|
61
55
|
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
62
56
|
resourceUri: resource.uri,
|
|
57
|
+
resourceDescriptor: resource,
|
|
63
58
|
});
|
|
64
59
|
}
|
|
65
60
|
}
|
|
@@ -68,18 +63,29 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
|
|
|
68
63
|
export function totalToolCount(state) {
|
|
69
64
|
let count = 0;
|
|
70
65
|
for (const metadata of state.toolMetadata.values()) {
|
|
71
|
-
count += metadata.length;
|
|
66
|
+
count += metadata.filter(tool => tool.resourceUri === undefined).length;
|
|
72
67
|
}
|
|
73
68
|
return count;
|
|
74
69
|
}
|
|
70
|
+
export function toToolDescriptor(server, tool) {
|
|
71
|
+
const { name: path, originalName: name, resourceUri: _resourceUri, resourceDescriptor: _resourceDescriptor, uiResourceUri: _uiResourceUri, uiVisibility: _uiVisibility, uiStreamMode: _uiStreamMode, ...descriptor } = tool;
|
|
72
|
+
return { ...descriptor, server, path, name };
|
|
73
|
+
}
|
|
74
|
+
export function catalogCoverage(state, server) {
|
|
75
|
+
const servers = Object.keys(state.config.mcpServers).filter(name => (!server || name === server) && !isServerDisabled(state.config.mcpServers[name]));
|
|
76
|
+
const knownServers = servers.filter(name => state.toolMetadata.has(name));
|
|
77
|
+
const unknownServers = servers.filter(name => !state.toolMetadata.has(name));
|
|
78
|
+
return { complete: unknownServers.length === 0, knownServers, unknownServers };
|
|
79
|
+
}
|
|
75
80
|
export function findToolByName(metadata, toolName) {
|
|
76
81
|
if (!metadata)
|
|
77
82
|
return undefined;
|
|
78
|
-
const exact = metadata.
|
|
79
|
-
if (exact)
|
|
80
|
-
return exact;
|
|
83
|
+
const exact = metadata.filter(m => m.name === toolName);
|
|
84
|
+
if (exact.length > 0)
|
|
85
|
+
return exact.length === 1 ? exact[0] : undefined;
|
|
81
86
|
const normalized = toolName.replace(/-/g, "_");
|
|
82
|
-
|
|
87
|
+
const matches = metadata.filter(m => m.name.replace(/-/g, "_") === normalized);
|
|
88
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
83
89
|
}
|
|
84
90
|
export function formatSchema(schema, indent = " ") {
|
|
85
91
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
package/dist/tool-registrar.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { McpContent, ContentBlock } from "./types.ts";
|
|
2
|
+
import { type McpPayloadFile } from "./mcp-output-guard.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Transform MCP content types to Pi content blocks.
|
|
4
5
|
*/
|
|
5
6
|
export declare function transformMcpContent(content: McpContent[]): ContentBlock[];
|
|
6
|
-
/**
|
|
7
|
-
* Resolve a tool result's content blocks, falling back to structuredContent
|
|
8
|
-
* when content is empty.
|
|
9
|
-
*/
|
|
7
|
+
/** Human content and structured content are complementary protocol fields. */
|
|
10
8
|
export declare function resolveMcpResultContent(result: Record<string, unknown>): ContentBlock[];
|
|
9
|
+
/** Persist content the model cannot consume; never imply an audio/blob payload was rendered. */
|
|
10
|
+
export declare function renderMcpResultContent(result: Record<string, unknown>, payloadFiles?: McpPayloadFile[]): ContentBlock[];
|
package/dist/tool-registrar.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// This keeps the LLM context small (1 tool instead of 100s).
|
|
1
|
+
// MCP protocol content to model-visible content.
|
|
2
|
+
import { formatMcpPayloadFile } from "./mcp-output-guard.js";
|
|
4
3
|
/**
|
|
5
4
|
* Transform MCP content types to Pi content blocks.
|
|
6
5
|
*/
|
|
@@ -18,7 +17,7 @@ export function transformMcpContent(content) {
|
|
|
18
17
|
}
|
|
19
18
|
if (c.type === "resource") {
|
|
20
19
|
const resourceUri = c.resource?.uri ?? "(no URI)";
|
|
21
|
-
const resourceContent = c.resource?.text ?? (c.resource ?
|
|
20
|
+
const resourceContent = c.resource?.text ?? (c.resource?.blob !== undefined ? "[Binary content; not rendered]" : "(no content)");
|
|
22
21
|
return {
|
|
23
22
|
type: "text",
|
|
24
23
|
text: `[Resource: ${resourceUri}]\n${resourceContent}`,
|
|
@@ -35,24 +34,30 @@ export function transformMcpContent(content) {
|
|
|
35
34
|
if (c.type === "audio") {
|
|
36
35
|
return {
|
|
37
36
|
type: "text",
|
|
38
|
-
text: `[Audio content: ${c.mimeType ?? "audio/*"}]`,
|
|
37
|
+
text: `[Audio content: ${c.mimeType ?? "audio/*"}; not rendered]`,
|
|
39
38
|
};
|
|
40
39
|
}
|
|
41
40
|
return { type: "text", text: JSON.stringify(c) };
|
|
42
41
|
});
|
|
43
42
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Resolve a tool result's content blocks, falling back to structuredContent
|
|
46
|
-
* when content is empty.
|
|
47
|
-
*/
|
|
43
|
+
/** Human content and structured content are complementary protocol fields. */
|
|
48
44
|
export function resolveMcpResultContent(result) {
|
|
49
45
|
const blocks = transformMcpContent((Array.isArray(result.content) ? result.content : []));
|
|
50
|
-
if (blocks.length > 0)
|
|
51
|
-
return blocks;
|
|
52
46
|
if (result.structuredContent !== undefined) {
|
|
53
|
-
|
|
47
|
+
blocks.push({ type: "text", text: stringifyStructuredContent(result.structuredContent) });
|
|
54
48
|
}
|
|
55
|
-
return
|
|
49
|
+
return blocks;
|
|
50
|
+
}
|
|
51
|
+
/** Persist content the model cannot consume; never imply an audio/blob payload was rendered. */
|
|
52
|
+
export function renderMcpResultContent(result, payloadFiles = []) {
|
|
53
|
+
const content = Array.isArray(result.contents)
|
|
54
|
+
? result.contents.map(resource => typeof resource.text === "string" ? { type: "text", text: resource.text } : { type: "resource", resource })
|
|
55
|
+
: Array.isArray(result.content) ? result.content : [];
|
|
56
|
+
const rendered = content.map((block, index) => {
|
|
57
|
+
const file = payloadFiles.find(file => file.index === index);
|
|
58
|
+
return file ? { type: "text", text: formatMcpPayloadFile(file) } : block;
|
|
59
|
+
});
|
|
60
|
+
return resolveMcpResultContent({ ...result, content: rendered });
|
|
56
61
|
}
|
|
57
62
|
function stringifyStructuredContent(value) {
|
|
58
63
|
try {
|
|
@@ -10,6 +10,9 @@ export interface McpProxyToolCallInput {
|
|
|
10
10
|
connect?: string;
|
|
11
11
|
describe?: string;
|
|
12
12
|
search?: string;
|
|
13
|
+
query?: string;
|
|
14
|
+
uri?: string;
|
|
15
|
+
ref?: string;
|
|
13
16
|
includeSchemas?: boolean;
|
|
14
17
|
server?: string;
|
|
15
18
|
action?: string;
|
|
@@ -22,15 +25,15 @@ export interface McpToolResultDisplay {
|
|
|
22
25
|
truncated: boolean;
|
|
23
26
|
}
|
|
24
27
|
declare class CollapsibleText implements Component {
|
|
28
|
+
private readonly fullText;
|
|
29
|
+
private readonly footerText;
|
|
30
|
+
private collapsedText;
|
|
25
31
|
private readonly text;
|
|
26
32
|
private readonly expanded;
|
|
27
33
|
private readonly maxCollapsedLines;
|
|
28
34
|
private readonly ellipsis;
|
|
29
35
|
private readonly expandHint;
|
|
30
36
|
private readonly preTruncated;
|
|
31
|
-
private readonly fullText;
|
|
32
|
-
private readonly footerText;
|
|
33
|
-
private collapsedText;
|
|
34
37
|
constructor(text: string, expanded: boolean, maxCollapsedLines: number, ellipsis: string, expandHint: string, preTruncated?: boolean);
|
|
35
38
|
render(width: number): string[];
|
|
36
39
|
invalidate(): void;
|
|
@@ -9,15 +9,15 @@ const DEFAULT_MAX_COLLAPSED_LINES = 3;
|
|
|
9
9
|
const DEFAULT_MAX_COLLAPSED_CHARS = 8000;
|
|
10
10
|
const COLLAPSED_RENDER_CHAR_SLACK = 8;
|
|
11
11
|
class CollapsibleText {
|
|
12
|
+
fullText;
|
|
13
|
+
footerText;
|
|
14
|
+
collapsedText = null;
|
|
12
15
|
text;
|
|
13
16
|
expanded;
|
|
14
17
|
maxCollapsedLines;
|
|
15
18
|
ellipsis;
|
|
16
19
|
expandHint;
|
|
17
20
|
preTruncated;
|
|
18
|
-
fullText;
|
|
19
|
-
footerText;
|
|
20
|
-
collapsedText = null;
|
|
21
21
|
constructor(text, expanded, maxCollapsedLines, ellipsis, expandHint, preTruncated = false) {
|
|
22
22
|
this.text = text;
|
|
23
23
|
this.expanded = expanded;
|
|
@@ -79,8 +79,15 @@ function hasUsefulObjectContent(value) {
|
|
|
79
79
|
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
80
80
|
}
|
|
81
81
|
export function formatMcpProxyToolCallLines(args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
|
|
82
|
-
if (args.action === "ui-messages")
|
|
82
|
+
if (args.action === "ui-messages" || args.action === "status")
|
|
83
83
|
return [`mcp ${args.action}`];
|
|
84
|
+
if (args.action) {
|
|
85
|
+
const target = args.tool ?? args.query ?? args.uri ?? args.ref;
|
|
86
|
+
let line = `mcp ${args.action}${target ? ` ${target}` : ""}${args.server ? `${target ? " @" : ""} ${args.server}` : ""}`;
|
|
87
|
+
if (args.action === "search" && args.includeSchemas === false)
|
|
88
|
+
line += " (schemas hidden)";
|
|
89
|
+
return args.action === "call" && args.args ? [line, formatJsonish(args.args, maxInputChars)] : [line];
|
|
90
|
+
}
|
|
84
91
|
if (args.tool) {
|
|
85
92
|
const target = args.server ? `${args.tool} @ ${args.server}` : args.tool;
|
|
86
93
|
const lines = [`mcp call ${target}`];
|
|
@@ -102,8 +109,6 @@ export function formatMcpProxyToolCallLines(args, maxInputChars = DEFAULT_MAX_CA
|
|
|
102
109
|
}
|
|
103
110
|
if (args.server)
|
|
104
111
|
return [`mcp list ${args.server}`];
|
|
105
|
-
if (args.action)
|
|
106
|
-
return [`mcp ${args.action}`];
|
|
107
112
|
return ["mcp status"];
|
|
108
113
|
}
|
|
109
114
|
export function formatMcpDirectToolCallLines(displayName, args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
|