@casys/mcp-server 0.2.1

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/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
+ }