-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtool-validation.ts
More file actions
138 lines (113 loc) · 4.75 KB
/
tool-validation.ts
File metadata and controls
138 lines (113 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
* Tool input validation enhancement for the MCP server.
*
* The upstream MCP SDK (`getParseErrorMessage` in `zod-compat.js`) extracts
* only the *first* Zod issue when a tool call fails validation. This module
* overrides `McpServer.validateToolInput` so that **all** issues are surfaced
* in a single, human-readable error message.
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
// ─── Error formatting ────────────────────────────────────────────────────────
/**
* Format all Zod validation issues into a single, human-readable message.
*
* - Groups missing-required-field errors into one line
* (`must have required properties: 'a', 'b'`).
* - Appends any other validation errors individually.
*/
export function formatAllValidationErrors(error: z.ZodError): string {
const { issues } = error;
if (issues.length === 0) return 'Unknown validation error';
// Partition into "required-field missing" vs "everything else"
const missingRequired: string[] = [];
const otherErrors: string[] = [];
for (const issue of issues) {
const path = issue.path.join('.');
if (issue.code === 'invalid_type' && issue.received === 'undefined' && path) {
missingRequired.push(`'${path}'`);
} else {
otherErrors.push(path ? `${path}: ${issue.message}` : issue.message);
}
}
const parts: string[] = [];
if (missingRequired.length === 1) {
parts.push(`must have required property ${missingRequired[0]}`);
} else if (missingRequired.length > 1) {
parts.push(`must have required properties: ${missingRequired.join(', ')}`);
}
parts.push(...otherErrors);
return parts.join('; ');
}
// ─── Schema resolution ───────────────────────────────────────────────────────
/**
* Resolve the tool's `inputSchema` into a parsable Zod schema.
*
* Handles both:
* - Raw Zod shapes (`{ owner: z.string(), ... }`) — wraps with `z.object()`
* - Pre-built Zod schemas (ZodObject, ZodEffects, etc.) — returns as-is
*/
function resolveZodSchema(inputSchema: unknown): z.ZodTypeAny | undefined {
if (!inputSchema || typeof inputSchema !== 'object') return undefined;
const schema = inputSchema as Record<string, unknown>;
// Already a Zod schema instance (has _def for Zod v3 or _zod for v4)
if ('_def' in schema || '_zod' in schema) {
return inputSchema as z.ZodTypeAny;
}
// Check for raw Zod shape (all values are Zod schemas)
const values = Object.values(schema);
if (
values.length > 0 &&
values.every(
(v) =>
typeof v === 'object' &&
v !== null &&
('_def' in (v as Record<string, unknown>) ||
'_zod' in (v as Record<string, unknown>) ||
typeof (v as Record<string, unknown>).parse === 'function'),
)
) {
return z.object(schema as z.ZodRawShape);
}
return undefined;
}
// ─── Instance patch ──────────────────────────────────────────────────────────
/**
* Patch `validateToolInput` on the given McpServer **instance** so that
* **all** validation errors are reported in a single response instead of
* only the first one.
*
* Call this once after constructing the McpServer and before connecting
* any transport.
*/
export function patchValidateToolInput(server: McpServer): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = server as any;
// Capture the original so we can delegate for unrecognized schema types
const originalValidateToolInput = instance.validateToolInput.bind(instance);
instance.validateToolInput = async function (
tool: { inputSchema?: unknown },
args: unknown,
toolName: string,
): Promise<unknown> {
if (!tool.inputSchema) {
return undefined;
}
const schema = resolveZodSchema(tool.inputSchema);
if (!schema) {
// Unrecognized schema type — delegate to the original SDK validation
// so mis-registered tools don't accidentally bypass input validation.
return originalValidateToolInput(tool, args, toolName);
}
const parseResult = await schema.safeParseAsync(args);
if (!parseResult.success) {
const errorMessage = formatAllValidationErrors(parseResult.error);
throw new McpError(
ErrorCode.InvalidParams,
`Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`,
);
}
return parseResult.data;
};
}