mcp-openapi-schema-explorer 1.0.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 (133) hide show
  1. package/.devcontainer/devcontainer.json +24 -0
  2. package/.github/dependabot.yml +13 -0
  3. package/.github/workflows/ci.yml +111 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.prettierignore +3 -0
  6. package/.prettierrc.json +12 -0
  7. package/.releaserc.json +23 -0
  8. package/CHANGELOG.md +32 -0
  9. package/CONTRIBUTING.md +67 -0
  10. package/Dockerfile +3 -0
  11. package/LICENSE +21 -0
  12. package/README.md +127 -0
  13. package/dist/src/config.d.ts +15 -0
  14. package/dist/src/config.js +19 -0
  15. package/dist/src/config.js.map +1 -0
  16. package/dist/src/handlers/component-detail-handler.d.ts +14 -0
  17. package/dist/src/handlers/component-detail-handler.js +87 -0
  18. package/dist/src/handlers/component-detail-handler.js.map +1 -0
  19. package/dist/src/handlers/component-map-handler.d.ts +14 -0
  20. package/dist/src/handlers/component-map-handler.js +63 -0
  21. package/dist/src/handlers/component-map-handler.js.map +1 -0
  22. package/dist/src/handlers/handler-utils.d.ts +69 -0
  23. package/dist/src/handlers/handler-utils.js +180 -0
  24. package/dist/src/handlers/handler-utils.js.map +1 -0
  25. package/dist/src/handlers/operation-handler.d.ts +14 -0
  26. package/dist/src/handlers/operation-handler.js +86 -0
  27. package/dist/src/handlers/operation-handler.js.map +1 -0
  28. package/dist/src/handlers/path-item-handler.d.ts +14 -0
  29. package/dist/src/handlers/path-item-handler.js +66 -0
  30. package/dist/src/handlers/path-item-handler.js.map +1 -0
  31. package/dist/src/handlers/top-level-field-handler.d.ts +14 -0
  32. package/dist/src/handlers/top-level-field-handler.js +72 -0
  33. package/dist/src/handlers/top-level-field-handler.js.map +1 -0
  34. package/dist/src/index.d.ts +2 -0
  35. package/dist/src/index.js +177 -0
  36. package/dist/src/index.js.map +1 -0
  37. package/dist/src/rendering/components.d.ts +67 -0
  38. package/dist/src/rendering/components.js +177 -0
  39. package/dist/src/rendering/components.js.map +1 -0
  40. package/dist/src/rendering/document.d.ts +36 -0
  41. package/dist/src/rendering/document.js +147 -0
  42. package/dist/src/rendering/document.js.map +1 -0
  43. package/dist/src/rendering/path-item.d.ts +45 -0
  44. package/dist/src/rendering/path-item.js +141 -0
  45. package/dist/src/rendering/path-item.js.map +1 -0
  46. package/dist/src/rendering/paths.d.ts +26 -0
  47. package/dist/src/rendering/paths.js +78 -0
  48. package/dist/src/rendering/paths.js.map +1 -0
  49. package/dist/src/rendering/types.d.ts +50 -0
  50. package/dist/src/rendering/types.js +12 -0
  51. package/dist/src/rendering/types.js.map +1 -0
  52. package/dist/src/rendering/utils.d.ts +31 -0
  53. package/dist/src/rendering/utils.js +79 -0
  54. package/dist/src/rendering/utils.js.map +1 -0
  55. package/dist/src/services/formatters.d.ts +36 -0
  56. package/dist/src/services/formatters.js +52 -0
  57. package/dist/src/services/formatters.js.map +1 -0
  58. package/dist/src/services/reference-transform.d.ts +27 -0
  59. package/dist/src/services/reference-transform.js +75 -0
  60. package/dist/src/services/reference-transform.js.map +1 -0
  61. package/dist/src/services/spec-loader.d.ts +27 -0
  62. package/dist/src/services/spec-loader.js +77 -0
  63. package/dist/src/services/spec-loader.js.map +1 -0
  64. package/dist/src/types.d.ts +11 -0
  65. package/dist/src/types.js +2 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/dist/src/utils/uri-builder.d.ts +81 -0
  68. package/dist/src/utils/uri-builder.js +121 -0
  69. package/dist/src/utils/uri-builder.js.map +1 -0
  70. package/dist/src/version.d.ts +1 -0
  71. package/dist/src/version.js +4 -0
  72. package/dist/src/version.js.map +1 -0
  73. package/eslint.config.js +88 -0
  74. package/jest.config.js +32 -0
  75. package/justfile +66 -0
  76. package/memory-bank/activeContext.md +139 -0
  77. package/memory-bank/productContext.md +39 -0
  78. package/memory-bank/progress.md +141 -0
  79. package/memory-bank/projectbrief.md +50 -0
  80. package/memory-bank/systemPatterns.md +224 -0
  81. package/memory-bank/techContext.md +131 -0
  82. package/package.json +76 -0
  83. package/scripts/generate-version.js +49 -0
  84. package/src/config.ts +33 -0
  85. package/src/handlers/component-detail-handler.ts +121 -0
  86. package/src/handlers/component-map-handler.ts +92 -0
  87. package/src/handlers/handler-utils.ts +230 -0
  88. package/src/handlers/operation-handler.ts +114 -0
  89. package/src/handlers/path-item-handler.ts +88 -0
  90. package/src/handlers/top-level-field-handler.ts +92 -0
  91. package/src/index.ts +222 -0
  92. package/src/rendering/components.ts +228 -0
  93. package/src/rendering/document.ts +167 -0
  94. package/src/rendering/path-item.ts +157 -0
  95. package/src/rendering/paths.ts +87 -0
  96. package/src/rendering/types.ts +63 -0
  97. package/src/rendering/utils.ts +107 -0
  98. package/src/services/formatters.ts +71 -0
  99. package/src/services/reference-transform.ts +105 -0
  100. package/src/services/spec-loader.ts +88 -0
  101. package/src/types.ts +17 -0
  102. package/src/utils/uri-builder.ts +134 -0
  103. package/src/version.ts +4 -0
  104. package/test/__tests__/e2e/format.test.ts +224 -0
  105. package/test/__tests__/e2e/resources.test.ts +369 -0
  106. package/test/__tests__/e2e/spec-loading.test.ts +172 -0
  107. package/test/__tests__/unit/config.test.ts +39 -0
  108. package/test/__tests__/unit/handlers/component-detail-handler.test.ts +241 -0
  109. package/test/__tests__/unit/handlers/component-map-handler.test.ts +187 -0
  110. package/test/__tests__/unit/handlers/handler-utils.test.ts +255 -0
  111. package/test/__tests__/unit/handlers/operation-handler.test.ts +202 -0
  112. package/test/__tests__/unit/handlers/path-item-handler.test.ts +153 -0
  113. package/test/__tests__/unit/handlers/top-level-field-handler.test.ts +182 -0
  114. package/test/__tests__/unit/rendering/components.test.ts +269 -0
  115. package/test/__tests__/unit/rendering/document.test.ts +172 -0
  116. package/test/__tests__/unit/rendering/path-item.test.ts +197 -0
  117. package/test/__tests__/unit/rendering/paths.test.ts +115 -0
  118. package/test/__tests__/unit/services/formatters.test.ts +109 -0
  119. package/test/__tests__/unit/services/reference-transform.test.ts +320 -0
  120. package/test/__tests__/unit/services/spec-loader.test.ts +214 -0
  121. package/test/__tests__/unit/utils/uri-builder.test.ts +103 -0
  122. package/test/fixtures/complex-endpoint.json +146 -0
  123. package/test/fixtures/empty-api.json +8 -0
  124. package/test/fixtures/multi-component-types.json +55 -0
  125. package/test/fixtures/paths-test.json +61 -0
  126. package/test/fixtures/sample-api.json +68 -0
  127. package/test/fixtures/sample-v2-api.json +39 -0
  128. package/test/setup.ts +32 -0
  129. package/test/utils/console-helpers.ts +48 -0
  130. package/test/utils/mcp-test-helpers.ts +66 -0
  131. package/test/utils/test-types.ts +54 -0
  132. package/tsconfig.json +25 -0
  133. package/tsconfig.test.json +5 -0
