@powerduck/dev-mcp-server 0.1.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/LICENSE +21 -0
- package/README.md +202 -0
- package/dist/cli.cjs +2 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.mjs +2 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +384 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.mjs +2 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Powerduck limited
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# @powerduck/dev-mcp-server
|
|
2
|
+
|
|
3
|
+
A developer-facing [Model Context Protocol](https://modelcontextprotocol.io/) server that gives AI coding agents (Claude Code, Codex CLI, Cursor, Claude Desktop) **accurate, dereferenced OpenAPI facts** for the backend you are implementing, plus **real verification against your local server**: ping, single requests, contract tests, and ordered multi-step scenarios.
|
|
4
|
+
|
|
5
|
+
> [PowerDuck](https://www.powerduck.com/) — design, debug, and verify APIs with AI.
|
|
6
|
+
|
|
7
|
+
This server is for the **people building the API**. It answers questions like "what exactly must `POST /orders` accept and return?" so the agent writes handlers that match the specification. It is **not** the server that exposes an API to AI clients for calling a running service; that is a separate package.
|
|
8
|
+
|
|
9
|
+
## What it provides
|
|
10
|
+
|
|
11
|
+
### Contract facts
|
|
12
|
+
|
|
13
|
+
Read-only truth, re-read from disk on every request (the document is cached by file mtime, so edits are visible without reconnecting):
|
|
14
|
+
|
|
15
|
+
| Tool | Purpose | Key arguments |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `spec_overview` | Title, version, servers, tags, protocols, security schemes, counts. | — |
|
|
18
|
+
| `list_operations` | Compact, filterable, paginated operation index. | `tag`, `method`, `protocol`, `secured`, `search`, `pageSize` (1–500, default 100), `cursor` |
|
|
19
|
+
| `get_operation` | Full implementation contract: parameters with validation rules, request body schemas/examples, every response (status, headers, body), effective security, servers. | One of `ref` (`"POST /orders"`), `operationId`, or `method` + `path` |
|
|
20
|
+
| `get_schema` | One dereferenced component schema. | `name` or `ref` (`#/components/schemas/...`) |
|
|
21
|
+
| `validate_spec` | Re-reads the file and returns errors/warnings with locations. | — |
|
|
22
|
+
| `get_auth_requirements` | All security schemes and, for an operation, the exact auth it requires. | Optional operation locator |
|
|
23
|
+
|
|
24
|
+
### Verification against a running backend
|
|
25
|
+
|
|
26
|
+
These tools execute real requests through [`@powerduck/openapi-cli`](https://www.powerduck.com/) and report normalized responses plus spec-derived assertion results. `baseUrl` defaults to the first concrete server URL in the specification.
|
|
27
|
+
|
|
28
|
+
| Tool | Purpose | Key arguments |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `ping_target` | Reachability probe; returns status and latency. | `baseUrl`, `timeoutMs` (100–60000, default 5000) |
|
|
31
|
+
| `send_request` | Send one operation with overrides and return per-assertion results. | locator, `baseUrl`, `timeoutMs`, `headers`, `variables`, `proxy`, `values` (`{path, query, header, body}`), `assertions[]` |
|
|
32
|
+
| `run_contract_tests` | Batch-run operations and evaluate the contract; returns a summary and per-operation results. | `baseUrl`, `concurrency` (1–20), `methods[]`, `tags[]`, `paths[]` (regex), `operationIds[]`, plus the common target options |
|
|
33
|
+
| `validate_scenario` | Statically resolve an ordered multi-step scenario; never sends traffic. | `scenario` |
|
|
34
|
+
| `run_scenario` | Run an ordered, stateful scenario with shared variables, extraction, and assertions. | `scenario`, `baseUrl`, `timeoutMs`, `variables` |
|
|
35
|
+
|
|
36
|
+
Common target options: `baseUrl`, `timeoutMs` (up to 600000), `headers` (string map), `variables` (`{{name}}` string map), and `proxy`.
|
|
37
|
+
|
|
38
|
+
A scenario is an ordered list of operations with a shared variable scope:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"name": "create then fetch a pet",
|
|
43
|
+
"stopOnFailure": true,
|
|
44
|
+
"steps": [
|
|
45
|
+
{
|
|
46
|
+
"ref": "POST /pets",
|
|
47
|
+
"request": {
|
|
48
|
+
"extract": [{ "name": "petId", "from": "body", "path": "$.id" }],
|
|
49
|
+
"assertions": [{ "name": "created", "assert": "status", "value": 201 }]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"ref": "GET /pets/{petId}",
|
|
54
|
+
"request": {
|
|
55
|
+
"values": { "path": { "petId": "{{petId}}" } },
|
|
56
|
+
"assertions": [
|
|
57
|
+
{ "name": "ok", "assert": "status", "value": 200 },
|
|
58
|
+
{ "name": "id roundtrips", "assert": "jsonPath", "path": "$.id", "exists": true }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Each step supports `extract` (`from: body | header | status`, with `path` JSONPath or header `key`), declarative `assertions` (`status`, `header`, `bodyContains`, `bodyEquals`, `jsonPath`, `responseTime`), per-step `values`/`serverUrl`, and `skip`. A runnable copy lives in [`examples/scenario.create-fetch.json`](examples/scenario.create-fetch.json).
|
|
67
|
+
|
|
68
|
+
Long runs emit `notifications/progress` when the client passes a progress token, and honor `notifications/cancelled` (the scenario aborts and remaining steps are reported as skipped).
|
|
69
|
+
|
|
70
|
+
Documents that are not OpenAPI 3.x are upgraded to 3.2 when that conversion is safe; otherwise facts are served best-effort from the original document and `validate_spec` reports the problems. Internal `$ref` values are inlined (cycle-safe); unresolved references fall back to the original `$ref`.
|
|
71
|
+
|
|
72
|
+
### Resources
|
|
73
|
+
|
|
74
|
+
| URI | Content |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `powerduck://spec/source` | Raw source document (YAML or JSON). |
|
|
77
|
+
| `powerduck://spec/overview` | JSON overview. |
|
|
78
|
+
| `powerduck://spec/operations` | JSON operation index (up to 500). |
|
|
79
|
+
| `powerduck://spec/operation/{ref}` | One operation; `ref` is the URL-encoded `"METHOD /path"`, e.g. `operation/POST%20/orders`. |
|
|
80
|
+
| `powerduck://spec/schema/{name}` | One component schema. |
|
|
81
|
+
|
|
82
|
+
Planned for later milestones: mock/stub tools for unavailable dependencies (payment, OTP), scenario persistence under `.powerduck/`, drift detection, and a Streamable HTTP transport with tokens.
|
|
83
|
+
|
|
84
|
+
## Run
|
|
85
|
+
|
|
86
|
+
Requires Node.js `>= 20.11`.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx -y @powerduck/dev-mcp-server --spec /absolute/path/to/openapi.yaml
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For local development, build from source and run the binary directly:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install
|
|
96
|
+
npm run build
|
|
97
|
+
node dist/cli.mjs --spec tests/fixtures/petstore.yaml
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### CLI options
|
|
101
|
+
|
|
102
|
+
| Option | Description |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `--spec <path>` | Path to the OpenAPI document (JSON or YAML). Required unless provided by config. |
|
|
105
|
+
| `--config <path>` | Path to a `powerduck.dev.json` file (defaults to `./powerduck.dev.json` when present). |
|
|
106
|
+
| `--base-url <url>` | Default local backend base URL used by verification tools when a call omits `baseUrl`. |
|
|
107
|
+
| `--project <dir>` | Backend project root (used by upcoming code-aware tools). |
|
|
108
|
+
|
|
109
|
+
### Project config (`powerduck.dev.json`)
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"spec": "./openapi/openapi.yaml",
|
|
114
|
+
"baseUrl": "http://localhost:8080",
|
|
115
|
+
"project": "./backend"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Relative paths resolve against the directory containing the config file. CLI flags override config values. This file is safe to commit; put secrets in environment variables, never in the config.
|
|
120
|
+
|
|
121
|
+
## Connect an editor
|
|
122
|
+
|
|
123
|
+
Use an absolute spec path in every configuration.
|
|
124
|
+
|
|
125
|
+
### Claude Code
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
claude mcp add powerduck-dev -- npx -y @powerduck/dev-mcp-server --spec /abs/path/openapi.yaml
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Claude Desktop
|
|
132
|
+
|
|
133
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"mcpServers": {
|
|
138
|
+
"powerduck-dev": {
|
|
139
|
+
"command": "npx",
|
|
140
|
+
"args": ["-y", "@powerduck/dev-mcp-server", "--spec", "/abs/path/openapi.yaml"]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Codex CLI
|
|
147
|
+
|
|
148
|
+
`~/.codex/config.toml`:
|
|
149
|
+
|
|
150
|
+
```toml
|
|
151
|
+
[mcp_servers.powerduck-dev]
|
|
152
|
+
command = "npx"
|
|
153
|
+
args = ["-y", "@powerduck/dev-mcp-server", "--spec", "/abs/path/openapi.yaml"]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Cursor
|
|
157
|
+
|
|
158
|
+
`.cursor/mcp.json` in the project root:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"mcpServers": {
|
|
163
|
+
"powerduck-dev": {
|
|
164
|
+
"command": "npx",
|
|
165
|
+
"args": ["-y", "@powerduck/dev-mcp-server", "--spec", "/abs/path/openapi.yaml"]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Embed programmatically
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
175
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
176
|
+
import { createDevMcpServer, SpecStore } from "@powerduck/dev-mcp-server";
|
|
177
|
+
|
|
178
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
179
|
+
const server = createDevMcpServer(new SpecStore("/abs/path/openapi.yaml"));
|
|
180
|
+
const client = new Client(
|
|
181
|
+
{ name: "my-app", version: "0.0.0" },
|
|
182
|
+
{ capabilities: {} },
|
|
183
|
+
);
|
|
184
|
+
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
185
|
+
|
|
186
|
+
const result = await client.callTool({ name: "spec_overview", arguments: {} });
|
|
187
|
+
console.log(result.content[0].text);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
A runnable copy lives in [`examples/programmatic.mjs`](examples/programmatic.mjs) (`node examples/programmatic.mjs [spec-path]` after build).
|
|
191
|
+
|
|
192
|
+
## Develop
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npm run typecheck # strict TypeScript
|
|
196
|
+
npm test # vitest: facts, in-process MCP client/server, and verification E2E
|
|
197
|
+
npm run build # tsc + tsup (ESM .mjs and CJS .cjs)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT © Powerduck limited
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var e=require("commander"),t=require("fs"),r=require("path"),o=require("@modelcontextprotocol/sdk/server/stdio.js"),n=require("js-yaml"),s=require("@powerduck/openapi-parser"),i=require("@modelcontextprotocol/sdk/server/index.js"),a=require("@modelcontextprotocol/sdk/types.js"),c=require("@powerduck/openapi-cli"),p=require("@powerduck/openapi-request");function u(e){return e&&e.__esModule?e:{default:e}}var d=u(t),l=u(r);function m(e){const t=d.default.readFileSync(e,"utf8");let r;try{r=JSON.parse(t)}catch(t){throw new Error(`Invalid JSON in ${e}: ${t instanceof Error?t.message:String(t)}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`${e} must contain a JSON object.`);const o={},n=r;return"string"==typeof n.spec&&(o.spec=n.spec),"string"==typeof n.baseUrl&&(o.baseUrl=n.baseUrl),"string"==typeof n.project&&(o.project=n.project),o}function f(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function h(e){if("string"==typeof e&&e.trim())return{message:e,severity:"error"};if(f(e)&&"string"==typeof e.message){const t="warning"===e.severity||"info"===e.severity?e.severity:"error",r=Array.isArray(e.path)?e.path.join("/"):"string"==typeof e.path?e.path:void 0;return r?{message:e.message,severity:t,path:r}:{message:e.message,severity:t}}return null}var y=class{constructor(e){this.specPath=e}specPath;cache=null;async load(e=!1){const t=d.default.statSync(this.specPath);if(!e&&this.cache&&this.cache.mtimeMs===t.mtimeMs)return this.cache.snapshot;const r=d.default.readFileSync(this.specPath,"utf8"),o=/\.ya?ml$/i.test(this.specPath),i=function(e,t){return t?n.load(e):JSON.parse(e)}(r,o);if(!f(i))throw new Error("The OpenAPI document must be a JSON or YAML object.");let a=i;const c="string"==typeof i.openapi?i.openapi:"";if(c&&!/^3\./.test(c))try{a=await s.upgradeOasTo32(i)}catch{a=i}const p=await async function(e){try{const t=await s.validate(e),r=[];for(const e of t.errors??[]){const t=h(e);t&&r.push(t)}return{valid:Boolean(t.valid),..."string"==typeof t.version?{openapiVersion:t.version}:{},issues:r}}catch(e){return{valid:!1,issues:[{message:e instanceof Error?e.message:String(e),severity:"error"}]}}}(a),u=await async function(e){try{const t=await s.dereference(e);return!t.errors?.length&&f(t.schema)?t.schema:e}catch{return e}}(a),l={path:this.specPath,document:a,dereferenced:u,sourceText:r,mediaType:o?"application/yaml":"application/json",validation:p,mtimeMs:t.mtimeMs};return this.cache={mtimeMs:t.mtimeMs,snapshot:l},l}},g=class extends Error{code;details;constructor(e,t,r){super(t),this.name="FactError",this.code=e,this.details=r}},v=["get","post","put","patch","delete","head","options","trace"],b=new Set(["get","head","put","delete","options","trace"]);function w(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function S(e){return Array.isArray(e)?e:[]}function x(e){return"string"==typeof e&&e.trim()?e:void 0}function j(e,t){if(!t.startsWith("#/"))return;let r=e;for(const e of t.slice(2).split("/")){const t=e.replace(/~1/g,"/").replace(/~0/g,"~");if(!w(r)||!(t in r))return;r=r[t]}return r}function q(e,t,r=new Set,o=0){if(o>6)return e;if(w(e)){if("string"==typeof e.$ref){const n=e.$ref;if(r.has(n))return{$ref:n};const s=j(t,n);if(void 0===s)return e;const i=new Set(r).add(n);return q(s,t,i,o+1)}const n={};for(const[s,i]of Object.entries(e))n[s]=q(i,t,r,o+1);return n}return Array.isArray(e)?e.map(e=>q(e,t,r,o+1)):e}function _(e,t){const r=w(e)&&e["x-protocol"]||w(t)&&t["x-protocol"];if("string"==typeof r&&r.trim())return r.trim().toLowerCase();if(w(e)&&e["x-grpc"]||w(t)&&t["x-grpc"])return"grpc";if(w(e)&&e["x-graphql"]||w(t)&&t["x-graphql"])return"graphql";if(w(e)&&e["x-mcp"]||w(t)&&t["x-mcp"])return"mcp";if(w(e)&&(e["x-ws"]||e["x-websocket"])||w(t)&&t["x-ws"])return"websocket";const o=w(e)?e.responses:void 0;for(const e of Object.values(w(o)?o:{}))if(w(e)&&e.content?.["text/event-stream"])return"sse";return"http"}function O(e){const t=w(e.paths)?e.paths:{},r=[];for(const[e,o]of Object.entries(t))if(w(o))for(const t of v){const n=o[t];w(n)&&r.push({method:t.toUpperCase(),path:e,pathItem:o,operation:n,protocol:_(n,o)})}return r}function T(e){const t=new Map;for(const r of S(e.pathItem.parameters))w(r)&&r.name&&r.in&&t.set(`${r.in}:${r.name}`,r);for(const r of S(e.operation.parameters))w(r)&&r.name&&r.in&&t.set(`${r.in}:${r.name}`,r);return[...t.values()]}function A(e,t){return(Array.isArray(t.security)?t.security:Array.isArray(e.security)?e.security:[]).filter(w).map(e=>{const t={};for(const[r,o]of Object.entries(e))t[r]=Array.isArray(o)?o.map(String):[];return t})}function I(e){const t=w(e.components?.securitySchemes)?e.components.securitySchemes:{},r=[];for(const[e,o]of Object.entries(t)){const t=w(o)?o:{},n={};if(w(t.flows))for(const[e,r]of Object.entries(t.flows))w(r)&&w(r.scopes)&&(n[e]=Object.keys(r.scopes));const s={name:e,type:"string"==typeof t.type?t.type:"unknown",flows:n};x(t.scheme)&&(s.scheme=t.scheme),x(t.bearerFormat)&&(s.bearerFormat=t.bearerFormat),x(t.in)&&(s.in=t.in),x(t.name)&&(s.parameterName=t.name),x(t.openIdConnectUrl)&&(s.openIdConnectUrl=t.openIdConnectUrl),r.push(s)}return r}function P(e){return S(e).filter(e=>"string"==typeof e?.url).map(e=>({url:e.url,...x(e.description)?{description:e.description}:{},variables:w(e.variables)?Object.keys(e.variables):[]}))}function U(e){const t=O(e),r=new Set,o=new Set;for(const e of t){for(const t of S(e.operation.tags))"string"==typeof t&&r.add(t);o.add(e.protocol)}const n=w(e.components?.schemas)?e.components.schemas:{};return{title:x(e.info?.title)??"Untitled API",version:x(e.info?.version)??"0.0.0",...x(e.info?.description)?{description:e.info.description}:{},...x(e.openapi)?{openapiVersion:e.openapi}:{},servers:P(e.servers),endpointCount:t.length,tags:[...r].sort(),protocols:[...o].sort(),schemaCount:Object.keys(n).length,securitySchemes:I(e)}}function k(e){return Buffer.from(JSON.stringify({offset:e}),"utf8").toString("base64url")}function $(e,t={}){const r=t.method?.trim().toUpperCase(),o=t.protocol?.trim().toLowerCase(),n=t.tag?.trim().toLowerCase(),s=t.search?.trim().toLowerCase(),i=t.secured,a=O(e).map(t=>function(e,t){const r=T(t),o=w(t.operation.responses)?t.operation.responses:{};return{ref:`${t.method} ${t.path}`,...x(t.operation.operationId)?{operationId:t.operation.operationId}:{},method:t.method,path:t.path,...x(t.operation.summary)?{summary:t.operation.summary}:{},tags:S(t.operation.tags).filter(e=>"string"==typeof e),secured:A(e,t.operation).length>0,statusCodes:Object.keys(o),hasParameters:r.length>0,hasRequestBody:w(t.operation.requestBody),protocol:t.protocol}}(e,t)).filter(e=>{if(r&&e.method!==r)return!1;if(o&&e.protocol!==o)return!1;if(n&&!e.tags.some(e=>e.toLowerCase()===n))return!1;if(void 0!==i&&e.secured!==i)return!1;if(s){if(![e.ref,e.operationId??"",e.summary??"",e.path,e.tags.join(" ")].join(" ").toLowerCase().includes(s))return!1}return!0}),c=a.length,p=Math.min(Math.max(t.pageSize??100,1),500),u=function(e){if(!e)return 0;try{const t=JSON.parse(Buffer.from(e,"base64url").toString("utf8"));return Number.isInteger(t.offset)&&(t.offset??0)>=0?t.offset??0:0}catch{throw new g("invalid_cursor","The pagination cursor is malformed.")}}(t.cursor),d=a.slice(u,u+p),l=u+d.length;return{items:d,total:c,...l<c?{nextCursor:k(l)}:{}}}function C(e,t){const r={};for(const[o,n]of Object.entries(e)){if(!w(n))continue;const e={schema:n.schema?q(n.schema,t):{},examples:w(n.examples)?Object.keys(n.examples):[]};"example"in n&&(e.example=n.example),r[o]=e}return r}function M(e,t,r){const o=function(e,t){const r=O(e);let o;if(t.operationId)o=r.find(e=>e.operation.operationId===t.operationId);else if(t.method&&t.path){const e=t.method.trim().toUpperCase();o=r.find(r=>r.method===e&&r.path===t.path.trim())}else t.ref&&(o=r.find(e=>`${e.method} ${e.path}`===t.ref.trim()));if(!o)throw new g("operation_not_found",`No operation matches ${t.ref??t.operationId??`${t.method??""} ${t.path??""}`.trim()}.`);return o}(e,r),{operation:n,method:s,path:i}=o,a=T(o).map(e=>{const r=e.in,o={name:String(e.name),in:r,required:"path"===r||Boolean(e.required),deprecated:Boolean(e.deprecated),schema:e.schema?q(e.schema,t):{}};return x(e.description)&&(o.description=e.description),"example"in e&&(o.example=e.example),o}).sort((e,t)=>"path"===e.in!=("path"===t.in)?"path"===e.in?-1:1:e.required!==t.required?e.required?-1:1:e.name.localeCompare(t.name));let c;w(n.requestBody)&&w(n.requestBody.content)&&(c={required:Boolean(n.requestBody.required),...x(n.requestBody.description)?{description:n.requestBody.description}:{},content:C(n.requestBody.content,t)});const p=Object.entries(w(n.responses)?n.responses:{}).map(([e,r])=>{const o=w(r)?r:{},n=[];for(const[e,r]of Object.entries(w(o.headers)?o.headers:{})){const o=w(r)?r:{};n.push({name:e,required:Boolean(o.required),...x(o.description)?{description:o.description}:{},schema:o.schema?q(o.schema,t):{}})}return{status:e,...x(o.description)?{description:o.description}:{},headers:n,content:w(o.content)?C(o.content,t):{}}}),u=A(e,n),d=I(e),l=new Set(u.flatMap(e=>Object.keys(e))),m=d.filter(e=>l.has(e.name));return{ref:`${s} ${i}`,...x(n.operationId)?{operationId:n.operationId}:{},method:s,path:i,...x(n.summary)?{summary:n.summary}:{},...x(n.description)?{description:n.description}:{},tags:S(n.tags).filter(e=>"string"==typeof e),deprecated:Boolean(n.deprecated),idempotent:b.has(s.toLowerCase()),protocol:o.protocol,parameters:a,...c?{requestBody:c}:{},responses:p,security:m,servers:P(n.servers??e.servers)}}function E(e,t,r){let o,n;if(r.ref){o=j(t,r.ref);const e=r.ref.split("/");n=e[e.length-1]}else r.name&&(n=r.name,o=t.components?.schemas?.[r.name]);if(void 0===o)throw new g("schema_not_found",`No schema matches ${r.ref??r.name??"(empty locator)"}.`);return{...n?{name:n}:{},schema:q(o,t)}}function R(e,t){const r=I(e).filter(t=>(e.security??[]).some(e=>w(e)&&t.name in e));if(!t)return{global:r};return{global:r,operation:M(e,e,t).security}}function N(e,t){if(!e)return;const r=Object.entries(e).map(([e,r])=>`${e}${t}${r}`);return r.length?r:void 0}function L(e,t){const r={spec:e.path};t.baseUrl&&(r.server=t.baseUrl),t.timeoutMs&&(r.timeout=t.timeoutMs),t.concurrency&&(r.concurrency=t.concurrency),t.proxy&&(r.proxy=t.proxy);const o=N(t.headers,":");o&&(r.header=o);const n=N(t.variables,"=");n&&(r.variable=n);const s=c.resolveConfig(r);return s.formats=[],s.outputDir="",s.failOnError=!1,t.auth&&(s.auth=t.auth),s}function D(e,t){if(void 0===e)return;if(!e||"object"!=typeof e||Array.isArray(e))throw new Error(`${t} must be an object of strings.`);const r={};for(const[o,n]of Object.entries(e)){if("string"!=typeof n)throw new Error(`${t}.${o} must be a string.`);r[o]=n}return r}function B(e,t){const r=function(e,t){if(t?.trim())return t.trim();const r=e.document.servers;if(Array.isArray(r))for(const e of r)if(e&&"string"==typeof e.url&&e.url.trim()&&!e.url.includes("{"))return e.url.trim()}(e,"string"==typeof t.baseUrl?t.baseUrl:void 0),o={};r&&(o.baseUrl=r),"number"==typeof t.timeoutMs&&(o.timeoutMs=J(t.timeoutMs,"timeoutMs",3e4,1,6e5)),"number"==typeof t.concurrency&&(o.concurrency=J(t.concurrency,"concurrency",5,1,20));const n=D(t.headers,"headers");n&&(o.headers=n);const s=D(t.variables,"variables");return s&&(o.variables=s),"string"==typeof t.proxy&&t.proxy.trim()&&(o.proxy=t.proxy.trim()),o}function J(e,t,r,o,n){if(null==e)return r;const s=Number(e);if(!Number.isFinite(s)||s<o)throw new Error(`${t} must be a number >= ${o}.`);return Math.min(Math.round(s),n)}var F={};function H(e){return c.collectOperations(e.dereferenced,F)}async function z(e,t,r,o){const n=L(e,{...t,...o.baseUrl?{baseUrl:o.baseUrl}:{}}),s=function(e,t){const r=H(e);let o;if(t.operationId)o=r.find(e=>e.operationId===t.operationId);else if(t.method&&t.path){const e=t.method.toUpperCase();o=r.find(r=>r.method.toUpperCase()===e&&r.path===t.path)}else t.ref&&(o=r.find(e=>`${e.method.toUpperCase()} ${e.path}`===t.ref.trim()));if(!o)throw new g("operation_not_found",`No executable operation matches ${t.ref??t.operationId??`${t.method??""} ${t.path??""}`.trim()}.`);return o}(e,r),i=p.createClient(),a={config:n,spec:e.dereferenced},u=function(e){if(void 0!==e){if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("values must be an object with path/query/header/body keys.");return e}}(o.values);u&&(a.values=u),o.variables&&(a.variables=o.variables);const d=function(e){if(void 0!==e){if(!Array.isArray(e))throw new Error("assertions must be an array.");return e.filter(e=>!!e&&"object"==typeof e&&"string"==typeof e.assert)}}(o.assertions);d&&(a.extraAssertions=d),o.baseUrl&&(a.serverUrl=o.baseUrl);return(await c.executeStep(i,s,a)).result}function G(e,t){if(void 0!==e){if(!Array.isArray(e)||e.some(e=>"string"!=typeof e))throw new g("invalid_arguments",`${t} must be an array of strings.`);return e}}function W(e){if(!e||"object"!=typeof e||Array.isArray(e))throw new g("invalid_scenario","scenario must be an object with name and steps.");const t=e;if("string"!=typeof t.name||!t.name.trim())throw new g("invalid_scenario","scenario.name is required.");if(!Array.isArray(t.steps)||0===t.steps.length)throw new g("invalid_scenario","scenario.steps must be a non-empty array.");return t}function V(e,t){let r;try{r=W(t)}catch(e){if(e instanceof g)return{valid:!1,errors:[e.message]};throw e}try{const t=c.resolveSteps(r,H(e));return{valid:!0,count:t.length,steps:t.map((e,t)=>({index:t,ref:e.ref,...e.step.name?{name:e.step.name}:{}}))}}catch(e){return{valid:!1,errors:[e instanceof Error?e.message:String(e)]}}}var Y=["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS","TRACE"],K={ref:{type:"string",description:'Operation reference, e.g. "POST /orders".'},operationId:{type:"string",description:"The operationId declared in the specification."},method:{type:"string",enum:Y},path:{type:"string",description:'The path, e.g. "/orders/{orderId}".'}},Q={baseUrl:{type:"string",description:"Local backend base URL. Defaults to the first spec server URL."},timeoutMs:{type:"integer",minimum:1,maximum:6e5,default:3e4},headers:{type:"object",description:"Extra headers sent with every request.",additionalProperties:{type:"string"}},variables:{type:"object",description:"Postman-style {{name}} variables.",additionalProperties:{type:"string"}},proxy:{type:"string",description:"HTTP/HTTPS proxy URL."}},X={type:"object",description:"ScenarioDefinition: { name, description?, variables?, stopOnFailure?, steps: [{ ref | method+path, name?, skip?, request: { values?, serverUrl?, extract?: [{name, from: body|header|status, path?, key?}], assertions?: [{name, assert, ...}] } }] }",properties:{name:{type:"string"},description:{type:"string"},stopOnFailure:{type:"boolean"},variables:{type:"object",additionalProperties:{type:"string"}},steps:{type:"array",items:{type:"object"}}},required:["name","steps"]};function Z(e){const t="string"==typeof e.ref?e.ref.trim():void 0,r="string"==typeof e.operationId?e.operationId.trim():void 0,o="string"==typeof e.method?e.method.trim():void 0,n="string"==typeof e.path?e.path.trim():void 0;if(t||r||o&&n)return{...t?{ref:t}:{},...r?{operationId:r}:{},...o&&n?{method:o,path:n}:{}};throw new g("invalid_arguments","Provide either ref, operationId, or both method and path.")}var ee=[{name:"spec_overview",description:"Get the API title, version, servers, tags, protocols, security schemes, and counts. Call this first to understand the specification.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"list_operations",description:"List operations as compact rows (reference, operationId, summary, tags, status codes, auth). Filter by tag, method, protocol, auth, or free-text search. Results are paginated.",inputSchema:{type:"object",properties:{tag:{type:"string",description:"Exact tag name."},method:{type:"string",enum:Y},protocol:{type:"string",description:"http, grpc, graphql, websocket, sse, or mcp."},secured:{type:"boolean",description:"Filter by secured operations."},search:{type:"string",description:"Case-insensitive substring."},pageSize:{type:"integer",minimum:1,maximum:500,default:100},cursor:{type:"string",description:"Opaque nextCursor from a previous page."}},additionalProperties:!1}},{name:"get_operation",description:"Get the full implementation contract for one operation: parameters with validation rules, request body JSON Schemas and examples, every documented response (status, headers, body), and effective security. Use this before writing handler code so field names, types, required flags, enums, and response shapes match the spec exactly.",inputSchema:{type:"object",properties:K,additionalProperties:!1}},{name:"get_schema",description:"Get one reusable component schema (dereferenced) by component name or JSON pointer.",inputSchema:{type:"object",properties:{name:{type:"string",description:'Component schema name, e.g. "Order".'},ref:{type:"string",description:'Local pointer, e.g. "#/components/schemas/Order".'}},additionalProperties:!1}},{name:"validate_spec",description:"Validate the current specification and list errors and warnings with their locations. The document is re-read from disk, so recent edits are reflected.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"get_auth_requirements",description:"List every security scheme and, when an operation is given, the exact auth that operation requires (scheme type, header/query placement, OAuth flows and scopes). Use before implementing auth middleware or guards.",inputSchema:{type:"object",properties:K,additionalProperties:!1}},{name:"ping_target",description:"Check that the local backend is reachable before testing. Returns status and latency. Use this first when a contract test fails at the connection layer.",inputSchema:{type:"object",properties:{...Q,timeoutMs:{type:"integer",minimum:100,maximum:6e4,default:5e3}},additionalProperties:!1}},{name:"send_request",description:"Send one operation request to the running backend with optional path/query/header/body values, variables, and extra assertions. Returns the normalized response and per-assertion pass/fail. Use this to verify a single handler you just implemented.",inputSchema:{type:"object",properties:{...K,...Q,values:{type:"object",description:"Request values merged over sampled defaults: { path, query, header, body }."},assertions:{type:"array",description:"Declarative assertions added to the operation's spec assertions.",items:{type:"object"}}},additionalProperties:!1}},{name:"run_contract_tests",description:"Execute the selected operations against the backend and evaluate the spec contract (status, schema, declared assertions). Returns per-operation results and a summary. Filter by methods, tags, path regexes, or operationIds.",inputSchema:{type:"object",properties:{...Q,concurrency:{type:"integer",minimum:1,maximum:20,default:5},methods:{type:"array",items:{type:"string"}},paths:{type:"array",items:{type:"string"}},tags:{type:"array",items:{type:"string"}},operationIds:{type:"array",items:{type:"string"}}},additionalProperties:!1}},{name:"validate_scenario",description:"Statically validate an ordered multi-step scenario: every step reference must resolve, and extraction/assertion shapes are checked. Never sends a request. Call this before run_scenario.",inputSchema:{type:"object",properties:{scenario:X},required:["scenario"],additionalProperties:!1}},{name:"run_scenario",description:"Run an ordered, stateful scenario (for example register then login then create order). Steps share variables; each step can extract response values (body JSONPath, header, status) for later steps and add assertions. Streams progress; supports cancellation.",inputSchema:{type:"object",properties:{scenario:X,...Q},required:["scenario"],additionalProperties:!1}}],te=new Set(ee.map(e=>e.name));async function re(e,t,r,o={}){if(!te.has(e))throw new g("unknown_tool",`Unknown tool: ${e}`);const n=await r.load(),s=n.dereferenced,i=n.document;switch(e){case"spec_overview":return U(s);case"list_operations":{const e={};return"string"==typeof t.tag&&(e.tag=t.tag),"string"==typeof t.method&&(e.method=t.method),"string"==typeof t.protocol&&(e.protocol=t.protocol),"boolean"==typeof t.secured&&(e.secured=t.secured),"string"==typeof t.search&&(e.search=t.search),"number"==typeof t.pageSize&&(e.pageSize=t.pageSize),"string"==typeof t.cursor&&(e.cursor=t.cursor),$(s,e)}case"get_operation":return M(s,i,Z(t));case"get_schema":{const e="string"==typeof t.name?t.name.trim():void 0,r="string"==typeof t.ref?t.ref.trim():void 0;if(!e&&!r)throw new g("invalid_arguments","Provide either a schema name or a $ref pointer.");return E(0,i,{...e?{name:e}:{},...r?{ref:r}:{}})}case"validate_spec":return(await r.load(!0)).validation;case"get_auth_requirements":return void 0!==t.ref||void 0!==t.operationId||void 0!==t.method?R(s,Z(t)):R(s);case"ping_target":{const e=B(n,t).baseUrl;if(!e)throw new g("missing_base_url","Provide baseUrl or declare a concrete server URL in the specification.");return async function(e,t){let r;try{r=new URL(e)}catch{return{url:e,reachable:!1,latencyMs:0,error:"Invalid URL."}}if("http:"!==r.protocol&&"https:"!==r.protocol)return{url:e,reachable:!1,latencyMs:0,error:"Only http and https targets are supported."};const o=new AbortController,n=setTimeout(()=>o.abort(),t),s=Date.now();try{const t=await fetch(r,{method:"GET",signal:o.signal,redirect:"follow"}),n=t.headers.get("content-type");return{url:e,reachable:!0,status:t.status,statusText:t.statusText,...n?{contentType:n}:{},latencyMs:Date.now()-s}}catch(r){const o=r instanceof DOMException&&"AbortError"===r.name;return{url:e,reachable:!1,latencyMs:Date.now()-s,error:o?`Timed out after ${t}ms.`:r instanceof Error?r.message:String(r)}}finally{clearTimeout(n)}}(e,"number"==typeof t.timeoutMs?J(t.timeoutMs,"timeoutMs",5e3,100,6e4):5e3)}case"send_request":return z(n,B(n,t),Z(t),{values:t.values??void 0,variables:t.variables??void 0,assertions:t.assertions??void 0});case"run_contract_tests":{const e=B(n,t),r=function(e){const t={},r=G(e.methods,"methods");r&&(t.methods=r.map(e=>e.toLowerCase()));const o=G(e.paths,"paths");o&&(t.paths=o);const n=G(e.tags,"tags");n&&(t.tags=n);const s=G(e.operationIds,"operationIds");return s&&(t.operationIds=s),t}(t);return async function(e,t,r,o={}){const n=L(e,t);Object.keys(r).length&&(n.filter=r);const s=c.collectOperations(e.dereferenced,n);if(0===s.length)throw new g("no_operations","No operations match the current specification and filter.");const i=p.createClient(),a=Math.max(1,Math.min(n.concurrency??5,s.length)),u=[...s],d=[],l=Date.now();let m=!1;const f=Array.from({length:a},async()=>{for(;u.length>0;){if(o.signal?.aborted)return void(m=!0);const t=u.shift();if(!t)return;const r={config:n,spec:e.dereferenced};n.variables&&(r.variables=n.variables);const a=await c.executeStep(i,t,r);d.push(a.result),o.report?.(d.length,s.length,`${t.method.toUpperCase()} ${t.path}`)}});await Promise.all(f),d.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method));const h=c.buildSummary(d,Date.now()-l);return m&&(h.skipped+=s.length-d.length,h.total=s.length),{summary:h,results:d,generatedAt:(new Date).toISOString(),version:"0.1.0"}}(n,e,r,o)}case"validate_scenario":return V(n,t.scenario);case"run_scenario":{const e=B(n,t);return async function(e,t,r,o={}){const n=W(t),s=V(e,n);if(!s.valid)throw new g("invalid_scenario","Scenario failed validation.",s.errors);const i={config:L(e,r),spec:e.dereferenced,onEvent:e=>{"step:start"===e.type&&"number"==typeof e.stepIndex?o.report?.(e.stepIndex+1,n.steps.length,e.type):"scenario:finish"===e.type&&o.report?.(n.steps.length,n.steps.length,e.type)}};return o.signal&&(i.signal=o.signal),function(e){const{config:t,...r}=e;return r}(await c.runScenario(n,i))}(n,t.scenario,e,o)}default:throw new g("unknown_tool",`Unknown tool: ${e}`)}}var oe="powerduck://spec/";async function ne(e){const t=await e.load();return[{uri:`${oe}source`,name:"OpenAPI source",description:"The raw OpenAPI document exactly as authored.",mimeType:t.mediaType},{uri:`${oe}overview`,name:"API overview",description:"Servers, tags, protocols, security schemes, and counts.",mimeType:"application/json"},{uri:`${oe}operations`,name:"Operations index",description:"Compact index of all operations (up to 500).",mimeType:"application/json"}]}var se=[{uriTemplate:`${oe}operation/{ref}`,name:"Operation contract",description:'Full contract for one operation. Use the reference form "METHOD /path", URL-encoded, e.g. operation/POST%20/orders.',mimeType:"application/json"},{uriTemplate:`${oe}schema/{name}`,name:"Component schema",description:"A dereferenced component schema by name.",mimeType:"application/json"}];var ie=["PowerDuck developer MCP gives you accurate, dereferenced OpenAPI facts for the backend you are implementing and verifies it against a running server.","Call spec_overview and list_operations to orient yourself, then get_operation before writing each handler so parameters, request bodies, responses, and auth match the specification.","Use ping_target, send_request, run_contract_tests, validate_scenario, and run_scenario to verify the implementation as you build it.","Call validate_spec after the specification changes. The document is re-read from disk on every request."].join(" ");function ae(e){return{isError:!0,content:[{type:"text",text:JSON.stringify({error:e.code,message:e.message,...void 0!==e.details?{details:e.details}:{}},null,2)}]}}function ce(e,t={}){const r=new i.Server({name:t.name??"powerduck-dev-mcp",version:t.version??"0.1.0"},{capabilities:{tools:{},resources:{}},instructions:t.instructions??ie});return r.setRequestHandler(a.ListToolsRequestSchema,async()=>({tools:ee})),r.setRequestHandler(a.CallToolRequestSchema,async(t,r)=>{const{name:o,arguments:n,_meta:s}=t.params,i=s?.progressToken,a={signal:r.signal};void 0!==i&&(a.report=(e,t,o)=>{r.sendNotification({method:"notifications/progress",params:{progressToken:i,progress:e,...t?{total:t}:{},...o?{message:o}:{}}})});try{const t=await re(o,n??{},e,a);return c=t,{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}catch(e){if(e instanceof g)return ae(e);const t=e instanceof Error?e.message:String(e);return ae(new g("internal_error",t))}var c}),r.setRequestHandler(a.ListResourcesRequestSchema,async()=>({resources:await ne(e)})),r.setRequestHandler(a.ListResourceTemplatesRequestSchema,async()=>({resourceTemplates:se})),r.setRequestHandler(a.ReadResourceRequestSchema,async t=>{const{uri:r}=t.params;try{const t=await async function(e,t){if(!e.startsWith(oe))throw new g("resource_not_found",`Unknown resource: ${e}`);const r=await t.load(),o=r.dereferenced,n=r.document,s=decodeURIComponent(e.slice(17));if("source"===s)return{uri:e,mimeType:r.mediaType,text:r.sourceText};if("overview"===s)return{uri:e,mimeType:"application/json",text:JSON.stringify(U(o),null,2)};if("operations"===s)return{uri:e,mimeType:"application/json",text:JSON.stringify($(o,{pageSize:500}),null,2)};if(s.startsWith("operation/")){const t=M(o,n,{ref:s.slice(10)});return{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}}if(s.startsWith("schema/")){const t=E(0,n,{name:s.slice(7)});return{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}}throw new g("resource_not_found",`Unknown resource: ${e}`)}(r,e);return{contents:[{uri:t.uri,mimeType:t.mimeType,text:t.text}]}}catch(e){if(e instanceof g)throw e;throw new g("resource_error",e instanceof Error?e.message:String(e))}}),r}async function pe(e){const t=function(){const e={log:console.log,info:console.info,debug:console.debug,dir:console.dir},t=(...e)=>console.error(...e);return console.log=t,console.info=t,console.debug=t,console.dir=t,()=>{console.log=e.log,console.info=e.info,console.debug=e.debug,console.dir=e.dir}}(),r=ce(new y(e.specPath),{...e.name?{name:e.name}:{},...e.version?{version:e.version}:{}}),n=new o.StdioServerTransport;let s=()=>{};const i=new Promise(e=>{s=e});let a=!1;const c=[],p=()=>{if(!a){a=!0;for(const e of c)e();t(),s()}};if(n.onerror=e=>{console.error("[dev-mcp:stdio] transport error:",e)},n.onclose=()=>p(),await r.connect(n),!1!==e.handleSignals)for(const e of["SIGINT","SIGTERM"]){const t=()=>{r.close().catch(()=>{})};process.once(e,t),c.push(()=>process.removeListener(e,t))}return{closed:i,close:async()=>{try{await r.close()}finally{p()}}}}(async function(){const t=new e.Command;t.name("powerduck-dev-mcp").description("Developer MCP server: accurate OpenAPI facts for AI coding agents. Runs over stdio.").version("0.1.0").option("--spec <path>","Path to the OpenAPI document (JSON or YAML).").option("--config <path>","Path to a powerduck.dev.json file.").option("--base-url <url>","Default local backend base URL.").option("--project <dir>","Backend project root.").action(async e=>{const{config:t,configDir:r}=function(e){const t=e?l.default.resolve(e):l.default.resolve(process.cwd(),"powerduck.dev.json");return d.default.existsSync(t)?{config:m(t),configDir:l.default.dirname(t)}:{config:{},configDir:process.cwd()}}(e.config),o=function(e,t){return t?l.default.resolve(e,t):void 0}(r,e.spec??t.spec);if(!o)throw new Error('Provide the OpenAPI document with --spec or a "spec" field in powerduck.dev.json.');e.baseUrl??t.baseUrl,e.project??t.project,await pe({specPath:o})}),await t.parseAsync(process.argv)})().catch(e=>{console.error("[powerduck-dev-mcp]",e instanceof Error?e.message:String(e)),process.exitCode=1});
|
package/dist/cli.d.cts
ADDED
package/dist/cli.d.ts
ADDED
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createRequire as e}from"module";import{Command as t}from"commander";import r from"fs";import o from"path";import{StdioServerTransport as n}from"@modelcontextprotocol/sdk/server/stdio.js";import{load as s}from"js-yaml";import{upgradeOasTo32 as i,validate as a,dereference as c}from"@powerduck/openapi-parser";import{Server as p}from"@modelcontextprotocol/sdk/server/index.js";import{ListToolsRequestSchema as u,CallToolRequestSchema as d,ListResourcesRequestSchema as m,ListResourceTemplatesRequestSchema as l,ReadResourceRequestSchema as f}from"@modelcontextprotocol/sdk/types.js";import{runScenario as h,resolveSteps as y,collectOperations as g,executeStep as v,buildSummary as b,resolveConfig as w}from"@powerduck/openapi-cli";import{createClient as x}from"@powerduck/openapi-request";function S(e){const t=r.readFileSync(e,"utf8");let o;try{o=JSON.parse(t)}catch(t){throw new Error(`Invalid JSON in ${e}: ${t instanceof Error?t.message:String(t)}`)}if(!o||"object"!=typeof o||Array.isArray(o))throw new Error(`${e} must contain a JSON object.`);const n={},s=o;return"string"==typeof s.spec&&(n.spec=s.spec),"string"==typeof s.baseUrl&&(n.baseUrl=s.baseUrl),"string"==typeof s.project&&(n.project=s.project),n}function j(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function _(e){if("string"==typeof e&&e.trim())return{message:e,severity:"error"};if(j(e)&&"string"==typeof e.message){const t="warning"===e.severity||"info"===e.severity?e.severity:"error",r=Array.isArray(e.path)?e.path.join("/"):"string"==typeof e.path?e.path:void 0;return r?{message:e.message,severity:t,path:r}:{message:e.message,severity:t}}return null}e(import.meta.url);var O=class{constructor(e){this.specPath=e}specPath;cache=null;async load(e=!1){const t=r.statSync(this.specPath);if(!e&&this.cache&&this.cache.mtimeMs===t.mtimeMs)return this.cache.snapshot;const o=r.readFileSync(this.specPath,"utf8"),n=/\.ya?ml$/i.test(this.specPath),p=function(e,t){return t?s(e):JSON.parse(e)}(o,n);if(!j(p))throw new Error("The OpenAPI document must be a JSON or YAML object.");let u=p;const d="string"==typeof p.openapi?p.openapi:"";if(d&&!/^3\./.test(d))try{u=await i(p)}catch{u=p}const m=await async function(e){try{const t=await a(e),r=[];for(const e of t.errors??[]){const t=_(e);t&&r.push(t)}return{valid:Boolean(t.valid),..."string"==typeof t.version?{openapiVersion:t.version}:{},issues:r}}catch(e){return{valid:!1,issues:[{message:e instanceof Error?e.message:String(e),severity:"error"}]}}}(u),l=await async function(e){try{const t=await c(e);return!t.errors?.length&&j(t.schema)?t.schema:e}catch{return e}}(u),f={path:this.specPath,document:u,dereferenced:l,sourceText:o,mediaType:n?"application/yaml":"application/json",validation:m,mtimeMs:t.mtimeMs};return this.cache={mtimeMs:t.mtimeMs,snapshot:f},f}},A=class extends Error{code;details;constructor(e,t,r){super(t),this.name="FactError",this.code=e,this.details=r}},I=["get","post","put","patch","delete","head","options","trace"],P=new Set(["get","head","put","delete","options","trace"]);function q(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function T(e){return Array.isArray(e)?e:[]}function U(e){return"string"==typeof e&&e.trim()?e:void 0}function k(e,t){if(!t.startsWith("#/"))return;let r=e;for(const e of t.slice(2).split("/")){const t=e.replace(/~1/g,"/").replace(/~0/g,"~");if(!q(r)||!(t in r))return;r=r[t]}return r}function $(e,t,r=new Set,o=0){if(o>6)return e;if(q(e)){if("string"==typeof e.$ref){const n=e.$ref;if(r.has(n))return{$ref:n};const s=k(t,n);if(void 0===s)return e;const i=new Set(r).add(n);return $(s,t,i,o+1)}const n={};for(const[s,i]of Object.entries(e))n[s]=$(i,t,r,o+1);return n}return Array.isArray(e)?e.map(e=>$(e,t,r,o+1)):e}function C(e,t){const r=q(e)&&e["x-protocol"]||q(t)&&t["x-protocol"];if("string"==typeof r&&r.trim())return r.trim().toLowerCase();if(q(e)&&e["x-grpc"]||q(t)&&t["x-grpc"])return"grpc";if(q(e)&&e["x-graphql"]||q(t)&&t["x-graphql"])return"graphql";if(q(e)&&e["x-mcp"]||q(t)&&t["x-mcp"])return"mcp";if(q(e)&&(e["x-ws"]||e["x-websocket"])||q(t)&&t["x-ws"])return"websocket";const o=q(e)?e.responses:void 0;for(const e of Object.values(q(o)?o:{}))if(q(e)&&e.content?.["text/event-stream"])return"sse";return"http"}function M(e){const t=q(e.paths)?e.paths:{},r=[];for(const[e,o]of Object.entries(t))if(q(o))for(const t of I){const n=o[t];q(n)&&r.push({method:t.toUpperCase(),path:e,pathItem:o,operation:n,protocol:C(n,o)})}return r}function E(e){const t=new Map;for(const r of T(e.pathItem.parameters))q(r)&&r.name&&r.in&&t.set(`${r.in}:${r.name}`,r);for(const r of T(e.operation.parameters))q(r)&&r.name&&r.in&&t.set(`${r.in}:${r.name}`,r);return[...t.values()]}function N(e,t){return(Array.isArray(t.security)?t.security:Array.isArray(e.security)?e.security:[]).filter(q).map(e=>{const t={};for(const[r,o]of Object.entries(e))t[r]=Array.isArray(o)?o.map(String):[];return t})}function L(e){const t=q(e.components?.securitySchemes)?e.components.securitySchemes:{},r=[];for(const[e,o]of Object.entries(t)){const t=q(o)?o:{},n={};if(q(t.flows))for(const[e,r]of Object.entries(t.flows))q(r)&&q(r.scopes)&&(n[e]=Object.keys(r.scopes));const s={name:e,type:"string"==typeof t.type?t.type:"unknown",flows:n};U(t.scheme)&&(s.scheme=t.scheme),U(t.bearerFormat)&&(s.bearerFormat=t.bearerFormat),U(t.in)&&(s.in=t.in),U(t.name)&&(s.parameterName=t.name),U(t.openIdConnectUrl)&&(s.openIdConnectUrl=t.openIdConnectUrl),r.push(s)}return r}function R(e){return T(e).filter(e=>"string"==typeof e?.url).map(e=>({url:e.url,...U(e.description)?{description:e.description}:{},variables:q(e.variables)?Object.keys(e.variables):[]}))}function D(e){const t=M(e),r=new Set,o=new Set;for(const e of t){for(const t of T(e.operation.tags))"string"==typeof t&&r.add(t);o.add(e.protocol)}const n=q(e.components?.schemas)?e.components.schemas:{};return{title:U(e.info?.title)??"Untitled API",version:U(e.info?.version)??"0.0.0",...U(e.info?.description)?{description:e.info.description}:{},...U(e.openapi)?{openapiVersion:e.openapi}:{},servers:R(e.servers),endpointCount:t.length,tags:[...r].sort(),protocols:[...o].sort(),schemaCount:Object.keys(n).length,securitySchemes:L(e)}}function B(e){return Buffer.from(JSON.stringify({offset:e}),"utf8").toString("base64url")}function J(e,t={}){const r=t.method?.trim().toUpperCase(),o=t.protocol?.trim().toLowerCase(),n=t.tag?.trim().toLowerCase(),s=t.search?.trim().toLowerCase(),i=t.secured,a=M(e).map(t=>function(e,t){const r=E(t),o=q(t.operation.responses)?t.operation.responses:{};return{ref:`${t.method} ${t.path}`,...U(t.operation.operationId)?{operationId:t.operation.operationId}:{},method:t.method,path:t.path,...U(t.operation.summary)?{summary:t.operation.summary}:{},tags:T(t.operation.tags).filter(e=>"string"==typeof e),secured:N(e,t.operation).length>0,statusCodes:Object.keys(o),hasParameters:r.length>0,hasRequestBody:q(t.operation.requestBody),protocol:t.protocol}}(e,t)).filter(e=>{if(r&&e.method!==r)return!1;if(o&&e.protocol!==o)return!1;if(n&&!e.tags.some(e=>e.toLowerCase()===n))return!1;if(void 0!==i&&e.secured!==i)return!1;if(s){if(![e.ref,e.operationId??"",e.summary??"",e.path,e.tags.join(" ")].join(" ").toLowerCase().includes(s))return!1}return!0}),c=a.length,p=Math.min(Math.max(t.pageSize??100,1),500),u=function(e){if(!e)return 0;try{const t=JSON.parse(Buffer.from(e,"base64url").toString("utf8"));return Number.isInteger(t.offset)&&(t.offset??0)>=0?t.offset??0:0}catch{throw new A("invalid_cursor","The pagination cursor is malformed.")}}(t.cursor),d=a.slice(u,u+p),m=u+d.length;return{items:d,total:c,...m<c?{nextCursor:B(m)}:{}}}function F(e,t){const r={};for(const[o,n]of Object.entries(e)){if(!q(n))continue;const e={schema:n.schema?$(n.schema,t):{},examples:q(n.examples)?Object.keys(n.examples):[]};"example"in n&&(e.example=n.example),r[o]=e}return r}function H(e,t,r){const o=function(e,t){const r=M(e);let o;if(t.operationId)o=r.find(e=>e.operation.operationId===t.operationId);else if(t.method&&t.path){const e=t.method.trim().toUpperCase();o=r.find(r=>r.method===e&&r.path===t.path.trim())}else t.ref&&(o=r.find(e=>`${e.method} ${e.path}`===t.ref.trim()));if(!o)throw new A("operation_not_found",`No operation matches ${t.ref??t.operationId??`${t.method??""} ${t.path??""}`.trim()}.`);return o}(e,r),{operation:n,method:s,path:i}=o,a=E(o).map(e=>{const r=e.in,o={name:String(e.name),in:r,required:"path"===r||Boolean(e.required),deprecated:Boolean(e.deprecated),schema:e.schema?$(e.schema,t):{}};return U(e.description)&&(o.description=e.description),"example"in e&&(o.example=e.example),o}).sort((e,t)=>"path"===e.in!=("path"===t.in)?"path"===e.in?-1:1:e.required!==t.required?e.required?-1:1:e.name.localeCompare(t.name));let c;q(n.requestBody)&&q(n.requestBody.content)&&(c={required:Boolean(n.requestBody.required),...U(n.requestBody.description)?{description:n.requestBody.description}:{},content:F(n.requestBody.content,t)});const p=Object.entries(q(n.responses)?n.responses:{}).map(([e,r])=>{const o=q(r)?r:{},n=[];for(const[e,r]of Object.entries(q(o.headers)?o.headers:{})){const o=q(r)?r:{};n.push({name:e,required:Boolean(o.required),...U(o.description)?{description:o.description}:{},schema:o.schema?$(o.schema,t):{}})}return{status:e,...U(o.description)?{description:o.description}:{},headers:n,content:q(o.content)?F(o.content,t):{}}}),u=N(e,n),d=L(e),m=new Set(u.flatMap(e=>Object.keys(e))),l=d.filter(e=>m.has(e.name));return{ref:`${s} ${i}`,...U(n.operationId)?{operationId:n.operationId}:{},method:s,path:i,...U(n.summary)?{summary:n.summary}:{},...U(n.description)?{description:n.description}:{},tags:T(n.tags).filter(e=>"string"==typeof e),deprecated:Boolean(n.deprecated),idempotent:P.has(s.toLowerCase()),protocol:o.protocol,parameters:a,...c?{requestBody:c}:{},responses:p,security:l,servers:R(n.servers??e.servers)}}function z(e,t,r){let o,n;if(r.ref){o=k(t,r.ref);const e=r.ref.split("/");n=e[e.length-1]}else r.name&&(n=r.name,o=t.components?.schemas?.[r.name]);if(void 0===o)throw new A("schema_not_found",`No schema matches ${r.ref??r.name??"(empty locator)"}.`);return{...n?{name:n}:{},schema:$(o,t)}}function G(e,t){const r=L(e).filter(t=>(e.security??[]).some(e=>q(e)&&t.name in e));if(!t)return{global:r};return{global:r,operation:H(e,e,t).security}}function W(e,t){if(!e)return;const r=Object.entries(e).map(([e,r])=>`${e}${t}${r}`);return r.length?r:void 0}function V(e,t){const r={spec:e.path};t.baseUrl&&(r.server=t.baseUrl),t.timeoutMs&&(r.timeout=t.timeoutMs),t.concurrency&&(r.concurrency=t.concurrency),t.proxy&&(r.proxy=t.proxy);const o=W(t.headers,":");o&&(r.header=o);const n=W(t.variables,"=");n&&(r.variable=n);const s=w(r);return s.formats=[],s.outputDir="",s.failOnError=!1,t.auth&&(s.auth=t.auth),s}function Y(e,t){if(void 0===e)return;if(!e||"object"!=typeof e||Array.isArray(e))throw new Error(`${t} must be an object of strings.`);const r={};for(const[o,n]of Object.entries(e)){if("string"!=typeof n)throw new Error(`${t}.${o} must be a string.`);r[o]=n}return r}function K(e,t){const r=function(e,t){if(t?.trim())return t.trim();const r=e.document.servers;if(Array.isArray(r))for(const e of r)if(e&&"string"==typeof e.url&&e.url.trim()&&!e.url.includes("{"))return e.url.trim()}(e,"string"==typeof t.baseUrl?t.baseUrl:void 0),o={};r&&(o.baseUrl=r),"number"==typeof t.timeoutMs&&(o.timeoutMs=Q(t.timeoutMs,"timeoutMs",3e4,1,6e5)),"number"==typeof t.concurrency&&(o.concurrency=Q(t.concurrency,"concurrency",5,1,20));const n=Y(t.headers,"headers");n&&(o.headers=n);const s=Y(t.variables,"variables");return s&&(o.variables=s),"string"==typeof t.proxy&&t.proxy.trim()&&(o.proxy=t.proxy.trim()),o}function Q(e,t,r,o,n){if(null==e)return r;const s=Number(e);if(!Number.isFinite(s)||s<o)throw new Error(`${t} must be a number >= ${o}.`);return Math.min(Math.round(s),n)}var X={};function Z(e){return g(e.dereferenced,X)}async function ee(e,t,r,o){const n=V(e,{...t,...o.baseUrl?{baseUrl:o.baseUrl}:{}}),s=function(e,t){const r=Z(e);let o;if(t.operationId)o=r.find(e=>e.operationId===t.operationId);else if(t.method&&t.path){const e=t.method.toUpperCase();o=r.find(r=>r.method.toUpperCase()===e&&r.path===t.path)}else t.ref&&(o=r.find(e=>`${e.method.toUpperCase()} ${e.path}`===t.ref.trim()));if(!o)throw new A("operation_not_found",`No executable operation matches ${t.ref??t.operationId??`${t.method??""} ${t.path??""}`.trim()}.`);return o}(e,r),i=x(),a={config:n,spec:e.dereferenced},c=function(e){if(void 0!==e){if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("values must be an object with path/query/header/body keys.");return e}}(o.values);c&&(a.values=c),o.variables&&(a.variables=o.variables);const p=function(e){if(void 0!==e){if(!Array.isArray(e))throw new Error("assertions must be an array.");return e.filter(e=>!!e&&"object"==typeof e&&"string"==typeof e.assert)}}(o.assertions);p&&(a.extraAssertions=p),o.baseUrl&&(a.serverUrl=o.baseUrl);return(await v(i,s,a)).result}function te(e,t){if(void 0!==e){if(!Array.isArray(e)||e.some(e=>"string"!=typeof e))throw new A("invalid_arguments",`${t} must be an array of strings.`);return e}}function re(e){if(!e||"object"!=typeof e||Array.isArray(e))throw new A("invalid_scenario","scenario must be an object with name and steps.");const t=e;if("string"!=typeof t.name||!t.name.trim())throw new A("invalid_scenario","scenario.name is required.");if(!Array.isArray(t.steps)||0===t.steps.length)throw new A("invalid_scenario","scenario.steps must be a non-empty array.");return t}function oe(e,t){let r;try{r=re(t)}catch(e){if(e instanceof A)return{valid:!1,errors:[e.message]};throw e}try{const t=y(r,Z(e));return{valid:!0,count:t.length,steps:t.map((e,t)=>({index:t,ref:e.ref,...e.step.name?{name:e.step.name}:{}}))}}catch(e){return{valid:!1,errors:[e instanceof Error?e.message:String(e)]}}}var ne=["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS","TRACE"],se={ref:{type:"string",description:'Operation reference, e.g. "POST /orders".'},operationId:{type:"string",description:"The operationId declared in the specification."},method:{type:"string",enum:ne},path:{type:"string",description:'The path, e.g. "/orders/{orderId}".'}},ie={baseUrl:{type:"string",description:"Local backend base URL. Defaults to the first spec server URL."},timeoutMs:{type:"integer",minimum:1,maximum:6e5,default:3e4},headers:{type:"object",description:"Extra headers sent with every request.",additionalProperties:{type:"string"}},variables:{type:"object",description:"Postman-style {{name}} variables.",additionalProperties:{type:"string"}},proxy:{type:"string",description:"HTTP/HTTPS proxy URL."}},ae={type:"object",description:"ScenarioDefinition: { name, description?, variables?, stopOnFailure?, steps: [{ ref | method+path, name?, skip?, request: { values?, serverUrl?, extract?: [{name, from: body|header|status, path?, key?}], assertions?: [{name, assert, ...}] } }] }",properties:{name:{type:"string"},description:{type:"string"},stopOnFailure:{type:"boolean"},variables:{type:"object",additionalProperties:{type:"string"}},steps:{type:"array",items:{type:"object"}}},required:["name","steps"]};function ce(e){const t="string"==typeof e.ref?e.ref.trim():void 0,r="string"==typeof e.operationId?e.operationId.trim():void 0,o="string"==typeof e.method?e.method.trim():void 0,n="string"==typeof e.path?e.path.trim():void 0;if(t||r||o&&n)return{...t?{ref:t}:{},...r?{operationId:r}:{},...o&&n?{method:o,path:n}:{}};throw new A("invalid_arguments","Provide either ref, operationId, or both method and path.")}var pe=[{name:"spec_overview",description:"Get the API title, version, servers, tags, protocols, security schemes, and counts. Call this first to understand the specification.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"list_operations",description:"List operations as compact rows (reference, operationId, summary, tags, status codes, auth). Filter by tag, method, protocol, auth, or free-text search. Results are paginated.",inputSchema:{type:"object",properties:{tag:{type:"string",description:"Exact tag name."},method:{type:"string",enum:ne},protocol:{type:"string",description:"http, grpc, graphql, websocket, sse, or mcp."},secured:{type:"boolean",description:"Filter by secured operations."},search:{type:"string",description:"Case-insensitive substring."},pageSize:{type:"integer",minimum:1,maximum:500,default:100},cursor:{type:"string",description:"Opaque nextCursor from a previous page."}},additionalProperties:!1}},{name:"get_operation",description:"Get the full implementation contract for one operation: parameters with validation rules, request body JSON Schemas and examples, every documented response (status, headers, body), and effective security. Use this before writing handler code so field names, types, required flags, enums, and response shapes match the spec exactly.",inputSchema:{type:"object",properties:se,additionalProperties:!1}},{name:"get_schema",description:"Get one reusable component schema (dereferenced) by component name or JSON pointer.",inputSchema:{type:"object",properties:{name:{type:"string",description:'Component schema name, e.g. "Order".'},ref:{type:"string",description:'Local pointer, e.g. "#/components/schemas/Order".'}},additionalProperties:!1}},{name:"validate_spec",description:"Validate the current specification and list errors and warnings with their locations. The document is re-read from disk, so recent edits are reflected.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"get_auth_requirements",description:"List every security scheme and, when an operation is given, the exact auth that operation requires (scheme type, header/query placement, OAuth flows and scopes). Use before implementing auth middleware or guards.",inputSchema:{type:"object",properties:se,additionalProperties:!1}},{name:"ping_target",description:"Check that the local backend is reachable before testing. Returns status and latency. Use this first when a contract test fails at the connection layer.",inputSchema:{type:"object",properties:{...ie,timeoutMs:{type:"integer",minimum:100,maximum:6e4,default:5e3}},additionalProperties:!1}},{name:"send_request",description:"Send one operation request to the running backend with optional path/query/header/body values, variables, and extra assertions. Returns the normalized response and per-assertion pass/fail. Use this to verify a single handler you just implemented.",inputSchema:{type:"object",properties:{...se,...ie,values:{type:"object",description:"Request values merged over sampled defaults: { path, query, header, body }."},assertions:{type:"array",description:"Declarative assertions added to the operation's spec assertions.",items:{type:"object"}}},additionalProperties:!1}},{name:"run_contract_tests",description:"Execute the selected operations against the backend and evaluate the spec contract (status, schema, declared assertions). Returns per-operation results and a summary. Filter by methods, tags, path regexes, or operationIds.",inputSchema:{type:"object",properties:{...ie,concurrency:{type:"integer",minimum:1,maximum:20,default:5},methods:{type:"array",items:{type:"string"}},paths:{type:"array",items:{type:"string"}},tags:{type:"array",items:{type:"string"}},operationIds:{type:"array",items:{type:"string"}}},additionalProperties:!1}},{name:"validate_scenario",description:"Statically validate an ordered multi-step scenario: every step reference must resolve, and extraction/assertion shapes are checked. Never sends a request. Call this before run_scenario.",inputSchema:{type:"object",properties:{scenario:ae},required:["scenario"],additionalProperties:!1}},{name:"run_scenario",description:"Run an ordered, stateful scenario (for example register then login then create order). Steps share variables; each step can extract response values (body JSONPath, header, status) for later steps and add assertions. Streams progress; supports cancellation.",inputSchema:{type:"object",properties:{scenario:ae,...ie},required:["scenario"],additionalProperties:!1}}],ue=new Set(pe.map(e=>e.name));async function de(e,t,r,o={}){if(!ue.has(e))throw new A("unknown_tool",`Unknown tool: ${e}`);const n=await r.load(),s=n.dereferenced,i=n.document;switch(e){case"spec_overview":return D(s);case"list_operations":{const e={};return"string"==typeof t.tag&&(e.tag=t.tag),"string"==typeof t.method&&(e.method=t.method),"string"==typeof t.protocol&&(e.protocol=t.protocol),"boolean"==typeof t.secured&&(e.secured=t.secured),"string"==typeof t.search&&(e.search=t.search),"number"==typeof t.pageSize&&(e.pageSize=t.pageSize),"string"==typeof t.cursor&&(e.cursor=t.cursor),J(s,e)}case"get_operation":return H(s,i,ce(t));case"get_schema":{const e="string"==typeof t.name?t.name.trim():void 0,r="string"==typeof t.ref?t.ref.trim():void 0;if(!e&&!r)throw new A("invalid_arguments","Provide either a schema name or a $ref pointer.");return z(0,i,{...e?{name:e}:{},...r?{ref:r}:{}})}case"validate_spec":return(await r.load(!0)).validation;case"get_auth_requirements":return void 0!==t.ref||void 0!==t.operationId||void 0!==t.method?G(s,ce(t)):G(s);case"ping_target":{const e=K(n,t).baseUrl;if(!e)throw new A("missing_base_url","Provide baseUrl or declare a concrete server URL in the specification.");return async function(e,t){let r;try{r=new URL(e)}catch{return{url:e,reachable:!1,latencyMs:0,error:"Invalid URL."}}if("http:"!==r.protocol&&"https:"!==r.protocol)return{url:e,reachable:!1,latencyMs:0,error:"Only http and https targets are supported."};const o=new AbortController,n=setTimeout(()=>o.abort(),t),s=Date.now();try{const t=await fetch(r,{method:"GET",signal:o.signal,redirect:"follow"}),n=t.headers.get("content-type");return{url:e,reachable:!0,status:t.status,statusText:t.statusText,...n?{contentType:n}:{},latencyMs:Date.now()-s}}catch(r){const o=r instanceof DOMException&&"AbortError"===r.name;return{url:e,reachable:!1,latencyMs:Date.now()-s,error:o?`Timed out after ${t}ms.`:r instanceof Error?r.message:String(r)}}finally{clearTimeout(n)}}(e,"number"==typeof t.timeoutMs?Q(t.timeoutMs,"timeoutMs",5e3,100,6e4):5e3)}case"send_request":return ee(n,K(n,t),ce(t),{values:t.values??void 0,variables:t.variables??void 0,assertions:t.assertions??void 0});case"run_contract_tests":{const e=K(n,t),r=function(e){const t={},r=te(e.methods,"methods");r&&(t.methods=r.map(e=>e.toLowerCase()));const o=te(e.paths,"paths");o&&(t.paths=o);const n=te(e.tags,"tags");n&&(t.tags=n);const s=te(e.operationIds,"operationIds");return s&&(t.operationIds=s),t}(t);return async function(e,t,r,o={}){const n=V(e,t);Object.keys(r).length&&(n.filter=r);const s=g(e.dereferenced,n);if(0===s.length)throw new A("no_operations","No operations match the current specification and filter.");const i=x(),a=Math.max(1,Math.min(n.concurrency??5,s.length)),c=[...s],p=[],u=Date.now();let d=!1;const m=Array.from({length:a},async()=>{for(;c.length>0;){if(o.signal?.aborted)return void(d=!0);const t=c.shift();if(!t)return;const r={config:n,spec:e.dereferenced};n.variables&&(r.variables=n.variables);const a=await v(i,t,r);p.push(a.result),o.report?.(p.length,s.length,`${t.method.toUpperCase()} ${t.path}`)}});await Promise.all(m),p.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method));const l=b(p,Date.now()-u);return d&&(l.skipped+=s.length-p.length,l.total=s.length),{summary:l,results:p,generatedAt:(new Date).toISOString(),version:"0.1.0"}}(n,e,r,o)}case"validate_scenario":return oe(n,t.scenario);case"run_scenario":{const e=K(n,t);return async function(e,t,r,o={}){const n=re(t),s=oe(e,n);if(!s.valid)throw new A("invalid_scenario","Scenario failed validation.",s.errors);const i={config:V(e,r),spec:e.dereferenced,onEvent:e=>{"step:start"===e.type&&"number"==typeof e.stepIndex?o.report?.(e.stepIndex+1,n.steps.length,e.type):"scenario:finish"===e.type&&o.report?.(n.steps.length,n.steps.length,e.type)}};return o.signal&&(i.signal=o.signal),function(e){const{config:t,...r}=e;return r}(await h(n,i))}(n,t.scenario,e,o)}default:throw new A("unknown_tool",`Unknown tool: ${e}`)}}var me="powerduck://spec/";async function le(e){const t=await e.load();return[{uri:`${me}source`,name:"OpenAPI source",description:"The raw OpenAPI document exactly as authored.",mimeType:t.mediaType},{uri:`${me}overview`,name:"API overview",description:"Servers, tags, protocols, security schemes, and counts.",mimeType:"application/json"},{uri:`${me}operations`,name:"Operations index",description:"Compact index of all operations (up to 500).",mimeType:"application/json"}]}var fe=[{uriTemplate:`${me}operation/{ref}`,name:"Operation contract",description:'Full contract for one operation. Use the reference form "METHOD /path", URL-encoded, e.g. operation/POST%20/orders.',mimeType:"application/json"},{uriTemplate:`${me}schema/{name}`,name:"Component schema",description:"A dereferenced component schema by name.",mimeType:"application/json"}];var he=["PowerDuck developer MCP gives you accurate, dereferenced OpenAPI facts for the backend you are implementing and verifies it against a running server.","Call spec_overview and list_operations to orient yourself, then get_operation before writing each handler so parameters, request bodies, responses, and auth match the specification.","Use ping_target, send_request, run_contract_tests, validate_scenario, and run_scenario to verify the implementation as you build it.","Call validate_spec after the specification changes. The document is re-read from disk on every request."].join(" ");function ye(e){return{isError:!0,content:[{type:"text",text:JSON.stringify({error:e.code,message:e.message,...void 0!==e.details?{details:e.details}:{}},null,2)}]}}function ge(e,t={}){const r=new p({name:t.name??"powerduck-dev-mcp",version:t.version??"0.1.0"},{capabilities:{tools:{},resources:{}},instructions:t.instructions??he});return r.setRequestHandler(u,async()=>({tools:pe})),r.setRequestHandler(d,async(t,r)=>{const{name:o,arguments:n,_meta:s}=t.params,i=s?.progressToken,a={signal:r.signal};void 0!==i&&(a.report=(e,t,o)=>{r.sendNotification({method:"notifications/progress",params:{progressToken:i,progress:e,...t?{total:t}:{},...o?{message:o}:{}}})});try{const t=await de(o,n??{},e,a);return c=t,{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}catch(e){if(e instanceof A)return ye(e);const t=e instanceof Error?e.message:String(e);return ye(new A("internal_error",t))}var c}),r.setRequestHandler(m,async()=>({resources:await le(e)})),r.setRequestHandler(l,async()=>({resourceTemplates:fe})),r.setRequestHandler(f,async t=>{const{uri:r}=t.params;try{const t=await async function(e,t){if(!e.startsWith(me))throw new A("resource_not_found",`Unknown resource: ${e}`);const r=await t.load(),o=r.dereferenced,n=r.document,s=decodeURIComponent(e.slice(17));if("source"===s)return{uri:e,mimeType:r.mediaType,text:r.sourceText};if("overview"===s)return{uri:e,mimeType:"application/json",text:JSON.stringify(D(o),null,2)};if("operations"===s)return{uri:e,mimeType:"application/json",text:JSON.stringify(J(o,{pageSize:500}),null,2)};if(s.startsWith("operation/")){const t=H(o,n,{ref:s.slice(10)});return{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}}if(s.startsWith("schema/")){const t=z(0,n,{name:s.slice(7)});return{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}}throw new A("resource_not_found",`Unknown resource: ${e}`)}(r,e);return{contents:[{uri:t.uri,mimeType:t.mimeType,text:t.text}]}}catch(e){if(e instanceof A)throw e;throw new A("resource_error",e instanceof Error?e.message:String(e))}}),r}async function ve(e){const t=function(){const e={log:console.log,info:console.info,debug:console.debug,dir:console.dir},t=(...e)=>console.error(...e);return console.log=t,console.info=t,console.debug=t,console.dir=t,()=>{console.log=e.log,console.info=e.info,console.debug=e.debug,console.dir=e.dir}}(),r=ge(new O(e.specPath),{...e.name?{name:e.name}:{},...e.version?{version:e.version}:{}}),o=new n;let s=()=>{};const i=new Promise(e=>{s=e});let a=!1;const c=[],p=()=>{if(!a){a=!0;for(const e of c)e();t(),s()}};if(o.onerror=e=>{console.error("[dev-mcp:stdio] transport error:",e)},o.onclose=()=>p(),await r.connect(o),!1!==e.handleSignals)for(const e of["SIGINT","SIGTERM"]){const t=()=>{r.close().catch(()=>{})};process.once(e,t),c.push(()=>process.removeListener(e,t))}return{closed:i,close:async()=>{try{await r.close()}finally{p()}}}}(async function(){const e=new t;e.name("powerduck-dev-mcp").description("Developer MCP server: accurate OpenAPI facts for AI coding agents. Runs over stdio.").version("0.1.0").option("--spec <path>","Path to the OpenAPI document (JSON or YAML).").option("--config <path>","Path to a powerduck.dev.json file.").option("--base-url <url>","Default local backend base URL.").option("--project <dir>","Backend project root.").action(async e=>{const{config:t,configDir:n}=function(e){const t=e?o.resolve(e):o.resolve(process.cwd(),"powerduck.dev.json");return r.existsSync(t)?{config:S(t),configDir:o.dirname(t)}:{config:{},configDir:process.cwd()}}(e.config),s=function(e,t){return t?o.resolve(e,t):void 0}(n,e.spec??t.spec);if(!s)throw new Error('Provide the OpenAPI document with --spec or a "spec" field in powerduck.dev.json.');e.baseUrl??t.baseUrl,e.project??t.project,await ve({specPath:s})}),await e.parseAsync(process.argv)})().catch(e=>{console.error("[powerduck-dev-mcp]",e instanceof Error?e.message:String(e)),process.exitCode=1});
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var e=require("@modelcontextprotocol/sdk/server/index.js"),t=require("@modelcontextprotocol/sdk/types.js"),r=require("@powerduck/openapi-cli"),o=require("@powerduck/openapi-request"),n=require("@modelcontextprotocol/sdk/server/stdio.js"),s=require("fs"),i=require("js-yaml"),a=require("@powerduck/openapi-parser"),c=require("path");function p(e){return e&&e.__esModule?e:{default:e}}var u=p(s),d=p(c),l=class extends Error{code;details;constructor(e,t,r){super(t),this.name="FactError",this.code=e,this.details=r}},m=["get","post","put","patch","delete","head","options","trace"],f=new Set(["get","head","put","delete","options","trace"]);function h(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function y(e){return Array.isArray(e)?e:[]}function g(e){return"string"==typeof e&&e.trim()?e:void 0}function v(e,t){if(!t.startsWith("#/"))return;let r=e;for(const e of t.slice(2).split("/")){const t=e.replace(/~1/g,"/").replace(/~0/g,"~");if(!h(r)||!(t in r))return;r=r[t]}return r}function b(e,t,r=new Set,o=0){if(o>6)return e;if(h(e)){if("string"==typeof e.$ref){const n=e.$ref;if(r.has(n))return{$ref:n};const s=v(t,n);if(void 0===s)return e;const i=new Set(r).add(n);return b(s,t,i,o+1)}const n={};for(const[s,i]of Object.entries(e))n[s]=b(i,t,r,o+1);return n}return Array.isArray(e)?e.map(e=>b(e,t,r,o+1)):e}function w(e,t){const r=h(e)&&e["x-protocol"]||h(t)&&t["x-protocol"];if("string"==typeof r&&r.trim())return r.trim().toLowerCase();if(h(e)&&e["x-grpc"]||h(t)&&t["x-grpc"])return"grpc";if(h(e)&&e["x-graphql"]||h(t)&&t["x-graphql"])return"graphql";if(h(e)&&e["x-mcp"]||h(t)&&t["x-mcp"])return"mcp";if(h(e)&&(e["x-ws"]||e["x-websocket"])||h(t)&&t["x-ws"])return"websocket";const o=h(e)?e.responses:void 0;for(const e of Object.values(h(o)?o:{}))if(h(e)&&e.content?.["text/event-stream"])return"sse";return"http"}function x(e){const t=h(e.paths)?e.paths:{},r=[];for(const[e,o]of Object.entries(t))if(h(o))for(const t of m){const n=o[t];h(n)&&r.push({method:t.toUpperCase(),path:e,pathItem:o,operation:n,protocol:w(n,o)})}return r}function S(e){const t=new Map;for(const r of y(e.pathItem.parameters))h(r)&&r.name&&r.in&&t.set(`${r.in}:${r.name}`,r);for(const r of y(e.operation.parameters))h(r)&&r.name&&r.in&&t.set(`${r.in}:${r.name}`,r);return[...t.values()]}function j(e,t){return(Array.isArray(t.security)?t.security:Array.isArray(e.security)?e.security:[]).filter(h).map(e=>{const t={};for(const[r,o]of Object.entries(e))t[r]=Array.isArray(o)?o.map(String):[];return t})}function q(e){const t=h(e.components?.securitySchemes)?e.components.securitySchemes:{},r=[];for(const[e,o]of Object.entries(t)){const t=h(o)?o:{},n={};if(h(t.flows))for(const[e,r]of Object.entries(t.flows))h(r)&&h(r.scopes)&&(n[e]=Object.keys(r.scopes));const s={name:e,type:"string"==typeof t.type?t.type:"unknown",flows:n};g(t.scheme)&&(s.scheme=t.scheme),g(t.bearerFormat)&&(s.bearerFormat=t.bearerFormat),g(t.in)&&(s.in=t.in),g(t.name)&&(s.parameterName=t.name),g(t.openIdConnectUrl)&&(s.openIdConnectUrl=t.openIdConnectUrl),r.push(s)}return r}function _(e){return y(e).filter(e=>"string"==typeof e?.url).map(e=>({url:e.url,...g(e.description)?{description:e.description}:{},variables:h(e.variables)?Object.keys(e.variables):[]}))}function O(e){const t=x(e),r=new Set,o=new Set;for(const e of t){for(const t of y(e.operation.tags))"string"==typeof t&&r.add(t);o.add(e.protocol)}const n=h(e.components?.schemas)?e.components.schemas:{};return{title:g(e.info?.title)??"Untitled API",version:g(e.info?.version)??"0.0.0",...g(e.info?.description)?{description:e.info.description}:{},...g(e.openapi)?{openapiVersion:e.openapi}:{},servers:_(e.servers),endpointCount:t.length,tags:[...r].sort(),protocols:[...o].sort(),schemaCount:Object.keys(n).length,securitySchemes:q(e)}}function T(e){return Buffer.from(JSON.stringify({offset:e}),"utf8").toString("base64url")}function A(e,t={}){const r=t.method?.trim().toUpperCase(),o=t.protocol?.trim().toLowerCase(),n=t.tag?.trim().toLowerCase(),s=t.search?.trim().toLowerCase(),i=t.secured,a=x(e).map(t=>function(e,t){const r=S(t),o=h(t.operation.responses)?t.operation.responses:{};return{ref:`${t.method} ${t.path}`,...g(t.operation.operationId)?{operationId:t.operation.operationId}:{},method:t.method,path:t.path,...g(t.operation.summary)?{summary:t.operation.summary}:{},tags:y(t.operation.tags).filter(e=>"string"==typeof e),secured:j(e,t.operation).length>0,statusCodes:Object.keys(o),hasParameters:r.length>0,hasRequestBody:h(t.operation.requestBody),protocol:t.protocol}}(e,t)).filter(e=>{if(r&&e.method!==r)return!1;if(o&&e.protocol!==o)return!1;if(n&&!e.tags.some(e=>e.toLowerCase()===n))return!1;if(void 0!==i&&e.secured!==i)return!1;if(s){if(![e.ref,e.operationId??"",e.summary??"",e.path,e.tags.join(" ")].join(" ").toLowerCase().includes(s))return!1}return!0}),c=a.length,p=Math.min(Math.max(t.pageSize??100,1),500),u=function(e){if(!e)return 0;try{const t=JSON.parse(Buffer.from(e,"base64url").toString("utf8"));return Number.isInteger(t.offset)&&(t.offset??0)>=0?t.offset??0:0}catch{throw new l("invalid_cursor","The pagination cursor is malformed.")}}(t.cursor),d=a.slice(u,u+p),m=u+d.length;return{items:d,total:c,...m<c?{nextCursor:T(m)}:{}}}function I(e,t){const r={};for(const[o,n]of Object.entries(e)){if(!h(n))continue;const e={schema:n.schema?b(n.schema,t):{},examples:h(n.examples)?Object.keys(n.examples):[]};"example"in n&&(e.example=n.example),r[o]=e}return r}function U(e,t,r){const o=function(e,t){const r=x(e);let o;if(t.operationId)o=r.find(e=>e.operation.operationId===t.operationId);else if(t.method&&t.path){const e=t.method.trim().toUpperCase();o=r.find(r=>r.method===e&&r.path===t.path.trim())}else t.ref&&(o=r.find(e=>`${e.method} ${e.path}`===t.ref.trim()));if(!o)throw new l("operation_not_found",`No operation matches ${t.ref??t.operationId??`${t.method??""} ${t.path??""}`.trim()}.`);return o}(e,r),{operation:n,method:s,path:i}=o,a=S(o).map(e=>{const r=e.in,o={name:String(e.name),in:r,required:"path"===r||Boolean(e.required),deprecated:Boolean(e.deprecated),schema:e.schema?b(e.schema,t):{}};return g(e.description)&&(o.description=e.description),"example"in e&&(o.example=e.example),o}).sort((e,t)=>"path"===e.in!=("path"===t.in)?"path"===e.in?-1:1:e.required!==t.required?e.required?-1:1:e.name.localeCompare(t.name));let c;h(n.requestBody)&&h(n.requestBody.content)&&(c={required:Boolean(n.requestBody.required),...g(n.requestBody.description)?{description:n.requestBody.description}:{},content:I(n.requestBody.content,t)});const p=Object.entries(h(n.responses)?n.responses:{}).map(([e,r])=>{const o=h(r)?r:{},n=[];for(const[e,r]of Object.entries(h(o.headers)?o.headers:{})){const o=h(r)?r:{};n.push({name:e,required:Boolean(o.required),...g(o.description)?{description:o.description}:{},schema:o.schema?b(o.schema,t):{}})}return{status:e,...g(o.description)?{description:o.description}:{},headers:n,content:h(o.content)?I(o.content,t):{}}}),u=j(e,n),d=q(e),m=new Set(u.flatMap(e=>Object.keys(e))),v=d.filter(e=>m.has(e.name));return{ref:`${s} ${i}`,...g(n.operationId)?{operationId:n.operationId}:{},method:s,path:i,...g(n.summary)?{summary:n.summary}:{},...g(n.description)?{description:n.description}:{},tags:y(n.tags).filter(e=>"string"==typeof e),deprecated:Boolean(n.deprecated),idempotent:f.has(s.toLowerCase()),protocol:o.protocol,parameters:a,...c?{requestBody:c}:{},responses:p,security:v,servers:_(n.servers??e.servers)}}function P(e,t,r){let o,n;if(r.ref){o=v(t,r.ref);const e=r.ref.split("/");n=e[e.length-1]}else r.name&&(n=r.name,o=t.components?.schemas?.[r.name]);if(void 0===o)throw new l("schema_not_found",`No schema matches ${r.ref??r.name??"(empty locator)"}.`);return{...n?{name:n}:{},schema:b(o,t)}}function $(e,t){const r=q(e).filter(t=>(e.security??[]).some(e=>h(e)&&t.name in e));if(!t)return{global:r};return{global:r,operation:U(e,e,t).security}}async function C(e,t){let r;try{r=new URL(e)}catch{return{url:e,reachable:!1,latencyMs:0,error:"Invalid URL."}}if("http:"!==r.protocol&&"https:"!==r.protocol)return{url:e,reachable:!1,latencyMs:0,error:"Only http and https targets are supported."};const o=new AbortController,n=setTimeout(()=>o.abort(),t),s=Date.now();try{const t=await fetch(r,{method:"GET",signal:o.signal,redirect:"follow"}),n=t.headers.get("content-type");return{url:e,reachable:!0,status:t.status,statusText:t.statusText,...n?{contentType:n}:{},latencyMs:Date.now()-s}}catch(r){const o=r instanceof DOMException&&"AbortError"===r.name;return{url:e,reachable:!1,latencyMs:Date.now()-s,error:o?`Timed out after ${t}ms.`:r instanceof Error?r.message:String(r)}}finally{clearTimeout(n)}}function k(e,t){if(!e)return;const r=Object.entries(e).map(([e,r])=>`${e}${t}${r}`);return r.length?r:void 0}function M(e,t){const o={spec:e.path};t.baseUrl&&(o.server=t.baseUrl),t.timeoutMs&&(o.timeout=t.timeoutMs),t.concurrency&&(o.concurrency=t.concurrency),t.proxy&&(o.proxy=t.proxy);const n=k(t.headers,":");n&&(o.header=n);const s=k(t.variables,"=");s&&(o.variable=s);const i=r.resolveConfig(o);return i.formats=[],i.outputDir="",i.failOnError=!1,t.auth&&(i.auth=t.auth),i}function R(e,t){if(t?.trim())return t.trim();const r=e.document.servers;if(Array.isArray(r))for(const e of r)if(e&&"string"==typeof e.url&&e.url.trim()&&!e.url.includes("{"))return e.url.trim()}function E(e,t){if(void 0===e)return;if(!e||"object"!=typeof e||Array.isArray(e))throw new Error(`${t} must be an object of strings.`);const r={};for(const[o,n]of Object.entries(e)){if("string"!=typeof n)throw new Error(`${t}.${o} must be a string.`);r[o]=n}return r}function N(e,t){const r=R(e,"string"==typeof t.baseUrl?t.baseUrl:void 0),o={};r&&(o.baseUrl=r),"number"==typeof t.timeoutMs&&(o.timeoutMs=L(t.timeoutMs,"timeoutMs",3e4,1,6e5)),"number"==typeof t.concurrency&&(o.concurrency=L(t.concurrency,"concurrency",5,1,20));const n=E(t.headers,"headers");n&&(o.headers=n);const s=E(t.variables,"variables");return s&&(o.variables=s),"string"==typeof t.proxy&&t.proxy.trim()&&(o.proxy=t.proxy.trim()),o}function L(e,t,r,o,n){if(null==e)return r;const s=Number(e);if(!Number.isFinite(s)||s<o)throw new Error(`${t} must be a number >= ${o}.`);return Math.min(Math.round(s),n)}var D={};function B(e){return r.collectOperations(e.dereferenced,D)}async function F(e,t,n,s){const i=M(e,{...t,...s.baseUrl?{baseUrl:s.baseUrl}:{}}),a=function(e,t){const r=B(e);let o;if(t.operationId)o=r.find(e=>e.operationId===t.operationId);else if(t.method&&t.path){const e=t.method.toUpperCase();o=r.find(r=>r.method.toUpperCase()===e&&r.path===t.path)}else t.ref&&(o=r.find(e=>`${e.method.toUpperCase()} ${e.path}`===t.ref.trim()));if(!o)throw new l("operation_not_found",`No executable operation matches ${t.ref??t.operationId??`${t.method??""} ${t.path??""}`.trim()}.`);return o}(e,n),c=o.createClient(),p={config:i,spec:e.dereferenced},u=function(e){if(void 0!==e){if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("values must be an object with path/query/header/body keys.");return e}}(s.values);u&&(p.values=u),s.variables&&(p.variables=s.variables);const d=function(e){if(void 0!==e){if(!Array.isArray(e))throw new Error("assertions must be an array.");return e.filter(e=>!!e&&"object"==typeof e&&"string"==typeof e.assert)}}(s.assertions);d&&(p.extraAssertions=d),s.baseUrl&&(p.serverUrl=s.baseUrl);return(await r.executeStep(c,a,p)).result}function J(e,t){if(void 0!==e){if(!Array.isArray(e)||e.some(e=>"string"!=typeof e))throw new l("invalid_arguments",`${t} must be an array of strings.`);return e}}function H(e){const t={},r=J(e.methods,"methods");r&&(t.methods=r.map(e=>e.toLowerCase()));const o=J(e.paths,"paths");o&&(t.paths=o);const n=J(e.tags,"tags");n&&(t.tags=n);const s=J(e.operationIds,"operationIds");return s&&(t.operationIds=s),t}async function z(e,t,n,s={}){const i=M(e,t);Object.keys(n).length&&(i.filter=n);const a=r.collectOperations(e.dereferenced,i);if(0===a.length)throw new l("no_operations","No operations match the current specification and filter.");const c=o.createClient(),p=Math.max(1,Math.min(i.concurrency??5,a.length)),u=[...a],d=[],m=Date.now();let f=!1;const h=Array.from({length:p},async()=>{for(;u.length>0;){if(s.signal?.aborted)return void(f=!0);const t=u.shift();if(!t)return;const o={config:i,spec:e.dereferenced};i.variables&&(o.variables=i.variables);const n=await r.executeStep(c,t,o);d.push(n.result),s.report?.(d.length,a.length,`${t.method.toUpperCase()} ${t.path}`)}});await Promise.all(h),d.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method));const y=r.buildSummary(d,Date.now()-m);return f&&(y.skipped+=a.length-d.length,y.total=a.length),{summary:y,results:d,generatedAt:(new Date).toISOString(),version:"0.1.0"}}function G(e){if(!e||"object"!=typeof e||Array.isArray(e))throw new l("invalid_scenario","scenario must be an object with name and steps.");const t=e;if("string"!=typeof t.name||!t.name.trim())throw new l("invalid_scenario","scenario.name is required.");if(!Array.isArray(t.steps)||0===t.steps.length)throw new l("invalid_scenario","scenario.steps must be a non-empty array.");return t}function W(e,t){let o;try{o=G(t)}catch(e){if(e instanceof l)return{valid:!1,errors:[e.message]};throw e}try{const t=r.resolveSteps(o,B(e));return{valid:!0,count:t.length,steps:t.map((e,t)=>({index:t,ref:e.ref,...e.step.name?{name:e.step.name}:{}}))}}catch(e){return{valid:!1,errors:[e instanceof Error?e.message:String(e)]}}}async function V(e,t,o,n={}){const s=G(t),i=W(e,s);if(!i.valid)throw new l("invalid_scenario","Scenario failed validation.",i.errors);const a={config:M(e,o),spec:e.dereferenced,onEvent:e=>{"step:start"===e.type&&"number"==typeof e.stepIndex?n.report?.(e.stepIndex+1,s.steps.length,e.type):"scenario:finish"===e.type&&n.report?.(s.steps.length,s.steps.length,e.type)}};n.signal&&(a.signal=n.signal);return function(e){const{config:t,...r}=e;return r}(await r.runScenario(s,a))}var Y=["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS","TRACE"],K={ref:{type:"string",description:'Operation reference, e.g. "POST /orders".'},operationId:{type:"string",description:"The operationId declared in the specification."},method:{type:"string",enum:Y},path:{type:"string",description:'The path, e.g. "/orders/{orderId}".'}},Q={baseUrl:{type:"string",description:"Local backend base URL. Defaults to the first spec server URL."},timeoutMs:{type:"integer",minimum:1,maximum:6e5,default:3e4},headers:{type:"object",description:"Extra headers sent with every request.",additionalProperties:{type:"string"}},variables:{type:"object",description:"Postman-style {{name}} variables.",additionalProperties:{type:"string"}},proxy:{type:"string",description:"HTTP/HTTPS proxy URL."}},X={type:"object",description:"ScenarioDefinition: { name, description?, variables?, stopOnFailure?, steps: [{ ref | method+path, name?, skip?, request: { values?, serverUrl?, extract?: [{name, from: body|header|status, path?, key?}], assertions?: [{name, assert, ...}] } }] }",properties:{name:{type:"string"},description:{type:"string"},stopOnFailure:{type:"boolean"},variables:{type:"object",additionalProperties:{type:"string"}},steps:{type:"array",items:{type:"object"}}},required:["name","steps"]};function Z(e){const t="string"==typeof e.ref?e.ref.trim():void 0,r="string"==typeof e.operationId?e.operationId.trim():void 0,o="string"==typeof e.method?e.method.trim():void 0,n="string"==typeof e.path?e.path.trim():void 0;if(t||r||o&&n)return{...t?{ref:t}:{},...r?{operationId:r}:{},...o&&n?{method:o,path:n}:{}};throw new l("invalid_arguments","Provide either ref, operationId, or both method and path.")}var ee=[{name:"spec_overview",description:"Get the API title, version, servers, tags, protocols, security schemes, and counts. Call this first to understand the specification.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"list_operations",description:"List operations as compact rows (reference, operationId, summary, tags, status codes, auth). Filter by tag, method, protocol, auth, or free-text search. Results are paginated.",inputSchema:{type:"object",properties:{tag:{type:"string",description:"Exact tag name."},method:{type:"string",enum:Y},protocol:{type:"string",description:"http, grpc, graphql, websocket, sse, or mcp."},secured:{type:"boolean",description:"Filter by secured operations."},search:{type:"string",description:"Case-insensitive substring."},pageSize:{type:"integer",minimum:1,maximum:500,default:100},cursor:{type:"string",description:"Opaque nextCursor from a previous page."}},additionalProperties:!1}},{name:"get_operation",description:"Get the full implementation contract for one operation: parameters with validation rules, request body JSON Schemas and examples, every documented response (status, headers, body), and effective security. Use this before writing handler code so field names, types, required flags, enums, and response shapes match the spec exactly.",inputSchema:{type:"object",properties:K,additionalProperties:!1}},{name:"get_schema",description:"Get one reusable component schema (dereferenced) by component name or JSON pointer.",inputSchema:{type:"object",properties:{name:{type:"string",description:'Component schema name, e.g. "Order".'},ref:{type:"string",description:'Local pointer, e.g. "#/components/schemas/Order".'}},additionalProperties:!1}},{name:"validate_spec",description:"Validate the current specification and list errors and warnings with their locations. The document is re-read from disk, so recent edits are reflected.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"get_auth_requirements",description:"List every security scheme and, when an operation is given, the exact auth that operation requires (scheme type, header/query placement, OAuth flows and scopes). Use before implementing auth middleware or guards.",inputSchema:{type:"object",properties:K,additionalProperties:!1}},{name:"ping_target",description:"Check that the local backend is reachable before testing. Returns status and latency. Use this first when a contract test fails at the connection layer.",inputSchema:{type:"object",properties:{...Q,timeoutMs:{type:"integer",minimum:100,maximum:6e4,default:5e3}},additionalProperties:!1}},{name:"send_request",description:"Send one operation request to the running backend with optional path/query/header/body values, variables, and extra assertions. Returns the normalized response and per-assertion pass/fail. Use this to verify a single handler you just implemented.",inputSchema:{type:"object",properties:{...K,...Q,values:{type:"object",description:"Request values merged over sampled defaults: { path, query, header, body }."},assertions:{type:"array",description:"Declarative assertions added to the operation's spec assertions.",items:{type:"object"}}},additionalProperties:!1}},{name:"run_contract_tests",description:"Execute the selected operations against the backend and evaluate the spec contract (status, schema, declared assertions). Returns per-operation results and a summary. Filter by methods, tags, path regexes, or operationIds.",inputSchema:{type:"object",properties:{...Q,concurrency:{type:"integer",minimum:1,maximum:20,default:5},methods:{type:"array",items:{type:"string"}},paths:{type:"array",items:{type:"string"}},tags:{type:"array",items:{type:"string"}},operationIds:{type:"array",items:{type:"string"}}},additionalProperties:!1}},{name:"validate_scenario",description:"Statically validate an ordered multi-step scenario: every step reference must resolve, and extraction/assertion shapes are checked. Never sends a request. Call this before run_scenario.",inputSchema:{type:"object",properties:{scenario:X},required:["scenario"],additionalProperties:!1}},{name:"run_scenario",description:"Run an ordered, stateful scenario (for example register then login then create order). Steps share variables; each step can extract response values (body JSONPath, header, status) for later steps and add assertions. Streams progress; supports cancellation.",inputSchema:{type:"object",properties:{scenario:X,...Q},required:["scenario"],additionalProperties:!1}}],te=new Set(ee.map(e=>e.name));async function re(e,t,r,o={}){if(!te.has(e))throw new l("unknown_tool",`Unknown tool: ${e}`);const n=await r.load(),s=n.dereferenced,i=n.document;switch(e){case"spec_overview":return O(s);case"list_operations":{const e={};return"string"==typeof t.tag&&(e.tag=t.tag),"string"==typeof t.method&&(e.method=t.method),"string"==typeof t.protocol&&(e.protocol=t.protocol),"boolean"==typeof t.secured&&(e.secured=t.secured),"string"==typeof t.search&&(e.search=t.search),"number"==typeof t.pageSize&&(e.pageSize=t.pageSize),"string"==typeof t.cursor&&(e.cursor=t.cursor),A(s,e)}case"get_operation":return U(s,i,Z(t));case"get_schema":{const e="string"==typeof t.name?t.name.trim():void 0,r="string"==typeof t.ref?t.ref.trim():void 0;if(!e&&!r)throw new l("invalid_arguments","Provide either a schema name or a $ref pointer.");return P(0,i,{...e?{name:e}:{},...r?{ref:r}:{}})}case"validate_spec":return(await r.load(!0)).validation;case"get_auth_requirements":return void 0!==t.ref||void 0!==t.operationId||void 0!==t.method?$(s,Z(t)):$(s);case"ping_target":{const e=N(n,t).baseUrl;if(!e)throw new l("missing_base_url","Provide baseUrl or declare a concrete server URL in the specification.");return C(e,"number"==typeof t.timeoutMs?L(t.timeoutMs,"timeoutMs",5e3,100,6e4):5e3)}case"send_request":return F(n,N(n,t),Z(t),{values:t.values??void 0,variables:t.variables??void 0,assertions:t.assertions??void 0});case"run_contract_tests":return z(n,N(n,t),H(t),o);case"validate_scenario":return W(n,t.scenario);case"run_scenario":{const e=N(n,t);return V(n,t.scenario,e,o)}default:throw new l("unknown_tool",`Unknown tool: ${e}`)}}var oe="powerduck://spec/";async function ne(e){const t=await e.load();return[{uri:`${oe}source`,name:"OpenAPI source",description:"The raw OpenAPI document exactly as authored.",mimeType:t.mediaType},{uri:`${oe}overview`,name:"API overview",description:"Servers, tags, protocols, security schemes, and counts.",mimeType:"application/json"},{uri:`${oe}operations`,name:"Operations index",description:"Compact index of all operations (up to 500).",mimeType:"application/json"}]}var se=[{uriTemplate:`${oe}operation/{ref}`,name:"Operation contract",description:'Full contract for one operation. Use the reference form "METHOD /path", URL-encoded, e.g. operation/POST%20/orders.',mimeType:"application/json"},{uriTemplate:`${oe}schema/{name}`,name:"Component schema",description:"A dereferenced component schema by name.",mimeType:"application/json"}];async function ie(e,t){if(!e.startsWith(oe))throw new l("resource_not_found",`Unknown resource: ${e}`);const r=await t.load(),o=r.dereferenced,n=r.document,s=decodeURIComponent(e.slice(17));if("source"===s)return{uri:e,mimeType:r.mediaType,text:r.sourceText};if("overview"===s)return{uri:e,mimeType:"application/json",text:JSON.stringify(O(o),null,2)};if("operations"===s)return{uri:e,mimeType:"application/json",text:JSON.stringify(A(o,{pageSize:500}),null,2)};if(s.startsWith("operation/")){const t=U(o,n,{ref:s.slice(10)});return{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}}if(s.startsWith("schema/")){const t=P(0,n,{name:s.slice(7)});return{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}}throw new l("resource_not_found",`Unknown resource: ${e}`)}var ae=["PowerDuck developer MCP gives you accurate, dereferenced OpenAPI facts for the backend you are implementing and verifies it against a running server.","Call spec_overview and list_operations to orient yourself, then get_operation before writing each handler so parameters, request bodies, responses, and auth match the specification.","Use ping_target, send_request, run_contract_tests, validate_scenario, and run_scenario to verify the implementation as you build it.","Call validate_spec after the specification changes. The document is re-read from disk on every request."].join(" ");function ce(e){return{isError:!0,content:[{type:"text",text:JSON.stringify({error:e.code,message:e.message,...void 0!==e.details?{details:e.details}:{}},null,2)}]}}function pe(r,o={}){const n=new e.Server({name:o.name??"powerduck-dev-mcp",version:o.version??"0.1.0"},{capabilities:{tools:{},resources:{}},instructions:o.instructions??ae});return n.setRequestHandler(t.ListToolsRequestSchema,async()=>({tools:ee})),n.setRequestHandler(t.CallToolRequestSchema,async(e,t)=>{const{name:o,arguments:n,_meta:s}=e.params,i=s?.progressToken,a={signal:t.signal};void 0!==i&&(a.report=(e,r,o)=>{t.sendNotification({method:"notifications/progress",params:{progressToken:i,progress:e,...r?{total:r}:{},...o?{message:o}:{}}})});try{const e=await re(o,n??{},r,a);return c=e,{content:[{type:"text",text:JSON.stringify(c,null,2)}]}}catch(e){if(e instanceof l)return ce(e);const t=e instanceof Error?e.message:String(e);return ce(new l("internal_error",t))}var c}),n.setRequestHandler(t.ListResourcesRequestSchema,async()=>({resources:await ne(r)})),n.setRequestHandler(t.ListResourceTemplatesRequestSchema,async()=>({resourceTemplates:se})),n.setRequestHandler(t.ReadResourceRequestSchema,async e=>{const{uri:t}=e.params;try{const e=await ie(t,r);return{contents:[{uri:e.uri,mimeType:e.mimeType,text:e.text}]}}catch(e){if(e instanceof l)throw e;throw new l("resource_error",e instanceof Error?e.message:String(e))}}),n}function ue(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function de(e){if("string"==typeof e&&e.trim())return{message:e,severity:"error"};if(ue(e)&&"string"==typeof e.message){const t="warning"===e.severity||"info"===e.severity?e.severity:"error",r=Array.isArray(e.path)?e.path.join("/"):"string"==typeof e.path?e.path:void 0;return r?{message:e.message,severity:t,path:r}:{message:e.message,severity:t}}return null}var le=class{constructor(e){this.specPath=e}specPath;cache=null;async load(e=!1){const t=u.default.statSync(this.specPath);if(!e&&this.cache&&this.cache.mtimeMs===t.mtimeMs)return this.cache.snapshot;const r=u.default.readFileSync(this.specPath,"utf8"),o=/\.ya?ml$/i.test(this.specPath),n=function(e,t){return t?i.load(e):JSON.parse(e)}(r,o);if(!ue(n))throw new Error("The OpenAPI document must be a JSON or YAML object.");let s=n;const c="string"==typeof n.openapi?n.openapi:"";if(c&&!/^3\./.test(c))try{s=await a.upgradeOasTo32(n)}catch{s=n}const p=await async function(e){try{const t=await a.validate(e),r=[];for(const e of t.errors??[]){const t=de(e);t&&r.push(t)}return{valid:Boolean(t.valid),..."string"==typeof t.version?{openapiVersion:t.version}:{},issues:r}}catch(e){return{valid:!1,issues:[{message:e instanceof Error?e.message:String(e),severity:"error"}]}}}(s),d=await async function(e){try{const t=await a.dereference(e);return!t.errors?.length&&ue(t.schema)?t.schema:e}catch{return e}}(s),l={path:this.specPath,document:s,dereferenced:d,sourceText:r,mediaType:o?"application/yaml":"application/json",validation:p,mtimeMs:t.mtimeMs};return this.cache={mtimeMs:t.mtimeMs,snapshot:l},l}};function me(e){const t=u.default.readFileSync(e,"utf8");let r;try{r=JSON.parse(t)}catch(t){throw new Error(`Invalid JSON in ${e}: ${t instanceof Error?t.message:String(t)}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`${e} must contain a JSON object.`);const o={},n=r;return"string"==typeof n.spec&&(o.spec=n.spec),"string"==typeof n.baseUrl&&(o.baseUrl=n.baseUrl),"string"==typeof n.project&&(o.project=n.project),o}exports.FactError=l,exports.HTTP_METHODS=m,exports.SpecStore=le,exports.authRequirements=$,exports.buildRunnerConfig=M,exports.contractFilterFromArgs=H,exports.createDevMcpServer=pe,exports.devTools=ee,exports.executeDevTool=re,exports.getOperation=U,exports.getSchema=P,exports.listOperations=A,exports.listSpecResources=ne,exports.loadDevConfig=function(e){const t=e?d.default.resolve(e):d.default.resolve(process.cwd(),"powerduck.dev.json");return u.default.existsSync(t)?{config:me(t),configDir:d.default.dirname(t)}:{config:{},configDir:process.cwd()}},exports.overview=O,exports.pingTarget=C,exports.readSpecResource=ie,exports.resolveBaseUrl=R,exports.resolveFrom=function(e,t){return t?d.default.resolve(e,t):void 0},exports.resolvePointer=v,exports.runContractSuite=z,exports.runOverridesFromArgs=N,exports.runScenarioTool=V,exports.sendOne=F,exports.specResourceTemplates=se,exports.startStdioServer=async function(e){const t=function(){const e={log:console.log,info:console.info,debug:console.debug,dir:console.dir},t=(...e)=>console.error(...e);return console.log=t,console.info=t,console.debug=t,console.dir=t,()=>{console.log=e.log,console.info=e.info,console.debug=e.debug,console.dir=e.dir}}(),r=pe(new le(e.specPath),{...e.name?{name:e.name}:{},...e.version?{version:e.version}:{}}),o=new n.StdioServerTransport;let s=()=>{};const i=new Promise(e=>{s=e});let a=!1;const c=[],p=()=>{if(!a){a=!0;for(const e of c)e();t(),s()}};if(o.onerror=e=>{console.error("[dev-mcp:stdio] transport error:",e)},o.onclose=()=>p(),await r.connect(o),!1!==e.handleSignals)for(const e of["SIGINT","SIGTERM"]){const t=()=>{r.close().catch(()=>{})};process.once(e,t),c.push(()=>process.removeListener(e,t))}return{closed:i,close:async()=>{try{await r.close()}finally{p()}}}},exports.validateScenario=W;
|