superacli 1.0.0
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/.env.example +14 -0
- package/README.md +173 -0
- package/cli/adapters/http.js +72 -0
- package/cli/adapters/mcp.js +193 -0
- package/cli/adapters/openapi.js +160 -0
- package/cli/ask.js +208 -0
- package/cli/config.js +133 -0
- package/cli/executor.js +117 -0
- package/cli/help-json.js +46 -0
- package/cli/mcp-local.js +72 -0
- package/cli/plan-runtime.js +32 -0
- package/cli/planner.js +67 -0
- package/cli/skills.js +240 -0
- package/cli/supercli.js +704 -0
- package/docs/features/adapters.md +25 -0
- package/docs/features/agent-friendly.md +28 -0
- package/docs/features/ask.md +32 -0
- package/docs/features/config-sync.md +22 -0
- package/docs/features/execution-plans.md +25 -0
- package/docs/features/observability.md +22 -0
- package/docs/features/skills.md +25 -0
- package/docs/features/storage.md +25 -0
- package/docs/features/workflows.md +33 -0
- package/docs/initial/AGENTS_FRIENDLY_TOOLS.md +553 -0
- package/docs/initial/agent-friendly.md +447 -0
- package/docs/initial/architecture.md +436 -0
- package/docs/initial/built-in-mcp-server.md +64 -0
- package/docs/initial/command-plan.md +532 -0
- package/docs/initial/core-features-2.md +428 -0
- package/docs/initial/core-features.md +366 -0
- package/docs/initial/dag.md +20 -0
- package/docs/initial/description.txt +9 -0
- package/docs/initial/idea.txt +564 -0
- package/docs/initial/initial-spec-details.md +726 -0
- package/docs/initial/initial-spec.md +731 -0
- package/docs/initial/mcp-local-mode.md +53 -0
- package/docs/initial/mcp-sse-mode.md +54 -0
- package/docs/initial/skills-support.md +246 -0
- package/docs/initial/storage-adapter-example.md +155 -0
- package/docs/initial/supercli-vs-gwc.md +109 -0
- package/examples/mcp-sse/install-demo.js +86 -0
- package/examples/mcp-sse/server.js +81 -0
- package/examples/mcp-stdio/install-demo.js +78 -0
- package/examples/mcp-stdio/server.js +50 -0
- package/package.json +21 -0
- package/server/app.js +59 -0
- package/server/public/app.js +18 -0
- package/server/routes/ask.js +92 -0
- package/server/routes/commands.js +126 -0
- package/server/routes/config.js +58 -0
- package/server/routes/jobs.js +122 -0
- package/server/routes/mcp.js +79 -0
- package/server/routes/plans.js +134 -0
- package/server/routes/specs.js +79 -0
- package/server/services/configService.js +88 -0
- package/server/storage/adapter.js +32 -0
- package/server/storage/file.js +64 -0
- package/server/storage/mongo.js +55 -0
- package/server/views/command-edit.ejs +110 -0
- package/server/views/commands.ejs +49 -0
- package/server/views/jobs.ejs +72 -0
- package/server/views/layout.ejs +42 -0
- package/server/views/mcp.ejs +80 -0
- package/server/views/partials/foot.ejs +5 -0
- package/server/views/partials/head.ejs +27 -0
- package/server/views/specs.ejs +91 -0
- package/tests/test-cli.js +367 -0
- package/tests/test-mcp.js +189 -0
- package/tests/test-openapi.js +101 -0
package/.env.example
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Storage Configuration
|
|
2
|
+
# Set to "true" to use MongoDB, otherwise uses local JSON files
|
|
3
|
+
SUPERCLI_USE_MONGO=false
|
|
4
|
+
SUPERCLI_STORAGE_DIR=./supercli_storage
|
|
5
|
+
|
|
6
|
+
# MongoDB connection (only used if SUPERCLI_USE_MONGO=true)
|
|
7
|
+
MONGO_URL=mongodb://127.0.0.1:27017
|
|
8
|
+
SUPERCLI_DB=supercli
|
|
9
|
+
|
|
10
|
+
# Server port
|
|
11
|
+
PORT=3000
|
|
12
|
+
|
|
13
|
+
# CLI server URL (used by CLI runtime)
|
|
14
|
+
SUPERCLI_SERVER=http://127.0.0.1:3000
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# SuperCLI
|
|
2
|
+
|
|
3
|
+
Config-driven, AI-friendly CLI that dynamically generates commands from cloud configuration.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Web UI (EJS + Vue3 + DaisyUI)
|
|
9
|
+
│
|
|
10
|
+
REST API
|
|
11
|
+
│
|
|
12
|
+
NodeJS + MongoDB
|
|
13
|
+
│
|
|
14
|
+
CLI Runtime
|
|
15
|
+
│
|
|
16
|
+
┌────────┼────────┐
|
|
17
|
+
OpenAPI HTTP MCP
|
|
18
|
+
Adapter Adapter Adapter
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Quick usage (no install, local-only by default)
|
|
25
|
+
npx supercli help
|
|
26
|
+
npx supercli skills teach
|
|
27
|
+
|
|
28
|
+
# Install
|
|
29
|
+
npm install
|
|
30
|
+
|
|
31
|
+
# Configure (copy and edit)
|
|
32
|
+
cp .env.example .env
|
|
33
|
+
|
|
34
|
+
# Start server (defaults to local JSON files, no MongoDB required!)
|
|
35
|
+
npm start
|
|
36
|
+
# Or alternatively, start via CLI:
|
|
37
|
+
# supercli --server
|
|
38
|
+
|
|
39
|
+
# Open Web UI
|
|
40
|
+
open http://localhost:3000
|
|
41
|
+
|
|
42
|
+
# CLI usage
|
|
43
|
+
node cli/supercli.js help
|
|
44
|
+
node cli/supercli.js commands
|
|
45
|
+
node cli/supercli.js <namespace> <resource> <action> [--args]
|
|
46
|
+
|
|
47
|
+
# Optional: sync commands from a remote SuperCLI server
|
|
48
|
+
export SUPERCLI_SERVER=http://localhost:3000
|
|
49
|
+
node cli/supercli.js sync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## CLI Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Discovery
|
|
56
|
+
supercli help # List namespaces
|
|
57
|
+
supercli <namespace> # List resources
|
|
58
|
+
supercli <namespace> <resource> # List actions
|
|
59
|
+
|
|
60
|
+
# Inspection
|
|
61
|
+
supercli inspect <ns> <res> <act> # Command details + schema
|
|
62
|
+
supercli <ns> <res> <act> --schema # Input/output schema
|
|
63
|
+
|
|
64
|
+
# Execution
|
|
65
|
+
supercli <ns> <res> <act> --arg value # Execute command
|
|
66
|
+
supercli <ns> <res> <act> --compact # Token-optimized output
|
|
67
|
+
|
|
68
|
+
# Plans (DAG)
|
|
69
|
+
supercli plan <ns> <res> <act> [--args] # Dry-run execution plan
|
|
70
|
+
supercli execute <plan_id> # Execute stored plan
|
|
71
|
+
|
|
72
|
+
# Skills (LLM bootstrap)
|
|
73
|
+
supercli skills list --json # Minimal skill metadata (name, description)
|
|
74
|
+
supercli skills get <ns.res.act> # Emit SKILL.md (default format)
|
|
75
|
+
supercli skills teach # Emit starter meta-skill (default format)
|
|
76
|
+
supercli skills get <ns.res.act> --show-dag
|
|
77
|
+
|
|
78
|
+
# Natural Language (AI)
|
|
79
|
+
export OPENAI_BASE_URL=https://api.openai.com/v1 # Enable local AI resolution
|
|
80
|
+
supercli ask "list the posts and summarize them" # Execute natural language queries
|
|
81
|
+
|
|
82
|
+
# Config & Server
|
|
83
|
+
supercli sync # Sync local cache from SUPERCLI_SERVER (when set)
|
|
84
|
+
supercli config show # Show cache info
|
|
85
|
+
supercli --server # Start the SuperCLI backend server directly
|
|
86
|
+
|
|
87
|
+
# Local MCP registry (no server required)
|
|
88
|
+
supercli mcp list
|
|
89
|
+
supercli mcp add summarize-local --url http://127.0.0.1:8787
|
|
90
|
+
supercli mcp remove summarize-local
|
|
91
|
+
|
|
92
|
+
# Stdio MCP demo (no server required)
|
|
93
|
+
node examples/mcp-stdio/install-demo.js
|
|
94
|
+
supercli ai text summarize --text "Hello world" --json
|
|
95
|
+
|
|
96
|
+
# Remote MCP SSE/HTTP demo
|
|
97
|
+
node examples/mcp-sse/server.js
|
|
98
|
+
node examples/mcp-sse/install-demo.js
|
|
99
|
+
supercli ai text summarize_remote --text "Hello world" --json
|
|
100
|
+
|
|
101
|
+
# Agent capability discovery
|
|
102
|
+
supercli --help-json # Machine-readable capabilities
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Output Modes
|
|
106
|
+
|
|
107
|
+
| Flag | Output |
|
|
108
|
+
|-------------|-------------------------------------------|
|
|
109
|
+
| (default) | JSON if piped, human-readable if TTY |
|
|
110
|
+
| `--json` | Structured JSON envelope |
|
|
111
|
+
| `--human` | Formatted tables and key-value output |
|
|
112
|
+
| `--compact` | Compressed JSON (shortened keys) |
|
|
113
|
+
|
|
114
|
+
## Output Envelope
|
|
115
|
+
|
|
116
|
+
Every command returns a deterministic envelope:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"version": "1.0",
|
|
121
|
+
"command": "namespace.resource.action",
|
|
122
|
+
"duration_ms": 42,
|
|
123
|
+
"data": { ... }
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Exit Codes
|
|
128
|
+
|
|
129
|
+
| Code | Type | Action |
|
|
130
|
+
|---------|---------------------|----------------------------|
|
|
131
|
+
| 0 | success | Proceed |
|
|
132
|
+
| 82 | validation_error | Fix input |
|
|
133
|
+
| 85 | invalid_argument | Fix argument |
|
|
134
|
+
| 92 | resource_not_found | Try different resource |
|
|
135
|
+
| 105 | integration_error | Retry with backoff |
|
|
136
|
+
| 110 | internal_error | Report bug |
|
|
137
|
+
|
|
138
|
+
## API Endpoints
|
|
139
|
+
|
|
140
|
+
| Method | Endpoint | Description |
|
|
141
|
+
|--------|-------------------------------|--------------------------|
|
|
142
|
+
| GET | `/api/config` | Full CLI config |
|
|
143
|
+
| GET | `/api/tree` | List namespaces |
|
|
144
|
+
| GET | `/api/tree/:ns` | List resources |
|
|
145
|
+
| GET | `/api/tree/:ns/:res` | List actions |
|
|
146
|
+
| GET | `/api/command/:ns/:res/:act` | Full command spec |
|
|
147
|
+
| CRUD | `/api/commands` | Manage commands |
|
|
148
|
+
| CRUD | `/api/specs` | Manage OpenAPI specs |
|
|
149
|
+
| CRUD | `/api/mcp` | Manage MCP servers |
|
|
150
|
+
| CRUD | `/api/plans` | Execution plans |
|
|
151
|
+
| GET | `/api/jobs` | Execution history |
|
|
152
|
+
| GET | `/api/jobs/stats` | Aggregate stats |
|
|
153
|
+
|
|
154
|
+
## Adapters
|
|
155
|
+
|
|
156
|
+
- **http** — Raw HTTP requests (method, url, headers)
|
|
157
|
+
- **openapi** — Resolves operation from OpenAPI spec
|
|
158
|
+
- **mcp** — Calls MCP server tools (supports both HTTP endpoints and local Stdio processes)
|
|
159
|
+
|
|
160
|
+
## Tech Stack
|
|
161
|
+
|
|
162
|
+
- NodeJS + Express
|
|
163
|
+
- Pluggable KV Storage (Local JSON files by default, MongoDB optional)
|
|
164
|
+
- EJS + Vue3 CDN + Tailwind CDN + DaisyUI CDN
|
|
165
|
+
- Zero build tools
|
|
166
|
+
|
|
167
|
+
## Contributors
|
|
168
|
+
|
|
169
|
+
Contributions are welcome! If you have ideas for improvements, new adapters, or bug fixes, just send a Pull Request (PR) and I will review it.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// HTTP Adapter
|
|
2
|
+
// Raw HTTP calls based on adapterConfig: method, url, headers, body
|
|
3
|
+
|
|
4
|
+
async function execute(cmd, flags, context) {
|
|
5
|
+
const config = cmd.adapterConfig || {}
|
|
6
|
+
let url = config.url
|
|
7
|
+
let method = (config.method || "GET").toUpperCase()
|
|
8
|
+
const headers = { ...(config.headers || {}) }
|
|
9
|
+
|
|
10
|
+
if (!url) {
|
|
11
|
+
throw new Error("HTTP adapter requires 'url' in adapterConfig")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Replace {param} placeholders in URL with flag values
|
|
15
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
16
|
+
if (["human", "json", "compact"].includes(k)) continue
|
|
17
|
+
url = url.replace(`{${k}}`, encodeURIComponent(v))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Build query string from flags for GET requests
|
|
21
|
+
if (method === "GET") {
|
|
22
|
+
const queryFlags = Object.entries(flags).filter(([k]) =>
|
|
23
|
+
!["human", "json", "compact"].includes(k) && !url.includes(encodeURIComponent(flags[k]))
|
|
24
|
+
)
|
|
25
|
+
if (queryFlags.length > 0 && !url.includes("?")) {
|
|
26
|
+
// Only add flags that weren't used as path params
|
|
27
|
+
const remaining = queryFlags.filter(([k]) => !config.url.includes(`{${k}}`))
|
|
28
|
+
if (remaining.length > 0) {
|
|
29
|
+
url += "?" + remaining.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fetchOpts = { method, headers }
|
|
35
|
+
|
|
36
|
+
// Build body for non-GET methods
|
|
37
|
+
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
38
|
+
if (config.body) {
|
|
39
|
+
fetchOpts.body = JSON.stringify(config.body)
|
|
40
|
+
} else {
|
|
41
|
+
const bodyObj = {}
|
|
42
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
43
|
+
if (!["human", "json", "compact"].includes(k)) {
|
|
44
|
+
bodyObj[k] = v
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (Object.keys(bodyObj).length > 0) {
|
|
48
|
+
fetchOpts.body = JSON.stringify(bodyObj)
|
|
49
|
+
headers["Content-Type"] = "application/json"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const r = await fetch(url, fetchOpts)
|
|
55
|
+
|
|
56
|
+
if (!r.ok) {
|
|
57
|
+
const text = await r.text().catch(() => "")
|
|
58
|
+
throw Object.assign(new Error(`HTTP request failed: ${r.status} ${r.statusText} ${text}`), {
|
|
59
|
+
code: r.status >= 500 ? 105 : 92,
|
|
60
|
+
type: r.status >= 500 ? "integration_error" : "resource_not_found",
|
|
61
|
+
recoverable: r.status >= 500
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const contentType = r.headers.get("content-type") || ""
|
|
66
|
+
if (contentType.includes("json")) {
|
|
67
|
+
return r.json()
|
|
68
|
+
}
|
|
69
|
+
return { raw: await r.text() }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { execute }
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// MCP Adapter
|
|
2
|
+
// Supports HTTP MCP endpoints and local stdio MCP commands.
|
|
3
|
+
|
|
4
|
+
const { spawn } = require("child_process");
|
|
5
|
+
|
|
6
|
+
async function callStdioTool(command, args, payload, timeoutMs) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(command, args || [], {
|
|
9
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
10
|
+
shell: !Array.isArray(args) || args.length === 0,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let stdout = "";
|
|
14
|
+
let stderr = "";
|
|
15
|
+
let settled = false;
|
|
16
|
+
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
if (settled) return;
|
|
19
|
+
settled = true;
|
|
20
|
+
child.kill("SIGTERM");
|
|
21
|
+
reject(
|
|
22
|
+
Object.assign(
|
|
23
|
+
new Error(`MCP stdio call timed out after ${timeoutMs}ms`),
|
|
24
|
+
{
|
|
25
|
+
code: 105,
|
|
26
|
+
type: "integration_error",
|
|
27
|
+
recoverable: true,
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
|
|
33
|
+
child.stdout.setEncoding("utf-8");
|
|
34
|
+
child.stderr.setEncoding("utf-8");
|
|
35
|
+
child.stdout.on("data", (chunk) => {
|
|
36
|
+
stdout += chunk;
|
|
37
|
+
});
|
|
38
|
+
child.stderr.on("data", (chunk) => {
|
|
39
|
+
stderr += chunk;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
child.on("error", (err) => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settled = true;
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
reject(
|
|
47
|
+
Object.assign(
|
|
48
|
+
new Error(`Failed to start MCP stdio command: ${err.message}`),
|
|
49
|
+
{
|
|
50
|
+
code: 105,
|
|
51
|
+
type: "integration_error",
|
|
52
|
+
recoverable: true,
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
child.on("close", (code) => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
if (code !== 0) {
|
|
63
|
+
reject(
|
|
64
|
+
Object.assign(
|
|
65
|
+
new Error(
|
|
66
|
+
`MCP stdio command exited with code ${code}: ${stderr.trim()}`,
|
|
67
|
+
),
|
|
68
|
+
{
|
|
69
|
+
code: 105,
|
|
70
|
+
type: "integration_error",
|
|
71
|
+
recoverable: true,
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
resolve(JSON.parse(stdout.trim()));
|
|
79
|
+
} catch {
|
|
80
|
+
reject(
|
|
81
|
+
Object.assign(
|
|
82
|
+
new Error(`MCP stdio response is not valid JSON: ${stdout.trim()}`),
|
|
83
|
+
{
|
|
84
|
+
code: 105,
|
|
85
|
+
type: "integration_error",
|
|
86
|
+
recoverable: true,
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
child.stdin.write(JSON.stringify(payload));
|
|
94
|
+
child.stdin.end();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function resolveHttpServerUrl(config, context) {
|
|
99
|
+
if (config.url) return config.url;
|
|
100
|
+
|
|
101
|
+
if (context.config && Array.isArray(context.config.mcp_servers)) {
|
|
102
|
+
const local = context.config.mcp_servers.find(
|
|
103
|
+
(s) => s && s.name === config.server,
|
|
104
|
+
);
|
|
105
|
+
if (local) return local.url;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (context.server) {
|
|
109
|
+
const r = await fetch(`${context.server}/api/mcp?format=json`);
|
|
110
|
+
if (!r.ok) {
|
|
111
|
+
throw Object.assign(
|
|
112
|
+
new Error(`Failed to fetch MCP servers list: ${r.status}`),
|
|
113
|
+
{
|
|
114
|
+
code: 105,
|
|
115
|
+
type: "integration_error",
|
|
116
|
+
recoverable: true,
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const servers = await r.json();
|
|
121
|
+
const srv = servers.find((s) => s.name === config.server);
|
|
122
|
+
if (srv) return srv.url;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw Object.assign(
|
|
126
|
+
new Error(
|
|
127
|
+
`MCP server '${config.server}' not found in local config. Add one with: supercli mcp add ${config.server} --url <mcp_url> or run supercli sync`,
|
|
128
|
+
),
|
|
129
|
+
{
|
|
130
|
+
code: 85,
|
|
131
|
+
type: "invalid_argument",
|
|
132
|
+
recoverable: false,
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function execute(cmd, flags, context) {
|
|
138
|
+
const config = cmd.adapterConfig || {};
|
|
139
|
+
const toolName = config.tool;
|
|
140
|
+
const hasHttpSource = !!(config.server || config.url);
|
|
141
|
+
const hasStdioSource = !!config.command;
|
|
142
|
+
|
|
143
|
+
if (!toolName || (!hasHttpSource && !hasStdioSource)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"MCP adapter requires 'tool' and one of: 'server', 'url', or 'command' in adapterConfig",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const input = {};
|
|
150
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
151
|
+
if (!["human", "json", "compact"].includes(k)) {
|
|
152
|
+
input[k] = v;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (hasStdioSource) {
|
|
157
|
+
const commandArgs = Array.isArray(config.commandArgs)
|
|
158
|
+
? config.commandArgs
|
|
159
|
+
: [];
|
|
160
|
+
const timeoutMs =
|
|
161
|
+
Number(config.timeout_ms) > 0 ? Number(config.timeout_ms) : 10000;
|
|
162
|
+
return callStdioTool(
|
|
163
|
+
config.command,
|
|
164
|
+
commandArgs,
|
|
165
|
+
{ tool: toolName, input },
|
|
166
|
+
timeoutMs,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const resolvedUrl = await resolveHttpServerUrl(config, context);
|
|
171
|
+
const toolUrl = resolvedUrl.replace(/\/+$/, "");
|
|
172
|
+
const tr = await fetch(`${toolUrl}/tool`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: JSON.stringify({ tool: toolName, input }),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!tr.ok) {
|
|
179
|
+
const text = await tr.text().catch(() => "");
|
|
180
|
+
throw Object.assign(
|
|
181
|
+
new Error(`MCP tool call failed: ${tr.status} ${text}`),
|
|
182
|
+
{
|
|
183
|
+
code: 105,
|
|
184
|
+
type: "integration_error",
|
|
185
|
+
recoverable: true,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return tr.json();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { execute };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// OpenAPI Adapter
|
|
2
|
+
// Fetches spec, resolves operation by operationId, builds URL, executes HTTP call
|
|
3
|
+
|
|
4
|
+
const specCache = {};
|
|
5
|
+
|
|
6
|
+
async function fetchSpec(specName, context) {
|
|
7
|
+
if (specCache[specName]) return specCache[specName];
|
|
8
|
+
|
|
9
|
+
const localSpecs =
|
|
10
|
+
context.config && Array.isArray(context.config.specs)
|
|
11
|
+
? context.config.specs
|
|
12
|
+
: [];
|
|
13
|
+
const localSpec = localSpecs.find((s) => s && s.name === specName);
|
|
14
|
+
if (localSpec) {
|
|
15
|
+
const sr = await fetch(localSpec.url);
|
|
16
|
+
if (!sr.ok)
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Failed to fetch OpenAPI spec from ${localSpec.url}: ${sr.status}`,
|
|
19
|
+
);
|
|
20
|
+
const specDoc = await sr.json();
|
|
21
|
+
specCache[specName] = { ...specDoc, _auth: localSpec.auth };
|
|
22
|
+
return specCache[specName];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!context.server) {
|
|
26
|
+
throw Object.assign(
|
|
27
|
+
new Error(
|
|
28
|
+
`OpenAPI spec '${specName}' not found in local config. Run supercli sync to load specs.`,
|
|
29
|
+
),
|
|
30
|
+
{
|
|
31
|
+
code: 85,
|
|
32
|
+
type: "invalid_argument",
|
|
33
|
+
recoverable: false,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fetch spec URL from server
|
|
39
|
+
const r = await fetch(`${context.server}/api/specs?format=json`);
|
|
40
|
+
if (!r.ok) throw new Error(`Failed to fetch specs list: ${r.status}`);
|
|
41
|
+
const specs = await r.json();
|
|
42
|
+
const spec = specs.find((s) => s.name === specName);
|
|
43
|
+
if (!spec) throw new Error(`OpenAPI spec '${specName}' not found`);
|
|
44
|
+
|
|
45
|
+
// Fetch the actual OpenAPI spec
|
|
46
|
+
const sr = await fetch(spec.url);
|
|
47
|
+
if (!sr.ok)
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Failed to fetch OpenAPI spec from ${spec.url}: ${sr.status}`,
|
|
50
|
+
);
|
|
51
|
+
const specDoc = await sr.json();
|
|
52
|
+
specCache[specName] = { ...specDoc, _auth: spec.auth };
|
|
53
|
+
return specCache[specName];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findOperation(spec, operationId) {
|
|
57
|
+
const paths = spec.paths || {};
|
|
58
|
+
for (const [pathStr, methods] of Object.entries(paths)) {
|
|
59
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
60
|
+
if (op.operationId === operationId) {
|
|
61
|
+
return { path: pathStr, method: method.toUpperCase(), operation: op };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Operation '${operationId}' not found in spec`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildUrl(baseUrl, pathStr, method, operation, flags) {
|
|
69
|
+
let url = pathStr;
|
|
70
|
+
|
|
71
|
+
// Replace path parameters
|
|
72
|
+
const pathParams = (operation.parameters || []).filter(
|
|
73
|
+
(p) => p.in === "path",
|
|
74
|
+
);
|
|
75
|
+
for (const p of pathParams) {
|
|
76
|
+
if (flags[p.name] !== undefined) {
|
|
77
|
+
url = url.replace(`{${p.name}}`, encodeURIComponent(flags[p.name]));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Build query parameters
|
|
82
|
+
const queryParams = (operation.parameters || []).filter(
|
|
83
|
+
(p) => p.in === "query",
|
|
84
|
+
);
|
|
85
|
+
const query = [];
|
|
86
|
+
for (const p of queryParams) {
|
|
87
|
+
if (flags[p.name] !== undefined) {
|
|
88
|
+
query.push(
|
|
89
|
+
`${encodeURIComponent(p.name)}=${encodeURIComponent(flags[p.name])}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let fullUrl = baseUrl.replace(/\/+$/, "") + url;
|
|
95
|
+
if (query.length > 0) {
|
|
96
|
+
fullUrl += "?" + query.join("&");
|
|
97
|
+
}
|
|
98
|
+
return { url: fullUrl, method };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function execute(cmd, flags, context) {
|
|
102
|
+
const config = cmd.adapterConfig || {};
|
|
103
|
+
const specName = config.spec;
|
|
104
|
+
const operationId = config.operationId;
|
|
105
|
+
|
|
106
|
+
if (!specName || !operationId) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"OpenAPI adapter requires 'spec' and 'operationId' in adapterConfig",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const spec = await fetchSpec(specName, context);
|
|
113
|
+
const { path: pathStr, method, operation } = findOperation(spec, operationId);
|
|
114
|
+
|
|
115
|
+
// Determine base URL from spec
|
|
116
|
+
const baseUrl =
|
|
117
|
+
(spec.servers && spec.servers[0] && spec.servers[0].url) || "";
|
|
118
|
+
const { url, method: httpMethod } = buildUrl(
|
|
119
|
+
baseUrl,
|
|
120
|
+
pathStr,
|
|
121
|
+
method,
|
|
122
|
+
operation,
|
|
123
|
+
flags,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const fetchOpts = { method: httpMethod, headers: {} };
|
|
127
|
+
|
|
128
|
+
// Handle request body for POST/PUT/PATCH
|
|
129
|
+
if (["POST", "PUT", "PATCH"].includes(httpMethod)) {
|
|
130
|
+
const bodyObj = {};
|
|
131
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
132
|
+
if (!["human", "json", "compact"].includes(k)) {
|
|
133
|
+
bodyObj[k] = v;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
fetchOpts.body = JSON.stringify(bodyObj);
|
|
137
|
+
fetchOpts.headers["Content-Type"] = "application/json";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const r = await fetch(url, fetchOpts);
|
|
141
|
+
if (!r.ok) {
|
|
142
|
+
const text = await r.text().catch(() => "");
|
|
143
|
+
throw Object.assign(
|
|
144
|
+
new Error(`API call failed: ${r.status} ${r.statusText} ${text}`),
|
|
145
|
+
{
|
|
146
|
+
code: r.status >= 500 ? 105 : 92,
|
|
147
|
+
type: r.status >= 500 ? "integration_error" : "resource_not_found",
|
|
148
|
+
recoverable: r.status >= 500,
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const contentType = r.headers.get("content-type") || "";
|
|
154
|
+
if (contentType.includes("json")) {
|
|
155
|
+
return r.json();
|
|
156
|
+
}
|
|
157
|
+
return { raw: await r.text() };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { execute };
|