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
package/src/index.ts ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; // Import ResourceTemplate
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { OpenAPI } from 'openapi-types'; // Import OpenAPIV3 as well
5
+ import { loadConfig } from './config.js';
6
+
7
+ // Import new handlers
8
+ import { TopLevelFieldHandler } from './handlers/top-level-field-handler.js';
9
+ import { PathItemHandler } from './handlers/path-item-handler.js';
10
+ import { OperationHandler } from './handlers/operation-handler.js';
11
+ import { ComponentMapHandler } from './handlers/component-map-handler.js';
12
+ import { ComponentDetailHandler } from './handlers/component-detail-handler.js';
13
+ import { OpenAPITransformer, ReferenceTransformService } from './services/reference-transform.js';
14
+ import { SpecLoaderService } from './services/spec-loader.js';
15
+ import { createFormatter } from './services/formatters.js';
16
+ import { encodeUriPathComponent } from './utils/uri-builder.js'; // Import specific function
17
+ import { isOpenAPIV3, getValidatedComponentMap } from './handlers/handler-utils.js'; // Import type guard and helper
18
+ import { VERSION } from './version.js'; // Import the generated version
19
+
20
+ async function main(): Promise<void> {
21
+ try {
22
+ // Get spec path and options from command line arguments
23
+ const [, , specPath, ...args] = process.argv;
24
+ const options = {
25
+ outputFormat: args.includes('--output-format')
26
+ ? args[args.indexOf('--output-format') + 1]
27
+ : undefined,
28
+ };
29
+
30
+ // Load configuration
31
+ const config = loadConfig(specPath, options);
32
+
33
+ // Initialize services
34
+ const referenceTransform = new ReferenceTransformService();
35
+ referenceTransform.registerTransformer('openapi', new OpenAPITransformer());
36
+
37
+ const specLoader = new SpecLoaderService(config.specPath, referenceTransform);
38
+ await specLoader.loadSpec();
39
+
40
+ // Get the loaded spec to extract the title
41
+ const spec: OpenAPI.Document = await specLoader.getSpec(); // Rename back to spec
42
+ // Get the transformed spec for use in completions
43
+ const transformedSpec: OpenAPI.Document = await specLoader.getTransformedSpec({
44
+ resourceType: 'schema', // Use a default context
45
+ format: 'openapi',
46
+ });
47
+ const defaultServerName = 'OpenAPI Schema Explorer';
48
+ // Use original spec for title
49
+ const serverName = spec.info?.title
50
+ ? `Schema Explorer for ${spec.info.title}`
51
+ : defaultServerName;
52
+
53
+ // Create MCP server with dynamic name
54
+ const server = new McpServer({
55
+ name: serverName,
56
+ version: VERSION, // Use the imported version
57
+ });
58
+
59
+ // Set up formatter and new handlers
60
+ const formatter = createFormatter(config.outputFormat);
61
+ const topLevelFieldHandler = new TopLevelFieldHandler(specLoader, formatter);
62
+ const pathItemHandler = new PathItemHandler(specLoader, formatter);
63
+ const operationHandler = new OperationHandler(specLoader, formatter);
64
+ const componentMapHandler = new ComponentMapHandler(specLoader, formatter);
65
+ const componentDetailHandler = new ComponentDetailHandler(specLoader, formatter);
66
+
67
+ // Register new resources
68
+ // 1. openapi://{field}
69
+ const fieldTemplate = new ResourceTemplate('openapi://{field}', {
70
+ list: undefined, // List is handled by the handler logic based on field value
71
+ complete: {
72
+ field: (): string[] => Object.keys(transformedSpec), // Use transformedSpec
73
+ },
74
+ });
75
+ server.resource(
76
+ 'openapi-field', // Unique ID for the resource registration
77
+ fieldTemplate,
78
+ {
79
+ // MimeType varies (text/plain for lists, JSON/YAML for details) - SDK might handle this? Or maybe set a default? Let's omit for now.
80
+ description:
81
+ 'Access top-level fields (info, servers, tags), list paths, or list component types.',
82
+ name: 'OpenAPI Field/List', // Generic name
83
+ },
84
+ topLevelFieldHandler.handleRequest
85
+ );
86
+
87
+ // 2. openapi://paths/{path}
88
+ const pathTemplate = new ResourceTemplate('openapi://paths/{path}', {
89
+ list: undefined, // List is handled by the handler
90
+ complete: {
91
+ path: (): string[] => Object.keys(transformedSpec.paths ?? {}).map(encodeUriPathComponent), // Use imported function directly
92
+ },
93
+ });
94
+ server.resource(
95
+ 'openapi-path-methods',
96
+ pathTemplate,
97
+ {
98
+ mimeType: 'text/plain', // This always returns a list
99
+ description:
100
+ 'List available HTTP methods for a specific path. (Note: {path} must be URL-encoded, e.g., /users/{id} becomes users%2F%7Bid%7D)',
101
+ name: 'Path Methods List',
102
+ },
103
+ pathItemHandler.handleRequest
104
+ );
105
+
106
+ // 3. openapi://paths/{path}/{method*}
107
+ const operationTemplate = new ResourceTemplate('openapi://paths/{path}/{method*}', {
108
+ list: undefined, // Detail view handled by handler
109
+ complete: {
110
+ path: (): string[] => Object.keys(transformedSpec.paths ?? {}).map(encodeUriPathComponent), // Use imported function directly
111
+ method: (): string[] => [
112
+ // Provide static list of common methods
113
+ 'GET',
114
+ 'POST',
115
+ 'PUT',
116
+ 'DELETE',
117
+ 'PATCH',
118
+ 'OPTIONS',
119
+ 'HEAD',
120
+ 'TRACE',
121
+ ],
122
+ },
123
+ });
124
+ server.resource(
125
+ 'openapi-operation-detail',
126
+ operationTemplate,
127
+ {
128
+ mimeType: formatter.getMimeType(), // Detail view uses formatter
129
+ description:
130
+ 'Get details for one or more specific API operations (methods). (Note: {path} must be URL-encoded, e.g., /users/{id} becomes users%2F%7Bid%7D)',
131
+ name: 'Operation Detail',
132
+ },
133
+ operationHandler.handleRequest
134
+ );
135
+
136
+ // 4. openapi://components/{type}
137
+ const componentMapTemplate = new ResourceTemplate('openapi://components/{type}', {
138
+ list: undefined, // List is handled by the handler
139
+ complete: {
140
+ type: (): string[] => {
141
+ // Use type guard to ensure spec is V3 before accessing components
142
+ if (isOpenAPIV3(transformedSpec)) {
143
+ return Object.keys(transformedSpec.components ?? {});
144
+ }
145
+ return []; // Return empty array if not V3 (shouldn't happen ideally)
146
+ },
147
+ },
148
+ });
149
+ server.resource(
150
+ 'openapi-component-list',
151
+ componentMapTemplate,
152
+ {
153
+ mimeType: 'text/plain', // This always returns a list
154
+ description: 'List available components of a specific type (e.g., schemas, parameters).',
155
+ name: 'Component List',
156
+ },
157
+ componentMapHandler.handleRequest
158
+ );
159
+
160
+ // 5. openapi://components/{type}/{name*}
161
+ const componentDetailTemplate = new ResourceTemplate('openapi://components/{type}/{name*}', {
162
+ list: undefined, // Detail view handled by handler
163
+ complete: {
164
+ type: (): string[] => {
165
+ // Use type guard to ensure spec is V3 before accessing components
166
+ if (isOpenAPIV3(transformedSpec)) {
167
+ return Object.keys(transformedSpec.components ?? {});
168
+ }
169
+ return []; // Return empty array if not V3
170
+ }, // <<< Added missing closing brace
171
+ name: (): string[] => {
172
+ // Provide names only if there's exactly one component type defined
173
+ if (
174
+ isOpenAPIV3(transformedSpec) &&
175
+ transformedSpec.components &&
176
+ Object.keys(transformedSpec.components).length === 1
177
+ ) {
178
+ // Get the single component type key (e.g., 'schemas')
179
+ const componentTypeKey = Object.keys(transformedSpec.components)[0];
180
+ // Use the helper to safely get the map
181
+ try {
182
+ const componentTypeMap = getValidatedComponentMap(transformedSpec, componentTypeKey);
183
+ return Object.keys(componentTypeMap);
184
+ } catch (error) {
185
+ // Should not happen if key came from Object.keys, but handle defensively
186
+ console.error(`Error getting component map for key ${componentTypeKey}:`, error);
187
+ return [];
188
+ }
189
+ }
190
+ // Otherwise, return no completions for name
191
+ return [];
192
+ },
193
+ },
194
+ });
195
+ server.resource(
196
+ 'openapi-component-detail',
197
+ componentDetailTemplate,
198
+ {
199
+ mimeType: formatter.getMimeType(), // Detail view uses formatter
200
+ description: 'Get details for one or more specific components (e.g., schemas, parameters).',
201
+ name: 'Component Detail',
202
+ },
203
+ componentDetailHandler.handleRequest
204
+ );
205
+
206
+ // Start server
207
+ const transport = new StdioServerTransport();
208
+ await server.connect(transport);
209
+ } catch (error) {
210
+ console.error(
211
+ 'Failed to start server:',
212
+ error instanceof Error ? error.message : String(error)
213
+ );
214
+ process.exit(1);
215
+ }
216
+ }
217
+
218
+ // Run the server
219
+ main().catch(error => {
220
+ console.error('Unhandled error:', error instanceof Error ? error.message : String(error));
221
+ process.exit(1);
222
+ });
@@ -0,0 +1,228 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
3
+ import { createErrorResult, generateListHint } from './utils.js'; // Add .js
4
+
5
+ // Define valid component types based on OpenAPIV3.ComponentsObject keys
6
+ export type ComponentType = keyof OpenAPIV3.ComponentsObject;
7
+ export const VALID_COMPONENT_TYPES: ComponentType[] = [
8
+ 'schemas',
9
+ 'responses',
10
+ 'parameters',
11
+ 'examples',
12
+ 'requestBodies',
13
+ 'headers',
14
+ 'securitySchemes',
15
+ 'links',
16
+ 'callbacks',
17
+ // 'pathItems' is technically allowed but we handle paths separately
18
+ ];
19
+
20
+ /**
21
+ * Wraps an OpenAPIV3.ComponentsObject to make it renderable.
22
+ * Handles listing the available component types.
23
+ */
24
+ export class RenderableComponents implements RenderableSpecObject {
25
+ constructor(private components: OpenAPIV3.ComponentsObject | undefined) {}
26
+
27
+ /**
28
+ * Renders a list of available component types found in the spec.
29
+ * Corresponds to the `openapi://components` URI.
30
+ */
31
+ renderList(context: RenderContext): RenderResultItem[] {
32
+ if (!this.components || Object.keys(this.components).length === 0) {
33
+ return createErrorResult('components', 'No components found in the specification.');
34
+ }
35
+
36
+ const availableTypes = Object.keys(this.components).filter((key): key is ComponentType =>
37
+ VALID_COMPONENT_TYPES.includes(key as ComponentType)
38
+ );
39
+
40
+ if (availableTypes.length === 0) {
41
+ return createErrorResult('components', 'No valid component types found.');
42
+ }
43
+
44
+ let listText = 'Available Component Types:\n\n';
45
+ availableTypes.sort().forEach(type => {
46
+ listText += `- ${type}\n`;
47
+ });
48
+
49
+ // Use the new hint generator structure
50
+ listText += generateListHint(context, { itemType: 'componentType' });
51
+
52
+ return [
53
+ {
54
+ uriSuffix: 'components',
55
+ data: listText,
56
+ renderAsList: true,
57
+ },
58
+ ];
59
+ }
60
+
61
+ /**
62
+ * Detail view for the main 'components' object isn't meaningful.
63
+ */
64
+ renderDetail(context: RenderContext): RenderResultItem[] {
65
+ return this.renderList(context);
66
+ }
67
+
68
+ /**
69
+ * Gets the map object for a specific component type.
70
+ * @param type - The component type (e.g., 'schemas').
71
+ * @returns The map (e.g., ComponentsObject['schemas']) or undefined.
72
+ */
73
+ getComponentMap(type: ComponentType):
74
+ | Record<
75
+ string,
76
+ | OpenAPIV3.SchemaObject
77
+ | OpenAPIV3.ResponseObject
78
+ | OpenAPIV3.ParameterObject
79
+ | OpenAPIV3.ExampleObject
80
+ | OpenAPIV3.RequestBodyObject
81
+ | OpenAPIV3.HeaderObject
82
+ | OpenAPIV3.SecuritySchemeObject
83
+ | OpenAPIV3.LinkObject
84
+ | OpenAPIV3.CallbackObject
85
+ | OpenAPIV3.ReferenceObject // Include ReferenceObject
86
+ >
87
+ | undefined {
88
+ // Use Map for safe access
89
+ if (!this.components) {
90
+ return undefined;
91
+ }
92
+ const componentsMap = new Map(Object.entries(this.components));
93
+ // Cast needed as Map.get returns the value type or undefined
94
+ return componentsMap.get(type) as ReturnType<RenderableComponents['getComponentMap']>;
95
+ }
96
+ }
97
+
98
+ // =====================================================================
99
+
100
+ /**
101
+ * Wraps a map of components of a specific type (e.g., all schemas).
102
+ * Handles listing component names and rendering component details.
103
+ */
104
+ export class RenderableComponentMap implements RenderableSpecObject {
105
+ constructor(
106
+ private componentMap: ReturnType<RenderableComponents['getComponentMap']>,
107
+ private componentType: ComponentType, // e.g., 'schemas'
108
+ private mapUriSuffix: string // e.g., 'components/schemas'
109
+ ) {}
110
+
111
+ /**
112
+ * Renders a list of component names for the specific type.
113
+ * Corresponds to the `openapi://components/{type}` URI.
114
+ */
115
+ renderList(context: RenderContext): RenderResultItem[] {
116
+ if (!this.componentMap || Object.keys(this.componentMap).length === 0) {
117
+ return createErrorResult(
118
+ this.mapUriSuffix,
119
+ `No components of type "${this.componentType}" found.`
120
+ );
121
+ }
122
+
123
+ const names = Object.keys(this.componentMap).sort();
124
+ let listText = `Available ${this.componentType}:\n\n`;
125
+ names.forEach(name => {
126
+ listText += `- ${name}\n`;
127
+ });
128
+
129
+ // Use the new hint generator structure, providing parent type
130
+ listText += generateListHint(context, {
131
+ itemType: 'componentName',
132
+ parentComponentType: this.componentType,
133
+ });
134
+
135
+ return [
136
+ {
137
+ uriSuffix: this.mapUriSuffix,
138
+ data: listText,
139
+ renderAsList: true,
140
+ },
141
+ ];
142
+ }
143
+
144
+ /**
145
+ * Renders the detail view for one or more specific named components
146
+ * Renders the detail view. For a component map, this usually means listing
147
+ * the component names, similar to renderList. The handler should call
148
+ * `renderComponentDetail` for specific component details.
149
+ */
150
+ renderDetail(context: RenderContext): RenderResultItem[] {
151
+ // Delegate to renderList as the primary view for a component map itself.
152
+ return this.renderList(context);
153
+ }
154
+
155
+ /**
156
+ * Renders the detail view for one or more specific named components
157
+ * within this map.
158
+ * Corresponds to the `openapi://components/{type}/{name*}` URI.
159
+ * This is called by the handler after identifying the name(s).
160
+ *
161
+ * @param _context - The rendering context (might be needed later).
162
+ * @param names - Array of component names.
163
+ * @returns An array of RenderResultItem representing the component details.
164
+ */
165
+ renderComponentDetail(_context: RenderContext, names: string[]): RenderResultItem[] {
166
+ if (!this.componentMap) {
167
+ // Create error results for all requested names if map is missing
168
+ return names.map(name => ({
169
+ uriSuffix: `${this.mapUriSuffix}/${name}`,
170
+ data: null,
171
+ isError: true,
172
+ errorText: `Component map for type "${this.componentType}" not found.`,
173
+ renderAsList: true,
174
+ }));
175
+ }
176
+
177
+ const results: RenderResultItem[] = [];
178
+ for (const name of names) {
179
+ const component = this.getComponent(name);
180
+ const componentUriSuffix = `${this.mapUriSuffix}/${name}`;
181
+
182
+ if (!component) {
183
+ results.push({
184
+ uriSuffix: componentUriSuffix,
185
+ data: null,
186
+ isError: true,
187
+ errorText: `Component "${name}" of type "${this.componentType}" not found.`,
188
+ renderAsList: true,
189
+ });
190
+ } else {
191
+ // Return the raw component object; handler will format it
192
+ results.push({
193
+ uriSuffix: componentUriSuffix,
194
+ data: component,
195
+ });
196
+ }
197
+ }
198
+ return results;
199
+ }
200
+
201
+ /**
202
+ * Gets a specific component object by name.
203
+ * @param name - The name of the component.
204
+ * @returns The component object (or ReferenceObject) or undefined.
205
+ */
206
+ getComponent(
207
+ name: string
208
+ ):
209
+ | OpenAPIV3.SchemaObject
210
+ | OpenAPIV3.ResponseObject
211
+ | OpenAPIV3.ParameterObject
212
+ | OpenAPIV3.ExampleObject
213
+ | OpenAPIV3.RequestBodyObject
214
+ | OpenAPIV3.HeaderObject
215
+ | OpenAPIV3.SecuritySchemeObject
216
+ | OpenAPIV3.LinkObject
217
+ | OpenAPIV3.CallbackObject
218
+ | OpenAPIV3.ReferenceObject
219
+ | undefined {
220
+ // Use Map for safe access
221
+ if (!this.componentMap) {
222
+ return undefined;
223
+ }
224
+ const detailsMap = new Map(Object.entries(this.componentMap));
225
+ // No cast needed, Map.get returns the correct type (ValueType | undefined)
226
+ return detailsMap.get(name);
227
+ }
228
+ }
@@ -0,0 +1,167 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
3
+ // No longer need ResourceContents here
4
+
5
+ // Placeholder for other renderable objects we'll create
6
+ // import { RenderablePaths } from './paths.js'; // Add .js
7
+ // import { RenderableComponents } from './components.js'; // Add .js
8
+
9
+ /**
10
+ * Wraps an OpenAPIV3.Document to make it renderable.
11
+ * Handles rendering for top-level fields like 'info', 'servers', etc.
12
+ * Delegates list rendering for 'paths' and 'components' to respective objects.
13
+ */
14
+ export class RenderableDocument implements RenderableSpecObject {
15
+ // TODO: Add RenderablePaths and RenderableComponents instances
16
+ // private renderablePaths: RenderablePaths;
17
+ // private renderableComponents: RenderableComponents;
18
+
19
+ constructor(private document: OpenAPIV3.Document) {
20
+ // Initialize renderable wrappers for paths and components here
21
+ // this.renderablePaths = new RenderablePaths(document.paths);
22
+ // this.renderableComponents = new RenderableComponents(document.components);
23
+ }
24
+
25
+ /**
26
+ * Renders a list view. For the document level, this is intended
27
+ * to be called only when the requested field is 'paths' or 'components'.
28
+ * The actual routing/delegation will happen in the handler based on the field.
29
+ */
30
+ renderList(_context: RenderContext): RenderResultItem[] {
31
+ // Prefix context with _
32
+ // This method should ideally not be called directly on the document
33
+ // without specifying 'paths' or 'components' as the field.
34
+ // The handler for openapi://{field} will delegate to the appropriate
35
+ // sub-object's renderList.
36
+ // Returning an error result item.
37
+ return [
38
+ {
39
+ uriSuffix: 'error',
40
+ data: null, // No specific data for this error
41
+ isError: true,
42
+ errorText:
43
+ 'Error: List rendering is only supported for specific fields like "paths" or "components" at the top level.',
44
+ renderAsList: true, // Errors often shown as plain text
45
+ },
46
+ ];
47
+ }
48
+
49
+ /**
50
+ * Renders the detail view. For the document level, this should not be called
51
+ * directly without specifying a field. The handler should call
52
+ * `renderTopLevelFieldDetail` instead.
53
+ */
54
+ renderDetail(_context: RenderContext): RenderResultItem[] {
55
+ // Prefix context with _
56
+ // This method implementation fulfills the interface requirement,
57
+ // but direct detail rendering of the whole document isn't meaningful here.
58
+ return [
59
+ {
60
+ uriSuffix: 'error',
61
+ data: null,
62
+ isError: true,
63
+ errorText:
64
+ 'Error: Detail rendering requires specifying a top-level field (e.g., "info", "servers").',
65
+ renderAsList: true, // Errors often shown as plain text
66
+ },
67
+ ];
68
+ }
69
+
70
+ /**
71
+ * Renders the detail view for a *specific* top-level field (e.g., 'info', 'servers').
72
+ * This is called by the handler after identifying the field.
73
+ *
74
+ * @param context - The rendering context.
75
+ * @param fieldObject - The actual top-level field object to render (e.g., document.info).
76
+ * @param fieldName - The name of the field being rendered (e.g., 'info').
77
+ * @returns An array of RenderResultItem representing the detail view.
78
+ */
79
+ renderTopLevelFieldDetail(
80
+ context: RenderContext,
81
+ fieldObject: unknown,
82
+ fieldName: string
83
+ ): RenderResultItem[] {
84
+ // Ensure fieldObject is provided (handler should validate fieldName exists)
85
+ if (fieldObject === undefined || fieldObject === null) {
86
+ return [
87
+ {
88
+ uriSuffix: fieldName,
89
+ data: null,
90
+ isError: true,
91
+ errorText: `Error: Field "${fieldName}" not found in the OpenAPI document.`,
92
+ renderAsList: true,
93
+ },
94
+ ];
95
+ }
96
+
97
+ // Avoid rendering structural fields that have dedicated list views
98
+ if (fieldName === 'paths' || fieldName === 'components') {
99
+ return [
100
+ {
101
+ uriSuffix: fieldName,
102
+ data: null,
103
+ isError: true,
104
+ errorText: `Error: Field "${fieldName}" should be accessed via its list view (${context.baseUri}${fieldName}). Use the list view first.`,
105
+ renderAsList: true,
106
+ },
107
+ ];
108
+ }
109
+
110
+ try {
111
+ // For successful detail rendering, return the data object itself.
112
+ // The handler will format it using the context.formatter.
113
+ return [
114
+ {
115
+ uriSuffix: fieldName,
116
+ data: fieldObject, // Pass the raw data
117
+ // isError defaults to false
118
+ // renderAsList defaults to false (meaning use detail formatter)
119
+ },
120
+ ];
121
+ } catch (error: unknown) {
122
+ // Handle potential errors during data access or initial checks
123
+ // Formatting errors will be caught by the handler later
124
+ return [
125
+ {
126
+ uriSuffix: fieldName,
127
+ data: null,
128
+ isError: true,
129
+ errorText: `Error preparing field "${fieldName}" for rendering: ${
130
+ error instanceof Error ? error.message : String(error)
131
+ }`,
132
+ renderAsList: true,
133
+ },
134
+ ];
135
+ }
136
+ } // End of renderTopLevelFieldDetail
137
+
138
+ // --- Helper methods to access specific parts ---
139
+
140
+ getPathsObject(): OpenAPIV3.PathsObject | undefined {
141
+ return this.document.paths;
142
+ }
143
+
144
+ getComponentsObject(): OpenAPIV3.ComponentsObject | undefined {
145
+ return this.document.components;
146
+ }
147
+
148
+ getTopLevelField(fieldName: string): unknown {
149
+ // Define allowed top-level OpenAPI document properties
150
+ const allowedFields: Array<keyof OpenAPIV3.Document> = [
151
+ 'openapi',
152
+ 'info',
153
+ 'servers',
154
+ 'paths',
155
+ 'components',
156
+ 'security',
157
+ 'tags',
158
+ 'externalDocs',
159
+ ];
160
+
161
+ // Only allow access to documented OpenAPI properties
162
+ if (allowedFields.includes(fieldName as keyof OpenAPIV3.Document)) {
163
+ return this.document[fieldName as keyof OpenAPIV3.Document];
164
+ }
165
+ return undefined;
166
+ }
167
+ } // End of RenderableDocument class