@casys/mcp-server 0.8.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/mod.ts +161 -0
- package/package.json +32 -0
- package/src/auth/config.ts +229 -0
- package/src/auth/jwt-provider.ts +175 -0
- package/src/auth/middleware.ts +170 -0
- package/src/auth/mod.ts +44 -0
- package/src/auth/presets.ts +129 -0
- package/src/auth/provider.ts +47 -0
- package/src/auth/scope-middleware.ts +59 -0
- package/src/auth/types.ts +69 -0
- package/src/concurrency/rate-limiter.ts +190 -0
- package/src/concurrency/request-queue.ts +140 -0
- package/src/concurrent-server.ts +1899 -0
- package/src/middleware/backpressure.ts +36 -0
- package/src/middleware/mod.ts +21 -0
- package/src/middleware/rate-limit.ts +45 -0
- package/src/middleware/runner.ts +63 -0
- package/src/middleware/types.ts +60 -0
- package/src/middleware/validation.ts +28 -0
- package/src/observability/metrics.ts +378 -0
- package/src/observability/mod.ts +20 -0
- package/src/observability/otel.ts +109 -0
- package/src/runtime/runtime.ts +220 -0
- package/src/runtime/types.ts +90 -0
- package/src/sampling/sampling-bridge.ts +191 -0
- package/src/security/channel-hmac.ts +140 -0
- package/src/security/csp.ts +87 -0
- package/src/security/message-signer.ts +223 -0
- package/src/types.ts +478 -0
- package/src/validation/schema-validator.ts +238 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the MCP Concurrent Server Framework
|
|
3
|
+
*
|
|
4
|
+
* This module provides TypeScript types for building high-performance
|
|
5
|
+
* MCP servers with built-in concurrency control and backpressure.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/server/types
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Rate limit configuration
|
|
12
|
+
*/
|
|
13
|
+
export interface RateLimitOptions {
|
|
14
|
+
/** Maximum requests per window */
|
|
15
|
+
maxRequests: number;
|
|
16
|
+
|
|
17
|
+
/** Time window in milliseconds */
|
|
18
|
+
windowMs: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Function to extract client identifier from request context
|
|
22
|
+
* Default: uses "default" for all requests (global rate limit)
|
|
23
|
+
*/
|
|
24
|
+
keyExtractor?: (context: RateLimitContext) => string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Behavior when rate limit is exceeded
|
|
28
|
+
* - 'reject': Return error immediately
|
|
29
|
+
* - 'wait': Wait for slot with backoff (default)
|
|
30
|
+
*/
|
|
31
|
+
onLimitExceeded?: "reject" | "wait";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Context passed to rate limit key extractor
|
|
36
|
+
*/
|
|
37
|
+
export interface RateLimitContext {
|
|
38
|
+
/** Tool being called */
|
|
39
|
+
toolName: string;
|
|
40
|
+
|
|
41
|
+
/** Tool arguments */
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Configuration options for ConcurrentMCPServer
|
|
47
|
+
*/
|
|
48
|
+
export interface ConcurrentServerOptions {
|
|
49
|
+
/** Server name (shown in MCP protocol) */
|
|
50
|
+
name: string;
|
|
51
|
+
|
|
52
|
+
/** Server version */
|
|
53
|
+
version: string;
|
|
54
|
+
|
|
55
|
+
/** Maximum concurrent requests (default: 10) */
|
|
56
|
+
maxConcurrent?: number;
|
|
57
|
+
|
|
58
|
+
/** Backpressure strategy when at capacity (default: 'sleep') */
|
|
59
|
+
backpressureStrategy?: "sleep" | "queue" | "reject";
|
|
60
|
+
|
|
61
|
+
/** Sleep duration in ms for 'sleep' strategy (default: 10) */
|
|
62
|
+
backpressureSleepMs?: number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rate limiting configuration
|
|
66
|
+
* If provided, requests will be rate limited per client
|
|
67
|
+
*/
|
|
68
|
+
rateLimit?: RateLimitOptions;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Enable JSON Schema validation for tool arguments (default: false)
|
|
72
|
+
* When enabled, validates arguments against tool's inputSchema before execution
|
|
73
|
+
*/
|
|
74
|
+
validateSchema?: boolean;
|
|
75
|
+
|
|
76
|
+
/** Enable sampling support for agentic tools (default: false) */
|
|
77
|
+
enableSampling?: boolean;
|
|
78
|
+
|
|
79
|
+
/** Sampling client implementation (required if enableSampling is true) */
|
|
80
|
+
samplingClient?: SamplingClient;
|
|
81
|
+
|
|
82
|
+
/** Custom logger function (default: console.error) */
|
|
83
|
+
logger?: (msg: string) => void;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* OAuth2/Bearer authentication configuration.
|
|
87
|
+
* When provided, HTTP requests require a valid Bearer token.
|
|
88
|
+
* STDIO transport is unaffected (local, no auth needed).
|
|
89
|
+
*/
|
|
90
|
+
auth?: import("./auth/types.js").AuthOptions;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Content Security Policy for HTML resources (MCP Apps).
|
|
94
|
+
* When provided, injects a CSP `<meta>` tag into HTML content before serving.
|
|
95
|
+
* This protects against XSS even in STDIO mode where HTTP headers are unavailable.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* resourceCsp: { allowInline: true }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
resourceCsp?: import("./security/csp.js").CspOptions;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pre-declare the `resources` capability before transport connection.
|
|
106
|
+
*
|
|
107
|
+
* When true, installs `resources/list` and `resources/read` handlers at
|
|
108
|
+
* construction time (before start/startHttp). Resources can then be added
|
|
109
|
+
* dynamically after startup via registerResource() without hitting the
|
|
110
|
+
* SDK's "Cannot register capabilities after connecting to transport" error.
|
|
111
|
+
*
|
|
112
|
+
* Use this when resources are discovered asynchronously (e.g., MCP relay/proxy
|
|
113
|
+
* that discovers child servers after the stdio handshake).
|
|
114
|
+
*/
|
|
115
|
+
expectResources?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================
|
|
119
|
+
// MCP Apps Types (SEP-1865)
|
|
120
|
+
// ============================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* MCP Apps UI metadata for tools (SEP-1865 + PML extensions)
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const tool: MCPTool = {
|
|
128
|
+
* name: "query_table",
|
|
129
|
+
* description: "Query database table",
|
|
130
|
+
* inputSchema: { ... },
|
|
131
|
+
* _meta: {
|
|
132
|
+
* ui: {
|
|
133
|
+
* resourceUri: "ui://mcp-std/table-viewer",
|
|
134
|
+
* emits: ["filter", "select"],
|
|
135
|
+
* accepts: ["setData", "highlight"]
|
|
136
|
+
* }
|
|
137
|
+
* }
|
|
138
|
+
* };
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export interface McpUiToolMeta {
|
|
142
|
+
/**
|
|
143
|
+
* Resource URI for the UI. MUST use ui:// scheme.
|
|
144
|
+
* @example "ui://mcp-std/table-viewer"
|
|
145
|
+
*/
|
|
146
|
+
resourceUri: string;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Visibility control: who can see/call this tool
|
|
150
|
+
* - "model": Only the AI model can see/call
|
|
151
|
+
* - "app": Only the UI app can call (hidden from model)
|
|
152
|
+
* - Default (both): Visible to model and app
|
|
153
|
+
*/
|
|
154
|
+
visibility?: Array<"model" | "app">;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Events this UI can emit (PML extension for sync rules)
|
|
158
|
+
* Used by PML orchestrator to build cross-UI event routing
|
|
159
|
+
* @example ["filter", "select", "sort", "paginate"]
|
|
160
|
+
*/
|
|
161
|
+
emits?: string[];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Events this UI can accept (PML extension for sync rules)
|
|
165
|
+
* Used by PML orchestrator to build cross-UI event routing
|
|
166
|
+
* @example ["setData", "highlight", "scrollTo"]
|
|
167
|
+
*/
|
|
168
|
+
accepts?: string[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* MCP Tool metadata container.
|
|
173
|
+
*
|
|
174
|
+
* Carries optional UI hints and routing metadata for MCP Apps (SEP-1865).
|
|
175
|
+
*/
|
|
176
|
+
export interface MCPToolMeta {
|
|
177
|
+
/** UI configuration for rendering this tool's output in an MCP App */
|
|
178
|
+
ui?: McpUiToolMeta;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* MCP Resource definition for registration
|
|
183
|
+
*/
|
|
184
|
+
export interface MCPResource {
|
|
185
|
+
/**
|
|
186
|
+
* Resource URI. SHOULD use ui:// scheme for MCP Apps.
|
|
187
|
+
* @example "ui://mcp-std/table-viewer"
|
|
188
|
+
*/
|
|
189
|
+
uri: string;
|
|
190
|
+
|
|
191
|
+
/** Human-readable name */
|
|
192
|
+
name: string;
|
|
193
|
+
|
|
194
|
+
/** Description of the resource */
|
|
195
|
+
description?: string;
|
|
196
|
+
|
|
197
|
+
/** MIME type. Defaults to MCP_APP_MIME_TYPE if not specified */
|
|
198
|
+
mimeType?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Content returned by a resource handler
|
|
203
|
+
*/
|
|
204
|
+
export interface ResourceContent {
|
|
205
|
+
/** URI of the resource (should match request) */
|
|
206
|
+
uri: string;
|
|
207
|
+
/** MIME type of the content */
|
|
208
|
+
mimeType: string;
|
|
209
|
+
/** The actual content (HTML for MCP Apps) */
|
|
210
|
+
text: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Resource handler callback
|
|
215
|
+
*
|
|
216
|
+
* @param uri - The requested resource URI as URL object
|
|
217
|
+
* @returns ResourceContent with uri, mimeType, and text
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* const handler: ResourceHandler = async (uri) => ({
|
|
222
|
+
* uri: uri.toString(),
|
|
223
|
+
* mimeType: MCP_APP_MIME_TYPE,
|
|
224
|
+
* text: "<html>...</html>"
|
|
225
|
+
* });
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export type ResourceHandler = (
|
|
229
|
+
uri: URL,
|
|
230
|
+
) => Promise<ResourceContent> | ResourceContent;
|
|
231
|
+
|
|
232
|
+
/** MCP Apps MIME type constant */
|
|
233
|
+
export const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app" as const;
|
|
234
|
+
|
|
235
|
+
/** URI scheme for MCP Apps resources */
|
|
236
|
+
export const MCP_APP_URI_SCHEME = "ui:" as const;
|
|
237
|
+
|
|
238
|
+
// ============================================
|
|
239
|
+
// MCP Tool Types
|
|
240
|
+
// ============================================
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* MCP Tool definition (compatible with MCP protocol)
|
|
244
|
+
*/
|
|
245
|
+
export interface MCPTool {
|
|
246
|
+
/** Tool name */
|
|
247
|
+
name: string;
|
|
248
|
+
|
|
249
|
+
/** Human-readable description */
|
|
250
|
+
description: string;
|
|
251
|
+
|
|
252
|
+
/** JSON Schema for tool input */
|
|
253
|
+
inputSchema: Record<string, unknown>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Tool metadata including UI configuration for MCP Apps
|
|
257
|
+
* @see McpUiToolMeta
|
|
258
|
+
*/
|
|
259
|
+
_meta?: MCPToolMeta;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Required OAuth scopes to call this tool.
|
|
263
|
+
* Only enforced when auth is configured on the server.
|
|
264
|
+
* If empty or undefined, no scope check is performed.
|
|
265
|
+
*/
|
|
266
|
+
requiredScopes?: string[];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Tool handler function.
|
|
271
|
+
*
|
|
272
|
+
* Receives validated arguments and returns a result (or throws).
|
|
273
|
+
* The return value is serialised as JSON inside a `text` content block.
|
|
274
|
+
*
|
|
275
|
+
* **Security**: Never pass `args` values directly to shell commands or SQL.
|
|
276
|
+
* Always validate / sanitise inside the handler or via `inputSchema`.
|
|
277
|
+
*
|
|
278
|
+
* @param args - Validated tool arguments from the MCP client
|
|
279
|
+
* @returns Tool result (string, object, or Promise thereof)
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* const handler: ToolHandler = async (args) => {
|
|
284
|
+
* const rows = await db.query(args.sql as string);
|
|
285
|
+
* return { rows, count: rows.length };
|
|
286
|
+
* };
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
export type ToolHandler = (
|
|
290
|
+
args: Record<string, unknown>,
|
|
291
|
+
) => Promise<unknown> | unknown;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Sampling client interface for bidirectional LLM delegation
|
|
295
|
+
* Compatible with the agentic sampling protocol (SEP-1577)
|
|
296
|
+
*/
|
|
297
|
+
export interface SamplingClient {
|
|
298
|
+
/**
|
|
299
|
+
* Request LLM completion from the client
|
|
300
|
+
* @param params - Sampling parameters (messages, tools, etc.)
|
|
301
|
+
* @returns Completion result with content and stop reason
|
|
302
|
+
*/
|
|
303
|
+
createMessage(params: SamplingParams): Promise<SamplingResult>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Parameters for sampling request
|
|
308
|
+
* Compatible with MCP sampling protocol
|
|
309
|
+
*/
|
|
310
|
+
export interface SamplingParams {
|
|
311
|
+
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
312
|
+
/** Tools available for the agent to use. Client handles execution. */
|
|
313
|
+
tools?: Array<{
|
|
314
|
+
name: string;
|
|
315
|
+
description: string;
|
|
316
|
+
inputSchema: Record<string, unknown>;
|
|
317
|
+
}>;
|
|
318
|
+
/** "auto" = LLM decides, "required" = must use tool, "none" = no tools */
|
|
319
|
+
toolChoice?: "auto" | "required" | "none";
|
|
320
|
+
maxTokens?: number;
|
|
321
|
+
/** Hint for client: max agentic loop iterations */
|
|
322
|
+
maxIterations?: number;
|
|
323
|
+
/** Tool name patterns to filter (e.g., ['git_*', 'vfs_*']) */
|
|
324
|
+
allowedToolPatterns?: string[];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Result from sampling request
|
|
329
|
+
* Compatible with MCP sampling protocol
|
|
330
|
+
*/
|
|
331
|
+
export interface SamplingResult {
|
|
332
|
+
content: Array<{
|
|
333
|
+
type: string;
|
|
334
|
+
text?: string;
|
|
335
|
+
name?: string;
|
|
336
|
+
input?: Record<string, unknown>;
|
|
337
|
+
}>;
|
|
338
|
+
stopReason: "end_turn" | "tool_use" | "max_tokens";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Queue metrics for monitoring
|
|
343
|
+
*/
|
|
344
|
+
export interface QueueMetrics {
|
|
345
|
+
/** Number of requests currently executing */
|
|
346
|
+
inFlight: number;
|
|
347
|
+
|
|
348
|
+
/** Number of requests waiting in queue */
|
|
349
|
+
queued: number;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Promise resolver for pending requests
|
|
354
|
+
*/
|
|
355
|
+
export interface PromiseResolver<T = unknown> {
|
|
356
|
+
resolve: (value: T) => void;
|
|
357
|
+
reject: (error: Error) => void;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Request queue options
|
|
362
|
+
*/
|
|
363
|
+
export interface QueueOptions {
|
|
364
|
+
maxConcurrent: number;
|
|
365
|
+
strategy: "sleep" | "queue" | "reject";
|
|
366
|
+
sleepMs: number;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ============================================
|
|
370
|
+
// HTTP Server Types
|
|
371
|
+
// ============================================
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Context passed to HTTP rate limit key extractor
|
|
375
|
+
*/
|
|
376
|
+
export interface HttpRateLimitContext {
|
|
377
|
+
/** Client IP address (from x-forwarded-for/x-real-ip) */
|
|
378
|
+
ip: string;
|
|
379
|
+
|
|
380
|
+
/** HTTP method */
|
|
381
|
+
method: string;
|
|
382
|
+
|
|
383
|
+
/** HTTP path (e.g. /mcp) */
|
|
384
|
+
path: string;
|
|
385
|
+
|
|
386
|
+
/** HTTP headers */
|
|
387
|
+
headers: Headers;
|
|
388
|
+
|
|
389
|
+
/** MCP session ID, if present */
|
|
390
|
+
sessionId?: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* HTTP rate limit configuration
|
|
395
|
+
*/
|
|
396
|
+
export interface HttpRateLimitOptions {
|
|
397
|
+
/** Maximum requests per window */
|
|
398
|
+
maxRequests: number;
|
|
399
|
+
|
|
400
|
+
/** Time window in milliseconds */
|
|
401
|
+
windowMs: number;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Function to extract client identifier from HTTP context
|
|
405
|
+
* Default: uses IP address
|
|
406
|
+
*/
|
|
407
|
+
keyExtractor?: (context: HttpRateLimitContext) => string;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Behavior when rate limit is exceeded
|
|
411
|
+
* - 'reject': Return error immediately
|
|
412
|
+
* - 'wait': Wait for slot with backoff
|
|
413
|
+
*/
|
|
414
|
+
onLimitExceeded?: "reject" | "wait";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Options for starting an HTTP server
|
|
419
|
+
*/
|
|
420
|
+
export interface HttpServerOptions {
|
|
421
|
+
/** Port to listen on */
|
|
422
|
+
port: number;
|
|
423
|
+
|
|
424
|
+
/** Hostname to bind to (default: "0.0.0.0") */
|
|
425
|
+
hostname?: string;
|
|
426
|
+
|
|
427
|
+
/** Enable CORS (default: true) */
|
|
428
|
+
cors?: boolean;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Allowed CORS origins (default: "*")
|
|
432
|
+
* Use an allowlist in production.
|
|
433
|
+
*/
|
|
434
|
+
corsOrigins?: "*" | string[];
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Maximum request body size in bytes (default: 1_000_000).
|
|
438
|
+
* Set to null to disable the limit.
|
|
439
|
+
*/
|
|
440
|
+
maxBodyBytes?: number | null;
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Require auth for HTTP mode. If true and auth is not configured, startHttp throws.
|
|
444
|
+
*/
|
|
445
|
+
requireAuth?: boolean;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* IP-based rate limiting for HTTP endpoints.
|
|
449
|
+
*/
|
|
450
|
+
ipRateLimit?: HttpRateLimitOptions;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Custom HTTP routes registered alongside MCP protocol routes.
|
|
454
|
+
* Uses Web standard Request/Response (no framework dependency).
|
|
455
|
+
*/
|
|
456
|
+
customRoutes?: Array<{
|
|
457
|
+
method: "get" | "post";
|
|
458
|
+
path: string;
|
|
459
|
+
handler: (req: Request) => Response | Promise<Response>;
|
|
460
|
+
}>;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Callback when server is ready
|
|
464
|
+
* @param info - Server address info
|
|
465
|
+
*/
|
|
466
|
+
onListen?: (info: { hostname: string; port: number }) => void;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* HTTP server instance returned by startHttp
|
|
471
|
+
*/
|
|
472
|
+
export interface HttpServerInstance {
|
|
473
|
+
/** Shutdown the HTTP server */
|
|
474
|
+
shutdown(): Promise<void>;
|
|
475
|
+
|
|
476
|
+
/** Server address info */
|
|
477
|
+
addr: { hostname: string; port: number };
|
|
478
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Validator
|
|
3
|
+
*
|
|
4
|
+
* JSON Schema validation using ajv for MCP tool arguments.
|
|
5
|
+
* Compiles schemas once for optimal performance.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/server/schema-validator
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// deno-lint-ignore-file no-explicit-any
|
|
11
|
+
import AjvDefault from "ajv";
|
|
12
|
+
|
|
13
|
+
// Get the Ajv constructor (handles ESM/CJS differences)
|
|
14
|
+
const Ajv = (AjvDefault as any).default ?? AjvDefault;
|
|
15
|
+
|
|
16
|
+
// Type definitions for ajv
|
|
17
|
+
interface AjvErrorObject {
|
|
18
|
+
keyword: string;
|
|
19
|
+
instancePath: string;
|
|
20
|
+
schemaPath: string;
|
|
21
|
+
params: Record<string, any>;
|
|
22
|
+
message?: string;
|
|
23
|
+
data?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AjvValidateFunction {
|
|
27
|
+
(data: unknown): boolean;
|
|
28
|
+
errors?: AjvErrorObject[] | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validation error with formatted message
|
|
33
|
+
*/
|
|
34
|
+
export interface ValidationError {
|
|
35
|
+
/** Error message */
|
|
36
|
+
message: string;
|
|
37
|
+
/** Path to invalid property */
|
|
38
|
+
path: string;
|
|
39
|
+
/** Invalid value */
|
|
40
|
+
value?: unknown;
|
|
41
|
+
/** Expected type or constraint */
|
|
42
|
+
expected?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validation result
|
|
47
|
+
*/
|
|
48
|
+
export interface ValidationResult {
|
|
49
|
+
valid: boolean;
|
|
50
|
+
errors: ValidationError[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Schema validator with compiled schema caching
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const validator = new SchemaValidator();
|
|
59
|
+
*
|
|
60
|
+
* // Register tool schema
|
|
61
|
+
* validator.addSchema("my_tool", {
|
|
62
|
+
* type: "object",
|
|
63
|
+
* properties: { count: { type: "number" } },
|
|
64
|
+
* required: ["count"]
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* // Validate arguments
|
|
68
|
+
* const result = validator.validate("my_tool", { count: 5 });
|
|
69
|
+
* if (!result.valid) {
|
|
70
|
+
* console.error(result.errors);
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export class SchemaValidator {
|
|
75
|
+
private ajv: any;
|
|
76
|
+
private validators = new Map<string, AjvValidateFunction>();
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
this.ajv = new Ajv({
|
|
80
|
+
allErrors: true, // Report all errors, not just first
|
|
81
|
+
strict: false, // Allow additional keywords
|
|
82
|
+
useDefaults: true, // Apply default values
|
|
83
|
+
coerceTypes: false, // Don't coerce types (strict validation)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add a schema for a tool
|
|
89
|
+
*
|
|
90
|
+
* @param toolName - Name of the tool
|
|
91
|
+
* @param schema - JSON Schema for tool arguments
|
|
92
|
+
*/
|
|
93
|
+
addSchema(toolName: string, schema: Record<string, unknown>): void {
|
|
94
|
+
// Compile and cache the validator
|
|
95
|
+
const validate = this.ajv.compile(schema);
|
|
96
|
+
this.validators.set(toolName, validate);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove a schema
|
|
101
|
+
*/
|
|
102
|
+
removeSchema(toolName: string): void {
|
|
103
|
+
this.validators.delete(toolName);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a schema exists
|
|
108
|
+
*/
|
|
109
|
+
hasSchema(toolName: string): boolean {
|
|
110
|
+
return this.validators.has(toolName);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate arguments against a tool's schema
|
|
115
|
+
*
|
|
116
|
+
* @param toolName - Name of the tool
|
|
117
|
+
* @param args - Arguments to validate
|
|
118
|
+
* @returns Validation result with errors if invalid
|
|
119
|
+
*/
|
|
120
|
+
validate(toolName: string, args: Record<string, unknown>): ValidationResult {
|
|
121
|
+
const validate = this.validators.get(toolName);
|
|
122
|
+
|
|
123
|
+
if (!validate) {
|
|
124
|
+
// No schema registered - pass through
|
|
125
|
+
return { valid: true, errors: [] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const valid = validate(args);
|
|
129
|
+
|
|
130
|
+
if (valid) {
|
|
131
|
+
return { valid: true, errors: [] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Format errors
|
|
135
|
+
const errors = this.formatErrors(validate.errors || []);
|
|
136
|
+
return { valid: false, errors };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validate and throw if invalid
|
|
141
|
+
*
|
|
142
|
+
* @throws Error with formatted validation message
|
|
143
|
+
*/
|
|
144
|
+
validateOrThrow(toolName: string, args: Record<string, unknown>): void {
|
|
145
|
+
const result = this.validate(toolName, args);
|
|
146
|
+
|
|
147
|
+
if (!result.valid) {
|
|
148
|
+
const messages = result.errors.map((e) => e.message).join("; ");
|
|
149
|
+
throw new Error(`Invalid arguments for ${toolName}: ${messages}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Format ajv errors into readable messages
|
|
155
|
+
*/
|
|
156
|
+
private formatErrors(errors: AjvErrorObject[]): ValidationError[] {
|
|
157
|
+
return errors.map((error) => {
|
|
158
|
+
const path = error.instancePath || "/";
|
|
159
|
+
const param = error.params;
|
|
160
|
+
|
|
161
|
+
let message: string;
|
|
162
|
+
let expected: string | undefined;
|
|
163
|
+
|
|
164
|
+
switch (error.keyword) {
|
|
165
|
+
case "required":
|
|
166
|
+
message = `Missing required property: ${param.missingProperty}`;
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case "type":
|
|
170
|
+
message = `Property ${path} must be ${param.type}`;
|
|
171
|
+
expected = param.type;
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case "enum":
|
|
175
|
+
message = `Property ${path} must be one of: ${
|
|
176
|
+
param.allowedValues?.join(", ")
|
|
177
|
+
}`;
|
|
178
|
+
expected = param.allowedValues?.join(" | ");
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case "minimum":
|
|
182
|
+
message = `Property ${path} must be >= ${param.limit}`;
|
|
183
|
+
expected = `>= ${param.limit}`;
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case "maximum":
|
|
187
|
+
message = `Property ${path} must be <= ${param.limit}`;
|
|
188
|
+
expected = `<= ${param.limit}`;
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case "minLength":
|
|
192
|
+
message =
|
|
193
|
+
`Property ${path} must have at least ${param.limit} characters`;
|
|
194
|
+
expected = `length >= ${param.limit}`;
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case "maxLength":
|
|
198
|
+
message =
|
|
199
|
+
`Property ${path} must have at most ${param.limit} characters`;
|
|
200
|
+
expected = `length <= ${param.limit}`;
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case "pattern":
|
|
204
|
+
message = `Property ${path} must match pattern: ${param.pattern}`;
|
|
205
|
+
expected = param.pattern;
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case "additionalProperties":
|
|
209
|
+
message = `Unknown property: ${param.additionalProperty}`;
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
default:
|
|
213
|
+
message = error.message || `Validation failed at ${path}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
message,
|
|
218
|
+
path,
|
|
219
|
+
value: error.data,
|
|
220
|
+
expected,
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get number of registered schemas
|
|
227
|
+
*/
|
|
228
|
+
get count(): number {
|
|
229
|
+
return this.validators.size;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Clear all schemas
|
|
234
|
+
*/
|
|
235
|
+
clear(): void {
|
|
236
|
+
this.validators.clear();
|
|
237
|
+
}
|
|
238
|
+
}
|