api-spec-cli 0.2.4 → 0.2.5

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
@@ -174,6 +174,49 @@ spec disable <name> # Disable without removing
174
174
  spec refresh <name> # Force re-fetch and update cache
175
175
  ```
176
176
 
177
+ `spec add` is an upsert — re-adding an existing name overwrites the entry (and clears its stale cache), since there's no separate `update` command. The response includes `"overwritten": true` when an existing entry was replaced.
178
+
179
+ ## Usage Ranking
180
+
181
+ `spec` records which operations/tools you call so agents can surface the ones that matter:
182
+
183
+ ```bash
184
+ spec list --spec petstore --top 5 # the 5 most-called operations first
185
+ spec usage # recorded usage across all specs
186
+ spec usage petstore # ranked operations for one spec
187
+ ```
188
+
189
+ Counts are stored locally in `~/spec-cli-config/usage.json`. `--top` applies after `--filter`/`--tag` and overrides `--limit`. Set `SPEC_NO_USAGE=1` to disable tracking.
190
+
191
+ ## Secrets & Environment Overrides
192
+
193
+ Stored values can reference environment variables instead of holding raw secrets. Use `${VAR}` in `--auth` or header values — it's expanded from the environment at call time, never stored expanded:
194
+
195
+ ```bash
196
+ spec add gh --mcp-http https://api.example.com/mcp --header "Authorization=Bearer ${GH_TOKEN}"
197
+ spec config set auth '${API_TOKEN}'
198
+ ```
199
+
200
+ A `.env` file in the working directory is auto-loaded on startup (real environment variables take precedence; set `SPEC_NO_DOTENV=1` to disable).
201
+
202
+ Two environment variables override a registered spec's connection per call — useful in CI:
203
+
204
+ ```bash
205
+ SPEC_URL=https://staging.example.com/mcp spec call --spec gh some_tool # override MCP/GraphQL endpoint
206
+ SPEC_HEADER_X_TENANT=acme spec list --spec gh # add/override a header
207
+ ```
208
+
209
+ `SPEC_HEADER_<NAME>` maps underscores to dashes (`SPEC_HEADER_X_TENANT` → `X-Tenant`). Precedence: call-time flags > env override > registry entry > project config.
210
+
211
+ ## Agent Skill
212
+
213
+ Install a ready-made skill that teaches an agent the explore-then-call workflow:
214
+
215
+ ```bash
216
+ spec skill install # copy SKILL.md into ~/.claude/skills/api-spec-cli/
217
+ spec skill path # print the bundled SKILL.md location
218
+ ```
219
+
177
220
  ---
178
221
 
179
222
  ## spec add options
@@ -259,7 +302,7 @@ spec auth myserver # Re-run the OAuth flow
259
302
  spec auth myserver --revoke # Clear stored token only
260
303
  ```
261
304
 
262
- Tokens are stored in `~/spec-cli-config/tokens/<name>.json` — separate from the cache, not touched by `spec refresh`.
305
+ Tokens are stored in `~/spec-cli-config/tokens/<name>.json` — separate from the cache, not touched by `spec refresh`. Access tokens are refreshed automatically: when a stored token expires, the refresh token is used and the rotated tokens are persisted, so the next command picks them up without re-authenticating.
263
306
 
264
307
  ### OAuth flags
265
308
 
@@ -301,9 +344,12 @@ JSON by default. Errors go to stderr as `{"error": "message"}` with a non-zero e
301
344
  ```bash
302
345
  spec list --spec petstore --format text
303
346
  spec show --spec petstore getPetById --format yaml
347
+ spec list --spec petstore --format toon # Token-Oriented Object Notation (densest)
304
348
  spec list --spec petstore --format=json # equals syntax also works
305
349
  ```
306
350
 
