From ae451bf8142f0752d3b3e164aa39153ee9cb0cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cmkczarkowski=E2=80=9D?= Date: Wed, 22 Oct 2025 14:38:06 +0200 Subject: [PATCH 1/2] feat: add rate limiting --- mcp-server/package-lock.json | 571 ++++++++++++++++++++++++----------- mcp-server/package.json | 2 +- mcp-server/src/index.ts | 92 +++++- mcp-server/src/rate-limit.ts | 177 +++++++++++ mcp-server/wrangler.jsonc | 15 +- 5 files changed, 670 insertions(+), 187 deletions(-) create mode 100644 mcp-server/src/rate-limit.ts diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index 11b78c7..b728104 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -1,15 +1,15 @@ { - "name": "remote-mcp-server-authless", - "version": "0.0.0", + "name": "10x-rules-mcp-server", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "remote-mcp-server-authless", - "version": "0.0.0", + "name": "10x-rules-mcp-server", + "version": "0.0.1", "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", - "agents": "^0.0.65", + "agents": "^0.2.14", "zod": "^3.24.2" }, "devDependencies": { @@ -18,92 +18,104 @@ "wrangler": "^4.13.2" } }, - "node_modules/@ai-sdk/provider": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", - "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "node_modules/@ai-sdk/gateway": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.39.tgz", + "integrity": "sha512-ijYCKG2sbn2RBVfIgaXNXvzHAf2HpFXxQODtjMI+T7Z4CLryflytchsZZ9qrGtsjiQVopKOV6m6kj4lq5fnbsg==", "license": "Apache-2.0", "dependencies": { - "json-schema": "^0.4.0" + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.12", + "@vercel/oidc": "3.0.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/provider-utils": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.7.tgz", - "integrity": "sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==", + "node_modules/@ai-sdk/openai": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.48.tgz", + "integrity": "sha512-dIGOVtHaScTNIQzxkE4I8T5PpoutFWxonR/awdRz+5sCpoO7V2kVL44+X6piJbQIMdFYUK/h+HTX3+BjTbRHmw==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.3", - "nanoid": "^3.3.8", - "secure-json-parse": "^2.7.0" + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.12" }, "engines": { "node": ">=18" }, "peerDependencies": { - "zod": "^3.23.8" + "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/provider-utils/node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=18" } }, - "node_modules/@ai-sdk/react": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.9.tgz", - "integrity": "sha512-/VYm8xifyngaqFDLXACk/1czDRCefNCdALUyp+kIX6DUIYUWTM93ISoZ+qJ8+3E+FiJAKBQz61o8lIIl+vYtzg==", + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.12.tgz", + "integrity": "sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider-utils": "2.2.7", - "@ai-sdk/ui-utils": "1.2.8", - "swr": "^2.2.5", - "throttleit": "2.1.0" + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.5" }, "engines": { "node": ">=18" }, "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } + "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/ui-utils": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.8.tgz", - "integrity": "sha512-nls/IJCY+ks3Uj6G/agNhXqQeLVqhNfoJbuNgCny+nX2veY5ADB91EcZUqVeQ/ionul2SeUswPY6Q/DxteY29Q==", - "license": "Apache-2.0", + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "license": "MIT", "dependencies": { - "@ai-sdk/provider": "1.1.3", - "@ai-sdk/provider-utils": "2.2.7", - "zod-to-json-schema": "^3.24.1" + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" }, "engines": { - "node": ">=18" + "node": ">= 16" }, - "peerDependencies": { - "zod": "^3.23.8" + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.43.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -1093,16 +1105,24 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", - "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", + "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==", "license": "MIT", "dependencies": { + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -1123,12 +1143,33 @@ "node": ">=8.0.0" } }, - "node_modules/@types/diff-match-patch": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", - "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@vercel/oidc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.2.tgz", + "integrity": "sha512-JekxQ0RApo4gS4un/iMGsIL1/k4KUBe3HmnGcDvzHuFBdQdudEJgTqcsJC7y6Ul4Yw5CeykgvQbX2XeEJd0+DA==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1166,49 +1207,78 @@ } }, "node_modules/agents": { - "version": "0.0.65", - "resolved": "https://registry.npmjs.org/agents/-/agents-0.0.65.tgz", - "integrity": "sha512-RcWgOVjIn0W+gMpgaMyfQCFnEaniyxw1gtpIrb8BL6RTpSNAnCH/6SGK0mPxpVOtJs6FCtpGFbZ4WFnrTL+ZFA==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/agents/-/agents-0.2.14.tgz", + "integrity": "sha512-DjMZomVmB11DCBglkeVyeHhb9FpdEfdtXT6+ZPpda7HT0yQxz1zDu6scKSpyodTu1sS7O3tA7xo4F1mvW+7M+Q==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.10.2", - "ai": "^4.3.9", + "@ai-sdk/openai": "2.0.48", + "@modelcontextprotocol/sdk": "^1.20.0", + "ai": "5.0.68", "cron-schedule": "^5.0.4", - "nanoid": "^5.1.5", - "partyserver": "^0.0.67", - "partysocket": "1.1.3", - "zod": "^3.24.3" + "json-schema": "^0.4.0", + "json-schema-to-typescript": "^15.0.4", + "mimetext": "^3.0.27", + "nanoid": "^5.1.6", + "partyserver": "^0.0.75", + "partysocket": "1.1.6", + "zod": "^3.25.76", + "zod-to-ts": "^1.2.0" }, "peerDependencies": { - "react": "*" + "react": "*", + "viem": ">=2.0.0", + "x402": "^0.6.5" + }, + "peerDependenciesMeta": { + "viem": { + "optional": true + }, + "x402": { + "optional": true + } } }, "node_modules/ai": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.9.tgz", - "integrity": "sha512-P2RpV65sWIPdUlA4f1pcJ11pB0N1YmqPVLEmC4j8WuBwKY0L3q9vGhYPh0Iv+spKHKyn0wUbMfas+7Z6nTfS0g==", + "version": "5.0.68", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.68.tgz", + "integrity": "sha512-SB6r+4TkKVlSg2ozGBSfuf6Is5hrcX/bpGBzOoyHIN3b4ILGhaly0IHEvP8+3GGIHXqtkPVEUmR6V05jKdjNlg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.3", - "@ai-sdk/provider-utils": "2.2.7", - "@ai-sdk/react": "1.2.9", - "@ai-sdk/ui-utils": "1.2.8", - "@opentelemetry/api": "1.9.0", - "jsondiffpatch": "0.6.0" + "@ai-sdk/gateway": "1.0.39", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.12", + "@opentelemetry/api": "1.9.0" }, "engines": { "node": ">=18" }, "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "zod": "^3.23.8" + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "peerDependenciesMeta": { - "react": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/as-table": { "version": "1.0.55", "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", @@ -1284,18 +1354,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1384,6 +1442,17 @@ "node": ">=6.6.0" } }, + "node_modules/core-js-pure": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz", + "integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1460,15 +1529,6 @@ "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -1480,12 +1540,6 @@ "node": ">=8" } }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", - "license": "Apache-2.0" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1620,9 +1674,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -1705,6 +1759,35 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -1906,6 +1989,27 @@ "license": "MIT", "optional": true }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -1918,29 +2022,65 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, - "node_modules/jsondiffpatch": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", - "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", "license": "MIT", "dependencies": { - "@types/diff-match-patch": "^1.0.36", - "chalk": "^5.3.0", - "diff-match-patch": "^1.0.5" + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" }, "bin": { - "jsondiffpatch": "bin/jsondiffpatch.js" + "json2ts": "dist/src/cli.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=16.0.0" } }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2005,6 +2145,43 @@ "node": ">= 0.6" } }, + "node_modules/mimetext": { + "version": "3.0.27", + "resolved": "https://registry.npmjs.org/mimetext/-/mimetext-3.0.27.tgz", + "integrity": "sha512-mUhWAsZD1N/K6dbN4+a5Yq78OPnYQw1ubOSMasBntsLQ2S7KVNlvDEA8dwpr4a7PszWMzeslKahAprtwYMgaBA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@babel/runtime-corejs3": "^7.26.0", + "js-base64": "^3.7.7", + "mime-types": "^2.1.35" + }, + "funding": { + "type": "patreon", + "url": "https://patreon.com/muratgozel" + } + }, + "node_modules/mimetext/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/mimetext/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/miniflare": { "version": "4.20250424.1", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250424.1.tgz", @@ -2041,6 +2218,15 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2058,9 +2244,9 @@ } }, "node_modules/nanoid": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", - "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", "funding": [ { "type": "github", @@ -2143,22 +2329,22 @@ } }, "node_modules/partyserver": { - "version": "0.0.67", - "resolved": "https://registry.npmjs.org/partyserver/-/partyserver-0.0.67.tgz", - "integrity": "sha512-GQ0fjJ7n5r5LrsFHFUkGR3Bd50YdBZaDNjvRTi8PowZgI5fvCFliT/XdDpRVuwfDDk0jb9es2cXcaAh1z5GsLA==", + "version": "0.0.75", + "resolved": "https://registry.npmjs.org/partyserver/-/partyserver-0.0.75.tgz", + "integrity": "sha512-i/18vvdxuGjx+rpQ+fDdExlvQoRb7EfTF+6b+kA2ILEpHemtpLWV8NdgDrOPEklRNdCc/4WlzDtYn05d17aZAQ==", "license": "ISC", "dependencies": { - "nanoid": "^5.1.5" + "nanoid": "^5.1.6" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240729.0" } }, "node_modules/partysocket": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.3.tgz", - "integrity": "sha512-87Jd/nqPoWnVfzHE6Z12WLWTJ+TAgxs0b7i2S163HfQSrVDUK5tW/FC64T5N8L5ss+gqF+EV0BwjZMWggMY3UA==", - "license": "ISC", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.6.tgz", + "integrity": "sha512-LkEk8N9hMDDsDT0iDK0zuwUDFVrVMUXFXCeN3850Ng8wtjPqPBeJlwdeY6ROlJSEh3tPoTTasXoSBYH76y118w==", + "license": "MIT", "dependencies": { "event-target-polyfill": "^0.0.4" } @@ -2188,6 +2374,18 @@ "dev": true, "license": "MIT" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", @@ -2197,6 +2395,21 @@ "node": ">=16.20.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", @@ -2217,6 +2430,15 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -2257,9 +2479,9 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "peer": true, "engines": { @@ -2308,12 +2530,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "license": "BSD-3-Clause" - }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -2557,29 +2773,20 @@ "npm": ">=6" } }, - "node_modules/swr": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.3.tgz", - "integrity": "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.4.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "license": "MIT", "engines": { - "node": ">=18" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/toidentifier": { @@ -2617,7 +2824,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2670,13 +2876,13 @@ "node": ">= 0.8" } }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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/vary": { @@ -2808,9 +3014,9 @@ } }, "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -2824,6 +3030,15 @@ "peerDependencies": { "zod": "^3.24.1" } + }, + "node_modules/zod-to-ts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zod-to-ts/-/zod-to-ts-1.2.0.tgz", + "integrity": "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==", + "peerDependencies": { + "typescript": "^4.9.4 || ^5.0.2", + "zod": "^3" + } } } } diff --git a/mcp-server/package.json b/mcp-server/package.json index 055c0f3..90a5c7a 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", - "agents": "^0.0.65", + "agents": "^0.2.14", "zod": "^3.24.2" } } diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index a216e91..326f7c8 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -2,8 +2,15 @@ import { McpAgent } from "agents/mcp"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { listAvailableRulesTool, getRuleContentTool } from "./tools/rulesTools"; import { z } from 'zod'; +import { + getClientIP, + checkRateLimit, + incrementRateLimit, + createRateLimitResponse, + getRateLimitHeaders +} from "./rate-limit"; -// Define our MCP agent with tools +// Define our MCP agent with tools (no DO-level rate limiting) export class MyMCP extends McpAgent { server = new McpServer({ name: "MCP Rules Server", @@ -40,15 +47,96 @@ export class MyMCP extends McpAgent { } } +// Utility function to check if Accept header includes SSE +function acceptsSSE(acceptHeader: string | null): boolean { + if (!acceptHeader) return true; // Backwards compatibility + + const types = acceptHeader.split(',').map(t => t.trim().split(';')[0]); + return types.some(type => + type === 'text/event-stream' || + type === 'text/*' || + type === '*/*' + ); +} + // Define more specific types for Env and ExecutionContext if known for the environment // Example for Cloudflare Workers: // interface Env { /* ... bindings ... */ } // interface ExecutionContext { waitUntil(promise: Promise): void; passThroughOnException(): void; } export default { - fetch(request: Request, env: Env, ctx: ExecutionContext) { + async fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url); + // Health check endpoint - lightweight, no DO creation, no rate limiting + if (url.pathname === "/health") { + return new Response(JSON.stringify({ + status: "healthy", + version: "1.0.0", + timestamp: new Date().toISOString(), + service: "10x-rules-mcp-server" + }), { + status: 200, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache" + } + }); + } + + // Global IP-based rate limiting for SSE and MCP endpoints + // This runs BEFORE DO routing to prevent DO creation abuse + if (url.pathname === "/sse" || url.pathname === "/sse/message" || url.pathname === "/mcp") { + // Extract client IP + const clientIP = getClientIP(request); + + // Check if KV is available (may not be in local dev) + // @ts-expect-error - env.RATE_LIMIT_KV exists but not typed + if (env.RATE_LIMIT_KV) { + // Check rate limit (fail open if KV unavailable) + try { + // @ts-expect-error - env.RATE_LIMIT_KV exists but not typed + const rateLimitResult = await checkRateLimit(env.RATE_LIMIT_KV, clientIP); + + if (!rateLimitResult.allowed) { + // Rate limit exceeded - return 429 + return createRateLimitResponse(rateLimitResult); + } + + // Rate limit passed - increment counter + // @ts-expect-error - env.RATE_LIMIT_KV exists but not typed + await incrementRateLimit(env.RATE_LIMIT_KV, clientIP, rateLimitResult.resetTime); + + // Add rate limit headers to context for later use + // (We can't add them to SSE responses directly without breaking the stream) + ctx.rateLimitHeaders = getRateLimitHeaders(rateLimitResult); + } catch (error) { + // Rate limit check failed - log and allow request (fail open) + console.error("Rate limit check failed:", error); + } + } else { + console.warn("Rate limiting disabled: KV namespace not available"); + } + } + + // Accept header validation for SSE endpoints if (url.pathname === "/sse" || url.pathname === "/sse/message") { + const accept = request.headers.get("accept"); + + if (!acceptsSSE(accept)) { + return new Response(JSON.stringify({ + error: "Not Acceptable", + message: "This endpoint requires 'Accept: text/event-stream' header", + hint: "Use a Server-Sent Events compatible client", + received: accept || "(none)" + }), { + status: 406, + headers: { + "Content-Type": "application/json", + "X-Expected-Accept": "text/event-stream" + } + }); + } + // @ts-expect-error - env is not typed return MyMCP.serveSSE("/sse").fetch(request, env, ctx); } diff --git a/mcp-server/src/rate-limit.ts b/mcp-server/src/rate-limit.ts new file mode 100644 index 0000000..384cb25 --- /dev/null +++ b/mcp-server/src/rate-limit.ts @@ -0,0 +1,177 @@ +// Global IP-based rate limiting using Cloudflare KV +// Phase 2 implementation + +// Configuration +export const RATE_LIMIT_WINDOW_SECONDS = 60; +export const RATE_LIMIT_MAX_REQUESTS = 10; +export const RATE_LIMIT_KEY_PREFIX = "ratelimit:"; + +// Rate limit result interface +export interface RateLimitResult { + allowed: boolean; + limit: number; + remaining: number; + resetTime: number; // Unix timestamp in seconds +} + +/** + * Extract client IP address from request + * Uses cf-connecting-ip header (provided by Cloudflare) + * Falls back to 127.0.0.1 for localhost testing + */ +export function getClientIP(request: Request): string { + const ip = request.headers.get("cf-connecting-ip"); + + // For localhost testing, use a default IP + if (!ip || ip === "localhost") { + return "127.0.0.1"; + } + + return ip; +} + +/** + * Check rate limit for an IP address + * Returns the current count and whether the request should be allowed + * + * Uses simple counter with TTL approach: + * - Key: ratelimit:${ip} + * - Value: count (string) + * - TTL: 60 seconds (auto-expires) + * + * @param kv - KV namespace binding + * @param ip - Client IP address + * @returns RateLimitResult with current rate limit state + */ +export async function checkRateLimit( + kv: KVNamespace, + ip: string +): Promise { + const key = `${RATE_LIMIT_KEY_PREFIX}${ip}`; + + try { + // Get current count from KV + const value = await kv.get(key); + const currentCount = value ? parseInt(value, 10) : 0; + + // Check if over limit + if (currentCount >= RATE_LIMIT_MAX_REQUESTS) { + // Get metadata to determine reset time + const metadata = await kv.getWithMetadata(key); + const resetTime = metadata.metadata?.resetTime as number || + Math.floor(Date.now() / 1000) + RATE_LIMIT_WINDOW_SECONDS; + + return { + allowed: false, + limit: RATE_LIMIT_MAX_REQUESTS, + remaining: 0, + resetTime + }; + } + + // Calculate remaining requests + const remaining = RATE_LIMIT_MAX_REQUESTS - currentCount - 1; + const resetTime = Math.floor(Date.now() / 1000) + RATE_LIMIT_WINDOW_SECONDS; + + return { + allowed: true, + limit: RATE_LIMIT_MAX_REQUESTS, + remaining: remaining < 0 ? 0 : remaining, + resetTime + }; + } catch (error) { + // Fail open: if KV is unavailable, allow the request + console.error("Rate limit check failed:", error); + return { + allowed: true, + limit: RATE_LIMIT_MAX_REQUESTS, + remaining: RATE_LIMIT_MAX_REQUESTS - 1, + resetTime: Math.floor(Date.now() / 1000) + RATE_LIMIT_WINDOW_SECONDS + }; + } +} + +/** + * Increment rate limit counter for an IP address + * Creates new counter if it doesn't exist, increments if it does + * Sets TTL to auto-expire after the rate limit window + * + * @param kv - KV namespace binding + * @param ip - Client IP address + * @param resetTime - Unix timestamp when the rate limit resets + */ +export async function incrementRateLimit( + kv: KVNamespace, + ip: string, + resetTime: number +): Promise { + const key = `${RATE_LIMIT_KEY_PREFIX}${ip}`; + + try { + // Get current count + const value = await kv.get(key); + const currentCount = value ? parseInt(value, 10) : 0; + const newCount = currentCount + 1; + + // Store with TTL and metadata + await kv.put( + key, + newCount.toString(), + { + expirationTtl: RATE_LIMIT_WINDOW_SECONDS, + metadata: { resetTime } + } + ); + } catch (error) { + // Log error but don't fail the request + console.error("Failed to increment rate limit:", error); + } +} + +/** + * Create rate limit headers for response + * Follows standard rate limiting header conventions: + * - X-RateLimit-Limit: Maximum requests allowed + * - X-RateLimit-Remaining: Remaining requests in current window + * - X-RateLimit-Reset: Unix timestamp when limit resets + * + * @param result - Rate limit result + * @returns Object with header key-value pairs + */ +export function getRateLimitHeaders(result: RateLimitResult): Record { + return { + "X-RateLimit-Limit": result.limit.toString(), + "X-RateLimit-Remaining": result.remaining.toString(), + "X-RateLimit-Reset": result.resetTime.toString() + }; +} + +/** + * Create 429 Too Many Requests response + * Includes: + * - Retry-After header (seconds until reset) + * - X-RateLimit-* headers + * - JSON error body with details + * + * @param result - Rate limit result + * @returns Response with 429 status + */ +export function createRateLimitResponse(result: RateLimitResult): Response { + const now = Math.floor(Date.now() / 1000); + const retryAfter = result.resetTime - now; + + return new Response(JSON.stringify({ + error: "Too Many Requests", + message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`, + limit: result.limit, + window: `${RATE_LIMIT_WINDOW_SECONDS} seconds`, + resetTime: result.resetTime + }), { + status: 429, + headers: { + "Content-Type": "application/json", + "Retry-After": retryAfter.toString(), + ...getRateLimitHeaders(result) + } + }); +} diff --git a/mcp-server/wrangler.jsonc b/mcp-server/wrangler.jsonc index 165a08f..563d70a 100644 --- a/mcp-server/wrangler.jsonc +++ b/mcp-server/wrangler.jsonc @@ -28,20 +28,25 @@ }, "observability": { "enabled": true - } + }, + "kv_namespaces": [ + { + "binding": "RATE_LIMIT_KV", + "id": "8406622125054f128b423970d737d512", + "remote": true + } + ] /** * Smart Placement * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement */ // "placement": { "mode": "smart" }, - /** * Bindings * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including * databases, object storage, AI inference, real-time communication and more. * https://developers.cloudflare.com/workers/runtime-apis/bindings/ */ - /** * Environment Variables * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables @@ -51,16 +56,14 @@ * Note: Use secrets to store sensitive data. * https://developers.cloudflare.com/workers/configuration/secrets/ */ - /** * Static Assets * https://developers.cloudflare.com/workers/static-assets/binding/ */ // "assets": { "directory": "./public/", "binding": "ASSETS" }, - /** * Service Bindings (communicate between multiple Workers) * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings */ // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] -} +} \ No newline at end of file From 8ec64cf978a4ccf3f2ab4d43aa0976dfee83cd95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cmkczarkowski=E2=80=9D?= Date: Wed, 22 Oct 2025 14:56:36 +0200 Subject: [PATCH 2/2] feat: merge with master changes --- mcp-server/src/index.ts | 126 ++++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 37 deletions(-) diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 326f7c8..b09a3bb 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -21,42 +21,63 @@ export class MyMCP extends McpAgent { // Register listAvailableRulesTool this.server.tool( listAvailableRulesTool.name, - listAvailableRulesTool.description, - async () => { - const result = await listAvailableRulesTool.execute(); - return { content: [{ type: 'text', text: JSON.stringify(result) }] }; - } + listAvailableRulesTool.description, + async () => { + const result = await listAvailableRulesTool.execute(); + return { content: [{ type: 'text', text: JSON.stringify(result) }] }; + } ); - const inputSchemaShape = getRuleContentTool.inputSchema instanceof z.ZodObject - ? getRuleContentTool.inputSchema.shape - : {}; + const inputSchemaShape = getRuleContentTool.inputSchema instanceof z.ZodObject + ? getRuleContentTool.inputSchema.shape + : {}; this.server.tool( getRuleContentTool.name, - inputSchemaShape, - async (args: unknown) => { - const parsedArgs = getRuleContentTool.inputSchema.safeParse(args); - if (!parsedArgs.success) { - return { content: [{ type: 'text', text: `Invalid input: ${parsedArgs.error.message}`}], isError: true }; - } - const result = await getRuleContentTool.execute(parsedArgs.data); - return { content: [{ type: 'text', text: JSON.stringify(result) }] }; - } + inputSchemaShape, + async (args: unknown) => { + const parsedArgs = getRuleContentTool.inputSchema.safeParse(args); + if (!parsedArgs.success) { + return { content: [{ type: 'text', text: `Invalid input: ${parsedArgs.error.message}`}], isError: true }; + } + const result = await getRuleContentTool.execute(parsedArgs.data); + return { content: [{ type: 'text', text: JSON.stringify(result) }] }; + } ); } } -// Utility function to check if Accept header includes SSE -function acceptsSSE(acceptHeader: string | null): boolean { - if (!acceptHeader) return true; // Backwards compatibility +// Enhanced request validation for SSE endpoints +// Combines Accept header validation with User-Agent security check +function validateSSERequest(request: Request): { valid: boolean; error?: string } { + // Check Accept header + const accept = request.headers.get("accept"); + if (accept) { + const types = accept.split(',').map(t => t.trim().split(';')[0]); + const acceptsSSE = types.some(type => + type === 'text/event-stream' || + type === 'text/*' || + type === '*/*' + ); + if (!acceptsSSE) { + return { + valid: false, + error: "Invalid Accept header. Must accept text/event-stream" + }; + } + } + // Accept missing header for backwards compatibility + + // Check User-Agent (block empty or suspicious) + const userAgent = request.headers.get("user-agent") || ""; + if (userAgent && userAgent.length < 5) { + return { + valid: false, + error: "Invalid User-Agent header" + }; + } - const types = acceptHeader.split(',').map(t => t.trim().split(';')[0]); - return types.some(type => - type === 'text/event-stream' || - type === 'text/*' || - type === '*/*' - ); + return { valid: true }; } // Define more specific types for Env and ExecutionContext if known for the environment @@ -68,12 +89,14 @@ export default { const url = new URL(request.url); // Health check endpoint - lightweight, no DO creation, no rate limiting - if (url.pathname === "/health") { + // Support both /health and / for convenience + if (url.pathname === "/health" || url.pathname === "/") { return new Response(JSON.stringify({ status: "healthy", version: "1.0.0", timestamp: new Date().toISOString(), - service: "10x-rules-mcp-server" + service: "10x-rules-mcp-server", + endpoints: ["/health", "/sse", "/mcp"] }), { status: 200, headers: { @@ -118,16 +141,14 @@ export default { } } - // Accept header validation for SSE endpoints + // Enhanced request validation for SSE endpoints if (url.pathname === "/sse" || url.pathname === "/sse/message") { - const accept = request.headers.get("accept"); - - if (!acceptsSSE(accept)) { + const validation = validateSSERequest(request); + if (!validation.valid) { return new Response(JSON.stringify({ error: "Not Acceptable", - message: "This endpoint requires 'Accept: text/event-stream' header", - hint: "Use a Server-Sent Events compatible client", - received: accept || "(none)" + message: validation.error || "Invalid request", + hint: "Use a Server-Sent Events compatible client with valid User-Agent" }), { status: 406, headers: { @@ -137,8 +158,26 @@ export default { }); } + // Extract sessionId for session reuse tracking + const sessionId = url.searchParams.get("sessionId"); + // @ts-expect-error - env is not typed - return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + const response = await MyMCP.serveSSE("/sse").fetch(request, env, ctx); + + // If this is a new session (no sessionId provided), add headers to encourage reuse + if (!sessionId && response.status === 200) { + const newHeaders = new Headers(response.headers); + newHeaders.set("X-Session-Reuse", "Save sessionId from URL and reuse for reconnections"); + newHeaders.set("X-Session-Info", "Reusing sessions reduces server load"); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders + }); + } + + return response; } if (url.pathname === "/mcp") { @@ -146,6 +185,19 @@ export default { return MyMCP.serve("/mcp").fetch(request, env, ctx); } - return new Response("Not found", { status: 404 }); + // Enhanced 404 response with helpful information + return new Response(JSON.stringify({ + error: "Not found", + message: "The requested endpoint does not exist", + availableEndpoints: [ + { path: "/health", method: "GET", description: "Health check endpoint" }, + { path: "/", method: "GET", description: "Health check (alias)" }, + { path: "/sse", method: "GET", description: "Server-Sent Events MCP endpoint" }, + { path: "/mcp", method: "POST", description: "Standard MCP endpoint" } + ] + }), { + status: 404, + headers: { "Content-Type": "application/json" } + }); }, };