@stll/folio-cli 0.3.4 → 0.4.1
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/README.md +37 -9
- package/dist/batch-operations.d.ts +29 -0
- package/dist/batch-operations.js +226 -0
- package/dist/document.d.ts +16 -2
- package/dist/document.js +32 -8
- package/dist/execute-read.d.ts +14 -10
- package/dist/execute-read.js +27 -15
- package/dist/execute-write.d.ts +1 -1
- package/dist/execute-write.js +36 -16
- package/dist/mcp-kit/envelope.d.ts +27 -0
- package/dist/mcp-kit/envelope.js +80 -0
- package/dist/mcp-kit/index.d.ts +6 -0
- package/dist/mcp-kit/index.js +5 -0
- package/dist/mcp-kit/input.d.ts +23 -0
- package/dist/mcp-kit/input.js +76 -0
- package/dist/mcp-kit/schema.d.ts +20 -0
- package/dist/mcp-kit/schema.js +97 -0
- package/dist/mcp-kit/surface.d.ts +19 -0
- package/dist/mcp-kit/surface.js +312 -0
- package/dist/mcp-kit/types.d.ts +98 -0
- package/dist/mcp-kit/types.js +1 -0
- package/dist/mcp.d.ts +8 -2
- package/dist/mcp.js +486 -72
- package/dist/package.js +1 -1
- package/dist/registry.js +4 -2
- package/dist/save.d.ts +1 -1
- package/package.json +4 -3
package/dist/execute-write.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expandBatchOperations } from "./batch-operations.js";
|
|
1
2
|
import { checkExpectedVersion, openReviewer, readDocumentFile } from "./document.js";
|
|
2
3
|
import { acquireLeaseForWrite } from "./editor-lease.js";
|
|
3
4
|
import { FOLIO_CLI_ERROR_CODES, cliError } from "./errors.js";
|
|
@@ -8,10 +9,10 @@ import { diffPackages, nextRevisionIdSeed } from "./package-parts.js";
|
|
|
8
9
|
import { SIDECAR_DIRECTORY, inspectPath, readSidecarFile, unsafePath } from "./sidecar.js";
|
|
9
10
|
import { commitTransaction } from "./transaction.js";
|
|
10
11
|
import { Result, panic } from "better-result";
|
|
11
|
-
import { createReviewerBridge } from "@stll/folio-agents/bridges/reviewer";
|
|
12
|
-
import { generateRedlineDocx } from "@stll/folio-agents/compare";
|
|
13
12
|
import { executeFolioToolCallUntyped } from "@stll/folio-agents/execute";
|
|
14
13
|
import { FOLIO_AGENT_TOOL_NAMES } from "@stll/folio-agents/types";
|
|
14
|
+
import { createReviewerBridge } from "@stll/folio-agents/bridges/reviewer";
|
|
15
|
+
import { generateRedlineDocx } from "@stll/folio-agents/compare";
|
|
15
16
|
import { realpath, stat } from "node:fs/promises";
|
|
16
17
|
import path from "node:path";
|
|
17
18
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -110,32 +111,50 @@ const saveReviewer = async (reviewer, repack) => {
|
|
|
110
111
|
const commentExists = (reviewer, commentId) => reviewer.getComments().some((thread) => String(thread.id) === commentId || thread.replies.some((reply) => String(reply.id) === commentId));
|
|
111
112
|
const mutateWithAgentTool = async ({ tool, source, args, options }) => {
|
|
112
113
|
if (tool.type !== "agentWrite") return panic("mutateWithAgentTool needs an agentWrite tool");
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
114
|
+
const opened = await openReviewer(source, options.author);
|
|
115
|
+
if (opened.isErr()) return Result.err(opened.error);
|
|
116
|
+
const { reviewer } = opened.value;
|
|
115
117
|
const idSeed = await nextRevisionIdSeed(source.bytes);
|
|
116
118
|
if (idSeed.isErr()) return Result.err(idSeed.error);
|
|
117
|
-
if ((tool.agentTool === FOLIO_AGENT_TOOL_NAMES.replyComment || tool.agentTool === FOLIO_AGENT_TOOL_NAMES.resolveComment) && !commentExists(reviewer
|
|
119
|
+
if ((tool.agentTool === FOLIO_AGENT_TOOL_NAMES.replyComment || tool.agentTool === FOLIO_AGENT_TOOL_NAMES.resolveComment) && !commentExists(reviewer, args["commentId"])) return Result.err(cliError({
|
|
118
120
|
code: FOLIO_CLI_ERROR_CODES.notFound,
|
|
119
121
|
message: `No comment with id ${JSON.stringify(args["commentId"])}.`,
|
|
120
122
|
hint: "Take the id from folio comments."
|
|
121
123
|
}));
|
|
122
|
-
const bridge = createReviewerBridge(reviewer
|
|
124
|
+
const bridge = createReviewerBridge(reviewer, {
|
|
123
125
|
mode: tool.editMode === "tracked-or-direct" ? options.mode : "tracked-changes",
|
|
124
126
|
revisionStamp: {
|
|
125
127
|
date: options.date,
|
|
126
128
|
idSeed: idSeed.value
|
|
127
129
|
}
|
|
128
130
|
});
|
|
129
|
-
|
|
131
|
+
let callArgs = args;
|
|
132
|
+
let callOptions = {};
|
|
133
|
+
let replaced = [];
|
|
134
|
+
if (tool.agentTool === FOLIO_AGENT_TOOL_NAMES.suggestChanges) {
|
|
135
|
+
const batch = expandBatchOperations(args["operations"], bridge);
|
|
136
|
+
if (batch.isErr()) return Result.err(batch.error);
|
|
137
|
+
callArgs = {
|
|
138
|
+
...args,
|
|
139
|
+
operations: batch.value.operations
|
|
140
|
+
};
|
|
141
|
+
callOptions = batch.value.options;
|
|
142
|
+
replaced = batch.value.replaced;
|
|
143
|
+
}
|
|
144
|
+
const executed = executeFolioToolCallUntyped(tool.agentTool, callArgs, bridge, callOptions);
|
|
130
145
|
if (!executed.ok) return Result.err(invalidInput(executed.error));
|
|
131
146
|
const refusal = refusalFor(executed.result);
|
|
132
147
|
if (refusal !== null) return Result.err(refusal);
|
|
133
|
-
const saved = await saveReviewer(reviewer
|
|
148
|
+
const saved = await saveReviewer(reviewer, options.repack);
|
|
134
149
|
if (saved.isErr()) return Result.err(saved.error);
|
|
135
150
|
const receipts = isRecord(executed.result) && Array.isArray(executed.result["receipts"]) ? executed.result["receipts"] : [];
|
|
151
|
+
const result = replaced.length > 0 && isRecord(executed.result) ? {
|
|
152
|
+
...executed.result,
|
|
153
|
+
replaced
|
|
154
|
+
} : executed.result;
|
|
136
155
|
return Result.ok({
|
|
137
156
|
...saved.value,
|
|
138
|
-
result
|
|
157
|
+
result,
|
|
139
158
|
receipts
|
|
140
159
|
});
|
|
141
160
|
};
|
|
@@ -145,11 +164,12 @@ const resolveChanges = async ({ source, args, options }) => {
|
|
|
145
164
|
if (!isResolveAction(action)) return Result.err(invalidInput("resolve_changes needs action \"accept\" or \"reject\"."));
|
|
146
165
|
const idList = Array.isArray(ids) && ids.every((id) => typeof id === "string") ? ids : void 0;
|
|
147
166
|
if ((idList !== void 0 && idList.length > 0) === (all === true)) return Result.err(invalidInput("Pass change ids or all, exactly one of them.", "folio accept --id 12 or --all."));
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
167
|
+
const opened = await openReviewer(source, options.author);
|
|
168
|
+
if (opened.isErr()) return Result.err(opened.error);
|
|
169
|
+
const { reviewer } = opened.value;
|
|
150
170
|
let resolved;
|
|
151
171
|
if (all === true) {
|
|
152
|
-
const count = action === "accept" ? reviewer.
|
|
172
|
+
const count = action === "accept" ? reviewer.acceptAll() : reviewer.rejectAll();
|
|
153
173
|
if (count === 0) return Result.err(cliError({
|
|
154
174
|
code: FOLIO_CLI_ERROR_CODES.operationRejected,
|
|
155
175
|
message: "The document has no pending tracked changes; nothing was written."
|
|
@@ -157,7 +177,7 @@ const resolveChanges = async ({ source, args, options }) => {
|
|
|
157
177
|
resolved = count;
|
|
158
178
|
} else {
|
|
159
179
|
const selected = idList ?? [];
|
|
160
|
-
const known = new Set(reviewer.
|
|
180
|
+
const known = new Set(reviewer.getChanges().map(({ id }) => String(id)));
|
|
161
181
|
const missing = selected.filter((id) => !known.has(id));
|
|
162
182
|
if (missing.length > 0) return Result.err(cliError({
|
|
163
183
|
code: FOLIO_CLI_ERROR_CODES.notFound,
|
|
@@ -165,20 +185,20 @@ const resolveChanges = async ({ source, args, options }) => {
|
|
|
165
185
|
hint: "Take ids from folio changes.",
|
|
166
186
|
details: { missing }
|
|
167
187
|
}));
|
|
168
|
-
for (const id of selected) if (!(action === "accept" ? reviewer.
|
|
188
|
+
for (const id of selected) if (!(action === "accept" ? reviewer.acceptChange(Number(id)) : reviewer.rejectChange(Number(id)))) return Result.err(cliError({
|
|
169
189
|
code: FOLIO_CLI_ERROR_CODES.operationRejected,
|
|
170
190
|
message: `Change ${id} could not be ${action}ed; nothing was written.`
|
|
171
191
|
}));
|
|
172
192
|
resolved = selected;
|
|
173
193
|
}
|
|
174
|
-
const saved = await saveReviewer(reviewer
|
|
194
|
+
const saved = await saveReviewer(reviewer, options.repack);
|
|
175
195
|
if (saved.isErr()) return Result.err(saved.error);
|
|
176
196
|
return Result.ok({
|
|
177
197
|
...saved.value,
|
|
178
198
|
result: {
|
|
179
199
|
action,
|
|
180
200
|
resolved,
|
|
181
|
-
remaining: reviewer.
|
|
201
|
+
remaining: reviewer.getChanges().length
|
|
182
202
|
},
|
|
183
203
|
receipts: []
|
|
184
204
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ToolCallResult, ToolError, ToolInputIssue, ToolOutcome } from "./types.js";
|
|
2
|
+
//#region src/mcp-kit/envelope.d.ts
|
|
3
|
+
/** Error codes the surface itself answers with; a tool adds its own. */
|
|
4
|
+
declare const KIT_ERROR_CODES: {
|
|
5
|
+
readonly validation: "validation_error";
|
|
6
|
+
readonly unknownTool: "unknown_tool";
|
|
7
|
+
readonly notFound: "not_found";
|
|
8
|
+
readonly internal: "internal_error";
|
|
9
|
+
};
|
|
10
|
+
declare const toolError: (error: {
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
hint?: string | undefined;
|
|
14
|
+
retryable?: boolean | undefined;
|
|
15
|
+
issues?: readonly ToolInputIssue[] | undefined;
|
|
16
|
+
details?: unknown;
|
|
17
|
+
}) => ToolError;
|
|
18
|
+
declare const failure: (error: Parameters<typeof toolError>[0]) => ToolOutcome;
|
|
19
|
+
declare const success: (value: unknown) => ToolOutcome;
|
|
20
|
+
declare const validationError: (message: string, issues: readonly ToolInputIssue[], hint?: string) => ToolOutcome;
|
|
21
|
+
/** Encode an outcome as a `tools/call` result, with the input notes after a success. */
|
|
22
|
+
declare const toCallResult: (outcome: ToolOutcome, notes?: readonly string[]) => ToolCallResult;
|
|
23
|
+
/** Up to three known names close to `name`, closest first. */
|
|
24
|
+
declare const closestNames: (name: string, known: readonly string[]) => string[];
|
|
25
|
+
declare const didYouMean: (suggestions: readonly string[]) => string;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { KIT_ERROR_CODES, closestNames, didYouMean, failure, success, toCallResult, toolError, validationError };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/mcp-kit/envelope.ts
|
|
2
|
+
/** Error codes the surface itself answers with; a tool adds its own. */
|
|
3
|
+
const KIT_ERROR_CODES = {
|
|
4
|
+
validation: "validation_error",
|
|
5
|
+
unknownTool: "unknown_tool",
|
|
6
|
+
notFound: "not_found",
|
|
7
|
+
internal: "internal_error"
|
|
8
|
+
};
|
|
9
|
+
const toolError = (error) => ({
|
|
10
|
+
code: error.code,
|
|
11
|
+
message: error.message,
|
|
12
|
+
...error.hint !== void 0 && { hint: error.hint },
|
|
13
|
+
retryable: error.retryable ?? false,
|
|
14
|
+
...error.issues !== void 0 && error.issues.length > 0 && { issues: error.issues },
|
|
15
|
+
...error.details !== void 0 && { details: error.details }
|
|
16
|
+
});
|
|
17
|
+
const failure = (error) => ({
|
|
18
|
+
ok: false,
|
|
19
|
+
error: toolError(error)
|
|
20
|
+
});
|
|
21
|
+
const success = (value) => ({
|
|
22
|
+
ok: true,
|
|
23
|
+
value
|
|
24
|
+
});
|
|
25
|
+
const validationError = (message, issues, hint) => failure({
|
|
26
|
+
code: KIT_ERROR_CODES.validation,
|
|
27
|
+
message,
|
|
28
|
+
issues,
|
|
29
|
+
hint,
|
|
30
|
+
retryable: true
|
|
31
|
+
});
|
|
32
|
+
/** Encode an outcome as a `tools/call` result, with the input notes after a success. */
|
|
33
|
+
const toCallResult = (outcome, notes = []) => {
|
|
34
|
+
if (!outcome.ok) return {
|
|
35
|
+
content: [{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: JSON.stringify({ error: outcome.error })
|
|
38
|
+
}],
|
|
39
|
+
isError: true
|
|
40
|
+
};
|
|
41
|
+
const content = [{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: JSON.stringify(outcome.value ?? null)
|
|
44
|
+
}];
|
|
45
|
+
const distinct = [...new Set(notes)];
|
|
46
|
+
if (distinct.length > 0) content.push({
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `Input read: ${distinct.join(" ")}`
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
content,
|
|
52
|
+
isError: false
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const editDistance = (a, b) => {
|
|
56
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
57
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
58
|
+
const current = [i];
|
|
59
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
60
|
+
const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
61
|
+
current[j] = Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution);
|
|
62
|
+
}
|
|
63
|
+
previous = current;
|
|
64
|
+
}
|
|
65
|
+
return previous[b.length] ?? 0;
|
|
66
|
+
};
|
|
67
|
+
/** Up to three known names close to `name`, closest first. */
|
|
68
|
+
const closestNames = (name, known) => {
|
|
69
|
+
const lowered = name.toLowerCase();
|
|
70
|
+
return known.map((candidate) => {
|
|
71
|
+
const lower = candidate.toLowerCase();
|
|
72
|
+
return {
|
|
73
|
+
candidate,
|
|
74
|
+
distance: lower.includes(lowered) || lowered.includes(lower) ? 0 : editDistance(lowered, lower)
|
|
75
|
+
};
|
|
76
|
+
}).filter(({ distance }) => distance <= Math.max(2, Math.floor(name.length / 3))).toSorted((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate)).slice(0, 3).map(({ candidate }) => candidate);
|
|
77
|
+
};
|
|
78
|
+
const didYouMean = (suggestions) => suggestions.length === 0 ? "" : `Did you mean ${suggestions.map((name) => `"${name}"`).join(" or ")}? `;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { KIT_ERROR_CODES, closestNames, didYouMean, failure, success, toCallResult, toolError, validationError };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { JsonSchema, ListedTool, ToolAccess, ToolCallResult, ToolDefinition, ToolError, ToolInputIssue, ToolOutcome } from "./types.js";
|
|
2
|
+
import { KIT_ERROR_CODES, closestNames, didYouMean, failure, success, toCallResult, toolError, validationError } from "./envelope.js";
|
|
3
|
+
import { ReadInput, readToolInput } from "./input.js";
|
|
4
|
+
import { CompactSchemaOptions, advertisedBytes, compactSchema, hoistRepeatedSchemas } from "./schema.js";
|
|
5
|
+
import { CAPABILITY_TOOL_NAMES, ToolSurface, ToolSurfaceOptions, createToolSurface } from "./surface.js";
|
|
6
|
+
export { CAPABILITY_TOOL_NAMES, type CompactSchemaOptions, type JsonSchema, KIT_ERROR_CODES, type ListedTool, type ReadInput, type ToolAccess, type ToolCallResult, type ToolDefinition, type ToolError, type ToolInputIssue, type ToolOutcome, type ToolSurface, type ToolSurfaceOptions, advertisedBytes, closestNames, compactSchema, createToolSurface, didYouMean, failure, hoistRepeatedSchemas, readToolInput, success, toCallResult, toolError, validationError };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { KIT_ERROR_CODES, closestNames, didYouMean, failure, success, toCallResult, toolError, validationError } from "./envelope.js";
|
|
2
|
+
import { readToolInput } from "./input.js";
|
|
3
|
+
import { advertisedBytes, compactSchema, hoistRepeatedSchemas } from "./schema.js";
|
|
4
|
+
import { CAPABILITY_TOOL_NAMES, createToolSurface } from "./surface.js";
|
|
5
|
+
export { CAPABILITY_TOOL_NAMES, KIT_ERROR_CODES, advertisedBytes, closestNames, compactSchema, createToolSurface, didYouMean, failure, hoistRepeatedSchemas, readToolInput, success, toCallResult, toolError, validationError };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { JsonSchema, ToolAccess, ToolInputIssue } from "./types.js";
|
|
2
|
+
//#region src/mcp-kit/input.d.ts
|
|
3
|
+
type ReadInput = {
|
|
4
|
+
ok: true;
|
|
5
|
+
value: Record<string, unknown>;
|
|
6
|
+
notes: readonly string[];
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
message: string;
|
|
10
|
+
issues: readonly ToolInputIssue[];
|
|
11
|
+
hint: string;
|
|
12
|
+
};
|
|
13
|
+
type ReadInputOptions = {
|
|
14
|
+
schema: JsonSchema;
|
|
15
|
+
value: unknown;
|
|
16
|
+
access: ToolAccess;
|
|
17
|
+
/** Properties passed through exactly as sent. */
|
|
18
|
+
exactProperties?: readonly string[] | undefined;
|
|
19
|
+
};
|
|
20
|
+
/** Check and leniently read one call's arguments against the tool's schema. */
|
|
21
|
+
declare const readToolInput: ({ schema, value, access, exactProperties }: ReadInputOptions) => ReadInput;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ReadInput, readToolInput };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { normalizeAgentInput } from "@stll/agent-input";
|
|
2
|
+
//#region src/mcp-kit/input.ts
|
|
3
|
+
/**
|
|
4
|
+
* Reading a model's arguments before a tool sees them: `null` in an optional
|
|
5
|
+
* property is absent, spellings that carry one meaning (`"true"`, `"20"`, an
|
|
6
|
+
* enum value in another case) are read through `@stll/agent-input` with a
|
|
7
|
+
* note, anything that carries two is asked about, and an argument the schema
|
|
8
|
+
* does not declare, or a required one that is missing, is refused rather than
|
|
9
|
+
* dropped.
|
|
10
|
+
*/
|
|
11
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
const propertiesOf = (schema) => isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
13
|
+
const requiredOf = (schema) => Array.isArray(schema["required"]) ? schema["required"].filter((key) => typeof key === "string") : [];
|
|
14
|
+
/**
|
|
15
|
+
* A placeholder in an optional property of a read is "not filtering" and is
|
|
16
|
+
* dropped with a note; on a write it may be an id that changes what the call
|
|
17
|
+
* does, so it is asked about.
|
|
18
|
+
*/
|
|
19
|
+
const PLACEHOLDER_POLICY = {
|
|
20
|
+
read: "absent",
|
|
21
|
+
write: "ask"
|
|
22
|
+
};
|
|
23
|
+
const refusal = (message, issues, hint) => ({
|
|
24
|
+
ok: false,
|
|
25
|
+
message,
|
|
26
|
+
issues,
|
|
27
|
+
hint
|
|
28
|
+
});
|
|
29
|
+
/** Check and leniently read one call's arguments against the tool's schema. */
|
|
30
|
+
const readToolInput = ({ schema, value, access, exactProperties = [] }) => {
|
|
31
|
+
if (value !== void 0 && !isRecord(value)) return refusal("Arguments must be a JSON object.", [], "Send the arguments as an object.");
|
|
32
|
+
const properties = propertiesOf(schema);
|
|
33
|
+
const required = requiredOf(schema);
|
|
34
|
+
const input = {};
|
|
35
|
+
const unknown = [];
|
|
36
|
+
for (const [key, entry] of Object.entries(value ?? {})) {
|
|
37
|
+
if (entry === void 0) continue;
|
|
38
|
+
if (!Object.hasOwn(properties, key)) unknown.push({
|
|
39
|
+
path: key,
|
|
40
|
+
message: `Unknown parameter: ${key}`
|
|
41
|
+
});
|
|
42
|
+
else if (entry !== null || required.includes(key)) Object.defineProperty(input, key, {
|
|
43
|
+
value: entry,
|
|
44
|
+
enumerable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
configurable: true
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (unknown.length > 0) return refusal("Arguments need clarification.", unknown, `Accepted parameters: ${Object.keys(properties).join(", ")}.`);
|
|
50
|
+
const exact = new Set(exactProperties);
|
|
51
|
+
const normalized = normalizeAgentInput({
|
|
52
|
+
placeholders: PLACEHOLDER_POLICY[access],
|
|
53
|
+
schema: {
|
|
54
|
+
...schema,
|
|
55
|
+
properties: Object.fromEntries(Object.entries(properties).map(([key, property]) => [key, exact.has(key) ? {} : property]))
|
|
56
|
+
},
|
|
57
|
+
value: input
|
|
58
|
+
});
|
|
59
|
+
if (!normalized.ok) return refusal("Arguments need clarification.", normalized.issues.map(({ path, received, expected }) => ({
|
|
60
|
+
path,
|
|
61
|
+
message: `${received} is not ${expected}.`
|
|
62
|
+
})), normalized.issues.map(({ path, hint }) => path.length === 0 ? hint : `${path}: ${hint}`).join(" "));
|
|
63
|
+
const read = isRecord(normalized.value) ? normalized.value : {};
|
|
64
|
+
const missing = required.filter((key) => read[key] === void 0).map((key) => ({
|
|
65
|
+
path: key,
|
|
66
|
+
message: `Missing parameter: ${key}`
|
|
67
|
+
}));
|
|
68
|
+
if (missing.length > 0) return refusal("Arguments are incomplete.", missing, `Required: ${required.join(", ")}.`);
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
value: read,
|
|
72
|
+
notes: normalized.notes
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { readToolInput };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JsonSchema, ListedTool } from "./types.js";
|
|
2
|
+
//#region src/mcp-kit/schema.d.ts
|
|
3
|
+
type CompactSchemaOptions = {
|
|
4
|
+
/** Keep descriptions this many property levels deep; 0 (the default) keeps none. */
|
|
5
|
+
describedDepth?: number;
|
|
6
|
+
};
|
|
7
|
+
/** The schema without annotation keywords, descriptions kept only as deep as asked. */
|
|
8
|
+
declare const compactSchema: (schema: JsonSchema, { describedDepth }?: CompactSchemaOptions) => JsonSchema;
|
|
9
|
+
/**
|
|
10
|
+
* Hoist every object subschema that occurs more than once and serializes to
|
|
11
|
+
* at least `minBytes` into `$defs`, referenced by `$ref`: a schema that
|
|
12
|
+
* repeats one shape for several properties states it once.
|
|
13
|
+
*/
|
|
14
|
+
declare const hoistRepeatedSchemas: (schema: JsonSchema, { minBytes }?: {
|
|
15
|
+
minBytes?: number;
|
|
16
|
+
}) => JsonSchema;
|
|
17
|
+
/** The UTF-8 size of what `tools/list` sends for these tools. */
|
|
18
|
+
declare const advertisedBytes: (tools: readonly ListedTool[]) => number;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { CompactSchemaOptions, advertisedBytes, compactSchema, hoistRepeatedSchemas };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//#region src/mcp-kit/schema.ts
|
|
2
|
+
const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
3
|
+
"$comment",
|
|
4
|
+
"$schema",
|
|
5
|
+
"default",
|
|
6
|
+
"description",
|
|
7
|
+
"examples",
|
|
8
|
+
"title"
|
|
9
|
+
]);
|
|
10
|
+
/** Keywords whose value is a map of property name to schema. */
|
|
11
|
+
const SCHEMA_MAP_KEYWORDS = /* @__PURE__ */ new Set([
|
|
12
|
+
"$defs",
|
|
13
|
+
"definitions",
|
|
14
|
+
"patternProperties",
|
|
15
|
+
"properties"
|
|
16
|
+
]);
|
|
17
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
/**
|
|
19
|
+
* `describedDepth` keeps `description` on schemas that many property maps
|
|
20
|
+
* down (1: the tool's own arguments) and strips it below, where a large
|
|
21
|
+
* nested schema repeats itself.
|
|
22
|
+
*/
|
|
23
|
+
const compactNode = (node, depth, describedDepth) => {
|
|
24
|
+
if (Array.isArray(node)) return node.map((item) => compactNode(item, depth, describedDepth));
|
|
25
|
+
if (!isRecord(node)) return node;
|
|
26
|
+
const compacted = {};
|
|
27
|
+
for (const [key, value] of Object.entries(node)) {
|
|
28
|
+
const keptDescription = key === "description" && depth > 0 && depth <= describedDepth;
|
|
29
|
+
if (ANNOTATION_KEYWORDS.has(key) && !keptDescription) continue;
|
|
30
|
+
if (key === "maximum" && value === Number.MAX_SAFE_INTEGER) continue;
|
|
31
|
+
compacted[key] = SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value) ? Object.fromEntries(Object.entries(value).map(([name, schema]) => [name, compactNode(schema, depth + 1, describedDepth)])) : compactNode(value, depth, describedDepth);
|
|
32
|
+
}
|
|
33
|
+
return compacted;
|
|
34
|
+
};
|
|
35
|
+
/** The schema without annotation keywords, descriptions kept only as deep as asked. */
|
|
36
|
+
const compactSchema = (schema, { describedDepth = 0 } = {}) => {
|
|
37
|
+
const compacted = compactNode(schema, 0, describedDepth);
|
|
38
|
+
return isRecord(compacted) ? compacted : {};
|
|
39
|
+
};
|
|
40
|
+
/** Count every object subschema below the root by its serialization, naming each by where it first appears. */
|
|
41
|
+
const countSubschemas = (node, name, seen, isRoot) => {
|
|
42
|
+
if (Array.isArray(node)) {
|
|
43
|
+
for (const item of node) countSubschemas(item, name, seen, false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(node)) return;
|
|
47
|
+
if (!isRoot) {
|
|
48
|
+
const key = JSON.stringify(node);
|
|
49
|
+
const occurrence = seen.get(key);
|
|
50
|
+
if (occurrence === void 0) seen.set(key, {
|
|
51
|
+
count: 1,
|
|
52
|
+
name,
|
|
53
|
+
bytes: key.length
|
|
54
|
+
});
|
|
55
|
+
else occurrence.count += 1;
|
|
56
|
+
}
|
|
57
|
+
for (const [key, value] of Object.entries(node)) if (SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value)) for (const [child, schema] of Object.entries(value)) countSubschemas(schema, child, seen, false);
|
|
58
|
+
else countSubschemas(value, name, seen, false);
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Hoist every object subschema that occurs more than once and serializes to
|
|
62
|
+
* at least `minBytes` into `$defs`, referenced by `$ref`: a schema that
|
|
63
|
+
* repeats one shape for several properties states it once.
|
|
64
|
+
*/
|
|
65
|
+
const hoistRepeatedSchemas = (schema, { minBytes = 200 } = {}) => {
|
|
66
|
+
const seen = /* @__PURE__ */ new Map();
|
|
67
|
+
countSubschemas(schema, "schema", seen, true);
|
|
68
|
+
const refs = /* @__PURE__ */ new Map();
|
|
69
|
+
const defs = {};
|
|
70
|
+
const repeated = [...seen.entries()].filter(([, { count, bytes }]) => count > 1 && bytes >= minBytes).toSorted(([, a], [, b]) => b.bytes - a.bytes);
|
|
71
|
+
for (const [key, { name }] of repeated) {
|
|
72
|
+
let defName = name;
|
|
73
|
+
for (let suffix = 2; defName in defs; suffix += 1) defName = `${name}${suffix}`;
|
|
74
|
+
refs.set(key, defName);
|
|
75
|
+
defs[defName] = null;
|
|
76
|
+
}
|
|
77
|
+
if (refs.size === 0) return schema;
|
|
78
|
+
const replace = (node, isRoot) => {
|
|
79
|
+
if (Array.isArray(node)) return node.map((item) => replace(item, false));
|
|
80
|
+
if (!isRecord(node)) return node;
|
|
81
|
+
const ref = isRoot ? void 0 : refs.get(JSON.stringify(node));
|
|
82
|
+
if (ref !== void 0) return { $ref: `#/$defs/${ref}` };
|
|
83
|
+
const replaced = {};
|
|
84
|
+
for (const [key, value] of Object.entries(node)) replaced[key] = SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value) ? Object.fromEntries(Object.entries(value).map(([child, sub]) => [child, replace(sub, false)])) : replace(value, false);
|
|
85
|
+
return replaced;
|
|
86
|
+
};
|
|
87
|
+
for (const [key, defName] of refs) defs[defName] = replace(JSON.parse(key), true);
|
|
88
|
+
const root = replace(schema, true);
|
|
89
|
+
return isRecord(root) ? {
|
|
90
|
+
...root,
|
|
91
|
+
$defs: defs
|
|
92
|
+
} : schema;
|
|
93
|
+
};
|
|
94
|
+
/** The UTF-8 size of what `tools/list` sends for these tools. */
|
|
95
|
+
const advertisedBytes = (tools) => new TextEncoder().encode(JSON.stringify(tools)).length;
|
|
96
|
+
//#endregion
|
|
97
|
+
export { advertisedBytes, compactSchema, hoistRepeatedSchemas };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ListedTool, ToolCallResult, ToolDefinition } from "./types.js";
|
|
2
|
+
//#region src/mcp-kit/surface.d.ts
|
|
3
|
+
declare const CAPABILITY_TOOL_NAMES: {
|
|
4
|
+
readonly list: "list_capabilities";
|
|
5
|
+
readonly describe: "describe_capability";
|
|
6
|
+
readonly invoke: "invoke_capability";
|
|
7
|
+
};
|
|
8
|
+
type ToolSurfaceOptions<Context> = {
|
|
9
|
+
readonly tools: readonly ToolDefinition<Context>[];
|
|
10
|
+
};
|
|
11
|
+
type ToolSurface<Context> = {
|
|
12
|
+
/** What `tools/list` returns: the direct tools, then the capability tools. */
|
|
13
|
+
readonly listTools: () => ListedTool[];
|
|
14
|
+
readonly callTool: (name: string, args: unknown, context: Context) => Promise<ToolCallResult>;
|
|
15
|
+
};
|
|
16
|
+
/** Build a surface over `tools`. Names must be unique and not a capability tool's. */
|
|
17
|
+
declare const createToolSurface: <Context>({ tools }: ToolSurfaceOptions<Context>) => ToolSurface<Context>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { CAPABILITY_TOOL_NAMES, ToolSurface, ToolSurfaceOptions, createToolSurface };
|