anyapi-mcp-server 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,25 @@
1
1
  # anyapi-mcp-server
2
2
 
3
- A universal [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that connects **any REST API** to AI assistants like Claude, Cursor, and other LLM-powered tools — just point it at an OpenAPI spec or Postman collection.
3
+ ### If it has an API, you can MCP it.
4
4
 
5
- Instead of building a custom MCP server for every API, `anyapi-mcp-server` reads your spec file and dynamically creates tools with **GraphQL-style field selection** and automatic schema inference.
5
+ <img src="public/datadog.gif" alt="anyapi-mcp-server demo Datadog API" width="1200" />
6
+
7
+ Traditional MCP servers hand-pick a handful of endpoints and call it a day — locking you into whatever subset someone decided was "enough." Why settle for a fraction of an API when you can have **all of it**?
8
+
9
+ `anyapi-mcp-server` is a universal [MCP](https://modelcontextprotocol.io) server that connects **any REST API** to AI assistants like Claude, Cursor, and other LLM-powered tools — just point it at an OpenAPI spec or Postman collection. Every endpoint the API provides becomes available instantly, with **GraphQL-style field selection** and automatic schema inference. No custom server code, no artificial limits.
10
+
11
+ Works with services like **Datadog**, **PostHog**, **Metabase**, **Cloudflare**, **Stripe**, **GitHub**, **Slack**, **Twilio**, **Shopify**, **HubSpot**, and anything else with a REST API — if it has an API, it just works.
6
12
 
7
13
  ## Features
8
14
 
9
- - **Works with any REST API** — provide an OpenAPI (JSON/YAML) or Postman Collection v2.x spec
15
+ - **Works with any REST API** — provide an OpenAPI (JSON/YAML) or Postman Collection v2.x spec as a local file or HTTPS URL
16
+ - **Remote spec caching** — HTTPS spec URLs are fetched once and cached locally in ``~/.cache/anyapi-mcp/` (Linux/macOS) or `%LOCALAPPDATA%\anyapi-mcp\` (Windows)`
10
17
  - **GraphQL-style queries** — select only the fields you need from API responses
11
18
  - **Automatic schema inference** — calls an endpoint once, infers the response schema, then lets you query specific fields
12
19
  - **Multi-sample merging** — samples up to 10 array elements to build richer schemas that capture fields missing from individual items
13
20
  - **Mutation support** — POST/PUT/DELETE/PATCH endpoints with OpenAPI request body schemas get GraphQL mutation types with typed inputs
14
21
  - **Smart query suggestions** — `call_api` returns ready-to-use GraphQL queries based on the inferred schema
22
+ - **Shape-aware schema caching** — schemas are cached by response structure (not just endpoint), so the same path returning different shapes gets distinct schemas; `shapeHash` is returned for cache-aware workflows
15
23
  - **Response caching** — 30-second TTL cache prevents duplicate HTTP calls across consecutive `call_api` → `query_api` flows
16
24
  - **Retry with backoff** — automatic retries with exponential backoff and jitter for 429/5xx errors, honoring `Retry-After` headers
17
25
  - **Multi-format responses** — parses JSON, XML, CSV, and plain text responses automatically
@@ -35,16 +43,9 @@ npm install -g anyapi-mcp-server
35
43
  | Flag | Description |
36
44
  |------|-------------|
37
45
  | `--name` | Server name (e.g. `petstore`) |
38
-
39
- ### Either one of
40
-
41
- | Flag | Description |
42
- |------|-------------|
43
- | `--spec` | Path to OpenAPI spec file (JSON or YAML) or Postman Collection |
46
+ | `--spec` | Path or HTTPS URL to OpenAPI spec (JSON or YAML) or Postman Collection. HTTPS URLs are cached locally in ``~/.cache/anyapi-mcp/` (Linux/macOS) or `%LOCALAPPDATA%\anyapi-mcp\` (Windows)`. Supports `${ENV_VAR}` interpolation. |
44
47
  | `--base-url` | API base URL (e.g. `https://api.example.com`). Supports `${ENV_VAR}` interpolation. |
45
48
 
46
- You can provide both `--spec` and `--base-url` together. If only `--spec` is given, the base URL is read from the spec. If only `--base-url` is given, endpoints are discovered dynamically.
47
-
48
49
  ### Optional arguments
49
50
 
50
51
  | Flag | Description |
@@ -67,7 +68,7 @@ Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop confi
67
68
  "-y",
68
69
  "anyapi-mcp-server",
69
70
  "--name", "cloudflare",
70
- "--spec", "/path/to/cloudflare-openapi.json",
71
+ "--spec", "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json",
71
72
  "--base-url", "https://api.cloudflare.com/client/v4",
72
73
  "--header", "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}"
73
74
  ],
@@ -90,8 +91,8 @@ Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop confi
90
91
  "-y",
91
92
  "anyapi-mcp-server",
92
93
  "--name", "datadog",
93
- "--spec", "/path/to/datadog-openapi.json",
94
- "--base-url", "https://api.datadoghq.com/api/v1",
94
+ "--spec", "https://raw.githubusercontent.com/DataDog/datadog-api-client-typescript/master/.generator/schemas/v1/openapi.yaml",
95
+ "--base-url", "https://api.datadoghq.com",
95
96
  "--header", "DD-API-KEY: ${DD_API_KEY}",
96
97
  "--header", "DD-APPLICATION-KEY: ${DD_APP_KEY}"
97
98
  ],