351
+ `toon` ([Token-Oriented Object Notation](https://github.com/toon-format/spec)) is the most token-efficient format for tabular/list output — the best choice when feeding results back into a model. Errors are always JSON regardless of format.
352
+
307
353
  ## Token Efficiency
308
354
 
309
355
  - `list` returns only IDs by default — no schemas
@@ -336,6 +382,7 @@ spec add fs --mcp-stdio "npx -y server /tmp" --env "TOKEN=${MY_SECRET}"
336
382
  | `~/spec-cli-config/registry.json` | Global named registry |
337
383
  | `~/spec-cli-config/cache/<name>.json` | Cached spec per registered entry |
338
384
  | `~/spec-cli-config/tokens/<name>.json` | OAuth tokens per MCP entry |
385
+ | `~/spec-cli-config/usage.json` | Operation/tool call counts for `--top` and `spec usage` |
339
386
  | `.spec-cli/config.json` | Project-local config (baseUrl, auth, headers) |
340
387
 
341
388
  ## Planned
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-spec-cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,8 +44,10 @@
44
44
  "node": ">=18.0.0"
45
45
  },
46
46
  "dependencies": {
47
- "@modelcontextprotocol/sdk": "^1.28.0",
48
- "yaml": "^2.7.0"
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "@toon-format/toon": "^2.3.0",
49
+ "yaml": "^2.9.0",
50
+ "yargs": "^18.0.0"
49
51
  },
50
52
  "devDependencies": {
51
53
  "prettier": "^3.8.1"
package/src/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import yargs from "yargs";
1
2
  import { listOperations } from "./commands/list.js";
2
3
  import { showOperation } from "./commands/show.js";
3
4
  import { callOperation } from "./commands/call.js";
@@ -8,6 +9,9 @@ import { addCmd } from "./commands/add.js";
8
9
  import { specsCmd, registryMutate } from "./commands/specs.js";
9
10
  import { grepCmd } from "./commands/grep.js";
10
11
  import { authCmd } from "./commands/auth.js";
12
+ import { usageCmd } from "./commands/usage.js";
13
+ import { skillCmd } from "./commands/skill.js";
14
+ import { loadDotenv } from "./dotenv.js";
11
15
  import { out, err, setFormat } from "./output.js";
12
16
 
13
17
  const HELP = `spec-cli — Explore and call APIs from the command line.
@@ -51,9 +55,12 @@ DISCOVER:
51
55
  spec list --spec <name> --filter user Search by keyword
52
56
  spec list --spec <name> --tag pets OpenAPI tag or GraphQL kind
53
57
  spec list --spec <name> --limit 10 Paginate
58
+ spec list --spec <name> --top 10 Rank by call count (most-used first)
54
59
  spec list --mcp-http <url> Inline: no registration needed
55
60
  spec grep <pattern> Search across all registered specs
56
61
  spec grep <pattern> --spec <name> Search within one spec
62
+ spec usage Show recorded usage for all specs
63
+ spec usage <name> Ranked operations for one spec
57
64
 
58
65
  INSPECT:
59
66
  spec show --spec <name> <op> Operation details (params, body, responses)
@@ -86,12 +93,22 @@ OTHER:
86
93
  spec auth <name> Re-authenticate an OAuth-protected MCP spec
87
94
  spec auth <name> --revoke Clear stored OAuth token
88
95
  spec validate <file-or-url> Check OpenAPI spec for errors
89
- --format json|text|yaml Output format (default: json)
96
+ spec skill install Install the agent skill into ~/.claude/skills/
97
+ spec skill path Print the bundled SKILL.md location
98
+ --format json|text|yaml|toon Output format (default: json; toon is densest)
90
99
 
91
- ENV VARS (MCP):
100
+ SECRETS & OVERRIDES:
101
+ Stored values (auth, headers) may use \${VAR} — expanded from the environment at call time.
102
+ A .env file in the working directory is auto-loaded (real env vars take precedence).
103
+ SPEC_URL=<url> Override a registered MCP/GraphQL spec's endpoint for this call
104
+ SPEC_HEADER_<NAME>=<value> Add/override a header (SPEC_HEADER_X_TENANT -> X-Tenant)
105
+
106
+ ENV VARS:
92
107
  MCP_MAX_RETRIES=3 Retry attempts on connection failure (default: 3)
93
108
  MCP_RETRY_DELAY=1000 Base retry delay in ms, doubles each attempt (default: 1000)
94
109
  SPEC_OAUTH_CALLBACK_PORT=3141 Default fixed port for browser OAuth callback
110
+ SPEC_NO_USAGE=1 Disable usage tracking
111
+ SPEC_NO_DOTENV=1 Disable .env auto-loading
95
112
 
96
113
  EXAMPLES:
97
114
  spec add agno --mcp-http https://docs.agno.com/mcp --description "Agno docs"
@@ -104,78 +121,216 @@ EXAMPLES:
104
121
  spec call --spec agno search_agno --var query="foo" --header X-Tenant=acme
105
122
  spec list --mcp-http https://docs.agno.com/mcp (inline, no registration)`;
106
123
 
107
- export async function run(args) {
108
- // Extract --format before routing (supports both --format json and --format=json)
109
- const newArgs = [];
110
- for (let i = 0; i < args.length; i++) {
111
- if (args[i] === "--format" && i + 1 < args.length) {
112
- setFormat(args[++i]);
113
- } else if (args[i].startsWith("--format=")) {
114
- setFormat(args[i].slice(9));
115
- } else {
116
- newArgs.push(args[i]);
117
- }
118
- }
119
- args = newArgs;
124
+ const specSourceOptions = {
125
+ spec: { type: "string", describe: "Use a registered spec" },
126
+ openapi: { type: "string", describe: "Inline OpenAPI URL or file" },
127
+ graphql: { type: "string", describe: "Inline GraphQL URL" },
128
+ "mcp-http": { type: "string", describe: "Inline MCP streamable-HTTP URL" },
129
+ "mcp-sse": { type: "string", describe: "Inline MCP SSE URL" },
130
+ "mcp-stdio": { type: "string", describe: "Inline MCP stdio command" },
131
+ };
132
+
133
+ const overrideOptions = {
134
+ auth: { type: "string", describe: "Override auth token" },
135
+ "base-url": { type: "string", describe: "Override base URL" },
136
+ header: { type: "string", array: true, describe: "Header k=v (repeatable)" },
137
+ "allow-tool": { type: "string", array: true, describe: "Allow tool glob (repeatable)" },
138
+ "disable-tool": { type: "string", array: true, describe: "Disable tool glob (repeatable)" },
139
+ env: { type: "string", array: true, describe: "Env KEY=VAL (repeatable, stdio only)" },
140
+ cwd: { type: "string", describe: "Working directory (stdio only)" },
141
+ };
120
142
 
121
- const cmd = args[0];
143
+ const sourceOptions = { ...specSourceOptions, ...overrideOptions };
144
+
145
+ const commands = (rest) => [
146
+ {
147
+ command: ["list", "ls"],
148
+ describe: "List operations or tools for a spec",
149
+ builder: (y) =>
150
+ y.options({
151
+ ...sourceOptions,
152
+ filter: { type: "string", describe: "Substring search across fields" },
153
+ compact: { type: "string", describe: "Set false to show full details" },
154
+ limit: { type: "string", describe: "Max results" },
155
+ offset: { type: "string", describe: "Skip the first N results" },
156
+ tag: { type: "string", describe: "OpenAPI tag or GraphQL kind" },
157
+ top: { type: "string", describe: "Rank by call count (most-used first)" },
158
+ }),
159
+ handler: () => listOperations(rest),
160
+ },
161
+ {
162
+ command: "show <operation>",
163
+ describe: "Show operation or tool details",
164
+ builder: (y) =>
165
+ y
166
+ .positional("operation", { type: "string", describe: "Operation id, path, or tool name" })
167
+ .options(sourceOptions),
168
+ handler: () => showOperation(rest),
169
+ },
170
+ {
171
+ command: "call <operation>",
172
+ describe: "Call an operation or MCP tool",
173
+ builder: (y) =>
174
+ y
175
+ .positional("operation", { type: "string", describe: "Operation id, path, or tool name" })
176
+ .options({
177
+ ...sourceOptions,
178
+ data: { type: "string", describe: "JSON body / MCP args, or - for stdin" },
179
+ "data-file": { type: "string", describe: "Read JSON body from a file" },
180
+ var: { type: "string", array: true, describe: "Path/GraphQL var k=v (repeatable)" },
181
+ query: { type: "string", array: true, describe: "Query param k=v (repeatable)" },
182
+ method: { type: "string", describe: "Override HTTP method" },
183
+ }),
184
+ handler: () => callOperation(rest),
185
+ },
186
+ {
187
+ command: ["types [type]", "type [type]"],
188
+ describe: "List schema/type names or inspect one type",
189
+ builder: (y) =>
190
+ y
191
+ .positional("type", { type: "string", describe: "Type or schema name to inspect" })
192
+ .options(sourceOptions),
193
+ handler: () => typesCmd(rest),
194
+ },
195
+ {
196
+ command: "grep <pattern>",
197
+ describe: "Search operations across registered specs",
198
+ builder: (y) =>
199
+ y
200
+ .positional("pattern", { type: "string", describe: "Glob or substring pattern" })
201
+ .option("spec", specSourceOptions.spec),
202
+ handler: () => grepCmd(rest),
203
+ },
204
+ {
205
+ command: "usage [name]",
206
+ describe: "Show recorded usage",
207
+ builder: (y) =>
208
+ y.positional("name", { type: "string", describe: "Spec name for ranked operations" }),
209
+ handler: () => usageCmd(rest),
210
+ },
211
+ {
212
+ command: "add <name>",
213
+ describe: "Register a spec in the registry",
214
+ builder: (y) =>
215
+ y.positional("name", { type: "string", describe: "Registry name" }).options({
216
+ ...specSourceOptions,
217
+ ...overrideOptions,
218
+ description: { type: "string", describe: "Human-readable description" },
219
+ "oauth-flow": { type: "string", choices: ["browser", "device"], describe: "OAuth flow" },
220
+ "oauth-client-id": { type: "string", describe: "Pre-registered OAuth client ID" },
221
+ "oauth-client-secret": {
222
+ type: "string",
223
+ describe: "OAuth client secret (stored securely)",
224
+ },
225
+ "oauth-callback-port": { type: "number", describe: "Fixed local OAuth callback port" },
226
+ }),
227
+ handler: () => addCmd(rest),
228
+ },
229
+ {
230
+ command: ["specs", "registry"],
231
+ describe: "List all registered specs",
232
+ builder: (y) =>
233
+ y.option("compact", { type: "string", describe: "Set false to show full entry config" }),
234
+ handler: () => specsCmd(rest),
235
+ },
236
+ {
237
+ command: "remove <name>",
238
+ describe: "Delete a spec from the registry",
239
+ builder: (y) => y.positional("name", { type: "string", describe: "Registry name" }),
240
+ handler: () => registryMutate("remove", rest),
241
+ },
242
+ {
243
+ command: "enable <name>",
244
+ describe: "Enable a disabled spec",
245
+ builder: (y) => y.positional("name", { type: "string", describe: "Registry name" }),
246
+ handler: () => registryMutate("enable", rest),
247
+ },
248
+ {
249
+ command: "disable <name>",
250
+ describe: "Disable a spec without removing it",
251
+ builder: (y) => y.positional("name", { type: "string", describe: "Registry name" }),
252
+ handler: () => registryMutate("disable", rest),
253
+ },
254
+ {
255
+ command: "refresh <name>",
256
+ describe: "Force re-fetch and update the cache",
257
+ builder: (y) => y.positional("name", { type: "string", describe: "Registry name" }),
258
+ handler: () => registryMutate("refresh", rest),
259
+ },
260
+ {
261
+ command: "auth <name>",
262
+ describe: "Re-authenticate or revoke an OAuth MCP spec",
263
+ builder: (y) =>
264
+ y
265
+ .positional("name", { type: "string", describe: "Registry name" })
266
+ .option("revoke", { type: "boolean", describe: "Clear the stored OAuth token" }),
267
+ handler: () => authCmd(rest),
268
+ },
269
+ {
270
+ command: ["config [action] [key] [value]", "cfg [action] [key] [value]"],
271
+ describe: "Get, set, or unset persisted config",
272
+ builder: (y) =>
273
+ y
274
+ .positional("action", { type: "string", choices: ["get", "show", "set", "unset"] })
275
+ .positional("key", { type: "string" })
276
+ .positional("value", { type: "string" }),
277
+ handler: () => configCmd(rest),
278
+ },
279
+ {
280
+ command: "validate <source>",
281
+ describe: "Check an OpenAPI spec for errors",
282
+ builder: (y) => y.positional("source", { type: "string", describe: "OpenAPI file or URL" }),
283
+ handler: () => validateSpec(rest),
284
+ },
285
+ {
286
+ command: "skill [sub]",
287
+ describe: "Manage the bundled agent skill",
288
+ builder: (y) =>
289
+ y
290
+ .positional("sub", { type: "string", choices: ["install", "path"] })
291
+ .option("install", { type: "boolean" })
292
+ .option("path", { type: "boolean" }),
293
+ handler: () => skillCmd(rest),
294
+ },
295
+ ];
296
+
297
+ function isHelpRequest(args) {
298
+ return !args[0] || args[0] === "help" || args.includes("--help") || args.includes("-h");
299
+ }
122
300
 
123
- if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
301
+ export async function run(argv) {
302
+ loadDotenv();
303
+
304
+ const args = [];
305
+ for (let i = 0; i < argv.length; i++) {
306
+ if (argv[i] === "--format" && i + 1 < argv.length) setFormat(argv[++i]);
307
+ else if (argv[i].startsWith("--format=")) setFormat(argv[i].slice(9));
308
+ else args.push(argv[i]);
309
+ }
310
+
311
+ if (isHelpRequest(args)) {
124
312
  out({ help: HELP });
125
313
  return;
126
314
  }
127
315
 
316
+ const rest = args.slice(1);
317
+ const validationArgs = args.filter((arg) => arg !== "-");
318
+ const cli = yargs(validationArgs)
319
+ .scriptName("spec")
320
+ .help(false)
321
+ .version(false)
322
+ .strict()
323
+ .demandCommand(1, "No command given. Run 'spec help' for usage.")
324
+ .fail((msg, error) => {
325
+ err(error?.message || msg);
326
+ process.exit(1);
327
+ })
328
+ .exitProcess(false);
329
+
330
+ for (const command of commands(rest)) cli.command(command);
331
+
128
332
  try {
129
- switch (cmd) {
130
- case "list":
131
- case "ls":
132
- await listOperations(args.slice(1));
133
- break;
134
- case "show":
135
- await showOperation(args.slice(1));
136
- break;
137
- case "call":
138
- await callOperation(args.slice(1));
139
- break;
140
- case "validate":
141
- await validateSpec(args.slice(1));
142
- break;
143
- case "types":
144
- case "type":
145
- await typesCmd(args.slice(1));
146
- break;
147
- case "config":
148
- case "cfg":
149
- await configCmd(args.slice(1));
150
- break;
151
- case "add":
152
- await addCmd(args.slice(1));
153
- break;
154
- case "specs":
155
- case "registry":
156
- await specsCmd(args.slice(1));
157
- break;
158
- case "remove":
159
- await registryMutate("remove", args.slice(1));
160
- break;
161
- case "enable":
162
- await registryMutate("enable", args.slice(1));
163
- break;
164
- case "disable":
165
- await registryMutate("disable", args.slice(1));
166
- break;
167
- case "refresh":
168
- await registryMutate("refresh", args.slice(1));
169
- break;
170
- case "grep":
171
- await grepCmd(args.slice(1));
172
- break;
173
- case "auth":
174
- await authCmd(args.slice(1));
175
- break;
176
- default:
177
- err(`Unknown command: ${cmd}. Run 'spec help' for usage.`);
178
- }
333
+ await cli.parseAsync();
179
334
  } catch (e) {
180
335
  err(e.message);
181
336
  process.exit(1);
@@ -1,5 +1,5 @@
1
1
  import { parseArgs, parseKV } from "../args.js";
2
- import { getRegistry, saveRegistry } from "../registry.js";
2
+ import { getRegistry, saveRegistry, removeCachedSpec } from "../registry.js";
3
3
  import { out } from "../output.js";
4
4
  import { saveTokenFile } from "../oauth/tokens.js";
5
5
  import { runOAuthFlow } from "../oauth/auth-flow.js";
@@ -17,10 +17,11 @@ export async function addCmd(args) {
17
17
 
18
18
  const registry = getRegistry();
19
19
 
20
- // Check for name collision across all sections
21
- for (const section of ["mcp", "openapi", "graphql"]) {
22
- if (registry[section]?.[name]) {
23
- throw new Error(`Spec '${name}' already exists. Run 'spec remove ${name}' first.`);
20
+ let overwritten = false;
21
+ for (const existingSection of ["mcp", "openapi", "graphql"]) {
22
+ if (registry[existingSection]?.[name]) {
23
+ delete registry[existingSection][name];
24
+ overwritten = true;
24
25
  }
25
26
  }
26
27
 
@@ -127,6 +128,7 @@ export async function addCmd(args) {
127
128
 
128
129
  registry[section][name] = entry;
129
130
  saveRegistry(registry);
131
+ if (overwritten) removeCachedSpec(name);
130
132
 
131
133
  // Store client secret in token file (not registry) — it's sensitive
132
134
  if (oauthClientSecret) {
@@ -142,7 +144,7 @@ export async function addCmd(args) {
142
144
  await probeAndAuth({ ...entry, name, _section: "mcp" });
143
145
  }
144
146
 
145
- out({ ok: true, name, section, type: entry.type });
147
+ out({ ok: true, name, section, type: entry.type, ...(overwritten ? { overwritten: true } : {}) });
146
148
  }
147
149
 
148
150
  async function probeAndAuth(entry) {
@@ -1,32 +1,32 @@
1
- import { parseArgs } from "../args.js";
2
- import { getEntry } from "../registry.js";
3
- import { clearTokenFile } from "../oauth/tokens.js";
4
- import { runOAuthFlow } from "../oauth/auth-flow.js";
5
- import { out } from "../output.js";
6
-
7
- export async function authCmd(args) {
8
- const { positional, flags } = parseArgs(args);
9
- const name = positional[0];
10
- if (!name) throw new Error("Usage: spec auth <name> [--revoke]");
11
-
12
- const entry = getEntry(name);
13
-
14
- if (entry._section !== "mcp" || (entry.type !== "http" && entry.type !== "sse")) {
15
- throw new Error(
16
- `'${name}' is not an HTTP/SSE MCP spec — OAuth only applies to mcp http and sse entries`
17
- );
18
- }
19
-
20
- if ("revoke" in flags) {
21
- // revokeAll wipes everything including clientSecret
22
- clearTokenFile(name, { revokeAll: true });
23
- out({ ok: true, name, revoked: true });
24
- return;
25
- }
26
-
27
- // Clear session tokens but preserve clientSecret so client credentials flow still works
28
- clearTokenFile(name);
29
-
30
- const { flow } = await runOAuthFlow(name, entry);
31
- out({ ok: true, name, flow });
32
- }
1
+ import { parseArgs } from "../args.js";
2
+ import { getEntry } from "../registry.js";
3
+ import { clearTokenFile } from "../oauth/tokens.js";
4
+ import { runOAuthFlow } from "../oauth/auth-flow.js";
5
+ import { out } from "../output.js";
6
+
7
+ export async function authCmd(args) {
8
+ const { positional, flags } = parseArgs(args);
9
+ const name = positional[0];
10
+ if (!name) throw new Error("Usage: spec auth <name> [--revoke]");
11
+
12
+ const entry = getEntry(name);
13
+
14
+ if (entry._section !== "mcp" || (entry.type !== "http" && entry.type !== "sse")) {
15
+ throw new Error(
16
+ `'${name}' is not an HTTP/SSE MCP spec — OAuth only applies to mcp http and sse entries`
17
+ );
18
+ }
19
+
20
+ if ("revoke" in flags) {
21
+ // revokeAll wipes everything including clientSecret
22
+ clearTokenFile(name, { revokeAll: true });
23
+ out({ ok: true, name, revoked: true });
24
+ return;
25
+ }
26
+
27
+ // Clear session tokens but preserve clientSecret so client credentials flow still works
28
+ clearTokenFile(name);
29
+
30
+ const { flow } = await runOAuthFlow(name, entry);
31
+ out({ ok: true, name, flow });
32
+ }
@@ -3,6 +3,7 @@ import { out } from "../output.js";
3
3
  import { parseArgs, parseKV } from "../args.js";
4
4
  import { createMcpClient } from "../mcp-client.js";
5
5
  import { resolveSpec, resolveConfig } from "../resolve.js";
6
+ import { recordUsage } from "../usage.js";
6
7
 
7
8
  const HTTP_TIMEOUT = parseInt(process.env.SPEC_HTTP_TIMEOUT ?? "30000");
8
9
 
@@ -60,6 +61,7 @@ async function callMCP(spec, entry, target, flags) {
60
61
  // Normalize MCP result: expose isError and content at the top level
61
62
  const isError = result.isError === true;
62
63
  out({ tool: tool.name, arguments: toolArgs, isError, content: result.content, result });
64
+ recordUsage(flags.spec, tool.name);
63
65
  if (isError) process.exit(1);
64
66
  } finally {
65
67
  await client.close();
@@ -122,6 +124,7 @@ async function callOpenAPI(spec, config, target, flags) {
122
124
  headers: Object.fromEntries(res.headers.entries()),
123
125
  body: responseBody,
124
126
  });
127
+ recordUsage(flags.spec, op.id);
125
128
  }
126
129
 
127
130
  async function callGraphQL(spec, config, target, flags) {
@@ -177,6 +180,7 @@ async function callGraphQL(spec, config, target, flags) {
177
180
  data: responseBody?.data || null,
178
181
  errors: responseBody?.errors || null,
179
182
  });
183
+ recordUsage(flags.spec, op.name);
180
184
  }
181
185
 
182
186
  function buildGraphQLQuery(op, types) {