mcp-openapi-schema-explorer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.devcontainer/devcontainer.json +24 -0
  2. package/.github/dependabot.yml +13 -0
  3. package/.github/workflows/ci.yml +111 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.prettierignore +3 -0
  6. package/.prettierrc.json +12 -0
  7. package/.releaserc.json +23 -0
  8. package/CHANGELOG.md +32 -0
  9. package/CONTRIBUTING.md +67 -0
  10. package/Dockerfile +3 -0
  11. package/LICENSE +21 -0
  12. package/README.md +127 -0
  13. package/dist/src/config.d.ts +15 -0
  14. package/dist/src/config.js +19 -0
  15. package/dist/src/config.js.map +1 -0
  16. package/dist/src/handlers/component-detail-handler.d.ts +14 -0
  17. package/dist/src/handlers/component-detail-handler.js +87 -0
  18. package/dist/src/handlers/component-detail-handler.js.map +1 -0
  19. package/dist/src/handlers/component-map-handler.d.ts +14 -0
  20. package/dist/src/handlers/component-map-handler.js +63 -0
  21. package/dist/src/handlers/component-map-handler.js.map +1 -0
  22. package/dist/src/handlers/handler-utils.d.ts +69 -0
  23. package/dist/src/handlers/handler-utils.js +180 -0
  24. package/dist/src/handlers/handler-utils.js.map +1 -0
  25. package/dist/src/handlers/operation-handler.d.ts +14 -0
  26. package/dist/src/handlers/operation-handler.js +86 -0
  27. package/dist/src/handlers/operation-handler.js.map +1 -0
  28. package/dist/src/handlers/path-item-handler.d.ts +14 -0
  29. package/dist/src/handlers/path-item-handler.js +66 -0
  30. package/dist/src/handlers/path-item-handler.js.map +1 -0
  31. package/dist/src/handlers/top-level-field-handler.d.ts +14 -0
  32. package/dist/src/handlers/top-level-field-handler.js +72 -0
  33. package/dist/src/handlers/top-level-field-handler.js.map +1 -0
  34. package/dist/src/index.d.ts +2 -0
  35. package/dist/src/index.js +177 -0
  36. package/dist/src/index.js.map +1 -0
  37. package/dist/src/rendering/components.d.ts +67 -0
  38. package/dist/src/rendering/components.js +177 -0
  39. package/dist/src/rendering/components.js.map +1 -0
  40. package/dist/src/rendering/document.d.ts +36 -0
  41. package/dist/src/rendering/document.js +147 -0
  42. package/dist/src/rendering/document.js.map +1 -0
  43. package/dist/src/rendering/path-item.d.ts +45 -0
  44. package/dist/src/rendering/path-item.js +141 -0
  45. package/dist/src/rendering/path-item.js.map +1 -0
  46. package/dist/src/rendering/paths.d.ts +26 -0
  47. package/dist/src/rendering/paths.js +78 -0
  48. package/dist/src/rendering/paths.js.map +1 -0
  49. package/dist/src/rendering/types.d.ts +50 -0
  50. package/dist/src/rendering/types.js +12 -0
  51. package/dist/src/rendering/types.js.map +1 -0
  52. package/dist/src/rendering/utils.d.ts +31 -0
  53. package/dist/src/rendering/utils.js +79 -0
  54. package/dist/src/rendering/utils.js.map +1 -0
  55. package/dist/src/services/formatters.d.ts +36 -0
  56. package/dist/src/services/formatters.js +52 -0
  57. package/dist/src/services/formatters.js.map +1 -0
  58. package/dist/src/services/reference-transform.d.ts +27 -0
  59. package/dist/src/services/reference-transform.js +75 -0
  60. package/dist/src/services/reference-transform.js.map +1 -0
  61. package/dist/src/services/spec-loader.d.ts +27 -0
  62. package/dist/src/services/spec-loader.js +77 -0
  63. package/dist/src/services/spec-loader.js.map +1 -0
  64. package/dist/src/types.d.ts +11 -0
  65. package/dist/src/types.js +2 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/dist/src/utils/uri-builder.d.ts +81 -0
  68. package/dist/src/utils/uri-builder.js +121 -0
  69. package/dist/src/utils/uri-builder.js.map +1 -0
  70. package/dist/src/version.d.ts +1 -0
  71. package/dist/src/version.js +4 -0
  72. package/dist/src/version.js.map +1 -0
  73. package/eslint.config.js +88 -0
  74. package/jest.config.js +32 -0
  75. package/justfile +66 -0
  76. package/memory-bank/activeContext.md +139 -0
  77. package/memory-bank/productContext.md +39 -0
  78. package/memory-bank/progress.md +141 -0
  79. package/memory-bank/projectbrief.md +50 -0
  80. package/memory-bank/systemPatterns.md +224 -0
  81. package/memory-bank/techContext.md +131 -0
  82. package/package.json +76 -0
  83. package/scripts/generate-version.js +49 -0
  84. package/src/config.ts +33 -0
  85. package/src/handlers/component-detail-handler.ts +121 -0
  86. package/src/handlers/component-map-handler.ts +92 -0
  87. package/src/handlers/handler-utils.ts +230 -0
  88. package/src/handlers/operation-handler.ts +114 -0
  89. package/src/handlers/path-item-handler.ts +88 -0
  90. package/src/handlers/top-level-field-handler.ts +92 -0
  91. package/src/index.ts +222 -0
  92. package/src/rendering/components.ts +228 -0
  93. package/src/rendering/document.ts +167 -0
  94. package/src/rendering/path-item.ts +157 -0
  95. package/src/rendering/paths.ts +87 -0
  96. package/src/rendering/types.ts +63 -0
  97. package/src/rendering/utils.ts +107 -0
  98. package/src/services/formatters.ts +71 -0
  99. package/src/services/reference-transform.ts +105 -0
  100. package/src/services/spec-loader.ts +88 -0
  101. package/src/types.ts +17 -0
  102. package/src/utils/uri-builder.ts +134 -0
  103. package/src/version.ts +4 -0
  104. package/test/__tests__/e2e/format.test.ts +224 -0
  105. package/test/__tests__/e2e/resources.test.ts +369 -0
  106. package/test/__tests__/e2e/spec-loading.test.ts +172 -0
  107. package/test/__tests__/unit/config.test.ts +39 -0
  108. package/test/__tests__/unit/handlers/component-detail-handler.test.ts +241 -0
  109. package/test/__tests__/unit/handlers/component-map-handler.test.ts +187 -0
  110. package/test/__tests__/unit/handlers/handler-utils.test.ts +255 -0
  111. package/test/__tests__/unit/handlers/operation-handler.test.ts +202 -0
  112. package/test/__tests__/unit/handlers/path-item-handler.test.ts +153 -0
  113. package/test/__tests__/unit/handlers/top-level-field-handler.test.ts +182 -0
  114. package/test/__tests__/unit/rendering/components.test.ts +269 -0
  115. package/test/__tests__/unit/rendering/document.test.ts +172 -0
  116. package/test/__tests__/unit/rendering/path-item.test.ts +197 -0
  117. package/test/__tests__/unit/rendering/paths.test.ts +115 -0
  118. package/test/__tests__/unit/services/formatters.test.ts +109 -0
  119. package/test/__tests__/unit/services/reference-transform.test.ts +320 -0
  120. package/test/__tests__/unit/services/spec-loader.test.ts +214 -0
  121. package/test/__tests__/unit/utils/uri-builder.test.ts +103 -0
  122. package/test/fixtures/complex-endpoint.json +146 -0
  123. package/test/fixtures/empty-api.json +8 -0
  124. package/test/fixtures/multi-component-types.json +55 -0
  125. package/test/fixtures/paths-test.json +61 -0
  126. package/test/fixtures/sample-api.json +68 -0
  127. package/test/fixtures/sample-v2-api.json +39 -0
  128. package/test/setup.ts +32 -0
  129. package/test/utils/console-helpers.ts +48 -0
  130. package/test/utils/mcp-test-helpers.ts +66 -0
  131. package/test/utils/test-types.ts +54 -0
  132. package/tsconfig.json +25 -0
  133. package/tsconfig.test.json +5 -0
