@claude-flow/mcp 3.0.0-alpha.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/.agentic-flow/intelligence.json +16 -0
- package/README.md +428 -0
- package/__tests__/integration.test.ts +449 -0
- package/__tests__/mcp.test.ts +641 -0
- package/dist/connection-pool.d.ts +36 -0
- package/dist/connection-pool.d.ts.map +1 -0
- package/dist/connection-pool.js +273 -0
- package/dist/connection-pool.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth.d.ts +146 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +318 -0
- package/dist/oauth.js.map +1 -0
- package/dist/prompt-registry.d.ts +90 -0
- package/dist/prompt-registry.d.ts.map +1 -0
- package/dist/prompt-registry.js +209 -0
- package/dist/prompt-registry.js.map +1 -0
- package/dist/rate-limiter.d.ts +86 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +197 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/resource-registry.d.ts +144 -0
- package/dist/resource-registry.d.ts.map +1 -0
- package/dist/resource-registry.js +405 -0
- package/dist/resource-registry.js.map +1 -0
- package/dist/sampling.d.ts +102 -0
- package/dist/sampling.d.ts.map +1 -0
- package/dist/sampling.js +268 -0
- package/dist/sampling.js.map +1 -0
- package/dist/schema-validator.d.ts +30 -0
- package/dist/schema-validator.d.ts.map +1 -0
- package/dist/schema-validator.js +182 -0
- package/dist/schema-validator.js.map +1 -0
- package/dist/server.d.ts +122 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +829 -0
- package/dist/server.js.map +1 -0
- package/dist/session-manager.d.ts +55 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +252 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/task-manager.d.ts +81 -0
- package/dist/task-manager.d.ts.map +1 -0
- package/dist/task-manager.js +337 -0
- package/dist/task-manager.js.map +1 -0
- package/dist/tool-registry.d.ts +88 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +353 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/transport/http.d.ts +55 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +446 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/index.d.ts +50 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +181 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/stdio.d.ts +43 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +194 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport/websocket.d.ts +65 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +314 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types.d.ts +473 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -0
- package/src/connection-pool.ts +344 -0
- package/src/index.ts +253 -0
- package/src/oauth.ts +447 -0
- package/src/prompt-registry.ts +296 -0
- package/src/rate-limiter.ts +266 -0
- package/src/resource-registry.ts +530 -0
- package/src/sampling.ts +363 -0
- package/src/schema-validator.ts +213 -0
- package/src/server.ts +1134 -0
- package/src/session-manager.ts +339 -0
- package/src/task-manager.ts +427 -0
- package/src/tool-registry.ts +475 -0
- package/src/transport/http.ts +532 -0
- package/src/transport/index.ts +233 -0
- package/src/transport/stdio.ts +252 -0
- package/src/transport/websocket.ts +396 -0
- package/src/types.ts +664 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +13 -0
package/src/sampling.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @claude-flow/mcp - Sampling (Server-Initiated LLM)
|
|
3
|
+
*
|
|
4
|
+
* MCP 2025-11-25 compliant sampling for server-initiated LLM calls
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import type {
|
|
9
|
+
SamplingMessage,
|
|
10
|
+
ModelPreferences,
|
|
11
|
+
CreateMessageRequest,
|
|
12
|
+
CreateMessageResult,
|
|
13
|
+
PromptContent,
|
|
14
|
+
ILogger,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* External LLM provider interface
|
|
19
|
+
*/
|
|
20
|
+
export interface LLMProvider {
|
|
21
|
+
name: string;
|
|
22
|
+
createMessage(request: CreateMessageRequest): Promise<CreateMessageResult>;
|
|
23
|
+
isAvailable(): Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sampling configuration
|
|
28
|
+
*/
|
|
29
|
+
export interface SamplingConfig {
|
|
30
|
+
/** Default model preferences */
|
|
31
|
+
defaultModelPreferences?: ModelPreferences;
|
|
32
|
+
/** Maximum tokens for any request */
|
|
33
|
+
maxTokensLimit?: number;
|
|
34
|
+
/** Default temperature */
|
|
35
|
+
defaultTemperature?: number;
|
|
36
|
+
/** Timeout for LLM calls (ms) */
|
|
37
|
+
timeout?: number;
|
|
38
|
+
/** Enable request logging */
|
|
39
|
+
enableLogging?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sampling request context
|
|
44
|
+
*/
|
|
45
|
+
export interface SamplingContext {
|
|
46
|
+
sessionId: string;
|
|
47
|
+
serverId?: string;
|
|
48
|
+
metadata?: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_CONFIG: Required<SamplingConfig> = {
|
|
52
|
+
defaultModelPreferences: {
|
|
53
|
+
intelligencePriority: 0.5,
|
|
54
|
+
speedPriority: 0.3,
|
|
55
|
+
costPriority: 0.2,
|
|
56
|
+
},
|
|
57
|
+
maxTokensLimit: 4096,
|
|
58
|
+
defaultTemperature: 0.7,
|
|
59
|
+
timeout: 30000,
|
|
60
|
+
enableLogging: true,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export class SamplingManager extends EventEmitter {
|
|
64
|
+
private readonly config: Required<SamplingConfig>;
|
|
65
|
+
private providers: Map<string, LLMProvider> = new Map();
|
|
66
|
+
private defaultProvider?: string;
|
|
67
|
+
private requestCount = 0;
|
|
68
|
+
private totalTokens = 0;
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly logger: ILogger,
|
|
72
|
+
config: Partial<SamplingConfig> = {}
|
|
73
|
+
) {
|
|
74
|
+
super();
|
|
75
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register an LLM provider
|
|
80
|
+
*/
|
|
81
|
+
registerProvider(provider: LLMProvider, isDefault: boolean = false): void {
|
|
82
|
+
this.providers.set(provider.name, provider);
|
|
83
|
+
if (isDefault || !this.defaultProvider) {
|
|
84
|
+
this.defaultProvider = provider.name;
|
|
85
|
+
}
|
|
86
|
+
this.logger.info('LLM provider registered', { name: provider.name, isDefault });
|
|
87
|
+
this.emit('provider:registered', { name: provider.name });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Unregister a provider
|
|
92
|
+
*/
|
|
93
|
+
unregisterProvider(name: string): boolean {
|
|
94
|
+
const removed = this.providers.delete(name);
|
|
95
|
+
if (removed && this.defaultProvider === name) {
|
|
96
|
+
this.defaultProvider = this.providers.keys().next().value;
|
|
97
|
+
}
|
|
98
|
+
return removed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a message (sampling/createMessage)
|
|
103
|
+
*/
|
|
104
|
+
async createMessage(
|
|
105
|
+
request: CreateMessageRequest,
|
|
106
|
+
context?: SamplingContext
|
|
107
|
+
): Promise<CreateMessageResult> {
|
|
108
|
+
const startTime = Date.now();
|
|
109
|
+
this.requestCount++;
|
|
110
|
+
|
|
111
|
+
// Validate request
|
|
112
|
+
this.validateRequest(request);
|
|
113
|
+
|
|
114
|
+
// Select provider
|
|
115
|
+
const provider = this.selectProvider(request.modelPreferences);
|
|
116
|
+
if (!provider) {
|
|
117
|
+
throw new Error('No LLM provider available');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Apply defaults
|
|
121
|
+
const fullRequest = this.applyDefaults(request);
|
|
122
|
+
|
|
123
|
+
if (this.config.enableLogging) {
|
|
124
|
+
this.logger.debug('Sampling request', {
|
|
125
|
+
provider: provider.name,
|
|
126
|
+
messageCount: request.messages.length,
|
|
127
|
+
maxTokens: fullRequest.maxTokens,
|
|
128
|
+
sessionId: context?.sessionId,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.emit('sampling:start', { provider: provider.name, context });
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Call provider with timeout
|
|
136
|
+
const result = await this.callWithTimeout(
|
|
137
|
+
provider.createMessage(fullRequest),
|
|
138
|
+
this.config.timeout
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const duration = Date.now() - startTime;
|
|
142
|
+
|
|
143
|
+
if (this.config.enableLogging) {
|
|
144
|
+
this.logger.info('Sampling complete', {
|
|
145
|
+
provider: provider.name,
|
|
146
|
+
duration: `${duration}ms`,
|
|
147
|
+
stopReason: result.stopReason,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.emit('sampling:complete', {
|
|
152
|
+
provider: provider.name,
|
|
153
|
+
duration,
|
|
154
|
+
result,
|
|
155
|
+
context,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const duration = Date.now() - startTime;
|
|
161
|
+
|
|
162
|
+
this.logger.error('Sampling failed', {
|
|
163
|
+
provider: provider.name,
|
|
164
|
+
duration: `${duration}ms`,
|
|
165
|
+
error,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.emit('sampling:error', {
|
|
169
|
+
provider: provider.name,
|
|
170
|
+
duration,
|
|
171
|
+
error,
|
|
172
|
+
context,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if sampling is available
|
|
181
|
+
*/
|
|
182
|
+
async isAvailable(): Promise<boolean> {
|
|
183
|
+
if (this.providers.size === 0) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const provider of this.providers.values()) {
|
|
188
|
+
if (await provider.isAvailable()) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get available providers
|
|
198
|
+
*/
|
|
199
|
+
getProviders(): string[] {
|
|
200
|
+
return Array.from(this.providers.keys());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get stats
|
|
205
|
+
*/
|
|
206
|
+
getStats(): {
|
|
207
|
+
requestCount: number;
|
|
208
|
+
totalTokens: number;
|
|
209
|
+
providerCount: number;
|
|
210
|
+
defaultProvider?: string;
|
|
211
|
+
} {
|
|
212
|
+
return {
|
|
213
|
+
requestCount: this.requestCount,
|
|
214
|
+
totalTokens: this.totalTokens,
|
|
215
|
+
providerCount: this.providers.size,
|
|
216
|
+
defaultProvider: this.defaultProvider,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validate sampling request
|
|
222
|
+
*/
|
|
223
|
+
private validateRequest(request: CreateMessageRequest): void {
|
|
224
|
+
if (!request.messages || request.messages.length === 0) {
|
|
225
|
+
throw new Error('Messages are required');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (request.maxTokens > this.config.maxTokensLimit) {
|
|
229
|
+
throw new Error(`maxTokens exceeds limit of ${this.config.maxTokensLimit}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (request.temperature !== undefined && (request.temperature < 0 || request.temperature > 2)) {
|
|
233
|
+
throw new Error('Temperature must be between 0 and 2');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Select provider based on preferences
|
|
239
|
+
*/
|
|
240
|
+
private selectProvider(preferences?: ModelPreferences): LLMProvider | undefined {
|
|
241
|
+
// If hints provided, try to find matching provider
|
|
242
|
+
if (preferences?.hints) {
|
|
243
|
+
for (const hint of preferences.hints) {
|
|
244
|
+
if (hint.name && this.providers.has(hint.name)) {
|
|
245
|
+
return this.providers.get(hint.name);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Use default provider
|
|
251
|
+
if (this.defaultProvider) {
|
|
252
|
+
return this.providers.get(this.defaultProvider);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Return first available
|
|
256
|
+
return this.providers.values().next().value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Apply default values to request
|
|
261
|
+
*/
|
|
262
|
+
private applyDefaults(request: CreateMessageRequest): CreateMessageRequest {
|
|
263
|
+
return {
|
|
264
|
+
...request,
|
|
265
|
+
modelPreferences: request.modelPreferences || this.config.defaultModelPreferences,
|
|
266
|
+
temperature: request.temperature ?? this.config.defaultTemperature,
|
|
267
|
+
maxTokens: Math.min(request.maxTokens, this.config.maxTokensLimit),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Call with timeout
|
|
273
|
+
*/
|
|
274
|
+
private async callWithTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
|
|
275
|
+
return Promise.race([
|
|
276
|
+
promise,
|
|
277
|
+
new Promise<T>((_, reject) => {
|
|
278
|
+
setTimeout(() => reject(new Error('Sampling timeout')), timeout);
|
|
279
|
+
}),
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function createSamplingManager(
|
|
285
|
+
logger: ILogger,
|
|
286
|
+
config?: Partial<SamplingConfig>
|
|
287
|
+
): SamplingManager {
|
|
288
|
+
return new SamplingManager(logger, config);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Create a mock LLM provider for testing
|
|
293
|
+
*/
|
|
294
|
+
export function createMockProvider(name: string = 'mock'): LLMProvider {
|
|
295
|
+
return {
|
|
296
|
+
name,
|
|
297
|
+
async createMessage(request: CreateMessageRequest): Promise<CreateMessageResult> {
|
|
298
|
+
// Simulate processing time
|
|
299
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
role: 'assistant',
|
|
303
|
+
content: {
|
|
304
|
+
type: 'text',
|
|
305
|
+
text: `Mock response to: ${JSON.stringify(request.messages[0]?.content)}`,
|
|
306
|
+
},
|
|
307
|
+
model: `${name}-model`,
|
|
308
|
+
stopReason: 'endTurn',
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
async isAvailable(): Promise<boolean> {
|
|
312
|
+
return true;
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Create an Anthropic provider (requires API key)
|
|
319
|
+
*/
|
|
320
|
+
export function createAnthropicProvider(apiKey: string): LLMProvider {
|
|
321
|
+
return {
|
|
322
|
+
name: 'anthropic',
|
|
323
|
+
async createMessage(request: CreateMessageRequest): Promise<CreateMessageResult> {
|
|
324
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
325
|
+
method: 'POST',
|
|
326
|
+
headers: {
|
|
327
|
+
'Content-Type': 'application/json',
|
|
328
|
+
'x-api-key': apiKey,
|
|
329
|
+
'anthropic-version': '2023-06-01',
|
|
330
|
+
},
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
model: 'claude-3-haiku-20240307',
|
|
333
|
+
max_tokens: request.maxTokens,
|
|
334
|
+
temperature: request.temperature,
|
|
335
|
+
system: request.systemPrompt,
|
|
336
|
+
messages: request.messages.map((m) => ({
|
|
337
|
+
role: m.role,
|
|
338
|
+
content: m.content.type === 'text' ? (m.content as any).text : m.content,
|
|
339
|
+
})),
|
|
340
|
+
}),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
throw new Error(`Anthropic API error: ${response.status}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const data = await response.json() as any;
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
role: 'assistant',
|
|
351
|
+
content: {
|
|
352
|
+
type: 'text',
|
|
353
|
+
text: data.content[0]?.text || '',
|
|
354
|
+
},
|
|
355
|
+
model: data.model,
|
|
356
|
+
stopReason: data.stop_reason === 'end_turn' ? 'endTurn' : 'maxTokens',
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
async isAvailable(): Promise<boolean> {
|
|
360
|
+
return !!apiKey;
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @claude-flow/mcp - JSON Schema Validator
|
|
3
|
+
*
|
|
4
|
+
* Lightweight JSON Schema validation for tool inputs
|
|
5
|
+
* Implements JSON Schema Draft 2020-12 subset
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { JSONSchema } from './types.js';
|
|
9
|
+
|
|
10
|
+
export interface ValidationError {
|
|
11
|
+
path: string;
|
|
12
|
+
message: string;
|
|
13
|
+
keyword: string;
|
|
14
|
+
params?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ValidationResult {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
errors: ValidationError[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate data against JSON Schema
|
|
24
|
+
*/
|
|
25
|
+
export function validateSchema(
|
|
26
|
+
data: unknown,
|
|
27
|
+
schema: JSONSchema,
|
|
28
|
+
path: string = ''
|
|
29
|
+
): ValidationResult {
|
|
30
|
+
const errors: ValidationError[] = [];
|
|
31
|
+
|
|
32
|
+
// Type validation
|
|
33
|
+
if (schema.type) {
|
|
34
|
+
const typeValid = validateType(data, schema.type);
|
|
35
|
+
if (!typeValid) {
|
|
36
|
+
errors.push({
|
|
37
|
+
path: path || 'root',
|
|
38
|
+
message: `Expected type "${schema.type}", got "${typeof data}"`,
|
|
39
|
+
keyword: 'type',
|
|
40
|
+
params: { expected: schema.type, actual: typeof data },
|
|
41
|
+
});
|
|
42
|
+
return { valid: false, errors };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Null check
|
|
47
|
+
if (data === null || data === undefined) {
|
|
48
|
+
if (schema.type && schema.type !== 'null') {
|
|
49
|
+
errors.push({
|
|
50
|
+
path: path || 'root',
|
|
51
|
+
message: 'Value cannot be null or undefined',
|
|
52
|
+
keyword: 'type',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return { valid: errors.length === 0, errors };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// String validations
|
|
59
|
+
if (schema.type === 'string' && typeof data === 'string') {
|
|
60
|
+
if (schema.minLength !== undefined && data.length < schema.minLength) {
|
|
61
|
+
errors.push({
|
|
62
|
+
path,
|
|
63
|
+
message: `String length must be >= ${schema.minLength}`,
|
|
64
|
+
keyword: 'minLength',
|
|
65
|
+
params: { limit: schema.minLength, actual: data.length },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (schema.maxLength !== undefined && data.length > schema.maxLength) {
|
|
69
|
+
errors.push({
|
|
70
|
+
path,
|
|
71
|
+
message: `String length must be <= ${schema.maxLength}`,
|
|
72
|
+
keyword: 'maxLength',
|
|
73
|
+
params: { limit: schema.maxLength, actual: data.length },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (schema.pattern) {
|
|
77
|
+
const regex = new RegExp(schema.pattern);
|
|
78
|
+
if (!regex.test(data)) {
|
|
79
|
+
errors.push({
|
|
80
|
+
path,
|
|
81
|
+
message: `String must match pattern "${schema.pattern}"`,
|
|
82
|
+
keyword: 'pattern',
|
|
83
|
+
params: { pattern: schema.pattern },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (schema.enum && !schema.enum.includes(data)) {
|
|
88
|
+
errors.push({
|
|
89
|
+
path,
|
|
90
|
+
message: `Value must be one of: ${schema.enum.join(', ')}`,
|
|
91
|
+
keyword: 'enum',
|
|
92
|
+
params: { allowedValues: schema.enum },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Number validations
|
|
98
|
+
if ((schema.type === 'number' || schema.type === 'integer') && typeof data === 'number') {
|
|
99
|
+
if (schema.type === 'integer' && !Number.isInteger(data)) {
|
|
100
|
+
errors.push({
|
|
101
|
+
path,
|
|
102
|
+
message: 'Value must be an integer',
|
|
103
|
+
keyword: 'type',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (schema.minimum !== undefined && data < schema.minimum) {
|
|
107
|
+
errors.push({
|
|
108
|
+
path,
|
|
109
|
+
message: `Value must be >= ${schema.minimum}`,
|
|
110
|
+
keyword: 'minimum',
|
|
111
|
+
params: { limit: schema.minimum, actual: data },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (schema.maximum !== undefined && data > schema.maximum) {
|
|
115
|
+
errors.push({
|
|
116
|
+
path,
|
|
117
|
+
message: `Value must be <= ${schema.maximum}`,
|
|
118
|
+
keyword: 'maximum',
|
|
119
|
+
params: { limit: schema.maximum, actual: data },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Array validations
|
|
125
|
+
if (schema.type === 'array' && Array.isArray(data)) {
|
|
126
|
+
if (schema.items) {
|
|
127
|
+
for (let i = 0; i < data.length; i++) {
|
|
128
|
+
const itemResult = validateSchema(data[i], schema.items, `${path}[${i}]`);
|
|
129
|
+
errors.push(...itemResult.errors);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Object validations
|
|
135
|
+
if (schema.type === 'object' && typeof data === 'object' && !Array.isArray(data)) {
|
|
136
|
+
const obj = data as Record<string, unknown>;
|
|
137
|
+
|
|
138
|
+
// Required properties
|
|
139
|
+
if (schema.required) {
|
|
140
|
+
for (const requiredProp of schema.required) {
|
|
141
|
+
if (!(requiredProp in obj)) {
|
|
142
|
+
errors.push({
|
|
143
|
+
path: path ? `${path}.${requiredProp}` : requiredProp,
|
|
144
|
+
message: `Required property "${requiredProp}" is missing`,
|
|
145
|
+
keyword: 'required',
|
|
146
|
+
params: { missingProperty: requiredProp },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Property validations
|
|
153
|
+
if (schema.properties) {
|
|
154
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
155
|
+
if (propName in obj) {
|
|
156
|
+
const propPath = path ? `${path}.${propName}` : propName;
|
|
157
|
+
const propResult = validateSchema(obj[propName], propSchema, propPath);
|
|
158
|
+
errors.push(...propResult.errors);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Additional properties check
|
|
164
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
165
|
+
const allowedProps = new Set(Object.keys(schema.properties));
|
|
166
|
+
for (const propName of Object.keys(obj)) {
|
|
167
|
+
if (!allowedProps.has(propName)) {
|
|
168
|
+
errors.push({
|
|
169
|
+
path: path ? `${path}.${propName}` : propName,
|
|
170
|
+
message: `Additional property "${propName}" is not allowed`,
|
|
171
|
+
keyword: 'additionalProperties',
|
|
172
|
+
params: { additionalProperty: propName },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { valid: errors.length === 0, errors };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validate type
|
|
184
|
+
*/
|
|
185
|
+
function validateType(data: unknown, expectedType: string): boolean {
|
|
186
|
+
if (expectedType === 'null') {
|
|
187
|
+
return data === null;
|
|
188
|
+
}
|
|
189
|
+
if (expectedType === 'array') {
|
|
190
|
+
return Array.isArray(data);
|
|
191
|
+
}
|
|
192
|
+
if (expectedType === 'integer') {
|
|
193
|
+
return typeof data === 'number' && Number.isInteger(data);
|
|
194
|
+
}
|
|
195
|
+
if (expectedType === 'object') {
|
|
196
|
+
return typeof data === 'object' && data !== null && !Array.isArray(data);
|
|
197
|
+
}
|
|
198
|
+
return typeof data === expectedType;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Format validation errors for display
|
|
203
|
+
*/
|
|
204
|
+
export function formatValidationErrors(errors: ValidationError[]): string {
|
|
205
|
+
return errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a validator function for a specific schema
|
|
210
|
+
*/
|
|
211
|
+
export function createValidator(schema: JSONSchema): (data: unknown) => ValidationResult {
|
|
212
|
+
return (data: unknown) => validateSchema(data, schema);
|
|
213
|
+
}
|