caplets 0.8.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:
@@ -28,7 +74,7 @@ the agent chooses that server and asks to search, list, inspect, or call them.
28
74
 
29
75
  ## What It Does
30
76
 
31
- - Reads downstream MCP server definitions, native OpenAPI endpoint definitions, native GraphQL endpoint definitions, and explicit HTTP API action definitions from `~/.caplets/config.json`.
77
+ - Reads downstream MCP server definitions, native OpenAPI endpoint definitions, native GraphQL endpoint definitions, and explicit HTTP API action definitions from the user config file.
32
78
  - Registers one generated MCP tool for each enabled MCP server, OpenAPI endpoint, GraphQL endpoint, or HTTP API.
33
79
  - Uses the configured server ID as the generated tool name.
34
80
  - Uses the configured `name` and `description` as the capability card shown to agents.
@@ -59,7 +105,7 @@ pnpm build
59
105
 
60
106
  ## Configure
61
107
 
62
- Create a starter `~/.caplets/config.json`:
108
+ Create a starter user config at `${XDG_CONFIG_HOME:-~/.config}/caplets/config.json` on Unix-like platforms or `%APPDATA%\caplets\config.json` on Windows:
63
109
 
64
110
  ```sh
65
111
  caplets init
@@ -143,7 +189,7 @@ you want Caplets to expose:
143
189
  }
144
190
  ```
145
191
 
146
- The default config path can be overridden with `CAPLETS_CONFIG`:
192
+ The default config path is `${XDG_CONFIG_HOME:-~/.config}/caplets/config.json` on Unix-like platforms and `%APPDATA%\caplets\config.json` on Windows. It can be overridden with `CAPLETS_CONFIG`:
147
193
 
148
194
  ```sh
149
195
  CAPLETS_CONFIG=/path/to/config.json caplets init
@@ -279,11 +325,13 @@ caplets install spiritledsoftware/caplets github linear
279
325
  ```
280
326
 
281
327
  `caplets install` accepts a GitHub `owner/repo` shorthand, a Git URL, or a local repository path.
282
- It installs into your user Caplets root, which is `~/.caplets` by default or the parent directory
283
- of `CAPLETS_CONFIG` when that environment variable is set. Existing Caplets are not overwritten
284
- unless `--force` is passed.
328
+ It installs into your user Caplets root, which is `${XDG_CONFIG_HOME:-~/.config}/caplets` on Unix-like platforms,
329
+ `%APPDATA%\caplets` on Windows, or the parent directory of `CAPLETS_CONFIG` when that environment variable is set.
330
+ Existing Caplets are not overwritten unless `--force` is passed.
331
+
332
+ On Unix-like platforms, relative `XDG_CONFIG_HOME` and `XDG_STATE_HOME` values are ignored.
285
333
 
286
- Caplets always loads user Caplet files from `~/.caplets`. Project `./.caplets/config.json`
334
+ Caplets always loads user Caplet files from the user Caplets root. Project `./.caplets/config.json`
287
335
  is still loaded as project config, but project Markdown Caplet files are executable
288
336
  configuration and are ignored unless explicitly trusted:
289
337
 
@@ -511,10 +559,11 @@ For headless terminals:
511
559
  caplets auth login <server> --no-open
