api-spec-cli 0.1.0 → 0.1.2
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 +35 -21
- package/package.json +2 -1
- package/src/cli.js +17 -4
- package/src/commands/call.js +29 -0
- package/src/commands/list.js +6 -0
- package/src/commands/load.js +62 -2
- package/src/commands/show.js +14 -0
- package/src/mcp-client.js +25 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# api-spec-cli
|
|
2
2
|
|
|
3
|
-
CLI for AI agents to explore and call OpenAPI and
|
|
3
|
+
CLI for AI agents to explore and call OpenAPI, GraphQL, and MCP APIs. Output is JSON by default — compact, parseable, token-efficient.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ CLI for AI agents to explore and call OpenAPI and GraphQL APIs. Output is JSON b
|
|
|
8
8
|
npm install -g api-spec-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Works with Node.js 18
|
|
11
|
+
Works with Node.js 18+. No other dependencies.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# Or run without installing
|
|
@@ -22,22 +22,34 @@ The CLI follows a progressive discovery pattern. You never dump an entire API sp
|
|
|
22
22
|
### Step 1: Load the spec
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
|
|
26
|
-
spec load
|
|
27
|
-
spec load
|
|
25
|
+
# OpenAPI
|
|
26
|
+
spec load https://petstore3.swagger.io/api/v3/openapi.json
|
|
27
|
+
spec load ./openapi.yaml
|
|
28
|
+
|
|
29
|
+
# GraphQL (auto-introspects)
|
|
30
|
+
spec load https://gql.hashnode.com
|
|
31
|
+
|
|
32
|
+
# MCP — stdio transport (spawn a local server)
|
|
33
|
+
spec load --mcp-stdio "npx -y @modelcontextprotocol/server-filesystem /tmp"
|
|
34
|
+
|
|
35
|
+
# MCP — SSE transport
|
|
36
|
+
spec load --mcp-sse http://localhost:3000/sse
|
|
37
|
+
|
|
38
|
+
# MCP — Streamable HTTP transport
|
|
39
|
+
spec load --mcp-http https://docs.agno.com/mcp
|
|
28
40
|
```
|
|
29
41
|
|
|
30
42
|
Output tells you what was loaded:
|
|
31
43
|
```json
|
|
32
|
-
{ "ok": true, "type": "
|
|
44
|
+
{ "ok": true, "type": "mcp", "transport": "streamable-http", "toolCount": 1 }
|
|
33
45
|
```
|
|
34
46
|
|
|
35
47
|
### Step 2: Find what you need
|
|
36
48
|
|
|
37
|
-
`list` is compact by default — just
|
|
49
|
+
`list` is compact by default — just IDs, no schemas. Use `--filter`, `--tag`, `--limit` to narrow down.
|
|
38
50
|
|
|
39
51
|
```bash
|
|
40
|
-
spec list # All operations (compact IDs only)
|
|
52
|
+
spec list # All operations/tools (compact IDs only)
|
|
41
53
|
spec list --filter publish # Search by keyword
|
|
42
54
|
spec list --tag pets # OpenAPI: filter by tag
|
|
43
55
|
spec list --tag mutation # GraphQL: filter by kind (query/mutation/subscription)
|
|
@@ -48,32 +60,32 @@ spec list --limit 10 --offset 10 # Next 10
|
|
|
48
60
|
Compact output (token-efficient):
|
|
49
61
|
```json
|
|
50
62
|
{
|
|
51
|
-
"type": "
|
|
52
|
-
"total":
|
|
53
|
-
"showing":
|
|
63
|
+
"type": "mcp",
|
|
64
|
+
"total": 1,
|
|
65
|
+
"showing": 1,
|
|
54
66
|
"operations": [
|
|
55
|
-
{ "id": "
|
|
56
|
-
{ "id": "publishDraft", "kind": "mutation" }
|
|
67
|
+
{ "id": "search_agno", "description": "Search across the Agno knowledge base..." }
|
|
57
68
|
]
|
|
58
69
|
}
|
|
59
70
|
```
|
|
60
71
|
|
|
61
|
-
Use `--compact false` for full details (
|
|
72
|
+
Use `--compact false` for full details (including `inputSchema` for MCP tools).
|
|
62
73
|
|
|
63
|
-
### Step 3: Inspect one operation
|
|
74
|
+
### Step 3: Inspect one operation or tool
|
|
64
75
|
|
|
65
|
-
`show` gives you everything you need to
|
|
76
|
+
`show` gives you everything you need to make a call — params, body schema, response, and related types — in one call.
|
|
66
77
|
|
|
67
78
|
```bash
|
|
68
79
|
spec show publishPost # GraphQL: by operation name
|
|
69
80
|
spec show getPetById # OpenAPI: by operationId
|
|
70
81
|
spec show /pet/{petId} # OpenAPI: by path
|
|
71
82
|
spec show "GET /pet/{petId}" # OpenAPI: by method + path
|
|
83
|
+
spec show search_agno # MCP: by tool name
|
|
72
84
|
```
|
|
73
85
|
|
|
74
|
-
|
|
86
|
+
MCP output includes the full `inputSchema` so you know exactly what arguments to pass.
|
|
75
87
|
|
|
76
|
-
### Step 4: Drill into types (
|
|
88
|
+
### Step 4: Drill into types (OpenAPI/GraphQL only)
|
|
77
89
|
|
|
78
90
|
```bash
|
|
79
91
|
spec types # List all schema/type names
|
|
@@ -81,12 +93,10 @@ spec types Pet # Inspect one schema
|
|
|
81
93
|
spec types PublishPostInput # Inspect a GraphQL input type
|
|
82
94
|
```
|
|
83
95
|
|
|
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.
|
|
85
|
-
|
|
86
96
|
### Step 5: Call the API
|
|
87
97
|
|
|
88
98
|
```bash
|
|
89
|
-
# Set base URL and auth first (persisted across calls)
|
|
99
|
+
# Set base URL and auth first (persisted across calls — OpenAPI/GraphQL)
|
|
90
100
|
spec config set baseUrl https://petstore3.swagger.io/api/v3
|
|
91
101
|
spec config set auth YOUR_TOKEN
|
|
92
102
|
|
|
@@ -98,6 +108,10 @@ spec call addPet --data '{"name":"Rex","photoUrls":[]}'
|
|
|
98
108
|
# GraphQL calls (auto-generates query from schema)
|
|
99
109
|
spec call me
|
|
100
110
|
spec call publication --var host=blog.hashnode.dev
|
|
111
|
+
|
|
112
|
+
# MCP calls — use --var for individual args or --data for the full JSON object
|
|
113
|
+
spec call search_agno --var query="how to create an agent"
|
|
114
|
+
spec call read_file --data '{"path":"/tmp/hello.txt"}'
|
|
101
115
|
```
|
|
102
116
|
|
|
103
117
|
## Config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-spec-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
@@ -12,7 +12,10 @@ All output is JSON. Designed for AI agents but works for humans too.
|
|
|
12
12
|
|
|
13
13
|
WORKFLOW (follow this order):
|
|
14
14
|
1. spec load <file-or-url> Load an OpenAPI or GraphQL spec
|
|
15
|
-
|
|
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)
|
|
16
19
|
3. spec show <operation> Get params, body, response for one op
|
|
17
20
|
4. spec call <operation> [options] Execute the request
|
|
18
21
|
|
|
@@ -30,16 +33,26 @@ INSPECT:
|
|
|
30
33
|
spec show /pet/{petId} Match by path
|
|
31
34
|
spec show "GET /pet/{petId}" Match by method + path
|
|
32
35
|
spec show publishPost GraphQL operation name
|
|
36
|
+
spec show read_file MCP tool name
|
|
33
37
|
spec types List all schema/type names
|
|
34
38
|
spec types Pet Inspect one schema (compact, no $ref explosion)
|
|
35
39
|
|
|
36
40
|
EXECUTE:
|
|
37
41
|
spec call <op> --var petId=1 Path or GraphQL variables
|
|
38
42
|
spec call <op> --query status=available Query string params
|
|
39
|
-
spec call <op> --data '{"name":"Rex"}' JSON body
|
|
43
|
+
spec call <op> --data '{"name":"Rex"}' JSON body / MCP tool arguments
|
|
40
44
|
spec call <op> --data-file /tmp/query.json JSON body from file (avoids shell escaping)
|
|
41
|
-
spec call <op> --header X-Custom=val Extra headers
|
|
42
|
-
spec call <op> --method PUT Override HTTP method
|
|
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"}'
|
|
43
56
|
|
|
44
57
|
CONFIG (persisted in .spec-cli/config.json):
|
|
45
58
|
spec config set baseUrl https://api.example.com
|
package/src/commands/call.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "fs";
|
|
|
2
2
|
import { getSpec, getConfig } from "../store.js";
|
|
3
3
|
import { out } from "../output.js";
|
|
4
4
|
import { parseArgs, parseKV } from "../args.js";
|
|
5
|
+
import { createMcpClient } from "../mcp-client.js";
|
|
5
6
|
|
|
6
7
|
export async function callOperation(args) {
|
|
7
8
|
const { flags, positional } = parseArgs(args);
|
|
@@ -20,11 +21,39 @@ export async function callOperation(args) {
|
|
|
20
21
|
|
|
21
22
|
if (spec.type === "openapi") {
|
|
22
23
|
await callOpenAPI(spec, config, target, flags);
|
|
24
|
+
} else if (spec.type === "mcp") {
|
|
25
|
+
await callMCP(spec, target, flags);
|
|
23
26
|
} else {
|
|
24
27
|
await callGraphQL(spec, config, target, flags);
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
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
|
+
|
|
28
57
|
async function callOpenAPI(spec, config, target, flags) {
|
|
29
58
|
const lower = target.toLowerCase();
|
|
30
59
|
|
package/src/commands/list.js
CHANGED
|
@@ -36,6 +36,12 @@ export async function listOperations(args) {
|
|
|
36
36
|
fullOps[i].tags?.some((t) => t.toLowerCase().includes(tag))
|
|
37
37
|
);
|
|
38
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
|
+
);
|
|
39
45
|
} else {
|
|
40
46
|
// graphql
|
|
41
47
|
operations = spec.operations.map((op) =>
|
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
|
@@ -12,6 +12,8 @@ export async function showOperation(args) {
|
|
|
12
12
|
|
|
13
13
|
if (spec.type === "openapi") {
|
|
14
14
|
showOpenAPI(spec, target);
|
|
15
|
+
} else if (spec.type === "mcp") {
|
|
16
|
+
showMCP(spec, target);
|
|
15
17
|
} else {
|
|
16
18
|
showGraphQL(spec, target);
|
|
17
19
|
}
|
|
@@ -87,6 +89,18 @@ function showGraphQL(spec, target) {
|
|
|
87
89
|
});
|
|
88
90
|
}
|
|
89
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
|
+
|
|
90
104
|
// --- Helpers ---
|
|
91
105
|
|
|
92
106
|
function resolveRef(obj, root) {
|
|
@@ -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
|
+
}
|