api-spec-cli 0.1.3 → 0.2.1
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 +129 -73
- package/package.json +1 -1
- package/src/args.js +1 -1
- package/src/cli.js +14 -0
- package/src/commands/add.js +9 -0
- package/src/commands/call.js +14 -0
- package/src/commands/grep.js +63 -0
- package/src/commands/load.js +28 -5
- package/src/mcp-client.js +33 -2
package/README.md
CHANGED
|
@@ -17,47 +17,67 @@ npx api-spec-cli <command>
|
|
|
17
17
|
|
|
18
18
|
## How It Works
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Every command is stateless — you specify the spec source on each call. Two paths:
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
| Path | When to use |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `--spec <name>` | Registered spec — auto-fetches and caches on first use |
|
|
25
|
+
| Inline flags | Ad-hoc — no registration, fetched each call |
|
|
26
|
+
|
|
27
|
+
### Register once, use everywhere
|
|
23
28
|
|
|
24
29
|
```bash
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
spec add petstore --openapi https://petstore3.swagger.io/api/v3/openapi.json \
|
|
31
|
+
--base-url https://petstore3.swagger.io/api/v3 \
|
|
32
|
+
--description "Petstore example"
|
|
33
|
+
|
|
34
|
+
spec add hashnode --graphql https://gql.hashnode.com --auth YOUR_TOKEN
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
spec add agno --mcp-http https://docs.agno.com/mcp --description "Agno docs"
|
|
37
|
+
|
|
38
|
+
spec add fs --mcp-stdio "npx -y @modelcontextprotocol/server-filesystem /tmp"
|
|
39
|
+
```
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
spec load --mcp-stdio "npx -y @modelcontextprotocol/server-filesystem /tmp"
|
|
41
|
+
Registration is instant — does not connect. Connection happens on first `list`/`show`/`call` and the result is cached at `~/spec-cli-config/cache/<name>.json`.
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
spec load --mcp-sse http://localhost:3000/sse
|
|
43
|
+
### Or use inline (no registration)
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
spec
|
|
45
|
+
```bash
|
|
46
|
+
spec list --openapi https://petstore3.swagger.io/api/v3/openapi.json
|
|
47
|
+
spec list --graphql https://gql.hashnode.com
|
|
48
|
+
spec list --mcp-http https://docs.agno.com/mcp
|
|
49
|
+
spec list --mcp-sse http://localhost:3000/sse
|
|
50
|
+
spec list --mcp-stdio "npx -y @modelcontextprotocol/server-filesystem /tmp"
|
|
40
51
|
```
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
Inline fetches every call, nothing cached.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Discovery
|
|
58
|
+
|
|
59
|
+
### List all specs in the registry
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
spec specs # Compact: name, type, enabled
|
|
63
|
+
spec specs --compact false # Full: includes source, config
|
|
45
64
|
```
|
|
46
65
|
|
|
47
|
-
###
|
|
66
|
+
### List operations / tools
|
|
48
67
|
|
|
49
68
|
`list` is compact by default — just IDs, no schemas. Use `--filter`, `--tag`, `--limit` to narrow down.
|
|
50
69
|
|
|
51
70
|
```bash
|
|
52
|
-
spec list #
|
|
53
|
-
spec list --filter
|
|
54
|
-
spec list --tag pets
|
|
55
|
-
spec list --tag mutation
|
|
56
|
-
spec list --limit 10
|
|
57
|
-
spec list --limit 10 --offset 10
|
|
71
|
+
spec list --spec agno # Registered spec (uses cache)
|
|
72
|
+
spec list --spec petstore --filter pet # Search by keyword
|
|
73
|
+
spec list --spec petstore --tag pets # OpenAPI: filter by tag
|
|
74
|
+
spec list --spec hashnode --tag mutation # GraphQL: filter by kind
|
|
75
|
+
spec list --spec petstore --limit 10 # First 10 only
|
|
76
|
+
spec list --spec petstore --limit 10 --offset 10 # Next 10
|
|
77
|
+
spec list --mcp-http https://docs.agno.com/mcp # Inline: no registration needed
|
|
58
78
|
```
|
|
59
79
|
|
|
60
|
-
Compact output
|
|
80
|
+
Compact output:
|
|
61
81
|
```json
|
|
62
82
|
{
|
|
63
83
|
"type": "mcp",
|
|
@@ -69,99 +89,135 @@ Compact output (token-efficient):
|
|
|
69
89
|
}
|
|
70
90
|
```
|
|
71
91
|
|
|
72
|
-
Use `--compact false` for full details
|
|
92
|
+
Use `--compact false` for full details including `inputSchema` for MCP tools.
|
|
73
93
|
|
|
74
|
-
###
|
|
94
|
+
### Inspect one operation or tool
|
|
75
95
|
|
|
76
|
-
`show` gives you everything
|
|
96
|
+
`show` gives you everything to make a call — params, body schema, response schemas, related types — in one call.
|
|
77
97
|
|
|
78
98
|
```bash
|
|
79
|
-
spec show
|
|
80
|
-
spec show
|
|
81
|
-
spec show /pet/{petId}
|
|
82
|
-
spec show
|
|
83
|
-
spec show search_agno
|
|
99
|
+
spec show --spec petstore getPetById # OpenAPI: by operationId
|
|
100
|
+
spec show --spec petstore /pet/{petId} # OpenAPI: by path
|
|
101
|
+
spec show --spec petstore "GET /pet/{petId}" # OpenAPI: by method + path
|
|
102
|
+
spec show --spec hashnode publishPost # GraphQL: by operation name
|
|
103
|
+
spec show --spec agno search_agno # MCP: by tool name
|
|
84
104
|
```
|
|
85
105
|
|
|
86
106
|
MCP output includes the full `inputSchema` so you know exactly what arguments to pass.
|
|
87
107
|
|
|
88
|
-
###
|
|
108
|
+
### Drill into types (OpenAPI/GraphQL only)
|
|
89
109
|
|
|
90
110
|
```bash
|
|
91
|
-
spec types
|
|
92
|
-
spec types Pet
|
|
93
|
-
spec types PublishPostInput
|
|
111
|
+
spec types --spec petstore # List all schema names
|
|
112
|
+
spec types --spec petstore Pet # Inspect one schema
|
|
113
|
+
spec types --spec hashnode PublishPostInput # GraphQL input type
|
|
94
114
|
```
|
|
95
115
|
|
|
96
|
-
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Calling APIs
|
|
97
119
|
|
|
98
120
|
```bash
|
|
99
|
-
#
|
|
100
|
-
spec
|
|
101
|
-
spec
|
|
121
|
+
# OpenAPI
|
|
122
|
+
spec call --spec petstore getPetById --var petId=1
|
|
123
|
+
spec call --spec petstore findPetsByStatus --query status=available
|
|
124
|
+
spec call --spec petstore addPet --data '{"name":"Rex","photoUrls":[]}'
|
|
102
125
|
|
|
103
|
-
#
|
|
104
|
-
spec call
|
|
105
|
-
spec call
|
|
106
|
-
spec call addPet --data '{"name":"Rex","photoUrls":[]}'
|
|
126
|
+
# GraphQL (auto-generates query from schema)
|
|
127
|
+
spec call --spec hashnode me
|
|
128
|
+
spec call --spec hashnode publication --var host=blog.hashnode.dev
|
|
107
129
|
|
|
108
|
-
#
|
|
109
|
-
spec call
|
|
110
|
-
spec call
|
|
130
|
+
# MCP
|
|
131
|
+
spec call --spec agno search_agno --var query="how to create an agent"
|
|
132
|
+
spec call --spec agno search_agno --data '{"query":"agents"}'
|
|
111
133
|
|
|
112
|
-
#
|
|
113
|
-
spec call
|
|
114
|
-
|
|
134
|
+
# Inline (no registration)
|
|
135
|
+
spec call --openapi https://petstore3.swagger.io/api/v3/openapi.json \
|
|
136
|
+
getPetById --var petId=1 --base-url https://petstore3.swagger.io/api/v3
|
|
115
137
|
```
|
|
116
138
|
|
|
139
|
+
### Per-call overrides
|
|
140
|
+
|
|
141
|
+
Flags passed at call time win over registry entry config, which wins over `.spec-cli/config.json`.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
spec call --spec agno search_agno --var query="foo" --header X-Tenant=acme
|
|
145
|
+
spec call --spec petstore getPetById --var petId=1 --auth staging-token
|
|
146
|
+
spec list --spec petstore --base-url https://staging.api.example.com
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Registry Management
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
spec remove <name> # Delete entry and remove cache
|
|
155
|
+
spec enable <name> # Re-enable a disabled spec
|
|
156
|
+
spec disable <name> # Disable without removing
|
|
157
|
+
spec refresh <name> # Force re-fetch and update cache
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## spec add options
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
spec add <name> --openapi <url-or-file> [--base-url <url>] [--auth <token>] [--header k=v]
|
|
166
|
+
spec add <name> --graphql <url> [--auth <token>] [--header k=v]
|
|
167
|
+
spec add <name> --mcp-http <url> [--header k=v]
|
|
168
|
+
spec add <name> --mcp-sse <url> [--header k=v]
|
|
169
|
+
spec add <name> --mcp-stdio "<cmd args>" [--env KEY=VAL]
|
|
170
|
+
[--description <text>] (all types)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Headers are sent on every request. For stdio MCP, use `--env` to pass environment variables to the subprocess.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
117
177
|
## Config
|
|
118
178
|
|
|
119
|
-
Persistent config stored in `.spec-cli/config.json
|
|
179
|
+
Persistent config stored in `.spec-cli/config.json` (lowest priority — overridden by registry entry config and call-time flags).
|
|
120
180
|
|
|
121
181
|
```bash
|
|
122
182
|
spec config set baseUrl https://api.example.com
|
|
123
183
|
spec config set auth my-token # Auto-adds "Bearer " prefix
|
|
124
|
-
spec config set auth "Basic dXNlcjpwYXNz" # Or explicit
|
|
125
|
-
spec config set headers.X-API-Key abc123 # Custom
|
|
126
|
-
spec config get
|
|
127
|
-
spec config unset auth
|
|
184
|
+
spec config set auth "Basic dXNlcjpwYXNz" # Or explicit scheme
|
|
185
|
+
spec config set headers.X-API-Key abc123 # Custom header (dot notation)
|
|
186
|
+
spec config get
|
|
187
|
+
spec config unset auth
|
|
128
188
|
```
|
|
129
189
|
|
|
130
190
|
## Validate
|
|
131
191
|
|
|
132
|
-
Check an OpenAPI spec for errors before using it:
|
|
133
|
-
|
|
134
192
|
```bash
|
|
135
193
|
spec validate https://api.example.com/openapi.json
|
|
194
|
+
spec validate ./openapi.yaml
|
|
136
195
|
```
|
|
137
196
|
|
|
138
197
|
Reports broken `$ref` references, missing required fields, duplicate operationIds, invalid schema types, and more.
|
|
139
198
|
|
|
140
199
|
## Output Format
|
|
141
200
|
|
|
142
|
-
JSON by default.
|
|
201
|
+
JSON by default. Errors go to stderr as `{"error": "message"}` with a non-zero exit code.
|
|
143
202
|
|
|
144
203
|
```bash
|
|
145
|
-
spec list --format text
|
|
146
|
-
spec show getPetById --format yaml
|
|
204
|
+
spec list --spec petstore --format text
|
|
205
|
+
spec show --spec petstore getPetById --format yaml
|
|
206
|
+
spec list --spec petstore --format=json # equals syntax also works
|
|
147
207
|
```
|
|
148
208
|
|
|
149
|
-
Errors always go to stderr as JSON: `{"error": "message"}` with non-zero exit code.
|
|
150
|
-
|
|
151
209
|
## Token Efficiency
|
|
152
210
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
- `
|
|
156
|
-
- `
|
|
157
|
-
- `types` lets you inspect one type at a time instead of loading all schemas
|
|
158
|
-
- `--limit` and `--offset` paginate large APIs
|
|
211
|
+
- `list` returns only IDs by default — no schemas
|
|
212
|
+
- `show` resolves `$ref` compactly — nested refs show as names, not explosions
|
|
213
|
+
- `types` lets you inspect one schema at a time
|
|
214
|
+
- `--limit` / `--offset` paginate large APIs
|
|
159
215
|
- `--filter` and `--tag` narrow results before output
|
|
160
216
|
|
|
161
217
|
## Storage
|
|
162
218
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
-
|
|
166
|
-
|
|
167
|
-
|
|
219
|
+
| Path | Purpose |
|
|
220
|
+
|---|---|
|
|
221
|
+
| `~/spec-cli-config/registry.json` | Global named registry |
|
|
222
|
+
| `~/spec-cli-config/cache/<name>.json` | Cached spec per registered entry |
|
|
223
|
+
| `.spec-cli/config.json` | Project-local config (baseUrl, auth, headers) |
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Supports: --flag value, --flag=value, and positional args
|
|
3
3
|
// Repeatable flags (--query, --header, --var) are collected into arrays.
|
|
4
4
|
|
|
5
|
-
const REPEATABLE = new Set(["query", "header", "var", "env"]);
|
|
5
|
+
const REPEATABLE = new Set(["query", "header", "var", "env", "allow-tool", "disable-tool"]);
|
|
6
6
|
|
|
7
7
|
export function parseArgs(args) {
|
|
8
8
|
const flags = {};
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { validateSpec } from "./commands/validate.js";
|
|
|
6
6
|
import { typesCmd } from "./commands/types.js";
|
|
7
7
|
import { addCmd } from "./commands/add.js";
|
|
8
8
|
import { specsCmd, registryMutate } from "./commands/specs.js";
|
|
9
|
+
import { grepCmd } from "./commands/grep.js";
|
|
9
10
|
import { out, err, setFormat } from "./output.js";
|
|
10
11
|
|
|
11
12
|
const HELP = `spec-cli — Explore and call APIs from the command line.
|
|
@@ -29,6 +30,9 @@ REGISTRY (register once, use anywhere):
|
|
|
29
30
|
spec add <name> --mcp-stdio "<cmd>" Register an MCP server (stdio)
|
|
30
31
|
Options: --description <text> --base-url <url> --auth <token>
|
|
31
32
|
--header k=v (repeatable) --env KEY=VAL (repeatable, stdio only)
|
|
33
|
+
--cwd <path> (stdio only)
|
|
34
|
+
--allow-tool <glob> (repeatable, MCP only)
|
|
35
|
+
--disable-tool <glob> (repeatable, MCP only)
|
|
32
36
|
|
|
33
37
|
spec specs List all registered specs
|
|
34
38
|
spec specs --compact false Show full entry config
|
|
@@ -43,6 +47,8 @@ DISCOVER:
|
|
|
43
47
|
spec list --spec <name> --tag pets OpenAPI tag or GraphQL kind
|
|
44
48
|
spec list --spec <name> --limit 10 Paginate
|
|
45
49
|
spec list --mcp-http <url> Inline: no registration needed
|
|
50
|
+
spec grep <pattern> Search across all registered specs
|
|
51
|
+
spec grep <pattern> --spec <name> Search within one spec
|
|
46
52
|
|
|
47
53
|
INSPECT:
|
|
48
54
|
spec show --spec <name> <op> Operation details (params, body, responses)
|
|
@@ -55,6 +61,7 @@ CALL:
|
|
|
55
61
|
spec call --spec <name> <op> --query status=available Query params
|
|
56
62
|
spec call --spec <name> <op> --data '{"name":"Rex"}' JSON body / MCP args
|
|
57
63
|
spec call --spec <name> <op> --data-file args.json Body from file
|
|
64
|
+
echo '{"query":"foo"}' | spec call --spec <name> <op> Pipe JSON from stdin
|
|
58
65
|
spec call --spec <name> <op> --header X-Custom=val Extra headers
|
|
59
66
|
spec call --spec <name> <op> --method PUT Override HTTP method
|
|
60
67
|
|
|
@@ -74,6 +81,10 @@ OTHER:
|
|
|
74
81
|
spec validate <file-or-url> Check OpenAPI spec for errors
|
|
75
82
|
--format json|text|yaml Output format (default: json)
|
|
76
83
|
|
|
84
|
+
ENV VARS (MCP):
|
|
85
|
+
MCP_MAX_RETRIES=3 Retry attempts on connection failure (default: 3)
|
|
86
|
+
MCP_RETRY_DELAY=1000 Base retry delay in ms, doubles each attempt (default: 1000)
|
|
87
|
+
|
|
77
88
|
EXAMPLES:
|
|
78
89
|
spec add agno --mcp-http https://docs.agno.com/mcp --description "Agno docs"
|
|
79
90
|
spec add petstore --openapi https://petstore3.swagger.io/api/v3/openapi.json \\
|
|
@@ -148,6 +159,9 @@ export async function run(args) {
|
|
|
148
159
|
case "refresh":
|
|
149
160
|
await registryMutate("refresh", args.slice(1));
|
|
150
161
|
break;
|
|
162
|
+
case "grep":
|
|
163
|
+
await grepCmd(args.slice(1));
|
|
164
|
+
break;
|
|
151
165
|
default:
|
|
152
166
|
err(`Unknown command: ${cmd}. Run 'spec help' for usage.`);
|
|
153
167
|
}
|
package/src/commands/add.js
CHANGED
|
@@ -44,6 +44,7 @@ export async function addCmd(args) {
|
|
|
44
44
|
entry.transport = "stdio";
|
|
45
45
|
entry.command = parts[0];
|
|
46
46
|
entry.args = parts.slice(1);
|
|
47
|
+
if (flags.cwd) entry.cwd = flags.cwd;
|
|
47
48
|
entry.config = { env: parseKV(flags.env) };
|
|
48
49
|
} else if (flags["mcp-sse"]) {
|
|
49
50
|
entry.type = "mcp";
|
|
@@ -61,6 +62,14 @@ export async function addCmd(args) {
|
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// Tool filtering (MCP only)
|
|
66
|
+
if (entry.type === "mcp") {
|
|
67
|
+
const allowed = flags["allow-tool"];
|
|
68
|
+
const disabled = flags["disable-tool"];
|
|
69
|
+
if (allowed?.length) entry.config.allowedTools = allowed;
|
|
70
|
+
if (disabled?.length) entry.config.disabledTools = disabled;
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
registry.push(entry);
|
|
65
74
|
saveRegistry(registry);
|
|
66
75
|
out({ ok: true, name, type: entry.type, transport: entry.transport });
|
package/src/commands/call.js
CHANGED
|
@@ -15,6 +15,20 @@ export async function callOperation(args) {
|
|
|
15
15
|
flags.data = readFileSync(flags["data-file"], "utf-8").trim();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Read from stdin when piped and no --data/--data-file provided.
|
|
19
|
+
// isTTY is true in a terminal, undefined when piped — so !isTTY catches piped input.
|
|
20
|
+
// Wrapped in try-catch so test runners with closed stdin don't crash.
|
|
21
|
+
if (!flags.data && !process.stdin.isTTY) {
|
|
22
|
+
try {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
25
|
+
const piped = Buffer.concat(chunks).toString("utf-8").trim();
|
|
26
|
+
if (piped) flags.data = piped;
|
|
27
|
+
} catch {
|
|
28
|
+
// stdin unavailable (test runner, closed pipe) — ignore
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
const { spec, entry } = await resolveActiveSpec(flags);
|
|
19
33
|
const config = resolveConfig(flags, entry);
|
|
20
34
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { out } from "../output.js";
|
|
2
|
+
import { parseArgs } from "../args.js";
|
|
3
|
+
import { getRegistry, getEntry, getCachedSpec, saveCachedSpec } from "../registry.js";
|
|
4
|
+
import { resolveSpec, matchGlob } from "./load.js";
|
|
5
|
+
|
|
6
|
+
export async function grepCmd(args) {
|
|
7
|
+
const { flags, positional } = parseArgs(args);
|
|
8
|
+
const pattern = positional[0];
|
|
9
|
+
if (!pattern) throw new Error(
|
|
10
|
+
"Usage: spec grep <pattern> [--spec <name>]\n" +
|
|
11
|
+
" Glob patterns: * matches anything, ? matches one char\n" +
|
|
12
|
+
" Plain text: substring match across name and description"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const entries = flags.spec
|
|
16
|
+
? [getEntry(flags.spec)]
|
|
17
|
+
: getRegistry().filter((e) => e.enabled);
|
|
18
|
+
|
|
19
|
+
if (entries.length === 0) throw new Error("No registered specs. Run 'spec add' first.");
|
|
20
|
+
|
|
21
|
+
const results = [];
|
|
22
|
+
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
let spec = getCachedSpec(entry.name);
|
|
25
|
+
if (!spec) {
|
|
26
|
+
spec = await resolveSpec(entry);
|
|
27
|
+
saveCachedSpec(entry.name, spec);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const matches = [];
|
|
31
|
+
|
|
32
|
+
if (spec.type === "mcp") {
|
|
33
|
+
for (const tool of spec.tools) {
|
|
34
|
+
const nameMatch = matchGlob(pattern, tool.name);
|
|
35
|
+
const descMatch = tool.description && matchGlob(pattern, tool.description);
|
|
36
|
+
if (nameMatch || descMatch) {
|
|
37
|
+
matches.push({ id: tool.name, description: tool.description });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else if (spec.type === "openapi") {
|
|
41
|
+
for (const op of spec.operations) {
|
|
42
|
+
if (matchGlob(pattern, op.id) || matchGlob(pattern, op.path) ||
|
|
43
|
+
(op.summary && matchGlob(pattern, op.summary))) {
|
|
44
|
+
matches.push({ id: op.id, method: op.method, path: op.path });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else if (spec.type === "graphql") {
|
|
48
|
+
for (const op of spec.operations) {
|
|
49
|
+
if (matchGlob(pattern, op.name) ||
|
|
50
|
+
(op.description && matchGlob(pattern, op.description))) {
|
|
51
|
+
matches.push({ id: op.name, kind: op.kind });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (matches.length > 0) {
|
|
57
|
+
results.push({ spec: entry.name, type: spec.type, matches });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const total = results.reduce((s, r) => s + r.matches.length, 0);
|
|
62
|
+
out({ pattern, total, results });
|
|
63
|
+
}
|
package/src/commands/load.js
CHANGED
|
@@ -88,6 +88,7 @@ export function inlineEntryFromFlags(flags) {
|
|
|
88
88
|
transport: "stdio",
|
|
89
89
|
command: parts[0],
|
|
90
90
|
args: parts.slice(1),
|
|
91
|
+
cwd: flags.cwd,
|
|
91
92
|
config: { env: parseKV(flags.env) },
|
|
92
93
|
};
|
|
93
94
|
}
|
|
@@ -118,10 +119,35 @@ export function inlineEntryFromFlags(flags) {
|
|
|
118
119
|
|
|
119
120
|
// --- Internal loaders ---
|
|
120
121
|
|
|
122
|
+
function matchGlob(pattern, str) {
|
|
123
|
+
// If no glob chars, use case-insensitive substring match
|
|
124
|
+
if (!pattern.includes("*") && !pattern.includes("?")) {
|
|
125
|
+
return str.toLowerCase().includes(pattern.toLowerCase());
|
|
126
|
+
}
|
|
127
|
+
const re = new RegExp(
|
|
128
|
+
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$",
|
|
129
|
+
"i"
|
|
130
|
+
);
|
|
131
|
+
return re.test(str);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { matchGlob };
|
|
135
|
+
|
|
121
136
|
async function loadMCPFromEntry(entry) {
|
|
122
137
|
const client = await createMcpClient(entry);
|
|
123
138
|
try {
|
|
124
139
|
const { tools } = await client.listTools();
|
|
140
|
+
let mapped = tools.map((t) => ({
|
|
141
|
+
name: t.name,
|
|
142
|
+
description: t.description || null,
|
|
143
|
+
inputSchema: t.inputSchema || null,
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const allowed = entry.config?.allowedTools;
|
|
147
|
+
const disabled = entry.config?.disabledTools;
|
|
148
|
+
if (allowed?.length) mapped = mapped.filter((t) => allowed.some((p) => matchGlob(p, t.name)));
|
|
149
|
+
if (disabled?.length) mapped = mapped.filter((t) => !disabled.some((p) => matchGlob(p, t.name)));
|
|
150
|
+
|
|
125
151
|
return {
|
|
126
152
|
type: "mcp",
|
|
127
153
|
title: entry.name || "MCP Server",
|
|
@@ -129,12 +155,9 @@ async function loadMCPFromEntry(entry) {
|
|
|
129
155
|
url: entry.url,
|
|
130
156
|
command: entry.command,
|
|
131
157
|
args: entry.args,
|
|
158
|
+
cwd: entry.cwd,
|
|
132
159
|
config: entry.config,
|
|
133
|
-
tools:
|
|
134
|
-
name: t.name,
|
|
135
|
-
description: t.description || null,
|
|
136
|
-
inputSchema: t.inputSchema || null,
|
|
137
|
-
})),
|
|
160
|
+
tools: mapped,
|
|
138
161
|
};
|
|
139
162
|
} finally {
|
|
140
163
|
await client.close();
|
package/src/mcp-client.js
CHANGED
|
@@ -3,15 +3,31 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
3
3
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
4
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const MAX_RETRIES = parseInt(process.env.MCP_MAX_RETRIES ?? "3");
|
|
7
|
+
const RETRY_DELAY = parseInt(process.env.MCP_RETRY_DELAY ?? "1000");
|
|
8
|
+
|
|
9
|
+
// Expand ${VAR} placeholders from process.env at call time
|
|
10
|
+
function expandEnv(val) {
|
|
11
|
+
return val.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
12
|
+
if (!(name in process.env)) throw new Error(`Environment variable not set: ${name}`);
|
|
13
|
+
return process.env[name];
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function connect(spec) {
|
|
7
18
|
const client = new Client({ name: "spec-cli", version: "1.0.0" });
|
|
8
19
|
|
|
9
20
|
let transport;
|
|
10
21
|
if (spec.transport === "stdio") {
|
|
22
|
+
const rawEnv = spec.config?.env || {};
|
|
23
|
+
const expandedEnv = Object.fromEntries(
|
|
24
|
+
Object.entries(rawEnv).map(([k, v]) => [k, expandEnv(v)])
|
|
25
|
+
);
|
|
11
26
|
transport = new StdioClientTransport({
|
|
12
27
|
command: spec.command,
|
|
13
28
|
args: spec.args,
|
|
14
|
-
env:
|
|
29
|
+
env: Object.keys(expandedEnv).length > 0 ? { ...process.env, ...expandedEnv } : undefined,
|
|
30
|
+
cwd: spec.cwd,
|
|
15
31
|
});
|
|
16
32
|
} else if (spec.transport === "sse") {
|
|
17
33
|
const h = spec.config?.headers;
|
|
@@ -30,3 +46,18 @@ export async function createMcpClient(spec) {
|
|
|
30
46
|
await client.connect(transport);
|
|
31
47
|
return client;
|
|
32
48
|
}
|
|
49
|
+
|
|
50
|
+
export async function createMcpClient(spec) {
|
|
51
|
+
let lastError;
|
|
52
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
return await connect(spec);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
lastError = e;
|
|
57
|
+
if (attempt < MAX_RETRIES) {
|
|
58
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY * Math.pow(2, attempt)));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw lastError;
|
|
63
|
+
}
|