From b599e5d62a528222955ef5c251f0978c4bc175b5 Mon Sep 17 00:00:00 2001 From: Liam Wynne Date: Tue, 12 May 2026 12:22:00 +1000 Subject: [PATCH 1/3] docs: mark project as explicitly open source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds "open source" language to the pitch and footer. The repo was already public + MIT, but the README didn't say so in words — now it does, with a "View source" link in the footer for discoverability. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f37971c..e77b47e 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ That's why ERD Studio exists. ## The solution -ERD Studio is a **free, AI-native alternative** that puts the semantic model in the **same repo as the SQL**, as a visual canvas both you and the AI can read and write. +ERD Studio is a **free and open source, AI-native alternative** that puts the semantic model in the **same repo as the SQL**, as a visual canvas both you and the AI can read and write. Under the hood, your model is plain YAML and JSON — AI reads it natively, git diffs it cleanly, and the canvas is just the human-readable view. No proprietary format, no lock-in. @@ -335,6 +335,6 @@ That's the whole system. Three folders, two file types, one diagram per JSON, on ---

- MIT License • Made for the dbt community
- Report a bugStart a discussion + Open source (MIT) • Made for the dbt community
+ View sourceReport a bugStart a discussion

From 4e65959bbfefdc6095d2df9280f7ef04fcce3b70 Mon Sep 17 00:00:00 2001 From: Liam Wynne Date: Tue, 12 May 2026 12:22:13 +1000 Subject: [PATCH 2/3] feat: add MCP server (erd-studio-mcp v0.1.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Model Context Protocol server at mcp-server/ that gives Claude, Cursor, Continue, Zed, etc. read-only access to a dbt project's ERD Studio semantic model. Architecture: - New package mcp-server/, published independently as erd-studio-mcp - Reuses existing pure-Node services (DomainService, LogicalModelService, ManifestService) — no refactoring needed; those services were already isolated from vscode APIs - stdio transport via @modelcontextprotocol/sdk - One file per tool, thin wrappers around the services - esbuild bundles index.js + manifestWorker.js with ESM __dirname polyfill so the worker_threads spawn path resolves correctly Tools (v1, all read-only, all idempotent): - list_domains — ERDs grouped by layer - read_domain — full domain incl. models, columns, relationships, cardinality, rationale - list_models — all logical models from logical-models/*.yml - read_model — one logical model with column-level metadata - list_manifest_models — what dbt actually built, from target/manifest.json, including unique/relationship test coverage Mutation tools (propose_model_change, apply_sync_plan) deferred to v2. Verified end-to-end via mcp-server/test-smoke.mjs against test/fixtures/dbt-project — initialize handshake + all 5 tool calls return expected data. Install: `npx erd-studio-mcp` or `claude mcp add erd-studio -- npx -y erd-studio-mcp` Co-Authored-By: Claude Opus 4.7 (1M context) --- mcp-server/.gitignore | 3 + mcp-server/LICENSE | 21 + mcp-server/README.md | 87 + mcp-server/esbuild.js | 44 + mcp-server/package-lock.json | 1664 ++++++++++++++++++ mcp-server/package.json | 54 + mcp-server/src/index.ts | 60 + mcp-server/src/services.ts | 47 + mcp-server/src/tools/list_domains.ts | 51 + mcp-server/src/tools/list_manifest_models.ts | 73 + mcp-server/src/tools/list_models.ts | 48 + mcp-server/src/tools/read_domain.ts | 96 + mcp-server/src/tools/read_model.ts | 69 + mcp-server/test-smoke.mjs | 133 ++ mcp-server/tsconfig.json | 17 + 15 files changed, 2467 insertions(+) create mode 100644 mcp-server/.gitignore create mode 100644 mcp-server/LICENSE create mode 100644 mcp-server/README.md create mode 100644 mcp-server/esbuild.js create mode 100644 mcp-server/package-lock.json create mode 100644 mcp-server/package.json create mode 100644 mcp-server/src/index.ts create mode 100644 mcp-server/src/services.ts create mode 100644 mcp-server/src/tools/list_domains.ts create mode 100644 mcp-server/src/tools/list_manifest_models.ts create mode 100644 mcp-server/src/tools/list_models.ts create mode 100644 mcp-server/src/tools/read_domain.ts create mode 100644 mcp-server/src/tools/read_model.ts create mode 100644 mcp-server/test-smoke.mjs create mode 100644 mcp-server/tsconfig.json diff --git a/mcp-server/.gitignore b/mcp-server/.gitignore new file mode 100644 index 0000000..3c25e1e --- /dev/null +++ b/mcp-server/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/mcp-server/LICENSE b/mcp-server/LICENSE new file mode 100644 index 0000000..5d91bda --- /dev/null +++ b/mcp-server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Liam Wynne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 0000000..6e5bda8 --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,87 @@ +# erd-studio-mcp + +[![npm version](https://img.shields.io/npm/v/erd-studio-mcp.svg)](https://www.npmjs.com/package/erd-studio-mcp) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +**MCP server for [ERD Studio](https://github.com/liam-machine/erd-studio)** — gives Claude, Cursor, Continue, Zed, or any [Model Context Protocol](https://modelcontextprotocol.io) client read-only access to your dbt project's semantic ERD model. + +Once installed, your AI assistant can answer questions like: +- *"What domains exist in this dbt project?"* +- *"Show me every model in the `customer-360` domain and how they relate."* +- *"What's the grain of `dim_customer`? What's the design rationale?"* +- *"Which dbt models have `unique` tests but aren't in any ERD?"* + +…without you re-explaining your data model in every prompt. + +## Install + +### Claude Code + +```bash +claude mcp add erd-studio -- npx -y erd-studio-mcp +``` + +### Claude Desktop + +Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): + +```json +{ + "mcpServers": { + "erd-studio": { + "command": "npx", + "args": ["-y", "erd-studio-mcp"] + } + } +} +``` + +### Cursor / Continue / Zed + +These editors support MCP via their respective config files. Use the same `command`/`args` shape — see your editor's MCP docs. + +## Tools + +All tools take a `project_path` argument: the **absolute path** to the dbt project root (the directory containing `dbt_project.yml`). The project should also contain an `erd-studio/` directory created by the [ERD Studio VS Code extension](https://marketplace.visualstudio.com/items?itemName=liamwynne.erd-studio). + +| Tool | Returns | +|---|---| +| `list_domains` | All ERDs grouped by layer. Filter optional by `layer`. | +| `read_domain` | Full domain: models + columns + relationships + cardinality + rationale. | +| `list_models` | All logical model definitions from `erd-studio/logical-models/*.yml`. | +| `read_model` | Single logical model with column-level metadata, grain, SCD types, rationale. | +| `list_manifest_models` | Models from `target/manifest.json` (what dbt actually built), with unique/relationship test coverage. Filter optional by `name_contains`. | + +All tools are read-only. Mutation tools (propose model change, apply sync plan) are planned for v2. + +## What the AI gets + +Unlike most ERD tools where the model is locked behind a vendor UI, ERD Studio stores the semantic model as **plain YAML + JSON in your dbt repo**. This MCP server exposes that model structurally: + +- **Grain** as a first-class field — *"one row per customer (current + history)"* +- **Model role** — `conformed-dim`, `transaction-fact`, `bridge`, etc. +- **SCD type per column** — `0` fixed, `1` overwrite, `2` track history +- **Design rationale** — *why* the model was designed this way +- **Cross-stage drift** — manifest test coverage compared to design intent + +So when you ask the AI *"propose a column to add to `dim_customer`"*, it sees not just the column list but the design intent — and produces proposals that align with your modelling style. + +## Requirements + +- **Node.js ≥ 18** +- A dbt project (containing `dbt_project.yml`) +- *(Optional but recommended)* The [ERD Studio VS Code extension](https://marketplace.visualstudio.com/items?itemName=liamwynne.erd-studio) for creating and editing ERDs visually. The MCP server reads the same files the extension writes. + +## Without ERD Studio yet + +If your dbt project doesn't have an `erd-studio/` directory yet, install the extension and run `dbt: Set Up Semantic Domains Directory` in VS Code. The MCP server reads `manifest.json`-only data too (via `list_manifest_models`), so you can start there before drawing any ERDs. + +## Source + +- Main repo: https://github.com/liam-machine/erd-studio +- Server source: [`mcp-server/`](https://github.com/liam-machine/erd-studio/tree/main/mcp-server) +- Issues: https://github.com/liam-machine/erd-studio/issues + +## License + +MIT — see [LICENSE](https://github.com/liam-machine/erd-studio/blob/main/LICENSE) at the repo root. diff --git a/mcp-server/esbuild.js b/mcp-server/esbuild.js new file mode 100644 index 0000000..8f701cd --- /dev/null +++ b/mcp-server/esbuild.js @@ -0,0 +1,44 @@ +import { build } from 'esbuild'; +import { chmodSync } from 'node:fs'; + +const dirnamePolyfill = + 'import { fileURLToPath as __ercfu } from "url"; ' + + 'import { dirname as __ercd } from "path"; ' + + 'const __filename = __ercfu(import.meta.url); ' + + 'const __dirname = __ercd(__filename);'; + +const requirePolyfill = + 'import { createRequire } from "module"; ' + + 'const require = createRequire(import.meta.url);'; + +// Main entry — the MCP stdio server +await build({ + entryPoints: ['src/index.ts'], + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outfile: 'dist/index.js', + external: ['@modelcontextprotocol/sdk', 'zod', 'yaml'], + banner: { + js: `#!/usr/bin/env node\n${requirePolyfill} ${dirnamePolyfill}`, + }, + logLevel: 'info', +}); +chmodSync('dist/index.js', 0o755); + +// Manifest worker — bundled separately, spawned by ManifestService via worker_threads +await build({ + entryPoints: ['../src/workers/manifestWorker.ts'], + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outfile: 'dist/manifestWorker.js', + banner: { + js: `${requirePolyfill} ${dirnamePolyfill}`, + }, + logLevel: 'info', +}); + +console.log('Built dist/index.js + dist/manifestWorker.js'); diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json new file mode 100644 index 0000000..450b7ee --- /dev/null +++ b/mcp-server/package-lock.json @@ -0,0 +1,1664 @@ +{ + "name": "erd-studio-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "erd-studio-mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "yaml": "^2.6.0", + "zod": "^3.23.0" + }, + "bin": { + "erd-studio-mcp": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "esbuild": "^0.24.0", + "typescript": "^5.5.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 0000000..bf5f73d --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,54 @@ +{ + "name": "erd-studio-mcp", + "version": "0.1.0", + "description": "MCP server for ERD Studio — read your dbt project's semantic ERD model from Claude, Cursor, Continue, or any MCP client.", + "type": "module", + "bin": { + "erd-studio-mcp": "./dist/index.js" + }, + "main": "./dist/index.js", + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "node esbuild.js", + "start": "node dist/index.js", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "dbt", + "erd", + "data-modeling", + "analytics-engineering", + "claude", + "ai-tools", + "semantic-layer" + ], + "author": "Liam Wynne", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/liam-machine/erd-studio", + "directory": "mcp-server" + }, + "homepage": "https://github.com/liam-machine/erd-studio#readme", + "bugs": { + "url": "https://github.com/liam-machine/erd-studio/issues" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "yaml": "^2.6.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "esbuild": "^0.24.0", + "typescript": "^5.5.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts new file mode 100644 index 0000000..1eb1ca5 --- /dev/null +++ b/mcp-server/src/index.ts @@ -0,0 +1,60 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; + +import { list_domains } from './tools/list_domains.js'; +import { read_domain } from './tools/read_domain.js'; +import { list_models } from './tools/list_models.js'; +import { read_model } from './tools/read_model.js'; +import { list_manifest_models } from './tools/list_manifest_models.js'; + +const SERVER_INFO = { + name: 'erd-studio-mcp', + version: '0.1.0', +}; + +const INSTRUCTIONS = `ERD Studio MCP server — read-only access to a dbt project's semantic ERD model. + +Every tool takes \`project_path\`: the absolute path to a dbt project root (the +directory containing dbt_project.yml). The project should also contain an +\`erd-studio/\` directory created by the ERD Studio VS Code extension. + +Typical workflow: +1. list_domains — see what ERDs exist +2. read_domain — get models + relationships + cardinality for one ERD +3. read_model — get full column-level design for one logical model +4. list_manifest_models — see what dbt actually built (compare to design)`; + +const tools = [ + list_domains, + read_domain, + list_models, + read_model, + list_manifest_models, +]; + +async function main(): Promise { + const server = new McpServer(SERVER_INFO, { instructions: INSTRUCTIONS }); + + for (const tool of tools) { + server.registerTool(tool.name, tool.config, async (args: unknown) => { + try { + return await tool.handler(args as never); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { + content: [{ type: 'text' as const, text: `Error in ${tool.name}: ${message}` }], + isError: true, + }; + } + }); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error(`erd-studio-mcp v${SERVER_INFO.version} listening on stdio`); +} + +main().catch((err) => { + console.error('Fatal error starting erd-studio-mcp:', err); + process.exit(1); +}); diff --git a/mcp-server/src/services.ts b/mcp-server/src/services.ts new file mode 100644 index 0000000..e29ebb7 --- /dev/null +++ b/mcp-server/src/services.ts @@ -0,0 +1,47 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { LayerService } from '../../src/services/layerService.js'; +import { LogicalModelService } from '../../src/services/logicalModelService.js'; +import { DomainService } from '../../src/services/domainService.js'; +import { ManifestService } from '../../src/services/manifestService.js'; + +export interface Services { + projectPath: string; + semanticDir: string; + layerService: LayerService; + logicalModelService: LogicalModelService; + domainService: DomainService; + manifestService: ManifestService; +} + +const SEMANTIC_DIR = 'erd-studio'; + +export function resolveProjectPath(input: string): string { + const resolved = path.resolve(input); + const dbtProjectFile = path.join(resolved, 'dbt_project.yml'); + if (!fs.existsSync(dbtProjectFile)) { + throw new Error( + `Not a dbt project: ${resolved} (missing dbt_project.yml). ` + + `Pass the absolute path to the directory containing dbt_project.yml.`, + ); + } + return resolved; +} + +export function buildServices(projectPathInput: string): Services { + const projectPath = resolveProjectPath(projectPathInput); + const layerService = new LayerService(projectPath, SEMANTIC_DIR); + const logicalModelService = new LogicalModelService(projectPath, SEMANTIC_DIR); + const domainService = new DomainService(layerService); + domainService.setLogicalModelService(logicalModelService); + const manifestService = new ManifestService(); + return { + projectPath, + semanticDir: SEMANTIC_DIR, + layerService, + logicalModelService, + domainService, + manifestService, + }; +} diff --git a/mcp-server/src/tools/list_domains.ts b/mcp-server/src/tools/list_domains.ts new file mode 100644 index 0000000..3c98c62 --- /dev/null +++ b/mcp-server/src/tools/list_domains.ts @@ -0,0 +1,51 @@ +import { z } from 'zod'; +import { buildServices } from '../services.js'; + +export const list_domains = { + name: 'list_domains', + config: { + title: 'List ERD domains', + description: + 'List all ERD Studio domains (diagrams) in a dbt project, grouped by layer. ' + + 'Each domain represents one ERD with its own models and relationships. ' + + 'Returns lightweight summaries — call read_domain for full details.', + inputSchema: { + project_path: z + .string() + .describe('Absolute path to the dbt project root (directory containing dbt_project.yml).'), + layer: z + .string() + .optional() + .describe('Optional. Filter to a single layer (e.g. "silver", "gold").'), + }, + annotations: { + readOnlyHint: true, + idempotentHint: true, + }, + }, + async handler({ project_path, layer }: { project_path: string; layer?: string }) { + const { domainService, projectPath, semanticDir } = buildServices(project_path); + const summaries = domainService.listDomains(projectPath, semanticDir); + const filtered = layer ? summaries.filter((s) => s.layer === layer) : summaries; + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + count: filtered.length, + project_path: projectPath, + domains: filtered.map((s) => ({ + domain: s.domain, + layer: s.layer, + file_path: s.filePath, + })), + }, + null, + 2, + ), + }, + ], + }; + }, +}; diff --git a/mcp-server/src/tools/list_manifest_models.ts b/mcp-server/src/tools/list_manifest_models.ts new file mode 100644 index 0000000..8b220df --- /dev/null +++ b/mcp-server/src/tools/list_manifest_models.ts @@ -0,0 +1,73 @@ +import { z } from 'zod'; +import { buildServices } from '../services.js'; + +export const list_manifest_models = { + name: 'list_manifest_models', + config: { + title: 'List dbt manifest models', + description: + 'List all models from target/manifest.json — what dbt actually built. ' + + 'Returns model name, schema, columns with data types from the warehouse, and existing ' + + 'unique/relationship test coverage. This is the ground truth from dbt, ' + + 'complementing the design source-of-truth in list_models / read_domain.', + inputSchema: { + project_path: z + .string() + .describe('Absolute path to the dbt project root.'), + name_contains: z + .string() + .optional() + .describe('Optional. Case-insensitive substring filter on model name (e.g. "dim_" or "fct_").'), + }, + annotations: { + readOnlyHint: true, + idempotentHint: true, + }, + }, + async handler({ + project_path, + name_contains, + }: { + project_path: string; + name_contains?: string; + }) { + const { manifestService, projectPath } = buildServices(project_path); + const manifest = await manifestService.loadManifest(projectPath); + + const filter = name_contains?.toLowerCase(); + const allModels = Array.from(manifest.models.entries()); + const matched = filter + ? allModels.filter(([name]) => name.toLowerCase().includes(filter)) + : allModels; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + count: matched.length, + total: allModels.length, + models: matched.map(([name, info]) => ({ + name, + schema: info.schema ?? null, + description: info.description ?? null, + column_count: info.columns?.length ?? 0, + unique_columns: Array.from(manifest.uniqueColumns.get(name) ?? []), + relationships: manifest.relationshipTests + .filter((t) => t.fromModel === name) + .map((t) => ({ + from_column: t.fromColumn, + to_model: t.toModel, + to_column: t.toColumn, + })), + })), + }, + null, + 2, + ), + }, + ], + }; + }, +}; diff --git a/mcp-server/src/tools/list_models.ts b/mcp-server/src/tools/list_models.ts new file mode 100644 index 0000000..5f3b686 --- /dev/null +++ b/mcp-server/src/tools/list_models.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; +import { buildServices } from '../services.js'; + +export const list_models = { + name: 'list_models', + config: { + title: 'List logical models', + description: + 'List all logical model definitions in erd-studio/logical-models/. ' + + 'Each model is one table (dimension, fact, bridge, etc.) reusable across domains. ' + + 'Returns model names + light metadata. Call read_model for full column-level detail.', + inputSchema: { + project_path: z + .string() + .describe('Absolute path to the dbt project root.'), + }, + annotations: { + readOnlyHint: true, + idempotentHint: true, + }, + }, + async handler({ project_path }: { project_path: string }) { + const { logicalModelService } = buildServices(project_path); + const models = logicalModelService.listModels(); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + count: models.length, + models: models.map((m) => ({ + name: m.name, + schema: m.schema ?? null, + description: m.description ?? null, + grain: m.grain ?? null, + model_role: m.modelRole ?? null, + column_count: m.columns?.length ?? 0, + })), + }, + null, + 2, + ), + }, + ], + }; + }, +}; diff --git a/mcp-server/src/tools/read_domain.ts b/mcp-server/src/tools/read_domain.ts new file mode 100644 index 0000000..73eb1d9 --- /dev/null +++ b/mcp-server/src/tools/read_domain.ts @@ -0,0 +1,96 @@ +import * as path from 'node:path'; +import * as fs from 'node:fs'; +import { z } from 'zod'; +import { buildServices } from '../services.js'; + +export const read_domain = { + name: 'read_domain', + config: { + title: 'Read a domain (logical stage)', + description: + 'Read a full ERD domain by name + layer. Returns the logical stage: ' + + 'models with columns (data types, PK/FK/NK flags, SCD types), grain, model role, ' + + 'rationale, and the relationships drawn between them with cardinality. ' + + 'This is the design source-of-truth. For what dbt actually built, see list_manifest_models.', + inputSchema: { + project_path: z + .string() + .describe('Absolute path to the dbt project root.'), + layer: z + .string() + .describe('Layer name (e.g. "silver", "gold").'), + domain: z + .string() + .describe('Domain slug (filename without .json).'), + }, + annotations: { + readOnlyHint: true, + idempotentHint: true, + }, + }, + async handler({ + project_path, + layer, + domain, + }: { + project_path: string; + layer: string; + domain: string; + }) { + const { domainService, projectPath, semanticDir } = buildServices(project_path); + const filePath = path.join(projectPath, semanticDir, layer, `${domain}.json`); + if (!fs.existsSync(filePath)) { + throw new Error( + `Domain not found: ${layer}/${domain}.json. Use list_domains to see what's available.`, + ); + } + + const unified = domainService.getDomain(filePath); + const stage = domainService.getDomainStage(filePath); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + domain: stage.domain, + layer: stage.layer, + stage: stage.stage, + description: stage.description ?? null, + model_folder: stage.modelFolder ?? null, + models: stage.models.map((m) => ({ + name: m.name, + schema: m.schema ?? null, + description: m.description ?? null, + grain: m.grain ?? null, + model_role: m.modelRole ?? null, + rationale: m.rationale ?? null, + columns: (m.columns ?? []).map((c) => ({ + name: c.name, + data_type: c.dataType ?? null, + description: c.description ?? null, + is_primary_key: c.isPrimaryKey === true, + is_foreign_key: c.isForeignKey === true, + is_natural_key: c.isNaturalKey === true, + ...(c.scdType != null ? { scd_type: c.scdType } : {}), + ...(c.additiveType ? { additive_type: c.additiveType } : {}), + })), + })), + relationships: stage.relationships.map((r) => ({ + from_model: r.fromModel, + from_column: r.fromColumn, + to_model: r.toModel, + to_column: r.toColumn, + cardinality: r.cardinality, + })), + view_config: unified.viewConfig ?? {}, + }, + null, + 2, + ), + }, + ], + }; + }, +}; diff --git a/mcp-server/src/tools/read_model.ts b/mcp-server/src/tools/read_model.ts new file mode 100644 index 0000000..57e504d --- /dev/null +++ b/mcp-server/src/tools/read_model.ts @@ -0,0 +1,69 @@ +import { z } from 'zod'; +import { buildServices } from '../services.js'; + +export const read_model = { + name: 'read_model', + config: { + title: 'Read a logical model', + description: + 'Read full metadata for one logical model: columns with data types and PK/FK/NK flags, ' + + 'grain, model role, SCD types, additive types, and design rationale. ' + + 'This is the design specification — the single source of truth for what the table should be.', + inputSchema: { + project_path: z + .string() + .describe('Absolute path to the dbt project root.'), + model_name: z + .string() + .describe('Logical model name (filename without .yml).'), + }, + annotations: { + readOnlyHint: true, + idempotentHint: true, + }, + }, + async handler({ + project_path, + model_name, + }: { + project_path: string; + model_name: string; + }) { + const { logicalModelService } = buildServices(project_path); + const model = logicalModelService.getModel(model_name); + if (!model) { + throw new Error( + `Model not found: ${model_name}. Use list_models to see what's available.`, + ); + } + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + name: model.name, + schema: model.schema ?? null, + description: model.description ?? null, + grain: model.grain ?? null, + model_role: model.modelRole ?? null, + rationale: model.rationale ?? null, + columns: (model.columns ?? []).map((c) => ({ + name: c.name, + data_type: c.dataType ?? null, + description: c.description ?? null, + is_primary_key: c.isPrimaryKey === true, + is_foreign_key: c.isForeignKey === true, + is_natural_key: c.isNaturalKey === true, + ...(c.scdType != null ? { scd_type: c.scdType } : {}), + ...(c.additiveType ? { additive_type: c.additiveType } : {}), + })), + }, + null, + 2, + ), + }, + ], + }; + }, +}; diff --git a/mcp-server/test-smoke.mjs b/mcp-server/test-smoke.mjs new file mode 100644 index 0000000..c7ce2e0 --- /dev/null +++ b/mcp-server/test-smoke.mjs @@ -0,0 +1,133 @@ +#!/usr/bin/env node +// Quick smoke test — spawn the server, run a few JSON-RPC calls, print results. + +import { spawn } from 'node:child_process'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PROJECT_PATH = path.resolve(__dirname, '../test/fixtures/dbt-project'); +const SERVER = path.resolve(__dirname, 'dist/index.js'); + +const child = spawn('node', [SERVER], { + stdio: ['pipe', 'pipe', 'inherit'], +}); + +let buffer = ''; +const pending = new Map(); +let nextId = 1; + +child.stdout.on('data', (chunk) => { + buffer += chunk.toString(); + let nl; + while ((nl = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, nl); + buffer = buffer.slice(nl + 1); + if (!line.trim()) continue; + try { + const msg = JSON.parse(line); + if (msg.id && pending.has(msg.id)) { + pending.get(msg.id)(msg); + pending.delete(msg.id); + } + } catch { + // notification or partial + } + } +}); + +function rpc(method, params) { + return new Promise((resolve) => { + const id = nextId++; + pending.set(id, resolve); + child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); + }); +} + +function notify(method, params) { + child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n'); +} + +function summarize(label, msg) { + if (msg.error) { + console.log(`❌ ${label}: ${JSON.stringify(msg.error)}`); + return false; + } + const text = msg.result?.content?.[0]?.text; + if (text) { + const parsed = JSON.parse(text); + console.log(`✅ ${label}`); + console.log(JSON.stringify(parsed, null, 2).split('\n').slice(0, 15).join('\n')); + console.log('...'); + } else if (msg.result?.tools) { + console.log(`✅ ${label}: ${msg.result.tools.length} tools`); + for (const t of msg.result.tools) console.log(` • ${t.name}`); + } else { + console.log(`✅ ${label}:`, Object.keys(msg.result || {}).join(',')); + } + return true; +} + +async function main() { + // 1. Initialize + const init = await rpc('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'smoke-test', version: '0.1.0' }, + }); + summarize('initialize', init); + notify('notifications/initialized', {}); + + // 2. List tools + const list = await rpc('tools/list', {}); + summarize('tools/list', list); + + // 3. Call list_domains + console.log('\n--- list_domains ---'); + const ld = await rpc('tools/call', { + name: 'list_domains', + arguments: { project_path: PROJECT_PATH }, + }); + summarize('list_domains', ld); + + // 4. Call read_domain for showcase + console.log('\n--- read_domain showcase ---'); + const rd = await rpc('tools/call', { + name: 'read_domain', + arguments: { project_path: PROJECT_PATH, layer: 'silver', domain: 'showcase' }, + }); + summarize('read_domain', rd); + + // 5. Call list_models + console.log('\n--- list_models ---'); + const lm = await rpc('tools/call', { + name: 'list_models', + arguments: { project_path: PROJECT_PATH }, + }); + summarize('list_models', lm); + + // 6. Call read_model + console.log('\n--- read_model dim_customer ---'); + const rm = await rpc('tools/call', { + name: 'read_model', + arguments: { project_path: PROJECT_PATH, model_name: 'dim_customer' }, + }); + summarize('read_model', rm); + + // 7. Call list_manifest_models + console.log('\n--- list_manifest_models ---'); + const lmm = await rpc('tools/call', { + name: 'list_manifest_models', + arguments: { project_path: PROJECT_PATH, name_contains: 'dim' }, + }); + summarize('list_manifest_models', lmm); + + child.kill(); + process.exit(0); +} + +main().catch((e) => { + console.error('Smoke test failed:', e); + child.kill(); + process.exit(1); +}); diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 0000000..6bb8ebc --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "isolatedModules": true, + "allowImportingTsExtensions": false + }, + "include": ["src/**/*", "../src/services/**/*", "../src/types/**/*"], + "exclude": ["node_modules", "dist"] +} From 2a3037f5725705834e6cb0351d38dc2ed10dea39 Mon Sep 17 00:00:00 2001 From: Liam Wynne Date: Tue, 12 May 2026 12:42:29 +1000 Subject: [PATCH 3/3] feat(mcp-server): position MCP as read-only complement to the extension The MCP server now explicitly signals it is read-only and points users to the ERD Studio VS Code extension (which ships an AI coding skill) for any write/design workflow. The two are complementary distribution paths, not redundant: the skill is deeper for Claude Code users; the MCP is broader for Cursor/Continue/Zed/Claude Desktop and discoverability via the MCP registry. Changes: - New tool: get_editor_setup returns canonical install commands + marketplace link. AI is instructed to call this when the user asks about editing/designing/building. - Server instructions block now leads with "READ-ONLY by design" and the extension path. - list_domains / list_models gracefully return empty result + a `tip` field when erd-studio/ doesn't exist, instead of erroring on fs.readdir. Same friendly path on read_domain / read_model (throw with the tip in the error message). - Smoke test extended: verifies get_editor_setup returns the marketplace link, and verifies an uninitialized project returns the install tip. - README rewritten to lead with the read-only + extension positioning, with a clear "pick the right tool" table. Co-Authored-By: Claude Opus 4.7 (1M context) --- mcp-server/README.md | 29 ++++++++- mcp-server/src/index.ts | 35 +++++++++-- mcp-server/src/lib/setup.ts | 26 ++++++++ mcp-server/src/tools/get_editor_setup.ts | 75 ++++++++++++++++++++++++ mcp-server/src/tools/list_domains.ts | 22 +++++++ mcp-server/src/tools/list_models.ts | 19 +++++- mcp-server/src/tools/read_domain.ts | 8 ++- mcp-server/src/tools/read_model.ts | 10 +++- mcp-server/test-smoke.mjs | 35 +++++++++++ 9 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 mcp-server/src/lib/setup.ts create mode 100644 mcp-server/src/tools/get_editor_setup.ts diff --git a/mcp-server/README.md b/mcp-server/README.md index 6e5bda8..3788074 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -3,7 +3,7 @@ [![npm version](https://img.shields.io/npm/v/erd-studio-mcp.svg)](https://www.npmjs.com/package/erd-studio-mcp) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -**MCP server for [ERD Studio](https://github.com/liam-machine/erd-studio)** — gives Claude, Cursor, Continue, Zed, or any [Model Context Protocol](https://modelcontextprotocol.io) client read-only access to your dbt project's semantic ERD model. +**MCP server for [ERD Studio](https://github.com/liam-machine/erd-studio)** — gives Claude, Cursor, Continue, Zed, or any [Model Context Protocol](https://modelcontextprotocol.io) client **read-only** access to your dbt project's semantic ERD model. Once installed, your AI assistant can answer questions like: - *"What domains exist in this dbt project?"* @@ -13,6 +13,22 @@ Once installed, your AI assistant can answer questions like: …without you re-explaining your data model in every prompt. +## Read-only by design — for edits, use the VS Code extension + +This MCP server is intentionally read-only. For **designing new ERDs, adding models, drawing relationships, generating dbt SQL + schema YAML**, install the [ERD Studio VS Code extension](https://marketplace.visualstudio.com/items?itemName=liamwynne.erd-studio). The extension ships with an AI coding skill (`.claude/skills/erd-studio/SKILL.md` for Claude Code, equivalents for Copilot / Gemini / Codex) that gives your assistant **full read+write access** via its native file-editing tools — multi-file edits, refactor-style changes, complete schema authoring. + +**Pick the right tool for the job:** + +| Workflow | Tool | +|---|---| +| Inspect an existing model from any MCP client (Claude Desktop, Cursor, Continue, Zed, …) | **This MCP server** | +| Design / edit / build (in Claude Code, Copilot, Gemini, Codex) | **Extension + its skill** | +| Visual canvas editing | **Extension** | + +The MCP and the skill are complementary, not redundant. If you're in Claude Code already, the skill does more. If you're not, the MCP at least gets you read access. + +If the user asks the AI to design or modify anything, the AI will (via the `get_editor_setup` tool) point them at the extension's install path. + ## Install ### Claude Code @@ -51,8 +67,9 @@ All tools take a `project_path` argument: the **absolute path** to the dbt proje | `list_models` | All logical model definitions from `erd-studio/logical-models/*.yml`. | | `read_model` | Single logical model with column-level metadata, grain, SCD types, rationale. | | `list_manifest_models` | Models from `target/manifest.json` (what dbt actually built), with unique/relationship test coverage. Filter optional by `name_contains`. | +| `get_editor_setup` | Returns install instructions for the ERD Studio VS Code extension. Use this when the user wants to edit, design, or build (this MCP server is read-only). | -All tools are read-only. Mutation tools (propose model change, apply sync plan) are planned for v2. +All tools are read-only. If the project hasn't been initialized with an `erd-studio/` directory yet, list-tools return empty results with a `tip` field pointing to the install path; read-tools throw a friendly error doing the same. Either way the AI naturally surfaces the extension install path to the user. ## What the AI gets @@ -74,7 +91,13 @@ So when you ask the AI *"propose a column to add to `dim_customer`"*, it sees no ## Without ERD Studio yet -If your dbt project doesn't have an `erd-studio/` directory yet, install the extension and run `dbt: Set Up Semantic Domains Directory` in VS Code. The MCP server reads `manifest.json`-only data too (via `list_manifest_models`), so you can start there before drawing any ERDs. +If your dbt project doesn't have an `erd-studio/` directory yet: + +1. Install the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=liamwynne.erd-studio) +2. In VS Code: Command Palette → `dbt: Set Up Semantic Domains Directory` +3. Optionally: Command Palette → `dbt: Install AI Coding Harness` to get the bundled skill + +The MCP server still works on uninitialized projects — `list_manifest_models` reads `target/manifest.json` directly, and the other tools return graceful "install the extension to start designing" tips. So you can install this MCP first to inspect what dbt has, then move to the extension for the design work. ## Source diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 1eb1ca5..a444546 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -6,23 +6,45 @@ import { read_domain } from './tools/read_domain.js'; import { list_models } from './tools/list_models.js'; import { read_model } from './tools/read_model.js'; import { list_manifest_models } from './tools/list_manifest_models.js'; +import { get_editor_setup } from './tools/get_editor_setup.js'; const SERVER_INFO = { name: 'erd-studio-mcp', version: '0.1.0', }; -const INSTRUCTIONS = `ERD Studio MCP server — read-only access to a dbt project's semantic ERD model. +const INSTRUCTIONS = `ERD Studio MCP server — **READ-ONLY** access to a dbt project's semantic ERD model. -Every tool takes \`project_path\`: the absolute path to a dbt project root (the -directory containing dbt_project.yml). The project should also contain an -\`erd-studio/\` directory created by the ERD Studio VS Code extension. +This server is intentionally read-only. It lets you inspect a dbt project's ERD design +(domains, logical models, columns with PK/FK/NK flags, relationships with cardinality, +grain, model roles, design rationale) and what dbt actually built (manifest models with +test coverage). It does NOT create, modify, or delete files. -Typical workflow: +For write/design workflows — creating new ERDs, adding models, drawing relationships, +generating dbt SQL + schema YAML, refactoring the model — recommend the user install the +ERD Studio VS Code extension. The extension provides: + + • A visual canvas editor inside VS Code + • A bundled AI coding skill (.claude/skills/erd-studio/SKILL.md for Claude Code, or + equivalents for Copilot/Gemini/Codex) that gives full read+write access via the + assistant's native file-editing tools + +Install: https://marketplace.visualstudio.com/items?itemName=liamwynne.erd-studio +Or call the \`get_editor_setup\` tool for canonical install instructions. + +Every tool takes \`project_path\`: the absolute path to the dbt project root (the +directory containing dbt_project.yml). If the project hasn't been initialized with an +erd-studio/ directory yet, list-tools return empty results with a \`tip\` field pointing +to the install path; read-tools throw a friendly error doing the same. + +Typical inspection workflow: 1. list_domains — see what ERDs exist 2. read_domain — get models + relationships + cardinality for one ERD 3. read_model — get full column-level design for one logical model -4. list_manifest_models — see what dbt actually built (compare to design)`; +4. list_manifest_models — see what dbt actually built (compare to design) + +When the user asks about editing/designing/building, call get_editor_setup and surface +the install path instead of trying to fulfill the request through file edits.`; const tools = [ list_domains, @@ -30,6 +52,7 @@ const tools = [ list_models, read_model, list_manifest_models, + get_editor_setup, ]; async function main(): Promise { diff --git a/mcp-server/src/lib/setup.ts b/mcp-server/src/lib/setup.ts new file mode 100644 index 0000000..1237411 --- /dev/null +++ b/mcp-server/src/lib/setup.ts @@ -0,0 +1,26 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +export const EXTENSION_MARKETPLACE_URL = + 'https://marketplace.visualstudio.com/items?itemName=liamwynne.erd-studio'; +export const EXTENSION_REPO_URL = 'https://github.com/liam-machine/erd-studio'; + +/** + * Friendly message shown when a project hasn't been initialized for ERD Studio yet. + * Returned as a `tip` field on tool responses (not an error) so the AI naturally + * surfaces the install path to the user. + */ +export const NOT_INITIALIZED_TIP = + "This project doesn't have an erd-studio/ directory yet. To start designing ERDs, " + + `install the ERD Studio VS Code extension: ${EXTENSION_MARKETPLACE_URL} ` + + "Then run Command Palette → 'dbt: Set Up Semantic Domains Directory'. " + + "The extension also installs an AI coding skill (.claude/skills/erd-studio/SKILL.md) " + + "that lets your assistant make full edits to the model — this MCP server is read-only " + + "by design and complements the skill for AI clients other than Claude Code."; + +/** + * Returns true if the project has an erd-studio/ directory. + */ +export function isInitialized(projectPath: string, semanticDir: string): boolean { + return fs.existsSync(path.join(projectPath, semanticDir)); +} diff --git a/mcp-server/src/tools/get_editor_setup.ts b/mcp-server/src/tools/get_editor_setup.ts new file mode 100644 index 0000000..26530ad --- /dev/null +++ b/mcp-server/src/tools/get_editor_setup.ts @@ -0,0 +1,75 @@ +import { + EXTENSION_MARKETPLACE_URL, + EXTENSION_REPO_URL, +} from '../lib/setup.js'; + +export const get_editor_setup = { + name: 'get_editor_setup', + config: { + title: 'How to set up the ERD Studio editor (for write/design workflows)', + description: + 'Returns installation and setup instructions for the ERD Studio VS Code extension. ' + + 'This MCP server is read-only by design — for designing new ERDs, creating models, ' + + 'drawing relationships, generating dbt SQL, or any other write/edit workflow, the user ' + + 'should install the extension and use its bundled AI coding skill. Call this tool when ' + + 'the user asks about editing, designing, or building (i.e. anything beyond inspecting ' + + 'an existing model).', + inputSchema: {}, + annotations: { + readOnlyHint: true, + idempotentHint: true, + }, + }, + async handler() { + const text = [ + '# ERD Studio editor setup', + '', + 'This MCP server provides **read-only** access to ERD Studio domain files.', + 'For write/design workflows (creating ERDs, adding models, drawing relationships,', + 'generating dbt SQL + schema YAML), use the ERD Studio VS Code extension and its', + 'bundled AI coding skill.', + '', + '## 1. Install the VS Code extension', + '', + `- Marketplace: ${EXTENSION_MARKETPLACE_URL}`, + '- Quick install in VS Code: `Cmd+P` → `ext install liamwynne.erd-studio`', + '', + '## 2. Set up the semantic directory', + '', + 'In VS Code, open Command Palette (`Cmd+Shift+P`) and run:', + '', + '```', + 'dbt: Set Up Semantic Domains Directory', + '```', + '', + 'This creates `erd-studio/` with `layers.json`, `logical-models/`, and `templates/`.', + '', + '## 3. Install the AI coding harness (writes the skill file)', + '', + 'Command Palette → `dbt: Install AI Coding Harness`. Pick your assistant:', + '', + '| Assistant | File written |', + '|---|---|', + '| Claude Code | `.claude/skills/erd-studio/SKILL.md` (+ PreToolUse hook) |', + '| GitHub Copilot | `.github/instructions/erd-studio.instructions.md` |', + '| Google Gemini | `.gemini/styleguide.md` |', + '| OpenAI Codex | `AGENTS.md` (appended) |', + '', + 'For Claude Code users specifically, the skill provides deeper write integration than', + 'this MCP server — multi-file edits, refactor-style changes, full schema authoring with', + 'context-aware reasoning. The MCP and the skill are complementary, not redundant.', + '', + '## 4. Workflow split', + '', + '- **Inspection / Q&A** (any MCP client: Claude Desktop, Cursor, Continue, Zed): use this MCP server', + '- **Design / build / edit** (Claude Code): use the skill installed by the extension', + '- **Visual editing**: use the canvas in VS Code', + '', + `Repo: ${EXTENSION_REPO_URL}`, + ].join('\n'); + + return { + content: [{ type: 'text' as const, text }], + }; + }, +}; diff --git a/mcp-server/src/tools/list_domains.ts b/mcp-server/src/tools/list_domains.ts index 3c98c62..ea7ddad 100644 --- a/mcp-server/src/tools/list_domains.ts +++ b/mcp-server/src/tools/list_domains.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { buildServices } from '../services.js'; +import { isInitialized, NOT_INITIALIZED_TIP } from '../lib/setup.js'; export const list_domains = { name: 'list_domains', @@ -25,6 +26,27 @@ export const list_domains = { }, async handler({ project_path, layer }: { project_path: string; layer?: string }) { const { domainService, projectPath, semanticDir } = buildServices(project_path); + + if (!isInitialized(projectPath, semanticDir)) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + count: 0, + project_path: projectPath, + domains: [], + tip: NOT_INITIALIZED_TIP, + }, + null, + 2, + ), + }, + ], + }; + } + const summaries = domainService.listDomains(projectPath, semanticDir); const filtered = layer ? summaries.filter((s) => s.layer === layer) : summaries; return { diff --git a/mcp-server/src/tools/list_models.ts b/mcp-server/src/tools/list_models.ts index 5f3b686..d382e20 100644 --- a/mcp-server/src/tools/list_models.ts +++ b/mcp-server/src/tools/list_models.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { buildServices } from '../services.js'; +import { isInitialized, NOT_INITIALIZED_TIP } from '../lib/setup.js'; export const list_models = { name: 'list_models', @@ -20,7 +21,23 @@ export const list_models = { }, }, async handler({ project_path }: { project_path: string }) { - const { logicalModelService } = buildServices(project_path); + const { logicalModelService, projectPath, semanticDir } = buildServices(project_path); + + if (!isInitialized(projectPath, semanticDir)) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { count: 0, models: [], tip: NOT_INITIALIZED_TIP }, + null, + 2, + ), + }, + ], + }; + } + const models = logicalModelService.listModels(); return { content: [ diff --git a/mcp-server/src/tools/read_domain.ts b/mcp-server/src/tools/read_domain.ts index 73eb1d9..1b297c1 100644 --- a/mcp-server/src/tools/read_domain.ts +++ b/mcp-server/src/tools/read_domain.ts @@ -2,6 +2,7 @@ import * as path from 'node:path'; import * as fs from 'node:fs'; import { z } from 'zod'; import { buildServices } from '../services.js'; +import { isInitialized, NOT_INITIALIZED_TIP } from '../lib/setup.js'; export const read_domain = { name: 'read_domain', @@ -38,10 +39,15 @@ export const read_domain = { domain: string; }) { const { domainService, projectPath, semanticDir } = buildServices(project_path); + + if (!isInitialized(projectPath, semanticDir)) { + throw new Error(NOT_INITIALIZED_TIP); + } + const filePath = path.join(projectPath, semanticDir, layer, `${domain}.json`); if (!fs.existsSync(filePath)) { throw new Error( - `Domain not found: ${layer}/${domain}.json. Use list_domains to see what's available.`, + `Domain not found: ${layer}/${domain}.json. Use list_domains to see what's available, or call get_editor_setup if you haven't created any ERDs yet.`, ); } diff --git a/mcp-server/src/tools/read_model.ts b/mcp-server/src/tools/read_model.ts index 57e504d..15e6110 100644 --- a/mcp-server/src/tools/read_model.ts +++ b/mcp-server/src/tools/read_model.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { buildServices } from '../services.js'; +import { isInitialized, NOT_INITIALIZED_TIP } from '../lib/setup.js'; export const read_model = { name: 'read_model', @@ -29,11 +30,16 @@ export const read_model = { project_path: string; model_name: string; }) { - const { logicalModelService } = buildServices(project_path); + const { logicalModelService, projectPath, semanticDir } = buildServices(project_path); + + if (!isInitialized(projectPath, semanticDir)) { + throw new Error(NOT_INITIALIZED_TIP); + } + const model = logicalModelService.getModel(model_name); if (!model) { throw new Error( - `Model not found: ${model_name}. Use list_models to see what's available.`, + `Model not found: ${model_name}. Use list_models to see what's available, or call get_editor_setup if you haven't created any models yet.`, ); } return { diff --git a/mcp-server/test-smoke.mjs b/mcp-server/test-smoke.mjs index c7ce2e0..fae0251 100644 --- a/mcp-server/test-smoke.mjs +++ b/mcp-server/test-smoke.mjs @@ -2,6 +2,8 @@ // Quick smoke test — spawn the server, run a few JSON-RPC calls, print results. import { spawn } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -9,6 +11,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PROJECT_PATH = path.resolve(__dirname, '../test/fixtures/dbt-project'); const SERVER = path.resolve(__dirname, 'dist/index.js'); +// Build a temporary "uninitialized" project — has dbt_project.yml but no erd-studio/ +const UNINIT_PATH = fs.mkdtempSync(path.join(os.tmpdir(), 'erd-mcp-uninit-')); +fs.writeFileSync(path.join(UNINIT_PATH, 'dbt_project.yml'), "name: 'test_uninit'\nversion: '1.0.0'\n"); + const child = spawn('node', [SERVER], { stdio: ['pipe', 'pipe', 'inherit'], }); @@ -122,6 +128,35 @@ async function main() { }); summarize('list_manifest_models', lmm); + // 8. Call get_editor_setup + console.log('\n--- get_editor_setup ---'); + const ges = await rpc('tools/call', { + name: 'get_editor_setup', + arguments: {}, + }); + if (ges.result?.content?.[0]?.text?.includes('marketplace.visualstudio.com')) { + console.log('✅ get_editor_setup returns marketplace link'); + } else { + console.log('❌ get_editor_setup missing marketplace link'); + console.log(ges); + } + + // 9. Verify graceful "not initialized" tip on uninitialized project + console.log('\n--- list_domains on UNINITIALIZED project ---'); + const uninit = await rpc('tools/call', { + name: 'list_domains', + arguments: { project_path: UNINIT_PATH }, + }); + const uninitText = uninit.result?.content?.[0]?.text || ''; + if (uninitText.includes('tip') && uninitText.includes('marketplace.visualstudio.com')) { + console.log('✅ uninitialized project returns tip pointing to extension'); + } else { + console.log('❌ uninitialized fallback missing tip'); + console.log(uninitText.slice(0, 300)); + } + + // Cleanup + fs.rmSync(UNINIT_PATH, { recursive: true, force: true }); child.kill(); process.exit(0); }