512
560
  ```
513
561
 
514
- OAuth/OIDC tokens are stored under `~/.caplets/auth/<server>.json` with owner-only file
515
- permissions where the platform supports them. Caplets supports well-known OAuth/OIDC
516
- discovery and dynamic client registration when advertised. When a token expires, run
517
- `caplets auth login <server>` again.
562
+ OAuth/OIDC tokens are stored under `${XDG_STATE_HOME:-~/.local/state}/caplets/auth/<server>.json`
563
+ on Unix-like platforms and `%LOCALAPPDATA%\caplets\auth\<server>.json` on Windows.
564
+ Token files use owner-only file permissions where the platform supports them. Caplets supports
565
+ well-known OAuth/OIDC discovery and dynamic client registration when advertised. When a token expires,
566
+ run `caplets auth login <server>` again.
518
567
 
519
568
  To inspect or remove stored OAuth credentials:
520
569
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from "node:module";
3
3
  import minproc, { stdin, stdout, default as process$1 } from "node:process";
4
4
  import { execFileSync } from "node:child_process";
5
- import minpath, { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
5
+ import minpath, { basename, dirname, extname, isAbsolute, join, parse, posix, relative, resolve, win32 } from "node:path";
6
6
  import { accessSync, chmodSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { createServer } from "node:http";
@@ -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.8.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;
@@ -10658,6 +10658,41 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
10658
10658
  return OAuthClientInformationFullSchema.parse(await response.json());
10659
10659
  }
10660
10660
  //#endregion
10661
+ //#region src/config/paths.ts
10662
+ function defaultConfigBaseDir(env = process.env, home = homedir(), platform = process.platform) {
10663
+ if (platform === "win32") return env.APPDATA && win32.isAbsolute(env.APPDATA) ? env.APPDATA : win32.join(home, "AppData", "Roaming");
10664
+ return env.XDG_CONFIG_HOME && posix.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : posix.join(home, ".config");
10665
+ }
10666
+ function defaultStateBaseDir(env = process.env, home = homedir(), platform = process.platform) {
10667
+ if (platform === "win32") return env.LOCALAPPDATA && win32.isAbsolute(env.LOCALAPPDATA) ? env.LOCALAPPDATA : win32.join(home, "AppData", "Local");
10668
+ return env.XDG_STATE_HOME && posix.isAbsolute(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : posix.join(home, ".local", "state");
10669
+ }
10670
+ function defaultConfigPath(env = process.env, home = homedir(), platform = process.platform) {
10671
+ return (platform === "win32" ? win32.join : posix.join)(defaultConfigBaseDir(env, home, platform), "caplets", "config.json");
10672
+ }
10673
+ function defaultAuthDir(env = process.env, home = homedir(), platform = process.platform) {
10674
+ return (platform === "win32" ? win32.join : posix.join)(defaultStateBaseDir(env, home, platform), "caplets", "auth");
10675
+ }
10676
+ const DEFAULT_CONFIG_PATH = defaultConfigPath();
10677
+ const DEFAULT_AUTH_DIR = defaultAuthDir();
10678
+ const PROJECT_CONFIG_FILE = join(".caplets", "config.json");
10679
+ const TRUST_PROJECT_CAPLETS_ENV = "CAPLETS_TRUST_PROJECT_CAPLETS";
10680
+ function resolveConfigPath(path) {
10681
+ return path ?? DEFAULT_CONFIG_PATH;
10682
+ }
10683
+ function resolveProjectConfigPath(cwd = process.cwd()) {
10684
+ return join(cwd, PROJECT_CONFIG_FILE);
10685
+ }
10686
+ function resolveCapletsRoot(configPath = resolveConfigPath()) {
10687
+ return dirname(configPath);
10688
+ }
10689
+ function resolveProjectCapletsRoot(cwd = process.cwd()) {
10690
+ return join(cwd, ".caplets");
10691
+ }
10692
+ function isTrustedEnvEnabled(value) {
10693
+ return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
10694
+ }
10695
+ //#endregion
10661
10696
  //#region src/errors.ts
10662
10697
  var CapletsError = class extends Error {
10663
10698
  code;
@@ -10708,11 +10743,12 @@ function errorResult(error, fallback) {
10708
10743
  }
10709
10744
  //#endregion
10710
10745
  //#region src/auth/store.ts
10711
- function authStorePath(server, authDir = join(homedir(), ".caplets", "auth")) {
10746
+ function authStorePath(server, authDir = DEFAULT_AUTH_DIR) {
10712
10747
  if (!server || server.includes("/") || server.includes("\\") || server.includes("..")) throw new CapletsError("REQUEST_INVALID", `Invalid auth store server name ${server}`);
10713
10748
  const authRoot = resolve(authDir);
10714
10749
  const candidate = resolve(authRoot, `${server}.json`);
10715
- if (candidate !== authRoot && candidate.startsWith(`${authRoot}${sep}`)) return candidate;
10750
+ const relativePath = relative(authRoot, candidate);
10751
+ if (relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath)) return candidate;
10716
10752
  throw new CapletsError("REQUEST_INVALID", `Invalid auth store server name ${server}`);
10717
10753
  }
10718
10754
  function readTokenBundle(server, authDir) {
@@ -10780,11 +10816,13 @@ var FileOAuthProvider = class {
10780
10816
  verifier = base64url(randomBytes(32));
10781
10817
  stateValue = base64url(randomBytes(24));
10782
10818
  clientInfo;
10819
+ clientMetadataUrl;
10783
10820
  constructor(server, redirectUrl, onRedirect, authDir) {
10784
10821
  this.server = server;
10785
10822
  this.redirectUrl = redirectUrl;
10786
10823
  this.onRedirect = onRedirect;
10787
10824
  this.authDir = authDir;
10825
+ if ((this.server.auth?.type === "oauth2" || this.server.auth?.type === "oidc") && this.server.auth.clientMetadataUrl) this.clientMetadataUrl = this.server.auth.clientMetadataUrl;
10788
10826
  }
10789
10827
  get clientMetadata() {
10790
10828
  return {
@@ -10884,10 +10922,19 @@ async function runOAuthFlow(server, options = {}) {
10884
10922
  authorizationCode: completion.code,
10885
10923
  ...scope ? { scope } : {}
10886
10924
  });
10925
+ } catch (error) {
10926
+ throw normalizeMcpOAuthError(server, error);
10887
10927
  } finally {
10888
10928
  await callback.close();
10889
10929
  }
10890
10930
  }
10931
+ function normalizeMcpOAuthError(server, error) {
10932
+ if ((server.auth?.type === "oauth2" || server.auth?.type === "oidc") && !server.auth.clientId && !server.auth.clientMetadataUrl && error instanceof Error && error.message.includes("does not support dynamic client registration")) return new CapletsError("AUTH_FAILED", "OAuth is not available for this server without a host-specific OAuth app or PAT auth", {
10933
+ server: server.server,
10934
+ nextAction: "configure_bearer_auth_or_host_oauth_app"
10935
+ });
10936
+ return error;
10937
+ }
10891
10938
  async function runGenericOAuthFlow(target, options = {}) {
10892
10939
  if (target.auth?.type !== "oauth2" && target.auth?.type !== "oidc") throw new CapletsError("REQUEST_INVALID", `${target.server} is not configured for OAuth`);
10893
10940
  const authConfig = target.auth;
@@ -11057,6 +11104,11 @@ async function resolveGenericClient(target, authConfig, metadata, redirectUri, a
11057
11104
  ...authConfig.clientSecret ? { clientSecret: authConfig.clientSecret } : {},
11058
11105
  dynamic: false
11059
11106
  };
11107
+ if (authConfig.clientMetadataUrl) return {
11108
+ clientId: authConfig.clientMetadataUrl,
11109
+ ...authConfig.clientSecret ? { clientSecret: authConfig.clientSecret } : {},
11110
+ dynamic: false
11111
+ };
11060
11112
  if (!metadata.registration_endpoint) throw new CapletsError("AUTH_FAILED", "OAuth clientId is required without dynamic registration", { server: target.server });
11061
11113
  const response = await fetchJson(metadata.registration_endpoint, target.requestTimeoutMs, {
11062
11114
  method: "POST",
@@ -11125,8 +11177,9 @@ function assertAllowedAuthUrl(value, label, allowLoopbackHttp = false) {
11125
11177
  throw new CapletsError("AUTH_FAILED", `${label} must use https except loopback development URLs`);
11126
11178
  }
11127
11179
  function assertTokenBundleMatchesTarget(bundle, target, authConfig) {
11180
+ const configuredClientId = authConfig.clientId ?? authConfig.clientMetadataUrl;
11128
11181
  const expectedOrigin = protectedResourceOrigin(target, authConfig);
11129
- if (bundle.authType !== authConfig.type || expectedOrigin && bundle.protectedResourceOrigin !== expectedOrigin || authConfig.clientId && bundle.clientId !== authConfig.clientId || authConfig.issuer && bundle.issuer !== authConfig.issuer) throw new CapletsError("AUTH_REQUIRED", `OAuth credentials for ${target.server} do not match the configured backend`, {
11182
+ if (bundle.authType !== authConfig.type || expectedOrigin && bundle.protectedResourceOrigin !== expectedOrigin || configuredClientId && bundle.clientId !== configuredClientId || authConfig.issuer && bundle.issuer !== authConfig.issuer) throw new CapletsError("AUTH_REQUIRED", `OAuth credentials for ${target.server} do not match the configured backend`, {
11130
11183
  server: target.server,
11131
11184
  backend: target.backend,
11132
11185
  authType: authConfig.type,
@@ -18762,6 +18815,7 @@ const capletRemoteAuthSchema = discriminatedUnion("type", [
18762
18815
  resourceMetadataUrl: string().min(1).optional(),
18763
18816
  authorizationServerMetadataUrl: string().min(1).optional(),
18764
18817
  openidConfigurationUrl: string().min(1).optional(),
18818
+ clientMetadataUrl: string().min(1).optional(),
18765
18819
  clientId: string().min(1).optional(),
18766
18820
  clientSecret: string().min(1).optional(),
18767
18821
  scopes: array(string().min(1)).optional(),
@@ -18775,6 +18829,7 @@ const capletRemoteAuthSchema = discriminatedUnion("type", [
18775
18829
  resourceMetadataUrl: string().min(1).optional(),
18776
18830
  authorizationServerMetadataUrl: string().min(1).optional(),
18777
18831
  openidConfigurationUrl: string().min(1).optional(),
18832
+ clientMetadataUrl: string().min(1).optional(),
18778
18833
  clientId: string().min(1).optional(),
18779
18834
  clientSecret: string().min(1).optional(),
18780
18835
  scopes: array(string().min(1)).optional(),
@@ -18799,6 +18854,7 @@ const capletEndpointAuthSchema = discriminatedUnion("type", [
18799
18854
  resourceMetadataUrl: string().min(1).optional(),
18800
18855
  authorizationServerMetadataUrl: string().min(1).optional(),
18801
18856
  openidConfigurationUrl: string().min(1).optional(),
18857
+ clientMetadataUrl: string().min(1).optional(),
18802
18858
  clientId: string().min(1).optional(),
18803
18859
  clientSecret: string().min(1).optional(),
18804
18860
  scopes: array(string().min(1)).optional(),
@@ -18812,6 +18868,7 @@ const capletEndpointAuthSchema = discriminatedUnion("type", [
18812
18868
  resourceMetadataUrl: string().min(1).optional(),
18813
18869
  authorizationServerMetadataUrl: string().min(1).optional(),
18814
18870
  openidConfigurationUrl: string().min(1).optional(),
18871
+ clientMetadataUrl: string().min(1).optional(),
18815
18872
  clientId: string().min(1).optional(),
18816
18873
  clientSecret: string().min(1).optional(),
18817
18874
  scopes: array(string().min(1)).optional(),
@@ -18855,10 +18912,11 @@ const capletMcpServerSchema = object$1({
18855
18912
  path: ["url"],
18856
18913
  message: "remote url must use https except loopback development urls"
18857
18914
  });
18858
- if (server.auth?.type === "oauth2") for (const field of [
18915
+ if (server.auth?.type === "oauth2" || server.auth?.type === "oidc") for (const field of [
18859
18916
  "authorizationUrl",
18860
18917
  "tokenUrl",
18861
18918
  "issuer",
18919
+ "clientMetadataUrl",
18862
18920
  "redirectUri"
18863
18921
  ]) {
18864
18922
  const value = server.auth[field];
@@ -19015,13 +19073,13 @@ function loadCapletFiles(root) {
19015
19073
  for (const candidate of discoverCapletFiles(root)) {
19016
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}`);
19017
19075
  const config = readCapletFile(candidate.path);
