From 77198c4acdaa41d84e74c7237c89d0933ae37cbb Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Thu, 2 Apr 2026 19:47:32 +0200 Subject: [PATCH 1/7] Add vitest test dependency --- package-lock.json | 1321 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 6 +- 2 files changed, 1277 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 192cfba..4346803 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,45 @@ "@types/node": "^25.0.6", "@types/pg": "^8.11.10", "tsx": "^4.21.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.2" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -482,6 +520,13 @@ "hono": "^4" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -537,6 +582,315 @@ } } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -548,6 +902,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -568,6 +933,20 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -643,27 +1022,140 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/abort-controller": { @@ -736,6 +1228,16 @@ } } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -804,6 +1306,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -838,6 +1350,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -922,6 +1441,16 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -981,6 +1510,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -1056,6 +1592,16 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1095,6 +1641,16 @@ "node": ">=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -1178,6 +1734,24 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -1467,47 +2041,318 @@ "node": ">= 12" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", - "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", - "license": "MIT", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, "funding": { - "url": "https://github.com/sponsors/panva" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -1570,6 +2415,25 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1640,6 +2504,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1734,6 +2609,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", @@ -1832,6 +2714,26 @@ "node": ">= 18" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -1841,6 +2743,35 @@ "node": ">=16.20.0" } }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -1951,6 +2882,40 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -2117,6 +3082,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-git": { "version": "3.33.0", "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", @@ -2132,6 +3104,16 @@ "url": "https://github.com/steveukx/git-js?sponsor=1" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2141,6 +3123,13 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -2150,6 +3139,57 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2165,6 +3205,14 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -2237,6 +3285,166 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", @@ -2277,6 +3485,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index f8b495a..527451a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "dev": "tsx watch src/index.ts", "seed-index": "tsx scripts/seed-index.ts", "test-search": "tsx scripts/test-search.ts", - "integration-test": "tsx scripts/integration-test.ts" + "integration-test": "tsx scripts/integration-test.ts", + "test": "vitest run" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.25.2", @@ -30,6 +31,7 @@ "@types/node": "^25.0.6", "@types/pg": "^8.11.10", "tsx": "^4.21.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.2" } } From 48d7300f01788c8fcf07911df4f6023583511bf8 Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Thu, 2 Apr 2026 19:47:41 +0200 Subject: [PATCH 2/7] Add collect tool data model: types, DB schema, and queries Introduces CollectToolConfig Zod schema with YAML-defined field validation, a discriminated union (AnyToolConfigSchema) for tool type dispatch, the collected_data DB table, and insertCollectedData query. Renames ToolConfig -> SearchToolConfig and moves cross-field validation to ServerConfigSchema.superRefine. --- src/db/queries.ts | 18 +++++++++++++++++ src/db/schema.ts | 7 +++++++ src/types.ts | 51 ++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/db/queries.ts b/src/db/queries.ts index 5e32b92..b0b72e0 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -226,6 +226,24 @@ export async function upsertIndexState(state: IndexState): Promise { ]); } +// --------------------------------------------------------------------------- +// Collected data +// --------------------------------------------------------------------------- + +/** + * Insert a row into the collected_data table. + */ +export async function insertCollectedData( + toolName: string, + data: Record, +): Promise { + const pool = getPool(); + await pool.query( + "INSERT INTO collected_data (tool_name, data) VALUES ($1, $2)", + [toolName, JSON.stringify(data)], + ); +} + // --------------------------------------------------------------------------- // Statistics // --------------------------------------------------------------------------- diff --git a/src/db/schema.ts b/src/db/schema.ts index 5164e7b..47062b8 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -42,6 +42,13 @@ CREATE TABLE IF NOT EXISTS index_state ( CREATE INDEX IF NOT EXISTS idx_chunks_embedding ON chunks USING hnsw (embedding vector_cosine_ops); CREATE INDEX IF NOT EXISTS idx_chunks_source_name ON chunks (source_name); CREATE INDEX IF NOT EXISTS idx_chunks_repo_url ON chunks (repo_url); + +CREATE TABLE IF NOT EXISTS collected_data ( + id SERIAL PRIMARY KEY, + tool_name TEXT NOT NULL, + data JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); `; } diff --git a/src/types.ts b/src/types.ts index d49ae18..2dbd4af 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,17 +39,47 @@ export const SourceConfigSchema = z.object({ // ── Tool configuration schemas ──────────────────────────────────────────────── -export const ToolConfigSchema = z.object({ +const SearchToolConfigObjectSchema = z.object({ name: z.string().min(1), + type: z.literal('search'), description: z.string().min(1), source: z.string().min(1), default_limit: z.number().int().positive(), max_limit: z.number().int().positive(), result_format: z.enum(['docs', 'code', 'raw']), -}).refine(t => t.default_limit <= t.max_limit, { - message: 'default_limit must not exceed max_limit', }); +// SearchToolConfig type is inferred from the object schema directly. +// Cross-field validation (default_limit <= max_limit) lives in ServerConfigSchema.superRefine. +export const SearchToolConfigSchema = SearchToolConfigObjectSchema; + +export const CollectToolConfigSchema = z.object({ + name: z.string().min(1), + type: z.literal('collect'), + description: z.string().min(1), + response: z.string().min(1), + schema: z.record(z.string(), z.object({ + type: z.enum(['string', 'number', 'enum']), + description: z.string().optional(), + required: z.boolean().optional(), + values: z.array(z.string()).optional(), + }).refine(f => f.type !== 'enum' || (f.values && f.values.length > 0), { + message: 'enum fields must have a non-empty values array', + }).refine(f => f.type === 'enum' || !f.values, { + message: 'values is only valid for enum fields', + })).refine( + s => Object.keys(s).length > 0, + { message: 'collect tool schema must define at least one field' }, + ), +}); + +// Cross-field constraints (e.g. default_limit <= max_limit for search tools) +// are enforced in ServerConfigSchema.superRefine, not here. +export const AnyToolConfigSchema = z.discriminatedUnion('type', [ + SearchToolConfigObjectSchema, + CollectToolConfigSchema, +]); + // ── Embedding configuration schemas ─────────────────────────────────────────── export const EmbeddingConfigSchema = z.object({ @@ -81,10 +111,20 @@ export const ServerConfigSchema = z.object({ version: z.string().min(1), }), sources: z.array(SourceConfigSchema).min(1), - tools: z.array(ToolConfigSchema).min(1), + tools: z.array(AnyToolConfigSchema).min(1), embedding: EmbeddingConfigSchema, indexing: IndexingConfigSchema, webhook: WebhookConfigSchema.optional(), +}).superRefine((cfg, ctx) => { + for (const tool of cfg.tools) { + if (tool.type === 'search' && tool.default_limit > tool.max_limit) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Tool "${tool.name}": default_limit must not exceed max_limit`, + path: ['tools'], + }); + } + } }); // ── Inferred TypeScript types from Zod schemas ──────────────────────────────── @@ -92,7 +132,8 @@ export const ServerConfigSchema = z.object({ export type UrlDerivationConfig = z.infer; export type ChunkConfig = z.infer; export type SourceConfig = z.infer; -export type ToolConfig = z.infer; +export type SearchToolConfig = z.infer; +export type CollectToolConfig = z.infer; export type EmbeddingConfig = z.infer; export type IndexingConfig = z.infer; export type WebhookConfig = z.infer; From 6c80edf77e19efc37bce653470a6691a00a0e294 Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Thu, 2 Apr 2026 19:47:50 +0200 Subject: [PATCH 3/7] Implement collect tool registration and server dispatch Adds registerCollectTool which converts YAML field definitions to Zod shapes at registration time and writes validated input to the DB. Updates createMcpServer with an exhaustive switch on tool type. Renames ToolConfig -> SearchToolConfig in search tool and hardens its error response to avoid leaking internal details. --- src/mcp/server.ts | 15 +++++++- src/mcp/tools/collect.ts | 76 ++++++++++++++++++++++++++++++++++++++++ src/mcp/tools/search.ts | 10 +++--- 3 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 src/mcp/tools/collect.ts diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 4bc5af0..a47bdbc 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { EmbeddingClient } from '../indexing/embeddings.js'; import { getConfig, getServerConfig } from '../config.js'; import { registerSearchTool } from './tools/search.js'; +import { registerCollectTool } from './tools/collect.js'; /** * Creates a new McpServer instance with all tools registered. @@ -23,7 +24,19 @@ export function createMcpServer(): McpServer { }); for (const tool of serverCfg.tools) { - registerSearchTool(server, embeddingClient, tool); + const toolType = tool.type; + switch (toolType) { + case 'collect': + registerCollectTool(server, tool); + break; + case 'search': + registerSearchTool(server, embeddingClient, tool); + break; + default: { + const _exhaustive: never = toolType; + throw new Error(`Unknown tool type "${_exhaustive}" for tool "${(tool as any).name}"`); + } + } } return server; diff --git a/src/mcp/tools/collect.ts b/src/mcp/tools/collect.ts new file mode 100644 index 0000000..3698557 --- /dev/null +++ b/src/mcp/tools/collect.ts @@ -0,0 +1,76 @@ +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { CollectToolConfig } from '../../types.js'; +import { insertCollectedData } from '../../db/queries.js'; + +type FieldDef = CollectToolConfig['schema'][string]; + +/** + * Convert a YAML-defined schema (record of field definitions) into a Zod shape record. + */ +export function yamlSchemaToZod(schema: CollectToolConfig['schema']): Record { + const shape: Record = {}; + + for (const [fieldName, field] of Object.entries(schema) as [string, FieldDef][]) { + let fieldSchema: z.ZodTypeAny; + + switch (field.type) { + case 'string': + fieldSchema = z.string(); + break; + case 'number': + fieldSchema = z.number(); + break; + case 'enum': + fieldSchema = z.enum(field.values as [string, ...string[]]); + break; + default: + throw new Error(`Unsupported field type "${field.type}" for field "${fieldName}"`); + } + + if (field.description) { + fieldSchema = fieldSchema.describe(field.description); + } + + if (!field.required) { + fieldSchema = fieldSchema.optional(); + } + + shape[fieldName] = fieldSchema; + } + + return shape; +} + +/** + * Register a collect tool on the MCP server. + * The tool validates inputs against the YAML-defined schema and writes to the DB. + * On DB failure, logs the error detail server-side and returns a generic error to the caller. + */ +export function registerCollectTool( + server: McpServer, + toolConfig: CollectToolConfig, +): void { + const zodShape = yamlSchemaToZod(toolConfig.schema); + + server.tool( + toolConfig.name, + toolConfig.description, + zodShape, + async (input) => { + try { + await insertCollectedData(toolConfig.name, input); + return { + content: [{ type: "text" as const, text: toolConfig.response }], + }; + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + console.error(`[${toolConfig.name}] Error inserting collected data: ${detail}`); + return { + content: [{ type: "text" as const, text: "Error: Failed to store data. Please try again later." }], + isError: true, + }; + } + }, + ); +} diff --git a/src/mcp/tools/search.ts b/src/mcp/tools/search.ts index ac13803..6c7845f 100644 --- a/src/mcp/tools/search.ts +++ b/src/mcp/tools/search.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { EmbeddingClient } from '../../indexing/embeddings.js'; -import type { ToolConfig, ChunkResult } from '../../types.js'; +import type { SearchToolConfig, ChunkResult } from '../../types.js'; import { searchChunks } from '../../db/queries.js'; function formatDocsResults(results: ChunkResult[]): string { @@ -47,7 +47,7 @@ function formatResults(results: ChunkResult[], format: string): string { export function registerSearchTool( server: McpServer, embeddingClient: EmbeddingClient, - toolConfig: ToolConfig, + toolConfig: SearchToolConfig, ): void { const inputSchema = { query: z.string().describe("The search query"), @@ -69,10 +69,10 @@ export function registerSearchTool( content: [{ type: "text" as const, text: formatResults(results, toolConfig.result_format) }], }; } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error(`[${toolConfig.name}] Error: ${message}`); + const detail = error instanceof Error ? error.message : String(error); + console.error(`[${toolConfig.name}] Error: ${detail}`); return { - content: [{ type: "text" as const, text: `Error: ${message}` }], + content: [{ type: "text" as const, text: "Error: Search failed. Please try again later." }], isError: true, }; } From ea39581e6efadacb3327bc61412558d14e477c35 Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Thu, 2 Apr 2026 19:47:56 +0200 Subject: [PATCH 4/7] Add config validation for collect tools and submit-feedback definition Defaults tool type to 'search' for backwards compatibility, validates tool name uniqueness, scopes source cross-validation to search tools only. Adds submit-feedback collect tool to mcp-docs.yaml and example. --- mcp-docs.example.yaml | 21 +++++++++++++++++++++ mcp-docs.yaml | 23 +++++++++++++++++++++++ src/config.ts | 21 +++++++++++++++++++-- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/mcp-docs.example.yaml b/mcp-docs.example.yaml index a5b42e6..8d556d6 100644 --- a/mcp-docs.example.yaml +++ b/mcp-docs.example.yaml @@ -24,6 +24,27 @@ tools: max_limit: 20 result_format: docs + # Collect tools write structured data from agents to the database. + # Define input schema, description, and a canned response — no code needed. + - name: submit-feedback + type: collect + description: "Submit feedback on whether search results were helpful." + response: "Feedback recorded. Thank you." + schema: + tool_name: + type: string + description: "Which search tool was used" + required: true + rating: + type: enum + values: ["helpful", "not_helpful"] + description: "Whether the results were helpful" + required: true + comment: + type: string + description: "What worked or didn't work" + required: true + embedding: provider: openai model: text-embedding-3-small diff --git a/mcp-docs.yaml b/mcp-docs.yaml index c514be4..0fb8621 100644 --- a/mcp-docs.yaml +++ b/mcp-docs.yaml @@ -118,6 +118,29 @@ tools: max_limit: 20 result_format: code + - name: submit-feedback + type: collect + description: "After using search results, you can submit feedback on whether the advice worked or not. Report what you tried and whether it succeeded or failed. If something failed but you later found the missing information elsewhere, mention that too. This feedback helps improve search quality." + response: "Feedback recorded. Thank you." + schema: + tool_name: + type: string + description: "Which search tool was used" + required: true + query: + type: string + description: "The original search query" + required: true + rating: + type: enum + values: ["helpful", "not_helpful"] + description: "Whether the results were helpful" + required: true + comment: + type: string + description: "What was tried, what failed/worked, what info was missing" + required: true + embedding: provider: openai model: text-embedding-3-small diff --git a/src/config.ts b/src/config.ts index 55962de..317811c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -99,6 +99,16 @@ function loadServerConfig(): ServerConfig { const raw = readFileSync(configPath, 'utf-8'); const parsed = parseYaml(raw); + // Default tool type to 'search' for backwards compatibility + if (Array.isArray(parsed?.tools)) { + for (const tool of parsed.tools) { + if (tool && typeof tool === 'object' && !('type' in tool)) { + console.warn(`[config] Tool "${tool.name}" has no type field — defaulting to "search". Add "type: search" explicitly to silence this warning.`); + tool.type = 'search'; + } + } + } + const result = ServerConfigSchema.safeParse(parsed); if (!result.success) { const issues = result.error.issues @@ -113,8 +123,15 @@ function loadServerConfig(): ServerConfig { throw new Error('Duplicate source names found in sources configuration.'); } - // Cross-validate: every tool.source must reference an existing source name - for (const tool of result.data.tools) { + // Validate tool name uniqueness + const toolNames = new Set(result.data.tools.map(t => t.name)); + if (toolNames.size !== result.data.tools.length) { + throw new Error('Duplicate tool names found in tools configuration.'); + } + + // Cross-validate: every search tool's source must reference an existing source name + const searchTools = result.data.tools.filter(t => t.type === 'search'); + for (const tool of searchTools) { if (!sourceNames.has(tool.source)) { throw new Error( `Tool "${tool.name}" references source "${tool.source}" which is not defined in sources.` From ab65bf212fa68def313bc24ee258c6ebda3a3598 Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Thu, 2 Apr 2026 19:48:02 +0200 Subject: [PATCH 5/7] Add request logging for collect tool calls Branches MCP request logging to show a JSON data preview for collect tools instead of the query/limit format used for search tools. --- src/index.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index a863aea..f68bd75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,10 +93,16 @@ app.post("/mcp", async (req: Request, res: Response) => { const params = req.body?.params as Record | undefined; const toolName = params?.name ?? 'unknown'; const args = params?.arguments as Record | undefined; - const query = args?.query ?? ''; - const limit = args?.limit; - const extra = limit ? ` limit=${limit}` : ''; - console.log(`[mcp] ${toolName}("${query}"${extra}) [${ip}]`); + const toolCfg = getServerConfig().tools.find(t => t.name === toolName); + if (toolCfg?.type === 'collect') { + const dataPreview = JSON.stringify(args ?? {}).slice(0, 200); + console.log(`[mcp] ${toolName}(${dataPreview}) [${ip}]`); + } else { + const query = args?.query ?? ''; + const limit = args?.limit; + const extra = limit ? ` limit=${limit}` : ''; + console.log(`[mcp] ${toolName}("${query}"${extra}) [${ip}]`); + } } else if (method === 'tools/list') { console.log(`[mcp] tools/list [${ip}]`); } From b820ee6a09364311cffa8013acffcdd638188d0f Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Thu, 2 Apr 2026 19:48:09 +0200 Subject: [PATCH 6/7] Add tests for collect tools, schema validation, and config parsing Covers yamlSchemaToZod conversion, MCP protocol round-trip (tool listing, successful call, DB failure, invalid input), CollectToolConfig and AnyToolConfigSchema validation, backwards-compat type defaulting, and ServerConfigSchema cross-field checks. --- src/__tests__/collect-mcp.test.ts | 113 +++++++++++++ src/__tests__/collect.test.ts | 128 +++++++++++++++ src/__tests__/tool-config.test.ts | 255 ++++++++++++++++++++++++++++++ 3 files changed, 496 insertions(+) create mode 100644 src/__tests__/collect-mcp.test.ts create mode 100644 src/__tests__/collect.test.ts create mode 100644 src/__tests__/tool-config.test.ts diff --git a/src/__tests__/collect-mcp.test.ts b/src/__tests__/collect-mcp.test.ts new file mode 100644 index 0000000..b8ddffa --- /dev/null +++ b/src/__tests__/collect-mcp.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { registerCollectTool } from '../mcp/tools/collect.js'; +import type { CollectToolConfig } from '../types.js'; + +vi.mock('../db/queries.js', () => ({ + insertCollectedData: vi.fn(), +})); + +import { insertCollectedData } from '../db/queries.js'; + +const toolConfig: CollectToolConfig = { + name: 'submit-feedback', + type: 'collect', + description: 'Submit feedback on search results.', + response: 'Feedback recorded. Thank you.', + schema: { + tool_name: { type: 'string', description: 'Which search tool', required: true }, + query: { type: 'string', description: 'The query', required: true }, + rating: { type: 'enum', values: ['helpful', 'not_helpful'], description: 'Rating', required: true }, + comment: { type: 'string', description: 'Details', required: true }, + }, +}; + +describe('collect tool via MCP protocol', () => { + let client: Client; + let server: McpServer; + + beforeAll(async () => { + server = new McpServer({ name: 'test', version: '1.0.0' }); + registerCollectTool(server, toolConfig); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + + client = new Client({ name: 'test-client', version: '1.0.0' }); + await client.connect(clientTransport); + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterAll(async () => { + await client.close(); + await server.close(); + }); + + it('lists the collect tool', async () => { + const { tools } = await client.listTools(); + const tool = tools.find(t => t.name === 'submit-feedback'); + + expect(tool).toBeDefined(); + expect(tool!.description).toBe(toolConfig.description); + expect(tool!.inputSchema.properties).toHaveProperty('tool_name'); + expect(tool!.inputSchema.properties).toHaveProperty('rating'); + }); + + it('calls the tool, returns canned response, and writes to DB', async () => { + const args = { + tool_name: 'search-docs', + query: 'how to auth', + rating: 'not_helpful', + comment: 'Docs referenced a deprecated API', + }; + + const result = await client.callTool({ + name: 'submit-feedback', + arguments: args, + }); + + expect(result.isError).toBeFalsy(); + const text = (result.content as Array<{ type: string; text: string }>)[0].text; + expect(text).toBe('Feedback recorded. Thank you.'); + + expect(insertCollectedData).toHaveBeenCalledWith('submit-feedback', args); + }); + + it('returns generic error on DB failure', async () => { + vi.mocked(insertCollectedData).mockRejectedValueOnce( + new Error('connection refused to 10.0.0.5:5432'), + ); + + const result = await client.callTool({ + name: 'submit-feedback', + arguments: { + tool_name: 'search-docs', + query: 'test', + rating: 'helpful', + comment: 'worked', + }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0].text; + expect(text).toBe('Error: Failed to store data. Please try again later.'); + expect(text).not.toContain('10.0.0.5'); + }); + + it('rejects invalid input', async () => { + const result = await client.callTool({ + name: 'submit-feedback', + arguments: { + tool_name: 'search-docs', + // missing required fields + }, + }); + + expect(result.isError).toBe(true); + }); +}); diff --git a/src/__tests__/collect.test.ts b/src/__tests__/collect.test.ts new file mode 100644 index 0000000..8bf7e48 --- /dev/null +++ b/src/__tests__/collect.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; +import { yamlSchemaToZod } from '../mcp/tools/collect.js'; +import type { CollectToolConfig } from '../types.js'; + +type Schema = CollectToolConfig['schema']; + +describe('yamlSchemaToZod', () => { + it('converts string fields', () => { + const schema: Schema = { + name: { type: 'string', required: true }, + }; + const shape = yamlSchemaToZod(schema); + const zod = z.object(shape); + + expect(zod.parse({ name: 'hello' })).toEqual({ name: 'hello' }); + expect(() => zod.parse({ name: 123 })).toThrow(); + expect(() => zod.parse({})).toThrow(); + }); + + it('converts number fields', () => { + const schema: Schema = { + count: { type: 'number', required: true }, + }; + const shape = yamlSchemaToZod(schema); + const zod = z.object(shape); + + expect(zod.parse({ count: 42 })).toEqual({ count: 42 }); + expect(() => zod.parse({ count: 'not a number' })).toThrow(); + }); + + it('converts enum fields', () => { + const schema: Schema = { + rating: { type: 'enum', values: ['good', 'bad'], required: true }, + }; + const shape = yamlSchemaToZod(schema); + const zod = z.object(shape); + + expect(zod.parse({ rating: 'good' })).toEqual({ rating: 'good' }); + expect(() => zod.parse({ rating: 'neutral' })).toThrow(); + }); + + it('makes fields optional when required is false', () => { + const schema: Schema = { + comment: { type: 'string', required: false }, + }; + const shape = yamlSchemaToZod(schema); + const zod = z.object(shape); + + expect(zod.parse({})).toEqual({}); + expect(zod.parse({ comment: 'hi' })).toEqual({ comment: 'hi' }); + }); + + it('makes fields optional when required is omitted', () => { + const schema: Schema = { + comment: { type: 'string' }, + }; + const shape = yamlSchemaToZod(schema); + const zod = z.object(shape); + + expect(zod.parse({})).toEqual({}); + }); + + it('attaches descriptions', () => { + const schema: Schema = { + query: { type: 'string', description: 'The search query', required: true }, + }; + const shape = yamlSchemaToZod(schema); + + expect(shape.query.description).toBe('The search query'); + }); + + it('rejects invalid inputs against the feedback schema', () => { + const schema: Schema = { + tool_name: { type: 'string', required: true }, + query: { type: 'string', required: true }, + rating: { type: 'enum', values: ['helpful', 'not_helpful'], required: true }, + comment: { type: 'string', required: true }, + }; + const shape = yamlSchemaToZod(schema); + const zod = z.object(shape); + + // completely empty + expect(zod.safeParse({}).success).toBe(false); + + // missing required fields + expect(zod.safeParse({ tool_name: 'search-docs' }).success).toBe(false); + + // wrong type for string field + expect(zod.safeParse({ + tool_name: 123, query: 'test', rating: 'helpful', comment: 'ok', + }).success).toBe(false); + + // invalid enum value + expect(zod.safeParse({ + tool_name: 'search-docs', query: 'test', rating: 'meh', comment: 'ok', + }).success).toBe(false); + + // wrong type for enum field + expect(zod.safeParse({ + tool_name: 'search-docs', query: 'test', rating: 42, comment: 'ok', + }).success).toBe(false); + }); + + it('throws on unsupported field type', () => { + const schema = { + flag: { type: 'boolean' as unknown as 'string', required: true }, + }; + expect(() => yamlSchemaToZod(schema)).toThrow('Unsupported field type "boolean" for field "flag"'); + }); + + it('handles a full feedback schema', () => { + const schema: Schema = { + tool_name: { type: 'string', description: 'Which tool', required: true }, + query: { type: 'string', description: 'The query', required: true }, + rating: { type: 'enum', values: ['helpful', 'not_helpful'], description: 'Rating', required: true }, + comment: { type: 'string', description: 'Details', required: true }, + }; + const shape = yamlSchemaToZod(schema); + const zod = z.object(shape); + + const valid = { tool_name: 'search-docs', query: 'how to auth', rating: 'helpful', comment: 'worked great' }; + expect(zod.parse(valid)).toEqual(valid); + + expect(() => zod.parse({ ...valid, rating: 'meh' })).toThrow(); + expect(() => zod.parse({ tool_name: 'search-docs', query: 'test', rating: 'helpful' })).toThrow(); + }); +}); diff --git a/src/__tests__/tool-config.test.ts b/src/__tests__/tool-config.test.ts new file mode 100644 index 0000000..7f4a235 --- /dev/null +++ b/src/__tests__/tool-config.test.ts @@ -0,0 +1,255 @@ +import { describe, it, expect } from 'vitest'; +import { CollectToolConfigSchema, AnyToolConfigSchema, ServerConfigSchema } from '../types.js'; + +describe('CollectToolConfigSchema', () => { + const validCollect = { + name: 'submit-feedback', + type: 'collect' as const, + description: 'Submit feedback', + response: 'Thanks!', + schema: { + rating: { type: 'enum' as const, values: ['good', 'bad'], required: true }, + }, + }; + + it('parses a valid collect tool config', () => { + const result = CollectToolConfigSchema.safeParse(validCollect); + expect(result.success).toBe(true); + }); + + it('rejects missing name', () => { + const { name, ...rest } = validCollect; + expect(CollectToolConfigSchema.safeParse(rest).success).toBe(false); + }); + + it('rejects missing description', () => { + const { description, ...rest } = validCollect; + expect(CollectToolConfigSchema.safeParse(rest).success).toBe(false); + }); + + it('rejects missing response', () => { + const { response, ...rest } = validCollect; + expect(CollectToolConfigSchema.safeParse(rest).success).toBe(false); + }); + + it('rejects missing schema', () => { + const { schema, ...rest } = validCollect; + expect(CollectToolConfigSchema.safeParse(rest).success).toBe(false); + }); + + it('rejects enum field without values', () => { + const config = { + ...validCollect, + schema: { + rating: { type: 'enum' as const, required: true }, + }, + }; + expect(CollectToolConfigSchema.safeParse(config).success).toBe(false); + }); + + it('rejects enum field with empty values', () => { + const config = { + ...validCollect, + schema: { + rating: { type: 'enum' as const, values: [], required: true }, + }, + }; + expect(CollectToolConfigSchema.safeParse(config).success).toBe(false); + }); + + it('rejects unknown field type', () => { + const config = { + ...validCollect, + schema: { + data: { type: 'boolean', required: true }, + }, + }; + expect(CollectToolConfigSchema.safeParse(config).success).toBe(false); + }); + + it('rejects values on non-enum fields', () => { + const config = { + ...validCollect, + schema: { + name: { type: 'string' as const, values: ['a', 'b'], required: true }, + }, + }; + expect(CollectToolConfigSchema.safeParse(config).success).toBe(false); + }); +}); + +describe('AnyToolConfigSchema', () => { + it('parses a search tool with explicit type', () => { + const config = { + name: 'search-docs', + type: 'search', + description: 'Search docs', + source: 'docs', + default_limit: 5, + max_limit: 20, + result_format: 'docs', + }; + const result = AnyToolConfigSchema.safeParse(config); + expect(result.success).toBe(true); + if (result.success) expect(result.data.type).toBe('search'); + }); + + it('parses a collect tool', () => { + const config = { + name: 'feedback', + type: 'collect', + description: 'Give feedback', + response: 'OK', + schema: { note: { type: 'string' } }, + }; + const result = AnyToolConfigSchema.safeParse(config); + expect(result.success).toBe(true); + if (result.success) expect(result.data.type).toBe('collect'); + }); + + it('rejects unknown tool type', () => { + const config = { + name: 'mystery', + type: 'magic', + description: 'Does magic', + }; + expect(AnyToolConfigSchema.safeParse(config).success).toBe(false); + }); + + it('rejects collect tool with empty schema', () => { + const config = { + name: 'feedback', + type: 'collect', + description: 'Give feedback', + response: 'OK', + schema: {}, + }; + expect(AnyToolConfigSchema.safeParse(config).success).toBe(false); + }); + + it('discriminates correctly between search and collect fields', () => { + // A collect tool should not need source/limits + const collect = { + name: 'feedback', + type: 'collect', + description: 'Give feedback', + response: 'OK', + schema: { note: { type: 'string' } }, + }; + expect(AnyToolConfigSchema.safeParse(collect).success).toBe(true); + + // A search tool should not need response/schema + const search = { + name: 'search', + type: 'search', + description: 'Search', + source: 'docs', + default_limit: 5, + max_limit: 20, + result_format: 'docs', + }; + expect(AnyToolConfigSchema.safeParse(search).success).toBe(true); + }); +}); + +describe('backwards-compat config defaulting', () => { + it('injects type "search" for tools missing a type field', () => { + // Mirrors the defaulting loop in loadServerConfig() from config.ts + const tools: Record[] = [ + { + name: 'search-docs', + description: 'Search docs', + source: 'docs', + default_limit: 5, + max_limit: 20, + result_format: 'docs', + }, + ]; + + for (const tool of tools) { + if (typeof tool === 'object' && tool !== null && !('type' in tool)) { + (tool as Record).type = 'search'; + } + } + + const result = AnyToolConfigSchema.safeParse(tools[0]); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('search'); + } + }); + + it('does not overwrite an explicit type field', () => { + const tools: Record[] = [ + { + name: 'feedback', + type: 'collect', + description: 'Give feedback', + response: 'OK', + schema: { note: { type: 'string' } }, + }, + ]; + + for (const tool of tools) { + if (typeof tool === 'object' && tool !== null && !('type' in tool)) { + (tool as Record).type = 'search'; + } + } + + const result = AnyToolConfigSchema.safeParse(tools[0]); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('collect'); + } + }); +}); + +describe('ServerConfigSchema', () => { + const minimalConfig = { + server: { name: 'test', version: '1.0.0' }, + sources: [{ + name: 'docs', + type: 'markdown', + repo: 'https://github.com/test/test.git', + path: 'docs/', + file_patterns: ['**/*.md'], + chunk: { target_tokens: 600, overlap_tokens: 50 }, + }], + embedding: { provider: 'openai', model: 'text-embedding-3-small', dimensions: 1536 }, + indexing: { auto_reindex: true, reindex_hour_utc: 3, stale_threshold_hours: 24 }, + }; + + it('rejects search tool where default_limit > max_limit', () => { + const config = { + ...minimalConfig, + tools: [{ + name: 'search-docs', + type: 'search', + description: 'Search', + source: 'docs', + default_limit: 30, + max_limit: 10, + result_format: 'docs', + }], + }; + const result = ServerConfigSchema.safeParse(config); + expect(result.success).toBe(false); + }); + + it('accepts search tool where default_limit <= max_limit', () => { + const config = { + ...minimalConfig, + tools: [{ + name: 'search-docs', + type: 'search', + description: 'Search', + source: 'docs', + default_limit: 5, + max_limit: 20, + result_format: 'docs', + }], + }; + const result = ServerConfigSchema.safeParse(config); + expect(result.success).toBe(true); + }); +}); From 7bd3fe50452e613563a35c70410c0fae74bacea1 Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Thu, 2 Apr 2026 19:48:16 +0200 Subject: [PATCH 7/7] Document collect tools in README Adds Collect Tools section with schema field reference and example config. Renames 'Tools' header to 'Search Tools' for clarity. Adds npm test to the development commands. --- README.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d35e177..107e246 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ sources: overlap_lines: 10 ``` -### Tools +### Search Tools -Each tool maps to a source and defines the MCP tool interface: +Each search tool maps to a source and defines the MCP tool interface: ```yaml tools: @@ -83,6 +83,36 @@ tools: result_format: docs ``` +### Collect Tools + +Collect tools let agents write structured data back to the server. Unlike search tools, they don't query anything — they validate the agent's input against a YAML-defined schema and store it as JSONB in the database. Use them to gather signal from agents without writing any code. + +The first built-in use case is search feedback: agents report whether search results were helpful, what they tried, and what went wrong. This surfaces broken or misleading documentation quickly. But collect tools are generic — you can define any schema for any use case (e.g., broken link reporting, feature requests, error logging). + +```yaml +tools: + - name: submit-feedback + type: collect + description: "Submit feedback on whether search results were helpful." + response: "Feedback recorded. Thank you." + schema: + tool_name: + type: string + description: "Which search tool was used" + required: true + rating: + type: enum + values: ["helpful", "not_helpful"] + description: "Whether the results were helpful" + required: true + comment: + type: string + description: "What worked or didn't work" + required: true +``` + +Each field in `schema` supports `type` (`string`, `number`, or `enum`), an optional `description` (shown to the agent), `required` (defaults to false), and `values` (required for `enum` fields). The validated input is written as JSONB to the `collected_data` table along with the tool name and a timestamp. + ### Built-in Chunker Types | Type | Best For | Splits On | @@ -187,6 +217,9 @@ docker compose up # Seed index docker compose exec app npx tsx scripts/seed-index.ts +# Run unit tests +npm test + # Test search docker compose exec app npx tsx scripts/test-search.ts "your query"