@@ -0,0 +1,157 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
3
+ import { getOperationSummary, createErrorResult, generateListHint } from './utils.js'; // Add .js
4
+
5
+ /**
6
+ * Wraps an OpenAPIV3.PathItemObject to make it renderable.
7
+ * Handles rendering the list of methods for a specific path and
8
+ * the details of specific operations (methods).
9
+ */
10
+ export class RenderablePathItem implements RenderableSpecObject {
11
+ constructor(
12
+ private pathItem: OpenAPIV3.PathItemObject | undefined,
13
+ private path: string, // The raw, decoded path string e.g., "/users/{userId}"
14
+ private pathUriSuffix: string // Built using buildPathItemUriSuffix(path) e.g., 'paths/users%7BuserId%7D'
15
+ ) {}
16
+
17
+ /**
18
+ * Renders a token-efficient list of methods available for this path.
19
+ * Corresponds to the `openapi://paths/{path}` URI.
20
+ */
21
+ renderList(context: RenderContext): RenderResultItem[] {
22
+ if (!this.pathItem) {
23
+ return createErrorResult(this.pathUriSuffix, 'Path item not found.');
24
+ }
25
+
26
+ // Correctly check if the lowercase key is one of the enum values
27
+ const methods = Object.keys(this.pathItem).filter(key =>
28
+ Object.values(OpenAPIV3.HttpMethods).includes(key.toLowerCase() as OpenAPIV3.HttpMethods)
29
+ ) as OpenAPIV3.HttpMethods[];
30
+
31
+ // Check if methods array is empty *after* filtering
32
+ if (methods.length === 0) {
33
+ // Return a specific non-error message indicating no methods were found
34
+ return [
35
+ {
36
+ uriSuffix: this.pathUriSuffix,
37
+ data: `No standard HTTP methods found for path: ${decodeURIComponent(
38
+ this.pathUriSuffix.substring('paths/'.length) // Get original path for display
39
+ )}`,
40
+ renderAsList: true,
41
+ // isError is implicitly false here
42
+ },
43
+ ];
44
+ }
45
+
46
+ // Generate hint first using the new structure
47
+ const hint = generateListHint(context, {
48
+ itemType: 'pathMethod',
49
+ parentPath: this.path, // Use the stored raw path
50
+ });
51
+ // Hint includes leading newline, so start output with it directly
52
+ let outputLines: string[] = [hint.trim(), '']; // Trim leading newline from hint for first line
53
+
54
+ methods.sort().forEach(method => {
55
+ const operation = this.getOperation(method);
56
+ // Use summary or operationId (via getOperationSummary)
57
+ const summaryText = getOperationSummary(operation);
58
+ // Format as METHOD: Summary or just METHOD if no summary/opId
59
+ outputLines.push(`${method.toUpperCase()}${summaryText ? `: ${summaryText}` : ''}`);
60
+ });
61
+
62
+ return [
63
+ {
64
+ uriSuffix: this.pathUriSuffix,
65
+ data: outputLines.join('\n'), // Join lines into a single string
66
+ renderAsList: true,
67
+ },
68
+ ];
69
+ }
70
+
71
+ /**
72
+ * Renders the detail view for one or more specific operations (methods)
73
+ * Renders the detail view. For a PathItem, this usually means listing
74
+ * the methods, similar to renderList. The handler should call
75
+ * `renderOperationDetail` for specific method details.
76
+ */
77
+ renderDetail(context: RenderContext): RenderResultItem[] {
78
+ // Delegate to renderList as the primary view for a path item itself.
79
+ return this.renderList(context);
80
+ }
81
+
82
+ /**
83
+ * Renders the detail view for one or more specific operations (methods)
84
+ * within this path item.
85
+ * Corresponds to the `openapi://paths/{path}/{method*}` URI.
86
+ * This is called by the handler after identifying the method(s).
87
+ *
88
+ * @param context - The rendering context.
89
+ * @param methods - Array of method names (e.g., ['get', 'post']).
90
+ * @returns An array of RenderResultItem representing the operation details.
91
+ */
92
+ renderOperationDetail(
93
+ _context: RenderContext, // Context might be needed later
94
+ methods: string[]
95
+ ): RenderResultItem[] {
96
+ if (!this.pathItem) {
97
+ // Create error results for all requested methods if path item is missing
98
+ return methods.map(method => ({
99
+ uriSuffix: `${this.pathUriSuffix}/${method}`,
100
+ data: null,
101
+ isError: true,
102
+ errorText: 'Path item not found.',
103
+ renderAsList: true,
104
+ }));
105
+ }
106
+
107
+ const results: RenderResultItem[] = [];
108
+
109
+ for (const method of methods) {
110
+ const operation = this.getOperation(method);
111
+ const operationUriSuffix = `${this.pathUriSuffix}/${method}`;
112
+
113
+ if (!operation) {
114
+ results.push({
115
+ uriSuffix: operationUriSuffix,
116
+ data: null,
117
+ isError: true,
118
+ errorText: `Method "${method.toUpperCase()}" not found for path.`,
119
+ renderAsList: true,
120
+ });
121
+ } else {
122
+ // Return the raw operation object; handler will format it
123
+ results.push({
124
+ uriSuffix: operationUriSuffix,
125
+ data: operation,
126
+ // isError: false (default)
127
+ // renderAsList: false (default)
128
+ });
129
+ }
130
+ }
131
+ return results;
132
+ }
133
+
134
+ /**
135
+ * Gets the OperationObject for a specific HTTP method within this path item.
136
+ * Performs case-insensitive lookup.
137
+ * @param method - The HTTP method string (e.g., 'get', 'POST').
138
+ * @returns The OperationObject or undefined if not found.
139
+ */
140
+ getOperation(method: string): OpenAPIV3.OperationObject | undefined {
141
+ if (!this.pathItem) {
142
+ return undefined;
143
+ }
144
+ const lowerMethod = method.toLowerCase();
145
+
146
+ // Check if the key is a standard HTTP method defined in the enum
147
+ if (Object.values(OpenAPIV3.HttpMethods).includes(lowerMethod as OpenAPIV3.HttpMethods)) {
148
+ const operation = this.pathItem[lowerMethod as keyof OpenAPIV3.PathItemObject];
149
+ // Basic check to ensure it looks like an operation object
150
+ if (typeof operation === 'object' && operation !== null && 'responses' in operation) {
151
+ // The check above narrows the type sufficiently, assertion is redundant
152
+ return operation;
153
+ }
154
+ }
155
+ return undefined; // Not a valid method or not an operation object
156
+ }
157
+ }
@@ -0,0 +1,87 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
3
+
4
+ /**
5
+ * Wraps an OpenAPIV3.PathsObject to make it renderable.
6
+ * Handles rendering the list of all paths and methods.
7
+ */
8
+ export class RenderablePaths implements RenderableSpecObject {
9
+ constructor(private paths: OpenAPIV3.PathsObject | undefined) {}
10
+
11
+ /**
12
+ * Renders a token-efficient list of all paths and their methods.
13
+ * Corresponds to the `openapi://paths` URI.
14
+ */
15
+ renderList(context: RenderContext): RenderResultItem[] {
16
+ if (!this.paths || Object.keys(this.paths).length === 0) {
17
+ return [
18
+ {
19
+ uriSuffix: 'paths',
20
+ data: 'No paths found in the specification.',
21
+ renderAsList: true,
22
+ },
23
+ ];
24
+ }
25
+
26
+ // Generate hint first and prepend "Hint: "
27
+ const hintText = `Use '${context.baseUri}paths/{encoded_path}' to list methods for a specific path, or '${context.baseUri}paths/{encoded_path}/{method}' to view details for a specific operation.`;
28
+ let outputLines: string[] = [`Hint: ${hintText}`, '']; // Start with hint and a blank line
29
+
30
+ const pathEntries = Object.entries(this.paths).sort(([pathA], [pathB]) =>
31
+ pathA.localeCompare(pathB)
32
+ );
33
+
34
+ for (const [path, pathItem] of pathEntries) {
35
+ if (!pathItem) continue;
36
+
37
+ // Create a list of valid, sorted, uppercase methods for the current path
38
+ const methods: string[] = [];
39
+ for (const key in pathItem) {
40
+ const lowerKey = key.toLowerCase();
41
+ if (Object.values(OpenAPIV3.HttpMethods).includes(lowerKey as OpenAPIV3.HttpMethods)) {
42
+ // Check if it's a valid operation object before adding the method
43
+ const operation = pathItem[key as keyof OpenAPIV3.PathItemObject];
44
+ if (typeof operation === 'object' && operation !== null && 'responses' in operation) {
45
+ methods.push(lowerKey.toUpperCase());
46
+ }
47
+ }
48
+ }
49
+ methods.sort(); // Sort methods alphabetically
50
+
51
+ // Format the line: METHODS /path
52
+ const methodsString = methods.length > 0 ? methods.join(' ') : '(No methods)';
53
+ outputLines.push(`${methodsString} ${path}`);
54
+ }
55
+
56
+ return [
57
+ {
58
+ uriSuffix: 'paths',
59
+ data: outputLines.join('\n'), // Join lines into a single string
60
+ renderAsList: true, // This result is always plain text
61
+ },
62
+ ];
63
+ }
64
+
65
+ /**
66
+ * Renders the detail view. For the Paths object level, this isn't
67
+ * typically used directly. Details are requested per path or operation.
68
+ */
69
+ renderDetail(context: RenderContext): RenderResultItem[] {
70
+ // Delegate to renderList as the primary view for the collection of paths
71
+ return this.renderList(context);
72
+ }
73
+
74
+ /**
75
+ * Gets the PathItemObject for a specific path.
76
+ * @param path - The decoded path string.
77
+ * @returns The PathItemObject or undefined if not found.
78
+ */
79
+ getPathItem(path: string): OpenAPIV3.PathItemObject | undefined {
80
+ // Use Map for safe access
81
+ if (!this.paths) {
82
+ return undefined;
83
+ }
84
+ const pathsMap = new Map(Object.entries(this.paths));
85
+ return pathsMap.get(path); // Map.get returns ValueType | undefined
86
+ }
87
+ }
@@ -0,0 +1,63 @@
1
+ import { IFormatter } from '../services/formatters';
2
+ // We don't need ResourceContents/ResourceContent here anymore
3
+
4
+ /**
5
+ * Intermediate result structure returned by render methods.
6
+ * Contains the core data needed to build the final ResourceContent.
7
+ */
8
+ export interface RenderResultItem {
9
+ /** The raw data object to be formatted. */
10
+ data: unknown;
11
+ /** The suffix to append to the base URI (e.g., 'info', 'paths/users', 'components/schemas/User'). */
12
+ uriSuffix: string;
13
+ /** Optional flag indicating an error for this specific item. */
14
+ isError?: boolean;
15
+ /** Optional error message if isError is true. */
16
+ errorText?: string;
17
+ /** Optional flag to indicate this should be rendered as a list (text/plain). */
18
+ renderAsList?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Context required for rendering OpenAPI specification objects.
23
+ */
24
+ export interface RenderContext {
25
+ /** Formatter instance for handling output (JSON/YAML). */
26
+ formatter: IFormatter;
27
+ /** Base URI for generating resource links (e.g., "openapi://"). */
28
+ baseUri: string;
29
+ }
30
+
31
+ /**
32
+ * Represents an OpenAPI specification object that can be rendered
33
+ * in different formats (list or detail).
34
+ */
35
+ export interface RenderableSpecObject {
36
+ /**
37
+ * Generates data for a token-efficient list representation.
38
+ * @param context - The rendering context.
39
+ * @returns An array of RenderResultItem.
40
+ */
41
+ renderList(context: RenderContext): RenderResultItem[];
42
+
43
+ /**
44
+ * Generates data for a detailed representation.
45
+ * @param context - The rendering context.
46
+ * @returns An array of RenderResultItem.
47
+ */
48
+ renderDetail(context: RenderContext): RenderResultItem[];
49
+ }
50
+
51
+ /**
52
+ * Type guard to check if an object implements RenderableSpecObject.
53
+ * @param obj - The object to check.
54
+ * @returns True if the object implements RenderableSpecObject, false otherwise.
55
+ */
56
+ export function isRenderableSpecObject(obj: unknown): obj is RenderableSpecObject {
57
+ return (
58
+ typeof obj === 'object' &&
59
+ obj !== null &&
60
+ typeof (obj as RenderableSpecObject).renderList === 'function' &&
61
+ typeof (obj as RenderableSpecObject).renderDetail === 'function'
62
+ );
63
+ }
@@ -0,0 +1,107 @@
1
+ // NOTE: This block replaces the previous import block to ensure types/interfaces are defined correctly.
2
+ import { OpenAPIV3 } from 'openapi-types';
3
+ import { RenderContext, RenderResultItem } from './types.js'; // Add .js
4
+ import {
5
+ buildComponentDetailUriSuffix,
6
+ buildComponentMapUriSuffix,
7
+ buildOperationUriSuffix,
8
+ // buildPathItemUriSuffix, // Not currently used by generateListHint
9
+ } from '../utils/uri-builder.js'; // Added .js extension
10
+
11
+ // Define possible types for list items to guide hint generation
12
+ type ListItemType = 'componentType' | 'componentName' | 'pathMethod';
13
+
14
+ // Define context needed for generating the correct detail URI suffix
15
+ interface HintContext {
16
+ itemType: ListItemType;
17
+ // For componentName hints, the parent component type is needed
18
+ parentComponentType?: string;
19
+ // For pathMethod hints, the parent path is needed
20
+ parentPath?: string;
21
+ }
22
+
23
+ /**
24
+ * Safely retrieves the summary from an Operation object.
25
+ * Handles cases where the operation might be undefined or lack a summary.
26
+ *
27
+ * @param operation - The Operation object or undefined.
28
+ * @returns The operation summary or operationId string, truncated if necessary, or null if neither is available.
29
+ */
30
+ export function getOperationSummary(
31
+ operation: OpenAPIV3.OperationObject | undefined
32
+ ): string | null {
33
+ // Return summary or operationId without truncation
34
+ return operation?.summary || operation?.operationId || null;
35
+ }
36
+
37
+ /**
38
+ * Helper to generate a standard hint text for list views, using the centralized URI builders.
39
+ * @param renderContext - The rendering context containing the base URI.
40
+ * @param hintContext - Context about the type of items being listed and their parent context.
41
+ * @returns The hint string.
42
+ */
43
+ export function generateListHint(renderContext: RenderContext, hintContext: HintContext): string {
44
+ let detailUriSuffixPattern: string;
45
+ let itemTypeName: string; // User-friendly name for the item type in the hint text
46
+
47
+ switch (hintContext.itemType) {
48
+ case 'componentType':
49
+ // Listing component types (e.g., schemas, responses) at openapi://components
50
+ // Hint should point to openapi://components/{type}
51
+ detailUriSuffixPattern = buildComponentMapUriSuffix('{type}'); // Use placeholder
52
+ itemTypeName = 'component type';
53
+ break;
54
+ case 'componentName':
55
+ // Listing component names (e.g., MySchema, User) at openapi://components/{type}
56
+ // Hint should point to openapi://components/{type}/{name}
57
+ if (!hintContext.parentComponentType) {
58
+ console.warn('generateListHint called for componentName without parentComponentType');
59
+ return ''; // Avoid generating a broken hint
60
+ }
61
+ // Use the actual parent type and a placeholder for the name
62
+ detailUriSuffixPattern = buildComponentDetailUriSuffix(
63
+ hintContext.parentComponentType,
64
+ '{name}'
65
+ );
66
+ itemTypeName = hintContext.parentComponentType.slice(0, -1); // e.g., 'schema' from 'schemas'
67
+ break;
68
+ case 'pathMethod':
69
+ // Listing methods (e.g., get, post) at openapi://paths/{path}
70
+ // Hint should point to openapi://paths/{path}/{method}
71
+ if (!hintContext.parentPath) {
72
+ console.warn('generateListHint called for pathMethod without parentPath');
73
+ return ''; // Avoid generating a broken hint
74
+ }
75
+ // Use the actual parent path and a placeholder for the method
76
+ detailUriSuffixPattern = buildOperationUriSuffix(hintContext.parentPath, '{method}');
77
+ itemTypeName = 'operation'; // Or 'method'? 'operation' seems clearer
78
+ break;
79
+ default:
80
+ // Explicitly cast to string to avoid potential 'never' type issue in template literal
81
+ console.warn(`Unknown itemType in generateListHint: ${String(hintContext.itemType)}`);
82
+ return ''; // Avoid generating a hint if context is unknown
83
+ }
84
+
85
+ // Construct the full hint URI pattern using the base URI
86
+ const fullHintPattern = `${renderContext.baseUri}${detailUriSuffixPattern}`;
87
+
88
+ return `\nHint: Use '${fullHintPattern}' to view details for a specific ${itemTypeName}.`;
89
+ }
90
+
91
+ /**
92
+ * Helper to generate a standard error item for RenderResultItem arrays.
93
+ * @param uriSuffix - The URI suffix for the error context.
94
+ * @param message - The error message.
95
+ * @returns A RenderResultItem array containing the error.
96
+ */
97
+ export function createErrorResult(uriSuffix: string, message: string): RenderResultItem[] {
98
+ return [
99
+ {
100
+ uriSuffix: uriSuffix,
101
+ data: null,
102
+ isError: true,
103
+ errorText: message,
104
+ renderAsList: true, // Errors are typically plain text
105
+ },
106
+ ];
107
+ }
@@ -0,0 +1,71 @@
1
+ import { dump as yamlDump } from 'js-yaml';
2
+
3
+ /**
4
+ * Supported output formats
5
+ */
6
+ export type OutputFormat = 'json' | 'yaml' | 'json-minified';
7
+
8
+ /**
9
+ * Interface for formatters that handle different output formats
10
+ */
11
+ export interface IFormatter {
12
+ format(data: unknown): string;
13
+ getMimeType(): string;
14
+ }
15
+
16
+ /**
17
+ * JSON formatter with pretty printing
18
+ */
19
+ export class JsonFormatter implements IFormatter {
20
+ format(data: unknown): string {
21
+ return JSON.stringify(data, null, 2);
22
+ }
23
+
24
+ getMimeType(): string {
25
+ return 'application/json';
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Formats data as minified JSON.
31
+ */
32
+ export class MinifiedJsonFormatter implements IFormatter {
33
+ format(data: unknown): string {
34
+ return JSON.stringify(data);
35
+ }
36
+
37
+ getMimeType(): string {
38
+ return 'application/json';
39
+ }
40
+ }
41
+
42
+ /**
43
+ * YAML formatter using js-yaml library
44
+ */
45
+ export class YamlFormatter implements IFormatter {
46
+ format(data: unknown): string {
47
+ return yamlDump(data, {
48
+ indent: 2,
49
+ lineWidth: -1, // Don't wrap long lines
50
+ noRefs: true, // Don't use references
51
+ });
52
+ }
53
+
54
+ getMimeType(): string {
55
+ return 'text/yaml';
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Creates a formatter instance based on format name
61
+ */
62
+ export function createFormatter(format: OutputFormat): IFormatter {
63
+ switch (format) {
64
+ case 'json':
65
+ return new JsonFormatter();
66
+ case 'yaml':
67
+ return new YamlFormatter();
68
+ case 'json-minified':
69
+ return new MinifiedJsonFormatter();
70
+ }
71
+ }
@@ -0,0 +1,105 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { buildComponentDetailUri } from '../utils/uri-builder.js'; // Added .js extension
3
+
4
+ export interface TransformContext {
5
+ resourceType: 'endpoint' | 'schema';
6
+ format: 'openapi' | 'asyncapi' | 'graphql';
7
+ path?: string;
8
+ method?: string;
9
+ }
10
+
11
+ export interface ReferenceObject {
12
+ $ref: string;
13
+ }
14
+
15
+ export interface TransformedReference {
16
+ $ref: string;
17
+ }
18
+
19
+ export interface ReferenceTransform<T> {
20
+ transformRefs(document: T, context: TransformContext): T;
21
+ }
22
+
23
+ export class ReferenceTransformService {
24
+ private transformers = new Map<string, ReferenceTransform<unknown>>();
25
+
26
+ registerTransformer<T>(format: string, transformer: ReferenceTransform<T>): void {
27
+ this.transformers.set(format, transformer as ReferenceTransform<unknown>);
28
+ }
29
+
30
+ transformDocument<T>(document: T, context: TransformContext): T {
31
+ const transformer = this.transformers.get(context.format) as ReferenceTransform<T>;
32
+ if (!transformer) {
33
+ throw new Error(`No transformer registered for format: ${context.format}`);
34
+ }
35
+ return transformer.transformRefs(document, context);
36
+ }
37
+ }
38
+
39
+ export class OpenAPITransformer implements ReferenceTransform<OpenAPIV3.Document> {
40
+ // Handle nested objects recursively
41
+ private transformObject(obj: unknown, _context: TransformContext): unknown {
42
+ if (!obj || typeof obj !== 'object') {
43
+ return obj;
44
+ }
45
+
46
+ // Handle arrays
47
+ if (Array.isArray(obj)) {
48
+ return obj.map(item => this.transformObject(item, _context));
49
+ }
50
+
51
+ // Handle references
52
+ if (this.isReferenceObject(obj)) {
53
+ return this.transformReference(obj.$ref);
54
+ }
55
+
56
+ // Recursively transform object properties
57
+ const result: Record<string, unknown> = {};
58
+ if (typeof obj === 'object') {
59
+ for (const [key, value] of Object.entries(obj)) {
60
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
61
+ Object.defineProperty(result, key, {
62
+ value: this.transformObject(value, _context),
63
+ enumerable: true,
64
+ writable: true,
65
+ configurable: true,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+
73
+ private isReferenceObject(obj: unknown): obj is ReferenceObject {
74
+ return typeof obj === 'object' && obj !== null && '$ref' in obj;
75
+ }
76
+
77
+ private transformReference(ref: string): TransformedReference {
78
+ // Handle only internal references for now
79
+ if (!ref.startsWith('#/')) {
80
+ return { $ref: ref }; // Keep external refs as-is
81
+ }
82
+
83
+ // Example ref: #/components/schemas/MySchema
84
+ const parts = ref.split('/');
85
+ // Check if it's an internal component reference
86
+ if (parts[0] === '#' && parts[1] === 'components' && parts.length === 4) {
87
+ const componentType = parts[2];
88
+ const componentName = parts[3];
89
+
90
+ // Use the centralized builder to create the correct URI
91
+ const newUri = buildComponentDetailUri(componentType, componentName);
92
+ return {
93
+ $ref: newUri,
94
+ };
95
+ }
96
+
97
+ // Keep other internal references (#/paths/...) and external references as-is
98
+ return { $ref: ref };
99
+ }
100
+
101
+ transformRefs(document: OpenAPIV3.Document, context: TransformContext): OpenAPIV3.Document {
102
+ const transformed = this.transformObject(document, context);
103
+ return transformed as OpenAPIV3.Document;
104
+ }
105
+ }
@@ -0,0 +1,88 @@
1
+ import * as swagger2openapi from 'swagger2openapi';
2
+ import { OpenAPI } from 'openapi-types';
3
+ import { ReferenceTransformService, TransformContext } from './reference-transform.js';
4
+
5
+ /**
6
+ * Service for loading and transforming OpenAPI specifications
7
+ */
8
+ export class SpecLoaderService {
9
+ private specData: OpenAPI.Document | null = null;
10
+
11
+ constructor(
12
+ private specPath: string,
13
+ private referenceTransform: ReferenceTransformService
14
+ ) {}
15
+
16
+ /**
17
+ * Load, potentially convert (from v2), and parse the OpenAPI specification.
18
+ */
19
+ async loadSpec(): Promise<OpenAPI.Document> {
20
+ const options = {
21
+ patch: true, // Fix minor errors in the spec
22
+ warnOnly: true, // Add warnings for non-patchable errors instead of throwing
23
+ origin: this.specPath, // Helps with resolving relative references if needed
24
+ source: this.specPath,
25
+ };
26
+
27
+ try {
28
+ let result;
29
+ // Check if specPath is a URL
30
+ if (this.specPath.startsWith('http://') || this.specPath.startsWith('https://')) {
31
+ result = await swagger2openapi.convertUrl(this.specPath, options);
32
+ } else {
33
+ result = await swagger2openapi.convertFile(this.specPath, options);
34
+ }
35
+
36
+ // swagger2openapi returns the result in result.openapi
37
+ if (!result || !result.openapi) {
38
+ throw new Error('Conversion or parsing failed to produce an OpenAPI document.');
39
+ }
40
+
41
+ // TODO: Check result.options?.warnings for potential issues?
42
+
43
+ this.specData = result.openapi as OpenAPI.Document; // Assuming result.openapi is compatible
44
+ return this.specData;
45
+ } catch (error) {
46
+ // Improve error message clarity
47
+ let message = `Failed to load/convert OpenAPI spec from ${this.specPath}: `;
48
+ if (error instanceof Error) {
49
+ message += error.message;
50
+ // Include stack trace if available and helpful?
51
+ // console.error(error.stack);
52
+ } else {
53
+ message += String(error);
54
+ }
55
+ throw new Error(message);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get the loaded specification
61
+ */
62
+ async getSpec(): Promise<OpenAPI.Document> {
63
+ if (!this.specData) {
64
+ await this.loadSpec();
65
+ }
66
+ return this.specData!;
67
+ }
68
+
69
+ /**
70
+ * Get transformed specification with MCP resource references
71
+ */
72
+ async getTransformedSpec(context: TransformContext): Promise<OpenAPI.Document> {
73
+ const spec = await this.getSpec();
74
+ return this.referenceTransform.transformDocument(spec, context);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Create and initialize a new SpecLoaderService instance
80
+ */
81
+ export async function createSpecLoader(
82
+ specPath: string,
83
+ referenceTransform: ReferenceTransformService
84
+ ): Promise<SpecLoaderService> {
85
+ const loader = new SpecLoaderService(specPath, referenceTransform);
86
+ await loader.loadSpec();
87
+ return loader;
88
+ }