@@ -167,6 +168,8 @@ Inspect an API endpoint by making a real request and returning the inferred Grap
167
168
  - Returns the full schema SDL showing all available fields and types
168
169
  - Returns accepted parameters (name, location, required) from the API spec
169
170
  - Returns `suggestedQueries` — ready-to-use GraphQL queries generated from the schema
171
+ - Returns `shapeHash` — a structural fingerprint of the response for cache-aware workflows
172
+ - Returns `bodyHash` for write operations when a request body is provided
170
173
  - Accepts optional `headers` to override defaults for this request
171
174
  - For write operations (POST/PUT/DELETE/PATCH) with request body schemas, the schema includes a Mutation type
172
175
 
@@ -207,6 +210,7 @@ Fetch data from an API endpoint, returning only the fields you select via GraphQ
207
210
  - Supports `limit` and `offset` for client-side slicing of already-fetched data
208
211
  - For API-level pagination, pass limit/offset inside `params` instead
209
212
  - Accepts optional `headers` to override defaults for this request
213
+ - Response includes `_shapeHash` (and `_bodyHash` for writes) for tracking schema identity
210
214
 
211
215
  ### `explain_api`
212
216
 
@@ -225,7 +229,7 @@ Fetch data from multiple endpoints concurrently in a single tool call.
225
229
  - Accepts an array of 1–10 requests, each with `method`, `path`, `params`, `body`, `query`, and optional `headers`
226
230
  - All requests execute in parallel via `Promise.allSettled` — one failure does not affect the others
227
231
  - Each request follows the `query_api` flow: HTTP fetch → schema inference → GraphQL field selection
228
- - Returns an array of results: `{ method, path, data }` on success or `{ method, path, error }` on failure
232
+ - Returns an array of results: `{ method, path, data, shapeHash }` on success or `{ method, path, error }` on failure
229
233
  - Run `call_api` first on each endpoint to discover the schema field names
230
234
 
231
235
  ## Workflow
@@ -259,9 +263,9 @@ OpenAPI/Postman spec
259
263
  response data
