@unifiedmemory/cli 1.3.1 → 1.3.6

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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Unit tests for lib/jwt-utils.js
3
+ *
4
+ * Tests JWT parsing, validation, and expiration checking functions.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import {
9
+ parseJWT,
10
+ isJWTExpired,
11
+ validateJWTStructure,
12
+ getTimeUntilExpiration,
13
+ } from '../../lib/jwt-utils.js';
14
+
15
+ describe('jwt-utils', () => {
16
+ describe('parseJWT', () => {
17
+ it('should parse a valid JWT and return the payload', () => {
18
+ // Create a valid JWT with base64url-encoded payload
19
+ const payload = { sub: 'user_123', email: 'test@test.com', exp: 9999999999 };
20
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64');
21
+ const token = `header.${encodedPayload}.signature`;
22
+
23
+ const result = parseJWT(token);
24
+
25
+ expect(result).toEqual(payload);
26
+ });
27
+
28
+ it('should return null for invalid JWT with wrong number of parts', () => {
29
+ expect(parseJWT('invalid')).toBeNull();
30
+ expect(parseJWT('only.two')).toBeNull();
31
+ expect(parseJWT('too.many.parts.here')).toBeNull();
32
+ });
33
+
34
+ it('should return null for JWT with invalid base64 payload', () => {
35
+ const token = 'header.!!!invalid-base64!!!.signature';
36
+
37
+ const result = parseJWT(token);
38
+
39
+ expect(result).toBeNull();
40
+ });
41
+
42
+ it('should return null for JWT with non-JSON payload', () => {
43
+ const encodedPayload = Buffer.from('not-json').toString('base64');
44
+ const token = `header.${encodedPayload}.signature`;
45
+
46
+ const result = parseJWT(token);
47
+
48
+ expect(result).toBeNull();
49
+ });
50
+
51
+ it('should return null for empty string', () => {
52
+ expect(parseJWT('')).toBeNull();
53
+ });
54
+
55
+ it('should handle JWT with special characters in payload', () => {
56
+ const payload = { sub: 'user_123', email: 'test+special@test.com', name: 'Test User' };
57
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64');
58
+ const token = `header.${encodedPayload}.signature`;
59
+
60
+ const result = parseJWT(token);
61
+
62
+ expect(result).toEqual(payload);
63
+ });
64
+ });
65
+
66
+ describe('isJWTExpired', () => {
67
+ beforeEach(() => {
68
+ // Mock Date.now() for consistent testing
69
+ vi.useFakeTimers();
70
+ vi.setSystemTime(new Date('2024-06-15T12:00:00.000Z'));
71
+ });
72
+
73
+ afterEach(() => {
74
+ vi.useRealTimers();
75
+ });
76
+
77
+ it('should return true for expired token', () => {
78
+ // Token expired on Jan 1, 2024
79
+ const decoded = { exp: Math.floor(new Date('2024-01-01T00:00:00.000Z').getTime() / 1000) };
80
+
81
+ expect(isJWTExpired(decoded)).toBe(true);
82
+ });
83
+
84
+ it('should return false for valid (non-expired) token', () => {
85
+ // Token expires on Dec 31, 2024
86
+ const decoded = { exp: Math.floor(new Date('2024-12-31T00:00:00.000Z').getTime() / 1000) };
87
+
88
+ expect(isJWTExpired(decoded)).toBe(false);
89
+ });
90
+
91
+ it('should return true when token has no exp claim', () => {
92
+ expect(isJWTExpired({})).toBe(true);
93
+ expect(isJWTExpired({ sub: 'user_123' })).toBe(true);
94
+ });
95
+
96
+ it('should return true for null/undefined decoded value', () => {
97
+ expect(isJWTExpired(null)).toBe(true);
98
+ expect(isJWTExpired(undefined)).toBe(true);
99
+ });
100
+
101
+ it('should respect buffer parameter', () => {
102
+ // Token expires in exactly 5 minutes from now
103
+ const fiveMinutesFromNow = Math.floor(Date.now() / 1000) + 300;
104
+ const decoded = { exp: fiveMinutesFromNow };
105
+
106
+ // Without buffer, not expired
107
+ expect(isJWTExpired(decoded, 0)).toBe(false);
108
+
109
+ // With 10-minute buffer (600000ms), considered expired
110
+ expect(isJWTExpired(decoded, 600000)).toBe(true);
111
+
112
+ // With 3-minute buffer (180000ms), not expired yet
113
+ expect(isJWTExpired(decoded, 180000)).toBe(false);
114
+ });
115
+
116
+ it('should handle edge case where exp equals current time', () => {
117
+ const now = Math.floor(Date.now() / 1000);
118
+ const decoded = { exp: now };
119
+
120
+ // Exactly at expiration time should be considered expired
121
+ expect(isJWTExpired(decoded)).toBe(true);
122
+ });
123
+ });
124
+
125
+ describe('validateJWTStructure', () => {
126
+ it('should return truthy for valid JWT structure with sub and exp', () => {
127
+ const decoded = { sub: 'user_123', exp: 9999999999 };
128
+
129
+ expect(validateJWTStructure(decoded)).toBeTruthy();
130
+ });
131
+
132
+ it('should return falsy when sub is missing', () => {
133
+ const decoded = { exp: 9999999999, email: 'test@test.com' };
134
+
135
+ expect(validateJWTStructure(decoded)).toBeFalsy();
136
+ });
137
+
138
+ it('should return falsy when exp is missing', () => {
139
+ const decoded = { sub: 'user_123', email: 'test@test.com' };
140
+
141
+ expect(validateJWTStructure(decoded)).toBeFalsy();
142
+ });
143
+
144
+ it('should return falsy for null/undefined', () => {
145
+ expect(validateJWTStructure(null)).toBeFalsy();
146
+ expect(validateJWTStructure(undefined)).toBeFalsy();
147
+ });
148
+
149
+ it('should return falsy for non-object values', () => {
150
+ expect(validateJWTStructure('string')).toBeFalsy();
151
+ expect(validateJWTStructure(123)).toBeFalsy();
152
+ expect(validateJWTStructure([])).toBeFalsy();
153
+ });
154
+
155
+ it('should return truthy when sub and exp have any truthy value', () => {
156
+ const decoded = { sub: 'any_value', exp: 1 };
157
+
158
+ expect(validateJWTStructure(decoded)).toBeTruthy();
159
+ });
160
+
161
+ it('should return falsy when sub or exp are falsy (0, empty string)', () => {
162
+ expect(validateJWTStructure({ sub: '', exp: 9999999999 })).toBeFalsy();
163
+ expect(validateJWTStructure({ sub: 'user_123', exp: 0 })).toBeFalsy();
164
+ });
165
+ });
166
+
167
+ describe('getTimeUntilExpiration', () => {
168
+ beforeEach(() => {
169
+ vi.useFakeTimers();
170
+ vi.setSystemTime(new Date('2024-06-15T12:00:00.000Z'));
171
+ });
172
+
173
+ afterEach(() => {
174
+ vi.useRealTimers();
175
+ });
176
+
177
+ it('should return positive milliseconds for future expiration', () => {
178
+ // Expires 1 hour from now
179
+ const oneHourFromNow = Math.floor(Date.now() / 1000) + 3600;
180
+ const decoded = { exp: oneHourFromNow };
181
+
182
+ const result = getTimeUntilExpiration(decoded);
183
+
184
+ // Should be approximately 1 hour in milliseconds
185
+ expect(result).toBeCloseTo(3600000, -2); // Allow small variance
186
+ });
187
+
188
+ it('should return negative milliseconds for past expiration', () => {
189
+ // Expired 1 hour ago
190
+ const oneHourAgo = Math.floor(Date.now() / 1000) - 3600;
191
+ const decoded = { exp: oneHourAgo };
192
+
193
+ const result = getTimeUntilExpiration(decoded);
194
+
195
+ expect(result).toBeCloseTo(-3600000, -2);
196
+ });
197
+
198
+ it('should return -1 when no exp claim exists', () => {
199
+ expect(getTimeUntilExpiration({})).toBe(-1);
200
+ expect(getTimeUntilExpiration({ sub: 'user_123' })).toBe(-1);
201
+ });
202
+
203
+ it('should return -1 for null/undefined decoded value', () => {
204
+ expect(getTimeUntilExpiration(null)).toBe(-1);
205
+ expect(getTimeUntilExpiration(undefined)).toBe(-1);
206
+ });
207
+
208
+ it('should return approximately 0 when exp equals current time', () => {
209
+ const now = Math.floor(Date.now() / 1000);
210
+ const decoded = { exp: now };
211
+
212
+ const result = getTimeUntilExpiration(decoded);
213
+
214
+ expect(result).toBeCloseTo(0, -2);
215
+ });
216
+ });
217
+ });
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Unit tests for lib/mcp-proxy.js
3
+ *
4
+ * Tests MCP proxy functions including tool schema transformation
5
+ * and context parameter injection.
6
+ *
7
+ * Note: The internal transformToolSchema and injectContextParams functions
8
+ * are tested indirectly through the exported functions.
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
12
+ import { setupServer } from 'msw/node';
13
+ import { http, HttpResponse } from 'msw';
14
+
15
+ const GATEWAY_MCP_URL = 'https://rose-asp-main-1c0b114.d2.zuplo.dev/mcp';
16
+
17
+ // Sample tool with pathParams and headers that should be transformed
18
+ const sampleToolWithContext = {
19
+ name: 'create_note',
20
+ description: 'Create a new note',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ content: { type: 'string', description: 'Note content' },
25
+ pathParams: {
26
+ type: 'object',
27
+ properties: {
28
+ org: { type: 'string', description: 'Organization ID' },
29
+ proj: { type: 'string', description: 'Project ID' },
30
+ user: { type: 'string', description: 'User ID' },
31
+ },
32
+ required: ['org', 'proj', 'user'],
33
+ },
34
+ headers: {
35
+ type: 'object',
36
+ properties: {
37
+ 'X-Org-Id': { type: 'string' },
38
+ 'X-User-Id': { type: 'string' },
39
+ },
40
+ },
41
+ },
42
+ required: ['content', 'pathParams', 'headers'],
43
+ },
44
+ };
45
+
46
+ // Tool with no context params (should remain unchanged)
47
+ const sampleToolNoContext = {
48
+ name: 'get_health',
49
+ description: 'Check health status',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ verbose: { type: 'boolean', description: 'Include details' },
54
+ },
55
+ },
56
+ };
57
+
58
+ // Setup MSW server for mocking fetch
59
+ const server = setupServer();
60
+
61
+ beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
62
+ afterEach(() => server.resetHandlers());
63
+ afterAll(() => server.close());
64
+
65
+ describe('mcp-proxy', () => {
66
+ describe('fetchRemoteMCPTools', () => {
67
+ it('should fetch tools and transform schemas to hide context params', async () => {
68
+ // Setup mock response
69
+ server.use(
70
+ http.post(GATEWAY_MCP_URL, () => {
71
+ return HttpResponse.json({
72
+ jsonrpc: '2.0',
73
+ id: 1,
74
+ result: {
75
+ tools: [sampleToolWithContext, sampleToolNoContext],
76
+ },
77
+ });
78
+ })
79
+ );
80
+
81
+ const { fetchRemoteMCPTools } = await import('../../lib/mcp-proxy.js');
82
+
83
+ const result = await fetchRemoteMCPTools({
84
+ Authorization: 'Bearer test_token',
85
+ 'X-Org-Id': 'org_123',
86
+ });
87
+
88
+ expect(result.tools).toHaveLength(2);
89
+
90
+ // First tool should have pathParams context removed
91
+ const transformedTool = result.tools[0];
92
+ expect(transformedTool.name).toBe('create_note');
93
+
94
+ // org, proj, user should be removed from pathParams
95
+ const pathParamProps = transformedTool.inputSchema.properties.pathParams?.properties || {};
96
+ expect(pathParamProps.org).toBeUndefined();
97
+ expect(pathParamProps.proj).toBeUndefined();
98
+ expect(pathParamProps.user).toBeUndefined();
99
+
100
+ // headers should be removed entirely
101
+ expect(transformedTool.inputSchema.properties.headers).toBeUndefined();
102
+
103
+ // headers should be removed from required array
104
+ expect(transformedTool.inputSchema.required).not.toContain('headers');
105
+
106
+ // Second tool should remain relatively unchanged
107
+ const healthTool = result.tools[1];
108
+ expect(healthTool.name).toBe('get_health');
109
+ expect(healthTool.inputSchema.properties.verbose).toBeDefined();
110
+ });
111
+
112
+ it('should handle empty tools list', async () => {
113
+ server.use(
114
+ http.post(GATEWAY_MCP_URL, () => {
115
+ return HttpResponse.json({
116
+ jsonrpc: '2.0',
117
+ id: 1,
118
+ result: { tools: [] },
119
+ });
120
+ })
121
+ );
122
+
123
+ const { fetchRemoteMCPTools } = await import('../../lib/mcp-proxy.js');
124
+
125
+ const result = await fetchRemoteMCPTools({
126
+ Authorization: 'Bearer test_token',
127
+ });
128
+
129
+ expect(result.tools).toEqual([]);
130
+ });
131
+
132
+ it('should throw error on 401 unauthorized response', async () => {
133
+ server.use(
134
+ http.post(GATEWAY_MCP_URL, () => {
135
+ return HttpResponse.json(
136
+ { error: 'Unauthorized' },
137
+ { status: 401 }
138
+ );
139
+ })
140
+ );
141
+
142
+ const { fetchRemoteMCPTools } = await import('../../lib/mcp-proxy.js');
143
+
144
+ await expect(
145
+ fetchRemoteMCPTools({ Authorization: 'Bearer invalid_token' })
146
+ ).rejects.toThrow('Authentication failed');
147
+ });
148
+
149
+ it('should throw error on 403 forbidden response', async () => {
150
+ server.use(
151
+ http.post(GATEWAY_MCP_URL, () => {
152
+ return HttpResponse.json(
153
+ { error: 'Forbidden' },
154
+ { status: 403 }
155
+ );
156
+ })
157
+ );
158
+
159
+ const { fetchRemoteMCPTools } = await import('../../lib/mcp-proxy.js');
160
+
161
+ await expect(
162
+ fetchRemoteMCPTools({ Authorization: 'Bearer test_token' })
163
+ ).rejects.toThrow('Access denied');
164
+ });
165
+
166
+ it('should throw error on MCP protocol error response', async () => {
167
+ server.use(
168
+ http.post(GATEWAY_MCP_URL, () => {
169
+ return HttpResponse.json({
170
+ jsonrpc: '2.0',
171
+ id: 1,
172
+ error: { code: -32600, message: 'Invalid Request' },
173
+ });
174
+ })
175
+ );
176
+
177
+ const { fetchRemoteMCPTools } = await import('../../lib/mcp-proxy.js');
178
+
179
+ await expect(
180
+ fetchRemoteMCPTools({ Authorization: 'Bearer test_token' })
181
+ ).rejects.toThrow('MCP error');
182
+ });
183
+
184
+ it('should throw connection error on network failure', async () => {
185
+ server.use(
186
+ http.post(GATEWAY_MCP_URL, () => {
187
+ return HttpResponse.error();
188
+ })
189
+ );
190
+
191
+ const { fetchRemoteMCPTools } = await import('../../lib/mcp-proxy.js');
192
+
193
+ await expect(
194
+ fetchRemoteMCPTools({ Authorization: 'Bearer test_token' })
195
+ ).rejects.toThrow();
196
+ });
197
+ });
198
+
199
+ describe('callRemoteMCPTool', () => {
200
+ it('should inject context params into tool call', async () => {
201
+ let capturedBody = null;
202
+
203
+ server.use(
204
+ http.post(GATEWAY_MCP_URL, async ({ request }) => {
205
+ capturedBody = await request.json();
206
+ return HttpResponse.json({
207
+ jsonrpc: '2.0',
208
+ id: 1,
209
+ result: {
210
+ content: [{ type: 'text', text: 'Success' }],
211
+ },
212
+ });
213
+ })
214
+ );
215
+
216
+ const { callRemoteMCPTool } = await import('../../lib/mcp-proxy.js');
217
+
218
+ const authHeaders = {
219
+ Authorization: 'Bearer test_token',
220
+ };
221
+
222
+ const authContext = {
223
+ decoded: { sub: 'user_123' },
224
+ selectedOrg: { id: 'org_456' },
225
+ };
226
+
227
+ const projectContext = {
228
+ project_id: 'proj_789',
229
+ org_id: 'org_456',
230
+ };
231
+
232
+ await callRemoteMCPTool(
233
+ 'create_note',
234
+ { content: 'Test note' },
235
+ authHeaders,
236
+ authContext,
237
+ projectContext
238
+ );
239
+
240
+ // Verify context was injected
241
+ expect(capturedBody.params.arguments.pathParams.user).toBe('user_123');
242
+ expect(capturedBody.params.arguments.pathParams.org).toBe('org_456');
243
+ expect(capturedBody.params.arguments.pathParams.proj).toBe('proj_789');
244
+ expect(capturedBody.params.arguments.headers['X-User-Id']).toBe('user_123');
245
+ expect(capturedBody.params.arguments.headers['X-Org-Id']).toBe('org_456');
246
+ });
247
+
248
+ it('should fallback to user_id for org when no selectedOrg', async () => {
249
+ let capturedBody = null;
250
+
251
+ server.use(
252
+ http.post(GATEWAY_MCP_URL, async ({ request }) => {
253
+ capturedBody = await request.json();
254
+ return HttpResponse.json({
255
+ jsonrpc: '2.0',
256
+ id: 1,
257
+ result: { content: [] },
258
+ });
259
+ })
260
+ );
261
+
262
+ const { callRemoteMCPTool } = await import('../../lib/mcp-proxy.js');
263
+
264
+ const authContext = {
265
+ decoded: { sub: 'user_123' },
266
+ // No selectedOrg
267
+ };
268
+
269
+ await callRemoteMCPTool(
270
+ 'create_note',
271
+ {},
272
+ { Authorization: 'Bearer test' },
273
+ authContext,
274
+ null
275
+ );
276
+
277
+ // Org should fallback to user_id
278
+ expect(capturedBody.params.arguments.pathParams.org).toBe('user_123');
279
+ expect(capturedBody.params.arguments.headers['X-Org-Id']).toBe('user_123');
280
+ });
281
+
282
+ it('should return tool execution result', async () => {
283
+ server.use(
284
+ http.post(GATEWAY_MCP_URL, () => {
285
+ return HttpResponse.json({
286
+ jsonrpc: '2.0',
287
+ id: 1,
288
+ result: {
289
+ content: [
290
+ { type: 'text', text: 'Note created successfully' },
291
+ { type: 'text', text: 'Note ID: note_123' },
292
+ ],
293
+ },
294
+ });
295
+ })
296
+ );
297
+
298
+ const { callRemoteMCPTool } = await import('../../lib/mcp-proxy.js');
299
+
300
+ const result = await callRemoteMCPTool(
301
+ 'create_note',
302
+ { content: 'Test' },
303
+ { Authorization: 'Bearer test' },
304
+ { decoded: { sub: 'user_123' } },
305
+ null
306
+ );
307
+
308
+ expect(result.content).toHaveLength(2);
309
+ expect(result.content[0].text).toBe('Note created successfully');
310
+ });
311
+
312
+ it('should throw error on tool execution failure', async () => {
313
+ server.use(
314
+ http.post(GATEWAY_MCP_URL, () => {
315
+ return HttpResponse.json({
316
+ jsonrpc: '2.0',
317
+ id: 1,
318
+ error: { code: -32000, message: 'Note creation failed: duplicate content' },
319
+ });
320
+ })
321
+ );
322
+
323
+ const { callRemoteMCPTool } = await import('../../lib/mcp-proxy.js');
324
+
325
+ await expect(
326
+ callRemoteMCPTool(
327
+ 'create_note',
328
+ { content: 'Duplicate' },
329
+ { Authorization: 'Bearer test' },
330
+ { decoded: { sub: 'user_123' } },
331
+ null
332
+ )
333
+ ).rejects.toThrow('Tool execution failed');
334
+ });
335
+
336
+ it('should throw 401 error message for unauthorized', async () => {
337
+ server.use(
338
+ http.post(GATEWAY_MCP_URL, () => {
339
+ return HttpResponse.json(
340
+ { error: 'Token expired' },
341
+ { status: 401 }
342
+ );
343
+ })
344
+ );
345
+
346
+ const { callRemoteMCPTool } = await import('../../lib/mcp-proxy.js');
347
+
348
+ await expect(
349
+ callRemoteMCPTool(
350
+ 'create_note',
351
+ {},
352
+ { Authorization: 'Bearer expired' },
353
+ { decoded: { sub: 'user_123' } },
354
+ null
355
+ )
356
+ ).rejects.toThrow('um login');
357
+ });
358
+ });
359
+
360
+ describe('schema transformation edge cases', () => {
361
+ it('should handle tool with pathParams but empty after context removal', async () => {
362
+ const toolWithOnlyContextParams = {
363
+ name: 'context_only_tool',
364
+ description: 'Tool with only context params',
365
+ inputSchema: {
366
+ type: 'object',
367
+ properties: {
368
+ pathParams: {
369
+ type: 'object',
370
+ properties: {
371
+ org: { type: 'string' },
372
+ proj: { type: 'string' },
373
+ },
374
+ required: ['org', 'proj'],
375
+ },
376
+ },
377
+ required: ['pathParams'],
378
+ },
379
+ };
380
+
381
+ server.use(
382
+ http.post(GATEWAY_MCP_URL, () => {
383
+ return HttpResponse.json({
384
+ jsonrpc: '2.0',
385
+ id: 1,
386
+ result: { tools: [toolWithOnlyContextParams] },
387
+ });
388
+ })
389
+ );
390
+
391
+ const { fetchRemoteMCPTools } = await import('../../lib/mcp-proxy.js');
392
+
393
+ const result = await fetchRemoteMCPTools({
394
+ Authorization: 'Bearer test',
395
+ });
396
+
397
+ const transformed = result.tools[0];
398
+
399
+ // pathParams should be removed entirely since it's empty after context removal
400
+ expect(transformed.inputSchema.properties.pathParams).toBeUndefined();
401
+
402
+ // pathParams should also be removed from required array (or required should be undefined/empty)
403
+ if (transformed.inputSchema.required) {
404
+ expect(transformed.inputSchema.required).not.toContain('pathParams');
405
+ }
406
+ });
407
+
408
+ it('should preserve non-context pathParams properties', async () => {
409
+ const toolWithMixedParams = {
410
+ name: 'mixed_params_tool',
411
+ description: 'Tool with mixed params',
412
+ inputSchema: {
413
+ type: 'object',
414
+ properties: {
415
+ pathParams: {
416
+ type: 'object',
417
+ properties: {
418
+ org: { type: 'string' },
419
+ noteId: { type: 'string', description: 'Note ID' },
420
+ version: { type: 'number', description: 'Version number' },
421
+ },
422
+ required: ['org', 'noteId'],
423
+ },
424
+ },
425
+ required: ['pathParams'],
426
+ },
427
+ };
428
+
429
+ server.use(
430
+ http.post(GATEWAY_MCP_URL, () => {
431
+ return HttpResponse.json({
432
+ jsonrpc: '2.0',
433
+ id: 1,
434
+ result: { tools: [toolWithMixedParams] },
435
+ });
436
+ })
437
+ );
438
+
439
+ const { fetchRemoteMCPTools } = await import('../../lib/mcp-proxy.js');
440
+
441
+ const result = await fetchRemoteMCPTools({
442
+ Authorization: 'Bearer test',
443
+ });
444
+
445
+ const transformed = result.tools[0];
446
+
447
+ // noteId and version should be preserved
448
+ expect(transformed.inputSchema.properties.pathParams.properties.noteId).toBeDefined();
449
+ expect(transformed.inputSchema.properties.pathParams.properties.version).toBeDefined();
450
+
451
+ // org should be removed
452
+ expect(transformed.inputSchema.properties.pathParams.properties.org).toBeUndefined();
453
+
454
+ // required should only have noteId (org removed)
455
+ expect(transformed.inputSchema.properties.pathParams.required).toContain('noteId');
456
+ expect(transformed.inputSchema.properties.pathParams.required).not.toContain('org');
457
+ });
458
+ });
459
+ });