pi-mcp-adapter 3.3.0 → 4.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 +51 -0
- package/OAUTH.md +4 -2
- package/README.md +61 -9
- package/commands.ts +20 -2
- package/config.ts +19 -1
- package/direct-tool-surface.ts +3 -2
- package/direct-tools.ts +5 -3
- package/dist/config.d.ts +6 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/mcp-auth-flow.d.ts +2 -0
- package/dist/mcp-auth-flow.js +12 -3
- package/dist/mcp-auth-flow.js.map +1 -1
- package/dist/metadata-cache.d.ts +12 -0
- package/dist/metadata-cache.js +45 -5
- package/dist/metadata-cache.js.map +1 -1
- package/dist/output-shape.d.ts +18 -5
- package/dist/output-shape.js +162 -18
- package/dist/output-shape.js.map +1 -1
- package/dist/package-mcp-loader.js +9 -5
- package/dist/package-mcp-loader.js.map +1 -1
- package/dist/server-manager.js +27 -16
- package/dist/server-manager.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/types.d.ts +11 -1
- package/dist/types.js.map +1 -1
- package/index.ts +72 -12
- package/init.ts +20 -5
- package/mcp-auth-flow.ts +13 -2
- package/mcp-code.ts +125 -77
- package/mcp-output-guard.ts +24 -5
- package/mcp-script-worker.mjs +6 -2
- package/mcp-setup-panel.ts +583 -302
- package/metadata-cache.ts +55 -5
- package/output-shape.ts +161 -22
- package/package-mcp-loader.ts +8 -5
- package/package.json +5 -5
- package/proxy-modes.ts +125 -109
- package/server-manager.ts +28 -15
- package/skills/mcp-scripting/SKILL.md +24 -30
- package/skills/mcp-scripting/references/jev.md +3 -0
- package/state.ts +2 -0
- package/types.ts +8 -1
package/metadata-cache.ts
CHANGED
|
@@ -58,27 +58,32 @@ export function loadMetadataCache(): MetadataCache | null {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function saveMetadataCache(cache: MetadataCache): void {
|
|
61
|
+
updateMetadataCacheFile(servers => ({ ...servers, ...cache.servers }));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Reads the cache file, applies one update, and writes the result; an update returning undefined skips the write. */
|
|
65
|
+
function updateMetadataCacheFile(update: (servers: MetadataCache["servers"]) => MetadataCache["servers"] | undefined): void {
|
|
61
66
|
const cachePath = getMetadataCachePath();
|
|
62
67
|
const dir = dirname(cachePath);
|
|
63
68
|
mkdirSync(dir, { recursive: true });
|
|
64
69
|
|
|
65
|
-
let
|
|
70
|
+
let servers: MetadataCache["servers"] = {};
|
|
66
71
|
try {
|
|
67
72
|
if (existsSync(cachePath)) {
|
|
68
73
|
const existing = JSON.parse(readFileSync(cachePath, "utf-8")) as MetadataCache;
|
|
69
74
|
if (existing && existing.version === CACHE_VERSION && existing.servers) {
|
|
70
|
-
|
|
75
|
+
servers = { ...existing.servers };
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
} catch {
|
|
74
79
|
// Ignore parse errors and proceed with empty cache
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
const next = update(servers);
|
|
83
|
+
if (!next) return;
|
|
79
84
|
|
|
80
85
|
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
81
|
-
writeFileSync(tmpPath, JSON.stringify(
|
|
86
|
+
writeFileSync(tmpPath, JSON.stringify({ version: CACHE_VERSION, servers: next }), "utf-8");
|
|
82
87
|
renameSync(tmpPath, cachePath);
|
|
83
88
|
}
|
|
84
89
|
|
|
@@ -117,6 +122,49 @@ export function computeServerHash(definition: ServerEntry, environment: NodeJS.P
|
|
|
117
122
|
return createHash("sha256").update(normalized).digest("hex");
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Identifies the tool definition an output shape was learned against. A shape is only used or saved while
|
|
127
|
+
* the tool's description and input schema still match, since a change there can mean a different result.
|
|
128
|
+
*/
|
|
129
|
+
export function outputShapeKey(tool: Pick<CachedTool, "description" | "inputSchema">): string {
|
|
130
|
+
return stableStringify({ description: tool.description ?? "", inputSchema: tool.inputSchema ?? null });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Saves one tool's observed output shape into its server's cache entry, if the entry matches the running config
|
|
135
|
+
* and tool. The entry comes from the same read the write replaces, so newer metadata from other Pi processes is kept.
|
|
136
|
+
*/
|
|
137
|
+
export function saveObservedOutput(
|
|
138
|
+
serverName: string,
|
|
139
|
+
definition: ServerEntry,
|
|
140
|
+
toolName: string,
|
|
141
|
+
toolKey: string,
|
|
142
|
+
output: NonNullable<ServerCacheEntry["outputShapes"]>[string],
|
|
143
|
+
): void {
|
|
144
|
+
const configHash = computeServerHash(definition);
|
|
145
|
+
updateMetadataCacheFile(servers => {
|
|
146
|
+
const entry = servers[serverName];
|
|
147
|
+
const cachedTool = entry?.tools?.find(tool => tool.name === toolName);
|
|
148
|
+
if (!entry || !cachedTool || entry.configHash !== configHash || outputShapeKey(cachedTool) !== toolKey) return undefined;
|
|
149
|
+
return { ...servers, [serverName]: { ...entry, outputShapes: { ...entry.outputShapes, [toolName]: output } } };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Output shapes to carry into a rewritten cache entry: same config, not private, and only tools whose definition kept its shape key. */
|
|
154
|
+
export function keepOutputShapes(
|
|
155
|
+
previous: ServerCacheEntry | undefined,
|
|
156
|
+
configHash: string,
|
|
157
|
+
tools: CachedTool[],
|
|
158
|
+
): ServerCacheEntry["outputShapes"] {
|
|
159
|
+
if (!previous?.outputShapes || previous.configHash !== configHash || previous.cacheScope === "private") return undefined;
|
|
160
|
+
const kept = Object.entries(previous.outputShapes).filter(([toolName]) => {
|
|
161
|
+
const before = previous.tools?.find(tool => tool.name === toolName);
|
|
162
|
+
const after = tools.find(tool => tool.name === toolName);
|
|
163
|
+
return before !== undefined && after !== undefined && outputShapeKey(before) === outputShapeKey(after);
|
|
164
|
+
});
|
|
165
|
+
return kept.length > 0 ? Object.fromEntries(kept) : undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
120
168
|
export function isServerCacheValid(
|
|
121
169
|
entry: ServerCacheEntry,
|
|
122
170
|
definition: ServerEntry,
|
|
@@ -131,6 +179,8 @@ export function isServerCacheValid(
|
|
|
131
179
|
}
|
|
132
180
|
if (!entry || entry.configHash !== configHash) return false;
|
|
133
181
|
if (!entry.cachedAt || typeof entry.cachedAt !== "number") return false;
|
|
182
|
+
// The persistent cache is not partitioned by authorization context.
|
|
183
|
+
if (entry.cacheScope === "private") return false;
|
|
134
184
|
const declaredTtlMs = entry.ttlMs;
|
|
135
185
|
if (typeof declaredTtlMs === "number" && Number.isSafeInteger(declaredTtlMs) && declaredTtlMs >= 0) {
|
|
136
186
|
if (declaredTtlMs === 0) return false;
|
package/output-shape.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { logger } from "./logger.ts";
|
|
2
|
+
import { outputShapeKey, saveObservedOutput } from "./metadata-cache.ts";
|
|
1
3
|
import type { McpExtensionState } from "./state.ts";
|
|
2
4
|
import { formatPropertyName } from "./ts-shape.ts";
|
|
3
|
-
import type { ToolMetadata } from "./types.ts";
|
|
5
|
+
import type { ServerCacheEntry, ServerDefinition, ToolMetadata } from "./types.ts";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
8
|
+
* Output shapes for tools that declare no outputSchema.
|
|
7
9
|
* Field names and broad JSON types only, never values; every dimension is
|
|
8
10
|
* bounded because inference runs on arbitrary upstream payloads after each call.
|
|
11
|
+
* Script sessions also save them in the metadata cache for later sessions.
|
|
9
12
|
*/
|
|
10
13
|
export interface OutputShape {
|
|
11
14
|
type?: "null" | "boolean" | "number" | "string" | "object" | "array";
|
|
@@ -19,7 +22,13 @@ export interface OutputShape {
|
|
|
19
22
|
export interface ObservedOutput {
|
|
20
23
|
source: "structuredContent" | "jsonText";
|
|
21
24
|
shape: OutputShape;
|
|
22
|
-
|
|
25
|
+
/** outputShapeKey of the tool definition the shape was learned against; other definitions do not see it. */
|
|
26
|
+
toolKey: string;
|
|
27
|
+
/**
|
|
28
|
+
* True once any observation merged into the shape was made under a private tool listing, which is tied to one
|
|
29
|
+
* authorization context. Such a shape is used in this session only and never saved.
|
|
30
|
+
*/
|
|
31
|
+
private: boolean;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
const MAX_DEPTH = 6;
|
|
@@ -33,61 +42,191 @@ const MAX_NODES_PER_CALL = 1000;
|
|
|
33
42
|
// Sizes count UTF-16 code units (string length), not bytes.
|
|
34
43
|
const MAX_SHAPE_CHARS = 4 * 1024;
|
|
35
44
|
const MAX_JSON_TEXT_CHARS = 256 * 1024;
|
|
45
|
+
// Object shapes at least this long that appear more than once are written once as a named type.
|
|
46
|
+
const MIN_ALIAS_CHARS = 80;
|
|
47
|
+
const SHAPE_TYPES = new Set(["null", "boolean", "number", "string", "object", "array"]);
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Looks the tool up when the call starts, so its result is recorded against the definition it was
|
|
51
|
+
* requested under even if a tools/list_changed refresh replaces that definition mid-call.
|
|
52
|
+
*/
|
|
53
|
+
export function observedOutputRecorder(state: McpExtensionState, serverName: string, toolName: string): (result: Record<string, unknown>) => void {
|
|
54
|
+
const tool = findTool(state, serverName, toolName);
|
|
55
|
+
const listingIsPrivate = () => state.manager.getConnection(serverName)?.toolListHints?.cacheScope === "private";
|
|
56
|
+
const privateAtStart = listingIsPrivate();
|
|
57
|
+
return result => recordObservedOutput(state, serverName, tool, privateAtStart || listingIsPrivate(), result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function recordObservedOutput(
|
|
38
61
|
state: McpExtensionState,
|
|
39
62
|
serverName: string,
|
|
40
|
-
|
|
63
|
+
tool: ToolMetadata | undefined,
|
|
64
|
+
observedPrivately: boolean,
|
|
41
65
|
result: Record<string, unknown>,
|
|
42
66
|
): void {
|
|
43
|
-
if (
|
|
67
|
+
if (!tool || tool.outputSchema !== undefined) return;
|
|
68
|
+
const toolName = tool.originalName;
|
|
44
69
|
const observed = readResultValue(result);
|
|
45
70
|
if (!observed) return;
|
|
46
71
|
const definition = state.config.mcpServers[serverName];
|
|
47
72
|
if (!definition) return;
|
|
48
73
|
const shape = inferShape(observed.value, 0, { nodes: MAX_NODES_PER_CALL });
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
if (!byTool) observedOutputs.set(definition, byTool = new Map());
|
|
74
|
+
const byTool = observedOutputsFor(state, definition);
|
|
75
|
+
const toolKey = outputShapeKey(tool);
|
|
52
76
|
const previous = byTool.get(toolName);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
const mergeable = previous?.source === observed.source && previous.toolKey === toolKey;
|
|
78
|
+
const next: ObservedOutput = {
|
|
79
|
+
source: observed.source,
|
|
80
|
+
shape: fitShape(mergeable ? mergeShapes(previous.shape, shape) : shape),
|
|
81
|
+
toolKey,
|
|
82
|
+
private: observedPrivately || (mergeable && previous.private),
|
|
83
|
+
};
|
|
84
|
+
byTool.set(toolName, next);
|
|
85
|
+
// Skip unchanged shapes to avoid repeated cache writes; sessions without mcpScript never write.
|
|
86
|
+
if (state.scriptTool === true && !next.private && JSON.stringify(next) !== JSON.stringify(previous)) {
|
|
87
|
+
try {
|
|
88
|
+
saveObservedOutput(serverName, definition, toolName, toolKey, { source: next.source, shape: next.shape });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// The shape stays in memory for this session; only later sessions miss it.
|
|
91
|
+
logger.debug(`MCP: failed to save output shape for ${serverName}/${toolName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Adds shapes from a cache entry, tagged with the entry's own tool definitions, unless this session already has one for that definition. */
|
|
97
|
+
export function seedObservedOutputs(state: McpExtensionState, serverName: string, entry: Pick<ServerCacheEntry, "tools" | "outputShapes">): void {
|
|
98
|
+
const definition = state.config.mcpServers[serverName];
|
|
99
|
+
const saved = entry.outputShapes;
|
|
100
|
+
if (!definition || typeof saved !== "object" || saved === null) return;
|
|
101
|
+
const byTool = observedOutputsFor(state, definition);
|
|
102
|
+
for (const [toolName, value] of Object.entries(saved)) {
|
|
103
|
+
const cachedTool = Array.isArray(entry.tools) ? entry.tools.find(tool => tool?.name === toolName) : undefined;
|
|
104
|
+
if (!cachedTool || typeof value !== "object" || value === null) continue;
|
|
105
|
+
const toolKey = outputShapeKey(cachedTool);
|
|
106
|
+
if (byTool.get(toolName)?.toolKey === toolKey) continue;
|
|
107
|
+
const { source, shape } = value as { source?: unknown; shape?: unknown };
|
|
108
|
+
if (source !== "structuredContent" && source !== "jsonText") continue;
|
|
109
|
+
const parsed = readSavedShape(shape, 0);
|
|
110
|
+
if (parsed && !isUnknown(parsed)) byTool.set(toolName, { source, shape: fitShape(parsed), toolKey, private: false });
|
|
111
|
+
}
|
|
56
112
|
}
|
|
57
113
|
|
|
58
114
|
export function getObservedOutput(
|
|
59
115
|
state: McpExtensionState,
|
|
60
116
|
serverName: string,
|
|
61
|
-
tool: Pick<ToolMetadata, "originalName" | "outputSchema">,
|
|
117
|
+
tool: Pick<ToolMetadata, "originalName" | "outputSchema" | "description" | "inputSchema">,
|
|
62
118
|
): ObservedOutput | undefined {
|
|
63
119
|
if (tool.outputSchema !== undefined) return undefined;
|
|
64
120
|
const definition = state.config.mcpServers[serverName];
|
|
65
121
|
const observed = definition && state.observedOutputs?.get(definition)?.get(tool.originalName);
|
|
66
|
-
return observed && !isUnknown(observed.shape) ? observed : undefined;
|
|
122
|
+
return observed && observed.toolKey === outputShapeKey(tool) && !isUnknown(observed.shape) ? observed : undefined;
|
|
67
123
|
}
|
|
68
124
|
|
|
69
125
|
export function renderOutputShape(shape: OutputShape): string {
|
|
70
|
-
|
|
126
|
+
// First pass: each object's full text, counted, so APIs that repeat a wide object (a user under
|
|
127
|
+
// user, assignee, and assignees) show its fields once instead of three times.
|
|
128
|
+
const plain = new Map<OutputShape, string>();
|
|
129
|
+
const uses = new Map<string, { count: number; field: string }>();
|
|
130
|
+
const once = renderShape(shape, "item", (node, field, text) => {
|
|
131
|
+
plain.set(node, text);
|
|
132
|
+
const use = uses.get(text);
|
|
133
|
+
if (use) use.count++;
|
|
134
|
+
else uses.set(text, { count: 1, field });
|
|
135
|
+
return text;
|
|
136
|
+
});
|
|
137
|
+
const names = new Map<string, string>();
|
|
138
|
+
const taken = new Set(["Record"]);
|
|
139
|
+
for (const [text, use] of uses) {
|
|
140
|
+
if (use.count < 2 || text.length < MIN_ALIAS_CHARS) continue;
|
|
141
|
+
const words = use.field.split(/[^A-Za-z0-9]+/).filter(Boolean).map(part => part[0]!.toUpperCase() + part.slice(1)).join("");
|
|
142
|
+
const base = /^[A-Za-z]/.test(words) ? words : `T${words}`;
|
|
143
|
+
let name = base;
|
|
144
|
+
for (let suffix = 2; taken.has(name); suffix++) name = `${base}${suffix}`;
|
|
145
|
+
taken.add(name);
|
|
146
|
+
names.set(text, name);
|
|
147
|
+
}
|
|
148
|
+
if (names.size === 0) return once;
|
|
149
|
+
const definitions = new Map<string, string>();
|
|
150
|
+
const main = renderShape(shape, "item", (node, _field, text) => {
|
|
151
|
+
const name = names.get(plain.get(node)!);
|
|
152
|
+
if (!name) return text;
|
|
153
|
+
if (!definitions.has(name)) definitions.set(name, text);
|
|
154
|
+
return name;
|
|
155
|
+
});
|
|
156
|
+
return [...[...definitions].map(([name, body]) => `type ${name} = ${body};`), main].join("\n");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderShape(
|
|
160
|
+
shape: OutputShape,
|
|
161
|
+
field: string,
|
|
162
|
+
onObject: (node: OutputShape, field: string, text: string) => string,
|
|
163
|
+
): string {
|
|
164
|
+
if (shape.anyOf) return shape.anyOf.map(variant => renderShape(variant, field, onObject)).join(" | ");
|
|
71
165
|
switch (shape.type) {
|
|
72
166
|
case undefined:
|
|
73
167
|
return "unknown";
|
|
74
168
|
case "object": {
|
|
75
|
-
if (shape.additionalProperties) return `Record<string, ${
|
|
169
|
+
if (shape.additionalProperties) return `Record<string, ${renderShape(shape.additionalProperties, field, onObject)}>`;
|
|
76
170
|
const required = new Set(shape.required);
|
|
77
171
|
const properties = Object.entries(shape.properties ?? {})
|
|
78
|
-
.map(([name, property]) => `${formatPropertyName(name)}${required.has(name) ? "" : "?"}: ${
|
|
79
|
-
return properties.length === 0 ? "{}" : `{ ${properties.join(" ")} }
|
|
172
|
+
.map(([name, property]) => `${formatPropertyName(name)}${required.has(name) ? "" : "?"}: ${renderShape(property, name, onObject)};`);
|
|
173
|
+
return onObject(shape, field, properties.length === 0 ? "{}" : `{ ${properties.join(" ")} }`);
|
|
80
174
|
}
|
|
81
175
|
case "array": {
|
|
82
176
|
if (!shape.items) return "unknown[]";
|
|
83
|
-
const item =
|
|
84
|
-
return
|
|
177
|
+
const item = renderShape(shape.items, field, onObject);
|
|
178
|
+
return shape.items.anyOf ? `(${item})[]` : `${item}[]`;
|
|
85
179
|
}
|
|
86
180
|
default:
|
|
87
181
|
return shape.type;
|
|
88
182
|
}
|
|
89
183
|
}
|
|
90
184
|
|
|
185
|
+
function observedOutputsFor(state: McpExtensionState, definition: ServerDefinition): Map<string, ObservedOutput> {
|
|
186
|
+
const observedOutputs = state.observedOutputs ??= new WeakMap();
|
|
187
|
+
let byTool = observedOutputs.get(definition);
|
|
188
|
+
if (!byTool) observedOutputs.set(definition, byTool = new Map());
|
|
189
|
+
return byTool;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Rebuilds a shape read from the cache file, or returns undefined when any part is not something inferShape writes. */
|
|
193
|
+
function readSavedShape(value: unknown, depth: number): OutputShape | undefined {
|
|
194
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || depth > MAX_DEPTH) return undefined;
|
|
195
|
+
const raw = value as Record<string, unknown>;
|
|
196
|
+
if (raw.anyOf !== undefined) {
|
|
197
|
+
// Inference writes flat unions only; rejecting nested ones first keeps recursion bounded by MAX_DEPTH.
|
|
198
|
+
if (!Array.isArray(raw.anyOf) || raw.anyOf.length < 2 || raw.anyOf.length > MAX_UNION
|
|
199
|
+
|| raw.anyOf.some(variant => typeof variant !== "object" || variant === null || "anyOf" in variant)) return undefined;
|
|
200
|
+
const variants = raw.anyOf.map(variant => readSavedShape(variant, depth));
|
|
201
|
+
return variants.every(variant => variant?.type !== undefined) ? { anyOf: variants as OutputShape[] } : undefined;
|
|
202
|
+
}
|
|
203
|
+
if (raw.type === undefined) return {};
|
|
204
|
+
if (typeof raw.type !== "string" || !SHAPE_TYPES.has(raw.type)) return undefined;
|
|
205
|
+
const shape: OutputShape = { type: raw.type as NonNullable<OutputShape["type"]> };
|
|
206
|
+
for (const key of ["items", "additionalProperties"] as const) {
|
|
207
|
+
if (raw[key] === undefined) continue;
|
|
208
|
+
const child = readSavedShape(raw[key], depth + 1);
|
|
209
|
+
if (!child) return undefined;
|
|
210
|
+
shape[key] = child;
|
|
211
|
+
}
|
|
212
|
+
if (raw.properties !== undefined) {
|
|
213
|
+
if (typeof raw.properties !== "object" || raw.properties === null || Array.isArray(raw.properties)) return undefined;
|
|
214
|
+
const entries = Object.entries(raw.properties);
|
|
215
|
+
if (entries.length > MAX_OBJECT_KEYS) return undefined;
|
|
216
|
+
const properties: Record<string, OutputShape> = {};
|
|
217
|
+
for (const [key, child] of entries) {
|
|
218
|
+
const property = key !== "__proto__" && FIELD_NAME.test(key) ? readSavedShape(child, depth + 1) : undefined;
|
|
219
|
+
if (!property) return undefined;
|
|
220
|
+
properties[key] = property;
|
|
221
|
+
}
|
|
222
|
+
shape.properties = properties;
|
|
223
|
+
shape.required = Array.isArray(raw.required)
|
|
224
|
+
? raw.required.filter((key): key is string => typeof key === "string" && Object.hasOwn(properties, key))
|
|
225
|
+
: [];
|
|
226
|
+
}
|
|
227
|
+
return shape;
|
|
228
|
+
}
|
|
229
|
+
|
|
91
230
|
function findTool(state: McpExtensionState, serverName: string, toolName: string): ToolMetadata | undefined {
|
|
92
231
|
return state.toolMetadata.get(serverName)?.find(tool => tool.originalName === toolName && !tool.resourceUri);
|
|
93
232
|
}
|
|
@@ -181,11 +320,11 @@ function mergeSameType(left: OutputShape, right: OutputShape): OutputShape {
|
|
|
181
320
|
return { type: "object", properties, required: (left.required ?? []).filter(key => rightRequired.has(key)) };
|
|
182
321
|
}
|
|
183
322
|
|
|
184
|
-
/** Drops nesting from the deepest level up until the
|
|
323
|
+
/** Drops nesting from the deepest level up until the rendered shape, which is what describe shows, fits the size bound. */
|
|
185
324
|
function fitShape(shape: OutputShape): OutputShape {
|
|
186
325
|
for (let depth = MAX_DEPTH; depth > 0; depth--) {
|
|
187
326
|
const pruned = pruneShape(shape, depth);
|
|
188
|
-
if (
|
|
327
|
+
if (renderOutputShape(pruned).length <= MAX_SHAPE_CHARS) return pruned;
|
|
189
328
|
}
|
|
190
329
|
return {};
|
|
191
330
|
}
|
package/package-mcp-loader.ts
CHANGED
|
@@ -103,11 +103,14 @@ function resolvePackageRoot(source: string, scope: "user" | "project", cwd: stri
|
|
|
103
103
|
? source.slice(4).trim()
|
|
104
104
|
: /^(?:(?:https?|ssh):\/\/|git@[^:]+:)/.test(source) ? source : null;
|
|
105
105
|
if (gitSource) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
106
|
+
// Pi installs URL sources under the bare hostname, without user or port.
|
|
107
|
+
const isUrl = /^[a-z]+:\/\//i.test(gitSource);
|
|
108
|
+
if (isUrl && !URL.canParse(gitSource)) return null;
|
|
109
|
+
const url = isUrl ? new URL(gitSource) : null;
|
|
110
|
+
const value = url ? url.hostname + url.pathname : gitSource.replace(/^git@([^:]+):/, "$1/");
|
|
111
|
+
// Like Pi, a non-empty ref starts at the first "@" after the host, so refs such as "@feature/x" keep their slash.
|
|
112
|
+
const refStart = value.indexOf("@", value.indexOf("/"));
|
|
113
|
+
const path = (refStart < 0 || refStart === value.length - 1 ? value : value.slice(0, refStart)).replace(/\.git$/, "");
|
|
111
114
|
return path && !path.startsWith("/") ? resolveContainedPath(join(baseDir, "git"), path) : null;
|
|
112
115
|
}
|
|
113
116
|
return isAbsolute(source) ? resolve(source) : resolve(baseDir, source);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-adapter",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.ts",
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
"zod": "^3.25.0 || ^4.0.0"
|
|
197
197
|
},
|
|
198
198
|
"peerDependencies": {
|
|
199
|
-
"@earendil-works/pi-ai": "^0.84.1 || ^0.85.0 || ^0.86.0 || ^0.87.0",
|
|
199
|
+
"@earendil-works/pi-ai": "^0.84.1 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.0",
|
|
200
200
|
"@earendil-works/pi-tui": "*",
|
|
201
201
|
"typebox": "*",
|
|
202
202
|
"zod": "^3.25.0 || ^4.0.0"
|
|
@@ -213,9 +213,9 @@
|
|
|
213
213
|
}
|
|
214
214
|
},
|
|
215
215
|
"devDependencies": {
|
|
216
|
-
"@earendil-works/pi-ai": "0.
|
|
217
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
218
|
-
"@earendil-works/pi-tui": "0.
|
|
216
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
217
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
218
|
+
"@earendil-works/pi-tui": "0.99.1",
|
|
219
219
|
"@modelcontextprotocol/conformance": "0.1.16",
|
|
220
220
|
"@types/bun": "^1.0.0",
|
|
221
221
|
"@types/cross-spawn": "6.0.6",
|