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,230 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { RenderContext, RenderResultItem } from '../rendering/types.js'; // Already has .js
3
+ // Remove McpError/ErrorCode import - use standard Error
4
+
5
+ // Define the structure expected for each item in the contents array
6
+ export type FormattedResultItem = {
7
+ uri: string;
8
+ mimeType?: string;
9
+ text: string;
10
+ isError?: boolean;
11
+ };
12
+
13
+ /**
14
+ * Formats RenderResultItem array into an array compatible with the 'contents'
15
+ * property of ReadResourceResultSchema (specifically TextResourceContents).
16
+ */
17
+ export function formatResults(
18
+ context: RenderContext,
19
+ items: RenderResultItem[]
20
+ ): FormattedResultItem[] {
21
+ // Add type check for formatter existence in context
22
+ if (!context.formatter) {
23
+ throw new Error('Formatter is missing in RenderContext for formatResults');
24
+ }
25
+ return items.map(item => {
26
+ const uri = `${context.baseUri}${item.uriSuffix}`;
27
+ let text: string;
28
+ let mimeType: string;
29
+
30
+ if (item.isError) {
31
+ text = item.errorText || 'An unknown error occurred.';
32
+ mimeType = 'text/plain';
33
+ } else if (item.renderAsList) {
34
+ text = typeof item.data === 'string' ? item.data : 'Invalid list data';
35
+ mimeType = 'text/plain';
36
+ } else {
37
+ // Detail view: format using the provided formatter
38
+ try {
39
+ text = context.formatter.format(item.data);
40
+ mimeType = context.formatter.getMimeType();
41
+ } catch (formatError: unknown) {
42
+ text = `Error formatting data for ${uri}: ${
43
+ formatError instanceof Error ? formatError.message : String(formatError)
44
+ }`;
45
+ mimeType = 'text/plain';
46
+ // Ensure isError is true if formatting fails
47
+ item.isError = true;
48
+ item.errorText = text; // Store the formatting error message
49
+ }
50
+ }
51
+
52
+ // Construct the final object, prioritizing item.isError
53
+ const finalItem: FormattedResultItem = {
54
+ uri: uri,
55
+ mimeType: mimeType,
56
+ text: item.isError ? item.errorText || 'An unknown error occurred.' : text,
57
+ isError: item.isError ?? false, // Default to false if not explicitly set
58
+ };
59
+ // Ensure mimeType is text/plain for errors
60
+ if (finalItem.isError) {
61
+ finalItem.mimeType = 'text/plain';
62
+ }
63
+
64
+ return finalItem;
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Type guard to check if an object is an OpenAPIV3.Document.
70
+ */
71
+ export function isOpenAPIV3(spec: unknown): spec is OpenAPIV3.Document {
72
+ return (
73
+ typeof spec === 'object' &&
74
+ spec !== null &&
75
+ 'openapi' in spec &&
76
+ typeof (spec as { openapi: unknown }).openapi === 'string' &&
77
+ (spec as { openapi: string }).openapi.startsWith('3.')
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Safely retrieves a PathItemObject from the specification using a Map.
83
+ * Throws an McpError if the path is not found.
84
+ *
85
+ * @param spec The OpenAPIV3 Document.
86
+ * @param path The decoded path string (e.g., '/users/{id}').
87
+ * @returns The validated PathItemObject.
88
+ * @throws {McpError} If the path is not found in spec.paths.
89
+ */
90
+ export function getValidatedPathItem(
91
+ spec: OpenAPIV3.Document,
92
+ path: string
93
+ ): OpenAPIV3.PathItemObject {
94
+ if (!spec.paths) {
95
+ // Use standard Error
96
+ throw new Error('Specification does not contain any paths.');
97
+ }
98
+ const pathsMap = new Map(Object.entries(spec.paths));
99
+ const pathItem = pathsMap.get(path);
100
+
101
+ if (!pathItem) {
102
+ const errorMessage = `Path "${path}" not found in the specification.`;
103
+ // Use standard Error
104
+ throw new Error(errorMessage);
105
+ }
106
+ // We assume the spec structure is valid if the key exists
107
+ return pathItem as OpenAPIV3.PathItemObject;
108
+ }
109
+
110
+ /**
111
+ * Validates requested HTTP methods against a PathItemObject using a Map.
112
+ * Returns the list of valid requested methods.
113
+ * Throws an McpError if none of the requested methods are valid for the path item.
114
+ *
115
+ * @param pathItem The PathItemObject to check against.
116
+ * @param requestedMethods An array of lowercase HTTP methods requested by the user.
117
+ * @param pathForError The path string, used for creating informative error messages.
118
+ * @returns An array of the requested methods that are valid for this path item.
119
+ * @throws {McpError} If none of the requested methods are valid.
120
+ */
121
+ export function getValidatedOperations(
122
+ pathItem: OpenAPIV3.PathItemObject,
123
+ requestedMethods: string[],
124
+ pathForError: string
125
+ ): string[] {
126
+ const operationsMap = new Map<string, OpenAPIV3.OperationObject>();
127
+ Object.entries(pathItem).forEach(([method, operation]) => {
128
+ // Check if the key is a standard HTTP method before adding
129
+ if (
130
+ ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(
131
+ method.toLowerCase()
132
+ )
133
+ ) {
134
+ operationsMap.set(method.toLowerCase(), operation as OpenAPIV3.OperationObject);
135
+ }
136
+ });
137
+
138
+ // Validate using lowercase versions, but preserve original case for return
139
+ const requestedMethodsLower = requestedMethods.map(m => m.toLowerCase());
140
+ const validLowerMethods = requestedMethodsLower.filter(m => operationsMap.has(m));
141
+
142
+ if (validLowerMethods.length === 0) {
143
+ const availableMethods = Array.from(operationsMap.keys()).join(', ');
144
+ // Show original case in error message for clarity
145
+ const errorMessage = `None of the requested methods (${requestedMethods.join(', ')}) are valid for path "${pathForError}". Available methods: ${availableMethods}`;
146
+ // Use standard Error
147
+ throw new Error(errorMessage);
148
+ }
149
+
150
+ // Return the methods from the *original* requestedMethods array
151
+ // that correspond to the valid lowercase methods found.
152
+ return requestedMethods.filter(m => validLowerMethods.includes(m.toLowerCase()));
153
+ }
154
+
155
+ /**
156
+ * Safely retrieves the component map for a specific type (e.g., schemas, responses)
157
+ * from the specification using a Map.
158
+ * Throws an McpError if spec.components or the specific type map is not found.
159
+ *
160
+ * @param spec The OpenAPIV3 Document.
161
+ * @param type The ComponentType string (e.g., 'schemas', 'responses').
162
+ * @returns The validated component map object (e.g., spec.components.schemas).
163
+ * @throws {McpError} If spec.components or the type map is not found.
164
+ */
165
+ export function getValidatedComponentMap(
166
+ spec: OpenAPIV3.Document,
167
+ type: string // Keep as string for validation flexibility
168
+ ): NonNullable<OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]> {
169
+ if (!spec.components) {
170
+ // Use standard Error
171
+ throw new Error('Specification does not contain a components section.');
172
+ }
173
+ // Validate the requested type against the actual keys in spec.components
174
+ const componentsMap = new Map(Object.entries(spec.components));
175
+ // Add type assertion for clarity, although the check below handles undefined
176
+ const componentMapObj = componentsMap.get(type) as
177
+ | OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]
178
+ | undefined;
179
+
180
+ if (!componentMapObj) {
181
+ const availableTypes = Array.from(componentsMap.keys()).join(', ');
182
+ const errorMessage = `Component type "${type}" not found in the specification. Available types: ${availableTypes}`;
183
+ // Use standard Error
184
+ throw new Error(errorMessage);
185
+ }
186
+ // We assume the spec structure is valid if the key exists
187
+ return componentMapObj as NonNullable<
188
+ OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]
189
+ >;
190
+ }
191
+
192
+ /**
193
+ * Validates requested component names against a specific component map (e.g., schemas).
194
+ * Returns an array of objects containing the valid name and its corresponding detail object.
195
+ * Throws an McpError if none of the requested names are valid for the component map.
196
+ *
197
+ * @param componentMap The specific component map object (e.g., spec.components.schemas).
198
+ * @param requestedNames An array of component names requested by the user.
199
+ * @param componentTypeForError The component type string, used for creating informative error messages.
200
+ * @param detailsMap A Map created from the specific component map object (e.g., new Map(Object.entries(spec.components.schemas))).
201
+ * @param requestedNames An array of component names requested by the user.
202
+ * @param componentTypeForError The component type string, used for creating informative error messages.
203
+ * @returns An array of { name: string, detail: V } for valid requested names, where V is the value type of the Map.
204
+ * @throws {McpError} If none of the requested names are valid.
205
+ */
206
+ // Modify to accept a Map directly
207
+ export function getValidatedComponentDetails<V extends OpenAPIV3.ReferenceObject | object>(
208
+ detailsMap: Map<string, V>, // Accept Map<string, V>
209
+ requestedNames: string[],
210
+ componentTypeForError: string
211
+ ): { name: string; detail: V }[] {
212
+ // No longer need to create the map inside the function
213
+ const validDetails = requestedNames
214
+ .map(name => {
215
+ const detail = detailsMap.get(name); // detail will be V | undefined
216
+ return detail ? { name, detail } : null;
217
+ })
218
+ // Type predicate ensures we filter out nulls and have the correct type
219
+ .filter((item): item is { name: string; detail: V } => item !== null);
220
+
221
+ if (validDetails.length === 0) {
222
+ // Sort available names for deterministic error messages
223
+ const availableNames = Array.from(detailsMap.keys()).sort().join(', ');
224
+ const errorMessage = `None of the requested names (${requestedNames.join(', ')}) are valid for component type "${componentTypeForError}". Available names: ${availableNames}`;
225
+ // Use standard Error
226
+ throw new Error(errorMessage);
227
+ }
228
+
229
+ return validDetails;
230
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ ReadResourceTemplateCallback,
3
+ ResourceTemplate,
4
+ } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
6
+ import { SpecLoaderService } from '../types.js';
7
+ import { IFormatter } from '../services/formatters.js';
8
+ import { RenderablePathItem } from '../rendering/path-item.js';
9
+ import { RenderContext, RenderResultItem } from '../rendering/types.js';
10
+ import { createErrorResult } from '../rendering/utils.js';
11
+ import { buildPathItemUriSuffix } from '../utils/uri-builder.js'; // Added .js extension
12
+ // Import shared handler utils
13
+ import {
14
+ formatResults,
15
+ isOpenAPIV3,
16
+ FormattedResultItem,
17
+ getValidatedPathItem, // Import new helper
18
+ getValidatedOperations, // Import new helper
19
+ } from './handler-utils.js'; // Already has .js
20
+
21
+ const BASE_URI = 'openapi://';
22
+
23
+ // Removed duplicated FormattedResultItem type - now imported from handler-utils
24
+ // Removed duplicated formatResults function - now imported from handler-utils
25
+ // Removed duplicated isOpenAPIV3 function - now imported from handler-utils
26
+
27
+ /**
28
+ * Handles requests for specific operation details within a path.
29
+ * Corresponds to the `openapi://paths/{path}/{method*}` template.
30
+ */
31
+ export class OperationHandler {
32
+ constructor(
33
+ private specLoader: SpecLoaderService,
34
+ private formatter: IFormatter
35
+ ) {}
36
+
37
+ getTemplate(): ResourceTemplate {
38
+ // TODO: Add completion logic if needed
39
+ return new ResourceTemplate(`${BASE_URI}paths/{path}/{method*}`, {
40
+ list: undefined,
41
+ complete: undefined,
42
+ });
43
+ }
44
+
45
+ handleRequest: ReadResourceTemplateCallback = async (
46
+ uri: URL,
47
+ variables: Variables
48
+ ): Promise<{ contents: FormattedResultItem[] }> => {
49
+ const encodedPath = variables.path as string;
50
+ // Correct variable access key: 'method', not 'method*'
51
+ const methodVar = variables['method']; // Can be string or string[]
52
+ // Decode the path received from the URI variable
53
+ const decodedPath = decodeURIComponent(encodedPath || '');
54
+ // Use the builder to create the suffix, which will re-encode the path correctly
55
+ const pathUriSuffix = buildPathItemUriSuffix(decodedPath);
56
+ const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
57
+ let resultItems: RenderResultItem[];
58
+
59
+ try {
60
+ // Normalize methods: Handle string for single value, array for multiple.
61
+ let methods: string[] = [];
62
+ if (Array.isArray(methodVar)) {
63
+ methods = methodVar.map(m => String(m).trim().toLowerCase()); // Ensure elements are strings
64
+ } else if (typeof methodVar === 'string') {
65
+ methods = [methodVar.trim().toLowerCase()]; // Treat as single item array
66
+ }
67
+ methods = methods.filter(m => m.length > 0); // Remove empty strings
68
+
69
+ if (methods.length === 0) {
70
+ throw new Error('No valid HTTP method specified.');
71
+ }
72
+
73
+ const spec = await this.specLoader.getTransformedSpec({
74
+ resourceType: 'schema', // Use 'schema' for now
75
+ format: 'openapi',
76
+ });
77
+
78
+ // Use imported type guard
79
+ if (!isOpenAPIV3(spec)) {
80
+ throw new Error('Only OpenAPI v3 specifications are supported');
81
+ }
82
+
83
+ // --- Use helper to get validated path item ---
84
+ const lookupPath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
85
+ const pathItemObj = getValidatedPathItem(spec, lookupPath);
86
+
87
+ // --- Use helper to get validated requested methods ---
88
+ const validMethods = getValidatedOperations(pathItemObj, methods, lookupPath);
89
+
90
+ // Instantiate RenderablePathItem with the validated pathItemObj
91
+ const renderablePathItem = new RenderablePathItem(
92
+ pathItemObj, // pathItemObj retrieved safely via helper
93
+ lookupPath, // Pass the raw, decoded path
94
+ pathUriSuffix // Pass the correctly built suffix
95
+ );
96
+
97
+ // Use the validated methods returned by the helper
98
+ resultItems = renderablePathItem.renderOperationDetail(context, validMethods);
99
+ } catch (error: unknown) {
100
+ // Catch errors from helpers (e.g., path/method not found) or rendering
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ console.error(`Error handling request ${uri.href}: ${message}`);
103
+ // Create a single error item representing the overall request failure
104
+ resultItems = createErrorResult(
105
+ uri.href.substring(BASE_URI.length), // Use request URI suffix
106
+ message
107
+ );
108
+ }
109
+
110
+ // Use imported formatResults
111
+ const contents = formatResults(context, resultItems);
112
+ return { contents };
113
+ };
114
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ ReadResourceTemplateCallback,
3
+ ResourceTemplate,
4
+ } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
6
+ import { SpecLoaderService } from '../types.js';
7
+ import { IFormatter } from '../services/formatters.js';
8
+ import { RenderablePathItem } from '../rendering/path-item.js';
9
+ import { RenderContext, RenderResultItem } from '../rendering/types.js';
10
+ import { createErrorResult } from '../rendering/utils.js';
11
+ import { buildPathItemUriSuffix } from '../utils/uri-builder.js'; // Added .js extension
12
+ // Import shared handler utils
13
+ import {
14
+ formatResults,
15
+ isOpenAPIV3,
16
+ FormattedResultItem,
17
+ getValidatedPathItem, // Import the helper
18
+ } from './handler-utils.js'; // Already has .js
19
+
20
+ const BASE_URI = 'openapi://';
21
+
22
+ // Removed duplicated FormattedResultItem type - now imported from handler-utils
23
+ // Removed duplicated formatResults function - now imported from handler-utils
24
+ // Removed duplicated isOpenAPIV3 function - now imported from handler-utils
25
+
26
+ /**
27
+ * Handles requests for listing methods for a specific path.
28
+ * Corresponds to the `openapi://paths/{path}` template.
29
+ */
30
+ export class PathItemHandler {
31
+ constructor(
32
+ private specLoader: SpecLoaderService,
33
+ private formatter: IFormatter // Although unused in list view, needed for context
34
+ ) {}
35
+
36
+ getTemplate(): ResourceTemplate {
37
+ // TODO: Add completion logic if needed
38
+ return new ResourceTemplate(`${BASE_URI}paths/{path}`, {
39
+ list: undefined,
40
+ complete: undefined,
41
+ });
42
+ }
43
+
44
+ handleRequest: ReadResourceTemplateCallback = async (
45
+ uri: URL,
46
+ variables: Variables
47
+ ): Promise<{ contents: FormattedResultItem[] }> => {
48
+ const encodedPath = variables.path as string;
49
+ // Decode the path received from the URI variable
50
+ const decodedPath = decodeURIComponent(encodedPath || '');
51
+ // Use the builder to create the suffix, which will re-encode the path correctly
52
+ const pathUriSuffix = buildPathItemUriSuffix(decodedPath);
53
+ const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
54
+ let resultItems: RenderResultItem[];
55
+
56
+ try {
57
+ const spec = await this.specLoader.getTransformedSpec({
58
+ resourceType: 'schema', // Use 'schema' for now
59
+ format: 'openapi',
60
+ });
61
+
62
+ // Use imported type guard
63
+ if (!isOpenAPIV3(spec)) {
64
+ throw new Error('Only OpenAPI v3 specifications are supported');
65
+ }
66
+
67
+ // --- Use helper to get validated path item ---
68
+ const lookupPath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
69
+ const pathItemObj = getValidatedPathItem(spec, lookupPath);
70
+
71
+ // Instantiate RenderablePathItem with the validated pathItemObj
72
+ const renderablePathItem = new RenderablePathItem(
73
+ pathItemObj, // pathItemObj retrieved safely via helper
74
+ lookupPath, // Pass the raw, decoded path
75
+ pathUriSuffix // Pass the correctly built suffix
76
+ );
77
+ resultItems = renderablePathItem.renderList(context);
78
+ } catch (error: unknown) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ console.error(`Error handling request ${uri.href}: ${message}`);
81
+ resultItems = createErrorResult(pathUriSuffix, message);
82
+ }
83
+
84
+ // Use imported formatResults
85
+ const contents = formatResults(context, resultItems);
86
+ return { contents };
87
+ };
88
+ }
@@ -0,0 +1,92 @@
1
+ import {
2
+ ReadResourceTemplateCallback,
3
+ ResourceTemplate,
4
+ } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
6
+ // ResourceContents is the base type for a single item, not the array type needed here.
7
+ // We'll define the array structure inline based on TextResourceContentsSchema.
8
+
9
+ import { SpecLoaderService } from '../types.js';
10
+ import { IFormatter } from '../services/formatters.js';
11
+ import { RenderableDocument } from '../rendering/document.js';
12
+ import { RenderablePaths } from '../rendering/paths.js';
13
+ import { RenderableComponents } from '../rendering/components.js';
14
+ import { RenderContext, RenderResultItem } from '../rendering/types.js';
15
+ import { createErrorResult } from '../rendering/utils.js';
16
+ // Import shared handler utils
17
+ import { formatResults, isOpenAPIV3, FormattedResultItem } from './handler-utils.js'; // Already has .js
18
+
19
+ const BASE_URI = 'openapi://';
20
+
21
+ // Removed duplicated FormattedResultItem type - now imported from handler-utils
22
+ // Removed duplicated formatResults function - now imported from handler-utils
23
+
24
+ /**
25
+ * Handles requests for top-level OpenAPI fields (info, servers, paths list, components list).
26
+ * Corresponds to the `openapi://{field}` template.
27
+ */
28
+ export class TopLevelFieldHandler {
29
+ constructor(
30
+ private specLoader: SpecLoaderService,
31
+ private formatter: IFormatter
32
+ ) {}
33
+
34
+ getTemplate(): ResourceTemplate {
35
+ // TODO: Add completion logic if needed
36
+ return new ResourceTemplate(`${BASE_URI}{field}`, {
37
+ list: undefined,
38
+ complete: undefined,
39
+ });
40
+ }
41
+
42
+ handleRequest: ReadResourceTemplateCallback = async (
43
+ uri: URL,
44
+ variables: Variables
45
+ // matchedTemplate is not needed if we only handle one template
46
+ ): Promise<{ contents: FormattedResultItem[] }> => {
47
+ // Return type uses the defined array structure
48
+ const field = variables.field as string;
49
+ const context: RenderContext = { formatter: this.formatter, baseUri: BASE_URI };
50
+ let resultItems: RenderResultItem[];
51
+
52
+ try {
53
+ const spec = await this.specLoader.getTransformedSpec({
54
+ // Use 'schema' as placeholder resourceType for transformation context
55
+ resourceType: 'schema',
56
+ format: 'openapi',
57
+ });
58
+
59
+ // Use imported type guard
60
+ if (!isOpenAPIV3(spec)) {
61
+ throw new Error('Only OpenAPI v3 specifications are supported');
62
+ }
63
+
64
+ const renderableDoc = new RenderableDocument(spec);
65
+
66
+ // Route based on the field name
67
+ if (field === 'paths') {
68
+ const pathsObj = renderableDoc.getPathsObject();
69
+ resultItems = new RenderablePaths(pathsObj).renderList(context);
70
+ } else if (field === 'components') {
71
+ const componentsObj = renderableDoc.getComponentsObject();
72
+ resultItems = new RenderableComponents(componentsObj).renderList(context);
73
+ } else {
74
+ // Handle other top-level fields (info, servers, tags, etc.)
75
+ const fieldObject = renderableDoc.getTopLevelField(field);
76
+ resultItems = renderableDoc.renderTopLevelFieldDetail(context, fieldObject, field);
77
+ }
78
+ } catch (error: unknown) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ console.error(`Error handling request ${uri.href}: ${message}`);
81
+ resultItems = createErrorResult(field, message); // Use field as uriSuffix for error
82
+ }
83
+
84
+ // Format results into the final structure
85
+ const contents: FormattedResultItem[] = formatResults(context, resultItems);
86
+ // Return the object with the correctly typed contents array
87
+ // Use imported formatResults
88
+ return { contents };
89
+ };
90
+
91
+ // Removed duplicated isOpenAPIV3 type guard - now imported from handler-utils
92
+ } // Ensure class closing brace is present