diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 8a9ea42..0000000 --- a/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules/ -tests/ -docs/ -.git/ -.github/ -*.md -tsconfig.json -scripts/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 12dc5af..0000000 --- a/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM oven/bun:alpine -WORKDIR /app -COPY package.json bun.lock ./ -RUN bun install --frozen-lockfile -COPY src/ src/ -USER bun -ENTRYPOINT ["bun", "src/index.ts"] diff --git a/bun.lock b/bun.lock index d00d454..d0c7ed1 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,6 @@ "": { "name": "@orkait-ai/hyperstack", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", "tsx": "^4.21.0", "yaml": "^2.5.1", "zod": "^3.23.0", @@ -69,204 +68,24 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], - "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], - "@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": "bin/esbuild" }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="], - - "http-errors": ["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" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "send": ["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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], } } diff --git a/generated/tool-index/local-tool-registry.json b/generated/tool-index/local-tool-registry.json index c5bf7c9..88e7a76 100644 --- a/generated/tool-index/local-tool-registry.json +++ b/generated/tool-index/local-tool-registry.json @@ -37,7 +37,6 @@ "golang_list_patterns": "src/plugins/golang/tools/list-patterns.js", "golang_list_practices": "src/plugins/golang/tools/list-practices.js", "golang_search_docs": "src/plugins/golang/tools/search-docs.js", - "hyperstack_setup": "src/plugins/hyperstack/tools/setup.js", "lenis_generate_setup": "src/plugins/lenis/tools/generate-setup.js", "lenis_get_api": "src/plugins/lenis/tools/get-api.js", "lenis_get_pattern": "src/plugins/lenis/tools/get-pattern.js", diff --git a/generated/tool-index/local-tool-registry.ts b/generated/tool-index/local-tool-registry.ts index d961c04..7258201 100644 --- a/generated/tool-index/local-tool-registry.ts +++ b/generated/tool-index/local-tool-registry.ts @@ -37,7 +37,6 @@ export const LOCAL_TOOL_REGISTRY = { "golang_list_patterns": () => import("src/plugins/golang/tools/list-patterns.js"), "golang_list_practices": () => import("src/plugins/golang/tools/list-practices.js"), "golang_search_docs": () => import("src/plugins/golang/tools/search-docs.js"), - "hyperstack_setup": () => import("src/plugins/hyperstack/tools/setup.js"), "lenis_generate_setup": () => import("src/plugins/lenis/tools/generate-setup.js"), "lenis_get_api": () => import("src/plugins/lenis/tools/get-api.js"), "lenis_get_pattern": () => import("src/plugins/lenis/tools/get-pattern.js"), diff --git a/package.json b/package.json index 20539da..767047a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@orkait-ai/hyperstack", - "version": "1.1.3", - "description": "Disciplined MCP server + skill system. 12 plugins, 80 tools, 21 skills with adversarial enforcement. Designer/DESIGN.md pipeline, shadcn/ui, React Flow, Motion, Lenis, React 19, Echo, Go, Rust, design tokens, UI/UX.", + "version": "2.0.0", + "description": "Topology-driven CLI for 11 plugin tool families covering designer, design tokens, UI/UX, React, shadcn, Motion, Lenis, React Flow, Echo, Go, and Rust.", "bin": { "hyperstack": "bin/hyperstack.mjs" }, @@ -11,12 +11,7 @@ "compile:context": "tsx src/internal/compile-runtime-context.ts", "generate:local-tools": "tsx scripts/generate-local-tool-registry.ts", "generate:topology": "tsx scripts/generate-topology-artifacts.ts", - "start": "bun src/index.ts", - "dev": "bun --watch src/index.ts", - "docker:run": "bun scripts/ensure-singleton.ts", - "skills:index": "tsx scripts/generate-skills-index.ts", - "mcp:start": "bun scripts/start-mcp.ts", - "setup": "tsx scripts/setup.ts" + "skills:index": "tsx scripts/generate-skills-index.ts" }, "author": "Orkait", "license": "MIT", @@ -24,7 +19,6 @@ "node": ">=18" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", "tsx": "^4.21.0", "yaml": "^2.5.1", "zod": "^3.23.0" diff --git a/scripts/ensure-singleton.ts b/scripts/ensure-singleton.ts deleted file mode 100644 index 635f519..0000000 --- a/scripts/ensure-singleton.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { execSync } from "child_process"; - -const IMAGE = "ghcr.io/orkait/hyperstack:main"; -const CONTAINER_NAME = "hyperstack-mcp"; - -function run() { - console.log("Checking for existing Hyperstack containers (cross-platform)..."); - - try { - // 1. Find containers by image and name - const byImage = execSync(`docker ps -aq --filter "ancestor=${IMAGE}"`).toString().trim().split(/\s+/).filter(Boolean); - const byName = execSync(`docker ps -aq --filter "name=${CONTAINER_NAME}"`).toString().trim().split(/\s+/).filter(Boolean); - - // 2. Combine and uniq - const allStale = Array.from(new Set([...byImage, ...byName])); - - if (allStale.length > 0) { - console.log(`Removing stale Hyperstack containers: ${allStale.join(", ")}`); - // We use a loop or join with space since 'docker rm -f' accepts multiple IDs - execSync(`docker rm -f ${allStale.join(" ")}`, { stdio: "inherit" }); - } else { - console.log("No stale containers found."); - } - - // 3. Start fresh container - console.log(`Starting fresh Hyperstack container: ${CONTAINER_NAME}`); - execSync( - `docker run -d --name ${CONTAINER_NAME} --restart unless-stopped \ - --memory=512m --cpus=1 \ - --entrypoint sleep \ - ${IMAGE} infinity`, - { stdio: "inherit" } - ); - - console.log("\nVerification:"); - execSync(`docker ps --filter name=${CONTAINER_NAME}`, { stdio: "inherit" }); - - } catch (error: any) { - console.error("Error ensuring singleton container:", error.message); - process.exit(1); - } -} - -run(); diff --git a/scripts/setup.ts b/scripts/setup.ts deleted file mode 100644 index 1edd852..0000000 --- a/scripts/setup.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as setup from "../src/internal/setup-hyperstack.js"; -import * as fs from "node:fs"; -import * as path from "node:path"; - -async function main() { - console.log("\n๐Ÿš€ Hyperstack Autonomous Setup (CLI)"); - console.log("=====================================\n"); - - const hintedPlatform = setup.detectEnvironment(); - console.log(`๐Ÿ“ก Hinted platform: ${hintedPlatform}`); - - const configPath = setup.findConfigFile(hintedPlatform); - - if (!configPath) { - console.warn("โš ๏ธ Could not find an MCP configuration file in any known location."); - console.log("Tried: .claude.json, .cursor/mcp.json, .codeium/windsurf/mcp_config.json, .roo/mcp.json, .gemini/settings.json, .kiro/settings/mcp.json, .qwen/settings.json"); - console.log("\n๐Ÿ’ก OpenAI Codex CLI? Run: codex mcp add hyperstack -- bun ~/.hyperstack/bin/hyperstack.mjs"); - console.log(" For any unknown IDE, use the Agentic Autopilot instead."); - process.exit(1); - } - - // Resolve the actual platform from the found config path - const platform = setup.detectPlatformFromConfigPath(configPath); - console.log(`โœ… Found config: ${configPath} (${platform})`); - - const skillPath = setup.findSkillPath(platform); - if (skillPath) { - const hyperstackSkills = path.join(process.cwd(), "skills"); - const skillTarget = path.join(skillPath, "hyperstack"); - console.log(`\n๐Ÿ“š Skill target: ${skillTarget}`); - console.log(`Run this to activate adversarial gates:`); - console.log(` ln -s "${hyperstackSkills}" "${skillTarget}"`); - } - - const pluginRoot = process.cwd(); - - const patch = setup.generateMcpPatch(configPath, pluginRoot, platform, "local"); - - // Proactively apply the patch - setup.applyMcpPatch(configPath, patch); - - console.log("\n๐Ÿ“‹ Configuration Summary:"); - console.log("---------------------------------"); - console.log(`โœ… Environment: ${platform}`); - console.log(`โœ… Config Path: ${configPath}`); - if (skillPath) { - console.log(`โœ… Skill Target: ${path.join(skillPath, "hyperstack")}`); - } - console.log("---------------------------------\n"); - - console.log("๐Ÿš€ Setup Complete! You must restart your AI client to pick up the new tools."); -} - -main().catch(console.error); diff --git a/scripts/start-mcp.ts b/scripts/start-mcp.ts deleted file mode 100644 index ffb7de1..0000000 --- a/scripts/start-mcp.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { execSync, spawn } from "child_process"; -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; - -const IMAGE = "ghcr.io/orkait/hyperstack:main"; -const CONTAINER_NAME = "hyperstack-mcp"; -const LOCK_FILE = path.join(os.tmpdir(), "hyperstack-mcp-startup.lock"); - -/** - * Standardized Cross-Platform MCP Startup Logic - * - Ensures the persistent container is running. - * - Bridges the local I/O to the containerized MCP process via 'docker exec'. - */ -async function startMcp() { - let lockFd: number | null = null; - - try { - // 1. Concurrency control via file lock - try { - lockFd = fs.openSync(LOCK_FILE, 'wx'); - - // Check if the container is already running - const running = execSync(`docker ps -q --filter "name=^${CONTAINER_NAME}$"`, { stdio: "pipe" }).toString().trim(); - - if (!running) { - // If not running, ensure it's removed and start fresh - // Suppress errors if container doesn't exist - try { execSync(`docker rm -f ${CONTAINER_NAME}`, { stdio: "ignore" }); } catch {} - - execSync( - `docker run -d --name ${CONTAINER_NAME} --restart unless-stopped \ - --memory=512m --cpus=1 \ - --entrypoint sleep \ - ${IMAGE} infinity`, - { stdio: "inherit" } - ); - - // Brief pause to allow Docker to initialize the process - await new Promise(r => setTimeout(r, 300)); - } - } catch (e: any) { - if (e.code === 'EEXIST') { - // Another process is handling startup, wait briefly - await new Promise(r => setTimeout(r, 500)); - } else { - throw e; - } - } finally { - if (lockFd !== null) { - fs.closeSync(lockFd); - try { fs.unlinkSync(LOCK_FILE); } catch {} - } - } - - // 2. Connect to the MCP server inside the container - // This allows the local agent/IDE to communicate with the containerized Bun process. - const proc = spawn("docker", ["exec", "-i", CONTAINER_NAME, "bun", "/app/src/index.ts"], { - stdio: "inherit" - }); - - proc.on("exit", (code) => process.exit(code || 0)); - proc.on("error", (err) => { - console.error("Failed to bridge to containerized MCP:", err.message); - process.exit(1); - }); - - } catch (error: any) { - console.error("Critical error starting Hyperstack MCP:", error.message); - process.exit(1); - } -} - -startMcp(); diff --git a/src/cli.ts b/src/cli.ts index 6bd102a..f689e45 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,16 +1,64 @@ import { validateArtifactPayload } from "./engine/artifact-validator.js"; import { routeRequest } from "./engine/router.js"; import { loadTopology } from "./engine/topology-loader.js"; +import { assertToolAllowedForAgent, getAgent } from "./engine/policy.js"; import { invokeLocalTool } from "./adapters/local-tools/index.js"; +type ParsedArgs = { + positional: string[]; + flags: Record; +}; + +function parseArgs(argv: string[]): ParsedArgs { + const positional: string[] = []; + const flags: Record = {}; + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i]; + if (token.startsWith("--")) { + const name = token.slice(2); + const next = argv[i + 1]; + if (next === undefined || next.startsWith("--")) { + flags[name] = "true"; + } else { + flags[name] = next; + i += 1; + } + } else { + positional.push(token); + } + } + return { positional, flags }; +} + +function usage(): never { + process.stderr.write( + "Usage:\n" + + " hyperstack tool --json '{...}' [--agent ]\n" + + " hyperstack route --json '{...}'\n" + + " hyperstack artifact validate --json '{...}'\n" + + "\n" + + "Topology enforcement:\n" + + " Pass --agent or set HYPERSTACK_AGENT to enforce bundle allow/deny\n" + + " on `tool` calls. When unset, tools run without topology check.\n", + ); + process.exit(1); +} + async function main() { - const [command, toolName, flag, json] = process.argv.slice(2); + const { positional, flags } = parseArgs(process.argv.slice(2)); + const [command, ...rest] = positional; const topology = loadTopology(process.cwd()); if (command === "tool") { - if (!toolName || flag !== "--json" || !json) { - process.stderr.write('Usage: hyperstack tool --json \'{"key":"value"}\'\n'); - process.exit(1); + const toolName = rest[0]; + const json = flags.json; + if (!toolName || !json) usage(); + + const agentId = flags.agent ?? process.env.HYPERSTACK_AGENT; + if (agentId) { + const agent = getAgent(topology, agentId); + const bundle = assertToolAllowedForAgent(topology, agent, toolName); + process.stderr.write(`[topology] agent=${agent.id} bundle=${bundle.id} tool=${toolName}\n`); } const args = JSON.parse(json) as Record; @@ -20,44 +68,29 @@ async function main() { } if (command === "route") { - if (toolName !== "--json" || !flag) { - process.stderr.write("Usage: hyperstack route --json '{...}'\n"); - process.exit(1); - } - - const input = JSON.parse(flag) as { + const json = flags.json; + if (!json) usage(); + const input = JSON.parse(json) as { requestId: string; domainTargets: string[]; capabilityTargets: string[]; workspaceInventory: { projectMode: "greenfield" | "existing"; existingPatterns: string[] }; changeClassification: string; }; - process.stdout.write(`${JSON.stringify(routeRequest(topology, input), null, 2)}\n`); process.exit(0); } - if (command === "artifact" && toolName === "validate") { - const artifactId = flag; - const jsonFlag = process.argv[5]; - const jsonPayload = process.argv[6]; - - if (!artifactId || jsonFlag !== "--json" || !jsonPayload) { - process.stderr.write("Usage: hyperstack artifact validate --json '{...}'\n"); - process.exit(1); - } - - const payload = JSON.parse(jsonPayload) as Record; + if (command === "artifact" && rest[0] === "validate") { + const artifactId = rest[1]; + const json = flags.json; + if (!artifactId || !json) usage(); + const payload = JSON.parse(json) as Record; process.stdout.write(`${JSON.stringify(validateArtifactPayload(topology, artifactId, payload), null, 2)}\n`); process.exit(0); } - process.stderr.write( - "Usage: hyperstack tool --json '{...}'\n" + - " or: hyperstack route --json '{...}'\n" + - " or: hyperstack artifact validate --json '{...}'\n", - ); - process.exit(1); + usage(); } main().catch((error) => { diff --git a/src/engine/injector.ts b/src/engine/injector.ts deleted file mode 100644 index cda1c20..0000000 --- a/src/engine/injector.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { BundlePolicy } from "./contracts.js"; - -export function buildInjectionSlice(bundle: BundlePolicy, capability: string) { - return { - bundle: bundle.id, - capability, - sources: bundle.sources, - toolPrefixes: bundle.toolPrefixes, - }; -} diff --git a/src/engine/navigation.ts b/src/engine/navigation.ts deleted file mode 100644 index 231f8ee..0000000 --- a/src/engine/navigation.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { BundlePolicy, LoadedTopology } from "./contracts.js"; - -export function getBundle(topology: LoadedTopology, bundleId: string): BundlePolicy { - const bundle = topology.bundles.find((entry) => entry.id === bundleId); - if (!bundle) { - throw new Error(`Unknown bundle: ${bundleId}`); - } - return bundle; -} - -export function listAgentRouting(topology: LoadedTopology) { - return topology.agents.map((agent) => ({ - id: agent.id, - allowedBundles: agent.allowedBundles, - forbiddenBundles: agent.forbiddenBundles, - })); -} diff --git a/src/engine/policy.ts b/src/engine/policy.ts index a3f8ee5..51031ae 100644 --- a/src/engine/policy.ts +++ b/src/engine/policy.ts @@ -8,14 +8,6 @@ export function getAgent(topology: LoadedTopology, agentId: string): AgentPolicy return agent; } -export function getBundleByCapability(topology: LoadedTopology, capability: string): BundlePolicy { - const bundle = topology.bundles.find((entry) => entry.capabilities.includes(capability)); - if (!bundle) { - throw new Error(`No bundle found for capability: ${capability}`); - } - return bundle; -} - export function getDomain(topology: LoadedTopology, domainId: string): DomainPolicy { const domain = topology.domains.find((entry) => entry.id === domainId); if (!domain) { @@ -24,6 +16,41 @@ export function getDomain(topology: LoadedTopology, domainId: string): DomainPol return domain; } +export function getBundleForTool(topology: LoadedTopology, toolName: string): BundlePolicy | null { + const matches = topology.bundles + .filter((bundle) => bundle.toolPrefixes.some((prefix) => toolName.startsWith(prefix))) + .sort((left, right) => { + const leftLen = Math.max(...left.toolPrefixes.filter((p) => toolName.startsWith(p)).map((p) => p.length)); + const rightLen = Math.max(...right.toolPrefixes.filter((p) => toolName.startsWith(p)).map((p) => p.length)); + return rightLen - leftLen; + }); + return matches[0] ?? null; +} + +export function assertToolAllowedForAgent( + topology: LoadedTopology, + agent: AgentPolicy, + toolName: string, +): BundlePolicy { + const bundle = getBundleForTool(topology, toolName); + if (!bundle) { + throw new Error( + `Tool ${toolName} is not mapped to any bundle (no matching tool_prefixes in topology/bundles/*).`, + ); + } + if (agent.forbiddenBundles.includes(bundle.id)) { + throw new Error( + `Agent ${agent.id} is forbidden from bundle ${bundle.id} (tool ${toolName}).`, + ); + } + if (!agent.allowedBundles.includes(bundle.id)) { + throw new Error( + `Agent ${agent.id} is not allowed to call tools in bundle ${bundle.id} (tool ${toolName}). Allowed bundles: ${agent.allowedBundles.join(", ") || "(none)"}.`, + ); + } + return bundle; +} + export function getStrictestProofMode(order: string[], proofModes: string[]): string { const ranked = proofModes .map((mode) => ({ mode, index: order.indexOf(mode) })) diff --git a/src/engine/resolver.ts b/src/engine/resolver.ts deleted file mode 100644 index 6d0789a..0000000 --- a/src/engine/resolver.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { LoadedTopology } from "./contracts.js"; -import { getAgent, getBundleByCapability, getDomain } from "./policy.js"; - -export function resolveCapabilityContext( - topology: LoadedTopology, - input: { agentId: string; capability: string }, -) { - const agent = getAgent(topology, input.agentId); - const bundle = getBundleByCapability(topology, input.capability); - const domain = getDomain(topology, bundle.domain); - - if (!agent.allowedBundles.includes(bundle.id)) { - throw new Error(`Agent ${agent.id} attempted forbidden bundle ${bundle.id}`); - } - - if (agent.forbiddenBundles.includes(bundle.id) || domain.forbiddenBundles.includes(bundle.id)) { - throw new Error(`Agent ${agent.id} attempted forbidden bundle ${bundle.id}`); - } - - return { agent, bundle, domain }; -} diff --git a/src/engine/skill-enforcer.ts b/src/engine/skill-enforcer.ts deleted file mode 100644 index 876fc2b..0000000 --- a/src/engine/skill-enforcer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { AgentPolicy } from "./contracts.js"; - -export function assertSkillAllowedForAgent(agent: AgentPolicy, skillName: string): void { - if (!agent.allowedSkills.includes(skillName)) { - throw new Error(`Skill ${skillName} is not allowed for agent ${agent.id}`); - } -} diff --git a/src/engine/tool-bridge.ts b/src/engine/tool-bridge.ts index cf5919f..15e13e1 100644 --- a/src/engine/tool-bridge.ts +++ b/src/engine/tool-bridge.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../shared/tool-types.js"; type ToolResult = { content?: Array<{ type?: string; text?: string }>; @@ -9,7 +9,7 @@ type ToolResult = { type ToolHandler = (args: Record) => Promise | ToolResult; export async function invokeRegisteredTool( - register: (server: McpServer) => void, + register: (server: ToolServer) => void, args: Record, ): Promise { let capturedHandler: ToolHandler | undefined; @@ -18,7 +18,7 @@ export async function invokeRegisteredTool( tool(_name: string, _description: string, _schema: unknown, handler: ToolHandler) { capturedHandler = handler; }, - } as unknown as McpServer; + } as unknown as ToolServer; register(server); diff --git a/src/index.ts b/src/index.ts deleted file mode 100755 index 8082695..0000000 --- a/src/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env node - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { loadPlugins } from "./registry.js"; -import { reactflowPlugin } from "./plugins/reactflow/index.js"; -import { motionPlugin } from "./plugins/motion/index.js"; -import { lenisPlugin } from "./plugins/lenis/index.js"; -import { reactPlugin } from "./plugins/react/index.js"; -import { echoPlugin } from "./plugins/echo/index.js"; -import { golangPlugin } from "./plugins/golang/index.js"; -import { rustPlugin } from "./plugins/rust/index.js"; -import { designTokensPlugin } from "./plugins/design-tokens/index.js"; -import { uiUxPlugin } from "./plugins/ui-ux/index.js"; -import { designerPlugin } from "./plugins/designer/index.js"; -import { shadcnPlugin } from "./plugins/shadcn/index.js"; -import { hyperstackPlugin } from "./plugins/hyperstack/index.js"; - -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const pkgPath = join(__dirname, "..", "package.json"); -const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - -const server = new McpServer({ - name: "hyperstack", - version: pkg.version, -}); - -export const allPlugins = [ - reactflowPlugin, - motionPlugin, - lenisPlugin, - reactPlugin, - echoPlugin, - golangPlugin, - rustPlugin, - designTokensPlugin, - uiUxPlugin, - designerPlugin, - shadcnPlugin, - hyperstackPlugin, -]; - -loadPlugins(server, allPlugins); - -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - - const shutdown = () => process.exit(0); - process.stdin.on("close", shutdown); - process.stdin.on("end", shutdown); - process.on("SIGTERM", shutdown); - process.on("SIGINT", shutdown); -} - -main().catch((err) => { - console.error("Failed to start MCP server:", err); - process.exit(1); -}); diff --git a/src/internal/setup-hyperstack.ts b/src/internal/setup-hyperstack.ts deleted file mode 100644 index 8a28290..0000000 --- a/src/internal/setup-hyperstack.ts +++ /dev/null @@ -1,338 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as os from "node:os"; -import { execSync } from "node:child_process"; - -export interface SetupResult { - detectedPlatform: string; - configPath: string | null; - skillPath: string | null; - status: "success" | "pending_research" | "error"; - proposedPatch?: any; - message: string; -} - -export type PlatformFormat = "json-mcpServers" | "json-contextServers" | "toml-mcp_servers" | "json-mcpServers-nested"; - -const KNOWN_PLATFORMS: Record = { - // โ”€โ”€โ”€ AI CLIs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - // Claude Code (CLI) - global user config - // Source: docs.anthropic.com, inventivehq.com (April 2025, multiple) - "claude-code": { - env: ["CLAUDE_PLUGIN_ROOT"], - configFiles: [".claude.json"], - skillPath: ".claude/skills", - format: "json-mcpServers", - }, - // Gemini CLI - global user config - // Source: geminicli.com, augmentcode.com (April 2025, multiple) - "gemini-cli": { - configFiles: [".gemini/settings.json"], - format: "json-mcpServers", - notes: "Run '/mcp' inside Gemini CLI to verify connection", - }, - // Antigravity (Google DeepMind) - local user config - "antigravity": { - configFiles: [".gemini/antigravity/mcp_config.json"], - skillPath: ".gemini/antigravity/skills", - format: "json-mcpServers", - }, - // Qwen Code (Alibaba) - global user config - // Source: github.io/qwen-code official docs (April 2025) - "qwen-code": { - configFiles: [".qwen/settings.json"], - skillPath: ".qwen/skills", - format: "json-mcpServers", - notes: "CLI alternative: qwen mcp add ", - }, - // OpenAI Codex CLI - global user config (TOML format) - // Source: openai.com official docs (April 2025) - "codex": { - configFiles: [".codex/config.toml"], - format: "toml-mcp_servers", - notes: "CLI alternative: codex mcp add hyperstack -- bun ~/.hyperstack/bin/hyperstack.mjs", - }, - - // โ”€โ”€โ”€ AI IDEs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - // Cursor IDE - global user config - // Source: cursor.com docs, confirmed via 10+ tutorials (April 2025) - "cursor": { - env: ["CURSOR_PLUGIN_ROOT"], - configFiles: [".cursor/mcp.json"], - skillPath: ".cursor/rules", - format: "json-mcpServers", - notes: "Project-level: .cursor/mcp.json in project root", - }, - // Windsurf IDE (Codeium) - global user config - // Source: windsurf.com official docs, bito.ai, zapier (April 2025) - "windsurf": { - configFiles: [".codeium/windsurf/mcp_config.json"], - format: "json-mcpServers", - notes: "Click 'Refresh' in Cascade panel after editing", - }, - // Kiro (Amazon AI IDE) - global user config - // Source: kiro.dev official docs, aws.com (April 2025) - "kiro": { - configFiles: [".kiro/settings/mcp.json"], - format: "json-mcpServers", - notes: "Workspace-level: .kiro/settings/mcp.json in project root", - }, - // Zed editor - global user config (uses 'context_servers' key, not 'mcpServers') - // Source: zed.dev official docs, skeet.build (April 2025) - "zed": { - configFiles: [".config/zed/settings.json"], - format: "json-contextServers", - notes: "Uses 'context_servers' key instead of 'mcpServers'", - }, - - // โ”€โ”€โ”€ VS Code Extensions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - // VS Code + GitHub Copilot - platform-specific global config - // Source: code.visualstudio.com official docs (April 2025) - "vscode": { - env: ["VSCODE_PID"], - configFiles: [ - ".config/Code/User/mcp.json", // Linux - "Library/Application Support/Code/User/mcp.json", // macOS - ], - format: "json-mcpServers", - notes: "Project-level: .vscode/mcp.json in project root", - }, - // Roo Code (VS Code extension) - // Global config has no fixed home path (stored in VS Code extension storage). - // Source: roocode.com official docs (April 2025) - "roo-code": { - configFiles: [".roo/mcp.json"], - skillPath: ".roo/rules", - format: "json-mcpServers", - notes: "Global config: open via Roo Code UI > Edit Global MCP", - }, - // Cline (VS Code extension) - Linux global path - // Source: cline.bot official docs, reddit (April 2025) - "cline": { - configFiles: [ - ".config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", // Linux - ".cline/data/settings/cline_mcp_settings.json", // Cline CLI fallback - ], - format: "json-mcpServers", - notes: "Access via Cline panel > MCP Servers > Configure", - }, - // Continue.dev (VS Code extension) - project-level only - // Source: continue.dev official docs (April 2025) - "continue-dev": { - configFiles: [".continue/mcpServers/mcp.json"], - format: "json-mcpServers", - notes: "Requires Agent Mode. Place config in .continue/mcpServers/ directory", - }, -}; - - -export function detectEnvironment(): string { - // Env-var hints (fast path, but unreliable in nested terminals) - if (process.env.ANTIGRAVITY_AGENT) return "antigravity"; - if (process.env.CLAUDE_PLUGIN_ROOT) return "claude-code"; - if (process.env.CURSOR_PLUGIN_ROOT) return "cursor"; - // VSCODE_PID fires inside any VS Code-hosted terminal (Gemini, Roo Code, etc.) - // Do NOT short-circuit here - fall through to config-file probing below. - return "unknown"; -} - -export function findConfigFile(platform: string): string | null { - const home = os.homedir(); - - // Always probe all known config files - this is the authoritative source of truth. - // Even if `platform` was resolved from env vars, the config file must exist. - const candidates: Array<{ platform: string; file: string }> = []; - - if (platform !== "unknown" && KNOWN_PLATFORMS[platform]) { - // Try hinted platform first - for (const file of KNOWN_PLATFORMS[platform].configFiles) { - candidates.push({ platform, file }); - } - } - - // Always append all other platforms as fallback probes - for (const [p, info] of Object.entries(KNOWN_PLATFORMS)) { - if (p !== platform) { - for (const file of info.configFiles) { - candidates.push({ platform: p, file }); - } - } - } - - for (const { file } of candidates) { - const fullPath = path.join(home, file); - if (fs.existsSync(fullPath)) return fullPath; - } - - return null; -} - -export function detectPlatformFromConfigPath(configPath: string): string { - for (const [p, info] of Object.entries(KNOWN_PLATFORMS)) { - for (const file of info.configFiles) { - if (configPath.endsWith(file)) return p; - } - } - return "unknown"; -} - -export function findSkillPath(platform: string): string | null { - const info = KNOWN_PLATFORMS[platform]; - if (!info || !info.skillPath) return null; - - return path.join(os.homedir(), info.skillPath); -} - - -export function getPlatformFormat(platform: string): PlatformFormat { - return KNOWN_PLATFORMS[platform]?.format ?? "json-mcpServers"; -} - -export function generateMcpPatch( - configPath: string, - pluginRoot: string, - platform: string, - method: "docker" | "local" = "local" -): { format: PlatformFormat; content: string | object } { - const binaryPath = path.join(pluginRoot, "bin", "hyperstack.mjs"); - const localServerConfig = { - command: "node", - args: [binaryPath], - env: { HYPERSTACK_ROOT: pluginRoot }, - }; - const dockerServerConfig = { - command: "docker", - args: ["exec", "-i", "hyperstack-mcp", "bun", "/app/src/index.ts"], - env: { HYPERSTACK_ROOT: pluginRoot }, - }; - const serverConfig = method === "docker" ? dockerServerConfig : localServerConfig; - const format = getPlatformFormat(platform); - - // Zed uses 'context_servers' key instead of 'mcpServers' - // Source: zed.dev official docs (April 2025) - if (format === "json-contextServers") { - return { - format, - content: { - context_servers: { - hyperstack: serverConfig, - }, - }, - }; - } - - // Codex CLI uses TOML format: [mcp_servers.] - // Source: openai.com official docs (April 2025) - if (format === "toml-mcp_servers") { - const tomlBlock = method === "docker" - ? `[mcp_servers.hyperstack]\ncommand = "docker"\nargs = ["exec", "-i", "hyperstack-mcp", "bun", "/app/src/index.ts"]\n[mcp_servers.hyperstack.env]\nHYPERSTACK_ROOT = "${pluginRoot}"` - : `[mcp_servers.hyperstack]\ncommand = "node"\nargs = ["${binaryPath}"]\n[mcp_servers.hyperstack.env]\nHYPERSTACK_ROOT = "${pluginRoot}"`; - return { format, content: tomlBlock }; - } - - // Default: standard mcpServers JSON schema - // Covers: Claude Code, Cursor, Windsurf, Roo Code, VS Code, Kiro, Qwen, Gemini, Cline, Continue.dev - return { - format, - content: { - mcpServers: { - hyperstack: serverConfig, - }, - }, - }; -} - -/** - * Persistently applies the generated patch to the user's config file - * without destroying existing settings. - */ -export function applyMcpPatch(configPath: string, patch: { format: PlatformFormat; content: any }) { - if (!fs.existsSync(configPath)) { - // If it doesn't exist, create it with the patch - if (typeof patch.content === "string") { - fs.writeFileSync(configPath, patch.content); - } else { - fs.writeFileSync(configPath, JSON.stringify(patch.content, null, 2)); - } - return; - } - - if (patch.format === "toml-mcp_servers") { - let content = fs.readFileSync(configPath, "utf8"); - if (!content.includes("[mcp_servers.hyperstack]")) { - fs.appendFileSync(configPath, "\n" + patch.content); - console.log(`โœ… Appended Hyperstack block to ${configPath}`); - } else { - console.log(`โ„น๏ธ Hyperstack block already exists in ${configPath}`); - } - return; - } - - // JSON deep-merge - try { - const existing = JSON.parse(fs.readFileSync(configPath, "utf8")); - const patchObj = patch.content as any; - - if (patch.format === "json-contextServers") { - existing.context_servers = { - ...(existing.context_servers || {}), - ...patchObj.context_servers, - }; - } else { - existing.mcpServers = { - ...(existing.mcpServers || {}), - ...patchObj.mcpServers, - }; - } - - fs.writeFileSync(configPath, JSON.stringify(existing, null, 2)); - console.log(`โœ… Deep-merged Hyperstack config into ${configPath}`); - } catch (err) { - console.error(`โŒ Failed to merge config: ${err}`); - } -} - -export function selfHealDocker() { - try { - // Check if Docker is available - execSync("docker --version", { stdio: "ignore" }); - - console.log("\n๐Ÿ›ก๏ธ Running Docker Self-Healing Protocol..."); - - // Check for existing containers - try { - // Find containers with our image or the exact name - const cmd = 'docker ps -aq --filter "ancestor=ghcr.io/orkait/hyperstack:main"'; - const existing = execSync(cmd).toString().trim().split(/\r?\n/).filter(Boolean); - - const namedCmd = 'docker ps -aq --filter "name=hyperstack-mcp"'; - const named = execSync(namedCmd).toString().trim().split(/\r?\n/).filter(Boolean); - - const allToPurge = [...new Set([...existing, ...named])]; - - if (allToPurge.length > 0) { - console.log(`๐Ÿงน Found ${allToPurge.length} old container(s) and fragments. Purging immediately...`); - execSync(`docker rm -f ${allToPurge.join(' ')}`, { stdio: "ignore" }); - } - } catch(e) {} - - console.log("๐Ÿ“ฅ Pulling the absolute latest ghcr.io/orkait/hyperstack:main..."); - execSync("docker pull ghcr.io/orkait/hyperstack:main", { stdio: "inherit" }); - - console.log("๐Ÿฅ Booting clean persistent container (hyperstack-mcp)..."); - execSync("docker run -d --name hyperstack-mcp --restart unless-stopped --memory=512m --cpus=1 --entrypoint sleep ghcr.io/orkait/hyperstack:main infinity", { stdio: "ignore" }); - - console.log("โœ… Registry & Engine synchronized successfully."); - } catch (err) { - console.log("\\nโš ๏ธ Docker skipped: Docker engine not responsive or not installed on this host."); - } -} diff --git a/src/plugins/design-tokens/index.ts b/src/plugins/design-tokens/index.ts deleted file mode 100644 index b9f7c71..0000000 --- a/src/plugins/design-tokens/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listCategories } from "./tools/list-categories.js"; -import { register as getCategory } from "./tools/get-category.js"; -import { register as getColorRamp } from "./tools/get-color-ramp.js"; -import { register as getProcedure } from "./tools/get-procedure.js"; -import { register as search } from "./tools/search.js"; -import { register as getGotchas } from "./tools/get-gotchas.js"; -import { register as generate } from "./tools/generate.js"; - -function register(server: McpServer): void { - listCategories(server); - getCategory(server); - getColorRamp(server); - getProcedure(server); - search(server); - getGotchas(server); - generate(server); -} - -export const designTokensPlugin: Plugin = { - name: "design-tokens", - register, -}; diff --git a/src/plugins/design-tokens/tools/generate.ts b/src/plugins/design-tokens/tools/generate.ts index 58a86d9..3500050 100644 --- a/src/plugins/design-tokens/tools/generate.ts +++ b/src/plugins/design-tokens/tools/generate.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { snippet } from "../loader.js"; @@ -7,7 +7,7 @@ const TEMPLATE_SPACING = snippet("templates/spacing.txt"); const TEMPLATE_TYPOGRAPHY = snippet("templates/typography.txt"); const TEMPLATE_MOTION = snippet("templates/motion.txt"); -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "design_tokens_generate", "Generate CSS token scaffolding from a description. Returns ready-to-use CSS custom properties.", diff --git a/src/plugins/design-tokens/tools/get-category.ts b/src/plugins/design-tokens/tools/get-category.ts index 6e07b58..9a227c5 100644 --- a/src/plugins/design-tokens/tools/get-category.ts +++ b/src/plugins/design-tokens/tools/get-category.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { TOKEN_CATEGORIES, getCategoryByName } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "design_tokens_get_category", "Get full details for a design token category including CSS examples, rules, and gotchas", diff --git a/src/plugins/design-tokens/tools/get-color-ramp.ts b/src/plugins/design-tokens/tools/get-color-ramp.ts index 9bdc709..85b0643 100644 --- a/src/plugins/design-tokens/tools/get-color-ramp.ts +++ b/src/plugins/design-tokens/tools/get-color-ramp.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { COLOR_RAMPS, getRampByName } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "design_tokens_get_color_ramp", "Get a color ramp (brand/neutral/pop) with all 11 OKLCH stops, semantic roles, and light/dark mode usage", diff --git a/src/plugins/design-tokens/tools/get-gotchas.ts b/src/plugins/design-tokens/tools/get-gotchas.ts index 8257bfe..d5dba5e 100644 --- a/src/plugins/design-tokens/tools/get-gotchas.ts +++ b/src/plugins/design-tokens/tools/get-gotchas.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { getAllGotchas } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "design_tokens_get_gotchas", "List all common design token mistakes and fixes across all categories", diff --git a/src/plugins/design-tokens/tools/get-procedure.ts b/src/plugins/design-tokens/tools/get-procedure.ts index 69468eb..fdec648 100644 --- a/src/plugins/design-tokens/tools/get-procedure.ts +++ b/src/plugins/design-tokens/tools/get-procedure.ts @@ -1,118 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { TOKEN_PROCEDURES, getProcedureByStep } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "frontend.design-tokens"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - procedures?: Record; -}; - -type CorpusProcedureEntry = { - step?: number; - title?: string; - description?: string; - code?: string; - rules?: string[]; - gotchas?: string[]; -}; - -type LoadedCorpusProcedure = { - step: number; - title: string; - description: string; - code: string; - rules: string[]; - gotchas: string[]; - source: string; -}; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.design-tokens"; - -let cachedCorpusProcedures: Map | null | undefined; - -function loadCorpusProcedures(): Map | null { - if (cachedCorpusProcedures !== undefined) { - return cachedCorpusProcedures; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusProcedures = null; - return cachedCorpusProcedures; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const procedureEntries = namespaceIndex?.procedures ?? {}; - const loaded = new Map(); - - for (const [key, meta] of Object.entries(procedureEntries)) { - const match = key.match(/^step-(\d+)$/i); - const step = match ? Number(match[1]) : Number.NaN; - const file = meta?.file; - if (!Number.isInteger(step) || !file) { - continue; - } - - const raw = readFileSync(join(corpusRoot, "frontend/design-tokens", file), "utf8"); - const entry = YAML.parse(raw) as CorpusProcedureEntry | null; - if ( - !entry || - entry.step !== step || - !entry.title || - !entry.description || - !entry.code || - !Array.isArray(entry.rules) || - !Array.isArray(entry.gotchas) - ) { - continue; - } - - loaded.set(step, { - step, - title: entry.title, - description: entry.description, - code: entry.code, - rules: entry.rules, - gotchas: entry.gotchas, - source: corpusNamespace, - }); - } - - cachedCorpusProcedures = loaded.size > 0 ? loaded : null; - return cachedCorpusProcedures; - } catch { - cachedCorpusProcedures = null; - return cachedCorpusProcedures; - } -} - -function getProcedure(step: number) { - const corpusEntry = loadCorpusProcedures()?.get(step); - if (corpusEntry) { - return corpusEntry; - } - - return getProcedureByStep(step); -} - function renderProcedure(proc: { step: number; title: string; @@ -120,7 +9,6 @@ function renderProcedure(proc: { code?: string; rules?: string[]; gotchas?: string[]; - source?: string; }): string { let text = `# Step ${proc.step}: ${proc.title}\n\n${proc.description}\n\n`; @@ -138,14 +26,10 @@ function renderProcedure(proc: { for (const gotcha of proc.gotchas) text += `- ${gotcha}\n`; } - if (proc.source) { - text += `\n**Corpus Source:** ${proc.source}`; - } - return text; } -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "design_tokens_get_procedure", "Get the step-by-step token system build procedure. Steps 1-8 cover the full production workflow.", @@ -153,9 +37,9 @@ export function register(server: McpServer): void { step: z.number().min(1).max(8).optional() .describe("Step number (1-8). Omit to get all steps."), }, - async ({ step }) => { + async ({ step }: { step?: number }) => { if (step !== undefined) { - const proc = getProcedure(step); + const proc = getProcedureByStep(step); if (!proc) { return { content: [{ type: "text", text: `Step ${step} not found. Valid steps: 1-${TOKEN_PROCEDURES.length}` }], @@ -168,14 +52,10 @@ export function register(server: McpServer): void { let text = "# Design Token Build Procedure\n\n"; text += "A complete token system = 10 categories. Follow in order.\n\n"; for (const proc of TOKEN_PROCEDURES) { - const corpusProc = loadCorpusProcedures()?.get(proc.step); - text += `## Step ${proc.step}: ${corpusProc?.title ?? proc.title}\n${corpusProc?.description ?? proc.description}\n\n`; - } - if (loadCorpusProcedures()) { - text += `**Corpus Source:** ${corpusNamespace}`; + text += `## Step ${proc.step}: ${proc.title}\n${proc.description}\n\n`; } text += "\nCall `design_tokens_get_procedure` with a step number for full code + rules."; return { content: [{ type: "text", text }] }; - } + }, ); } diff --git a/src/plugins/design-tokens/tools/list-categories.ts b/src/plugins/design-tokens/tools/list-categories.ts index 324ebbb..a818f93 100644 --- a/src/plugins/design-tokens/tools/list-categories.ts +++ b/src/plugins/design-tokens/tools/list-categories.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { TOKEN_CATEGORIES } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "design_tokens_list_categories", "List all 10 design token categories with descriptions and architecture layer", diff --git a/src/plugins/design-tokens/tools/search.ts b/src/plugins/design-tokens/tools/search.ts index 3959e94..aee413f 100644 --- a/src/plugins/design-tokens/tools/search.ts +++ b/src/plugins/design-tokens/tools/search.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { searchTokens } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "design_tokens_search", "Search design token documentation by keyword across categories, ramps, and procedures", diff --git a/src/plugins/designer/index.ts b/src/plugins/designer/index.ts deleted file mode 100644 index 4c24d70..0000000 --- a/src/plugins/designer/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listPersonalities } from "./tools/list-personalities.js"; -import { register as getPersonality } from "./tools/get-personality.js"; -import { register as getIndustryRules } from "./tools/get-industry-rules.js"; -import { register as getCognitiveLaw } from "./tools/get-cognitive-law.js"; -import { register as getDesignSystem } from "./tools/get-design-system.js"; -import { register as getCompositionRules } from "./tools/get-composition-rules.js"; -import { register as getInteractionPattern } from "./tools/get-interaction-pattern.js"; -import { register as getUxWriting } from "./tools/get-ux-writing.js"; -import { register as getLandingPattern } from "./tools/get-landing-pattern.js"; -import { register as getAntiPatterns } from "./tools/get-anti-patterns.js"; -import { register as resolveIntent } from "./tools/resolve-intent.js"; -import { register as search } from "./tools/search.js"; -import { register as generateDesignBrief } from "./tools/generate-design-brief.js"; -import { register as getPageTemplate } from "./tools/get-page-template.js"; -import { register as getPreset } from "./tools/get-preset.js"; -import { register as listPresets } from "./tools/list-presets.js"; -import { register as getFontPairing } from "./tools/get-font-pairing.js"; -import { register as generateImplementationPlan } from "./tools/generate-implementation-plan.js"; -import { register as verifyImplementation } from "./tools/verify-implementation.js"; - -function register(server: McpServer): void { - listPersonalities(server); - getPersonality(server); - getIndustryRules(server); - getCognitiveLaw(server); - getDesignSystem(server); - getCompositionRules(server); - getInteractionPattern(server); - getUxWriting(server); - getLandingPattern(server); - getAntiPatterns(server); - resolveIntent(server); - search(server); - generateDesignBrief(server); - getPageTemplate(server); - getPreset(server); - listPresets(server); - getFontPairing(server); - generateImplementationPlan(server); - verifyImplementation(server); -} - -export const designerPlugin: Plugin = { - name: "designer", - register, -}; diff --git a/src/plugins/designer/tools/generate-design-brief.ts b/src/plugins/designer/tools/generate-design-brief.ts index 6aed85d..908711b 100644 --- a/src/plugins/designer/tools/generate-design-brief.ts +++ b/src/plugins/designer/tools/generate-design-brief.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PERSONALITY_CLUSTERS, @@ -27,7 +27,7 @@ const PERSONALITY_TO_SYSTEM: Record = { "enterprise-trust": "ibm-carbon", }; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_generate_design_brief", "Generate a complete design brief for a product: resolves intent, then assembles visual theme, industry rules, style, cognitive laws, anti-patterns, design system inspiration, and composition rules", diff --git a/src/plugins/designer/tools/generate-implementation-plan.ts b/src/plugins/designer/tools/generate-implementation-plan.ts index e48cbd5..8c37a92 100644 --- a/src/plugins/designer/tools/generate-implementation-plan.ts +++ b/src/plugins/designer/tools/generate-implementation-plan.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { readFileSync, existsSync } from "fs"; import { isAbsolute } from "path"; @@ -8,7 +8,7 @@ import { isAbsolute } from "path"; * an implementation plan with exact MCP calls per section. This is the bridge * that forge-plan uses to turn a DESIGN.md contract into executable tasks. */ -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_generate_implementation_plan", "Parse a DESIGN.md file into its 10 sections and generate a structured implementation plan. Each section becomes one or more tasks with exact MCP calls (shadcn_get_component, motion_generate_animation, design_tokens_generate, etc.) and self-review assertions. This is the bridge from designer to forge-plan.", diff --git a/src/plugins/designer/tools/get-anti-patterns.ts b/src/plugins/designer/tools/get-anti-patterns.ts index 91560fc..2833124 100644 --- a/src/plugins/designer/tools/get-anti-patterns.ts +++ b/src/plugins/designer/tools/get-anti-patterns.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ANTI_PATTERN_CATEGORIES, INDUSTRY_CATEGORIES, getAntiPatterns } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_anti_patterns", "Get design anti-patterns filtered by category and/or industry", diff --git a/src/plugins/designer/tools/get-cognitive-law.ts b/src/plugins/designer/tools/get-cognitive-law.ts index 6a68d95..fcf59ef 100644 --- a/src/plugins/designer/tools/get-cognitive-law.ts +++ b/src/plugins/designer/tools/get-cognitive-law.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { COGNITIVE_LAW_NAMES, getCognitiveLaw } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_cognitive_law", "Get a cognitive law: formula, key insight, UI applications, common violations, and academic source", diff --git a/src/plugins/designer/tools/get-composition-rules.ts b/src/plugins/designer/tools/get-composition-rules.ts index 5335be5..9fb13df 100644 --- a/src/plugins/designer/tools/get-composition-rules.ts +++ b/src/plugins/designer/tools/get-composition-rules.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { COMPOSITION_TOPICS, getCompositionRule } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_composition_rules", "Get visual composition rules: key rule, detail, applications, and violations", diff --git a/src/plugins/designer/tools/get-design-system.ts b/src/plugins/designer/tools/get-design-system.ts index 57a49f9..d54048e 100644 --- a/src/plugins/designer/tools/get-design-system.ts +++ b/src/plugins/designer/tools/get-design-system.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { DESIGN_SYSTEM_NAMES, getDesignSystem, CROSS_SYSTEM_CONVERGENCES } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_design_system", "Get design system reference: signature, key insights, typography, color, spacing, reference code, and cross-system convergences", diff --git a/src/plugins/designer/tools/get-font-pairing.ts b/src/plugins/designer/tools/get-font-pairing.ts index 3ca2fb8..2d3889f 100644 --- a/src/plugins/designer/tools/get-font-pairing.ts +++ b/src/plugins/designer/tools/get-font-pairing.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { FONT_MOODS, INDUSTRY_CATEGORIES, FONT_PAIRINGS, getFontPairings } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_font_pairing", "Get curated font pairings filtered by mood (technical, elegant, friendly, editorial, bold, corporate, playful, luxury, startup, minimal) and/or industry. Returns heading + body + mono fonts with weights, tracking, line-height, Google Fonts import, and rationale. 21 pairings available.", diff --git a/src/plugins/designer/tools/get-industry-rules.ts b/src/plugins/designer/tools/get-industry-rules.ts index 3780dba..7c93b0b 100644 --- a/src/plugins/designer/tools/get-industry-rules.ts +++ b/src/plugins/designer/tools/get-industry-rules.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { INDUSTRY_CATEGORIES, getIndustryRule } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_industry_rules", "Get design rules for an industry: primary/secondary style, must-have features, never-use patterns, color mood, emotional target", diff --git a/src/plugins/designer/tools/get-interaction-pattern.ts b/src/plugins/designer/tools/get-interaction-pattern.ts index e8d8be9..2f9cc63 100644 --- a/src/plugins/designer/tools/get-interaction-pattern.ts +++ b/src/plugins/designer/tools/get-interaction-pattern.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { INTERACTION_CATEGORIES, getInteractionPattern } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_interaction_pattern", "Get interaction pattern: key rule, detail, best practices, and anti-patterns", diff --git a/src/plugins/designer/tools/get-landing-pattern.ts b/src/plugins/designer/tools/get-landing-pattern.ts index 16ec2fb..0955228 100644 --- a/src/plugins/designer/tools/get-landing-pattern.ts +++ b/src/plugins/designer/tools/get-landing-pattern.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { LANDING_TOPICS, getLandingPattern } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_landing_pattern", "Get landing page pattern: key stats, best practices, and anti-patterns", diff --git a/src/plugins/designer/tools/get-page-template.ts b/src/plugins/designer/tools/get-page-template.ts index bb6b9a0..d943ce0 100644 --- a/src/plugins/designer/tools/get-page-template.ts +++ b/src/plugins/designer/tools/get-page-template.ts @@ -1,126 +1,16 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PAGE_TYPES, getPageTemplate } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "frontend.designer"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - pageTemplates?: { - dashboard?: { - file?: string; - }; - }; -}; - -type CorpusPageSection = { - name?: string; - required?: boolean; - components?: string[]; - notes?: string; -}; - -type CorpusPageTemplate = { - type?: string; - description?: string; - layout?: string; - sections?: CorpusPageSection[]; - cognitiveApply?: string[]; - compositionApply?: string[]; - interactionApply?: string[]; - writingApply?: string[]; - keyRules?: string[]; -}; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.designer"; - -let cachedCorpusDashboard: { template: CorpusPageTemplate; source: string } | null | undefined; - -function loadCorpusDashboardTemplate(): { template: CorpusPageTemplate; source: string } | null { - if (cachedCorpusDashboard !== undefined) { - return cachedCorpusDashboard; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusDashboard = null; - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const pageTemplatePath = namespaceIndex?.pageTemplates?.dashboard?.file; - if (!pageTemplatePath) { - cachedCorpusDashboard = null; - return null; - } - - const templateRaw = readFileSync(join(corpusRoot, "frontend/designer", pageTemplatePath), "utf8"); - const template = YAML.parse(templateRaw) as CorpusPageTemplate | null; - const sections = template?.sections; - const cognitiveApply = template?.cognitiveApply; - const compositionApply = template?.compositionApply; - const interactionApply = template?.interactionApply; - const writingApply = template?.writingApply; - const keyRules = template?.keyRules; - if ( - !template || - template.type?.toLowerCase() !== "dashboard" || - !template.description || - !template.layout || - !Array.isArray(sections) || - sections.length === 0 || - !Array.isArray(cognitiveApply) || - !Array.isArray(compositionApply) || - !Array.isArray(interactionApply) || - !Array.isArray(writingApply) || - !Array.isArray(keyRules) - ) { - cachedCorpusDashboard = null; - return null; - } - - cachedCorpusDashboard = { template, source: corpusNamespace }; - return cachedCorpusDashboard; - } catch { - cachedCorpusDashboard = null; - return null; - } -} - -function getCorpusAwarePageTemplate(type: (typeof PAGE_TYPES)[number]) { - if (type === "dashboard") { - return loadCorpusDashboardTemplate()?.template ?? getPageTemplate(type); - } - - return getPageTemplate(type); -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_page_template", "Get section anatomy, component inventory, and applicable cognitive laws for a page type. Covers landing, dashboard, auth, settings, checkout, blog, docs, admin, profile, error-page, ai-chat, pricing, and onboarding.", { type: z.enum(PAGE_TYPES).describe("Page type to get template for"), }, - async ({ type }) => { - const corpusEntry = type === "dashboard" ? loadCorpusDashboardTemplate() : null; - const template = getCorpusAwarePageTemplate(type); + async ({ type }: { type: (typeof PAGE_TYPES)[number] }) => { + const template = getPageTemplate(type); if (!template) { return { content: [{ type: "text" as const, text: `Page type "${type}" not found. Available: ${PAGE_TYPES.join(", ")}` }], @@ -153,10 +43,6 @@ export function register(server: McpServer): void { text += `- ${rule}\n`; } - if (corpusEntry) { - text += `\n**Corpus Source:** ${corpusEntry.source}`; - } - return { content: [{ type: "text" as const, text }] }; }, ); diff --git a/src/plugins/designer/tools/get-personality.ts b/src/plugins/designer/tools/get-personality.ts index 9ed3f04..748a742 100644 --- a/src/plugins/designer/tools/get-personality.ts +++ b/src/plugins/designer/tools/get-personality.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PERSONALITY_CLUSTERS, getPersonality } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_personality", "Get full personality profile: description, exemplars, visual vocabulary, mode, and CSS example", diff --git a/src/plugins/designer/tools/get-preset.ts b/src/plugins/designer/tools/get-preset.ts index 15a6d5e..14804f6 100644 --- a/src/plugins/designer/tools/get-preset.ts +++ b/src/plugins/designer/tools/get-preset.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PRESET_NAMES, PRESETS, getPreset } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_preset", "Get a complete, code-ready design token preset based on a real premium design system. Returns colors (OKLCH), typography (font, scale, weights, tracking), spacing, radius, shadows, motion, and CSS example. Available presets: linear, stripe, vercel, apple, carbon, shadcn, notion, supabase, figma.", diff --git a/src/plugins/designer/tools/get-ux-writing.ts b/src/plugins/designer/tools/get-ux-writing.ts index e17e586..b550fa5 100644 --- a/src/plugins/designer/tools/get-ux-writing.ts +++ b/src/plugins/designer/tools/get-ux-writing.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { WRITING_TOPICS, getWritingGuideline } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_get_ux_writing", "Get UX writing guideline: key rule, evidence, do/don't examples", diff --git a/src/plugins/designer/tools/list-personalities.ts b/src/plugins/designer/tools/list-personalities.ts index 8d4a1e1..3bbf42e 100644 --- a/src/plugins/designer/tools/list-personalities.ts +++ b/src/plugins/designer/tools/list-personalities.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { PERSONALITIES } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_list_personalities", "List all 6 designer personality clusters with descriptions and exemplar names", diff --git a/src/plugins/designer/tools/list-presets.ts b/src/plugins/designer/tools/list-presets.ts index 82f05df..5103be6 100644 --- a/src/plugins/designer/tools/list-presets.ts +++ b/src/plugins/designer/tools/list-presets.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { PRESETS } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_list_presets", "List all available design presets - complete, code-ready design token configurations based on real premium design systems (Linear, Stripe, Vercel, Apple, Carbon, shadcn, Notion, Supabase, Figma).", diff --git a/src/plugins/designer/tools/resolve-intent.ts b/src/plugins/designer/tools/resolve-intent.ts index a5f519d..87eea71 100644 --- a/src/plugins/designer/tools/resolve-intent.ts +++ b/src/plugins/designer/tools/resolve-intent.ts @@ -1,11 +1,11 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { resolveFullIntent } from "../data.js"; const USER_TYPES = ["developer", "consumer", "enterprise", "child", "creative", "healthcare"] as const; const EMOTIONAL_TARGETS = ["trustworthy", "playful", "premium", "energetic", "calm", "technical", "bold", "editorial"] as const; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_resolve_intent", "Resolve a product description into a full design intent: industry, personality, style, mode, density, color mood, must-have/never-use lists", diff --git a/src/plugins/designer/tools/search.ts b/src/plugins/designer/tools/search.ts index 9830494..484b3a1 100644 --- a/src/plugins/designer/tools/search.ts +++ b/src/plugins/designer/tools/search.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { searchDesigner } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_search", "Search across all designer knowledge: personalities, styles, industries, cognitive laws, design systems, composition, interactions, writing, landing, anti-patterns, and master principles", diff --git a/src/plugins/designer/tools/verify-implementation.ts b/src/plugins/designer/tools/verify-implementation.ts index b9349a8..b3c5d29 100644 --- a/src/plugins/designer/tools/verify-implementation.ts +++ b/src/plugins/designer/tools/verify-implementation.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { readFileSync, existsSync, statSync } from "fs"; import { isAbsolute } from "path"; @@ -7,7 +7,7 @@ import { isAbsolute } from "path"; * Programmatically verifies an implementation against its DESIGN.md contract. * This runs during ship-gate as the hard compliance gate. */ -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "designer_verify_implementation", "Verify implementation code against its DESIGN.md contract. Runs pattern checks for anti-patterns (AI purple, font-weight 500 everywhere, cold shadows, etc.), verifies OKLCH tokens present, checks for prefers-reduced-motion, and reports per-section compliance. Use this before ship-gate. Returns a pass/fail report with specific violations.", diff --git a/src/plugins/echo/index.ts b/src/plugins/echo/index.ts deleted file mode 100644 index bad41ae..0000000 --- a/src/plugins/echo/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listRecipes } from "./tools/list-recipes.js"; -import { register as getRecipe } from "./tools/get-recipe.js"; -import { register as listMiddleware } from "./tools/list-middleware.js"; -import { register as getMiddleware } from "./tools/get-middleware.js"; -import { register as searchDocs } from "./tools/search-docs.js"; -import { register as decisionMatrix } from "./tools/decision-matrix.js"; - -function register(server: McpServer): void { - listRecipes(server); - getRecipe(server); - listMiddleware(server); - getMiddleware(server); - searchDocs(server); - decisionMatrix(server); -} - -export const echoPlugin: Plugin = { name: "echo", register }; diff --git a/src/plugins/echo/tools/decision-matrix.ts b/src/plugins/echo/tools/decision-matrix.ts index 0630aac..142a7c5 100644 --- a/src/plugins/echo/tools/decision-matrix.ts +++ b/src/plugins/echo/tools/decision-matrix.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { DECISION_MATRIX } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "echo_decision_matrix", "Given a need or requirement, get the recommended Echo pattern and recipe to use.", diff --git a/src/plugins/echo/tools/get-middleware.ts b/src/plugins/echo/tools/get-middleware.ts index 590b04b..ee517a9 100644 --- a/src/plugins/echo/tools/get-middleware.ts +++ b/src/plugins/echo/tools/get-middleware.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { MIDDLEWARE, getMiddlewareByName, searchMiddleware, formatMiddleware } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "echo_get_middleware", "Get detailed config and usage for a specific Echo middleware.", diff --git a/src/plugins/echo/tools/get-recipe.ts b/src/plugins/echo/tools/get-recipe.ts index 0ca4a83..91cd370 100644 --- a/src/plugins/echo/tools/get-recipe.ts +++ b/src/plugins/echo/tools/get-recipe.ts @@ -1,113 +1,16 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -import { RECIPES, getRecipeByName, searchRecipes, formatRecipe, type Recipe } from "../data.js"; +import { RECIPES, getRecipeByName, searchRecipes, formatRecipe } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "backend.echo"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - recipes?: Record; -}; - -type CorpusRecipeEntry = { - name?: string; - category?: string; - description?: string; - when?: string; - code?: string; - gotchas?: string[]; - relatedRecipes?: string[]; -}; - -type LoadedCorpusRecipe = { recipe: Recipe; source: string }; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "backend.echo"; - -const cachedCorpusRecipes = new Map(); - -function normalize(name: string): string { - return name.toLowerCase().trim(); -} - -function loadCorpusRecipe(name: string): LoadedCorpusRecipe | null { - const key = normalize(name); - if (cachedCorpusRecipes.has(key)) { - return cachedCorpusRecipes.get(key) ?? null; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusRecipes.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const recipePath = namespaceIndex?.recipes?.[key]?.file; - if (!recipePath) { - cachedCorpusRecipes.set(key, null); - return null; - } - - const raw = readFileSync(join(corpusRoot, "backend/echo", recipePath), "utf8"); - const entry = YAML.parse(raw) as CorpusRecipeEntry | null; - if ( - !entry || - normalize(entry.name ?? "") !== key || - !entry.category || - !entry.description || - !entry.when || - !entry.code - ) { - cachedCorpusRecipes.set(key, null); - return null; - } - - const loaded: LoadedCorpusRecipe = { - recipe: { - name: entry.name ?? name, - category: entry.category as Recipe["category"], - description: entry.description, - when: entry.when, - code: entry.code, - gotchas: entry.gotchas, - relatedRecipes: entry.relatedRecipes, - }, - source: corpusNamespace, - }; - cachedCorpusRecipes.set(key, loaded); - return loaded; - } catch { - cachedCorpusRecipes.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "echo_get_recipe", "Get a specific Echo framework recipe with full working Go code, gotchas, and related recipes.", { name: z.string().describe("Recipe name (e.g., 'crud-api', 'websocket', 'sse', 'jwt-auth', 'graceful-shutdown')"), }, - async ({ name }) => { - const corpusEntry = loadCorpusRecipe(name); - const recipe = corpusEntry?.recipe ?? getRecipeByName(name); + async ({ name }: { name: string }) => { + const recipe = getRecipeByName(name); if (!recipe) { const suggestions = searchRecipes(name).map((r) => r.name); const allNames = RECIPES.map((r) => r.name).join(", "); @@ -121,15 +24,7 @@ export function register(server: McpServer): void { isError: true, }; } - const formatted = formatRecipe(recipe); - return { - content: [ - { - type: "text", - text: corpusEntry ? `${formatted}\n**Corpus Source:** ${corpusEntry.source}` : formatted, - }, - ], - }; - } + return { content: [{ type: "text", text: formatRecipe(recipe) }] }; + }, ); } diff --git a/src/plugins/echo/tools/list-middleware.ts b/src/plugins/echo/tools/list-middleware.ts index 0c0c61d..eb98b9c 100644 --- a/src/plugins/echo/tools/list-middleware.ts +++ b/src/plugins/echo/tools/list-middleware.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { MIDDLEWARE } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "echo_list_middleware", "List all available Echo framework middleware with their purpose and recommended chain order.", diff --git a/src/plugins/echo/tools/list-recipes.ts b/src/plugins/echo/tools/list-recipes.ts index 1682d58..ba6f5c7 100644 --- a/src/plugins/echo/tools/list-recipes.ts +++ b/src/plugins/echo/tools/list-recipes.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { RECIPES, RECIPE_CATEGORIES } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "echo_list_recipes", "List all Go Echo framework recipes. Optionally filter by category.", diff --git a/src/plugins/echo/tools/search-docs.ts b/src/plugins/echo/tools/search-docs.ts index abce20d..b3c59fd 100644 --- a/src/plugins/echo/tools/search-docs.ts +++ b/src/plugins/echo/tools/search-docs.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { searchRecipes, searchMiddleware, RECIPE_CATEGORIES } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "echo_search_docs", "Search Echo framework recipes and middleware by keyword.", diff --git a/src/plugins/golang/index.ts b/src/plugins/golang/index.ts deleted file mode 100644 index 2dafd4e..0000000 --- a/src/plugins/golang/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listPractices } from "./tools/list-practices.js"; -import { register as getPractice } from "./tools/get-practice.js"; -import { register as listPatterns } from "./tools/list-patterns.js"; -import { register as getPattern } from "./tools/get-pattern.js"; -import { register as getAntipatterns } from "./tools/get-antipatterns.js"; -import { register as searchDocs } from "./tools/search-docs.js"; - -function register(server: McpServer): void { - listPractices(server); - getPractice(server); - listPatterns(server); - getPattern(server); - getAntipatterns(server); - searchDocs(server); -} - -export const golangPlugin: Plugin = { - name: "golang", - register, -}; diff --git a/src/plugins/golang/tools/get-antipatterns.ts b/src/plugins/golang/tools/get-antipatterns.ts index f8f8af6..b616bfe 100644 --- a/src/plugins/golang/tools/get-antipatterns.ts +++ b/src/plugins/golang/tools/get-antipatterns.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { ANTI_PATTERNS } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "golang_get_antipatterns", "List all Go anti-patterns to avoid", diff --git a/src/plugins/golang/tools/get-pattern.ts b/src/plugins/golang/tools/get-pattern.ts index e17dd28..59f6bc4 100644 --- a/src/plugins/golang/tools/get-pattern.ts +++ b/src/plugins/golang/tools/get-pattern.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { DESIGN_PATTERNS } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "golang_get_pattern", "Get a Go design pattern with idiomatic code", diff --git a/src/plugins/golang/tools/get-practice.ts b/src/plugins/golang/tools/get-practice.ts index 2c1b352..ca96d9f 100644 --- a/src/plugins/golang/tools/get-practice.ts +++ b/src/plugins/golang/tools/get-practice.ts @@ -1,115 +1,16 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -import { BEST_PRACTICES, type BestPractice } from "../data.js"; +import { BEST_PRACTICES } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "backend.golang"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - practices?: Record; -}; - -type CorpusPracticeEntry = { - name?: string; - topic?: string; - priority?: string; - rule?: string; - reason?: string; - good?: string; - bad?: string; -}; - -type LoadedCorpusPractice = { practice: BestPractice; source: string }; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "backend.golang"; - -const cachedCorpusPractices = new Map(); - -function normalize(name: string): string { - return name.toLowerCase().trim(); -} - -function loadCorpusPractice(name: string): LoadedCorpusPractice | null { - const key = normalize(name); - if (cachedCorpusPractices.has(key)) { - return cachedCorpusPractices.get(key) ?? null; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusPractices.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const practicePath = namespaceIndex?.practices?.[key]?.file; - if (!practicePath) { - cachedCorpusPractices.set(key, null); - return null; - } - - const raw = readFileSync(join(corpusRoot, "backend/golang", practicePath), "utf8"); - const entry = YAML.parse(raw) as CorpusPracticeEntry | null; - if ( - !entry || - normalize(entry.name ?? "") !== key || - !entry.topic || - (entry.priority !== "P0" && entry.priority !== "P1") || - !entry.rule || - !entry.reason - ) { - cachedCorpusPractices.set(key, null); - return null; - } - - const loaded: LoadedCorpusPractice = { - practice: { - name: entry.name ?? name, - topic: entry.topic as BestPractice["topic"], - priority: entry.priority, - rule: entry.rule, - reason: entry.reason, - good: entry.good, - bad: entry.bad, - }, - source: corpusNamespace, - }; - cachedCorpusPractices.set(key, loaded); - return loaded; - } catch { - cachedCorpusPractices.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "golang_get_practice", "Get a Go best practice with good/bad code examples", { name: z.string().describe("Practice name (e.g. 'error-wrapping', 'goroutine-lifecycle', 'crypto-rand', 'table-driven-tests', 'thin-handlers')"), }, - async ({ name }) => { - const corpusEntry = loadCorpusPractice(name); - const practice = - corpusEntry?.practice ?? - BEST_PRACTICES.find((p) => p.name.toLowerCase() === name.toLowerCase()); + async ({ name }: { name: string }) => { + const practice = BEST_PRACTICES.find((p) => p.name.toLowerCase() === name.toLowerCase()); if (!practice) { return { content: [{ type: "text", text: `Practice "${name}" not found.\n\nAvailable: ${BEST_PRACTICES.map((p) => p.name).join(", ")}` }], @@ -122,12 +23,7 @@ export function register(server: McpServer): void { text += `**Why:** ${practice.reason}\n\n`; if (practice.good) text += `## โœ… Good\n\`\`\`go\n${practice.good}\n\`\`\`\n\n`; if (practice.bad) text += `## โŒ Bad\n\`\`\`go\n${practice.bad}\n\`\`\`\n`; - - if (corpusEntry) { - text += `\n**Corpus Source:** ${corpusEntry.source}`; - } - return { content: [{ type: "text", text }] }; - } + }, ); } diff --git a/src/plugins/golang/tools/list-patterns.ts b/src/plugins/golang/tools/list-patterns.ts index d7b1b72..da038ec 100644 --- a/src/plugins/golang/tools/list-patterns.ts +++ b/src/plugins/golang/tools/list-patterns.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { DESIGN_PATTERNS, getPatternsByCategory } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "golang_list_patterns", "List Go design patterns by category (creational, structural, behavioral, concurrency)", diff --git a/src/plugins/golang/tools/list-practices.ts b/src/plugins/golang/tools/list-practices.ts index 8c74903..c792142 100644 --- a/src/plugins/golang/tools/list-practices.ts +++ b/src/plugins/golang/tools/list-practices.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { BEST_PRACTICES, TOPICS, getPracticesByTopic } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "golang_list_practices", "List Go best practices by topic and priority (P0=critical, P1=standard)", diff --git a/src/plugins/golang/tools/search-docs.ts b/src/plugins/golang/tools/search-docs.ts index bf2f28b..95b4f7d 100644 --- a/src/plugins/golang/tools/search-docs.ts +++ b/src/plugins/golang/tools/search-docs.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { searchAll } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "golang_search_docs", "Search Go best practices and design patterns by keyword", diff --git a/src/plugins/hyperstack/index.ts b/src/plugins/hyperstack/index.ts deleted file mode 100644 index 7904f0a..0000000 --- a/src/plugins/hyperstack/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Plugin } from "../../registry.js"; -import { registerSetupTool } from "./tools/setup.js"; - -export const hyperstackPlugin: Plugin = { - name: "hyperstack", - register: (server: McpServer) => { - registerSetupTool(server); - }, -}; diff --git a/src/plugins/hyperstack/tools/setup.ts b/src/plugins/hyperstack/tools/setup.ts deleted file mode 100644 index 7499bf0..0000000 --- a/src/plugins/hyperstack/tools/setup.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import * as setup from "../../../internal/setup-hyperstack.js"; -import * as path from "node:path"; - -export function registerSetupTool(server: McpServer) { - server.tool( - "hyperstack_setup", - "Identify current IDE/CLI environment and generate a tailored MCP configuration patch for Hyperstack.", - { - researchResult: z.string().optional().describe("If the environment was unknown, provide the researched config path or schema details here."), - method: z.enum(["docker", "local"]).default("docker").describe("Preferred installation method. Use 'docker' (default) for stable persistent environments, 'local' for fallback."), - }, - async ({ researchResult, method }) => { - const platform = setup.detectEnvironment(); - const configPath = setup.findConfigFile(platform); - - const pluginRoot = process.env.HYPERSTACK_ROOT || process.cwd(); - - if (!configPath && !researchResult) { - return { - content: [{ - type: "text", - text: `Detection failed for environment: ${platform}.\n\nPlease use 'web_search' to find where ${platform} stores its MCP configuration (e.g. 'Cursor MCP config path' or 'Windsurf MCP config location').\n\nOnce found, provide the path as 'researchResult'.` - }] - }; - } - - const activeConfigPath = configPath || (researchResult ? path.resolve(researchResult) : null); - - if (!activeConfigPath) { - return { - isError: true, - content: [{ - type: "text", - text: "Could not resolve a valid configuration path." - }] - }; - } - - const patch = setup.generateMcpPatch(activeConfigPath, pluginRoot, method); - - return { - content: [{ - type: "text", - text: `Environment Identified: ${platform}\nTarget Config: ${activeConfigPath}\n\nProposed Patch (JSON):\n${JSON.stringify(patch, null, 2)}\n\nTo apply this, the agent should read the file, merge the 'hyperstack' server entry, and write it back.` - }] - }; - } - ); -} diff --git a/src/plugins/lenis/index.ts b/src/plugins/lenis/index.ts deleted file mode 100644 index e28b539..0000000 --- a/src/plugins/lenis/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listApis } from "./tools/list-apis.js"; -import { register as getApi } from "./tools/get-api.js"; -import { register as searchDocs } from "./tools/search-docs.js"; -import { register as getPattern } from "./tools/get-pattern.js"; -import { register as generateSetup } from "./tools/generate-setup.js"; -import { register as cheatsheet } from "./tools/cheatsheet.js"; - -function register(server: McpServer): void { - listApis(server); - getApi(server); - searchDocs(server); - getPattern(server); - generateSetup(server); - cheatsheet(server); -} - -export const lenisPlugin: Plugin = { - name: "lenis", - register, -}; diff --git a/src/plugins/lenis/tools/cheatsheet.ts b/src/plugins/lenis/tools/cheatsheet.ts index 967eaed..218873a 100644 --- a/src/plugins/lenis/tools/cheatsheet.ts +++ b/src/plugins/lenis/tools/cheatsheet.ts @@ -1,6 +1,6 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.resource( "lenis-cheatsheet", "lenis://react/cheatsheet", diff --git a/src/plugins/lenis/tools/generate-setup.ts b/src/plugins/lenis/tools/generate-setup.ts index 3a84ce9..6d93cad 100644 --- a/src/plugins/lenis/tools/generate-setup.ts +++ b/src/plugins/lenis/tools/generate-setup.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "lenis_generate_setup", "Generate complete Lenis setup code from a natural-language description. Handles Next.js, GSAP, Framer Motion, basic React, and custom container scenarios.", diff --git a/src/plugins/lenis/tools/get-api.ts b/src/plugins/lenis/tools/get-api.ts index 42afecb..a02d908 100644 --- a/src/plugins/lenis/tools/get-api.ts +++ b/src/plugins/lenis/tools/get-api.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ALL_APIS, searchApis, getApiByName, formatApiReference } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "lenis_get_api", "Get detailed API reference for a specific Lenis API - props, options, usage examples, and tips.", diff --git a/src/plugins/lenis/tools/get-pattern.ts b/src/plugins/lenis/tools/get-pattern.ts index e01dd34..afbd1c5 100644 --- a/src/plugins/lenis/tools/get-pattern.ts +++ b/src/plugins/lenis/tools/get-pattern.ts @@ -1,77 +1,10 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PATTERNS } from "../data.js"; const PATTERN_NAMES = Object.keys(PATTERNS) as [string, ...string[]]; -type CorpusIndex = { - namespaces?: { - "frontend.lenis"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - patterns?: { - [name: string]: { - file?: string; - }; - }; -}; - -type CorpusPatternEntry = { - name?: string; - description?: string; - code?: string; - tips?: string[]; -}; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.lenis"; - -let cachedCorpusPattern: { name: string; pattern: CorpusPatternEntry; source: string } | undefined; - -function loadCorpusPattern(name: string): { name: string; pattern: CorpusPatternEntry; source: string } | null { - if (cachedCorpusPattern?.name === name) { - return cachedCorpusPattern; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const patternPath = namespaceIndex?.patterns?.[name]?.file; - if (!patternPath) { - return null; - } - - const patternRaw = readFileSync(join(corpusRoot, "frontend/lenis", patternPath), "utf8"); - const pattern = YAML.parse(patternRaw) as CorpusPatternEntry | null; - if (!pattern || pattern.name?.toLowerCase() !== name.toLowerCase() || !pattern.description || !pattern.code) { - return null; - } - - cachedCorpusPattern = { name, pattern, source: corpusNamespace }; - return cachedCorpusPattern; - } catch { - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "lenis_get_pattern", "Get a complete Lenis integration pattern with full production-ready code. Covers Next.js setup, GSAP integration, Framer Motion sync, custom containers, accessibility, and navigation.", @@ -82,9 +15,8 @@ export function register(server: McpServer): void { "Pattern name: full-page | next-js | gsap-integration | framer-motion-integration | custom-container | accessibility | scroll-to-nav", ), }, - async ({ name }) => { - const corpusEntry = loadCorpusPattern(name); - const pattern = corpusEntry?.pattern ?? PATTERNS[name]; + async ({ name }: { name: string }) => { + const pattern = PATTERNS[name]; if (!pattern) { return { content: [ @@ -107,11 +39,6 @@ export function register(server: McpServer): void { text += `## Key Notes\n\n`; for (const tip of pattern.tips) text += `- ${tip}\n`; } - - if (corpusEntry) { - text += `\n**Corpus Source:** ${corpusEntry.source}`; - } - return { content: [{ type: "text", text }] }; }, ); diff --git a/src/plugins/lenis/tools/list-apis.ts b/src/plugins/lenis/tools/list-apis.ts index 511afe6..6293a67 100644 --- a/src/plugins/lenis/tools/list-apis.ts +++ b/src/plugins/lenis/tools/list-apis.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ALL_APIS, API_KINDS, capitalize } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "lenis_list_apis", "List all Lenis smooth scroll APIs - ReactLenis component, useLenis hook, LenisRef and LenisOptions types.", diff --git a/src/plugins/lenis/tools/search-docs.ts b/src/plugins/lenis/tools/search-docs.ts index 018e20f..50745c1 100644 --- a/src/plugins/lenis/tools/search-docs.ts +++ b/src/plugins/lenis/tools/search-docs.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { searchApis, PATTERNS } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "lenis_search_docs", "Search Lenis documentation by keyword. Searches API names, descriptions, code examples, and integration patterns.", diff --git a/src/plugins/motion/index.ts b/src/plugins/motion/index.ts deleted file mode 100644 index a29a56f..0000000 --- a/src/plugins/motion/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listApis } from "./tools/list-apis.js"; -import { register as getApi } from "./tools/get-api.js"; -import { register as searchDocs } from "./tools/search-docs.js"; -import { register as getExamples } from "./tools/get-examples.js"; -import { register as getTransitions } from "./tools/get-transitions.js"; -import { register as generateAnimation } from "./tools/generate-animation.js"; -import { register as cheatsheet } from "./tools/cheatsheet.js"; - -function register(server: McpServer): void { - listApis(server); - getApi(server); - searchDocs(server); - getExamples(server); - getTransitions(server); - generateAnimation(server); - cheatsheet(server); -} - -export const motionPlugin: Plugin = { - name: "motion", - register, -}; diff --git a/src/plugins/motion/tools/cheatsheet.ts b/src/plugins/motion/tools/cheatsheet.ts index 8fcdde0..de868f0 100644 --- a/src/plugins/motion/tools/cheatsheet.ts +++ b/src/plugins/motion/tools/cheatsheet.ts @@ -1,6 +1,6 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.resource( "cheatsheet", "motion://react/cheatsheet", diff --git a/src/plugins/motion/tools/generate-animation.ts b/src/plugins/motion/tools/generate-animation.ts index 79acdb5..c0043dd 100644 --- a/src/plugins/motion/tools/generate-animation.ts +++ b/src/plugins/motion/tools/generate-animation.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "motion_generate_animation", "Generate a Motion for React animation snippet from a natural-language description. Returns ready-to-use JSX.", diff --git a/src/plugins/motion/tools/get-api.ts b/src/plugins/motion/tools/get-api.ts index 3999369..c6d3e63 100644 --- a/src/plugins/motion/tools/get-api.ts +++ b/src/plugins/motion/tools/get-api.ts @@ -1,105 +1,16 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ALL_APIS, searchApis, getApiByName, formatApiReference } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "frontend.motion"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - apis?: Record; -}; - -type CorpusApiEntry = { - name?: string; - kind?: string; - description?: string; - importPath?: string; - props?: Array<{ name: string; type: string; description: string; default?: string }>; - returns?: string; - usage?: string; - examples?: Array<{ title: string; code: string; description?: string; category: string }>; - tips?: string[]; - relatedApis?: string[]; -}; - -type LoadedCorpusApi = { api: CorpusApiEntry; source: string }; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.motion"; - -const cachedCorpusApis = new Map(); - -function normalizeApiName(name: string): string { - return name.toLowerCase().replace(/[<>/()]/g, "").trim(); -} - -function loadCorpusApi(name: string): LoadedCorpusApi | null { - const key = normalizeApiName(name); - if (cachedCorpusApis.has(key)) { - return cachedCorpusApis.get(key) ?? null; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusApis.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const apiPath = namespaceIndex?.apis?.[key]?.file; - if (!apiPath) { - cachedCorpusApis.set(key, null); - return null; - } - - const apiRaw = readFileSync(join(corpusRoot, "frontend/motion", apiPath), "utf8"); - const api = YAML.parse(apiRaw) as CorpusApiEntry | null; - if ( - !api || - normalizeApiName(api.name ?? "") !== key || - !api.usage || - !api.importPath || - !api.description || - !api.kind - ) { - cachedCorpusApis.set(key, null); - return null; - } - - const loaded: LoadedCorpusApi = { api, source: corpusNamespace }; - cachedCorpusApis.set(key, loaded); - return loaded; - } catch { - cachedCorpusApis.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "motion_get_api", "Get detailed API reference for a specific Motion for React component, hook, or utility. Includes props, usage, examples, and tips.", { name: z.string().describe("API name (e.g., 'motion', 'AnimatePresence', 'useAnimate', 'useScroll', 'stagger', 'Reorder.Group')"), }, - async ({ name }) => { - const corpusEntry = loadCorpusApi(name); - const api = corpusEntry?.api ?? getApiByName(name); + async ({ name }: { name: string }) => { + const api = getApiByName(name); if (!api) { const suggestions = searchApis(name).map((r) => r.api.name); return { @@ -107,15 +18,7 @@ export function register(server: McpServer): void { isError: true, }; } - const rendered = formatApiReference(api as Parameters[0]); - return { - content: [ - { - type: "text", - text: corpusEntry ? `${rendered}\n**Corpus Source:** ${corpusEntry.source}` : rendered, - }, - ], - }; + return { content: [{ type: "text", text: formatApiReference(api) }] }; }, ); } diff --git a/src/plugins/motion/tools/get-examples.ts b/src/plugins/motion/tools/get-examples.ts index 34fe8c5..d307e80 100644 --- a/src/plugins/motion/tools/get-examples.ts +++ b/src/plugins/motion/tools/get-examples.ts @@ -1,114 +1,19 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { CATEGORIES, getExamplesByCategory, formatExample, capitalize } from "../data.js"; -import type { Example } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "frontend.motion"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - examples?: Record; -}; - -type CorpusExampleFile = { - category?: string; - examples?: Array<{ - title?: string; - description?: string; - code?: string; - category?: string; - }>; -}; - -type ParsedCorpusExample = { - title: string; - description?: string; - code: string; - category?: string; -}; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.motion"; - -let cachedCorpusExamples: Map | undefined; - -function loadCorpusExamples(category: string): { examples: Example[]; source: string } | null { - const key = category.toLowerCase().trim(); - cachedCorpusExamples ??= new Map(); - const cached = cachedCorpusExamples.get(key); - if (cached !== undefined) { - return cached; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusExamples.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const examplePath = namespaceIndex?.examples?.[key]?.file; - if (!examplePath) { - cachedCorpusExamples.set(key, null); - return null; - } - - const fileRaw = readFileSync(join(corpusRoot, "frontend/motion", examplePath), "utf8"); - const file = YAML.parse(fileRaw) as CorpusExampleFile | null; - const examples = (file?.examples ?? []) - .filter((ex): ex is ParsedCorpusExample => Boolean(ex?.title && ex?.code)) - .map((ex) => ({ - title: ex.title, - code: ex.code, - description: ex.description, - category: (ex.category ?? key) as Example["category"], - })); - - if (examples.length === 0) { - cachedCorpusExamples.set(key, null); - return null; - } - - const result = { examples, source: corpusNamespace }; - cachedCorpusExamples.set(key, result); - return result; - } catch { - cachedCorpusExamples.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "motion_get_examples", "Get code examples for a specific animation category", { category: z.string().describe(`Category: ${CATEGORIES.join(", ")}`) }, - async ({ category }) => { - const corpusEntry = loadCorpusExamples(category); - const examples = corpusEntry?.examples ?? getExamplesByCategory(category); + async ({ category }: { category: string }) => { + const examples = getExamplesByCategory(category); if (examples.length === 0) { return { content: [{ type: "text", text: `No examples for category "${category}". Available: ${CATEGORIES.join(", ")}` }] }; } let text = `# ${capitalize(category)} Examples\n\n`; for (const ex of examples) text += formatExample(ex, 2); - if (corpusEntry) { - text += `**Corpus Source:** ${corpusEntry.source}\n`; - } return { content: [{ type: "text", text }] }; }, ); diff --git a/src/plugins/motion/tools/get-transitions.ts b/src/plugins/motion/tools/get-transitions.ts index 93e7631..8f7c1ae 100644 --- a/src/plugins/motion/tools/get-transitions.ts +++ b/src/plugins/motion/tools/get-transitions.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { TRANSITIONS_REFERENCE } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "motion_get_transitions", "Get the complete transition types reference (tween, spring, inertia, orchestration, per-value config)", diff --git a/src/plugins/motion/tools/list-apis.ts b/src/plugins/motion/tools/list-apis.ts index 3ec6d49..30be9fd 100644 --- a/src/plugins/motion/tools/list-apis.ts +++ b/src/plugins/motion/tools/list-apis.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ALL_APIS, API_KINDS, capitalize } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "motion_list_apis", "List all available Motion for React APIs (components, hooks, utilities)", diff --git a/src/plugins/motion/tools/search-docs.ts b/src/plugins/motion/tools/search-docs.ts index 3a0d928..b5dd6b2 100644 --- a/src/plugins/motion/tools/search-docs.ts +++ b/src/plugins/motion/tools/search-docs.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { CATEGORIES, searchApis, formatExample } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "motion_search_docs", "Search Motion for React documentation by keyword. Searches API names, descriptions, and code examples.", diff --git a/src/plugins/react/index.ts b/src/plugins/react/index.ts deleted file mode 100644 index 2c2e1d3..0000000 --- a/src/plugins/react/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listPatterns } from "./tools/list-patterns.js"; -import { register as getPattern } from "./tools/get-pattern.js"; -import { register as getConstraints } from "./tools/get-constraints.js"; -import { register as searchDocs } from "./tools/search-docs.js"; - -function register(server: McpServer): void { - listPatterns(server); - getPattern(server); - getConstraints(server); - searchDocs(server); -} - -export const reactPlugin: Plugin = { - name: "react", - register, -}; diff --git a/src/plugins/react/tools/get-constraints.ts b/src/plugins/react/tools/get-constraints.ts index 102683e..484b1f2 100644 --- a/src/plugins/react/tools/get-constraints.ts +++ b/src/plugins/react/tools/get-constraints.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { CONSTRAINTS } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "react_get_constraints", "List all forbidden React/Next.js patterns and their reasons", diff --git a/src/plugins/react/tools/get-pattern.ts b/src/plugins/react/tools/get-pattern.ts index a953bad..d4d4bb2 100644 --- a/src/plugins/react/tools/get-pattern.ts +++ b/src/plugins/react/tools/get-pattern.ts @@ -1,113 +1,16 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -import { PATTERNS, getPatternByName, type Pattern } from "../data.js"; +import { PATTERNS, getPatternByName } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "frontend.react"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - patterns?: Record; -}; - -type CorpusPatternEntry = { - name?: string; - category?: string; - description?: string; - when?: string; - code?: string; - antiPattern?: string; - tips?: string[]; -}; - -type LoadedCorpusPattern = { pattern: Pattern; source: string }; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.react"; - -const cachedCorpusPatterns = new Map(); - -function normalize(name: string): string { - return name.toLowerCase().trim(); -} - -function loadCorpusPattern(name: string): LoadedCorpusPattern | null { - const key = normalize(name); - if (cachedCorpusPatterns.has(key)) { - return cachedCorpusPatterns.get(key) ?? null; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusPatterns.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const patternPath = namespaceIndex?.patterns?.[key]?.file; - if (!patternPath) { - cachedCorpusPatterns.set(key, null); - return null; - } - - const raw = readFileSync(join(corpusRoot, "frontend/react", patternPath), "utf8"); - const entry = YAML.parse(raw) as CorpusPatternEntry | null; - if ( - !entry || - normalize(entry.name ?? "") !== key || - !entry.category || - !entry.description || - !entry.when || - !entry.code - ) { - cachedCorpusPatterns.set(key, null); - return null; - } - - const loaded: LoadedCorpusPattern = { - pattern: { - name: entry.name ?? name, - category: entry.category as Pattern["category"], - description: entry.description, - when: entry.when, - code: entry.code, - antiPattern: entry.antiPattern, - tips: entry.tips, - }, - source: corpusNamespace, - }; - cachedCorpusPatterns.set(key, loaded); - return loaded; - } catch { - cachedCorpusPatterns.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "react_get_pattern", "Get a React/Next.js pattern with full code example and anti-pattern", { name: z.string().describe("Pattern name (e.g. 'rsc-default', 'state-hierarchy', 'zustand-store', 'suspense-boundary', 'nextjs-metadata', 'composition-pattern', 'component-template')"), }, - async ({ name }) => { - const corpusEntry = loadCorpusPattern(name); - const pattern = corpusEntry?.pattern ?? getPatternByName(name); + async ({ name }: { name: string }) => { + const pattern = getPatternByName(name); if (!pattern) { const available = PATTERNS.map((p) => p.name).join(", "); return { @@ -129,12 +32,7 @@ export function register(server: McpServer): void { text += `## Tips\n`; for (const tip of pattern.tips) text += `- ${tip}\n`; } - - if (corpusEntry) { - text += `\n**Corpus Source:** ${corpusEntry.source}`; - } - return { content: [{ type: "text", text }] }; - } + }, ); } diff --git a/src/plugins/react/tools/list-patterns.ts b/src/plugins/react/tools/list-patterns.ts index 53abfca..c0a30d6 100644 --- a/src/plugins/react/tools/list-patterns.ts +++ b/src/plugins/react/tools/list-patterns.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PATTERNS, PATTERN_CATEGORIES, getPatternsByCategory } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "react_list_patterns", "List all React/Next.js patterns by category", diff --git a/src/plugins/react/tools/search-docs.ts b/src/plugins/react/tools/search-docs.ts index e13ab78..41c953f 100644 --- a/src/plugins/react/tools/search-docs.ts +++ b/src/plugins/react/tools/search-docs.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { searchPatterns, CONSTRAINTS } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "react_search_docs", "Search React/Next.js patterns and constraints by keyword", diff --git a/src/plugins/reactflow/index.ts b/src/plugins/reactflow/index.ts deleted file mode 100644 index 9586952..0000000 --- a/src/plugins/reactflow/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listApis } from "./tools/list-apis.js"; -import { register as getApi } from "./tools/get-api.js"; -import { register as searchDocs } from "./tools/search-docs.js"; -import { register as getExamples } from "./tools/get-examples.js"; -import { register as getPattern } from "./tools/get-pattern.js"; -import { register as getTemplate } from "./tools/get-template.js"; -import { register as getMigrationGuide } from "./tools/get-migration-guide.js"; -import { register as generateFlow } from "./tools/generate-flow.js"; -import { register as cheatsheet } from "./tools/cheatsheet.js"; - -function register(server: McpServer): void { - listApis(server); - getApi(server); - searchDocs(server); - getExamples(server); - getPattern(server); - getTemplate(server); - getMigrationGuide(server); - generateFlow(server); - cheatsheet(server); -} - -export const reactflowPlugin: Plugin = { - name: "reactflow", - register, -}; diff --git a/src/plugins/reactflow/tools/cheatsheet.ts b/src/plugins/reactflow/tools/cheatsheet.ts index 0838d02..1466221 100644 --- a/src/plugins/reactflow/tools/cheatsheet.ts +++ b/src/plugins/reactflow/tools/cheatsheet.ts @@ -1,6 +1,6 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.resource( "cheatsheet", "reactflow://cheatsheet", diff --git a/src/plugins/reactflow/tools/generate-flow.ts b/src/plugins/reactflow/tools/generate-flow.ts index 4369071..d730df8 100644 --- a/src/plugins/reactflow/tools/generate-flow.ts +++ b/src/plugins/reactflow/tools/generate-flow.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "reactflow_generate_flow", "Generate a React Flow component from a natural-language description. Returns ready-to-use TSX with proper imports.", diff --git a/src/plugins/reactflow/tools/get-api.ts b/src/plugins/reactflow/tools/get-api.ts index 8c99119..fe89963 100644 --- a/src/plugins/reactflow/tools/get-api.ts +++ b/src/plugins/reactflow/tools/get-api.ts @@ -1,124 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ALL_APIS, searchApis, getApiByName, formatApiReference } from "../data/index.js"; -type CorpusIndex = { - namespaces?: { - "frontend.reactflow"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - apis?: Record; -}; - -type CorpusApiEntry = { - name?: string; - kind?: string; - description?: string; - importPath?: string; - props?: Array<{ name: string; type: string; description: string; default?: string }>; - returns?: string; - usage?: string; - examples?: Array<{ title?: string; code?: string; description?: string; category?: string }>; - tips?: string[]; - relatedApis?: string[]; -}; - -type LoadedCorpusApi = { - api: Parameters[0]; - source: string; -}; - -type ReactFlowExample = Parameters[0]["examples"][number]; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.reactflow"; - -const cachedCorpusApis = new Map(); - -function normalizeApiName(name: string): string { - return name.toLowerCase().replace(/[<>/()]/g, "").trim(); -} - -function loadCorpusApi(name: string): LoadedCorpusApi | null { - const key = normalizeApiName(name); - if (cachedCorpusApis.has(key)) { - return cachedCorpusApis.get(key) ?? null; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusApis.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const apiPath = namespaceIndex?.apis?.[key]?.file; - if (!apiPath) { - cachedCorpusApis.set(key, null); - return null; - } - - const apiRaw = readFileSync(join(corpusRoot, "frontend/reactflow", apiPath), "utf8"); - const api = YAML.parse(apiRaw) as CorpusApiEntry | null; - if ( - !api || - normalizeApiName(api.name ?? "") !== key || - !api.usage || - !api.importPath || - !api.description || - !api.kind - ) { - cachedCorpusApis.set(key, null); - return null; - } - - const loaded: LoadedCorpusApi = { - api: { - name: api.name ?? name, - kind: api.kind as Parameters[0]["kind"], - description: api.description, - importPath: api.importPath, - props: api.props, - returns: api.returns, - usage: api.usage, - examples: (api.examples ?? []) - .filter((ex): ex is { title: string; code: string; description?: string; category?: ReactFlowExample["category"] } => - Boolean(ex?.title && ex?.code), - ) - .map((ex) => ({ - title: ex.title, - code: ex.code, - description: ex.description, - category: ex.category ?? "quickstart", - })), - tips: api.tips ?? [], - relatedApis: api.relatedApis ?? [], - }, - source: corpusNamespace, - }; - cachedCorpusApis.set(key, loaded); - return loaded; - } catch { - cachedCorpusApis.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "reactflow_get_api", "Get detailed API reference for a specific React Flow component, hook, utility, or type. Includes props, usage, examples, and tips.", @@ -129,9 +13,8 @@ export function register(server: McpServer): void { "API name (e.g., 'ReactFlow', 'useReactFlow', 'Handle', 'addEdge', 'Node', 'Edge', 'NodeProps')", ), }, - async ({ name }) => { - const corpusEntry = loadCorpusApi(name); - const api = corpusEntry?.api ?? getApiByName(name, ALL_APIS); + async ({ name }: { name: string }) => { + const api = getApiByName(name, ALL_APIS); if (!api) { const suggestions = searchApis(name, ALL_APIS) .slice(0, 5) @@ -146,15 +29,7 @@ export function register(server: McpServer): void { isError: true, }; } - const rendered = formatApiReference(api); - return { - content: [ - { - type: "text", - text: corpusEntry ? `${rendered}\n**Corpus Source:** ${corpusEntry.source}` : rendered, - }, - ], - }; + return { content: [{ type: "text", text: formatApiReference(api) }] }; }, ); } diff --git a/src/plugins/reactflow/tools/get-examples.ts b/src/plugins/reactflow/tools/get-examples.ts index b1dfcc6..cd8ceb3 100644 --- a/src/plugins/reactflow/tools/get-examples.ts +++ b/src/plugins/reactflow/tools/get-examples.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ALL_APIS, CATEGORIES, getExamplesByCategory, formatExample, capitalize } from "../data/index.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "reactflow_get_examples", "Get code examples for a specific React Flow category", diff --git a/src/plugins/reactflow/tools/get-migration-guide.ts b/src/plugins/reactflow/tools/get-migration-guide.ts index 2ef077c..39c9700 100644 --- a/src/plugins/reactflow/tools/get-migration-guide.ts +++ b/src/plugins/reactflow/tools/get-migration-guide.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { V12_MIGRATION } from "../data/index.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "reactflow_get_migration_guide", "Get the React Flow v11 to v12 migration guide with all breaking changes, import changes, and type changes", diff --git a/src/plugins/reactflow/tools/get-pattern.ts b/src/plugins/reactflow/tools/get-pattern.ts index 45deb82..b7f09e6 100644 --- a/src/plugins/reactflow/tools/get-pattern.ts +++ b/src/plugins/reactflow/tools/get-pattern.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PATTERNS, PATTERN_SECTIONS } from "../data/index.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "reactflow_get_pattern", "Get an enterprise React Flow pattern with full implementation code. Patterns include store architecture, undo/redo, drag-and-drop, auto-layout, context menus, copy/paste, save/restore, DAG validation, keyboard shortcuts, performance, dark mode, SSR, subflows, edge reconnection, and more.", diff --git a/src/plugins/reactflow/tools/get-template.ts b/src/plugins/reactflow/tools/get-template.ts index 6190b31..61eebe4 100644 --- a/src/plugins/reactflow/tools/get-template.ts +++ b/src/plugins/reactflow/tools/get-template.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { TEMPLATES, capitalize } from "../data/index.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "reactflow_get_template", "Get a production-ready code template: custom-node (Tailwind + toolbar + handles + status), custom-edge (delete button + BaseEdge), or zustand-store (full store with selectors)", @@ -11,7 +11,7 @@ export function register(server: McpServer): void { .enum(["custom-node", "custom-edge", "zustand-store"] as const) .describe("Template name"), }, - async ({ template }) => { + async ({ template }: { template: "custom-node" | "custom-edge" | "zustand-store" }) => { const code = TEMPLATES[template]; return { content: [ diff --git a/src/plugins/reactflow/tools/list-apis.ts b/src/plugins/reactflow/tools/list-apis.ts index 8cf3376..4a9181d 100644 --- a/src/plugins/reactflow/tools/list-apis.ts +++ b/src/plugins/reactflow/tools/list-apis.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ALL_APIS, capitalize, API_KINDS } from "../data/index.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "reactflow_list_apis", "List all React Flow v12 APIs - components, hooks, utilities, and types", diff --git a/src/plugins/reactflow/tools/search-docs.ts b/src/plugins/reactflow/tools/search-docs.ts index 9827120..11eddc0 100644 --- a/src/plugins/reactflow/tools/search-docs.ts +++ b/src/plugins/reactflow/tools/search-docs.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { ALL_APIS, CATEGORIES, PATTERN_SECTIONS, searchApis, formatExample } from "../data/index.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "reactflow_search_docs", "Search React Flow documentation by keyword. Searches API names, descriptions, code examples, and tips.", diff --git a/src/plugins/rust/index.ts b/src/plugins/rust/index.ts deleted file mode 100644 index 1f521bc..0000000 --- a/src/plugins/rust/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listPractices } from "./tools/list-practices.js"; -import { register as getPractice } from "./tools/get-practice.js"; -import { register as searchDocs } from "./tools/search-docs.js"; -import { register as cheatsheet } from "./tools/cheatsheet.js"; - -function register(server: McpServer): void { - listPractices(server); - getPractice(server); - searchDocs(server); - cheatsheet(server); -} - -export const rustPlugin: Plugin = { - name: "rust", - register, -}; diff --git a/src/plugins/rust/tools/cheatsheet.ts b/src/plugins/rust/tools/cheatsheet.ts index 2cf0a0a..a871917 100644 --- a/src/plugins/rust/tools/cheatsheet.ts +++ b/src/plugins/rust/tools/cheatsheet.ts @@ -1,9 +1,9 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { snippet } from "../loader.js"; const CHEATSHEET = snippet("cheatsheet.txt"); -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "rust_cheatsheet", "Quick Rust reference: ownership rules, error handling, clippy commands, and key patterns", diff --git a/src/plugins/rust/tools/get-practice.ts b/src/plugins/rust/tools/get-practice.ts index 25cf811..0055670 100644 --- a/src/plugins/rust/tools/get-practice.ts +++ b/src/plugins/rust/tools/get-practice.ts @@ -1,114 +1,16 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -import { BEST_PRACTICES, type BestPractice } from "../data.js"; +import { BEST_PRACTICES } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "backend.rust"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - practices?: Record; -}; - -type CorpusPracticeEntry = { - name?: string; - chapter?: string; - rule?: string; - reason?: string; - good?: string; - bad?: string; - tips?: string[]; -}; - -type LoadedCorpusPractice = { practice: BestPractice; source: string }; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "backend.rust"; - -const cachedCorpusPractices = new Map(); - -function normalize(name: string): string { - return name.toLowerCase().trim(); -} - -function loadCorpusPractice(name: string): LoadedCorpusPractice | null { - const key = normalize(name); - if (cachedCorpusPractices.has(key)) { - return cachedCorpusPractices.get(key) ?? null; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusPractices.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const practicePath = namespaceIndex?.practices?.[key]?.file; - if (!practicePath) { - cachedCorpusPractices.set(key, null); - return null; - } - - const raw = readFileSync(join(corpusRoot, "backend/rust", practicePath), "utf8"); - const entry = YAML.parse(raw) as CorpusPracticeEntry | null; - if ( - !entry || - normalize(entry.name ?? "") !== key || - !entry.chapter || - !entry.rule || - !entry.reason - ) { - cachedCorpusPractices.set(key, null); - return null; - } - - const loaded: LoadedCorpusPractice = { - practice: { - name: entry.name ?? name, - chapter: entry.chapter as BestPractice["chapter"], - rule: entry.rule, - reason: entry.reason, - good: entry.good, - bad: entry.bad, - tips: entry.tips, - }, - source: corpusNamespace, - }; - cachedCorpusPractices.set(key, loaded); - return loaded; - } catch { - cachedCorpusPractices.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "rust_get_practice", "Get a Rust best practice with code examples", { name: z.string().describe("Practice name (e.g. 'borrow-over-clone', 'result-not-panic', 'thiserror-vs-anyhow', 'type-state-pattern', 'clippy-command')"), }, - async ({ name }) => { - const corpusEntry = loadCorpusPractice(name); - const practice = - corpusEntry?.practice ?? - BEST_PRACTICES.find((p) => p.name.toLowerCase() === name.toLowerCase()); + async ({ name }: { name: string }) => { + const practice = BEST_PRACTICES.find((p) => p.name.toLowerCase() === name.toLowerCase()); if (!practice) { return { content: [{ type: "text", text: `Practice "${name}" not found.\n\nAvailable: ${BEST_PRACTICES.map((p) => p.name).join(", ")}` }], @@ -125,12 +27,7 @@ export function register(server: McpServer): void { text += `## Tips\n`; for (const tip of practice.tips) text += `- ${tip}\n`; } - - if (corpusEntry) { - text += `\n**Corpus Source:** ${corpusEntry.source}`; - } - return { content: [{ type: "text", text }] }; - } + }, ); } diff --git a/src/plugins/rust/tools/list-practices.ts b/src/plugins/rust/tools/list-practices.ts index 761ae6e..9075b3e 100644 --- a/src/plugins/rust/tools/list-practices.ts +++ b/src/plugins/rust/tools/list-practices.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { BEST_PRACTICES, CHAPTERS, getPracticesByChapter } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "rust_list_practices", "List Rust best practices by chapter", diff --git a/src/plugins/rust/tools/search-docs.ts b/src/plugins/rust/tools/search-docs.ts index 3398154..307494e 100644 --- a/src/plugins/rust/tools/search-docs.ts +++ b/src/plugins/rust/tools/search-docs.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { searchPractices } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "rust_search_docs", "Search Rust best practices by keyword", diff --git a/src/plugins/shadcn/index.ts b/src/plugins/shadcn/index.ts deleted file mode 100644 index fb2fb90..0000000 --- a/src/plugins/shadcn/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listComponents } from "./tools/list-components.js"; -import { register as getComponent } from "./tools/get-component.js"; -import { register as getRules } from "./tools/get-rules.js"; -import { register as getSnippet } from "./tools/get-snippet.js"; -import { register as getComposition } from "./tools/get-composition.js"; - -function register(server: McpServer): void { - listComponents(server); - getComponent(server); - getRules(server); - getSnippet(server); - getComposition(server); -} - -export const shadcnPlugin: Plugin = { - name: "shadcn", - register, -}; diff --git a/src/plugins/shadcn/tools/get-component.ts b/src/plugins/shadcn/tools/get-component.ts index e21761f..f0555a8 100644 --- a/src/plugins/shadcn/tools/get-component.ts +++ b/src/plugins/shadcn/tools/get-component.ts @@ -1,122 +1,16 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -import { getComponentByName, SHADCN_COMPONENTS, type ShadcnComponent } from "../data.js"; +import { getComponentByName, SHADCN_COMPONENTS } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "frontend.shadcn"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - components?: Record; -}; - -type CorpusComponentEntry = { - name?: string; - category?: string; - description?: string; - basePrimitive?: string; - dataSlots?: string[]; - variants?: string[]; - sizes?: string[]; - requiresUseClient?: boolean; - usageSnippet?: string; - pairsWith?: string[]; -}; - -type LoadedCorpusComponent = { component: ShadcnComponent; source: string }; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.shadcn"; - -const cachedCorpusComponents = new Map(); - -function normalize(name: string): string { - return name.toLowerCase().trim(); -} - -function loadCorpusComponent(name: string): LoadedCorpusComponent | null { - const key = normalize(name); - if (cachedCorpusComponents.has(key)) { - return cachedCorpusComponents.get(key) ?? null; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusComponents.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const componentPath = namespaceIndex?.components?.[key]?.file; - if (!componentPath) { - cachedCorpusComponents.set(key, null); - return null; - } - - const raw = readFileSync(join(corpusRoot, "frontend/shadcn", componentPath), "utf8"); - const entry = YAML.parse(raw) as CorpusComponentEntry | null; - if ( - !entry || - normalize(entry.name ?? "") !== key || - !entry.category || - !entry.description || - !entry.basePrimitive || - !Array.isArray(entry.dataSlots) || - !entry.usageSnippet || - typeof entry.requiresUseClient !== "boolean" || - !Array.isArray(entry.pairsWith) - ) { - cachedCorpusComponents.set(key, null); - return null; - } - - const loaded: LoadedCorpusComponent = { - component: { - name: entry.name ?? name, - category: entry.category as ShadcnComponent["category"], - description: entry.description, - basePrimitive: entry.basePrimitive, - dataSlots: entry.dataSlots, - variants: entry.variants, - sizes: entry.sizes, - requiresUseClient: entry.requiresUseClient, - usageSnippet: entry.usageSnippet, - pairsWith: entry.pairsWith, - }, - source: corpusNamespace, - }; - cachedCorpusComponents.set(key, loaded); - return loaded; - } catch { - cachedCorpusComponents.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "shadcn_get_component", "Get full details for a shadcn/ui component: base primitive, data-slots, variants, sizes, usage example, and which other components it pairs with. Uses curated reference data.", { name: z.string().describe("Component name (e.g., 'Button', 'Dialog', 'Field', 'Select'). Case-insensitive."), }, - async ({ name }) => { - const corpusEntry = loadCorpusComponent(name); - const component = corpusEntry?.component ?? getComponentByName(name); + async ({ name }: { name: string }) => { + const component = getComponentByName(name); if (!component) { const available = SHADCN_COMPONENTS.map((c) => c.name).join(", "); return { @@ -164,11 +58,7 @@ export function register(server: McpServer): void { text += `- [ ] Props spread (...props) to underlying primitive\n`; text += `- [ ] OKLCH color tokens from design system (not hardcoded hex)\n`; - if (corpusEntry) { - text += `\n**Corpus Source:** ${corpusEntry.source}`; - } - return { content: [{ type: "text" as const, text }] }; - } + }, ); } diff --git a/src/plugins/shadcn/tools/get-composition.ts b/src/plugins/shadcn/tools/get-composition.ts index e8ad27b..b191706 100644 --- a/src/plugins/shadcn/tools/get-composition.ts +++ b/src/plugins/shadcn/tools/get-composition.ts @@ -1,4 +1,4 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PAGE_TYPE_NAMES, getPageComposition } from "../data.js"; @@ -7,7 +7,7 @@ import { PAGE_TYPE_NAMES, getPageComposition } from "../data.js"; * When forge-plan processes a DESIGN.md with a specific page type, it calls this tool * to get concrete shadcn component recommendations per section. */ -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "shadcn_get_composition", "Get shadcn/ui component composition for a specific page type. Returns which components to combine per section with rationale. This is the bridge from designer's page templates to concrete implementation. Use after designer_get_page_template.", diff --git a/src/plugins/shadcn/tools/get-rules.ts b/src/plugins/shadcn/tools/get-rules.ts index cd42a15..765987d 100644 --- a/src/plugins/shadcn/tools/get-rules.ts +++ b/src/plugins/shadcn/tools/get-rules.ts @@ -1,7 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { SHADCN_RULES, ARCHITECTURAL_CHECKLIST } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "shadcn_get_rules", "Get the architectural rules and mandatory checklist for shadcn/ui (Base UI edition). Call this before proposing any new component or modification.", diff --git a/src/plugins/shadcn/tools/get-snippet.ts b/src/plugins/shadcn/tools/get-snippet.ts index 44791b0..29732bd 100644 --- a/src/plugins/shadcn/tools/get-snippet.ts +++ b/src/plugins/shadcn/tools/get-snippet.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { getComponentByName, SHADCN_COMPONENTS } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "shadcn_get_snippet", "Get a usage code snippet for a shadcn/ui component. Returns the canonical example showing variants, sizes, and composition patterns.", diff --git a/src/plugins/shadcn/tools/list-components.ts b/src/plugins/shadcn/tools/list-components.ts index 9edd68b..3a53b4e 100644 --- a/src/plugins/shadcn/tools/list-components.ts +++ b/src/plugins/shadcn/tools/list-components.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { SHADCN_COMPONENTS, COMPONENT_CATEGORIES, getComponentsByCategory } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "shadcn_list_components", "List all curated shadcn/ui components (Base UI edition). Optionally filter by category: button, input, card, dialog, dropdown, tabs, table, form, navigation, feedback, overlay, data-display.", diff --git a/src/plugins/ui-ux/index.ts b/src/plugins/ui-ux/index.ts deleted file mode 100644 index e7443a1..0000000 --- a/src/plugins/ui-ux/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Plugin } from "../../registry.js"; -import { register as listPrinciples } from "./tools/list-principles.js"; -import { register as getPrinciple } from "./tools/get-principle.js"; -import { register as getComponentPattern } from "./tools/get-component-pattern.js"; -import { register as getChecklist } from "./tools/get-checklist.js"; -import { register as search } from "./tools/search.js"; -import { register as getGotchas } from "./tools/get-gotchas.js"; - -function register(server: McpServer): void { - listPrinciples(server); - getPrinciple(server); - getComponentPattern(server); - getChecklist(server); - search(server); - getGotchas(server); -} - -export const uiUxPlugin: Plugin = { - name: "ui-ux", - register, -}; diff --git a/src/plugins/ui-ux/tools/get-checklist.ts b/src/plugins/ui-ux/tools/get-checklist.ts index 49aa310..e3592e5 100644 --- a/src/plugins/ui-ux/tools/get-checklist.ts +++ b/src/plugins/ui-ux/tools/get-checklist.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { DOMAINS, getChecklistByDomain } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "ui_ux_get_checklist", "Get quality checklist for a UI/UX domain before shipping", diff --git a/src/plugins/ui-ux/tools/get-component-pattern.ts b/src/plugins/ui-ux/tools/get-component-pattern.ts index 189ea47..473b8e5 100644 --- a/src/plugins/ui-ux/tools/get-component-pattern.ts +++ b/src/plugins/ui-ux/tools/get-component-pattern.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { COMPONENT_PATTERNS, getComponentPatternByName } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "ui_ux_get_component_pattern", "Get component pattern spec including variants, states, and sizing rules", diff --git a/src/plugins/ui-ux/tools/get-gotchas.ts b/src/plugins/ui-ux/tools/get-gotchas.ts index 0f07e2d..c196307 100644 --- a/src/plugins/ui-ux/tools/get-gotchas.ts +++ b/src/plugins/ui-ux/tools/get-gotchas.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { DOMAINS, getAllGotchas } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "ui_ux_get_gotchas", "List all common UI/UX mistakes and fixes, optionally filtered by domain", diff --git a/src/plugins/ui-ux/tools/get-principle.ts b/src/plugins/ui-ux/tools/get-principle.ts index d1309be..f5d8f36 100644 --- a/src/plugins/ui-ux/tools/get-principle.ts +++ b/src/plugins/ui-ux/tools/get-principle.ts @@ -1,114 +1,16 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import YAML from "yaml"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; -import { PRINCIPLES, type Principle } from "../data.js"; +import { PRINCIPLES } from "../data.js"; -type CorpusIndex = { - namespaces?: { - "frontend.ui-ux"?: { - index?: string; - }; - }; -}; - -type CorpusNamespaceIndex = { - namespace?: string; - principles?: Record; -}; - -type CorpusPrincipleEntry = { - name?: string; - domain?: string; - rule?: string; - detail?: string; - examples?: string[]; - antiPatterns?: string[]; - cssExample?: string; -}; - -type LoadedCorpusPrinciple = { principle: Principle; source: string }; - -const moduleDir = dirname(fileURLToPath(import.meta.url)); -const corpusRoot = join(moduleDir, "../../../../corpus"); -const corpusNamespace = "frontend.ui-ux"; - -const cachedCorpusPrinciples = new Map(); - -function normalize(name: string): string { - return name.toLowerCase().trim(); -} - -function loadCorpusPrinciple(name: string): LoadedCorpusPrinciple | null { - const key = normalize(name); - if (cachedCorpusPrinciples.has(key)) { - return cachedCorpusPrinciples.get(key) ?? null; - } - - try { - const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); - const index = YAML.parse(indexRaw) as CorpusIndex | null; - const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; - if (!namespaceIndexPath) { - cachedCorpusPrinciples.set(key, null); - return null; - } - - const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); - const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; - const principlePath = namespaceIndex?.principles?.[key]?.file; - if (!principlePath) { - cachedCorpusPrinciples.set(key, null); - return null; - } - - const raw = readFileSync(join(corpusRoot, "frontend/ui-ux", principlePath), "utf8"); - const entry = YAML.parse(raw) as CorpusPrincipleEntry | null; - if ( - !entry || - normalize(entry.name ?? "") !== key || - !entry.domain || - !entry.rule || - !entry.detail - ) { - cachedCorpusPrinciples.set(key, null); - return null; - } - - const loaded: LoadedCorpusPrinciple = { - principle: { - name: entry.name ?? name, - domain: entry.domain as Principle["domain"], - rule: entry.rule, - detail: entry.detail, - examples: entry.examples, - antiPatterns: entry.antiPatterns, - cssExample: entry.cssExample, - }, - source: corpusNamespace, - }; - cachedCorpusPrinciples.set(key, loaded); - return loaded; - } catch { - cachedCorpusPrinciples.set(key, null); - return null; - } -} - -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "ui_ux_get_principle", "Get full details for a UI/UX principle including examples, anti-patterns, and CSS examples", { name: z.string().describe("Principle name (e.g. 'type-scale', 'wcag-contrast', 'dark-mode-principles', 'touch-targets', 'easing-rules')"), }, - async ({ name }) => { - const corpusEntry = loadCorpusPrinciple(name); - const principle = - corpusEntry?.principle ?? - PRINCIPLES.find((p) => p.name.toLowerCase() === name.toLowerCase()); + async ({ name }: { name: string }) => { + const principle = PRINCIPLES.find((p) => p.name.toLowerCase() === name.toLowerCase()); if (!principle) { const available = PRINCIPLES.map((p) => p.name).join(", "); @@ -136,12 +38,7 @@ export function register(server: McpServer): void { text += `## Anti-patterns (avoid)\n`; for (const ap of principle.antiPatterns) text += `- โŒ ${ap}\n`; } - - if (corpusEntry) { - text += `\n**Corpus Source:** ${corpusEntry.source}`; - } - return { content: [{ type: "text", text }] }; - } + }, ); } diff --git a/src/plugins/ui-ux/tools/list-principles.ts b/src/plugins/ui-ux/tools/list-principles.ts index 271620e..bcf0af0 100644 --- a/src/plugins/ui-ux/tools/list-principles.ts +++ b/src/plugins/ui-ux/tools/list-principles.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { PRINCIPLES, DOMAINS, getPrinciplesByDomain } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "ui_ux_list_principles", "List all UI/UX principles by domain (typography, color, spacing, elevation, motion, accessibility, responsive, components)", diff --git a/src/plugins/ui-ux/tools/search.ts b/src/plugins/ui-ux/tools/search.ts index 2544f43..876b271 100644 --- a/src/plugins/ui-ux/tools/search.ts +++ b/src/plugins/ui-ux/tools/search.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolServer } from "../../../shared/tool-types.js"; import { z } from "zod"; import { searchPrinciples, COMPONENT_PATTERNS } from "../data.js"; -export function register(server: McpServer): void { +export function register(server: ToolServer): void { server.tool( "ui_ux_search", "Search UI/UX principles and component patterns by keyword", diff --git a/src/registry.ts b/src/registry.ts deleted file mode 100755 index 1ece8b6..0000000 --- a/src/registry.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; - -export interface Plugin { - name: string; - register: (server: McpServer) => void; -} - -export function loadPlugins(server: McpServer, plugins: Plugin[]): void { - for (const plugin of plugins) { - plugin.register(server); - } -} diff --git a/src/shared/tool-types.ts b/src/shared/tool-types.ts new file mode 100644 index 0000000..60908d9 --- /dev/null +++ b/src/shared/tool-types.ts @@ -0,0 +1,29 @@ +export type ToolContent = { type: "text"; text: string }; + +export type ToolResult = { + content: ToolContent[]; + isError?: boolean; +}; + +export type ToolHandler = (args: TArgs) => Promise | ToolResult; + +export interface ToolServer { + tool( + name: string, + description: string, + schema: unknown, + handler: ToolHandler, + ): void; + resource( + name: string, + uriTemplate: string, + metadata: unknown, + handler: (...args: any[]) => Promise | unknown, + ): void; + prompt(name: string, description: string, schema: unknown, handler: (...args: any[]) => unknown): void; +} + +export interface Plugin { + name: string; + register: (server: ToolServer) => void; +}