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 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$5(o) {
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$5(o)) return { ...o };
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$5(shape)) throw new Error("Invalid input to extend: expected a plain object");
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$5(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
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$5(a) && isPlainObject$5(b)) {
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$5(input)) {
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.9.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$4(config) && config.backend === "openapi") {
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$4(config) && config.backend === "graphql") {
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$4(config) && config.backend === "http") {
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$4(file.data.matter) || Object.keys(file.data.matter).length === 0) throw new Error("empty frontmatter");
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$4(value) {
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$3(endpoint) ? normalize(endpoint, baseDir) : endpoint]));
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$3(endpoint.operations) ? Object.fromEntries(Object.entries(endpoint.operations).map(([name, operation]) => [name, isPlainObject$3(operation) ? {
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$3(value) {
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$2(value) {
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$2(baseValue) && isPlainObject$2(addValue)) result[k] = {
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$1(resolved)) throw new CapletsError("REQUEST_INVALID", `HTTP action ${name} mapping must resolve to an object`);
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$1(value) ? value : {};
51333
+ return isPlainObject$2(value) ? value : {};
51329
51334
  }
51330
- function isPlainObject$1(value) {
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/tools.ts
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 generatedToolInputSchema = object$1({
61342
- operation: _enum(operations).describe([
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: string().optional().describe("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."),
61348
- limit: number$1().int().positive().optional().describe("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."),
61349
- tool: string().optional().describe("Exact downstream tool name for get_tool or call_tool. Example: {\"operation\":\"get_tool\",\"tool\":\"web_search_exa\"} before calling it."),
61350
- arguments: record(string(), unknown()).optional().describe("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.")
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": return backendFor(server, downstream, openapi, graphql, http).callTool(server, parsed.tool, parsed.arguments);
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(["tool", "arguments"]);
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.9.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",