19018
- if (isPlainObject$4(config) && config.backend === "openapi") {
19076
+ if (isPlainObject$5(config) && config.backend === "openapi") {
19019
19077
  const { backend: _backend, ...endpoint } = config;
19020
19078
  openapiEndpoints[candidate.id] = endpoint;
19021
- } else if (isPlainObject$4(config) && config.backend === "graphql") {
19079
+ } else if (isPlainObject$5(config) && config.backend === "graphql") {
19022
19080
  const { backend: _backend, ...endpoint } = config;
19023
19081
  graphqlEndpoints[candidate.id] = endpoint;
19024
- } else if (isPlainObject$4(config) && config.backend === "http") {
19082
+ } else if (isPlainObject$5(config) && config.backend === "http") {
19025
19083
  const { backend: _backend, ...endpoint } = config;
19026
19084
  httpApis[candidate.id] = endpoint;
19027
19085
  } else servers[candidate.id] = config;
@@ -19142,7 +19200,7 @@ function parseFrontmatter(text, path) {
19142
19200
  value: text
19143
19201
  });
19144
19202
  matter(file, { strip: true });
19145
- 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");
19146
19204
  return {
19147
19205
  frontmatter: file.data.matter,
19148
19206
  body: String(file)
@@ -19151,7 +19209,7 @@ function parseFrontmatter(text, path) {
19151
19209
  throw new CapletsError("CONFIG_INVALID", `Caplet file at ${path} has invalid YAML frontmatter`, redactSecrets(error));
19152
19210
  }
19153
19211
  }
