diff --git a/README.md b/README.md index b4e68eb..9377717 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,23 @@ npm install INFLUXDB_TOKEN=your_token npm start ``` +You can also start the server with Streamable HTTP transport by providing the `--http` option with an optional port number (defaults to 3000). This mode uses an internal Express.js server: + +```bash +# Start with Streamable HTTP transport on default port 3000 +INFLUXDB_TOKEN=your_token npm start -- --http + +# Start with Streamable HTTP transport on a specific port +INFLUXDB_TOKEN=your_token npm start -- --http 8080 +``` + +If you installed globally or are using npx, you can run: +```bash +INFLUXDB_TOKEN=your_token influxdb-mcp-server --http +# or +INFLUXDB_TOKEN=your_token influxdb-mcp-server --http 8080 +``` + ## Integration with Claude for Desktop Add the server to your `claude_desktop_config.json`: diff --git a/package-lock.json b/package-lock.json index 3ee8a10..150dd8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", + "commander": "^14.0.0", + "express": "^5.1.0", "node-fetch": "^3.3.2", "zod": "^3.24.2" }, @@ -1515,16 +1517,16 @@ } }, "node_modules/body-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", - "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", - "iconv-lite": "^0.5.2", + "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", @@ -1534,44 +1536,6 @@ "node": ">=18" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1883,6 +1847,15 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2011,12 +1984,12 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2061,16 +2034,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2369,46 +2332,45 @@ } }, "node_modules/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", - "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.0.1", + "body-parser": "^2.2.0", "content-disposition": "^1.0.0", - "content-type": "~1.0.4", - "cookie": "0.7.1", + "content-type": "^1.0.5", + "cookie": "^0.7.1", "cookie-signature": "^1.2.1", - "debug": "4.3.6", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "^2.0.0", - "fresh": "2.0.0", - "http-errors": "2.0.0", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", - "methods": "~1.1.2", "mime-types": "^3.0.0", - "on-finished": "2.4.1", - "once": "1.4.0", - "parseurl": "~1.3.3", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "router": "^2.0.0", - "safe-buffer": "5.2.1", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", "send": "^1.1.0", - "serve-static": "^2.1.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "^2.0.0", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -2529,29 +2491,6 @@ "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2833,12 +2772,12 @@ } }, "node_modules/iconv-lite": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -3908,15 +3847,6 @@ "dev": true, "license": "MIT" }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3932,21 +3862,21 @@ } }, "node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "^1.53.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -3983,9 +3913,9 @@ "license": "MIT" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/nan": { @@ -4434,12 +4364,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -4472,18 +4402,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -4571,11 +4489,13 @@ } }, "node_modules/router": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", - "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" @@ -4621,19 +4541,18 @@ } }, "node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", - "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", - "fresh": "^0.5.2", + "fresh": "^2.0.0", "http-errors": "^2.0.0", - "mime-types": "^2.1.35", + "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", @@ -4643,52 +4562,16 @@ "node": ">= 18" } }, - "node_modules/send/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/serve-static": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", - "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", - "send": "^1.0.0" + "send": "^1.2.0" }, "engines": { "node": ">= 18" @@ -5172,9 +5055,9 @@ } }, "node_modules/type-is": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", - "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -5253,15 +5136,6 @@ "dev": true, "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", diff --git a/package.json b/package.json index 80b25a4..38ad37e 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "homepage": "https://github.com/idoru/influxdb-mcp-server#readme", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", + "commander": "^14.0.0", + "express": "^5.1.0", "node-fetch": "^3.3.2", "zod": "^3.24.2" }, diff --git a/src/index.js b/src/index.js index 8e5cfb0..aad1aa9 100755 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,10 @@ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; +import { program } from "commander"; +import express from "express"; // Import config import { validateEnvironment } from "./config/env.js"; @@ -30,82 +33,91 @@ import { lineProtocolGuidePrompt } from "./prompts/lineProtocolGuidePrompt.js"; configureLogger(); validateEnvironment(); -// Create MCP server -const server = new McpServer({ - name: "InfluxDB", - version: "0.1.1", -}); +// Parse command-line arguments +program + .option("--http [port]", "Start server with Streamable HTTP transport on specified port (default: 3000)") + .parse(process.argv); + +const options = program.opts(); + +// Function to create and configure a new MCP server instance +const createMcpServer = () => { + const server = new McpServer({ + name: "InfluxDB", + version: "0.1.1", + }); + + // Register resources + server.resource("orgs", "influxdb://orgs", listOrganizations); + server.resource("buckets", "influxdb://buckets", listBuckets); + server.resource( + "bucket-measurements", + new ResourceTemplate("influxdb://bucket/{bucketName}/measurements", { + list: undefined, + }), + bucketMeasurements, + ); + server.resource( + "query", + new ResourceTemplate("influxdb://query/{orgName}/{fluxQuery}", { + list: undefined, + }), + executeQuery, + ); + + // Register tools + server.tool( + "write-data", + { + org: z.string().describe("The organization name"), + bucket: z.string().describe("The bucket name"), + data: z.string().describe("Data in InfluxDB line protocol format"), + precision: z.enum(["ns", "us", "ms", "s"]).optional().describe( + "Timestamp precision (ns, us, ms, s)", + ), + }, + writeData, + ); + server.tool( + "query-data", + { + org: z.string().describe("The organization name"), + query: z.string().describe("Flux query string"), + }, + queryData, + ); + server.tool( + "create-bucket", + { + name: z.string().describe("The bucket name"), + orgID: z.string().describe("The organization ID"), + retentionPeriodSeconds: z.number().optional().describe( + "Retention period in seconds (optional)", + ), + }, + createBucket, + ); + server.tool( + "create-org", + { + name: z.string().describe("The organization name"), + description: z.string().optional().describe( + "Organization description (optional)", + ), + }, + createOrg, + ); + + // Register prompts + server.prompt("flux-query-examples", {}, fluxQueryExamplesPrompt); + server.prompt("line-protocol-guide", {}, lineProtocolGuidePrompt); + + return server; +}; + +// Create MCP server for stdio or as a template for HTTP +const globalServer = createMcpServer(); -// Register resources -server.resource("orgs", "influxdb://orgs", listOrganizations); - -server.resource("buckets", "influxdb://buckets", listBuckets); - -server.resource( - "bucket-measurements", - new ResourceTemplate("influxdb://bucket/{bucketName}/measurements", { - list: undefined, - }), - bucketMeasurements, -); - -server.resource( - "query", - new ResourceTemplate("influxdb://query/{orgName}/{fluxQuery}", { - list: undefined, - }), - executeQuery, -); - -// Register tools -server.tool( - "write-data", - { - org: z.string().describe("The organization name"), - bucket: z.string().describe("The bucket name"), - data: z.string().describe("Data in InfluxDB line protocol format"), - precision: z.enum(["ns", "us", "ms", "s"]).optional().describe( - "Timestamp precision (ns, us, ms, s)", - ), - }, - writeData, -); - -server.tool( - "query-data", - { - org: z.string().describe("The organization name"), - query: z.string().describe("Flux query string"), - }, - queryData, -); - -server.tool( - "create-bucket", - { - name: z.string().describe("The bucket name"), - orgID: z.string().describe("The organization ID"), - retentionPeriodSeconds: z.number().optional().describe( - "Retention period in seconds (optional)", - ), - }, - createBucket, -); - -server.tool( - "create-org", - { - name: z.string().describe("The organization name"), - description: z.string().optional().describe( - "Organization description (optional)", - ), - }, - createOrg, -); - -// Register prompts -server.prompt("flux-query-examples", {}, fluxQueryExamplesPrompt); -server.prompt("line-protocol-guide", {}, lineProtocolGuidePrompt); // Add a global error handler process.on("unhandledRejection", (reason, promise) => { @@ -126,32 +138,12 @@ function logMcpError(...args) { originalConsoleError("[MCP-ERROR]", ...args); } -// Start the server with stdio transport -console.log("Starting MCP server with stdio transport..."); -const transport = new StdioServerTransport(); - -// Add extra debugging to the transport -if (transport._send) { - const originalSend = transport._send; - transport._send = function (data) { - logMcpDebug("SENDING:", JSON.stringify(data)); - return originalSend.call(this, data); - }; -} - -// If the transport has a _receive method, wrap it for debugging -if (transport._receive) { - const originalReceive = transport._receive; - transport._receive = function (data) { - logMcpDebug("RECEIVED:", JSON.stringify(data)); - return originalReceive.call(this, data); - }; -} - // Enable extra protocol tracing for all requests/responses -if (server.server) { - const originalOnMessage = server.server.onmessage; - server.server.onmessage = function (message) { +// This debugging for globalServer.server is primarily for Stdio mode or if a global server instance were used. +// For HTTP mode, per-request server instances are created. +if (globalServer.server && !options.http) { // Only apply this if not in HTTP mode, or adjust as needed + const originalOnMessage = globalServer.server.onmessage; + globalServer.server.onmessage = function (message) { logMcpDebug("SERVER RECEIVED MESSAGE:", JSON.stringify(message)); if (originalOnMessage) { return originalOnMessage.call(this, message); @@ -159,102 +151,224 @@ if (server.server) { }; // Log server responses - const originalSendResponse = server.server._sendResponse; + const originalSendResponse = globalServer.server._sendResponse; if (originalSendResponse) { - server.server._sendResponse = function (id, result) { + globalServer.server._sendResponse = function (id, result) { logMcpDebug("SERVER SENDING RESPONSE:", JSON.stringify({ id, result })); return originalSendResponse.call(this, id, result); }; } // Log server errors - const originalSendError = server.server._sendError; + const originalSendError = globalServer.server._sendError; if (originalSendError) { - server.server._sendError = function (id, error) { + globalServer.server._sendError = function (id, error) { logMcpDebug("SERVER SENDING ERROR:", JSON.stringify({ id, error })); return originalSendError.call(this, id, error); }; } } -// Override transport callbacks for debugging -const originalOnMessage = transport.onmessage; -transport.onmessage = function (message) { - logMcpDebug("MESSAGE RECEIVED:", JSON.stringify(message)); - if (originalOnMessage) { - return originalOnMessage.call(this, message); +// The rest of the debugging and connection logic will be handled differently +// for StdioServerTransport vs StreamableHTTPServerTransport. +// This will be addressed in the next step when setting up the Express server. + +if (!options.http) { + // Start the server with stdio transport + console.log("Starting MCP server with stdio transport..."); + const stdioTransport = new StdioServerTransport(); + + // Add extra debugging to the stdioTransport + if (stdioTransport._send) { + const originalSend = stdioTransport._send; + stdioTransport._send = function (data) { + logMcpDebug("STDIO SENDING:", JSON.stringify(data)); + return originalSend.call(this, data); + }; } -}; -// Check if we're in test mode -const isTestMode = process.env.MCP_TEST_MODE === "true"; -if (isTestMode) { - console.log("Running in test mode with enhanced protocol debugging"); + if (stdioTransport._receive) { + const originalReceive = stdioTransport._receive; + stdioTransport._receive = function (data) { + logMcpDebug("STDIO RECEIVED:", JSON.stringify(data)); + return originalReceive.call(this, data); + }; + } - // Add debugging for server methods - const originalConnect = server.connect; - server.connect = async function (transport) { - logMcpDebug("Server.connect() called"); - try { - const result = await originalConnect.call(this, transport); - logMcpDebug("Server.connect() succeeded"); - return result; - } catch (err) { - logMcpError("Server.connect() failed:", err); - throw err; + const originalStdioOnMessageCallback = stdioTransport.onmessage; + stdioTransport.onmessage = function (message) { + logMcpDebug("MESSAGE RECEIVED VIA STDIO:", JSON.stringify(message)); + if (originalStdioOnMessageCallback) { + return originalStdioOnMessageCallback.call(this, message); } }; -} -// Create a function to handle connection -const connectServer = async () => { - try { - console.log("Connecting server to transport..."); - await server.connect(transport); - console.log("Server successfully connected to transport"); - - // In test mode, perform extra validation - if (isTestMode) { - // Track heartbeat interval in global so it can be cleaned up - if (!global.mcpHeartbeatInterval) { - // Add a heartbeat timer to keep the connection alive during tests - global.mcpHeartbeatInterval = setInterval(() => { - // Only log if we're not in cleanup mode - if (!global.testCleanupInProgress) { - console.log("[Heartbeat] MCP server is still running..."); - } - }, 3000); - - // Clean up the interval if the process exits - process.on("exit", () => { - if (global.mcpHeartbeatInterval) { - clearInterval(global.mcpHeartbeatInterval); - global.mcpHeartbeatInterval = null; - } - }); + // Check if we're in test mode + const isTestMode = process.env.MCP_TEST_MODE === "true"; + if (isTestMode) { + console.log("Running in test mode with enhanced protocol debugging for STDIO"); + + // Add debugging for server methods + const originalConnect = globalServer.connect; + globalServer.connect = async function (transportInstance) { + logMcpDebug("GlobalServer.connect() called with stdio transport"); + try { + const result = await originalConnect.call(this, transportInstance); + logMcpDebug("GlobalServer.connect() with stdio succeeded"); + return result; + } catch (err) { + logMcpError("GlobalServer.connect() with stdio failed:", err); + throw err; } + }; + } - // Add extra hooks for protocol debugging - server.server.onclose = () => { - logMcpError("SERVER CONNECTION CLOSED"); - // Clear interval on connection close - if (global.mcpHeartbeatInterval) { - clearInterval(global.mcpHeartbeatInterval); - global.mcpHeartbeatInterval = null; + // Create a function to handle connection for stdio + const connectStdioServer = async () => { + try { + console.log("Connecting global server to stdio transport..."); + await globalServer.connect(stdioTransport); // Use stdioTransport here + console.log("Global server successfully connected to stdio transport"); + + if (isTestMode) { + if (!global.mcpHeartbeatInterval) { + global.mcpHeartbeatInterval = setInterval(() => { + if (!global.testCleanupInProgress) { + console.log("[Heartbeat] MCP server (stdio) is still running..."); + } + }, 3000); + process.on("exit", () => { + if (global.mcpHeartbeatInterval) { + clearInterval(global.mcpHeartbeatInterval); + global.mcpHeartbeatInterval = null; + } + }); } - }; - - server.server.onerror = (err) => { - logMcpError("SERVER ERROR:", err); - }; + if (globalServer.server) { + globalServer.server.onclose = () => { + logMcpError("STDIO SERVER CONNECTION CLOSED"); + if (global.mcpHeartbeatInterval) { + clearInterval(global.mcpHeartbeatInterval); + global.mcpHeartbeatInterval = null; + } + }; + globalServer.server.onerror = (err) => { + logMcpError("STDIO SERVER ERROR:", err); + }; + } + } + } catch (err) { + console.error("Error starting MCP server with stdio:", err); + process.exit(1); } - } catch (err) { - console.error("Error starting MCP server:", err); - process.exit(1); - } -}; + }; -// Connect with a small delay to ensure proper initialization -setTimeout(() => { - connectServer(); -}, 200); + setTimeout(() => { + connectStdioServer(); + }, 200); +} else { + // Start the server with Streamable HTTP transport + const app = express(); + app.use(express.json()); + + const port = typeof options.http === 'string' ? parseInt(options.http, 10) : 3000; + + app.post('/mcp', async (req, res) => { + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. + logMcpDebug("HTTP POST /mcp received, creating new server and transport."); + let server; + let transport; + try { + server = createMcpServer(); + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, // Stateless + }); + + // Attach logger to the specific transport instance + if (transport._send) { + const originalSend = transport._send; + transport._send = function (data) { + logMcpDebug("HTTP SENDING:", JSON.stringify(data)); + return originalSend.call(this, data); + }; + } + if (transport._receive) { + const originalReceive = transport._receive; + transport._receive = function (data) { + logMcpDebug("HTTP RECEIVED:", JSON.stringify(data)); + return originalReceive.call(this, data); + }; + } + const originalOnMessageCallback = transport.onmessage; + transport.onmessage = function (message) { + logMcpDebug("HTTP MESSAGE RECEIVED:", JSON.stringify(message)); + if (originalOnMessageCallback) { + return originalOnMessageCallback.call(this, message); + } + }; + + + res.on('close', () => { + logMcpDebug('HTTP POST /mcp request closed, cleaning up server and transport.'); + if (transport) transport.close(); + if (server) server.close(); + }); + + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + logMcpError('Error handling MCP HTTP request:', error); + if (server) server.close(); // Ensure server is closed on error + if (transport) transport.close(); // Ensure transport is closed on error + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: req.body?.id || null, + }); + } + } + }); + + app.get('/mcp', async (req, res) => { + logMcpDebug('Received GET /mcp request'); + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Method not allowed for stateless transport." + }, + id: null + })); + }); + + app.delete('/mcp', async (req, res) => { + logMcpDebug('Received DELETE /mcp request'); + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Method not allowed for stateless transport." + }, + id: null + })); + }); + + const httpServer = app.listen(port, () => { + console.log(`MCP Streamable HTTP Server listening on port ${port}`); + }); + + httpServer.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`Error: Port ${port} is already in use. Please choose a different port or free up port ${port}.`); + process.exit(1); + } else { + console.error('Failed to start HTTP server:', err); + process.exit(1); + } + }); +}