From 6197f8e4e6a7b964e84102ae9ea12aff4d87e775 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Mon, 1 Dec 2025 21:39:16 -0300 Subject: [PATCH 1/3] change connection type deprecated sse to streamable http --- package-lock.json | 262 ++++++++---- package.json | 4 +- src/config/index.ts | 1 - src/features/issues/issues.router.ts | 172 ++++---- src/features/issues/issues.service.ts | 30 +- .../pullRequests/pullRequest.router.ts | 260 ++++++------ .../pullRequests/pullRequest.service.ts | 48 +-- .../repositories/repositories.router.ts | 307 +++++++------- .../repositories/repositories.service.ts | 70 ++-- src/main.ts | 94 ++++- src/middleware/auth.ts | 6 +- src/server.ts | 373 ++---------------- src/services/api.ts | 26 +- src/types/express.d.ts | 1 + tsconfig.json | 3 + 15 files changed, 779 insertions(+), 878 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd2a5b0..9dab470 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,27 +9,29 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.4", + "@modelcontextprotocol/sdk": "^1.23.0", "axios": "^1.9.0", "cors": "^2.8.5", "dompurify": "^3.2.6", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-rate-limit": "^8.1.0", + "helmet": "^8.1.0", "http-terminator": "^3.2.0", "jsdom": "^26.1.0", "raw-body": "^3.0.0", - "zod": "^3.24.4" + "zod": "^3.25.0" }, "devDependencies": { "@types/cors": "^2.8.19", "@types/dompurify": "^3.0.5", "@types/express": "^5.0.1", "@types/express-rate-limit": "^5.1.3", + "@types/helmet": "^0.0.48", "@types/jest": "^30.0.0", "@types/jsdom": "^21.1.7", "@types/node": "^22.14.1", "eventsource": "^4.0.0", - "express-rate-limit": "^8.1.0", "jest": "^30.0.5", "node-fetch": "^3.3.2", "supertest": "^7.1.4", @@ -1137,12 +1139,13 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.4.tgz", - "integrity": "sha512-zq24hfuAmmlNZvik0FLI58uE5sriN0WWsQzIlYnzSuKDAHFqJtBFrl/LfB1NLgJT5Y7dEBzaX4yAKqOPrcetaw==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.23.0.tgz", + "integrity": "sha512-MCGd4K9aZKvuSqdoBkdMvZNcYXCkZRYVs/Gh92mdV5IHbctX9H9uIvd4X93+9g8tBbXv08sxc/QHXTzf8y65bA==", "license": "MIT", "dependencies": { - "ajv": "^6.12.6", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -1152,11 +1155,23 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, "node_modules/@modelcontextprotocol/sdk/node_modules/eventsource": { @@ -1405,6 +1420,16 @@ "@types/send": "*" } }, + "node_modules/@types/helmet": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/helmet/-/helmet-0.0.48.tgz", + "integrity": "sha512-C7MpnvSDrunS1q2Oy1VWCY7CDWHozqSnM8P4tFeRTuzwqni+PYOjEredwcqWG+kLpYcgLsgcY3orHB54gbx2Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -1853,21 +1878,38 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1951,9 +1993,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2067,23 +2109,43 @@ "license": "MIT" }, "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==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -2563,9 +2625,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2943,18 +3005,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -2988,7 +3051,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", - "dev": true, "license": "MIT", "dependencies": { "ip-address": "10.0.1" @@ -3013,6 +3075,7 @@ "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-printf": { @@ -3031,6 +3094,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -3344,9 +3423,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -3454,6 +3533,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/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -3474,28 +3562,23 @@ "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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-proxy-agent": { @@ -3625,7 +3708,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -4422,9 +4504,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -4495,9 +4577,9 @@ "license": "MIT" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/json5": { @@ -5283,18 +5365,34 @@ } }, "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==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/react-is": { @@ -5314,6 +5412,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -6255,15 +6362,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.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", @@ -6650,12 +6748,12 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", "license": "ISC", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } } } diff --git a/package.json b/package.json index 434de6d..d129509 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ }, "homepage": "https://github.com/JesusMaster/github-see-mcp-server#readme", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.4", + "@modelcontextprotocol/sdk": "^1.23.0", "axios": "^1.9.0", "cors": "^2.8.5", "dompurify": "^3.2.6", @@ -49,7 +49,7 @@ "http-terminator": "^3.2.0", "jsdom": "^26.1.0", "raw-body": "^3.0.0", - "zod": "^3.24.4" + "zod": "^3.25.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/src/config/index.ts b/src/config/index.ts index 105eb78..4db61db 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -10,7 +10,6 @@ function findAndLoadToken(): string | undefined { logger.info('GitHub token loaded successfully.'); return token; } - logger.warn('WARNING: No GitHub token found. API requests may be rate limited or fail.'); return undefined; } diff --git a/src/features/issues/issues.router.ts b/src/features/issues/issues.router.ts index 60b3c5f..f8d79b4 100644 --- a/src/features/issues/issues.router.ts +++ b/src/features/issues/issues.router.ts @@ -3,100 +3,112 @@ import { z } from 'zod'; import Issues from "#features/issues/issues.service"; export function registerIssueTools(server: McpServer, issuesInstance: Issues) { - server.tool( + + + server.registerTool( 'get_issue', - 'Gets the contents of an issue within a repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - issueNumber: z.number().describe('Issue number (number, required)'), + description: 'Gets the contents of an issue within a repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + issueNumber: z.number().describe('Issue number (number, required)'), + } }, - async (args) => { + async (args, req: any) => { try { - let info = await issuesInstance.getIssues(args); + let info = await issuesInstance.getIssues(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ); - - server.tool( + + server.registerTool( 'get_issue_comments', - 'Get comments for a GitHub issue', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - issueNumber: z.number().describe('Issue number (number, required)'), + description: 'Get comments for a GitHub issue', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + issueNumber: z.number().describe('Issue number (number, required)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await issuesInstance.getComments(args); + let info = await issuesInstance.getComments(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ); - - server.tool( + + server.registerTool( 'create_issue', - 'Create a new issue in a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - title: z.string().describe('Issue title (string, required)'), - body: z.string().optional().describe('Issue body (string, optional)'), - assignees: z.array(z.string()).optional().describe('Usernames to assign to this issue (string[], optional)'), - labels: z.array(z.string()).optional().describe('Labels to apply to this issue (string[], optional)'), - milestone: z.number().optional().describe('ID of the milestone to associate this issue with (number, optional)'), + description: 'Create a new issue in a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + title: z.string().describe('Issue title (string, required)'), + body: z.string().optional().describe('Issue body (string, optional)'), + assignees: z.array(z.string()).optional().describe('Usernames to assign to this issue (string[], optional)'), + labels: z.array(z.string()).optional().describe('Labels to apply to this issue (string[], optional)'), + milestone: z.number().optional().describe('ID of the milestone to associate this issue with (number, optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await issuesInstance.createIssue(args); + let info = await issuesInstance.createIssue(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ); - - server.tool( + + server.registerTool( 'add_issue_comment', - 'Add a comment to an issue', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - issueNumber: z.number().describe('Issue number (number, required)'), - comment: z.string().describe('Comment text (string, required)'), + description: 'Add a comment to an issue', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + issueNumber: z.number().describe('Issue number (number, required)'), + comment: z.string().describe('Comment text (string, required)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await issuesInstance.addComment(args); + let info = await issuesInstance.addComment(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ); - - server.tool( + + server.registerTool( 'list_issues', - 'List and filter repository issues', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - state: z.enum(['open', 'closed','all']).optional().describe("Filter by state ('open', 'closed', 'all') (string, optional)"), - labels: z.array(z.string()).optional().describe('Labels to filter by (string[], optional)'), - sort: z.enum(['created', 'updated', 'comments']).optional().describe("Sort by ('created', 'updated', 'comments') (string, optional)"), - direction: z.enum(['asc', 'desc']).optional().describe("Sort direction ('asc', 'desc') (string, optional)"), - since: z.string().optional().describe('Filter by date (ISO 8601 timestamp) (string, optional)'), - page: z.number().optional().describe('Page number (number, optional)'), - per_page: z.number().optional().describe('Results per page (number, optional)'), + description: 'List and filter repository issues', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + state: z.enum(['open', 'closed', 'all']).optional().describe("Filter by state ('open', 'closed', 'all') (string, optional)"), + labels: z.array(z.string()).optional().describe('Labels to filter by (string[], optional)'), + sort: z.enum(['created', 'updated', 'comments']).optional().describe("Sort by ('created', 'updated', 'comments') (string, optional)"), + direction: z.enum(['asc', 'desc']).optional().describe("Sort direction ('asc', 'desc') (string, optional)"), + since: z.string().optional().describe('Filter by date (ISO 8601 timestamp) (string, optional)'), + page: z.number().optional().describe('Page number (number, optional)'), + per_page: z.number().optional().describe('Results per page (number, optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await issuesInstance.listIssues(args); + let info = await issuesInstance.listIssues(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { @@ -104,51 +116,55 @@ export function registerIssueTools(server: McpServer, issuesInstance: Issues) { } } ); - - server.tool( + + server.registerTool( 'update_issue', - 'Update an issue in a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - issueNumber: z.number().describe('Issue number (number, required)'), - title: z.string().optional().describe('New issue title (string, optional)'), - body: z.string().optional().describe('New issue body (string, optional)'), - assignees: z.array(z.string()).optional().describe('Usernames to assign to this issue (string[], optional)'), - state: z.enum(['open', 'closed']).optional().describe("New issue state ('open', 'closed') (string, optional)"), - milestone: z.number().optional().describe('New milestone ID (number, optional)'), - labels: z.array(z.string()).optional().describe('New labels (string[], optional)'), + description: 'Update an issue in a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + issueNumber: z.number().describe('Issue number (number, required)'), + title: z.string().optional().describe('New issue title (string, optional)'), + body: z.string().optional().describe('New issue body (string, optional)'), + assignees: z.array(z.string()).optional().describe('Usernames to assign to this issue (string[], optional)'), + state: z.enum(['open', 'closed']).optional().describe("New issue state ('open', 'closed') (string, optional)"), + milestone: z.number().optional().describe('New milestone ID (number, optional)'), + labels: z.array(z.string()).optional().describe('New labels (string[], optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await issuesInstance.updateIssue(args); + let info = await issuesInstance.updateIssue(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ); - - server.tool( + + server.registerTool( 'search_issues', - 'Search for issues and pull requests', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - q: z.string().describe('Search query (string, required)'), - sort: z.enum(['created', 'updated', 'comments']).optional().describe("Sort by ('created', 'updated', 'comments') (string, optional)"), - order: z.enum(['asc', 'desc']).optional().describe("Sort order ('asc', 'desc') (string, optional)"), - page: z.number().optional().describe('Page number (number, optional)'), - per_page: z.number().optional().describe('Results per page (number, optional)'), - fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + description: 'Search for issues and pull requests', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + q: z.string().describe('Search query (string, required)'), + sort: z.enum(['created', 'updated', 'comments']).optional().describe("Sort by ('created', 'updated', 'comments') (string, optional)"), + order: z.enum(['asc', 'desc']).optional().describe("Sort order ('asc', 'desc') (string, optional)"), + page: z.number().optional().describe('Page number (number, optional)'), + per_page: z.number().optional().describe('Results per page (number, optional)'), + fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await issuesInstance.searchIssues(args); + let info = await issuesInstance.searchIssues(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ); -} \ No newline at end of file +} diff --git a/src/features/issues/issues.service.ts b/src/features/issues/issues.service.ts index a00f3bc..28a970e 100644 --- a/src/features/issues/issues.service.ts +++ b/src/features/issues/issues.service.ts @@ -13,32 +13,32 @@ export interface SearchIssuesOptions { owner: string; repo: string; q: string; s class Issues extends GitHubClient { - async getIssues(options: GetIssuesOptions) { + async getIssues(options: GetIssuesOptions, token: string) { const { owner, repo, issueNumber } = options; - return this.get(`repos/${owner}/${repo}/issues/${issueNumber}`); + return this.get(`repos/${owner}/${repo}/issues/${issueNumber}`, {}, token); } - async getComments(options: GetCommentsOptions) { + async getComments(options: GetCommentsOptions, token: string) { const { owner, repo, issueNumber } = options; - return this.get(`repos/${owner}/${repo}/issues/${issueNumber}/comments`); + return this.get(`repos/${owner}/${repo}/issues/${issueNumber}/comments`, {}, token); } - async createIssue(options: CreateIssueOptions) { + async createIssue(options: CreateIssueOptions, token: string) { const { owner, repo, ...payload } = options; if (payload.title) payload.title = sanitize(payload.title); if (payload.body) payload.body = sanitize(payload.body); - return this.post(`repos/${owner}/${repo}/issues`, payload); + return this.post(`repos/${owner}/${repo}/issues`, payload, token); } - async addComment(options: AddCommentOptions) { + async addComment(options: AddCommentOptions, token: string) { const { owner, repo, issueNumber, comment } = options; const payload = { body: sanitize(comment) }; - return this.post(`repos/${owner}/${repo}/issues/${issueNumber}/comments`, payload); + return this.post(`repos/${owner}/${repo}/issues/${issueNumber}/comments`, payload, token); } - async listIssues(options: ListIssuesOptions) { + async listIssues(options: ListIssuesOptions, token: string) { const { owner, repo, fields, ...params } = options; - const results = await this.get(`repos/${owner}/${repo}/issues`, { per_page: 5, ...params }); + const results = await this.get(`repos/${owner}/${repo}/issues`, { per_page: 5, ...params }, token); if (fields?.length) { return (results as any[]).map((item: any) => { @@ -54,17 +54,17 @@ class Issues extends GitHubClient { return results; } - async updateIssue(options: UpdateIssueOptions) { + async updateIssue(options: UpdateIssueOptions, token: string) { const { owner, repo, issueNumber, ...payload } = options; if (payload.title) payload.title = sanitize(payload.title); if (payload.body) payload.body = sanitize(payload.body); - return this.patch(`repos/${owner}/${repo}/issues/${issueNumber}`, payload); + return this.patch(`repos/${owner}/${repo}/issues/${issueNumber}`, payload, token); } - async searchIssues(options: SearchIssuesOptions) { + async searchIssues(options: SearchIssuesOptions, token: string) { const { owner, repo, fields, q, ...params } = options; const payload = { q: sanitize(q), per_page: 5, ...params }; - const results: any = await this.get('search/issues', payload); + const results: any = await this.get('search/issues', payload, token); if (fields?.length && results.items) { results.items = results.items.map((item: any) => { @@ -81,4 +81,4 @@ class Issues extends GitHubClient { } } -export default Issues; \ No newline at end of file +export default Issues; diff --git a/src/features/pullRequests/pullRequest.router.ts b/src/features/pullRequests/pullRequest.router.ts index 9cb5a74..b84362f 100644 --- a/src/features/pullRequests/pullRequest.router.ts +++ b/src/features/pullRequests/pullRequest.router.ts @@ -4,17 +4,19 @@ import PullRequest from "#features/pullRequests/pullRequest.service"; export function registerPullRequestTools(server: McpServer, pullRequestInstance: PullRequest) { - server.tool( + server.registerTool( 'get_pull_request', - 'Get details of a specific pull request', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), + description: 'Get details of a specific pull request', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.getPullRequest(args); + let info = await pullRequestInstance.getPullRequest(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -22,22 +24,24 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'list_pull_requests', - 'List and filter repository pull requests', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - state: z.string().optional().describe('State of the pull request (open, closed, all)'), - sort: z.string().optional().describe('Sort field (created, updated, popularity, long-running)'), - direction: z.enum(['asc', 'desc']).optional().describe('Sort direction (asc or desc)'), - perPage: z.number().optional().describe('Number of results per page'), - page: z.number().optional().describe('Page number to retrieve'), - fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + description: 'List and filter repository pull requests', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + state: z.string().optional().describe('State of the pull request (open, closed, all)'), + sort: z.string().optional().describe('Sort field (created, updated, popularity, long-running)'), + direction: z.enum(['asc', 'desc']).optional().describe('Sort direction (asc or desc)'), + perPage: z.number().optional().describe('Number of results per page'), + page: z.number().optional().describe('Page number to retrieve'), + fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.getListPullRequests(args); + let info = await pullRequestInstance.getListPullRequests(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -45,20 +49,22 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'merge_pull_request', - 'Merge a pull request', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), - commitMessage: z.string().optional().describe('Commit message (string, optional)'), - commit_title: z.string().optional().describe('Commit title (string, optional)'), - merge_method: z.enum(['merge', 'squash', 'rebase']).optional().describe("Merge method ('merge', 'squash', 'rebase') (string, optional)"), + description: 'Merge a pull request', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + commitMessage: z.string().optional().describe('Commit message (string, optional)'), + commit_title: z.string().optional().describe('Commit title (string, optional)'), + merge_method: z.enum(['merge', 'squash', 'rebase']).optional().describe("Merge method ('merge', 'squash', 'rebase') (string, optional)"), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.mergePullRequest(args); + let info = await pullRequestInstance.mergePullRequest(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -66,17 +72,19 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'get_pull_request_files', - 'Get the list of files changed in a pull request', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), + description: 'Get the list of files changed in a pull request', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.getPullRequestFiles(args); + let info = await pullRequestInstance.getPullRequestFiles(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -84,17 +92,19 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'get_pull_request_status', - 'Get the combined status of all status checks for a pull request', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), + description: 'Get the combined status of all status checks for a pull request', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.getPullRequestStatus(args); + let info = await pullRequestInstance.getPullRequestStatus(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -102,36 +112,40 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'update_pull_request_branch', - 'Update a pull request branch with the latest changes from the base branch', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), - expectedHeadSha: z.string().optional().describe("The expected SHA of the pull request's HEAD ref (string, optional)"), + description: 'Update a pull request branch with the latest changes from the base branch', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + expectedHeadSha: z.string().optional().describe("The expected SHA of the pull request's HEAD ref (string, optional)"), + }, }, - async (args) =>{ + async (args, req: any) => { try { - let info = await pullRequestInstance.updatePullRequestBranch(args); + let info = await pullRequestInstance.updatePullRequestBranch(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; - }catch (error: any) { + } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ); - server.tool( + server.registerTool( 'get_pull_request_comments', - 'Get the review comments on a pull request', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), + description: 'Get the review comments on a pull request', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.getPullRequestComments(args); + let info = await pullRequestInstance.getPullRequestComments(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -139,17 +153,19 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'get_pull_request_reviews', - 'Get the reviews on a pull request', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), + description: 'Get the reviews on a pull request', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.getPullRequestReviews(args); + let info = await pullRequestInstance.getPullRequestReviews(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -157,21 +173,23 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'create_pull_request_review', - 'Create a review on a pull request review', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), - body: z.string().optional().describe('Review body (string, optional)'), - event: z.enum(['approve', 'request_changes', 'comment']).optional().describe("Review event ('approve', 'request_changes', 'comment') (string, optional)"), - commitId: z.string().optional().describe('SHA of the commit to review (string, optional)'), - comments: z.array(z.string()).optional().describe('Array of comment objects (array, optional)'), + description: 'Create a review on a pull request review', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + body: z.string().optional().describe('Review body (string, optional)'), + event: z.enum(['approve', 'request_changes', 'comment']).optional().describe("Review event ('approve', 'request_changes', 'comment') (string, optional)"), + commitId: z.string().optional().describe('SHA of the commit to review (string, optional)'), + comments: z.array(z.string()).optional().describe('Array of comment objects (array, optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.createPullRequestReview(args); + let info = await pullRequestInstance.createPullRequestReview(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -179,22 +197,24 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'create_pull_request', - 'Create a new pull request', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - title: z.string().describe('Pull request title (string, required)'), - head: z.string().describe('The name of the branch where your changes are implemented (string, required)'), - base: z.string().describe('The name of the branch you want your changes pulled into (string, required)'), - body: z.string().optional().describe('Pull request body (string, optional)'), - draft: z.boolean().optional().describe('Indicates if this is a draft pull request (boolean, optional)'), - maintainer_can_modify: z.boolean().optional().describe('Indicates if the pull request can be modified by the maintainer (boolean, optional)'), + description: 'Create a new pull request', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + title: z.string().describe('Pull request title (string, required)'), + head: z.string().describe('The name of the branch where your changes are implemented (string, required)'), + base: z.string().describe('The name of the branch you want your changes pulled into (string, required)'), + body: z.string().optional().describe('Pull request body (string, optional)'), + draft: z.boolean().optional().describe('Indicates if this is a draft pull request (boolean, optional)'), + maintainer_can_modify: z.boolean().optional().describe('Indicates if the pull request can be modified by the maintainer (boolean, optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.createPullRequest(args); + let info = await pullRequestInstance.createPullRequest(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -202,26 +222,28 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'add_pull_request_review_comment', - 'Add a review comment to a pull request or reply to an existing comment', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), - body: z.string().describe('Comment body (string, required)'), - path: z.string().describe('Path to the file (string, required)'), - commit_id: z.string().optional().describe('SHA of the commit to comment on (string, optional)'), - in_reply_to: z.number().optional().describe('ID of the comment to reply to (number, optional)'), - subject_type: z.string().optional().describe('Type of the subject (string, optional)'), - line: z.number().optional().describe('Line number in the file (number, optional)'), - side: z.string().optional().describe('Side of the file (string, optional)'), - start_line: z.number().optional().describe('Starting line number (number, optional)'), - start_side: z.string().optional().describe('Starting side of the file (string, optional)'), + description: 'Add a review comment to a pull request or reply to an existing comment', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + body: z.string().describe('Comment body (string, required)'), + path: z.string().describe('Path to the file (string, required)'), + commit_id: z.string().optional().describe('SHA of the commit to comment on (string, optional)'), + in_reply_to: z.number().optional().describe('ID of the comment to reply to (number, optional)'), + subject_type: z.string().optional().describe('Type of the subject (string, optional)'), + line: z.number().optional().describe('Line number in the file (number, optional)'), + side: z.string().optional().describe('Side of the file (string, optional)'), + start_line: z.number().optional().describe('Starting line number (number, optional)'), + start_side: z.string().optional().describe('Starting side of the file (string, optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.addPullRequestReviewComment(args); + let info = await pullRequestInstance.addPullRequestReviewComment(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -229,26 +251,28 @@ export function registerPullRequestTools(server: McpServer, pullRequestInstance: } ); - server.tool( + server.registerTool( 'update_pull_request', - 'Update an existing pull request in a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - pullNumber: z.number().describe('Pull request number (number, required)'), - title: z.string().optional().describe('Pull request title (string, optional)'), - body: z.string().optional().describe('Pull request body (string, optional)'), - state: z.enum(['open', 'closed']).optional().describe("State of the pull request ('open', 'closed') (string, optional)"), - base: z.string().optional().describe('New base branch name (string, optional)'), - maintainer_can_modify: z.boolean().optional().describe('Indicates if the pull request can be modified by the maintainer (boolean, optional)'), + description: 'Update an existing pull request in a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + pullNumber: z.number().describe('Pull request number (number, required)'), + title: z.string().optional().describe('Pull request title (string, optional)'), + body: z.string().optional().describe('Pull request body (string, optional)'), + state: z.enum(['open', 'closed']).optional().describe("State of the pull request ('open', 'closed') (string, optional)"), + base: z.string().optional().describe('New base branch name (string, optional)'), + maintainer_can_modify: z.boolean().optional().describe('Indicates if the pull request can be modified by the maintainer (boolean, optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await pullRequestInstance.updatePullRequest(args); + let info = await pullRequestInstance.updatePullRequest(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } - ); + ); } diff --git a/src/features/pullRequests/pullRequest.service.ts b/src/features/pullRequests/pullRequest.service.ts index 3f9ac90..fddacc1 100644 --- a/src/features/pullRequests/pullRequest.service.ts +++ b/src/features/pullRequests/pullRequest.service.ts @@ -18,15 +18,15 @@ export interface UpdatePullRequestOptions { owner: string; repo: string; pullNum class PullRequest extends GitHubClient { - async getPullRequest(options: GetPullRequestOptions) { + async getPullRequest(options: GetPullRequestOptions, token: string) { const { owner, repo, pullNumber } = options; - return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}`); + return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}`, {}, token); } - async getListPullRequests(options: ListPullRequestsOptions) { + async getListPullRequests(options: ListPullRequestsOptions, token: string) { const { owner, repo, fields, ...params } = options; const payload = { state: "open", per_page: 5, page: 1, ...params }; - const results = await this.get(`repos/${owner}/${repo}/pulls`, payload); + const results = await this.get(`repos/${owner}/${repo}/pulls`, payload, token); if (fields?.length) { return (results as any[]).map((item: any) => { @@ -40,64 +40,64 @@ class PullRequest extends GitHubClient { return results; } - async mergePullRequest(options: MergePullRequestOptions) { + async mergePullRequest(options: MergePullRequestOptions, token: string) { const { owner, repo, pullNumber, ...payload } = options; if (payload.commit_title) payload.commit_title = sanitize(payload.commit_title); if (payload.commitMessage) payload.commitMessage = sanitize(payload.commitMessage); - return this.put(`repos/${owner}/${repo}/pulls/${pullNumber}/merge`, payload); + return this.put(`repos/${owner}/${repo}/pulls/${pullNumber}/merge`, payload, token); } - async getPullRequestFiles(options: GetPullRequestFilesOptions) { + async getPullRequestFiles(options: GetPullRequestFilesOptions, token: string) { const { owner, repo, pullNumber } = options; - return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/files`); + return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/files`, {}, token); } - async getPullRequestStatus(options: GetPullRequestStatusOptions) { + async getPullRequestStatus(options: GetPullRequestStatusOptions, token: string) { const { owner, repo, pullNumber } = options; - return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/merge`); + return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/merge`, {}, token); } - async updatePullRequestBranch(options: UpdatePullRequestBranchOptions) { + async updatePullRequestBranch(options: UpdatePullRequestBranchOptions, token: string) { const { owner, repo, pullNumber, ...params } = options; - return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/update-branch`, params); + return this.put(`repos/${owner}/${repo}/pulls/${pullNumber}/update-branch`, params, token); } - async getPullRequestComments(options: GetPullRequestCommentsOptions) { + async getPullRequestComments(options: GetPullRequestCommentsOptions, token: string) { const { owner, repo, pullNumber } = options; - return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/comments`); + return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/comments`, {}, token); } - async getPullRequestReviews(options: GetPullRequestReviewsOptions) { + async getPullRequestReviews(options: GetPullRequestReviewsOptions, token: string) { const { owner, repo, pullNumber } = options; - return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/reviews`); + return this.get(`repos/${owner}/${repo}/pulls/${pullNumber}/reviews`, {}, token); } - async createPullRequestReview(options: CreatePullRequestReviewOptions) { + async createPullRequestReview(options: CreatePullRequestReviewOptions, token: string) { const { owner, repo, pullNumber, commitId, ...rest } = options; const payload: any = { ...rest }; if (payload.body) payload.body = sanitize(payload.body); if (commitId) payload.commit_id = commitId; - return this.post(`repos/${owner}/${repo}/pulls/${pullNumber}/reviews`, payload); + return this.post(`repos/${owner}/${repo}/pulls/${pullNumber}/reviews`, payload, token); } - async createPullRequest(options: CreatePullRequestOptions) { + async createPullRequest(options: CreatePullRequestOptions, token: string) { const { owner, repo, ...payload } = options; if (payload.title) payload.title = sanitize(payload.title); if (payload.body) payload.body = sanitize(payload.body); - return this.post(`repos/${owner}/${repo}/pulls`, payload); + return this.post(`repos/${owner}/${repo}/pulls`, payload, token); } - async addPullRequestReviewComment(options: AddPullRequestReviewCommentOptions) { + async addPullRequestReviewComment(options: AddPullRequestReviewCommentOptions, token: string) { const { owner, repo, pullNumber, ...payload } = options; if (payload.body) payload.body = sanitize(payload.body); - return this.post(`repos/${owner}/${repo}/pulls/${pullNumber}/comments`, payload); + return this.post(`repos/${owner}/${repo}/pulls/${pullNumber}/comments`, payload, token); } - async updatePullRequest(options: UpdatePullRequestOptions) { + async updatePullRequest(options: UpdatePullRequestOptions, token: string) { const { owner, repo, pullNumber, ...payload } = options; if (payload.title) payload.title = sanitize(payload.title); if (payload.body) payload.body = sanitize(payload.body); - return this.patch(`repos/${owner}/${repo}/pulls/${pullNumber}`, payload); + return this.patch(`repos/${owner}/${repo}/pulls/${pullNumber}`, payload, token); } } diff --git a/src/features/repositories/repositories.router.ts b/src/features/repositories/repositories.router.ts index 5e4a1e5..f4b5adb 100644 --- a/src/features/repositories/repositories.router.ts +++ b/src/features/repositories/repositories.router.ts @@ -4,21 +4,23 @@ import Repositories from "#features/repositories/repositories.service"; export function registerRepositoriesTools(server: McpServer, repositoriesInstance: Repositories) { - server.tool( + server.registerTool( 'create_file', - 'Create a single file in a repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - path: z.string().describe('File path (string, required)'), - message: z.string().describe('Commit message (string, required)'), - content: z.string().describe('File content (string, required)'), - branch: z.string().optional().describe('Branch name (string, optional)'), - sha: z.string().optional().describe('SHA of the file to update (string, optional)'), + description: 'Create a single file in a repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + path: z.string().describe('File path (string, required)'), + message: z.string().describe('Commit message (string, required)'), + content: z.string().describe('File content (string, required)'), + branch: z.string().optional().describe('Branch name (string, optional)'), + sha: z.string().optional().describe('SHA of the file to update (string, optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await repositoriesInstance.createFileContents(args); + let info = await repositoriesInstance.createFileContents(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -26,21 +28,23 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'update_file', - 'Update a single file in a repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - path: z.string().describe('File path (string, required)'), - message: z.string().describe('Commit message (string, required)'), - content: z.string().describe('File content (string, required)'), - branch: z.string().optional().describe('Branch name (string, optional)'), - sha: z.string().describe('SHA of the file to update (string, required)'), + description: 'Update a single file in a repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + path: z.string().describe('File path (string, required)'), + message: z.string().describe('Commit message (string, required)'), + content: z.string().describe('File content (string, required)'), + branch: z.string().optional().describe('Branch name (string, optional)'), + sha: z.string().describe('SHA of the file to update (string, required)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await repositoriesInstance.updateFileContents(args); + let info = await repositoriesInstance.updateFileContents(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -48,20 +52,22 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'list_branches', - 'List branches in a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - page: z.number().optional().describe('Page number (number, optional)'), - per_page: z.number().optional().describe('Number of items per page (number, optional)'), - fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), - fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + description: 'List branches in a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + page: z.number().optional().describe('Page number (number, optional)'), + per_page: z.number().optional().describe('Number of items per page (number, optional)'), + fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), + fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + }, }, - async (args) => { + async (args, req: any) => { try { - let info = await repositoriesInstance.listBranches(args); + let info = await repositoriesInstance.listBranches(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -69,19 +75,21 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'push_files', - 'Push multiple files in a single commit', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - branch: z.string().describe('Branch name (string, optional)'), - commitMessage: z.string().describe('Commit message (string, required)'), - files: z.array(z.object({ path: z.string(), content: z.string() })).describe('Array of file objects to push'), + description: 'Push multiple files in a single commit', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + branch: z.string().describe('Branch name (string, optional)'), + commitMessage: z.string().describe('Commit message (string, required)'), + files: z.array(z.object({ path: z.string(), content: z.string() })).describe('Array of file objects to push'), + }, }, - async(args)=>{ + async (args, req: any) => { try { - let info = await repositoriesInstance.pushMultipleFiles(args); + let info = await repositoriesInstance.pushMultipleFiles(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -89,21 +97,23 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'search_repositories', - 'Search for GitHub repositories', { - query: z.string().describe('Search query (string, required)'), - sort: z.string().optional().describe('Sort field (string, optional)'), - order: z.enum(['asc', 'desc']).optional().describe("Sort order ('asc', 'desc') (string, optional)"), - page: z.number().optional().describe('Page number (number, optional)'), - perPage: z.number().optional().describe('Results per page (number, optional)'), - fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), - fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), - }, - async(args)=>{ + description: 'Search for GitHub repositories', + inputSchema: { + query: z.string().describe('Search query (string, required)'), + sort: z.string().optional().describe('Sort field (string, optional)'), + order: z.enum(['asc', 'desc']).optional().describe("Sort order ('asc', 'desc') (string, optional)"), + page: z.number().optional().describe('Page number (number, optional)'), + perPage: z.number().optional().describe('Results per page (number, optional)'), + fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), + fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + }, + }, + async (args, req: any) => { try { - let info = await repositoriesInstance.searchRepositories(args); + let info = await repositoriesInstance.searchRepositories(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -111,17 +121,19 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'create_repository', - 'Create a new GitHub repository', { - name: z.string().describe('Repository name (string, required)'), - description: z.string().describe('Repository description (string, optional)'), - private: z.boolean().describe('Whether the repository is private (boolean, optional)'), - autoInit: z.boolean().describe('Auto-initialize with README (boolean, optional)'), - },async(args)=>{ + description: 'Create a new GitHub repository', + inputSchema: { + name: z.string().describe('Repository name (string, required)'), + description: z.string().describe('Repository description (string, optional)'), + private: z.boolean().describe('Whether the repository is private (boolean, optional)'), + autoInit: z.boolean().describe('Auto-initialize with README (boolean, optional)'), + } + }, async (args, req: any) => { try { - let info = await repositoriesInstance.createRepository(args); + let info = await repositoriesInstance.createRepository(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -129,36 +141,40 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'get_repository_info', - 'Get information about a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - },async(args)=>{ + description: 'Get information about a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + } + }, async (args, req: any) => { try { // NOTE: The service expects { repoName, userName }, but the tool defines { owner, repo }. // Mapping them here. - let info = await repositoriesInstance.getUserRepoInfo({ userName: args.owner, repoName: args.repo }); + let info = await repositoriesInstance.getUserRepoInfo({ userName: args.owner, repoName: args.repo }, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ); - - server.tool( + + server.registerTool( 'get_user_repositories', - 'Get information about a GitHub user repositories', { - userName: z.string().describe('GitHub username (string, required)'), - page: z.number().optional().describe('Page number (number, optional)'), - perPage: z.number().optional().describe('Results per page (number, optional)'), - fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), - fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), - },async(args)=>{ + description: 'Get information about a GitHub user repositories', + inputSchema: { + userName: z.string().describe('GitHub username (string, required)'), + page: z.number().optional().describe('Page number (number, optional)'), + perPage: z.number().optional().describe('Results per page (number, optional)'), + fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), + fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + } + }, async (args, req: any) => { try { - let info = await repositoriesInstance.getUserRepos(args); + let info = await repositoriesInstance.getUserRepos(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -166,18 +182,20 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'get_file_contents', - 'Get the contents of a file in a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - path: z.string().describe('Path to the file (string, required)'), - ref: z.string().optional().describe('Git reference (string, optional)'), + description: 'Get the contents of a file in a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + path: z.string().describe('Path to the file (string, required)'), + ref: z.string().optional().describe('Git reference (string, optional)'), + }, }, - async(args)=>{ + async (args, req: any) => { try { - let info = await repositoriesInstance.getFileContents(args); + let info = await repositoriesInstance.getFileContents(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -185,17 +203,19 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ) - server.tool( + server.registerTool( 'create_fork', - 'Create a fork of a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - organization: z.string().optional().describe('Organization name (string, optional)'), + description: 'Create a fork of a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + organization: z.string().optional().describe('Organization name (string, optional)'), + }, }, - async(args)=>{ + async (args, req: any) => { try { - let info = await repositoriesInstance.createFork(args); + let info = await repositoriesInstance.createFork(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -203,20 +223,22 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ) - server.tool( + server.registerTool( 'create_branch', - 'Create a new branch in a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - branch: z.string().describe('Branch name (string, required)'), - sha: z.string().describe('SHA of the commit to base the new branch on (string, required)'), + description: 'Create a new branch in a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + branch: z.string().describe('Branch name (string, required)'), + sha: z.string().describe('SHA of the commit to base the new branch on (string, required)'), + }, }, - async(args)=>{ + async (args, req: any) => { try { // NOTE: The service expects { branchName, baseBranch }, but the tool defines { branch, sha }. // Mapping them here. - let info = await repositoriesInstance.createBranch({owner: args.owner, repo: args.repo, branchName: args.branch, baseBranch: args.sha}); + let info = await repositoriesInstance.createBranch({ owner: args.owner, repo: args.repo, branchName: args.branch, baseBranch: args.sha }, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -224,16 +246,19 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'get_branch_info', - 'Get information about a branch in a GitHub repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - branch: z.string().describe('Branch name (string, required)'), - },async(args)=>{ + description: 'Get information about a branch in a GitHub repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + branch: z.string().describe('Branch name (string, required)'), + }, + }, + async (args, req: any) => { try { - let info = await repositoriesInstance.getBranchInfo(args); + let info = await repositoriesInstance.getBranchInfo(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -241,22 +266,24 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ); - server.tool( + server.registerTool( 'list_commits', - 'Get a list of commits of a branch in a repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - sha: z.string().optional().describe('Branch name, tag, or commit SHA (string, optional)'), - path: z.string().optional().describe('Only commits containing this file path (string, optional)'), - page: z.number().optional().describe('Page number (number, optional)'), - perPage: z.number().optional().describe('Results per page (number, optional)'), - fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), - fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + description: 'Get a list of commits of a branch in a repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + sha: z.string().optional().describe('Branch name, tag, or commit SHA (string, optional)'), + path: z.string().optional().describe('Only commits containing this file path (string, optional)'), + page: z.number().optional().describe('Page number (number, optional)'), + perPage: z.number().optional().describe('Results per page (number, optional)'), + fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), + fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + }, }, - async(args)=>{ + async (args, req: any) => { try { - let info = await repositoriesInstance.listCommits(args); + let info = await repositoriesInstance.listCommits(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -264,21 +291,23 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ) - server.tool( + server.registerTool( 'get_commit', - 'Get details for a commit from a repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - sha: z.string().optional().describe('Branch name, tag, or commit SHA (string, optional)'), - page: z.number().optional().describe('Page number (number, optional)'), - perPage: z.number().optional().describe('Results per page (number, optional)'), - fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), - fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + description: 'Get details for a commit from a repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + sha: z.string().optional().describe('Branch name, tag, or commit SHA (string, optional)'), + page: z.number().optional().describe('Page number (number, optional)'), + perPage: z.number().optional().describe('Results per page (number, optional)'), + fetchAll: z.boolean().optional().describe('Fetch all pages (boolean, optional)'), + fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'), + }, }, - async(args)=>{ + async (args, req: any) => { try { - let info = await repositoriesInstance.getCommit(args); + let info = await repositoriesInstance.getCommit(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; @@ -286,21 +315,23 @@ export function registerRepositoriesTools(server: McpServer, repositoriesInstanc } ) - server.tool( + server.registerTool( 'get_specific_commit', - 'Get details for an specific commit from a repository', { - owner: z.string().describe('Repository owner (string, required)'), - repo: z.string().describe('Repository name (string, required)'), - sha: z.string().describe('Branch name, tag, or commit SHA (string, optional)'), + description: 'Get details for an specific commit from a repository', + inputSchema: { + owner: z.string().describe('Repository owner (string, required)'), + repo: z.string().describe('Repository name (string, required)'), + sha: z.string().describe('Branch name, tag, or commit SHA (string, optional)'), + }, }, - async(args)=>{ + async (args, req: any) => { try { - let info = await repositoriesInstance.getSpecificCommit(args); + let info = await repositoriesInstance.getSpecificCommit(args, req.requestInfo.headers.github_token); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true }; } } ) -} \ No newline at end of file +} diff --git a/src/features/repositories/repositories.service.ts b/src/features/repositories/repositories.service.ts index 4ff0683..e22b15d 100644 --- a/src/features/repositories/repositories.service.ts +++ b/src/features/repositories/repositories.service.ts @@ -25,27 +25,27 @@ class Repositories extends GitHubClient { return /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(encodedString); } - private _upsertFile(options: CreateFileContentsOptions | UpdateFileContentsOptions) { + private _upsertFile(options: CreateFileContentsOptions | UpdateFileContentsOptions, token: string) { const { owner, repo, path, ...payload } = options as any; if (payload.message) payload.message = sanitize(payload.message); if (payload.content && !this.isBase64(payload.content)) { payload.content = Buffer.from(sanitize(payload.content)).toString('base64'); } - return this.put(`repos/${owner}/${repo}/contents/${sanitize(path)}`, payload); + return this.put(`repos/${owner}/${repo}/contents/${sanitize(path)}`, payload, token); } - async createFileContents(options: CreateFileContentsOptions) { - return this._upsertFile(options); + async createFileContents(options: CreateFileContentsOptions, token: string) { + return this._upsertFile(options, token); } - async updateFileContents(options: UpdateFileContentsOptions) { - return this._upsertFile(options); + async updateFileContents(options: UpdateFileContentsOptions, token: string) { + return this._upsertFile(options, token); } - async listBranches(options: ListBranchesOptions) { + async listBranches(options: ListBranchesOptions, token: string) { const { owner, repo, fields, fetchAll, ...params } = options; const url = `${this.baseUrl}/repos/${owner}/${repo}/branches`; - const config = { params: { per_page: 5, ...params }, headers: { Authorization: `token ${this.token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; + const config = { params: { per_page: 5, ...params }, headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; let results = await paginate(url, config, fetchAll); if (fields?.length) { @@ -58,36 +58,36 @@ class Repositories extends GitHubClient { return results; } - async pushMultipleFiles(options: PushMultipleFilesOptions) { + async pushMultipleFiles(options: PushMultipleFilesOptions, token: string) { const { owner, repo, branch, commitMessage, files } = options; - const branchInfo = await this.get(`repos/${owner}/${repo}/branches/${sanitize(branch)}`); + const branchInfo = await this.get(`repos/${owner}/${repo}/branches/${sanitize(branch)}`, {}, token); const baseTreeSha = (branchInfo as any).commit.commit.tree.sha; const tree = await this.post(`repos/${owner}/${repo}/git/trees`, { base_tree: baseTreeSha, tree: files.map((file: { path: string; content: string }) => ({ path: sanitize(file.path), mode: '100644', type: 'blob', content: sanitize(file.content) })), - }); + }, token); const newCommit = await this.post(`repos/${owner}/${repo}/git/commits`, { message: sanitize(commitMessage), tree: (tree as any).sha, parents: [(branchInfo as any).commit.sha], - }); + }, token); - await this.patch(`repos/${owner}/${repo}/git/refs/heads/${sanitize(branch)}`, { sha: (newCommit as any).sha }); + await this.patch(`repos/${owner}/${repo}/git/refs/heads/${sanitize(branch)}`, { sha: (newCommit as any).sha }, token); return newCommit; } - async createRepository(options: CreateRepositoryOptions) { + async createRepository(options: CreateRepositoryOptions, token: string) { const { name, description, privateRepo, autoInit } = options; const payload = { name: sanitize(name), description: description ? sanitize(description) : undefined, private: privateRepo, auto_init: autoInit }; - return this.post('user/repos', payload); + return this.post('user/repos', payload, token); } - async searchRepositories(options: SearchRepositoriesOptions) { + async searchRepositories(options: SearchRepositoriesOptions, token: string) { const { fields, fetchAll, query, ...params } = options; const url = `${this.baseUrl}/search/repositories`; - const config = { params: { q: sanitize(query), per_page: 5, ...params }, headers: { Authorization: `token ${this.token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; + const config = { params: { q: sanitize(query), per_page: 5, ...params }, headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; let results: any = await paginate(url, config, fetchAll); if (fields?.length && results.items) { @@ -100,10 +100,10 @@ class Repositories extends GitHubClient { return results; } - async getUserRepos(options: GetUserReposOptions) { + async getUserRepos(options: GetUserReposOptions, token: string) { const { userName, fields, fetchAll, ...params } = options; const url = `${this.baseUrl}/users/${userName}/repos`; - const config = { params: { per_page: 5, ...params }, headers: { Authorization: `token ${this.token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; + const config = { params: { per_page: 5, ...params }, headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; let results = await paginate(url, config, fetchAll); if (fields?.length) { @@ -116,40 +116,40 @@ class Repositories extends GitHubClient { return results; } - async getUserRepoInfo(options: GetUserRepoInfoOptions) { + async getUserRepoInfo(options: GetUserRepoInfoOptions, token: string) { const { repoName, userName } = options; - return this.get(`repos/${userName}/${repoName}`); + return this.get(`repos/${userName}/${repoName}`, {}, token); } - async getFileContents(options: GetFileContentsOptions) { + async getFileContents(options: GetFileContentsOptions, token: string) { const { owner, repo, path, ...params } = options; - const response: any = await this.get(`repos/${owner}/${repo}/contents/${sanitize(path)}`, params); + const response: any = await this.get(`repos/${owner}/${repo}/contents/${sanitize(path)}`, params, token); if (response?.content) { response.decodedContent = Buffer.from(response.content, 'base64').toString('utf-8'); } return response; } - async createFork(options: CreateForkOptions) { + async createFork(options: CreateForkOptions, token: string) { const { owner, repo, ...payload } = options; - return this.post(`repos/${owner}/${repo}/forks`, payload); + return this.post(`repos/${owner}/${repo}/forks`, payload, token); } - async getBranchInfo(options: GetBranchInfoOptions) { + async getBranchInfo(options: GetBranchInfoOptions, token: string) { const { owner, repo, branch } = options; - return this.get(`repos/${owner}/${repo}/git/ref/heads/${sanitize(branch)}`); + return this.get(`repos/${owner}/${repo}/git/ref/heads/${sanitize(branch)}`, {}, token); } - async createBranch(options: CreateBranchOptions) { + async createBranch(options: CreateBranchOptions, token: string) { const { owner, repo, branchName, baseBranch } = options; - return this.post(`repos/${owner}/${repo}/git/refs`, { ref: `refs/heads/${sanitize(branchName)}`, sha: sanitize(baseBranch) }); + return this.post(`repos/${owner}/${repo}/git/refs`, { ref: `refs/heads/${sanitize(branchName)}`, sha: sanitize(baseBranch) }, token); } - async listCommits(options: ListCommitsOptions) { + async listCommits(options: ListCommitsOptions, token: string) { const { owner, repo, fields, fetchAll, ...params } = options; if (params.path) params.path = sanitize(params.path); const url = `${this.baseUrl}/repos/${owner}/${repo}/commits`; - const config = { params: { per_page: 5, ...params }, headers: { Authorization: `token ${this.token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; + const config = { params: { per_page: 5, ...params }, headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; let results = await paginate(url, config, fetchAll); if (fields?.length) { @@ -162,11 +162,11 @@ class Repositories extends GitHubClient { return results; } - async getCommit(options: GetCommitOptions) { + async getCommit(options: GetCommitOptions, token: string) { const { owner, repo, fields, fetchAll, ...params } = options; if (params.sha) params.sha = sanitize(params.sha); const url = `${this.baseUrl}/repos/${owner}/${repo}/commits`; - const config = { params: { page: 1, per_page: 5, ...params }, headers: { Authorization: `token ${this.token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; + const config = { params: { page: 1, per_page: 5, ...params }, headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json' }, timeout: 5000 }; let results = await paginate(url, config, fetchAll); if (fields?.length) { @@ -179,9 +179,9 @@ class Repositories extends GitHubClient { return results; } - async getSpecificCommit(options: GetSpecificCommitOptions) { + async getSpecificCommit(options: GetSpecificCommitOptions, token: string) { const { owner, repo, sha } = options; - return this.get(`repos/${owner}/${repo}/commits/${sanitize(sha)}`); + return this.get(`repos/${owner}/${repo}/commits/${sanitize(sha)}`, {}, token); } } diff --git a/src/main.ts b/src/main.ts index f7255d2..e0d8bcd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,7 @@ import { config } from '#config/index'; import { logger } from '#core/logger'; // Importa los nuevos módulos de servidor y servicios -import { createServer, closeAllSseConnections } from './server.js'; +import { createServer } from './server.js'; import Issues from "#features/issues/issues.service"; import PullRequest from "#features/pullRequests/pullRequest.service"; import Repositories from "#features/repositories/repositories.service"; @@ -19,9 +19,7 @@ import { registerRepositoriesTools } from "#features/repositories/repositories.r // --- 1. Instancia del Servidor MCP --- const mcpServer = new McpServer({ name: "mcp-sse-github", - version: "1.0.0", - description: "GitHub MCP SSE Server", - timeoutMs: config.mcpTimeout, + version: "1.0.0" }); logger.info(`MCP Server configured with timeout: ${config.mcpTimeout}ms`); @@ -35,7 +33,7 @@ registerIssueTools(mcpServer, issuesService); registerPullRequestTools(mcpServer, pullRequestService); registerRepositoriesTools(mcpServer, repositoriesService); -// --- 4. Lógica de Búsqueda de Puerto --- + const isPortAvailable = async (port: number): Promise => { return new Promise((resolve) => { const server = net.createServer(); @@ -51,34 +49,27 @@ const findAvailablePort = async (startPort: number, maxAttempts: number = 10): P if (await isPortAvailable(port)) { return port; } - logger.info(`Port ${port} is in use, trying next port...`); } throw new Error(`Could not find an available port after ${maxAttempts} attempts`); }; -// --- 5. Manejadores de Errores Globales --- -process.on('uncaughtException', (error) => { - logger.error('Uncaught exception:', error); -}); - process.on('unhandledRejection', (reason) => { logger.error('Unhandled promise rejection:', reason); }); -// --- 6. Función de Inicio del Servidor --- const startServer = async () => { try { + const port = await findAvailablePort(config.ssePort); - logger.info(`Starting MCP SSE GitHub server on port ${port}...`); - - const httpServer = createServer(mcpServer, port); + logger.info(`Starting MCP GITHUB server on port ${port}...`); + + const httpServer = await createServer(mcpServer, port); const httpTerminator = createHttpTerminator({ server: httpServer }); - logger.info(`MCP SSE GitHub server started successfully on port ${port}`); + logger.info(`MCP GITHUB server started successfully on port ${port}`); const shutdown = async (signal: string) => { logger.info(`Received ${signal} signal. Starting graceful shutdown...`); - closeAllSseConnections(); await new Promise(resolve => setTimeout(resolve, 500)); try { await httpTerminator.terminate(); @@ -99,4 +90,71 @@ const startServer = async () => { } }; -startServer(); \ No newline at end of file +startServer(); + + +// // --- 4. Lógica de Búsqueda de Puerto --- +// const isPortAvailable = async (port: number): Promise => { +// return new Promise((resolve) => { +// const server = net.createServer(); +// server.once('error', () => resolve(false)); +// server.once('listening', () => server.close(() => resolve(true))); +// server.listen(port); +// }); +// }; + +// const findAvailablePort = async (startPort: number, maxAttempts: number = 10): Promise => { +// for (let i = 0; i < maxAttempts; i++) { +// const port = startPort + i; +// if (await isPortAvailable(port)) { +// return port; +// } +// logger.info(`Port ${port} is in use, trying next port...`); +// } +// throw new Error(`Could not find an available port after ${maxAttempts} attempts`); +// }; + +// // --- 5. Manejadores de Errores Globales --- +// process.on('uncaughtException', (error) => { +// logger.error('Uncaught exception:', error); +// }); + +// process.on('unhandledRejection', (reason) => { +// logger.error('Unhandled promise rejection:', reason); +// }); + +// // --- 6. Función de Inicio del Servidor --- +// const startServer = async () => { +// try { +// const port = await findAvailablePort(config.ssePort); +// logger.info(`Starting MCP SSE GitHub server on port ${port}...`); + +// const httpServer = createServer(mcpServer, port); +// const httpTerminator = createHttpTerminator({ server: httpServer }); + +// logger.info(`MCP SSE GitHub server started successfully on port ${port}`); + +// const shutdown = async (signal: string) => { +// logger.info(`Received ${signal} signal. Starting graceful shutdown...`); +// closeAllSseConnections(); +// await new Promise(resolve => setTimeout(resolve, 500)); +// try { +// await httpTerminator.terminate(); +// logger.info('HTTP server terminated successfully. Exiting.'); +// process.exit(0); +// } catch (error) { +// logger.error('Error during HTTP server termination:', error); +// process.exit(1); +// } +// }; + +// process.on('SIGINT', () => shutdown('SIGINT')); +// process.on('SIGTERM', () => shutdown('SIGTERM')); + +// } catch (error) { +// logger.error('Failed to start server:', error); +// process.exit(1); +// } +// }; + +// startServer(); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index b062e93..e633d26 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -21,11 +21,7 @@ export const authenticate = (req: Request, res: Response, next: NextFunction): v const token = tokenParts[1]; - if (token !== config.apiKey) { - logger.warn('Authentication failed: Invalid API key'); - res.status(401).json({ error: 'Invalid API key' }); - return; - } + req.githubToken = token; logger.info('Authentication successful'); next(); diff --git a/src/server.ts b/src/server.ts index 2bf3fb4..f2cfea5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,375 +1,50 @@ import http from 'http'; import cors from 'cors'; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - StreamableHTTPServerTransport, - StreamableHTTPServerTransportOptions -} from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import { MultiplexingSSEServerTransport } from "./multiplexing-sse-transport.js"; -import { randomUUID } from 'crypto'; -import { z } from 'zod'; - -// Importar configuración y logger centralizados -import { config } from '#config/index'; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { logger } from '#core/logger'; -import rateLimit from 'express-rate-limit'; -import express, { Request, Response, NextFunction } from 'express'; -import { authenticate } from './middleware/auth.js'; - -const generalLimiter = rateLimit({ - windowMs: config.rateLimitWindowMs, - max: config.rateLimitMaxRequests, - message: { - error: 'Too many requests from this IP', - retryAfter: `${config.rateLimitWindowMs / 60000} minutes` - }, - standardHeaders: true, - legacyHeaders: false, - handler: (req, res) => { - logger.warn(`Rate limit exceeded for IP: ${req.ip} on ${req.method} ${req.url}`); - res.status(429).json({ - error: 'Rate limit exceeded', - retryAfter: Math.ceil((req.rateLimit?.resetTime?.getTime() ?? Date.now()) / 1000) - }); - } -}); - -const sseLimiter = rateLimit({ - windowMs: 60 * 1000, // 1 minuto - max: config.rateLimitSseMax, - message: 'Too many SSE connections from this IP' -}); - -const messageLimiter = rateLimit({ - windowMs: 60 * 1000, // 1 minuto - max: config.rateLimitMessagesMax, - message: 'Too many messages from this IP', // This will be overridden by handler - handler: (req, res) => { - logger.warn(`Message Rate limit exceeded for IP: ${req.ip}`); - res.status(429).json({ - error: 'Rate limit exceeded', - message: 'Too many messages from this IP' - }); - } -}); - -const createUserLimiter = () => rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hora - max: (req: Request) => { - return req.user?.rateLimits?.requestsPerHour ?? config.defaultUserRateLimit; - }, - message: 'User rate limit exceeded' -}); - -const CRITICAL_TOOLS = [ - 'create_repository', - 'merge_pull_request', - 'push_files', - 'create_fork' -]; - -const isCriticalOperation = (toolName: string): boolean => { - return CRITICAL_TOOLS.includes(toolName); -}; - -const criticalOperationsLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hora - max: 10, // Solo 10 operaciones críticas por hora - message: 'Critical operation rate limit exceeded', // This will be overridden by handler - handler: (req, res) => { - logger.warn(`Critical operation rate limit exceeded for IP: ${req.ip}`); - res.status(429).json({ - error: 'Rate limit exceeded', - message: 'Critical operation rate limit exceeded' - }); - } -}); - -const rateLimitMonitor = (req: Request, res: Response, next: NextFunction) => { - const remaining = req.rateLimit?.remaining ?? 0; - const total = req.rateLimit?.limit ?? 0; +import express from 'express'; - if (remaining > 0 && remaining < total * 0.1) { - logger.warn(`Rate limit warning for ${req.ip} on ${req.method} ${req.url}: ${remaining}/${total} remaining`); - } - - if (remaining === 0) { - logger.error(`Rate limit exceeded for ${req.ip} on ${req.method} ${req.url}`); - } - - next(); -}; - -const sseTransports: Record = {}; -let multiplexingTransport: MultiplexingSSEServerTransport | null = null; - -export function closeAllSseConnections() { - if (config.useMultiplexing && multiplexingTransport) { - logger.info('Closing multiplexing SSE transport...'); - multiplexingTransport.close().catch(error => { - logger.error('Error closing multiplexing transport:', error); - }); - } else { - logger.info(`Closing all active SSE connections (${Object.keys(sseTransports).length})...`); - for (const sessionId in sseTransports) { - const { res } = sseTransports[sessionId]; - try { - if (!res.writableEnded) { - res.write('event: server-shutdown\n'); - res.write('data: {"message": "Server is shutting down. Please reconnect."}\n\n'); - res.end(); - logger.info(`Closed SSE connection for session: ${sessionId}`); - } - } catch (error) { - logger.error(`Error closing SSE connection for session ${sessionId}:`, error); - } - } - } -} - -export function createServer(mcpServer: McpServer, port: number): http.Server { +export async function createServer(mcpServer: McpServer, port: number): Promise> { const app = express(); - app.use(cors({ - origin: config.corsAllowOrigin, - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - credentials: true + origin: "*", + allowedHeaders: ["Content-Type", "Authorization", "Mcp-Session-Id", "Mcp-Protocol-Version", "Accept", "User-Agent", "Authorization", "authorization","GITHUB_TOKEN","github_token"], + exposedHeaders: ["Mcp-Session-Id", "Mcp-Protocol-Version"], })); - logger.info(`CORS configured with origin: ${config.corsAllowOrigin}`); - app.use(express.json({ - limit: '1mb', // Reducir de 300mb actual - verify: (req, res, buf) => { - if (buf.length > 1048576) { // 1MB - throw new Error('Payload too large'); + app.use((req, res, next) => { + // Fix Accept header for MCP SDK strict check + if (req.headers.accept === "*/*" || !req.headers.accept) { + req.headers.accept = "application/json, text/event-stream"; } - } -})); - - // Aplicar rate limiting general a todas las rutas - app.use(generalLimiter); - app.use(rateLimitMonitor); // Aplicar el monitor después del limiter - - app.get('/health', (req: express.Request, res: express.Response) => { - res.status(200).json({ - status: 'ok', - timestamp: new Date().toISOString(), - version: '1.0.0', - serverName: 'mcp-sse-github' - }); + next(); }); - const streamableTransportOptions: StreamableHTTPServerTransportOptions = { - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (sessionId) => { - logger.info(`Streamable HTTP session initialized: ${sessionId}`); - }, - }; - - const mcpStreamableTransport = new StreamableHTTPServerTransport(streamableTransportOptions); - mcpServer.connect(mcpStreamableTransport).catch(error => { - logger.error("Failed to connect McpServer to StreamableHTTPServerTransport:", error); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, }); - if (config.useMultiplexing) { - logger.info('Initializing MultiplexingSSEServerTransport...'); - multiplexingTransport = new MultiplexingSSEServerTransport(); - mcpServer.connect(multiplexingTransport).then(() => { - logger.info('MultiplexingSSEServerTransport connected to McpServer'); - }).catch(error => { - logger.error('Failed to connect McpServer to MultiplexingSSEServerTransport:', error); - multiplexingTransport = null; - }); - } - - app.all('/mcp', authenticate, createUserLimiter(), (req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.debug(`New Streamable HTTP request: ${req.method} ${req.url}`); - if (req.method === 'OPTIONS') { - res.status(200).end(); - return; - } - - // Check for critical operations - const toolName = req.body?.toolName; // Assuming toolName is in the request body - if (toolName && isCriticalOperation(toolName)) { - criticalOperationsLimiter(req, res, () => { - // Continue with the original /mcp logic after criticalOperationsLimiter - const requestTimeout = setTimeout(() => { - if (!res.writableEnded) { - logger.error(`Request timeout for ${req.url}`); - res.status(408).json({ - error: 'Request timeout', - message: 'The request took too long to process' - }); - } - }, config.mcpTimeout); - - mcpStreamableTransport.handleRequest(req, res, req.body) - .then(() => clearTimeout(requestTimeout)) - .catch((error) => { - clearTimeout(requestTimeout); - logger.error(`Error handling Streamable HTTP request for ${req.url}:`, error); - if (!res.writableEnded) { - res.status(500).json({ - error: 'Error processing Streamable HTTP request', - message: error instanceof Error ? error.message : 'Unknown error' - }); - } - }); - }); - } else { - // Original /mcp logic if not a critical operation - const requestTimeout = setTimeout(() => { - if (!res.writableEnded) { - logger.error(`Request timeout for ${req.url}`); - res.status(408).json({ - error: 'Request timeout', - message: 'The request took too long to process' - }); - } - }, config.mcpTimeout); - - mcpStreamableTransport.handleRequest(req, res, req.body) - .then(() => clearTimeout(requestTimeout)) - .catch((error) => { - clearTimeout(requestTimeout); - logger.error(`Error handling Streamable HTTP request for ${req.url}:`, error); - if (!res.writableEnded) { - res.status(500).json({ - error: 'Error processing Streamable HTTP request', - message: error instanceof Error ? error.message : 'Unknown error' - }); - } - }); - } - }); - - app.get('/sse', sseLimiter, (req: express.Request, res: express.Response) => { - logger.info('New SSE connection request'); - if (config.useMultiplexing && multiplexingTransport) { - const clientSessionId = randomUUID(); - logger.info(`Using MultiplexingSSEServerTransport for client: ${clientSessionId}`); - multiplexingTransport.addClient(clientSessionId, res); - - const heartbeatInterval = setInterval(() => { - if (res.writableEnded) { - clearInterval(heartbeatInterval); - return; - } - logger.debug(`Sending heartbeat to multiplexed session ${clientSessionId}`); - res.write(': heartbeat\n\n'); - }, 10000); - - req.on('close', () => { - clearInterval(heartbeatInterval); - logger.warn(`Multiplexed SSE connection closed for session: ${clientSessionId}.`); - if (multiplexingTransport) { - multiplexingTransport.removeClient(clientSessionId); - } - }); - } else { - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - res.setHeader('X-Accel-Buffering', 'no'); - - const transport = new SSEServerTransport('/messages', res); - - const connectionTimeout = setTimeout(() => { - if (!res.writableEnded) { - logger.error('SSE connection timeout'); - res.status(408).send('Connection timeout'); - } - }, config.sseTimeout); - - mcpServer.connect(transport).then(() => { - clearTimeout(connectionTimeout); - sseTransports[transport.sessionId] = { transport, res }; - logger.info(`SSE connection established: ${transport.sessionId}`); + await mcpServer.connect(transport); - const heartbeatInterval = setInterval(() => { - if (res.writableEnded) { - clearInterval(heartbeatInterval); - return; - } - logger.debug(`Sending heartbeat to session ${transport.sessionId}`); - res.write(': heartbeat\n\n'); - }, 10000); - - req.on('close', () => { - clearInterval(heartbeatInterval); - logger.warn(`SSE connection closed for session: ${transport.sessionId}.`); - delete sseTransports[transport.sessionId]; - }); - }).catch((error) => { - clearTimeout(connectionTimeout); - logger.error(`Error connecting MCP Server for SSE: ${error}`); - if (!res.writableEnded) { - res.status(500).send('MCP Server connection error for SSE'); - } - }); + app.all("/sse", async (req, res) => { + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + logger.error("Error handling /sse:", error); } }); - app.post('/messages', authenticate, messageLimiter, (req: express.Request, res: express.Response) => { + app.all("/messages", async (req, res) => { try { - const sessionIdSchema = z.string().uuid(); - const sessionId = sessionIdSchema.parse(req.query.sessionId); - - const bodyContent = (typeof req.body === 'object' && req.body !== null) - ? JSON.stringify(req.body) - : (req.body?.toString() ?? ''); - - const messageTimeout = setTimeout(() => { - if (!res.writableEnded) { - logger.error(`Message processing timeout for session ${sessionId}`); - res.status(408).json({ error: 'Request timeout' }); - } - }, config.mcpTimeout); - - const transport = (config.useMultiplexing && multiplexingTransport) - ? multiplexingTransport - : sseTransports[sessionId]?.transport; - - if (!transport) { - logger.error(`POST /messages error: Session not found for ID ${sessionId}`); - res.status(404).json({ error: 'Session not found' }); - clearTimeout(messageTimeout); - return; - } - - const handlePromise = config.useMultiplexing && multiplexingTransport - ? multiplexingTransport.handleClientPostMessage(sessionId, bodyContent) - : (transport as SSEServerTransport).handlePostMessage(req, res, bodyContent); - - handlePromise - .then(() => clearTimeout(messageTimeout)) - .catch((error) => { - clearTimeout(messageTimeout); - logger.error(`Error handling message for session ${sessionId}:`, error); - if (!res.writableEnded) { - res.status(500).json({ error: 'Internal server error' }); - } - }); - } catch (error:any) { - logger.error('Invalid sessionId format'); - res.status(400).json({ error: `Invalid session ID format, ${error.message}` }); + await transport.handleRequest(req, res, req.body); + } catch (error) { + logger.error("Error handling /messages:", error); } }); - - app.use((req: express.Request, res: express.Response) => { - logger.debug(`Unhandled request: ${req.method} ${req.url}`); - res.status(404).json({ error: 'Not found' }); - }); const httpServer = http.createServer(app); - httpServer.timeout = config.sseTimeout; - httpServer.keepAliveTimeout = config.sseTimeout; - httpServer.headersTimeout = config.sseTimeout; - - logger.info(`HTTP server timeouts configured: timeout=${httpServer.timeout}ms, keepAliveTimeout=${httpServer.keepAliveTimeout}ms`); httpServer.listen(port, () => { logger.info(`MCP Server (Express with SSE & Streamable HTTP) listening on port ${port}`); diff --git a/src/services/api.ts b/src/services/api.ts index 8b10963..5bfee96 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -11,47 +11,47 @@ export default class GitHubClient { this.timeout = 600000; // 10 minutes timeout } - private getHeaders() { + private getHeaders(token: string) { return { - Authorization: `token ${this.token}`, + Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json', }; } - protected async get(endpoint: string, params: Record = {}): Promise { + protected async get(endpoint: string, params: Record = {}, token: string): Promise { const response = await axios.get(`${this.baseUrl}/${endpoint}`, { - headers: this.getHeaders(), + headers: this.getHeaders(token), params, timeout: this.timeout, }); return response.data; } - protected async post(endpoint: string, data: Record): Promise { + protected async post(endpoint: string, data: Record, token: string): Promise { const response = await axios.post(`${this.baseUrl}/${endpoint}`, data, { - headers: this.getHeaders(), + headers: this.getHeaders(token), timeout: this.timeout, }); return response.data; } - protected async patch(endpoint: string, data: Record): Promise { + protected async patch(endpoint: string, data: Record, token: string): Promise { const response = await axios.patch(`${this.baseUrl}/${endpoint}`, data, { - headers: this.getHeaders(), + headers: this.getHeaders(token), timeout: this.timeout, }); return response.data; } - protected async put(endpoint: string, data: Record): Promise { + protected async put(endpoint: string, data: Record, token: string): Promise { const response = await axios.put(`${this.baseUrl}/${endpoint}`, data, { - headers: this.getHeaders(), + headers: this.getHeaders(token), timeout: this.timeout, }); return response.data; } - async getUserInfo() { - return this.get('user'); + async getUserInfo(token: string) { + return this.get('user', {}, token); } -} \ No newline at end of file +} diff --git a/src/types/express.d.ts b/src/types/express.d.ts index 74d67cd..06ddb92 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -11,5 +11,6 @@ declare namespace Express { requestsPerHour?: number; }; }; + githubToken?: string; } } diff --git a/tsconfig.json b/tsconfig.json index 1ff2820..6411a5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,9 @@ ], "#config/*": [ "config/*" + ], + "#middleware/*": [ + "middleware/*" ] } }, From b031b68caf445f0bad9e89c77c32f9351be67859 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Mon, 1 Dec 2025 21:52:26 -0300 Subject: [PATCH 2/3] change connection type deprecated sse to streamable http --- src/main.ts | 69 +-------- src/middleware/auth.ts | 1 - src/multiplexing-sse-transport.ts | 240 ------------------------------ 3 files changed, 1 insertion(+), 309 deletions(-) delete mode 100644 src/multiplexing-sse-transport.ts diff --git a/src/main.ts b/src/main.ts index e0d8bcd..175ed14 100644 --- a/src/main.ts +++ b/src/main.ts @@ -90,71 +90,4 @@ const startServer = async () => { } }; -startServer(); - - -// // --- 4. Lógica de Búsqueda de Puerto --- -// const isPortAvailable = async (port: number): Promise => { -// return new Promise((resolve) => { -// const server = net.createServer(); -// server.once('error', () => resolve(false)); -// server.once('listening', () => server.close(() => resolve(true))); -// server.listen(port); -// }); -// }; - -// const findAvailablePort = async (startPort: number, maxAttempts: number = 10): Promise => { -// for (let i = 0; i < maxAttempts; i++) { -// const port = startPort + i; -// if (await isPortAvailable(port)) { -// return port; -// } -// logger.info(`Port ${port} is in use, trying next port...`); -// } -// throw new Error(`Could not find an available port after ${maxAttempts} attempts`); -// }; - -// // --- 5. Manejadores de Errores Globales --- -// process.on('uncaughtException', (error) => { -// logger.error('Uncaught exception:', error); -// }); - -// process.on('unhandledRejection', (reason) => { -// logger.error('Unhandled promise rejection:', reason); -// }); - -// // --- 6. Función de Inicio del Servidor --- -// const startServer = async () => { -// try { -// const port = await findAvailablePort(config.ssePort); -// logger.info(`Starting MCP SSE GitHub server on port ${port}...`); - -// const httpServer = createServer(mcpServer, port); -// const httpTerminator = createHttpTerminator({ server: httpServer }); - -// logger.info(`MCP SSE GitHub server started successfully on port ${port}`); - -// const shutdown = async (signal: string) => { -// logger.info(`Received ${signal} signal. Starting graceful shutdown...`); -// closeAllSseConnections(); -// await new Promise(resolve => setTimeout(resolve, 500)); -// try { -// await httpTerminator.terminate(); -// logger.info('HTTP server terminated successfully. Exiting.'); -// process.exit(0); -// } catch (error) { -// logger.error('Error during HTTP server termination:', error); -// process.exit(1); -// } -// }; - -// process.on('SIGINT', () => shutdown('SIGINT')); -// process.on('SIGTERM', () => shutdown('SIGTERM')); - -// } catch (error) { -// logger.error('Failed to start server:', error); -// process.exit(1); -// } -// }; - -// startServer(); +startServer(); \ No newline at end of file diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index e633d26..9170e60 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,5 +1,4 @@ import { Request, Response, NextFunction } from 'express'; -import { config } from '#config/index'; import { logger } from '#core/logger'; export const authenticate = (req: Request, res: Response, next: NextFunction): void => { diff --git a/src/multiplexing-sse-transport.ts b/src/multiplexing-sse-transport.ts deleted file mode 100644 index a29fd21..0000000 --- a/src/multiplexing-sse-transport.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { Transport, TransportSendOptions } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { JSONRPCMessage, RequestId } from "@modelcontextprotocol/sdk/types.js"; // Import RequestId -import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; // Import AuthInfo -import { randomUUID } from 'crypto'; -import { Response } from 'express'; - -export class MultiplexingSSEServerTransport implements Transport { - public readonly sessionId: string; - private readonly clients: Map = new Map(); // Map of clientSessionId -> Response - private readonly requestClientMap: Map = new Map(); // Map of requestId -> clientSessionId - private readonly heartbeatInterval: NodeJS.Timeout | undefined; - public onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo; }) => void; - public onclose?: () => void; - public onerror?: (error: Error) => void; - - constructor() { - this.sessionId = randomUUID(); // Unique ID for this multiplexing transport - this.heartbeatInterval = setInterval(() => this.heartbeat(), 20000); // Send heartbeat every 20 seconds - } - - private heartbeat(): void { - const heartbeatMessage = `:heartbeat\n\n`; - this.clients.forEach((res, clientSessionId) => { - if (!res.writableEnded) { - try { - res.write(heartbeatMessage); - if (typeof (res as any).flush === 'function') { - (res as any).flush(); - } - } catch (error) { - this.removeClient(clientSessionId); - console.log(`Exception while doing something: ${error}`); - } - } else { - this.removeClient(clientSessionId); - } - }); - } - - async start(): Promise { - // ping clients, esta vacio porque se imprimian los logs de inicio de sesion - } - - // Extract message details for logging - private extractMessageDetails(message: JSONRPCMessage): { - messageType: string; - methodName?: string; - messageId: any - } { - let messageType: string = 'unknown'; - let methodName: string | undefined; - let messageId: any = null; - - if ('method' in message) { - messageType = 'request/notification'; - methodName = message.method; - if ('id' in message) { - messageId = message.id; - } - } else if ('result' in message || 'error' in message) { - messageType = 'response'; - if ('id' in message) { - messageId = message.id; - } - } - - return { messageType, methodName, messageId }; - } - - // Log message information - private logMessageInfo( - messageString: string, - messageType: string, - methodName?: string, - messageId: any = null - ): void { - if (methodName === 'mcp/toolListChanged') { - console.log(`MultiplexingSSEServerTransport (${this.sessionId}): Detected mcp/toolListChanged notification.`); - } - } - - // Send message to a specific client - private sendToTargetClient( - messageString: string, - targetClientSessionId: string, - relatedRequestId: RequestId - ): boolean { - const res = this.clients.get(targetClientSessionId); - - if (!res || res.writableEnded) { - return false; - } - - try { - res.write(`data: ${messageString}\n\n`); - if (typeof (res as any).flush === 'function') { - (res as any).flush(); - } - return true; - } catch (error: any) { - this.removeClient(targetClientSessionId); - this.onerror?.(new Error(`Failed to send message to client ${targetClientSessionId}: ${error.message}`)); - return false; - } - } - - async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { - try { - const messageString = JSON.stringify(message); - const { messageType, methodName, messageId } = this.extractMessageDetails(message); - - this.logMessageInfo(messageString, messageType, methodName, messageId); - - const isTargetedResponse = options?.relatedRequestId && ('result' in message || 'error' in message); - - if (!isTargetedResponse) { - this.broadcastMessage(messageString); - return; - } - - // Handle targeted response - const targetClientSessionId = this.requestClientMap.get(options.relatedRequestId as RequestId); - - if (!targetClientSessionId) { - this.broadcastMessage(messageString); - return; - } - - // We know relatedRequestId exists at this point - const sentSuccessfully = this.sendToTargetClient( - messageString, - targetClientSessionId, - options.relatedRequestId as RequestId - ); - - if (!sentSuccessfully) { - // Fallback to broadcast if targeted send failed - this.broadcastMessage(messageString); - } else { - // Clean up the request mapping after successful delivery - this.requestClientMap.delete(options.relatedRequestId as RequestId); - } - } catch (error: any) { - this.onerror?.(error); - } - } - - private broadcastMessage(messageString: string): void { - let successfulBroadcasts = 0; - const clientCount = this.clients.size; - - if (clientCount === 0) { - return; - } - - this.clients.forEach((res, clientSessionId) => { - if (!res.writableEnded) { - try { - res.write(`data: ${messageString}\n\n`); - if (typeof (res as any).flush === 'function') { - (res as any).flush(); - } - successfulBroadcasts++; - } catch (error: any) { - this.removeClient(clientSessionId); - this.onerror?.(new Error(`Failed to broadcast to client ${clientSessionId}: ${error.message}`)); - } - } else { - this.removeClient(clientSessionId); - } - }); - - } - - async close(): Promise { - this.clients.forEach((res, clientSessionId) => { - if (!res.writableEnded) { - res.end(); - } - }); - this.clients.clear(); - this.requestClientMap.clear(); // Clear request map on close - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); - } - this.onclose?.(); - } - - // This method is called by the Express POST /messages route - // It takes a client's sessionId and the message body, then forwards it to the McpServer via onmessage - async handleClientPostMessage(clientSessionId: string, bodyContent: string): Promise { - - if (!this.onmessage) { - return; - } - - try { - const parsedMessage: JSONRPCMessage = JSON.parse(bodyContent); - - // If it's a request, store the mapping - if ('id' in parsedMessage && 'method' in parsedMessage) { - this.requestClientMap.set(parsedMessage.id, clientSessionId); - } - - // Forward the message to the MCP server - this.onmessage(parsedMessage); - - return "Message processed successfully"; - } catch (error: any) { - const errorMessage = `Error processing client message from ${clientSessionId}: ${error.message}`; - this.onerror?.(new Error(errorMessage)); - throw error; // Let the caller handle the error response - } - } - // Method to add a new SSE client connection - addClient(clientSessionId: string, res: Response) { - this.clients.set(clientSessionId, res); - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }); - // Ensure headers are flushed immediately - if (typeof res.flushHeaders === 'function') { - res.flushHeaders(); - } - // Send the initial endpoint message in the format expected by SSE clients - res.write(`event: endpoint\n`); - res.write(`data: /messages?sessionId=${clientSessionId}\n\n`); - - // Flush to ensure initial message is sent - if (typeof (res as any).flush === 'function') { - (res as any).flush(); - } - } - // Method to remove a disconnected SSE client - removeClient(clientSessionId: string) { - this.clients.delete(clientSessionId); - } -} From 936fbdb09240c61053d840b0a384ee41d23334b1 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Mon, 1 Dec 2025 21:59:13 -0300 Subject: [PATCH 3/3] change readme and chagelog --- CHANGELOG.md | 8 +- README.md | 267 +++------------------------------------------------ 2 files changed, 21 insertions(+), 254 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af1ad8..8bc1582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to the GitHub See MCP Server project will be documented in t The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.1] - 2025-01-12 + +### Changed + +- Updated and simplified `README.md` for clarity. +- Updated the connection command for Claude. + ## [1.5.0] - 2025-08-25 ### Added @@ -87,4 +94,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Resolved connection handling issues with certain model types - Fixed webhook payload parsing for large events - Improved error handling and logging - diff --git a/README.md b/README.md index 4379e7b..e767eb5 100644 --- a/README.md +++ b/README.md @@ -2,52 +2,6 @@ A Model Context Protocol (MCP) server that provides GitHub API integration through Server-Sent Events (SSE) transport. -## Features - -- **Modular, Feature-Based Architecture**: Code is organized by features (issues, pull requests, etc.) for better maintainability and scalability. -- GitHub API integration through MCP tools -- Support for issues, pull requests, repositories, and more -- Server-Sent Events (SSE) transport for real-time communication -- **Multiplexing SSE transport** for efficient handling of multiple client connections -- Modern Streamable HTTP and legacy SSE transport support -- Automatic port finding if the specified port is in use -- Graceful shutdown handling for clean server termination -- Configurable timeouts, CORS settings, and logging levels -- Robust error handling and detailed logging - -## Authentication - -This server uses API key authentication to protect its endpoints. All requests to `/mcp` and `/messages` must include an `Authorization` header with a valid bearer token. - -Example: `Authorization: Bearer your-secret-api-key` - -To set up authentication, add the following variable to your `.env` file: - -`API_KEY=your-secret-api-key` - -## Project Structure - -The project follows a modular, feature-based architecture. All source code is located in the `src` directory. - -``` -/src/ -├───config/ # Centralizes application configuration (ports, env vars, etc.) -├───core/ # Shared utilities like the logger and custom error classes. -├───features/ # Contains the core business logic, organized by feature. -│ ├───issues/ -│ ├───pullRequests/ -│ └───repositories/ -├───services/ # Contains a reusable client for the external GitHub API. -├───utils/ # General utility functions (e.g., pagination). -├───server.ts # Express server setup, middleware, and transport configuration. -└───main.ts # The main entry point of the application. -``` - -- **`features`**: Each subdirectory within `features` represents a module. It contains a `*.service.ts` file for business logic and a `*.router.ts` file for defining MCP tools. -- **`services`**: Holds reusable clients for external APIs. `github.ts` is a generic client for the GitHub API. -- **`core`**: Contains the application's core functionalities, such as the configurable `logger`. -- **`config`**: Manages all environment variables and application configuration. - ## Prerequisites - Node.js (v16 or higher) @@ -71,37 +25,9 @@ The project follows a modular, feature-based architecture. All source code is lo 3. Create a `.env` file in the root directory with the following content: ``` - # GitHub MCP SSE Server Configuration - # GitHub API Token (required for API access) - # Generate a token at https://github.com/settings/tokens GITHUB_TOKEN=your_github_token_here - - # Authentication API_KEY=your-secret-api-key - - # Server Port Configuration MCP_SSE_PORT=3200 - - # Timeout Configuration (in milliseconds) - MCP_TIMEOUT=180000 - - # Log Level (debug, info, warn, error) - LOG_LEVEL=info - - # CORS Configuration - CORS_ALLOW_ORIGIN=* - - # Multiplexing SSE Transport Configuration - # Set to 'true' to enable multiplexing SSE transport (handles multiple clients with a single transport) - # Set to 'false' to use individual SSE transport for each client (legacy behavior) - USE_MULTIPLEXING_SSE=false - - # Rate Limiting Configuration - RATE_LIMIT_WINDOW_MS=900000 # Time window for rate limiting in milliseconds (e.g., 900000 for 15 minutes) - RATE_LIMIT_MAX_REQUESTS=100 # Maximum number of requests allowed per window per IP - RATE_LIMIT_SSE_MAX=5 # Maximum number of SSE connections allowed per minute per IP - RATE_LIMIT_MESSAGES_MAX=30 # Maximum number of messages allowed per minute per IP - DEFAULT_USER_RATE_LIMIT=1000 # Default number of requests allowed per hour for a user ``` 4. Build the project: @@ -121,8 +47,6 @@ npm run start pnpm run start ``` -The server will start on the port specified in the `.env` file (default: 3200). If the port is in use, it will automatically find an available port. - ### Development Mode ```bash @@ -131,8 +55,6 @@ npm run dev pnpm run dev ``` -This will build the TypeScript code and start the server. - ### Docker You can also run the server using Docker. @@ -142,44 +64,19 @@ You can also run the server using Docker. ```bash docker build -t github-see-mcp-server . docker run -d -p 8080:8080 \ - -e USE_MULTIPLEXING_SSE="true" \ - -e MCP_TIMEOUT="1800000" \ - -e SSE_TIMEOUT="1800000" \ - -e LOG_LEVEL="info" \ - -e CORS_ALLOW_ORIGIN="*" \ - -e GITHUB_TOKEN="{YOUR GITHUB TOKEN}" \ - -e MCP_SSE_PORT="8080" \ - -e RATE_LIMIT_WINDOW_MS="900000" \ - -e RATE_LIMIT_MAX_REQUESTS="100" \ - -e RATE_LIMIT_SSE_MAX="5" \ - -e RATE_LIMIT_MESSAGES_MAX="30" \ - -e DEFAULT_USER_RATE_LIMIT="1000" \ - -e HSTS_MAX_AGE="31536000" \ - -e CSP_REPORT_ONLY="true" \ - -e CSP_REPORT_URI="https://apprecio.cl/csp-report" \ - -e NODE_ENV="production" \ - -e DISABLE_HSTS="false" \ - -e API_KEY="{YOUR AUTHORIZATION TOKEN}" \ --name github-see-mcp-server \ github-see-mcp-server ``` -This command: - -- Runs the container in detached mode (`-d`) -- Maps port 3200 on the host to port 3200 in the container -- Sets all the environment variables with their default values -- Names the container "github-see-mcp-server" - #### Using Docker Compose -For a more streamlined approach, you can use Docker Compose. Make sure you have a `.env` file created as described in the "Installation" section. +Make sure you have a `.env` file created as described in the "Installation" section. ```bash docker-compose up -d ``` -This command will build the image if it doesn't exist and start the container in the background. The configuration will be loaded from the `.env` file. To stop the service, run: +To stop the service, run: ```bash docker-compose down @@ -191,166 +88,30 @@ To connect to this MCP server with Claude, add the following configuration to yo ```json { - "mcpServers": { - "GitHub": { + "GITHUB":{ "command": "npx", "args": [ "-y", - "mcp-remote@0.1.15", + "mcp-remote@latest", "https://{Your domain}/sse", "--header", - "Authorization: Bearer {YOUR AUTHORIZATION TOKEN}", - "--transport", - "sse-only" - ] + "GITHUB_TOKEN:${GITHUB_TOKEN}" + ], + "env": { + "GITHUB_TOKEN": "here your github token" + } } - } } ``` -Replace `{Your domain}` with your actual domain where the server is running. - -## API Endpoints - -- `/health` - Health check endpoint that returns server status and version information -- `/mcp` - Modern MCP Streamable HTTP endpoint for efficient bidirectional communication -- `/sse` - Server-Sent Events endpoint for legacy clients (establishes SSE connection) -- `/messages` - Message endpoint for legacy SSE clients (for sending messages to the server) - -The server supports both modern and legacy communication methods: - -1. **Modern Streamable HTTP** (`/mcp`): Recommended for new implementations, providing efficient bidirectional communication -2. **Legacy SSE** (`/sse` and `/messages`): For backward compatibility with older clients - - **Individual SSE Transport**: Default mode where each client gets its own transport instance - - **Multiplexing SSE Transport**: Optional mode where multiple clients share a single transport instance for better resource efficiency - -### Multiplexing SSE Transport - -The multiplexing SSE transport is an advanced feature that allows the server to handle multiple client connections through a single transport instance. This provides several benefits: - -- **Resource Efficiency**: Reduces memory usage and connection overhead when handling multiple clients -- **Simplified Message Broadcasting**: Makes it easier to send messages to all connected clients -- **Better Connection Management**: Centralized handling of client connections and disconnections -- **Improved Scalability**: Better performance when dealing with many concurrent connections - -To enable multiplexing SSE transport, set `USE_MULTIPLEXING_SSE=true` in your `.env` file. - -When multiplexing is enabled: - -- All SSE clients connect through a shared transport instance -- Each client receives a unique session ID for message routing -- The server can efficiently broadcast messages to all clients or send targeted messages to specific clients -- Connection state is managed centrally for all clients - ## Available GitHub Tools -The server provides the following GitHub API tools: - -### Issues - -- `get_issue` - Get details of a specific issue -- `get_issue_comments` - Get comments for a GitHub issue -- `create_issue` - Create a new issue in a GitHub repository -- `add_issue_comment` - Add a comment to an issue -- `list_issues` - List and filter repository issues -- `update_issue` - Update an issue in a GitHub repository -- `search_issues` - Search for issues and pull requests - -### Pull Requests - -- `get_pull_request` - Get details of a specific pull request -- `list_pull_requests` - List and filter repository pull requests -- `merge_pull_request` - Merge a pull request -- `get_pull_request_files` - Get the list of files changed in a pull request -- `get_pull_request_status` - Get the combined status of all status checks for a pull request -- `update_pull_request_branch` - Update a pull request branch with the latest changes from the base branch -- `get_pull_request_comments` - Get the review comments on a pull request -- `get_pull_request_reviews` - Get the reviews on a pull request -- `create_pull_request_review` - Create a review on a pull request -- `create_pull_request` - Create a new pull request -- `add_pull_request_review_comment` - Add a review comment to a pull request -- `update_pull_request` - Update an existing pull request - -### Repositories - -- `create_file` - Create a single file in a repository -- `update_file` - Update a single file in a repository -- `list_branches` - List branches in a GitHub repository -- `push_files` - Push multiple files in a single commit -- `search_repositories` - Search for GitHub repositories -- `create_repository` - Create a new GitHub repository -- `get_repository_info` - Get information about a GitHub repository -- `get_user_repositories` - Get information about a GitHub user's repositories -- `get_file_contents` - Get the contents of a file in a GitHub repository -- `create_fork` - Create a fork of a GitHub repository -- `create_branch` - Create a new branch in a GitHub repository -- `get_branch_info` - Get information about a branch in a GitHub repository -- `list_commits` - Get a list of commits of a branch in a repository -- `get_commit` - Get details for a commit from a repository -- `get_specific_commit` - Get details for a specific commit from a repository - -### User - -- `get_me` - Get details of the authenticated user - -### Rate Limiting - -This server implements a robust rate limiting strategy to ensure fair usage and protect against abuse. The rate limiting is configured in `src/server.ts` and includes several layers of protection: - -- **General Limiter**: A global rate limit is applied to all incoming requests to prevent excessive traffic from a single IP address. -- **SSE Limiter**: A specific rate limit for Server-Sent Events (SSE) connections to manage real-time communication resources. -- **Message Limiter**: A rate limit on the number of messages that can be sent to the server to prevent spam and overload. -- **User-Specific Limiter**: A dynamic rate limit that can be customized for individual users, providing more flexible and granular control. -- **Critical Operations Limiter**: A stricter rate limit for critical operations such as creating repositories or merging pull requests to prevent accidental or malicious use of sensitive features. - -The rate limiting is implemented using the `express-rate-limit` library, which provides a flexible and easy-to-configure solution for Express-based applications. The configuration is managed through environment variables, allowing for easy adjustments without modifying the code. - -## Troubleshooting - -### Connection Issues - -If you're experiencing connection issues: - -1. Check that the GitHub token is valid and has the necessary permissions -2. Ensure the server is running and accessible -3. Check the server logs for any error messages -4. Verify that the client is connecting to the correct endpoint -5. Check if there are any network issues or firewalls blocking the connection - -### Timeout Errors - -If you're experiencing timeout errors: - -1. Increase the `MCP_TIMEOUT` value in the `.env` file -2. Check if the GitHub API is responding slowly -3. Verify that the client is not sending too many requests - -### Logging and Debugging - -The server supports different logging levels that can be set in the `.env` file: - -- `debug` - Verbose logging for detailed debugging -- `info` - Standard logging for general operation information (default) -- `warn` - Only warnings and errors -- `error` - Only error messages - -To enable more detailed logging for troubleshooting: - -``` -LOG_LEVEL=debug -``` - -This will provide more detailed information about requests, responses, and internal operations. - -### Multiplexing SSE Transport Issues - -If you're experiencing issues with the multiplexing SSE transport: +The server provides tools for managing: -1. **Check the configuration**: Ensure `USE_MULTIPLEXING_SSE` is set correctly in the `.env` file -2. **Enable debug logging**: Set `LOG_LEVEL=debug` to see detailed multiplexing operations -3. **Monitor client connections**: The logs will show when clients connect/disconnect from the multiplexing transport -4. **Verify message routing**: Debug logs will show how messages are routed between clients and the server -5. **Fall back to individual transport**: If issues persist, set `USE_MULTIPLEXING_SSE=false` to use the legacy behavior +- Issues +- Pull Requests +- Repositories +- Users ## License