mcp-tool-bridge 1.1.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/LICENSE +21 -0
- package/README.md +492 -0
- package/dist/chunk-XMYSMKGR.cjs +111 -0
- package/dist/chunk-YGJJEZXI.js +111 -0
- package/dist/index.cjs +1328 -0
- package/dist/index.d.cts +668 -0
- package/dist/index.d.ts +668 -0
- package/dist/index.js +1328 -0
- package/dist/types-tss7dg3j.d.cts +81 -0
- package/dist/types-tss7dg3j.d.ts +81 -0
- package/dist/zod.cjs +51 -0
- package/dist/zod.d.cts +18 -0
- package/dist/zod.d.ts +18 -0
- package/dist/zod.js +51 -0
- package/package.json +99 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much harm a tool can do, from `none` (no side effect at all) to
|
|
3
|
+
* `critical`. The level drives the confirmation guard and the MCP
|
|
4
|
+
* `readOnlyHint` annotation; it is never shown to the model as such.
|
|
5
|
+
*/
|
|
6
|
+
declare const SENSITIVITY_LEVELS: readonly ["none", "low", "medium", "high", "critical"];
|
|
7
|
+
type Sensitivity = (typeof SENSITIVITY_LEVELS)[number];
|
|
8
|
+
declare function isSensitivity(value: unknown): value is Sensitivity;
|
|
9
|
+
/** Position of a level on the scale: `none` is 0, `critical` is 4. */
|
|
10
|
+
declare function sensitivityRank(level: Sensitivity): number;
|
|
11
|
+
/**
|
|
12
|
+
* Who is calling. The bridge only reads `id` (for audit and for binding
|
|
13
|
+
* confirmations) and `roles` (for access control); anything else the host
|
|
14
|
+
* needs belongs in its own context.
|
|
15
|
+
*/
|
|
16
|
+
interface Principal {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly roles: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
21
|
+
type JsonValue = JsonPrimitive | readonly JsonValue[] | JsonObject;
|
|
22
|
+
interface JsonObject {
|
|
23
|
+
readonly [key: string]: JsonValue;
|
|
24
|
+
}
|
|
25
|
+
interface TextContent {
|
|
26
|
+
readonly type: 'text';
|
|
27
|
+
readonly text: string;
|
|
28
|
+
}
|
|
29
|
+
interface ImageContent {
|
|
30
|
+
readonly type: 'image';
|
|
31
|
+
/** Base64-encoded bytes. */
|
|
32
|
+
readonly data: string;
|
|
33
|
+
readonly mimeType: string;
|
|
34
|
+
}
|
|
35
|
+
interface AudioContent {
|
|
36
|
+
readonly type: 'audio';
|
|
37
|
+
/** Base64-encoded bytes. */
|
|
38
|
+
readonly data: string;
|
|
39
|
+
readonly mimeType: string;
|
|
40
|
+
}
|
|
41
|
+
type ContentBlock = TextContent | ImageContent | AudioContent;
|
|
42
|
+
/** The shape of an MCP `tools/call` result, restricted to what the bridge emits. */
|
|
43
|
+
interface ToolResult {
|
|
44
|
+
readonly content: readonly ContentBlock[];
|
|
45
|
+
readonly structuredContent?: JsonObject;
|
|
46
|
+
readonly isError?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What a handler may return: a plain string (sent as text) or a full result,
|
|
50
|
+
* usually built with `text()` or `json()`.
|
|
51
|
+
*/
|
|
52
|
+
type ToolOutput = string | ToolResult;
|
|
53
|
+
|
|
54
|
+
/** A JSON Schema whose root is an object, as MCP requires for tool inputs. */
|
|
55
|
+
interface JsonSchemaObject extends JsonObject {
|
|
56
|
+
readonly type: 'object';
|
|
57
|
+
}
|
|
58
|
+
/** One reason the arguments were rejected. `path` is a JSON Pointer; the root is `""`. */
|
|
59
|
+
interface ArgIssue {
|
|
60
|
+
readonly path: string;
|
|
61
|
+
readonly message: string;
|
|
62
|
+
}
|
|
63
|
+
type ParseResult<T> = {
|
|
64
|
+
readonly ok: true;
|
|
65
|
+
readonly value: T;
|
|
66
|
+
} | {
|
|
67
|
+
readonly ok: false;
|
|
68
|
+
readonly issues: readonly ArgIssue[];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The contract every argument schema meets, whatever produced it. `jsonSchema`
|
|
72
|
+
* is what the model sees; `parse` is what the bridge enforces. Both adapters
|
|
73
|
+
* shipped with the package derive the two from a single declaration, so they
|
|
74
|
+
* cannot drift apart.
|
|
75
|
+
*/
|
|
76
|
+
interface ArgsSchema<T> {
|
|
77
|
+
readonly jsonSchema: JsonSchemaObject;
|
|
78
|
+
readonly parse: (input: unknown) => ParseResult<T>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { type ArgsSchema as A, type ContentBlock as C, type ImageContent as I, type JsonSchemaObject as J, type Principal as P, type Sensitivity as S, type ToolOutput as T, type JsonObject as a, type ArgIssue as b, type JsonValue as c, type ToolResult as d, type AudioContent as e, type JsonPrimitive as f, type ParseResult as g, SENSITIVITY_LEVELS as h, type TextContent as i, isSensitivity as j, sensitivityRank as s };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much harm a tool can do, from `none` (no side effect at all) to
|
|
3
|
+
* `critical`. The level drives the confirmation guard and the MCP
|
|
4
|
+
* `readOnlyHint` annotation; it is never shown to the model as such.
|
|
5
|
+
*/
|
|
6
|
+
declare const SENSITIVITY_LEVELS: readonly ["none", "low", "medium", "high", "critical"];
|
|
7
|
+
type Sensitivity = (typeof SENSITIVITY_LEVELS)[number];
|
|
8
|
+
declare function isSensitivity(value: unknown): value is Sensitivity;
|
|
9
|
+
/** Position of a level on the scale: `none` is 0, `critical` is 4. */
|
|
10
|
+
declare function sensitivityRank(level: Sensitivity): number;
|
|
11
|
+
/**
|
|
12
|
+
* Who is calling. The bridge only reads `id` (for audit and for binding
|
|
13
|
+
* confirmations) and `roles` (for access control); anything else the host
|
|
14
|
+
* needs belongs in its own context.
|
|
15
|
+
*/
|
|
16
|
+
interface Principal {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly roles: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
21
|
+
type JsonValue = JsonPrimitive | readonly JsonValue[] | JsonObject;
|
|
22
|
+
interface JsonObject {
|
|
23
|
+
readonly [key: string]: JsonValue;
|
|
24
|
+
}
|
|
25
|
+
interface TextContent {
|
|
26
|
+
readonly type: 'text';
|
|
27
|
+
readonly text: string;
|
|
28
|
+
}
|
|
29
|
+
interface ImageContent {
|
|
30
|
+
readonly type: 'image';
|
|
31
|
+
/** Base64-encoded bytes. */
|
|
32
|
+
readonly data: string;
|
|
33
|
+
readonly mimeType: string;
|
|
34
|
+
}
|
|
35
|
+
interface AudioContent {
|
|
36
|
+
readonly type: 'audio';
|
|
37
|
+
/** Base64-encoded bytes. */
|
|
38
|
+
readonly data: string;
|
|
39
|
+
readonly mimeType: string;
|
|
40
|
+
}
|
|
41
|
+
type ContentBlock = TextContent | ImageContent | AudioContent;
|
|
42
|
+
/** The shape of an MCP `tools/call` result, restricted to what the bridge emits. */
|
|
43
|
+
interface ToolResult {
|
|
44
|
+
readonly content: readonly ContentBlock[];
|
|
45
|
+
readonly structuredContent?: JsonObject;
|
|
46
|
+
readonly isError?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What a handler may return: a plain string (sent as text) or a full result,
|
|
50
|
+
* usually built with `text()` or `json()`.
|
|
51
|
+
*/
|
|
52
|
+
type ToolOutput = string | ToolResult;
|
|
53
|
+
|
|
54
|
+
/** A JSON Schema whose root is an object, as MCP requires for tool inputs. */
|
|
55
|
+
interface JsonSchemaObject extends JsonObject {
|
|
56
|
+
readonly type: 'object';
|
|
57
|
+
}
|
|
58
|
+
/** One reason the arguments were rejected. `path` is a JSON Pointer; the root is `""`. */
|
|
59
|
+
interface ArgIssue {
|
|
60
|
+
readonly path: string;
|
|
61
|
+
readonly message: string;
|
|
62
|
+
}
|
|
63
|
+
type ParseResult<T> = {
|
|
64
|
+
readonly ok: true;
|
|
65
|
+
readonly value: T;
|
|
66
|
+
} | {
|
|
67
|
+
readonly ok: false;
|
|
68
|
+
readonly issues: readonly ArgIssue[];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The contract every argument schema meets, whatever produced it. `jsonSchema`
|
|
72
|
+
* is what the model sees; `parse` is what the bridge enforces. Both adapters
|
|
73
|
+
* shipped with the package derive the two from a single declaration, so they
|
|
74
|
+
* cannot drift apart.
|
|
75
|
+
*/
|
|
76
|
+
interface ArgsSchema<T> {
|
|
77
|
+
readonly jsonSchema: JsonSchemaObject;
|
|
78
|
+
readonly parse: (input: unknown) => ParseResult<T>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { type ArgsSchema as A, type ContentBlock as C, type ImageContent as I, type JsonSchemaObject as J, type Principal as P, type Sensitivity as S, type ToolOutput as T, type JsonObject as a, type ArgIssue as b, type JsonValue as c, type ToolResult as d, type AudioContent as e, type JsonPrimitive as f, type ParseResult as g, SENSITIVITY_LEVELS as h, type TextContent as i, isSensitivity as j, sensitivityRank as s };
|
package/dist/zod.cjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
var _chunkXMYSMKGRcjs = require('./chunk-XMYSMKGR.cjs');
|
|
10
|
+
|
|
11
|
+
// src/schema/zod.ts
|
|
12
|
+
var _zod = require('zod'); var z = _interopRequireWildcard(_zod);
|
|
13
|
+
function zodSchema(schema) {
|
|
14
|
+
let generated;
|
|
15
|
+
try {
|
|
16
|
+
generated = z.toJSONSchema(schema, {
|
|
17
|
+
target: "draft-2020-12",
|
|
18
|
+
io: "input",
|
|
19
|
+
unrepresentable: "throw"
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new (0, _chunkXMYSMKGRcjs.ToolDefinitionError)(
|
|
23
|
+
"invalid_schema",
|
|
24
|
+
`this Zod schema has no JSON Schema equivalent: ${error instanceof Error ? error.message : String(error)}`,
|
|
25
|
+
{ cause: error }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const root = _chunkXMYSMKGRcjs.freezeRootSchema.call(void 0, withoutDialect(generated));
|
|
29
|
+
const parse = (input) => {
|
|
30
|
+
const copy = _chunkXMYSMKGRcjs.cloneJson.call(void 0, input);
|
|
31
|
+
if (!copy.ok) return { ok: false, issues: [_chunkXMYSMKGRcjs.NOT_JSON_ISSUE] };
|
|
32
|
+
const result = schema.safeParse(copy.value);
|
|
33
|
+
if (result.success) return { ok: true, value: result.data };
|
|
34
|
+
return { ok: false, issues: toIssues(result.error.issues) };
|
|
35
|
+
};
|
|
36
|
+
return Object.freeze({ jsonSchema: root, parse });
|
|
37
|
+
}
|
|
38
|
+
function withoutDialect(generated) {
|
|
39
|
+
const copy = _chunkXMYSMKGRcjs.cloneJson.call(void 0, generated);
|
|
40
|
+
if (!copy.ok || !_chunkXMYSMKGRcjs.isJsonObject.call(void 0, copy.value)) return generated;
|
|
41
|
+
return Object.fromEntries(Object.entries(copy.value).filter(([keyword]) => keyword !== "$schema"));
|
|
42
|
+
}
|
|
43
|
+
function toIssues(issues) {
|
|
44
|
+
return issues.slice(0, _chunkXMYSMKGRcjs.MAX_ISSUES).map((issue) => ({
|
|
45
|
+
path: issue.path.map((segment) => `/${_chunkXMYSMKGRcjs.pointerSegment.call(void 0, String(segment))}`).join(""),
|
|
46
|
+
message: issue.message
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
exports.zodSchema = zodSchema;
|
package/dist/zod.d.cts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { A as ArgsSchema } from './types-tss7dg3j.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a Zod (v4) object schema. The JSON Schema shown to the model is
|
|
6
|
+
* generated from the *input* side of the schema, so transforms and defaults
|
|
7
|
+
* describe what the model must send, while the handler receives the parsed
|
|
8
|
+
* output.
|
|
9
|
+
*
|
|
10
|
+
* Types that JSON cannot carry (dates, bigints, custom checks with no JSON
|
|
11
|
+
* Schema equivalent) fail at startup. Refinements that do not translate to
|
|
12
|
+
* JSON Schema are still enforced by `parse`, but the model is not told about
|
|
13
|
+
* them: put the rule in the field's `.describe()` too. Async refinements are
|
|
14
|
+
* not supported.
|
|
15
|
+
*/
|
|
16
|
+
declare function zodSchema<S extends z.ZodObject>(schema: S): ArgsSchema<z.output<S>>;
|
|
17
|
+
|
|
18
|
+
export { zodSchema };
|
package/dist/zod.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { A as ArgsSchema } from './types-tss7dg3j.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a Zod (v4) object schema. The JSON Schema shown to the model is
|
|
6
|
+
* generated from the *input* side of the schema, so transforms and defaults
|
|
7
|
+
* describe what the model must send, while the handler receives the parsed
|
|
8
|
+
* output.
|
|
9
|
+
*
|
|
10
|
+
* Types that JSON cannot carry (dates, bigints, custom checks with no JSON
|
|
11
|
+
* Schema equivalent) fail at startup. Refinements that do not translate to
|
|
12
|
+
* JSON Schema are still enforced by `parse`, but the model is not told about
|
|
13
|
+
* them: put the rule in the field's `.describe()` too. Async refinements are
|
|
14
|
+
* not supported.
|
|
15
|
+
*/
|
|
16
|
+
declare function zodSchema<S extends z.ZodObject>(schema: S): ArgsSchema<z.output<S>>;
|
|
17
|
+
|
|
18
|
+
export { zodSchema };
|
package/dist/zod.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_ISSUES,
|
|
3
|
+
NOT_JSON_ISSUE,
|
|
4
|
+
ToolDefinitionError,
|
|
5
|
+
cloneJson,
|
|
6
|
+
freezeRootSchema,
|
|
7
|
+
isJsonObject,
|
|
8
|
+
pointerSegment
|
|
9
|
+
} from "./chunk-YGJJEZXI.js";
|
|
10
|
+
|
|
11
|
+
// src/schema/zod.ts
|
|
12
|
+
import * as z from "zod";
|
|
13
|
+
function zodSchema(schema) {
|
|
14
|
+
let generated;
|
|
15
|
+
try {
|
|
16
|
+
generated = z.toJSONSchema(schema, {
|
|
17
|
+
target: "draft-2020-12",
|
|
18
|
+
io: "input",
|
|
19
|
+
unrepresentable: "throw"
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new ToolDefinitionError(
|
|
23
|
+
"invalid_schema",
|
|
24
|
+
`this Zod schema has no JSON Schema equivalent: ${error instanceof Error ? error.message : String(error)}`,
|
|
25
|
+
{ cause: error }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const root = freezeRootSchema(withoutDialect(generated));
|
|
29
|
+
const parse = (input) => {
|
|
30
|
+
const copy = cloneJson(input);
|
|
31
|
+
if (!copy.ok) return { ok: false, issues: [NOT_JSON_ISSUE] };
|
|
32
|
+
const result = schema.safeParse(copy.value);
|
|
33
|
+
if (result.success) return { ok: true, value: result.data };
|
|
34
|
+
return { ok: false, issues: toIssues(result.error.issues) };
|
|
35
|
+
};
|
|
36
|
+
return Object.freeze({ jsonSchema: root, parse });
|
|
37
|
+
}
|
|
38
|
+
function withoutDialect(generated) {
|
|
39
|
+
const copy = cloneJson(generated);
|
|
40
|
+
if (!copy.ok || !isJsonObject(copy.value)) return generated;
|
|
41
|
+
return Object.fromEntries(Object.entries(copy.value).filter(([keyword]) => keyword !== "$schema"));
|
|
42
|
+
}
|
|
43
|
+
function toIssues(issues) {
|
|
44
|
+
return issues.slice(0, MAX_ISSUES).map((issue) => ({
|
|
45
|
+
path: issue.path.map((segment) => `/${pointerSegment(String(segment))}`).join(""),
|
|
46
|
+
message: issue.message
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
zodSchema
|
|
51
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-tool-bridge",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A generic MCP server that exposes a declared tool catalog to a model, with role-based filtering and a server-side confirmation guard.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Julien",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/julien-simplogic/mcp-tool-bridge.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/julien-simplogic/mcp-tool-bridge#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/julien-simplogic/mcp-tool-bridge/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"llm",
|
|
19
|
+
"tools",
|
|
20
|
+
"agents",
|
|
21
|
+
"rbac",
|
|
22
|
+
"confirmation"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/index.d.cts",
|
|
36
|
+
"default": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"./zod": {
|
|
40
|
+
"import": {
|
|
41
|
+
"types": "./dist/zod.d.ts",
|
|
42
|
+
"default": "./dist/zod.js"
|
|
43
|
+
},
|
|
44
|
+
"require": {
|
|
45
|
+
"types": "./dist/zod.d.cts",
|
|
46
|
+
"default": "./dist/zod.cjs"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"./package.json": "./package.json"
|
|
50
|
+
},
|
|
51
|
+
"main": "./dist/index.cjs",
|
|
52
|
+
"module": "./dist/index.js",
|
|
53
|
+
"types": "./dist/index.d.cts",
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
],
|
|
59
|
+
"sideEffects": false,
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"check:dist": "node scripts/check-dist.mjs && node scripts/check-dist.cjs",
|
|
63
|
+
"check:mutations": "node scripts/check-mutations.mjs",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"lint": "eslint .",
|
|
66
|
+
"format": "prettier --write .",
|
|
67
|
+
"format:check": "prettier --check .",
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"test:watch": "vitest",
|
|
70
|
+
"prepublishOnly": "npm run build && npm run check:dist"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"zod": "^4.0.0"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"zod": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"dependencies": {
|
|
81
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
82
|
+
"ajv": "^8.20.0",
|
|
83
|
+
"ajv-formats": "^3.0.1",
|
|
84
|
+
"json-schema-to-ts": "^3.1.1"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@eslint/js": "^10.0.1",
|
|
88
|
+
"@types/node": "^20.19.43",
|
|
89
|
+
"eslint": "^10.11.0",
|
|
90
|
+
"eslint-config-prettier": "^10.1.8",
|
|
91
|
+
"prettier": "^3.9.9",
|
|
92
|
+
"tsup": "^8.5.1",
|
|
93
|
+
"tsx": "^4.23.15",
|
|
94
|
+
"typescript": "~6.0.3",
|
|
95
|
+
"typescript-eslint": "^8.71.0",
|
|
96
|
+
"vitest": "^4.1.11",
|
|
97
|
+
"zod": "^4.6.5"
|
|
98
|
+
}
|
|
99
|
+
}
|