From 30ea8f2b1c091833351d083f61a9654070e46d1a Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 16:07:04 +0100 Subject: [PATCH 01/10] chore: update package.json to add mcp script and new dependencies --- bun.lock | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 7 ++- 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 65c634e..45668ec 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@elysiajs/cors": "^1.4.0", "@elysiajs/swagger": "^1.3.1", + "@modelcontextprotocol/sdk": "^1.25.1", "@prisma/adapter-pg": "^7.1.0", "@prisma/client": "^7.1.0", "@types/pg": "^8.16.0", @@ -14,6 +15,7 @@ "figlet": "^1.9.4", "pg": "^8.16.3", "prisma": "^7.1.0", + "zod": "^4.2.1", }, "devDependencies": { "@biomejs/biome": "2.3.9", @@ -72,7 +74,9 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="], + "@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="], "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.12.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w=="], @@ -178,16 +182,30 @@ "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -200,8 +218,16 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -214,26 +240,56 @@ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], "elysia": ["elysia@1.4.19", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "0.2.5", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-DZb9y8FnWyX5IuqY44SvqAV0DjJ15NeCWHrLdgXrKgTPDPsl3VNwWHqrEr9bmnOCpg1vh6QUvAX/tcxNj88jLA=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "exact-mirror": ["exact-mirror@0.2.5", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.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", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "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.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], @@ -244,46 +300,78 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-port-please": ["get-port-please@3.1.2", "", {}, "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="], "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "knip": ["knip@5.75.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "js-yaml": "^4.1.1", "minimist": "^1.2.8", "oxc-resolver": "^11.15.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-raguBFxTUO5JKrv8rtC8wrOtzrDwWp/fOu1F1GhrHD1F3TD2fqI1Z74JB+PyFZubL+RxqOkhGStdPAvaaXSOWQ=="], "lefthook": ["lefthook@2.0.12", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.12", "lefthook-darwin-x64": "2.0.12", "lefthook-freebsd-arm64": "2.0.12", "lefthook-freebsd-x64": "2.0.12", "lefthook-linux-arm64": "2.0.12", "lefthook-linux-x64": "2.0.12", "lefthook-openbsd-arm64": "2.0.12", "lefthook-openbsd-x64": "2.0.12", "lefthook-windows-arm64": "2.0.12", "lefthook-windows-x64": "2.0.12" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-I2FdA9cdnq1icwlNz4RADs7exuqe47q1N9+p2LmcP/WfchWh16mvTB82OAD7w7zK9GxblS9GpF7pASaOSl4c7A=="], @@ -316,12 +404,22 @@ "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -332,18 +430,32 @@ "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "oxc-resolver": ["oxc-resolver@11.15.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.15.0", "@oxc-resolver/binding-android-arm64": "11.15.0", "@oxc-resolver/binding-darwin-arm64": "11.15.0", "@oxc-resolver/binding-darwin-x64": "11.15.0", "@oxc-resolver/binding-freebsd-x64": "11.15.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.15.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.15.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.15.0", "@oxc-resolver/binding-linux-arm64-musl": "11.15.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.15.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.15.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.15.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.15.0", "@oxc-resolver/binding-linux-x64-gnu": "11.15.0", "@oxc-resolver/binding-linux-x64-musl": "11.15.0", "@oxc-resolver/binding-openharmony-arm64": "11.15.0", "@oxc-resolver/binding-wasm32-wasi": "11.15.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.15.0", "@oxc-resolver/binding-win32-ia32-msvc": "11.15.0", "@oxc-resolver/binding-win32-x64-msvc": "11.15.0" } }, "sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], @@ -368,6 +480,8 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], @@ -384,10 +498,18 @@ "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], @@ -400,22 +522,40 @@ "remeda": ["remeda@2.21.3", "", { "dependencies": { "type-fest": "^4.39.1" } }, "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="], @@ -424,6 +564,8 @@ "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], @@ -434,24 +576,34 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "zeptomatch": ["zeptomatch@2.0.2", "", { "dependencies": { "grammex": "^3.1.10" } }, "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g=="], @@ -460,6 +612,10 @@ "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "@prisma/dev/@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="], + "@prisma/dev/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.1.0", "", { "dependencies": { "@prisma/debug": "7.1.0" } }, "sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q=="], @@ -472,6 +628,8 @@ "c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/package.json b/package.json index 435a910..38d3818 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "prisma:push": "bunx --bun prisma db push", "seed": "bun run prisma/seed.ts", "test": "bun test --timeout 5000", - "test:coverage": "bun test --coverage --timeout 5000" + "test:coverage": "bun test --coverage --timeout 5000", + "mcp": "bun run src/mcp/server.ts" }, "devDependencies": { "@biomejs/biome": "2.3.9", @@ -30,12 +31,14 @@ "dependencies": { "@elysiajs/cors": "^1.4.0", "@elysiajs/swagger": "^1.3.1", + "@modelcontextprotocol/sdk": "^1.25.1", "@prisma/adapter-pg": "^7.1.0", "@prisma/client": "^7.1.0", "@types/pg": "^8.16.0", "elysia": "^1.4.19", "figlet": "^1.9.4", "pg": "^8.16.3", - "prisma": "^7.1.0" + "prisma": "^7.1.0", + "zod": "^4.2.1" } } From f3a4a0a86d15840a9c77edbfabd4581a393cd22a Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 16:08:07 +0100 Subject: [PATCH 02/10] feat: expose additional port 3001 for the application --- Dockerfile | 1 + compose.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index cdafc33..8aa0863 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,4 +40,5 @@ COPY --from=prerelease /usr/src/app/prisma ./prisma # run the app USER bun EXPOSE 3000/tcp +EXPOSE 3001/tcp ENTRYPOINT [ "bun", "run", "index.ts" ] \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 1cb26ed..cab3643 100644 --- a/compose.yaml +++ b/compose.yaml @@ -25,6 +25,7 @@ services: container_name: MyHouseServer ports: - "0.0.0.0:3000:3000" + - "0.0.0.0:3001:3001" environment: - NODE_ENV=production - DATABASE_URL=postgresql://root:root@db:5432/myhouse From ff2d0f2a1681ca2c32ad92277242856ef8834b99 Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 16:09:47 +0100 Subject: [PATCH 03/10] feat: start MCP server and update server information in the console log --- index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.ts b/index.ts index 9b522af..e9d2f88 100644 --- a/index.ts +++ b/index.ts @@ -5,6 +5,7 @@ import figlet from "figlet"; import { routes } from "./src/routes"; import { initRuleEngine } from "./src/rules/engine"; import { EVENTS, eventBus } from "./src/utils/eventBus"; +import { startMcpServer } from "./src/mcp/server"; export const app = new Elysia() .use( @@ -41,9 +42,11 @@ eventBus.on(EVENTS.NEW_CONNECTION, () => { if (import.meta.main) { initRuleEngine(); + startMcpServer(); const PORT_BUN_SERVER = process.env.PORT_BUN_SERVER || 3000; const PORT_WEB_SERVER = process.env.PORT_WEB_SERVER || 8080; + const PORT_MCP_SERVER = process.env.MCP_PORT || 3001; app.listen(PORT_BUN_SERVER); console.log(` @@ -54,6 +57,7 @@ if (import.meta.main) { │ 🔗 API: http://192.168.4.2:${PORT_BUN_SERVER} │ │ 📖 Swagger: http://192.168.4.2:${PORT_BUN_SERVER}/swagger │ │ 🔌 WebSocket: ws://192.168.4.2:${PORT_BUN_SERVER}/ws │ +│ 🤖 MCP: http://192.168.4.2:${PORT_MCP_SERVER}/mcp │ │ 🌐 Web Server: http://192.168.4.3:${PORT_WEB_SERVER} │ └────────────────────────────────────────────────────┘ `); From 27f9e1edc206e1276db3d73eb1e7b822b6ec1b1d Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 16:13:42 +0100 Subject: [PATCH 04/10] feat: add integration tests for MCP tools and authentication verification --- tests/integration/e2e.test.ts | 1 - tests/mcp/tools.test.ts | 202 ++++++++++++++++++++++++++++++++++ tests/mocks/prisma.ts | 5 - tests/routes/features.test.ts | 3 - tests/routes/ws.test.ts | 8 -- tests/rules/engine.test.ts | 1 - tests/utils/auth.test.ts | 67 +++++++++++ 7 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 tests/mcp/tools.test.ts create mode 100644 tests/utils/auth.test.ts diff --git a/tests/integration/e2e.test.ts b/tests/integration/e2e.test.ts index d0bc016..f4f961f 100644 --- a/tests/integration/e2e.test.ts +++ b/tests/integration/e2e.test.ts @@ -45,7 +45,6 @@ describe("E2E Integration Tests", async () => { mockClients.set("MasterServer", master); } - // Setup mock implementations mockPrisma.client.findUnique.mockImplementation((args) => { const client = mockClients.get(args?.where?.ClientID); return Promise.resolve(client || null); diff --git a/tests/mcp/tools.test.ts b/tests/mcp/tools.test.ts new file mode 100644 index 0000000..b7dd3cc --- /dev/null +++ b/tests/mcp/tools.test.ts @@ -0,0 +1,202 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { setAuthorization, tools } from "../../src/mcp/tools"; +import "../mocks/prisma"; + +const originalFetch = globalThis.fetch; +let mockFetchImplementation: (url: string, options?: RequestInit) => Promise; + +beforeAll(() => { + globalThis.fetch = mock((url: string | URL | Request, options?: RequestInit) => { + return mockFetchImplementation(url.toString(), options); + }) as typeof fetch; +}); + +afterAll(() => { + globalThis.fetch = originalFetch; +}); + +describe("MCP Tools", () => { + beforeEach(() => { + setAuthorization("test:token"); + }); + + describe("toggle_light", () => { + it("toggles light successfully", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ light: true }), { status: 200 }); + + const result = await tools.toggle_light.handler({}); + expect(result.success).toBe(true); + expect(result.light).toBe(true); + expect(result.message).toBe("Light is now ON"); + }); + + it("handles light off state", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ light: false }), { status: 200 }); + + const result = await tools.toggle_light.handler({}); + expect(result.success).toBe(true); + expect(result.light).toBe(false); + expect(result.message).toBe("Light is now OFF"); + }); + + it("throws error on API failure", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ error: "Server error" }), { status: 500 }); + + await expect(tools.toggle_light.handler({})).rejects.toThrow("Server error"); + }); + }); + + describe("toggle_door", () => { + it("opens door successfully", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ door: true }), { status: 200 }); + + const result = await tools.toggle_door.handler({}); + expect(result.success).toBe(true); + expect(result.door).toBe(true); + expect(result.message).toBe("Door is now OPEN"); + }); + + it("closes door successfully", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ door: false }), { status: 200 }); + + const result = await tools.toggle_door.handler({}); + expect(result.success).toBe(true); + expect(result.door).toBe(false); + expect(result.message).toBe("Door is now CLOSED"); + }); + + it("throws error on API failure", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); + + await expect(tools.toggle_door.handler({})).rejects.toThrow("Unauthorized"); + }); + }); + + describe("toggle_heat", () => { + it("turns heat on successfully", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ heat: true }), { status: 200 }); + + const result = await tools.toggle_heat.handler({}); + expect(result.success).toBe(true); + expect(result.heat).toBe(true); + expect(result.message).toBe("Heating is now ON"); + }); + + it("turns heat off successfully", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ heat: false }), { status: 200 }); + + const result = await tools.toggle_heat.handler({}); + expect(result.success).toBe(true); + expect(result.heat).toBe(false); + expect(result.message).toBe("Heating is now OFF"); + }); + }); + + describe("set_temperature", () => { + it("sets temperature successfully", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ temp: "23.5" }), { status: 200 }); + + const result = await tools.set_temperature.handler({ temp: "23.5" }); + expect(result.success).toBe(true); + expect(result.temperature).toBe("23.5"); + expect(result.message).toBe("Temperature set to 23.5"); + }); + + it("throws error on API failure", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ error: "Invalid temperature" }), { status: 400 }); + + await expect(tools.set_temperature.handler({ temp: "invalid" })).rejects.toThrow( + "Invalid temperature", + ); + }); + }); + + describe("get_home_state", () => { + it("returns complete home state", async () => { + mockFetchImplementation = async (url: string) => { + if (url.includes("/toggle/light")) { + return new Response(JSON.stringify({ light: true }), { status: 200 }); + } + if (url.includes("/toggle/door")) { + return new Response(JSON.stringify({ door: false }), { status: 200 }); + } + if (url.includes("/toggle/heat")) { + return new Response(JSON.stringify({ heat: true }), { status: 200 }); + } + if (url.includes("/temp")) { + return new Response(JSON.stringify({ temp: "22.0" }), { status: 200 }); + } + return new Response("Not found", { status: 404 }); + }; + + const result = await tools.get_home_state.handler({}); + expect(result.light).toBe(true); + expect(result.door).toBe(false); + expect(result.heat).toBe(true); + expect(result.temperature).toBe("22.0"); + }); + }); + + describe("get_history", () => { + it("returns history with default limit", async () => { + const mockHistory = [ + { type: "light", value: "true", createdAt: "2024-01-01T00:00:00Z" }, + { type: "door", value: "false", createdAt: "2024-01-01T00:01:00Z" }, + ]; + + mockFetchImplementation = async (url: string) => { + expect(url).toContain("limit=50"); + return new Response(JSON.stringify({ data: mockHistory, count: 2 }), { status: 200 }); + }; + + const result = await tools.get_history.handler({}); + expect(result.count).toBe(2); + expect(result.events).toHaveLength(2); + expect(result.events[0].type).toBe("light"); + }); + + it("respects custom limit", async () => { + mockFetchImplementation = async (url: string) => { + expect(url).toContain("limit=10"); + return new Response(JSON.stringify({ data: [], count: 0 }), { status: 200 }); + }; + + const result = await tools.get_history.handler({ limit: 10 }); + expect(result.count).toBe(0); + expect(result.events).toHaveLength(0); + }); + + it("throws error on API failure", async () => { + mockFetchImplementation = async () => + new Response(JSON.stringify({ error: "Database error" }), { status: 500 }); + + await expect(tools.get_history.handler({})).rejects.toThrow("Database error"); + }); + }); + + describe("setAuthorization", () => { + it("sets authorization header for API calls", async () => { + let capturedHeaders: Headers | undefined; + + mockFetchImplementation = async (_url: string, options?: RequestInit) => { + capturedHeaders = new Headers(options?.headers); + return new Response(JSON.stringify({ light: true }), { status: 200 }); + }; + + setAuthorization("myClient:myToken"); + await tools.toggle_light.handler({}); + + expect(capturedHeaders?.get("Authorization")).toBe("myClient:myToken"); + }); + }); +}); diff --git a/tests/mocks/prisma.ts b/tests/mocks/prisma.ts index 55747a6..b5018cd 100644 --- a/tests/mocks/prisma.ts +++ b/tests/mocks/prisma.ts @@ -1,9 +1,6 @@ import { mock } from "bun:test"; import { db } from "../../prisma/db"; -// Mock Prisma centralisé partagé par tous les tests -// Chaque test peut réinitialiser les implémentations dans beforeEach - export const mockPrisma = { client: { findUnique: mock((..._args: never[]) => Promise.resolve(null)), @@ -31,6 +28,4 @@ export const mockPrisma = { $queryRaw: mock((..._args: never[]) => Promise.resolve([1])), }; -// Injecter le mock directement dans le container db -// Cela fonctionne car prisma est un Proxy qui délègue à db.prisma db.prisma = mockPrisma; diff --git a/tests/routes/features.test.ts b/tests/routes/features.test.ts index 1e2aacb..18c889d 100644 --- a/tests/routes/features.test.ts +++ b/tests/routes/features.test.ts @@ -13,7 +13,6 @@ const mockState = { const encryptedUserToken = encrypt("Token"); // TODO: Ces tests sont skippés car le mocking Prisma ne fonctionne pas de manière fiable -// entre Windows (local) et Linux (CI). À investiguer avec une future version de Bun. describe.skip("Toggle & Temp Routes", async () => { const { app } = await import("../../index"); const authHeader = { Authorization: "User:Token" }; @@ -27,7 +26,6 @@ describe.skip("Toggle & Temp Routes", async () => { heat: false, }); - // Réinitialiser les mocks avec les valeurs de ce test mockPrisma.client.findUnique.mockImplementation(() => Promise.resolve({ ClientID: "User", ClientToken: encryptedUserToken } as never), ); @@ -36,7 +34,6 @@ describe.skip("Toggle & Temp Routes", async () => { mockPrisma.homeState.upsert.mockImplementation((args: never) => { const updateData = (args as { update?: Record })?.update; - // Ne muter mockState que si update contient réellement des données if (updateData && Object.keys(updateData).length > 0) { Object.assign(mockState, updateData); } diff --git a/tests/routes/ws.test.ts b/tests/routes/ws.test.ts index f44cf05..e6800f5 100644 --- a/tests/routes/ws.test.ts +++ b/tests/routes/ws.test.ts @@ -11,7 +11,6 @@ const mockState = { }; // TODO: Ces tests sont skippés car le mocking Prisma ne fonctionne pas de manière fiable -// entre Windows (local) et Linux (CI). À investiguer avec une future version de Bun. describe.skip("WebSocket Route", async () => { const { app } = await import("../../index"); @@ -32,14 +31,12 @@ describe.skip("WebSocket Route", async () => { heat: false, }); - // Réinitialiser les mocks pour ce test mockPrisma.client.findUnique.mockImplementation(() => Promise.resolve(null)); mockPrisma.client.findFirst.mockImplementation(() => Promise.resolve(null)); mockPrisma.client.upsert.mockImplementation(() => Promise.resolve({} as never)); mockPrisma.homeState.upsert.mockImplementation((args: never) => { const updateData = (args as { update?: Record })?.update; - // Ne muter mockState que si update contient réellement des données if (updateData && Object.keys(updateData).length > 0) { Object.assign(mockState, updateData); } @@ -74,7 +71,6 @@ describe.skip("WebSocket Route", async () => { const message = (await messagePromise) as { type: string; data: Record }; expect(message.type).toBe("INIT"); - // Vérifie la structure plutôt que les valeurs exactes pour éviter les problèmes d'isolation expect(message.data).toHaveProperty("temperature"); expect(message.data).toHaveProperty("light"); expect(message.data).toHaveProperty("door"); @@ -88,13 +84,11 @@ describe.skip("WebSocket Route", async () => { const ws = new WebSocket(wsUrl); try { - // Attendre que la connexion soit ouverte await new Promise((resolve) => { if (ws.readyState === WebSocket.OPEN) resolve(); else ws.onopen = () => resolve(); }); - // Attendre le message INIT d'abord await new Promise((resolve) => { ws.onmessage = (event) => { const msg = JSON.parse(event.data as string); @@ -104,7 +98,6 @@ describe.skip("WebSocket Route", async () => { }; }); - // Maintenant écouter les updates const updatePromise = new Promise<{ type: string; data: Record }>( (resolve) => { ws.onmessage = (event) => { @@ -116,7 +109,6 @@ describe.skip("WebSocket Route", async () => { }, ); - // Émettre l'événement après avoir configuré le listener eventBus.emit(EVENTS.STATE_CHANGE, { type: "TEMP", value: "25.0" }); const update = await updatePromise; diff --git a/tests/rules/engine.test.ts b/tests/rules/engine.test.ts index ae69e2e..3cacf6f 100644 --- a/tests/rules/engine.test.ts +++ b/tests/rules/engine.test.ts @@ -27,7 +27,6 @@ describe("Rule Engine", async () => { }); afterEach(() => { - // Nettoyer les listeners pour éviter les interférences avec d'autres tests eventBus.removeAllListeners(EVENTS.STATE_CHANGE); }); diff --git a/tests/utils/auth.test.ts b/tests/utils/auth.test.ts new file mode 100644 index 0000000..6c9d6b3 --- /dev/null +++ b/tests/utils/auth.test.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it } from "bun:test"; +import { encrypt } from "../../src/utils/crypto"; +import { mockPrisma } from "../mocks/prisma"; + +const encryptedToken = encrypt("secret123"); + +describe("verifyClientAuth", async () => { + const { verifyClientAuth } = await import("../../src/utils/auth"); + + beforeEach(() => { + mockPrisma.client.findUnique.mockImplementation((args) => { + if (args?.where?.ClientID === "validClient") { + return Promise.resolve({ ClientID: "validClient", ClientToken: encryptedToken }); + } + return Promise.resolve(null); + }); + }); + + it("returns error when authorization is null", async () => { + const result = await verifyClientAuth(null); + expect(result.valid).toBe(false); + expect(result.error).toBe("Authorization header missing"); + }); + + it("returns error when authorization is empty string", async () => { + const result = await verifyClientAuth(""); + expect(result.valid).toBe(false); + expect(result.error).toBe("Authorization header missing"); + }); + + it("returns error when authorization format is invalid (no colon)", async () => { + const result = await verifyClientAuth("invalidformat"); + expect(result.valid).toBe(false); + expect(result.error).toBe("Invalid authorization format. Expected 'id:token'"); + }); + + it("returns error when clientId is empty", async () => { + const result = await verifyClientAuth(":token"); + expect(result.valid).toBe(false); + expect(result.error).toBe("Invalid credentials format"); + }); + + it("returns error when token is empty", async () => { + const result = await verifyClientAuth("clientId:"); + expect(result.valid).toBe(false); + expect(result.error).toBe("Invalid credentials format"); + }); + + it("returns error when client does not exist", async () => { + const result = await verifyClientAuth("unknownClient:sometoken"); + expect(result.valid).toBe(false); + expect(result.error).toBe("Invalid credentials"); + }); + + it("returns error when token is incorrect", async () => { + const result = await verifyClientAuth("validClient:wrongtoken"); + expect(result.valid).toBe(false); + expect(result.error).toBe("Invalid credentials"); + }); + + it("returns valid when credentials are correct", async () => { + const result = await verifyClientAuth("validClient:secret123"); + expect(result.valid).toBe(true); + expect(result.clientId).toBe("validClient"); + expect(result.error).toBeUndefined(); + }); +}); From 6ffa5192133f973b9b2f0a1f1b28b0924a60b8e0 Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 16:14:56 +0100 Subject: [PATCH 05/10] feat: implement client authorization verification function --- src/utils/auth.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/utils/auth.ts diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..25f9dda --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,46 @@ +import { prisma } from "../../prisma/db"; +import { decrypt } from "./crypto"; + +export interface AuthResult { + valid: boolean; + clientId?: string; + error?: string; +} + +export async function verifyClientAuth(authorization: string | null): Promise { + if (!authorization) { + return { valid: false, error: "Authorization header missing" }; + } + + if (!authorization.includes(":")) { + return { valid: false, error: "Invalid authorization format. Expected 'id:token'" }; + } + + const [clientId, clientToken] = authorization.split(":"); + + if (!clientId || !clientToken) { + return { valid: false, error: "Invalid credentials format" }; + } + + const client = await prisma.client.findUnique({ + where: { ClientID: clientId }, + }); + + if (!client) { + console.log(`Unauthorized access attempt by unknown client: ${clientId}`); + return { valid: false, error: "Invalid credentials" }; + } + + try { + const decryptedToken = decrypt(client.ClientToken); + if (decryptedToken !== clientToken) { + console.log(`Invalid token provided for client: ${clientId}`); + return { valid: false, error: "Invalid credentials" }; + } + } catch (error) { + console.error(`Token verification failed for ${clientId}:`, error); + return { valid: false, error: "Invalid credentials" }; + } + + return { valid: true, clientId }; +} From 4937316c38244572ce4289ac7a81f461ba5bacad Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 16:17:20 +0100 Subject: [PATCH 06/10] refactor: clean up db.ts by removing unnecessary comments and improving clarity --- prisma/db.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/prisma/db.ts b/prisma/db.ts index 5bf2cc8..d232d18 100644 --- a/prisma/db.ts +++ b/prisma/db.ts @@ -7,22 +7,16 @@ const connectionString = process.env.DATABASE_URL; // biome-ignore lint/suspicious/noExplicitAny: Dynamic prisma type for test/prod type PrismaType = PrismaClient | any; -// Container object - les tests peuvent modifier db.prisma directement -// et tous les modules qui utilisent db.prisma verront le changement export const db: { prisma: PrismaType } = { prisma: {} as PrismaType, }; if (connectionString) { - // Production/Development: use real database connection const pool = new Pool({ connectionString }); const adapter = new PrismaPg(pool); db.prisma = new PrismaClient({ adapter }); } -// Export pour compatibilité avec le code existant -// Note: cet export est une référence à db.prisma, donc si db.prisma change, -// ce changement sera visible partout export const prisma = new Proxy({} as PrismaType, { get(_target, prop) { return (db.prisma as Record)[prop]; From aebc09664942f9dabc2afd2d9c235dbba194c672 Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 16:23:57 +0100 Subject: [PATCH 07/10] feat: implement MCP server and tools for home automation control --- src/mcp/index.ts | 1 + src/mcp/server.ts | 103 ++++++++++++++++++++++++++++ src/mcp/tools.ts | 170 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 src/mcp/index.ts create mode 100644 src/mcp/server.ts create mode 100644 src/mcp/tools.ts diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 0000000..4e5b805 --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1 @@ +export { setAuthorization, tools } from "./tools"; diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 0000000..ed7f819 --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,103 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; +import { tools, setAuthorization } from "./tools"; +import { verifyClientAuth } from "../utils/auth"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, mcp-session-id, Last-Event-ID, mcp-protocol-version", + "Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version", +}; + +export async function startMcpServer() { + const MCP_PORT = Number(process.env.MCP_PORT) || 3001; + + const server = new McpServer({ + name: "myhouse-os", + version: "1.0.0", + }); + + for (const [name, tool] of Object.entries(tools)) { + server.tool(name, tool.description, tool.inputSchema.shape, async (args) => { + try { + const result = await tool.handler(args as Parameters[0]); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ error: message }, null, 2), + }, + ], + isError: true, + }; + } + }); + } + + const transport = new WebStandardStreamableHTTPServerTransport(); + + await server.connect(transport); + + Bun.serve({ + port: MCP_PORT, + async fetch(req) { + const url = new URL(req.url); + + if (req.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders }); + } + + if (url.pathname === "/health") { + return new Response(JSON.stringify({ status: "ok", server: "myhouse-os-mcp" }), { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + if (url.pathname === "/mcp") { + const authorization = req.headers.get("authorization"); + + const authResult = await verifyClientAuth(authorization); + if (!authResult.valid) { + return new Response(JSON.stringify({ error: authResult.error }), { + status: 401, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + console.log(`MCP request from authenticated client: ${authResult.clientId}`); + + setAuthorization(authorization || ""); + + const response = await transport.handleRequest(req); + + const newHeaders = new Headers(response.headers); + for (const [key, value] of Object.entries(corsHeaders)) { + newHeaders.set(key, value); + } + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, + }); + } + + return new Response("Not Found", { status: 404, headers: corsHeaders }); + }, + }); + + console.log(`MCP server running on http://localhost:${MCP_PORT}`); + console.log(`MCP endpoint: http://localhost:${MCP_PORT}/mcp`); + console.log(`Health check: http://localhost:${MCP_PORT}/health`); +} diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts new file mode 100644 index 0000000..c884101 --- /dev/null +++ b/src/mcp/tools.ts @@ -0,0 +1,170 @@ +import { z } from "zod"; + +const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3000"; + +let currentAuthorization = ""; + +export function setAuthorization(auth: string) { + currentAuthorization = auth; +} + +async function apiCall( + endpoint: string, + method: "GET" | "POST", + body?: Record, +): Promise { + const options: RequestInit = { + method, + headers: { + Authorization: currentAuthorization, + "Content-Type": "application/json", + }, + }; + + if (body) { + options.body = JSON.stringify(body); + } + + return fetch(`${API_BASE_URL}${endpoint}`, options); +} + +export const tools = { + toggle_light: { + description: "Toggle the light on or off.", + inputSchema: z.object({}), + handler: async () => { + const response = await apiCall("/toggle/light", "POST"); + const data = (await response.json()) as { light: boolean; error?: string }; + + if (!response.ok) { + throw new Error(data.error || "Failed to toggle light"); + } + + return { + success: true, + light: data.light, + message: data.light ? "Light is now ON" : "Light is now OFF", + }; + }, + }, + + toggle_door: { + description: "Open or close the door.", + inputSchema: z.object({}), + handler: async () => { + const response = await apiCall("/toggle/door", "POST"); + const data = (await response.json()) as { door: boolean; error?: string }; + + if (!response.ok) { + throw new Error(data.error || "Failed to toggle door"); + } + + return { + success: true, + door: data.door, + message: data.door ? "Door is now OPEN" : "Door is now CLOSED", + }; + }, + }, + + toggle_heat: { + description: "Turn the heating on or off.", + inputSchema: z.object({}), + handler: async () => { + const response = await apiCall("/toggle/heat", "POST"); + const data = (await response.json()) as { heat: boolean; error?: string }; + + if (!response.ok) { + throw new Error(data.error || "Failed to toggle heat"); + } + + return { + success: true, + heat: data.heat, + message: data.heat ? "Heating is now ON" : "Heating is now OFF", + }; + }, + }, + + set_temperature: { + description: "Set the current temperature reading (e.g., from a sensor).", + inputSchema: z.object({ + temp: z.string().describe("Temperature value as string (e.g., '23.5')"), + }), + handler: async (args: { temp: string }) => { + const response = await apiCall("/temp", "POST", { temp: args.temp }); + const data = (await response.json()) as { temp: string; error?: string }; + + if (!response.ok) { + throw new Error(data.error || "Failed to set temperature"); + } + + return { + success: true, + temperature: data.temp, + message: `Temperature set to ${data.temp}`, + }; + }, + }, + + get_home_state: { + description: + "Get the current state of the home including temperature, light, door, and heating status.", + inputSchema: z.object({}), + handler: async () => { + const [lightRes, doorRes, heatRes, tempRes] = await Promise.all([ + apiCall("/toggle/light", "GET"), + apiCall("/toggle/door", "GET"), + apiCall("/toggle/heat", "GET"), + apiCall("/temp", "GET"), + ]); + + const [lightData, doorData, heatData, tempData] = (await Promise.all([ + lightRes.json(), + doorRes.json(), + heatRes.json(), + tempRes.json(), + ])) as [{ light: boolean }, { door: boolean }, { heat: boolean }, { temp: string }]; + + return { + temperature: tempData.temp, + light: lightData.light, + door: doorData.door, + heat: heatData.heat, + }; + }, + }, + + get_history: { + description: "Get the history of home events (temperature changes, light/door/heat toggles).", + inputSchema: z.object({ + limit: z + .number() + .optional() + .default(50) + .describe("Number of events to retrieve (default: 50)"), + }), + handler: async (args: { limit?: number }) => { + const limit = args.limit ?? 50; + const response = await apiCall(`/history?limit=${limit}`, "GET"); + const data = (await response.json()) as { + data: { type: string; value: string; createdAt: string }[]; + count: number; + error?: string; + }; + + if (!response.ok) { + throw new Error(data.error || "Failed to get history"); + } + + return { + count: data.count, + events: data.data.map((event) => ({ + type: event.type, + value: event.value, + createdAt: event.createdAt, + })), + }; + }, + }, +}; From f753b1cfd18a8d8bf3b9704b26d1f3370571a7a2 Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 17:48:52 +0100 Subject: [PATCH 08/10] refactor: reorganize imports in index.ts and server.ts for clarity --- index.ts | 2 +- src/mcp/server.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index e9d2f88..506b2bc 100644 --- a/index.ts +++ b/index.ts @@ -2,10 +2,10 @@ import { cors } from "@elysiajs/cors"; import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; import figlet from "figlet"; +import { startMcpServer } from "./src/mcp/server"; import { routes } from "./src/routes"; import { initRuleEngine } from "./src/rules/engine"; import { EVENTS, eventBus } from "./src/utils/eventBus"; -import { startMcpServer } from "./src/mcp/server"; export const app = new Elysia() .use( diff --git a/src/mcp/server.ts b/src/mcp/server.ts index ed7f819..7e9c250 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1,7 +1,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; -import { tools, setAuthorization } from "./tools"; import { verifyClientAuth } from "../utils/auth"; +import { setAuthorization, tools } from "./tools"; const corsHeaders = { "Access-Control-Allow-Origin": "*", From ef1e04252bdde0c2bfd6b44f2484a2f6d85545c8 Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 18:01:16 +0100 Subject: [PATCH 09/10] refactor: remove unused export from mcp index.ts --- src/mcp/index.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/mcp/index.ts diff --git a/src/mcp/index.ts b/src/mcp/index.ts deleted file mode 100644 index 4e5b805..0000000 --- a/src/mcp/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { setAuthorization, tools } from "./tools"; From 47189738889a070e1d6f42130b3601c8c82c079f Mon Sep 17 00:00:00 2001 From: devZenta Date: Sun, 28 Dec 2025 18:24:25 +0100 Subject: [PATCH 10/10] feat: add wrapToolHandler for consistent error handling and response formatting test: implement tests for wrapToolHandler and enhance verifyClientAuth error handling refactor: update mockPrisma methods to use unknown type for better type safety --- src/mcp/server.ts | 59 ++++--- tests/mcp/server.test.ts | 341 +++++++++++++++++++++++++++++++++++++++ tests/mocks/prisma.ts | 6 +- tests/utils/auth.test.ts | 17 ++ 4 files changed, 398 insertions(+), 25 deletions(-) create mode 100644 tests/mcp/server.test.ts diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 7e9c250..eed8092 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -3,6 +3,39 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/ import { verifyClientAuth } from "../utils/auth"; import { setAuthorization, tools } from "./tools"; +export interface McpToolResponse { + content: Array<{ type: "text"; text: string }>; + isError?: boolean; +} + +export async function wrapToolHandler( + handler: (args: Record) => Promise, + args: Record, +): Promise { + try { + const result = await handler(args); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ error: message }, null, 2), + }, + ], + isError: true, + }; + } +} + const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", @@ -21,28 +54,10 @@ export async function startMcpServer() { for (const [name, tool] of Object.entries(tools)) { server.tool(name, tool.description, tool.inputSchema.shape, async (args) => { - try { - const result = await tool.handler(args as Parameters[0]); - return { - content: [ - { - type: "text" as const, - text: JSON.stringify(result, null, 2), - }, - ], - }; - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - return { - content: [ - { - type: "text" as const, - text: JSON.stringify({ error: message }, null, 2), - }, - ], - isError: true, - }; - } + return wrapToolHandler( + tool.handler as (args: Record) => Promise, + args, + ); }); } diff --git a/tests/mcp/server.test.ts b/tests/mcp/server.test.ts new file mode 100644 index 0000000..34921e7 --- /dev/null +++ b/tests/mcp/server.test.ts @@ -0,0 +1,341 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { encrypt } from "../../src/utils/crypto"; +import { mockPrisma } from "../mocks/prisma"; + +const encryptedToken = encrypt("secret123"); + +describe("MCP Server", () => { + let mockBunServe: ReturnType; + let capturedFetchHandler: (req: Request) => Promise; + const originalBunServe = Bun.serve; + const originalConsoleLog = console.log; + + beforeAll(() => { + console.log = mock(() => {}); + mockBunServe = mock((config: { port: number; fetch: (req: Request) => Promise }) => { + capturedFetchHandler = config.fetch; + return { port: config.port }; + }); + (Bun as { serve: typeof Bun.serve }).serve = mockBunServe as typeof Bun.serve; + }); + + afterAll(() => { + (Bun as { serve: typeof Bun.serve }).serve = originalBunServe; + console.log = originalConsoleLog; + }); + + beforeEach(() => { + mockPrisma.client.findUnique.mockImplementation((args: unknown) => { + const typedArgs = args as { where?: { ClientID?: string } } | undefined; + if (typedArgs?.where?.ClientID === "validClient") { + return Promise.resolve({ ClientID: "validClient", ClientToken: encryptedToken }); + } + return Promise.resolve(null); + }); + }); + + describe("startMcpServer", () => { + it("starts server on default port 3001", async () => { + const { startMcpServer } = await import("../../src/mcp/server"); + await startMcpServer(); + + expect(mockBunServe).toHaveBeenCalled(); + const callArgs = mockBunServe.mock.calls[0] as [{ port: number }]; + expect(callArgs[0].port).toBe(3001); + }); + }); + + describe("HTTP handler", () => { + beforeEach(async () => { + mockBunServe.mockClear(); + const { startMcpServer } = await import("../../src/mcp/server"); + await startMcpServer(); + }); + + describe("OPTIONS requests (CORS preflight)", () => { + it("returns 204 with CORS headers", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "OPTIONS", + }); + + const response = await capturedFetchHandler(request); + + expect(response.status).toBe(204); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + expect(response.headers.get("Access-Control-Allow-Methods")).toContain("POST"); + expect(response.headers.get("Access-Control-Allow-Headers")).toContain("Authorization"); + }); + }); + + describe("GET /health", () => { + it("returns health status", async () => { + const request = new Request("http://localhost:3001/health", { + method: "GET", + }); + + const response = await capturedFetchHandler(request); + const body = (await response.json()) as { status: string; server: string }; + + expect(response.status).toBe(200); + expect(body.status).toBe("ok"); + expect(body.server).toBe("myhouse-os-mcp"); + }); + + it("includes CORS headers", async () => { + const request = new Request("http://localhost:3001/health", { + method: "GET", + }); + + const response = await capturedFetchHandler(request); + + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + }); + }); + + describe("POST /mcp", () => { + it("returns 401 when authorization is missing", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + + const response = await capturedFetchHandler(request); + const body = (await response.json()) as { error: string }; + + expect(response.status).toBe(401); + expect(body.error).toBe("Authorization header missing"); + }); + + it("returns 401 when authorization format is invalid", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "invalidformat", + }, + body: JSON.stringify({}), + }); + + const response = await capturedFetchHandler(request); + const body = (await response.json()) as { error: string }; + + expect(response.status).toBe(401); + expect(body.error).toBe("Invalid authorization format. Expected 'id:token'"); + }); + + it("returns 401 when client does not exist", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "unknownClient:token", + }, + body: JSON.stringify({}), + }); + + const response = await capturedFetchHandler(request); + const body = (await response.json()) as { error: string }; + + expect(response.status).toBe(401); + expect(body.error).toBe("Invalid credentials"); + }); + + it("returns 401 when token is wrong", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "validClient:wrongtoken", + }, + body: JSON.stringify({}), + }); + + const response = await capturedFetchHandler(request); + const body = (await response.json()) as { error: string }; + + expect(response.status).toBe(401); + expect(body.error).toBe("Invalid credentials"); + }); + + it("handles valid MCP request with correct credentials", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "validClient:secret123", + }, + body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 1 }), + }); + + const response = await capturedFetchHandler(request); + + // Response should not be 401 if auth is valid + expect(response.status).not.toBe(401); + // CORS headers should be present + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + }); + + it("includes CORS headers on 401 response", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + + const response = await capturedFetchHandler(request); + + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + }); + }); + + describe("Unknown routes", () => { + it("returns 404 for unknown paths", async () => { + const request = new Request("http://localhost:3001/unknown", { + method: "GET", + }); + + const response = await capturedFetchHandler(request); + + expect(response.status).toBe(404); + }); + + it("returns 404 for unknown POST paths", async () => { + const request = new Request("http://localhost:3001/api/unknown", { + method: "POST", + body: JSON.stringify({}), + }); + + const response = await capturedFetchHandler(request); + + expect(response.status).toBe(404); + }); + + it("includes CORS headers on 404", async () => { + const request = new Request("http://localhost:3001/unknown", { + method: "GET", + }); + + const response = await capturedFetchHandler(request); + + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + }); + }); + }); + + describe("CORS headers", () => { + beforeEach(async () => { + mockBunServe.mockClear(); + const { startMcpServer } = await import("../../src/mcp/server"); + await startMcpServer(); + }); + + it("exposes mcp-session-id header", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "OPTIONS", + }); + + const response = await capturedFetchHandler(request); + + expect(response.headers.get("Access-Control-Expose-Headers")).toContain("mcp-session-id"); + }); + + it("exposes mcp-protocol-version header", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "OPTIONS", + }); + + const response = await capturedFetchHandler(request); + + expect(response.headers.get("Access-Control-Expose-Headers")).toContain( + "mcp-protocol-version", + ); + }); + + it("allows mcp-session-id in requests", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "OPTIONS", + }); + + const response = await capturedFetchHandler(request); + + expect(response.headers.get("Access-Control-Allow-Headers")).toContain("mcp-session-id"); + }); + + it("allows DELETE method", async () => { + const request = new Request("http://localhost:3001/mcp", { + method: "OPTIONS", + }); + + const response = await capturedFetchHandler(request); + + expect(response.headers.get("Access-Control-Allow-Methods")).toContain("DELETE"); + }); + }); +}); + +describe("wrapToolHandler", () => { + it("wraps successful tool result in MCP format", async () => { + const { wrapToolHandler } = await import("../../src/mcp/server"); + const successHandler = async () => ({ success: true, data: "test" }); + + const result = await wrapToolHandler(successHandler, {}); + + expect(result.content).toBeDefined(); + expect(result.content.length).toBeGreaterThan(0); + expect(result.content[0].type).toBe("text"); + expect(result.content[0].text).toContain("success"); + expect(result.content[0].text).toContain("true"); + expect(result.isError).toBeUndefined(); + }); + + it("wraps Error in MCP error format", async () => { + const { wrapToolHandler } = await import("../../src/mcp/server"); + const errorHandler = async () => { + throw new Error("Test error message"); + }; + + const result = await wrapToolHandler(errorHandler, {}); + + expect(result.content).toBeDefined(); + expect(result.content.length).toBeGreaterThan(0); + expect(result.content[0].type).toBe("text"); + expect(result.content[0].text).toContain("Test error message"); + expect(result.isError).toBe(true); + }); + + it("handles non-Error thrown values", async () => { + const { wrapToolHandler } = await import("../../src/mcp/server"); + const errorHandler = async () => { + throw "string error"; + }; + + const result = await wrapToolHandler(errorHandler, {}); + + expect(result.content[0].text).toContain("Unknown error"); + expect(result.isError).toBe(true); + }); + + it("formats result as JSON with indentation", async () => { + const { wrapToolHandler } = await import("../../src/mcp/server"); + const handler = async () => ({ nested: { value: 123 } }); + + const result = await wrapToolHandler(handler, {}); + + expect(result.content[0].text).toContain("\n"); + expect(result.content[0].text).toContain(" "); + }); + + it("passes args to the handler", async () => { + const { wrapToolHandler } = await import("../../src/mcp/server"); + let receivedArgs: Record = {}; + const handler = async (args: Record) => { + receivedArgs = args; + return { received: true }; + }; + + await wrapToolHandler(handler, { temp: "23.5" }); + + expect(receivedArgs).toEqual({ temp: "23.5" }); + }); +}); diff --git a/tests/mocks/prisma.ts b/tests/mocks/prisma.ts index b5018cd..06d1b67 100644 --- a/tests/mocks/prisma.ts +++ b/tests/mocks/prisma.ts @@ -3,9 +3,9 @@ import { db } from "../../prisma/db"; export const mockPrisma = { client: { - findUnique: mock((..._args: never[]) => Promise.resolve(null)), - findFirst: mock((..._args: never[]) => Promise.resolve(null)), - upsert: mock((..._args: never[]) => Promise.resolve({})), + findUnique: mock((..._args: unknown[]) => Promise.resolve(null as unknown)), + findFirst: mock((..._args: unknown[]) => Promise.resolve(null as unknown)), + upsert: mock((..._args: unknown[]) => Promise.resolve({} as unknown)), }, homeState: { upsert: mock((..._args: never[]) => diff --git a/tests/utils/auth.test.ts b/tests/utils/auth.test.ts index 6c9d6b3..969f8ba 100644 --- a/tests/utils/auth.test.ts +++ b/tests/utils/auth.test.ts @@ -64,4 +64,21 @@ describe("verifyClientAuth", async () => { expect(result.clientId).toBe("validClient"); expect(result.error).toBeUndefined(); }); + + it("returns error when token decryption fails", async () => { + mockPrisma.client.findUnique.mockImplementation((args: unknown) => { + const typedArgs = args as { where?: { ClientID?: string } } | undefined; + if (typedArgs?.where?.ClientID === "corruptedClient") { + return Promise.resolve({ + ClientID: "corruptedClient", + ClientToken: "invalid-encrypted-data", + }); + } + return Promise.resolve(null); + }); + + const result = await verifyClientAuth("corruptedClient:anytoken"); + expect(result.valid).toBe(false); + expect(result.error).toBe("Invalid credentials"); + }); });