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.
- package/.devcontainer/devcontainer.json +24 -0
- package/.github/dependabot.yml +13 -0
- package/.github/workflows/ci.yml +111 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +12 -0
- package/.releaserc.json +23 -0
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +67 -0
- package/Dockerfile +3 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +19 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/handlers/component-detail-handler.d.ts +14 -0
- package/dist/src/handlers/component-detail-handler.js +87 -0
- package/dist/src/handlers/component-detail-handler.js.map +1 -0
- package/dist/src/handlers/component-map-handler.d.ts +14 -0
- package/dist/src/handlers/component-map-handler.js +63 -0
- package/dist/src/handlers/component-map-handler.js.map +1 -0
- package/dist/src/handlers/handler-utils.d.ts +69 -0
- package/dist/src/handlers/handler-utils.js +180 -0
- package/dist/src/handlers/handler-utils.js.map +1 -0
- package/dist/src/handlers/operation-handler.d.ts +14 -0
- package/dist/src/handlers/operation-handler.js +86 -0
- package/dist/src/handlers/operation-handler.js.map +1 -0
- package/dist/src/handlers/path-item-handler.d.ts +14 -0
- package/dist/src/handlers/path-item-handler.js +66 -0
- package/dist/src/handlers/path-item-handler.js.map +1 -0
- package/dist/src/handlers/top-level-field-handler.d.ts +14 -0
- package/dist/src/handlers/top-level-field-handler.js +72 -0
- package/dist/src/handlers/top-level-field-handler.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +177 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/rendering/components.d.ts +67 -0
- package/dist/src/rendering/components.js +177 -0
- package/dist/src/rendering/components.js.map +1 -0
- package/dist/src/rendering/document.d.ts +36 -0
- package/dist/src/rendering/document.js +147 -0
- package/dist/src/rendering/document.js.map +1 -0
- package/dist/src/rendering/path-item.d.ts +45 -0
- package/dist/src/rendering/path-item.js +141 -0
- package/dist/src/rendering/path-item.js.map +1 -0
- package/dist/src/rendering/paths.d.ts +26 -0
- package/dist/src/rendering/paths.js +78 -0
- package/dist/src/rendering/paths.js.map +1 -0
- package/dist/src/rendering/types.d.ts +50 -0
- package/dist/src/rendering/types.js +12 -0
- package/dist/src/rendering/types.js.map +1 -0
- package/dist/src/rendering/utils.d.ts +31 -0
- package/dist/src/rendering/utils.js +79 -0
- package/dist/src/rendering/utils.js.map +1 -0
- package/dist/src/services/formatters.d.ts +36 -0
- package/dist/src/services/formatters.js +52 -0
- package/dist/src/services/formatters.js.map +1 -0
- package/dist/src/services/reference-transform.d.ts +27 -0
- package/dist/src/services/reference-transform.js +75 -0
- package/dist/src/services/reference-transform.js.map +1 -0
- package/dist/src/services/spec-loader.d.ts +27 -0
- package/dist/src/services/spec-loader.js +77 -0
- package/dist/src/services/spec-loader.js.map +1 -0
- package/dist/src/types.d.ts +11 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/uri-builder.d.ts +81 -0
- package/dist/src/utils/uri-builder.js +121 -0
- package/dist/src/utils/uri-builder.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +4 -0
- package/dist/src/version.js.map +1 -0
- package/eslint.config.js +88 -0
- package/jest.config.js +32 -0
- package/justfile +66 -0
- package/memory-bank/activeContext.md +139 -0
- package/memory-bank/productContext.md +39 -0
- package/memory-bank/progress.md +141 -0
- package/memory-bank/projectbrief.md +50 -0
- package/memory-bank/systemPatterns.md +224 -0
- package/memory-bank/techContext.md +131 -0
- package/package.json +76 -0
- package/scripts/generate-version.js +49 -0
- package/src/config.ts +33 -0
- package/src/handlers/component-detail-handler.ts +121 -0
- package/src/handlers/component-map-handler.ts +92 -0
- package/src/handlers/handler-utils.ts +230 -0
- package/src/handlers/operation-handler.ts +114 -0
- package/src/handlers/path-item-handler.ts +88 -0
- package/src/handlers/top-level-field-handler.ts +92 -0
- package/src/index.ts +222 -0
- package/src/rendering/components.ts +228 -0
- package/src/rendering/document.ts +167 -0
- package/src/rendering/path-item.ts +157 -0
- package/src/rendering/paths.ts +87 -0
- package/src/rendering/types.ts +63 -0
- package/src/rendering/utils.ts +107 -0
- package/src/services/formatters.ts +71 -0
- package/src/services/reference-transform.ts +105 -0
- package/src/services/spec-loader.ts +88 -0
- package/src/types.ts +17 -0
- package/src/utils/uri-builder.ts +134 -0
- package/src/version.ts +4 -0
- package/test/__tests__/e2e/format.test.ts +224 -0
- package/test/__tests__/e2e/resources.test.ts +369 -0
- package/test/__tests__/e2e/spec-loading.test.ts +172 -0
- package/test/__tests__/unit/config.test.ts +39 -0
- package/test/__tests__/unit/handlers/component-detail-handler.test.ts +241 -0
- package/test/__tests__/unit/handlers/component-map-handler.test.ts +187 -0
- package/test/__tests__/unit/handlers/handler-utils.test.ts +255 -0
- package/test/__tests__/unit/handlers/operation-handler.test.ts +202 -0
- package/test/__tests__/unit/handlers/path-item-handler.test.ts +153 -0
- package/test/__tests__/unit/handlers/top-level-field-handler.test.ts +182 -0
- package/test/__tests__/unit/rendering/components.test.ts +269 -0
- package/test/__tests__/unit/rendering/document.test.ts +172 -0
- package/test/__tests__/unit/rendering/path-item.test.ts +197 -0
- package/test/__tests__/unit/rendering/paths.test.ts +115 -0
- package/test/__tests__/unit/services/formatters.test.ts +109 -0
- package/test/__tests__/unit/services/reference-transform.test.ts +320 -0
- package/test/__tests__/unit/services/spec-loader.test.ts +214 -0
- package/test/__tests__/unit/utils/uri-builder.test.ts +103 -0
- package/test/fixtures/complex-endpoint.json +146 -0
- package/test/fixtures/empty-api.json +8 -0
- package/test/fixtures/multi-component-types.json +55 -0
- package/test/fixtures/paths-test.json +61 -0
- package/test/fixtures/sample-api.json +68 -0
- package/test/fixtures/sample-v2-api.json +39 -0
- package/test/setup.ts +32 -0
- package/test/utils/console-helpers.ts +48 -0
- package/test/utils/mcp-test-helpers.ts +66 -0
- package/test/utils/test-types.ts +54 -0
- package/tsconfig.json +25 -0
- 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
|