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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # api-spec-cli
2
2
 
3
- CLI for AI agents to explore and call OpenAPI and GraphQL APIs. Output is JSON by default — compact, parseable, token-efficient.
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+ or Bun. No other dependencies.
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
- 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)
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": "graphql", "operationCount": 114, "source": "https://gql.hashnode.com" }
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 operation IDs, no schemas. Use `--filter`, `--tag`, `--limit` to narrow down.
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": "graphql",
52
- "total": 5,
53
- "showing": 5,
63
+ "type": "mcp",
64
+ "total": 1,
65
+ "showing": 1,
54
66
  "operations": [
55
- { "id": "publishPost", "kind": "mutation" },
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 (summary, tags, args).
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 call an operation — params, body schema, response, and related types — in one call.
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
- 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>`.
86
+ MCP output includes the full `inputSchema` so you know exactly what arguments to pass.
75
87
 
76
- ### Step 4: Drill into types (if needed)
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.0",
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
- 2. spec list Browse operations (compact IDs)
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
@@ -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
 
@@ -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) =>
@@ -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();
@@ -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
+ }