caplets 0.9.0 → 0.10.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/README.md +46 -0
- package/dist/index.js +254 -52
- package/package.json +7 -2
- package/schemas/caplets-config.schema.json +8 -0
package/README.md
CHANGED
|
@@ -11,6 +11,52 @@ or call that backend's underlying tools or operations.
|
|
|
11
11
|
This keeps the initial MCP tool list small, makes tool selection easier, and avoids
|
|
12
12
|
flattened tool-name collisions across servers.
|
|
13
13
|
|
|
14
|
+
## Why It Matters
|
|
15
|
+
|
|
16
|
+
Large MCP setups make agents worse before they make them better. If every downstream
|
|
17
|
+
server exposes every tool up front, the model starts with a noisy flat list, duplicate
|
|
18
|
+
tool names, and a bigger context surface before it knows which capability matters.
|
|
19
|
+
|
|
20
|
+
Caplets turns that flat tool wall into progressive disclosure: one capability card first,
|
|
21
|
+
then scoped discovery only after the agent chooses the relevant domain.
|
|
22
|
+
|
|
23
|
+
## Benchmark Results
|
|
24
|
+
|
|
25
|
+
In Caplets' reproducible coding-agent benchmark, the same three mock MCP servers are
|
|
26
|
+
exposed two ways: direct flat MCP aggregation versus Caplets progressive disclosure.
|
|
27
|
+
|
|
28
|
+
| Initial Agent Surface | Direct Flat MCP | Caplets | Reduction |
|
|
29
|
+
| ------------------------- | ----------------: | -----------: | ------------: |
|
|
30
|
+
| Visible tools | 106 | 3 | 97.2% fewer |
|
|
31
|
+
| Serialized MCP payload | 32,090 bytes | 8,358 bytes | 74.0% smaller |
|
|
32
|
+
| Approx. context surface | 8,023 tokens | 2,090 tokens | 5,933 fewer |
|
|
33
|
+
| Top-level name collisions | 3 duplicate names | 0 | eliminated |
|
|
34
|
+
|
|
35
|
+
The important part: Caplets does not remove access to the downstream tools. It hides
|
|
36
|
+
them behind scoped discovery operations like `search_tools`, `get_tool`, and `call_tool`,
|
|
37
|
+
so the agent sees less up front while still being able to reach the same capabilities.
|
|
38
|
+
|
|
39
|
+
A local OpenCode live benchmark also completed the full benchmark matrix successfully:
|
|
40
|
+
|
|
41
|
+
| Agent | Mode | Tasks Passed |
|
|
42
|
+
| ------------------------------ | --------------- | -----------: |
|
|
43
|
+
| OpenCode `openai/gpt-5.5-fast` | Direct flat MCP | 2/2 |
|
|
44
|
+
| OpenCode `openai/gpt-5.5-fast` | Caplets | 2/2 |
|
|
45
|
+
|
|
46
|
+
Live results are intentionally not committed as product claims because they depend on
|
|
47
|
+
local agent CLIs, credentials, models, providers, and agent behavior. The deterministic
|
|
48
|
+
surface benchmark is the reproducible claim.
|
|
49
|
+
|
|
50
|
+
See [`docs/benchmarks/coding-agent.md`](docs/benchmarks/coding-agent.md) for methodology,
|
|
51
|
+
limitations, and reproduction commands.
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pnpm benchmark
|
|
55
|
+
pnpm benchmark:check
|
|
56
|
+
pnpm build
|
|
57
|
+
CAPLETS_BENCH_LIVE=1 pnpm benchmark:live:opencode -- --model openai/gpt-5.5-fast
|
|
58
|
+
```
|
|
59
|
+
|
|
14
60
|
## Inspiration
|
|
15
61
|
|
|
16
62
|
Caplets is a mashup of two ideas that work well separately but leave a gap together:
|
package/dist/index.js
CHANGED
|
@@ -180,7 +180,7 @@ const allowsEval = /* @__PURE__ */ cached(() => {
|
|
|
180
180
|
return false;
|
|
181
181
|
}
|
|
182
182
|
});
|
|
183
|
-
function isPlainObject$
|
|
183
|
+
function isPlainObject$6(o) {
|
|
184
184
|
if (isObject(o) === false) return false;
|
|
185
185
|
const ctor = o.constructor;
|
|
186
186
|
if (ctor === void 0) return true;
|
|
@@ -191,7 +191,7 @@ function isPlainObject$5(o) {
|
|
|
191
191
|
return true;
|
|
192
192
|
}
|
|
193
193
|
function shallowClone(o) {
|
|
194
|
-
if (isPlainObject$
|
|
194
|
+
if (isPlainObject$6(o)) return { ...o };
|
|
195
195
|
if (Array.isArray(o)) return [...o];
|
|
196
196
|
if (o instanceof Map) return new Map(o);
|
|
197
197
|
if (o instanceof Set) return new Set(o);
|
|
@@ -274,7 +274,7 @@ function omit(schema, mask) {
|
|
|
274
274
|
}));
|
|
275
275
|
}
|
|
276
276
|
function extend(schema, shape) {
|
|
277
|
-
if (!isPlainObject$
|
|
277
|
+
if (!isPlainObject$6(shape)) throw new Error("Invalid input to extend: expected a plain object");
|
|
278
278
|
const checks = schema._zod.def.checks;
|
|
279
279
|
if (checks && checks.length > 0) {
|
|
280
280
|
const existingShape = schema._zod.def.shape;
|
|
@@ -290,7 +290,7 @@ function extend(schema, shape) {
|
|
|
290
290
|
} }));
|
|
291
291
|
}
|
|
292
292
|
function safeExtend(schema, shape) {
|
|
293
|
-
if (!isPlainObject$
|
|
293
|
+
if (!isPlainObject$6(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
294
294
|
return clone(schema, mergeDefs(schema._zod.def, { get shape() {
|
|
295
295
|
const _shape = {
|
|
296
296
|
...schema._zod.def.shape,
|
|
@@ -1904,7 +1904,7 @@ function mergeValues$1(a, b) {
|
|
|
1904
1904
|
valid: true,
|
|
1905
1905
|
data: a
|
|
1906
1906
|
};
|
|
1907
|
-
if (isPlainObject$
|
|
1907
|
+
if (isPlainObject$6(a) && isPlainObject$6(b)) {
|
|
1908
1908
|
const bKeys = Object.keys(b);
|
|
1909
1909
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
1910
1910
|
const newObj = {
|
|
@@ -1980,7 +1980,7 @@ const $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
1980
1980
|
$ZodType.init(inst, def);
|
|
1981
1981
|
inst._zod.parse = (payload, ctx) => {
|
|
1982
1982
|
const input = payload.value;
|
|
1983
|
-
if (!isPlainObject$
|
|
1983
|
+
if (!isPlainObject$6(input)) {
|
|
1984
1984
|
payload.issues.push({
|
|
1985
1985
|
expected: "record",
|
|
1986
1986
|
code: "invalid_type",
|
|
@@ -9619,7 +9619,7 @@ const { program, createCommand, createArgument, createOption, CommanderError, In
|
|
|
9619
9619
|
})))(), 1)).default;
|
|
9620
9620
|
//#endregion
|
|
9621
9621
|
//#region package.json
|
|
9622
|
-
var version = "0.
|
|
9622
|
+
var version = "0.10.0";
|
|
9623
9623
|
//#endregion
|
|
9624
9624
|
//#region node_modules/.pnpm/pkce-challenge@5.0.1/node_modules/pkce-challenge/dist/index.node.js
|
|
9625
9625
|
let crypto;
|
|
@@ -19073,13 +19073,13 @@ function loadCapletFiles(root) {
|
|
|
19073
19073
|
for (const candidate of discoverCapletFiles(root)) {
|
|
19074
19074
|
if (servers[candidate.id] || openapiEndpoints[candidate.id] || graphqlEndpoints[candidate.id] || httpApis[candidate.id]) throw new CapletsError("CONFIG_INVALID", `Duplicate Caplet ID ${candidate.id} under ${root}`);
|
|
19075
19075
|
const config = readCapletFile(candidate.path);
|
|
19076
|
-
if (isPlainObject$
|
|
19076
|
+
if (isPlainObject$5(config) && config.backend === "openapi") {
|
|
19077
19077
|
const { backend: _backend, ...endpoint } = config;
|
|
19078
19078
|
openapiEndpoints[candidate.id] = endpoint;
|
|
19079
|
-
} else if (isPlainObject$
|
|
19079
|
+
} else if (isPlainObject$5(config) && config.backend === "graphql") {
|
|
19080
19080
|
const { backend: _backend, ...endpoint } = config;
|
|
19081
19081
|
graphqlEndpoints[candidate.id] = endpoint;
|
|
19082
|
-
} else if (isPlainObject$
|
|
19082
|
+
} else if (isPlainObject$5(config) && config.backend === "http") {
|
|
19083
19083
|
const { backend: _backend, ...endpoint } = config;
|
|
19084
19084
|
httpApis[candidate.id] = endpoint;
|
|
19085
19085
|
} else servers[candidate.id] = config;
|
|
@@ -19200,7 +19200,7 @@ function parseFrontmatter(text, path) {
|
|
|
19200
19200
|
value: text
|
|
19201
19201
|
});
|
|
19202
19202
|
matter(file, { strip: true });
|
|
19203
|
-
if (!isPlainObject$
|
|
19203
|
+
if (!isPlainObject$5(file.data.matter) || Object.keys(file.data.matter).length === 0) throw new Error("empty frontmatter");
|
|
19204
19204
|
return {
|
|
19205
19205
|
frontmatter: file.data.matter,
|
|
19206
19206
|
body: String(file)
|
|
@@ -19209,7 +19209,7 @@ function parseFrontmatter(text, path) {
|
|
|
19209
19209
|
throw new CapletsError("CONFIG_INVALID", `Caplet file at ${path} has invalid YAML frontmatter`, redactSecrets(error));
|
|
19210
19210
|
}
|
|
19211
19211
|
}
|
|
19212
|
-
function isPlainObject$
|
|
19212
|
+
function isPlainObject$5(value) {
|
|
19213
19213
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19214
19214
|
}
|
|
19215
19215
|
function validateCapletId(id, path) {
|
|
@@ -19386,6 +19386,7 @@ const httpActionSchema = object$1({
|
|
|
19386
19386
|
path: string().min(1).regex(/^\//, "HTTP action path must start with /").describe("URL path appended to the HTTP API baseUrl.").refine((value) => !value.startsWith("//"), "HTTP action path must not start with //").refine((value) => !isUrl(value), "HTTP action path must be a URL path, not a URL"),
|
|
19387
19387
|
description: string().min(1).optional().describe("Action capability description."),
|
|
19388
19388
|
inputSchema: record(string(), unknown()).optional().describe("JSON Schema for call_tool arguments."),
|
|
19389
|
+
outputSchema: record(string(), unknown()).optional().describe("JSON Schema for structuredContent returned by this action."),
|
|
19389
19390
|
query: httpScalarMappingSchema.optional().describe("Query parameter mapping."),
|
|
19390
19391
|
headers: httpScalarMappingSchema.optional().describe("Request header mapping."),
|
|
19391
19392
|
jsonBody: unknown().optional().describe("JSON request body mapping.")
|
|
@@ -19650,7 +19651,7 @@ function normalizeLocalPaths(input, baseDir) {
|
|
|
19650
19651
|
}
|
|
19651
19652
|
function normalizeEndpointPaths(endpoints, baseDir, normalize) {
|
|
19652
19653
|
if (!endpoints) return;
|
|
19653
|
-
return Object.fromEntries(Object.entries(endpoints).map(([id, endpoint]) => [id, isPlainObject$
|
|
19654
|
+
return Object.fromEntries(Object.entries(endpoints).map(([id, endpoint]) => [id, isPlainObject$4(endpoint) ? normalize(endpoint, baseDir) : endpoint]));
|
|
19654
19655
|
}
|
|
19655
19656
|
function normalizeOpenApiPath(endpoint, baseDir) {
|
|
19656
19657
|
return {
|
|
@@ -19659,7 +19660,7 @@ function normalizeOpenApiPath(endpoint, baseDir) {
|
|
|
19659
19660
|
};
|
|
19660
19661
|
}
|
|
19661
19662
|
function normalizeGraphQlPath(endpoint, baseDir) {
|
|
19662
|
-
const operations = isPlainObject$
|
|
19663
|
+
const operations = isPlainObject$4(endpoint.operations) ? Object.fromEntries(Object.entries(endpoint.operations).map(([name, operation]) => [name, isPlainObject$4(operation) ? {
|
|
19663
19664
|
...operation,
|
|
19664
19665
|
documentPath: normalizeLocalPath(operation.documentPath, baseDir)
|
|
19665
19666
|
} : operation])) : endpoint.operations;
|
|
@@ -19778,7 +19779,7 @@ function isPublicMetadataPath(path) {
|
|
|
19778
19779
|
if (path.length < 3 || path[0] !== "mcpServers" && path[0] !== "openapiEndpoints" && path[0] !== "graphqlEndpoints" && path[0] !== "httpApis") return false;
|
|
19779
19780
|
return NON_INTERPOLATED_SERVER_FIELDS.has(path[2] ?? "");
|
|
19780
19781
|
}
|
|
19781
|
-
function isPlainObject$
|
|
19782
|
+
function isPlainObject$4(value) {
|
|
19782
19783
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19783
19784
|
}
|
|
19784
19785
|
function hasEnvReference(value) {
|
|
@@ -25864,7 +25865,7 @@ var Protocol = class {
|
|
|
25864
25865
|
};
|
|
25865
25866
|
}
|
|
25866
25867
|
};
|
|
25867
|
-
function isPlainObject$
|
|
25868
|
+
function isPlainObject$3(value) {
|
|
25868
25869
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25869
25870
|
}
|
|
25870
25871
|
function mergeCapabilities(base, additional) {
|
|
@@ -25874,7 +25875,7 @@ function mergeCapabilities(base, additional) {
|
|
|
25874
25875
|
const addValue = additional[k];
|
|
25875
25876
|
if (addValue === void 0) continue;
|
|
25876
25877
|
const baseValue = result[k];
|
|
25877
|
-
if (isPlainObject$
|
|
25878
|
+
if (isPlainObject$3(baseValue) && isPlainObject$3(addValue)) result[k] = {
|
|
25878
25879
|
...baseValue,
|
|
25879
25880
|
...addValue
|
|
25880
25881
|
};
|
|
@@ -35770,7 +35771,8 @@ var DownstreamManager = class {
|
|
|
35770
35771
|
tool: tool.name,
|
|
35771
35772
|
...tool.description ? { description: tool.description } : {},
|
|
35772
35773
|
...tool.annotations ? { annotations: tool.annotations } : {},
|
|
35773
|
-
hasInputSchema: Boolean(tool.inputSchema)
|
|
35774
|
+
hasInputSchema: Boolean(tool.inputSchema),
|
|
35775
|
+
hasOutputSchema: Boolean(tool.outputSchema)
|
|
35774
35776
|
};
|
|
35775
35777
|
}
|
|
35776
35778
|
search(server, tools, query, limit) {
|
|
@@ -50749,7 +50751,8 @@ var GraphQLManager = class {
|
|
|
50749
50751
|
tool: tool.name,
|
|
50750
50752
|
...tool.description ? { description: tool.description } : {},
|
|
50751
50753
|
...tool.annotations ? { annotations: tool.annotations } : {},
|
|
50752
|
-
hasInputSchema: Boolean(tool.inputSchema)
|
|
50754
|
+
hasInputSchema: Boolean(tool.inputSchema),
|
|
50755
|
+
hasOutputSchema: Boolean(tool.outputSchema)
|
|
50753
50756
|
};
|
|
50754
50757
|
}
|
|
50755
50758
|
search(endpoint, tools, query, limit) {
|
|
@@ -51147,7 +51150,8 @@ var HttpActionManager = class {
|
|
|
51147
51150
|
tool: tool.name,
|
|
51148
51151
|
...tool.description ? { description: tool.description } : {},
|
|
51149
51152
|
...tool.annotations ? { annotations: tool.annotations } : {},
|
|
51150
|
-
hasInputSchema: Boolean(tool.inputSchema)
|
|
51153
|
+
hasInputSchema: Boolean(tool.inputSchema),
|
|
51154
|
+
hasOutputSchema: Boolean(tool.outputSchema)
|
|
51151
51155
|
};
|
|
51152
51156
|
}
|
|
51153
51157
|
search(api, tools, query, limit) {
|
|
@@ -51159,6 +51163,7 @@ var HttpActionManager = class {
|
|
|
51159
51163
|
name: operation.name,
|
|
51160
51164
|
...operation.description ? { description: operation.description } : {},
|
|
51161
51165
|
inputSchema: operation.inputSchema ?? DEFAULT_INPUT_SCHEMA,
|
|
51166
|
+
...operation.outputSchema ? { outputSchema: operation.outputSchema } : {},
|
|
51162
51167
|
annotations: {
|
|
51163
51168
|
readOnlyHint: operation.method === "GET",
|
|
51164
51169
|
destructiveHint: operation.method === "DELETE"
|
|
@@ -51230,7 +51235,7 @@ function resolveMapping(mapping, input) {
|
|
|
51230
51235
|
function resolveMappingToRecord(mapping, input, name) {
|
|
51231
51236
|
if (mapping === void 0) return {};
|
|
51232
51237
|
const resolved = resolveMapping(mapping, input);
|
|
51233
|
-
if (!isPlainObject$
|
|
51238
|
+
if (!isPlainObject$2(resolved)) throw new CapletsError("REQUEST_INVALID", `HTTP action ${name} mapping must resolve to an object`);
|
|
51234
51239
|
return resolved;
|
|
51235
51240
|
}
|
|
51236
51241
|
function valueAtPath(input, path) {
|
|
@@ -51325,9 +51330,9 @@ function buildActionUrl(base, actionPath, options = {}) {
|
|
|
51325
51330
|
return baseUrl;
|
|
51326
51331
|
}
|
|
51327
51332
|
function asRecord$1(value) {
|
|
51328
|
-
return isPlainObject$
|
|
51333
|
+
return isPlainObject$2(value) ? value : {};
|
|
51329
51334
|
}
|
|
51330
|
-
function isPlainObject$
|
|
51335
|
+
function isPlainObject$2(value) {
|
|
51331
51336
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
51332
51337
|
}
|
|
51333
51338
|
//#endregion
|
|
@@ -60941,7 +60946,8 @@ var OpenApiManager = class {
|
|
|
60941
60946
|
tool: tool.name,
|
|
60942
60947
|
...tool.description ? { description: tool.description } : {},
|
|
60943
60948
|
...tool.annotations ? { annotations: tool.annotations } : {},
|
|
60944
|
-
hasInputSchema: Boolean(tool.inputSchema)
|
|
60949
|
+
hasInputSchema: Boolean(tool.inputSchema),
|
|
60950
|
+
hasOutputSchema: Boolean(tool.outputSchema)
|
|
60945
60951
|
};
|
|
60946
60952
|
}
|
|
60947
60953
|
search(endpoint, tools, query, limit) {
|
|
@@ -60984,6 +60990,7 @@ var OpenApiManager = class {
|
|
|
60984
60990
|
name: operation.name,
|
|
60985
60991
|
...operation.summary || operation.description ? { description: operation.summary ?? operation.description } : {},
|
|
60986
60992
|
inputSchema: operation.inputSchema,
|
|
60993
|
+
...operation.outputSchema ? { outputSchema: operation.outputSchema } : {},
|
|
60987
60994
|
annotations: {
|
|
60988
60995
|
readOnlyHint: operation.method === "get" || operation.method === "head",
|
|
60989
60996
|
destructiveHint: operation.method === "delete"
|
|
@@ -61019,6 +61026,7 @@ function extractOperations(endpoint, document) {
|
|
|
61019
61026
|
seen.add(name);
|
|
61020
61027
|
const parameters = [...inheritedParameters, ...Array.isArray(operation.parameters) ? operation.parameters : []];
|
|
61021
61028
|
const requestBody = requestBodyFor(operation);
|
|
61029
|
+
const outputSchema = outputSchemaFor(operation);
|
|
61022
61030
|
const baseUrl = endpoint.baseUrl ?? firstServerUrl(document);
|
|
61023
61031
|
validateOperationBaseUrl(endpoint, baseUrl);
|
|
61024
61032
|
operations.push({
|
|
@@ -61028,6 +61036,7 @@ function extractOperations(endpoint, document) {
|
|
|
61028
61036
|
...typeof operation.summary === "string" ? { summary: operation.summary } : {},
|
|
61029
61037
|
...typeof operation.description === "string" ? { description: operation.description } : {},
|
|
61030
61038
|
inputSchema: inputSchemaFor(parameters, requestBody),
|
|
61039
|
+
...outputSchema ? { outputSchema } : {},
|
|
61031
61040
|
...requestBody?.contentType ? { requestBodyContentType: requestBody.contentType } : {},
|
|
61032
61041
|
...baseUrl ? { baseUrl } : {}
|
|
61033
61042
|
});
|
|
@@ -61048,6 +61057,53 @@ function requestBodyFor(operation) {
|
|
|
61048
61057
|
contentType
|
|
61049
61058
|
};
|
|
61050
61059
|
}
|
|
61060
|
+
function outputSchemaFor(operation) {
|
|
61061
|
+
const responses = operation.responses;
|
|
61062
|
+
if (!responses || typeof responses !== "object") return;
|
|
61063
|
+
const schemas = [];
|
|
61064
|
+
for (const [status, response] of Object.entries(responses)) {
|
|
61065
|
+
if (!/^2\d\d$/.test(status) || !response || typeof response !== "object") continue;
|
|
61066
|
+
const content = response.content;
|
|
61067
|
+
if (!content || typeof content !== "object") continue;
|
|
61068
|
+
const contentType = JSON_CONTENT_TYPES.find((candidate) => content[candidate]);
|
|
61069
|
+
if (!contentType) continue;
|
|
61070
|
+
const schema = actualSchema(content[contentType]?.schema);
|
|
61071
|
+
if (!schema) continue;
|
|
61072
|
+
schemas.push(schema);
|
|
61073
|
+
}
|
|
61074
|
+
if (schemas.length === 0) return;
|
|
61075
|
+
const firstSchema = schemas[0];
|
|
61076
|
+
if (schemas.slice(1).some((schema) => JSON.stringify(schema) !== JSON.stringify(firstSchema))) return;
|
|
61077
|
+
return structuredOutputSchema(firstSchema);
|
|
61078
|
+
}
|
|
61079
|
+
function actualSchema(value) {
|
|
61080
|
+
rejectExternalRefs(value);
|
|
61081
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
61082
|
+
const schema = value;
|
|
61083
|
+
return typeof schema.$ref === "string" ? void 0 : schema;
|
|
61084
|
+
}
|
|
61085
|
+
function structuredOutputSchema(bodySchema) {
|
|
61086
|
+
return {
|
|
61087
|
+
type: "object",
|
|
61088
|
+
additionalProperties: false,
|
|
61089
|
+
required: [
|
|
61090
|
+
"status",
|
|
61091
|
+
"statusText",
|
|
61092
|
+
"headers"
|
|
61093
|
+
],
|
|
61094
|
+
properties: {
|
|
61095
|
+
status: { type: "number" },
|
|
61096
|
+
statusText: { type: "string" },
|
|
61097
|
+
headers: {
|
|
61098
|
+
type: "object",
|
|
61099
|
+
additionalProperties: false,
|
|
61100
|
+
required: ["content-type"],
|
|
61101
|
+
properties: { "content-type": { type: "string" } }
|
|
61102
|
+
},
|
|
61103
|
+
body: bodySchema
|
|
61104
|
+
}
|
|
61105
|
+
};
|
|
61106
|
+
}
|
|
61051
61107
|
function inputSchemaFor(parameters, requestBody) {
|
|
61052
61108
|
const schema = {
|
|
61053
61109
|
type: "object",
|
|
@@ -61207,6 +61263,26 @@ function openApiCacheKey(endpoint) {
|
|
|
61207
61263
|
});
|
|
61208
61264
|
}
|
|
61209
61265
|
//#endregion
|
|
61266
|
+
//#region src/capability-description.mjs
|
|
61267
|
+
function capabilityDescription(server) {
|
|
61268
|
+
const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : server.backend === "graphql" ? "GraphQL endpoint" : "HTTP API";
|
|
61269
|
+
const checkOperation = server.backend === "mcp" ? "check_mcp_server" : "check_backend";
|
|
61270
|
+
const hint = [
|
|
61271
|
+
`Use this Caplet to inspect and call tools from its ${backendName} backend.`,
|
|
61272
|
+
"",
|
|
61273
|
+
"Recommended flow:",
|
|
61274
|
+
"- Read the full Caplet card: {\"operation\":\"get_caplet\"}",
|
|
61275
|
+
`- Check the backend: {"operation":"${checkOperation}"}`,
|
|
61276
|
+
"- Discover tools: {\"operation\":\"list_tools\"} or {\"operation\":\"search_tools\",\"query\":\"<what you need>\"}",
|
|
61277
|
+
"- Read one tool schema: {\"operation\":\"get_tool\",\"tool\":\"<tool name>\"}",
|
|
61278
|
+
"- Invoke one downstream tool: {\"operation\":\"call_tool\",\"tool\":\"<tool name>\",\"arguments\":{...}}",
|
|
61279
|
+
"",
|
|
61280
|
+
"Important: Do not put downstream arguments at the top level; put them inside \"arguments\".",
|
|
61281
|
+
"After get_tool shows outputSchema (non-GraphQL), call_tool may use fields: [\"path.to.field\"]."
|
|
61282
|
+
].join("\n");
|
|
61283
|
+
return `${server.name}\n\n${server.description}\n\n${hint}`;
|
|
61284
|
+
}
|
|
61285
|
+
//#endregion
|
|
61210
61286
|
//#region src/registry.ts
|
|
61211
61287
|
var ServerRegistry = class {
|
|
61212
61288
|
config;
|
|
@@ -61274,23 +61350,6 @@ var ServerRegistry = class {
|
|
|
61274
61350
|
];
|
|
61275
61351
|
}
|
|
61276
61352
|
};
|
|
61277
|
-
function capabilityDescription(server) {
|
|
61278
|
-
const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : server.backend === "graphql" ? "GraphQL endpoint" : "HTTP API";
|
|
61279
|
-
const checkOperation = server.backend === "mcp" ? "check_mcp_server" : "check_backend";
|
|
61280
|
-
const hint = [
|
|
61281
|
-
`Use this Caplet to inspect and call tools from its ${backendName} backend.`,
|
|
61282
|
-
"",
|
|
61283
|
-
"Recommended flow:",
|
|
61284
|
-
"- Read the full Caplet card: {\"operation\":\"get_caplet\"}",
|
|
61285
|
-
`- Check the backend: {"operation":"${checkOperation}"}`,
|
|
61286
|
-
"- Discover tools: {\"operation\":\"list_tools\"} or {\"operation\":\"search_tools\",\"query\":\"<what you need>\"}",
|
|
61287
|
-
"- Read one tool schema: {\"operation\":\"get_tool\",\"tool\":\"<tool name>\"}",
|
|
61288
|
-
"- Invoke one downstream tool: {\"operation\":\"call_tool\",\"tool\":\"<tool name>\",\"arguments\":{...}}",
|
|
61289
|
-
"",
|
|
61290
|
-
"Important: call_tool requires a top-level \"arguments\" JSON object containing the downstream tool inputs. Do not put downstream arguments at the top level of this wrapper request."
|
|
61291
|
-
].join("\n");
|
|
61292
|
-
return `${server.name}\n\n${server.description}\n\n${hint}`;
|
|
61293
|
-
}
|
|
61294
61353
|
function backendDetail(server) {
|
|
61295
61354
|
if (server.backend === "openapi") return {
|
|
61296
61355
|
type: "openapi",
|
|
@@ -61328,7 +61387,110 @@ function graphQlSource(server) {
|
|
|
61328
61387
|
return "introspection";
|
|
61329
61388
|
}
|
|
61330
61389
|
//#endregion
|
|
61331
|
-
//#region src/
|
|
61390
|
+
//#region src/field-selection.ts
|
|
61391
|
+
function projectStructuredContent(value, outputSchema, fields) {
|
|
61392
|
+
validateFieldSelection(outputSchema, fields);
|
|
61393
|
+
if (!isPlainObject$1(value)) throwInvalid("Field selection requires object structured content");
|
|
61394
|
+
const result = createJsonObject();
|
|
61395
|
+
for (const field of fields) {
|
|
61396
|
+
const projected = projectPath(value, outputSchema, field.split("."));
|
|
61397
|
+
if (projected !== void 0) mergeValue(result, projected);
|
|
61398
|
+
}
|
|
61399
|
+
return result;
|
|
61400
|
+
}
|
|
61401
|
+
function validateFieldSelection(outputSchema, fields) {
|
|
61402
|
+
if (!isPlainObject$1(outputSchema)) throwInvalid("Field selection requires an output schema");
|
|
61403
|
+
if (!Array.isArray(fields) || fields.some((field) => typeof field !== "string")) throwInvalid("Field selection requires an array of field paths");
|
|
61404
|
+
for (const field of fields) validateSchemaPath(outputSchema, field.split("."), field);
|
|
61405
|
+
}
|
|
61406
|
+
function validateSchemaPath(schema, path, field) {
|
|
61407
|
+
let current = schema;
|
|
61408
|
+
for (const segment of path) {
|
|
61409
|
+
if (!isSupportedSegment(segment)) throwInvalid(`Unsupported field selection path: ${field}`);
|
|
61410
|
+
if (current?.type === "array") current = Array.isArray(current.items) ? void 0 : current.items;
|
|
61411
|
+
current = getOwnSchemaProperty(current?.properties, segment);
|
|
61412
|
+
if (!current) throwInvalid(`Field is not allowed by output schema: ${field}`);
|
|
61413
|
+
}
|
|
61414
|
+
}
|
|
61415
|
+
function getOwnSchemaProperty(properties, segment) {
|
|
61416
|
+
if (!properties || !Object.prototype.hasOwnProperty.call(properties, segment)) return;
|
|
61417
|
+
return properties[segment];
|
|
61418
|
+
}
|
|
61419
|
+
function projectPath(value, schema, path) {
|
|
61420
|
+
if (path.length === 0) return pruneToSchema(value, schema);
|
|
61421
|
+
if (Array.isArray(value)) {
|
|
61422
|
+
const itemSchema = arrayItemSchema(schema);
|
|
61423
|
+
return value.map((item) => projectPath(item, itemSchema, path) ?? {});
|
|
61424
|
+
}
|
|
61425
|
+
const segment = path[0];
|
|
61426
|
+
if (!isPlainObject$1(value) || !Object.prototype.hasOwnProperty.call(value, segment)) return;
|
|
61427
|
+
const rest = path.slice(1);
|
|
61428
|
+
const propertySchema = getSchemaProperty(schema, segment);
|
|
61429
|
+
const projected = projectPath(value[segment], propertySchema, rest);
|
|
61430
|
+
if (projected === void 0) return;
|
|
61431
|
+
return { [segment]: projected };
|
|
61432
|
+
}
|
|
61433
|
+
function pruneToSchema(value, schema) {
|
|
61434
|
+
if (Array.isArray(value)) {
|
|
61435
|
+
const itemSchema = arrayItemSchema(schema);
|
|
61436
|
+
return value.map((item) => pruneToSchema(item, itemSchema));
|
|
61437
|
+
}
|
|
61438
|
+
if (!isPlainObject$1(value)) return cloneJsonValue(value);
|
|
61439
|
+
const properties = isPlainObject$1(schema) ? schema.properties : void 0;
|
|
61440
|
+
if (!isPlainObject$1(properties)) return cloneJsonValue(value);
|
|
61441
|
+
const result = createJsonObject();
|
|
61442
|
+
for (const [key, nestedSchema] of Object.entries(properties)) if (isSupportedSegment(key) && Object.prototype.hasOwnProperty.call(value, key)) result[key] = pruneToSchema(value[key], nestedSchema);
|
|
61443
|
+
return result;
|
|
61444
|
+
}
|
|
61445
|
+
function getSchemaProperty(schema, segment) {
|
|
61446
|
+
const properties = isPlainObject$1(schema) ? schema.properties : void 0;
|
|
61447
|
+
if (!properties || !Object.prototype.hasOwnProperty.call(properties, segment)) return;
|
|
61448
|
+
return properties[segment];
|
|
61449
|
+
}
|
|
61450
|
+
function arrayItemSchema(schema) {
|
|
61451
|
+
if (!isPlainObject$1(schema) || Array.isArray(schema.items)) return;
|
|
61452
|
+
return schema.items;
|
|
61453
|
+
}
|
|
61454
|
+
function mergeValue(target, value) {
|
|
61455
|
+
if (!isPlainObject$1(value)) return;
|
|
61456
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
61457
|
+
if (!isSupportedSegment(key)) continue;
|
|
61458
|
+
target[key] = mergeNested(target[key], nested);
|
|
61459
|
+
}
|
|
61460
|
+
}
|
|
61461
|
+
function mergeNested(existing, next) {
|
|
61462
|
+
if (next === void 0) return existing;
|
|
61463
|
+
if (Array.isArray(existing) && Array.isArray(next)) return Array.from({ length: Math.max(existing.length, next.length) }, (_, index) => mergeNested(existing[index], next[index]));
|
|
61464
|
+
if (isPlainObject$1(existing) && isPlainObject$1(next)) {
|
|
61465
|
+
const merged = Object.assign(createJsonObject(), existing);
|
|
61466
|
+
mergeValue(merged, next);
|
|
61467
|
+
return merged;
|
|
61468
|
+
}
|
|
61469
|
+
return next;
|
|
61470
|
+
}
|
|
61471
|
+
function isSupportedSegment(segment) {
|
|
61472
|
+
return segment !== "" && segment !== "*" && segment !== "__proto__" && segment !== "prototype" && segment !== "constructor" && !/^\d+$/.test(segment);
|
|
61473
|
+
}
|
|
61474
|
+
function isPlainObject$1(value) {
|
|
61475
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
61476
|
+
}
|
|
61477
|
+
function createJsonObject() {
|
|
61478
|
+
return Object.create(null);
|
|
61479
|
+
}
|
|
61480
|
+
function cloneJsonValue(value) {
|
|
61481
|
+
if (Array.isArray(value)) return value.map(cloneJsonValue);
|
|
61482
|
+
if (isPlainObject$1(value)) {
|
|
61483
|
+
const result = createJsonObject();
|
|
61484
|
+
for (const [key, nested] of Object.entries(value)) if (isSupportedSegment(key)) result[key] = cloneJsonValue(nested);
|
|
61485
|
+
return result;
|
|
61486
|
+
}
|
|
61487
|
+
return value;
|
|
61488
|
+
}
|
|
61489
|
+
function throwInvalid(message) {
|
|
61490
|
+
throw new CapletsError("REQUEST_INVALID", message);
|
|
61491
|
+
}
|
|
61492
|
+
//#endregion
|
|
61493
|
+
//#region src/generated-tool-input-schema.mjs
|
|
61332
61494
|
const operations = [
|
|
61333
61495
|
"get_caplet",
|
|
61334
61496
|
"check_backend",
|
|
@@ -61338,16 +61500,25 @@ const operations = [
|
|
|
61338
61500
|
"get_tool",
|
|
61339
61501
|
"call_tool"
|
|
61340
61502
|
];
|
|
61341
|
-
const
|
|
61342
|
-
operation:
|
|
61503
|
+
const generatedToolInputDescriptions = {
|
|
61504
|
+
operation: [
|
|
61343
61505
|
"Caplets wrapper operation to perform for this configured Caplet backend.",
|
|
61344
61506
|
"Use get_caplet to read the full Caplet card, check_backend to check any backend, check_mcp_server to check an MCP backend, list_tools or search_tools to discover downstream tools, get_tool to read a downstream input schema, and call_tool to run one downstream tool or OpenAPI operation.",
|
|
61345
61507
|
"For call_tool, pass downstream inputs only inside the top-level \"arguments\" object."
|
|
61346
|
-
].join(" ")
|
|
61347
|
-
query:
|
|
61348
|
-
limit:
|
|
61349
|
-
tool:
|
|
61350
|
-
arguments:
|
|
61508
|
+
].join(" "),
|
|
61509
|
+
query: "Required only for search_tools. Example: {\"operation\":\"search_tools\",\"query\":\"web search\",\"limit\":5}. Do not use query for call_tool; put downstream query values under arguments.query.",
|
|
61510
|
+
limit: "Optional only for search_tools; defaults to the configured search limit. For downstream result limits, use call_tool.arguments with the downstream schema field name.",
|
|
61511
|
+
tool: "Exact downstream tool name for get_tool or call_tool. Example: {\"operation\":\"get_tool\",\"tool\":\"web_search_exa\"} before calling it.",
|
|
61512
|
+
arguments: "Required JSON object only for call_tool. Put every downstream tool input inside this object. Example: {\"operation\":\"call_tool\",\"tool\":\"web_search_exa\",\"arguments\":{\"query\":\"latest MCP docs\",\"numResults\":3}}. Do not send downstream inputs as top-level query, limit, url, path, or other fields.",
|
|
61513
|
+
fields: "Optional for call_tool after get_tool shows outputSchema on a non-GraphQL tool. Example: fields: [\"path.to.field\"]."
|
|
61514
|
+
};
|
|
61515
|
+
const generatedToolInputSchema = object$1({
|
|
61516
|
+
operation: _enum(operations).describe(generatedToolInputDescriptions.operation),
|
|
61517
|
+
query: string().optional().describe(generatedToolInputDescriptions.query),
|
|
61518
|
+
limit: number$1().int().positive().optional().describe(generatedToolInputDescriptions.limit),
|
|
61519
|
+
tool: string().optional().describe(generatedToolInputDescriptions.tool),
|
|
61520
|
+
arguments: record(string(), unknown()).optional().describe(generatedToolInputDescriptions.arguments),
|
|
61521
|
+
fields: array(string().min(1)).min(1).optional().describe(generatedToolInputDescriptions.fields)
|
|
61351
61522
|
}).strict();
|
|
61352
61523
|
async function handleServerTool(server, request, registry, downstream, openapi, graphql, http) {
|
|
61353
61524
|
const parsed = validateOperationRequest(request, registry.config.options.maxSearchLimit);
|
|
@@ -61382,7 +61553,15 @@ async function handleServerTool(server, request, registry, downstream, openapi,
|
|
|
61382
61553
|
tool
|
|
61383
61554
|
});
|
|
61384
61555
|
}
|
|
61385
|
-
case "call_tool":
|
|
61556
|
+
case "call_tool": {
|
|
61557
|
+
const backend = backendFor(server, downstream, openapi, graphql, http);
|
|
61558
|
+
if (parsed.fields === void 0) return backend.callTool(server, parsed.tool, parsed.arguments);
|
|
61559
|
+
if (server.backend === "graphql") throw new CapletsError("REQUEST_INVALID", "call_tool.fields is not supported for GraphQL-backed Caplets; select fields in the GraphQL operation document instead");
|
|
61560
|
+
const tool = await backend.getTool(server, parsed.tool);
|
|
61561
|
+
if (!tool.outputSchema) throw new CapletsError("REQUEST_INVALID", "Field selection requires an output schema");
|
|
61562
|
+
validateFieldSelection(tool.outputSchema, parsed.fields);
|
|
61563
|
+
return projectCallToolResult(await backend.callTool(server, parsed.tool, parsed.arguments), tool.outputSchema, parsed.fields);
|
|
61564
|
+
}
|
|
61386
61565
|
}
|
|
61387
61566
|
}
|
|
61388
61567
|
function validateOperationRequest(request, maxSearchLimit) {
|
|
@@ -61423,13 +61602,22 @@ function validateOperationRequest(request, maxSearchLimit) {
|
|
|
61423
61602
|
tool: value.tool
|
|
61424
61603
|
};
|
|
61425
61604
|
case "call_tool":
|
|
61426
|
-
allowed([
|
|
61605
|
+
allowed([
|
|
61606
|
+
"tool",
|
|
61607
|
+
"arguments",
|
|
61608
|
+
"fields"
|
|
61609
|
+
]);
|
|
61427
61610
|
if (!value.tool) throw new CapletsError("REQUEST_INVALID", "call_tool requires tool");
|
|
61428
61611
|
if (!isPlainObject(value.arguments)) throw new CapletsError("REQUEST_INVALID", "call_tool.arguments must be a JSON object");
|
|
61429
|
-
return {
|
|
61612
|
+
return value.fields === void 0 ? {
|
|
61430
61613
|
operation: "call_tool",
|
|
61431
61614
|
tool: value.tool,
|
|
61432
61615
|
arguments: value.arguments
|
|
61616
|
+
} : {
|
|
61617
|
+
operation: "call_tool",
|
|
61618
|
+
tool: value.tool,
|
|
61619
|
+
arguments: value.arguments,
|
|
61620
|
+
fields: value.fields
|
|
61433
61621
|
};
|
|
61434
61622
|
}
|
|
61435
61623
|
}
|
|
@@ -61442,6 +61630,20 @@ function jsonResult(value) {
|
|
|
61442
61630
|
structuredContent: { result: value }
|
|
61443
61631
|
};
|
|
61444
61632
|
}
|
|
61633
|
+
function projectCallToolResult(result, outputSchema, fields) {
|
|
61634
|
+
if (result.isError === true) return result;
|
|
61635
|
+
const structuredContent = result.structuredContent;
|
|
61636
|
+
if (!isPlainObject(structuredContent)) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Field selection requires the downstream tool to return object structuredContent");
|
|
61637
|
+
const projected = projectStructuredContent(structuredContent, outputSchema, fields);
|
|
61638
|
+
return {
|
|
61639
|
+
...result,
|
|
61640
|
+
content: [{
|
|
61641
|
+
type: "text",
|
|
61642
|
+
text: JSON.stringify(projected, null, 2)
|
|
61643
|
+
}],
|
|
61644
|
+
structuredContent: projected
|
|
61645
|
+
};
|
|
61646
|
+
}
|
|
61445
61647
|
function isPlainObject(value) {
|
|
61446
61648
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
61447
61649
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caplets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Progressive disclosure gateway for MCP servers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"caplets",
|
|
@@ -67,6 +67,11 @@
|
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "rolldown -c",
|
|
69
69
|
"build:watch": "rolldown -c --watch",
|
|
70
|
+
"benchmark": "node benchmarks/run-deterministic.mjs",
|
|
71
|
+
"benchmark:check": "node benchmarks/run-deterministic.mjs --check",
|
|
72
|
+
"benchmark:live": "node benchmarks/run-live.mjs",
|
|
73
|
+
"benchmark:live:opencode": "node benchmarks/run-live.mjs --agent opencode",
|
|
74
|
+
"benchmark:live:pi": "node benchmarks/run-live.mjs --agent pi",
|
|
70
75
|
"changeset": "changeset",
|
|
71
76
|
"dev": "node ./scripts/dev.mjs",
|
|
72
77
|
"format": "oxfmt .",
|
|
@@ -78,7 +83,7 @@
|
|
|
78
83
|
"schema:generate": "rolldown -c rolldown.schema.config.ts && node dist-schema/generate-config-schema.js && rm -rf dist-schema",
|
|
79
84
|
"typecheck": "tsc --noEmit",
|
|
80
85
|
"test": "vitest run",
|
|
81
|
-
"verify": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm schema:check && pnpm test && pnpm build",
|
|
86
|
+
"verify": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm schema:check && pnpm test && pnpm benchmark:check && pnpm build",
|
|
82
87
|
"version-packages": "changeset version"
|
|
83
88
|
}
|
|
84
89
|
}
|
|
@@ -1041,6 +1041,14 @@
|
|
|
1041
1041
|
},
|
|
1042
1042
|
"additionalProperties": {}
|
|
1043
1043
|
},
|
|
1044
|
+
"outputSchema": {
|
|
1045
|
+
"description": "JSON Schema for structuredContent returned by this action.",
|
|
1046
|
+
"type": "object",
|
|
1047
|
+
"propertyNames": {
|
|
1048
|
+
"type": "string"
|
|
1049
|
+
},
|
|
1050
|
+
"additionalProperties": {}
|
|
1051
|
+
},
|
|
1044
1052
|
"query": {
|
|
1045
1053
|
"description": "Query parameter mapping.",
|
|
1046
1054
|
"type": "object",
|