From 606d9ea6bed443dfd5638667ab8f35fc8e6ef317 Mon Sep 17 00:00:00 2001 From: Rachelle Date: Mon, 23 Mar 2026 16:57:23 +1100 Subject: [PATCH 1/4] 0.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d91ccf7..a14d1b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multicorn-shield", - "version": "0.2.1", + "version": "0.2.2", "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.", "license": "MIT", "type": "module", From d39645f6d46edb0658f9bdb6560360e62dc075c9 Mon Sep 17 00:00:00 2001 From: Rachelle Date: Mon, 23 Mar 2026 16:58:48 +1100 Subject: [PATCH 2/4] 0.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a14d1b2..d91ccf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multicorn-shield", - "version": "0.2.2", + "version": "0.2.1", "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.", "license": "MIT", "type": "module", From 201d618d64ea581af61804833e906f737813a38f Mon Sep 17 00:00:00 2001 From: Rachelle Date: Mon, 23 Mar 2026 21:07:25 +1100 Subject: [PATCH 3/4] claude --- .github/workflows/ci.yml | 3 + .gitignore | 3 + CHANGELOG.md | 7 + README.md | 24 + bin/multicorn-shield.ts | 38 + bin/shield-extension.ts | 14 + docs/desktop-extension-research.md | 37 + icon.png | Bin 0 -> 70 bytes manifest.json | 50 + package.json | 18 +- pnpm-lock.yaml | 938 ++++++++++++++++++ src/extension/__tests__/child-manager.test.ts | 37 + src/extension/__tests__/config-reader.test.ts | 108 ++ .../__tests__/server.integration.test.ts | 93 ++ src/extension/__tests__/tool-router.test.ts | 37 + src/extension/child-manager.ts | 126 +++ src/extension/config-reader.ts | 176 ++++ src/extension/json-rpc-child.ts | 135 +++ src/extension/restore.ts | 52 + src/extension/runtime.ts | 267 +++++ src/extension/server.ts | 185 ++++ src/extension/tool-router.ts | 88 ++ src/mcp-tool-mapper.test.ts | 64 ++ src/mcp-tool-mapper.ts | 189 ++++ src/package-meta.ts | 9 + tsup.config.ts | 28 + 26 files changed, 2721 insertions(+), 5 deletions(-) create mode 100644 bin/multicorn-shield.ts create mode 100644 bin/shield-extension.ts create mode 100644 docs/desktop-extension-research.md create mode 100644 icon.png create mode 100644 manifest.json create mode 100644 src/extension/__tests__/child-manager.test.ts create mode 100644 src/extension/__tests__/config-reader.test.ts create mode 100644 src/extension/__tests__/server.integration.test.ts create mode 100644 src/extension/__tests__/tool-router.test.ts create mode 100644 src/extension/child-manager.ts create mode 100644 src/extension/config-reader.ts create mode 100644 src/extension/json-rpc-child.ts create mode 100644 src/extension/restore.ts create mode 100644 src/extension/runtime.ts create mode 100644 src/extension/server.ts create mode 100644 src/extension/tool-router.ts create mode 100644 src/mcp-tool-mapper.test.ts create mode 100644 src/mcp-tool-mapper.ts create mode 100644 src/package-meta.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e05bf12..485533c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -211,6 +211,9 @@ jobs: - name: Build run: pnpm build + - name: Validate MCPB extension manifest + run: pnpm run validate:extension + trigger-learn-rebuild: if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 71b52ef..e1187a4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ npm-debug.log* # Temporary files tmp/ .tmp/ + +# MCPB staging (mcpb pack) +extension-pack/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc3a2f..aa4a707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.2] - Unreleased + +### Added + +- Claude Desktop Extension (.mcpb) for one-click install. Packages Shield as a Desktop Extension that wraps existing MCP servers, enforces permissions via the Shield API, and logs all tool calls. Install by opening the .mcpb file in Claude Desktop. +- npx multicorn-shield restore command to recover original MCP server config after disabling the extension. + ## [0.2.1] - 2026-03-23 ### Security diff --git a/README.md b/README.md index 6419f41..324a836 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,30 @@ That's it. Every tool call now goes through Shield's permission layer, and activ See the [full MCP proxy guide](https://multicorn.ai/docs/mcp-proxy) for Claude Code, OpenClaw, and generic MCP client examples. +### Claude Desktop Extension (.mcpb) + +Install Shield without the terminal: download the `.mcpb` bundle (or use **Install** from the Shield product page), open it in Claude Desktop, and enter your API key when prompted. The extension reads your existing MCP servers from `claude_desktop_config.json`, runs them as child processes, merges their tools, and checks every `tools/call` with the Shield API. Activity still shows up in your [Multicorn dashboard](https://app.multicorn.ai). + +**Disable or uninstall recovery:** On each start the extension saves a copy of your `mcpServers` block to `~/.multicorn/extension-backup.json`. If you turn the extension off and need your original Claude Desktop MCP entries back, run: + +```bash +npx multicorn-shield restore +``` + +Then restart Claude Desktop. That overwrites `mcpServers` in your config with the last backup. + +**Duplicate tool names:** If two MCP servers expose the same tool name, the first server in your config file keeps the name. The duplicate is skipped and a warning is written to the extension logs (stderr). Rename tools on the server side if you need both. + +**Extension icon:** The repo ships a minimal placeholder `icon.png` for packaging. TODO: replace with a proper PNG export from the Multicorn learn site favicon at `multicorn-learn/public/learn/favicon.svg` (relative to the monorepo root). + +Build the bundle locally (requires a full `pnpm build` first): + +```bash +pnpm run pack:extension +``` + +This runs `mcpb validate` and writes `dist/multicorn-shield.mcpb`. + ### Option 2: OpenClaw Plugin (native integration) If you're running [OpenClaw](https://openclaw.ai), Shield integrates directly as a plugin. No proxy layer, no code changes. The plugin intercepts every tool call at the infrastructure level before it executes. diff --git a/bin/multicorn-shield.ts b/bin/multicorn-shield.ts new file mode 100644 index 0000000..9c6b486 --- /dev/null +++ b/bin/multicorn-shield.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env node +/** + * Multicorn Shield CLI (restore and future subcommands). + * + * @module bin/multicorn-shield + */ + +import { restoreClaudeDesktopMcpFromBackup } from "../src/extension/restore.js"; + +async function main(): Promise { + const arg = process.argv[2]; + if (arg === "restore") { + await restoreClaudeDesktopMcpFromBackup(); + process.stderr.write( + "Restored MCP server entries from ~/.multicorn/extension-backup.json into Claude Desktop config.\n" + + "Restart Claude Desktop to apply changes.\n", + ); + return; + } + + process.stderr.write( + [ + "multicorn-shield: Shield CLI", + "", + "Usage:", + " npx multicorn-shield restore", + " Restore MCP servers in claude_desktop_config.json from the Shield extension backup.", + "", + ].join("\n"), + ); + process.exit(arg === undefined || arg === "help" || arg === "--help" ? 0 : 1); +} + +void main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`Error: ${message}\n`); + process.exit(1); +}); diff --git a/bin/shield-extension.ts b/bin/shield-extension.ts new file mode 100644 index 0000000..7b109dd --- /dev/null +++ b/bin/shield-extension.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env node +/** + * Entry point for the Claude Desktop Extension MCP server (.mcpb bundle). + * + * @module bin/shield-extension + */ + +import { runShieldExtension } from "../src/extension/server.js"; + +void runShieldExtension().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`multicorn-shield-extension: ${message}\n`); + process.exit(1); +}); diff --git a/docs/desktop-extension-research.md b/docs/desktop-extension-research.md new file mode 100644 index 0000000..30c1c6d --- /dev/null +++ b/docs/desktop-extension-research.md @@ -0,0 +1,37 @@ +# Desktop Extension (.mcpb) research spike + +Answers the four questions for SR-04 before implementation. Sources: Anthropic Desktop Extensions article, open MCPB manifest spec (`anthropics/dxt` MANIFEST.md), and the multicorn-shield codebase. + +## (1) Can a .mcpb extension act as a proxy or wrapper for other MCP servers? + +Not as transport-layer middleware. A Desktop Extension is one MCP server process that Claude Desktop launches over stdio. The format does not define a way to sit in front of other MCP servers at the host level. + +The extension can still behave like a proxy by: + +- Reading the user's Claude Desktop MCP configuration from disk (paths already used in this repo for wizard flows). +- Spawning those MCP servers as child processes with stdio pipes. +- Implementing `tools/list` by merging child responses and `tools/call` by routing to the correct child after permission checks. + +So the bundle is a standalone MCP server that multiplexes child servers, not a plugin that rewrites Claude Desktop's transport to existing servers. + +## (2) How does the extension discover which other MCP servers the user has installed? + +The MCPB manifest and Claude Desktop do not expose an API for "list other extensions" or "read merged MCP config" to the running server. + +Practical discovery is reading `claude_desktop_config.json` (or equivalent) from the known OS paths at runtime. The Node-based server can use `fs` like any local process. + +Optional future path: users register targets in the Multicorn dashboard (hosted proxy config). That is extra setup and not required for the local config file approach. + +## (3) Can the manifest collect the Shield API key via `user_config`? + +Yes. Declare a `user_config` field with `type: "string"`, `sensitive: true`, and `required: true`. Claude Desktop prompts on first enable, stores the value in the OS secret store, and substitutes `${user_config.}` into `mcp_config` env or args. Sensitive values should not be written to project JSON on disk by the extension itself. + +## (4) What happens when the extension is disabled? + +Claude Desktop stops starting that MCP server. Tools exposed only through the extension disappear from the client. + +Other MCP entries that still exist in `claude_desktop_config.json` are unchanged on disk unless the user edited them during onboarding. To make disable and uninstall safe, Shield backs up the `mcpServers` object before relying on wrapped behaviour and ships a `restore` CLI that writes the backup back into `claude_desktop_config.json`. See the README Desktop Extension section. + +## Tooling note + +Run `npx @anthropic-ai/mcpb init` to generate a manifest baseline. The CLI currently emits `manifest_version` **0.2** (verify on upgrade). Use `mcpb validate` and `mcpb pack` for the bundle; do not hand-roll the zip format. diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f37764b1f7606623616dcdc169cc858273ea2d94 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZYBRYe;|OLfu)tPp=D){ QB2a?C)78&qol`;+0Lr!y6951J literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..bb97a41 --- /dev/null +++ b/manifest.json @@ -0,0 +1,50 @@ +{ + "manifest_version": "0.2", + "name": "multicorn-shield", + "display_name": "Multicorn Shield", + "version": "0.2.1", + "description": "Permission controls and audit logging for your MCP servers. Wraps servers from Claude Desktop config and checks every tool call with Shield.", + "author": { + "name": "Multicorn AI", + "url": "https://multicorn.ai" + }, + "homepage": "https://multicorn.ai", + "repository": { + "type": "git", + "url": "https://github.com/Multicorn-AI/multicorn-shield.git" + }, + "documentation": "https://multicorn.ai/docs/mcp-proxy", + "icon": "icon.png", + "license": "MIT", + "privacy_policies": ["https://multicorn.ai/privacy"], + "tools_generated": true, + "keywords": ["permissions", "security", "audit", "mcp", "shield"], + "server": { + "type": "node", + "entry_point": "server/index.js", + "mcp_config": { + "command": "node", + "args": ["${__dirname}/server/index.js"], + "env": { + "MULTICORN_API_KEY": "${user_config.api_key}", + "MULTICORN_BASE_URL": "https://api.multicorn.ai", + "MULTICORN_SHIELD_EXTENSION": "1" + } + } + }, + "user_config": { + "api_key": { + "type": "string", + "title": "Shield API key", + "description": "Your Multicorn Shield API key. Create one at https://app.multicorn.ai/settings/api-keys", + "sensitive": true, + "required": true + } + }, + "compatibility": { + "platforms": ["darwin", "win32"], + "runtimes": { + "node": ">=20.0.0" + } + } +} diff --git a/package.json b/package.json index d91ccf7..0661e94 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ } }, "bin": { - "multicorn-proxy": "./dist/multicorn-proxy.js" + "multicorn-proxy": "./dist/multicorn-proxy.js", + "multicorn-shield": "./dist/multicorn-shield.js" }, "files": [ "dist", @@ -41,7 +42,11 @@ "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit", "docs": "typedoc", - "clean": "rm -rf dist coverage docs/api", + "clean": "rm -rf dist coverage docs/api extension-pack", + "stage-extension-pack": "rm -rf extension-pack && mkdir -p extension-pack/server && cp manifest.json extension-pack/ && cp icon.png extension-pack/ && cp dist/shield-extension.js extension-pack/server/index.js", + "validate:extension": "pnpm run stage-extension-pack && mcpb validate extension-pack/manifest.json", + "build:extension": "tsup", + "pack:extension": "pnpm run build && pnpm run stage-extension-pack && mcpb validate extension-pack/manifest.json && mcpb pack extension-pack dist/multicorn-shield.mcpb", "size": "size-limit", "prepublishOnly": "pnpm run clean && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build", "prepare": "husky", @@ -59,12 +64,16 @@ ] }, "dependencies": { - "lit": "^3.2.0" + "@modelcontextprotocol/sdk": "^1.27.1", + "lit": "^3.2.0", + "zod": "^4.3.6" }, "devDependencies": { - "@types/node": "^22.0.0", + "@anthropic-ai/mcpb": "^2.1.2", "@eslint/js": "^9.19.0", "@open-wc/testing-helpers": "^3.0.1", + "@size-limit/file": "^11.1.6", + "@types/node": "^22.0.0", "@vitest/coverage-istanbul": "^3.0.5", "eslint": "^9.19.0", "eslint-config-prettier": "^10.0.1", @@ -75,7 +84,6 @@ "jsdom": "^25.0.1", "lint-staged": "^16.2.7", "prettier": "^3.4.2", - "@size-limit/file": "^11.1.6", "size-limit": "^11.1.6", "tsup": "^8.3.6", "typedoc": "^0.28.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0686928..bb5df43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,19 @@ importers: .: dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.27.1(zod@4.3.6) lit: specifier: ^3.2.0 version: 3.3.2 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: + '@anthropic-ai/mcpb': + specifier: ^2.1.2 + version: 2.1.2 '@eslint/js': specifier: ^9.19.0 version: 9.39.2 @@ -80,6 +89,10 @@ importers: packages: + '@anthropic-ai/mcpb@2.1.2': + resolution: {integrity: sha512-goRbBC8ySo7SWb7tRzr+tL6FxDc4JPTRCdgfD2omba7freofvjq5rom1lBnYHZHo6Mizs1jAHJeN53aZbDoy8A==} + hasBin: true + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -375,6 +388,12 @@ packages: '@gerrit0/mini-shiki@3.22.0': resolution: {integrity: sha512-jMpciqEVUBKE1QwU64S4saNMzpsSza6diNCk4MWAeCxO2+LFi2FIFmL2S0VDLzEJCxuvCbU783xi8Hp/gkM5CQ==} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -391,6 +410,62 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/checkbox@3.0.1': + resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@4.0.1': + resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==} + engines: {node: '>=18'} + + '@inquirer/core@9.2.1': + resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} + engines: {node: '>=18'} + + '@inquirer/editor@3.0.1': + resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==} + engines: {node: '>=18'} + + '@inquirer/expand@3.0.1': + resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@3.0.1': + resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==} + engines: {node: '>=18'} + + '@inquirer/number@2.0.1': + resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==} + engines: {node: '>=18'} + + '@inquirer/password@3.0.1': + resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==} + engines: {node: '>=18'} + + '@inquirer/prompts@6.0.1': + resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==} + engines: {node: '>=18'} + + '@inquirer/rawlist@3.0.1': + resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==} + engines: {node: '>=18'} + + '@inquirer/search@2.0.1': + resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==} + engines: {node: '>=18'} + + '@inquirer/select@3.0.1': + resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==} + engines: {node: '>=18'} + + '@inquirer/type@2.0.0': + resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} + engines: {node: '>=18'} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -421,6 +496,16 @@ packages: '@lit/reactive-element@2.1.2': resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@open-wc/dedupe-mixin@2.0.1': resolution: {integrity: sha512-+R4VxvceUxHAUJXJQipkkoV9fy10vNo+OnUnGKZnVmcwxMl460KLzytnUM4S35SI073R0yZQp9ra0MbPUwVcEA==} @@ -608,6 +693,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + '@types/node@22.19.11': resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} @@ -620,6 +708,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@typescript-eslint/eslint-plugin@8.56.0': resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -713,6 +804,10 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -727,9 +822,24 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -771,6 +881,10 @@ packages: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@5.0.2: resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} engines: {node: 20 || >=22} @@ -798,6 +912,10 @@ packages: resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==} engines: {node: '>= 0.8'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -806,6 +924,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -821,6 +943,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -845,6 +970,10 @@ packages: resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} engines: {node: '>=20'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -859,6 +988,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -874,12 +1007,32 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-js-compat@3.48.0: resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -915,6 +1068,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -922,6 +1079,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} @@ -934,6 +1094,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -974,6 +1138,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1043,13 +1210,39 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1059,6 +1252,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1068,6 +1264,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1076,6 +1275,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -1094,6 +1297,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flora-colossus@2.0.0: + resolution: {integrity: sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==} + engines: {node: '>= 12'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1102,6 +1309,18 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1110,6 +1329,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + galactus@1.0.0: + resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==} + engines: {node: '>= 12'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1147,6 +1370,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1163,6 +1389,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.12.8: + resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + engines: {node: '>=16.9.0'} + hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -1174,6 +1404,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1187,10 +1421,18 @@ packages: engines: {node: '>=18'} hasBin: true + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1215,6 +1457,17 @@ packages: resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-builtin-module@4.0.0: resolution: {integrity: sha512-rWP3AMAalQSesXO8gleROyL2iKU73SX5Er66losQn9rWOWL4Gef0a/xOEOVqjWGMuR2vHG3FJ8UUmT700O8oFg==} engines: {node: '>=18.20'} @@ -1242,6 +1495,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1272,6 +1528,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1311,6 +1570,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1319,6 +1584,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1402,6 +1670,14 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1410,10 +1686,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -1432,6 +1716,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1450,6 +1738,14 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-forge@1.3.3: + resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} + engines: {node: '>= 6.13.0'} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -1464,6 +1760,17 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -1472,6 +1779,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1494,6 +1805,10 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1506,6 +1821,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1533,6 +1851,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -1571,6 +1893,14 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -1579,6 +1909,18 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + read-package-up@11.0.0: resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} engines: {node: '>=18'} @@ -1599,6 +1941,10 @@ packages: resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} hasBin: true + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1619,6 +1965,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -1641,6 +1991,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1649,6 +2010,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1688,6 +2065,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -1782,10 +2163,18 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -1830,10 +2219,18 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typedoc@0.28.17: resolution: {integrity: sha512-ZkJ2G7mZrbxrKxinTQMjFqsCoYY6a5Luwv2GKbTnBCEgV2ihYm5CflA9JnJAwH0pZWavqfYxmDkFHPt4yx2oDQ==} engines: {node: '>= 18', pnpm: '>= 10'} @@ -1866,6 +2263,14 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1878,6 +2283,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1986,6 +2395,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1998,6 +2411,9 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -2029,8 +2445,37 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: + '@anthropic-ai/mcpb@2.1.2': + dependencies: + '@inquirer/prompts': 6.0.1 + commander: 13.1.0 + fflate: 0.8.2 + galactus: 1.0.0 + ignore: 7.0.5 + node-forge: 1.3.3 + pretty-bytes: 5.6.0 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -2291,6 +2736,10 @@ snapshots: '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 + '@hono/node-server@1.19.11(hono@4.12.8)': + dependencies: + hono: 4.12.8 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -2302,6 +2751,102 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/checkbox@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + + '@inquirer/confirm@4.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/core@9.2.1': + dependencies: + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + '@types/mute-stream': 0.0.4 + '@types/node': 22.19.11 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/editor@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + external-editor: 3.1.0 + + '@inquirer/expand@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/number@2.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/password@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + + '@inquirer/prompts@6.0.1': + dependencies: + '@inquirer/checkbox': 3.0.1 + '@inquirer/confirm': 4.0.1 + '@inquirer/editor': 3.0.1 + '@inquirer/expand': 3.0.1 + '@inquirer/input': 3.0.1 + '@inquirer/number': 2.0.1 + '@inquirer/password': 3.0.1 + '@inquirer/rawlist': 3.0.1 + '@inquirer/search': 2.0.1 + '@inquirer/select': 3.0.1 + + '@inquirer/rawlist@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/search@2.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/select@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + + '@inquirer/type@2.0.0': + dependencies: + mute-stream: 1.0.0 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2338,6 +2883,28 @@ snapshots: dependencies: '@lit-labs/ssr-dom-shim': 1.5.1 + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.8) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@open-wc/dedupe-mixin@2.0.1': {} '@open-wc/scoped-elements@3.0.6': @@ -2468,6 +3035,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/mute-stream@0.0.4': + dependencies: + '@types/node': 22.19.11 + '@types/node@22.19.11': dependencies: undici-types: 6.21.0 @@ -2478,6 +3049,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/wrap-ansi@3.0.0': {} + '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2627,6 +3200,11 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -2635,6 +3213,10 @@ snapshots: agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -2642,6 +3224,17 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -2668,6 +3261,20 @@ snapshots: baseline-browser-mapping@2.9.19: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@5.0.2: dependencies: balanced-match: 4.0.3 @@ -2693,6 +3300,8 @@ snapshots: bytes-iec@3.1.1: {} + bytes@3.1.2: {} + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -2700,6 +3309,11 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} caniuse-lite@1.0.30001770: {} @@ -2717,6 +3331,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chardet@0.7.0: {} + check-error@2.1.3: {} chokidar@4.0.3: @@ -2738,6 +3354,8 @@ snapshots: slice-ansi: 7.1.2 string-width: 8.1.1 + cli-width@4.1.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2750,6 +3368,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@13.1.0: {} + commander@14.0.3: {} commander@4.1.1: {} @@ -2758,12 +3378,25 @@ snapshots: consola@3.4.2: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + core-js-compat@3.48.0: dependencies: browserslist: 4.28.1 + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2792,6 +3425,8 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2800,6 +3435,8 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + electron-to-chromium@1.5.286: {} emoji-regex@10.6.0: {} @@ -2808,6 +3445,8 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + entities@4.5.0: {} entities@6.0.1: {} @@ -2862,6 +3501,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -2964,20 +3605,76 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expect-type@1.3.0: {} + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -2986,6 +3683,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up-simple@1.0.1: {} find-up@5.0.0: @@ -3006,6 +3714,13 @@ snapshots: flatted@3.4.2: {} + flora-colossus@2.0.0: + dependencies: + debug: 4.4.3 + fs-extra: 10.1.0 + transitivePeerDependencies: + - supports-color + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -3019,11 +3734,29 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + galactus@1.0.0: + dependencies: + debug: 4.4.3 + flora-colossus: 2.0.0 + fs-extra: 10.1.0 + transitivePeerDependencies: + - supports-color + gensync@1.0.0-beta.2: {} get-east-asian-width@1.4.0: {} @@ -3065,6 +3798,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -3077,6 +3812,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.12.8: {} + hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -3087,6 +3824,14 @@ snapshots: html-escaper@2.0.2: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -3103,10 +3848,18 @@ snapshots: husky@9.1.7: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3122,6 +3875,12 @@ snapshots: index-to-position@1.2.0: {} + inherits@2.0.4: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + is-builtin-module@4.0.0: dependencies: builtin-modules: 4.0.0 @@ -3142,6 +3901,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -3183,6 +3944,8 @@ snapshots: jiti@2.6.1: {} + jose@6.2.2: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -3229,10 +3992,20 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3338,6 +4111,10 @@ snapshots: mdurl@2.0.0: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -3345,10 +4122,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-function@5.0.1: {} minimatch@10.2.4: @@ -3366,6 +4149,8 @@ snapshots: ms@2.1.3: {} + mute-stream@1.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -3382,6 +4167,10 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + + node-forge@1.3.3: {} + node-releases@2.0.27: {} normalize-package-data@6.0.2: @@ -3394,6 +4183,16 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -3407,6 +4206,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + os-tmpdir@1.0.2: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -3431,6 +4232,8 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -3440,6 +4243,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -3454,6 +4259,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -3480,10 +4287,30 @@ snapshots: prettier@3.8.1: {} + pretty-bytes@5.6.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode.js@2.3.1: {} punycode@2.3.1: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + read-package-up@11.0.0: dependencies: find-up-simple: 1.0.1 @@ -3506,6 +4333,8 @@ snapshots: dependencies: jsesc: 3.0.2 + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -3548,6 +4377,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.7.1: {} rrweb-cssom@0.8.0: {} @@ -3562,12 +4401,67 @@ snapshots: semver@7.7.4: {} + send@1.2.1: + 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 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -3607,6 +4501,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} string-argv@0.3.2: {} @@ -3701,10 +4597,16 @@ snapshots: dependencies: tldts-core: 6.1.86 + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -3753,8 +4655,16 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.21.3: {} + type-fest@4.41.0: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typedoc@0.28.17(typescript@5.9.3): dependencies: '@gerrit0/mini-shiki': 3.22.0 @@ -3785,6 +4695,10 @@ snapshots: unicorn-magic@0.1.0: {} + universalify@2.0.1: {} + + unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -3800,6 +4714,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vary@1.1.2: {} + vite-node@3.2.4(@types/node@22.19.11)(jiti@2.6.1)(yaml@2.8.2): dependencies: cac: 6.7.14 @@ -3905,6 +4821,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -3923,6 +4845,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + ws@8.19.0: {} xml-name-validator@5.0.0: {} @@ -3934,3 +4858,17 @@ snapshots: yaml@2.8.2: {} yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@3.25.76: {} + + zod@4.3.6: {} diff --git a/src/extension/__tests__/child-manager.test.ts b/src/extension/__tests__/child-manager.test.ts new file mode 100644 index 0000000..1535207 --- /dev/null +++ b/src/extension/__tests__/child-manager.test.ts @@ -0,0 +1,37 @@ +/** + * @vitest-environment node + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { ChildManager } from "../child-manager.js"; +import { createLogger } from "../../proxy/logger.js"; +import { startMockMcpServer } from "../../proxy/__fixtures__/mockMcpServer.js"; + +describe("child-manager", () => { + let manager: ChildManager | undefined; + + afterEach(() => { + if (manager !== undefined) { + manager.stopAll(); + } + }); + + it("starts a mock MCP child and returns tools from tools/list", async () => { + const mockServer = startMockMcpServer(); + const command = mockServer.command; + const args = [...mockServer.args]; + await mockServer.stop(); + + manager = new ChildManager({ logger: createLogger("error") }); + await manager.startAll({ + mock: { command, args }, + }); + + const map = await manager.listToolsForAll(); + const tools = map.get("mock"); + expect(tools).toBeDefined(); + expect(tools?.map((t) => t.name).sort()).toEqual( + ["calendar_create_event", "gmail_send_email", "payments_charge"].sort(), + ); + }); +}); diff --git a/src/extension/__tests__/config-reader.test.ts b/src/extension/__tests__/config-reader.test.ts new file mode 100644 index 0000000..4e5bc1f --- /dev/null +++ b/src/extension/__tests__/config-reader.test.ts @@ -0,0 +1,108 @@ +/** + * @vitest-environment node + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + isShieldExtensionEntry, + readClaudeDesktopMcpConfig, + readExtensionBackup, + type McpServerEntry, +} from "../config-reader.js"; + +const readFileMock = vi.hoisted(() => vi.fn()); +const writeFileMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +const mkdirMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); + +vi.mock("node:fs/promises", () => { + const exports = { + readFile: readFileMock, + writeFile: writeFileMock, + mkdir: mkdirMock, + }; + return { default: exports, ...exports }; +}); + +vi.mock("../../proxy/config.js", () => ({ + getClaudeDesktopConfigPath: () => "/tmp/claude_desktop_config.test.json", +})); + +describe("config-reader", () => { + beforeEach(() => { + readFileMock.mockReset(); + writeFileMock.mockReset(); + mkdirMock.mockReset(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("isShieldExtensionEntry", () => { + it("returns true for server key multicorn-shield", () => { + const entry: McpServerEntry = { command: "node", args: ["server.js"] }; + expect(isShieldExtensionEntry("multicorn-shield", entry)).toBe(true); + }); + + it("returns true when args reference shield-extension", () => { + const entry: McpServerEntry = { + command: "node", + args: ["/app/extension-pack/server/shield-extension.js"], + }; + expect(isShieldExtensionEntry("filesystem", entry)).toBe(true); + }); + + it("returns true when MULTICORN_SHIELD_EXTENSION env is 1", () => { + const entry: McpServerEntry = { + command: "node", + args: ["server.js"], + env: { MULTICORN_SHIELD_EXTENSION: "1" }, + }; + expect(isShieldExtensionEntry("other", entry)).toBe(true); + }); + + it("returns false for a normal npx MCP server", () => { + const entry: McpServerEntry = { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], + }; + expect(isShieldExtensionEntry("filesystem", entry)).toBe(false); + }); + }); + + describe("readExtensionBackup", () => { + it("returns null when file is missing", async () => { + readFileMock.mockRejectedValue(new Error("ENOENT")); + const result = await readExtensionBackup(); + expect(result).toBeNull(); + }); + + it("parses a valid v1 backup", async () => { + readFileMock.mockResolvedValue( + JSON.stringify({ + version: 1, + createdAt: "2026-01-01T00:00:00.000Z", + claudeDesktopConfigPath: "/tmp/c.json", + mcpServers: { a: { command: "npx", args: ["x"] } }, + }), + ); + const result = await readExtensionBackup(); + expect(result?.version).toBe(1); + expect(result?.mcpServers["a"]).toEqual({ command: "npx", args: ["x"] }); + }); + }); + + it("readClaudeDesktopMcpConfig skips invalid server entries", async () => { + readFileMock.mockResolvedValue( + JSON.stringify({ + mcpServers: { + fs: { command: "npx", args: ["-y", "pkg"] }, + bad: { command: 1 }, + }, + }), + ); + const result = await readClaudeDesktopMcpConfig(); + expect(result?.mcpServers["fs"]).toEqual({ command: "npx", args: ["-y", "pkg"] }); + expect(result?.mcpServers["bad"]).toBeUndefined(); + }); +}); diff --git a/src/extension/__tests__/server.integration.test.ts b/src/extension/__tests__/server.integration.test.ts new file mode 100644 index 0000000..792a2db --- /dev/null +++ b/src/extension/__tests__/server.integration.test.ts @@ -0,0 +1,93 @@ +/** + * @vitest-environment node + */ + +import { describe, it, expect, afterEach, vi } from "vitest"; +import { ShieldExtensionRuntime } from "../runtime.js"; +import { createLogger } from "../../proxy/logger.js"; +import { deriveDashboardUrl } from "../../proxy/consent.js"; +import { + startMockMulticornService, + type MockMulticornService, +} from "../../proxy/__fixtures__/mockMulticornService.js"; + +const readFileMock = vi.hoisted(() => vi.fn()); +const writeFileMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +const mkdirMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); + +vi.mock("node:fs/promises", () => { + const exports = { + readFile: readFileMock, + writeFile: writeFileMock, + mkdir: mkdirMock, + }; + return { default: exports, ...exports }; +}); + +describe("Shield extension runtime (integration)", () => { + let mockService: MockMulticornService | undefined; + let runtime: ShieldExtensionRuntime | undefined; + + afterEach(async () => { + readFileMock.mockReset(); + writeFileMock.mockReset(); + mkdirMock.mockReset(); + const rt = runtime; + const svc = mockService; + runtime = undefined; + mockService = undefined; + if (rt !== undefined) { + await rt.stop(); + } + if (svc !== undefined) { + await svc.stop(); + } + }); + + it("allows a tool call when scopes permit execute on the service", async () => { + readFileMock.mockRejectedValue(new Error("ENOENT")); + + mockService = await startMockMulticornService({ + scopes: [{ service: "gmail", permissionLevel: "write" }], + }); + const baseUrl = mockService.baseUrl.replace("127.0.0.1", "localhost"); + + runtime = new ShieldExtensionRuntime({ + apiKey: "test-key", + agentName: "test-agent", + baseUrl, + dashboardUrl: deriveDashboardUrl(baseUrl), + logger: createLogger("error"), + }); + + await runtime.start(); + + const decision = await runtime.evaluateToolCall("gmail_send_email"); + + expect(decision.allow).toBe(true); + }); + + it("denies when Shield cannot reach the API (offline agent id)", async () => { + readFileMock.mockRejectedValue(new Error("ENOENT")); + + runtime = new ShieldExtensionRuntime({ + apiKey: "test-key", + agentName: "test-agent", + baseUrl: "http://127.0.0.1:1", + dashboardUrl: "https://app.multicorn.ai", + logger: createLogger("error"), + }); + + await runtime.start(); + + const decision = await runtime.evaluateToolCall("gmail_send_email"); + + expect(decision.allow).toBe(false); + if (!decision.allow) { + expect(decision.result.isError).toBe(true); + const text = decision.result.content[0]; + expect(text?.type).toBe("text"); + expect((text as { text: string }).text).toContain("unreachable"); + } + }); +}); diff --git a/src/extension/__tests__/tool-router.test.ts b/src/extension/__tests__/tool-router.test.ts new file mode 100644 index 0000000..239943c --- /dev/null +++ b/src/extension/__tests__/tool-router.test.ts @@ -0,0 +1,37 @@ +/** + * @vitest-environment node + */ + +import { describe, it, expect, vi } from "vitest"; +import { buildToolRouter, parseToolsListResult } from "../tool-router.js"; +import { createLogger } from "../../proxy/logger.js"; + +describe("tool-router", () => { + it("keeps the first server when two children expose the same tool name", () => { + const warn = vi.fn(); + const logger = createLogger("error"); + logger.warn = warn; + + const toolsByServer = new Map([ + ["first", [{ name: "dup", description: "a" }]], + ["second", [{ name: "dup", description: "b" }]], + ]); + + const { tools, routing } = buildToolRouter(toolsByServer, logger); + expect(tools).toHaveLength(1); + expect(tools[0]?.sourceServerName).toBe("first"); + expect(routing.get("dup")).toBe("first"); + expect(warn).toHaveBeenCalledTimes(1); + }); + + it("parseToolsListResult extracts tool definitions", () => { + const list = parseToolsListResult({ + tools: [ + { name: "t1", description: "d", inputSchema: { type: "object" } }, + { name: "", description: "skip" }, + ], + }); + expect(list).toHaveLength(1); + expect(list[0]?.name).toBe("t1"); + }); +}); diff --git a/src/extension/child-manager.ts b/src/extension/child-manager.ts new file mode 100644 index 0000000..c361bdc --- /dev/null +++ b/src/extension/child-manager.ts @@ -0,0 +1,126 @@ +/** + * Spawns MCP child processes from Claude Desktop config entries and exposes JSON-RPC. + * + * @module extension/child-manager + */ + +import { spawn, type ChildProcess } from "node:child_process"; +import { JsonRpcChildSession } from "./json-rpc-child.js"; +import type { McpServerEntry } from "./config-reader.js"; +import type { McpToolDefinition } from "./tool-router.js"; +import { parseToolsListResult } from "./tool-router.js"; +import type { ProxyLogger } from "../proxy/logger.js"; +import { PACKAGE_VERSION } from "../package-meta.js"; + +export interface ManagedChild { + readonly serverName: string; + readonly process: ChildProcess; + readonly session: JsonRpcChildSession; +} + +const MCP_PROTOCOL_VERSION = "2024-11-05"; + +export interface ChildManagerOptions { + readonly logger: ProxyLogger; +} + +export class ChildManager { + private readonly logger: ProxyLogger; + private readonly children: ManagedChild[] = []; + + constructor(options: ChildManagerOptions) { + this.logger = options.logger; + } + + /** + * Starts all MCP servers from config (caller must exclude Shield self). + */ + async startAll( + entries: Readonly>, + ): Promise { + const names = Object.keys(entries); + for (const serverName of names) { + const entry = entries[serverName]; + if (entry === undefined) continue; + + try { + const managed = await this.startOne(serverName, entry); + this.children.push(managed); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error("Failed to start MCP child server.", { serverName, error: message }); + } + } + return this.children; + } + + private async startOne(serverName: string, entry: McpServerEntry): Promise { + const env = { ...process.env, ...entry.env } as NodeJS.ProcessEnv; + + const child = spawn(entry.command, [...entry.args], { + stdio: ["pipe", "pipe", "pipe"], + env, + }); + + child.on("error", (err) => { + this.logger.error("MCP child process error.", { serverName, error: err.message }); + }); + + child.on("exit", (code, signal) => { + this.logger.info("MCP child process exited.", { + serverName, + code: code ?? undefined, + signal: signal ?? undefined, + }); + }); + + const session = new JsonRpcChildSession({ + child, + label: serverName, + logStderr: (line) => { + this.logger.debug("MCP child stderr.", { serverName, line }); + }, + }); + + await session.request("initialize", { + protocolVersion: MCP_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: "multicorn-shield-extension", version: PACKAGE_VERSION }, + }); + + session.notify("notifications/initialized", {}); + + return { serverName, process: child, session }; + } + + async listToolsForAll(): Promise> { + const map = new Map(); + + for (const c of this.children) { + try { + const result = await c.session.request("tools/list", {}); + map.set(c.serverName, parseToolsListResult(result)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.warn("tools/list failed for MCP child.", { + serverName: c.serverName, + error: message, + }); + map.set(c.serverName, []); + } + } + + return map; + } + + getChildByServerName(serverName: string): ManagedChild | undefined { + return this.children.find((c) => c.serverName === serverName); + } + + stopAll(): void { + for (const c of this.children) { + c.process.kill("SIGTERM"); + } + this.children.length = 0; + } +} diff --git a/src/extension/config-reader.ts b/src/extension/config-reader.ts new file mode 100644 index 0000000..008c3e7 --- /dev/null +++ b/src/extension/config-reader.ts @@ -0,0 +1,176 @@ +/** + * Claude Desktop MCP config paths, parsing, and Shield extension backup. + * + * @module extension/config-reader + */ + +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { homedir } from "node:os"; +import { getClaudeDesktopConfigPath } from "../proxy/config.js"; + +export const EXTENSION_BACKUP_FILENAME = "extension-backup.json"; + +export function getExtensionBackupPath(): string { + return join(homedir(), ".multicorn", EXTENSION_BACKUP_FILENAME); +} + +export interface McpServerEntry { + readonly command: string; + readonly args: readonly string[]; + readonly env?: Readonly>; +} + +export interface ClaudeDesktopMcpConfig { + readonly configPath: string; + readonly mcpServers: Readonly>; +} + +export interface ExtensionBackupV1 { + readonly version: 1; + readonly createdAt: string; + readonly claudeDesktopConfigPath: string; + readonly mcpServers: Record; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isMcpServerEntry(value: unknown): value is McpServerEntry { + if (!isRecord(value)) return false; + const command = value["command"]; + const args = value["args"]; + if (typeof command !== "string" || command.length === 0) return false; + if (!Array.isArray(args)) return false; + for (const a of args) { + if (typeof a !== "string") return false; + } + const env = value["env"]; + if (env !== undefined) { + if (!isRecord(env)) return false; + for (const v of Object.values(env)) { + if (typeof v !== "string") return false; + } + } + return true; +} + +/** + * Returns true when this config entry is the Shield Desktop Extension server + * (must not be spawned as a child). + */ +export function isShieldExtensionEntry(serverKey: string, entry: McpServerEntry): boolean { + const key = serverKey.trim().toLowerCase(); + if (key === "multicorn-shield") { + return true; + } + + const argBlob = entry.args.join(" ").toLowerCase(); + if (argBlob.includes("shield-extension")) { + return true; + } + + if (entry.command.toLowerCase().includes("shield-extension")) { + return true; + } + + if (entry.env?.["MULTICORN_SHIELD_EXTENSION"] === "1") { + return true; + } + + return false; +} + +/** + * Reads Claude Desktop config and returns parsed MCP server entries. + */ +export async function readClaudeDesktopMcpConfig(): Promise { + const configPath = getClaudeDesktopConfigPath(); + let raw: string; + try { + raw = await readFile(configPath, "utf8"); + } catch { + return null; + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return null; + } + + if (!isRecord(parsed)) return null; + const mcpServersRaw = parsed["mcpServers"]; + if (!isRecord(mcpServersRaw)) { + return { configPath, mcpServers: {} }; + } + + const mcpServers: Record = {}; + for (const [name, entry] of Object.entries(mcpServersRaw)) { + if (isMcpServerEntry(entry)) { + mcpServers[name] = { + command: entry.command, + args: [...entry.args], + ...(entry.env !== undefined ? { env: { ...entry.env } } : {}), + }; + } + } + + return { configPath, mcpServers }; +} + +/** + * Writes a backup of the current `mcpServers` block for uninstall or disable recovery. + */ +export async function writeExtensionBackup( + claudeDesktopConfigPath: string, + mcpServers: Readonly>, +): Promise { + const dir = join(homedir(), ".multicorn"); + await mkdir(dir, { recursive: true, mode: 0o700 }); + + const serializable: Record = {}; + for (const [k, v] of Object.entries(mcpServers)) { + serializable[k] = { + command: v.command, + args: [...v.args], + ...(v.env !== undefined ? { env: { ...v.env } } : {}), + }; + } + + const payload: ExtensionBackupV1 = { + version: 1, + createdAt: new Date().toISOString(), + claudeDesktopConfigPath, + mcpServers: serializable, + }; + + await writeFile(getExtensionBackupPath(), JSON.stringify(payload, null, 2) + "\n", { + encoding: "utf8", + mode: 0o600, + }); +} + +export async function readExtensionBackup(): Promise { + try { + const raw = await readFile(getExtensionBackupPath(), "utf8"); + const parsed: unknown = JSON.parse(raw); + if (!isRecord(parsed)) return null; + if (parsed["version"] !== 1) return null; + if (typeof parsed["createdAt"] !== "string") return null; + if (typeof parsed["claudeDesktopConfigPath"] !== "string") return null; + const mcpServers = parsed["mcpServers"]; + if (!isRecord(mcpServers)) return null; + + return { + version: 1, + createdAt: parsed["createdAt"], + claudeDesktopConfigPath: parsed["claudeDesktopConfigPath"], + mcpServers, + }; + } catch { + return null; + } +} diff --git a/src/extension/json-rpc-child.ts b/src/extension/json-rpc-child.ts new file mode 100644 index 0000000..fa85267 --- /dev/null +++ b/src/extension/json-rpc-child.ts @@ -0,0 +1,135 @@ +/** + * JSON-RPC 2.0 over newline-delimited stdio for a spawned MCP child process. + * + * @module extension/json-rpc-child + */ + +import { createInterface } from "node:readline"; +import type { ChildProcess } from "node:child_process"; + +export interface JsonRpcChildSessionOptions { + readonly child: ChildProcess; + readonly label: string; + readonly requestTimeoutMs?: number; + readonly logStderr?: (line: string) => void; +} + +export class JsonRpcChildSession { + private readonly child: ChildProcess; + private readonly label: string; + private readonly requestTimeoutMs: number; + private nextId = 1; + private readonly pending = new Map< + number, + { resolve: (value: unknown) => void; reject: (reason: unknown) => void; timer: NodeJS.Timeout } + >(); + + constructor(options: JsonRpcChildSessionOptions) { + this.child = options.child; + this.label = options.label; + this.requestTimeoutMs = options.requestTimeoutMs ?? 60_000; + const logStderr = options.logStderr; + + const stdout = this.child.stdout; + if (stdout === null) { + throw new Error(`Child "${this.label}" has no stdout pipe.`); + } + + const rl = createInterface({ input: stdout, terminal: false }); + rl.on("line", (line) => { + this.handleLine(line); + }); + + const stderr = this.child.stderr; + if (stderr !== null) { + stderr.on("data", (chunk: Buffer) => { + const text = chunk.toString().trim(); + if (text.length > 0 && logStderr !== undefined) { + for (const part of text.split("\n")) { + logStderr(part); + } + } + }); + } + } + + async request(method: string, params?: unknown): Promise { + const stdin = this.child.stdin; + if (stdin === null) { + throw new Error(`Child "${this.label}" has no stdin pipe.`); + } + + const id = this.nextId; + this.nextId += 1; + + const payload: Record = { + jsonrpc: "2.0", + id, + method, + }; + if (params !== undefined) { + payload["params"] = params; + } + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new Error(`Request "${method}" timed out for child "${this.label}".`)); + }, this.requestTimeoutMs); + + this.pending.set(id, { resolve, reject, timer }); + stdin.write(JSON.stringify(payload) + "\n"); + }); + } + + notify(method: string, params?: unknown): void { + const stdin = this.child.stdin; + if (stdin === null) { + return; + } + const payload: Record = { jsonrpc: "2.0", method }; + if (params !== undefined) { + payload["params"] = params; + } + stdin.write(JSON.stringify(payload) + "\n"); + } + + private handleLine(line: string): void { + const trimmed = line.trim(); + if (trimmed.length === 0) return; + + let parsed: unknown; + try { + parsed = JSON.parse(trimmed); + } catch { + return; + } + + if (typeof parsed !== "object" || parsed === null) return; + const obj = parsed as Record; + + if (!("id" in obj) || obj["id"] === null || obj["id"] === undefined) { + return; + } + + const id = obj["id"]; + const numericId = + typeof id === "number" ? id : typeof id === "string" ? Number(id) : Number.NaN; + if (Number.isNaN(numericId)) return; + + const entry = this.pending.get(numericId); + if (entry === undefined) return; + + clearTimeout(entry.timer); + this.pending.delete(numericId); + + if (obj["error"] !== undefined) { + const err = obj["error"] as Record; + const message = typeof err["message"] === "string" ? err["message"] : "Child JSON-RPC error"; + entry.reject(new Error(`${this.label}: ${message}`)); + return; + } + + entry.resolve(obj["result"]); + } +} diff --git a/src/extension/restore.ts b/src/extension/restore.ts new file mode 100644 index 0000000..a74fac1 --- /dev/null +++ b/src/extension/restore.ts @@ -0,0 +1,52 @@ +/** + * Restores Claude Desktop `mcpServers` from Shield extension backup. + * + * @module extension/restore + */ + +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { dirname } from "node:path"; +import { getClaudeDesktopConfigPath } from "../proxy/config.js"; +import { readExtensionBackup } from "./config-reader.js"; + +function isErrnoException(e: unknown): e is NodeJS.ErrnoException { + return typeof e === "object" && e !== null && "code" in e; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Writes `mcpServers` from `~/.multicorn/extension-backup.json` into the current machine's + * Claude Desktop config file. Preserves other top-level keys when the file already exists. + */ +export async function restoreClaudeDesktopMcpFromBackup(): Promise { + const backup = await readExtensionBackup(); + if (backup === null) { + throw new Error( + "No Shield extension backup found. Expected ~/.multicorn/extension-backup.json " + + "from a previous Shield Desktop Extension session.", + ); + } + + const configPath = getClaudeDesktopConfigPath(); + let root: Record = {}; + + try { + const raw = await readFile(configPath, "utf8"); + const parsed: unknown = JSON.parse(raw); + if (isRecord(parsed)) { + root = parsed; + } + } catch (error) { + if (!isErrnoException(error) || error.code !== "ENOENT") { + throw error; + } + } + + root["mcpServers"] = backup.mcpServers; + + await mkdir(dirname(configPath), { recursive: true }); + await writeFile(configPath, JSON.stringify(root, null, 2) + "\n", { encoding: "utf8" }); +} diff --git a/src/extension/runtime.ts b/src/extension/runtime.ts new file mode 100644 index 0000000..36eee1c --- /dev/null +++ b/src/extension/runtime.ts @@ -0,0 +1,267 @@ +/** + * Shield permission and audit layer for the Desktop Extension (mirrors MCP proxy behaviour). + * + * @module extension/runtime + */ + +import { validateScopeAccess, hasScope } from "../scopes/scope-validator.js"; +import { createActionLogger } from "../logger/action-logger.js"; +import type { Scope } from "../types/index.js"; +import type { ActionLogger } from "../logger/action-logger.js"; +import { + buildBlockedResponse, + buildInternalErrorResponse, + buildServiceUnreachableResponse, +} from "../proxy/interceptor.js"; +import { mapMcpToolToScope } from "../mcp-tool-mapper.js"; +import { + fetchGrantedScopes, + saveCachedScopes, + waitForConsent, + resolveAgentRecord, +} from "../proxy/consent.js"; +import type { ProxyLogger } from "../proxy/logger.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +const JSON_RPC_ID = 0; + +export interface ShieldExtensionRuntimeConfig { + readonly apiKey: string; + readonly agentName: string; + readonly baseUrl: string; + readonly dashboardUrl: string; + readonly logger: ProxyLogger; + readonly scopeRefreshIntervalMs?: number; +} + +function toolError(text: string): CallToolResult { + return { + isError: true, + content: [{ type: "text", text }], + }; +} + +function messageFromJsonRpcResponse(json: string): string { + try { + const parsed: unknown = JSON.parse(json); + if (typeof parsed === "object" && parsed !== null) { + const err = (parsed as Record)["error"]; + if (typeof err === "object" && err !== null) { + const msg = (err as Record)["message"]; + if (typeof msg === "string") return msg; + } + } + } catch { + return "Action blocked by Multicorn Shield."; + } + return "Action blocked by Multicorn Shield."; +} + +const DEFAULT_SCOPE_REFRESH_INTERVAL_MS = 60_000; + +export class ShieldExtensionRuntime { + private readonly config: ShieldExtensionRuntimeConfig; + private actionLogger: ActionLogger | null = null; + private grantedScopes: readonly Scope[] = []; + private agentId = ""; + private authInvalid = false; + private refreshTimer: ReturnType | null = null; + private consentInProgress = false; + + constructor(config: ShieldExtensionRuntimeConfig) { + this.config = config; + } + + async start(): Promise { + const cfg = this.config; + if ( + !cfg.baseUrl.startsWith("https://") && + !cfg.baseUrl.startsWith("http://localhost") && + !cfg.baseUrl.startsWith("http://127.0.0.1") + ) { + throw new Error( + `[multicorn-shield-extension] Base URL must use HTTPS. Received: "${cfg.baseUrl}".`, + ); + } + + const agentRecord = await resolveAgentRecord( + cfg.agentName, + cfg.apiKey, + cfg.baseUrl, + cfg.logger, + ); + + this.agentId = agentRecord.id; + this.grantedScopes = agentRecord.scopes; + this.authInvalid = agentRecord.authInvalid === true; + + this.actionLogger = createActionLogger({ + apiKey: cfg.apiKey, + baseUrl: cfg.baseUrl, + batchMode: { enabled: false }, + onError: (err) => { + cfg.logger.warn("Action log failed.", { error: err.message }); + }, + }); + + const refreshIntervalMs = cfg.scopeRefreshIntervalMs ?? DEFAULT_SCOPE_REFRESH_INTERVAL_MS; + this.refreshTimer = setInterval(() => { + void this.refreshScopes(); + }, refreshIntervalMs); + + const timer = this.refreshTimer as unknown as { unref?: () => void }; + if (typeof timer.unref === "function") { + timer.unref(); + } + } + + async stop(): Promise { + if (this.refreshTimer !== null) { + clearInterval(this.refreshTimer); + this.refreshTimer = null; + } + if (this.actionLogger !== null) { + await this.actionLogger.shutdown(); + this.actionLogger = null; + } + } + + private async refreshScopes(): Promise { + if (this.agentId.length === 0) return; + try { + const scopes = await fetchGrantedScopes( + this.agentId, + this.config.apiKey, + this.config.baseUrl, + ); + this.grantedScopes = scopes; + if (scopes.length > 0) { + await saveCachedScopes(this.config.agentName, this.agentId, scopes, this.config.apiKey); + } + } catch (error) { + this.config.logger.warn("Scope refresh failed.", { + error: error instanceof Error ? error.message : String(error), + }); + } + } + + private async ensureConsent(requestedScope?: Scope): Promise { + if (this.agentId.length === 0) return; + + if (requestedScope !== undefined) { + if (hasScope(this.grantedScopes, requestedScope) || this.consentInProgress) return; + } else { + if (this.grantedScopes.length > 0 || this.consentInProgress) return; + } + + this.consentInProgress = true; + try { + const scopeParam = + requestedScope !== undefined + ? { service: requestedScope.service, permissionLevel: requestedScope.permissionLevel } + : undefined; + const scopes = await waitForConsent( + this.agentId, + this.config.agentName, + this.config.apiKey, + this.config.baseUrl, + this.config.dashboardUrl, + this.config.logger, + scopeParam, + ); + this.grantedScopes = scopes; + await saveCachedScopes(this.config.agentName, this.agentId, scopes, this.config.apiKey); + } finally { + this.consentInProgress = false; + } + } + + /** + * Returns whether the tool call may proceed to the child MCP server. + */ + async evaluateToolCall( + toolName: string, + ): Promise<{ allow: true } | { allow: false; result: CallToolResult }> { + const cfg = this.config; + + try { + if (this.authInvalid) { + return { + allow: false, + result: toolError( + "Action blocked: Shield API key is invalid or has been revoked. Open Claude Desktop, open the Multicorn Shield extension settings, and enter a valid API key.", + ), + }; + } + + if (this.agentId.length === 0) { + const blocked = buildServiceUnreachableResponse(JSON_RPC_ID, cfg.dashboardUrl); + return { + allow: false, + result: toolError(messageFromJsonRpcResponse(JSON.stringify(blocked))), + }; + } + + const mapped = mapMcpToolToScope(toolName); + const { service, permissionLevel, actionType } = mapped; + const requestedScope: Scope = { service, permissionLevel }; + let validation = validateScopeAccess(this.grantedScopes, requestedScope); + + cfg.logger.debug("Tool call intercepted.", { + tool: toolName, + service, + permissionLevel, + allowed: validation.allowed, + }); + + if (!validation.allowed) { + await this.ensureConsent(requestedScope); + validation = validateScopeAccess(this.grantedScopes, requestedScope); + if (!validation.allowed) { + if (this.actionLogger !== null && cfg.agentName.trim().length > 0) { + await this.actionLogger.logAction({ + agent: cfg.agentName, + service, + actionType, + status: "blocked", + }); + } + const blocked = buildBlockedResponse( + JSON_RPC_ID, + service, + permissionLevel, + cfg.dashboardUrl, + ); + return { + allow: false, + result: toolError(messageFromJsonRpcResponse(JSON.stringify(blocked))), + }; + } + } + + if (this.actionLogger !== null) { + if (cfg.agentName.trim().length === 0) { + cfg.logger.warn("Cannot log action: agent name not resolved."); + } else { + await this.actionLogger.logAction({ + agent: cfg.agentName, + service, + actionType, + status: "approved", + }); + } + } + + return { allow: true }; + } catch (error) { + cfg.logger.error("Tool call handler error.", { + error: error instanceof Error ? error.message : String(error), + }); + const blocked = buildInternalErrorResponse(JSON_RPC_ID); + return { + allow: false, + result: toolError(messageFromJsonRpcResponse(JSON.stringify(blocked))), + }; + } + } +} diff --git a/src/extension/server.ts b/src/extension/server.ts new file mode 100644 index 0000000..6545fbf --- /dev/null +++ b/src/extension/server.ts @@ -0,0 +1,185 @@ +/** + * Multicorn Shield Claude Desktop Extension MCP server (stdio). + * + * @module extension/server + */ + +import * as z from "zod/v4"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { createLogger, isValidLogLevel, type LogLevel } from "../proxy/logger.js"; +import { deriveDashboardUrl } from "../proxy/consent.js"; +import { ChildManager } from "./child-manager.js"; +import { + readClaudeDesktopMcpConfig, + writeExtensionBackup, + isShieldExtensionEntry, + type McpServerEntry, +} from "./config-reader.js"; +import { buildToolRouter } from "./tool-router.js"; +import { ShieldExtensionRuntime } from "./runtime.js"; +import { PACKAGE_VERSION } from "../package-meta.js"; + +const ARGS_SCHEMA = z.record(z.string(), z.unknown()); + +function readApiKey(): string | null { + const key = process.env["MULTICORN_API_KEY"]?.trim(); + if (key === undefined || key.length === 0) return null; + return key; +} + +function readBaseUrl(): string { + const raw = process.env["MULTICORN_BASE_URL"]?.trim(); + return raw !== undefined && raw.length > 0 ? raw : "https://api.multicorn.ai"; +} + +function readAgentName(): string { + const raw = process.env["MULTICORN_AGENT_NAME"]?.trim(); + return raw !== undefined && raw.length > 0 ? raw : "claude-desktop-shield"; +} + +function readLogLevel(): LogLevel { + const raw = process.env["MULTICORN_LOG_LEVEL"]?.trim(); + if (raw !== undefined && isValidLogLevel(raw)) return raw; + return "info"; +} + +function asCallToolResult(value: unknown): CallToolResult { + if (typeof value !== "object" || value === null) { + return { content: [{ type: "text", text: String(value) }] }; + } + const obj = value as Record; + if (Array.isArray(obj["content"])) { + return value as CallToolResult; + } + return { content: [{ type: "text", text: JSON.stringify(value) }] }; +} + +export async function runShieldExtension(): Promise { + const logger = createLogger(readLogLevel()); + + const apiKey = readApiKey(); + if (apiKey === null) { + logger.error("MULTICORN_API_KEY is not set. Configure the extension in Claude Desktop."); + process.exit(1); + } + + const baseUrl = readBaseUrl(); + const agentName = readAgentName(); + const dashboardUrl = deriveDashboardUrl(baseUrl); + + const desktop = await readClaudeDesktopMcpConfig(); + if (desktop !== null) { + await writeExtensionBackup(desktop.configPath, desktop.mcpServers); + } else { + logger.warn("Could not read Claude Desktop config. No MCP backup was written.", {}); + } + + const childEntries: Record = {}; + if (desktop !== null) { + for (const [name, entry] of Object.entries(desktop.mcpServers)) { + if (!isShieldExtensionEntry(name, entry)) { + childEntries[name] = entry; + } + } + } + + const childManager = new ChildManager({ logger }); + await childManager.startAll(childEntries); + + const toolsByServer = await childManager.listToolsForAll(); + const { tools: routedTools, routing } = buildToolRouter(toolsByServer, logger); + + const runtime = new ShieldExtensionRuntime({ + apiKey, + agentName, + baseUrl, + dashboardUrl, + logger, + }); + await runtime.start(); + + const mcpServer = new McpServer( + { name: "multicorn-shield", version: PACKAGE_VERSION }, + { capabilities: { tools: {} } }, + ); + + for (const tool of routedTools) { + const sourceServer = routing.get(tool.name); + if (sourceServer === undefined) continue; + + mcpServer.registerTool( + tool.name, + { + description: tool.description ?? "", + inputSchema: ARGS_SCHEMA, + }, + async (args) => { + const decision = await runtime.evaluateToolCall(tool.name); + if (!decision.allow) { + return decision.result; + } + + const child = childManager.getChildByServerName(sourceServer); + if (child === undefined) { + return { + isError: true, + content: [ + { + type: "text", + text: `Shield could not route tool "${tool.name}" to MCP server "${sourceServer}".`, + }, + ], + }; + } + + try { + const result = await child.session.request("tools/call", { + name: tool.name, + arguments: args, + }); + return asCallToolResult(result); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + isError: true, + content: [{ type: "text", text: `Tool call failed: ${message}` }], + }; + } + }, + ); + } + + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); + + const cleanup = async (): Promise => { + await runtime.stop(); + childManager.stopAll(); + await mcpServer.close(); + }; + + const stdinEnded = new Promise((resolve) => { + process.stdin.resume(); + process.stdin.once("end", () => { + resolve(); + }); + }); + + const onSignal = (): void => { + void cleanup() + .then(() => { + process.exit(0); + }) + .catch(() => { + process.exit(1); + }); + }; + + process.once("SIGTERM", onSignal); + process.once("SIGINT", onSignal); + + await stdinEnded; + await cleanup(); +} diff --git a/src/extension/tool-router.ts b/src/extension/tool-router.ts new file mode 100644 index 0000000..1eab4b4 --- /dev/null +++ b/src/extension/tool-router.ts @@ -0,0 +1,88 @@ +/** + * Merges tool lists from multiple MCP children. Duplicate tool names: first wins. + * + * @module extension/tool-router + */ + +import type { ProxyLogger } from "../proxy/logger.js"; + +/** MCP tool shape from `tools/list` (subset used by Shield). */ +export interface McpToolDefinition { + readonly name: string; + readonly description?: string; + readonly inputSchema?: unknown; +} + +export interface RoutedTool extends McpToolDefinition { + readonly sourceServerName: string; +} + +export interface ToolRouterResult { + readonly tools: readonly RoutedTool[]; + readonly routing: ReadonlyMap; +} + +function isToolDefinition(value: unknown): value is McpToolDefinition { + if (typeof value !== "object" || value === null) return false; + const name = (value as Record)["name"]; + return typeof name === "string" && name.length > 0; +} + +/** + * Builds the combined tool list and a map from tool name to originating server key. + * When two children expose the same tool name, the first server in iteration order wins; + * the duplicate is skipped and a warning is logged. + */ +export function buildToolRouter( + toolsByServer: ReadonlyMap, + logger: ProxyLogger, +): ToolRouterResult { + const routing = new Map(); + const tools: RoutedTool[] = []; + + for (const [serverName, list] of toolsByServer) { + for (const tool of list) { + if (!isToolDefinition(tool)) continue; + const name = tool.name; + if (routing.has(name)) { + const existing = routing.get(name); + logger.warn("Skipping duplicate tool name from MCP server.", { + tool: name, + skippedServer: serverName, + keptServer: existing ?? "", + }); + continue; + } + routing.set(name, serverName); + const routed: RoutedTool = { + name, + sourceServerName: serverName, + ...(tool.description !== undefined ? { description: tool.description } : {}), + ...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}), + }; + tools.push(routed); + } + } + + return { tools, routing }; +} + +export function parseToolsListResult(result: unknown): readonly McpToolDefinition[] { + if (typeof result !== "object" || result === null) return []; + const tools = (result as Record)["tools"]; + if (!Array.isArray(tools)) return []; + + const out: McpToolDefinition[] = []; + for (const t of tools) { + if (isToolDefinition(t)) { + const row = t as unknown as Record; + const def: McpToolDefinition = { + name: t.name, + ...(typeof t.description === "string" ? { description: t.description } : {}), + ...(row["inputSchema"] !== undefined ? { inputSchema: row["inputSchema"] } : {}), + }; + out.push(def); + } + } + return out; +} diff --git a/src/mcp-tool-mapper.test.ts b/src/mcp-tool-mapper.test.ts new file mode 100644 index 0000000..083fb3a --- /dev/null +++ b/src/mcp-tool-mapper.test.ts @@ -0,0 +1,64 @@ +/** + * @vitest-environment node + */ + +import { describe, it, expect } from "vitest"; +import { mapMcpToolToScope } from "./mcp-tool-mapper.js"; + +describe("mapMcpToolToScope", () => { + it("maps MCP filesystem read tools to filesystem:read", () => { + expect(mapMcpToolToScope("read_file")).toEqual({ + service: "filesystem", + permissionLevel: "read", + actionType: "read_file", + }); + expect(mapMcpToolToScope("list_directory")).toEqual({ + service: "filesystem", + permissionLevel: "read", + actionType: "list_directory", + }); + }); + + it("maps MCP filesystem write tools to filesystem:write", () => { + expect(mapMcpToolToScope("write_file")).toEqual({ + service: "filesystem", + permissionLevel: "write", + actionType: "write_file", + }); + expect(mapMcpToolToScope("edit_file")).toEqual({ + service: "filesystem", + permissionLevel: "write", + actionType: "edit_file", + }); + }); + + it("maps gmail_send_email to gmail:write", () => { + expect(mapMcpToolToScope("gmail_send_email")).toEqual({ + service: "gmail", + permissionLevel: "write", + actionType: "gmail_send_email", + }); + }); + + it("maps calendar_create_event to google_calendar with inferred write", () => { + expect(mapMcpToolToScope("calendar_create_event")).toEqual({ + service: "google_calendar", + permissionLevel: "write", + actionType: "calendar_create_event", + }); + }); + + it("maps run_terminal_cmd to terminal:execute", () => { + expect(mapMcpToolToScope("run_terminal_cmd")).toEqual({ + service: "terminal", + permissionLevel: "execute", + actionType: "run_terminal_cmd", + }); + }); + + it("does not treat read_file as service read with action file", () => { + const result = mapMcpToolToScope("read_file"); + expect(result.service).toBe("filesystem"); + expect(result.permissionLevel).toBe("read"); + }); +}); diff --git a/src/mcp-tool-mapper.ts b/src/mcp-tool-mapper.ts new file mode 100644 index 0000000..8722e63 --- /dev/null +++ b/src/mcp-tool-mapper.ts @@ -0,0 +1,189 @@ +/** + * Maps MCP tool names (stdio servers, Claude Desktop) to Shield service and permission level. + * + * Uses explicit tables for common MCP servers (filesystem, terminal, browser) and the same + * integration-style prefix rules as OpenClaw's tool-mapper for names like `gmail_send_email`. + * + * @module mcp-tool-mapper + */ + +import type { PermissionLevel } from "./types/index.js"; + +export interface McpToolScopeMapping { + readonly service: string; + readonly permissionLevel: PermissionLevel; + /** Original tool name for audit logs. */ + readonly actionType: string; +} + +/** Tools from MCP filesystem servers and common variants (read side). */ +const FILESYSTEM_READ_TOOLS: ReadonlySet = new Set([ + "read_file", + "read_text_file", + "read_media_file", + "read_multiple_files", + "list_directory", + "list_dir", + "directory_tree", + "tree", + "get_file_info", + "stat", + "search_files", + "glob_file_search", + "list_allowed_directories", + "file_search", +]); + +/** Tools from MCP filesystem servers (write / mutate side). */ +const FILESYSTEM_WRITE_TOOLS: ReadonlySet = new Set([ + "write_file", + "edit_file", + "create_directory", + "mkdir", + "move_file", + "rename", + "delete_file", + "remove_file", + "copy_file", +]); + +const TERMINAL_EXECUTE_TOOLS: ReadonlySet = new Set([ + "run_terminal_cmd", + "execute_command", + "terminal_run", + "run_command", +]); + +const BROWSER_EXECUTE_TOOLS: ReadonlySet = new Set([ + "web_fetch", + "fetch_url", + "browser_navigate", + "navigate", + "mcp_web_fetch", +]); + +/** + * Service prefixes aligned with OpenClaw `tool-mapper` integration rules. + */ +const INTEGRATION_SERVICE_BY_PREFIX: Readonly> = { + gmail: "gmail", + google_calendar: "google_calendar", + calendar: "google_calendar", + google_drive: "google_drive", + drive: "google_drive", + slack: "slack", + payments: "payments", + payment: "payments", + stripe: "payments", + github: "github", + gitlab: "gitlab", + notion: "notion", + linear: "linear", + jira: "jira", +}; + +function inferPermissionFromToolName(normalized: string): PermissionLevel { + if ( + normalized.includes("_read") || + normalized.includes("_get") || + normalized.includes("_list") || + normalized.endsWith("_fetch") || + normalized.includes("_search") + ) { + return "read"; + } + if ( + normalized.includes("_write") || + normalized.includes("_send") || + normalized.includes("_create") || + normalized.includes("_update") || + normalized.includes("_delete") || + normalized.includes("_push") || + normalized.includes("_commit") || + normalized.includes("_post") || + normalized.includes("_patch") + ) { + return "write"; + } + return "execute"; +} + +/** + * Maps an MCP `tools/call` tool name to Shield `service` + `permissionLevel` for scope checks. + */ +export function mapMcpToolToScope(toolName: string): McpToolScopeMapping { + const actionType = toolName.trim(); + const normalized = actionType.toLowerCase(); + + if (normalized.length === 0) { + return { service: "unknown", permissionLevel: "execute", actionType }; + } + + if (FILESYSTEM_READ_TOOLS.has(normalized)) { + return { service: "filesystem", permissionLevel: "read", actionType }; + } + if (FILESYSTEM_WRITE_TOOLS.has(normalized)) { + return { service: "filesystem", permissionLevel: "write", actionType }; + } + if (TERMINAL_EXECUTE_TOOLS.has(normalized)) { + return { service: "terminal", permissionLevel: "execute", actionType }; + } + if (BROWSER_EXECUTE_TOOLS.has(normalized)) { + return { service: "browser", permissionLevel: "execute", actionType }; + } + + // OpenClaw-style single-token tools (rare in MCP, but cheap to support) + if (normalized === "read") { + return { service: "filesystem", permissionLevel: "read", actionType }; + } + if (normalized === "write" || normalized === "edit") { + return { service: "filesystem", permissionLevel: "write", actionType }; + } + if (normalized === "exec") { + return { service: "terminal", permissionLevel: "execute", actionType }; + } + + // git_* → git service + if (normalized.startsWith("git_")) { + const permissionLevel = inferPermissionFromToolName(normalized); + return { service: "git", permissionLevel, actionType }; + } + + // Integration prefixes (gmail_send_email, slack_read_channels, …) + for (const [prefix, service] of Object.entries(INTEGRATION_SERVICE_BY_PREFIX)) { + if (normalized.startsWith(`${prefix}_`) || normalized === prefix) { + const permissionLevel = inferPermissionFromToolName(normalized); + return { service, permissionLevel, actionType }; + } + } + + // Underscore split: first segment as service, infer level from remainder + const idx = normalized.indexOf("_"); + if (idx === -1) { + return { service: normalized, permissionLevel: "execute", actionType }; + } + + const head = normalized.slice(0, idx); + const tail = normalized.slice(idx + 1); + let permissionLevel: PermissionLevel = "execute"; + if ( + tail.includes("read") || + tail.includes("list") || + tail.includes("get") || + tail.includes("search") || + tail.includes("fetch") + ) { + permissionLevel = "read"; + } else if ( + tail.includes("write") || + tail.includes("send") || + tail.includes("create") || + tail.includes("update") || + tail.includes("delete") || + tail.includes("remove") + ) { + permissionLevel = "write"; + } + + return { service: head, permissionLevel, actionType }; +} diff --git a/src/package-meta.ts b/src/package-meta.ts new file mode 100644 index 0000000..0efe2b5 --- /dev/null +++ b/src/package-meta.ts @@ -0,0 +1,9 @@ +/** + * Package metadata from the root manifest (single source of truth for version strings). + * + * @module package-meta + */ + +import packageJson from "../package.json" with { type: "json" }; + +export const PACKAGE_VERSION: string = packageJson.version; diff --git a/tsup.config.ts b/tsup.config.ts index 6a8410a..74e86c0 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -64,4 +64,32 @@ export default defineConfig([ ); }, }, + { + entry: { "shield-extension": "bin/shield-extension.ts" }, + format: ["esm"], + dts: false, + splitting: false, + sourcemap: false, + clean: false, + treeshake: true, + minify: false, + outDir: "dist", + platform: "node", + banner: { + js: "// Multicorn Shield Claude Desktop Extension - https://multicorn.ai", + }, + noExternal: ["@modelcontextprotocol/sdk", "zod"], + }, + { + entry: { "multicorn-shield": "bin/multicorn-shield.ts" }, + format: ["esm"], + dts: false, + splitting: false, + sourcemap: false, + clean: false, + treeshake: true, + minify: false, + outDir: "dist", + platform: "node", + }, ]); From a607f6714222deb73e59eb3d7f8e771274442b9e Mon Sep 17 00:00:00 2001 From: Rachelle Date: Tue, 24 Mar 2026 20:29:09 +1100 Subject: [PATCH 4/4] updates --- .../__tests__/server.integration.test.ts | 3 +- src/extension/server.ts | 248 ++++++++++++------ 2 files changed, 172 insertions(+), 79 deletions(-) diff --git a/src/extension/__tests__/server.integration.test.ts b/src/extension/__tests__/server.integration.test.ts index 792a2db..89675f0 100644 --- a/src/extension/__tests__/server.integration.test.ts +++ b/src/extension/__tests__/server.integration.test.ts @@ -73,7 +73,8 @@ describe("Shield extension runtime (integration)", () => { runtime = new ShieldExtensionRuntime({ apiKey: "test-key", agentName: "test-agent", - baseUrl: "http://127.0.0.1:1", + // localhost is allowed by ActionLogger; port 1 should refuse so agent id stays empty. + baseUrl: "http://localhost:1", dashboardUrl: "https://app.multicorn.ai", logger: createLogger("error"), }); diff --git a/src/extension/server.ts b/src/extension/server.ts index 6545fbf..bf17a29 100644 --- a/src/extension/server.ts +++ b/src/extension/server.ts @@ -5,9 +5,19 @@ */ import * as z from "zod/v4"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { + normalizeObjectSchema, + safeParseAsync, + getParseErrorMessage, +} from "@modelcontextprotocol/sdk/server/zod-compat.js"; +import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + type CallToolResult, +} from "@modelcontextprotocol/sdk/types.js"; import { createLogger, isValidLogLevel, type LogLevel } from "../proxy/logger.js"; import { deriveDashboardUrl } from "../proxy/consent.js"; import { ChildManager } from "./child-manager.js"; @@ -23,6 +33,20 @@ import { PACKAGE_VERSION } from "../package-meta.js"; const ARGS_SCHEMA = z.record(z.string(), z.unknown()); +const ARGS_OBJECT_SCHEMA = normalizeObjectSchema(ARGS_SCHEMA); +const ARGS_INPUT_JSON_SCHEMA = + ARGS_OBJECT_SCHEMA !== undefined + ? toJsonSchemaCompat(ARGS_OBJECT_SCHEMA, { + strictUnions: true, + pipeStrategy: "input", + }) + : ({ type: "object" } as const); + +interface RegisteredShieldTool { + description: string; + call: (args: Record) => Promise; +} + function readApiKey(): string | null { const key = process.env["MULTICORN_API_KEY"]?.trim(); if (key === undefined || key.length === 0) return null; @@ -69,95 +93,163 @@ export async function runShieldExtension(): Promise { const agentName = readAgentName(); const dashboardUrl = deriveDashboardUrl(baseUrl); - const desktop = await readClaudeDesktopMcpConfig(); - if (desktop !== null) { - await writeExtensionBackup(desktop.configPath, desktop.mcpServers); - } else { - logger.warn("Could not read Claude Desktop config. No MCP backup was written.", {}); - } + let resolveReady!: () => void; + let rejectReady!: (reason: unknown) => void; + const readyPromise = new Promise((resolve, reject) => { + resolveReady = resolve; + rejectReady = reject; + }); - const childEntries: Record = {}; - if (desktop !== null) { - for (const [name, entry] of Object.entries(desktop.mcpServers)) { - if (!isShieldExtensionEntry(name, entry)) { - childEntries[name] = entry; - } - } - } + const toolRegistry = new Map(); + + // McpServer cannot express connect-first + deferred tools without SDK internals; Server is the supported low-level API here. + // eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional + const server = new Server( + { name: "multicorn-shield", version: PACKAGE_VERSION }, + { capabilities: { tools: { listChanged: true } } }, + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + await readyPromise; + const tools = Array.from(toolRegistry.entries()).map(([name, entry]) => ({ + name, + description: entry.description, + inputSchema: ARGS_INPUT_JSON_SCHEMA, + })); + return { tools }; + }); - const childManager = new ChildManager({ logger }); - await childManager.startAll(childEntries); + server.setRequestHandler(CallToolRequestSchema, async (request) => { + await readyPromise; + const toolName = request.params.name; + const registered = toolRegistry.get(toolName); + if (registered === undefined) { + return { + isError: true, + content: [ + { + type: "text", + text: `Tool ${toolName} not found`, + }, + ], + }; + } - const toolsByServer = await childManager.listToolsForAll(); - const { tools: routedTools, routing } = buildToolRouter(toolsByServer, logger); + const parseResult = await safeParseAsync(ARGS_SCHEMA, request.params.arguments ?? {}); + if (!parseResult.success) { + return { + isError: true, + content: [ + { + type: "text", + text: `Input validation error: Invalid arguments for tool ${toolName}: ${getParseErrorMessage(parseResult.error)}`, + }, + ], + }; + } - const runtime = new ShieldExtensionRuntime({ - apiKey, - agentName, - baseUrl, - dashboardUrl, - logger, + return registered.call(parseResult.data); }); - await runtime.start(); - const mcpServer = new McpServer( - { name: "multicorn-shield", version: PACKAGE_VERSION }, - { capabilities: { tools: {} } }, - ); + const transport = new StdioServerTransport(); + await server.connect(transport); - for (const tool of routedTools) { - const sourceServer = routing.get(tool.name); - if (sourceServer === undefined) continue; - - mcpServer.registerTool( - tool.name, - { - description: tool.description ?? "", - inputSchema: ARGS_SCHEMA, - }, - async (args) => { - const decision = await runtime.evaluateToolCall(tool.name); - if (!decision.allow) { - return decision.result; - } + let childManager: ChildManager | undefined; + let runtime: ShieldExtensionRuntime | undefined; - const child = childManager.getChildByServerName(sourceServer); - if (child === undefined) { - return { - isError: true, - content: [ - { - type: "text", - text: `Shield could not route tool "${tool.name}" to MCP server "${sourceServer}".`, - }, - ], - }; - } + void (async () => { + try { + const desktop = await readClaudeDesktopMcpConfig(); + if (desktop !== null) { + await writeExtensionBackup(desktop.configPath, desktop.mcpServers); + } else { + logger.warn("Could not read Claude Desktop config. No MCP backup was written.", {}); + } - try { - const result = await child.session.request("tools/call", { - name: tool.name, - arguments: args, - }); - return asCallToolResult(result); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - isError: true, - content: [{ type: "text", text: `Tool call failed: ${message}` }], - }; + const childEntries: Record = {}; + if (desktop !== null) { + for (const [name, entry] of Object.entries(desktop.mcpServers)) { + if (!isShieldExtensionEntry(name, entry)) { + childEntries[name] = entry; + } } - }, - ); - } + } - const transport = new StdioServerTransport(); - await mcpServer.connect(transport); + childManager = new ChildManager({ logger }); + await childManager.startAll(childEntries); + + const toolsByServer = await childManager.listToolsForAll(); + const { tools: routedTools, routing } = buildToolRouter(toolsByServer, logger); + + runtime = new ShieldExtensionRuntime({ + apiKey, + agentName, + baseUrl, + dashboardUrl, + logger, + }); + await runtime.start(); + + const rt = runtime; + const cm = childManager; + + for (const tool of routedTools) { + const sourceServer = routing.get(tool.name); + if (sourceServer === undefined) continue; + + toolRegistry.set(tool.name, { + description: tool.description ?? "", + call: async (args) => { + const decision = await rt.evaluateToolCall(tool.name); + if (!decision.allow) { + return decision.result; + } + + const child = cm.getChildByServerName(sourceServer); + if (child === undefined) { + return { + isError: true, + content: [ + { + type: "text", + text: `Shield could not route tool "${tool.name}" to MCP server "${sourceServer}".`, + }, + ], + }; + } + + try { + const result = await child.session.request("tools/call", { + name: tool.name, + arguments: args, + }); + return asCallToolResult(result); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + isError: true, + content: [{ type: "text", text: `Tool call failed: ${message}` }], + }; + } + }, + }); + } + + await server.sendToolListChanged(); + resolveReady(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.error(`Shield extension setup failed: ${message}`, {}); + rejectReady(error); + } + })(); const cleanup = async (): Promise => { - await runtime.stop(); - childManager.stopAll(); - await mcpServer.close(); + if (runtime !== undefined) { + await runtime.stop(); + } + childManager?.stopAll(); + await server.close(); }; const stdinEnded = new Promise((resolve) => {