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 +48 -1
- package/package.json +5 -3
- package/src/cli.js +222 -67
- package/src/commands/add.js +8 -6
- package/src/commands/auth.js +32 -32
- package/src/commands/call.js +4 -0
- package/src/commands/fetch.js +344 -344
- package/src/commands/grep.js +67 -67
- package/src/commands/list.js +13 -2
- package/src/commands/show.js +224 -224
- package/src/commands/skill.js +40 -0
- package/src/commands/specs.js +82 -82
- package/src/commands/types.js +167 -167
- package/src/commands/usage.js +15 -0
- package/src/commands/validate.js +295 -295
- package/src/dotenv.js +38 -0
- package/src/glob.js +34 -34
- package/src/mcp-client.js +23 -20
- package/src/oauth/auth-flow.js +2 -2
- package/src/oauth/provider.js +3 -4
- package/src/oauth/tokens.js +6 -0
- package/src/output.js +65 -61
- package/src/registry.js +79 -79
- package/src/resolve.js +21 -19
- package/src/secrets.js +46 -0
- package/src/skill/SKILL.md +112 -0
- package/src/usage.js +62 -0
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.
|
|
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.
|
|
48
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/commands/add.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
for (const
|
|
22
|
-
if (registry[
|
|
23
|
-
|
|
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) {
|
package/src/commands/auth.js
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/call.js
CHANGED
|
@@ -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) {
|