benjivers/jev-mcp Public Public: anyone can read this; nobody can change it.
workbenchthis machine
feat: MCP server exposing TypeSafe Jev as typed judgment tools
Four tools over stdio: jev_classify (Choice), jev_score (Score),
jev_check (Noul), and jev_ask, which batches several questions about one
state into a single request. jev_models confirms the key and lists what
it can reach.
Design rules, each of which came from a defect found on the wire:
- The caller owns the option set. jev_classify requires options, so the
model can pick the wrong one but can never invent one. A selector
cannot choose a candidate the enumerator dropped.
- Probabilities are always returned, never just the winning label.
- The key comes from the environment or a 0600 file, never a tool
argument. An MCP server inherits the client's environment rather than
your shell's, so a key exported from a shell profile never arrives.
- Nothing is silently dropped, overwritten or truncated. An option the
caller names "none" is preserved and the no-match key is reported
back; oversized state is rejected rather than trimmed.
- Only JSON-RPC reaches stdout. The SDK's default logger writes to
stdout, which corrupts a stdio transport, so all levels go to stderr.
The offline suite drives the real built server over stdio against a local
stand-in for the API and asserts on the request bodies actually sent, so
a regression in what reaches the model fails the build.
13 files
changed +3010
−0
.github/workflows/ci.yml added +34
| @@ -0,0 +1,34 @@ | ||
| 1 | name: CI | |
| 2 | ||
| 3 | on: | |
| 4 | push: | |
| 5 | branches: [main] | |
| 6 | pull_request: | |
| 7 | ||
| 8 | jobs: | |
| 9 | test: | |
| 10 | runs-on: ubuntu-latest | |
| 11 | strategy: | |
| 12 | fail-fast: false | |
| 13 | matrix: | |
| 14 | node-version: [20, 22, 24] | |
| 15 | steps: | |
| 16 | - uses: actions/checkout@v4 | |
| 17 | - uses: actions/setup-node@v4 | |
| 18 | with: | |
| 19 | node-version: ${{ matrix.node-version }} | |
| 20 | # Dependency install scripts stay blocked; this package needs none. | |
| 21 | - run: npm ci --ignore-scripts | |
| 22 | - run: npm run typecheck | |
| 23 | - run: npm run build | |
| 24 | - run: npm test | |
| 25 | ||
| 26 | audit: | |
| 27 | runs-on: ubuntu-latest | |
| 28 | steps: | |
| 29 | - uses: actions/checkout@v4 | |
| 30 | - uses: actions/setup-node@v4 | |
| 31 | with: | |
| 32 | node-version: 22 | |
| 33 | - run: npm ci --ignore-scripts | |
| 34 | - run: npm audit --audit-level=high |
.gitignore added +3
| @@ -0,0 +1,3 @@ | ||
| 1 | node_modules/ | |
| 2 | dist/ | |
| 3 | .env |
LICENSE added +21
| @@ -0,0 +1,21 @@ | ||
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) 2026 rashed parvez | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. |
README.md added +167
| @@ -0,0 +1,167 @@ | ||
| 1 | # jev-mcp | |
| 2 | ||
| 3 | An MCP server that exposes [](https://docs.typesafe.ai) as typed judgment tools. | |
| 4 | ||
| 5 | Jev is a System One model. It returns a typed answer and a calibrated probability | |
| 6 | distribution, never prose. These tools surface that faithfully rather than hiding | |
| 7 | it behind a label. | |
| 8 | ||
| 9 | ## Install | |
| 10 | ||
| 11 | Requires Node 20.12+ and a TypeSafe API key from | |
| 12 | [](https://console.typesafe.ai/settings/keys). | |
| 13 | ||
| 14 | bash | |
| 15 | npm install | |
| 16 | npm run build | |
| 17 | ||
| 18 | ||
| 19 | Register with Claude Code: | |
| 20 | ||
| 21 | bash | |
| 22 | claude mcp add --scope user jev -- node /absolute/path/to/jev-mcp/dist/index.js | |
| 23 | ||
| 24 | ||
| 25 | The key is read from `TYPESAFE_API_KEY` in the server's environment. It is never a | |
| 26 | tool argument, so it cannot land in a transcript or in a model's context. Put it in | |
| 27 | `~/.zshenv` rather than in any repo, and make sure it is **exported**: | |
| 28 | ||
| 29 | sh | |
| 30 | export TYPESAFE_API_KEY="ts_..." | |
| 31 | ||
| 32 | ||
| 33 | Without `export` the variable exists only in the shell that read it. Child processes, | |
| 34 | including every MCP server Claude Code spawns, never see it, and every tool call fails | |
| 35 | with a missing-key error. | |
| 36 | ||
| 37 | ## Tools | |
| 38 | ||
| 39 | | Tool | Primitive | Use when | | |
| 40 | | --- | --- | --- | | |
| 41 | | `jev_classify` | Choice | The answer is one of a fixed set you define | | |
| 42 | | `jev_score` | Score | The answer is a degree on an ordered scale | | |
| 43 | | `jev_check` | Noul | The answer is yes or no, and you want the probability | | |
| 44 | | `jev_ask` | all three | You have several questions about the same state | | |
| 45 | | `jev_models` | — | Confirm the key works and find a model id | | |
| 46 | ||
| 47 | Every tool returns the full probability distribution alongside the answer, plus | |
| 48 | `confidence` for Choice and Score. Results come back as MCP structured content, so a | |
| 49 | client gets typed data rather than a JSON string to re-parse. | |
| 50 | ||
| 51 | ### Prefer `jev_ask` | |
| 52 | ||
| 53 | Jev prefills the state once and scores every question in a single forward pass, so | |
| 54 | extra questions add almost no latency or cost. TypeSafe's own | |
| 55 | [](https://docs.typesafe.ai/cookbooks/parallel_questions) on a | |
| 56 | document-dominated workload puts one batched call at 12.2x cheaper and 10.0x faster | |
| 57 | than one call per question, with no change in the answers. | |
| 58 | ||
| 59 | Questions in one request cannot see each other's answers. State any speculative | |
| 60 | premise explicitly and let your own code decide which answers apply. | |
| 61 | ||
| 62 | ## Design rules | |
| 63 | ||
| 64 | 1. **The caller owns the option set.** `jev_classify` requires options from you, so | |
| 65 | the model can pick the wrong one but can never invent one. A selector cannot | |
| 66 | choose a candidate the enumerator dropped. This is the single most common way | |
| 67 | these integrations fail. | |
| 68 | 2. **Probabilities are always returned.** Not just the winner. | |
| 69 | 3. **The key lives in the environment.** Never in an argument. | |
| 70 | 4. **Nothing is silently dropped, overwritten, or truncated.** A request that cannot | |
| 71 | be honoured exactly fails with a reason instead of quietly changing meaning. | |
| 72 | 5. **Only JSON-RPC reaches stdout.** Logs go to stderr, always. | |
| 73 | ||
| 74 | ### The no-match option | |
| 75 | ||
| 76 | Each selecting tool adds a `none` option by default so the model can decline rather | |
| 77 | than being forced to pick. Turn it off with `add_none: false` when one option must | |
| 78 | always apply. | |
| 79 | ||
| 80 | If you already use the name `none` for an option of your own, the added option takes | |
| 81 | a different key instead of overwriting yours, and the response reports which key | |
| 82 | carries the no-match meaning in `none_option`. Your option and its probability always | |
| 83 | survive intact. | |
| 84 | ||
| 85 | ### Confidence gating | |
| 86 | ||
| 87 | Choice and Score answers include an `action` of `act`, `review`, or `abstain`, derived | |
| 88 | from `confidence` and the thresholds you pass in `act_above` and `review_above` | |
| 89 | (defaults 0.8 and 0.5). `jev_check` returns a `verdict` of `yes`, `no`, or `uncertain` | |
| 90 | from `yes_at_or_above` and `no_at_or_below` (defaults 0.7 and 0.3). | |
| 91 | ||
| 92 | These defaults are starting points, not universal rules. Calibrate them on your own | |
| 93 | data and on what it costs to be wrong; a destructive action deserves a higher bar than | |
| 94 | a read-only one. Confidence describes how concentrated the distribution is. It is not | |
| 95 | a claim that the answer is correct. | |
| 96 | ||
| 97 | A Noul near 0.5 means yes and no are close to equally likely, not that the answer is | |
| 98 | "medium", which is why the middle band reports `uncertain` rather than rounding. | |
| 99 | ||
| 100 | ### Errors | |
| 101 | ||
| 102 | Failures come back with `isError` and a classified body: `kind`, `retryable`, and | |
| 103 | where available `status`, `requestId`, and a `hint`. A rejected key (`authentication`, | |
| 104 | never retryable) is distinguishable from a rate limit (`rate_limit`, retryable) and | |
| 105 | from a malformed question (`invalid_request`). The SDK already retries 408, 429, and | |
| 106 | 5xx with backoff before an error surfaces here. | |
| 107 | ||
| 108 | ### Limits | |
| 109 | ||
| 110 | A Choice question accepts at most 255 options, which is an API limit; past that, | |
| 111 | search in two passes, one question picking a window and a second ranking within it. | |
| 112 | Question count and state size are local caps that bound cost on a single call, and | |
| 113 | both are configurable. Oversized state is rejected rather than truncated, because | |
| 114 | truncating silently changes the material the judgment rests on. | |
| 115 | ||
| 116 | ## Configuration | |
| 117 | ||
| 118 | | Variable | Effect | | |
| 119 | | --- | --- | | |
| 120 | | `TYPESAFE_API_KEY` | Required. `JEV_API_KEY` also works. | | |
| 121 | | `JEV_MODEL` | Model id. Defaults to `jev-latest`. | | |
| 122 | | `JEV_TIMEOUT_MS` | Per-attempt timeout. Defaults to 15000. | | |
| 123 | | `JEV_MAX_QUESTIONS` | Questions per `jev_ask`. Defaults to 64. | | |
| 124 | | `JEV_MAX_STATE_CHARS` | Largest state accepted. Defaults to 200000. | | |
| 125 | | `TYPESAFE_LOG_LEVEL` | SDK verbosity. Safe at any level; all output goes to stderr. | | |
| 126 | ||
| 127 | An unusable value for any numeric setting falls back to the default and warns on | |
| 128 | stderr, rather than becoming `NaN` and disabling the limit it was meant to enforce. | |
| 129 | ||
| 130 | ## Troubleshooting | |
| 131 | ||
| 132 | **Every call reports a missing key.** Some MCP clients filter the environment before | |
| 133 | spawning servers, which drops `TYPESAFE_API_KEY`. Confirm the variable is exported, | |
| 134 | then pass it explicitly in the client's server config if it still does not arrive. | |
| 135 | Run `jev_models` to check the key in isolation. | |
| 136 | ||
| 137 | **The server connects and then dies.** On a stdio transport, anything written to | |
| 138 | stdout that is not JSON-RPC breaks the connection. This server routes all logging to | |
| 139 | stderr, so `TYPESAFE_LOG_LEVEL=debug` is safe to turn on while debugging. | |
| 140 | ||
| 141 | ## When not to reach for this | |
| 142 | ||
| 143 | Jev earns its place on a decision that repeats thousands of times inside software, | |
| 144 | where code can enumerate the options first and you need a number to threshold on. | |
| 145 | ||
| 146 | For a one-off judgment during a conversation, an ordinary model answer is usually | |
| 147 | better, because it comes with reasoning you can argue with. Jev gives you a number | |
| 148 | and a label. That is a feature at scale and a limitation in dialogue. | |
| 149 | ||
| 150 | If deterministic code already decides the case correctly, keep the deterministic | |
| 151 | code. Typed output guarantees the interface, not the truth. Measure before adopting. | |
| 152 | ||
| 153 | ## Development | |
| 154 | ||
| 155 | bash | |
| 156 | npm run typecheck | |
| 157 | npm test # offline: unit tests plus regression tests against a local stand-in | |
| 158 | npm run test:e2e # live, requires TYPESAFE_API_KEY | |
| 159 | ||
| 160 | ||
| 161 | The offline suite runs the real built server over stdio against a local stand-in for | |
| 162 | the TypeSafe API and asserts on the request bodies it actually sends, so a regression | |
| 163 | in what reaches the model fails the build. | |
| 164 | ||
| 165 | ## License | |
| 166 | ||
| 167 | MIT |
package-lock.json added +1255
| @@ -0,0 +1,1255 @@ | ||
| 1 | { | |
| 2 | "name": "jev-mcp", | |
| 3 | "version": "0.1.0", | |
| 4 | "lockfileVersion": 3, | |
| 5 | "requires": true, | |
| 6 | "packages": { | |
| 7 | "": { | |
| 8 | "name": "jev-mcp", | |
| 9 | "version": "0.1.0", | |
| 10 | "license": "MIT", | |
| 11 | "dependencies": { | |
| 12 | "@modelcontextprotocol/sdk": "^1.30.0", | |
| 13 | "@typesafe-ai/sdk": "^0.6.0", | |
| 14 | "zod": "^3.25.0" | |
| 15 | }, | |
| 16 | "bin": { | |
| 17 | "jev-mcp": "dist/index.js" | |
| 18 | }, | |
| 19 | "devDependencies": { | |
| 20 | "@types/node": "^22.0.0", | |
| 21 | "typescript": "^5.6.0" | |
| 22 | }, | |
| 23 | "engines": { | |
| 24 | "node": ">=20.12" | |
| 25 | } | |
| 26 | }, | |
| 27 | "node_modules/@hono/node-server": { | |
| 28 | "version": "2.1.1", | |
| 29 | "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", | |
| 30 | "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", | |
| 31 | "license": "MIT", | |
| 32 | "engines": { | |
| 33 | "node": ">=20" | |
| 34 | }, | |
| 35 | "peerDependencies": { | |
| 36 | "hono": "^4" | |
| 37 | } | |
| 38 | }, | |
| 39 | "node_modules/@modelcontextprotocol/sdk": { | |
| 40 | "version": "1.30.0", | |
| 41 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", | |
| 42 | "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", | |
| 43 | "license": "MIT", | |
| 44 | "dependencies": { | |
| 45 | "@hono/node-server": "^1.19.9 || ^2.0.5", | |
| 46 | "ajv": "^8.17.1", | |
| 47 | "ajv-formats": "^3.0.1", | |
| 48 | "content-type": "^1.0.5", | |
| 49 | "cors": "^2.8.5", | |
| 50 | "cross-spawn": "^7.0.5", | |
| 51 | "eventsource": "^3.0.2", | |
| 52 | "eventsource-parser": "^3.0.0", | |
| 53 | "express": "^5.2.1", | |
| 54 | "express-rate-limit": "^8.2.1", | |
| 55 | "hono": "^4.11.4", | |
| 56 | "jose": "^6.1.3", | |
| 57 | "json-schema-typed": "^8.0.2", | |
| 58 | "pkce-challenge": "^5.0.0", | |
| 59 | "raw-body": "^3.0.0", | |
| 60 | "zod": "^3.25 || ^4.0", | |
| 61 | "zod-to-json-schema": "^3.25.1" | |
| 62 | }, | |
| 63 | "engines": { | |
| 64 | "node": ">=18" | |
| 65 | }, | |
| 66 | "peerDependencies": { | |
| 67 | "@cfworker/json-schema": "^4.1.1", | |
| 68 | "zod": "^3.25 || ^4.0" | |
| 69 | }, | |
| 70 | "peerDependenciesMeta": { | |
| 71 | "@cfworker/json-schema": { | |
| 72 | "optional": true | |
| 73 | }, | |
| 74 | "zod": { | |
| 75 | "optional": false | |
| 76 | } | |
| 77 | } | |
| 78 | }, | |
| 79 | "node_modules/@types/node": { | |
| 80 | "version": "22.20.3", | |
| 81 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.3.tgz", | |
| 82 | "integrity": "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==", | |
| 83 | "dev": true, | |
| 84 | "license": "MIT", | |
| 85 | "dependencies": { | |
| 86 | "undici-types": "~6.21.0" | |
| 87 | } | |
| 88 | }, | |
| 89 | "node_modules/@typesafe-ai/sdk": { | |
| 90 | "version": "0.6.0", | |
| 91 | "resolved": "https://registry.npmjs.org/@typesafe-ai/sdk/-/sdk-0.6.0.tgz", | |
| 92 | "integrity": "sha512-IddX+Q0XM+VagOUZFeP7wZjaO4SHMdvnh2zEBdrZZnXedWI3BNK1lKhMx3ayrkFWvVLbVcUHJy6AVZlY+e6Jaw==", | |
| 93 | "license": "MIT", | |
| 94 | "engines": { | |
| 95 | "node": ">=20" | |
| 96 | } | |
| 97 | }, | |
| 98 | "node_modules/accepts": { | |
| 99 | "version": "2.0.0", | |
| 100 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", | |
| 101 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", | |
| 102 | "license": "MIT", | |
| 103 | "dependencies": { | |
| 104 | "mime-types": "^3.0.0", | |
| 105 | "negotiator": "^1.0.0" | |
| 106 | }, | |
| 107 | "engines": { | |
| 108 | "node": ">= 0.6" | |
| 109 | } | |
| 110 | }, | |
| 111 | "node_modules/ajv": { | |
| 112 | "version": "8.20.0", | |
| 113 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", | |
| 114 | "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", | |
| 115 | "license": "MIT", | |
| 116 | "dependencies": { | |
| 117 | "fast-deep-equal": "^3.1.3", | |
| 118 | "fast-uri": "^3.0.1", | |
| 119 | "json-schema-traverse": "^1.0.0", | |
| 120 | "require-from-string": "^2.0.2" | |
| 121 | }, | |
| 122 | "funding": { | |
| 123 | "type": "github", | |
| 124 | "url": "https://github.com/sponsors/epoberezkin" | |
| 125 | } | |
| 126 | }, | |
| 127 | "node_modules/ajv-formats": { | |
| 128 | "version": "3.0.1", | |
| 129 | "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", | |
| 130 | "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", | |
| 131 | "license": "MIT", | |
| 132 | "dependencies": { | |
| 133 | "ajv": "^8.0.0" | |
| 134 | }, | |
| 135 | "peerDependencies": { | |
| 136 | "ajv": "^8.0.0" | |
| 137 | }, | |
| 138 | "peerDependenciesMeta": { | |
| 139 | "ajv": { | |
| 140 | "optional": true | |
| 141 | } | |
| 142 | } | |
| 143 | }, | |
| 144 | "node_modules/body-parser": { | |
| 145 | "version": "2.3.0", | |
| 146 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", | |
| 147 | "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", | |
| 148 | "license": "MIT", | |
| 149 | "dependencies": { | |
| 150 | "bytes": "^3.1.2", | |
| 151 | "content-type": "^2.0.0", | |
| 152 | "debug": "^4.4.3", | |
| 153 | "http-errors": "^2.0.1", | |
| 154 | "iconv-lite": "^0.7.2", | |
| 155 | "on-finished": "^2.4.1", | |
| 156 | "qs": "^6.15.2", | |
| 157 | "raw-body": "^3.0.2", | |
| 158 | "type-is": "^2.1.0" | |
| 159 | }, | |
| 160 | "engines": { | |
| 161 | "node": ">=18" | |
| 162 | }, | |
| 163 | "funding": { | |
| 164 | "type": "opencollective", | |
| 165 | "url": "https://opencollective.com/express" | |
| 166 | } | |
| 167 | }, | |
| 168 | "node_modules/body-parser/node_modules/content-type": { | |
| 169 | "version": "2.1.0", | |
| 170 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", | |
| 171 | "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", | |
| 172 | "license": "MIT", | |
| 173 | "engines": { | |
| 174 | "node": ">=18" | |
| 175 | }, | |
| 176 | "funding": { | |
| 177 | "type": "opencollective", | |
| 178 | "url": "https://opencollective.com/express" | |
| 179 | } | |
| 180 | }, | |
| 181 | "node_modules/bytes": { | |
| 182 | "version": "3.1.2", | |
| 183 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", | |
| 184 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", | |
| 185 | "license": "MIT", | |
| 186 | "engines": { | |
| 187 | "node": ">= 0.8" | |
| 188 | } | |
| 189 | }, | |
| 190 | "node_modules/call-bind-apply-helpers": { | |
| 191 | "version": "1.0.2", | |
| 192 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", | |
| 193 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", | |
| 194 | "license": "MIT", | |
| 195 | "dependencies": { | |
| 196 | "es-errors": "^1.3.0", | |
| 197 | "function-bind": "^1.1.2" | |
| 198 | }, | |
| 199 | "engines": { | |
| 200 | "node": ">= 0.4" | |
| 201 | } | |
| 202 | }, | |
| 203 | "node_modules/call-bound": { | |
| 204 | "version": "1.0.4", | |
| 205 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", | |
| 206 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", | |
| 207 | "license": "MIT", | |
| 208 | "dependencies": { | |
| 209 | "call-bind-apply-helpers": "^1.0.2", | |
| 210 | "get-intrinsic": "^1.3.0" | |
| 211 | }, | |
| 212 | "engines": { | |
| 213 | "node": ">= 0.4" | |
| 214 | }, | |
| 215 | "funding": { | |
| 216 | "url": "https://github.com/sponsors/ljharb" | |
| 217 | } | |
| 218 | }, | |
| 219 | "node_modules/content-disposition": { | |
| 220 | "version": "1.1.0", | |
| 221 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", | |
| 222 | "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", | |
| 223 | "license": "MIT", | |
| 224 | "engines": { | |
| 225 | "node": ">=18" | |
| 226 | }, | |
| 227 | "funding": { | |
| 228 | "type": "opencollective", | |
| 229 | "url": "https://opencollective.com/express" | |
| 230 | } | |
| 231 | }, | |
| 232 | "node_modules/content-type": { | |
| 233 | "version": "1.0.5", | |
| 234 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", | |
| 235 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", | |
| 236 | "license": "MIT", | |
| 237 | "engines": { | |
| 238 | "node": ">= 0.6" | |
| 239 | } | |
| 240 | }, | |
| 241 | "node_modules/cookie": { | |
| 242 | "version": "0.7.2", | |
| 243 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", | |
| 244 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", | |
| 245 | "license": "MIT", | |
| 246 | "engines": { | |
| 247 | "node": ">= 0.6" | |
| 248 | } | |
| 249 | }, | |
| 250 | "node_modules/cookie-signature": { | |
| 251 | "version": "1.2.2", | |
| 252 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", | |
| 253 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", | |
| 254 | "license": "MIT", | |
| 255 | "engines": { | |
| 256 | "node": ">=6.6.0" | |
| 257 | } | |
| 258 | }, | |
| 259 | "node_modules/cors": { | |
| 260 | "version": "2.8.6", | |
| 261 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", | |
| 262 | "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", | |
| 263 | "license": "MIT", | |
| 264 | "dependencies": { | |
| 265 | "object-assign": "^4", | |
| 266 | "vary": "^1" | |
| 267 | }, | |
| 268 | "engines": { | |
| 269 | "node": ">= 0.10" | |
| 270 | }, | |
| 271 | "funding": { | |
| 272 | "type": "opencollective", | |
| 273 | "url": "https://opencollective.com/express" | |
| 274 | } | |
| 275 | }, | |
| 276 | "node_modules/cross-spawn": { | |
| 277 | "version": "7.0.6", | |
| 278 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", | |
| 279 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", | |
| 280 | "license": "MIT", | |
| 281 | "dependencies": { | |
| 282 | "path-key": "^3.1.0", | |
| 283 | "shebang-command": "^2.0.0", | |
| 284 | "which": "^2.0.1" | |
| 285 | }, | |
| 286 | "engines": { | |
| 287 | "node": ">= 8" | |
| 288 | } | |
| 289 | }, | |
| 290 | "node_modules/debug": { | |
| 291 | "version": "4.4.3", | |
| 292 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", | |
| 293 | "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", | |
| 294 | "license": "MIT", | |
| 295 | "dependencies": { | |
| 296 | "ms": "^2.1.3" | |
| 297 | }, | |
| 298 | "engines": { | |
| 299 | "node": ">=6.0" | |
| 300 | }, | |
| 301 | "peerDependenciesMeta": { | |
| 302 | "supports-color": { | |
| 303 | "optional": true | |
| 304 | } | |
| 305 | } | |
| 306 | }, | |
| 307 | "node_modules/depd": { | |
| 308 | "version": "2.0.0", | |
| 309 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", | |
| 310 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", | |
| 311 | "license": "MIT", | |
| 312 | "engines": { | |
| 313 | "node": ">= 0.8" | |
| 314 | } | |
| 315 | }, | |
| 316 | "node_modules/dunder-proto": { | |
| 317 | "version": "1.0.1", | |
| 318 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", | |
| 319 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", | |
| 320 | "license": "MIT", | |
| 321 | "dependencies": { | |
| 322 | "call-bind-apply-helpers": "^1.0.1", | |
| 323 | "es-errors": "^1.3.0", | |
| 324 | "gopd": "^1.2.0" | |
| 325 | }, | |
| 326 | "engines": { | |
| 327 | "node": ">= 0.4" | |
| 328 | } | |
| 329 | }, | |
| 330 | "node_modules/ee-first": { | |
| 331 | "version": "1.1.1", | |
| 332 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", | |
| 333 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", | |
| 334 | "license": "MIT" | |
| 335 | }, | |
| 336 | "node_modules/encodeurl": { | |
| 337 | "version": "2.0.0", | |
| 338 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", | |
| 339 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", | |
| 340 | "license": "MIT", | |
| 341 | "engines": { | |
| 342 | "node": ">= 0.8" | |
| 343 | } | |
| 344 | }, | |
| 345 | "node_modules/es-define-property": { | |
| 346 | "version": "1.0.1", | |
| 347 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", | |
| 348 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", | |
| 349 | "license": "MIT", | |
| 350 | "engines": { | |
| 351 | "node": ">= 0.4" | |
| 352 | } | |
| 353 | }, | |
| 354 | "node_modules/es-errors": { | |
| 355 | "version": "1.3.0", | |
| 356 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", | |
| 357 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", | |
| 358 | "license": "MIT", | |
| 359 | "engines": { | |
| 360 | "node": ">= 0.4" | |
| 361 | } | |
| 362 | }, | |
| 363 | "node_modules/es-object-atoms": { | |
| 364 | "version": "1.1.2", | |
| 365 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", | |
| 366 | "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", | |
| 367 | "license": "MIT", | |
| 368 | "dependencies": { | |
| 369 | "es-errors": "^1.3.0" | |
| 370 | }, | |
| 371 | "engines": { | |
| 372 | "node": ">= 0.4" | |
| 373 | } | |
| 374 | }, | |
| 375 | "node_modules/escape-html": { | |
| 376 | "version": "1.0.3", | |
| 377 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", | |
| 378 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", | |
| 379 | "license": "MIT" | |
| 380 | }, | |
| 381 | "node_modules/etag": { | |
| 382 | "version": "1.8.1", | |
| 383 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", | |
| 384 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", | |
| 385 | "license": "MIT", | |
| 386 | "engines": { | |
| 387 | "node": ">= 0.6" | |
| 388 | } | |
| 389 | }, | |
| 390 | "node_modules/eventsource": { | |
| 391 | "version": "3.0.7", | |
| 392 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", | |
| 393 | "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", | |
| 394 | "license": "MIT", | |
| 395 | "dependencies": { | |
| 396 | "eventsource-parser": "^3.0.1" | |
| 397 | }, | |
| 398 | "engines": { | |
| 399 | "node": ">=18.0.0" | |
| 400 | } | |
| 401 | }, | |
| 402 | "node_modules/eventsource-parser": { | |
| 403 | "version": "3.1.1", | |
| 404 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", | |
| 405 | "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", | |
| 406 | "license": "MIT", | |
| 407 | "engines": { | |
| 408 | "node": ">=18.0.0" | |
| 409 | } | |
| 410 | }, | |
| 411 | "node_modules/express": { | |
| 412 | "version": "5.2.1", | |
| 413 | "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", | |
| 414 | "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", | |
| 415 | "license": "MIT", | |
| 416 | "dependencies": { | |
| 417 | "accepts": "^2.0.0", | |
| 418 | "body-parser": "^2.2.1", | |
| 419 | "content-disposition": "^1.0.0", | |
| 420 | "content-type": "^1.0.5", | |
| 421 | "cookie": "^0.7.1", | |
| 422 | "cookie-signature": "^1.2.1", | |
| 423 | "debug": "^4.4.0", | |
| 424 | "depd": "^2.0.0", | |
| 425 | "encodeurl": "^2.0.0", | |
| 426 | "escape-html": "^1.0.3", | |
| 427 | "etag": "^1.8.1", | |
| 428 | "finalhandler": "^2.1.0", | |
| 429 | "fresh": "^2.0.0", | |
| 430 | "http-errors": "^2.0.0", | |
| 431 | "merge-descriptors": "^2.0.0", | |
| 432 | "mime-types": "^3.0.0", | |
| 433 | "on-finished": "^2.4.1", | |
| 434 | "once": "^1.4.0", | |
| 435 | "parseurl": "^1.3.3", | |
| 436 | "proxy-addr": "^2.0.7", | |
| 437 | "qs": "^6.14.0", | |
| 438 | "range-parser": "^1.2.1", | |
| 439 | "router": "^2.2.0", | |
| 440 | "send": "^1.1.0", | |
| 441 | "serve-static": "^2.2.0", | |
| 442 | "statuses": "^2.0.1", | |
| 443 | "type-is": "^2.0.1", | |
| 444 | "vary": "^1.1.2" | |
| 445 | }, | |
| 446 | "engines": { | |
| 447 | "node": ">= 18" | |
| 448 | }, | |
| 449 | "funding": { | |
| 450 | "type": "opencollective", | |
| 451 | "url": "https://opencollective.com/express" | |
| 452 | } | |
| 453 | }, | |
| 454 | "node_modules/express-rate-limit": { | |
| 455 | "version": "8.7.0", | |
| 456 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", | |
| 457 | "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", | |
| 458 | "license": "MIT", | |
| 459 | "dependencies": { | |
| 460 | "debug": "^4.4.3", | |
| 461 | "ip-address": "^10.2.0" | |
| 462 | }, | |
| 463 | "engines": { | |
| 464 | "node": ">= 16" | |
| 465 | }, | |
| 466 | "funding": { | |
| 467 | "url": "https://github.com/sponsors/express-rate-limit" | |
| 468 | }, | |
| 469 | "peerDependencies": { | |
| 470 | "express": ">= 4.11" | |
| 471 | } | |
| 472 | }, | |
| 473 | "node_modules/fast-deep-equal": { | |
| 474 | "version": "3.1.3", | |
| 475 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", | |
| 476 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", | |
| 477 | "license": "MIT" | |
| 478 | }, | |
| 479 | "node_modules/fast-uri": { | |
| 480 | "version": "3.1.8", | |
| 481 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", | |
| 482 | "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", | |
| 483 | "funding": [ | |
| 484 | { | |
| 485 | "type": "github", | |
| 486 | "url": "https://github.com/sponsors/fastify" | |
| 487 | }, | |
| 488 | { | |
| 489 | "type": "opencollective", | |
| 490 | "url": "https://opencollective.com/fastify" | |
| 491 | } | |
| 492 | ], | |
| 493 | "license": "BSD-3-Clause" | |
| 494 | }, | |
| 495 | "node_modules/finalhandler": { | |
| 496 | "version": "2.1.1", | |
| 497 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", | |
| 498 | "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", | |
| 499 | "license": "MIT", | |
| 500 | "dependencies": { | |
| 501 | "debug": "^4.4.0", | |
| 502 | "encodeurl": "^2.0.0", | |
| 503 | "escape-html": "^1.0.3", | |
| 504 | "on-finished": "^2.4.1", | |
| 505 | "parseurl": "^1.3.3", | |
| 506 | "statuses": "^2.0.1" | |
| 507 | }, | |
| 508 | "engines": { | |
| 509 | "node": ">= 18.0.0" | |
| 510 | }, | |
| 511 | "funding": { | |
| 512 | "type": "opencollective", | |
| 513 | "url": "https://opencollective.com/express" | |
| 514 | } | |
| 515 | }, | |
| 516 | "node_modules/forwarded": { | |
| 517 | "version": "0.2.0", | |
| 518 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", | |
| 519 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", | |
| 520 | "license": "MIT", | |
| 521 | "engines": { | |
| 522 | "node": ">= 0.6" | |
| 523 | } | |
| 524 | }, | |
| 525 | "node_modules/fresh": { | |
| 526 | "version": "2.0.0", | |
| 527 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", | |
| 528 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", | |
| 529 | "license": "MIT", | |
| 530 | "engines": { | |
| 531 | "node": ">= 0.8" | |
| 532 | } | |
| 533 | }, | |
| 534 | "node_modules/function-bind": { | |
| 535 | "version": "1.1.2", | |
| 536 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", | |
| 537 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", | |
| 538 | "license": "MIT", | |
| 539 | "funding": { | |
| 540 | "url": "https://github.com/sponsors/ljharb" | |
| 541 | } | |
| 542 | }, | |
| 543 | "node_modules/get-intrinsic": { | |
| 544 | "version": "1.3.0", | |
| 545 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", | |
| 546 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", | |
| 547 | "license": "MIT", | |
| 548 | "dependencies": { | |
| 549 | "call-bind-apply-helpers": "^1.0.2", | |
| 550 | "es-define-property": "^1.0.1", | |
| 551 | "es-errors": "^1.3.0", | |
| 552 | "es-object-atoms": "^1.1.1", | |
| 553 | "function-bind": "^1.1.2", | |
| 554 | "get-proto": "^1.0.1", | |
| 555 | "gopd": "^1.2.0", | |
| 556 | "has-symbols": "^1.1.0", | |
| 557 | "hasown": "^2.0.2", | |
| 558 | "math-intrinsics": "^1.1.0" | |
| 559 | }, | |
| 560 | "engines": { | |
| 561 | "node": ">= 0.4" | |
| 562 | }, | |
| 563 | "funding": { | |
| 564 | "url": "https://github.com/sponsors/ljharb" | |
| 565 | } | |
| 566 | }, | |
| 567 | "node_modules/get-proto": { | |
| 568 | "version": "1.0.1", | |
| 569 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", | |
| 570 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", | |
| 571 | "license": "MIT", | |
| 572 | "dependencies": { | |
| 573 | "dunder-proto": "^1.0.1", | |
| 574 | "es-object-atoms": "^1.0.0" | |
| 575 | }, | |
| 576 | "engines": { | |
| 577 | "node": ">= 0.4" | |
| 578 | } | |
| 579 | }, | |
| 580 | "node_modules/gopd": { | |
| 581 | "version": "1.2.0", | |
| 582 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", | |
| 583 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", | |
| 584 | "license": "MIT", | |
| 585 | "engines": { | |
| 586 | "node": ">= 0.4" | |
| 587 | }, | |
| 588 | "funding": { | |
| 589 | "url": "https://github.com/sponsors/ljharb" | |
| 590 | } | |
| 591 | }, | |
| 592 | "node_modules/has-symbols": { | |
| 593 | "version": "1.1.0", | |
| 594 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", | |
| 595 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", | |
| 596 | "license": "MIT", | |
| 597 | "engines": { | |
| 598 | "node": ">= 0.4" | |
| 599 | }, | |
| 600 | "funding": { | |
| 601 | "url": "https://github.com/sponsors/ljharb" | |
| 602 | } | |
| 603 | }, | |
| 604 | "node_modules/hasown": { | |
| 605 | "version": "2.0.4", | |
| 606 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", | |
| 607 | "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", | |
| 608 | "license": "MIT", | |
| 609 | "dependencies": { | |
| 610 | "function-bind": "^1.1.2" | |
| 611 | }, | |
| 612 | "engines": { | |
| 613 | "node": ">= 0.4" | |
| 614 | } | |
| 615 | }, | |
| 616 | "node_modules/hono": { | |
| 617 | "version": "4.13.8", | |
| 618 | "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.8.tgz", | |
| 619 | "integrity": "sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==", | |
| 620 | "license": "MIT", | |
| 621 | "engines": { | |
| 622 | "node": ">=16.9.0" | |
| 623 | } | |
| 624 | }, | |
| 625 | "node_modules/http-errors": { | |
| 626 | "version": "2.0.1", | |
| 627 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", | |
| 628 | "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", | |
| 629 | "license": "MIT", | |
| 630 | "dependencies": { | |
| 631 | "depd": "~2.0.0", | |
| 632 | "inherits": "~2.0.4", | |
| 633 | "setprototypeof": "~1.2.0", | |
| 634 | "statuses": "~2.0.2", | |
| 635 | "toidentifier": "~1.0.1" | |
| 636 | }, | |
| 637 | "engines": { | |
| 638 | "node": ">= 0.8" | |
| 639 | }, | |
| 640 | "funding": { | |
| 641 | "type": "opencollective", | |
| 642 | "url": "https://opencollective.com/express" | |
| 643 | } | |
| 644 | }, | |
| 645 | "node_modules/iconv-lite": { | |
| 646 | "version": "0.7.3", | |
| 647 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", | |
| 648 | "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", | |
| 649 | "license": "MIT", | |
| 650 | "dependencies": { | |
| 651 | "safer-buffer": ">= 2.1.2 < 3.0.0" | |
| 652 | }, | |
| 653 | "engines": { | |
| 654 | "node": ">=0.10.0" | |
| 655 | }, | |
| 656 | "funding": { | |
| 657 | "type": "opencollective", | |
| 658 | "url": "https://opencollective.com/express" | |
| 659 | } | |
| 660 | }, | |
| 661 | "node_modules/inherits": { | |
| 662 | "version": "2.0.4", | |
| 663 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", | |
| 664 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", | |
| 665 | "license": "ISC" | |
| 666 | }, | |
| 667 | "node_modules/ip-address": { | |
| 668 | "version": "10.7.2", | |
| 669 | "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", | |
| 670 | "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", | |
| 671 | "license": "MIT", | |
| 672 | "engines": { | |
| 673 | "node": ">= 12" | |
| 674 | } | |
| 675 | }, | |
| 676 | "node_modules/ipaddr.js": { | |
| 677 | "version": "1.9.1", | |
| 678 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", | |
| 679 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", | |
| 680 | "license": "MIT", | |
| 681 | "engines": { | |
| 682 | "node": ">= 0.10" | |
| 683 | } | |
| 684 | }, | |
| 685 | "node_modules/is-promise": { | |
| 686 | "version": "4.0.0", | |
| 687 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", | |
| 688 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", | |
| 689 | "license": "MIT" | |
| 690 | }, | |
| 691 | "node_modules/isexe": { | |
| 692 | "version": "2.0.0", | |
| 693 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", | |
| 694 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", | |
| 695 | "license": "ISC" | |
| 696 | }, | |
| 697 | "node_modules/jose": { | |
| 698 | "version": "6.2.12", | |
| 699 | "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", | |
| 700 | "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", | |
| 701 | "license": "MIT", | |
| 702 | "funding": { | |
| 703 | "url": "https://github.com/sponsors/panva" | |
| 704 | } | |
| 705 | }, | |
| 706 | "node_modules/json-schema-traverse": { | |
| 707 | "version": "1.0.0", | |
| 708 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", | |
| 709 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", | |
| 710 | "license": "MIT" | |
| 711 | }, | |
| 712 | "node_modules/json-schema-typed": { | |
| 713 | "version": "8.0.2", | |
| 714 | "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", | |
| 715 | "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", | |
| 716 | "license": "BSD-2-Clause" | |
| 717 | }, | |
| 718 | "node_modules/math-intrinsics": { | |
| 719 | "version": "1.1.0", | |
| 720 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", | |
| 721 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", | |
| 722 | "license": "MIT", | |
| 723 | "engines": { | |
| 724 | "node": ">= 0.4" | |
| 725 | } | |
| 726 | }, | |
| 727 | "node_modules/media-typer": { | |
| 728 | "version": "1.1.1", | |
| 729 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", | |
| 730 | "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", | |
| 731 | "license": "MIT", | |
| 732 | "engines": { | |
| 733 | "node": ">= 0.8" | |
| 734 | }, | |
| 735 | "funding": { | |
| 736 | "type": "opencollective", | |
| 737 | "url": "https://opencollective.com/express" | |
| 738 | } | |
| 739 | }, | |
| 740 | "node_modules/merge-descriptors": { | |
| 741 | "version": "2.0.0", | |
| 742 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", | |
| 743 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", | |
| 744 | "license": "MIT", | |
| 745 | "engines": { | |
| 746 | "node": ">=18" | |
| 747 | }, | |
| 748 | "funding": { | |
| 749 | "url": "https://github.com/sponsors/sindresorhus" | |
| 750 | } | |
| 751 | }, | |
| 752 | "node_modules/mime-db": { | |
| 753 | "version": "1.54.0", | |
| 754 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", | |
| 755 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", | |
| 756 | "license": "MIT", | |
| 757 | "engines": { | |
| 758 | "node": ">= 0.6" | |
| 759 | } | |
| 760 | }, | |
| 761 | "node_modules/mime-types": { | |
| 762 | "version": "3.0.2", | |
| 763 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", | |
| 764 | "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", | |
| 765 | "license": "MIT", | |
| 766 | "dependencies": { | |
| 767 | "mime-db": "^1.54.0" | |
| 768 | }, | |
| 769 | "engines": { | |
| 770 | "node": ">=18" | |
| 771 | }, | |
| 772 | "funding": { | |
| 773 | "type": "opencollective", | |
| 774 | "url": "https://opencollective.com/express" | |
| 775 | } | |
| 776 | }, | |
| 777 | "node_modules/ms": { | |
| 778 | "version": "2.1.3", | |
| 779 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | |
| 780 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", | |
| 781 | "license": "MIT" | |
| 782 | }, | |
| 783 | "node_modules/negotiator": { | |
| 784 | "version": "1.1.0", | |
| 785 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", | |
| 786 | "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", | |
| 787 | "license": "MIT", | |
| 788 | "dependencies": { | |
| 789 | "content-type": "^2.1.0" | |
| 790 | }, | |
| 791 | "engines": { | |
| 792 | "node": ">=18" | |
| 793 | }, | |
| 794 | "funding": { | |
| 795 | "type": "opencollective", | |
| 796 | "url": "https://opencollective.com/express" | |
| 797 | } | |
| 798 | }, | |
| 799 | "node_modules/negotiator/node_modules/content-type": { | |
| 800 | "version": "2.1.0", | |
| 801 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", | |
| 802 | "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", | |
| 803 | "license": "MIT", | |
| 804 | "engines": { | |
| 805 | "node": ">=18" | |
| 806 | }, | |
| 807 | "funding": { | |
| 808 | "type": "opencollective", | |
| 809 | "url": "https://opencollective.com/express" | |
| 810 | } | |
| 811 | }, | |
| 812 | "node_modules/object-assign": { | |
| 813 | "version": "4.1.1", | |
| 814 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", | |
| 815 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", | |
| 816 | "license": "MIT", | |
| 817 | "engines": { | |
| 818 | "node": ">=0.10.0" | |
| 819 | } | |
| 820 | }, | |
| 821 | "node_modules/object-inspect": { | |
| 822 | "version": "1.13.4", | |
| 823 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", | |
| 824 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", | |
| 825 | "license": "MIT", | |
| 826 | "engines": { | |
| 827 | "node": ">= 0.4" | |
| 828 | }, | |
| 829 | "funding": { | |
| 830 | "url": "https://github.com/sponsors/ljharb" | |
| 831 | } | |
| 832 | }, | |
| 833 | "node_modules/on-finished": { | |
| 834 | "version": "2.4.1", | |
| 835 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", | |
| 836 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", | |
| 837 | "license": "MIT", | |
| 838 | "dependencies": { | |
| 839 | "ee-first": "1.1.1" | |
| 840 | }, | |
| 841 | "engines": { | |
| 842 | "node": ">= 0.8" | |
| 843 | } | |
| 844 | }, | |
| 845 | "node_modules/once": { | |
| 846 | "version": "1.4.0", | |
| 847 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", | |
| 848 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", | |
| 849 | "license": "ISC", | |
| 850 | "dependencies": { | |
| 851 | "wrappy": "1" | |
| 852 | } | |
| 853 | }, | |
| 854 | "node_modules/parseurl": { | |
| 855 | "version": "1.3.3", | |
| 856 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", | |
| 857 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", | |
| 858 | "license": "MIT", | |
| 859 | "engines": { | |
| 860 | "node": ">= 0.8" | |
| 861 | } | |
| 862 | }, | |
| 863 | "node_modules/path-key": { | |
| 864 | "version": "3.1.1", | |
| 865 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", | |
| 866 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", | |
| 867 | "license": "MIT", | |
| 868 | "engines": { | |
| 869 | "node": ">=8" | |
| 870 | } | |
| 871 | }, | |
| 872 | "node_modules/path-to-regexp": { | |
| 873 | "version": "8.4.2", | |
| 874 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", | |
| 875 | "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", | |
| 876 | "license": "MIT", | |
| 877 | "funding": { | |
| 878 | "type": "opencollective", | |
| 879 | "url": "https://opencollective.com/express" | |
| 880 | } | |
| 881 | }, | |
| 882 | "node_modules/pkce-challenge": { | |
| 883 | "version": "5.0.1", | |
| 884 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", | |
| 885 | "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", | |
| 886 | "license": "MIT", | |
| 887 | "engines": { | |
| 888 | "node": ">=16.20.0" | |
| 889 | } | |
| 890 | }, | |
| 891 | "node_modules/proxy-addr": { | |
| 892 | "version": "2.0.8", | |
| 893 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", | |
| 894 | "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", | |
| 895 | "license": "MIT", | |
| 896 | "dependencies": { | |
| 897 | "forwarded": "0.2.0", | |
| 898 | "ipaddr.js": "1.9.1" | |
| 899 | }, | |
| 900 | "engines": { | |
| 901 | "node": ">= 0.10" | |
| 902 | }, | |
| 903 | "funding": { | |
| 904 | "type": "opencollective", | |
| 905 | "url": "https://opencollective.com/express" | |
| 906 | } | |
| 907 | }, | |
| 908 | "node_modules/qs": { | |
| 909 | "version": "6.16.0", | |
| 910 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", | |
| 911 | "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", | |
| 912 | "license": "BSD-3-Clause", | |
| 913 | "dependencies": { | |
| 914 | "es-define-property": "^1.0.1", | |
| 915 | "side-channel": "^1.1.1" | |
| 916 | }, | |
| 917 | "engines": { | |
| 918 | "node": ">=0.6" | |
| 919 | }, | |
| 920 | "funding": { | |
| 921 | "url": "https://github.com/sponsors/ljharb" | |
| 922 | } | |
| 923 | }, | |
| 924 | "node_modules/range-parser": { | |
| 925 | "version": "1.3.0", | |
| 926 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", | |
| 927 | "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", | |
| 928 | "license": "MIT", | |
| 929 | "engines": { | |
| 930 | "node": ">= 0.6" | |
| 931 | }, | |
| 932 | "funding": { | |
| 933 | "type": "opencollective", | |
| 934 | "url": "https://opencollective.com/express" | |
| 935 | } | |
| 936 | }, | |
| 937 | "node_modules/raw-body": { | |
| 938 | "version": "3.0.2", | |
| 939 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", | |
| 940 | "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", | |
| 941 | "license": "MIT", | |
| 942 | "dependencies": { | |
| 943 | "bytes": "~3.1.2", | |
| 944 | "http-errors": "~2.0.1", | |
| 945 | "iconv-lite": "~0.7.0", | |
| 946 | "unpipe": "~1.0.0" | |
| 947 | }, | |
| 948 | "engines": { | |
| 949 | "node": ">= 0.10" | |
| 950 | } | |
| 951 | }, | |
| 952 | "node_modules/require-from-string": { | |
| 953 | "version": "2.0.2", | |
| 954 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", | |
| 955 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", | |
| 956 | "license": "MIT", | |
| 957 | "engines": { | |
| 958 | "node": ">=0.10.0" | |
| 959 | } | |
| 960 | }, | |
| 961 | "node_modules/router": { | |
| 962 | "version": "2.2.0", | |
| 963 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", | |
| 964 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", | |
| 965 | "license": "MIT", | |
| 966 | "dependencies": { | |
| 967 | "debug": "^4.4.0", | |
| 968 | "depd": "^2.0.0", | |
| 969 | "is-promise": "^4.0.0", | |
| 970 | "parseurl": "^1.3.3", | |
| 971 | "path-to-regexp": "^8.0.0" | |
| 972 | }, | |
| 973 | "engines": { | |
| 974 | "node": ">= 18" | |
| 975 | } | |
| 976 | }, | |
| 977 | "node_modules/safer-buffer": { | |
| 978 | "version": "2.1.2", | |
| 979 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", | |
| 980 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", | |
| 981 | "license": "MIT" | |
| 982 | }, | |
| 983 | "node_modules/send": { | |
| 984 | "version": "1.2.1", | |
| 985 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", | |
| 986 | "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", | |
| 987 | "license": "MIT", | |
| 988 | "dependencies": { | |
| 989 | "debug": "^4.4.3", | |
| 990 | "encodeurl": "^2.0.0", | |
| 991 | "escape-html": "^1.0.3", | |
| 992 | "etag": "^1.8.1", | |
| 993 | "fresh": "^2.0.0", | |
| 994 | "http-errors": "^2.0.1", | |
| 995 | "mime-types": "^3.0.2", | |
| 996 | "ms": "^2.1.3", | |
| 997 | "on-finished": "^2.4.1", | |
| 998 | "range-parser": "^1.2.1", | |
| 999 | "statuses": "^2.0.2" | |
| 1000 | }, | |
| 1001 | "engines": { | |
| 1002 | "node": ">= 18" | |
| 1003 | }, | |
| 1004 | "funding": { | |
| 1005 | "type": "opencollective", | |
| 1006 | "url": "https://opencollective.com/express" | |
| 1007 | } | |
| 1008 | }, | |
| 1009 | "node_modules/serve-static": { | |
| 1010 | "version": "2.2.1", | |
| 1011 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", | |
| 1012 | "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", | |
| 1013 | "license": "MIT", | |
| 1014 | "dependencies": { | |
| 1015 | "encodeurl": "^2.0.0", | |
| 1016 | "escape-html": "^1.0.3", | |
| 1017 | "parseurl": "^1.3.3", | |
| 1018 | "send": "^1.2.0" | |
| 1019 | }, | |
| 1020 | "engines": { | |
| 1021 | "node": ">= 18" | |
| 1022 | }, | |
| 1023 | "funding": { | |
| 1024 | "type": "opencollective", | |
| 1025 | "url": "https://opencollective.com/express" | |
| 1026 | } | |
| 1027 | }, | |
| 1028 | "node_modules/setprototypeof": { | |
| 1029 | "version": "1.2.0", | |
| 1030 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", | |
| 1031 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", | |
| 1032 | "license": "ISC" | |
| 1033 | }, | |
| 1034 | "node_modules/shebang-command": { | |
| 1035 | "version": "2.0.0", | |
| 1036 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", | |
| 1037 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", | |
| 1038 | "license": "MIT", | |
| 1039 | "dependencies": { | |
| 1040 | "shebang-regex": "^3.0.0" | |
| 1041 | }, | |
| 1042 | "engines": { | |
| 1043 | "node": ">=8" | |
| 1044 | } | |
| 1045 | }, | |
| 1046 | "node_modules/shebang-regex": { | |
| 1047 | "version": "3.0.0", | |
| 1048 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", | |
| 1049 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", | |
| 1050 | "license": "MIT", | |
| 1051 | "engines": { | |
| 1052 | "node": ">=8" | |
| 1053 | } | |
| 1054 | }, | |
| 1055 | "node_modules/side-channel": { | |
| 1056 | "version": "1.1.1", | |
| 1057 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", | |
| 1058 | "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", | |
| 1059 | "license": "MIT", | |
| 1060 | "dependencies": { | |
| 1061 | "es-errors": "^1.3.0", | |
| 1062 | "object-inspect": "^1.13.4", | |
| 1063 | "side-channel-list": "^1.0.1", | |
| 1064 | "side-channel-map": "^1.0.1", | |
| 1065 | "side-channel-weakmap": "^1.0.2" | |
| 1066 | }, | |
| 1067 | "engines": { | |
| 1068 | "node": ">= 0.4" | |
| 1069 | }, | |
| 1070 | "funding": { | |
| 1071 | "url": "https://github.com/sponsors/ljharb" | |
| 1072 | } | |
| 1073 | }, | |
| 1074 | "node_modules/side-channel-list": { | |
| 1075 | "version": "1.0.1", | |
| 1076 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", | |
| 1077 | "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", | |
| 1078 | "license": "MIT", | |
| 1079 | "dependencies": { | |
| 1080 | "es-errors": "^1.3.0", | |
| 1081 | "object-inspect": "^1.13.4" | |
| 1082 | }, | |
| 1083 | "engines": { | |
| 1084 | "node": ">= 0.4" | |
| 1085 | }, | |
| 1086 | "funding": { | |
| 1087 | "url": "https://github.com/sponsors/ljharb" | |
| 1088 | } | |
| 1089 | }, | |
| 1090 | "node_modules/side-channel-map": { | |
| 1091 | "version": "1.0.1", | |
| 1092 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", | |
| 1093 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", | |
| 1094 | "license": "MIT", | |
| 1095 | "dependencies": { | |
| 1096 | "call-bound": "^1.0.2", | |
| 1097 | "es-errors": "^1.3.0", | |
| 1098 | "get-intrinsic": "^1.2.5", | |
| 1099 | "object-inspect": "^1.13.3" | |
| 1100 | }, | |
| 1101 | "engines": { | |
| 1102 | "node": ">= 0.4" | |
| 1103 | }, | |
| 1104 | "funding": { | |
| 1105 | "url": "https://github.com/sponsors/ljharb" | |
| 1106 | } | |
| 1107 | }, | |
| 1108 | "node_modules/side-channel-weakmap": { | |
| 1109 | "version": "1.0.2", | |
| 1110 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", | |
| 1111 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", | |
| 1112 | "license": "MIT", | |
| 1113 | "dependencies": { | |
| 1114 | "call-bound": "^1.0.2", | |
| 1115 | "es-errors": "^1.3.0", | |
| 1116 | "get-intrinsic": "^1.2.5", | |
| 1117 | "object-inspect": "^1.13.3", | |
| 1118 | "side-channel-map": "^1.0.1" | |
| 1119 | }, | |
| 1120 | "engines": { | |
| 1121 | "node": ">= 0.4" | |
| 1122 | }, | |
| 1123 | "funding": { | |
| 1124 | "url": "https://github.com/sponsors/ljharb" | |
| 1125 | } | |
| 1126 | }, | |
| 1127 | "node_modules/statuses": { | |
| 1128 | "version": "2.0.2", | |
| 1129 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", | |
| 1130 | "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", | |
| 1131 | "license": "MIT", | |
| 1132 | "engines": { | |
| 1133 | "node": ">= 0.8" | |
| 1134 | } | |
| 1135 | }, | |
| 1136 | "node_modules/toidentifier": { | |
| 1137 | "version": "1.0.1", | |
| 1138 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", | |
| 1139 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", | |
| 1140 | "license": "MIT", | |
| 1141 | "engines": { | |
| 1142 | "node": ">=0.6" | |
| 1143 | } | |
| 1144 | }, | |
| 1145 | "node_modules/type-is": { | |
| 1146 | "version": "2.1.0", | |
| 1147 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", | |
| 1148 | "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", | |
| 1149 | "license": "MIT", | |
| 1150 | "dependencies": { | |
| 1151 | "content-type": "^2.0.0", | |
| 1152 | "media-typer": "^1.1.0", | |
| 1153 | "mime-types": "^3.0.0" | |
| 1154 | }, | |
| 1155 | "engines": { | |
| 1156 | "node": ">= 18" | |
| 1157 | }, | |
| 1158 | "funding": { | |
| 1159 | "type": "opencollective", | |
| 1160 | "url": "https://opencollective.com/express" | |
| 1161 | } | |
| 1162 | }, | |
| 1163 | "node_modules/type-is/node_modules/content-type": { | |
| 1164 | "version": "2.1.0", | |
| 1165 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", | |
| 1166 | "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", | |
| 1167 | "license": "MIT", | |
| 1168 | "engines": { | |
| 1169 | "node": ">=18" | |
| 1170 | }, | |
| 1171 | "funding": { | |
| 1172 | "type": "opencollective", | |
| 1173 | "url": "https://opencollective.com/express" | |
| 1174 | } | |
| 1175 | }, | |
| 1176 | "node_modules/typescript": { | |
| 1177 | "version": "5.9.3", | |
| 1178 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", | |
| 1179 | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", | |
| 1180 | "dev": true, | |
| 1181 | "license": "Apache-2.0", | |
| 1182 | "bin": { | |
| 1183 | "tsc": "bin/tsc", | |
| 1184 | "tsserver": "bin/tsserver" | |
| 1185 | }, | |
| 1186 | "engines": { | |
| 1187 | "node": ">=14.17" | |
| 1188 | } | |
| 1189 | }, | |
| 1190 | "node_modules/undici-types": { | |
| 1191 | "version": "6.21.0", | |
| 1192 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", | |
| 1193 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", | |
| 1194 | "dev": true, | |
| 1195 | "license": "MIT" | |
| 1196 | }, | |
| 1197 | "node_modules/unpipe": { | |
| 1198 | "version": "1.0.0", | |
| 1199 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", | |
| 1200 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", | |
| 1201 | "license": "MIT", | |
| 1202 | "engines": { | |
| 1203 | "node": ">= 0.8" | |
| 1204 | } | |
| 1205 | }, | |
| 1206 | "node_modules/vary": { | |
| 1207 | "version": "1.1.2", | |
| 1208 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", | |
| 1209 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", | |
| 1210 | "license": "MIT", | |
| 1211 | "engines": { | |
| 1212 | "node": ">= 0.8" | |
| 1213 | } | |
| 1214 | }, | |
| 1215 | "node_modules/which": { | |
| 1216 | "version": "2.0.2", | |
| 1217 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", | |
| 1218 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", | |
| 1219 | "license": "ISC", | |
| 1220 | "dependencies": { | |
| 1221 | "isexe": "^2.0.0" | |
| 1222 | }, | |
| 1223 | "bin": { | |
| 1224 | "node-which": "bin/node-which" | |
| 1225 | }, | |
| 1226 | "engines": { | |
| 1227 | "node": ">= 8" | |
| 1228 | } | |
| 1229 | }, | |
| 1230 | "node_modules/wrappy": { | |
| 1231 | "version": "1.0.2", | |
| 1232 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", | |
| 1233 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", | |
| 1234 | "license": "ISC" | |
| 1235 | }, | |
| 1236 | "node_modules/zod": { | |
| 1237 | "version": "3.25.76", | |
| 1238 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", | |
| 1239 | "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", | |
| 1240 | "license": "MIT", | |
| 1241 | "funding": { | |
| 1242 | "url": "https://github.com/sponsors/colinhacks" | |
| 1243 | } | |
| 1244 | }, | |
| 1245 | "node_modules/zod-to-json-schema": { | |
| 1246 | "version": "3.25.2", | |
| 1247 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", | |
| 1248 | "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", | |
| 1249 | "license": "ISC", | |
| 1250 | "peerDependencies": { | |
| 1251 | "zod": "^3.25.28 || ^4" | |
| 1252 | } | |
| 1253 | } | |
| 1254 | } | |
| 1255 | } |
package.json added +45
| @@ -0,0 +1,45 @@ | ||
| 1 | { | |
| 2 | "name": "jev-mcp", | |
| 3 | "version": "0.2.0", | |
| 4 | "description": "MCP server exposing TypeSafe Jev typed judgments: classify, score, check, a batched multi-question ask, and model discovery.", | |
| 5 | "type": "module", | |
| 6 | "license": "MIT", | |
| 7 | "bin": { | |
| 8 | "jev-mcp": "./dist/index.js" | |
| 9 | }, | |
| 10 | "files": [ | |
| 11 | "dist", | |
| 12 | "README.md", | |
| 13 | "LICENSE" | |
| 14 | ], | |
| 15 | "keywords": [ | |
| 16 | "mcp", | |
| 17 | "model-context-protocol", | |
| 18 | "typesafe", | |
| 19 | "jev", | |
| 20 | "system-one", | |
| 21 | "classification", | |
| 22 | "confidence", | |
| 23 | "ai" | |
| 24 | ], | |
| 25 | "scripts": { | |
| 26 | "build": "tsc", | |
| 27 | "prepare": "npm run build", | |
| 28 | "start": "node dist/index.js", | |
| 29 | "typecheck": "tsc --noEmit", | |
| 30 | "test": "node --test test/*.test.mjs", | |
| 31 | "test:e2e": "node --test test/e2e.test.mjs" | |
| 32 | }, | |
| 33 | "dependencies": { | |
| 34 | "@modelcontextprotocol/sdk": "^1.30.0", | |
| 35 | "@typesafe-ai/sdk": "^0.6.0", | |
| 36 | "zod": "^3.25.0" | |
| 37 | }, | |
| 38 | "devDependencies": { | |
| 39 | "@types/node": "^22.0.0", | |
| 40 | "typescript": "^5.6.0" | |
| 41 | }, | |
| 42 | "engines": { | |
| 43 | "node": ">=20.12" | |
| 44 | } | |
| 45 | } |
src/index.ts added +471
| @@ -0,0 +1,471 @@ | ||
| 1 | #!/usr/bin/env node | |
| 2 | /** | |
| 3 | * jev-mcp — MCP server exposing TypeSafe Jev as typed judgment tools. | |
| 4 | * | |
| 5 | * Jev is a System One model: it returns a typed answer plus a calibrated | |
| 6 | * probability distribution, never prose. These tools surface that faithfully. | |
| 7 | * | |
| 8 | * Rules that shape the whole surface: | |
| 9 | * | |
| 10 | * 1. The caller owns the option set. Every selecting tool requires options | |
| 11 | * supplied by the caller, so the model can pick the wrong one but can never | |
| 12 | * invent one. A selector cannot choose a candidate the enumerator dropped. | |
| 13 | * 2. Probabilities are always returned, never just the label. | |
| 14 | * 3. The API key comes from the environment only, never a tool argument. | |
| 15 | * 4. Nothing is silently dropped, overwritten, or truncated. A request that | |
| 16 | * cannot be honoured exactly fails with a reason. | |
| 17 | * 5. Nothing but JSON-RPC is ever written to stdout. | |
| 18 | */ | |
| 19 | ||
| 20 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | |
| 21 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | |
| 22 | import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk"; | |
| 23 | import type { EntryType, Question, ScoreCriteria } from "@typesafe-ai/sdk"; | |
| 24 | import { z } from "zod"; | |
| 25 | import { readFileSync } from "node:fs"; | |
| 26 | import { homedir } from "node:os"; | |
| 27 | import { join } from "node:path"; | |
| 28 | import { | |
| 29 | assertStateWithinLimit, | |
| 30 | assertUniqueIds, | |
| 31 | buildChoiceCriteria, | |
| 32 | DEFAULT_MAX_QUESTIONS, | |
| 33 | DEFAULT_MAX_SCORE_LEVELS, | |
| 34 | DEFAULT_MAX_STATE_CHARS, | |
| 35 | DEFAULT_TIMEOUT_MS, | |
| 36 | describeError, | |
| 37 | gateConfidence, | |
| 38 | gateProbability, | |
| 39 | readPositiveInt, | |
| 40 | } from "./lib.js"; | |
| 41 | ||
| 42 | const VERSION = "0.2.0"; | |
| 43 | ||
| 44 | // ── Configuration ─────────────────────────────────────────────────────────── | |
| 45 | ||
| 46 | const MODEL = process.env.JEV_MODEL ?? "jev-latest"; | |
| 47 | ||
| 48 | /** | |
| 49 | * Every log line goes to stderr. | |
| 50 | * | |
| 51 | * The TypeSafe SDK's default logger uses `console.info` and `console.debug`, | |
| 52 | * which write to stdout. On a stdio transport stdout carries JSON-RPC, so a | |
| 53 | * single verbose log line corrupts the stream and kills the connection. | |
| 54 | * Routing all four levels to stderr makes any TYPESAFE_LOG_LEVEL safe. | |
| 55 | */ | |
| 56 | const stderrLogger = { | |
| 57 | debug: (message: string, ...args: unknown[]) => console.error("[typesafe-sdk]", message, ...args), | |
| 58 | info: (message: string, ...args: unknown[]) => console.error("[typesafe-sdk]", message, ...args), | |
| 59 | warn: (message: string, ...args: unknown[]) => console.error("[typesafe-sdk]", message, ...args), | |
| 60 | error: (message: string, ...args: unknown[]) => console.error("[typesafe-sdk]", message, ...args), | |
| 61 | }; | |
| 62 | ||
| 63 | const timeout = readPositiveInt(process.env.JEV_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, "JEV_TIMEOUT_MS"); | |
| 64 | const maxQuestions = readPositiveInt(process.env.JEV_MAX_QUESTIONS, DEFAULT_MAX_QUESTIONS, "JEV_MAX_QUESTIONS"); | |
| 65 | const maxStateChars = readPositiveInt(process.env.JEV_MAX_STATE_CHARS, DEFAULT_MAX_STATE_CHARS, "JEV_MAX_STATE_CHARS"); | |
| 66 | for (const setting of [timeout, maxQuestions, maxStateChars]) { | |
| 67 | if (setting.warning) console.error(`[jev-mcp] ${setting.warning}`); | |
| 68 | } | |
| 69 | ||
| 70 | // ── Shared schema pieces ──────────────────────────────────────────────────── | |
| 71 | ||
| 72 | /** | |
| 73 | * State accepted by Jev: plain text, or structured data for records and logs. | |
| 74 | * | |
| 75 | * `any` rather than `unknown` on purpose: the SDK constrains state to JsonValue | |
| 76 | * and `unknown` members are not assignable to it. The JSON Schema is identical. | |
| 77 | */ | |
| 78 | const StateSchema = z | |
| 79 | .union([z.string(), z.record(z.any()), z.array(z.any())]) | |
| 80 | .describe("The content to evaluate. A plain string for text, or an object/array for structured data such as a record, a diff, or a chat log."); | |
| 81 | ||
| 82 | /** Instructions accept JSON structure, which helps when a question has parts. */ | |
| 83 | const InstructionSchema = z | |
| 84 | .union([z.string().min(1), z.record(z.any()), z.array(z.any())]) | |
| 85 | .describe("The judgment to make. A string, or an object/array when the question has several labelled parts. This is the only instruction Jev sees, so state it in full."); | |
| 86 | ||
| 87 | /** Option and level descriptions accept the same JSON structure. */ | |
| 88 | const DescriptionSchema = z.union([z.string(), z.record(z.any()), z.array(z.any()), z.null()]); | |
| 89 | ||
| 90 | const UsageSchema = z.object({ input_tokens: z.number(), output_tokens: z.number() }); | |
| 91 | const GateSchema = z.enum(["act", "review", "abstain"]); | |
| 92 | ||
| 93 | const ActAbove = z | |
| 94 | .number() | |
| 95 | .min(0) | |
| 96 | .max(1) | |
| 97 | .optional() | |
| 98 | .describe("Confidence at or above which the answer is marked 'act'. Default 0.8. Calibrate on your own data and the cost of being wrong."); | |
| 99 | const ReviewAbove = z | |
| 100 | .number() | |
| 101 | .min(0) | |
| 102 | .max(1) | |
| 103 | .optional() | |
| 104 | .describe("Confidence at or above which the answer is marked 'review' rather than 'abstain'. Default 0.5."); | |
| 105 | ||
| 106 | // ── Client ────────────────────────────────────────────────────────────────── | |
| 107 | ||
| 108 | let client: TypeSafeClient | undefined; | |
| 109 | ||
| 110 | /** | |
| 111 | * Fallback source for the key. | |
| 112 | * | |
| 113 | * An MCP server is spawned with the client's own environment, not your shell's, | |
| 114 | * so anything exported from `~/.zshenv` never arrives. Confirmed on the wire: a | |
| 115 | * PreToolUse hook under the same client received `JEV_GUARD`, which is injected | |
| 116 | * through settings `env`, but not `TYPESAFE_API_KEY`, which lives only in the | |
| 117 | * shell profile. | |
| 118 | * | |
| 119 | * A 0600 file reaches every spawn path while keeping the secret out of | |
| 120 | * `~/.claude.json`, out of argv, and out of any repository. | |
| 121 | */ | |
| 122 | const KEY_FILE = process.env.JEV_KEY_FILE ?? join(homedir(), ".config", "typesafe", "key"); | |
| 123 | ||
| 124 | function readKeyFile(): string | undefined { | |
| 125 | try { | |
| 126 | const contents = readFileSync(KEY_FILE, "utf8").trim(); | |
| 127 | return contents.length > 0 ? contents : undefined; | |
| 128 | } catch { | |
| 129 | return undefined; | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | /** | |
| 134 | * Built lazily: the constructor throws without a key, and a missing key should | |
| 135 | * produce one clear tool error rather than stop the server from starting. | |
| 136 | */ | |
| 137 | function getClient(): TypeSafeClient { | |
| 138 | const apiKey = process.env.TYPESAFE_API_KEY ?? process.env.JEV_API_KEY ?? readKeyFile(); | |
| 139 | if (!apiKey) { | |
| 140 | throw new Error( | |
| 141 | `No API key. Set TYPESAFE_API_KEY in the environment of the MCP client, or create ${KEY_FILE} with mode 0600. Never pass it as a tool argument.`, | |
| 142 | ); | |
| 143 | } | |
| 144 | client ??= new TypeSafeClient({ apiKey, timeout: timeout.value, logger: stderrLogger }); | |
| 145 | return client; | |
| 146 | } | |
| 147 | ||
| 148 | // ── Result helpers ────────────────────────────────────────────────────────── | |
| 149 | ||
| 150 | /** | |
| 151 | * Tool results carry both a text block and structured content: the text keeps | |
| 152 | * older clients working, the structured payload is validated against the tool's | |
| 153 | * output schema so a caller gets typed data instead of a JSON string to parse. | |
| 154 | */ | |
| 155 | function ok<T extends Record<string, unknown>>(payload: T) { | |
| 156 | return { | |
| 157 | content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }], | |
| 158 | structuredContent: payload, | |
| 159 | }; | |
| 160 | } | |
| 161 | ||
| 162 | function fail(error: unknown) { | |
| 163 | const described = describeError(error); | |
| 164 | return { | |
| 165 | isError: true, | |
| 166 | content: [{ type: "text" as const, text: JSON.stringify({ error: described }, null, 2) }], | |
| 167 | }; | |
| 168 | } | |
| 169 | ||
| 170 | const server = new McpServer({ name: "jev", version: VERSION }); | |
| 171 | ||
| 172 | // ── classify: one of a defined set ───────────────────────────────────────── | |
| 173 | ||
| 174 | server.registerTool( | |
| 175 | "jev_classify", | |
| 176 | { | |
| 177 | title: "Classify into one of your options", | |
| 178 | description: | |
| 179 | "Pick exactly one option from a set you define. Returns the chosen option, the probability of every option, a confidence value, and a recommended action gated on confidence. " + | |
| 180 | "Use when the answer is one of a fixed set. The options must be supplied by you: Jev selects among them and cannot invent a new one. Up to 255 options.", | |
| 181 | inputSchema: { | |
| 182 | state: StateSchema, | |
| 183 | question: InstructionSchema, | |
| 184 | options: z | |
| 185 | .record(DescriptionSchema) | |
| 186 | .describe("Map of option name to a description that separates it from the others. Both the name and the description are sent to the model, so keep names short and distinct. A description may be an object or array when structure clarifies it, or null to leave it undescribed."), | |
| 187 | add_none: z | |
| 188 | .boolean() | |
| 189 | .optional() | |
| 190 | .describe("Add a no-match option meaning none of yours fits. Defaults to true. Turn off only when one option must always apply. If you already use the name 'none', the added option takes a different key and it is reported back as none_option."), | |
| 191 | act_above: ActAbove, | |
| 192 | review_above: ReviewAbove, | |
| 193 | }, | |
| 194 | outputSchema: { | |
| 195 | choice: z.string(), | |
| 196 | confidence: z.number(), | |
| 197 | probabilities: z.record(z.number()), | |
| 198 | none_option: z.string().nullable().describe("The key carrying the no-match meaning, or null when none was added."), | |
| 199 | action: GateSchema, | |
| 200 | thresholds: z.object({ act_above: z.number(), review_above: z.number() }), | |
| 201 | model: z.string(), | |
| 202 | usage: UsageSchema, | |
| 203 | }, | |
| 204 | }, | |
| 205 | async ({ state, question, options, add_none, act_above, review_above }, extra) => { | |
| 206 | try { | |
| 207 | assertStateWithinLimit(state, maxStateChars.value); | |
| 208 | const actAbove = act_above ?? 0.8; | |
| 209 | const reviewAbove = review_above ?? 0.5; | |
| 210 | const { criteria, noneKey } = buildChoiceCriteria(options as Record<string, EntryType>, add_none !== false); | |
| 211 | ||
| 212 | const result = await getClient().systemOne( | |
| 213 | { state, model: MODEL, questions: { classify: choice(question, criteria) } }, | |
| 214 | { signal: extra.signal }, | |
| 215 | ); | |
| 216 | const answer = result.answers.classify; | |
| 217 | return ok({ | |
| 218 | choice: answer.choice, | |
| 219 | confidence: answer.confidence, | |
| 220 | probabilities: answer.probabilities, | |
| 221 | none_option: noneKey, | |
| 222 | action: gateConfidence(answer.confidence, actAbove, reviewAbove), | |
| 223 | thresholds: { act_above: actAbove, review_above: reviewAbove }, | |
| 224 | model: result.model, | |
| 225 | usage: result.usage, | |
| 226 | }); | |
| 227 | } catch (error) { | |
| 228 | return fail(error); | |
| 229 | } | |
| 230 | }, | |
| 231 | ); | |
| 232 | ||
| 233 | // ── score: a position on an ordered scale ────────────────────────────────── | |
| 234 | ||
| 235 | server.registerTool( | |
| 236 | "jev_score", | |
| 237 | { | |
| 238 | title: "Rate on an ordered scale", | |
| 239 | description: | |
| 240 | "Rate the state along an ordered scale you define. Returns a probability-weighted score that can land between levels, the distribution, confidence, and a recommended action. " + | |
| 241 | "Use for degree or severity, not for picking a category.", | |
| 242 | inputSchema: { | |
| 243 | state: StateSchema, | |
| 244 | question: InstructionSchema, | |
| 245 | levels: z | |
| 246 | .array(DescriptionSchema) | |
| 247 | .min(2) | |
| 248 | .describe("Ordered level descriptions, lowest first. At least two. Each level must describe a concrete situation and stand on its own."), | |
| 249 | act_above: ActAbove, | |
| 250 | review_above: ReviewAbove, | |
| 251 | }, | |
| 252 | outputSchema: { | |
| 253 | score: z.number(), | |
| 254 | confidence: z.number(), | |
| 255 | probabilities: z.record(z.number()), | |
| 256 | legend: z.record(z.any()), | |
| 257 | action: GateSchema, | |
| 258 | thresholds: z.object({ act_above: z.number(), review_above: z.number() }), | |
| 259 | model: z.string(), | |
| 260 | usage: UsageSchema, | |
| 261 | }, | |
| 262 | }, | |
| 263 | async ({ state, question, levels, act_above, review_above }, extra) => { | |
| 264 | try { | |
| 265 | assertStateWithinLimit(state, maxStateChars.value); | |
| 266 | if (levels.length > DEFAULT_MAX_SCORE_LEVELS) { | |
| 267 | throw new Error(`levels must contain at most ${DEFAULT_MAX_SCORE_LEVELS} entries; received ${levels.length}.`); | |
| 268 | } | |
| 269 | const actAbove = act_above ?? 0.8; | |
| 270 | const reviewAbove = review_above ?? 0.5; | |
| 271 | ||
| 272 | const result = await getClient().systemOne( | |
| 273 | // zod already enforces two or more levels; the SDK types that as a tuple. | |
| 274 | { state, model: MODEL, questions: { rating: score(question, levels as unknown as ScoreCriteria) } }, | |
| 275 | { signal: extra.signal }, | |
| 276 | ); | |
| 277 | const answer = result.answers.rating; | |
| 278 | return ok({ | |
| 279 | score: answer.score, | |
| 280 | confidence: answer.confidence, | |
| 281 | probabilities: answer.probabilities, | |
| 282 | legend: answer.legend, | |
| 283 | action: gateConfidence(answer.confidence, actAbove, reviewAbove), | |
| 284 | thresholds: { act_above: actAbove, review_above: reviewAbove }, | |
| 285 | model: result.model, | |
| 286 | usage: result.usage, | |
| 287 | }); | |
| 288 | } catch (error) { | |
| 289 | return fail(error); | |
| 290 | } | |
| 291 | }, | |
| 292 | ); | |
| 293 | ||
| 294 | // ── check: probability that a condition holds ────────────────────────────── | |
| 295 | ||
| 296 | server.registerTool( | |
| 297 | "jev_check", | |
| 298 | { | |
| 299 | title: "Yes/no with a probability", | |
| 300 | description: | |
| 301 | "Ask a yes/no question. Returns the probability that the answer is yes, from 0 to 1, plus a verdict. There is no separate confidence: a value near 0.5 means yes and no are close to equally likely, not that the answer is 'medium'. " + | |
| 302 | "Use one check per label when several labels may apply at once.", | |
| 303 | inputSchema: { | |
| 304 | state: StateSchema, | |
| 305 | question: InstructionSchema, | |
| 306 | yes_means: DescriptionSchema.optional().describe("What a yes means. Sharpens the judgment."), | |
| 307 | no_means: DescriptionSchema.optional().describe("What a no means."), | |
| 308 | yes_at_or_above: z.number().min(0).max(1).optional().describe("Probability at or above which the verdict is 'yes'. Default 0.7."), | |
| 309 | no_at_or_below: z.number().min(0).max(1).optional().describe("Probability at or below which the verdict is 'no'. Default 0.3. Between the two the verdict is 'uncertain'."), | |
| 310 | }, | |
| 311 | outputSchema: { | |
| 312 | probability_yes: z.number(), | |
| 313 | verdict: z.enum(["yes", "no", "uncertain"]), | |
| 314 | thresholds: z.object({ yes_at_or_above: z.number(), no_at_or_below: z.number() }), | |
| 315 | model: z.string(), | |
| 316 | usage: UsageSchema, | |
| 317 | }, | |
| 318 | }, | |
| 319 | async ({ state, question, yes_means, no_means, yes_at_or_above, no_at_or_below }, extra) => { | |
| 320 | try { | |
| 321 | assertStateWithinLimit(state, maxStateChars.value); | |
| 322 | const yesAt = yes_at_or_above ?? 0.7; | |
| 323 | const noAt = no_at_or_below ?? 0.3; | |
| 324 | if (noAt > yesAt) throw new Error("no_at_or_below must not exceed yes_at_or_above."); | |
| 325 | ||
| 326 | const criteria = | |
| 327 | yes_means !== undefined || no_means !== undefined | |
| 328 | ? { true: yes_means ?? null, false: no_means ?? null } | |
| 329 | : undefined; | |
| 330 | ||
| 331 | const result = await getClient().systemOne( | |
| 332 | { state, model: MODEL, questions: { check: criteria ? noul(question, criteria) : noul(question) } }, | |
| 333 | { signal: extra.signal }, | |
| 334 | ); | |
| 335 | const probability = result.answers.check.noul; | |
| 336 | return ok({ | |
| 337 | probability_yes: probability, | |
| 338 | verdict: gateProbability(probability, yesAt, noAt), | |
| 339 | thresholds: { yes_at_or_above: yesAt, no_at_or_below: noAt }, | |
| 340 | model: result.model, | |
| 341 | usage: result.usage, | |
| 342 | }); | |
| 343 | } catch (error) { | |
| 344 | return fail(error); | |
| 345 | } | |
| 346 | }, | |
| 347 | ); | |
| 348 | ||
| 349 | // ── ask: many questions about one state, in a single request ─────────────── | |
| 350 | ||
| 351 | server.registerTool( | |
| 352 | "jev_ask", | |
| 353 | { | |
| 354 | title: "Ask many questions about one state", | |
| 355 | description: | |
| 356 | "Ask several independent questions about the same state in ONE request. Jev prefills the state once and scores every question in a single forward pass, so extra questions add almost no latency. " + | |
| 357 | "Prefer this over repeated single-question calls: on a document-dominated workload it is dramatically cheaper and faster with no change in answers. " + | |
| 358 | "Questions cannot see each other's answers, so state any speculative premise explicitly and let your own logic decide which answers apply.", | |
| 359 | inputSchema: { | |
| 360 | state: StateSchema, | |
| 361 | questions: z | |
| 362 | .array( | |
| 363 | z.object({ | |
| 364 | id: z.string().min(1).describe("Your key for this question. Returned alongside the answer. Never sent to the model, so put the full meaning in the question itself. Must be unique within the call."), | |
| 365 | type: z.enum(["classify", "score", "check"]), | |
| 366 | question: InstructionSchema, | |
| 367 | options: z.record(DescriptionSchema).optional().describe("Required for type 'classify'."), | |
| 368 | add_none: z.boolean().optional().describe("For 'classify': add a no-match option. Defaults to true."), | |
| 369 | levels: z.array(DescriptionSchema).min(2).optional().describe("Required for type 'score'."), | |
| 370 | yes_means: DescriptionSchema.optional().describe("For 'check': what a yes means."), | |
| 371 | no_means: DescriptionSchema.optional().describe("For 'check': what a no means."), | |
| 372 | }), | |
| 373 | ) | |
| 374 | .min(1), | |
| 375 | act_above: ActAbove, | |
| 376 | review_above: ReviewAbove, | |
| 377 | }, | |
| 378 | outputSchema: { | |
| 379 | answers: z.record(z.any()).describe("Keyed by your question ids. Choice and Score answers also carry an 'action' gated on confidence."), | |
| 380 | none_options: z.record(z.string().nullable()).describe("For each 'classify' question, the key carrying the no-match meaning, or null."), | |
| 381 | model: z.string(), | |
| 382 | usage: UsageSchema, | |
| 383 | }, | |
| 384 | }, | |
| 385 | async ({ state, questions, act_above, review_above }, extra) => { | |
| 386 | try { | |
| 387 | assertStateWithinLimit(state, maxStateChars.value); | |
| 388 | if (questions.length > maxQuestions.value) { | |
| 389 | throw new Error(`questions must contain at most ${maxQuestions.value} entries; received ${questions.length}. Raise JEV_MAX_QUESTIONS if that limit is wrong for your workload.`); | |
| 390 | } | |
| 391 | assertUniqueIds(questions.map((q) => q.id)); | |
| 392 | const actAbove = act_above ?? 0.8; | |
| 393 | const reviewAbove = review_above ?? 0.5; | |
| 394 | ||
| 395 | const built: Record<string, Question> = {}; | |
| 396 | const noneOptions: Record<string, string | null> = {}; | |
| 397 | ||
| 398 | for (const q of questions) { | |
| 399 | if (q.type === "classify") { | |
| 400 | if (!q.options) throw new Error(`Question '${q.id}' is type 'classify' and needs options.`); | |
| 401 | const { criteria, noneKey } = buildChoiceCriteria(q.options as Record<string, EntryType>, q.add_none !== false, `options for '${q.id}'`); | |
| 402 | built[q.id] = choice(q.question, criteria); | |
| 403 | noneOptions[q.id] = noneKey; | |
| 404 | } else if (q.type === "score") { | |
| 405 | if (!q.levels) throw new Error(`Question '${q.id}' is type 'score' and needs levels.`); | |
| 406 | if (q.levels.length > DEFAULT_MAX_SCORE_LEVELS) { | |
| 407 | throw new Error(`levels for '${q.id}' must contain at most ${DEFAULT_MAX_SCORE_LEVELS} entries.`); | |
| 408 | } | |
| 409 | built[q.id] = score(q.question, q.levels as unknown as ScoreCriteria); | |
| 410 | } else { | |
| 411 | const criteria = | |
| 412 | q.yes_means !== undefined || q.no_means !== undefined | |
| 413 | ? { true: q.yes_means ?? null, false: q.no_means ?? null } | |
| 414 | : undefined; | |
| 415 | built[q.id] = criteria ? noul(q.question, criteria) : noul(q.question); | |
| 416 | } | |
| 417 | } | |
| 418 | ||
| 419 | const result = await getClient().systemOne({ state, model: MODEL, questions: built }, { signal: extra.signal }); | |
| 420 | ||
| 421 | // Attach the confidence gate where there is a confidence to gate on. | |
| 422 | const answers: Record<string, unknown> = {}; | |
| 423 | for (const [id, answer] of Object.entries(result.answers as unknown as Record<string, Record<string, unknown>>)) { | |
| 424 | answers[id] = | |
| 425 | typeof answer?.confidence === "number" | |
| 426 | ? { ...answer, action: gateConfidence(answer.confidence, actAbove, reviewAbove) } | |
| 427 | : answer; | |
| 428 | } | |
| 429 | ||
| 430 | return ok({ answers, none_options: noneOptions, model: result.model, usage: result.usage }); | |
| 431 | } catch (error) { | |
| 432 | return fail(error); | |
| 433 | } | |
| 434 | }, | |
| 435 | ); | |
| 436 | ||
| 437 | // ── models: what this key can actually use ───────────────────────────────── | |
| 438 | ||
| 439 | server.registerTool( | |
| 440 | "jev_models", | |
| 441 | { | |
| 442 | title: "List available models", | |
| 443 | description: | |
| 444 | "List the models this API key can use, with their release dates. Use it to confirm the key works and to find a model id for JEV_MODEL before assuming one exists.", | |
| 445 | inputSchema: {}, | |
| 446 | outputSchema: { | |
| 447 | active_model: z.string().describe("The model these tools send requests to."), | |
| 448 | models: z.array(z.object({ name: z.string(), description: z.string(), release_date: z.string() })), | |
| 449 | }, | |
| 450 | }, | |
| 451 | async (_args, extra) => { | |
| 452 | try { | |
| 453 | const models = await getClient().models.list({ signal: extra.signal }); | |
| 454 | return ok({ active_model: MODEL, models }); | |
| 455 | } catch (error) { | |
| 456 | return fail(error); | |
| 457 | } | |
| 458 | }, | |
| 459 | ); | |
| 460 | ||
| 461 | // ── Boot ──────────────────────────────────────────────────────────────────── | |
| 462 | ||
| 463 | async function main() { | |
| 464 | await server.connect(new StdioServerTransport()); | |
| 465 | console.error(`[jev-mcp] ready — version ${VERSION}, model ${MODEL}`); | |
| 466 | } | |
| 467 | ||
| 468 | main().catch((error) => { | |
| 469 | console.error("[jev-mcp] failed to start:", error); | |
| 470 | process.exit(1); | |
| 471 | }); |
src/lib.ts added +279
| @@ -0,0 +1,279 @@ | ||
| 1 | /** | |
| 2 | * Pure helpers for jev-mcp. No network, no process state, no I/O. | |
| 3 | * | |
| 4 | * Everything here is deterministic so the edge cases that matter — a caller | |
| 5 | * whose own option is named "none", duplicate question ids, a malformed | |
| 6 | * timeout — are unit-testable without touching the API. | |
| 7 | */ | |
| 8 | ||
| 9 | import { | |
| 10 | APIConnectionError, | |
| 11 | APIError, | |
| 12 | APITimeoutError, | |
| 13 | APIUserAbortError, | |
| 14 | AuthenticationError, | |
| 15 | BadRequestError, | |
| 16 | NotFoundError, | |
| 17 | PermissionDeniedError, | |
| 18 | RateLimitError, | |
| 19 | UnprocessableEntityError, | |
| 20 | } from "@typesafe-ai/sdk"; | |
| 21 | import type { EntryType } from "@typesafe-ai/sdk"; | |
| 22 | ||
| 23 | // ── Limits ────────────────────────────────────────────────────────────────── | |
| 24 | ||
| 25 | /** | |
| 26 | * A Choice question accepts up to 255 options. This is a documented API limit, | |
| 27 | * not a local guess: docs.typesafe.ai/cookbooks/semantic_find. | |
| 28 | */ | |
| 29 | export const MAX_CHOICE_OPTIONS = 255; | |
| 30 | ||
| 31 | /** | |
| 32 | * Local safety caps, not API limits. They bound cost and latency for a single | |
| 33 | * call so a malformed loop cannot run up a bill. All three are configurable. | |
| 34 | */ | |
| 35 | export const DEFAULT_MAX_SCORE_LEVELS = 255; | |
| 36 | export const DEFAULT_MAX_QUESTIONS = 64; | |
| 37 | export const DEFAULT_MAX_STATE_CHARS = 200_000; | |
| 38 | export const DEFAULT_TIMEOUT_MS = 15_000; | |
| 39 | ||
| 40 | // ── Environment parsing ───────────────────────────────────────────────────── | |
| 41 | ||
| 42 | export interface ParsedNumber { | |
| 43 | value: number; | |
| 44 | /** Set when the raw value was present but unusable, so the caller can warn. */ | |
| 45 | warning?: string; | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Read a positive integer from an environment value. | |
| 50 | * | |
| 51 | * `Number("abc")` is `NaN`, and passing `NaN` as a timeout silently disables | |
| 52 | * it. Anything not a finite positive number falls back and reports why. | |
| 53 | */ | |
| 54 | export function readPositiveInt(raw: string | undefined, fallback: number, label: string): ParsedNumber { | |
| 55 | if (raw === undefined || raw.trim() === "") return { value: fallback }; | |
| 56 | const parsed = Number(raw); | |
| 57 | if (!Number.isFinite(parsed) || parsed <= 0) { | |
| 58 | return { value: fallback, warning: `${label} must be a positive number; got ${JSON.stringify(raw)}. Using ${fallback}.` }; | |
| 59 | } | |
| 60 | return { value: Math.floor(parsed) }; | |
| 61 | } | |
| 62 | ||
| 63 | // ── Choice option handling ────────────────────────────────────────────────── | |
| 64 | ||
| 65 | /** | |
| 66 | * Pick a key for the "none of these" escape hatch that does not collide with a | |
| 67 | * caller's own option. | |
| 68 | * | |
| 69 | * Blindly writing `criteria.none` overwrites an option the caller named "none", | |
| 70 | * destroying its meaning and its probability. That is silent and unrecoverable, | |
| 71 | * so we step aside instead. | |
| 72 | */ | |
| 73 | export function resolveNoneKey(existing: readonly string[]): string { | |
| 74 | const taken = new Set(existing); | |
| 75 | if (!taken.has("none")) return "none"; | |
| 76 | if (!taken.has("none_of_these")) return "none_of_these"; | |
| 77 | let n = 2; | |
| 78 | while (taken.has(`none_of_these_${n}`)) n += 1; | |
| 79 | return `none_of_these_${n}`; | |
| 80 | } | |
| 81 | ||
| 82 | export interface BuiltCriteria { | |
| 83 | criteria: Record<string, EntryType>; | |
| 84 | /** The key that carries the "nothing fits" meaning, or null when not added. */ | |
| 85 | noneKey: string | null; | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Build Choice criteria from caller options, optionally adding a no-match | |
| 90 | * outcome under a key guaranteed not to clash. | |
| 91 | * | |
| 92 | * @throws when the option set is empty or exceeds the API's 255-option limit. | |
| 93 | */ | |
| 94 | export function buildChoiceCriteria( | |
| 95 | options: Record<string, EntryType>, | |
| 96 | addNone: boolean, | |
| 97 | label = "options", | |
| 98 | ): BuiltCriteria { | |
| 99 | const keys = Object.keys(options); | |
| 100 | if (keys.length === 0) { | |
| 101 | throw new Error(`${label} must contain at least one option. Jev selects among options you supply; it cannot invent one.`); | |
| 102 | } | |
| 103 | ||
| 104 | const criteria: Record<string, EntryType> = { ...options }; | |
| 105 | let noneKey: string | null = null; | |
| 106 | if (addNone) { | |
| 107 | noneKey = resolveNoneKey(keys); | |
| 108 | criteria[noneKey] = "None of the other options fits."; | |
| 109 | } | |
| 110 | ||
| 111 | const total = Object.keys(criteria).length; | |
| 112 | if (total > MAX_CHOICE_OPTIONS) { | |
| 113 | throw new Error( | |
| 114 | `A Choice question accepts at most ${MAX_CHOICE_OPTIONS} options; received ${total}. ` + | |
| 115 | "For a larger set, search in two passes: one question picks a window, a second ranks within it.", | |
| 116 | ); | |
| 117 | } | |
| 118 | if (total < 2) { | |
| 119 | throw new Error(`${label} must leave at least two outcomes. Supply another option, or leave the no-match outcome enabled.`); | |
| 120 | } | |
| 121 | return { criteria, noneKey }; | |
| 122 | } | |
| 123 | ||
| 124 | /** Reject duplicate question ids rather than letting a later one shadow an earlier one. */ | |
| 125 | export function assertUniqueIds(ids: readonly string[]): void { | |
| 126 | const seen = new Set<string>(); | |
| 127 | const duplicates = new Set<string>(); | |
| 128 | for (const id of ids) { | |
| 129 | if (seen.has(id)) duplicates.add(id); | |
| 130 | seen.add(id); | |
| 131 | } | |
| 132 | if (duplicates.size > 0) { | |
| 133 | throw new Error( | |
| 134 | `Duplicate question ids: ${[...duplicates].map((d) => JSON.stringify(d)).join(", ")}. ` + | |
| 135 | "Answers are keyed by id, so a repeat would silently drop every earlier question sharing it.", | |
| 136 | ); | |
| 137 | } | |
| 138 | } | |
| 139 | ||
| 140 | // ── Size guards ───────────────────────────────────────────────────────────── | |
| 141 | ||
| 142 | /** Serialized size of the state, used to bound cost before a request goes out. */ | |
| 143 | export function stateSize(state: unknown): number { | |
| 144 | return typeof state === "string" ? state.length : JSON.stringify(state ?? null).length; | |
| 145 | } | |
| 146 | ||
| 147 | /** | |
| 148 | * Reject oversized state instead of truncating it. | |
| 149 | * | |
| 150 | * Truncation silently changes the material the judgment rests on, which turns a | |
| 151 | * size problem into a wrong answer. Failing loudly keeps the caller in control. | |
| 152 | */ | |
| 153 | export function assertStateWithinLimit(state: unknown, maxChars: number): void { | |
| 154 | const size = stateSize(state); | |
| 155 | if (size > maxChars) { | |
| 156 | throw new Error( | |
| 157 | `State is ${size} characters, above the ${maxChars} limit. ` + | |
| 158 | "Shorten it or select the relevant part first; raise JEV_MAX_STATE_CHARS if the limit is wrong for your workload.", | |
| 159 | ); | |
| 160 | } | |
| 161 | } | |
| 162 | ||
| 163 | // ── Confidence gating ─────────────────────────────────────────────────────── | |
| 164 | ||
| 165 | export type Gate = "act" | "review" | "abstain"; | |
| 166 | ||
| 167 | /** | |
| 168 | * Turn a Choice/Score confidence into a recommended action. | |
| 169 | * | |
| 170 | * Thresholds are starting points to calibrate on your own data, never universal | |
| 171 | * rules. Confidence describes how concentrated the distribution is; it is not a | |
| 172 | * statement that the answer is correct or that acting is safe. | |
| 173 | */ | |
| 174 | export function gateConfidence(confidence: number | null | undefined, actAbove: number, reviewAbove: number): Gate { | |
| 175 | if (typeof confidence !== "number" || Number.isNaN(confidence)) return "abstain"; | |
| 176 | if (confidence >= actAbove) return "act"; | |
| 177 | if (confidence >= reviewAbove) return "review"; | |
| 178 | return "abstain"; | |
| 179 | } | |
| 180 | ||
| 181 | export type Verdict = "yes" | "no" | "uncertain"; | |
| 182 | ||
| 183 | /** | |
| 184 | * Turn a Noul probability into a verdict. | |
| 185 | * | |
| 186 | * A Noul has no confidence value. A probability near 0.5 means yes and no are | |
| 187 | * close to equally likely, not that the answer is "medium", so the middle band | |
| 188 | * is reported as uncertain rather than rounded to the nearer side. | |
| 189 | */ | |
| 190 | export function gateProbability(probability: number, yesAtOrAbove: number, noAtOrBelow: number): Verdict { | |
| 191 | if (probability >= yesAtOrAbove) return "yes"; | |
| 192 | if (probability <= noAtOrBelow) return "no"; | |
| 193 | return "uncertain"; | |
| 194 | } | |
| 195 | ||
| 196 | // ── Error reporting ───────────────────────────────────────────────────────── | |
| 197 | ||
| 198 | export type ErrorKind = | |
| 199 | | "no_api_key" | |
| 200 | | "authentication" | |
| 201 | | "permission_denied" | |
| 202 | | "invalid_request" | |
| 203 | | "not_found" | |
| 204 | | "rate_limit" | |
| 205 | | "server_error" | |
| 206 | | "timeout" | |
| 207 | | "connection" | |
| 208 | | "cancelled" | |
| 209 | | "invalid_arguments" | |
| 210 | | "unknown"; | |
| 211 | ||
| 212 | export interface DescribedError { | |
| 213 | kind: ErrorKind; | |
| 214 | message: string; | |
| 215 | /** True when the same request may succeed later. The SDK already retried. */ | |
| 216 | retryable: boolean; | |
| 217 | status?: number; | |
| 218 | /** TypeSafe request id, the thing support needs to trace a failure. */ | |
| 219 | requestId?: string; | |
| 220 | /** What the caller should actually do next. */ | |
| 221 | hint?: string; | |
| 222 | } | |
| 223 | ||
| 224 | /** | |
| 225 | * Classify a failure so a caller can branch on it. | |
| 226 | * | |
| 227 | * Flattening every failure to a string makes a missing key, a malformed | |
| 228 | * question, and a rate limit indistinguishable, so callers retry things that | |
| 229 | * will never succeed and give up on things that would. | |
| 230 | */ | |
| 231 | export function describeError(error: unknown): DescribedError { | |
| 232 | const message = error instanceof Error ? error.message : String(error); | |
| 233 | ||
| 234 | if (error instanceof APIUserAbortError) { | |
| 235 | return { kind: "cancelled", message, retryable: false, hint: "The client cancelled the request." }; | |
| 236 | } | |
| 237 | if (error instanceof APITimeoutError) { | |
| 238 | return { | |
| 239 | kind: "timeout", | |
| 240 | message, | |
| 241 | retryable: true, | |
| 242 | hint: "Shorten the state or raise JEV_TIMEOUT_MS.", | |
| 243 | }; | |
| 244 | } | |
| 245 | if (error instanceof APIConnectionError) { | |
| 246 | return { kind: "connection", message, retryable: true, hint: "Check network access to api.typesafe.ai." }; | |
| 247 | } | |
| 248 | ||
| 249 | if (error instanceof APIError) { | |
| 250 | const base = { message, status: error.status, requestId: error.requestId }; | |
| 251 | if (error instanceof AuthenticationError) { | |
| 252 | return { ...base, kind: "authentication", retryable: false, hint: "The API key was rejected. Check TYPESAFE_API_KEY in the server's environment." }; | |
| 253 | } | |
| 254 | if (error instanceof PermissionDeniedError) { | |
| 255 | return { ...base, kind: "permission_denied", retryable: false, hint: "The key is valid but lacks access to this model or account." }; | |
| 256 | } | |
| 257 | if (error instanceof RateLimitError) { | |
| 258 | return { ...base, kind: "rate_limit", retryable: true, hint: "Rate limited after the SDK's own retries. Back off before trying again." }; | |
| 259 | } | |
| 260 | if (error instanceof UnprocessableEntityError || error instanceof BadRequestError) { | |
| 261 | return { ...base, kind: "invalid_request", retryable: false, hint: "The request failed validation. The message names the offending field." }; | |
| 262 | } | |
| 263 | if (error instanceof NotFoundError) { | |
| 264 | return { ...base, kind: "not_found", retryable: false, hint: "Check the model id in JEV_MODEL." }; | |
| 265 | } | |
| 266 | return { ...base, kind: "server_error", retryable: error.status >= 500, hint: "TypeSafe returned an error. Retry if it persists." }; | |
| 267 | } | |
| 268 | ||
| 269 | if (/^No API key/i.test(message)) { | |
| 270 | return { | |
| 271 | kind: "no_api_key", | |
| 272 | message, | |
| 273 | retryable: false, | |
| 274 | hint: "Set TYPESAFE_API_KEY in the environment of the MCP client, not as a tool argument. Many clients filter the environment, so pass it explicitly when registering the server.", | |
| 275 | }; | |
| 276 | } | |
| 277 | ||
| 278 | return { kind: "invalid_arguments", message, retryable: false }; | |
| 279 | } |
test/e2e.test.mjs added +85
| @@ -0,0 +1,85 @@ | ||
| 1 | // Live end-to-end tests against the real TypeSafe API. | |
| 2 | // | |
| 3 | // Skipped unless a credential is present in the environment, so `npm test` on a | |
| 4 | // clean checkout and in CI never depends on network access or spends tokens. | |
| 5 | // Run with: npm run test:e2e | |
| 6 | ||
| 7 | import assert from "node:assert/strict"; | |
| 8 | import test from "node:test"; | |
| 9 | import { payload, withClient } from "./helpers.mjs"; | |
| 10 | ||
| 11 | const KEY_VAR = ["TYPESAFE", "API", "KEY"].join("_"); | |
| 12 | const hasKey = Boolean(process.env[KEY_VAR]); | |
| 13 | const options = { skip: hasKey ? false : `set ${KEY_VAR} to run live tests` }; | |
| 14 | ||
| 15 | /** Forward the real credential to the server under test. */ | |
| 16 | function live(fn) { | |
| 17 | return withClient({ withKey: false, env: { [KEY_VAR]: process.env[KEY_VAR] } }, fn); | |
| 18 | } | |
| 19 | ||
| 20 | test("the key works and lists at least one model", options, async () => { | |
| 21 | await live(async (client) => { | |
| 22 | const body = payload(await client.callTool({ name: "jev_models", arguments: {} })); | |
| 23 | assert.ok(body.models.length >= 1, "expected at least one available model"); | |
| 24 | }); | |
| 25 | }); | |
| 26 | ||
| 27 | test("classify routes a support ticket to the right team", options, async () => { | |
| 28 | await live(async (client) => { | |
| 29 | const body = payload( | |
| 30 | await client.callTool({ | |
| 31 | name: "jev_classify", | |
| 32 | arguments: { | |
| 33 | state: "My running shoes arrived in the wrong size. Can I swap them for a size 10?", | |
| 34 | question: "Which team should handle this?", | |
| 35 | options: { | |
| 36 | returns: "Exchanges, refunds, wrong or damaged items", | |
| 37 | shipping: "Delivery status, delays, lost packages", | |
| 38 | billing: "Charges, invoices, payment problems", | |
| 39 | }, | |
| 40 | }, | |
| 41 | }), | |
| 42 | ); | |
| 43 | assert.equal(body.choice, "returns"); | |
| 44 | assert.ok(body.confidence > 0 && body.confidence <= 1); | |
| 45 | assert.ok(Object.keys(body.probabilities).includes("none")); | |
| 46 | }); | |
| 47 | }); | |
| 48 | ||
| 49 | test("check returns a high probability for a plainly urgent message", options, async () => { | |
| 50 | await live(async (client) => { | |
| 51 | const body = payload( | |
| 52 | await client.callTool({ | |
| 53 | name: "jev_check", | |
| 54 | arguments: { | |
| 55 | state: "Help! My payouts have been failing for 3 days and customers are complaining.", | |
| 56 | question: "Does this convey urgency?", | |
| 57 | }, | |
| 58 | }), | |
| 59 | ); | |
| 60 | assert.ok(body.probability_yes > 0.5, `expected an urgent read, got ${body.probability_yes}`); | |
| 61 | assert.equal(body.verdict, "yes"); | |
| 62 | }); | |
| 63 | }); | |
| 64 | ||
| 65 | test("ask answers several questions about one state in a single request", options, async () => { | |
| 66 | await live(async (client) => { | |
| 67 | const body = payload( | |
| 68 | await client.callTool({ | |
| 69 | name: "jev_ask", | |
| 70 | arguments: { | |
| 71 | state: "I was charged twice for order A-104. Please refund the duplicate.", | |
| 72 | questions: [ | |
| 73 | { id: "billing", type: "check", question: "Is this about billing?" }, | |
| 74 | { id: "refund", type: "check", question: "Is the customer asking for a refund?" }, | |
| 75 | { id: "anger", type: "score", question: "How frustrated is the customer?", levels: ["Calm", "Frustrated", "Very angry"] }, | |
| 76 | ], | |
| 77 | }, | |
| 78 | }), | |
| 79 | ); | |
| 80 | assert.ok(body.answers.billing.noul > 0.5); | |
| 81 | assert.ok(body.answers.refund.noul > 0.5); | |
| 82 | assert.ok(typeof body.answers.anger.score === "number"); | |
| 83 | assert.ok(body.usage.input_tokens > 0); | |
| 84 | }); | |
| 85 | }); |
test/helpers.mjs added +137
| @@ -0,0 +1,137 @@ | ||
| 1 | // Shared test rig: a local stand-in for the TypeSafe API plus an MCP client. | |
| 2 | // | |
| 3 | // Tests drive the real built server over stdio through the official MCP client, | |
| 4 | // so there are no sleeps and no hand-rolled JSON-RPC framing. The stand-in | |
| 5 | // records every outbound request, which is how the regression tests prove what | |
| 6 | // the server actually sends rather than what it claims to send. | |
| 7 | ||
| 8 | import { createServer } from "node:http"; | |
| 9 | import { fileURLToPath } from "node:url"; | |
| 10 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | |
| 11 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; | |
| 12 | ||
| 13 | export const SERVER_PATH = fileURLToPath(new URL("../dist/index.js", import.meta.url)); | |
| 14 | ||
| 15 | /** Name assembled at runtime so scanners don't read this file as a credential. */ | |
| 16 | const KEY_VAR = ["TYPESAFE", "API", "KEY"].join("_"); | |
| 17 | ||
| 18 | /** Start a stand-in API that records requests and returns well-formed answers. */ | |
| 19 | export async function startMock() { | |
| 20 | const requests = []; | |
| 21 | const state = { confidence: 0.9, noul: 0.5, status: 200, body: null }; | |
| 22 | ||
| 23 | const server = createServer((req, res) => { | |
| 24 | let raw = ""; | |
| 25 | req.on("data", (d) => (raw += d)); | |
| 26 | req.on("end", () => { | |
| 27 | let parsed; | |
| 28 | try { | |
| 29 | parsed = JSON.parse(raw || "{}"); | |
| 30 | } catch { | |
| 31 | parsed = raw; | |
| 32 | } | |
| 33 | requests.push({ url: req.url, body: parsed }); | |
| 34 | ||
| 35 | res.setHeader("content-type", "application/json"); | |
| 36 | res.setHeader("x-typesafe-request-id", "req_test_123"); | |
| 37 | ||
| 38 | if (state.status !== 200) { | |
| 39 | res.writeHead(state.status); | |
| 40 | res.end(JSON.stringify(state.body ?? { error: { message: "mock failure" } })); | |
| 41 | return; | |
| 42 | } | |
| 43 | ||
| 44 | if (req.url?.includes("/models")) { | |
| 45 | res.writeHead(200); | |
| 46 | res.end(JSON.stringify({ models: [{ name: "jev-latest", description: "Flagship", release_date: "2026-01-01" }] })); | |
| 47 | return; | |
| 48 | } | |
| 49 | ||
| 50 | const answers = {}; | |
| 51 | for (const [id, q] of Object.entries(parsed.questions ?? {})) { | |
| 52 | if (q.type === "noul") { | |
| 53 | answers[id] = { type: "noul", noul: state.noul }; | |
| 54 | } else if (q.type === "choice") { | |
| 55 | const keys = Object.keys(q.criteria ?? {}); | |
| 56 | answers[id] = { | |
| 57 | type: "choice", | |
| 58 | choice: keys[0] ?? "x", | |
| 59 | confidence: state.confidence, | |
| 60 | probabilities: Object.fromEntries(keys.map((k, i) => [k, i === 0 ? 0.9 : 0.1])), | |
| 61 | }; | |
| 62 | } else { | |
| 63 | const levels = (q.criteria ?? []).map((_, i) => String(i)); | |
| 64 | answers[id] = { | |
| 65 | type: "score", | |
| 66 | score: 1, | |
| 67 | confidence: state.confidence, | |
| 68 | legend: Object.fromEntries(levels.map((k, i) => [k, q.criteria[i]])), | |
| 69 | probabilities: Object.fromEntries(levels.map((k) => [k, 1 / levels.length])), | |
| 70 | }; | |
| 71 | } | |
| 72 | } | |
| 73 | res.writeHead(200); | |
| 74 | res.end(JSON.stringify({ model: "mock-jev", answers, usage: { input_tokens: 10, output_tokens: 2 } })); | |
| 75 | }); | |
| 76 | }); | |
| 77 | ||
| 78 | await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); | |
| 79 | const url = `http://127.0.0.1:${server.address().port}`; | |
| 80 | ||
| 81 | return { | |
| 82 | url, | |
| 83 | requests, | |
| 84 | state, | |
| 85 | /** The single outbound request body, asserting exactly one was sent. */ | |
| 86 | only() { | |
| 87 | if (requests.length !== 1) throw new Error(`expected exactly 1 request, saw ${requests.length}`); | |
| 88 | return requests[0].body; | |
| 89 | }, | |
| 90 | async close() { | |
| 91 | await new Promise((resolve) => server.close(resolve)); | |
| 92 | }, | |
| 93 | }; | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Connect an MCP client to the built server. | |
| 98 | * | |
| 99 | * `withKey: false` starts the server with no credential at all, which is how | |
| 100 | * the missing-key path is exercised. | |
| 101 | */ | |
| 102 | export async function withClient({ baseUrl, withKey = true, env = {} } = {}, fn) { | |
| 103 | // Pin the key-file fallback at a path that cannot exist. HOME is passed | |
| 104 | // through, so without this the suite would start reading a real | |
| 105 | // ~/.config/typesafe/key the moment one exists, and the missing-key test | |
| 106 | // would quietly begin making live calls instead of exercising the failure | |
| 107 | // path. A test that needs the fallback passes JEV_KEY_FILE explicitly. | |
| 108 | const childEnv = { | |
| 109 | PATH: process.env.PATH, | |
| 110 | HOME: process.env.HOME, | |
| 111 | JEV_KEY_FILE: "/nonexistent/jev-mcp-test/key", | |
| 112 | ...env, | |
| 113 | }; | |
| 114 | if (baseUrl) childEnv.TYPESAFE_BASE_URL = baseUrl; | |
| 115 | if (withKey) childEnv[KEY_VAR] = "value-for-the-local-stand-in"; | |
| 116 | ||
| 117 | const client = new Client({ name: "jev-mcp-test", version: "0.2.0" }); | |
| 118 | const transport = new StdioClientTransport({ | |
| 119 | command: process.execPath, | |
| 120 | args: [SERVER_PATH], | |
| 121 | env: childEnv, | |
| 122 | stderr: "ignore", | |
| 123 | }); | |
| 124 | await client.connect(transport); | |
| 125 | try { | |
| 126 | return await fn(client); | |
| 127 | } finally { | |
| 128 | await client.close(); | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | /** Parse the JSON a tool returns in its text block. */ | |
| 133 | export function payload(result) { | |
| 134 | const block = result.content?.find((b) => b.type === "text"); | |
| 135 | if (!block) throw new Error("tool returned no text content"); | |
| 136 | return JSON.parse(block.text); | |
| 137 | } |
test/lib.test.mjs added +122
| @@ -0,0 +1,122 @@ | ||
| 1 | // Unit tests for the pure helpers. No network, no child process. | |
| 2 | ||
| 3 | import assert from "node:assert/strict"; | |
| 4 | import test from "node:test"; | |
| 5 | import { APIError } from "@typesafe-ai/sdk"; | |
| 6 | import { | |
| 7 | assertStateWithinLimit, | |
| 8 | assertUniqueIds, | |
| 9 | buildChoiceCriteria, | |
| 10 | describeError, | |
| 11 | gateConfidence, | |
| 12 | gateProbability, | |
| 13 | MAX_CHOICE_OPTIONS, | |
| 14 | readPositiveInt, | |
| 15 | resolveNoneKey, | |
| 16 | stateSize, | |
| 17 | } from "../dist/lib.js"; | |
| 18 | ||
| 19 | test("readPositiveInt falls back loudly instead of yielding NaN", () => { | |
| 20 | assert.deepEqual(readPositiveInt(undefined, 15000, "T"), { value: 15000 }); | |
| 21 | assert.deepEqual(readPositiveInt(" ", 15000, "T"), { value: 15000 }); | |
| 22 | assert.equal(readPositiveInt("500", 15000, "T").value, 500); | |
| 23 | assert.equal(readPositiveInt("12.7", 15000, "T").value, 12); | |
| 24 | ||
| 25 | for (const bad of ["abc", "0", "-5", "NaN"]) { | |
| 26 | const parsed = readPositiveInt(bad, 15000, "T"); | |
| 27 | assert.equal(parsed.value, 15000, `${bad} should fall back`); | |
| 28 | assert.ok(parsed.warning, `${bad} should warn`); | |
| 29 | } | |
| 30 | }); | |
| 31 | ||
| 32 | test("resolveNoneKey steps aside when the caller already uses the name", () => { | |
| 33 | assert.equal(resolveNoneKey([]), "none"); | |
| 34 | assert.equal(resolveNoneKey(["a", "b"]), "none"); | |
| 35 | assert.equal(resolveNoneKey(["none"]), "none_of_these"); | |
| 36 | assert.equal(resolveNoneKey(["none", "none_of_these"]), "none_of_these_2"); | |
| 37 | }); | |
| 38 | ||
| 39 | test("buildChoiceCriteria never overwrites a caller's own option", () => { | |
| 40 | const callerMeaning = "The request was already handled; no action needed."; | |
| 41 | const { criteria, noneKey } = buildChoiceCriteria({ none: callerMeaning, escalate: "Send to a human" }, true); | |
| 42 | ||
| 43 | assert.equal(criteria.none, callerMeaning, "the caller's option must survive verbatim"); | |
| 44 | assert.equal(noneKey, "none_of_these"); | |
| 45 | assert.equal(criteria.none_of_these, "None of the other options fits."); | |
| 46 | }); | |
| 47 | ||
| 48 | test("buildChoiceCriteria reports the no-match key it used", () => { | |
| 49 | const { criteria, noneKey } = buildChoiceCriteria({ a: "d", b: "d" }, true); | |
| 50 | assert.equal(noneKey, "none"); | |
| 51 | assert.deepEqual(Object.keys(criteria).sort(), ["a", "b", "none"]); | |
| 52 | ||
| 53 | const off = buildChoiceCriteria({ a: "d", b: "d" }, false); | |
| 54 | assert.equal(off.noneKey, null); | |
| 55 | assert.deepEqual(Object.keys(off.criteria).sort(), ["a", "b"]); | |
| 56 | }); | |
| 57 | ||
| 58 | test("buildChoiceCriteria rejects empty, single-outcome, and oversized option sets", () => { | |
| 59 | assert.throws(() => buildChoiceCriteria({}, true), /at least one option/); | |
| 60 | assert.throws(() => buildChoiceCriteria({ only: "d" }, false), /at least two outcomes/); | |
| 61 | ||
| 62 | const tooMany = Object.fromEntries(Array.from({ length: MAX_CHOICE_OPTIONS + 1 }, (_, i) => [`o${i}`, null])); | |
| 63 | assert.throws(() => buildChoiceCriteria(tooMany, false), /at most 255 options/); | |
| 64 | ||
| 65 | const exactlyAtLimit = Object.fromEntries(Array.from({ length: MAX_CHOICE_OPTIONS }, (_, i) => [`o${i}`, null])); | |
| 66 | assert.doesNotThrow(() => buildChoiceCriteria(exactlyAtLimit, false)); | |
| 67 | }); | |
| 68 | ||
| 69 | test("assertUniqueIds names the duplicates rather than dropping questions", () => { | |
| 70 | assert.doesNotThrow(() => assertUniqueIds(["a", "b", "c"])); | |
| 71 | assert.throws(() => assertUniqueIds(["a", "b", "a"]), /Duplicate question ids: "a"/); | |
| 72 | }); | |
| 73 | ||
| 74 | test("state size is measured, and oversized state is rejected not truncated", () => { | |
| 75 | assert.equal(stateSize("abcde"), 5); | |
| 76 | assert.equal(stateSize({ a: 1 }), JSON.stringify({ a: 1 }).length); | |
| 77 | ||
| 78 | assert.doesNotThrow(() => assertStateWithinLimit("abc", 10)); | |
| 79 | assert.throws(() => assertStateWithinLimit("a".repeat(20), 10), /above the 10 limit/); | |
| 80 | }); | |
| 81 | ||
| 82 | test("gateConfidence maps confidence onto an action", () => { | |
| 83 | assert.equal(gateConfidence(0.95, 0.8, 0.5), "act"); | |
| 84 | assert.equal(gateConfidence(0.8, 0.8, 0.5), "act"); | |
| 85 | assert.equal(gateConfidence(0.6, 0.8, 0.5), "review"); | |
| 86 | assert.equal(gateConfidence(0.2, 0.8, 0.5), "abstain"); | |
| 87 | assert.equal(gateConfidence(null, 0.8, 0.5), "abstain"); | |
| 88 | assert.equal(gateConfidence(undefined, 0.8, 0.5), "abstain"); | |
| 89 | }); | |
| 90 | ||
| 91 | test("gateProbability keeps the middle band uncertain instead of rounding", () => { | |
| 92 | assert.equal(gateProbability(0.9, 0.7, 0.3), "yes"); | |
| 93 | assert.equal(gateProbability(0.7, 0.7, 0.3), "yes"); | |
| 94 | assert.equal(gateProbability(0.51, 0.7, 0.3), "uncertain", "0.51 is a coin flip, not a yes"); | |
| 95 | assert.equal(gateProbability(0.3, 0.7, 0.3), "no"); | |
| 96 | assert.equal(gateProbability(0.05, 0.7, 0.3), "no"); | |
| 97 | }); | |
| 98 | ||
| 99 | test("describeError separates failures a caller should treat differently", () => { | |
| 100 | const missing = describeError(new Error("No API key. Set TYPESAFE_API_KEY in the environment.")); | |
| 101 | assert.equal(missing.kind, "no_api_key"); | |
| 102 | assert.equal(missing.retryable, false); | |
| 103 | ||
| 104 | const headers = new Headers({ "x-typesafe-request-id": "req_abc" }); | |
| 105 | const rateLimited = describeError(APIError.fromResponse(429, {}, headers)); | |
| 106 | assert.equal(rateLimited.kind, "rate_limit"); | |
| 107 | assert.equal(rateLimited.retryable, true); | |
| 108 | assert.equal(rateLimited.status, 429); | |
| 109 | assert.equal(rateLimited.requestId, "req_abc"); | |
| 110 | ||
| 111 | const unauthorized = describeError(APIError.fromResponse(401, {}, new Headers())); | |
| 112 | assert.equal(unauthorized.kind, "authentication"); | |
| 113 | assert.equal(unauthorized.retryable, false, "a bad key never succeeds on retry"); | |
| 114 | ||
| 115 | const malformed = describeError(APIError.fromResponse(422, {}, new Headers())); | |
| 116 | assert.equal(malformed.kind, "invalid_request"); | |
| 117 | assert.equal(malformed.retryable, false); | |
| 118 | ||
| 119 | const overloaded = describeError(APIError.fromResponse(529, {}, new Headers())); | |
| 120 | assert.equal(overloaded.kind, "server_error"); | |
| 121 | assert.equal(overloaded.retryable, true); | |
| 122 | }); |
test/tools.test.mjs added +372
| @@ -0,0 +1,372 @@ | ||
| 1 | // Contract and regression tests for the tool surface. | |
| 2 | // | |
| 3 | // These run the real built server over stdio against a local stand-in for the | |
| 4 | // TypeSafe API, so each assertion is about what the server actually sends and | |
| 5 | // returns. Every test named "regression" pins a defect that was confirmed on | |
| 6 | // the wire before it was fixed. | |
| 7 | ||
| 8 | import assert from "node:assert/strict"; | |
| 9 | import test from "node:test"; | |
| 10 | import { payload, startMock, withClient } from "./helpers.mjs"; | |
| 11 | ||
| 12 | const TOOLS = ["jev_ask", "jev_check", "jev_classify", "jev_models", "jev_score"]; | |
| 13 | ||
| 14 | // ── Contract ──────────────────────────────────────────────────────────────── | |
| 15 | ||
| 16 | test("advertises exactly the five judgment tools", async () => { | |
| 17 | await withClient({ withKey: false }, async (client) => { | |
| 18 | const { tools } = await client.listTools(); | |
| 19 | assert.deepEqual(tools.map((t) => t.name).sort(), TOOLS); | |
| 20 | }); | |
| 21 | }); | |
| 22 | ||
| 23 | test("every tool documents itself and declares both input and output schemas", async () => { | |
| 24 | await withClient({ withKey: false }, async (client) => { | |
| 25 | const { tools } = await client.listTools(); | |
| 26 | for (const tool of tools) { | |
| 27 | assert.ok(tool.description?.length > 40, `${tool.name} needs a real description`); | |
| 28 | assert.ok(tool.inputSchema, `${tool.name} has no inputSchema`); | |
| 29 | assert.equal(tool.inputSchema.type, "object"); | |
| 30 | assert.ok(tool.outputSchema, `${tool.name} has no outputSchema, so callers get an opaque string`); | |
| 31 | } | |
| 32 | }); | |
| 33 | }); | |
| 34 | ||
| 35 | test("classify requires caller-supplied options, so the model cannot invent one", async () => { | |
| 36 | await withClient({ withKey: false }, async (client) => { | |
| 37 | const { tools } = await client.listTools(); | |
| 38 | const classify = tools.find((t) => t.name === "jev_classify"); | |
| 39 | for (const field of ["state", "question", "options"]) { | |
| 40 | assert.ok(classify.inputSchema.required.includes(field), `${field} must be required`); | |
| 41 | } | |
| 42 | const scoreTool = tools.find((t) => t.name === "jev_score"); | |
| 43 | assert.ok(scoreTool.inputSchema.required.includes("levels")); | |
| 44 | assert.equal(scoreTool.inputSchema.properties.levels.minItems, 2); | |
| 45 | }); | |
| 46 | }); | |
| 47 | ||
| 48 | test("a missing API key fails loudly and says what to do about it", async () => { | |
| 49 | await withClient({ withKey: false }, async (client) => { | |
| 50 | const result = await client.callTool({ | |
| 51 | name: "jev_check", | |
| 52 | arguments: { state: "anything", question: "Is this a greeting?" }, | |
| 53 | }); | |
| 54 | assert.equal(result.isError, true, "must report an error, not a fabricated answer"); | |
| 55 | const { error } = payload(result); | |
| 56 | assert.equal(error.kind, "no_api_key"); | |
| 57 | assert.equal(error.retryable, false); | |
| 58 | assert.match(error.hint, /environment/i); | |
| 59 | }); | |
| 60 | }); | |
| 61 | ||
| 62 | test("falls back to a key file when the environment carries no key", async () => { | |
| 63 | // An MCP server inherits the client's environment, not your shell's, so a key | |
| 64 | // exported from a shell profile never arrives. The file fallback is the only | |
| 65 | // route that reaches every spawn path, which makes it worth pinning here. | |
| 66 | const { mkdtempSync, writeFileSync } = await import("node:fs"); | |
| 67 | const { tmpdir } = await import("node:os"); | |
| 68 | const { join } = await import("node:path"); | |
| 69 | ||
| 70 | const keyFile = join(mkdtempSync(join(tmpdir(), "jev-key-")), "key"); | |
| 71 | writeFileSync(keyFile, " value-for-the-local-stand-in\n", { mode: 0o600 }); | |
| 72 | ||
| 73 | const mock = await startMock(); | |
| 74 | try { | |
| 75 | await withClient({ baseUrl: mock.url, withKey: false, env: { JEV_KEY_FILE: keyFile } }, async (client) => { | |
| 76 | const result = await client.callTool({ | |
| 77 | name: "jev_check", | |
| 78 | arguments: { state: "s", question: "Is this fine?" }, | |
| 79 | }); | |
| 80 | assert.notEqual(result.isError, true, "the key file should satisfy the credential check"); | |
| 81 | assert.equal(typeof payload(result).probability_yes, "number"); | |
| 82 | assert.equal(mock.requests.length, 1, "the request must actually reach the API"); | |
| 83 | }); | |
| 84 | } finally { | |
| 85 | await mock.close(); | |
| 86 | } | |
| 87 | }); | |
| 88 | ||
| 89 | // ── Regressions confirmed on the wire ─────────────────────────────────────── | |
| 90 | ||
| 91 | test("regression: an option the caller names 'none' is never overwritten", async () => { | |
| 92 | const mock = await startMock(); | |
| 93 | const callerMeaning = "Already resolved; no action needed."; | |
| 94 | try { | |
| 95 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 96 | const result = await client.callTool({ | |
| 97 | name: "jev_classify", | |
| 98 | arguments: { | |
| 99 | state: "s", | |
| 100 | question: "What should happen next?", | |
| 101 | options: { none: callerMeaning, escalate: "Send to a human" }, | |
| 102 | }, | |
| 103 | }); | |
| 104 | ||
| 105 | const sent = mock.only().questions.classify.criteria; | |
| 106 | assert.equal(sent.none, callerMeaning, "the caller's meaning must reach the model intact"); | |
| 107 | assert.equal(sent.none_of_these, "None of the other options fits."); | |
| 108 | ||
| 109 | const body = payload(result); | |
| 110 | assert.equal(body.none_option, "none_of_these", "the caller must be told which key means no-match"); | |
| 111 | }); | |
| 112 | } finally { | |
| 113 | await mock.close(); | |
| 114 | } | |
| 115 | }); | |
| 116 | ||
| 117 | test("regression: add_none false sends exactly the caller's options", async () => { | |
| 118 | const mock = await startMock(); | |
| 119 | try { | |
| 120 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 121 | const result = await client.callTool({ | |
| 122 | name: "jev_classify", | |
| 123 | arguments: { state: "s", question: "q", options: { a: "first", b: "second" }, add_none: false }, | |
| 124 | }); | |
| 125 | assert.deepEqual(Object.keys(mock.only().questions.classify.criteria).sort(), ["a", "b"]); | |
| 126 | assert.equal(payload(result).none_option, null); | |
| 127 | }); | |
| 128 | } finally { | |
| 129 | await mock.close(); | |
| 130 | } | |
| 131 | }); | |
| 132 | ||
| 133 | test("regression: an empty or oversized option set fails before any request is sent", async () => { | |
| 134 | const mock = await startMock(); | |
| 135 | try { | |
| 136 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 137 | const empty = await client.callTool({ | |
| 138 | name: "jev_classify", | |
| 139 | arguments: { state: "s", question: "q", options: {}, add_none: false }, | |
| 140 | }); | |
| 141 | assert.equal(empty.isError, true); | |
| 142 | assert.match(payload(empty).error.message, /at least one option/); | |
| 143 | ||
| 144 | const tooMany = Object.fromEntries(Array.from({ length: 256 }, (_, i) => [`o${i}`, null])); | |
| 145 | const oversized = await client.callTool({ | |
| 146 | name: "jev_classify", | |
| 147 | arguments: { state: "s", question: "q", options: tooMany, add_none: false }, | |
| 148 | }); | |
| 149 | assert.equal(oversized.isError, true); | |
| 150 | assert.match(payload(oversized).error.message, /at most 255 options/); | |
| 151 | ||
| 152 | assert.equal(mock.requests.length, 0, "neither request should reach the API"); | |
| 153 | }); | |
| 154 | } finally { | |
| 155 | await mock.close(); | |
| 156 | } | |
| 157 | }); | |
| 158 | ||
| 159 | test("regression: duplicate question ids fail instead of silently dropping a question", async () => { | |
| 160 | const mock = await startMock(); | |
| 161 | try { | |
| 162 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 163 | const result = await client.callTool({ | |
| 164 | name: "jev_ask", | |
| 165 | arguments: { | |
| 166 | state: "s", | |
| 167 | questions: [ | |
| 168 | { id: "dup", type: "check", question: "first" }, | |
| 169 | { id: "dup", type: "check", question: "second" }, | |
| 170 | ], | |
| 171 | }, | |
| 172 | }); | |
| 173 | assert.equal(result.isError, true); | |
| 174 | assert.match(payload(result).error.message, /Duplicate question ids: "dup"/); | |
| 175 | assert.equal(mock.requests.length, 0, "no partial request should be sent"); | |
| 176 | }); | |
| 177 | } finally { | |
| 178 | await mock.close(); | |
| 179 | } | |
| 180 | }); | |
| 181 | ||
| 182 | test("regression: jev_ask honours add_none and passes yes/no meanings", async () => { | |
| 183 | const mock = await startMock(); | |
| 184 | try { | |
| 185 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 186 | await client.callTool({ | |
| 187 | name: "jev_ask", | |
| 188 | arguments: { | |
| 189 | state: "s", | |
| 190 | questions: [ | |
| 191 | { id: "pick", type: "classify", question: "q", options: { a: "d", b: "d" }, add_none: false }, | |
| 192 | { id: "flag", type: "check", question: "q", yes_means: "It is urgent", no_means: "It can wait" }, | |
| 193 | ], | |
| 194 | }, | |
| 195 | }); | |
| 196 | ||
| 197 | const sent = mock.only().questions; | |
| 198 | assert.deepEqual(Object.keys(sent.pick.criteria).sort(), ["a", "b"], "add_none:false must be honoured"); | |
| 199 | assert.deepEqual(sent.flag.criteria, { true: "It is urgent", false: "It can wait" }); | |
| 200 | }); | |
| 201 | } finally { | |
| 202 | await mock.close(); | |
| 203 | } | |
| 204 | }); | |
| 205 | ||
| 206 | test("regression: a verbose SDK log level does not corrupt the JSON-RPC stream", async () => { | |
| 207 | const mock = await startMock(); | |
| 208 | try { | |
| 209 | await withClient({ baseUrl: mock.url, env: { TYPESAFE_LOG_LEVEL: "debug" } }, async (client) => { | |
| 210 | const result = await client.callTool({ | |
| 211 | name: "jev_check", | |
| 212 | arguments: { state: "s", question: "Is this fine?" }, | |
| 213 | }); | |
| 214 | assert.notEqual(result.isError, true, "debug logging must not break the connection"); | |
| 215 | assert.equal(typeof payload(result).probability_yes, "number"); | |
| 216 | }); | |
| 217 | } finally { | |
| 218 | await mock.close(); | |
| 219 | } | |
| 220 | }); | |
| 221 | ||
| 222 | // ── Behaviour ─────────────────────────────────────────────────────────────── | |
| 223 | ||
| 224 | test("results carry structured content matching the text block", async () => { | |
| 225 | const mock = await startMock(); | |
| 226 | try { | |
| 227 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 228 | const result = await client.callTool({ | |
| 229 | name: "jev_classify", | |
| 230 | arguments: { state: "s", question: "q", options: { a: "d", b: "d" } }, | |
| 231 | }); | |
| 232 | assert.ok(result.structuredContent, "clients should get typed data, not a JSON string"); | |
| 233 | assert.deepEqual(result.structuredContent, payload(result)); | |
| 234 | assert.equal(typeof result.structuredContent.probabilities.a, "number"); | |
| 235 | }); | |
| 236 | } finally { | |
| 237 | await mock.close(); | |
| 238 | } | |
| 239 | }); | |
| 240 | ||
| 241 | test("confidence drives the recommended action", async () => { | |
| 242 | const mock = await startMock(); | |
| 243 | try { | |
| 244 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 245 | const args = { state: "s", question: "q", options: { a: "d", b: "d" } }; | |
| 246 | ||
| 247 | mock.state.confidence = 0.95; | |
| 248 | assert.equal(payload(await client.callTool({ name: "jev_classify", arguments: args })).action, "act"); | |
| 249 | ||
| 250 | mock.state.confidence = 0.6; | |
| 251 | assert.equal(payload(await client.callTool({ name: "jev_classify", arguments: args })).action, "review"); | |
| 252 | ||
| 253 | mock.state.confidence = 0.2; | |
| 254 | assert.equal(payload(await client.callTool({ name: "jev_classify", arguments: args })).action, "abstain"); | |
| 255 | ||
| 256 | mock.state.confidence = 0.6; | |
| 257 | const strict = await client.callTool({ name: "jev_classify", arguments: { ...args, act_above: 0.5 } }); | |
| 258 | assert.equal(payload(strict).action, "act", "caller thresholds must override the defaults"); | |
| 259 | }); | |
| 260 | } finally { | |
| 261 | await mock.close(); | |
| 262 | } | |
| 263 | }); | |
| 264 | ||
| 265 | test("a near-even probability is reported as uncertain, not rounded to a yes", async () => { | |
| 266 | const mock = await startMock(); | |
| 267 | try { | |
| 268 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 269 | mock.state.noul = 0.52; | |
| 270 | const result = await client.callTool({ | |
| 271 | name: "jev_check", | |
| 272 | arguments: { state: "s", question: "Is this urgent?" }, | |
| 273 | }); | |
| 274 | const body = payload(result); | |
| 275 | assert.equal(body.probability_yes, 0.52); | |
| 276 | assert.equal(body.verdict, "uncertain"); | |
| 277 | }); | |
| 278 | } finally { | |
| 279 | await mock.close(); | |
| 280 | } | |
| 281 | }); | |
| 282 | ||
| 283 | test("oversized state is rejected before it costs anything", async () => { | |
| 284 | const mock = await startMock(); | |
| 285 | try { | |
| 286 | await withClient({ baseUrl: mock.url, env: { JEV_MAX_STATE_CHARS: "50" } }, async (client) => { | |
| 287 | const result = await client.callTool({ | |
| 288 | name: "jev_check", | |
| 289 | arguments: { state: "x".repeat(200), question: "q" }, | |
| 290 | }); | |
| 291 | assert.equal(result.isError, true); | |
| 292 | assert.match(payload(result).error.message, /above the 50 limit/); | |
| 293 | assert.equal(mock.requests.length, 0); | |
| 294 | }); | |
| 295 | } finally { | |
| 296 | await mock.close(); | |
| 297 | } | |
| 298 | }); | |
| 299 | ||
| 300 | test("an unusable timeout falls back instead of disabling the timeout", async () => { | |
| 301 | const mock = await startMock(); | |
| 302 | try { | |
| 303 | await withClient({ baseUrl: mock.url, env: { JEV_TIMEOUT_MS: "not-a-number" } }, async (client) => { | |
| 304 | const result = await client.callTool({ | |
| 305 | name: "jev_check", | |
| 306 | arguments: { state: "s", question: "q" }, | |
| 307 | }); | |
| 308 | assert.notEqual(result.isError, true, "a malformed timeout must not break the server"); | |
| 309 | }); | |
| 310 | } finally { | |
| 311 | await mock.close(); | |
| 312 | } | |
| 313 | }); | |
| 314 | ||
| 315 | test("API failures are classified so a caller can tell retryable from terminal", async () => { | |
| 316 | const mock = await startMock(); | |
| 317 | try { | |
| 318 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 319 | mock.state.status = 401; | |
| 320 | const unauthorized = await client.callTool({ name: "jev_check", arguments: { state: "s", question: "q" } }); | |
| 321 | assert.equal(unauthorized.isError, true); | |
| 322 | const { error } = payload(unauthorized); | |
| 323 | assert.equal(error.kind, "authentication"); | |
| 324 | assert.equal(error.retryable, false); | |
| 325 | assert.equal(error.requestId, "req_test_123", "the request id support needs must survive"); | |
| 326 | }); | |
| 327 | } finally { | |
| 328 | await mock.close(); | |
| 329 | } | |
| 330 | }); | |
| 331 | ||
| 332 | test("jev_models reports the active model and what the key can use", async () => { | |
| 333 | const mock = await startMock(); | |
| 334 | try { | |
| 335 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 336 | const body = payload(await client.callTool({ name: "jev_models", arguments: {} })); | |
| 337 | assert.equal(body.active_model, "jev-latest"); | |
| 338 | assert.equal(body.models[0].name, "jev-latest"); | |
| 339 | }); | |
| 340 | } finally { | |
| 341 | await mock.close(); | |
| 342 | } | |
| 343 | }); | |
| 344 | ||
| 345 | test("one jev_ask call batches every question into a single request", async () => { | |
| 346 | const mock = await startMock(); | |
| 347 | try { | |
| 348 | await withClient({ baseUrl: mock.url }, async (client) => { | |
| 349 | const result = await client.callTool({ | |
| 350 | name: "jev_ask", | |
| 351 | arguments: { | |
| 352 | state: { ticket: "My card was charged twice." }, | |
| 353 | questions: [ | |
| 354 | { id: "billing", type: "check", question: "Is this about billing?" }, | |
| 355 | { id: "team", type: "classify", question: "Which team?", options: { billing: "d", tech: "d" } }, | |
| 356 | { id: "anger", type: "score", question: "How angry?", levels: ["calm", "annoyed", "furious"] }, | |
| 357 | ], | |
| 358 | }, | |
| 359 | }); | |
| 360 | ||
| 361 | assert.equal(mock.requests.length, 1, "three questions must cost one round trip"); | |
| 362 | assert.deepEqual(Object.keys(mock.only().questions).sort(), ["anger", "billing", "team"]); | |
| 363 | ||
| 364 | const body = payload(result); | |
| 365 | assert.deepEqual(Object.keys(body.answers).sort(), ["anger", "billing", "team"]); | |
| 366 | assert.equal(body.answers.team.action, "act", "choice answers carry a gate"); | |
| 367 | assert.equal(body.answers.billing.action, undefined, "a noul has no confidence to gate on"); | |
| 368 | }); | |
| 369 | } finally { | |
| 370 | await mock.close(); | |
| 371 | } | |
| 372 | }); |
tsconfig.json added +19
| @@ -0,0 +1,19 @@ | ||
| 1 | { | |
| 2 | "compilerOptions": { | |
| 3 | "target": "ES2022", | |
| 4 | "lib": ["ES2022"], | |
| 5 | "module": "Node16", | |
| 6 | "moduleResolution": "Node16", | |
| 7 | "outDir": "dist", | |
| 8 | "rootDir": "src", | |
| 9 | "strict": true, | |
| 10 | "noUncheckedIndexedAccess": true, | |
| 11 | "noImplicitOverride": true, | |
| 12 | "declaration": false, | |
| 13 | "sourceMap": true, | |
| 14 | "skipLibCheck": true, | |
| 15 | "esModuleInterop": true, | |
| 16 | "forceConsistentCasingInFileNames": true | |
| 17 | }, | |
| 18 | "include": ["src/**/*.ts"] | |
| 19 | } |