19154
- function isPlainObject$4(value) {
19212
+ function isPlainObject$5(value) {
19155
19213
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
19156
19214
  }
19157
19215
  function validateCapletId(id, path) {
@@ -19161,27 +19219,6 @@ function hasEnvReference$1(value) {
19161
19219
  return /\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$env:[A-Za-z_][A-Za-z0-9_]*/.test(value);
19162
19220
  }
19163
19221
  //#endregion
19164
- //#region src/config/paths.ts
19165
- const DEFAULT_CONFIG_PATH = join(homedir(), ".caplets", "config.json");
19166
- const DEFAULT_AUTH_DIR = join(homedir(), ".caplets", "auth");
19167
- const PROJECT_CONFIG_FILE = join(".caplets", "config.json");
19168
- const TRUST_PROJECT_CAPLETS_ENV = "CAPLETS_TRUST_PROJECT_CAPLETS";
19169
- function resolveConfigPath(path) {
19170
- return path ?? DEFAULT_CONFIG_PATH;
19171
- }
19172
- function resolveProjectConfigPath(cwd = process.cwd()) {
19173
- return join(cwd, PROJECT_CONFIG_FILE);
19174
- }
19175
- function resolveCapletsRoot(configPath = resolveConfigPath()) {
19176
- return dirname(configPath);
19177
- }
19178
- function resolveProjectCapletsRoot(cwd = process.cwd()) {
19179
- return join(cwd, ".caplets");
19180
- }
19181
- function isTrustedEnvEnabled(value) {
19182
- return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
19183
- }
19184
- //#endregion
19185
19222
  //#region src/config.ts
19186
19223
  const NON_INTERPOLATED_SERVER_FIELDS = new Set([
19187
19224
  "name",
@@ -19207,6 +19244,7 @@ const remoteAuthSchema = discriminatedUnion("type", [
19207
19244
  resourceMetadataUrl: string().url().optional(),
19208
19245
  authorizationServerMetadataUrl: string().url().optional(),
19209
19246
  openidConfigurationUrl: string().url().optional(),
19247
+ clientMetadataUrl: string().url().optional(),
19210
19248
  clientId: string().min(1).optional(),
19211
19249
  clientSecret: string().min(1).optional(),
19212
19250
  scopes: array(string().min(1)).optional(),
@@ -19220,6 +19258,7 @@ const remoteAuthSchema = discriminatedUnion("type", [
19220
19258
  resourceMetadataUrl: string().url().optional(),
19221
19259
  authorizationServerMetadataUrl: string().url().optional(),
19222
19260
  openidConfigurationUrl: string().url().optional(),
19261
+ clientMetadataUrl: string().url().optional(),
19223
19262
  clientId: string().min(1).optional(),
19224
19263
  clientSecret: string().min(1).optional(),
19225
19264
  scopes: array(string().min(1)).optional(),
@@ -19234,6 +19273,7 @@ const oauthLikeAuthSchema = union([object$1({
19234
19273
  resourceMetadataUrl: string().url().optional(),
19235
19274
  authorizationServerMetadataUrl: string().url().optional(),
19236
19275
  openidConfigurationUrl: string().url().optional(),
19276
+ clientMetadataUrl: string().url().optional(),
19237
19277
  clientId: string().min(1).optional(),
19238
19278
  clientSecret: string().min(1).optional(),
19239
19279
  scopes: array(string().min(1)).optional(),
@@ -19246,6 +19286,7 @@ const oauthLikeAuthSchema = union([object$1({
19246
19286
  resourceMetadataUrl: string().url().optional(),
19247
19287
  authorizationServerMetadataUrl: string().url().optional(),
19248
19288
  openidConfigurationUrl: string().url().optional(),
19289
+ clientMetadataUrl: string().url().optional(),
19249
19290
  clientId: string().min(1).optional(),
19250
19291
  clientSecret: string().min(1).optional(),
19251
19292
  scopes: array(string().min(1)).optional(),
@@ -19345,6 +19386,7 @@ const httpActionSchema = object$1({
19345
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"),
19346
19387
  description: string().min(1).optional().describe("Action capability description."),
19347
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."),
19348
19390
  query: httpScalarMappingSchema.optional().describe("Query parameter mapping."),
19349
19391
  headers: httpScalarMappingSchema.optional().describe("Request header mapping."),
19350
19392
  jsonBody: unknown().optional().describe("JSON request body mapping.")
@@ -19609,7 +19651,7 @@ function normalizeLocalPaths(input, baseDir) {
19609
19651
  }
19610
19652
  function normalizeEndpointPaths(endpoints, baseDir, normalize) {
19611
19653
  if (!endpoints) return;
19612
- 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]));
19613
19655
  }
19614
19656
  function normalizeOpenApiPath(endpoint, baseDir) {
19615
19657
  return {
@@ -19618,7 +19660,7 @@ function normalizeOpenApiPath(endpoint, baseDir) {
19618
19660
  };
19619
19661
  }
19620
19662
  function normalizeGraphQlPath(endpoint, baseDir) {
19621
- 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) ? {
19622
19664
  ...operation,
19623
19665
  documentPath: normalizeLocalPath(operation.documentPath, baseDir)
19624
19666
  } : operation])) : endpoint.operations;
@@ -19737,7 +19779,7 @@ function isPublicMetadataPath(path) {
19737
19779
  if (path.length < 3 || path[0] !== "mcpServers" && path[0] !== "openapiEndpoints" && path[0] !== "graphqlEndpoints" && path[0] !== "httpApis") return false;
19738
19780
  return NON_INTERPOLATED_SERVER_FIELDS.has(path[2] ?? "");
19739
19781
  }
19740
- function isPlainObject$3(value) {
19782
+ function isPlainObject$4(value) {
19741
19783
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
19742
19784
  }
19743
19785
  function hasEnvReference(value) {
@@ -19896,12 +19938,14 @@ function formatCapletList(rows) {
19896
19938
  }
19897
19939
  function resolveCliConfigPaths(envConfigPath, authDir) {
19898
19940
  const configPath = resolveConfigPath(envConfigPath);
19941
+ const effectiveAuthDir = authDir ?? DEFAULT_AUTH_DIR;
19899
19942
  return {
19900
19943
  userConfig: configPath,
19901
19944
  projectConfig: resolveProjectConfigPath(),
19902
19945
  userRoot: resolveCapletsRoot(configPath),
19946
+ stateRoot: dirname(effectiveAuthDir),
19903
19947
  projectRoot: resolveProjectCapletsRoot(),
19904
- authDir: authDir ?? DEFAULT_AUTH_DIR,
19948
+ authDir: effectiveAuthDir,
19905
19949
  envConfig: envConfigPath ?? null,
19906
19950
  projectCapletsTrusted: isTrustedProjectCapletsEnabled()
19907
19951
  };
@@ -19911,6 +19955,7 @@ function formatConfigPaths(paths) {
19911
19955
  `userConfig: ${paths.userConfig}`,
19912
19956
  `projectConfig: ${paths.projectConfig}`,
19913
19957
  `userRoot: ${paths.userRoot}`,
19958
+ `stateRoot: ${paths.stateRoot}`,
19914
19959
  `projectRoot: ${paths.projectRoot}`,
19915
19960
  `authDir: ${paths.authDir}`,
19916
19961
  `envConfig: ${paths.envConfig ?? "unset"}`,
@@ -25820,7 +25865,7 @@ var Protocol = class {
25820
25865
  };
25821
25866
  }
25822
25867
  };
25823
- function isPlainObject$2(value) {
25868
+ function isPlainObject$3(value) {
25824
25869
  return value !== null && typeof value === "object" && !Array.isArray(value);
25825
25870
  }
25826
25871
  function mergeCapabilities(base, additional) {
@@ -25830,7 +25875,7 @@ function mergeCapabilities(base, additional) {
25830
25875
  const addValue = additional[k];
25831
25876
  if (addValue === void 0) continue;
25832
25877
  const baseValue = result[k];
25833
- if (isPlainObject$2(baseValue) && isPlainObject$2(addValue)) result[k] = {
25878
+ if (isPlainObject$3(baseValue) && isPlainObject$3(addValue)) result[k] = {
25834
25879
  ...baseValue,
25835
25880
  ...addValue
25836
25881
  };
@@ -35726,7 +35771,8 @@ var DownstreamManager = class {
35726
35771
  tool: tool.name,
35727
35772
  ...tool.description ? { description: tool.description } : {},
35728
35773
  ...tool.annotations ? { annotations: tool.annotations } : {},
35729
- hasInputSchema: Boolean(tool.inputSchema)
35774
+ hasInputSchema: Boolean(tool.inputSchema),
35775
+ hasOutputSchema: Boolean(tool.outputSchema)
35730
35776
  };
35731
35777
  }
35732
35778
  search(server, tools, query, limit) {
@@ -50705,7 +50751,8 @@ var GraphQLManager = class {
50705
50751
  tool: tool.name,
50706
50752
  ...tool.description ? { description: tool.description } : {},
50707
50753
  ...tool.annotations ? { annotations: tool.annotations } : {},
50708
- hasInputSchema: Boolean(tool.inputSchema)
50754
+ hasInputSchema: Boolean(tool.inputSchema),
50755
+ hasOutputSchema: Boolean(tool.outputSchema)
50709
50756
  };
50710
50757
  }
50711
50758
  search(endpoint, tools, query, limit) {
@@ -51103,7 +51150,8 @@ var HttpActionManager = class {
51103
51150
  tool: tool.name,
51104
51151
  ...tool.description ? { description: tool.description } : {},
51105
51152
  ...tool.annotations ? { annotations: tool.annotations } : {},
51106
- hasInputSchema: Boolean(tool.inputSchema)
51153
+ hasInputSchema: Boolean(tool.inputSchema),
51154
+ hasOutputSchema: Boolean(tool.outputSchema)
51107
51155
  };
51108
51156
  }
51109
51157
  search(api, tools, query, limit) {
@@ -51115,6 +51163,7 @@ var HttpActionManager = class {
51115
51163
  name: operation.name,
51116
51164
  ...operation.description ? { description: operation.description } : {},
51117
51165
  inputSchema: operation.inputSchema ?? DEFAULT_INPUT_SCHEMA,
51166
+ ...operation.outputSchema ? { outputSchema: operation.outputSchema } : {},
51118
51167
  annotations: {
51119
51168
  readOnlyHint: operation.method === "GET",
51120
51169
  destructiveHint: operation.method === "DELETE"
@@ -51186,7 +51235,7 @@ function resolveMapping(mapping, input) {
51186
51235
  function resolveMappingToRecord(mapping, input, name) {
51187
51236
  if (mapping === void 0) return {};
51188
51237
  const resolved = resolveMapping(mapping, input);
51189
- 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`);
51190
51239
  return resolved;
51191
51240
  }
51192
51241
  function valueAtPath(input, path) {
@@ -51281,9 +51330,9 @@ function buildActionUrl(base, actionPath, options = {}) {
51281
51330
  return baseUrl;
51282
51331
  }
51283
51332
  function asRecord$1(value) {
51284
- return isPlainObject$1(value) ? value : {};
51333
+ return isPlainObject$2(value) ? value : {};
51285
51334
  }
51286
- function isPlainObject$1(value) {
51335
+ function isPlainObject$2(value) {
51287
51336
  return value !== null && typeof value === "object" && !Array.isArray(value);
51288
51337
  }
51289
51338
  //#endregion
@@ -60897,7 +60946,8 @@ var OpenApiManager = class {
60897
60946
  tool: tool.name,
60898
60947
  ...tool.description ? { description: tool.description } : {},
60899
60948
  ...tool.annotations ? { annotations: tool.annotations } : {},
60900
- hasInputSchema: Boolean(tool.inputSchema)
60949
+ hasInputSchema: Boolean(tool.inputSchema),
60950
+ hasOutputSchema: Boolean(tool.outputSchema)
60901
60951
  };
60902
60952
  }
60903
60953
  search(endpoint, tools, query, limit) {
@@ -60940,6 +60990,7 @@ var OpenApiManager = class {
60940
60990
  name: operation.name,
60941
60991
  ...operation.summary || operation.description ? { description: operation.summary ?? operation.description } : {},
60942
60992
  inputSchema: operation.inputSchema,
60993
+ ...operation.outputSchema ? { outputSchema: operation.outputSchema } : {},
60943
60994
  annotations: {
60944
60995
  readOnlyHint: operation.method === "get" || operation.method === "head",
60945
60996
  destructiveHint: operation.method === "delete"
@@ -60975,6 +61026,7 @@ function extractOperations(endpoint, document) {
60975
61026
  seen.add(name);
60976
61027
  const parameters = [...inheritedParameters, ...Array.isArray(operation.parameters) ? operation.parameters : []];
60977
61028
  const requestBody = requestBodyFor(operation);
61029
+ const outputSchema = outputSchemaFor(operation);
60978
61030
  const baseUrl = endpoint.baseUrl ?? firstServerUrl(document);
60979
61031
  validateOperationBaseUrl(endpoint, baseUrl);
60980
61032
  operations.push({
@@ -60984,6 +61036,7 @@ function extractOperations(endpoint, document) {
60984
61036
  ...typeof operation.summary === "string" ? { summary: operation.summary } : {},
60985
61037
  ...typeof operation.description === "string" ? { description: operation.description } : {},
60986
61038
  inputSchema: inputSchemaFor(parameters, requestBody),
61039
+ ...outputSchema ? { outputSchema } : {},
60987
61040
  ...requestBody?.contentType ? { requestBodyContentType: requestBody.contentType } : {},
60988
61041
  ...baseUrl ? { baseUrl } : {}
60989
61042
  });
@@ -61004,6 +61057,53 @@ function requestBodyFor(operation) {
61004
61057
  contentType
61005
61058
  };
61006
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
+ }
61007
61107
  function inputSchemaFor(parameters, requestBody) {
61008
61108
  const schema = {
61009
61109
  type: "object",
@@ -61163,6 +61263,26 @@ function openApiCacheKey(endpoint) {
61163
61263
  });
61164
61264
  }
61165
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
61166
61286
  //#region src/registry.ts
61167
61287
  var ServerRegistry = class {
61168
61288
  config;
@@ -61230,23 +61350,6 @@ var ServerRegistry = class {
61230
61350
  ];
61231
61351
  }
61232
61352
  };
61233
- function capabilityDescription(server) {
61234
- const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : server.backend === "graphql" ? "GraphQL endpoint" : "HTTP API";
61235
- const checkOperation = server.backend === "mcp" ? "check_mcp_server" : "check_backend";
61236
- const hint = [
61237
- `Use this Caplet to inspect and call tools from its ${backendName} backend.`,
61238
- "",
61239
- "Recommended flow:",
61240
- "- Read the full Caplet card: {\"operation\":\"get_caplet\"}",
61241
- `- Check the backend: {"operation":"${checkOperation}"}`,
61242
- "- Discover tools: {\"operation\":\"list_tools\"} or {\"operation\":\"search_tools\",\"query\":\"<what you need>\"}",
61243
- "- Read one tool schema: {\"operation\":\"get_tool\",\"tool\":\"<tool name>\"}",
61244
- "- Invoke one downstream tool: {\"operation\":\"call_tool\",\"tool\":\"<tool name>\",\"arguments\":{...}}",
61245
- "",
61246
- "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."
61247
- ].join("\n");
61248
- return `${server.name}\n\n${server.description}\n\n${hint}`;
61249
- }
61250
61353
  function backendDetail(server) {
61251
61354
  if (server.backend === "openapi") return {
61252
61355
  type: "openapi",
@@ -61284,7 +61387,110 @@ function graphQlSource(server) {
61284
61387
  return "introspection";
61285
61388
  }
61286
61389
  //#endregion
61287
- //#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
61288
61494
  const operations = [
61289
61495
  "get_caplet",
61290
61496
  "check_backend",
@@ -61294,16 +61500,25 @@ const operations = [
61294
61500
  "get_tool",
61295
61501
  "call_tool"
61296
61502
  ];
61297
- const generatedToolInputSchema = object$1({
61298
- operation: _enum(operations).describe([
61503
+ const generatedToolInputDescriptions = {
61504
+ operation: [
61299
61505
  "Caplets wrapper operation to perform for this configured Caplet backend.",
61300
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.",
61301
61507
  "For call_tool, pass downstream inputs only inside the top-level \"arguments\" object."
61302
- ].join(" ")),
61303
- 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."),
61304
- 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."),
61305
- 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."),
61306
- 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)
61307
61522
  }).strict();
61308
61523
  async function handleServerTool(server, request, registry, downstream, openapi, graphql, http) {
61309
61524
  const parsed = validateOperationRequest(request, registry.config.options.maxSearchLimit);
@@ -61338,7 +61553,15 @@ async function handleServerTool(server, request, registry, downstream, openapi,
61338
61553
  tool
61339
61554
  });
61340
61555
  }
61341
- 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
+ }
61342
61565
  }
61343
61566
  }
61344
61567
  function validateOperationRequest(request, maxSearchLimit) {
@@ -61379,13 +61602,22 @@ function validateOperationRequest(request, maxSearchLimit) {
61379
61602
  tool: value.tool
61380
61603
  };
61381
61604
  case "call_tool":
61382
- allowed(["tool", "arguments"]);
61605
+ allowed([
61606
+ "tool",
61607
+ "arguments",
61608
+ "fields"
61609
+ ]);
61383
61610
  if (!value.tool) throw new CapletsError("REQUEST_INVALID", "call_tool requires tool");
61384
61611
  if (!isPlainObject(value.arguments)) throw new CapletsError("REQUEST_INVALID", "call_tool.arguments must be a JSON object");
61385
- return {
61612
+ return value.fields === void 0 ? {
61386
61613
  operation: "call_tool",
61387
61614
  tool: value.tool,
61388
61615
  arguments: value.arguments
61616
+ } : {
61617
+ operation: "call_tool",
61618
+ tool: value.tool,
61619
+ arguments: value.arguments,
61620
+ fields: value.fields
61389
61621
  };
61390
61622
  }
61391
61623
  }
@@ -61398,6 +61630,20 @@ function jsonResult(value) {
61398
61630
  structuredContent: { result: value }
61399
61631
  };
61400
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
+ }
61401
61647
  function isPlainObject(value) {
61402
61648
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
61403
61649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caplets",
3
- "version": "0.8.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
  }
@@ -149,6 +149,10 @@
149
149
  "type": "string",
150
150
  "minLength": 1
151
151
  },
152
+ "clientMetadataUrl": {
153
+ "type": "string",
154
+ "minLength": 1
155
+ },
152
156
  "clientId": {
153
157
  "type": "string",
154
158
  "minLength": 1
@@ -203,6 +207,10 @@
203
207
  "type": "string",
204
208
  "minLength": 1
205
209
  },
210
+ "clientMetadataUrl": {
211
+ "type": "string",
212
+ "minLength": 1
213
+ },
206
214
  "clientId": {
207
215
  "type": "string",
208
216
  "minLength": 1
@@ -353,6 +361,10 @@
353
361
  "type": "string",
354
362
  "minLength": 1
355
363
  },
364
+ "clientMetadataUrl": {
365
+ "type": "string",
366
+ "minLength": 1
367
+ },
356
368
  "clientId": {
357
369
  "type": "string",
358
370
  "minLength": 1
@@ -407,6 +419,10 @@
407
419
  "type": "string",
408
420
  "minLength": 1
409
421
  },
422
+ "clientMetadataUrl": {
423
+ "type": "string",
424
+ "minLength": 1
425
+ },
410
426
  "clientId": {
411
427
  "type": "string",
412
428
  "minLength": 1
@@ -591,6 +607,10 @@
591
607
  "type": "string",
592
608
  "minLength": 1
593
609
  },
610
+ "clientMetadataUrl": {
611
+ "type": "string",
612
+ "minLength": 1
613
+ },
594
614
  "clientId": {
595
615
  "type": "string",
596
616
  "minLength": 1
@@ -645,6 +665,10 @@
645
665
  "type": "string",
646
666
  "minLength": 1
647
667
  },
668
+ "clientMetadataUrl": {
669
+ "type": "string",
670
+ "minLength": 1
671
+ },
648
672
  "clientId": {
649
673
  "type": "string",
650
674
  "minLength": 1
@@ -788,6 +812,10 @@
788
812
  "type": "string",
789
813
  "minLength": 1
790
814
  },
815
+ "clientMetadataUrl": {
816
+ "type": "string",
817
+ "minLength": 1
818
+ },
791
819
  "clientId": {
792
820
  "type": "string",
793
821
  "minLength": 1
@@ -842,6 +870,10 @@
842
870
  "type": "string",
843
871
  "minLength": 1
844
872
  },
873
+ "clientMetadataUrl": {
874
+ "type": "string",
875
+ "minLength": 1
876
+ },
845
877
  "clientId": {
846
878
  "type": "string",
847
879
  "minLength": 1
@@ -168,6 +168,10 @@
168
168
  "type": "string",
169
169
  "format": "uri"
170
170
  },
171
+ "clientMetadataUrl": {
172
+ "type": "string",
173
+ "format": "uri"
174
+ },
171
175
  "clientId": {
172
176
  "type": "string",
173
177
  "minLength": 1
@@ -222,6 +226,10 @@
222
226
  "type": "string",
223
227
  "format": "uri"
224
228
  },
229
+ "clientMetadataUrl": {
230
+ "type": "string",
231
+ "format": "uri"
232
+ },
225
233
  "clientId": {
226
234
  "type": "string",
227
235
  "minLength": 1
@@ -403,6 +411,10 @@
403
411
  "type": "string",
404
412
  "format": "uri"
405
413
  },
414
+ "clientMetadataUrl": {
415
+ "type": "string",
416
+ "format": "uri"
417
+ },
406
418
  "clientId": {
407
419
  "type": "string",
408
420
  "minLength": 1
@@ -457,6 +469,10 @@
457
469
  "type": "string",
458
470
  "format": "uri"
459
471
  },
472
+ "clientMetadataUrl": {
473
+ "type": "string",
474
+ "format": "uri"
475
+ },
460
476
  "clientId": {
461
477
  "type": "string",
462
478
  "minLength": 1
@@ -670,6 +686,10 @@
670
686
  "type": "string",
671
687
  "format": "uri"
672
688
  },
689
+ "clientMetadataUrl": {
690
+ "type": "string",
691
+ "format": "uri"
692
+ },
673
693
  "clientId": {
674
694
  "type": "string",
675
695
  "minLength": 1
@@ -724,6 +744,10 @@
724
744
  "type": "string",
725
745
  "format": "uri"
726
746
  },
747
+ "clientMetadataUrl": {
748
+ "type": "string",
749
+ "format": "uri"
750
+ },
727
751
  "clientId": {
728
752
  "type": "string",
729
753
  "minLength": 1
@@ -896,6 +920,10 @@
896
920
  "type": "string",
897
921
  "format": "uri"
898
922
  },
923
+ "clientMetadataUrl": {
924
+ "type": "string",
925
+ "format": "uri"
926
+ },
899
927
  "clientId": {
900
928
  "type": "string",
901
929
  "minLength": 1
@@ -950,6 +978,10 @@
950
978
  "type": "string",
951
979
  "format": "uri"
952
980
  },
981
+ "clientMetadataUrl": {
982
+ "type": "string",
983
+ "format": "uri"
984
+ },
953
985
  "clientId": {
954
986
  "type": "string",
955
987
  "minLength": 1
@@ -1009,6 +1041,14 @@
1009
1041
  },
1010
1042
  "additionalProperties": {}
1011
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
+ },
1012
1052
  "query": {
1013
1053
  "description": "Query parameter mapping.",
1014
1054
  "type": "object",