260
264
  ```
261
265
 
262
- 1. The spec file is parsed at startup into an endpoint index with tags, paths, parameters, and request body schemas
263
- 2. `call_api` makes a real HTTP request, infers a GraphQL schema from the JSON response, and caches both the response (30s TTL) and the schema
264
- 3. `query_api` re-uses the cached response if called within 30s, executes your GraphQL field selection against the data, and returns only the fields you asked for
266
+ 1. The spec is loaded at startup (from a local file or fetched from an HTTPS URL with filesystem caching) and parsed into an endpoint index with tags, paths, parameters, and request body schemas
267
+ 2. `call_api` makes a real HTTP request, infers a GraphQL schema from the JSON response, and caches both the response (30s TTL) and the schema. Schemas are keyed by endpoint + response shape, so the same path returning different structures gets distinct schemas
268
+ 3. `query_api` re-uses the cached response if called within 30s, executes your GraphQL field selection against the data, and returns only the fields you asked for. Includes `_shapeHash` in the response for tracking schema identity
265
269
  4. Write operations (POST/PUT/DELETE/PATCH) with OpenAPI request body schemas get a Mutation type with typed `GraphQLInputObjectType` inputs
266
270
 
267
271
  ## Supported Spec Formats
@@ -1,4 +1,3 @@
1
- import { readFileSync } from "node:fs";
2
1
  import yaml from "js-yaml";
3
2
  const PAGE_SIZE = 20;
4
3
  const HTTP_METHODS = new Set(["get", "post", "put", "delete", "patch"]);
@@ -119,10 +118,14 @@ function postmanUrlToPath(url) {
119
118
  export class ApiIndex {
120
119
  byTag = new Map();
121
120
  allEndpoints = [];
122
- constructor(specPath) {
123
- const raw = readFileSync(specPath, "utf-8");
124
- const isYaml = /\.ya?ml$/i.test(specPath);
125
- const parsed = isYaml ? yaml.load(raw) : JSON.parse(raw);
121
+ constructor(specContent) {
122
+ let parsed;
123
+ try {
124
+ parsed = JSON.parse(specContent);
125
+ }
126
+ catch {
127
+ parsed = yaml.load(specContent);
128
+ }
126
129
  if (isPostmanCollection(parsed)) {
127
130
  this.parsePostman(parsed);
128
131
  }
package/build/config.js CHANGED
@@ -1,4 +1,8 @@
1
1
  import { resolve } from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
2
6
  function getArg(flag) {
3
7
  const idx = process.argv.indexOf(flag);
4
8
  if (idx === -1 || !process.argv[idx + 1])
@@ -23,25 +27,51 @@ function interpolateEnv(value) {
23
27
  return envValue;
24
28
  });
25
29
  }
26
- const USAGE = `Usage: anyapi-mcp --name <name> --spec <path> --base-url <url> [--header "Key: Value"]...
30
+ const CACHE_DIR = process.platform === "win32"
31
+ ? join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "anyapi-mcp")
32
+ : join(homedir(), ".cache", "anyapi-mcp");
33
+ function isUrl(value) {
34
+ return /^https?:\/\//i.test(value);
35
+ }
36
+ async function loadSpec(specValue) {
37
+ if (!isUrl(specValue)) {
38
+ return readFileSync(resolve(specValue), "utf-8");
39
+ }
40
+ const hash = createHash("sha256").update(specValue).digest("hex");
41
+ const ext = /\.ya?ml$/i.test(specValue) ? ".yaml" : ".json";
42
+ const cachePath = join(CACHE_DIR, hash + ext);
43
+ if (existsSync(cachePath)) {
44
+ return readFileSync(cachePath, "utf-8");
45
+ }
46
+ const res = await fetch(specValue);
47
+ if (!res.ok) {
48
+ throw new Error(`Failed to fetch spec from ${specValue}: ${res.status} ${res.statusText}`);
49
+ }
50
+ const body = await res.text();
51
+ mkdirSync(CACHE_DIR, { recursive: true });
52
+ writeFileSync(cachePath, body, "utf-8");
53
+ return body;
54
+ }
55
+ const USAGE = `Usage: anyapi-mcp --name <name> --spec <path-or-url> --base-url <url> [--header "Key: Value"]...
27
56
 
28
57
  Required:
29
58
  --name Server name (e.g. "petstore")
30
- --spec Path to OpenAPI spec file (JSON or YAML)
59
+ --spec Path or URL to OpenAPI spec (JSON or YAML). HTTPS URLs are cached locally.
31
60
  --base-url API base URL (e.g. "https://api.example.com")
32
61
 
33
62
  Optional:
34
63
  --header HTTP header as "Key: Value" (repeatable)
35
64
  Supports \${ENV_VAR} interpolation in values
36
65
  --log Path to request/response log file (NDJSON format)`;
