@opencontextprotocol/agent 0.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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/dist/src/agent.d.ts +112 -0
  4. package/dist/src/agent.d.ts.map +1 -0
  5. package/dist/src/agent.js +358 -0
  6. package/dist/src/agent.js.map +1 -0
  7. package/dist/src/context.d.ts +108 -0
  8. package/dist/src/context.d.ts.map +1 -0
  9. package/dist/src/context.js +196 -0
  10. package/dist/src/context.js.map +1 -0
  11. package/dist/src/errors.d.ts +40 -0
  12. package/dist/src/errors.d.ts.map +1 -0
  13. package/dist/src/errors.js +63 -0
  14. package/dist/src/errors.js.map +1 -0
  15. package/dist/src/headers.d.ts +63 -0
  16. package/dist/src/headers.d.ts.map +1 -0
  17. package/dist/src/headers.js +238 -0
  18. package/dist/src/headers.js.map +1 -0
  19. package/dist/src/http_client.d.ts +82 -0
  20. package/dist/src/http_client.d.ts.map +1 -0
  21. package/dist/src/http_client.js +181 -0
  22. package/dist/src/http_client.js.map +1 -0
  23. package/dist/src/index.d.ts +25 -0
  24. package/dist/src/index.d.ts.map +1 -0
  25. package/dist/src/index.js +35 -0
  26. package/dist/src/index.js.map +1 -0
  27. package/dist/src/registry.d.ts +52 -0
  28. package/dist/src/registry.d.ts.map +1 -0
  29. package/dist/src/registry.js +164 -0
  30. package/dist/src/registry.js.map +1 -0
  31. package/dist/src/schema_discovery.d.ts +149 -0
  32. package/dist/src/schema_discovery.d.ts.map +1 -0
  33. package/dist/src/schema_discovery.js +707 -0
  34. package/dist/src/schema_discovery.js.map +1 -0
  35. package/dist/src/schemas/ocp-context.json +138 -0
  36. package/dist/src/storage.d.ts +110 -0
  37. package/dist/src/storage.d.ts.map +1 -0
  38. package/dist/src/storage.js +399 -0
  39. package/dist/src/storage.js.map +1 -0
  40. package/dist/src/validation.d.ts +169 -0
  41. package/dist/src/validation.d.ts.map +1 -0
  42. package/dist/src/validation.js +92 -0
  43. package/dist/src/validation.js.map +1 -0
  44. package/dist/tests/agent.test.d.ts +5 -0
  45. package/dist/tests/agent.test.d.ts.map +1 -0
  46. package/dist/tests/agent.test.js +536 -0
  47. package/dist/tests/agent.test.js.map +1 -0
  48. package/dist/tests/context.test.d.ts +5 -0
  49. package/dist/tests/context.test.d.ts.map +1 -0
  50. package/dist/tests/context.test.js +285 -0
  51. package/dist/tests/context.test.js.map +1 -0
  52. package/dist/tests/headers.test.d.ts +5 -0
  53. package/dist/tests/headers.test.d.ts.map +1 -0
  54. package/dist/tests/headers.test.js +356 -0
  55. package/dist/tests/headers.test.js.map +1 -0
  56. package/dist/tests/http_client.test.d.ts +5 -0
  57. package/dist/tests/http_client.test.d.ts.map +1 -0
  58. package/dist/tests/http_client.test.js +373 -0
  59. package/dist/tests/http_client.test.js.map +1 -0
  60. package/dist/tests/registry.test.d.ts +5 -0
  61. package/dist/tests/registry.test.d.ts.map +1 -0
  62. package/dist/tests/registry.test.js +232 -0
  63. package/dist/tests/registry.test.js.map +1 -0
  64. package/dist/tests/schema_discovery.test.d.ts +5 -0
  65. package/dist/tests/schema_discovery.test.d.ts.map +1 -0
  66. package/dist/tests/schema_discovery.test.js +1074 -0
  67. package/dist/tests/schema_discovery.test.js.map +1 -0
  68. package/dist/tests/storage.test.d.ts +5 -0
  69. package/dist/tests/storage.test.d.ts.map +1 -0
  70. package/dist/tests/storage.test.js +414 -0
  71. package/dist/tests/storage.test.js.map +1 -0
  72. package/dist/tests/validation.test.d.ts +5 -0
  73. package/dist/tests/validation.test.d.ts.map +1 -0
  74. package/dist/tests/validation.test.js +254 -0
  75. package/dist/tests/validation.test.js.map +1 -0
  76. package/package.json +51 -0
