mcp-openapi-schema-explorer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.devcontainer/devcontainer.json +24 -0
  2. package/.github/dependabot.yml +13 -0
  3. package/.github/workflows/ci.yml +111 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.prettierignore +3 -0
  6. package/.prettierrc.json +12 -0
  7. package/.releaserc.json +23 -0
  8. package/CHANGELOG.md +32 -0
  9. package/CONTRIBUTING.md +67 -0
  10. package/Dockerfile +3 -0
  11. package/LICENSE +21 -0
  12. package/README.md +127 -0
  13. package/dist/src/config.d.ts +15 -0
  14. package/dist/src/config.js +19 -0
  15. package/dist/src/config.js.map +1 -0
  16. package/dist/src/handlers/component-detail-handler.d.ts +14 -0
  17. package/dist/src/handlers/component-detail-handler.js +87 -0
  18. package/dist/src/handlers/component-detail-handler.js.map +1 -0
  19. package/dist/src/handlers/component-map-handler.d.ts +14 -0
  20. package/dist/src/handlers/component-map-handler.js +63 -0
  21. package/dist/src/handlers/component-map-handler.js.map +1 -0
  22. package/dist/src/handlers/handler-utils.d.ts +69 -0
  23. package/dist/src/handlers/handler-utils.js +180 -0
  24. package/dist/src/handlers/handler-utils.js.map +1 -0
  25. package/dist/src/handlers/operation-handler.d.ts +14 -0
  26. package/dist/src/handlers/operation-handler.js +86 -0
  27. package/dist/src/handlers/operation-handler.js.map +1 -0
  28. package/dist/src/handlers/path-item-handler.d.ts +14 -0
  29. package/dist/src/handlers/path-item-handler.js +66 -0
  30. package/dist/src/handlers/path-item-handler.js.map +1 -0
  31. package/dist/src/handlers/top-level-field-handler.d.ts +14 -0
  32. package/dist/src/handlers/top-level-field-handler.js +72 -0
  33. package/dist/src/handlers/top-level-field-handler.js.map +1 -0
  34. package/dist/src/index.d.ts +2 -0
  35. package/dist/src/index.js +177 -0
  36. package/dist/src/index.js.map +1 -0
  37. package/dist/src/rendering/components.d.ts +67 -0
  38. package/dist/src/rendering/components.js +177 -0
  39. package/dist/src/rendering/components.js.map +1 -0
  40. package/dist/src/rendering/document.d.ts +36 -0
  41. package/dist/src/rendering/document.js +147 -0
  42. package/dist/src/rendering/document.js.map +1 -0
  43. package/dist/src/rendering/path-item.d.ts +45 -0
  44. package/dist/src/rendering/path-item.js +141 -0
  45. package/dist/src/rendering/path-item.js.map +1 -0
  46. package/dist/src/rendering/paths.d.ts +26 -0
  47. package/dist/src/rendering/paths.js +78 -0
  48. package/dist/src/rendering/paths.js.map +1 -0
  49. package/dist/src/rendering/types.d.ts +50 -0
  50. package/dist/src/rendering/types.js +12 -0
  51. package/dist/src/rendering/types.js.map +1 -0
  52. package/dist/src/rendering/utils.d.ts +31 -0
  53. package/dist/src/rendering/utils.js +79 -0
  54. package/dist/src/rendering/utils.js.map +1 -0
  55. package/dist/src/services/formatters.d.ts +36 -0
  56. package/dist/src/services/formatters.js +52 -0
  57. package/dist/src/services/formatters.js.map +1 -0
  58. package/dist/src/services/reference-transform.d.ts +27 -0
  59. package/dist/src/services/reference-transform.js +75 -0
  60. package/dist/src/services/reference-transform.js.map +1 -0
  61. package/dist/src/services/spec-loader.d.ts +27 -0
  62. package/dist/src/services/spec-loader.js +77 -0
  63. package/dist/src/services/spec-loader.js.map +1 -0
  64. package/dist/src/types.d.ts +11 -0
  65. package/dist/src/types.js +2 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/dist/src/utils/uri-builder.d.ts +81 -0
  68. package/dist/src/utils/uri-builder.js +121 -0
  69. package/dist/src/utils/uri-builder.js.map +1 -0
  70. package/dist/src/version.d.ts +1 -0
  71. package/dist/src/version.js +4 -0
  72. package/dist/src/version.js.map +1 -0
  73. package/eslint.config.js +88 -0
  74. package/jest.config.js +32 -0
  75. package/justfile +66 -0
  76. package/memory-bank/activeContext.md +139 -0
  77. package/memory-bank/productContext.md +39 -0
  78. package/memory-bank/progress.md +141 -0
  79. package/memory-bank/projectbrief.md +50 -0
  80. package/memory-bank/systemPatterns.md +224 -0
  81. package/memory-bank/techContext.md +131 -0
  82. package/package.json +76 -0
  83. package/scripts/generate-version.js +49 -0
  84. package/src/config.ts +33 -0
  85. package/src/handlers/component-detail-handler.ts +121 -0
  86. package/src/handlers/component-map-handler.ts +92 -0
  87. package/src/handlers/handler-utils.ts +230 -0
  88. package/src/handlers/operation-handler.ts +114 -0
  89. package/src/handlers/path-item-handler.ts +88 -0
  90. package/src/handlers/top-level-field-handler.ts +92 -0
  91. package/src/index.ts +222 -0
  92. package/src/rendering/components.ts +228 -0
  93. package/src/rendering/document.ts +167 -0
  94. package/src/rendering/path-item.ts +157 -0
  95. package/src/rendering/paths.ts +87 -0
  96. package/src/rendering/types.ts +63 -0
  97. package/src/rendering/utils.ts +107 -0
  98. package/src/services/formatters.ts +71 -0
  99. package/src/services/reference-transform.ts +105 -0
  100. package/src/services/spec-loader.ts +88 -0
  101. package/src/types.ts +17 -0
  102. package/src/utils/uri-builder.ts +134 -0
  103. package/src/version.ts +4 -0
  104. package/test/__tests__/e2e/format.test.ts +224 -0
  105. package/test/__tests__/e2e/resources.test.ts +369 -0
  106. package/test/__tests__/e2e/spec-loading.test.ts +172 -0
  107. package/test/__tests__/unit/config.test.ts +39 -0
  108. package/test/__tests__/unit/handlers/component-detail-handler.test.ts +241 -0
  109. package/test/__tests__/unit/handlers/component-map-handler.test.ts +187 -0
  110. package/test/__tests__/unit/handlers/handler-utils.test.ts +255 -0
  111. package/test/__tests__/unit/handlers/operation-handler.test.ts +202 -0
  112. package/test/__tests__/unit/handlers/path-item-handler.test.ts +153 -0
  113. package/test/__tests__/unit/handlers/top-level-field-handler.test.ts +182 -0
  114. package/test/__tests__/unit/rendering/components.test.ts +269 -0
  115. package/test/__tests__/unit/rendering/document.test.ts +172 -0
  116. package/test/__tests__/unit/rendering/path-item.test.ts +197 -0
  117. package/test/__tests__/unit/rendering/paths.test.ts +115 -0
  118. package/test/__tests__/unit/services/formatters.test.ts +109 -0
  119. package/test/__tests__/unit/services/reference-transform.test.ts +320 -0
  120. package/test/__tests__/unit/services/spec-loader.test.ts +214 -0
  121. package/test/__tests__/unit/utils/uri-builder.test.ts +103 -0
  122. package/test/fixtures/complex-endpoint.json +146 -0
  123. package/test/fixtures/empty-api.json +8 -0
  124. package/test/fixtures/multi-component-types.json +55 -0
  125. package/test/fixtures/paths-test.json +61 -0
  126. package/test/fixtures/sample-api.json +68 -0
  127. package/test/fixtures/sample-v2-api.json +39 -0
  128. package/test/setup.ts +32 -0
  129. package/test/utils/console-helpers.ts +48 -0
  130. package/test/utils/mcp-test-helpers.ts +66 -0
  131. package/test/utils/test-types.ts +54 -0
  132. package/tsconfig.json +25 -0
  133. package/tsconfig.test.json +5 -0
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { OpenAPI } from 'openapi-types';
2
+ import type { TransformContext } from './services/reference-transform.js';
3
+
4
+ /** Common HTTP methods used in OpenAPI specs */
5
+ export type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'patch';
6
+
7
+ /** Interface for spec loader */
8
+ export interface SpecLoaderService {
9
+ getSpec(): Promise<OpenAPI.Document>;
10
+ getTransformedSpec(context: TransformContext): Promise<OpenAPI.Document>;
11
+ }
12
+
13
+ // Re-export transform types
14
+ export type { TransformContext };
15
+
16
+ // Re-export OpenAPI types
17
+ export type { OpenAPI };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Utility functions for building standardized MCP URIs for this server.
3
+ */
4
+
5
+ const BASE_URI_SCHEME = 'openapi://';
6
+
7
+ /**
8
+ * Encodes a string component for safe inclusion in a URI path segment.
9
+ * Uses standard encodeURIComponent.
10
+ * Encodes a path string for safe inclusion in a URI.
11
+ * This specifically targets path strings which might contain characters
12
+ * like '{', '}', etc., that need encoding when forming the URI path part.
13
+ * Uses standard encodeURIComponent.
14
+ * Encodes a path string for safe inclusion in a URI path segment.
15
+ * This is necessary because the path segment comes from the user potentially
16
+ * containing characters that need encoding (like '{', '}').
17
+ * Uses standard encodeURIComponent.
18
+ * @param path The path string to encode.
19
+ * @returns The encoded path string, with leading slashes removed before encoding.
20
+ */
21
+ export function encodeUriPathComponent(path: string): string {
22
+ // Added export
23
+ // Remove leading slashes before encoding
24
+ const pathWithoutLeadingSlash = path.replace(/^\/+/, '');
25
+ return encodeURIComponent(pathWithoutLeadingSlash);
26
+ }
27
+
28
+ // --- Full URI Builders ---
29
+
30
+ /**
31
+ * Builds the URI for accessing a specific component's details.
32
+ * Example: openapi://components/schemas/MySchema
33
+ * @param type The component type (e.g., 'schemas', 'responses').
34
+ * @param name The component name.
35
+ * @returns The full component detail URI.
36
+ */
37
+ export function buildComponentDetailUri(type: string, name: string): string {
38
+ // Per user instruction, do not encode type or name here.
39
+ return `${BASE_URI_SCHEME}components/${type}/${name}`;
40
+ }
41
+
42
+ /**
43
+ * Builds the URI for listing components of a specific type.
44
+ * Example: openapi://components/schemas
45
+ * @param type The component type (e.g., 'schemas', 'responses').
46
+ * @returns The full component map URI.
47
+ */
48
+ export function buildComponentMapUri(type: string): string {
49
+ // Per user instruction, do not encode type here.
50
+ return `${BASE_URI_SCHEME}components/${type}`;
51
+ }
52
+
53
+ /**
54
+ * Builds the URI for accessing a specific operation's details.
55
+ * Example: openapi://paths/users/{userId}/GET
56
+ * @param path The API path (e.g., '/users/{userId}').
57
+ * @param method The HTTP method (e.g., 'GET', 'POST').
58
+ * @returns The full operation detail URI.
59
+ */
60
+ export function buildOperationUri(path: string, method: string): string {
61
+ // Encode only the path component. Assume 'path' is raw/decoded.
62
+ // Method is assumed to be safe or handled by SDK/client.
63
+ return `${BASE_URI_SCHEME}paths/${encodeUriPathComponent(path)}/${method.toLowerCase()}`; // Standardize method to lowercase
64
+ }
65
+
66
+ /**
67
+ * Builds the URI for listing methods available at a specific path.
68
+ * Example: openapi://paths/users/{userId}
69
+ * @param path The API path (e.g., '/users/{userId}').
70
+ * @returns The full path item URI.
71
+ */
72
+ export function buildPathItemUri(path: string): string {
73
+ // Encode only the path component. Assume 'path' is raw/decoded.
74
+ return `${BASE_URI_SCHEME}paths/${encodeUriPathComponent(path)}`;
75
+ }
76
+
77
+ /**
78
+ * Builds the URI for accessing a top-level field (like 'info' or 'servers')
79
+ * or triggering a list view ('paths', 'components').
80
+ * Example: openapi://info, openapi://paths
81
+ * @param field The top-level field name.
82
+ * @returns The full top-level field URI.
83
+ */
84
+ export function buildTopLevelFieldUri(field: string): string {
85
+ // Per user instruction, do not encode field here.
86
+ return `${BASE_URI_SCHEME}${field}`;
87
+ }
88
+
89
+ // --- URI Suffix Builders (for RenderResultItem) ---
90
+
91
+ /**
92
+ * Builds the URI suffix for a specific component's details.
93
+ * Example: components/schemas/MySchema
94
+ */
95
+ export function buildComponentDetailUriSuffix(type: string, name: string): string {
96
+ // Per user instruction, do not encode type or name here.
97
+ return `components/${type}/${name}`;
98
+ }
99
+
100
+ /**
101
+ * Builds the URI suffix for listing components of a specific type.
102
+ * Example: components/schemas
103
+ */
104
+ export function buildComponentMapUriSuffix(type: string): string {
105
+ // Per user instruction, do not encode type here.
106
+ return `components/${type}`;
107
+ }
108
+
109
+ /**
110
+ * Builds the URI suffix for a specific operation's details.
111
+ * Example: paths/users/{userId}/get
112
+ */
113
+ export function buildOperationUriSuffix(path: string, method: string): string {
114
+ // Encode only the path component for the suffix. Assume 'path' is raw/decoded.
115
+ return `paths/${encodeUriPathComponent(path)}/${method.toLowerCase()}`;
116
+ }
117
+
118
+ /**
119
+ * Builds the URI suffix for listing methods available at a specific path.
120
+ * Example: paths/users/{userId}
121
+ */
122
+ export function buildPathItemUriSuffix(path: string): string {
123
+ // Encode only the path component for the suffix. Assume 'path' is raw/decoded.
124
+ return `paths/${encodeUriPathComponent(path)}`;
125
+ }
126
+
127
+ /**
128
+ * Builds the URI suffix for a top-level field.
129
+ * Example: info, paths
130
+ */
131
+ export function buildTopLevelFieldUriSuffix(field: string): string {
132
+ // Per user instruction, do not encode field here.
133
+ return field;
134
+ }
package/src/version.ts ADDED
@@ -0,0 +1,4 @@
1
+ // Auto-generated by scripts/generate-version.js during semantic-release prepare step
2
+ // Do not edit this file manually.
3
+
4
+ export const VERSION = '1.0.0';
@@ -0,0 +1,224 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { startMcpServer, McpTestContext } from '../../utils/mcp-test-helpers.js'; // Import McpTestContext
3
+ import { load as yamlLoad } from 'js-yaml';
4
+ // Remove old test types/guards if not needed, or adapt them
5
+ // import { isEndpointErrorResponse } from '../../utils/test-types.js';
6
+ // import type { EndpointResponse, ResourceResponse } from '../../utils/test-types.js';
7
+ // Import specific SDK types needed
8
+ import { ReadResourceResult, TextResourceContents } from '@modelcontextprotocol/sdk/types.js';
9
+ // Generic type guard for simple object check
10
+ function isObject(obj: unknown): obj is Record<string, unknown> {
11
+ return typeof obj === 'object' && obj !== null;
12
+ }
13
+
14
+ // Type guard to check if content is TextResourceContents
15
+ function hasTextContent(
16
+ content: ReadResourceResult['contents'][0]
17
+ ): content is TextResourceContents {
18
+ // Check for the 'text' property specifically and ensure it's not undefined
19
+ return content && typeof (content as TextResourceContents).text === 'string';
20
+ }
21
+
22
+ function parseJson(text: string | undefined): unknown {
23
+ if (text === undefined) throw new Error('Cannot parse undefined text');
24
+ return JSON.parse(text);
25
+ }
26
+
27
+ function parseYaml(text: string | undefined): unknown {
28
+ if (text === undefined) throw new Error('Cannot parse undefined text');
29
+ const result = yamlLoad(text);
30
+ if (result === undefined) {
31
+ throw new Error('Invalid YAML: parsing resulted in undefined');
32
+ }
33
+ return result;
34
+ }
35
+
36
+ function safeParse(text: string | undefined, format: 'json' | 'yaml'): unknown {
37
+ try {
38
+ return format === 'json' ? parseJson(text) : parseYaml(text);
39
+ } catch (error) {
40
+ throw new Error(
41
+ `Failed to parse ${format} content: ${error instanceof Error ? error.message : String(error)}`
42
+ );
43
+ }
44
+ }
45
+
46
+ // Removed old parseEndpointResponse
47
+
48
+ describe('Output Format E2E', () => {
49
+ let testContext: McpTestContext;
50
+ let client: Client;
51
+
52
+ afterEach(async () => {
53
+ await testContext?.cleanup();
54
+ });
55
+
56
+ describe('JSON format (default)', () => {
57
+ beforeEach(async () => {
58
+ testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
59
+ outputFormat: 'json',
60
+ });
61
+ client = testContext.client;
62
+ });
63
+
64
+ it('should return JSON for openapi://info', async () => {
65
+ const result = await client.readResource({ uri: 'openapi://info' });
66
+ expect(result.contents).toHaveLength(1);
67
+ const content = result.contents[0];
68
+ expect(content.mimeType).toBe('application/json');
69
+ if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
70
+ expect(() => safeParse(content.text, 'json')).not.toThrow();
71
+ const data = safeParse(content.text, 'json');
72
+ expect(isObject(data) && data['title']).toBe('Complex Endpoint Test API'); // Use bracket notation after guard
73
+ });
74
+
75
+ it('should return JSON for operation detail', async () => {
76
+ const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
77
+ const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
78
+ expect(result.contents).toHaveLength(1);
79
+ const content = result.contents[0];
80
+ expect(content.mimeType).toBe('application/json');
81
+ if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
82
+ expect(() => safeParse(content.text, 'json')).not.toThrow();
83
+ const data = safeParse(content.text, 'json');
84
+ expect(isObject(data) && data['operationId']).toBe('getProjectTasks'); // Use bracket notation after guard
85
+ });
86
+
87
+ it('should return JSON for component detail', async () => {
88
+ const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
89
+ expect(result.contents).toHaveLength(1);
90
+ const content = result.contents[0];
91
+ expect(content.mimeType).toBe('application/json');
92
+ if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
93
+ expect(() => safeParse(content.text, 'json')).not.toThrow();
94
+ const data = safeParse(content.text, 'json');
95
+ expect(isObject(data) && data['type']).toBe('object'); // Use bracket notation after guard
96
+ expect(
97
+ isObject(data) &&
98
+ isObject(data['properties']) &&
99
+ isObject(data['properties']['id']) &&
100
+ data['properties']['id']['type']
101
+ ).toBe('string'); // Use bracket notation with type checking
102
+ });
103
+ });
104
+
105
+ describe('YAML format', () => {
106
+ beforeEach(async () => {
107
+ testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
108
+ outputFormat: 'yaml',
109
+ });
110
+ client = testContext.client;
111
+ });
112
+
113
+ it('should return YAML for openapi://info', async () => {
114
+ const result = await client.readResource({ uri: 'openapi://info' });
115
+ expect(result.contents).toHaveLength(1);
116
+ const content = result.contents[0];
117
+ expect(content.mimeType).toBe('text/yaml');
118
+ if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
119
+ expect(() => safeParse(content.text, 'yaml')).not.toThrow();
120
+ expect(content.text).toContain('title: Complex Endpoint Test API');
121
+ expect(content.text).toMatch(/\n$/);
122
+ });
123
+
124
+ it('should return YAML for operation detail', async () => {
125
+ const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
126
+ const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
127
+ expect(result.contents).toHaveLength(1);
128
+ const content = result.contents[0];
129
+ expect(content.mimeType).toBe('text/yaml');
130
+ if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
131
+ expect(() => safeParse(content.text, 'yaml')).not.toThrow();
132
+ expect(content.text).toContain('operationId: getProjectTasks');
133
+ expect(content.text).toMatch(/\n$/);
134
+ });
135
+
136
+ it('should return YAML for component detail', async () => {
137
+ const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
138
+ expect(result.contents).toHaveLength(1);
139
+ const content = result.contents[0];
140
+ expect(content.mimeType).toBe('text/yaml');
141
+ if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
142
+ expect(() => safeParse(content.text, 'yaml')).not.toThrow();
143
+ expect(content.text).toContain('type: object');
144
+ expect(content.text).toContain('properties:');
145
+ expect(content.text).toContain('id:');
146
+ expect(content.text).toMatch(/\n$/);
147
+ });
148
+
149
+ // Note: The test for listResourceTemplates is removed as it tested old template structure.
150
+ // We could add a new test here if needed, but the mimeType for templates isn't explicitly set anymore.
151
+
152
+ it('should handle errors in YAML format (e.g., invalid component name)', async () => {
153
+ const result = await client.readResource({ uri: 'openapi://components/schemas/InvalidName' });
154
+ expect(result.contents).toHaveLength(1);
155
+ const content = result.contents[0];
156
+ // Errors are always text/plain, regardless of configured output format
157
+ expect(content.mimeType).toBe('text/plain');
158
+ expect(content.isError).toBe(true);
159
+ if (!hasTextContent(content)) throw new Error('Expected text');
160
+ // Updated error message from getValidatedComponentDetails with sorted names
161
+ expect(content.text).toContain(
162
+ 'None of the requested names (InvalidName) are valid for component type "schemas". Available names: CreateTaskRequest, Task, TaskList'
163
+ );
164
+ });
165
+ });
166
+
167
+ describe('Minified JSON format', () => {
168
+ beforeEach(async () => {
169
+ testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
170
+ outputFormat: 'json-minified',
171
+ });
172
+ client = testContext.client;
173
+ });
174
+
175
+ it('should return minified JSON for openapi://info', async () => {
176
+ const result = await client.readResource({ uri: 'openapi://info' });
177
+ expect(result.contents).toHaveLength(1);
178
+ const content = result.contents[0];
179
+ expect(content.mimeType).toBe('application/json');
180
+ if (!hasTextContent(content)) throw new Error('Expected text content');
181
+ expect(() => safeParse(content.text, 'json')).not.toThrow();
182
+ const data = safeParse(content.text, 'json');
183
+ expect(isObject(data) && data['title']).toBe('Complex Endpoint Test API');
184
+ // Check for lack of pretty-printing whitespace
185
+ expect(content.text).not.toContain('\n ');
186
+ expect(content.text).not.toContain(' '); // Double check no indentation
187
+ });
188
+
189
+ it('should return minified JSON for operation detail', async () => {
190
+ const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
191
+ const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
192
+ expect(result.contents).toHaveLength(1);
193
+ const content = result.contents[0];
194
+ expect(content.mimeType).toBe('application/json');
195
+ if (!hasTextContent(content)) throw new Error('Expected text content');
196
+ expect(() => safeParse(content.text, 'json')).not.toThrow();
197
+ const data = safeParse(content.text, 'json');
198
+ expect(isObject(data) && data['operationId']).toBe('getProjectTasks');
199
+ // Check for lack of pretty-printing whitespace
200
+ expect(content.text).not.toContain('\n ');
201
+ expect(content.text).not.toContain(' ');
202
+ });
203
+
204
+ it('should return minified JSON for component detail', async () => {
205
+ const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
206
+ expect(result.contents).toHaveLength(1);
207
+ const content = result.contents[0];
208
+ expect(content.mimeType).toBe('application/json');
209
+ if (!hasTextContent(content)) throw new Error('Expected text content');
210
+ expect(() => safeParse(content.text, 'json')).not.toThrow();
211
+ const data = safeParse(content.text, 'json');
212
+ expect(isObject(data) && data['type']).toBe('object');
213
+ expect(
214
+ isObject(data) &&
215
+ isObject(data['properties']) &&
216
+ isObject(data['properties']['id']) &&
217
+ data['properties']['id']['type']
218
+ ).toBe('string');
219
+ // Check for lack of pretty-printing whitespace
220
+ expect(content.text).not.toContain('\n ');
221
+ expect(content.text).not.toContain(' ');
222
+ });
223
+ });
224
+ });