From 44db895303a9209416236e3d519c8a609ad85f61 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:51:57 +0100 Subject: [PATCH 1/8] chore: init config for changeset --- .changeset/README.md | 107 ++++++++++++++++++++++++++++++ .changeset/config.json | 11 +++ .changeset/flat-candies-wonder.md | 5 ++ 3 files changed, 123 insertions(+) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .changeset/flat-candies-wonder.md diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..0cd956f9 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,107 @@ +# Changesets + +This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos or single-package repos to help version and publish code. Full documentation is available in the [Changesets repository](https://github.com/changesets/changesets). + +## What are Changesets? + +Changesets are a way to track changes to packages in your repository. Each changeset: + +- Describes the changes you've made +- Specifies the type of version bump needed (patch, minor, or major) +- Connects these changes with release notes +- Automates the versioning and publishing process + +## How to Use Changesets in Task Master + +### 2. Making Changes + +1. Create a new branch for your changes +2. Make your code changes +3. Write tests and ensure all tests pass + +### 3. Creating a Changeset + +After making changes, create a changeset by running: + +```bash +npx changeset +``` + +This will: + +- Walk you through a CLI to describe your changes +- Ask you to select impact level (patch, minor, major) +- Create a markdown file in the `.changeset` directory + +### 4. Impact Level Guidelines + +When choosing the impact level for your changes: + +- **Patch**: Bug fixes and minor changes that don't affect how users interact with the system + - Example: Fixing a typo in output text, optimizing code without changing behavior +- **Minor**: New features or enhancements that don't break existing functionality + - Example: Adding a new flag to an existing command, adding new task metadata fields +- **Major**: Breaking changes that require users to update their usage + - Example: Renaming a command, changing the format of the tasks.json file + +### 5. Writing Good Changeset Descriptions + +Your changeset description should: + +- Be written for end-users, not developers +- Clearly explain what changed and why +- Include any migration steps or backward compatibility notes +- Reference related issues or pull requests with `#issue-number` + +Examples: + +```md +# Good + +Added new `--research` flag to the `expand` command that uses Perplexity AI +to provide research-backed task expansions. Requires PERPLEXITY_API_KEY +environment variable. + +# Not Good + +Fixed stuff and added new flag +``` + +### 6. Committing Your Changes + +Commit both your code changes and the generated changeset file: + +```bash +git add . +git commit -m "Add feature X with changeset" +git push +``` + +### 7. Pull Request Process + +1. Open a pull request +2. Ensure CI passes +3. Await code review +4. Once approved and merged, your changeset will be used during the next release + +## Release Process (for Maintainers) + +When it's time to make a release: + +1. Ensure all desired changesets are merged +2. Run `npx changeset version` to update package versions and changelog +3. Review and commit the changes +4. Run `npm publish` to publish to npm + +This can be automated through Github Actions + +## Common Issues and Solutions + +- **Merge Conflicts in Changeset Files**: Resolve just like any other merge conflict +- **Multiple Changes in One PR**: Create multiple changesets if changes affect different areas +- **Accidentally Committed Without Changeset**: Create the changeset after the fact and commit it separately + +## Additional Resources + +- [Changesets Documentation](https://github.com/changesets/changesets) +- [Common Questions](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..d88011f6 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/flat-candies-wonder.md b/.changeset/flat-candies-wonder.md new file mode 100644 index 00000000..3256a26f --- /dev/null +++ b/.changeset/flat-candies-wonder.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": patch +--- + +Added changeset config #39 From eafdb47418b444c03c092f653b438cc762d4bca8 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:43:08 +0100 Subject: [PATCH 2/8] feat: add github actions to automate github and npm releases --- .changeset/config.json | 5 +- .changeset/nice-cougars-itch.md | 5 + .github/release.yml | 28 + package-lock.json | 1040 ++++++++++++++++++++++++++++++- package.json | 8 +- 5 files changed, 1081 insertions(+), 5 deletions(-) create mode 100644 .changeset/nice-cougars-itch.md create mode 100644 .github/release.yml diff --git a/.changeset/config.json b/.changeset/config.json index d88011f6..c2180ffa 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,9 @@ { "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", - "changelog": "@changesets/cli/changelog", + "changelog": [ + "@changesets/changelog-github", + { "repo": "eyaltoledano/claude-task-master" } + ], "commit": false, "fixed": [], "linked": [], diff --git a/.changeset/nice-cougars-itch.md b/.changeset/nice-cougars-itch.md new file mode 100644 index 00000000..aebc76bf --- /dev/null +++ b/.changeset/nice-cougars-itch.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": minor +--- + +add github actions to automate github and npm releases diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..68bec635 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,28 @@ +name: Release +on: + push: + branches: + - main + - next +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install Dependencies + run: npm install + + - name: Create Release Pull Request or Publish to npm + uses: changesets/action@1.4.10 + with: + publish: npm run release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package-lock.json b/package-lock.json index acf6ee8d..9fe24aaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "task-master-ai", - "version": "0.9.16", + "version": "0.9.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "task-master-ai", - "version": "0.9.16", + "version": "0.9.18", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", @@ -25,6 +25,8 @@ "task-master-init": "bin/task-master-init.js" }, "devDependencies": { + "@changesets/changelog-github": "^0.5.1", + "@changesets/cli": "^2.28.1", "@types/jest": "^29.5.14", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", @@ -495,6 +497,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", @@ -550,6 +565,350 @@ "dev": true, "license": "MIT" }, + "node_modules/@changesets/apply-release-plan": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.10.tgz", + "integrity": "sha512-wNyeIJ3yDsVspYvHnEz1xQDq18D9ifed3lI+wxRQRK4pArUcuHgCTrHv0QRnnwjhVCQACxZ+CBih3wgOct6UXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/config": "^3.1.1", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.2", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "detect-indent": "^6.0.0", + "fs-extra": "^7.0.1", + "lodash.startcase": "^4.4.0", + "outdent": "^0.5.0", + "prettier": "^2.7.1", + "resolve-from": "^5.0.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/apply-release-plan/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.6.tgz", + "integrity": "sha512-Frkj8hWJ1FRZiY3kzVCKzS0N5mMwWKwmv9vpam7vt8rZjLL1JMthdh6pSDVSPumHPshTTkKZ0VtNbE0cJHZZUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/assemble-release-plan/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", + "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0" + } + }, + "node_modules/@changesets/changelog-github": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.5.1.tgz", + "integrity": "sha512-BVuHtF+hrhUScSoHnJwTELB4/INQxVFc+P/Qdt20BLiBFIHFJDDUaGsZw+8fQeJTRP5hJZrzpt3oZWh0G19rAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/get-github-info": "^0.6.0", + "@changesets/types": "^6.1.0", + "dotenv": "^8.1.0" + } + }, + "node_modules/@changesets/changelog-github/node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/cli": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.28.1.tgz", + "integrity": "sha512-PiIyGRmSc6JddQJe/W1hRPjiN4VrMvb2VfQ6Uydy2punBioQrsxppyG5WafinKcW1mT0jOe/wU4k9Zy5ff21AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^7.0.10", + "@changesets/assemble-release-plan": "^6.0.6", + "@changesets/changelog-git": "^0.2.1", + "@changesets/config": "^3.1.1", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/get-release-plan": "^4.0.8", + "@changesets/git": "^3.0.2", + "@changesets/logger": "^0.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.3", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@changesets/write": "^0.4.0", + "@manypkg/get-packages": "^1.1.3", + "ansi-colors": "^4.1.3", + "ci-info": "^3.7.0", + "enquirer": "^2.4.1", + "external-editor": "^3.1.0", + "fs-extra": "^7.0.1", + "mri": "^1.2.0", + "p-limit": "^2.2.0", + "package-manager-detector": "^0.2.0", + "picocolors": "^1.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.3", + "spawndamnit": "^3.0.1", + "term-size": "^2.1.0" + }, + "bin": { + "changeset": "bin.js" + } + }, + "node_modules/@changesets/cli/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@changesets/cli/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/config": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.1.tgz", + "integrity": "sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/logger": "^0.1.1", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1", + "micromatch": "^4.0.8" + } + }, + "node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "dev": true, + "license": "MIT", + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz", + "integrity": "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "picocolors": "^1.1.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/get-dependents-graph/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@changesets/get-github-info": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.6.0.tgz", + "integrity": "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dataloader": "^1.4.0", + "node-fetch": "^2.5.0" + } + }, + "node_modules/@changesets/get-release-plan": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.8.tgz", + "integrity": "sha512-MM4mq2+DQU1ZT7nqxnpveDMTkMBLnwNX44cX7NSxlXmr7f8hO6/S2MXNiXG54uf/0nYnefv0cfy4Czf/ZL/EKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/assemble-release-plan": "^6.0.6", + "@changesets/config": "^3.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.3", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/get-version-range-type": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/git": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.2.tgz", + "integrity": "sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" + } + }, + "node_modules/@changesets/logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/parse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.1.tgz", + "integrity": "sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "js-yaml": "^3.13.1" + } + }, + "node_modules/@changesets/pre": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz", + "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, + "node_modules/@changesets/read": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.3.tgz", + "integrity": "sha512-9H4p/OuJ3jXEUTjaVGdQEhBdqoT2cO5Ts95JTFsQyawmKzpL8FnIeJSyhTDPW1MBRDnwZlHFEM9SpPwJDY5wIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^3.0.2", + "@changesets/logger": "^0.1.1", + "@changesets/parse": "^0.4.1", + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0", + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz", + "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz", + "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/write": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz", + "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "human-id": "^4.1.1", + "prettier": "^2.7.1" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -988,6 +1347,116 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" + } + }, + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1231,6 +1700,16 @@ "node": ">=8" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1311,6 +1790,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -1447,6 +1936,19 @@ "dev": true, "license": "MIT" }, + "node_modules/better-path-resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", + "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-windows": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/boxen": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", @@ -1654,6 +2156,13 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1984,6 +2493,13 @@ "node": ">= 12" } }, + "node_modules/dataloader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", + "integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2036,6 +2552,16 @@ "node": ">=0.4.0" } }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2067,6 +2593,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -2119,6 +2658,43 @@ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2290,6 +2866,45 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/extendable-error": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", + "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "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", @@ -2304,6 +2919,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2446,6 +3071,21 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2591,6 +3231,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2601,6 +3254,27 @@ "node": ">=4" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2710,6 +3384,16 @@ "dev": true, "license": "MIT" }, + "node_modules/human-id": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz", + "integrity": "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2729,6 +3413,29 @@ "ms": "^2.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2801,6 +3508,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2820,6 +3537,19 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -2855,6 +3585,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-subdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", + "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "better-path-resolve": "1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -2867,6 +3610,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3608,6 +4361,16 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3648,6 +4411,13 @@ "node": ">=8" } }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -3753,6 +4523,16 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3856,6 +4636,16 @@ "node": ">=12.0.0" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4046,6 +4836,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/outdent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", + "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4091,6 +4911,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -4101,6 +4931,16 @@ "node": ">=6" } }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4157,6 +4997,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4177,6 +5027,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -4200,6 +5060,22 @@ "node": ">=8" } }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -4275,6 +5151,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -4282,6 +5196,39 @@ "dev": true, "license": "MIT" }, + "node_modules/read-yaml-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", + "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-yaml-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4362,6 +5309,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4521,6 +5510,17 @@ "source-map": "^0.6.0" } }, + "node_modules/spawndamnit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", + "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4715,6 +5715,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4746,6 +5759,19 @@ "tinycolor2": "^1.0.0" } }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -4794,6 +5820,16 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/package.json b/package.json index 94853215..88db54cc 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage", "prepare-package": "node scripts/prepare-package.js", "prepublishOnly": "npm run prepare-package", - "prepare": "chmod +x bin/task-master.js bin/task-master-init.js" + "prepare": "chmod +x bin/task-master.js bin/task-master-init.js", + "changeset": "changeset", + "release": "changeset publish" }, "keywords": [ "claude", @@ -66,10 +68,12 @@ "whatwg-url": "^11.0.0" }, "devDependencies": { + "@changesets/changelog-github": "^0.5.1", + "@changesets/cli": "^2.28.1", "@types/jest": "^29.5.14", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", "mock-fs": "^5.5.0", "supertest": "^7.1.0" } -} \ No newline at end of file +} From f3e6f0b70eb8bbb8d2670c570fe350cdc8734c26 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:31:36 +0000 Subject: [PATCH 3/8] feat(wip): initial commits for sub-tasks 1,2,3 for task 23 --- mcp-server/README.md | 170 ++++ mcp-server/server.js | 44 + mcp-server/src/api-handlers.js | 970 ++++++++++++++++++ mcp-server/src/auth.js | 285 ++++++ mcp-server/src/context-manager.js | 873 ++++++++++++++++ mcp-server/src/index.js | 366 +++++++ package-lock.json | 1558 ++++++++++++++++++++++++++++- package.json | 18 +- tasks/task_023.txt | 115 +++ tasks/tasks.json | 64 +- 10 files changed, 4418 insertions(+), 45 deletions(-) create mode 100644 mcp-server/README.md create mode 100755 mcp-server/server.js create mode 100644 mcp-server/src/api-handlers.js create mode 100644 mcp-server/src/auth.js create mode 100644 mcp-server/src/context-manager.js create mode 100644 mcp-server/src/index.js diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 00000000..9c8b1300 --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,170 @@ +# Task Master MCP Server + +This module implements a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for Task Master, allowing external applications to access Task Master functionality and context through a standardized API. + +## Features + +- MCP-compliant server implementation using FastMCP +- RESTful API for context management +- Authentication and authorization for secure access +- Context storage and retrieval with metadata and tagging +- Context windowing and truncation for handling size limits +- Integration with Task Master for task management operations + +## Installation + +The MCP server is included with Task Master. Install Task Master globally to use the MCP server: + +```bash +npm install -g task-master-ai +``` + +Or use it locally: + +```bash +npm install task-master-ai +``` + +## Environment Configuration + +The MCP server can be configured using environment variables or a `.env` file: + +| Variable | Description | Default | +| -------------------- | ---------------------------------------- | ----------------------------- | +| `MCP_SERVER_PORT` | Port for the MCP server | 3000 | +| `MCP_SERVER_HOST` | Host for the MCP server | localhost | +| `MCP_CONTEXT_DIR` | Directory for context storage | ./mcp-server/contexts | +| `MCP_API_KEYS_FILE` | File for API key storage | ./mcp-server/api-keys.json | +| `MCP_JWT_SECRET` | Secret for JWT token generation | task-master-mcp-server-secret | +| `MCP_JWT_EXPIRATION` | JWT token expiration time | 24h | +| `LOG_LEVEL` | Logging level (debug, info, warn, error) | info | + +## Getting Started + +### Starting the Server + +Start the MCP server as a standalone process: + +```bash +npx task-master-mcp-server +``` + +Or start it programmatically: + +```javascript +import { TaskMasterMCPServer } from "task-master-ai/mcp-server"; + +const server = new TaskMasterMCPServer(); +await server.start({ port: 3000, host: "localhost" }); +``` + +### Authentication + +The MCP server uses API key authentication with JWT tokens for secure access. A default admin API key is generated on first startup and can be found in the `api-keys.json` file. + +To get a JWT token: + +```bash +curl -X POST http://localhost:3000/auth/token \ + -H "x-api-key: YOUR_API_KEY" +``` + +Use the token for subsequent requests: + +```bash +curl http://localhost:3000/mcp/tools \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" +``` + +### Creating a New API Key + +Admin users can create new API keys: + +```bash +curl -X POST http://localhost:3000/auth/api-keys \ + -H "Authorization: Bearer ADMIN_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"clientId": "user1", "role": "user"}' +``` + +## Available MCP Endpoints + +The MCP server implements the following MCP-compliant endpoints: + +### Context Management + +- `GET /mcp/context` - List all contexts +- `POST /mcp/context` - Create a new context +- `GET /mcp/context/{id}` - Get a specific context +- `PUT /mcp/context/{id}` - Update a context +- `DELETE /mcp/context/{id}` - Delete a context + +### Models + +- `GET /mcp/models` - List available models +- `GET /mcp/models/{id}` - Get model details + +### Execution + +- `POST /mcp/execute` - Execute an operation with context + +## Available MCP Tools + +The MCP server provides the following tools: + +### Context Tools + +- `createContext` - Create a new context +- `getContext` - Retrieve a context by ID +- `updateContext` - Update an existing context +- `deleteContext` - Delete a context +- `listContexts` - List available contexts +- `addTags` - Add tags to a context +- `truncateContext` - Truncate a context to a maximum size + +### Task Master Tools + +- `listTasks` - List tasks from Task Master +- `getTaskDetails` - Get detailed task information +- `executeWithContext` - Execute operations using context + +## Examples + +### Creating a Context + +```javascript +// Using the MCP client +const client = new MCPClient("http://localhost:3000"); +await client.authenticate("YOUR_API_KEY"); + +const context = await client.createContext("my-context", { + title: "My Project", + tasks: ["Implement feature X", "Fix bug Y"], +}); +``` + +### Executing an Operation with Context + +```javascript +// Using the MCP client +const result = await client.execute("generateTask", "my-context", { + title: "New Task", + description: "Create a new task based on context", +}); +``` + +## Integration with Other Tools + +The Task Master MCP server can be integrated with other MCP-compatible tools and clients: + +- LLM applications that support the MCP protocol +- Task management systems that support context-aware operations +- Development environments with MCP integration + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/mcp-server/server.js b/mcp-server/server.js new file mode 100755 index 00000000..ed5c3c69 --- /dev/null +++ b/mcp-server/server.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +import TaskMasterMCPServer from "./src/index.js"; +import dotenv from "dotenv"; +import { logger } from "../scripts/modules/utils.js"; + +// Load environment variables +dotenv.config(); + +// Constants +const PORT = process.env.MCP_SERVER_PORT || 3000; +const HOST = process.env.MCP_SERVER_HOST || "localhost"; + +/** + * Start the MCP server + */ +async function startServer() { + const server = new TaskMasterMCPServer(); + + // Handle graceful shutdown + process.on("SIGINT", async () => { + logger.info("Received SIGINT, shutting down gracefully..."); + await server.stop(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + logger.info("Received SIGTERM, shutting down gracefully..."); + await server.stop(); + process.exit(0); + }); + + try { + await server.start({ port: PORT, host: HOST }); + logger.info(`MCP server running at http://${HOST}:${PORT}`); + logger.info("Press Ctrl+C to stop"); + } catch (error) { + logger.error(`Failed to start MCP server: ${error.message}`); + process.exit(1); + } +} + +// Start the server +startServer(); diff --git a/mcp-server/src/api-handlers.js b/mcp-server/src/api-handlers.js new file mode 100644 index 00000000..ead546f2 --- /dev/null +++ b/mcp-server/src/api-handlers.js @@ -0,0 +1,970 @@ +import { z } from "zod"; +import { logger } from "../../scripts/modules/utils.js"; +import ContextManager from "./context-manager.js"; + +/** + * MCP API Handlers class + * Implements handlers for the MCP API endpoints + */ +class MCPApiHandlers { + constructor(server) { + this.server = server; + this.contextManager = new ContextManager(); + this.logger = logger; + + // Bind methods + this.registerEndpoints = this.registerEndpoints.bind(this); + this.setupContextHandlers = this.setupContextHandlers.bind(this); + this.setupModelHandlers = this.setupModelHandlers.bind(this); + this.setupExecuteHandlers = this.setupExecuteHandlers.bind(this); + + // Register all handlers + this.registerEndpoints(); + } + + /** + * Register all MCP API endpoints + */ + registerEndpoints() { + this.setupContextHandlers(); + this.setupModelHandlers(); + this.setupExecuteHandlers(); + + this.logger.info("Registered all MCP API endpoint handlers"); + } + + /** + * Set up handlers for the /context endpoint + */ + setupContextHandlers() { + // Add a tool to create context + this.server.addTool({ + name: "createContext", + description: + "Create a new context with the given data and optional metadata", + parameters: z.object({ + contextId: z.string().describe("Unique identifier for the context"), + data: z.any().describe("The context data to store"), + metadata: z + .object({}) + .optional() + .describe("Optional metadata for the context"), + }), + execute: async (args) => { + try { + const context = await this.contextManager.createContext( + args.contextId, + args.data, + args.metadata || {} + ); + return { success: true, context }; + } catch (error) { + this.logger.error(`Error creating context: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to get context + this.server.addTool({ + name: "getContext", + description: + "Retrieve a context by its ID, optionally a specific version", + parameters: z.object({ + contextId: z.string().describe("The ID of the context to retrieve"), + versionId: z + .string() + .optional() + .describe("Optional specific version ID to retrieve"), + }), + execute: async (args) => { + try { + const context = await this.contextManager.getContext( + args.contextId, + args.versionId + ); + return { success: true, context }; + } catch (error) { + this.logger.error(`Error retrieving context: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to update context + this.server.addTool({ + name: "updateContext", + description: "Update an existing context with new data and/or metadata", + parameters: z.object({ + contextId: z.string().describe("The ID of the context to update"), + data: z + .any() + .optional() + .describe("New data to update the context with"), + metadata: z + .object({}) + .optional() + .describe("New metadata to update the context with"), + createNewVersion: z + .boolean() + .optional() + .default(true) + .describe( + "Whether to create a new version (true) or update in place (false)" + ), + }), + execute: async (args) => { + try { + const context = await this.contextManager.updateContext( + args.contextId, + args.data || {}, + args.metadata || {}, + args.createNewVersion + ); + return { success: true, context }; + } catch (error) { + this.logger.error(`Error updating context: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to delete context + this.server.addTool({ + name: "deleteContext", + description: "Delete a context by its ID", + parameters: z.object({ + contextId: z.string().describe("The ID of the context to delete"), + }), + execute: async (args) => { + try { + const result = await this.contextManager.deleteContext( + args.contextId + ); + return { success: result }; + } catch (error) { + this.logger.error(`Error deleting context: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to list contexts with pagination and advanced filtering + this.server.addTool({ + name: "listContexts", + description: + "List available contexts with filtering, pagination and sorting", + parameters: z.object({ + // Filtering parameters + filters: z + .object({ + tag: z.string().optional().describe("Filter contexts by tag"), + metadataKey: z + .string() + .optional() + .describe("Filter contexts by metadata key"), + metadataValue: z + .string() + .optional() + .describe("Filter contexts by metadata value"), + createdAfter: z + .string() + .optional() + .describe("Filter contexts created after date (ISO format)"), + updatedAfter: z + .string() + .optional() + .describe("Filter contexts updated after date (ISO format)"), + }) + .optional() + .describe("Filters to apply to the context list"), + + // Pagination parameters + limit: z + .number() + .optional() + .default(100) + .describe("Maximum number of contexts to return"), + offset: z + .number() + .optional() + .default(0) + .describe("Number of contexts to skip"), + + // Sorting parameters + sortBy: z + .string() + .optional() + .default("updated") + .describe("Field to sort by (id, created, updated, size)"), + sortDirection: z + .enum(["asc", "desc"]) + .optional() + .default("desc") + .describe("Sort direction"), + + // Search query + query: z.string().optional().describe("Free text search query"), + }), + execute: async (args) => { + try { + const result = await this.contextManager.listContexts(args); + return { + success: true, + ...result, + }; + } catch (error) { + this.logger.error(`Error listing contexts: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to get context history + this.server.addTool({ + name: "getContextHistory", + description: "Get the version history of a context", + parameters: z.object({ + contextId: z + .string() + .describe("The ID of the context to get history for"), + }), + execute: async (args) => { + try { + const history = await this.contextManager.getContextHistory( + args.contextId + ); + return { + success: true, + history, + contextId: args.contextId, + }; + } catch (error) { + this.logger.error(`Error getting context history: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to merge contexts + this.server.addTool({ + name: "mergeContexts", + description: "Merge multiple contexts into a new context", + parameters: z.object({ + contextIds: z + .array(z.string()) + .describe("Array of context IDs to merge"), + newContextId: z.string().describe("ID for the new merged context"), + metadata: z + .object({}) + .optional() + .describe("Optional metadata for the new context"), + }), + execute: async (args) => { + try { + const mergedContext = await this.contextManager.mergeContexts( + args.contextIds, + args.newContextId, + args.metadata || {} + ); + return { + success: true, + context: mergedContext, + }; + } catch (error) { + this.logger.error(`Error merging contexts: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to add tags to a context + this.server.addTool({ + name: "addTags", + description: "Add tags to a context", + parameters: z.object({ + contextId: z.string().describe("The ID of the context to tag"), + tags: z + .array(z.string()) + .describe("Array of tags to add to the context"), + }), + execute: async (args) => { + try { + const context = await this.contextManager.addTags( + args.contextId, + args.tags + ); + return { success: true, context }; + } catch (error) { + this.logger.error(`Error adding tags to context: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to remove tags from a context + this.server.addTool({ + name: "removeTags", + description: "Remove tags from a context", + parameters: z.object({ + contextId: z + .string() + .describe("The ID of the context to remove tags from"), + tags: z + .array(z.string()) + .describe("Array of tags to remove from the context"), + }), + execute: async (args) => { + try { + const context = await this.contextManager.removeTags( + args.contextId, + args.tags + ); + return { success: true, context }; + } catch (error) { + this.logger.error( + `Error removing tags from context: ${error.message}` + ); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to truncate context + this.server.addTool({ + name: "truncateContext", + description: "Truncate a context to a maximum size", + parameters: z.object({ + contextId: z.string().describe("The ID of the context to truncate"), + maxSize: z + .number() + .describe("Maximum size (in characters) for the context"), + strategy: z + .enum(["start", "end", "middle"]) + .default("end") + .describe("Truncation strategy: start, end, or middle"), + }), + execute: async (args) => { + try { + const context = await this.contextManager.truncateContext( + args.contextId, + args.maxSize, + args.strategy + ); + return { success: true, context }; + } catch (error) { + this.logger.error(`Error truncating context: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + this.logger.info("Registered context endpoint handlers"); + } + + /** + * Set up handlers for the /models endpoint + */ + setupModelHandlers() { + // Add a tool to list available models + this.server.addTool({ + name: "listModels", + description: "List all available models with their capabilities", + parameters: z.object({}), + execute: async () => { + // Here we could get models from a more dynamic source + // For now, returning static list of models supported by Task Master + const models = [ + { + id: "claude-3-opus-20240229", + provider: "anthropic", + capabilities: [ + "text-generation", + "embeddings", + "context-window-100k", + ], + }, + { + id: "claude-3-7-sonnet-20250219", + provider: "anthropic", + capabilities: [ + "text-generation", + "embeddings", + "context-window-200k", + ], + }, + { + id: "sonar-medium-online", + provider: "perplexity", + capabilities: ["text-generation", "web-search", "research"], + }, + ]; + + return { success: true, models }; + }, + }); + + // Add a tool to get model details + this.server.addTool({ + name: "getModelDetails", + description: "Get detailed information about a specific model", + parameters: z.object({ + modelId: z.string().describe("The ID of the model to get details for"), + }), + execute: async (args) => { + // Here we could get model details from a more dynamic source + // For now, returning static information + const modelsMap = { + "claude-3-opus-20240229": { + id: "claude-3-opus-20240229", + provider: "anthropic", + capabilities: [ + "text-generation", + "embeddings", + "context-window-100k", + ], + maxTokens: 100000, + temperature: { min: 0, max: 1, default: 0.7 }, + pricing: { input: 0.000015, output: 0.000075 }, + }, + "claude-3-7-sonnet-20250219": { + id: "claude-3-7-sonnet-20250219", + provider: "anthropic", + capabilities: [ + "text-generation", + "embeddings", + "context-window-200k", + ], + maxTokens: 200000, + temperature: { min: 0, max: 1, default: 0.7 }, + pricing: { input: 0.000003, output: 0.000015 }, + }, + "sonar-medium-online": { + id: "sonar-medium-online", + provider: "perplexity", + capabilities: ["text-generation", "web-search", "research"], + maxTokens: 4096, + temperature: { min: 0, max: 1, default: 0.7 }, + }, + }; + + const model = modelsMap[args.modelId]; + if (!model) { + return { + success: false, + error: `Model with ID ${args.modelId} not found`, + }; + } + + return { success: true, model }; + }, + }); + + this.logger.info("Registered models endpoint handlers"); + } + + /** + * Set up handlers for the /execute endpoint + */ + setupExecuteHandlers() { + // Add a tool to execute operations with context + this.server.addTool({ + name: "executeWithContext", + description: "Execute an operation with the provided context", + parameters: z.object({ + operation: z.string().describe("The operation to execute"), + contextId: z.string().describe("The ID of the context to use"), + parameters: z + .record(z.any()) + .optional() + .describe("Additional parameters for the operation"), + versionId: z + .string() + .optional() + .describe("Optional specific context version to use"), + }), + execute: async (args) => { + try { + // Get the context first, with version if specified + const context = await this.contextManager.getContext( + args.contextId, + args.versionId + ); + + // Execute different operations based on the operation name + switch (args.operation) { + case "generateTask": + return await this.executeGenerateTask(context, args.parameters); + case "expandTask": + return await this.executeExpandTask(context, args.parameters); + case "analyzeComplexity": + return await this.executeAnalyzeComplexity( + context, + args.parameters + ); + case "mergeContexts": + return await this.executeMergeContexts(context, args.parameters); + case "searchContexts": + return await this.executeSearchContexts(args.parameters); + case "extractInsights": + return await this.executeExtractInsights( + context, + args.parameters + ); + case "syncWithRepository": + return await this.executeSyncWithRepository( + context, + args.parameters + ); + default: + return { + success: false, + error: `Unknown operation: ${args.operation}`, + }; + } + } catch (error) { + this.logger.error(`Error executing operation: ${error.message}`); + return { + success: false, + error: error.message, + operation: args.operation, + contextId: args.contextId, + }; + } + }, + }); + + // Add tool for batch operations + this.server.addTool({ + name: "executeBatchOperations", + description: "Execute multiple operations in a single request", + parameters: z.object({ + operations: z + .array( + z.object({ + operation: z.string().describe("The operation to execute"), + contextId: z.string().describe("The ID of the context to use"), + parameters: z + .record(z.any()) + .optional() + .describe("Additional parameters"), + versionId: z + .string() + .optional() + .describe("Optional context version"), + }) + ) + .describe("Array of operations to execute in sequence"), + }), + execute: async (args) => { + const results = []; + let hasErrors = false; + + for (const op of args.operations) { + try { + const context = await this.contextManager.getContext( + op.contextId, + op.versionId + ); + + let result; + switch (op.operation) { + case "generateTask": + result = await this.executeGenerateTask(context, op.parameters); + break; + case "expandTask": + result = await this.executeExpandTask(context, op.parameters); + break; + case "analyzeComplexity": + result = await this.executeAnalyzeComplexity( + context, + op.parameters + ); + break; + case "mergeContexts": + result = await this.executeMergeContexts( + context, + op.parameters + ); + break; + case "searchContexts": + result = await this.executeSearchContexts(op.parameters); + break; + case "extractInsights": + result = await this.executeExtractInsights( + context, + op.parameters + ); + break; + case "syncWithRepository": + result = await this.executeSyncWithRepository( + context, + op.parameters + ); + break; + default: + result = { + success: false, + error: `Unknown operation: ${op.operation}`, + }; + hasErrors = true; + } + + results.push({ + operation: op.operation, + contextId: op.contextId, + result: result, + }); + + if (!result.success) { + hasErrors = true; + } + } catch (error) { + this.logger.error( + `Error in batch operation ${op.operation}: ${error.message}` + ); + results.push({ + operation: op.operation, + contextId: op.contextId, + result: { + success: false, + error: error.message, + }, + }); + hasErrors = true; + } + } + + return { + success: !hasErrors, + results: results, + }; + }, + }); + + this.logger.info("Registered execute endpoint handlers"); + } + + /** + * Execute the generateTask operation + * @param {object} context - The context to use + * @param {object} parameters - Additional parameters + * @returns {Promise} The result of the operation + */ + async executeGenerateTask(context, parameters = {}) { + // This is a placeholder for actual task generation logic + // In a real implementation, this would use Task Master's task generation + + this.logger.info(`Generating task with context ${context.id}`); + + // Improved task generation with more detailed result + const task = { + id: Math.floor(Math.random() * 1000), + title: parameters.title || "New Task", + description: parameters.description || "Task generated from context", + status: "pending", + dependencies: parameters.dependencies || [], + priority: parameters.priority || "medium", + details: `This task was generated using context ${ + context.id + }.\n\n${JSON.stringify(context.data, null, 2)}`, + metadata: { + generatedAt: new Date().toISOString(), + generatedFrom: context.id, + contextVersion: context.metadata.version, + generatedBy: parameters.user || "system", + }, + }; + + return { + success: true, + task, + contextUsed: { + id: context.id, + version: context.metadata.version, + }, + }; + } + + /** + * Execute the expandTask operation + * @param {object} context - The context to use + * @param {object} parameters - Additional parameters + * @returns {Promise} The result of the operation + */ + async executeExpandTask(context, parameters = {}) { + // This is a placeholder for actual task expansion logic + // In a real implementation, this would use Task Master's task expansion + + this.logger.info(`Expanding task with context ${context.id}`); + + // Enhanced task expansion with more configurable options + const numSubtasks = parameters.numSubtasks || 3; + const subtaskPrefix = parameters.subtaskPrefix || ""; + const subtasks = []; + + for (let i = 1; i <= numSubtasks; i++) { + subtasks.push({ + id: `${subtaskPrefix}${i}`, + title: parameters.titleTemplate + ? parameters.titleTemplate.replace("{i}", i) + : `Subtask ${i}`, + description: parameters.descriptionTemplate + ? parameters.descriptionTemplate + .replace("{i}", i) + .replace("{taskId}", parameters.taskId || "unknown") + : `Subtask ${i} for ${parameters.taskId || "unknown task"}`, + dependencies: i > 1 ? [i - 1] : [], + status: "pending", + metadata: { + expandedAt: new Date().toISOString(), + expandedFrom: context.id, + contextVersion: context.metadata.version, + expandedBy: parameters.user || "system", + }, + }); + } + + return { + success: true, + taskId: parameters.taskId, + subtasks, + contextUsed: { + id: context.id, + version: context.metadata.version, + }, + }; + } + + /** + * Execute the analyzeComplexity operation + * @param {object} context - The context to use + * @param {object} parameters - Additional parameters + * @returns {Promise} The result of the operation + */ + async executeAnalyzeComplexity(context, parameters = {}) { + // This is a placeholder for actual complexity analysis logic + // In a real implementation, this would use Task Master's complexity analysis + + this.logger.info(`Analyzing complexity with context ${context.id}`); + + // Enhanced complexity analysis with more detailed factors + const complexityScore = Math.floor(Math.random() * 10) + 1; + const recommendedSubtasks = Math.floor(complexityScore / 2) + 1; + + // More detailed analysis with weighted factors + const factors = [ + { + name: "Task scope breadth", + score: Math.floor(Math.random() * 10) + 1, + weight: 0.3, + description: "How broad is the scope of this task", + }, + { + name: "Technical complexity", + score: Math.floor(Math.random() * 10) + 1, + weight: 0.4, + description: "How technically complex is the implementation", + }, + { + name: "External dependencies", + score: Math.floor(Math.random() * 10) + 1, + weight: 0.2, + description: "How many external dependencies does this task have", + }, + { + name: "Risk assessment", + score: Math.floor(Math.random() * 10) + 1, + weight: 0.1, + description: "What is the risk level of this task", + }, + ]; + + return { + success: true, + analysis: { + taskId: parameters.taskId || "unknown", + complexityScore, + recommendedSubtasks, + factors, + recommendedTimeEstimate: `${complexityScore * 2}-${ + complexityScore * 4 + } hours`, + metadata: { + analyzedAt: new Date().toISOString(), + analyzedUsing: context.id, + contextVersion: context.metadata.version, + analyzedBy: parameters.user || "system", + }, + }, + contextUsed: { + id: context.id, + version: context.metadata.version, + }, + }; + } + + /** + * Execute the mergeContexts operation + * @param {object} primaryContext - The primary context to use + * @param {object} parameters - Additional parameters + * @returns {Promise} The result of the operation + */ + async executeMergeContexts(primaryContext, parameters = {}) { + this.logger.info( + `Merging contexts with primary context ${primaryContext.id}` + ); + + if ( + !parameters.contextIds || + !Array.isArray(parameters.contextIds) || + parameters.contextIds.length === 0 + ) { + return { + success: false, + error: "No context IDs provided for merging", + }; + } + + if (!parameters.newContextId) { + return { + success: false, + error: "New context ID is required for the merged context", + }; + } + + try { + // Add the primary context to the list if not already included + if (!parameters.contextIds.includes(primaryContext.id)) { + parameters.contextIds.unshift(primaryContext.id); + } + + const mergedContext = await this.contextManager.mergeContexts( + parameters.contextIds, + parameters.newContextId, + { + mergedAt: new Date().toISOString(), + mergedBy: parameters.user || "system", + mergeStrategy: parameters.strategy || "concatenate", + ...parameters.metadata, + } + ); + + return { + success: true, + mergedContext, + sourceContexts: parameters.contextIds, + }; + } catch (error) { + this.logger.error(`Error merging contexts: ${error.message}`); + return { + success: false, + error: error.message, + }; + } + } + + /** + * Execute the searchContexts operation + * @param {object} parameters - Search parameters + * @returns {Promise} The result of the operation + */ + async executeSearchContexts(parameters = {}) { + this.logger.info( + `Searching contexts with query: ${parameters.query || ""}` + ); + + try { + const searchResults = await this.contextManager.listContexts({ + query: parameters.query || "", + filters: parameters.filters || {}, + limit: parameters.limit || 100, + offset: parameters.offset || 0, + sortBy: parameters.sortBy || "updated", + sortDirection: parameters.sortDirection || "desc", + }); + + return { + success: true, + ...searchResults, + }; + } catch (error) { + this.logger.error(`Error searching contexts: ${error.message}`); + return { + success: false, + error: error.message, + }; + } + } + + /** + * Execute the extractInsights operation + * @param {object} context - The context to analyze + * @param {object} parameters - Additional parameters + * @returns {Promise} The result of the operation + */ + async executeExtractInsights(context, parameters = {}) { + this.logger.info(`Extracting insights from context ${context.id}`); + + // Placeholder for actual insight extraction + // In a real implementation, this would perform analysis on the context data + + const insights = [ + { + type: "summary", + content: `Summary of context ${context.id}`, + confidence: 0.85, + }, + { + type: "key_points", + content: ["First key point", "Second key point", "Third key point"], + confidence: 0.78, + }, + { + type: "recommendations", + content: ["First recommendation", "Second recommendation"], + confidence: 0.72, + }, + ]; + + return { + success: true, + insights, + contextUsed: { + id: context.id, + version: context.metadata.version, + }, + metadata: { + extractedAt: new Date().toISOString(), + model: parameters.model || "default", + extractedBy: parameters.user || "system", + }, + }; + } + + /** + * Execute the syncWithRepository operation + * @param {object} context - The context to sync + * @param {object} parameters - Additional parameters + * @returns {Promise} The result of the operation + */ + async executeSyncWithRepository(context, parameters = {}) { + this.logger.info(`Syncing context ${context.id} with repository`); + + // Placeholder for actual repository sync + // In a real implementation, this would sync the context with an external repository + + return { + success: true, + syncStatus: "complete", + syncedTo: parameters.repository || "default", + syncTimestamp: new Date().toISOString(), + contextUsed: { + id: context.id, + version: context.metadata.version, + }, + }; + } +} + +export default MCPApiHandlers; diff --git a/mcp-server/src/auth.js b/mcp-server/src/auth.js new file mode 100644 index 00000000..22c36973 --- /dev/null +++ b/mcp-server/src/auth.js @@ -0,0 +1,285 @@ +import jwt from "jsonwebtoken"; +import { logger } from "../../scripts/modules/utils.js"; +import crypto from "crypto"; +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; + +// Constants +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const API_KEYS_FILE = + process.env.MCP_API_KEYS_FILE || path.join(__dirname, "../api-keys.json"); +const JWT_SECRET = + process.env.MCP_JWT_SECRET || "task-master-mcp-server-secret"; +const JWT_EXPIRATION = process.env.MCP_JWT_EXPIRATION || "24h"; + +/** + * Authentication middleware and utilities for MCP server + */ +class MCPAuth { + constructor() { + this.apiKeys = new Map(); + this.logger = logger; + this.loadApiKeys(); + } + + /** + * Load API keys from disk + */ + async loadApiKeys() { + try { + // Create API keys file if it doesn't exist + try { + await fs.access(API_KEYS_FILE); + } catch (error) { + // File doesn't exist, create it with a default admin key + const defaultApiKey = this.generateApiKey(); + const defaultApiKeys = { + keys: [ + { + id: "admin", + key: defaultApiKey, + role: "admin", + created: new Date().toISOString(), + }, + ], + }; + + await fs.mkdir(path.dirname(API_KEYS_FILE), { recursive: true }); + await fs.writeFile( + API_KEYS_FILE, + JSON.stringify(defaultApiKeys, null, 2), + "utf8" + ); + + this.logger.info( + `Created default API keys file with admin key: ${defaultApiKey}` + ); + } + + // Load API keys + const data = await fs.readFile(API_KEYS_FILE, "utf8"); + const apiKeys = JSON.parse(data); + + apiKeys.keys.forEach((key) => { + this.apiKeys.set(key.key, { + id: key.id, + role: key.role, + created: key.created, + }); + }); + + this.logger.info(`Loaded ${this.apiKeys.size} API keys`); + } catch (error) { + this.logger.error(`Failed to load API keys: ${error.message}`); + throw error; + } + } + + /** + * Save API keys to disk + */ + async saveApiKeys() { + try { + const keys = []; + + this.apiKeys.forEach((value, key) => { + keys.push({ + id: value.id, + key, + role: value.role, + created: value.created, + }); + }); + + await fs.writeFile( + API_KEYS_FILE, + JSON.stringify({ keys }, null, 2), + "utf8" + ); + + this.logger.info(`Saved ${keys.length} API keys`); + } catch (error) { + this.logger.error(`Failed to save API keys: ${error.message}`); + throw error; + } + } + + /** + * Generate a new API key + * @returns {string} The generated API key + */ + generateApiKey() { + return crypto.randomBytes(32).toString("hex"); + } + + /** + * Create a new API key + * @param {string} id - Client identifier + * @param {string} role - Client role (admin, user) + * @returns {string} The generated API key + */ + async createApiKey(id, role = "user") { + const apiKey = this.generateApiKey(); + + this.apiKeys.set(apiKey, { + id, + role, + created: new Date().toISOString(), + }); + + await this.saveApiKeys(); + + this.logger.info(`Created new API key for ${id} with role ${role}`); + return apiKey; + } + + /** + * Revoke an API key + * @param {string} apiKey - The API key to revoke + * @returns {boolean} True if the key was revoked + */ + async revokeApiKey(apiKey) { + if (!this.apiKeys.has(apiKey)) { + return false; + } + + this.apiKeys.delete(apiKey); + await this.saveApiKeys(); + + this.logger.info(`Revoked API key`); + return true; + } + + /** + * Validate an API key + * @param {string} apiKey - The API key to validate + * @returns {object|null} The API key details if valid, null otherwise + */ + validateApiKey(apiKey) { + return this.apiKeys.get(apiKey) || null; + } + + /** + * Generate a JWT token for a client + * @param {string} clientId - Client identifier + * @param {string} role - Client role + * @returns {string} The JWT token + */ + generateToken(clientId, role) { + return jwt.sign({ clientId, role }, JWT_SECRET, { + expiresIn: JWT_EXPIRATION, + }); + } + + /** + * Verify a JWT token + * @param {string} token - The JWT token to verify + * @returns {object|null} The token payload if valid, null otherwise + */ + verifyToken(token) { + try { + return jwt.verify(token, JWT_SECRET); + } catch (error) { + this.logger.error(`Failed to verify token: ${error.message}`); + return null; + } + } + + /** + * Express middleware for API key authentication + * @param {object} req - Express request object + * @param {object} res - Express response object + * @param {function} next - Express next function + */ + authenticateApiKey(req, res, next) { + const apiKey = req.headers["x-api-key"]; + + if (!apiKey) { + return res.status(401).json({ + success: false, + error: "API key is required", + }); + } + + const keyDetails = this.validateApiKey(apiKey); + + if (!keyDetails) { + return res.status(401).json({ + success: false, + error: "Invalid API key", + }); + } + + // Attach client info to request + req.client = { + id: keyDetails.id, + role: keyDetails.role, + }; + + next(); + } + + /** + * Express middleware for JWT authentication + * @param {object} req - Express request object + * @param {object} res - Express response object + * @param {function} next - Express next function + */ + authenticateToken(req, res, next) { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + return res.status(401).json({ + success: false, + error: "Authentication token is required", + }); + } + + const payload = this.verifyToken(token); + + if (!payload) { + return res.status(401).json({ + success: false, + error: "Invalid or expired token", + }); + } + + // Attach client info to request + req.client = { + id: payload.clientId, + role: payload.role, + }; + + next(); + } + + /** + * Express middleware for role-based authorization + * @param {Array} roles - Array of allowed roles + * @returns {function} Express middleware + */ + authorizeRoles(roles) { + return (req, res, next) => { + if (!req.client || !req.client.role) { + return res.status(401).json({ + success: false, + error: "Unauthorized: Authentication required", + }); + } + + if (!roles.includes(req.client.role)) { + return res.status(403).json({ + success: false, + error: "Forbidden: Insufficient permissions", + }); + } + + next(); + }; + } +} + +export default MCPAuth; diff --git a/mcp-server/src/context-manager.js b/mcp-server/src/context-manager.js new file mode 100644 index 00000000..5b94b538 --- /dev/null +++ b/mcp-server/src/context-manager.js @@ -0,0 +1,873 @@ +import { logger } from "../../scripts/modules/utils.js"; +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; +import crypto from "crypto"; +import Fuse from "fuse.js"; + +// Constants +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const CONTEXT_DIR = + process.env.MCP_CONTEXT_DIR || path.join(__dirname, "../contexts"); +const MAX_CONTEXT_HISTORY = parseInt( + process.env.MCP_MAX_CONTEXT_HISTORY || "10", + 10 +); + +/** + * Context Manager for MCP server + * Handles storage, retrieval, and manipulation of context data + * Implements efficient indexing, versioning, and advanced context operations + */ +class ContextManager { + constructor() { + this.contexts = new Map(); + this.contextHistory = new Map(); // For version history + this.contextIndex = null; // For fuzzy search + this.logger = logger; + this.ensureContextDir(); + this.rebuildSearchIndex(); + } + + /** + * Ensure the contexts directory exists + */ + async ensureContextDir() { + try { + await fs.mkdir(CONTEXT_DIR, { recursive: true }); + this.logger.info(`Context directory ensured at ${CONTEXT_DIR}`); + + // Also create a versions subdirectory for history + await fs.mkdir(path.join(CONTEXT_DIR, "versions"), { recursive: true }); + } catch (error) { + this.logger.error(`Failed to create context directory: ${error.message}`); + throw error; + } + } + + /** + * Rebuild the search index for efficient context lookup + */ + async rebuildSearchIndex() { + await this.loadAllContextsFromDisk(); + + const contextsForIndex = Array.from(this.contexts.values()).map((ctx) => ({ + id: ctx.id, + content: + typeof ctx.data === "string" ? ctx.data : JSON.stringify(ctx.data), + tags: ctx.tags.join(" "), + metadata: Object.entries(ctx.metadata) + .map(([k, v]) => `${k}:${v}`) + .join(" "), + })); + + this.contextIndex = new Fuse(contextsForIndex, { + keys: ["id", "content", "tags", "metadata"], + includeScore: true, + threshold: 0.6, + }); + + this.logger.info( + `Rebuilt search index with ${contextsForIndex.length} contexts` + ); + } + + /** + * Create a new context + * @param {string} contextId - Unique identifier for the context + * @param {object|string} contextData - Initial context data + * @param {object} metadata - Optional metadata for the context + * @returns {object} The created context + */ + async createContext(contextId, contextData, metadata = {}) { + if (this.contexts.has(contextId)) { + throw new Error(`Context with ID ${contextId} already exists`); + } + + const timestamp = new Date().toISOString(); + const versionId = this.generateVersionId(); + + const context = { + id: contextId, + data: contextData, + metadata: { + created: timestamp, + updated: timestamp, + version: versionId, + ...metadata, + }, + tags: metadata.tags || [], + size: this.estimateSize(contextData), + }; + + this.contexts.set(contextId, context); + + // Initialize version history + this.contextHistory.set(contextId, [ + { + versionId, + timestamp, + data: JSON.parse(JSON.stringify(contextData)), // Deep clone + metadata: { ...context.metadata }, + }, + ]); + + await this.persistContext(contextId); + await this.persistContextVersion(contextId, versionId); + + // Update the search index + this.rebuildSearchIndex(); + + this.logger.info(`Created context: ${contextId} (version: ${versionId})`); + return context; + } + + /** + * Retrieve a context by ID + * @param {string} contextId - The context ID to retrieve + * @param {string} versionId - Optional specific version to retrieve + * @returns {object} The context object + */ + async getContext(contextId, versionId = null) { + // If specific version requested, try to get it from history + if (versionId) { + return this.getContextVersion(contextId, versionId); + } + + // Try to get from memory first + if (this.contexts.has(contextId)) { + return this.contexts.get(contextId); + } + + // Try to load from disk + try { + const context = await this.loadContextFromDisk(contextId); + if (context) { + this.contexts.set(contextId, context); + return context; + } + } catch (error) { + this.logger.error( + `Failed to load context ${contextId}: ${error.message}` + ); + } + + throw new Error(`Context with ID ${contextId} not found`); + } + + /** + * Get a specific version of a context + * @param {string} contextId - The context ID + * @param {string} versionId - The version ID + * @returns {object} The versioned context + */ + async getContextVersion(contextId, versionId) { + // Check if version history is in memory + if (this.contextHistory.has(contextId)) { + const history = this.contextHistory.get(contextId); + const version = history.find((v) => v.versionId === versionId); + if (version) { + return { + id: contextId, + data: version.data, + metadata: version.metadata, + tags: version.metadata.tags || [], + size: this.estimateSize(version.data), + versionId: version.versionId, + }; + } + } + + // Try to load from disk + try { + const versionPath = path.join( + CONTEXT_DIR, + "versions", + `${contextId}_${versionId}.json` + ); + const data = await fs.readFile(versionPath, "utf8"); + const version = JSON.parse(data); + + // Add to memory cache + if (!this.contextHistory.has(contextId)) { + this.contextHistory.set(contextId, []); + } + const history = this.contextHistory.get(contextId); + history.push(version); + + return { + id: contextId, + data: version.data, + metadata: version.metadata, + tags: version.metadata.tags || [], + size: this.estimateSize(version.data), + versionId: version.versionId, + }; + } catch (error) { + this.logger.error( + `Failed to load context version ${contextId}@${versionId}: ${error.message}` + ); + throw new Error( + `Context version ${versionId} for ${contextId} not found` + ); + } + } + + /** + * Update an existing context + * @param {string} contextId - The context ID to update + * @param {object|string} contextData - New context data + * @param {object} metadata - Optional metadata updates + * @param {boolean} createNewVersion - Whether to create a new version + * @returns {object} The updated context + */ + async updateContext( + contextId, + contextData, + metadata = {}, + createNewVersion = true + ) { + const context = await this.getContext(contextId); + const timestamp = new Date().toISOString(); + + // Generate a new version ID if requested + const versionId = createNewVersion + ? this.generateVersionId() + : context.metadata.version; + + // Create a backup of the current state for versioning + if (createNewVersion) { + // Store the current version in history + if (!this.contextHistory.has(contextId)) { + this.contextHistory.set(contextId, []); + } + + const history = this.contextHistory.get(contextId); + + // Add current state to history + history.push({ + versionId: context.metadata.version, + timestamp: context.metadata.updated, + data: JSON.parse(JSON.stringify(context.data)), // Deep clone + metadata: { ...context.metadata }, + }); + + // Trim history if it exceeds the maximum size + if (history.length > MAX_CONTEXT_HISTORY) { + const excessVersions = history.splice( + 0, + history.length - MAX_CONTEXT_HISTORY + ); + // Clean up excess versions from disk + for (const version of excessVersions) { + this.removeContextVersionFile(contextId, version.versionId).catch( + (err) => + this.logger.error( + `Failed to remove old version file: ${err.message}` + ) + ); + } + } + + // Persist version + await this.persistContextVersion(contextId, context.metadata.version); + } + + // Update the context + context.data = contextData; + context.metadata = { + ...context.metadata, + ...metadata, + updated: timestamp, + }; + + if (createNewVersion) { + context.metadata.version = versionId; + context.metadata.previousVersion = context.metadata.version; + } + + if (metadata.tags) { + context.tags = metadata.tags; + } + + // Update size estimate + context.size = this.estimateSize(contextData); + + this.contexts.set(contextId, context); + await this.persistContext(contextId); + + // Update the search index + this.rebuildSearchIndex(); + + this.logger.info(`Updated context: ${contextId} (version: ${versionId})`); + return context; + } + + /** + * Delete a context and all its versions + * @param {string} contextId - The context ID to delete + * @returns {boolean} True if deletion was successful + */ + async deleteContext(contextId) { + if (!this.contexts.has(contextId)) { + const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); + try { + await fs.access(contextPath); + } catch (error) { + throw new Error(`Context with ID ${contextId} not found`); + } + } + + this.contexts.delete(contextId); + + // Remove from history + const history = this.contextHistory.get(contextId) || []; + this.contextHistory.delete(contextId); + + try { + // Delete main context file + const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); + await fs.unlink(contextPath); + + // Delete all version files + for (const version of history) { + await this.removeContextVersionFile(contextId, version.versionId); + } + + // Update the search index + this.rebuildSearchIndex(); + + this.logger.info(`Deleted context: ${contextId}`); + return true; + } catch (error) { + this.logger.error( + `Failed to delete context files for ${contextId}: ${error.message}` + ); + throw error; + } + } + + /** + * List all available contexts with pagination and advanced filtering + * @param {object} options - Options for listing contexts + * @param {object} options.filters - Filters to apply + * @param {number} options.limit - Maximum number of contexts to return + * @param {number} options.offset - Number of contexts to skip + * @param {string} options.sortBy - Field to sort by + * @param {string} options.sortDirection - Sort direction ('asc' or 'desc') + * @param {string} options.query - Free text search query + * @returns {Array} Array of context objects + */ + async listContexts(options = {}) { + // Load all contexts from disk first + await this.loadAllContextsFromDisk(); + + const { + filters = {}, + limit = 100, + offset = 0, + sortBy = "updated", + sortDirection = "desc", + query = "", + } = options; + + let contexts; + + // If there's a search query, use the search index + if (query && this.contextIndex) { + const searchResults = this.contextIndex.search(query); + contexts = searchResults.map((result) => + this.contexts.get(result.item.id) + ); + } else { + contexts = Array.from(this.contexts.values()); + } + + // Apply filters + if (filters.tag) { + contexts = contexts.filter( + (ctx) => ctx.tags && ctx.tags.includes(filters.tag) + ); + } + + if (filters.metadataKey && filters.metadataValue) { + contexts = contexts.filter( + (ctx) => + ctx.metadata && + ctx.metadata[filters.metadataKey] === filters.metadataValue + ); + } + + if (filters.createdAfter) { + const timestamp = new Date(filters.createdAfter); + contexts = contexts.filter( + (ctx) => new Date(ctx.metadata.created) >= timestamp + ); + } + + if (filters.updatedAfter) { + const timestamp = new Date(filters.updatedAfter); + contexts = contexts.filter( + (ctx) => new Date(ctx.metadata.updated) >= timestamp + ); + } + + // Apply sorting + contexts.sort((a, b) => { + let valueA, valueB; + + if (sortBy === "created" || sortBy === "updated") { + valueA = new Date(a.metadata[sortBy]).getTime(); + valueB = new Date(b.metadata[sortBy]).getTime(); + } else if (sortBy === "size") { + valueA = a.size || 0; + valueB = b.size || 0; + } else if (sortBy === "id") { + valueA = a.id; + valueB = b.id; + } else { + valueA = a.metadata[sortBy]; + valueB = b.metadata[sortBy]; + } + + if (valueA === valueB) return 0; + + const sortFactor = sortDirection === "asc" ? 1 : -1; + return valueA < valueB ? -1 * sortFactor : 1 * sortFactor; + }); + + // Apply pagination + const paginatedContexts = contexts.slice(offset, offset + limit); + + return { + contexts: paginatedContexts, + total: contexts.length, + offset, + limit, + hasMore: offset + limit < contexts.length, + }; + } + + /** + * Get the version history of a context + * @param {string} contextId - The context ID + * @returns {Array} Array of version objects + */ + async getContextHistory(contextId) { + // Ensure context exists + await this.getContext(contextId); + + // Load history if not in memory + if (!this.contextHistory.has(contextId)) { + await this.loadContextHistoryFromDisk(contextId); + } + + const history = this.contextHistory.get(contextId) || []; + + // Return versions in reverse chronological order (newest first) + return history.sort((a, b) => { + const timeA = new Date(a.timestamp).getTime(); + const timeB = new Date(b.timestamp).getTime(); + return timeB - timeA; + }); + } + + /** + * Add tags to a context + * @param {string} contextId - The context ID + * @param {Array} tags - Array of tags to add + * @returns {object} The updated context + */ + async addTags(contextId, tags) { + const context = await this.getContext(contextId); + + const currentTags = context.tags || []; + const uniqueTags = [...new Set([...currentTags, ...tags])]; + + // Update context with new tags + return this.updateContext( + contextId, + context.data, + { + tags: uniqueTags, + }, + false + ); // Don't create a new version for tag updates + } + + /** + * Remove tags from a context + * @param {string} contextId - The context ID + * @param {Array} tags - Array of tags to remove + * @returns {object} The updated context + */ + async removeTags(contextId, tags) { + const context = await this.getContext(contextId); + + const currentTags = context.tags || []; + const newTags = currentTags.filter((tag) => !tags.includes(tag)); + + // Update context with new tags + return this.updateContext( + contextId, + context.data, + { + tags: newTags, + }, + false + ); // Don't create a new version for tag updates + } + + /** + * Handle context windowing and truncation + * @param {string} contextId - The context ID + * @param {number} maxSize - Maximum size in tokens/chars + * @param {string} strategy - Truncation strategy ('start', 'end', 'middle') + * @returns {object} The truncated context + */ + async truncateContext(contextId, maxSize, strategy = "end") { + const context = await this.getContext(contextId); + const contextText = + typeof context.data === "string" + ? context.data + : JSON.stringify(context.data); + + if (contextText.length <= maxSize) { + return context; // No truncation needed + } + + let truncatedData; + + switch (strategy) { + case "start": + truncatedData = contextText.slice(contextText.length - maxSize); + break; + case "middle": + const halfSize = Math.floor(maxSize / 2); + truncatedData = + contextText.slice(0, halfSize) + + "...[truncated]..." + + contextText.slice(contextText.length - halfSize); + break; + case "end": + default: + truncatedData = contextText.slice(0, maxSize); + break; + } + + // If original data was an object, try to parse the truncated data + // Otherwise use it as a string + let updatedData; + if (typeof context.data === "object") { + try { + // This may fail if truncation broke JSON structure + updatedData = { + ...context.data, + truncated: true, + truncation_strategy: strategy, + original_size: contextText.length, + truncated_size: truncatedData.length, + }; + } catch (error) { + updatedData = truncatedData; + } + } else { + updatedData = truncatedData; + } + + // Update with truncated data + return this.updateContext( + contextId, + updatedData, + { + truncated: true, + truncation_strategy: strategy, + original_size: contextText.length, + truncated_size: truncatedData.length, + }, + true + ); // Create a new version for the truncated data + } + + /** + * Merge multiple contexts into a new context + * @param {Array} contextIds - Array of context IDs to merge + * @param {string} newContextId - ID for the new merged context + * @param {object} metadata - Optional metadata for the new context + * @returns {object} The new merged context + */ + async mergeContexts(contextIds, newContextId, metadata = {}) { + if (contextIds.length === 0) { + throw new Error("At least one context ID must be provided for merging"); + } + + if (this.contexts.has(newContextId)) { + throw new Error(`Context with ID ${newContextId} already exists`); + } + + // Load all contexts to be merged + const contextsToMerge = []; + for (const id of contextIds) { + try { + const context = await this.getContext(id); + contextsToMerge.push(context); + } catch (error) { + this.logger.error( + `Could not load context ${id} for merging: ${error.message}` + ); + throw new Error(`Failed to merge contexts: ${error.message}`); + } + } + + // Check data types and decide how to merge + const allStrings = contextsToMerge.every((c) => typeof c.data === "string"); + const allObjects = contextsToMerge.every( + (c) => typeof c.data === "object" && c.data !== null + ); + + let mergedData; + + if (allStrings) { + // Merge strings with newlines between them + mergedData = contextsToMerge.map((c) => c.data).join("\n\n"); + } else if (allObjects) { + // Merge objects by combining their properties + mergedData = {}; + for (const context of contextsToMerge) { + mergedData = { ...mergedData, ...context.data }; + } + } else { + // Convert everything to strings and concatenate + mergedData = contextsToMerge + .map((c) => + typeof c.data === "string" ? c.data : JSON.stringify(c.data) + ) + .join("\n\n"); + } + + // Collect all tags from merged contexts + const allTags = new Set(); + for (const context of contextsToMerge) { + for (const tag of context.tags || []) { + allTags.add(tag); + } + } + + // Create merged metadata + const mergedMetadata = { + ...metadata, + tags: [...allTags], + merged_from: contextIds, + merged_at: new Date().toISOString(), + }; + + // Create the new merged context + return this.createContext(newContextId, mergedData, mergedMetadata); + } + + /** + * Persist a context to disk + * @param {string} contextId - The context ID to persist + * @returns {Promise} + */ + async persistContext(contextId) { + const context = this.contexts.get(contextId); + if (!context) { + throw new Error(`Context with ID ${contextId} not found`); + } + + const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); + try { + await fs.writeFile(contextPath, JSON.stringify(context, null, 2), "utf8"); + this.logger.debug(`Persisted context ${contextId} to disk`); + } catch (error) { + this.logger.error( + `Failed to persist context ${contextId}: ${error.message}` + ); + throw error; + } + } + + /** + * Persist a context version to disk + * @param {string} contextId - The context ID + * @param {string} versionId - The version ID + * @returns {Promise} + */ + async persistContextVersion(contextId, versionId) { + if (!this.contextHistory.has(contextId)) { + throw new Error(`Context history for ${contextId} not found`); + } + + const history = this.contextHistory.get(contextId); + const version = history.find((v) => v.versionId === versionId); + + if (!version) { + throw new Error(`Version ${versionId} of context ${contextId} not found`); + } + + const versionPath = path.join( + CONTEXT_DIR, + "versions", + `${contextId}_${versionId}.json` + ); + try { + await fs.writeFile(versionPath, JSON.stringify(version, null, 2), "utf8"); + this.logger.debug( + `Persisted context version ${contextId}@${versionId} to disk` + ); + } catch (error) { + this.logger.error( + `Failed to persist context version ${contextId}@${versionId}: ${error.message}` + ); + throw error; + } + } + + /** + * Remove a context version file from disk + * @param {string} contextId - The context ID + * @param {string} versionId - The version ID + * @returns {Promise} + */ + async removeContextVersionFile(contextId, versionId) { + const versionPath = path.join( + CONTEXT_DIR, + "versions", + `${contextId}_${versionId}.json` + ); + try { + await fs.unlink(versionPath); + this.logger.debug( + `Removed context version file ${contextId}@${versionId}` + ); + } catch (error) { + if (error.code !== "ENOENT") { + this.logger.error( + `Failed to remove context version file ${contextId}@${versionId}: ${error.message}` + ); + throw error; + } + } + } + + /** + * Load a context from disk + * @param {string} contextId - The context ID to load + * @returns {Promise} The loaded context + */ + async loadContextFromDisk(contextId) { + const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); + try { + const data = await fs.readFile(contextPath, "utf8"); + const context = JSON.parse(data); + this.logger.debug(`Loaded context ${contextId} from disk`); + return context; + } catch (error) { + this.logger.error( + `Failed to load context ${contextId} from disk: ${error.message}` + ); + throw error; + } + } + + /** + * Load context history from disk + * @param {string} contextId - The context ID + * @returns {Promise} The loaded history + */ + async loadContextHistoryFromDisk(contextId) { + try { + const files = await fs.readdir(path.join(CONTEXT_DIR, "versions")); + const versionFiles = files.filter( + (file) => file.startsWith(`${contextId}_`) && file.endsWith(".json") + ); + + const history = []; + + for (const file of versionFiles) { + try { + const data = await fs.readFile( + path.join(CONTEXT_DIR, "versions", file), + "utf8" + ); + const version = JSON.parse(data); + history.push(version); + } catch (error) { + this.logger.error( + `Failed to load context version file ${file}: ${error.message}` + ); + } + } + + this.contextHistory.set(contextId, history); + this.logger.debug( + `Loaded ${history.length} versions for context ${contextId}` + ); + + return history; + } catch (error) { + this.logger.error( + `Failed to load context history for ${contextId}: ${error.message}` + ); + this.contextHistory.set(contextId, []); + return []; + } + } + + /** + * Load all contexts from disk + * @returns {Promise} + */ + async loadAllContextsFromDisk() { + try { + const files = await fs.readdir(CONTEXT_DIR); + const contextFiles = files.filter((file) => file.endsWith(".json")); + + for (const file of contextFiles) { + const contextId = path.basename(file, ".json"); + if (!this.contexts.has(contextId)) { + try { + const context = await this.loadContextFromDisk(contextId); + this.contexts.set(contextId, context); + } catch (error) { + // Already logged in loadContextFromDisk + } + } + } + + this.logger.info(`Loaded ${this.contexts.size} contexts from disk`); + } catch (error) { + this.logger.error(`Failed to load contexts from disk: ${error.message}`); + throw error; + } + } + + /** + * Generate a unique version ID + * @returns {string} A unique version ID + */ + generateVersionId() { + return crypto.randomBytes(8).toString("hex"); + } + + /** + * Estimate the size of context data + * @param {object|string} data - The context data + * @returns {number} Estimated size in bytes + */ + estimateSize(data) { + if (typeof data === "string") { + return Buffer.byteLength(data, "utf8"); + } + + if (typeof data === "object" && data !== null) { + return Buffer.byteLength(JSON.stringify(data), "utf8"); + } + + return 0; + } +} + +export default ContextManager; diff --git a/mcp-server/src/index.js b/mcp-server/src/index.js new file mode 100644 index 00000000..eb820f95 --- /dev/null +++ b/mcp-server/src/index.js @@ -0,0 +1,366 @@ +import { FastMCP } from "fastmcp"; +import { z } from "zod"; +import path from "path"; +import fs from "fs/promises"; +import dotenv from "dotenv"; +import { fileURLToPath } from "url"; +import express from "express"; +import cors from "cors"; +import helmet from "helmet"; +import { logger } from "../../scripts/modules/utils.js"; +import MCPAuth from "./auth.js"; +import MCPApiHandlers from "./api-handlers.js"; +import ContextManager from "./context-manager.js"; + +// Load environment variables +dotenv.config(); + +// Constants +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const DEFAULT_PORT = process.env.MCP_SERVER_PORT || 3000; +const DEFAULT_HOST = process.env.MCP_SERVER_HOST || "localhost"; + +/** + * Main MCP server class that integrates with Task Master + */ +class TaskMasterMCPServer { + constructor(options = {}) { + this.options = { + name: "Task Master MCP Server", + version: process.env.PROJECT_VERSION || "1.0.0", + ...options, + }; + + this.server = new FastMCP(this.options); + this.expressApp = null; + this.initialized = false; + this.auth = new MCPAuth(); + this.contextManager = new ContextManager(); + + // Bind methods + this.init = this.init.bind(this); + this.start = this.start.bind(this); + this.stop = this.stop.bind(this); + + // Setup logging + this.logger = logger; + } + + /** + * Initialize the MCP server with necessary tools and routes + */ + async init() { + if (this.initialized) return; + + this.logger.info("Initializing Task Master MCP server..."); + + // Set up express for additional customization if needed + this.expressApp = express(); + this.expressApp.use(cors()); + this.expressApp.use(helmet()); + this.expressApp.use(express.json()); + + // Set up authentication middleware + this.setupAuthentication(); + + // Register API handlers + this.apiHandlers = new MCPApiHandlers(this.server); + + // Register additional task master specific tools + this.registerTaskMasterTools(); + + this.initialized = true; + this.logger.info("Task Master MCP server initialized successfully"); + + return this; + } + + /** + * Set up authentication for the MCP server + */ + setupAuthentication() { + // Add a health check endpoint that doesn't require authentication + this.expressApp.get("/health", (req, res) => { + res.status(200).json({ + status: "ok", + service: this.options.name, + version: this.options.version, + }); + }); + + // Add an authenticate endpoint to get a JWT token using an API key + this.expressApp.post("/auth/token", async (req, res) => { + const apiKey = req.headers["x-api-key"]; + + if (!apiKey) { + return res.status(401).json({ + success: false, + error: "API key is required", + }); + } + + const keyDetails = this.auth.validateApiKey(apiKey); + + if (!keyDetails) { + return res.status(401).json({ + success: false, + error: "Invalid API key", + }); + } + + const token = this.auth.generateToken(keyDetails.id, keyDetails.role); + + res.status(200).json({ + success: true, + token, + expiresIn: process.env.MCP_JWT_EXPIRATION || "24h", + clientId: keyDetails.id, + role: keyDetails.role, + }); + }); + + // Create authenticator middleware for FastMCP + this.server.setAuthenticator((request) => { + // Get token from Authorization header + const authHeader = request.headers?.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return null; + } + + const token = authHeader.split(" ")[1]; + const payload = this.auth.verifyToken(token); + + if (!payload) { + return null; + } + + return { + clientId: payload.clientId, + role: payload.role, + }; + }); + + // Set up a protected route for API key management (admin only) + this.expressApp.post( + "/auth/api-keys", + (req, res, next) => { + this.auth.authenticateToken(req, res, next); + }, + (req, res, next) => { + this.auth.authorizeRoles(["admin"])(req, res, next); + }, + async (req, res) => { + const { clientId, role } = req.body; + + if (!clientId) { + return res.status(400).json({ + success: false, + error: "Client ID is required", + }); + } + + try { + const apiKey = await this.auth.createApiKey(clientId, role || "user"); + + res.status(201).json({ + success: true, + apiKey, + clientId, + role: role || "user", + }); + } catch (error) { + this.logger.error(`Error creating API key: ${error.message}`); + + res.status(500).json({ + success: false, + error: "Failed to create API key", + }); + } + } + ); + + this.logger.info("Set up MCP authentication"); + } + + /** + * Register Task Master specific tools with the MCP server + */ + registerTaskMasterTools() { + // Add a tool to get tasks from Task Master + this.server.addTool({ + name: "listTasks", + description: "List all tasks from Task Master", + parameters: z.object({ + status: z.string().optional().describe("Filter tasks by status"), + withSubtasks: z + .boolean() + .optional() + .describe("Include subtasks in the response"), + }), + execute: async (args) => { + try { + // In a real implementation, this would use the Task Master API + // to fetch tasks. For now, returning mock data. + + this.logger.info( + `Listing tasks with filters: ${JSON.stringify(args)}` + ); + + // Mock task data + const tasks = [ + { + id: 1, + title: "Implement Task Data Structure", + status: "done", + dependencies: [], + priority: "high", + }, + { + id: 2, + title: "Develop Command Line Interface Foundation", + status: "done", + dependencies: [1], + priority: "high", + }, + { + id: 23, + title: "Implement MCP Server Functionality", + status: "in-progress", + dependencies: [22], + priority: "medium", + subtasks: [ + { + id: "23.1", + title: "Create Core MCP Server Module", + status: "in-progress", + dependencies: [], + }, + { + id: "23.2", + title: "Implement Context Management System", + status: "pending", + dependencies: ["23.1"], + }, + ], + }, + ]; + + // Apply status filter if provided + let filteredTasks = tasks; + if (args.status) { + filteredTasks = tasks.filter((task) => task.status === args.status); + } + + // Remove subtasks if not requested + if (!args.withSubtasks) { + filteredTasks = filteredTasks.map((task) => { + const { subtasks, ...taskWithoutSubtasks } = task; + return taskWithoutSubtasks; + }); + } + + return { success: true, tasks: filteredTasks }; + } catch (error) { + this.logger.error(`Error listing tasks: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + // Add a tool to get task details + this.server.addTool({ + name: "getTaskDetails", + description: "Get detailed information about a specific task", + parameters: z.object({ + taskId: z + .union([z.number(), z.string()]) + .describe("The ID of the task to get details for"), + }), + execute: async (args) => { + try { + // In a real implementation, this would use the Task Master API + // to fetch task details. For now, returning mock data. + + this.logger.info(`Getting details for task ${args.taskId}`); + + // Mock task details + const taskDetails = { + id: 23, + title: "Implement MCP Server Functionality", + description: + "Extend Task Master to function as an MCP server, allowing it to provide context management services to other applications.", + status: "in-progress", + dependencies: [22], + priority: "medium", + details: + "This task involves implementing the Model Context Protocol server capabilities within Task Master.", + testStrategy: + "Testing should include unit tests, integration tests, and compatibility tests.", + subtasks: [ + { + id: "23.1", + title: "Create Core MCP Server Module", + status: "in-progress", + dependencies: [], + }, + { + id: "23.2", + title: "Implement Context Management System", + status: "pending", + dependencies: ["23.1"], + }, + ], + }; + + return { success: true, task: taskDetails }; + } catch (error) { + this.logger.error(`Error getting task details: ${error.message}`); + return { success: false, error: error.message }; + } + }, + }); + + this.logger.info("Registered Task Master specific tools"); + } + + /** + * Start the MCP server + */ + async start({ port = DEFAULT_PORT, host = DEFAULT_HOST } = {}) { + if (!this.initialized) { + await this.init(); + } + + this.logger.info( + `Starting Task Master MCP server on http://${host}:${port}` + ); + + // Start the FastMCP server + await this.server.start({ + port, + host, + transportType: "sse", + expressApp: this.expressApp, + }); + + this.logger.info( + `Task Master MCP server running at http://${host}:${port}` + ); + + return this; + } + + /** + * Stop the MCP server + */ + async stop() { + if (this.server) { + this.logger.info("Stopping Task Master MCP server..."); + await this.server.stop(); + this.logger.info("Task Master MCP server stopped"); + } + } +} + +export default TaskMasterMCPServer; diff --git a/package-lock.json b/package-lock.json index 9fe24aaf..198d4529 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "task-master-ai", - "version": "0.9.18", + "version": "0.9.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "task-master-ai", - "version": "0.9.18", + "version": "0.9.30", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", @@ -14,15 +14,22 @@ "chalk": "^4.1.2", "cli-table3": "^0.6.5", "commander": "^11.1.0", + "cors": "^2.8.5", "dotenv": "^16.3.1", + "express": "^4.21.2", + "fastmcp": "^1.20.5", "figlet": "^1.8.0", + "fuse.js": "^7.0.0", "gradient-string": "^3.0.0", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.2", "openai": "^4.89.0", "ora": "^8.2.0" }, "bin": { "task-master": "bin/task-master.js", - "task-master-init": "bin/task-master-init.js" + "task-master-init": "bin/task-master-init.js", + "task-master-mcp-server": "mcp-server/server.js" }, "devDependencies": { "@changesets/changelog-github": "^0.5.1", @@ -1419,6 +1426,317 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz", + "integrity": "sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^4.1.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/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.0.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", + "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.0.1", + "content-disposition": "^1.0.0", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "^1.2.1", + "debug": "4.3.6", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "^2.0.0", + "fresh": "2.0.0", + "http-errors": "2.0.0", + "merge-descriptors": "^2.0.0", + "methods": "~1.1.2", + "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.13.0", + "range-parser": "~1.2.1", + "router": "^2.0.0", + "safe-buffer": "5.2.1", + "send": "^1.1.0", + "serve-static": "^2.1.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "^2.0.0", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1457,6 +1775,12 @@ "node": ">= 8" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1464,6 +1788,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -1484,6 +1820,30 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1638,6 +1998,19 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -1790,6 +2163,12 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -1949,6 +2328,60 @@ "node": ">=4" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/boxen": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", @@ -2050,6 +2483,12 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2057,6 +2496,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2074,7 +2522,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2285,7 +2732,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -2300,7 +2746,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2310,14 +2755,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -2332,7 +2775,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2345,7 +2787,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2433,6 +2874,27 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2440,6 +2902,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", @@ -2447,6 +2924,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2473,7 +2963,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2504,7 +2993,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2552,6 +3040,25 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -2632,6 +3139,21 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.123", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.123.tgz", @@ -2658,6 +3180,15 @@ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -2754,12 +3285,17 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -2784,6 +3320,15 @@ "node": ">=4" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2793,6 +3338,27 @@ "node": ">=6" } }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2866,6 +3432,97 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -2919,6 +3576,131 @@ "dev": true, "license": "MIT" }, + "node_modules/fastmcp": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-1.20.5.tgz", + "integrity": "sha512-jwcPgMF9bcE9qsEG82YMlAG26/n5CSYsr95e60ntqWWd+3kgTBbUIasB3HfpqHLTNaQuoX6/jl18fpDcybBjcQ==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.0", + "execa": "^9.5.2", + "file-type": "^20.3.0", + "fuse.js": "^7.1.0", + "mcp-proxy": "^2.10.4", + "strict-event-emitter-types": "^2.0.0", + "undici": "^7.4.0", + "uri-templates": "^0.2.0", + "yargs": "^17.7.2", + "zod": "^3.24.2", + "zod-to-json-schema": "^3.24.3" + }, + "bin": { + "fastmcp": "dist/bin/fastmcp.js" + } + }, + "node_modules/fastmcp/node_modules/execa": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz", + "integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fastmcp/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/fastmcp/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastmcp/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -2971,6 +3753,12 @@ "node": ">= 8" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figlet": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.0.tgz", @@ -2983,6 +3771,39 @@ "node": ">= 0.4.0" } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2996,6 +3817,39 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -3071,6 +3925,24 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -3117,6 +3989,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3131,7 +4012,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -3367,6 +4247,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/hexoid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", @@ -3384,6 +4273,22 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/human-id": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz", @@ -3417,7 +4322,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -3426,6 +4330,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3482,9 +4406,17 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3572,6 +4504,24 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3624,7 +4574,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -4371,6 +5320,61 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4411,6 +5415,48 @@ "node": ">=8" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.startcase": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", @@ -4516,6 +5562,38 @@ "node": ">= 0.4" } }, + "node_modules/mcp-proxy": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-2.12.0.tgz", + "integrity": "sha512-hL2Y6EtK7vkgAOZxOQe9M4Z9g5xEnvR4ZYBKqFi/5tjhz/1jyNEz5NL87Uzv46k8iZQPVNEof/T6arEooBU5bQ==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.0", + "eventsource": "^3.0.5", + "yargs": "^17.7.2" + }, + "bin": { + "mcp-proxy": "dist/bin/mcp-proxy.js" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4537,7 +5615,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4659,6 +5736,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -4733,11 +5819,19 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4746,11 +5840,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -4960,6 +6065,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4984,7 +6110,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4997,6 +6122,12 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -5007,6 +6138,19 @@ "node": ">=8" } }, + "node_modules/peek-readable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5047,6 +6191,15 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", + "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", + "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", @@ -5104,6 +6257,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -5118,6 +6286,19 @@ "node": ">= 6" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -5139,7 +6320,6 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -5189,6 +6369,30 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5233,7 +6437,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5320,6 +6523,31 @@ "node": ">=0.10.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/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/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5344,11 +6572,30 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -5361,11 +6608,91 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5378,7 +6705,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5388,7 +6714,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5408,7 +6733,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5425,7 +6749,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5444,7 +6767,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5541,6 +6863,15 @@ "node": ">=10" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -5553,6 +6884,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "license": "ISC" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -5655,6 +6992,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", + "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", @@ -5792,6 +7146,32 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5814,12 +7194,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz", + "integrity": "sha512-gaFsbThjrDGvAaD670r81RZro/s6H2PVZF640Qn0p5kZK+/rim7/mmyfp2W7VB5vOMaFM8vuFBJUaMlaZTYHlA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -5830,6 +7256,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -5861,6 +7296,21 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-templates": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uri-templates/-/uri-templates-0.2.0.tgz", + "integrity": "sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==", + "license": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -5876,6 +7326,15 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -5899,7 +7358,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5959,7 +7417,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -5987,7 +7444,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -6004,7 +7460,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -6023,7 +7478,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -6033,7 +7487,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6043,14 +7496,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6065,7 +7516,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6086,6 +7536,36 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "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" + } } } } diff --git a/package.json b/package.json index 88db54cc..bf085c98 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "type": "module", "bin": { "task-master": "bin/task-master.js", - "task-master-init": "bin/task-master-init.js" + "task-master-init": "bin/task-master-init.js", + "task-master-mcp-server": "mcp-server/server.js" }, "scripts": { "test": "node --experimental-vm-modules node_modules/.bin/jest", @@ -26,7 +27,9 @@ "development", "cursor", "anthropic", - "llm" + "llm", + "mcp", + "context" ], "author": "Eyal Toledano", "license": "MIT", @@ -36,11 +39,17 @@ "chalk": "^4.1.2", "cli-table3": "^0.6.5", "commander": "^11.1.0", + "cors": "^2.8.5", "dotenv": "^16.3.1", + "express": "^4.21.2", + "fastmcp": "^1.20.5", "figlet": "^1.8.0", "gradient-string": "^3.0.0", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.2", "openai": "^4.89.0", - "ora": "^8.2.0" + "ora": "^8.2.0", + "fuse.js": "^7.0.0" }, "engines": { "node": ">=14.0.0" @@ -61,7 +70,8 @@ ".cursor/**", "README-task-master.md", "index.js", - "bin/**" + "bin/**", + "mcp-server/**" ], "overrides": { "node-fetch": "^3.3.2", diff --git a/tasks/task_023.txt b/tasks/task_023.txt index a34085a0..35e721d4 100644 --- a/tasks/task_023.txt +++ b/tasks/task_023.txt @@ -56,3 +56,118 @@ Testing for the MCP server functionality should include: - Test for common API vulnerabilities (injection, CSRF, etc.) All tests should be automated and included in the CI/CD pipeline. Documentation should include examples of how to test the MCP server functionality manually using tools like curl or Postman. + +# Subtasks: +## 1. Create Core MCP Server Module and Basic Structure [done] +### Dependencies: None +### Description: Create the foundation for the MCP server implementation by setting up the core module structure, configuration, and server initialization. +### Details: +Implementation steps: +1. Create a new module `mcp-server.js` with the basic server structure +2. Implement configuration options to enable/disable the MCP server +3. Set up Express.js routes for the required MCP endpoints (/context, /models, /execute) +4. Create middleware for request validation and response formatting +5. Implement basic error handling according to MCP specifications +6. Add logging infrastructure for MCP operations +7. Create initialization and shutdown procedures for the MCP server +8. Set up integration with the main Task Master application + +Testing approach: +- Unit tests for configuration loading and validation +- Test server initialization and shutdown procedures +- Verify that routes are properly registered +- Test basic error handling with invalid requests + +## 2. Implement Context Management System [done] +### Dependencies: 23.1 +### Description: Develop a robust context management system that can efficiently store, retrieve, and manipulate context data according to the MCP specification. +### Details: +Implementation steps: +1. Design and implement data structures for context storage +2. Create methods for context creation, retrieval, updating, and deletion +3. Implement context windowing and truncation algorithms for handling size limits +4. Add support for context metadata and tagging +5. Create utilities for context serialization and deserialization +6. Implement efficient indexing for quick context lookups +7. Add support for context versioning and history +8. Develop mechanisms for context persistence (in-memory, disk-based, or database) + +Testing approach: +- Unit tests for all context operations (CRUD) +- Performance tests for context retrieval with various sizes +- Test context windowing and truncation with edge cases +- Verify metadata handling and tagging functionality +- Test persistence mechanisms with simulated failures + +## 3. Implement MCP Endpoints and API Handlers [done] +### Dependencies: 23.1, 23.2 +### Description: Develop the complete API handlers for all required MCP endpoints, ensuring they follow the protocol specification and integrate with the context management system. +### Details: +Implementation steps: +1. Implement the `/context` endpoint for: + - GET: retrieving existing context + - POST: creating new context + - PUT: updating existing context + - DELETE: removing context +2. Implement the `/models` endpoint to list available models +3. Develop the `/execute` endpoint for performing operations with context +4. Create request validators for each endpoint +5. Implement response formatters according to MCP specifications +6. Add detailed error handling for each endpoint +7. Set up proper HTTP status codes for different scenarios +8. Implement pagination for endpoints that return lists + +Testing approach: +- Unit tests for each endpoint handler +- Integration tests with mock context data +- Test various request formats and edge cases +- Verify response formats match MCP specifications +- Test error handling with invalid inputs +- Benchmark endpoint performance + +## 4. Implement Authentication and Authorization System [pending] +### Dependencies: 23.1, 23.3 +### Description: Create a secure authentication and authorization mechanism for MCP clients to ensure only authorized applications can access the MCP server functionality. +### Details: +Implementation steps: +1. Design authentication scheme (API keys, OAuth, JWT, etc.) +2. Implement authentication middleware for all MCP endpoints +3. Create an API key management system for client applications +4. Develop role-based access control for different operations +5. Implement rate limiting to prevent abuse +6. Add secure token validation and handling +7. Create endpoints for managing client credentials +8. Implement audit logging for authentication events + +Testing approach: +- Security testing for authentication mechanisms +- Test access control with various permission levels +- Verify rate limiting functionality +- Test token validation with valid and invalid tokens +- Simulate unauthorized access attempts +- Verify audit logs contain appropriate information + +## 5. Optimize Performance and Finalize Documentation [pending] +### Dependencies: 23.1, 23.2, 23.3, 23.4 +### Description: Optimize the MCP server implementation for performance, especially for context retrieval operations, and create comprehensive documentation for users. +### Details: +Implementation steps: +1. Profile the MCP server to identify performance bottlenecks +2. Implement caching mechanisms for frequently accessed contexts +3. Optimize context serialization and deserialization +4. Add connection pooling for database operations (if applicable) +5. Implement request batching for bulk operations +6. Create comprehensive API documentation with examples +7. Add setup and configuration guides to the Task Master documentation +8. Create example client implementations +9. Add monitoring endpoints for server health and metrics +10. Implement graceful degradation under high load + +Testing approach: +- Load testing with simulated concurrent clients +- Measure response times for various operations +- Test with large context sizes to verify performance +- Verify documentation accuracy with sample requests +- Test monitoring endpoints +- Perform stress testing to identify failure points + diff --git a/tasks/tasks.json b/tasks/tasks.json index a7d6c333..ea4c7082 100644 --- a/tasks/tasks.json +++ b/tasks/tasks.json @@ -1343,8 +1343,68 @@ 22 ], "priority": "medium", - "details": "This task involves implementing the Model Context Protocol server capabilities within Task Master using FastMCP. The implementation should:\n\n1. Use FastMCP to create the MCP server module (`mcp-server.ts` or equivalent)\n2. Implement the required MCP endpoints using FastMCP:\n - `/context` - For retrieving and updating context\n - `/models` - For listing available models\n - `/execute` - For executing operations with context\n3. Utilize FastMCP's built-in features for context management, including:\n - Efficient context storage and retrieval\n - Context windowing and truncation\n - Metadata and tagging support\n4. Add authentication and authorization mechanisms using FastMCP capabilities\n5. Implement error handling and response formatting as per MCP specifications\n6. Configure Task Master to enable/disable MCP server functionality via FastMCP settings\n7. Add documentation on using Task Master as an MCP server with FastMCP\n8. Ensure compatibility with existing MCP clients by adhering to FastMCP's compliance features\n9. Optimize performance using FastMCP tools, especially for context retrieval operations\n10. Add logging for MCP server operations using FastMCP's logging utilities\n\nThe implementation should follow RESTful API design principles and leverage FastMCP's concurrency handling for multiple client requests. Consider using TypeScript for better type safety and integration with FastMCP[1][2].", - "testStrategy": "Testing for the MCP server functionality should include:\n\n1. Unit tests:\n - Test each MCP endpoint handler function independently using FastMCP\n - Verify context storage and retrieval mechanisms provided by FastMCP\n - Test authentication and authorization logic\n - Validate error handling for various failure scenarios\n\n2. Integration tests:\n - Set up a test MCP server instance using FastMCP\n - Test complete request/response cycles for each endpoint\n - Verify context persistence across multiple requests\n - Test with various payload sizes and content types\n\n3. Compatibility tests:\n - Test with existing MCP client libraries\n - Verify compliance with the MCP specification\n - Ensure backward compatibility with any MCP versions supported by FastMCP\n\n4. Performance tests:\n - Measure response times for context operations with various context sizes\n - Test concurrent request handling using FastMCP's concurrency tools\n - Verify memory usage remains within acceptable limits during extended operation\n\n5. Security tests:\n - Verify authentication mechanisms cannot be bypassed\n - Test for common API vulnerabilities (injection, CSRF, etc.)\n\nAll tests should be automated and included in the CI/CD pipeline. Documentation should include examples of how to test the MCP server functionality manually using tools like curl or Postman." + "details": "This task involves implementing the Model Context Protocol server capabilities within Task Master. The implementation should:\n\n1. Create a new module `mcp-server.js` that implements the core MCP server functionality\n2. Implement the required MCP endpoints:\n - `/context` - For retrieving and updating context\n - `/models` - For listing available models\n - `/execute` - For executing operations with context\n3. Develop a context management system that can:\n - Store and retrieve context data efficiently\n - Handle context windowing and truncation when limits are reached\n - Support context metadata and tagging\n4. Add authentication and authorization mechanisms for MCP clients\n5. Implement proper error handling and response formatting according to MCP specifications\n6. Create configuration options in Task Master to enable/disable the MCP server functionality\n7. Add documentation for how to use Task Master as an MCP server\n8. Ensure the implementation is compatible with existing MCP clients\n9. Optimize for performance, especially for context retrieval operations\n10. Add logging for MCP server operations\n\nThe implementation should follow RESTful API design principles and should be able to handle concurrent requests from multiple clients.", + "testStrategy": "Testing for the MCP server functionality should include:\n\n1. Unit tests:\n - Test each MCP endpoint handler function independently\n - Verify context storage and retrieval mechanisms\n - Test authentication and authorization logic\n - Validate error handling for various failure scenarios\n\n2. Integration tests:\n - Set up a test MCP server instance\n - Test complete request/response cycles for each endpoint\n - Verify context persistence across multiple requests\n - Test with various payload sizes and content types\n\n3. Compatibility tests:\n - Test with existing MCP client libraries\n - Verify compliance with the MCP specification\n - Ensure backward compatibility with any MCP versions supported\n\n4. Performance tests:\n - Measure response times for context operations with various context sizes\n - Test concurrent request handling\n - Verify memory usage remains within acceptable limits during extended operation\n\n5. Security tests:\n - Verify authentication mechanisms cannot be bypassed\n - Test for common API vulnerabilities (injection, CSRF, etc.)\n\nAll tests should be automated and included in the CI/CD pipeline. Documentation should include examples of how to test the MCP server functionality manually using tools like curl or Postman.", + "subtasks": [ + { + "id": 1, + "title": "Create Core MCP Server Module and Basic Structure", + "description": "Create the foundation for the MCP server implementation by setting up the core module structure, configuration, and server initialization.", + "dependencies": [], + "details": "Implementation steps:\n1. Create a new module `mcp-server.js` with the basic server structure\n2. Implement configuration options to enable/disable the MCP server\n3. Set up Express.js routes for the required MCP endpoints (/context, /models, /execute)\n4. Create middleware for request validation and response formatting\n5. Implement basic error handling according to MCP specifications\n6. Add logging infrastructure for MCP operations\n7. Create initialization and shutdown procedures for the MCP server\n8. Set up integration with the main Task Master application\n\nTesting approach:\n- Unit tests for configuration loading and validation\n- Test server initialization and shutdown procedures\n- Verify that routes are properly registered\n- Test basic error handling with invalid requests", + "status": "done", + "parentTaskId": 23 + }, + { + "id": 2, + "title": "Implement Context Management System", + "description": "Develop a robust context management system that can efficiently store, retrieve, and manipulate context data according to the MCP specification.", + "dependencies": [ + 1 + ], + "details": "Implementation steps:\n1. Design and implement data structures for context storage\n2. Create methods for context creation, retrieval, updating, and deletion\n3. Implement context windowing and truncation algorithms for handling size limits\n4. Add support for context metadata and tagging\n5. Create utilities for context serialization and deserialization\n6. Implement efficient indexing for quick context lookups\n7. Add support for context versioning and history\n8. Develop mechanisms for context persistence (in-memory, disk-based, or database)\n\nTesting approach:\n- Unit tests for all context operations (CRUD)\n- Performance tests for context retrieval with various sizes\n- Test context windowing and truncation with edge cases\n- Verify metadata handling and tagging functionality\n- Test persistence mechanisms with simulated failures", + "status": "done", + "parentTaskId": 23 + }, + { + "id": 3, + "title": "Implement MCP Endpoints and API Handlers", + "description": "Develop the complete API handlers for all required MCP endpoints, ensuring they follow the protocol specification and integrate with the context management system.", + "dependencies": [ + 1, + 2 + ], + "details": "Implementation steps:\n1. Implement the `/context` endpoint for:\n - GET: retrieving existing context\n - POST: creating new context\n - PUT: updating existing context\n - DELETE: removing context\n2. Implement the `/models` endpoint to list available models\n3. Develop the `/execute` endpoint for performing operations with context\n4. Create request validators for each endpoint\n5. Implement response formatters according to MCP specifications\n6. Add detailed error handling for each endpoint\n7. Set up proper HTTP status codes for different scenarios\n8. Implement pagination for endpoints that return lists\n\nTesting approach:\n- Unit tests for each endpoint handler\n- Integration tests with mock context data\n- Test various request formats and edge cases\n- Verify response formats match MCP specifications\n- Test error handling with invalid inputs\n- Benchmark endpoint performance", + "status": "done", + "parentTaskId": 23 + }, + { + "id": 4, + "title": "Implement Authentication and Authorization System", + "description": "Create a secure authentication and authorization mechanism for MCP clients to ensure only authorized applications can access the MCP server functionality.", + "dependencies": [ + 1, + 3 + ], + "details": "Implementation steps:\n1. Design authentication scheme (API keys, OAuth, JWT, etc.)\n2. Implement authentication middleware for all MCP endpoints\n3. Create an API key management system for client applications\n4. Develop role-based access control for different operations\n5. Implement rate limiting to prevent abuse\n6. Add secure token validation and handling\n7. Create endpoints for managing client credentials\n8. Implement audit logging for authentication events\n\nTesting approach:\n- Security testing for authentication mechanisms\n- Test access control with various permission levels\n- Verify rate limiting functionality\n- Test token validation with valid and invalid tokens\n- Simulate unauthorized access attempts\n- Verify audit logs contain appropriate information", + "status": "pending", + "parentTaskId": 23 + }, + { + "id": 5, + "title": "Optimize Performance and Finalize Documentation", + "description": "Optimize the MCP server implementation for performance, especially for context retrieval operations, and create comprehensive documentation for users.", + "dependencies": [ + 1, + 2, + 3, + 4 + ], + "details": "Implementation steps:\n1. Profile the MCP server to identify performance bottlenecks\n2. Implement caching mechanisms for frequently accessed contexts\n3. Optimize context serialization and deserialization\n4. Add connection pooling for database operations (if applicable)\n5. Implement request batching for bulk operations\n6. Create comprehensive API documentation with examples\n7. Add setup and configuration guides to the Task Master documentation\n8. Create example client implementations\n9. Add monitoring endpoints for server health and metrics\n10. Implement graceful degradation under high load\n\nTesting approach:\n- Load testing with simulated concurrent clients\n- Measure response times for various operations\n- Test with large context sizes to verify performance\n- Verify documentation accuracy with sample requests\n- Test monitoring endpoints\n- Perform stress testing to identify failure points", + "status": "pending", + "parentTaskId": 23 + } + ] }, { "id": 24, From 37c5b83f60ca0971ff4c083ac6ef47090e207711 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:39:20 +0000 Subject: [PATCH 4/8] feat(wip): set up mcp server and tools, but mcp on cursor not working despite working in inspector --- .cursor/mcp.json | 8 + mcp-server/server.js | 12 +- mcp-server/src/api-handlers.js | 970 -------------------------- mcp-server/src/auth.js | 285 -------- mcp-server/src/context-manager.js | 873 ----------------------- mcp-server/src/index.js | 314 +-------- mcp-server/src/logger.js | 68 ++ mcp-server/src/tools/addTask.js | 56 ++ mcp-server/src/tools/expandTask.js | 66 ++ mcp-server/src/tools/index.js | 29 + mcp-server/src/tools/listTasks.js | 51 ++ mcp-server/src/tools/nextTask.js | 45 ++ mcp-server/src/tools/setTaskStatus.js | 52 ++ mcp-server/src/tools/showTask.js | 45 ++ mcp-server/src/tools/utils.js | 90 +++ 15 files changed, 529 insertions(+), 2435 deletions(-) create mode 100644 .cursor/mcp.json delete mode 100644 mcp-server/src/api-handlers.js delete mode 100644 mcp-server/src/auth.js delete mode 100644 mcp-server/src/context-manager.js create mode 100644 mcp-server/src/logger.js create mode 100644 mcp-server/src/tools/addTask.js create mode 100644 mcp-server/src/tools/expandTask.js create mode 100644 mcp-server/src/tools/index.js create mode 100644 mcp-server/src/tools/listTasks.js create mode 100644 mcp-server/src/tools/nextTask.js create mode 100644 mcp-server/src/tools/setTaskStatus.js create mode 100644 mcp-server/src/tools/showTask.js create mode 100644 mcp-server/src/tools/utils.js diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..3b7160ae --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "taskMaster": { + "command": "node", + "args": ["mcp-server/server.js"] + } + } +} diff --git a/mcp-server/server.js b/mcp-server/server.js index ed5c3c69..dfca0f55 100755 --- a/mcp-server/server.js +++ b/mcp-server/server.js @@ -2,15 +2,11 @@ import TaskMasterMCPServer from "./src/index.js"; import dotenv from "dotenv"; -import { logger } from "../scripts/modules/utils.js"; +import logger from "./src/logger.js"; // Load environment variables dotenv.config(); -// Constants -const PORT = process.env.MCP_SERVER_PORT || 3000; -const HOST = process.env.MCP_SERVER_HOST || "localhost"; - /** * Start the MCP server */ @@ -19,21 +15,17 @@ async function startServer() { // Handle graceful shutdown process.on("SIGINT", async () => { - logger.info("Received SIGINT, shutting down gracefully..."); await server.stop(); process.exit(0); }); process.on("SIGTERM", async () => { - logger.info("Received SIGTERM, shutting down gracefully..."); await server.stop(); process.exit(0); }); try { - await server.start({ port: PORT, host: HOST }); - logger.info(`MCP server running at http://${HOST}:${PORT}`); - logger.info("Press Ctrl+C to stop"); + await server.start(); } catch (error) { logger.error(`Failed to start MCP server: ${error.message}`); process.exit(1); diff --git a/mcp-server/src/api-handlers.js b/mcp-server/src/api-handlers.js deleted file mode 100644 index ead546f2..00000000 --- a/mcp-server/src/api-handlers.js +++ /dev/null @@ -1,970 +0,0 @@ -import { z } from "zod"; -import { logger } from "../../scripts/modules/utils.js"; -import ContextManager from "./context-manager.js"; - -/** - * MCP API Handlers class - * Implements handlers for the MCP API endpoints - */ -class MCPApiHandlers { - constructor(server) { - this.server = server; - this.contextManager = new ContextManager(); - this.logger = logger; - - // Bind methods - this.registerEndpoints = this.registerEndpoints.bind(this); - this.setupContextHandlers = this.setupContextHandlers.bind(this); - this.setupModelHandlers = this.setupModelHandlers.bind(this); - this.setupExecuteHandlers = this.setupExecuteHandlers.bind(this); - - // Register all handlers - this.registerEndpoints(); - } - - /** - * Register all MCP API endpoints - */ - registerEndpoints() { - this.setupContextHandlers(); - this.setupModelHandlers(); - this.setupExecuteHandlers(); - - this.logger.info("Registered all MCP API endpoint handlers"); - } - - /** - * Set up handlers for the /context endpoint - */ - setupContextHandlers() { - // Add a tool to create context - this.server.addTool({ - name: "createContext", - description: - "Create a new context with the given data and optional metadata", - parameters: z.object({ - contextId: z.string().describe("Unique identifier for the context"), - data: z.any().describe("The context data to store"), - metadata: z - .object({}) - .optional() - .describe("Optional metadata for the context"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.createContext( - args.contextId, - args.data, - args.metadata || {} - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error creating context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to get context - this.server.addTool({ - name: "getContext", - description: - "Retrieve a context by its ID, optionally a specific version", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to retrieve"), - versionId: z - .string() - .optional() - .describe("Optional specific version ID to retrieve"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.getContext( - args.contextId, - args.versionId - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error retrieving context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to update context - this.server.addTool({ - name: "updateContext", - description: "Update an existing context with new data and/or metadata", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to update"), - data: z - .any() - .optional() - .describe("New data to update the context with"), - metadata: z - .object({}) - .optional() - .describe("New metadata to update the context with"), - createNewVersion: z - .boolean() - .optional() - .default(true) - .describe( - "Whether to create a new version (true) or update in place (false)" - ), - }), - execute: async (args) => { - try { - const context = await this.contextManager.updateContext( - args.contextId, - args.data || {}, - args.metadata || {}, - args.createNewVersion - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error updating context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to delete context - this.server.addTool({ - name: "deleteContext", - description: "Delete a context by its ID", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to delete"), - }), - execute: async (args) => { - try { - const result = await this.contextManager.deleteContext( - args.contextId - ); - return { success: result }; - } catch (error) { - this.logger.error(`Error deleting context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to list contexts with pagination and advanced filtering - this.server.addTool({ - name: "listContexts", - description: - "List available contexts with filtering, pagination and sorting", - parameters: z.object({ - // Filtering parameters - filters: z - .object({ - tag: z.string().optional().describe("Filter contexts by tag"), - metadataKey: z - .string() - .optional() - .describe("Filter contexts by metadata key"), - metadataValue: z - .string() - .optional() - .describe("Filter contexts by metadata value"), - createdAfter: z - .string() - .optional() - .describe("Filter contexts created after date (ISO format)"), - updatedAfter: z - .string() - .optional() - .describe("Filter contexts updated after date (ISO format)"), - }) - .optional() - .describe("Filters to apply to the context list"), - - // Pagination parameters - limit: z - .number() - .optional() - .default(100) - .describe("Maximum number of contexts to return"), - offset: z - .number() - .optional() - .default(0) - .describe("Number of contexts to skip"), - - // Sorting parameters - sortBy: z - .string() - .optional() - .default("updated") - .describe("Field to sort by (id, created, updated, size)"), - sortDirection: z - .enum(["asc", "desc"]) - .optional() - .default("desc") - .describe("Sort direction"), - - // Search query - query: z.string().optional().describe("Free text search query"), - }), - execute: async (args) => { - try { - const result = await this.contextManager.listContexts(args); - return { - success: true, - ...result, - }; - } catch (error) { - this.logger.error(`Error listing contexts: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to get context history - this.server.addTool({ - name: "getContextHistory", - description: "Get the version history of a context", - parameters: z.object({ - contextId: z - .string() - .describe("The ID of the context to get history for"), - }), - execute: async (args) => { - try { - const history = await this.contextManager.getContextHistory( - args.contextId - ); - return { - success: true, - history, - contextId: args.contextId, - }; - } catch (error) { - this.logger.error(`Error getting context history: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to merge contexts - this.server.addTool({ - name: "mergeContexts", - description: "Merge multiple contexts into a new context", - parameters: z.object({ - contextIds: z - .array(z.string()) - .describe("Array of context IDs to merge"), - newContextId: z.string().describe("ID for the new merged context"), - metadata: z - .object({}) - .optional() - .describe("Optional metadata for the new context"), - }), - execute: async (args) => { - try { - const mergedContext = await this.contextManager.mergeContexts( - args.contextIds, - args.newContextId, - args.metadata || {} - ); - return { - success: true, - context: mergedContext, - }; - } catch (error) { - this.logger.error(`Error merging contexts: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to add tags to a context - this.server.addTool({ - name: "addTags", - description: "Add tags to a context", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to tag"), - tags: z - .array(z.string()) - .describe("Array of tags to add to the context"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.addTags( - args.contextId, - args.tags - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error adding tags to context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to remove tags from a context - this.server.addTool({ - name: "removeTags", - description: "Remove tags from a context", - parameters: z.object({ - contextId: z - .string() - .describe("The ID of the context to remove tags from"), - tags: z - .array(z.string()) - .describe("Array of tags to remove from the context"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.removeTags( - args.contextId, - args.tags - ); - return { success: true, context }; - } catch (error) { - this.logger.error( - `Error removing tags from context: ${error.message}` - ); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to truncate context - this.server.addTool({ - name: "truncateContext", - description: "Truncate a context to a maximum size", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to truncate"), - maxSize: z - .number() - .describe("Maximum size (in characters) for the context"), - strategy: z - .enum(["start", "end", "middle"]) - .default("end") - .describe("Truncation strategy: start, end, or middle"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.truncateContext( - args.contextId, - args.maxSize, - args.strategy - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error truncating context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - this.logger.info("Registered context endpoint handlers"); - } - - /** - * Set up handlers for the /models endpoint - */ - setupModelHandlers() { - // Add a tool to list available models - this.server.addTool({ - name: "listModels", - description: "List all available models with their capabilities", - parameters: z.object({}), - execute: async () => { - // Here we could get models from a more dynamic source - // For now, returning static list of models supported by Task Master - const models = [ - { - id: "claude-3-opus-20240229", - provider: "anthropic", - capabilities: [ - "text-generation", - "embeddings", - "context-window-100k", - ], - }, - { - id: "claude-3-7-sonnet-20250219", - provider: "anthropic", - capabilities: [ - "text-generation", - "embeddings", - "context-window-200k", - ], - }, - { - id: "sonar-medium-online", - provider: "perplexity", - capabilities: ["text-generation", "web-search", "research"], - }, - ]; - - return { success: true, models }; - }, - }); - - // Add a tool to get model details - this.server.addTool({ - name: "getModelDetails", - description: "Get detailed information about a specific model", - parameters: z.object({ - modelId: z.string().describe("The ID of the model to get details for"), - }), - execute: async (args) => { - // Here we could get model details from a more dynamic source - // For now, returning static information - const modelsMap = { - "claude-3-opus-20240229": { - id: "claude-3-opus-20240229", - provider: "anthropic", - capabilities: [ - "text-generation", - "embeddings", - "context-window-100k", - ], - maxTokens: 100000, - temperature: { min: 0, max: 1, default: 0.7 }, - pricing: { input: 0.000015, output: 0.000075 }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - provider: "anthropic", - capabilities: [ - "text-generation", - "embeddings", - "context-window-200k", - ], - maxTokens: 200000, - temperature: { min: 0, max: 1, default: 0.7 }, - pricing: { input: 0.000003, output: 0.000015 }, - }, - "sonar-medium-online": { - id: "sonar-medium-online", - provider: "perplexity", - capabilities: ["text-generation", "web-search", "research"], - maxTokens: 4096, - temperature: { min: 0, max: 1, default: 0.7 }, - }, - }; - - const model = modelsMap[args.modelId]; - if (!model) { - return { - success: false, - error: `Model with ID ${args.modelId} not found`, - }; - } - - return { success: true, model }; - }, - }); - - this.logger.info("Registered models endpoint handlers"); - } - - /** - * Set up handlers for the /execute endpoint - */ - setupExecuteHandlers() { - // Add a tool to execute operations with context - this.server.addTool({ - name: "executeWithContext", - description: "Execute an operation with the provided context", - parameters: z.object({ - operation: z.string().describe("The operation to execute"), - contextId: z.string().describe("The ID of the context to use"), - parameters: z - .record(z.any()) - .optional() - .describe("Additional parameters for the operation"), - versionId: z - .string() - .optional() - .describe("Optional specific context version to use"), - }), - execute: async (args) => { - try { - // Get the context first, with version if specified - const context = await this.contextManager.getContext( - args.contextId, - args.versionId - ); - - // Execute different operations based on the operation name - switch (args.operation) { - case "generateTask": - return await this.executeGenerateTask(context, args.parameters); - case "expandTask": - return await this.executeExpandTask(context, args.parameters); - case "analyzeComplexity": - return await this.executeAnalyzeComplexity( - context, - args.parameters - ); - case "mergeContexts": - return await this.executeMergeContexts(context, args.parameters); - case "searchContexts": - return await this.executeSearchContexts(args.parameters); - case "extractInsights": - return await this.executeExtractInsights( - context, - args.parameters - ); - case "syncWithRepository": - return await this.executeSyncWithRepository( - context, - args.parameters - ); - default: - return { - success: false, - error: `Unknown operation: ${args.operation}`, - }; - } - } catch (error) { - this.logger.error(`Error executing operation: ${error.message}`); - return { - success: false, - error: error.message, - operation: args.operation, - contextId: args.contextId, - }; - } - }, - }); - - // Add tool for batch operations - this.server.addTool({ - name: "executeBatchOperations", - description: "Execute multiple operations in a single request", - parameters: z.object({ - operations: z - .array( - z.object({ - operation: z.string().describe("The operation to execute"), - contextId: z.string().describe("The ID of the context to use"), - parameters: z - .record(z.any()) - .optional() - .describe("Additional parameters"), - versionId: z - .string() - .optional() - .describe("Optional context version"), - }) - ) - .describe("Array of operations to execute in sequence"), - }), - execute: async (args) => { - const results = []; - let hasErrors = false; - - for (const op of args.operations) { - try { - const context = await this.contextManager.getContext( - op.contextId, - op.versionId - ); - - let result; - switch (op.operation) { - case "generateTask": - result = await this.executeGenerateTask(context, op.parameters); - break; - case "expandTask": - result = await this.executeExpandTask(context, op.parameters); - break; - case "analyzeComplexity": - result = await this.executeAnalyzeComplexity( - context, - op.parameters - ); - break; - case "mergeContexts": - result = await this.executeMergeContexts( - context, - op.parameters - ); - break; - case "searchContexts": - result = await this.executeSearchContexts(op.parameters); - break; - case "extractInsights": - result = await this.executeExtractInsights( - context, - op.parameters - ); - break; - case "syncWithRepository": - result = await this.executeSyncWithRepository( - context, - op.parameters - ); - break; - default: - result = { - success: false, - error: `Unknown operation: ${op.operation}`, - }; - hasErrors = true; - } - - results.push({ - operation: op.operation, - contextId: op.contextId, - result: result, - }); - - if (!result.success) { - hasErrors = true; - } - } catch (error) { - this.logger.error( - `Error in batch operation ${op.operation}: ${error.message}` - ); - results.push({ - operation: op.operation, - contextId: op.contextId, - result: { - success: false, - error: error.message, - }, - }); - hasErrors = true; - } - } - - return { - success: !hasErrors, - results: results, - }; - }, - }); - - this.logger.info("Registered execute endpoint handlers"); - } - - /** - * Execute the generateTask operation - * @param {object} context - The context to use - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeGenerateTask(context, parameters = {}) { - // This is a placeholder for actual task generation logic - // In a real implementation, this would use Task Master's task generation - - this.logger.info(`Generating task with context ${context.id}`); - - // Improved task generation with more detailed result - const task = { - id: Math.floor(Math.random() * 1000), - title: parameters.title || "New Task", - description: parameters.description || "Task generated from context", - status: "pending", - dependencies: parameters.dependencies || [], - priority: parameters.priority || "medium", - details: `This task was generated using context ${ - context.id - }.\n\n${JSON.stringify(context.data, null, 2)}`, - metadata: { - generatedAt: new Date().toISOString(), - generatedFrom: context.id, - contextVersion: context.metadata.version, - generatedBy: parameters.user || "system", - }, - }; - - return { - success: true, - task, - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - }; - } - - /** - * Execute the expandTask operation - * @param {object} context - The context to use - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeExpandTask(context, parameters = {}) { - // This is a placeholder for actual task expansion logic - // In a real implementation, this would use Task Master's task expansion - - this.logger.info(`Expanding task with context ${context.id}`); - - // Enhanced task expansion with more configurable options - const numSubtasks = parameters.numSubtasks || 3; - const subtaskPrefix = parameters.subtaskPrefix || ""; - const subtasks = []; - - for (let i = 1; i <= numSubtasks; i++) { - subtasks.push({ - id: `${subtaskPrefix}${i}`, - title: parameters.titleTemplate - ? parameters.titleTemplate.replace("{i}", i) - : `Subtask ${i}`, - description: parameters.descriptionTemplate - ? parameters.descriptionTemplate - .replace("{i}", i) - .replace("{taskId}", parameters.taskId || "unknown") - : `Subtask ${i} for ${parameters.taskId || "unknown task"}`, - dependencies: i > 1 ? [i - 1] : [], - status: "pending", - metadata: { - expandedAt: new Date().toISOString(), - expandedFrom: context.id, - contextVersion: context.metadata.version, - expandedBy: parameters.user || "system", - }, - }); - } - - return { - success: true, - taskId: parameters.taskId, - subtasks, - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - }; - } - - /** - * Execute the analyzeComplexity operation - * @param {object} context - The context to use - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeAnalyzeComplexity(context, parameters = {}) { - // This is a placeholder for actual complexity analysis logic - // In a real implementation, this would use Task Master's complexity analysis - - this.logger.info(`Analyzing complexity with context ${context.id}`); - - // Enhanced complexity analysis with more detailed factors - const complexityScore = Math.floor(Math.random() * 10) + 1; - const recommendedSubtasks = Math.floor(complexityScore / 2) + 1; - - // More detailed analysis with weighted factors - const factors = [ - { - name: "Task scope breadth", - score: Math.floor(Math.random() * 10) + 1, - weight: 0.3, - description: "How broad is the scope of this task", - }, - { - name: "Technical complexity", - score: Math.floor(Math.random() * 10) + 1, - weight: 0.4, - description: "How technically complex is the implementation", - }, - { - name: "External dependencies", - score: Math.floor(Math.random() * 10) + 1, - weight: 0.2, - description: "How many external dependencies does this task have", - }, - { - name: "Risk assessment", - score: Math.floor(Math.random() * 10) + 1, - weight: 0.1, - description: "What is the risk level of this task", - }, - ]; - - return { - success: true, - analysis: { - taskId: parameters.taskId || "unknown", - complexityScore, - recommendedSubtasks, - factors, - recommendedTimeEstimate: `${complexityScore * 2}-${ - complexityScore * 4 - } hours`, - metadata: { - analyzedAt: new Date().toISOString(), - analyzedUsing: context.id, - contextVersion: context.metadata.version, - analyzedBy: parameters.user || "system", - }, - }, - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - }; - } - - /** - * Execute the mergeContexts operation - * @param {object} primaryContext - The primary context to use - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeMergeContexts(primaryContext, parameters = {}) { - this.logger.info( - `Merging contexts with primary context ${primaryContext.id}` - ); - - if ( - !parameters.contextIds || - !Array.isArray(parameters.contextIds) || - parameters.contextIds.length === 0 - ) { - return { - success: false, - error: "No context IDs provided for merging", - }; - } - - if (!parameters.newContextId) { - return { - success: false, - error: "New context ID is required for the merged context", - }; - } - - try { - // Add the primary context to the list if not already included - if (!parameters.contextIds.includes(primaryContext.id)) { - parameters.contextIds.unshift(primaryContext.id); - } - - const mergedContext = await this.contextManager.mergeContexts( - parameters.contextIds, - parameters.newContextId, - { - mergedAt: new Date().toISOString(), - mergedBy: parameters.user || "system", - mergeStrategy: parameters.strategy || "concatenate", - ...parameters.metadata, - } - ); - - return { - success: true, - mergedContext, - sourceContexts: parameters.contextIds, - }; - } catch (error) { - this.logger.error(`Error merging contexts: ${error.message}`); - return { - success: false, - error: error.message, - }; - } - } - - /** - * Execute the searchContexts operation - * @param {object} parameters - Search parameters - * @returns {Promise} The result of the operation - */ - async executeSearchContexts(parameters = {}) { - this.logger.info( - `Searching contexts with query: ${parameters.query || ""}` - ); - - try { - const searchResults = await this.contextManager.listContexts({ - query: parameters.query || "", - filters: parameters.filters || {}, - limit: parameters.limit || 100, - offset: parameters.offset || 0, - sortBy: parameters.sortBy || "updated", - sortDirection: parameters.sortDirection || "desc", - }); - - return { - success: true, - ...searchResults, - }; - } catch (error) { - this.logger.error(`Error searching contexts: ${error.message}`); - return { - success: false, - error: error.message, - }; - } - } - - /** - * Execute the extractInsights operation - * @param {object} context - The context to analyze - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeExtractInsights(context, parameters = {}) { - this.logger.info(`Extracting insights from context ${context.id}`); - - // Placeholder for actual insight extraction - // In a real implementation, this would perform analysis on the context data - - const insights = [ - { - type: "summary", - content: `Summary of context ${context.id}`, - confidence: 0.85, - }, - { - type: "key_points", - content: ["First key point", "Second key point", "Third key point"], - confidence: 0.78, - }, - { - type: "recommendations", - content: ["First recommendation", "Second recommendation"], - confidence: 0.72, - }, - ]; - - return { - success: true, - insights, - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - metadata: { - extractedAt: new Date().toISOString(), - model: parameters.model || "default", - extractedBy: parameters.user || "system", - }, - }; - } - - /** - * Execute the syncWithRepository operation - * @param {object} context - The context to sync - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeSyncWithRepository(context, parameters = {}) { - this.logger.info(`Syncing context ${context.id} with repository`); - - // Placeholder for actual repository sync - // In a real implementation, this would sync the context with an external repository - - return { - success: true, - syncStatus: "complete", - syncedTo: parameters.repository || "default", - syncTimestamp: new Date().toISOString(), - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - }; - } -} - -export default MCPApiHandlers; diff --git a/mcp-server/src/auth.js b/mcp-server/src/auth.js deleted file mode 100644 index 22c36973..00000000 --- a/mcp-server/src/auth.js +++ /dev/null @@ -1,285 +0,0 @@ -import jwt from "jsonwebtoken"; -import { logger } from "../../scripts/modules/utils.js"; -import crypto from "crypto"; -import fs from "fs/promises"; -import path from "path"; -import { fileURLToPath } from "url"; - -// Constants -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const API_KEYS_FILE = - process.env.MCP_API_KEYS_FILE || path.join(__dirname, "../api-keys.json"); -const JWT_SECRET = - process.env.MCP_JWT_SECRET || "task-master-mcp-server-secret"; -const JWT_EXPIRATION = process.env.MCP_JWT_EXPIRATION || "24h"; - -/** - * Authentication middleware and utilities for MCP server - */ -class MCPAuth { - constructor() { - this.apiKeys = new Map(); - this.logger = logger; - this.loadApiKeys(); - } - - /** - * Load API keys from disk - */ - async loadApiKeys() { - try { - // Create API keys file if it doesn't exist - try { - await fs.access(API_KEYS_FILE); - } catch (error) { - // File doesn't exist, create it with a default admin key - const defaultApiKey = this.generateApiKey(); - const defaultApiKeys = { - keys: [ - { - id: "admin", - key: defaultApiKey, - role: "admin", - created: new Date().toISOString(), - }, - ], - }; - - await fs.mkdir(path.dirname(API_KEYS_FILE), { recursive: true }); - await fs.writeFile( - API_KEYS_FILE, - JSON.stringify(defaultApiKeys, null, 2), - "utf8" - ); - - this.logger.info( - `Created default API keys file with admin key: ${defaultApiKey}` - ); - } - - // Load API keys - const data = await fs.readFile(API_KEYS_FILE, "utf8"); - const apiKeys = JSON.parse(data); - - apiKeys.keys.forEach((key) => { - this.apiKeys.set(key.key, { - id: key.id, - role: key.role, - created: key.created, - }); - }); - - this.logger.info(`Loaded ${this.apiKeys.size} API keys`); - } catch (error) { - this.logger.error(`Failed to load API keys: ${error.message}`); - throw error; - } - } - - /** - * Save API keys to disk - */ - async saveApiKeys() { - try { - const keys = []; - - this.apiKeys.forEach((value, key) => { - keys.push({ - id: value.id, - key, - role: value.role, - created: value.created, - }); - }); - - await fs.writeFile( - API_KEYS_FILE, - JSON.stringify({ keys }, null, 2), - "utf8" - ); - - this.logger.info(`Saved ${keys.length} API keys`); - } catch (error) { - this.logger.error(`Failed to save API keys: ${error.message}`); - throw error; - } - } - - /** - * Generate a new API key - * @returns {string} The generated API key - */ - generateApiKey() { - return crypto.randomBytes(32).toString("hex"); - } - - /** - * Create a new API key - * @param {string} id - Client identifier - * @param {string} role - Client role (admin, user) - * @returns {string} The generated API key - */ - async createApiKey(id, role = "user") { - const apiKey = this.generateApiKey(); - - this.apiKeys.set(apiKey, { - id, - role, - created: new Date().toISOString(), - }); - - await this.saveApiKeys(); - - this.logger.info(`Created new API key for ${id} with role ${role}`); - return apiKey; - } - - /** - * Revoke an API key - * @param {string} apiKey - The API key to revoke - * @returns {boolean} True if the key was revoked - */ - async revokeApiKey(apiKey) { - if (!this.apiKeys.has(apiKey)) { - return false; - } - - this.apiKeys.delete(apiKey); - await this.saveApiKeys(); - - this.logger.info(`Revoked API key`); - return true; - } - - /** - * Validate an API key - * @param {string} apiKey - The API key to validate - * @returns {object|null} The API key details if valid, null otherwise - */ - validateApiKey(apiKey) { - return this.apiKeys.get(apiKey) || null; - } - - /** - * Generate a JWT token for a client - * @param {string} clientId - Client identifier - * @param {string} role - Client role - * @returns {string} The JWT token - */ - generateToken(clientId, role) { - return jwt.sign({ clientId, role }, JWT_SECRET, { - expiresIn: JWT_EXPIRATION, - }); - } - - /** - * Verify a JWT token - * @param {string} token - The JWT token to verify - * @returns {object|null} The token payload if valid, null otherwise - */ - verifyToken(token) { - try { - return jwt.verify(token, JWT_SECRET); - } catch (error) { - this.logger.error(`Failed to verify token: ${error.message}`); - return null; - } - } - - /** - * Express middleware for API key authentication - * @param {object} req - Express request object - * @param {object} res - Express response object - * @param {function} next - Express next function - */ - authenticateApiKey(req, res, next) { - const apiKey = req.headers["x-api-key"]; - - if (!apiKey) { - return res.status(401).json({ - success: false, - error: "API key is required", - }); - } - - const keyDetails = this.validateApiKey(apiKey); - - if (!keyDetails) { - return res.status(401).json({ - success: false, - error: "Invalid API key", - }); - } - - // Attach client info to request - req.client = { - id: keyDetails.id, - role: keyDetails.role, - }; - - next(); - } - - /** - * Express middleware for JWT authentication - * @param {object} req - Express request object - * @param {object} res - Express response object - * @param {function} next - Express next function - */ - authenticateToken(req, res, next) { - const authHeader = req.headers["authorization"]; - const token = authHeader && authHeader.split(" ")[1]; - - if (!token) { - return res.status(401).json({ - success: false, - error: "Authentication token is required", - }); - } - - const payload = this.verifyToken(token); - - if (!payload) { - return res.status(401).json({ - success: false, - error: "Invalid or expired token", - }); - } - - // Attach client info to request - req.client = { - id: payload.clientId, - role: payload.role, - }; - - next(); - } - - /** - * Express middleware for role-based authorization - * @param {Array} roles - Array of allowed roles - * @returns {function} Express middleware - */ - authorizeRoles(roles) { - return (req, res, next) => { - if (!req.client || !req.client.role) { - return res.status(401).json({ - success: false, - error: "Unauthorized: Authentication required", - }); - } - - if (!roles.includes(req.client.role)) { - return res.status(403).json({ - success: false, - error: "Forbidden: Insufficient permissions", - }); - } - - next(); - }; - } -} - -export default MCPAuth; diff --git a/mcp-server/src/context-manager.js b/mcp-server/src/context-manager.js deleted file mode 100644 index 5b94b538..00000000 --- a/mcp-server/src/context-manager.js +++ /dev/null @@ -1,873 +0,0 @@ -import { logger } from "../../scripts/modules/utils.js"; -import fs from "fs/promises"; -import path from "path"; -import { fileURLToPath } from "url"; -import crypto from "crypto"; -import Fuse from "fuse.js"; - -// Constants -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const CONTEXT_DIR = - process.env.MCP_CONTEXT_DIR || path.join(__dirname, "../contexts"); -const MAX_CONTEXT_HISTORY = parseInt( - process.env.MCP_MAX_CONTEXT_HISTORY || "10", - 10 -); - -/** - * Context Manager for MCP server - * Handles storage, retrieval, and manipulation of context data - * Implements efficient indexing, versioning, and advanced context operations - */ -class ContextManager { - constructor() { - this.contexts = new Map(); - this.contextHistory = new Map(); // For version history - this.contextIndex = null; // For fuzzy search - this.logger = logger; - this.ensureContextDir(); - this.rebuildSearchIndex(); - } - - /** - * Ensure the contexts directory exists - */ - async ensureContextDir() { - try { - await fs.mkdir(CONTEXT_DIR, { recursive: true }); - this.logger.info(`Context directory ensured at ${CONTEXT_DIR}`); - - // Also create a versions subdirectory for history - await fs.mkdir(path.join(CONTEXT_DIR, "versions"), { recursive: true }); - } catch (error) { - this.logger.error(`Failed to create context directory: ${error.message}`); - throw error; - } - } - - /** - * Rebuild the search index for efficient context lookup - */ - async rebuildSearchIndex() { - await this.loadAllContextsFromDisk(); - - const contextsForIndex = Array.from(this.contexts.values()).map((ctx) => ({ - id: ctx.id, - content: - typeof ctx.data === "string" ? ctx.data : JSON.stringify(ctx.data), - tags: ctx.tags.join(" "), - metadata: Object.entries(ctx.metadata) - .map(([k, v]) => `${k}:${v}`) - .join(" "), - })); - - this.contextIndex = new Fuse(contextsForIndex, { - keys: ["id", "content", "tags", "metadata"], - includeScore: true, - threshold: 0.6, - }); - - this.logger.info( - `Rebuilt search index with ${contextsForIndex.length} contexts` - ); - } - - /** - * Create a new context - * @param {string} contextId - Unique identifier for the context - * @param {object|string} contextData - Initial context data - * @param {object} metadata - Optional metadata for the context - * @returns {object} The created context - */ - async createContext(contextId, contextData, metadata = {}) { - if (this.contexts.has(contextId)) { - throw new Error(`Context with ID ${contextId} already exists`); - } - - const timestamp = new Date().toISOString(); - const versionId = this.generateVersionId(); - - const context = { - id: contextId, - data: contextData, - metadata: { - created: timestamp, - updated: timestamp, - version: versionId, - ...metadata, - }, - tags: metadata.tags || [], - size: this.estimateSize(contextData), - }; - - this.contexts.set(contextId, context); - - // Initialize version history - this.contextHistory.set(contextId, [ - { - versionId, - timestamp, - data: JSON.parse(JSON.stringify(contextData)), // Deep clone - metadata: { ...context.metadata }, - }, - ]); - - await this.persistContext(contextId); - await this.persistContextVersion(contextId, versionId); - - // Update the search index - this.rebuildSearchIndex(); - - this.logger.info(`Created context: ${contextId} (version: ${versionId})`); - return context; - } - - /** - * Retrieve a context by ID - * @param {string} contextId - The context ID to retrieve - * @param {string} versionId - Optional specific version to retrieve - * @returns {object} The context object - */ - async getContext(contextId, versionId = null) { - // If specific version requested, try to get it from history - if (versionId) { - return this.getContextVersion(contextId, versionId); - } - - // Try to get from memory first - if (this.contexts.has(contextId)) { - return this.contexts.get(contextId); - } - - // Try to load from disk - try { - const context = await this.loadContextFromDisk(contextId); - if (context) { - this.contexts.set(contextId, context); - return context; - } - } catch (error) { - this.logger.error( - `Failed to load context ${contextId}: ${error.message}` - ); - } - - throw new Error(`Context with ID ${contextId} not found`); - } - - /** - * Get a specific version of a context - * @param {string} contextId - The context ID - * @param {string} versionId - The version ID - * @returns {object} The versioned context - */ - async getContextVersion(contextId, versionId) { - // Check if version history is in memory - if (this.contextHistory.has(contextId)) { - const history = this.contextHistory.get(contextId); - const version = history.find((v) => v.versionId === versionId); - if (version) { - return { - id: contextId, - data: version.data, - metadata: version.metadata, - tags: version.metadata.tags || [], - size: this.estimateSize(version.data), - versionId: version.versionId, - }; - } - } - - // Try to load from disk - try { - const versionPath = path.join( - CONTEXT_DIR, - "versions", - `${contextId}_${versionId}.json` - ); - const data = await fs.readFile(versionPath, "utf8"); - const version = JSON.parse(data); - - // Add to memory cache - if (!this.contextHistory.has(contextId)) { - this.contextHistory.set(contextId, []); - } - const history = this.contextHistory.get(contextId); - history.push(version); - - return { - id: contextId, - data: version.data, - metadata: version.metadata, - tags: version.metadata.tags || [], - size: this.estimateSize(version.data), - versionId: version.versionId, - }; - } catch (error) { - this.logger.error( - `Failed to load context version ${contextId}@${versionId}: ${error.message}` - ); - throw new Error( - `Context version ${versionId} for ${contextId} not found` - ); - } - } - - /** - * Update an existing context - * @param {string} contextId - The context ID to update - * @param {object|string} contextData - New context data - * @param {object} metadata - Optional metadata updates - * @param {boolean} createNewVersion - Whether to create a new version - * @returns {object} The updated context - */ - async updateContext( - contextId, - contextData, - metadata = {}, - createNewVersion = true - ) { - const context = await this.getContext(contextId); - const timestamp = new Date().toISOString(); - - // Generate a new version ID if requested - const versionId = createNewVersion - ? this.generateVersionId() - : context.metadata.version; - - // Create a backup of the current state for versioning - if (createNewVersion) { - // Store the current version in history - if (!this.contextHistory.has(contextId)) { - this.contextHistory.set(contextId, []); - } - - const history = this.contextHistory.get(contextId); - - // Add current state to history - history.push({ - versionId: context.metadata.version, - timestamp: context.metadata.updated, - data: JSON.parse(JSON.stringify(context.data)), // Deep clone - metadata: { ...context.metadata }, - }); - - // Trim history if it exceeds the maximum size - if (history.length > MAX_CONTEXT_HISTORY) { - const excessVersions = history.splice( - 0, - history.length - MAX_CONTEXT_HISTORY - ); - // Clean up excess versions from disk - for (const version of excessVersions) { - this.removeContextVersionFile(contextId, version.versionId).catch( - (err) => - this.logger.error( - `Failed to remove old version file: ${err.message}` - ) - ); - } - } - - // Persist version - await this.persistContextVersion(contextId, context.metadata.version); - } - - // Update the context - context.data = contextData; - context.metadata = { - ...context.metadata, - ...metadata, - updated: timestamp, - }; - - if (createNewVersion) { - context.metadata.version = versionId; - context.metadata.previousVersion = context.metadata.version; - } - - if (metadata.tags) { - context.tags = metadata.tags; - } - - // Update size estimate - context.size = this.estimateSize(contextData); - - this.contexts.set(contextId, context); - await this.persistContext(contextId); - - // Update the search index - this.rebuildSearchIndex(); - - this.logger.info(`Updated context: ${contextId} (version: ${versionId})`); - return context; - } - - /** - * Delete a context and all its versions - * @param {string} contextId - The context ID to delete - * @returns {boolean} True if deletion was successful - */ - async deleteContext(contextId) { - if (!this.contexts.has(contextId)) { - const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); - try { - await fs.access(contextPath); - } catch (error) { - throw new Error(`Context with ID ${contextId} not found`); - } - } - - this.contexts.delete(contextId); - - // Remove from history - const history = this.contextHistory.get(contextId) || []; - this.contextHistory.delete(contextId); - - try { - // Delete main context file - const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); - await fs.unlink(contextPath); - - // Delete all version files - for (const version of history) { - await this.removeContextVersionFile(contextId, version.versionId); - } - - // Update the search index - this.rebuildSearchIndex(); - - this.logger.info(`Deleted context: ${contextId}`); - return true; - } catch (error) { - this.logger.error( - `Failed to delete context files for ${contextId}: ${error.message}` - ); - throw error; - } - } - - /** - * List all available contexts with pagination and advanced filtering - * @param {object} options - Options for listing contexts - * @param {object} options.filters - Filters to apply - * @param {number} options.limit - Maximum number of contexts to return - * @param {number} options.offset - Number of contexts to skip - * @param {string} options.sortBy - Field to sort by - * @param {string} options.sortDirection - Sort direction ('asc' or 'desc') - * @param {string} options.query - Free text search query - * @returns {Array} Array of context objects - */ - async listContexts(options = {}) { - // Load all contexts from disk first - await this.loadAllContextsFromDisk(); - - const { - filters = {}, - limit = 100, - offset = 0, - sortBy = "updated", - sortDirection = "desc", - query = "", - } = options; - - let contexts; - - // If there's a search query, use the search index - if (query && this.contextIndex) { - const searchResults = this.contextIndex.search(query); - contexts = searchResults.map((result) => - this.contexts.get(result.item.id) - ); - } else { - contexts = Array.from(this.contexts.values()); - } - - // Apply filters - if (filters.tag) { - contexts = contexts.filter( - (ctx) => ctx.tags && ctx.tags.includes(filters.tag) - ); - } - - if (filters.metadataKey && filters.metadataValue) { - contexts = contexts.filter( - (ctx) => - ctx.metadata && - ctx.metadata[filters.metadataKey] === filters.metadataValue - ); - } - - if (filters.createdAfter) { - const timestamp = new Date(filters.createdAfter); - contexts = contexts.filter( - (ctx) => new Date(ctx.metadata.created) >= timestamp - ); - } - - if (filters.updatedAfter) { - const timestamp = new Date(filters.updatedAfter); - contexts = contexts.filter( - (ctx) => new Date(ctx.metadata.updated) >= timestamp - ); - } - - // Apply sorting - contexts.sort((a, b) => { - let valueA, valueB; - - if (sortBy === "created" || sortBy === "updated") { - valueA = new Date(a.metadata[sortBy]).getTime(); - valueB = new Date(b.metadata[sortBy]).getTime(); - } else if (sortBy === "size") { - valueA = a.size || 0; - valueB = b.size || 0; - } else if (sortBy === "id") { - valueA = a.id; - valueB = b.id; - } else { - valueA = a.metadata[sortBy]; - valueB = b.metadata[sortBy]; - } - - if (valueA === valueB) return 0; - - const sortFactor = sortDirection === "asc" ? 1 : -1; - return valueA < valueB ? -1 * sortFactor : 1 * sortFactor; - }); - - // Apply pagination - const paginatedContexts = contexts.slice(offset, offset + limit); - - return { - contexts: paginatedContexts, - total: contexts.length, - offset, - limit, - hasMore: offset + limit < contexts.length, - }; - } - - /** - * Get the version history of a context - * @param {string} contextId - The context ID - * @returns {Array} Array of version objects - */ - async getContextHistory(contextId) { - // Ensure context exists - await this.getContext(contextId); - - // Load history if not in memory - if (!this.contextHistory.has(contextId)) { - await this.loadContextHistoryFromDisk(contextId); - } - - const history = this.contextHistory.get(contextId) || []; - - // Return versions in reverse chronological order (newest first) - return history.sort((a, b) => { - const timeA = new Date(a.timestamp).getTime(); - const timeB = new Date(b.timestamp).getTime(); - return timeB - timeA; - }); - } - - /** - * Add tags to a context - * @param {string} contextId - The context ID - * @param {Array} tags - Array of tags to add - * @returns {object} The updated context - */ - async addTags(contextId, tags) { - const context = await this.getContext(contextId); - - const currentTags = context.tags || []; - const uniqueTags = [...new Set([...currentTags, ...tags])]; - - // Update context with new tags - return this.updateContext( - contextId, - context.data, - { - tags: uniqueTags, - }, - false - ); // Don't create a new version for tag updates - } - - /** - * Remove tags from a context - * @param {string} contextId - The context ID - * @param {Array} tags - Array of tags to remove - * @returns {object} The updated context - */ - async removeTags(contextId, tags) { - const context = await this.getContext(contextId); - - const currentTags = context.tags || []; - const newTags = currentTags.filter((tag) => !tags.includes(tag)); - - // Update context with new tags - return this.updateContext( - contextId, - context.data, - { - tags: newTags, - }, - false - ); // Don't create a new version for tag updates - } - - /** - * Handle context windowing and truncation - * @param {string} contextId - The context ID - * @param {number} maxSize - Maximum size in tokens/chars - * @param {string} strategy - Truncation strategy ('start', 'end', 'middle') - * @returns {object} The truncated context - */ - async truncateContext(contextId, maxSize, strategy = "end") { - const context = await this.getContext(contextId); - const contextText = - typeof context.data === "string" - ? context.data - : JSON.stringify(context.data); - - if (contextText.length <= maxSize) { - return context; // No truncation needed - } - - let truncatedData; - - switch (strategy) { - case "start": - truncatedData = contextText.slice(contextText.length - maxSize); - break; - case "middle": - const halfSize = Math.floor(maxSize / 2); - truncatedData = - contextText.slice(0, halfSize) + - "...[truncated]..." + - contextText.slice(contextText.length - halfSize); - break; - case "end": - default: - truncatedData = contextText.slice(0, maxSize); - break; - } - - // If original data was an object, try to parse the truncated data - // Otherwise use it as a string - let updatedData; - if (typeof context.data === "object") { - try { - // This may fail if truncation broke JSON structure - updatedData = { - ...context.data, - truncated: true, - truncation_strategy: strategy, - original_size: contextText.length, - truncated_size: truncatedData.length, - }; - } catch (error) { - updatedData = truncatedData; - } - } else { - updatedData = truncatedData; - } - - // Update with truncated data - return this.updateContext( - contextId, - updatedData, - { - truncated: true, - truncation_strategy: strategy, - original_size: contextText.length, - truncated_size: truncatedData.length, - }, - true - ); // Create a new version for the truncated data - } - - /** - * Merge multiple contexts into a new context - * @param {Array} contextIds - Array of context IDs to merge - * @param {string} newContextId - ID for the new merged context - * @param {object} metadata - Optional metadata for the new context - * @returns {object} The new merged context - */ - async mergeContexts(contextIds, newContextId, metadata = {}) { - if (contextIds.length === 0) { - throw new Error("At least one context ID must be provided for merging"); - } - - if (this.contexts.has(newContextId)) { - throw new Error(`Context with ID ${newContextId} already exists`); - } - - // Load all contexts to be merged - const contextsToMerge = []; - for (const id of contextIds) { - try { - const context = await this.getContext(id); - contextsToMerge.push(context); - } catch (error) { - this.logger.error( - `Could not load context ${id} for merging: ${error.message}` - ); - throw new Error(`Failed to merge contexts: ${error.message}`); - } - } - - // Check data types and decide how to merge - const allStrings = contextsToMerge.every((c) => typeof c.data === "string"); - const allObjects = contextsToMerge.every( - (c) => typeof c.data === "object" && c.data !== null - ); - - let mergedData; - - if (allStrings) { - // Merge strings with newlines between them - mergedData = contextsToMerge.map((c) => c.data).join("\n\n"); - } else if (allObjects) { - // Merge objects by combining their properties - mergedData = {}; - for (const context of contextsToMerge) { - mergedData = { ...mergedData, ...context.data }; - } - } else { - // Convert everything to strings and concatenate - mergedData = contextsToMerge - .map((c) => - typeof c.data === "string" ? c.data : JSON.stringify(c.data) - ) - .join("\n\n"); - } - - // Collect all tags from merged contexts - const allTags = new Set(); - for (const context of contextsToMerge) { - for (const tag of context.tags || []) { - allTags.add(tag); - } - } - - // Create merged metadata - const mergedMetadata = { - ...metadata, - tags: [...allTags], - merged_from: contextIds, - merged_at: new Date().toISOString(), - }; - - // Create the new merged context - return this.createContext(newContextId, mergedData, mergedMetadata); - } - - /** - * Persist a context to disk - * @param {string} contextId - The context ID to persist - * @returns {Promise} - */ - async persistContext(contextId) { - const context = this.contexts.get(contextId); - if (!context) { - throw new Error(`Context with ID ${contextId} not found`); - } - - const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); - try { - await fs.writeFile(contextPath, JSON.stringify(context, null, 2), "utf8"); - this.logger.debug(`Persisted context ${contextId} to disk`); - } catch (error) { - this.logger.error( - `Failed to persist context ${contextId}: ${error.message}` - ); - throw error; - } - } - - /** - * Persist a context version to disk - * @param {string} contextId - The context ID - * @param {string} versionId - The version ID - * @returns {Promise} - */ - async persistContextVersion(contextId, versionId) { - if (!this.contextHistory.has(contextId)) { - throw new Error(`Context history for ${contextId} not found`); - } - - const history = this.contextHistory.get(contextId); - const version = history.find((v) => v.versionId === versionId); - - if (!version) { - throw new Error(`Version ${versionId} of context ${contextId} not found`); - } - - const versionPath = path.join( - CONTEXT_DIR, - "versions", - `${contextId}_${versionId}.json` - ); - try { - await fs.writeFile(versionPath, JSON.stringify(version, null, 2), "utf8"); - this.logger.debug( - `Persisted context version ${contextId}@${versionId} to disk` - ); - } catch (error) { - this.logger.error( - `Failed to persist context version ${contextId}@${versionId}: ${error.message}` - ); - throw error; - } - } - - /** - * Remove a context version file from disk - * @param {string} contextId - The context ID - * @param {string} versionId - The version ID - * @returns {Promise} - */ - async removeContextVersionFile(contextId, versionId) { - const versionPath = path.join( - CONTEXT_DIR, - "versions", - `${contextId}_${versionId}.json` - ); - try { - await fs.unlink(versionPath); - this.logger.debug( - `Removed context version file ${contextId}@${versionId}` - ); - } catch (error) { - if (error.code !== "ENOENT") { - this.logger.error( - `Failed to remove context version file ${contextId}@${versionId}: ${error.message}` - ); - throw error; - } - } - } - - /** - * Load a context from disk - * @param {string} contextId - The context ID to load - * @returns {Promise} The loaded context - */ - async loadContextFromDisk(contextId) { - const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); - try { - const data = await fs.readFile(contextPath, "utf8"); - const context = JSON.parse(data); - this.logger.debug(`Loaded context ${contextId} from disk`); - return context; - } catch (error) { - this.logger.error( - `Failed to load context ${contextId} from disk: ${error.message}` - ); - throw error; - } - } - - /** - * Load context history from disk - * @param {string} contextId - The context ID - * @returns {Promise} The loaded history - */ - async loadContextHistoryFromDisk(contextId) { - try { - const files = await fs.readdir(path.join(CONTEXT_DIR, "versions")); - const versionFiles = files.filter( - (file) => file.startsWith(`${contextId}_`) && file.endsWith(".json") - ); - - const history = []; - - for (const file of versionFiles) { - try { - const data = await fs.readFile( - path.join(CONTEXT_DIR, "versions", file), - "utf8" - ); - const version = JSON.parse(data); - history.push(version); - } catch (error) { - this.logger.error( - `Failed to load context version file ${file}: ${error.message}` - ); - } - } - - this.contextHistory.set(contextId, history); - this.logger.debug( - `Loaded ${history.length} versions for context ${contextId}` - ); - - return history; - } catch (error) { - this.logger.error( - `Failed to load context history for ${contextId}: ${error.message}` - ); - this.contextHistory.set(contextId, []); - return []; - } - } - - /** - * Load all contexts from disk - * @returns {Promise} - */ - async loadAllContextsFromDisk() { - try { - const files = await fs.readdir(CONTEXT_DIR); - const contextFiles = files.filter((file) => file.endsWith(".json")); - - for (const file of contextFiles) { - const contextId = path.basename(file, ".json"); - if (!this.contexts.has(contextId)) { - try { - const context = await this.loadContextFromDisk(contextId); - this.contexts.set(contextId, context); - } catch (error) { - // Already logged in loadContextFromDisk - } - } - } - - this.logger.info(`Loaded ${this.contexts.size} contexts from disk`); - } catch (error) { - this.logger.error(`Failed to load contexts from disk: ${error.message}`); - throw error; - } - } - - /** - * Generate a unique version ID - * @returns {string} A unique version ID - */ - generateVersionId() { - return crypto.randomBytes(8).toString("hex"); - } - - /** - * Estimate the size of context data - * @param {object|string} data - The context data - * @returns {number} Estimated size in bytes - */ - estimateSize(data) { - if (typeof data === "string") { - return Buffer.byteLength(data, "utf8"); - } - - if (typeof data === "object" && data !== null) { - return Buffer.byteLength(JSON.stringify(data), "utf8"); - } - - return 0; - } -} - -export default ContextManager; diff --git a/mcp-server/src/index.js b/mcp-server/src/index.js index eb820f95..3fe17b58 100644 --- a/mcp-server/src/index.js +++ b/mcp-server/src/index.js @@ -1,16 +1,10 @@ import { FastMCP } from "fastmcp"; -import { z } from "zod"; import path from "path"; -import fs from "fs/promises"; import dotenv from "dotenv"; import { fileURLToPath } from "url"; -import express from "express"; -import cors from "cors"; -import helmet from "helmet"; -import { logger } from "../../scripts/modules/utils.js"; -import MCPAuth from "./auth.js"; -import MCPApiHandlers from "./api-handlers.js"; -import ContextManager from "./context-manager.js"; +import fs from "fs"; +import logger from "./logger.js"; +import { registerTaskMasterTools } from "./tools/index.js"; // Load environment variables dotenv.config(); @@ -18,25 +12,27 @@ dotenv.config(); // Constants const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const DEFAULT_PORT = process.env.MCP_SERVER_PORT || 3000; -const DEFAULT_HOST = process.env.MCP_SERVER_HOST || "localhost"; /** * Main MCP server class that integrates with Task Master */ class TaskMasterMCPServer { - constructor(options = {}) { + constructor() { + // Get version from package.json using synchronous fs + const packagePath = path.join(__dirname, "../../package.json"); + const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); + this.options = { name: "Task Master MCP Server", - version: process.env.PROJECT_VERSION || "1.0.0", - ...options, + version: packageJson.version, }; this.server = new FastMCP(this.options); - this.expressApp = null; this.initialized = false; - this.auth = new MCPAuth(); - this.contextManager = new ContextManager(); + + // this.server.addResource({}); + + // this.server.addResourceTemplate({}); // Bind methods this.init = this.init.bind(this); @@ -53,301 +49,27 @@ class TaskMasterMCPServer { async init() { if (this.initialized) return; - this.logger.info("Initializing Task Master MCP server..."); - - // Set up express for additional customization if needed - this.expressApp = express(); - this.expressApp.use(cors()); - this.expressApp.use(helmet()); - this.expressApp.use(express.json()); - - // Set up authentication middleware - this.setupAuthentication(); - - // Register API handlers - this.apiHandlers = new MCPApiHandlers(this.server); - - // Register additional task master specific tools - this.registerTaskMasterTools(); + // Register Task Master tools + registerTaskMasterTools(this.server); this.initialized = true; - this.logger.info("Task Master MCP server initialized successfully"); return this; } - /** - * Set up authentication for the MCP server - */ - setupAuthentication() { - // Add a health check endpoint that doesn't require authentication - this.expressApp.get("/health", (req, res) => { - res.status(200).json({ - status: "ok", - service: this.options.name, - version: this.options.version, - }); - }); - - // Add an authenticate endpoint to get a JWT token using an API key - this.expressApp.post("/auth/token", async (req, res) => { - const apiKey = req.headers["x-api-key"]; - - if (!apiKey) { - return res.status(401).json({ - success: false, - error: "API key is required", - }); - } - - const keyDetails = this.auth.validateApiKey(apiKey); - - if (!keyDetails) { - return res.status(401).json({ - success: false, - error: "Invalid API key", - }); - } - - const token = this.auth.generateToken(keyDetails.id, keyDetails.role); - - res.status(200).json({ - success: true, - token, - expiresIn: process.env.MCP_JWT_EXPIRATION || "24h", - clientId: keyDetails.id, - role: keyDetails.role, - }); - }); - - // Create authenticator middleware for FastMCP - this.server.setAuthenticator((request) => { - // Get token from Authorization header - const authHeader = request.headers?.authorization; - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return null; - } - - const token = authHeader.split(" ")[1]; - const payload = this.auth.verifyToken(token); - - if (!payload) { - return null; - } - - return { - clientId: payload.clientId, - role: payload.role, - }; - }); - - // Set up a protected route for API key management (admin only) - this.expressApp.post( - "/auth/api-keys", - (req, res, next) => { - this.auth.authenticateToken(req, res, next); - }, - (req, res, next) => { - this.auth.authorizeRoles(["admin"])(req, res, next); - }, - async (req, res) => { - const { clientId, role } = req.body; - - if (!clientId) { - return res.status(400).json({ - success: false, - error: "Client ID is required", - }); - } - - try { - const apiKey = await this.auth.createApiKey(clientId, role || "user"); - - res.status(201).json({ - success: true, - apiKey, - clientId, - role: role || "user", - }); - } catch (error) { - this.logger.error(`Error creating API key: ${error.message}`); - - res.status(500).json({ - success: false, - error: "Failed to create API key", - }); - } - } - ); - - this.logger.info("Set up MCP authentication"); - } - - /** - * Register Task Master specific tools with the MCP server - */ - registerTaskMasterTools() { - // Add a tool to get tasks from Task Master - this.server.addTool({ - name: "listTasks", - description: "List all tasks from Task Master", - parameters: z.object({ - status: z.string().optional().describe("Filter tasks by status"), - withSubtasks: z - .boolean() - .optional() - .describe("Include subtasks in the response"), - }), - execute: async (args) => { - try { - // In a real implementation, this would use the Task Master API - // to fetch tasks. For now, returning mock data. - - this.logger.info( - `Listing tasks with filters: ${JSON.stringify(args)}` - ); - - // Mock task data - const tasks = [ - { - id: 1, - title: "Implement Task Data Structure", - status: "done", - dependencies: [], - priority: "high", - }, - { - id: 2, - title: "Develop Command Line Interface Foundation", - status: "done", - dependencies: [1], - priority: "high", - }, - { - id: 23, - title: "Implement MCP Server Functionality", - status: "in-progress", - dependencies: [22], - priority: "medium", - subtasks: [ - { - id: "23.1", - title: "Create Core MCP Server Module", - status: "in-progress", - dependencies: [], - }, - { - id: "23.2", - title: "Implement Context Management System", - status: "pending", - dependencies: ["23.1"], - }, - ], - }, - ]; - - // Apply status filter if provided - let filteredTasks = tasks; - if (args.status) { - filteredTasks = tasks.filter((task) => task.status === args.status); - } - - // Remove subtasks if not requested - if (!args.withSubtasks) { - filteredTasks = filteredTasks.map((task) => { - const { subtasks, ...taskWithoutSubtasks } = task; - return taskWithoutSubtasks; - }); - } - - return { success: true, tasks: filteredTasks }; - } catch (error) { - this.logger.error(`Error listing tasks: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to get task details - this.server.addTool({ - name: "getTaskDetails", - description: "Get detailed information about a specific task", - parameters: z.object({ - taskId: z - .union([z.number(), z.string()]) - .describe("The ID of the task to get details for"), - }), - execute: async (args) => { - try { - // In a real implementation, this would use the Task Master API - // to fetch task details. For now, returning mock data. - - this.logger.info(`Getting details for task ${args.taskId}`); - - // Mock task details - const taskDetails = { - id: 23, - title: "Implement MCP Server Functionality", - description: - "Extend Task Master to function as an MCP server, allowing it to provide context management services to other applications.", - status: "in-progress", - dependencies: [22], - priority: "medium", - details: - "This task involves implementing the Model Context Protocol server capabilities within Task Master.", - testStrategy: - "Testing should include unit tests, integration tests, and compatibility tests.", - subtasks: [ - { - id: "23.1", - title: "Create Core MCP Server Module", - status: "in-progress", - dependencies: [], - }, - { - id: "23.2", - title: "Implement Context Management System", - status: "pending", - dependencies: ["23.1"], - }, - ], - }; - - return { success: true, task: taskDetails }; - } catch (error) { - this.logger.error(`Error getting task details: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - this.logger.info("Registered Task Master specific tools"); - } - /** * Start the MCP server */ - async start({ port = DEFAULT_PORT, host = DEFAULT_HOST } = {}) { + async start() { if (!this.initialized) { await this.init(); } - this.logger.info( - `Starting Task Master MCP server on http://${host}:${port}` - ); - // Start the FastMCP server await this.server.start({ - port, - host, - transportType: "sse", - expressApp: this.expressApp, + transportType: "stdio", }); - this.logger.info( - `Task Master MCP server running at http://${host}:${port}` - ); - return this; } @@ -356,9 +78,7 @@ class TaskMasterMCPServer { */ async stop() { if (this.server) { - this.logger.info("Stopping Task Master MCP server..."); await this.server.stop(); - this.logger.info("Task Master MCP server stopped"); } } } diff --git a/mcp-server/src/logger.js b/mcp-server/src/logger.js new file mode 100644 index 00000000..80c0e55c --- /dev/null +++ b/mcp-server/src/logger.js @@ -0,0 +1,68 @@ +import chalk from "chalk"; + +// Define log levels +const LOG_LEVELS = { + debug: 0, + info: 1, + warn: 2, + error: 3, + success: 4, +}; + +// Get log level from environment or default to info +const LOG_LEVEL = process.env.LOG_LEVEL + ? LOG_LEVELS[process.env.LOG_LEVEL.toLowerCase()] + : LOG_LEVELS.info; + +/** + * Logs a message with the specified level + * @param {string} level - The log level (debug, info, warn, error, success) + * @param {...any} args - Arguments to log + */ +function log(level, ...args) { + const icons = { + debug: chalk.gray("🔍"), + info: chalk.blue("â„šī¸"), + warn: chalk.yellow("âš ī¸"), + error: chalk.red("❌"), + success: chalk.green("✅"), + }; + + if (LOG_LEVELS[level] >= LOG_LEVEL) { + const icon = icons[level] || ""; + + if (level === "error") { + console.error(icon, chalk.red(...args)); + } else if (level === "warn") { + console.warn(icon, chalk.yellow(...args)); + } else if (level === "success") { + console.log(icon, chalk.green(...args)); + } else if (level === "info") { + console.log(icon, chalk.blue(...args)); + } else { + console.log(icon, ...args); + } + } +} + +/** + * Create a logger object with methods for different log levels + * Can be used as a drop-in replacement for existing logger initialization + * @returns {Object} Logger object with info, error, debug, warn, and success methods + */ +export function createLogger() { + return { + debug: (message) => log("debug", message), + info: (message) => log("info", message), + warn: (message) => log("warn", message), + error: (message) => log("error", message), + success: (message) => log("success", message), + log: log, // Also expose the raw log function + }; +} + +// Export a default logger instance +const logger = createLogger(); + +export default logger; +export { log, LOG_LEVELS }; diff --git a/mcp-server/src/tools/addTask.js b/mcp-server/src/tools/addTask.js new file mode 100644 index 00000000..0622d0e8 --- /dev/null +++ b/mcp-server/src/tools/addTask.js @@ -0,0 +1,56 @@ +/** + * tools/addTask.js + * Tool to add a new task using AI + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the addTask tool with the MCP server + * @param {FastMCP} server - FastMCP server instance + */ +export function registerAddTaskTool(server) { + server.addTool({ + name: "addTask", + description: "Add a new task using AI", + parameters: z.object({ + prompt: z.string().describe("Description of the task to add"), + dependencies: z + .string() + .optional() + .describe("Comma-separated list of task IDs this task depends on"), + priority: z + .string() + .optional() + .describe("Task priority (high, medium, low)"), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Adding new task: ${args.prompt}`); + + const cmdArgs = [`--prompt="${args.prompt}"`]; + if (args.dependencies) + cmdArgs.push(`--dependencies=${args.dependencies}`); + if (args.priority) cmdArgs.push(`--priority=${args.priority}`); + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("add-task", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error adding task: ${error.message}`); + return createErrorResponse(`Error adding task: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/expandTask.js b/mcp-server/src/tools/expandTask.js new file mode 100644 index 00000000..b94d00d4 --- /dev/null +++ b/mcp-server/src/tools/expandTask.js @@ -0,0 +1,66 @@ +/** + * tools/expandTask.js + * Tool to break down a task into detailed subtasks + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the expandTask tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerExpandTaskTool(server) { + server.addTool({ + name: "expandTask", + description: "Break down a task into detailed subtasks", + parameters: z.object({ + id: z.union([z.string(), z.number()]).describe("Task ID to expand"), + num: z.number().optional().describe("Number of subtasks to generate"), + research: z + .boolean() + .optional() + .describe( + "Enable Perplexity AI for research-backed subtask generation" + ), + prompt: z + .string() + .optional() + .describe("Additional context to guide subtask generation"), + force: z + .boolean() + .optional() + .describe( + "Force regeneration of subtasks for tasks that already have them" + ), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Expanding task ${args.id}`); + + const cmdArgs = [`--id=${args.id}`]; + if (args.num) cmdArgs.push(`--num=${args.num}`); + if (args.research) cmdArgs.push("--research"); + if (args.prompt) cmdArgs.push(`--prompt="${args.prompt}"`); + if (args.force) cmdArgs.push("--force"); + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("expand", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error expanding task: ${error.message}`); + return createErrorResponse(`Error expanding task: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/index.js b/mcp-server/src/tools/index.js new file mode 100644 index 00000000..97d47438 --- /dev/null +++ b/mcp-server/src/tools/index.js @@ -0,0 +1,29 @@ +/** + * tools/index.js + * Export all Task Master CLI tools for MCP server + */ + +import logger from "../logger.js"; +import { registerListTasksTool } from "./listTasks.js"; +import { registerShowTaskTool } from "./showTask.js"; +import { registerSetTaskStatusTool } from "./setTaskStatus.js"; +import { registerExpandTaskTool } from "./expandTask.js"; +import { registerNextTaskTool } from "./nextTask.js"; +import { registerAddTaskTool } from "./addTask.js"; + +/** + * Register all Task Master tools with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerTaskMasterTools(server) { + registerListTasksTool(server); + registerShowTaskTool(server); + registerSetTaskStatusTool(server); + registerExpandTaskTool(server); + registerNextTaskTool(server); + registerAddTaskTool(server); +} + +export default { + registerTaskMasterTools, +}; diff --git a/mcp-server/src/tools/listTasks.js b/mcp-server/src/tools/listTasks.js new file mode 100644 index 00000000..7da65692 --- /dev/null +++ b/mcp-server/src/tools/listTasks.js @@ -0,0 +1,51 @@ +/** + * tools/listTasks.js + * Tool to list all tasks from Task Master + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the listTasks tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerListTasksTool(server) { + server.addTool({ + name: "listTasks", + description: "List all tasks from Task Master", + parameters: z.object({ + status: z.string().optional().describe("Filter tasks by status"), + withSubtasks: z + .boolean() + .optional() + .describe("Include subtasks in the response"), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Listing tasks with filters: ${JSON.stringify(args)}`); + + const cmdArgs = []; + if (args.status) cmdArgs.push(`--status=${args.status}`); + if (args.withSubtasks) cmdArgs.push("--with-subtasks"); + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("list", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error listing tasks: ${error.message}`); + return createErrorResponse(`Error listing tasks: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/nextTask.js b/mcp-server/src/tools/nextTask.js new file mode 100644 index 00000000..4003ce04 --- /dev/null +++ b/mcp-server/src/tools/nextTask.js @@ -0,0 +1,45 @@ +/** + * tools/nextTask.js + * Tool to show the next task to work on based on dependencies and status + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the nextTask tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerNextTaskTool(server) { + server.addTool({ + name: "nextTask", + description: + "Show the next task to work on based on dependencies and status", + parameters: z.object({ + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Finding next task to work on`); + + const cmdArgs = []; + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("next", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error finding next task: ${error.message}`); + return createErrorResponse(`Error finding next task: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/setTaskStatus.js b/mcp-server/src/tools/setTaskStatus.js new file mode 100644 index 00000000..5681dd7b --- /dev/null +++ b/mcp-server/src/tools/setTaskStatus.js @@ -0,0 +1,52 @@ +/** + * tools/setTaskStatus.js + * Tool to set the status of a task + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the setTaskStatus tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerSetTaskStatusTool(server) { + server.addTool({ + name: "setTaskStatus", + description: "Set the status of a task", + parameters: z.object({ + id: z + .union([z.string(), z.number()]) + .describe("Task ID (can be comma-separated for multiple tasks)"), + status: z + .string() + .describe("New status (todo, in-progress, review, done)"), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Setting status of task(s) ${args.id} to: ${args.status}`); + + const cmdArgs = [`--id=${args.id}`, `--status=${args.status}`]; + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("set-status", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error setting task status: ${error.message}`); + return createErrorResponse( + `Error setting task status: ${error.message}` + ); + } + }, + }); +} diff --git a/mcp-server/src/tools/showTask.js b/mcp-server/src/tools/showTask.js new file mode 100644 index 00000000..c44d9463 --- /dev/null +++ b/mcp-server/src/tools/showTask.js @@ -0,0 +1,45 @@ +/** + * tools/showTask.js + * Tool to show detailed information about a specific task + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the showTask tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerShowTaskTool(server) { + server.addTool({ + name: "showTask", + description: "Show detailed information about a specific task", + parameters: z.object({ + id: z.union([z.string(), z.number()]).describe("Task ID to show"), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Showing task details for ID: ${args.id}`); + + const cmdArgs = [args.id]; + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("show", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error showing task: ${error.message}`); + return createErrorResponse(`Error showing task: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/utils.js b/mcp-server/src/tools/utils.js new file mode 100644 index 00000000..24745d2e --- /dev/null +++ b/mcp-server/src/tools/utils.js @@ -0,0 +1,90 @@ +/** + * tools/utils.js + * Utility functions for Task Master CLI integration + */ + +import { spawnSync } from "child_process"; + +/** + * Execute a Task Master CLI command using child_process + * @param {string} command - The command to execute + * @param {Object} log - The logger object from FastMCP + * @param {Array} args - Arguments for the command + * @returns {Object} - The result of the command execution + */ +export function executeTaskMasterCommand(command, log, args = []) { + try { + log.info( + `Executing task-master ${command} with args: ${JSON.stringify(args)}` + ); + + // Prepare full arguments array + const fullArgs = [command, ...args]; + + // Execute the command using the global task-master CLI or local script + // Try the global CLI first + let result = spawnSync("task-master", fullArgs, { encoding: "utf8" }); + + // If global CLI is not available, try fallback to the local script + if (result.error && result.error.code === "ENOENT") { + log.info("Global task-master not found, falling back to local script"); + result = spawnSync("node", ["scripts/dev.js", ...fullArgs], { + encoding: "utf8", + }); + } + + if (result.error) { + throw new Error(`Command execution error: ${result.error.message}`); + } + + if (result.status !== 0) { + throw new Error( + `Command failed with exit code ${result.status}: ${result.stderr}` + ); + } + + return { + success: true, + stdout: result.stdout, + stderr: result.stderr, + }; + } catch (error) { + log.error(`Error executing task-master command: ${error.message}`); + return { + success: false, + error: error.message, + }; + } +} + +/** + * Creates standard content response for tools + * @param {string} text - Text content to include in response + * @returns {Object} - Content response object + */ +export function createContentResponse(text) { + return { + content: [ + { + text, + type: "text", + }, + ], + }; +} + +/** + * Creates error response for tools + * @param {string} errorMessage - Error message to include in response + * @returns {Object} - Error content response object + */ +export function createErrorResponse(errorMessage) { + return { + content: [ + { + text: errorMessage, + type: "text", + }, + ], + }; +} From 3582798293534cde82e5a436e951b93a94bdece1 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:00:00 +0000 Subject: [PATCH 5/8] fix(mcp): get everything working, cleanup, and test all tools --- .cursor/mcp.json | 8 -- README-task-master.md | 70 ++++++++++- README.md | 70 ++++++++++- mcp-server/README.md | 170 -------------------------- mcp-server/src/tools/addTask.js | 12 +- mcp-server/src/tools/expandTask.js | 16 ++- mcp-server/src/tools/listTasks.js | 16 ++- mcp-server/src/tools/nextTask.js | 14 ++- mcp-server/src/tools/setTaskStatus.js | 16 ++- mcp-server/src/tools/showTask.js | 18 ++- mcp-server/src/tools/utils.js | 32 +++-- 11 files changed, 241 insertions(+), 201 deletions(-) delete mode 100644 mcp-server/README.md diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 3b7160ae..e69de29b 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "taskMaster": { - "command": "node", - "args": ["mcp-server/server.js"] - } - } -} diff --git a/README-task-master.md b/README-task-master.md index cf46772c..26cce92b 100644 --- a/README-task-master.md +++ b/README-task-master.md @@ -1,4 +1,5 @@ # Task Master + ### by [@eyaltoledano](https://x.com/eyaltoledano) A task management system for AI-driven development with Claude, designed to work seamlessly with Cursor AI. @@ -15,9 +16,11 @@ A task management system for AI-driven development with Claude, designed to work The script can be configured through environment variables in a `.env` file at the root of the project: ### Required Configuration + - `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude ### Optional Configuration + - `MODEL`: Specify which Claude model to use (default: "claude-3-7-sonnet-20250219") - `MAX_TOKENS`: Maximum tokens for model responses (default: 4000) - `TEMPERATURE`: Temperature for model responses (default: 0.7) @@ -123,6 +126,21 @@ Claude Task Master is designed to work seamlessly with [Cursor AI](https://www.c 3. Place your PRD document in the `scripts/` directory (e.g., `scripts/prd.txt`) 4. Open Cursor's AI chat and switch to Agent mode +### Setting up MCP in Cursor + +To enable enhanced task management capabilities directly within Cursor using the Model Control Protocol (MCP): + +1. Go to Cursor settings +2. Navigate to the MCP section +3. Click on "Add New MCP Server" +4. Configure with the following details: + - Name: "Task Master" + - Type: "Command" + - Command: "npx -y task-master-mcp" +5. Save the settings + +Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience. + ### Initial Task Generation In Cursor's AI chat, instruct the agent to generate tasks from your PRD: @@ -132,11 +150,13 @@ Please use the task-master parse-prd command to generate tasks from my PRD. The ``` The agent will execute: + ```bash task-master parse-prd scripts/prd.txt ``` This will: + - Parse your PRD document - Generate a structured `tasks.json` file with tasks, dependencies, priorities, and test strategies - The agent will understand this process due to the Cursor rules @@ -150,6 +170,7 @@ Please generate individual task files from tasks.json ``` The agent will execute: + ```bash task-master generate ``` @@ -169,6 +190,7 @@ What tasks are available to work on next? ``` The agent will: + - Run `task-master list` to see all tasks - Run `task-master next` to determine the next task to work on - Analyze dependencies to determine which tasks are ready to be worked on @@ -178,12 +200,14 @@ The agent will: ### 2. Task Implementation When implementing a task, the agent will: + - Reference the task's details section for implementation specifics - Consider dependencies on previous tasks - Follow the project's coding standards - Create appropriate tests based on the task's testStrategy You can ask: + ``` Let's implement task 3. What does it involve? ``` @@ -191,6 +215,7 @@ Let's implement task 3. What does it involve? ### 3. Task Verification Before marking a task as complete, verify it according to: + - The task's specified testStrategy - Any automated tests in the codebase - Manual verification if required @@ -204,6 +229,7 @@ Task 3 is now complete. Please update its status. ``` The agent will execute: + ```bash task-master set-status --id=3 --status=done ``` @@ -211,16 +237,19 @@ task-master set-status --id=3 --status=done ### 5. Handling Implementation Drift If during implementation, you discover that: + - The current approach differs significantly from what was planned - Future tasks need to be modified due to current implementation choices - New dependencies or requirements have emerged Tell the agent: + ``` We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change. ``` The agent will execute: + ```bash task-master update --from=4 --prompt="Now we are using Express instead of Fastify." ``` @@ -236,36 +265,43 @@ Task 5 seems complex. Can you break it down into subtasks? ``` The agent will execute: + ```bash task-master expand --id=5 --num=3 ``` You can provide additional context: + ``` Please break down task 5 with a focus on security considerations. ``` The agent will execute: + ```bash task-master expand --id=5 --prompt="Focus on security aspects" ``` You can also expand all pending tasks: + ``` Please break down all pending tasks into subtasks. ``` The agent will execute: + ```bash task-master expand --all ``` For research-backed subtask generation using Perplexity AI: + ``` Please break down task 5 using research-backed generation. ``` The agent will execute: + ```bash task-master expand --id=5 --research ``` @@ -275,6 +311,7 @@ task-master expand --id=5 --research Here's a comprehensive reference of all available commands: ### Parse PRD + ```bash # Parse a PRD file and generate tasks task-master parse-prd @@ -284,6 +321,7 @@ task-master parse-prd --num-tasks=10 ``` ### List Tasks + ```bash # List all tasks task-master list @@ -299,12 +337,14 @@ task-master list --status= --with-subtasks ``` ### Show Next Task + ```bash # Show the next task to work on based on dependencies and status task-master next ``` ### Show Specific Task + ```bash # Show details of a specific task task-master show @@ -316,18 +356,21 @@ task-master show 1.2 ``` ### Update Tasks + ```bash # Update tasks from a specific ID and provide context task-master update --from= --prompt="" ``` ### Generate Task Files + ```bash # Generate individual task files from tasks.json task-master generate ``` ### Set Task Status + ```bash # Set status of a single task task-master set-status --id= --status= @@ -342,6 +385,7 @@ task-master set-status --id=1.1,1.2 --status= When marking a task as "done", all of its subtasks will automatically be marked as "done" as well. ### Expand Tasks + ```bash # Expand a specific task with subtasks task-master expand --id= --num= @@ -363,6 +407,7 @@ task-master expand --all --research ``` ### Clear Subtasks + ```bash # Clear subtasks from a specific task task-master clear-subtasks --id= @@ -375,6 +420,7 @@ task-master clear-subtasks --all ``` ### Analyze Task Complexity + ```bash # Analyze complexity of all tasks task-master analyze-complexity @@ -396,6 +442,7 @@ task-master analyze-complexity --research ``` ### View Complexity Report + ```bash # Display the task complexity analysis report task-master complexity-report @@ -405,6 +452,7 @@ task-master complexity-report --file=my-report.json ``` ### Managing Task Dependencies + ```bash # Add a dependency to a task task-master add-dependency --id= --depends-on= @@ -420,6 +468,7 @@ task-master fix-dependencies ``` ### Add a New Task + ```bash # Add a new task using AI task-master add-task --prompt="Description of the new task" @@ -436,6 +485,7 @@ task-master add-task --prompt="Description" --priority=high ### Analyzing Task Complexity The `analyze-complexity` command: + - Analyzes each task using AI to assess its complexity on a scale of 1-10 - Recommends optimal number of subtasks based on configured DEFAULT_SUBTASKS - Generates tailored prompts for expanding each task @@ -443,6 +493,7 @@ The `analyze-complexity` command: - Saves the report to scripts/task-complexity-report.json by default The generated report contains: + - Complexity analysis for each task (scored 1-10) - Recommended number of subtasks based on complexity - AI-generated expansion prompts customized for each task @@ -451,6 +502,7 @@ The generated report contains: ### Viewing Complexity Report The `complexity-report` command: + - Displays a formatted, easy-to-read version of the complexity analysis report - Shows tasks organized by complexity score (highest to lowest) - Provides complexity distribution statistics (low, medium, high) @@ -463,12 +515,14 @@ The `complexity-report` command: The `expand` command automatically checks for and uses the complexity report: When a complexity report exists: + - Tasks are automatically expanded using the recommended subtask count and prompts - When expanding all tasks, they're processed in order of complexity (highest first) - Research-backed generation is preserved from the complexity analysis - You can still override recommendations with explicit command-line options Example workflow: + ```bash # Generate the complexity analysis report with research capabilities task-master analyze-complexity --research @@ -485,6 +539,7 @@ task-master expand --all ### Finding the Next Task The `next` command: + - Identifies tasks that are pending/in-progress and have all dependencies satisfied - Prioritizes tasks by priority level, dependency count, and task ID - Displays comprehensive information about the selected task: @@ -499,6 +554,7 @@ The `next` command: ### Viewing Specific Task Details The `show` command: + - Displays comprehensive details about a specific task or subtask - Shows task status, priority, dependencies, and detailed implementation notes - For parent tasks, displays all subtasks and their status @@ -529,43 +585,51 @@ The `show` command: ## Example Cursor AI Interactions ### Starting a new project + ``` -I've just initialized a new project with Claude Task Master. I have a PRD at scripts/prd.txt. +I've just initialized a new project with Claude Task Master. I have a PRD at scripts/prd.txt. Can you help me parse it and set up the initial tasks? ``` ### Working on tasks + ``` What's the next task I should work on? Please consider dependencies and priorities. ``` ### Implementing a specific task + ``` I'd like to implement task 4. Can you help me understand what needs to be done and how to approach it? ``` ### Managing subtasks + ``` I need to regenerate the subtasks for task 3 with a different approach. Can you help me clear and regenerate them? ``` ### Handling changes + ``` We've decided to use MongoDB instead of PostgreSQL. Can you update all future tasks to reflect this change? ``` ### Completing work + ``` -I've finished implementing the authentication system described in task 2. All tests are passing. +I've finished implementing the authentication system described in task 2. All tests are passing. Please mark it as complete and tell me what I should work on next. ``` ### Analyzing complexity + ``` Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further? ``` ### Viewing complexity report + ``` Can you show me the complexity report in a more readable format? -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index 6e24c651..b0803a99 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Task Master + ### by [@eyaltoledano](https://x.com/eyaltoledano) A task management system for AI-driven development with Claude, designed to work seamlessly with Cursor AI. @@ -15,9 +16,11 @@ A task management system for AI-driven development with Claude, designed to work The script can be configured through environment variables in a `.env` file at the root of the project: ### Required Configuration + - `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude ### Optional Configuration + - `MODEL`: Specify which Claude model to use (default: "claude-3-7-sonnet-20250219") - `MAX_TOKENS`: Maximum tokens for model responses (default: 4000) - `TEMPERATURE`: Temperature for model responses (default: 0.7) @@ -123,6 +126,21 @@ Claude Task Master is designed to work seamlessly with [Cursor AI](https://www.c 3. Place your PRD document in the `scripts/` directory (e.g., `scripts/prd.txt`) 4. Open Cursor's AI chat and switch to Agent mode +### Setting up MCP in Cursor + +To enable enhanced task management capabilities directly within Cursor using the Model Control Protocol (MCP): + +1. Go to Cursor settings +2. Navigate to the MCP section +3. Click on "Add New MCP Server" +4. Configure with the following details: + - Name: "Task Master" + - Type: "Command" + - Command: "npx -y task-master-mcp" +5. Save the settings + +Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience. + ### Initial Task Generation In Cursor's AI chat, instruct the agent to generate tasks from your PRD: @@ -132,11 +150,13 @@ Please use the task-master parse-prd command to generate tasks from my PRD. The ``` The agent will execute: + ```bash task-master parse-prd scripts/prd.txt ``` This will: + - Parse your PRD document - Generate a structured `tasks.json` file with tasks, dependencies, priorities, and test strategies - The agent will understand this process due to the Cursor rules @@ -150,6 +170,7 @@ Please generate individual task files from tasks.json ``` The agent will execute: + ```bash task-master generate ``` @@ -169,6 +190,7 @@ What tasks are available to work on next? ``` The agent will: + - Run `task-master list` to see all tasks - Run `task-master next` to determine the next task to work on - Analyze dependencies to determine which tasks are ready to be worked on @@ -178,12 +200,14 @@ The agent will: ### 2. Task Implementation When implementing a task, the agent will: + - Reference the task's details section for implementation specifics - Consider dependencies on previous tasks - Follow the project's coding standards - Create appropriate tests based on the task's testStrategy You can ask: + ``` Let's implement task 3. What does it involve? ``` @@ -191,6 +215,7 @@ Let's implement task 3. What does it involve? ### 3. Task Verification Before marking a task as complete, verify it according to: + - The task's specified testStrategy - Any automated tests in the codebase - Manual verification if required @@ -204,6 +229,7 @@ Task 3 is now complete. Please update its status. ``` The agent will execute: + ```bash task-master set-status --id=3 --status=done ``` @@ -211,16 +237,19 @@ task-master set-status --id=3 --status=done ### 5. Handling Implementation Drift If during implementation, you discover that: + - The current approach differs significantly from what was planned - Future tasks need to be modified due to current implementation choices - New dependencies or requirements have emerged Tell the agent: + ``` We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change. ``` The agent will execute: + ```bash task-master update --from=4 --prompt="Now we are using Express instead of Fastify." ``` @@ -236,36 +265,43 @@ Task 5 seems complex. Can you break it down into subtasks? ``` The agent will execute: + ```bash task-master expand --id=5 --num=3 ``` You can provide additional context: + ``` Please break down task 5 with a focus on security considerations. ``` The agent will execute: + ```bash task-master expand --id=5 --prompt="Focus on security aspects" ``` You can also expand all pending tasks: + ``` Please break down all pending tasks into subtasks. ``` The agent will execute: + ```bash task-master expand --all ``` For research-backed subtask generation using Perplexity AI: + ``` Please break down task 5 using research-backed generation. ``` The agent will execute: + ```bash task-master expand --id=5 --research ``` @@ -275,6 +311,7 @@ task-master expand --id=5 --research Here's a comprehensive reference of all available commands: ### Parse PRD + ```bash # Parse a PRD file and generate tasks task-master parse-prd @@ -284,6 +321,7 @@ task-master parse-prd --num-tasks=10 ``` ### List Tasks + ```bash # List all tasks task-master list @@ -299,12 +337,14 @@ task-master list --status= --with-subtasks ``` ### Show Next Task + ```bash # Show the next task to work on based on dependencies and status task-master next ``` ### Show Specific Task + ```bash # Show details of a specific task task-master show @@ -316,18 +356,21 @@ task-master show 1.2 ``` ### Update Tasks + ```bash # Update tasks from a specific ID and provide context task-master update --from= --prompt="" ``` ### Generate Task Files + ```bash # Generate individual task files from tasks.json task-master generate ``` ### Set Task Status + ```bash # Set status of a single task task-master set-status --id= --status= @@ -342,6 +385,7 @@ task-master set-status --id=1.1,1.2 --status= When marking a task as "done", all of its subtasks will automatically be marked as "done" as well. ### Expand Tasks + ```bash # Expand a specific task with subtasks task-master expand --id= --num= @@ -363,6 +407,7 @@ task-master expand --all --research ``` ### Clear Subtasks + ```bash # Clear subtasks from a specific task task-master clear-subtasks --id= @@ -375,6 +420,7 @@ task-master clear-subtasks --all ``` ### Analyze Task Complexity + ```bash # Analyze complexity of all tasks task-master analyze-complexity @@ -396,6 +442,7 @@ task-master analyze-complexity --research ``` ### View Complexity Report + ```bash # Display the task complexity analysis report task-master complexity-report @@ -405,6 +452,7 @@ task-master complexity-report --file=my-report.json ``` ### Managing Task Dependencies + ```bash # Add a dependency to a task task-master add-dependency --id= --depends-on= @@ -420,6 +468,7 @@ task-master fix-dependencies ``` ### Add a New Task + ```bash # Add a new task using AI task-master add-task --prompt="Description of the new task" @@ -866,6 +915,7 @@ task-master add-task --prompt="Description" --priority=high ### Analyzing Task Complexity The `analyze-complexity` command: + - Analyzes each task using AI to assess its complexity on a scale of 1-10 - Recommends optimal number of subtasks based on configured DEFAULT_SUBTASKS - Generates tailored prompts for expanding each task @@ -873,6 +923,7 @@ The `analyze-complexity` command: - Saves the report to scripts/task-complexity-report.json by default The generated report contains: + - Complexity analysis for each task (scored 1-10) - Recommended number of subtasks based on complexity - AI-generated expansion prompts customized for each task @@ -881,6 +932,7 @@ The generated report contains: ### Viewing Complexity Report The `complexity-report` command: + - Displays a formatted, easy-to-read version of the complexity analysis report - Shows tasks organized by complexity score (highest to lowest) - Provides complexity distribution statistics (low, medium, high) @@ -893,12 +945,14 @@ The `complexity-report` command: The `expand` command automatically checks for and uses the complexity report: When a complexity report exists: + - Tasks are automatically expanded using the recommended subtask count and prompts - When expanding all tasks, they're processed in order of complexity (highest first) - Research-backed generation is preserved from the complexity analysis - You can still override recommendations with explicit command-line options Example workflow: + ```bash # Generate the complexity analysis report with research capabilities task-master analyze-complexity --research @@ -915,6 +969,7 @@ task-master expand --all ### Finding the Next Task The `next` command: + - Identifies tasks that are pending/in-progress and have all dependencies satisfied - Prioritizes tasks by priority level, dependency count, and task ID - Displays comprehensive information about the selected task: @@ -929,6 +984,7 @@ The `next` command: ### Viewing Specific Task Details The `show` command: + - Displays comprehensive details about a specific task or subtask - Shows task status, priority, dependencies, and detailed implementation notes - For parent tasks, displays all subtasks and their status @@ -959,43 +1015,51 @@ The `show` command: ## Example Cursor AI Interactions ### Starting a new project + ``` -I've just initialized a new project with Claude Task Master. I have a PRD at scripts/prd.txt. +I've just initialized a new project with Claude Task Master. I have a PRD at scripts/prd.txt. Can you help me parse it and set up the initial tasks? ``` ### Working on tasks + ``` What's the next task I should work on? Please consider dependencies and priorities. ``` ### Implementing a specific task + ``` I'd like to implement task 4. Can you help me understand what needs to be done and how to approach it? ``` ### Managing subtasks + ``` I need to regenerate the subtasks for task 3 with a different approach. Can you help me clear and regenerate them? ``` ### Handling changes + ``` We've decided to use MongoDB instead of PostgreSQL. Can you update all future tasks to reflect this change? ``` ### Completing work + ``` -I've finished implementing the authentication system described in task 2. All tests are passing. +I've finished implementing the authentication system described in task 2. All tests are passing. Please mark it as complete and tell me what I should work on next. ``` ### Analyzing complexity + ``` Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further? ``` ### Viewing complexity report + ``` Can you show me the complexity report in a more readable format? -``` \ No newline at end of file +``` diff --git a/mcp-server/README.md b/mcp-server/README.md deleted file mode 100644 index 9c8b1300..00000000 --- a/mcp-server/README.md +++ /dev/null @@ -1,170 +0,0 @@ -# Task Master MCP Server - -This module implements a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for Task Master, allowing external applications to access Task Master functionality and context through a standardized API. - -## Features - -- MCP-compliant server implementation using FastMCP -- RESTful API for context management -- Authentication and authorization for secure access -- Context storage and retrieval with metadata and tagging -- Context windowing and truncation for handling size limits -- Integration with Task Master for task management operations - -## Installation - -The MCP server is included with Task Master. Install Task Master globally to use the MCP server: - -```bash -npm install -g task-master-ai -``` - -Or use it locally: - -```bash -npm install task-master-ai -``` - -## Environment Configuration - -The MCP server can be configured using environment variables or a `.env` file: - -| Variable | Description | Default | -| -------------------- | ---------------------------------------- | ----------------------------- | -| `MCP_SERVER_PORT` | Port for the MCP server | 3000 | -| `MCP_SERVER_HOST` | Host for the MCP server | localhost | -| `MCP_CONTEXT_DIR` | Directory for context storage | ./mcp-server/contexts | -| `MCP_API_KEYS_FILE` | File for API key storage | ./mcp-server/api-keys.json | -| `MCP_JWT_SECRET` | Secret for JWT token generation | task-master-mcp-server-secret | -| `MCP_JWT_EXPIRATION` | JWT token expiration time | 24h | -| `LOG_LEVEL` | Logging level (debug, info, warn, error) | info | - -## Getting Started - -### Starting the Server - -Start the MCP server as a standalone process: - -```bash -npx task-master-mcp-server -``` - -Or start it programmatically: - -```javascript -import { TaskMasterMCPServer } from "task-master-ai/mcp-server"; - -const server = new TaskMasterMCPServer(); -await server.start({ port: 3000, host: "localhost" }); -``` - -### Authentication - -The MCP server uses API key authentication with JWT tokens for secure access. A default admin API key is generated on first startup and can be found in the `api-keys.json` file. - -To get a JWT token: - -```bash -curl -X POST http://localhost:3000/auth/token \ - -H "x-api-key: YOUR_API_KEY" -``` - -Use the token for subsequent requests: - -```bash -curl http://localhost:3000/mcp/tools \ - -H "Authorization: Bearer YOUR_JWT_TOKEN" -``` - -### Creating a New API Key - -Admin users can create new API keys: - -```bash -curl -X POST http://localhost:3000/auth/api-keys \ - -H "Authorization: Bearer ADMIN_JWT_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"clientId": "user1", "role": "user"}' -``` - -## Available MCP Endpoints - -The MCP server implements the following MCP-compliant endpoints: - -### Context Management - -- `GET /mcp/context` - List all contexts -- `POST /mcp/context` - Create a new context -- `GET /mcp/context/{id}` - Get a specific context -- `PUT /mcp/context/{id}` - Update a context -- `DELETE /mcp/context/{id}` - Delete a context - -### Models - -- `GET /mcp/models` - List available models -- `GET /mcp/models/{id}` - Get model details - -### Execution - -- `POST /mcp/execute` - Execute an operation with context - -## Available MCP Tools - -The MCP server provides the following tools: - -### Context Tools - -- `createContext` - Create a new context -- `getContext` - Retrieve a context by ID -- `updateContext` - Update an existing context -- `deleteContext` - Delete a context -- `listContexts` - List available contexts -- `addTags` - Add tags to a context -- `truncateContext` - Truncate a context to a maximum size - -### Task Master Tools - -- `listTasks` - List tasks from Task Master -- `getTaskDetails` - Get detailed task information -- `executeWithContext` - Execute operations using context - -## Examples - -### Creating a Context - -```javascript -// Using the MCP client -const client = new MCPClient("http://localhost:3000"); -await client.authenticate("YOUR_API_KEY"); - -const context = await client.createContext("my-context", { - title: "My Project", - tasks: ["Implement feature X", "Fix bug Y"], -}); -``` - -### Executing an Operation with Context - -```javascript -// Using the MCP client -const result = await client.execute("generateTask", "my-context", { - title: "New Task", - description: "Create a new task based on context", -}); -``` - -## Integration with Other Tools - -The Task Master MCP server can be integrated with other MCP-compatible tools and clients: - -- LLM applications that support the MCP protocol -- Task management systems that support context-aware operations -- Development environments with MCP integration - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/mcp-server/src/tools/addTask.js b/mcp-server/src/tools/addTask.js index 0622d0e8..0b12d9fc 100644 --- a/mcp-server/src/tools/addTask.js +++ b/mcp-server/src/tools/addTask.js @@ -29,6 +29,11 @@ export function registerAddTaskTool(server) { .optional() .describe("Task priority (high, medium, low)"), file: z.string().optional().describe("Path to the tasks file"), + projectRoot: z + .string() + .describe( + "Root directory of the project (default: current working directory)" + ), }), execute: async (args, { log }) => { try { @@ -40,7 +45,12 @@ export function registerAddTaskTool(server) { if (args.priority) cmdArgs.push(`--priority=${args.priority}`); if (args.file) cmdArgs.push(`--file=${args.file}`); - const result = executeTaskMasterCommand("add-task", log, cmdArgs); + const result = executeTaskMasterCommand( + "add-task", + log, + cmdArgs, + projectRoot + ); if (!result.success) { throw new Error(result.error); diff --git a/mcp-server/src/tools/expandTask.js b/mcp-server/src/tools/expandTask.js index b94d00d4..ae0b4550 100644 --- a/mcp-server/src/tools/expandTask.js +++ b/mcp-server/src/tools/expandTask.js @@ -19,7 +19,7 @@ export function registerExpandTaskTool(server) { name: "expandTask", description: "Break down a task into detailed subtasks", parameters: z.object({ - id: z.union([z.string(), z.number()]).describe("Task ID to expand"), + id: z.string().describe("Task ID to expand"), num: z.number().optional().describe("Number of subtasks to generate"), research: z .boolean() @@ -38,6 +38,11 @@ export function registerExpandTaskTool(server) { "Force regeneration of subtasks for tasks that already have them" ), file: z.string().optional().describe("Path to the tasks file"), + projectRoot: z + .string() + .describe( + "Root directory of the project (default: current working directory)" + ), }), execute: async (args, { log }) => { try { @@ -50,7 +55,14 @@ export function registerExpandTaskTool(server) { if (args.force) cmdArgs.push("--force"); if (args.file) cmdArgs.push(`--file=${args.file}`); - const result = executeTaskMasterCommand("expand", log, cmdArgs); + const projectRoot = args.projectRoot; + + const result = executeTaskMasterCommand( + "expand", + log, + cmdArgs, + projectRoot + ); if (!result.success) { throw new Error(result.error); diff --git a/mcp-server/src/tools/listTasks.js b/mcp-server/src/tools/listTasks.js index 7da65692..af6f4844 100644 --- a/mcp-server/src/tools/listTasks.js +++ b/mcp-server/src/tools/listTasks.js @@ -25,6 +25,11 @@ export function registerListTasksTool(server) { .optional() .describe("Include subtasks in the response"), file: z.string().optional().describe("Path to the tasks file"), + projectRoot: z + .string() + .describe( + "Root directory of the project (default: current working directory)" + ), }), execute: async (args, { log }) => { try { @@ -35,12 +40,21 @@ export function registerListTasksTool(server) { if (args.withSubtasks) cmdArgs.push("--with-subtasks"); if (args.file) cmdArgs.push(`--file=${args.file}`); - const result = executeTaskMasterCommand("list", log, cmdArgs); + const projectRoot = args.projectRoot; + + const result = executeTaskMasterCommand( + "list", + log, + cmdArgs, + projectRoot + ); if (!result.success) { throw new Error(result.error); } + log.info(`Listing tasks result: ${result.stdout}`, result.stdout); + return createContentResponse(result.stdout); } catch (error) { log.error(`Error listing tasks: ${error.message}`); diff --git a/mcp-server/src/tools/nextTask.js b/mcp-server/src/tools/nextTask.js index 4003ce04..729c5fec 100644 --- a/mcp-server/src/tools/nextTask.js +++ b/mcp-server/src/tools/nextTask.js @@ -21,6 +21,11 @@ export function registerNextTaskTool(server) { "Show the next task to work on based on dependencies and status", parameters: z.object({ file: z.string().optional().describe("Path to the tasks file"), + projectRoot: z + .string() + .describe( + "Root directory of the project (default: current working directory)" + ), }), execute: async (args, { log }) => { try { @@ -29,7 +34,14 @@ export function registerNextTaskTool(server) { const cmdArgs = []; if (args.file) cmdArgs.push(`--file=${args.file}`); - const result = executeTaskMasterCommand("next", log, cmdArgs); + const projectRoot = args.projectRoot; + + const result = executeTaskMasterCommand( + "next", + log, + cmdArgs, + projectRoot + ); if (!result.success) { throw new Error(result.error); diff --git a/mcp-server/src/tools/setTaskStatus.js b/mcp-server/src/tools/setTaskStatus.js index 5681dd7b..d2c0b2c1 100644 --- a/mcp-server/src/tools/setTaskStatus.js +++ b/mcp-server/src/tools/setTaskStatus.js @@ -20,12 +20,17 @@ export function registerSetTaskStatusTool(server) { description: "Set the status of a task", parameters: z.object({ id: z - .union([z.string(), z.number()]) + .string() .describe("Task ID (can be comma-separated for multiple tasks)"), status: z .string() .describe("New status (todo, in-progress, review, done)"), file: z.string().optional().describe("Path to the tasks file"), + projectRoot: z + .string() + .describe( + "Root directory of the project (default: current working directory)" + ), }), execute: async (args, { log }) => { try { @@ -34,7 +39,14 @@ export function registerSetTaskStatusTool(server) { const cmdArgs = [`--id=${args.id}`, `--status=${args.status}`]; if (args.file) cmdArgs.push(`--file=${args.file}`); - const result = executeTaskMasterCommand("set-status", log, cmdArgs); + const projectRoot = args.projectRoot; + + const result = executeTaskMasterCommand( + "set-status", + log, + cmdArgs, + projectRoot + ); if (!result.success) { throw new Error(result.error); diff --git a/mcp-server/src/tools/showTask.js b/mcp-server/src/tools/showTask.js index c44d9463..86130570 100644 --- a/mcp-server/src/tools/showTask.js +++ b/mcp-server/src/tools/showTask.js @@ -19,17 +19,29 @@ export function registerShowTaskTool(server) { name: "showTask", description: "Show detailed information about a specific task", parameters: z.object({ - id: z.union([z.string(), z.number()]).describe("Task ID to show"), + id: z.string().describe("Task ID to show"), file: z.string().optional().describe("Path to the tasks file"), + projectRoot: z + .string() + .describe( + "Root directory of the project (default: current working directory)" + ), }), execute: async (args, { log }) => { try { log.info(`Showing task details for ID: ${args.id}`); - const cmdArgs = [args.id]; + const cmdArgs = [`--id=${args.id}`]; if (args.file) cmdArgs.push(`--file=${args.file}`); - const result = executeTaskMasterCommand("show", log, cmdArgs); + const projectRoot = args.projectRoot; + + const result = executeTaskMasterCommand( + "show", + log, + cmdArgs, + projectRoot + ); if (!result.success) { throw new Error(result.error); diff --git a/mcp-server/src/tools/utils.js b/mcp-server/src/tools/utils.js index 24745d2e..872363e0 100644 --- a/mcp-server/src/tools/utils.js +++ b/mcp-server/src/tools/utils.js @@ -10,27 +10,39 @@ import { spawnSync } from "child_process"; * @param {string} command - The command to execute * @param {Object} log - The logger object from FastMCP * @param {Array} args - Arguments for the command + * @param {string} cwd - Working directory for command execution (defaults to current project root) * @returns {Object} - The result of the command execution */ -export function executeTaskMasterCommand(command, log, args = []) { +export function executeTaskMasterCommand( + command, + log, + args = [], + cwd = process.cwd() +) { try { log.info( - `Executing task-master ${command} with args: ${JSON.stringify(args)}` + `Executing task-master ${command} with args: ${JSON.stringify( + args + )} in directory: ${cwd}` ); // Prepare full arguments array const fullArgs = [command, ...args]; + // Common options for spawn + const spawnOptions = { + encoding: "utf8", + cwd: cwd, + }; + // Execute the command using the global task-master CLI or local script // Try the global CLI first - let result = spawnSync("task-master", fullArgs, { encoding: "utf8" }); + let result = spawnSync("task-master", fullArgs, spawnOptions); // If global CLI is not available, try fallback to the local script if (result.error && result.error.code === "ENOENT") { log.info("Global task-master not found, falling back to local script"); - result = spawnSync("node", ["scripts/dev.js", ...fullArgs], { - encoding: "utf8", - }); + result = spawnSync("node", ["scripts/dev.js", ...fullArgs], spawnOptions); } if (result.error) { @@ -38,8 +50,14 @@ export function executeTaskMasterCommand(command, log, args = []) { } if (result.status !== 0) { + // Improve error handling by combining stderr and stdout if stderr is empty + const errorOutput = result.stderr + ? result.stderr.trim() + : result.stdout + ? result.stdout.trim() + : "Unknown error"; throw new Error( - `Command failed with exit code ${result.status}: ${result.stderr}` + `Command failed with exit code ${result.status}: ${errorOutput}` ); } From 0663ff1beae7a81174f4160c27bd5d478c353773 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Thu, 27 Mar 2025 18:44:17 +0100 Subject: [PATCH 6/8] chore: cleanup --- .cursor/mcp.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .cursor/mcp.json diff --git a/.cursor/mcp.json b/.cursor/mcp.json deleted file mode 100644 index e69de29b..00000000 From a8e4bb040790504fd2dcb4a9188a0b7c1e001366 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:25:31 +0100 Subject: [PATCH 7/8] fix: apply @rtuin suggestions --- README-task-master.md | 2 +- README.md | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/README-task-master.md b/README-task-master.md index 26cce92b..d6485936 100644 --- a/README-task-master.md +++ b/README-task-master.md @@ -136,7 +136,7 @@ To enable enhanced task management capabilities directly within Cursor using the 4. Configure with the following details: - Name: "Task Master" - Type: "Command" - - Command: "npx -y task-master-mcp" + - Command: "npx -y --package task-master-ai task-master-mcp" 5. Save the settings Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience. diff --git a/README.md b/README.md index b0803a99..ddcdd4dd 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ To enable enhanced task management capabilities directly within Cursor using the 4. Configure with the following details: - Name: "Task Master" - Type: "Command" - - Command: "npx -y task-master-mcp" + - Command: "npx -y --package task-master-ai task-master-mcp" 5. Save the settings Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience. @@ -469,7 +469,7 @@ task-master fix-dependencies ### Add a New Task -```bash +````bash # Add a new task using AI task-master add-task --prompt="Description of the new task" @@ -517,7 +517,7 @@ npm install -g task-master-ai # OR install locally within your project npm install task-master-ai -``` +```` ### Initialize a new project @@ -611,11 +611,13 @@ Please use the task-master parse-prd command to generate tasks from my PRD. The ``` The agent will execute: + ```bash task-master parse-prd scripts/prd.txt ``` This will: + - Parse your PRD document - Generate a structured `tasks.json` file with tasks, dependencies, priorities, and test strategies - The agent will understand this process due to the Cursor rules @@ -629,6 +631,7 @@ Please generate individual task files from tasks.json ``` The agent will execute: + ```bash task-master generate ``` @@ -648,6 +651,7 @@ What tasks are available to work on next? ``` The agent will: + - Run `task-master list` to see all tasks - Run `task-master next` to determine the next task to work on - Analyze dependencies to determine which tasks are ready to be worked on @@ -657,12 +661,14 @@ The agent will: ### 2. Task Implementation When implementing a task, the agent will: + - Reference the task's details section for implementation specifics - Consider dependencies on previous tasks - Follow the project's coding standards - Create appropriate tests based on the task's testStrategy You can ask: + ``` Let's implement task 3. What does it involve? ``` @@ -670,6 +676,7 @@ Let's implement task 3. What does it involve? ### 3. Task Verification Before marking a task as complete, verify it according to: + - The task's specified testStrategy - Any automated tests in the codebase - Manual verification if required @@ -683,6 +690,7 @@ Task 3 is now complete. Please update its status. ``` The agent will execute: + ```bash task-master set-status --id=3 --status=done ``` @@ -690,16 +698,19 @@ task-master set-status --id=3 --status=done ### 5. Handling Implementation Drift If during implementation, you discover that: + - The current approach differs significantly from what was planned - Future tasks need to be modified due to current implementation choices - New dependencies or requirements have emerged Tell the agent: + ``` We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change. ``` The agent will execute: + ```bash task-master update --from=4 --prompt="Now we are using Express instead of Fastify." ``` @@ -715,36 +726,43 @@ Task 5 seems complex. Can you break it down into subtasks? ``` The agent will execute: + ```bash task-master expand --id=5 --num=3 ``` You can provide additional context: + ``` Please break down task 5 with a focus on security considerations. ``` The agent will execute: + ```bash task-master expand --id=5 --prompt="Focus on security aspects" ``` You can also expand all pending tasks: + ``` Please break down all pending tasks into subtasks. ``` The agent will execute: + ```bash task-master expand --all ``` For research-backed subtask generation using Perplexity AI: + ``` Please break down task 5 using research-backed generation. ``` The agent will execute: + ```bash task-master expand --id=5 --research ``` @@ -754,6 +772,7 @@ task-master expand --id=5 --research Here's a comprehensive reference of all available commands: ### Parse PRD + ```bash # Parse a PRD file and generate tasks task-master parse-prd @@ -763,6 +782,7 @@ task-master parse-prd --num-tasks=10 ``` ### List Tasks + ```bash # List all tasks task-master list @@ -778,12 +798,14 @@ task-master list --status= --with-subtasks ``` ### Show Next Task + ```bash # Show the next task to work on based on dependencies and status task-master next ``` ### Show Specific Task + ```bash # Show details of a specific task task-master show @@ -795,18 +817,21 @@ task-master show 1.2 ``` ### Update Tasks + ```bash # Update tasks from a specific ID and provide context task-master update --from= --prompt="" ``` ### Generate Task Files + ```bash # Generate individual task files from tasks.json task-master generate ``` ### Set Task Status + ```bash # Set status of a single task task-master set-status --id= --status= @@ -821,6 +846,7 @@ task-master set-status --id=1.1,1.2 --status= When marking a task as "done", all of its subtasks will automatically be marked as "done" as well. ### Expand Tasks + ```bash # Expand a specific task with subtasks task-master expand --id= --num= @@ -842,6 +868,7 @@ task-master expand --all --research ``` ### Clear Subtasks + ```bash # Clear subtasks from a specific task task-master clear-subtasks --id= @@ -854,6 +881,7 @@ task-master clear-subtasks --all ``` ### Analyze Task Complexity + ```bash # Analyze complexity of all tasks task-master analyze-complexity @@ -875,6 +903,7 @@ task-master analyze-complexity --research ``` ### View Complexity Report + ```bash # Display the task complexity analysis report task-master complexity-report @@ -884,6 +913,7 @@ task-master complexity-report --file=my-report.json ``` ### Managing Task Dependencies + ```bash # Add a dependency to a task task-master add-dependency --id= --depends-on= @@ -899,6 +929,7 @@ task-master fix-dependencies ``` ### Add a New Task + ```bash # Add a new task using AI task-master add-task --prompt="Description of the new task" From a8b055f05d451708b8d74e51681018f7feae4971 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Fri, 28 Mar 2025 20:38:37 +0100 Subject: [PATCH 8/8] chore: add changeset for PR --- .changeset/odd-weeks-melt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/odd-weeks-melt.md diff --git a/.changeset/odd-weeks-melt.md b/.changeset/odd-weeks-melt.md new file mode 100644 index 00000000..840d4756 --- /dev/null +++ b/.changeset/odd-weeks-melt.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": minor +--- + +Implement MCP server for all commands using tools.