37
- export function loadConfig() {
66
+ export async function loadConfig() {
38
67
  const name = getArg("--name");
39
- const spec = getArg("--spec");
68
+ const specUrl = getArg("--spec");
40
69
  const baseUrl = getArg("--base-url");
41
- if (!name || !spec || !baseUrl) {
70
+ if (!name || !specUrl || !baseUrl) {
42
71
  console.error(USAGE);
43
72
  process.exit(1);
44
73
  }
74
+ const spec = await loadSpec(interpolateEnv(specUrl));
45
75
  const headers = {};
46
76
  for (const raw of getAllArgs("--header")) {
47
77
  const colonIdx = raw.indexOf(":");
@@ -56,7 +86,7 @@ export function loadConfig() {
56
86
  const logPath = getArg("--log");
57
87
  return {
58
88
  name,
59
- spec: resolve(spec),
89
+ spec,
60
90
  baseUrl: interpolateEnv(baseUrl).replace(/\/+$/, ""),
61
91
  headers: Object.keys(headers).length > 0 ? headers : undefined,
62
92
  logPath: logPath ? resolve(logPath) : undefined,
@@ -1,4 +1,5 @@
1
1
  import { GraphQLSchema, GraphQLObjectType, GraphQLInputObjectType, GraphQLScalarType, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLList, GraphQLNonNull, graphql as executeGraphQL, printSchema, } from "graphql";
2
+ import { createHash } from "node:crypto";
2
3
  const DEFAULT_ARRAY_LIMIT = 50;
3
4
  const MAX_SAMPLE_SIZE = 30;
4
5
  const MAX_INFER_DEPTH = 4;
@@ -28,8 +29,8 @@ export function truncateIfArray(data, limit, offset) {
28
29
  const sliced = data.slice(off, off + lim);
29
30
  return { data: sliced, truncated: sliced.length < total, total };
30
31
  }
31
- function cacheKey(method, pathTemplate) {
32
- return `${method}:${pathTemplate}`;
32
+ function cacheKey(method, pathTemplate, hash) {
33
+ return `${method}:${pathTemplate}:${hash}`;
33
34
  }
34
35
  /**
35
36
  * Sanitize a JSON key into a valid GraphQL field name.
@@ -155,6 +156,51 @@ function mergeArraySamples(arr) {
155
156
  return null;
156
157
  return mergeSamples(objectSamples);
157
158
  }
159
+ /**
160
+ * Produce a structural fingerprint string from JSON data.
161
+ * Captures keys + recursive type structure but NOT values.
162
+ * Sorted keys ensure determinism regardless of object key order.
163
+ * Bounded by MAX_INFER_DEPTH to match inference behavior.
164
+ */
165
+ function shapeFingerprint(data, depth = 0) {
166
+ if (data === null || data === undefined)
167
+ return "n";
168
+ if (depth >= MAX_INFER_DEPTH)
169
+ return "J";
170
+ if (Array.isArray(data)) {
171
+ if (data.length === 0)
172
+ return "[]";
173
+ if (hasMixedTypes(data))
174
+ return "[J]";
175
+ const mergeResult = mergeArraySamples(data);
176
+ if (mergeResult)
177
+ return `[${shapeFingerprint(mergeResult.merged, depth + 1)}]`;
178
+ return `[${shapeFingerprint(data[0], depth + 1)}]`;
179
+ }
180
+ if (typeof data === "object") {
181
+ const obj = data;
182
+ const keys = Object.keys(obj).sort();
183
+ if (keys.length === 0)
184
+ return "{}";
185
+ return `{${keys.map((k) => `${k}:${shapeFingerprint(obj[k], depth + 1)}`).join(",")}}`;
186
+ }
187
+ switch (typeof data) {
188
+ case "number":
189
+ return Number.isInteger(data) ? "i" : "f";
190
+ case "boolean":
191
+ return "b";
192
+ default:
193
+ return "s";
194
+ }
195
+ }
196
+ /**
197
+ * Compute a truncated SHA-256 hash of the structural fingerprint of JSON data.
198
+ * Returns a 12-character hex string.
199
+ */
200
+ export function computeShapeHash(data) {
201
+ const fp = shapeFingerprint(data);
202
+ return createHash("sha256").update(fp).digest("hex").slice(0, 12);
203
+ }
158
204
  /**
159
205
  * Recursively infer a GraphQL type from a JSON value.
160
206
  * For objects, creates a named GraphQLObjectType with explicit resolvers
@@ -376,15 +422,22 @@ export function buildSchemaFromData(data, method, pathTemplate, requestBodySchem
376
422
  }
377
423
  /**
378
424
  * Get a cached schema or build + cache a new one from the response data.
425
+ *
426
+ * @param cacheHash Optional hash to use as the cache key discriminator.
427
+ * If provided (e.g. body hash for mutations), it is used instead of the
428
+ * response shape hash for cache lookup. The shapeHash is always computed
429
+ * from the response data and returned regardless.
379
430
  */
380
- export function getOrBuildSchema(data, method, pathTemplate, requestBodySchema) {
381
- const key = cacheKey(method, pathTemplate);
431
+ export function getOrBuildSchema(data, method, pathTemplate, requestBodySchema, cacheHash) {
432
+ const shapeHash = computeShapeHash(data);
433
+ const effectiveHash = cacheHash ?? shapeHash;
434
+ const key = cacheKey(method, pathTemplate, effectiveHash);
382
435
  const cached = schemaCache.get(key);
383
436
  if (cached)
384
- return cached;
437
+ return { schema: cached, shapeHash };
385
438
  const schema = buildSchemaFromData(data, method, pathTemplate, requestBodySchema);
386
439
  schemaCache.set(key, schema);
387
- return schema;
440
+ return { schema, shapeHash };
388
441
  }
389
442
  /**
390
443
  * Convert a GraphQL schema to SDL string for display.
package/build/index.js CHANGED
@@ -7,8 +7,9 @@ import { ApiIndex } from "./api-index.js";
7
7
  import { callApi } from "./api-client.js";
8
8
  import { initLogger } from "./logger.js";
9
9
  import { generateSuggestions } from "./query-suggestions.js";
10
- import { getOrBuildSchema, executeQuery, schemaToSDL, truncateIfArray, } from "./graphql-schema.js";
11
- const config = loadConfig();
10
+ import { getOrBuildSchema, executeQuery, schemaToSDL, truncateIfArray, computeShapeHash, } from "./graphql-schema.js";
11
+ const WRITE_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
12
+ const config = await loadConfig();
12
13
  initLogger(config.logPath ?? null);
13
14
  const apiIndex = new ApiIndex(config.spec);
14
15
  const server = new McpServer({
@@ -74,7 +75,7 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
74
75
  ? "{ items { method path summary tag parameters { name in required description } } _count }"
75
76
  : "{ items { tag endpointCount } _count }";
76
77
  const effectiveQuery = query ?? defaultQuery;
77
- const schema = getOrBuildSchema(data, "LIST", category ?? search ?? "_categories");
78
+ const { schema } = getOrBuildSchema(data, "LIST", category ?? search ?? "_categories");
78
79
  const { data: sliced, truncated, total } = truncateIfArray(data, limit ?? 20, offset);
79
80
  const queryResult = await executeQuery(schema, sliced, effectiveQuery);
80
81
  if (truncated && typeof queryResult === "object" && queryResult !== null) {
@@ -135,9 +136,12 @@ server.tool("call_api", `Inspect a ${config.name} API endpoint. Makes a real req
135
136
  try {
136
137
  const data = await callApi(config, method, path, params, body, headers, "populate");
137
138
  const endpoint = apiIndex.getEndpoint(method, path);
138
- const schema = getOrBuildSchema(data, method, path, endpoint?.requestBodySchema);
139
+ const bodyHash = WRITE_METHODS.has(method) && body ? computeShapeHash(body) : undefined;
140
+ const { schema, shapeHash } = getOrBuildSchema(data, method, path, endpoint?.requestBodySchema, bodyHash);
139
141
  const sdl = schemaToSDL(schema);
140
- const result = { graphqlSchema: sdl };
142
+ const result = { graphqlSchema: sdl, shapeHash };
143
+ if (bodyHash)
144
+ result.bodyHash = bodyHash;
141
145
  if (endpoint && endpoint.parameters.length > 0) {
142
146
  result.parameters = endpoint.parameters.map((p) => ({
143
147
  name: p.name,
@@ -233,16 +237,22 @@ server.tool("query_api", `Fetch data from a ${config.name} API endpoint, returni
233
237
  try {
234
238
  const rawData = await callApi(config, method, path, params, body, headers, "consume");
235
239
  const endpoint = apiIndex.getEndpoint(method, path);
236
- const schema = getOrBuildSchema(rawData, method, path, endpoint?.requestBodySchema);
240
+ const bodyHash = WRITE_METHODS.has(method) && body ? computeShapeHash(body) : undefined;
241
+ const { schema, shapeHash } = getOrBuildSchema(rawData, method, path, endpoint?.requestBodySchema, bodyHash);
237
242
  const { data, truncated, total } = truncateIfArray(rawData, limit, offset);
238
243
  const queryResult = await executeQuery(schema, data, query);
239
- if (truncated && typeof queryResult === "object" && queryResult !== null) {
240
- queryResult._meta = {
241
- total,
242
- offset: offset ?? 0,
243
- limit: limit ?? 50,
244
- hasMore: true,
245
- };
244
+ if (typeof queryResult === "object" && queryResult !== null) {
245
+ queryResult._shapeHash = shapeHash;
246
+ if (bodyHash)
247
+ queryResult._bodyHash = bodyHash;
248
+ if (truncated) {
249
+ queryResult._meta = {
250
+ total,
251
+ offset: offset ?? 0,
252
+ limit: limit ?? 50,
253
+ hasMore: true,
254
+ };
255
+ }
246
256
  }
247
257
  return {
248
258
  content: [
@@ -380,9 +390,18 @@ server.tool("batch_query", `Fetch data from multiple ${config.name} API endpoint
380
390
  const settled = await Promise.allSettled(requests.map(async (req) => {
381
391
  const rawData = await callApi(config, req.method, req.path, req.params, req.body, req.headers, "none");
382
392
  const endpoint = apiIndex.getEndpoint(req.method, req.path);
383
- const schema = getOrBuildSchema(rawData, req.method, req.path, endpoint?.requestBodySchema);
393
+ const bodyHash = WRITE_METHODS.has(req.method) && req.body
394
+ ? computeShapeHash(req.body)
395
+ : undefined;
396
+ const { schema, shapeHash } = getOrBuildSchema(rawData, req.method, req.path, endpoint?.requestBodySchema, bodyHash);
384
397
  const queryResult = await executeQuery(schema, rawData, req.query);
385
- return { method: req.method, path: req.path, data: queryResult };
398
+ return {
399
+ method: req.method,
400
+ path: req.path,
401
+ data: queryResult,
402
+ shapeHash,
403
+ ...(bodyHash ? { bodyHash } : {}),
404
+ };
386
405
  }));
387
406
  const results = settled.map((outcome, i) => {
388
407
  if (outcome.status === "fulfilled") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyapi-mcp-server",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A universal MCP server that connects any REST API (via OpenAPI spec) to AI assistants, with GraphQL-style field selection and automatic schema inference.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -31,6 +31,7 @@
31
31
  "scripts": {
32
32
  "build": "tsc",
33
33
  "start": "node build/index.js",
34
+ "test": "vitest run",
34
35
  "prepublishOnly": "npm run build"
35
36
  },
36
37
  "engines": {
@@ -46,6 +47,7 @@
46
47
  "devDependencies": {
47
48
  "@types/js-yaml": "^4.0.9",
48
49
  "@types/node": "^22.0.0",
49
- "typescript": "^5.7.0"
50
+ "typescript": "^5.7.0",
51
+ "vitest": "^4.0.18"
50
52
  }
51
53
  }