diff --git a/.env.example b/.env.example index d633e2a..53b925d 100644 --- a/.env.example +++ b/.env.example @@ -23,8 +23,12 @@ AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project # API Version to use (optional, defaults to latest) # AZURE_DEVOPS_API_VERSION=6.0 -# Note: This server uses stdio for communication, not HTTP -# The following variables are not used by the server but might be used by scripts: +# Transport protocol to use (optional, defaults to 'stdio') +# Supported values: 'stdio', 'http' +TRANSPORT=stdio + +# The port to run HTTP transport over (defaults to 8000) +# HTTP_PORT=8000 # Logging Level (debug, info, warn, error) LOG_LEVEL=info diff --git a/package-lock.json b/package-lock.json index 273c1d8..ee0d765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,11 @@ "license": "MIT", "dependencies": { "@azure/identity": "^4.8.0", - "@modelcontextprotocol/sdk": "^1.6.0", + "@modelcontextprotocol/sdk": "^1.12.1", "axios": "^1.8.3", "azure-devops-node-api": "^13.0.0", "dotenv": "^16.3.1", + "express": "^5.1.0", "minimatch": "^10.0.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5" @@ -24,6 +25,7 @@ "devDependencies": { "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", + "@types/express": "^5.0.2", "@types/jest": "^29.5.0", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^8.27.0", @@ -31,8 +33,6 @@ "commitizen": "^4.3.1", "cz-conventional-changelog": "^3.3.0", "eslint": "^8.0.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.3", "jest": "^29.0.0", "lint-staged": "^15.5.0", @@ -1787,17 +1787,18 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.6.0.tgz", - "integrity": "sha512-585s8g+jzuGBomzgzDeP5l8gEyiSs+KhoAHbA2ZZ24Zgm83IZsyCLl/fmWhPHbfYsuLG8NE6SWGZA5ZBql8jSw==", - "license": "MIT", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", + "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", "dependencies": { + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", - "pkce-challenge": "^4.1.0", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" @@ -1844,19 +1845,6 @@ "node": ">= 8" } }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1957,6 +1945,25 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", @@ -1967,6 +1974,29 @@ "@types/node": "*" } }, + "node_modules/@types/express": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1977,6 +2007,12 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2015,6 +2051,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, "node_modules/@types/node": { "version": "20.17.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", @@ -2025,6 +2067,39 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2362,7 +2437,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -2688,16 +2762,15 @@ } }, "node_modules/body-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", - "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", - "license": "MIT", + "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==", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", - "iconv-lite": "^0.5.2", + "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", @@ -3476,7 +3549,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", @@ -3719,16 +3791,6 @@ "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-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -3853,8 +3915,7 @@ "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" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/ejs": { "version": "3.1.10", @@ -3903,7 +3964,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3999,8 +4059,7 @@ "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" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -4072,50 +4131,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -4252,7 +4267,6 @@ "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" } @@ -4358,46 +4372,44 @@ } }, "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", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.0.1", + "body-parser": "^2.2.0", "content-disposition": "^1.0.0", - "content-type": "~1.0.4", - "cookie": "0.7.1", + "content-type": "^1.0.5", + "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", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", - "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", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", "send": "^1.1.0", - "serve-static": "^2.1.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "^2.0.0", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -4415,23 +4427,6 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, - "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/express/node_modules/mime-db": { "version": "1.53.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", @@ -4453,27 +4448,6 @@ "node": ">= 0.6" } }, - "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/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/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -4506,16 +4480,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -4550,7 +4516,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -4673,47 +4638,21 @@ } }, "node_modules/finalhandler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.0.0.tgz", - "integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", - "license": "MIT", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "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/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/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/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-node-modules": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz", @@ -5305,12 +5244,11 @@ } }, "node_modules/iconv-lite": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", - "license": "MIT", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -5624,8 +5562,7 @@ "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" + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, "node_modules/is-stream": { "version": "2.0.1", @@ -5702,7 +5639,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": { @@ -6463,7 +6399,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -7376,7 +7311,6 @@ "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" } @@ -7430,15 +7364,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -7626,7 +7551,6 @@ "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" }, @@ -7817,7 +7741,6 @@ "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" } @@ -7846,7 +7769,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" @@ -7863,7 +7785,6 @@ "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" } @@ -7912,10 +7833,9 @@ } }, "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", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", "engines": { "node": ">=16.20.0" } @@ -8015,19 +7935,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -8093,7 +8000,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8156,7 +8062,6 @@ "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" } @@ -8176,18 +8081,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/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/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -8371,11 +8264,12 @@ } }, "node_modules/router": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", - "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" @@ -8479,19 +8373,17 @@ } }, "node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dependencies": { "debug": "^4.3.5", - "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", - "fresh": "^0.5.2", + "fresh": "^2.0.0", "http-errors": "^2.0.0", - "mime-types": "^2.1.35", + "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", @@ -8501,25 +8393,34 @@ "node": ">= 18" } }, - "node_modules/send/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", + "node_modules/send/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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/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==", + "dependencies": { + "mime-db": "^1.54.0" + }, "engines": { "node": ">= 0.6" } }, "node_modules/serve-static": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", - "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", - "send": "^1.0.0" + "send": "^1.2.0" }, "engines": { "node": ">= 18" @@ -8535,7 +8436,6 @@ "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" @@ -8548,7 +8448,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" @@ -8894,23 +8793,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9276,10 +9158,9 @@ } }, "node_modules/type-is": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", - "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", - "license": "MIT", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -9290,21 +9171,19 @@ } }, "node_modules/type-is/node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", - "license": "MIT", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "engines": { "node": ">= 0.6" } }, "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", - "license": "MIT", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dependencies": { - "mime-db": "^1.53.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -9415,7 +9294,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -9428,15 +9306,6 @@ "dev": true, "license": "MIT" }, - "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/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -9501,7 +9370,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" diff --git a/package.json b/package.json index 7f46e69..0385e75 100644 --- a/package.json +++ b/package.json @@ -85,10 +85,11 @@ "license": "MIT", "dependencies": { "@azure/identity": "^4.8.0", - "@modelcontextprotocol/sdk": "^1.6.0", + "@modelcontextprotocol/sdk": "^1.12.1", "axios": "^1.8.3", "azure-devops-node-api": "^13.0.0", "dotenv": "^16.3.1", + "express": "^5.1.0", "minimatch": "^10.0.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5" @@ -96,6 +97,7 @@ "devDependencies": { "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", + "@types/express": "^5.0.2", "@types/jest": "^29.5.0", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^8.27.0", @@ -111,4 +113,4 @@ "ts-node-dev": "^2.0.0", "typescript": "^5.8.2" } -} \ No newline at end of file +} diff --git a/src/features/organizations/__test__/test-helpers.ts b/src/features/organizations/__test__/test-helpers.ts index 2aecd5c..3a94833 100644 --- a/src/features/organizations/__test__/test-helpers.ts +++ b/src/features/organizations/__test__/test-helpers.ts @@ -1,4 +1,4 @@ -import { AzureDevOpsConfig } from '../../../shared/types'; +import { AzureDevOpsConfig, TransportProtocol } from '../../../shared/types'; import { AuthenticationMethod } from '../../../shared/auth'; /** @@ -17,6 +17,8 @@ export function getTestConfig(): AzureDevOpsConfig | null { authMethod: AuthenticationMethod.PersonalAccessToken, personalAccessToken: pat, defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT, + transport: TransportProtocol.Stdio, + http_port: 8000, }; } diff --git a/src/features/organizations/index.ts b/src/features/organizations/index.ts index e24259e..e57847f 100644 --- a/src/features/organizations/index.ts +++ b/src/features/organizations/index.ts @@ -15,7 +15,7 @@ import { RequestHandler, } from '../../shared/types/request-handler'; import { listOrganizations } from './list-organizations'; -import { AzureDevOpsConfig } from '../../shared/types'; +import { AzureDevOpsConfig, TransportProtocol } from '../../shared/types'; import { AuthenticationMethod } from '../../shared/auth'; /** @@ -49,6 +49,8 @@ export const handleOrganizationsRequest: RequestHandler = async ( : AuthenticationMethod.AzureIdentity, personalAccessToken: process.env.AZURE_DEVOPS_PAT, organizationUrl: connection.serverUrl || '', + transport: TransportProtocol.Stdio, + http_port: 8000, }; const result = await listOrganizations(config); diff --git a/src/features/organizations/list-organizations/feature.spec.unit.ts b/src/features/organizations/list-organizations/feature.spec.unit.ts index 51ee01a..2574e60 100644 --- a/src/features/organizations/list-organizations/feature.spec.unit.ts +++ b/src/features/organizations/list-organizations/feature.spec.unit.ts @@ -2,6 +2,7 @@ import { listOrganizations } from './feature'; import { AzureDevOpsAuthenticationError } from '../../../shared/errors'; import axios from 'axios'; import { AuthenticationMethod } from '../../../shared/auth'; +import { TransportProtocol } from '@/shared/types'; // Mock axios jest.mock('axios'); @@ -27,6 +28,8 @@ describe('listOrganizations unit', () => { const config = { organizationUrl: 'https://dev.azure.com/test-org', authMethod: AuthenticationMethod.PersonalAccessToken, + transport: TransportProtocol.Stdio, + http_port: 8000, // No PAT provided }; @@ -45,6 +48,8 @@ describe('listOrganizations unit', () => { organizationUrl: 'https://dev.azure.com/test-org', authMethod: AuthenticationMethod.PersonalAccessToken, personalAccessToken: 'test-pat', + transport: TransportProtocol.Stdio, + http_port: 8000, }; // Mock axios to throw an error with properties expected by axios.isAxiosError @@ -78,6 +83,8 @@ describe('listOrganizations unit', () => { organizationUrl: 'https://dev.azure.com/test-org', authMethod: AuthenticationMethod.PersonalAccessToken, personalAccessToken: 'test-pat', + transport: TransportProtocol.Stdio, + http_port: 8000, }; // Mock profile API response diff --git a/src/features/pull-requests/get-pull-request-comments/feature.spec.int.ts b/src/features/pull-requests/get-pull-request-comments/feature.spec.int.ts index b6ba312..02d824e 100644 --- a/src/features/pull-requests/get-pull-request-comments/feature.spec.int.ts +++ b/src/features/pull-requests/get-pull-request-comments/feature.spec.int.ts @@ -118,7 +118,7 @@ describe('getPullRequestComments integration', () => { expect(firstComment.id).toBeDefined(); expect(firstComment.publishedDate).toBeDefined(); expect(firstComment.author).toBeDefined(); - + // Verify new fields are present (may be undefined/null for general comments) expect(firstComment).toHaveProperty('filePath'); expect(firstComment).toHaveProperty('lineNumber'); diff --git a/src/features/pull-requests/get-pull-request-comments/feature.spec.unit.ts b/src/features/pull-requests/get-pull-request-comments/feature.spec.unit.ts index 51a2869..dee38a0 100644 --- a/src/features/pull-requests/get-pull-request-comments/feature.spec.unit.ts +++ b/src/features/pull-requests/get-pull-request-comments/feature.spec.unit.ts @@ -81,9 +81,9 @@ describe('getPullRequestComments', () => { // Verify results expect(result).toHaveLength(1); expect(result[0].comments).toHaveLength(2); - + // Verify file path and line number are added to each comment - result[0].comments?.forEach(comment => { + result[0].comments?.forEach((comment) => { expect(comment).toHaveProperty('filePath', '/src/app.ts'); expect(comment).toHaveProperty('lineNumber', 10); }); @@ -146,7 +146,7 @@ describe('getPullRequestComments', () => { // Verify results expect(result).toHaveLength(1); expect(result[0].comments).toHaveLength(1); - + // Verify file path and line number are null for comments without thread context const comment = result[0].comments![0]; expect(comment).toHaveProperty('filePath', undefined); @@ -206,7 +206,7 @@ describe('getPullRequestComments', () => { // Verify results expect(result).toHaveLength(1); expect(result[0].comments).toHaveLength(1); - + // Verify line number is taken from leftFileStart const comment = result[0].comments![0]; expect(comment).toHaveProperty('filePath', '/src/app.ts'); @@ -273,7 +273,7 @@ describe('getPullRequestComments', () => { expect(result).toHaveLength(1); expect(result[0].id).toBe(threadId); expect(result[0].comments).toHaveLength(1); - + // Verify file path and line number are added const comment = result[0].comments![0]; expect(comment).toHaveProperty('filePath', '/src/utils.ts'); @@ -353,14 +353,16 @@ describe('getPullRequestComments', () => { // Verify results (should only include first 2 threads) expect(result).toHaveLength(2); - expect(result).toEqual(mockCommentThreads.slice(0, 2).map(thread => ({ - ...thread, - comments: thread.comments?.map(comment => ({ - ...comment, - filePath: thread.threadContext?.filePath, - lineNumber: thread.threadContext?.rightFileStart?.line ?? null, + expect(result).toEqual( + mockCommentThreads.slice(0, 2).map((thread) => ({ + ...thread, + comments: thread.comments?.map((comment) => ({ + ...comment, + filePath: thread.threadContext?.filePath, + lineNumber: thread.threadContext?.rightFileStart?.line ?? null, + })), })), - }))); + ); expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1); expect(mockGitApi.getThreads).toHaveBeenCalledTimes(1); }); diff --git a/src/features/pull-requests/get-pull-request-comments/feature.ts b/src/features/pull-requests/get-pull-request-comments/feature.ts index 6718615..604d28f 100644 --- a/src/features/pull-requests/get-pull-request-comments/feature.ts +++ b/src/features/pull-requests/get-pull-request-comments/feature.ts @@ -64,19 +64,22 @@ export async function getPullRequestComments( * @param thread The original comment thread * @returns Transformed comment thread with additional fields */ -function transformThread(thread: GitPullRequestCommentThread): GitPullRequestCommentThread { +function transformThread( + thread: GitPullRequestCommentThread, +): GitPullRequestCommentThread { if (!thread.comments) { return thread; } // Get file path and line number from thread context const filePath = thread.threadContext?.filePath; - const lineNumber = thread.threadContext?.rightFileStart?.line ?? - thread.threadContext?.leftFileStart?.line ?? - null; + const lineNumber = + thread.threadContext?.rightFileStart?.line ?? + thread.threadContext?.leftFileStart?.line ?? + null; // Transform each comment to include the new fields - const transformedComments = thread.comments.map(comment => ({ + const transformedComments = thread.comments.map((comment) => ({ ...comment, filePath, lineNumber, diff --git a/src/features/repositories/get-all-repositories-tree/feature.spec.int.ts b/src/features/repositories/get-all-repositories-tree/feature.spec.int.ts index ba996f6..f0f3967 100644 --- a/src/features/repositories/get-all-repositories-tree/feature.spec.int.ts +++ b/src/features/repositories/get-all-repositories-tree/feature.spec.int.ts @@ -1,7 +1,7 @@ import { getConnection } from '../../../server'; import { shouldSkipIntegrationTest } from '../../../shared/test/test-helpers'; import { getAllRepositoriesTree } from './feature'; -import { AzureDevOpsConfig } from '../../../shared/types'; +import { AzureDevOpsConfig, TransportProtocol } from '../../../shared/types'; import { WebApi } from 'azure-devops-node-api'; import { AuthenticationMethod } from '../../../shared/auth'; @@ -26,6 +26,8 @@ describeOrSkip('getAllRepositoriesTree (Integration)', () => { authMethod: AuthenticationMethod.PersonalAccessToken, personalAccessToken: process.env.AZURE_DEVOPS_PAT || '', defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '', + transport: TransportProtocol.Stdio, + http_port: 8000, }; // Use test project - should be defined in .env file diff --git a/src/features/repositories/get-file-content/feature.spec.int.ts b/src/features/repositories/get-file-content/feature.spec.int.ts index f4ac764..239517a 100644 --- a/src/features/repositories/get-file-content/feature.spec.int.ts +++ b/src/features/repositories/get-file-content/feature.spec.int.ts @@ -2,7 +2,7 @@ import { getConnection } from '../../../server'; import { shouldSkipIntegrationTest } from '../../../shared/test/test-helpers'; import { getFileContent } from './feature'; import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { AzureDevOpsConfig } from '../../../shared/types'; +import { AzureDevOpsConfig, TransportProtocol } from '../../../shared/types'; import { WebApi } from 'azure-devops-node-api'; import { AuthenticationMethod } from '../../../shared/auth'; @@ -28,6 +28,8 @@ describeOrSkip('getFileContent (Integration)', () => { authMethod: AuthenticationMethod.PersonalAccessToken, personalAccessToken: process.env.AZURE_DEVOPS_PAT || '', defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '', + transport: TransportProtocol.Stdio, + http_port: 8000, }; // Use a test repository/project - should be defined in .env file diff --git a/src/features/search/search-work-items/feature.spec.int.ts b/src/features/search/search-work-items/feature.spec.int.ts index 943509b..b0438d6 100644 --- a/src/features/search/search-work-items/feature.spec.int.ts +++ b/src/features/search/search-work-items/feature.spec.int.ts @@ -1,7 +1,7 @@ import { WebApi } from 'azure-devops-node-api'; import { searchWorkItems } from './feature'; import { getConnection } from '../../../server'; -import { AzureDevOpsConfig } from '../../../shared/types'; +import { AzureDevOpsConfig, TransportProtocol } from '../../../shared/types'; import { AuthenticationMethod } from '../../../shared/auth'; // Skip tests if no PAT is available @@ -20,6 +20,8 @@ describeOrSkip('searchWorkItems (Integration)', () => { authMethod: AuthenticationMethod.PersonalAccessToken, personalAccessToken: process.env.AZURE_DEVOPS_PAT || '', defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '', + transport: TransportProtocol.Stdio, + http_port: 8000, }; connection = await getConnection(config); @@ -186,6 +188,8 @@ describeOrSkip('searchWorkItems (Integration)', () => { organizationUrl: process.env.AZURE_DEVOPS_ORG_URL, authMethod: AuthenticationMethod.AzureIdentity, defaultProject: process.env.TEST_PROJECT_ID, + transport: TransportProtocol.Stdio, + http_port: 8000, }; // Create the connection using the config diff --git a/src/index.ts b/src/index.ts index 14ed450..8493995 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,13 @@ import { createAzureDevOpsServer } from './server'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import dotenv from 'dotenv'; -import { AzureDevOpsConfig } from './shared/types'; +import { AzureDevOpsConfig, TransportProtocol } from './shared/types'; import { AuthenticationMethod } from './shared/auth/auth-factory'; +import express from 'express'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; +import { randomUUID } from 'node:crypto'; /** * Normalize auth method string to a valid AuthenticationMethod enum value @@ -46,6 +51,40 @@ export function normalizeAuthMethod( return AuthenticationMethod.AzureIdentity; } +/** + * Normalize transport protocol string to a valid TransportMethod enum value + * in a case-insensitive manner + */ +export function normalizeTransportProtocol( + transportProtocolStr?: string, +): TransportProtocol { + const normalizedProtocol = transportProtocolStr?.toLowerCase(); + + if (normalizedProtocol === TransportProtocol.Stdio.toLowerCase()) { + return TransportProtocol.Stdio; + } else if (normalizedProtocol === TransportProtocol.Http.toLowerCase()) { + return TransportProtocol.Http; + } + + process.stderr.write( + `WARNING: Unrecognized transport protocol '${transportProtocolStr}'. Using default (${TransportProtocol.Stdio}).\n`, + ); + return TransportProtocol.Stdio; +} + +export function normalizeHttpPort(httpPortStr?: string): number { + const normalizedPort = Number(httpPortStr); + + if (!Number.isInteger(normalizedPort)) { + process.stderr.write( + `WARNING: Unrecognized port '${httpPortStr}'. Using default (8000).\n`, + ); + return 8000; + } + + return normalizedPort; +} + // Load environment variables dotenv.config(); @@ -58,6 +97,8 @@ function getConfig(): AzureDevOpsConfig { AZURE_DEVOPS_DEFAULT_PROJECT: ${process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'NOT SET'} AZURE_DEVOPS_API_VERSION: ${process.env.AZURE_DEVOPS_API_VERSION || 'NOT SET'} NODE_ENV: ${process.env.NODE_ENV || 'NOT SET'} + TRANSPORT: ${process.env.TRANSPORT || 'NOT SET'} + HTTP_PORT: ${process.env.TRANSPORT == 'http' ? process.env.HTTP_PORT : 'NOT USED (use TRANSPORT=http)'} \n`); return { @@ -66,19 +107,92 @@ function getConfig(): AzureDevOpsConfig { personalAccessToken: process.env.AZURE_DEVOPS_PAT, defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT, apiVersion: process.env.AZURE_DEVOPS_API_VERSION, + transport: normalizeTransportProtocol(process.env.TRANSPORT), + http_port: normalizeHttpPort(process.env.HTTP_PORT), + }; +} + +// Set up the Express transport server. +async function connectHttpTransport(server: Server, port: number) { + const app = express(); + app.use(express.json()); + + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + + app.post('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + let transport: StreamableHTTPServerTransport; + + if (sessionId && transports[sessionId]) { + transport = transports[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId) => { + transports[sessionId] = transport; + }, + }); + + transport.onclose = () => { + if (transport.sessionId) { + delete transports[transport.sessionId]; + } + }; + + await server.connect(transport); + } else { + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -3200, + message: 'Bad Request: No valid session ID provided', + }, + id: null, + }); + return; + } + + await transport.handleRequest(req, res, req.body); + }); + + const handleSessionRequest = async ( + req: express.Request, + res: express.Response, + ) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); }; + + app.get('/mcp', handleSessionRequest); + app.delete('/mcp', handleSessionRequest); + + process.stderr.write(`INFO: Azure DevOPS MCP Server running on :${port}`); + app.listen(port); } +async function connectStdioTransport(server: Server) { + const transport = new StdioServerTransport(); + await server.connect(transport); + process.stderr.write('Azure DevOps MCP Server running on stdio\n'); +} async function main() { try { // Create the server with configuration - const server = createAzureDevOpsServer(getConfig()); - - // Connect to stdio transport - const transport = new StdioServerTransport(); - await server.connect(transport); + const config = getConfig(); + const server = createAzureDevOpsServer(config); - process.stderr.write('Azure DevOps MCP Server running on stdio\n'); + if (config.transport === TransportProtocol.Http) { + await connectHttpTransport(server, config.http_port); + } else { + await connectStdioTransport(server); + } } catch (error) { process.stderr.write(`Error starting server: ${error}\n`); process.exit(1); diff --git a/src/shared/test/test-helpers.ts b/src/shared/test/test-helpers.ts index c910a84..c65fb0e 100644 --- a/src/shared/test/test-helpers.ts +++ b/src/shared/test/test-helpers.ts @@ -1,6 +1,6 @@ import { WebApi } from 'azure-devops-node-api'; import { getPersonalAccessTokenHandler } from 'azure-devops-node-api'; -import { AzureDevOpsConfig } from '../types'; +import { AzureDevOpsConfig, TransportProtocol } from '../types'; import { AuthenticationMethod } from '../auth'; /** @@ -38,6 +38,8 @@ export function getTestConfig(): AzureDevOpsConfig | null { authMethod: AuthenticationMethod.PersonalAccessToken, personalAccessToken: pat, defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT, + transport: TransportProtocol.Stdio, + http_port: 8000, }; } diff --git a/src/shared/types/config.ts b/src/shared/types/config.ts index 038e779..abf181b 100644 --- a/src/shared/types/config.ts +++ b/src/shared/types/config.ts @@ -1,5 +1,19 @@ import { AuthenticationMethod } from '../auth/auth-factory'; +/** + * Transport protocols supported by the MCP server + */ +export enum TransportProtocol { + /** + * Stdio transport + */ + Stdio = 'stdio', + /** + * HTTP transport + */ + Http = 'http', +} + /** * Azure DevOps configuration type definition */ @@ -29,4 +43,16 @@ export interface AzureDevOpsConfig { * Optional API version to use (defaults to latest) */ apiVersion?: string; + /** + * Transport protocol to use (stdio, http) + * + * @default 'stdio' + */ + transport: TransportProtocol; + /** + * Port to use for HTTP MCP transport (required when using HTTP transport) + * + * @default 8000 + */ + http_port: number; }