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