diff --git a/package-lock.json b/package-lock.json index d95c87e28b..df29d82db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6065,6 +6065,307 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", + "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.9", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", @@ -18446,6 +18747,27 @@ "node": ">=0.4.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/exceljs": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", @@ -18763,8 +19085,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-json-stringify": { "version": "2.7.13", @@ -22275,6 +22596,12 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -27696,10 +28023,10 @@ } }, "node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", - "dev": true, + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -32277,6 +32604,15 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -33483,11 +33819,12 @@ } }, "node_modules/qs": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", - "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -34405,6 +34742,31 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -39358,7 +39720,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -40555,6 +40916,7 @@ "version": "3.19.3", "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-3.19.3.tgz", "integrity": "sha512-/+ODrTUHtlDPRH9qIC0JREH8+7nsRcjDl3Bxn2Xo/rvAaVvixH5275jHwg0C85g4QsF4P6M2ojfScPPAl+pLAg==", + "license": "BSD-2-Clause", "dependencies": { "@npmcli/arborist": "^4.0.4", "are-we-there-yet": "^2.0.0", @@ -41240,6 +41602,7 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-5.10.0.tgz", "integrity": "sha512-iDUKykV7L4nDNzeYSedRmSeJ5eMYFucnKDi6KN1WNASXErgPepKqsQw55TgXPHnmpcyOh2Dd/LAZkyc+f0qaAw==", + "license": "BSD-2-Clause", "dependencies": { "chalk": "^4.1.0", "dargs": "^7.0.0", @@ -41975,6 +42338,24 @@ "node": "*" } }, + "node_modules/zod": { + "version": "3.25.60", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.60.tgz", + "integrity": "sha512-VyPT/ZfyggoJtTnGCfPGx2OOW9idfmyqsqZCAvYscOlXKCGKPCeSfr44Ewp8fqvtfNMe1T88qjWIFKopTL9jSQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "packages/cache": { "name": "@sourceloop/cache", "version": "4.0.1", @@ -42028,6 +42409,7 @@ "license": "MIT", "dependencies": { "@loopback/cli": "^5.2.1", + "@modelcontextprotocol/sdk": "^1.12.1", "@oclif/command": "^1.8.16", "@oclif/config": "^1.18.3", "@oclif/plugin-autocomplete": "1.3.10", @@ -42045,7 +42427,7 @@ "ts-morph": "^19.0.0", "tslib": "^2.6.2", "yeoman-environment": "^3.19.3", - "yeoman-generator": "^5.9.0", + "yeoman-generator": "^5.10.0", "yosay": "^2.0.2" }, "bin": { diff --git a/packages/cli/README.md b/packages/cli/README.md index 8e3879bb84..6073ca8899 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -9,6 +9,7 @@ To install sourceloop-cli, run ```shell npm install @sourceloop/cli ``` + Once the above command is executed, you will be able to access the CLI commands directly from your terminal. You can use either `sl` or `arc` as shorthand to run any of the `sourceloop` commands listed below. A sample usage is provided for reference: ## Usage @@ -34,6 +35,7 @@ USAGE * [`sl cdk`](#sl-cdk) * [`sl extension [NAME]`](#sl-extension-name) * [`sl help [COMMAND]`](#sl-help-command) +* [`sl mcp`](#sl-mcp) * [`sl microservice [NAME]`](#sl-microservice-name) * [`sl scaffold [NAME]`](#sl-scaffold-name) * [`sl update`](#sl-update) @@ -79,7 +81,7 @@ OPTIONS -p, --packageJsonName=packageJsonName Package name for arc-cdk - -r, --relativePathToApp=relativePathToApp Relative path to the service you want to deploy + -r, --relativePathToApp=relativePathToApp Relative path to the application ts file --help show manual pages ``` @@ -88,7 +90,7 @@ _See code: [src/commands/cdk.ts](https://github.com/sourcefuse/loopback4-microse ## `sl extension [NAME]` -add an extension +This generates a local package in the packages folder of a ARC generated monorepo. This package can then be installed and used inside other modules in the monorepo. ``` USAGE @@ -120,9 +122,33 @@ OPTIONS _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.18/src/commands/help.ts)_ +## `sl mcp` + +Command that runs an MCP server for the sourceloop CLI, this is not supposed to be run directly, but rather used by the MCP client to interact with the CLI commands. + +``` +USAGE + $ sl mcp + +OPTIONS + --help show manual pages + +DESCRIPTION + Command that runs an MCP server for the sourceloop CLI, this is not supposed to be run directly, but rather used by + the MCP client to interact with the CLI commands. + You can use it using the following MCP server configuration: + "sourceloop": { + "command": "npx", + "args": ["@sourceloop/cli", "mcp"], + "timeout": 300 + } +``` + +_See code: [src/commands/mcp.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v10.0.0/src/commands/mcp.ts)_ + ## `sl microservice [NAME]` -add a microservice +Add a microservice in the services or facade folder of a ARC generated monorepo. This can also optionally add migrations for the same microservice. ``` USAGE @@ -153,7 +179,7 @@ OPTIONS Type of the datasource --[no-]facade - Create as facade + Create as facade inside the facades folder --help show manual pages @@ -169,7 +195,7 @@ _See code: [src/commands/microservice.ts](https://github.com/sourcefuse/loopback ## `sl scaffold [NAME]` -create a project scaffold +Setup a ARC based monorepo using npm workspaces with an empty services, facades and packages folder ``` USAGE diff --git a/packages/cli/package.json b/packages/cli/package.json index d4786026e7..3a0166ebb9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,6 +38,7 @@ "license": "MIT", "dependencies": { "@loopback/cli": "^5.2.1", + "@modelcontextprotocol/sdk": "^1.12.1", "@oclif/command": "^1.8.16", "@oclif/config": "^1.18.3", "@oclif/plugin-autocomplete": "1.3.10", @@ -55,7 +56,7 @@ "ts-morph": "^19.0.0", "tslib": "^2.6.2", "yeoman-environment": "^3.19.3", - "yeoman-generator": "^5.9.0", + "yeoman-generator": "^5.10.0", "yosay": "^2.0.2" }, "devDependencies": { diff --git a/packages/cli/src/__tests__/commands/mcp.test.ts b/packages/cli/src/__tests__/commands/mcp.test.ts new file mode 100644 index 0000000000..19ce23fed4 --- /dev/null +++ b/packages/cli/src/__tests__/commands/mcp.test.ts @@ -0,0 +1,68 @@ +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp'; +import {Config} from '@oclif/config'; +import {expect} from 'chai'; +import {stub} from 'sinon'; +import {Mcp} from '../../commands/mcp'; +import {TestMCPCommand} from '../fixtures'; +import {McpServerStub} from '../fixtures/mcp-service-stub'; + +describe('mcp', () => { + let command: Mcp; + let callStub: sinon.SinonStub; + let server: McpServerStub; + beforeEach(async () => { + command = new Mcp([], new Config({root: ''}), stub(), undefined, [ + TestMCPCommand, + ]); + callStub = stub(); + server = new McpServerStub(callStub); + command.server = server as unknown as McpServer; + + await command.run(); + expect(callStub.callCount).to.eq(1); + }); + + afterEach(() => { + callStub.resetHistory(); + }); + + it('should call tool with correct parameters', async () => { + const result = await server.callTool('TestMCPCommand', { + name: 'test', + description: 'This is a test command', + owner: 'test-owner', + cwd: '/test/cwd', + }); + expect(JSON.parse(result.content[0].text)).to.deep.equal({ + inputs: { + name: 'test', + description: 'This is a test command', + owner: 'test-owner', + cwd: '/test/cwd', + // not passed so default value is false for booleans + integrate: false, + }, + command: 'test-mcp-command', + }); + }); + + it('should call throw error for registered command if invalid payload is provided', async () => { + const result = await server.callTool('TestMCPCommand', { + name: 'test', + description: 'This is a test command', + owner: 'test-owner', + cwd: '/test/cwd', + }); + expect(JSON.parse(result.content[0].text)).to.deep.equal({ + inputs: { + name: 'test', + description: 'This is a test command', + owner: 'test-owner', + cwd: '/test/cwd', + // not passed so default value is false for booleans + integrate: false, + }, + command: 'test-mcp-command', + }); + }); +}); diff --git a/packages/cli/src/__tests__/fixtures/index.ts b/packages/cli/src/__tests__/fixtures/index.ts new file mode 100644 index 0000000000..21e353b13a --- /dev/null +++ b/packages/cli/src/__tests__/fixtures/index.ts @@ -0,0 +1,2 @@ +export * from './mcp-service-stub'; +export * from './test-mcp-command'; diff --git a/packages/cli/src/__tests__/fixtures/mcp-service-stub.ts b/packages/cli/src/__tests__/fixtures/mcp-service-stub.ts new file mode 100644 index 0000000000..927e317324 --- /dev/null +++ b/packages/cli/src/__tests__/fixtures/mcp-service-stub.ts @@ -0,0 +1,51 @@ +// eslint-disable-next-line @typescript-eslint/naming-convention +import Sinon from 'sinon'; +import {z} from 'zod'; +import {AnyObject} from '../../types'; + +export class McpServerStub { + private callStub: Sinon.SinonStub; + constructor(callStub: Sinon.SinonStub) { + this.callStub = callStub; + } + + private toolMap: Record = {}; + + tool( + name: string, + description: string, + params: Record, + run: ( + args: Record, + ) => Promise, + ): void { + this.toolMap[name] = async (args: Record) => { + try { + const parsedArgs = z.object(params).parse(args); + return await run(parsedArgs); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Validation error: ${error.message}`); + } + throw error; + } + }; + } + + callTool( + name: string, + args: Record, + ): Promise { + if (!this.toolMap[name]) { + throw new Error(`Tool ${name} not found`); + } + return this.toolMap[name](args); + } + + async connect() { + this.callStub({ + type: 'connect', + message: 'MCP Server connected successfully', + }); + } +} diff --git a/packages/cli/src/__tests__/fixtures/test-mcp-command.ts b/packages/cli/src/__tests__/fixtures/test-mcp-command.ts new file mode 100644 index 0000000000..fd2061dc1e --- /dev/null +++ b/packages/cli/src/__tests__/fixtures/test-mcp-command.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Sourcefuse Technologies +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +import {flags} from '@oclif/command'; +import {AnyObject, McpTextResponse} from '../../types'; + +export class TestMCPCommand { + static readonly description = 'A dummy command to test the MCP functionality'; + static readonly mcpDescription = ` + This is a dummy command to test the MCP functionality. + `; + + static readonly flags = { + help: flags.boolean({ + name: 'help', + description: 'show manual pages', + type: 'boolean', + }), + cwd: flags.string({ + name: 'working-directory', + description: + 'Directory where project will be scaffolded, instead of the project name', + }), + owner: flags.string({ + name: 'owner', + description: 'owner of the repo', + }), + description: flags.string({ + name: 'description', + description: 'description of the repo', + }), + integrate: flags.boolean({ + name: 'integrate', + description: 'Do you want to include integration files?', + }), + }; + static readonly args = [ + {name: 'name', description: 'name of the project', required: false}, + ]; + + static async mcpRun(inputs: AnyObject): Promise { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + inputs, + command: 'test-mcp-command', + }), + }, + ], + }; + } +} diff --git a/packages/cli/src/__tests__/helper/command-test.helper.ts b/packages/cli/src/__tests__/helper/command-test.helper.ts index 3f9c67a48e..a34eb76e8d 100644 --- a/packages/cli/src/__tests__/helper/command-test.helper.ts +++ b/packages/cli/src/__tests__/helper/command-test.helper.ts @@ -36,8 +36,6 @@ export function commandTest(testCase: CommandTestCase, command: ICommand) { for (let i = 0; i < calls.length; i++) { expect(calls[i].args[0][0]).to.be.deep.equal(testCase.prompts[i].input); } - // get second argument of first call of env.run - expect(stubEnv.run.getCall(0).args[1]).is.deep.equal(testCase.options); }); } diff --git a/packages/cli/src/__tests__/suite/mcp.ts b/packages/cli/src/__tests__/suite/mcp.ts new file mode 100644 index 0000000000..47820c784d --- /dev/null +++ b/packages/cli/src/__tests__/suite/mcp.ts @@ -0,0 +1,7 @@ +export const mcpSuite = [ + { + name: 'mcp command without any option', + options: {}, + prompts: [], + }, +]; diff --git a/packages/cli/src/__tests__/suite/microservice-prompts.ts b/packages/cli/src/__tests__/suite/microservice-prompts.ts index 433d5049b6..009c339514 100644 --- a/packages/cli/src/__tests__/suite/microservice-prompts.ts +++ b/packages/cli/src/__tests__/suite/microservice-prompts.ts @@ -22,7 +22,7 @@ export const microservicePromptsSuite = [ input: { name: 'facade', type: 'confirm', - message: 'Create as facade', + message: 'Create as facade inside the facades folder', default: false, }, output: true, @@ -64,7 +64,7 @@ export const microservicePromptsSuite = [ input: { name: 'facade', type: 'confirm', - message: 'Create as facade', + message: 'Create as facade inside the facades folder', default: false, }, output: false, @@ -160,7 +160,7 @@ export const microservicePromptsSuite = [ input: { name: 'facade', type: 'confirm', - message: 'Create as facade', + message: 'Create as facade inside the facades folder', default: false, }, output: false, diff --git a/packages/cli/src/app-generator.ts b/packages/cli/src/app-generator.ts index 0dbe008bae..543156c70e 100644 --- a/packages/cli/src/app-generator.ts +++ b/packages/cli/src/app-generator.ts @@ -8,4 +8,17 @@ import * as Generator from 'yeoman-generator'; /* eslint-enable @typescript-eslint/naming-convention */ export default class AppGenerator< T extends Generator.GeneratorOptions, -> extends AppGeneratorLB4 {} +> extends AppGeneratorLB4 { + exitGeneration: string | Error = ''; + exit(reason: string | Error) { + if (!reason) return; + this.exitGeneration = reason; + if (this.options.inMcp) { + if (reason instanceof Error) { + throw new Error(reason.message); + } else { + throw new Error(reason); + } + } + } +} diff --git a/packages/cli/src/base-generator.ts b/packages/cli/src/base-generator.ts index f3c7f11cee..cb16c51026 100644 --- a/packages/cli/src/base-generator.ts +++ b/packages/cli/src/base-generator.ts @@ -12,7 +12,7 @@ export abstract class BaseGenerator< T extends Generator.GeneratorOptions, > extends Generator { root = ''; - private exitGeneration?: string; + private exitGeneration: string | Error = ''; async copyTemplateAsync() { const readdriAsync = promisify(readdir); @@ -40,9 +40,16 @@ export abstract class BaseGenerator< return super.destinationRoot(rootPath); } - exit(reason?: string) { - if (reason) return; + exit(reason?: string | Error) { + if (!reason) return; this.exitGeneration = reason; + if (this.options.inMcp) { + if (reason instanceof Error) { + throw new Error(reason.message); + } else { + throw new Error(reason); + } + } } shouldExit() { diff --git a/packages/cli/src/command-base.ts b/packages/cli/src/command-base.ts index c1f6a42cd5..e026a305d7 100644 --- a/packages/cli/src/command-base.ts +++ b/packages/cli/src/command-base.ts @@ -12,7 +12,8 @@ import {Input as FlagInput} from '@oclif/parser/lib/flags'; import inquirer, {Question} from 'inquirer'; import fetch from 'node-fetch'; import Environment, {createEnv} from 'yeoman-environment'; -import {AnyObject, PromptFunction} from './types'; +import {AnyObject, McpTextResponse, PromptFunction} from './types'; +import {yeomanRun} from './utilities/yeoman'; const chalk = require('chalk'); //NOSONAR /* eslint-enable @typescript-eslint/naming-convention */ const IGNORED_FLAGS = ['help', 'cwd']; @@ -54,6 +55,49 @@ export default abstract class CommandBase extends Command { }); } + static async mcpResponse( + inputs: AnyObject, + name: string, + args: string[], + ): Promise { + const originalCwd = process.cwd(); + if (inputs.workingDir) { + process.chdir(inputs.workingDir); + } + let output: McpTextResponse = { + content: [ + {type: 'text', text: 'Command executed successfully!', isError: false}, + ], + }; + try { + await yeomanRun(inputs.cwd ?? process.cwd(), name, args, inputs); + } catch (err) { + if (err instanceof Error) { + output = { + content: [ + { + type: 'text', + text: `Error executing command: ${err.message}`, + isError: true, + }, + ], + }; + } else { + output = { + content: [ + { + type: 'text', + text: `Error executing command: ${err}`, + isError: true, + }, + ], + }; + } + } + process.chdir(originalCwd); + return output; + } + private async promptArgs(args: IArg[], options: AnyObject) { const prompts: Question[] = args .filter(arg => !options[arg.name]) diff --git a/packages/cli/src/commands/cdk.ts b/packages/cli/src/commands/cdk.ts index 5b8bbdb168..e1695823d8 100644 --- a/packages/cli/src/commands/cdk.ts +++ b/packages/cli/src/commands/cdk.ts @@ -9,12 +9,17 @@ import {Project} from 'ts-morph'; // eslint-disable-next-line @typescript-eslint/naming-convention import Base from '../command-base'; import {IacList} from '../enum'; -import {CdkOptions} from '../types'; +import {AnyObject, CdkOptions} from '../types'; const DEFAULT_APP_PATH = 'src/application.ts'; export class Cdk extends Base { static readonly description = 'add arc-cdk'; - + static readonly mcpDescription = ` + Use this command to add arc-cdk to your project. + The arc-cdk is a library that provides a set of tools and utilities to help you build and deploy your applications on AWS using the AWS Cloud Development Kit (CDK). + It provides a set of constructs that can be used to build and deploy your applications on AWS using the AWS CDK. + Refer existing service if any for discovering the parameters not provided by the user, or ask the user directly + `; static readonly flags = { help: flags.boolean({ name: 'help', @@ -49,7 +54,7 @@ export class Cdk extends Base { relativePathToApp: flags.string({ name: 'relativePathToApp', char: 'r', - description: 'Relative path to the service you want to deploy', + description: 'Relative path to the application ts file', required: false, default: this.getDefaultAppPath(), }), @@ -62,6 +67,15 @@ export class Cdk extends Base { }), }; + static readonly mcpFlags = { + workingDir: flags.string({ + name: 'workingDir', + description: + 'path of the microservice or facade folder you want to add cdk to, note that this not the root directory of the monorepo', + required: true, + }), + }; + static getDefaultAppClassName() { let appClassName: string | undefined = undefined; const currentDir = process.cwd(); @@ -88,4 +102,8 @@ export class Cdk extends Base { async run() { await super.generate('cdk', Cdk); } + + static async mcpRun(inputs: AnyObject) { + return Base.mcpResponse(inputs, 'cdk', []); + } } diff --git a/packages/cli/src/commands/extension.ts b/packages/cli/src/commands/extension.ts index 69fb9b01e1..d84ebd5451 100644 --- a/packages/cli/src/commands/extension.ts +++ b/packages/cli/src/commands/extension.ts @@ -5,10 +5,27 @@ import {flags} from '@oclif/command'; // eslint-disable-next-line @typescript-eslint/naming-convention import Base from '../command-base'; -import {ExtensionOptions} from '../types'; +import {AnyObject, ExtensionOptions} from '../types'; +import {buildOptions} from '../utils'; export class Extension extends Base { - static readonly description = 'add an extension'; + static readonly description = + 'This generates a local package in the packages folder of a ARC generated monorepo. This package can then be installed and used inside other modules in the monorepo.'; + static readonly mcpDescription = ` + Use this command to generate or add a local packages in the ARC based monorepo following the ARC standards and best practices. + The package will be created in the packages folder of the monorepo. + You can use 'npm install @local/' to install the package in other modules of the monorepo. + You can not update existing packages using this. + Refer existing packages if any for discovering the parameters not provided by the user, or ask the user directly if no reference package is available and you can not infer it from context + `; + + static readonly mcpFlags = { + workingDir: flags.string({ + name: 'workingDir', + description: 'path of the root directory of the monorepo', + required: false, + }), + }; static readonly flags = { help: flags.boolean({ @@ -24,4 +41,19 @@ export class Extension extends Base { async run() { await super.generate('extension', Extension); } + + static async mcpRun(inputs: AnyObject) { + return Base.mcpResponse( + { + config: JSON.stringify({ + ...buildOptions, + applicationName: inputs.name, + description: `ARC based ${inputs.name}`, + }), + ...inputs, + }, + 'extension', + [inputs.name, '-y'], + ); + } } diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts new file mode 100644 index 0000000000..ea53d4620b --- /dev/null +++ b/packages/cli/src/commands/mcp.ts @@ -0,0 +1,213 @@ +// Copyright (c) 2023 Sourcefuse Technologies +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; +import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; +import {flags} from '@oclif/command'; +import {IFlag, IOptionFlag} from '@oclif/command/lib/flags'; +import {IConfig} from '@oclif/config'; +// eslint-disable-next-line @typescript-eslint/naming-convention +import Environment from 'yeoman-environment'; +import {z} from 'zod'; +import {ICommand} from '../__tests__/helper/command-test.helper'; +// eslint-disable-next-line @typescript-eslint/naming-convention +import Base from '../command-base'; +import {AnyObject, IArg, ICommandWithMcpFlags, PromptFunction} from '../types'; +import {Cdk} from './cdk'; +import {Extension} from './extension'; +import {Microservice} from './microservice'; +import {Scaffold} from './scaffold'; +import {Update} from './update'; + +export class Mcp extends Base<{}> { + commands: ICommandWithMcpFlags[] = []; + constructor( + argv: string[], + config: IConfig, + prompt: PromptFunction, + env?: Environment, + cmds?: ICommandWithMcpFlags[], + ) { + super(argv, config, prompt, env); + if (cmds) { + this.commands = cmds; + } else { + this.commands = [Cdk, Extension, Microservice, Scaffold, Update]; + } + } + static readonly description = ` + Command that runs an MCP server for the sourceloop CLI, this is not supposed to be run directly, but rather used by the MCP client to interact with the CLI commands. + You can use it using the following MCP server configuration: + "sourceloop": { + "command": "npx", + "args": ["@sourceloop/cli", "mcp"], + "timeout": 300 + } + `; + + static readonly flags = { + help: flags.boolean({ + name: 'help', + description: 'show manual pages', + type: 'boolean', + }), + }; + + server: McpServer = new McpServer( + { + name: '@sourceloop/cli', + version: require('../../package.json').version, + }, + { + capabilities: { + logging: {}, // key + tools: {}, + }, + }, + ); + setup() { + this.commands.forEach(command => { + const params: Record = {}; + command.args?.forEach(arg => { + params[arg.name] = this.argToZod(arg); + }); + Object.entries(command.flags ?? {}).forEach(([name, flag]) => { + if (name === 'help') { + // skip help flag as it is not needed in MCP + return; + } + params[name] = this.flagToZod(flag); + }); + if (this._hasMcpFlags(command)) { + Object.entries(command.mcpFlags ?? {}).forEach( + ([name, flag]: [string, IFlag]) => { + params[name] = this.flagToZod(flag, true); + }, + ); + } + this.hookProcessMethods(); + this.server.tool( + command.name, + command.mcpDescription, + params, + async args => { + return command.mcpRun(args as Record); + }, + ); + }); + } + + async run() { + this.setup(); + const transport = new StdioServerTransport(); + await this.server.connect(transport); + } + + private hookProcessMethods() { + // stub process.exit to throw an error + // so that we can catch it in the MCP client + // and handle it gracefully instead of exiting the process + process.exit = () => { + this.server.server + .sendLoggingMessage({ + level: 'debug', + message: `Process exited with code 0 using process.exit()`, + timestamp: new Date().toISOString(), + }) + .catch(err => { + console.error('Error sending exit message:', err); + }); + return undefined as never; + }; + const original = console.error; + console.error = (...args: AnyObject[]) => { + // log errors to the MCP client + this.server.server + .sendLoggingMessage({ + level: 'debug', + message: args.join(' '), + timestamp: new Date().toISOString(), + }) + .catch(err => { + original('Error sending logging message:', err); + }); + original(...args); + }; + console.log = (...args: AnyObject[]) => { + // log messages to the MCP client + this.server.server + .sendLoggingMessage({ + level: 'info', + message: args.join(' '), + timestamp: new Date().toISOString(), + }) + .catch(err => { + console.error('Error sending logging message:', err); + }); + }; + } + + private argToZod(arg: IArg) { + const option = z.string().describe(arg.description ?? ''); + return option; + } + + private flagToZod(flag: IFlag, checkRequired = false) { + let option; + let description = flag.description ?? ''; + switch (true) { + case flag.type === 'boolean': + option = z.boolean().optional(); + option = option.default((flag.default as boolean) ?? false); + break; + case this._isOptionFlag(flag) && flag.options !== undefined: { + // typescript is not able to infer type + const typedFlag = flag as IOptionFlag; + option = z.enum(typedFlag.options as [string, ...string[]]); + description += ` (options: ${typedFlag.options?.join(', ')})`; + break; + } + case this._isOptionFlag(flag) && flag.options === undefined: + option = z.string().optional(); + break; + default: + throw new Error( + `Unsupported flag type: ${flag.type}. Supported types are boolean, option, enum, and integer.`, + ); + } + + if (flag.dependsOn) { + description += ` (required if ${flag.dependsOn.join(', ')} ${flag.dependsOn.length > 1 ? 'are' : 'is'} provided)`; + option = option.optional(); + } else { + description += ' (required)'; + } + + if (checkRequired && !flag.required) { + description += ' (not required)'; + option = option.optional(); + } + + if (flag.exclusive) { + description += ` (can not be provided with ${flag.exclusive.join(', ')} option${flag.exclusive.length > 1 ? 's' : ''})`; + option = option.optional(); + } + + return option.describe(description); + } + + private _hasMcpFlags( + command: T, + ): command is T & ICommandWithMcpFlags { + return ( + 'mcpFlags' in command && + typeof command.mcpFlags === 'object' && + command.mcpFlags !== null + ); + } + + private _isOptionFlag(flag: IFlag): flag is IOptionFlag { + return flag.type === 'option'; + } +} diff --git a/packages/cli/src/commands/microservice.ts b/packages/cli/src/commands/microservice.ts index 2493a747be..b148cff3cd 100644 --- a/packages/cli/src/commands/microservice.ts +++ b/packages/cli/src/commands/microservice.ts @@ -6,11 +6,27 @@ import {flags} from '@oclif/command'; // eslint-disable-next-line @typescript-eslint/naming-convention import Base from '../command-base'; import {DATASOURCES, SERVICES} from '../enum'; -import {MicroserviceOptions} from '../types'; +import {AnyObject, MicroserviceOptions} from '../types'; +import {buildOptions} from '../utils'; export class Microservice extends Base { - static readonly description = 'add a microservice'; - + static readonly description = + 'Add a microservice in the services or facade folder of a ARC generated monorepo. This can also optionally add migrations for the same microservice.'; + static readonly mcpDescription = ` + Use this command to generate or add a microservice in the ARC based monorepo following the ARC standards and best practices. + The generated microservice can be used as a standalone service or as a facade in the monorepo. + It can also generate migrations for the same microservice. + The microservice will be created in the services or facades folder of the monorepo depending on the options passed. + It can also generate datasource and import migrations from the ARC services code. It can not update existing services or facades, + for example, you can not run it again just with migration option as it would fail, saying the service already exists. + Refer existing service if any for discovering the parameters not provided by the user, or ask the user directly`; + static readonly mcpFlags = { + workingDir: flags.string({ + name: 'workingDir', + description: 'Path of the root directory of the monorepo', + required: false, + }), + }; static readonly flags = { help: flags.boolean({ name: 'help', @@ -18,7 +34,7 @@ export class Microservice extends Base { }), facade: flags.boolean({ name: 'facade', - description: 'Create as facade', + description: 'Create as facade inside the facades folder', allowNo: true, }), baseOnService: flags.boolean({ @@ -61,7 +77,6 @@ export class Microservice extends Base { required: false, exclusive: ['facade'], }), - includeMigrations: flags.boolean({ name: 'includeMigrations', description: 'Include base microservice migrations', @@ -81,4 +96,20 @@ export class Microservice extends Base { async run() { await super.generate('microservice', Microservice); } + + static async mcpRun(inputs: AnyObject) { + return Base.mcpResponse( + { + ...inputs, + config: JSON.stringify({ + ...buildOptions, + applicationName: inputs.name, + description: `ARC based ${inputs.name}`, + }), + inMcp: true, + }, + 'microservice', + [inputs.name, '-y'], + ); + } } diff --git a/packages/cli/src/commands/scaffold.ts b/packages/cli/src/commands/scaffold.ts index c650b4bc54..9966da6f5f 100644 --- a/packages/cli/src/commands/scaffold.ts +++ b/packages/cli/src/commands/scaffold.ts @@ -5,10 +5,15 @@ import {flags} from '@oclif/command'; // eslint-disable-next-line @typescript-eslint/naming-convention import Base from '../command-base'; -import {ScaffoldOptions} from '../types'; +import {AnyObject, ScaffoldOptions} from '../types'; export class Scaffold extends Base { - static readonly description = 'create a project scaffold'; + static readonly description = + 'Setup a ARC based monorepo using npm workspaces with an empty services, facades and packages folder'; + static readonly mcpDescription = ` + Use this command to generate a npm workspaces based monorepo following the ARC standards and best practices. + The generated monorepo contains services, facades and packages folders by default. + `; static readonly flags = { help: flags.boolean({ @@ -56,4 +61,8 @@ export class Scaffold extends Base { async run() { await super.generate('scaffold', Scaffold); } + + static async mcpRun(inputs: AnyObject) { + return Base.mcpResponse(inputs, 'scaffold', []); + } } diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 25477372ad..ee027fd3d5 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -6,9 +6,24 @@ import {flags} from '@oclif/command'; // eslint-disable-next-line @typescript-eslint/naming-convention import Base from '../command-base'; import {UpdateOptions} from '../generators/update/types/types'; +import {AnyObject} from '../types'; export class Update extends Base { static readonly description = 'update the dependencies of a loopback project'; + static readonly mcpDescription = ` + Use this command to update the dependencies of a LoopBack project. + It will update the dependencies in the package.json file and install the latest versions of the dependencies. + This command is useful when you want to keep your project up-to-date with the latest versions of the dependencies. + `; + + static readonly mcpFlags = { + workingDir: flags.string({ + name: 'workingDir', + description: + 'Absolute path of the root directory of the monorepo, relative paths are not supported yet', + required: false, + }), + }; static readonly flags = { help: flags.boolean({ @@ -21,4 +36,8 @@ export class Update extends Base { async run() { await super.generate('update', Update); } + + static async mcpRun(inputs: AnyObject) { + return Base.mcpResponse(inputs, 'update', []); + } } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 61d91d13ba..187194473e 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -2,6 +2,7 @@ // // This software is released under the MIT License. // https://opensource.org/licenses/MIT +import {IFlag} from '@oclif/command/lib/flags'; import {Question} from 'inquirer'; // eslint-disable-next-line @typescript-eslint/naming-convention import Generator from 'yeoman-generator'; @@ -80,3 +81,22 @@ export type CommandTestCase = { output: string | boolean | AnyObject; }[]; }; + +export type ICommandWithMcpFlags = { + mcpFlags?: Record>; + args?: IArg[]; + flags?: Record> | undefined; + name: string; + mcpDescription: string; + mcpRun: (args: Record) => Promise; +}; + +export type IArg = { + name: string; + description?: string; + required?: boolean; +}; + +export type McpTextResponse = { + content: {type: 'text'; text: string; isError?: boolean}[]; +}; diff --git a/packages/cli/src/utilities/mcp-adapter.ts b/packages/cli/src/utilities/mcp-adapter.ts new file mode 100644 index 0000000000..18224f932d --- /dev/null +++ b/packages/cli/src/utilities/mcp-adapter.ts @@ -0,0 +1,16 @@ +import {Answers} from 'yeoman-environment'; +// eslint-disable-next-line @typescript-eslint/naming-convention +import TerminalAdapter from 'yeoman-environment/lib/adapter'; + +export class McpAdapter extends TerminalAdapter { + async prompt( + questions: TerminalAdapter.Questions, + answers?: T, + cb?: (answers: T) => void | Promise, + ) { + this.console.error(JSON.stringify(questions, undefined, 2)); + throw Error( + `The generator is expecting an input from prompt, please check the inputs`, + ); + } +} diff --git a/packages/cli/src/utilities/yeoman.ts b/packages/cli/src/utilities/yeoman.ts new file mode 100644 index 0000000000..aa35abf477 --- /dev/null +++ b/packages/cli/src/utilities/yeoman.ts @@ -0,0 +1,36 @@ +// eslint-disable-next-line @typescript-eslint/naming-convention +import Environment, {createEnv} from 'yeoman-environment'; +import {AnyObject} from '../types'; +import {McpAdapter} from './mcp-adapter'; +export async function yeomanRun( + workspace: string, + name: string, + args: string[] | undefined, + opts: AnyObject, +) { + const env = getEnv(workspace, name); + await runWithEnv(env, name, args, opts); +} + +function getEnv(workspace: string, name: string) { + const env = createEnv([], {cwd: workspace}, new McpAdapter({})); + registerGenerators(env, name); + return env; +} + +async function runWithEnv( + env: Environment, + name: string, + args: string[] | undefined, + opts: AnyObject, +) { + const yeomanArgs: [string, ...string[]] = [`sl:${name}`, ...(args ?? [])]; + return env.run(yeomanArgs, opts); +} + +function registerGenerators(env: Environment, generator: string) { + env.register( + require.resolve(`../generators/${generator}/index`), + `sl:${generator}`, + ); +} diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index b04654f421..30c907ef1b 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -34,3 +34,14 @@ export function getDependencyVersion( throw new Error(`Missing dependency version in config: ${name}`); } } + +export const buildOptions = { + loopbackBuild: true, + eslint: true, + prettier: true, + mocha: true, + vscode: true, + docker: true, + repositories: true, + services: true, +}; diff --git a/packages/cli/tsconfig.tsbuildinfo b/packages/cli/tsconfig.tsbuildinfo new file mode 100644 index 0000000000..2e8bdb4784 --- /dev/null +++ b/packages/cli/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app-generator.ts","./src/base-generator.ts","./src/command-base.ts","./src/enum.ts","./src/extension-generator.ts","./src/index.ts","./src/types.ts","./src/update-generator.ts","./src/utils.ts","./src/__tests__/commands/extension.test.ts","./src/__tests__/commands/mcp.test.ts","./src/__tests__/commands/microservice.test.ts","./src/__tests__/commands/scaffold.test.ts","./src/__tests__/commands/update.test.ts","./src/__tests__/helper/command-test.helper.ts","./src/__tests__/suite/extension.ts","./src/__tests__/suite/mcp.ts","./src/__tests__/suite/microservice-options.ts","./src/__tests__/suite/microservice-prompts.ts","./src/__tests__/suite/scaffold.ts","./src/__tests__/suite/update.ts","./src/commands/cdk.ts","./src/commands/extension.ts","./src/commands/mcp.ts","./src/commands/microservice.ts","./src/commands/scaffold.ts","./src/commands/update.ts","./src/generators/backstage-integration/index.ts","./src/generators/cdk/index.ts","./src/generators/extension/index.ts","./src/generators/microservice/index.ts","./src/generators/scaffold/index.ts","./src/generators/update/index.ts","./src/generators/update/types/types.ts","./src/types/app.d.ts","./src/types/base.d.ts","./src/types/extension.d.ts","./src/types/project.d.ts","./src/types/update.d.ts","./src/utilities/command-adapter.ts","./src/utilities/index.ts","./src/utilities/yeoman-runner.ts"],"errors":true,"version":"5.8.3"} \ No newline at end of file