@@ -0,0 +1,707 @@
1
+ /**
2
+ * OCP Schema Discovery
3
+ *
4
+ * Automatic OpenAPI schema discovery and tool extraction for OCP agents.
5
+ */
6
+ import { SchemaDiscoveryError } from './errors.js';
7
+ import { readFileSync } from 'fs';
8
+ import { resolve } from 'path';
9
+ import { homedir } from 'os';
10
+ import yaml from 'js-yaml';
11
+ // Configuration constants
12
+ const DEFAULT_API_TITLE = 'Unknown API';
13
+ const DEFAULT_API_VERSION = '1.0.0';
14
+ const SUPPORTED_HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
15
+ /**
16
+ * OCP Schema Discovery Client
17
+ *
18
+ * Discovers and parses OpenAPI specifications to extract available tools.
19
+ */
20
+ export class OCPSchemaDiscovery {
21
+ constructor() {
22
+ this.cache = new Map();
23
+ }
24
+ /**
25
+ * Discover API from OpenAPI specification.
26
+ *
27
+ * @param specPath - URL or file path to OpenAPI specification (JSON or YAML)
28
+ * @param baseUrl - Optional override for API base URL
29
+ * @param includeResources - Optional list of resource names to filter tools by (case-insensitive, first resource segment matching)
30
+ * @param pathPrefix - Optional path prefix to strip before filtering (e.g., '/v1', '/api/v2')
31
+ * @returns API specification with extracted tools
32
+ */
33
+ async discoverApi(specPath, baseUrl, includeResources, pathPrefix) {
34
+ // Normalize cache key (absolute path for files, URL as-is)
35
+ const cacheKey = this._normalizeCacheKey(specPath);
36
+ // Check cache
37
+ if (this.cache.has(cacheKey)) {
38
+ return this.cache.get(cacheKey);
39
+ }
40
+ try {
41
+ const spec = await this._fetchSpec(specPath);
42
+ this._specVersion = this._detectSpecVersion(spec);
43
+ const apiSpec = this._parseOpenApiSpec(spec, baseUrl);
44
+ // Cache the result
45
+ this.cache.set(cacheKey, apiSpec);
46
+ // Apply resource filtering if specified (only on newly parsed specs)
47
+ if (includeResources) {
48
+ const filteredTools = this._filterToolsByResources(apiSpec.tools, includeResources, pathPrefix);
49
+ return {
50
+ base_url: apiSpec.base_url,
51
+ title: apiSpec.title,
52
+ version: apiSpec.version,
53
+ description: apiSpec.description,
54
+ tools: filteredTools,
55
+ raw_spec: apiSpec.raw_spec
56
+ };
57
+ }
58
+ return apiSpec;
59
+ }
60
+ catch (error) {
61
+ throw new SchemaDiscoveryError(`Failed to discover API: ${error instanceof Error ? error.message : String(error)}`);
62
+ }
63
+ }
64
+ /**
65
+ * Normalize cache key: URLs as-is, file paths to absolute.
66
+ */
67
+ _normalizeCacheKey(specPath) {
68
+ if (specPath.startsWith('http://') || specPath.startsWith('https://')) {
69
+ return specPath;
70
+ }
71
+ // Expand ~ and resolve to absolute path
72
+ let expanded = specPath;
73
+ if (specPath === '~' || specPath.startsWith('~/')) {
74
+ expanded = specPath.replace(/^~/, homedir());
75
+ }
76
+ return resolve(expanded);
77
+ }
78
+ /**
79
+ * Fetch OpenAPI spec from URL or local file.
80
+ */
81
+ async _fetchSpec(specPath) {
82
+ if (specPath.startsWith('http://') || specPath.startsWith('https://')) {
83
+ return this._fetchFromUrl(specPath);
84
+ }
85
+ else {
86
+ return this._fetchFromFile(specPath);
87
+ }
88
+ }
89
+ /**
90
+ * Fetch OpenAPI specification from URL without resolving $refs.
91
+ * References are resolved lazily during tool creation.
92
+ */
93
+ async _fetchFromUrl(url) {
94
+ try {
95
+ const response = await fetch(url);
96
+ if (!response.ok) {
97
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
98
+ }
99
+ return await response.json();
100
+ }
101
+ catch (error) {
102
+ throw new SchemaDiscoveryError(`Failed to fetch OpenAPI spec from ${url}: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ }
105
+ /**
106
+ * Load OpenAPI specification from local JSON or YAML file.
107
+ */
108
+ _fetchFromFile(filePath) {
109
+ try {
110
+ // Expand ~ for home directory
111
+ let expandedPath = filePath;
112
+ if (filePath === '~' || filePath.startsWith('~/')) {
113
+ expandedPath = filePath.replace(/^~/, homedir());
114
+ }
115
+ // Resolve to absolute path
116
+ const resolvedPath = resolve(expandedPath);
117
+ // Check file extension
118
+ const lowerPath = resolvedPath.toLowerCase();
119
+ const isJson = lowerPath.endsWith('.json');
120
+ const isYaml = lowerPath.endsWith('.yaml') || lowerPath.endsWith('.yml');
121
+ if (!isJson && !isYaml) {
122
+ const ext = resolvedPath.substring(resolvedPath.lastIndexOf('.'));
123
+ throw new SchemaDiscoveryError(`Unsupported file format: ${ext}. Supported formats: .json, .yaml, .yml`);
124
+ }
125
+ // Read file
126
+ const content = readFileSync(resolvedPath, 'utf-8');
127
+ // Parse based on format
128
+ if (isJson) {
129
+ return JSON.parse(content);
130
+ }
131
+ else {
132
+ return yaml.load(content);
133
+ }
134
+ }
135
+ catch (error) {
136
+ if (error instanceof SchemaDiscoveryError) {
137
+ throw error;
138
+ }
139
+ // YAML errors
140
+ if (error instanceof yaml.YAMLException) {
141
+ throw new SchemaDiscoveryError(`Invalid YAML in file ${filePath}: ${error.message}`);
142
+ }
143
+ // JSON errors
144
+ if (error instanceof SyntaxError) {
145
+ throw new SchemaDiscoveryError(`Invalid JSON in file ${filePath}: ${error.message}`);
146
+ }
147
+ // File not found
148
+ if (error.code === 'ENOENT') {
149
+ throw new SchemaDiscoveryError(`File not found: ${filePath}`);
150
+ }
151
+ throw new SchemaDiscoveryError(`Failed to load spec from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
152
+ }
153
+ }
154
+ /**
155
+ * Detect OpenAPI/Swagger version from spec.
156
+ *
157
+ * @returns Version string: 'swagger_2', 'openapi_3.0', 'openapi_3.1', 'openapi_3.2'
158
+ */
159
+ _detectSpecVersion(spec) {
160
+ if ('swagger' in spec) {
161
+ const swaggerVersion = spec.swagger;
162
+ if (typeof swaggerVersion === 'string' && swaggerVersion.startsWith('2.')) {
163
+ return 'swagger_2';
164
+ }
165
+ throw new SchemaDiscoveryError(`Unsupported Swagger version: ${swaggerVersion}`);
166
+ }
167
+ else if ('openapi' in spec) {
168
+ const openapiVersion = spec.openapi;
169
+ if (typeof openapiVersion === 'string') {
170
+ if (openapiVersion.startsWith('3.0')) {
171
+ return 'openapi_3.0';
172
+ }
173
+ else if (openapiVersion.startsWith('3.1')) {
174
+ return 'openapi_3.1';
175
+ }
176
+ else if (openapiVersion.startsWith('3.2')) {
177
+ return 'openapi_3.2';
178
+ }
179
+ }
180
+ throw new SchemaDiscoveryError(`Unsupported OpenAPI version: ${openapiVersion}`);
181
+ }
182
+ throw new SchemaDiscoveryError('Unable to detect spec version: missing "swagger" or "openapi" field');
183
+ }
184
+ /**
185
+ * Recursively resolve $ref references in OpenAPI spec with polymorphic keyword handling.
186
+ *
187
+ * @param obj - Current object being processed (object, array, or primitive)
188
+ * @param root - Root spec document for looking up references
189
+ * @param resolutionStack - Stack of refs currently being resolved (for circular detection)
190
+ * @param memo - Memoization cache to store resolved references
191
+ * @param insidePolymorphicKeyword - True if currently inside anyOf/oneOf/allOf
192
+ * @returns Object with all resolvable $refs replaced by their definitions
193
+ */
194
+ _resolveRefs(obj, root, resolutionStack = [], memo = {}, insidePolymorphicKeyword = false) {
195
+ // Initialize on first call
196
+ if (root === undefined) {
197
+ root = obj;
198
+ }
199
+ // Handle object types
200
+ if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
201
+ // Check for polymorphic keywords - process with flag set
202
+ if ('anyOf' in obj) {
203
+ const result = {
204
+ anyOf: obj.anyOf.map((item) => this._resolveRefs(item, root, resolutionStack, memo, true))
205
+ };
206
+ // Include other keys if present
207
+ for (const [k, v] of Object.entries(obj)) {
208
+ if (k !== 'anyOf') {
209
+ result[k] = this._resolveRefs(v, root, resolutionStack, memo, insidePolymorphicKeyword);
210
+ }
211
+ }
212
+ return result;
213
+ }
214
+ if ('oneOf' in obj) {
215
+ const result = {
216
+ oneOf: obj.oneOf.map((item) => this._resolveRefs(item, root, resolutionStack, memo, true))
217
+ };
218
+ for (const [k, v] of Object.entries(obj)) {
219
+ if (k !== 'oneOf') {
220
+ result[k] = this._resolveRefs(v, root, resolutionStack, memo, insidePolymorphicKeyword);
221
+ }
222
+ }
223
+ return result;
224
+ }
225
+ if ('allOf' in obj) {
226
+ const result = {
227
+ allOf: obj.allOf.map((item) => this._resolveRefs(item, root, resolutionStack, memo, true))
228
+ };
229
+ for (const [k, v] of Object.entries(obj)) {
230
+ if (k !== 'allOf') {
231
+ result[k] = this._resolveRefs(v, root, resolutionStack, memo, insidePolymorphicKeyword);
232
+ }
233
+ }
234
+ return result;
235
+ }
236
+ // Check if this is a $ref
237
+ if ('$ref' in obj && Object.keys(obj).length === 1) {
238
+ const refPath = obj.$ref;
239
+ // Only handle internal refs (start with #/)
240
+ if (!refPath.startsWith('#/')) {
241
+ return obj;
242
+ }
243
+ // If inside polymorphic keyword, check if ref points to an object
244
+ if (insidePolymorphicKeyword) {
245
+ try {
246
+ const resolved = this._lookupRef(root, refPath);
247
+ if (resolved !== null) {
248
+ // Check if it's an object schema
249
+ if (resolved.type === 'object' || 'properties' in resolved) {
250
+ // Keep the $ref unresolved for object schemas
251
+ return obj;
252
+ }
253
+ }
254
+ }
255
+ catch {
256
+ // If lookup fails, keep the ref
257
+ return obj;
258
+ }
259
+ }
260
+ // Check memo cache
261
+ if (refPath in memo) {
262
+ return memo[refPath];
263
+ }
264
+ // Check for circular reference
265
+ if (resolutionStack.includes(refPath)) {
266
+ // Return a placeholder to break the cycle
267
+ const placeholder = { type: 'object', description: 'Circular reference' };
268
+ memo[refPath] = placeholder;
269
+ return placeholder;
270
+ }
271
+ // Resolve the reference
272
+ try {
273
+ const resolved = this._lookupRef(root, refPath);
274
+ if (resolved !== null) {
275
+ // Recursively resolve the resolved object with updated stack
276
+ const newStack = [...resolutionStack, refPath];
277
+ const resolvedObj = this._resolveRefs(resolved, root, newStack, memo, insidePolymorphicKeyword);
278
+ memo[refPath] = resolvedObj;
279
+ return resolvedObj;
280
+ }
281
+ }
282
+ catch {
283
+ // If lookup fails, return a placeholder
284
+ const placeholder = { type: 'object', description: 'Unresolved reference' };
285
+ memo[refPath] = placeholder;
286
+ return placeholder;
287
+ }
288
+ return obj;
289
+ }
290
+ // Not a $ref, recursively process all values
291
+ const result = {};
292
+ for (const [key, value] of Object.entries(obj)) {
293
+ result[key] = this._resolveRefs(value, root, resolutionStack, memo, insidePolymorphicKeyword);
294
+ }
295
+ return result;
296
+ }
297
+ // Handle array types
298
+ if (Array.isArray(obj)) {
299
+ return obj.map(item => this._resolveRefs(item, root, resolutionStack, memo, insidePolymorphicKeyword));
300
+ }
301
+ // Primitives pass through unchanged
302
+ return obj;
303
+ }
304
+ /**
305
+ * Look up a reference path in the spec document.
306
+ *
307
+ * @param root - Root spec document
308
+ * @param refPath - Reference path like '#/components/schemas/User'
309
+ * @returns The referenced object, or null if not found
310
+ */
311
+ _lookupRef(root, refPath) {
312
+ // Remove the leading '#/' and split by '/'
313
+ if (!refPath.startsWith('#/')) {
314
+ return null;
315
+ }
316
+ const pathParts = refPath.substring(2).split('/');
317
+ // Navigate through the spec
318
+ let current = root;
319
+ for (const part of pathParts) {
320
+ if (current !== null && typeof current === 'object' && part in current) {
321
+ current = current[part];
322
+ }
323
+ else {
324
+ return null;
325
+ }
326
+ }
327
+ return current;
328
+ }
329
+ /**
330
+ * Parse OpenAPI specification and extract tools with lazy $ref resolution.
331
+ */
332
+ _parseOpenApiSpec(spec, baseUrlOverride) {
333
+ // Initialize memoization cache for lazy $ref resolution
334
+ const memoCache = {};
335
+ // Extract API info
336
+ const info = spec.info || {};
337
+ const title = info.title || DEFAULT_API_TITLE;
338
+ const version = info.version || '1.0.0';
339
+ const description = info.description || '';
340
+ // Extract base URL (version-specific)
341
+ let baseUrl = baseUrlOverride;
342
+ if (!baseUrl) {
343
+ baseUrl = this._extractBaseUrl(spec);
344
+ }
345
+ // Extract tools from paths
346
+ const tools = [];
347
+ const paths = spec.paths || {};
348
+ for (const [path, pathItem] of Object.entries(paths)) {
349
+ if (typeof pathItem !== 'object' || pathItem === null)
350
+ continue;
351
+ for (const [method, operation] of Object.entries(pathItem)) {
352
+ if (!SUPPORTED_HTTP_METHODS.includes(method.toLowerCase())) {
353
+ continue;
354
+ }
355
+ const tool = this._createToolFromOperation(path, method, operation, spec, memoCache);
356
+ if (tool) {
357
+ tools.push(tool);
358
+ }
359
+ }
360
+ }
361
+ return {
362
+ base_url: baseUrl,
363
+ title,
364
+ version,
365
+ description,
366
+ tools,
367
+ raw_spec: spec
368
+ };
369
+ }
370
+ /**
371
+ * Extract base URL from spec (version-aware).
372
+ */
373
+ _extractBaseUrl(spec) {
374
+ if (this._specVersion === 'swagger_2') {
375
+ // Swagger 2.0: construct from host, basePath, and schemes
376
+ const schemes = spec.schemes || ['https'];
377
+ const host = spec.host || '';
378
+ const basePath = spec.basePath || '';
379
+ if (host) {
380
+ const scheme = schemes.length > 0 ? schemes[0] : 'https';
381
+ return `${scheme}://${host}${basePath}`;
382
+ }
383
+ return '';
384
+ }
385
+ else {
386
+ // OpenAPI 3.x: use servers array
387
+ if (spec.servers && spec.servers.length > 0) {
388
+ return spec.servers[0].url || '';
389
+ }
390
+ return '';
391
+ }
392
+ }
393
+ /**
394
+ * Create tool definition from OpenAPI operation.
395
+ */
396
+ _createToolFromOperation(path, method, operation, specData, memoCache) {
397
+ if (!operation || typeof operation !== 'object') {
398
+ return null;
399
+ }
400
+ // Generate tool name with proper validation and fallback logic
401
+ const operationId = operation.operationId;
402
+ let toolName = null;
403
+ // Try operationId first
404
+ if (operationId) {
405
+ const normalizedName = this._normalizeToolName(operationId);
406
+ if (this._isValidToolName(normalizedName)) {
407
+ toolName = normalizedName;
408
+ }
409
+ }
410
+ // If operationId failed, try fallback naming
411
+ if (!toolName) {
412
+ // Generate name from path and method
413
+ const cleanPath = path.replace(/\//g, '_').replace(/[{}]/g, '');
414
+ const fallbackName = `${method.toLowerCase()}${cleanPath}`;
415
+ const normalizedFallback = this._normalizeToolName(fallbackName);
416
+ if (this._isValidToolName(normalizedFallback)) {
417
+ toolName = normalizedFallback;
418
+ }
419
+ }
420
+ // If we can't generate a valid tool name, skip this operation
421
+ if (!toolName) {
422
+ console.warn(`Skipping operation ${method} ${path}: unable to generate valid tool name`);
423
+ return null;
424
+ }
425
+ const description = operation.summary || operation.description || 'No description provided';
426
+ const tags = operation.tags || [];
427
+ // Parse parameters (version-aware)
428
+ const parameters = this._parseParameters(operation.parameters || [], specData, memoCache);
429
+ // Add request body parameters (version-specific)
430
+ if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
431
+ if (this._specVersion === 'swagger_2') {
432
+ // Swagger 2.0: body is in parameters array
433
+ for (const param of (operation.parameters || [])) {
434
+ const bodyParams = this._parseSwagger2BodyParameter(param, specData, memoCache);
435
+ Object.assign(parameters, bodyParams);
436
+ }
437
+ }
438
+ else {
439
+ // OpenAPI 3.x: separate requestBody field
440
+ if (operation.requestBody) {
441
+ const bodyParams = this._parseOpenApi3RequestBody(operation.requestBody, specData, memoCache);
442
+ Object.assign(parameters, bodyParams);
443
+ }
444
+ }
445
+ }
446
+ // Parse response schema
447
+ const responseSchema = this._parseResponses(operation.responses || {}, specData, memoCache);
448
+ return {
449
+ name: toolName,
450
+ description,
451
+ method: method.toUpperCase(),
452
+ path,
453
+ parameters,
454
+ response_schema: responseSchema,
455
+ operation_id: operationId,
456
+ tags
457
+ };
458
+ }
459
+ /**
460
+ * Normalize tool name to camelCase, removing special characters.
461
+ */
462
+ _normalizeToolName(name) {
463
+ if (!name) {
464
+ return name;
465
+ }
466
+ // First, split PascalCase/camelCase words (e.g., "FetchAccount" -> "Fetch Account")
467
+ // Insert space before uppercase letters that follow lowercase letters or digits
468
+ const pascalSplit = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
469
+ // Replace separators (/, _, -, .) with spaces for processing
470
+ // Also handle multiple consecutive separators like //
471
+ const normalized = pascalSplit.replace(/[\/_.-]+/g, ' ');
472
+ // Split into words and filter out empty strings
473
+ const words = normalized.split(' ').filter(word => word);
474
+ if (words.length === 0) {
475
+ return name;
476
+ }
477
+ // Convert to camelCase: first word lowercase, rest capitalize
478
+ const camelCaseWords = [words[0].toLowerCase()];
479
+ for (let i = 1; i < words.length; i++) {
480
+ camelCaseWords.push(words[i].charAt(0).toUpperCase() + words[i].slice(1).toLowerCase());
481
+ }
482
+ return camelCaseWords.join('');
483
+ }
484
+ /**
485
+ * Check if a normalized tool name is valid.
486
+ */
487
+ _isValidToolName(name) {
488
+ if (!name) {
489
+ return false;
490
+ }
491
+ // Must start with a letter
492
+ if (!/^[a-zA-Z]/.test(name)) {
493
+ return false;
494
+ }
495
+ // Must contain at least one alphanumeric character
496
+ if (!/[a-zA-Z0-9]/.test(name)) {
497
+ return false;
498
+ }
499
+ return true;
500
+ }
501
+ /**
502
+ * Parse OpenAPI parameters with lazy $ref resolution.
503
+ */
504
+ _parseParameters(parameters, specData, memoCache) {
505
+ const result = {};
506
+ for (const param of parameters) {
507
+ if (!param || typeof param !== 'object')
508
+ continue;
509
+ const name = param.name;
510
+ if (!name)
511
+ continue;
512
+ const paramSchema = {
513
+ description: param.description || '',
514
+ required: param.required || false,
515
+ location: param.in || 'query',
516
+ type: 'string' // Default type
517
+ };
518
+ // Extract type from schema
519
+ const schema = param.schema || {};
520
+ if (schema) {
521
+ // Resolve any $refs in the parameter schema
522
+ const resolvedSchema = this._resolveRefs(schema, specData, [], memoCache);
523
+ paramSchema.type = resolvedSchema.type || 'string';
524
+ if ('enum' in resolvedSchema) {
525
+ paramSchema.enum = resolvedSchema.enum;
526
+ }
527
+ if ('format' in resolvedSchema) {
528
+ paramSchema.format = resolvedSchema.format;
529
+ }
530
+ }
531
+ result[name] = paramSchema;
532
+ }
533
+ return result;
534
+ }
535
+ /**
536
+ * Parse OpenAPI 3.x request body with lazy $ref resolution.
537
+ */
538
+ _parseOpenApi3RequestBody(requestBody, specData, memoCache) {
539
+ if (!requestBody || typeof requestBody !== 'object') {
540
+ return {};
541
+ }
542
+ const content = requestBody.content || {};
543
+ const jsonContent = content['application/json'];
544
+ if (!jsonContent || !jsonContent.schema) {
545
+ return {};
546
+ }
547
+ // Resolve the schema if it contains $refs
548
+ const schema = this._resolveRefs(jsonContent.schema, specData, [], memoCache);
549
+ // Handle object schemas
550
+ if (schema.type === 'object') {
551
+ const properties = schema.properties || {};
552
+ const required = schema.required || [];
553
+ const result = {};
554
+ for (const [name, propSchema] of Object.entries(properties)) {
555
+ if (typeof propSchema !== 'object' || propSchema === null)
556
+ continue;
557
+ const prop = propSchema;
558
+ result[name] = {
559
+ description: prop.description || '',
560
+ required: required.includes(name),
561
+ location: 'body',
562
+ type: prop.type || 'string'
563
+ };
564
+ if ('enum' in prop) {
565
+ result[name].enum = prop.enum;
566
+ }
567
+ }
568
+ return result;
569
+ }
570
+ return {};
571
+ }
572
+ /**
573
+ * Parse Swagger 2.0 body parameter into parameters.
574
+ */
575
+ _parseSwagger2BodyParameter(param, specData, memoCache) {
576
+ if (!param || typeof param !== 'object' || param.in !== 'body' || !param.schema) {
577
+ return {};
578
+ }
579
+ // Resolve the schema if it contains $refs
580
+ const schema = this._resolveRefs(param.schema, specData, [], memoCache);
581
+ // Handle object schemas
582
+ if (schema.type === 'object') {
583
+ const properties = schema.properties || {};
584
+ const required = schema.required || [];
585
+ const result = {};
586
+ for (const [name, propSchema] of Object.entries(properties)) {
587
+ if (typeof propSchema !== 'object' || propSchema === null)
588
+ continue;
589
+ const prop = propSchema;
590
+ result[name] = {
591
+ description: prop.description || '',
592
+ required: required.includes(name),
593
+ location: 'body',
594
+ type: prop.type || 'string'
595
+ };
596
+ if ('enum' in prop) {
597
+ result[name].enum = prop.enum;
598
+ }
599
+ }
600
+ return result;
601
+ }
602
+ return {};
603
+ }
604
+ /**
605
+ * Parse OpenAPI responses with lazy $ref resolution (version-aware).
606
+ */
607
+ _parseResponses(responses, specData, memoCache) {
608
+ // Find first 2xx response
609
+ for (const [statusCode, response] of Object.entries(responses)) {
610
+ if (statusCode.startsWith('2') && typeof response === 'object' && response !== null) {
611
+ if (this._specVersion === 'swagger_2') {
612
+ // Swagger 2.0: schema is directly in response
613
+ if (response.schema) {
614
+ // Resolve the schema if it contains $refs
615
+ return this._resolveRefs(response.schema, specData, [], memoCache);
616
+ }
617
+ }
618
+ else {
619
+ // OpenAPI 3.x: schema is in content.application/json
620
+ const content = response.content || {};
621
+ const jsonContent = content['application/json'];
622
+ if (jsonContent && jsonContent.schema) {
623
+ // Resolve the schema if it contains $refs
624
+ return this._resolveRefs(jsonContent.schema, specData, [], memoCache);
625
+ }
626
+ }
627
+ }
628
+ }
629
+ return undefined;
630
+ }
631
+ /**
632
+ * Get tools filtered by tag.
633
+ */
634
+ getToolsByTag(apiSpec, tag) {
635
+ return apiSpec.tools.filter(tool => tool.tags && tool.tags.includes(tag));
636
+ }
637
+ /**
638
+ * Filter tools to only include those whose first resource segment matches includeResources.
639
+ */
640
+ _filterToolsByResources(tools, includeResources, pathPrefix) {
641
+ if (!includeResources || includeResources.length === 0) {
642
+ return tools;
643
+ }
644
+ // Normalize resource names to lowercase for case-insensitive matching
645
+ const normalizedResources = includeResources.map(r => r.toLowerCase());
646
+ return tools.filter(tool => {
647
+ let path = tool.path;
648
+ // Strip path prefix if provided
649
+ if (pathPrefix) {
650
+ const prefixLower = pathPrefix.toLowerCase();
651
+ const pathLower = path.toLowerCase();
652
+ if (pathLower.startsWith(prefixLower)) {
653
+ path = path.substring(pathPrefix.length);
654
+ }
655
+ }
656
+ // Extract path segments by splitting on both '/' and '.'
657
+ const pathLower = path.toLowerCase();
658
+ // Replace dots with slashes for uniform splitting
659
+ const pathNormalized = pathLower.replace(/\./g, '/');
660
+ // Split by '/' and filter out empty segments and parameter placeholders
661
+ const segments = pathNormalized.split('/').filter(seg => seg && !seg.startsWith('{'));
662
+ // Check if the first segment matches any of the includeResources
663
+ return segments.length > 0 && normalizedResources.includes(segments[0]);
664
+ });
665
+ }
666
+ /**
667
+ * Search tools by name or description.
668
+ */
669
+ searchTools(apiSpec, query) {
670
+ const lowerQuery = query.toLowerCase();
671
+ return apiSpec.tools.filter(tool => tool.name.toLowerCase().includes(lowerQuery) ||
672
+ tool.description.toLowerCase().includes(lowerQuery));
673
+ }
674
+ /**
675
+ * Generate human-readable tool documentation.
676
+ */
677
+ generateToolDocumentation(tool) {
678
+ const docLines = [
679
+ `## ${tool.name}`,
680
+ `**Method:** ${tool.method}`,
681
+ `**Path:** ${tool.path}`,
682
+ `**Description:** ${tool.description}`,
683
+ ''
684
+ ];
685
+ if (Object.keys(tool.parameters).length > 0) {
686
+ docLines.push('### Parameters:');
687
+ for (const [paramName, paramInfo] of Object.entries(tool.parameters)) {
688
+ const required = paramInfo.required ? ' (required)' : ' (optional)';
689
+ const location = ` [${paramInfo.location || 'query'}]`;
690
+ docLines.push(`- **${paramName}**${required}${location}: ${paramInfo.description || ''}`);
691
+ }
692
+ docLines.push('');
693
+ }
694
+ if (tool.tags && tool.tags.length > 0) {
695
+ docLines.push(`**Tags:** ${tool.tags.join(', ')}`);
696
+ docLines.push('');
697
+ }
698
+ return docLines.join('\n');
699
+ }
700
+ /**
701
+ * Clear the discovery cache.
702
+ */
703
+ clearCache() {
704
+ this.cache.clear();
705
+ }
706
+ }
707
+ //# sourceMappingURL=schema_discovery.js.map