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 CHANGED
@@ -1,6 +1,6 @@
1
- # spec-cli
1
+ # api-spec-cli
2
2
 
3
- Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs. Designed for AI coding agents — all output is structured JSON by default, with optional text and YAML formats.
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
- Or with bun:
11
+ Works with Node.js 18+ or Bun. No other dependencies.
12
12
 
13
13
  ```bash
14
- bun install -g api-spec-cli
14
+ # Or run without installing
15
+ npx api-spec-cli <command>
15
16
  ```
16
17
 
17
- Or run without installing:
18
+ ## How It Works
18
19
 
19
- ```bash
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
- ## Quick Start
22
+ ### Step 1: Load the spec
25
23
 
26
24
  ```bash
27
- # Load an OpenAPI spec
28
- spec load https://petstore3.swagger.io/api/v3/openapi.json
29
-
30
- # Load a GraphQL endpoint (introspection)
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
- # List operations
34
- spec list
35
- spec list --filter pets
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
- # Show operation details
38
- spec show getPetById
39
- spec show publishPost
35
+ ### Step 2: Find what you need
40
36
 
41
- # Call an endpoint
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
- # GraphQL with auth
46
- spec config set auth YOUR_TOKEN
47
- spec call me
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
- ## Commands
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
- ### `spec load <file-or-url>`
61
+ Use `--compact false` for full details (summary, tags, args).
53
62
 
54
- Load an API spec from a local file (JSON/YAML) or URL.
63
+ ### Step 3: Inspect one operation
55
64
 
56
- - **OpenAPI/Swagger**: Detects JSON or YAML, supports v2 and v3
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 load ./openapi.yaml
61
- spec load https://api.example.com/openapi.json
62
- spec load https://gql.example.com/graphql
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
- ### `spec list [--filter <text>]`
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
- List all operations in the loaded spec.
76
+ ### Step 4: Drill into types (if needed)
68
77
 
69
78
  ```bash
70
- spec list # all operations
71
- spec list --filter user # filter by keyword
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
- **OpenAPI output**: `id`, `method`, `path`, `summary`, `tags`, `deprecated`
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
- Show full details of an operation.
86
+ ### Step 5: Call the API
80
87
 
81
88
  ```bash
82
- spec show getPetById # by operationId
83
- spec show /pet/{petId} # by path
84
- spec show "GET /pet/{petId}" # by method + path
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
- ```bash
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 updatePet --method PUT --data '{"id":1,"name":"Rex"}'
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.example.com
105
- spec call publishPost --data '{"query":"mutation { ... }"}'
100
+ spec call publication --var host=blog.hashnode.dev
106
101
  ```
107
102
 
108
- **Options:**
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
- For GraphQL calls without `--data`, the CLI auto-generates a query from the operation's schema, selecting scalar fields from the return type.
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 validate ./openapi.yaml
125
- spec validate https://api.example.com/openapi.json
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
- Checks:
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
- ### `spec config`
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 config get # show all config
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
- ## Output Formats
124
+ Reports broken `$ref` references, missing required fields, duplicate operationIds, invalid schema types, and more.
154
125
 
155
- All commands support `--format`:
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 json # JSON (default)
159
- spec list --format text # human-readable
160
- spec list --format yaml # YAML
131
+ spec list --format text
132
+ spec show getPetById --format yaml
161
133
  ```
162
134
 
163
- Errors always output as JSON to stderr for reliable agent parsing.
164
-
165
- ## GraphQL Coverage
135
+ Errors always go to stderr as JSON: `{"error": "message"}` with non-zero exit code.
166
136
 
167
- Full GraphQL schema support via introspection:
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
- ## For AI Agents
139
+ The CLI is designed to minimize context window usage for AI agents:
175
140
 
176
- This CLI is designed to be used by AI coding agents (Claude, GPT, etc.) as an MCP tool or shell command:
177
-
178
- 1. **Structured output** JSON by default, every field is predictable
179
- 2. **Error format** `{"error": "message"}` on stderr, non-zero exit code
180
- 3. **Discoverable** — `list` and `show` let agents explore APIs without docs
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 is stored in `.spec-cli/` in the current directory:
188
- - `spec.json` — cached loaded spec
189
- - `config.json` — base URL, headers, auth
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.0.4",
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 — Agent-friendly API spec explorer
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
- Commands:
12
- spec load <file-or-url> Load an OpenAPI (JSON/YAML) or GraphQL endpoint
13
- spec list [--filter <text>] List all operations
14
- spec show <operation> Show operation details (params, body, response)
15
- spec call <operation> [options] Execute a request
16
- spec validate <file-or-url> Validate an OpenAPI spec (errors + warnings)
17
- spec config set <key> <value> Set config (baseUrl, headers.X-Key, auth)
18
- spec config get [key] Show config
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
- Call options:
22
- --data '{"key":"val"}' Request body (JSON)
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
- Output format (all commands):
29
- --format json JSON (default, best for agents)
30
- --format text Human-readable text
31
- --format yaml YAML output
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
- All output defaults to JSON for agent consumption.`;
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));
@@ -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 from --var flags
120
- const variables = parseKV(flags.var);
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",
@@ -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
- id: op.id,
17
- method: op.method,
18
- path: op.path,
19
- summary: op.summary,
20
- tags: op.tags,
21
- deprecated: op.deprecated,
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
- id: op.name,
27
- kind: op.kind,
28
- description: op.description,
29
- args: op.args.map((a) => a.name),
30
- returnType: op.returnType,
31
- isDeprecated: op.isDeprecated,
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
- count: operations.length,
85
+ total,
86
+ showing: operations.length,
87
+ offset: offset || 0,
45
88
  operations,
46
89
  });
47
90
  }
@@ -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 source = args[0];
64
- if (!source) throw new Error("Usage: spec load <file-or-url>");
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();
@@ -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 target = args[0];
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
- // Resolve $ref in parameters, requestBody, and responses
37
- const resolved = {
38
- ...op,
39
- parameters: op.parameters.map((p) => resolveRef(p, root)),
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: resolveResponses(op.responses, root),
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
- ...op,
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
- const result = { ...resolved, content: {} };
83
- for (const [mediaType, value] of Object.entries(resolved.content)) {
84
- result.content[mediaType] = {
85
- ...value,
86
- schema: resolveSchema(value.schema, root),
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
- return result;
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
- function resolveResponses(responses, root) {
95
- if (!responses) return responses;
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
- result[code] = {
101
- ...resolved,
102
- content: {},
103
- };
104
- for (const [mediaType, value] of Object.entries(resolved.content)) {
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 > 5) return schema;
168
+ if (!schema || depth > 3) return schema;
119
169
  if (schema.$ref) {
120
- return resolveRef(schema, root);
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 = { ...schema, properties: {} };
175
+ const result = { type: schema.type, required: schema.required };
176
+ result.properties = {};
124
177
  for (const [key, val] of Object.entries(schema.properties)) {
125
- result.properties[key] = resolveSchema(val, root, depth + 1);
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
- return { ...schema, items: resolveSchema(schema.items, root, depth + 1) };
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
- kind: t.kind,
157
- fields: t.fields?.map((f) => ({
158
- name: f.name,
159
- type: flattenType(f.type),
160
- description: f.description,
161
- })),
162
- enumValues: t.enumValues,
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
+ }