api-spec-cli 0.0.4 → 0.1.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 +86 -130
- package/package.json +2 -1
- package/src/cli.js +58 -21
- package/src/commands/call.js +41 -3
- package/src/commands/list.js +60 -17
- package/src/commands/load.js +62 -2
- package/src/commands/show.js +120 -46
- package/src/commands/types.js +165 -0
- package/src/mcp-client.js +25 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# spec-cli
|
|
1
|
+
# api-spec-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI for AI agents to explore and call OpenAPI and GraphQL APIs. Output is JSON by default — compact, parseable, token-efficient.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,190 +8,146 @@ Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs. Designed
|
|
|
8
8
|
npm install -g api-spec-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Works with Node.js 18+ or Bun. No other dependencies.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
# Or run without installing
|
|
15
|
+
npx api-spec-cli <command>
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
## How It Works
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
npx api-spec-cli <command>
|
|
21
|
-
bunx api-spec-cli <command>
|
|
22
|
-
```
|
|
20
|
+
The CLI follows a progressive discovery pattern. You never dump an entire API spec at once — instead you narrow down to what you need.
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
### Step 1: Load the spec
|
|
25
23
|
|
|
26
24
|
```bash
|
|
27
|
-
|
|
28
|
-
spec load
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
spec load https://gql.hashnode.com
|
|
25
|
+
spec load https://petstore3.swagger.io/api/v3/openapi.json # OpenAPI
|
|
26
|
+
spec load ./openapi.yaml # Local file
|
|
27
|
+
spec load https://gql.hashnode.com # GraphQL (introspection)
|
|
28
|
+
```
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
Output tells you what was loaded:
|
|
31
|
+
```json
|
|
32
|
+
{ "ok": true, "type": "graphql", "operationCount": 114, "source": "https://gql.hashnode.com" }
|
|
33
|
+
```
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
spec show getPetById
|
|
39
|
-
spec show publishPost
|
|
35
|
+
### Step 2: Find what you need
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
spec config set baseUrl https://petstore3.swagger.io/api/v3
|
|
43
|
-
spec call findPetsByStatus --query status=available
|
|
37
|
+
`list` is compact by default — just operation IDs, no schemas. Use `--filter`, `--tag`, `--limit` to narrow down.
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
spec
|
|
47
|
-
spec
|
|
39
|
+
```bash
|
|
40
|
+
spec list # All operations (compact IDs only)
|
|
41
|
+
spec list --filter publish # Search by keyword
|
|
42
|
+
spec list --tag pets # OpenAPI: filter by tag
|
|
43
|
+
spec list --tag mutation # GraphQL: filter by kind (query/mutation/subscription)
|
|
44
|
+
spec list --limit 10 # First 10 only
|
|
45
|
+
spec list --limit 10 --offset 10 # Next 10
|
|
48
46
|
```
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
Compact output (token-efficient):
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"type": "graphql",
|
|
52
|
+
"total": 5,
|
|
53
|
+
"showing": 5,
|
|
54
|
+
"operations": [
|
|
55
|
+
{ "id": "publishPost", "kind": "mutation" },
|
|
56
|
+
{ "id": "publishDraft", "kind": "mutation" }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
Use `--compact false` for full details (summary, tags, args).
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
### Step 3: Inspect one operation
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
- **GraphQL**: Runs introspection query on the endpoint
|
|
65
|
+
`show` gives you everything you need to call an operation — params, body schema, response, and related types — in one call.
|
|
58
66
|
|
|
59
67
|
```bash
|
|
60
|
-
spec
|
|
61
|
-
spec
|
|
62
|
-
spec
|
|
68
|
+
spec show publishPost # GraphQL: by operation name
|
|
69
|
+
spec show getPetById # OpenAPI: by operationId
|
|
70
|
+
spec show /pet/{petId} # OpenAPI: by path
|
|
71
|
+
spec show "GET /pet/{petId}" # OpenAPI: by method + path
|
|
63
72
|
```
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
Schemas are compact. Nested `$ref` references show as type names (not exploded), so the output stays small. If you need details on a referenced type, use `spec types <name>`.
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
### Step 4: Drill into types (if needed)
|
|
68
77
|
|
|
69
78
|
```bash
|
|
70
|
-
spec
|
|
71
|
-
spec
|
|
79
|
+
spec types # List all schema/type names
|
|
80
|
+
spec types Pet # Inspect one schema
|
|
81
|
+
spec types PublishPostInput # Inspect a GraphQL input type
|
|
72
82
|
```
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
**GraphQL output**: `id`, `kind` (query/mutation/subscription), `description`, `args`, `returnType`
|
|
76
|
-
|
|
77
|
-
### `spec show <operation>`
|
|
84
|
+
This is optional — `show` already includes related types inline. Use `types` only when you need a type that wasn't included in the `show` output.
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
### Step 5: Call the API
|
|
80
87
|
|
|
81
88
|
```bash
|
|
82
|
-
|
|
83
|
-
spec
|
|
84
|
-
spec
|
|
85
|
-
spec show publishPost # GraphQL operation name
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
For OpenAPI: resolves `$ref` references in parameters, request body, and responses.
|
|
89
|
-
For GraphQL: includes related types with field definitions.
|
|
90
|
-
|
|
91
|
-
### `spec call <operation> [options]`
|
|
92
|
-
|
|
93
|
-
Execute an API request.
|
|
89
|
+
# Set base URL and auth first (persisted across calls)
|
|
90
|
+
spec config set baseUrl https://petstore3.swagger.io/api/v3
|
|
91
|
+
spec config set auth YOUR_TOKEN
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
# OpenAPI
|
|
97
|
-
spec call addPet --data '{"name":"Rex","photoUrls":[]}'
|
|
93
|
+
# OpenAPI calls
|
|
98
94
|
spec call getPetById --var petId=1
|
|
99
95
|
spec call findPetsByStatus --query status=available
|
|
100
|
-
spec call
|
|
96
|
+
spec call addPet --data '{"name":"Rex","photoUrls":[]}'
|
|
101
97
|
|
|
102
|
-
# GraphQL
|
|
98
|
+
# GraphQL calls (auto-generates query from schema)
|
|
103
99
|
spec call me
|
|
104
|
-
spec call publication --var host=blog.
|
|
105
|
-
spec call publishPost --data '{"query":"mutation { ... }"}'
|
|
100
|
+
spec call publication --var host=blog.hashnode.dev
|
|
106
101
|
```
|
|
107
102
|
|
|
108
|
-
|
|
109
|
-
| Flag | Description |
|
|
110
|
-
|------|-------------|
|
|
111
|
-
| `--data '{"key":"val"}'` | Request body (JSON) |
|
|
112
|
-
| `--query key=val` | Query parameter (repeatable) |
|
|
113
|
-
| `--header key=val` | Per-request header (repeatable) |
|
|
114
|
-
| `--var key=val` | Path variable or GraphQL variable (repeatable) |
|
|
115
|
-
| `--method GET\|POST\|...` | Override HTTP method |
|
|
103
|
+
## Config
|
|
116
104
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
### `spec validate <file-or-url>`
|
|
120
|
-
|
|
121
|
-
Validate an OpenAPI spec and report errors and warnings.
|
|
105
|
+
Persistent config stored in `.spec-cli/config.json`. Set once, used for all calls.
|
|
122
106
|
|
|
123
107
|
```bash
|
|
124
|
-
spec
|
|
125
|
-
spec
|
|
108
|
+
spec config set baseUrl https://api.example.com
|
|
109
|
+
spec config set auth my-token # Auto-adds "Bearer " prefix
|
|
110
|
+
spec config set auth "Basic dXNlcjpwYXNz" # Or explicit auth header
|
|
111
|
+
spec config set headers.X-API-Key abc123 # Custom headers (dot notation)
|
|
112
|
+
spec config get # Show all config
|
|
113
|
+
spec config unset auth # Remove a key
|
|
126
114
|
```
|
|
127
115
|
|
|
128
|
-
|
|
129
|
-
- Required fields (`info`, `info.title`, `info.version`, `paths`)
|
|
130
|
-
- Valid HTTP methods and schema types
|
|
131
|
-
- Unique `operationId` values
|
|
132
|
-
- Broken `$ref` references
|
|
133
|
-
- Array schemas have `items`
|
|
134
|
-
- Path parameters are declared
|
|
135
|
-
- Server/host definitions
|
|
136
|
-
- Unusual patterns (request body on GET)
|
|
116
|
+
## Validate
|
|
137
117
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Manage persistent configuration stored in `.spec-cli/config.json`.
|
|
118
|
+
Check an OpenAPI spec for errors before using it:
|
|
141
119
|
|
|
142
120
|
```bash
|
|
143
|
-
spec
|
|
144
|
-
spec config get baseUrl # show single key
|
|
145
|
-
spec config set baseUrl https://api.example.com
|
|
146
|
-
spec config set auth my-api-token # adds Bearer prefix
|
|
147
|
-
spec config set auth "Bearer my-token" # explicit Bearer
|
|
148
|
-
spec config set auth "Basic dXNlcjpwYXNz" # Basic auth
|
|
149
|
-
spec config set headers.X-API-Key abc123 # custom header (dot notation)
|
|
150
|
-
spec config unset headers.X-API-Key # remove a key
|
|
121
|
+
spec validate https://api.example.com/openapi.json
|
|
151
122
|
```
|
|
152
123
|
|
|
153
|
-
|
|
124
|
+
Reports broken `$ref` references, missing required fields, duplicate operationIds, invalid schema types, and more.
|
|
154
125
|
|
|
155
|
-
|
|
126
|
+
## Output Format
|
|
127
|
+
|
|
128
|
+
JSON by default. Use `--format text` or `--format yaml` for alternatives:
|
|
156
129
|
|
|
157
130
|
```bash
|
|
158
|
-
spec list --format
|
|
159
|
-
spec
|
|
160
|
-
spec list --format yaml # YAML
|
|
131
|
+
spec list --format text
|
|
132
|
+
spec show getPetById --format yaml
|
|
161
133
|
```
|
|
162
134
|
|
|
163
|
-
Errors always
|
|
164
|
-
|
|
165
|
-
## GraphQL Coverage
|
|
135
|
+
Errors always go to stderr as JSON: `{"error": "message"}` with non-zero exit code.
|
|
166
136
|
|
|
167
|
-
|
|
168
|
-
- **Queries** — all root query fields
|
|
169
|
-
- **Mutations** — all root mutation fields
|
|
170
|
-
- **Subscriptions** — all root subscription fields
|
|
171
|
-
- **Types** — input objects, enums, scalars, object types with fields
|
|
172
|
-
- **Args** — full argument definitions with types and defaults
|
|
137
|
+
## Token Efficiency
|
|
173
138
|
|
|
174
|
-
|
|
139
|
+
The CLI is designed to minimize context window usage for AI agents:
|
|
175
140
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
4. **Auto-query building** — GraphQL calls auto-generate queries from the schema
|
|
182
|
-
5. **Persistent config** — set auth once, use across calls
|
|
183
|
-
6. **No interactive prompts** — everything is flags and args
|
|
141
|
+
- `list` returns only IDs by default (not full schemas)
|
|
142
|
+
- `show` resolves schemas compactly — nested refs show as names, not deep explosions
|
|
143
|
+
- `types` lets you inspect one type at a time instead of loading all schemas
|
|
144
|
+
- `--limit` and `--offset` paginate large APIs
|
|
145
|
+
- `--filter` and `--tag` narrow results before output
|
|
184
146
|
|
|
185
147
|
## Storage
|
|
186
148
|
|
|
187
|
-
All state
|
|
188
|
-
- `spec.json` —
|
|
189
|
-
- `config.json` — base URL,
|
|
149
|
+
All state lives in `.spec-cli/` in the working directory:
|
|
150
|
+
- `spec.json` — loaded spec cache
|
|
151
|
+
- `config.json` — base URL, auth, headers
|
|
190
152
|
|
|
191
153
|
Add `.spec-cli/` to your `.gitignore`.
|
|
192
|
-
|
|
193
|
-
## Dependencies
|
|
194
|
-
|
|
195
|
-
- [yaml](https://www.npmjs.com/package/yaml) — YAML parsing (for OpenAPI YAML specs and YAML output)
|
|
196
|
-
|
|
197
|
-
No other runtime dependencies. Works with Node.js 18+ or Bun (uses native `fetch`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-spec-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"node": ">=18.0.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
38
39
|
"yaml": "^2.7.0"
|
|
39
40
|
}
|
|
40
41
|
}
|
package/src/cli.js
CHANGED
|
@@ -4,33 +4,66 @@ import { showOperation } from "./commands/show.js";
|
|
|
4
4
|
import { callOperation } from "./commands/call.js";
|
|
5
5
|
import { configCmd } from "./commands/config.js";
|
|
6
6
|
import { validateSpec } from "./commands/validate.js";
|
|
7
|
+
import { typesCmd } from "./commands/types.js";
|
|
7
8
|
import { out, err, setFormat } from "./output.js";
|
|
8
9
|
|
|
9
|
-
const HELP = `spec-cli —
|
|
10
|
+
const HELP = `spec-cli — Explore and call APIs from the command line.
|
|
11
|
+
All output is JSON. Designed for AI agents but works for humans too.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
spec load <file-or-url>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
spec
|
|
17
|
-
spec
|
|
18
|
-
spec
|
|
19
|
-
spec config unset <key> Remove config key
|
|
13
|
+
WORKFLOW (follow this order):
|
|
14
|
+
1. spec load <file-or-url> Load an OpenAPI or GraphQL spec
|
|
15
|
+
spec load --mcp-stdio <cmd> Load an MCP server via stdio
|
|
16
|
+
spec load --mcp-sse <url> Load an MCP server via SSE
|
|
17
|
+
spec load --mcp-http <url> Load an MCP server via streamable HTTP
|
|
18
|
+
2. spec list Browse operations/tools (compact IDs)
|
|
19
|
+
3. spec show <operation> Get params, body, response for one op
|
|
20
|
+
4. spec call <operation> [options] Execute the request
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
--query key=val Query parameters (repeatable)
|
|
24
|
-
--header key=val Per-request headers (repeatable)
|
|
25
|
-
--var key=val Path variables (repeatable)
|
|
26
|
-
--method GET|POST|... Override HTTP method
|
|
22
|
+
Use spec types [name] to inspect a schema/type referenced by show.
|
|
23
|
+
Use spec config to set baseUrl, auth, and headers before calling.
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
--
|
|
25
|
+
DISCOVERY (narrowing down):
|
|
26
|
+
spec list All operations (just IDs)
|
|
27
|
+
spec list --filter user Search across all fields
|
|
28
|
+
spec list --tag pets OpenAPI tag or GraphQL kind (query/mutation)
|
|
29
|
+
spec list --limit 10 --offset 20 Paginate large APIs
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
INSPECT:
|
|
32
|
+
spec show getPetById Match by operationId
|
|
33
|
+
spec show /pet/{petId} Match by path
|
|
34
|
+
spec show "GET /pet/{petId}" Match by method + path
|
|
35
|
+
spec show publishPost GraphQL operation name
|
|
36
|
+
spec show read_file MCP tool name
|
|
37
|
+
spec types List all schema/type names
|
|
38
|
+
spec types Pet Inspect one schema (compact, no $ref explosion)
|
|
39
|
+
|
|
40
|
+
EXECUTE:
|
|
41
|
+
spec call <op> --var petId=1 Path or GraphQL variables
|
|
42
|
+
spec call <op> --query status=available Query string params
|
|
43
|
+
spec call <op> --data '{"name":"Rex"}' JSON body / MCP tool arguments
|
|
44
|
+
spec call <op> --data-file /tmp/query.json JSON body from file (avoids shell escaping)
|
|
45
|
+
spec call <op> --header X-Custom=val Extra headers (OpenAPI/GraphQL)
|
|
46
|
+
spec call <op> --method PUT Override HTTP method (OpenAPI)
|
|
47
|
+
|
|
48
|
+
MCP EXAMPLES:
|
|
49
|
+
spec load --mcp-stdio "npx -y @modelcontextprotocol/server-filesystem /tmp"
|
|
50
|
+
spec load --mcp-sse http://localhost:3000/sse
|
|
51
|
+
spec load --mcp-http https://example.com/mcp
|
|
52
|
+
spec list
|
|
53
|
+
spec show read_file
|
|
54
|
+
spec call read_file --var path=/tmp/hello.txt
|
|
55
|
+
spec call read_file --data '{"path":"/tmp/hello.txt"}'
|
|
56
|
+
|
|
57
|
+
CONFIG (persisted in .spec-cli/config.json):
|
|
58
|
+
spec config set baseUrl https://api.example.com
|
|
59
|
+
spec config set auth <token> Auto-adds Bearer prefix
|
|
60
|
+
spec config set headers.X-API-Key <key> Dot notation for nested keys
|
|
61
|
+
spec config get Show current config
|
|
62
|
+
spec config unset auth Remove a key
|
|
63
|
+
|
|
64
|
+
OTHER:
|
|
65
|
+
spec validate <file-or-url> Check OpenAPI spec for errors
|
|
66
|
+
--format json|text|yaml Output format (default: json)`;
|
|
34
67
|
|
|
35
68
|
export async function run(args) {
|
|
36
69
|
// Extract --format before routing
|
|
@@ -65,6 +98,10 @@ export async function run(args) {
|
|
|
65
98
|
case "validate":
|
|
66
99
|
await validateSpec(args.slice(1));
|
|
67
100
|
break;
|
|
101
|
+
case "types":
|
|
102
|
+
case "type":
|
|
103
|
+
await typesCmd(args.slice(1));
|
|
104
|
+
break;
|
|
68
105
|
case "config":
|
|
69
106
|
case "cfg":
|
|
70
107
|
await configCmd(args.slice(1));
|
package/src/commands/call.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
1
2
|
import { getSpec, getConfig } from "../store.js";
|
|
2
3
|
import { out } from "../output.js";
|
|
3
4
|
import { parseArgs, parseKV } from "../args.js";
|
|
5
|
+
import { createMcpClient } from "../mcp-client.js";
|
|
4
6
|
|
|
5
7
|
export async function callOperation(args) {
|
|
6
8
|
const { flags, positional } = parseArgs(args);
|
|
7
9
|
const target = positional[0];
|
|
8
|
-
if (!target) throw new Error("Usage: spec call <operationId-or-path> [--data '{}'] [--query k=v] [--header k=v] [--var k=v] [--method GET]");
|
|
10
|
+
if (!target) throw new Error("Usage: spec call <operationId-or-path> [--data '{}'] [--data-file path.json] [--query k=v] [--header k=v] [--var k=v] [--method GET]");
|
|
11
|
+
|
|
12
|
+
// Support --data-file to avoid shell escaping issues
|
|
13
|
+
if (flags["data-file"] && !flags.data) {
|
|
14
|
+
flags.data = readFileSync(flags["data-file"], "utf-8").trim();
|
|
15
|
+
}
|
|
9
16
|
|
|
10
17
|
const spec = getSpec();
|
|
11
18
|
if (!spec) throw new Error("No spec loaded. Run: spec load <file-or-url>");
|
|
@@ -14,11 +21,39 @@ export async function callOperation(args) {
|
|
|
14
21
|
|
|
15
22
|
if (spec.type === "openapi") {
|
|
16
23
|
await callOpenAPI(spec, config, target, flags);
|
|
24
|
+
} else if (spec.type === "mcp") {
|
|
25
|
+
await callMCP(spec, target, flags);
|
|
17
26
|
} else {
|
|
18
27
|
await callGraphQL(spec, config, target, flags);
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
async function callMCP(spec, target, flags) {
|
|
32
|
+
const tool = spec.tools.find((t) => t.name.toLowerCase() === target.toLowerCase());
|
|
33
|
+
if (!tool) throw new Error(`Tool not found: ${target}. Run 'spec list' to see available tools.`);
|
|
34
|
+
|
|
35
|
+
// Build arguments: --data for full JSON object, --var for individual keys
|
|
36
|
+
let toolArgs = {};
|
|
37
|
+
if (flags.data) {
|
|
38
|
+
try {
|
|
39
|
+
toolArgs = JSON.parse(flags.data);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error("--data must be valid JSON when calling an MCP tool");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// --var key=value overrides / extends --data args
|
|
45
|
+
const varOverrides = parseKV(flags.var);
|
|
46
|
+
toolArgs = { ...toolArgs, ...varOverrides };
|
|
47
|
+
|
|
48
|
+
const client = await createMcpClient(spec);
|
|
49
|
+
try {
|
|
50
|
+
const result = await client.callTool({ name: tool.name, arguments: toolArgs });
|
|
51
|
+
out({ tool: tool.name, arguments: toolArgs, result });
|
|
52
|
+
} finally {
|
|
53
|
+
await client.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
22
57
|
async function callOpenAPI(spec, config, target, flags) {
|
|
23
58
|
const lower = target.toLowerCase();
|
|
24
59
|
|
|
@@ -103,11 +138,13 @@ async function callGraphQL(spec, config, target, flags) {
|
|
|
103
138
|
|
|
104
139
|
// Build query from operation
|
|
105
140
|
let query;
|
|
141
|
+
let dataVariables;
|
|
106
142
|
if (flags.data) {
|
|
107
143
|
// If --data is provided, treat as raw GraphQL query
|
|
108
144
|
try {
|
|
109
145
|
const parsed = JSON.parse(flags.data);
|
|
110
146
|
query = parsed.query || flags.data;
|
|
147
|
+
dataVariables = parsed.variables;
|
|
111
148
|
} catch {
|
|
112
149
|
query = flags.data;
|
|
113
150
|
}
|
|
@@ -116,8 +153,9 @@ async function callGraphQL(spec, config, target, flags) {
|
|
|
116
153
|
query = buildGraphQLQuery(op, spec.types);
|
|
117
154
|
}
|
|
118
155
|
|
|
119
|
-
// Variables
|
|
120
|
-
const
|
|
156
|
+
// Variables: --data variables merged with --var overrides
|
|
157
|
+
const varOverrides = parseKV(flags.var);
|
|
158
|
+
const variables = { ...dataVariables, ...varOverrides };
|
|
121
159
|
|
|
122
160
|
const headers = {
|
|
123
161
|
"Content-Type": "application/json",
|
package/src/commands/list.js
CHANGED
|
@@ -8,28 +8,59 @@ export async function listOperations(args) {
|
|
|
8
8
|
|
|
9
9
|
const opts = parseArgs(args);
|
|
10
10
|
const filter = opts.flags.filter?.toLowerCase();
|
|
11
|
+
const compact = opts.flags.compact !== "false"; // compact by default
|
|
12
|
+
const limit = parseInt(opts.flags.limit) || 0;
|
|
13
|
+
const offset = parseInt(opts.flags.offset) || 0;
|
|
14
|
+
const tag = opts.flags.tag?.toLowerCase();
|
|
11
15
|
|
|
12
16
|
let operations;
|
|
13
17
|
|
|
14
18
|
if (spec.type === "openapi") {
|
|
15
|
-
operations = spec.operations.map((op) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
operations = spec.operations.map((op) =>
|
|
20
|
+
compact
|
|
21
|
+
? { id: op.id, method: op.method, path: op.path }
|
|
22
|
+
: {
|
|
23
|
+
id: op.id,
|
|
24
|
+
method: op.method,
|
|
25
|
+
path: op.path,
|
|
26
|
+
summary: op.summary,
|
|
27
|
+
tags: op.tags,
|
|
28
|
+
deprecated: op.deprecated,
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Filter by tag
|
|
33
|
+
if (tag) {
|
|
34
|
+
const fullOps = spec.operations;
|
|
35
|
+
operations = operations.filter((_, i) =>
|
|
36
|
+
fullOps[i].tags?.some((t) => t.toLowerCase().includes(tag))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
} else if (spec.type === "mcp") {
|
|
40
|
+
operations = spec.tools.map((t) =>
|
|
41
|
+
compact
|
|
42
|
+
? { id: t.name, description: t.description }
|
|
43
|
+
: { id: t.name, description: t.description, inputSchema: t.inputSchema }
|
|
44
|
+
);
|
|
23
45
|
} else {
|
|
24
46
|
// graphql
|
|
25
|
-
operations = spec.operations.map((op) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
operations = spec.operations.map((op) =>
|
|
48
|
+
compact
|
|
49
|
+
? { id: op.name, kind: op.kind }
|
|
50
|
+
: {
|
|
51
|
+
id: op.name,
|
|
52
|
+
kind: op.kind,
|
|
53
|
+
description: op.description,
|
|
54
|
+
args: op.args.map((a) => a.name),
|
|
55
|
+
returnType: op.returnType,
|
|
56
|
+
isDeprecated: op.isDeprecated,
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Filter by kind (query/mutation/subscription)
|
|
61
|
+
if (tag) {
|
|
62
|
+
operations = operations.filter((op) => op.kind === tag);
|
|
63
|
+
}
|
|
33
64
|
}
|
|
34
65
|
|
|
35
66
|
if (filter) {
|
|
@@ -39,9 +70,21 @@ export async function listOperations(args) {
|
|
|
39
70
|
});
|
|
40
71
|
}
|
|
41
72
|
|
|
73
|
+
const total = operations.length;
|
|
74
|
+
|
|
75
|
+
// Pagination
|
|
76
|
+
if (offset > 0) {
|
|
77
|
+
operations = operations.slice(offset);
|
|
78
|
+
}
|
|
79
|
+
if (limit > 0) {
|
|
80
|
+
operations = operations.slice(0, limit);
|
|
81
|
+
}
|
|
82
|
+
|
|
42
83
|
out({
|
|
43
84
|
type: spec.type,
|
|
44
|
-
|
|
85
|
+
total,
|
|
86
|
+
showing: operations.length,
|
|
87
|
+
offset: offset || 0,
|
|
45
88
|
operations,
|
|
46
89
|
});
|
|
47
90
|
}
|
package/src/commands/load.js
CHANGED
|
@@ -3,6 +3,8 @@ import { resolve } from "path";
|
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
import { saveSpec } from "../store.js";
|
|
5
5
|
import { out, err } from "../output.js";
|
|
6
|
+
import { parseArgs } from "../args.js";
|
|
7
|
+
import { createMcpClient } from "../mcp-client.js";
|
|
6
8
|
|
|
7
9
|
const INTROSPECTION_QUERY = `{
|
|
8
10
|
__schema {
|
|
@@ -60,8 +62,24 @@ fragment TypeRef on __Type {
|
|
|
60
62
|
}`;
|
|
61
63
|
|
|
62
64
|
export async function loadSpec(args) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
+
const { flags, positional } = parseArgs(args);
|
|
66
|
+
|
|
67
|
+
// MCP transport flags
|
|
68
|
+
if (flags["mcp-stdio"] || flags["mcp-sse"] || flags["mcp-http"]) {
|
|
69
|
+
const spec = await loadMCP(flags);
|
|
70
|
+
saveSpec(spec);
|
|
71
|
+
out({
|
|
72
|
+
ok: true,
|
|
73
|
+
type: "mcp",
|
|
74
|
+
title: spec.title,
|
|
75
|
+
transport: spec.transport,
|
|
76
|
+
toolCount: spec.tools.length,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const source = positional[0];
|
|
82
|
+
if (!source) throw new Error("Usage: spec load <file-or-url> | spec load --mcp-stdio <cmd> | spec load --mcp-sse <url> | spec load --mcp-http <url>");
|
|
65
83
|
|
|
66
84
|
// Detect if it's a URL or file
|
|
67
85
|
const isUrl = source.startsWith("http://") || source.startsWith("https://");
|
|
@@ -84,6 +102,48 @@ export async function loadSpec(args) {
|
|
|
84
102
|
});
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
async function loadMCP(flags) {
|
|
106
|
+
let transportConfig;
|
|
107
|
+
|
|
108
|
+
if (flags["mcp-stdio"]) {
|
|
109
|
+
const raw = flags["mcp-stdio"];
|
|
110
|
+
// Split on whitespace, respecting that the value is already a single flag string
|
|
111
|
+
const parts = raw.match(/(?:[^\s"]+|"[^"]*")+/g).map((p) => p.replace(/^"|"$/g, ""));
|
|
112
|
+
transportConfig = {
|
|
113
|
+
transport: "stdio",
|
|
114
|
+
command: parts[0],
|
|
115
|
+
args: parts.slice(1),
|
|
116
|
+
};
|
|
117
|
+
} else if (flags["mcp-sse"]) {
|
|
118
|
+
transportConfig = {
|
|
119
|
+
transport: "sse",
|
|
120
|
+
url: flags["mcp-sse"],
|
|
121
|
+
};
|
|
122
|
+
} else {
|
|
123
|
+
transportConfig = {
|
|
124
|
+
transport: "streamable-http",
|
|
125
|
+
url: flags["mcp-http"],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const client = await createMcpClient(transportConfig);
|
|
130
|
+
try {
|
|
131
|
+
const { tools } = await client.listTools();
|
|
132
|
+
return {
|
|
133
|
+
type: "mcp",
|
|
134
|
+
title: "MCP Server",
|
|
135
|
+
...transportConfig,
|
|
136
|
+
tools: tools.map((t) => ({
|
|
137
|
+
name: t.name,
|
|
138
|
+
description: t.description || null,
|
|
139
|
+
inputSchema: t.inputSchema || null,
|
|
140
|
+
})),
|
|
141
|
+
};
|
|
142
|
+
} finally {
|
|
143
|
+
await client.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
87
147
|
async function loadFromUrl(url) {
|
|
88
148
|
// Try GraphQL introspection first if URL doesn't end with known extensions
|
|
89
149
|
const lowerUrl = url.toLowerCase();
|
package/src/commands/show.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getSpec } from "../store.js";
|
|
2
2
|
import { out } from "../output.js";
|
|
3
|
+
import { parseArgs } from "../args.js";
|
|
3
4
|
|
|
4
5
|
export async function showOperation(args) {
|
|
5
|
-
const
|
|
6
|
+
const { positional } = parseArgs(args);
|
|
7
|
+
const target = positional[0];
|
|
6
8
|
if (!target) throw new Error("Usage: spec show <operationId-or-path>");
|
|
7
9
|
|
|
8
10
|
const spec = getSpec();
|
|
@@ -10,6 +12,8 @@ export async function showOperation(args) {
|
|
|
10
12
|
|
|
11
13
|
if (spec.type === "openapi") {
|
|
12
14
|
showOpenAPI(spec, target);
|
|
15
|
+
} else if (spec.type === "mcp") {
|
|
16
|
+
showMCP(spec, target);
|
|
13
17
|
} else {
|
|
14
18
|
showGraphQL(spec, target);
|
|
15
19
|
}
|
|
@@ -18,7 +22,6 @@ export async function showOperation(args) {
|
|
|
18
22
|
function showOpenAPI(spec, target) {
|
|
19
23
|
const lower = target.toLowerCase();
|
|
20
24
|
|
|
21
|
-
// Match by operationId, path, or "METHOD path"
|
|
22
25
|
const op = spec.operations.find((o) => {
|
|
23
26
|
return (
|
|
24
27
|
o.id.toLowerCase() === lower ||
|
|
@@ -33,15 +36,29 @@ function showOpenAPI(spec, target) {
|
|
|
33
36
|
|
|
34
37
|
const root = spec.raw || spec.components;
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
out({
|
|
40
|
+
id: op.id,
|
|
41
|
+
method: op.method,
|
|
42
|
+
path: op.path,
|
|
43
|
+
summary: op.summary,
|
|
44
|
+
description: op.description,
|
|
45
|
+
tags: op.tags,
|
|
46
|
+
deprecated: op.deprecated,
|
|
47
|
+
parameters: op.parameters.map((p) => {
|
|
48
|
+
const resolved = resolveRef(p, root);
|
|
49
|
+
return {
|
|
50
|
+
name: resolved.name,
|
|
51
|
+
in: resolved.in,
|
|
52
|
+
required: resolved.required || false,
|
|
53
|
+
type: resolved.schema?.type || null,
|
|
54
|
+
format: resolved.schema?.format || undefined,
|
|
55
|
+
description: resolved.description || undefined,
|
|
56
|
+
enum: resolved.schema?.enum || undefined,
|
|
57
|
+
};
|
|
58
|
+
}),
|
|
40
59
|
requestBody: op.requestBody ? resolveRequestBody(op.requestBody, root) : null,
|
|
41
|
-
responses:
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
out(resolved);
|
|
60
|
+
responses: resolveResponsesCompact(op.responses, root),
|
|
61
|
+
});
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
function showGraphQL(spec, target) {
|
|
@@ -53,15 +70,39 @@ function showGraphQL(spec, target) {
|
|
|
53
70
|
throw new Error(`Operation not found: ${target}. Run 'spec list' to see available operations.`);
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
// Also find related types
|
|
57
73
|
const relatedTypes = findRelatedTypes(op, spec.types);
|
|
58
74
|
|
|
59
75
|
out({
|
|
60
|
-
|
|
76
|
+
name: op.name,
|
|
77
|
+
kind: op.kind,
|
|
78
|
+
description: op.description,
|
|
79
|
+
returnType: op.returnType,
|
|
80
|
+
isDeprecated: op.isDeprecated,
|
|
81
|
+
args: op.args?.map((a) => ({
|
|
82
|
+
name: a.name,
|
|
83
|
+
type: flattenType(a.type),
|
|
84
|
+
required: a.type?.kind === "NON_NULL",
|
|
85
|
+
description: a.description || undefined,
|
|
86
|
+
defaultValue: a.defaultValue || undefined,
|
|
87
|
+
})),
|
|
61
88
|
relatedTypes,
|
|
62
89
|
});
|
|
63
90
|
}
|
|
64
91
|
|
|
92
|
+
function showMCP(spec, target) {
|
|
93
|
+
const tool = spec.tools.find((t) => t.name.toLowerCase() === target.toLowerCase());
|
|
94
|
+
if (!tool) {
|
|
95
|
+
throw new Error(`Tool not found: ${target}. Run 'spec list' to see available tools.`);
|
|
96
|
+
}
|
|
97
|
+
out({
|
|
98
|
+
name: tool.name,
|
|
99
|
+
description: tool.description,
|
|
100
|
+
inputSchema: tool.inputSchema,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Helpers ---
|
|
105
|
+
|
|
65
106
|
function resolveRef(obj, root) {
|
|
66
107
|
if (!obj || typeof obj !== "object") return obj;
|
|
67
108
|
if (obj.$ref) {
|
|
@@ -79,55 +120,81 @@ function resolveRequestBody(body, root) {
|
|
|
79
120
|
if (!body) return null;
|
|
80
121
|
const resolved = resolveRef(body, root);
|
|
81
122
|
if (resolved?.content) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
123
|
+
// Only show application/json if available (most useful for agents)
|
|
124
|
+
const jsonContent = resolved.content["application/json"];
|
|
125
|
+
if (jsonContent) {
|
|
126
|
+
return {
|
|
127
|
+
description: resolved.description || undefined,
|
|
128
|
+
required: resolved.required || undefined,
|
|
129
|
+
schema: resolveSchema(jsonContent.schema, root),
|
|
87
130
|
};
|
|
88
131
|
}
|
|
89
|
-
|
|
132
|
+
// Fallback: show first content type
|
|
133
|
+
const [mediaType, value] = Object.entries(resolved.content)[0];
|
|
134
|
+
return {
|
|
135
|
+
description: resolved.description || undefined,
|
|
136
|
+
required: resolved.required || undefined,
|
|
137
|
+
mediaType,
|
|
138
|
+
schema: resolveSchema(value.schema, root),
|
|
139
|
+
};
|
|
90
140
|
}
|
|
91
141
|
return resolved;
|
|
92
142
|
}
|
|
93
143
|
|
|
94
|
-
|
|
95
|
-
|
|
144
|
+
// Only show success response schema — agent doesn't need error schemas to make a call
|
|
145
|
+
function resolveResponsesCompact(responses, root) {
|
|
146
|
+
if (!responses) return null;
|
|
96
147
|
const result = {};
|
|
97
148
|
for (const [code, resp] of Object.entries(responses)) {
|
|
98
149
|
const resolved = resolveRef(resp, root);
|
|
99
150
|
if (resolved?.content) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
result[code].content[mediaType] = {
|
|
106
|
-
...value,
|
|
107
|
-
schema: resolveSchema(value.schema, root),
|
|
151
|
+
const jsonContent = resolved.content["application/json"];
|
|
152
|
+
if (jsonContent) {
|
|
153
|
+
result[code] = {
|
|
154
|
+
description: resolved.description,
|
|
155
|
+
schema: resolveSchema(jsonContent.schema, root),
|
|
108
156
|
};
|
|
157
|
+
} else {
|
|
158
|
+
result[code] = { description: resolved.description };
|
|
109
159
|
}
|
|
110
160
|
} else {
|
|
111
|
-
result[code] = resolved;
|
|
161
|
+
result[code] = { description: resolved.description };
|
|
112
162
|
}
|
|
113
163
|
}
|
|
114
164
|
return result;
|
|
115
165
|
}
|
|
116
166
|
|
|
117
167
|
function resolveSchema(schema, root, depth = 0) {
|
|
118
|
-
if (!schema || depth >
|
|
168
|
+
if (!schema || depth > 3) return schema;
|
|
119
169
|
if (schema.$ref) {
|
|
120
|
-
|
|
170
|
+
const resolved = resolveRef(schema, root);
|
|
171
|
+
// Resolve one more level for the top-level ref
|
|
172
|
+
return resolveSchema(resolved, root, depth + 1);
|
|
121
173
|
}
|
|
122
174
|
if (schema.properties) {
|
|
123
|
-
const result = {
|
|
175
|
+
const result = { type: schema.type, required: schema.required };
|
|
176
|
+
result.properties = {};
|
|
124
177
|
for (const [key, val] of Object.entries(schema.properties)) {
|
|
125
|
-
|
|
178
|
+
if (val.$ref) {
|
|
179
|
+
const refName = val.$ref.split("/").pop();
|
|
180
|
+
result.properties[key] = { $ref: refName };
|
|
181
|
+
} else if (val.type === "array" && val.items?.$ref) {
|
|
182
|
+
result.properties[key] = { type: "array", items: val.items.$ref.split("/").pop() };
|
|
183
|
+
} else {
|
|
184
|
+
const prop = { type: val.type };
|
|
185
|
+
if (val.format) prop.format = val.format;
|
|
186
|
+
if (val.enum) prop.enum = val.enum;
|
|
187
|
+
if (val.description) prop.description = val.description;
|
|
188
|
+
result.properties[key] = prop;
|
|
189
|
+
}
|
|
126
190
|
}
|
|
127
191
|
return result;
|
|
128
192
|
}
|
|
129
193
|
if (schema.items) {
|
|
130
|
-
|
|
194
|
+
if (schema.items.$ref) {
|
|
195
|
+
return { type: "array", items: schema.items.$ref.split("/").pop() };
|
|
196
|
+
}
|
|
197
|
+
return { type: "array", items: resolveSchema(schema.items, root, depth + 1) };
|
|
131
198
|
}
|
|
132
199
|
return schema;
|
|
133
200
|
}
|
|
@@ -135,7 +202,6 @@ function resolveSchema(schema, root, depth = 0) {
|
|
|
135
202
|
function findRelatedTypes(op, types) {
|
|
136
203
|
const names = new Set();
|
|
137
204
|
|
|
138
|
-
// Collect type names from args and return type
|
|
139
205
|
function extractTypeNames(typeStr) {
|
|
140
206
|
if (!typeStr) return;
|
|
141
207
|
const cleaned = typeStr.replace(/[[\]!]/g, "");
|
|
@@ -147,20 +213,28 @@ function findRelatedTypes(op, types) {
|
|
|
147
213
|
extractTypeNames(flattenType(arg.type));
|
|
148
214
|
}
|
|
149
215
|
|
|
150
|
-
// Filter out built-in scalar types
|
|
151
216
|
const scalars = new Set(["String", "Int", "Float", "Boolean", "ID"]);
|
|
152
217
|
return types
|
|
153
218
|
.filter((t) => names.has(t.name) && !scalars.has(t.name))
|
|
154
|
-
.map((t) =>
|
|
155
|
-
name: t.name,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
219
|
+
.map((t) => {
|
|
220
|
+
const result = { name: t.name, kind: t.kind };
|
|
221
|
+
if (t.fields) {
|
|
222
|
+
result.fields = t.fields.map((f) => ({
|
|
223
|
+
name: f.name,
|
|
224
|
+
type: flattenType(f.type),
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
if (t.inputFields) {
|
|
228
|
+
result.inputFields = t.inputFields.map((f) => ({
|
|
229
|
+
name: f.name,
|
|
230
|
+
type: flattenType(f.type),
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
if (t.enumValues) {
|
|
234
|
+
result.enumValues = t.enumValues.map((e) => e.name);
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
});
|
|
164
238
|
}
|
|
165
239
|
|
|
166
240
|
function flattenType(t) {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { getSpec } from "../store.js";
|
|
2
|
+
import { out } from "../output.js";
|
|
3
|
+
import { parseArgs } from "../args.js";
|
|
4
|
+
|
|
5
|
+
export async function typesCmd(args) {
|
|
6
|
+
const spec = getSpec();
|
|
7
|
+
if (!spec) throw new Error("No spec loaded. Run: spec load <file-or-url>");
|
|
8
|
+
|
|
9
|
+
const { positional, flags } = parseArgs(args);
|
|
10
|
+
const target = positional[0];
|
|
11
|
+
|
|
12
|
+
if (spec.type === "openapi") {
|
|
13
|
+
showOpenAPISchema(spec, target, flags);
|
|
14
|
+
} else {
|
|
15
|
+
showGraphQLType(spec, target, flags);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function showOpenAPISchema(spec, target, flags) {
|
|
20
|
+
const schemas = spec.raw?.components?.schemas || spec.raw?.definitions || {};
|
|
21
|
+
|
|
22
|
+
if (!target) {
|
|
23
|
+
// List all schema names — just names, very compact
|
|
24
|
+
const names = Object.keys(schemas);
|
|
25
|
+
out({
|
|
26
|
+
type: "openapi",
|
|
27
|
+
count: names.length,
|
|
28
|
+
schemas: names,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Find schema (case-insensitive)
|
|
34
|
+
const lower = target.toLowerCase();
|
|
35
|
+
const key = Object.keys(schemas).find((k) => k.toLowerCase() === lower);
|
|
36
|
+
|
|
37
|
+
if (!key) {
|
|
38
|
+
throw new Error(`Schema not found: ${target}. Run 'spec types' to list available schemas.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const schema = schemas[key];
|
|
42
|
+
const root = spec.raw;
|
|
43
|
+
|
|
44
|
+
// Resolve one level deep — don't recursively explode nested schemas
|
|
45
|
+
const resolved = resolveSchemaCompact(schema, root);
|
|
46
|
+
|
|
47
|
+
out({
|
|
48
|
+
name: key,
|
|
49
|
+
...resolved,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function showGraphQLType(spec, target, flags) {
|
|
54
|
+
const scalars = new Set(["String", "Int", "Float", "Boolean", "ID"]);
|
|
55
|
+
const userTypes = spec.types?.filter((t) => !t.name.startsWith("__") && !scalars.has(t.name)) || [];
|
|
56
|
+
|
|
57
|
+
if (!target) {
|
|
58
|
+
// List type names grouped by kind — compact
|
|
59
|
+
const grouped = {};
|
|
60
|
+
for (const t of userTypes) {
|
|
61
|
+
if (!grouped[t.kind]) grouped[t.kind] = [];
|
|
62
|
+
grouped[t.kind].push(t.name);
|
|
63
|
+
}
|
|
64
|
+
out({
|
|
65
|
+
type: "graphql",
|
|
66
|
+
count: userTypes.length,
|
|
67
|
+
types: grouped,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Find specific type
|
|
73
|
+
const lower = target.toLowerCase();
|
|
74
|
+
const type = userTypes.find((t) => t.name.toLowerCase() === lower);
|
|
75
|
+
|
|
76
|
+
if (!type) {
|
|
77
|
+
throw new Error(`Type not found: ${target}. Run 'spec types' to list available types.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = {
|
|
81
|
+
name: type.name,
|
|
82
|
+
kind: type.kind,
|
|
83
|
+
description: type.description || null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (type.fields) {
|
|
87
|
+
result.fields = type.fields.map((f) => ({
|
|
88
|
+
name: f.name,
|
|
89
|
+
type: flattenType(f.type),
|
|
90
|
+
args: f.args?.length > 0 ? f.args.map((a) => ({ name: a.name, type: flattenType(a.type) })) : undefined,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (type.inputFields) {
|
|
95
|
+
result.inputFields = type.inputFields.map((f) => ({
|
|
96
|
+
name: f.name,
|
|
97
|
+
type: flattenType(f.type),
|
|
98
|
+
defaultValue: f.defaultValue || undefined,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (type.enumValues) {
|
|
103
|
+
result.enumValues = type.enumValues.map((e) => e.name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
out(result);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveSchemaCompact(schema, root) {
|
|
110
|
+
if (!schema) return schema;
|
|
111
|
+
|
|
112
|
+
if (schema.$ref) {
|
|
113
|
+
const path = schema.$ref.replace("#/", "").split("/");
|
|
114
|
+
let resolved = root;
|
|
115
|
+
for (const p of path) resolved = resolved?.[p];
|
|
116
|
+
return resolveSchemaCompact(resolved, root);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = {};
|
|
120
|
+
if (schema.type) result.type = schema.type;
|
|
121
|
+
if (schema.description) result.description = schema.description;
|
|
122
|
+
if (schema.required) result.required = schema.required;
|
|
123
|
+
if (schema.enum) result.enum = schema.enum;
|
|
124
|
+
|
|
125
|
+
if (schema.properties) {
|
|
126
|
+
result.properties = {};
|
|
127
|
+
for (const [key, val] of Object.entries(schema.properties)) {
|
|
128
|
+
if (val.$ref) {
|
|
129
|
+
// Just show the type name, don't resolve
|
|
130
|
+
const refName = val.$ref.split("/").pop();
|
|
131
|
+
result.properties[key] = { $ref: refName };
|
|
132
|
+
} else if (val.type === "array" && val.items?.$ref) {
|
|
133
|
+
const refName = val.items.$ref.split("/").pop();
|
|
134
|
+
result.properties[key] = { type: "array", items: refName };
|
|
135
|
+
} else {
|
|
136
|
+
result.properties[key] = { type: val.type || null };
|
|
137
|
+
if (val.enum) result.properties[key].enum = val.enum;
|
|
138
|
+
if (val.format) result.properties[key].format = val.format;
|
|
139
|
+
if (val.description) result.properties[key].description = val.description;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (schema.items) {
|
|
145
|
+
if (schema.items.$ref) {
|
|
146
|
+
result.items = schema.items.$ref.split("/").pop();
|
|
147
|
+
} else {
|
|
148
|
+
result.items = { type: schema.items.type };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function flattenType(t) {
|
|
156
|
+
if (!t) return null;
|
|
157
|
+
if (t.name) return t.kind === "NON_NULL" ? `${t.name}!` : t.name;
|
|
158
|
+
if (t.ofType) {
|
|
159
|
+
const inner = flattenType(t.ofType);
|
|
160
|
+
if (t.kind === "LIST") return `[${inner}]`;
|
|
161
|
+
if (t.kind === "NON_NULL") return `${inner}!`;
|
|
162
|
+
return inner;
|
|
163
|
+
}
|
|
164
|
+
return t.kind;
|
|
165
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
|
+
|
|
6
|
+
export async function createMcpClient(spec) {
|
|
7
|
+
const client = new Client({ name: "spec-cli", version: "1.0.0" });
|
|
8
|
+
|
|
9
|
+
let transport;
|
|
10
|
+
if (spec.transport === "stdio") {
|
|
11
|
+
transport = new StdioClientTransport({
|
|
12
|
+
command: spec.command,
|
|
13
|
+
args: spec.args,
|
|
14
|
+
});
|
|
15
|
+
} else if (spec.transport === "sse") {
|
|
16
|
+
transport = new SSEClientTransport(new URL(spec.url));
|
|
17
|
+
} else if (spec.transport === "streamable-http") {
|
|
18
|
+
transport = new StreamableHTTPClientTransport(new URL(spec.url));
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error(`Unknown MCP transport: ${spec.transport}. Supported: stdio, sse, streamable-http`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await client.connect(transport);
|
|
24
|
+
return client;
|
|
25
|
+
}
|