@@ -0,0 +1,202 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { OperationHandler } from '../../../../src/handlers/operation-handler';
3
+ import { SpecLoaderService } from '../../../../src/types';
4
+ import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
5
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
7
+ import { suppressExpectedConsoleError } from '../../../utils/console-helpers';
8
+
9
+ // Mocks
10
+ const mockGetTransformedSpec = jest.fn();
11
+ const mockSpecLoader: SpecLoaderService = {
12
+ getSpec: jest.fn(),
13
+ getTransformedSpec: mockGetTransformedSpec,
14
+ };
15
+
16
+ const mockFormatter: IFormatter = new JsonFormatter();
17
+
18
+ // Sample Data
19
+ const getOperation: OpenAPIV3.OperationObject = {
20
+ summary: 'Get Item',
21
+ responses: { '200': { description: 'OK' } },
22
+ };
23
+ const postOperation: OpenAPIV3.OperationObject = {
24
+ summary: 'Create Item',
25
+ responses: { '201': { description: 'Created' } },
26
+ };
27
+ const sampleSpec: OpenAPIV3.Document = {
28
+ openapi: '3.0.3',
29
+ info: { title: 'Test API', version: '1.0.0' },
30
+ paths: {
31
+ '/items': {
32
+ get: getOperation,
33
+ post: postOperation,
34
+ },
35
+ '/items/{id}': {
36
+ get: { summary: 'Get Single Item', responses: { '200': { description: 'OK' } } },
37
+ },
38
+ },
39
+ components: {},
40
+ };
41
+
42
+ const encodedPathItems = encodeURIComponent('items');
43
+ const encodedPathNonExistent = encodeURIComponent('nonexistent');
44
+
45
+ describe('OperationHandler', () => {
46
+ let handler: OperationHandler;
47
+
48
+ beforeEach(() => {
49
+ handler = new OperationHandler(mockSpecLoader, mockFormatter);
50
+ mockGetTransformedSpec.mockReset();
51
+ mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
52
+ });
53
+
54
+ it('should return the correct template', () => {
55
+ const template = handler.getTemplate();
56
+ expect(template).toBeInstanceOf(ResourceTemplate);
57
+ expect(template.uriTemplate.toString()).toBe('openapi://paths/{path}/{method*}');
58
+ });
59
+
60
+ describe('handleRequest', () => {
61
+ const mockExtra = { signal: new AbortController().signal };
62
+
63
+ it('should return detail for a single valid method', async () => {
64
+ const variables: Variables = { path: encodedPathItems, method: 'get' }; // Use 'method' key
65
+ const uri = new URL(`openapi://paths/${encodedPathItems}/get`);
66
+
67
+ const result = await handler.handleRequest(uri, variables, mockExtra);
68
+
69
+ expect(mockGetTransformedSpec).toHaveBeenCalledWith({
70
+ resourceType: 'schema',
71
+ format: 'openapi',
72
+ });
73
+ expect(result.contents).toHaveLength(1);
74
+ expect(result.contents[0]).toEqual({
75
+ uri: `openapi://paths/${encodedPathItems}/get`,
76
+ mimeType: 'application/json',
77
+ text: JSON.stringify(getOperation, null, 2),
78
+ isError: false,
79
+ });
80
+ });
81
+
82
+ it('should return details for multiple valid methods (array input)', async () => {
83
+ const variables: Variables = { path: encodedPathItems, method: ['get', 'post'] }; // Use 'method' key with array
84
+ const uri = new URL(`openapi://paths/${encodedPathItems}/get,post`); // URI might not reflect array input
85
+
86
+ const result = await handler.handleRequest(uri, variables, mockExtra);
87
+
88
+ expect(result.contents).toHaveLength(2);
89
+ expect(result.contents).toContainEqual({
90
+ uri: `openapi://paths/${encodedPathItems}/get`,
91
+ mimeType: 'application/json',
92
+ text: JSON.stringify(getOperation, null, 2),
93
+ isError: false,
94
+ });
95
+ expect(result.contents).toContainEqual({
96
+ uri: `openapi://paths/${encodedPathItems}/post`,
97
+ mimeType: 'application/json',
98
+ text: JSON.stringify(postOperation, null, 2),
99
+ isError: false,
100
+ });
101
+ });
102
+
103
+ it('should return error for non-existent path', async () => {
104
+ const variables: Variables = { path: encodedPathNonExistent, method: 'get' };
105
+ const uri = new URL(`openapi://paths/${encodedPathNonExistent}/get`);
106
+ const expectedLogMessage = /Path "\/nonexistent" not found/;
107
+
108
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
109
+ handler.handleRequest(uri, variables, mockExtra)
110
+ );
111
+
112
+ expect(result.contents).toHaveLength(1);
113
+ // Expect the specific error message from getValidatedPathItem
114
+ expect(result.contents[0]).toEqual({
115
+ uri: `openapi://paths/${encodedPathNonExistent}/get`,
116
+ mimeType: 'text/plain',
117
+ text: 'Path "/nonexistent" not found in the specification.',
118
+ isError: true,
119
+ });
120
+ });
121
+
122
+ it('should return error for non-existent method', async () => {
123
+ const variables: Variables = { path: encodedPathItems, method: 'put' };
124
+ const uri = new URL(`openapi://paths/${encodedPathItems}/put`);
125
+ const expectedLogMessage = /None of the requested methods \(put\) are valid/;
126
+
127
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
128
+ handler.handleRequest(uri, variables, mockExtra)
129
+ );
130
+
131
+ expect(result.contents).toHaveLength(1);
132
+ // Expect the specific error message from getValidatedOperations
133
+ expect(result.contents[0]).toEqual({
134
+ uri: `openapi://paths/${encodedPathItems}/put`,
135
+ mimeType: 'text/plain',
136
+ text: 'None of the requested methods (put) are valid for path "/items". Available methods: get, post',
137
+ isError: true,
138
+ });
139
+ });
140
+
141
+ // Remove test for mix of valid/invalid methods, as getValidatedOperations throws now
142
+ // it('should handle mix of valid and invalid methods', async () => { ... });
143
+
144
+ it('should handle empty method array', async () => {
145
+ const variables: Variables = { path: encodedPathItems, method: [] };
146
+ const uri = new URL(`openapi://paths/${encodedPathItems}/`);
147
+ const expectedLogMessage = /No valid HTTP method specified/;
148
+
149
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
150
+ handler.handleRequest(uri, variables, mockExtra)
151
+ );
152
+
153
+ expect(result.contents).toHaveLength(1);
154
+ expect(result.contents[0]).toEqual({
155
+ uri: `openapi://paths/${encodedPathItems}/`,
156
+ mimeType: 'text/plain',
157
+ text: 'No valid HTTP method specified.',
158
+ isError: true,
159
+ });
160
+ });
161
+
162
+ it('should handle spec loading errors', async () => {
163
+ const error = new Error('Spec load failed');
164
+ mockGetTransformedSpec.mockRejectedValue(error);
165
+ const variables: Variables = { path: encodedPathItems, method: 'get' };
166
+ const uri = new URL(`openapi://paths/${encodedPathItems}/get`);
167
+ const expectedLogMessage = /Spec load failed/;
168
+
169
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
170
+ handler.handleRequest(uri, variables, mockExtra)
171
+ );
172
+
173
+ expect(result.contents).toHaveLength(1);
174
+ expect(result.contents[0]).toEqual({
175
+ uri: `openapi://paths/${encodedPathItems}/get`,
176
+ mimeType: 'text/plain',
177
+ text: 'Spec load failed',
178
+ isError: true,
179
+ });
180
+ });
181
+
182
+ it('should handle non-OpenAPI v3 spec', async () => {
183
+ const invalidSpec = { swagger: '2.0', info: {} };
184
+ mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
185
+ const variables: Variables = { path: encodedPathItems, method: 'get' };
186
+ const uri = new URL(`openapi://paths/${encodedPathItems}/get`);
187
+ const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
188
+
189
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
190
+ handler.handleRequest(uri, variables, mockExtra)
191
+ );
192
+
193
+ expect(result.contents).toHaveLength(1);
194
+ expect(result.contents[0]).toEqual({
195
+ uri: `openapi://paths/${encodedPathItems}/get`,
196
+ mimeType: 'text/plain',
197
+ text: 'Only OpenAPI v3 specifications are supported',
198
+ isError: true,
199
+ });
200
+ });
201
+ });
202
+ });
@@ -0,0 +1,153 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { PathItemHandler } from '../../../../src/handlers/path-item-handler';
3
+ import { SpecLoaderService } from '../../../../src/types';
4
+ import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
5
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
7
+ import { suppressExpectedConsoleError } from '../../../utils/console-helpers';
8
+
9
+ // Mocks
10
+ const mockGetTransformedSpec = jest.fn();
11
+ const mockSpecLoader: SpecLoaderService = {
12
+ getSpec: jest.fn(),
13
+ getTransformedSpec: mockGetTransformedSpec,
14
+ };
15
+
16
+ const mockFormatter: IFormatter = new JsonFormatter(); // Needed for context
17
+
18
+ // Sample Data
19
+ const samplePathItem: OpenAPIV3.PathItemObject = {
20
+ get: { summary: 'Get Item', responses: { '200': { description: 'OK' } } },
21
+ post: { summary: 'Create Item', responses: { '201': { description: 'Created' } } },
22
+ };
23
+ const sampleSpec: OpenAPIV3.Document = {
24
+ openapi: '3.0.3',
25
+ info: { title: 'Test API', version: '1.0.0' },
26
+ paths: {
27
+ '/items': samplePathItem,
28
+ '/empty': {}, // Path with no methods
29
+ },
30
+ components: {},
31
+ };
32
+
33
+ const encodedPathItems = encodeURIComponent('items');
34
+ const encodedPathEmpty = encodeURIComponent('empty');
35
+ const encodedPathNonExistent = encodeURIComponent('nonexistent');
36
+
37
+ describe('PathItemHandler', () => {
38
+ let handler: PathItemHandler;
39
+
40
+ beforeEach(() => {
41
+ handler = new PathItemHandler(mockSpecLoader, mockFormatter);
42
+ mockGetTransformedSpec.mockReset();
43
+ mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
44
+ });
45
+
46
+ it('should return the correct template', () => {
47
+ const template = handler.getTemplate();
48
+ expect(template).toBeInstanceOf(ResourceTemplate);
49
+ expect(template.uriTemplate.toString()).toBe('openapi://paths/{path}');
50
+ });
51
+
52
+ describe('handleRequest (List Methods)', () => {
53
+ const mockExtra = { signal: new AbortController().signal };
54
+
55
+ it('should list methods for a valid path', async () => {
56
+ const variables: Variables = { path: encodedPathItems };
57
+ const uri = new URL(`openapi://paths/${encodedPathItems}`);
58
+
59
+ const result = await handler.handleRequest(uri, variables, mockExtra);
60
+
61
+ expect(mockGetTransformedSpec).toHaveBeenCalledWith({
62
+ resourceType: 'schema',
63
+ format: 'openapi',
64
+ });
65
+ expect(result.contents).toHaveLength(1);
66
+ expect(result.contents[0]).toMatchObject({
67
+ uri: `openapi://paths/${encodedPathItems}`,
68
+ mimeType: 'text/plain',
69
+ isError: false,
70
+ });
71
+ // Check for hint first, then methods
72
+ expect(result.contents[0].text).toContain("Hint: Use 'openapi://paths/items/{method}'");
73
+ expect(result.contents[0].text).toContain('GET: Get Item');
74
+ expect(result.contents[0].text).toContain('POST: Create Item');
75
+ // Ensure the old "Methods for..." header is not present if hint is first
76
+ expect(result.contents[0].text).not.toContain('Methods for items:');
77
+ });
78
+
79
+ it('should handle path with no methods', async () => {
80
+ const variables: Variables = { path: encodedPathEmpty };
81
+ const uri = new URL(`openapi://paths/${encodedPathEmpty}`);
82
+
83
+ const result = await handler.handleRequest(uri, variables, mockExtra);
84
+
85
+ expect(result.contents).toHaveLength(1);
86
+ expect(result.contents[0]).toEqual({
87
+ uri: `openapi://paths/${encodedPathEmpty}`,
88
+ mimeType: 'text/plain',
89
+ text: 'No standard HTTP methods found for path: empty',
90
+ isError: false, // Not an error, just no methods
91
+ });
92
+ });
93
+
94
+ it('should return error for non-existent path', async () => {
95
+ const variables: Variables = { path: encodedPathNonExistent };
96
+ const uri = new URL(`openapi://paths/${encodedPathNonExistent}`);
97
+ const expectedLogMessage = /Path "\/nonexistent" not found/;
98
+
99
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
100
+ handler.handleRequest(uri, variables, mockExtra)
101
+ );
102
+
103
+ expect(result.contents).toHaveLength(1);
104
+ // Expect the specific error message from getValidatedPathItem
105
+ expect(result.contents[0]).toEqual({
106
+ uri: `openapi://paths/${encodedPathNonExistent}`,
107
+ mimeType: 'text/plain',
108
+ text: 'Path "/nonexistent" not found in the specification.',
109
+ isError: true,
110
+ });
111
+ });
112
+
113
+ it('should handle spec loading errors', async () => {
114
+ const error = new Error('Spec load failed');
115
+ mockGetTransformedSpec.mockRejectedValue(error);
116
+ const variables: Variables = { path: encodedPathItems };
117
+ const uri = new URL(`openapi://paths/${encodedPathItems}`);
118
+ const expectedLogMessage = /Spec load failed/;
119
+
120
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
121
+ handler.handleRequest(uri, variables, mockExtra)
122
+ );
123
+
124
+ expect(result.contents).toHaveLength(1);
125
+ expect(result.contents[0]).toEqual({
126
+ uri: `openapi://paths/${encodedPathItems}`,
127
+ mimeType: 'text/plain',
128
+ text: 'Spec load failed',
129
+ isError: true,
130
+ });
131
+ });
132
+
133
+ it('should handle non-OpenAPI v3 spec', async () => {
134
+ const invalidSpec = { swagger: '2.0', info: {} };
135
+ mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
136
+ const variables: Variables = { path: encodedPathItems };
137
+ const uri = new URL(`openapi://paths/${encodedPathItems}`);
138
+ const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
139
+
140
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
141
+ handler.handleRequest(uri, variables, mockExtra)
142
+ );
143
+
144
+ expect(result.contents).toHaveLength(1);
145
+ expect(result.contents[0]).toEqual({
146
+ uri: `openapi://paths/${encodedPathItems}`,
147
+ mimeType: 'text/plain',
148
+ text: 'Only OpenAPI v3 specifications are supported',
149
+ isError: true,
150
+ });
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,182 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+ import { TopLevelFieldHandler } from '../../../../src/handlers/top-level-field-handler';
3
+ import { SpecLoaderService } from '../../../../src/types';
4
+ import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
5
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
7
+ import { suppressExpectedConsoleError } from '../../../utils/console-helpers';
8
+
9
+ // Mocks
10
+ const mockGetTransformedSpec = jest.fn();
11
+ const mockSpecLoader: SpecLoaderService = {
12
+ getSpec: jest.fn(), // Not used by this handler directly
13
+ getTransformedSpec: mockGetTransformedSpec,
14
+ };
15
+
16
+ const mockFormatter: IFormatter = new JsonFormatter(); // Use real formatter for structure check
17
+
18
+ // Sample Data
19
+ const sampleSpec: OpenAPIV3.Document = {
20
+ openapi: '3.0.3',
21
+ info: { title: 'Test API', version: '1.1.0' },
22
+ paths: { '/test': { get: { responses: { '200': { description: 'OK' } } } } },
23
+ components: { schemas: { Test: { type: 'string' } } },
24
+ servers: [{ url: 'http://example.com' }],
25
+ };
26
+
27
+ describe('TopLevelFieldHandler', () => {
28
+ let handler: TopLevelFieldHandler;
29
+
30
+ beforeEach(() => {
31
+ handler = new TopLevelFieldHandler(mockSpecLoader, mockFormatter);
32
+ mockGetTransformedSpec.mockReset(); // Reset mock before each test
33
+ });
34
+
35
+ it('should return the correct template', () => {
36
+ const template = handler.getTemplate();
37
+ expect(template).toBeInstanceOf(ResourceTemplate);
38
+ // Compare against the string representation of the UriTemplate object
39
+ expect(template.uriTemplate.toString()).toBe('openapi://{field}');
40
+ });
41
+
42
+ describe('handleRequest', () => {
43
+ // Create a mock extra object with the required signal property
44
+ const mockExtra = {
45
+ signal: new AbortController().signal,
46
+ };
47
+ // Cast to the expected type for the handler call signature if needed,
48
+ // but the object structure itself is simpler.
49
+ // Note: We might need to cast this later if strict type checks complain,
50
+ // but let's try the minimal structure first.
51
+
52
+ it('should handle request for "info" field', async () => {
53
+ mockGetTransformedSpec.mockResolvedValue(sampleSpec);
54
+ const variables: Variables = { field: 'info' };
55
+ const uri = new URL('openapi://info');
56
+
57
+ // Pass the mock extra object as the third argument
58
+ const result = await handler.handleRequest(uri, variables, mockExtra);
59
+
60
+ expect(mockGetTransformedSpec).toHaveBeenCalledWith({
61
+ resourceType: 'schema',
62
+ format: 'openapi',
63
+ });
64
+ expect(result.contents).toHaveLength(1);
65
+ expect(result.contents[0]).toEqual({
66
+ uri: 'openapi://info',
67
+ mimeType: 'application/json',
68
+ text: JSON.stringify(sampleSpec.info, null, 2),
69
+ isError: false,
70
+ });
71
+ });
72
+
73
+ it('should handle request for "servers" field', async () => {
74
+ mockGetTransformedSpec.mockResolvedValue(sampleSpec);
75
+ const variables: Variables = { field: 'servers' };
76
+ const uri = new URL('openapi://servers');
77
+
78
+ const result = await handler.handleRequest(uri, variables, mockExtra);
79
+
80
+ expect(result.contents).toHaveLength(1);
81
+ expect(result.contents[0]).toEqual({
82
+ uri: 'openapi://servers',
83
+ mimeType: 'application/json',
84
+ text: JSON.stringify(sampleSpec.servers, null, 2),
85
+ isError: false,
86
+ });
87
+ });
88
+
89
+ it('should handle request for "paths" field (list view)', async () => {
90
+ mockGetTransformedSpec.mockResolvedValue(sampleSpec);
91
+ const variables: Variables = { field: 'paths' };
92
+ const uri = new URL('openapi://paths');
93
+
94
+ const result = await handler.handleRequest(uri, variables, mockExtra);
95
+
96
+ expect(result.contents).toHaveLength(1);
97
+ expect(result.contents[0].uri).toBe('openapi://paths');
98
+ expect(result.contents[0].mimeType).toBe('text/plain');
99
+ expect(result.contents[0].isError).toBe(false);
100
+ expect(result.contents[0].text).toContain('GET /test'); // Check content format
101
+ // Check that the hint contains the essential URI patterns
102
+ expect(result.contents[0].text).toContain('Hint:');
103
+ expect(result.contents[0].text).toContain('openapi://paths/{encoded_path}');
104
+ expect(result.contents[0].text).toContain('openapi://paths/{encoded_path}/{method}');
105
+ });
106
+
107
+ it('should handle request for "components" field (list view)', async () => {
108
+ mockGetTransformedSpec.mockResolvedValue(sampleSpec);
109
+ const variables: Variables = { field: 'components' };
110
+ const uri = new URL('openapi://components');
111
+
112
+ const result = await handler.handleRequest(uri, variables, mockExtra);
113
+
114
+ expect(result.contents).toHaveLength(1);
115
+ expect(result.contents[0].uri).toBe('openapi://components');
116
+ expect(result.contents[0].mimeType).toBe('text/plain');
117
+ expect(result.contents[0].isError).toBe(false);
118
+ expect(result.contents[0].text).toContain('- schemas'); // Check content format
119
+ expect(result.contents[0].text).toContain("Hint: Use 'openapi://components/{type}'");
120
+ });
121
+
122
+ it('should return error for non-existent field', async () => {
123
+ mockGetTransformedSpec.mockResolvedValue(sampleSpec);
124
+ const variables: Variables = { field: 'nonexistent' };
125
+ const uri = new URL('openapi://nonexistent');
126
+
127
+ const result = await handler.handleRequest(uri, variables, mockExtra);
128
+
129
+ expect(result.contents).toHaveLength(1);
130
+ expect(result.contents[0]).toEqual({
131
+ uri: 'openapi://nonexistent',
132
+ mimeType: 'text/plain',
133
+ text: 'Error: Field "nonexistent" not found in the OpenAPI document.',
134
+ isError: true,
135
+ });
136
+ });
137
+
138
+ it('should handle spec loading errors', async () => {
139
+ const error = new Error('Failed to load spec');
140
+ mockGetTransformedSpec.mockRejectedValue(error);
141
+ const variables: Variables = { field: 'info' };
142
+ const uri = new URL('openapi://info');
143
+ // Match the core error message using RegExp
144
+ const expectedLogMessage = /Failed to load spec/;
145
+
146
+ // Use the helper, letting TypeScript infer the return type
147
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
148
+ handler.handleRequest(uri, variables, mockExtra)
149
+ );
150
+
151
+ expect(result.contents).toHaveLength(1);
152
+ expect(result.contents[0]).toEqual({
153
+ uri: 'openapi://info',
154
+ mimeType: 'text/plain',
155
+ text: 'Failed to load spec',
156
+ isError: true,
157
+ });
158
+ });
159
+
160
+ it('should handle non-OpenAPI v3 spec', async () => {
161
+ const invalidSpec = { swagger: '2.0', info: {} }; // Not OpenAPI v3
162
+ mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
163
+ const variables: Variables = { field: 'info' };
164
+ const uri = new URL('openapi://info');
165
+ // Match the core error message using RegExp
166
+ const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
167
+
168
+ // Use the helper, letting TypeScript infer the return type
169
+ const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
170
+ handler.handleRequest(uri, variables, mockExtra)
171
+ );
172
+
173
+ expect(result.contents).toHaveLength(1);
174
+ expect(result.contents[0]).toEqual({
175
+ uri: 'openapi://info',
176
+ mimeType: 'text/plain',
177
+ text: 'Only OpenAPI v3 specifications are supported',
178
+ isError: true,
179
+ });
180
+ });
181
+ });
182
+ });