diff --git a/package-lock.json b/package-lock.json index 53c98b2e..18de9f8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3806,9 +3806,11 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", + "@vitest/coverage-v8": "^2.1.8", "prettier": "^2.8.8", "shx": "^0.3.4", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "vitest": "^2.1.8" } }, "src/filesystem": { diff --git a/src/everything/__tests__/prompts.test.ts b/src/everything/__tests__/prompts.test.ts new file mode 100644 index 00000000..867d996e --- /dev/null +++ b/src/everything/__tests__/prompts.test.ts @@ -0,0 +1,266 @@ +import { describe, it, expect, vi } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerSimplePrompt } from '../prompts/simple.js'; +import { registerArgumentsPrompt } from '../prompts/args.js'; +import { registerPromptWithCompletions } from '../prompts/completions.js'; +import { registerEmbeddedResourcePrompt } from '../prompts/resource.js'; + +// Helper to capture registered prompt handlers +function createMockServer() { + const handlers: Map = new Map(); + const configs: Map = new Map(); + + const mockServer = { + registerPrompt: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + configs.set(name, config); + }), + } as unknown as McpServer; + + return { mockServer, handlers, configs }; +} + +describe('Prompts', () => { + describe('simple-prompt', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerSimplePrompt(mockServer); + + expect(mockServer.registerPrompt).toHaveBeenCalledWith( + 'simple-prompt', + expect.objectContaining({ + title: 'Simple Prompt', + description: 'A prompt with no arguments', + }), + expect.any(Function) + ); + }); + + it('should return fixed message with no arguments', () => { + const { mockServer, handlers } = createMockServer(); + registerSimplePrompt(mockServer); + + const handler = handlers.get('simple-prompt')!; + const result = handler(); + + expect(result).toEqual({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'This is a simple prompt without arguments.', + }, + }, + ], + }); + }); + + it('should return message with user role', () => { + const { mockServer, handlers } = createMockServer(); + registerSimplePrompt(mockServer); + + const handler = handlers.get('simple-prompt')!; + const result = handler(); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + }); + }); + + describe('args-prompt', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerArgumentsPrompt(mockServer); + + expect(mockServer.registerPrompt).toHaveBeenCalledWith( + 'args-prompt', + expect.objectContaining({ + title: 'Arguments Prompt', + description: 'A prompt with two arguments, one required and one optional', + }), + expect.any(Function) + ); + }); + + it('should include city in message', () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); + + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'San Francisco' }); + + expect(result.messages[0].content.text).toBe("What's weather in San Francisco?"); + }); + + it('should include city and state in message', () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); + + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'San Francisco', state: 'California' }); + + expect(result.messages[0].content.text).toBe( + "What's weather in San Francisco, California?" + ); + }); + + it('should handle city only (optional state omitted)', () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); + + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'New York' }); + + expect(result.messages[0].content.text).toBe("What's weather in New York?"); + expect(result.messages[0].content.text).not.toContain(','); + }); + + it('should return message with user role', () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); + + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'Boston' }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + }); + }); + + describe('completable-prompt', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerPromptWithCompletions(mockServer); + + expect(mockServer.registerPrompt).toHaveBeenCalledWith( + 'completable-prompt', + expect.objectContaining({ + title: 'Team Management', + description: 'First argument choice narrows values for second argument.', + }), + expect.any(Function) + ); + }); + + it('should generate promotion message with department and name', () => { + const { mockServer, handlers } = createMockServer(); + registerPromptWithCompletions(mockServer); + + const handler = handlers.get('completable-prompt')!; + const result = handler({ department: 'Engineering', name: 'Alice' }); + + expect(result.messages[0].content.text).toBe( + 'Please promote Alice to the head of the Engineering team.' + ); + }); + + it('should work with different departments', () => { + const { mockServer, handlers } = createMockServer(); + registerPromptWithCompletions(mockServer); + + const handler = handlers.get('completable-prompt')!; + + const salesResult = handler({ department: 'Sales', name: 'David' }); + expect(salesResult.messages[0].content.text).toContain('Sales'); + expect(salesResult.messages[0].content.text).toContain('David'); + + const marketingResult = handler({ department: 'Marketing', name: 'Grace' }); + expect(marketingResult.messages[0].content.text).toContain('Marketing'); + expect(marketingResult.messages[0].content.text).toContain('Grace'); + }); + + it('should return message with user role', () => { + const { mockServer, handlers } = createMockServer(); + registerPromptWithCompletions(mockServer); + + const handler = handlers.get('completable-prompt')!; + const result = handler({ department: 'Support', name: 'John' }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + }); + }); + + describe('resource-prompt', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + expect(mockServer.registerPrompt).toHaveBeenCalledWith( + 'resource-prompt', + expect.objectContaining({ + title: 'Resource Prompt', + description: 'A prompt that includes an embedded resource reference', + }), + expect.any(Function) + ); + }); + + it('should return text resource reference', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Text', resourceId: '1' }); + + expect(result.messages).toHaveLength(2); + expect(result.messages[0].content.text).toContain('Text'); + expect(result.messages[0].content.text).toContain('1'); + expect(result.messages[1].content.type).toBe('resource'); + expect(result.messages[1].content.resource.uri).toContain('text/1'); + }); + + it('should return blob resource reference', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Blob', resourceId: '5' }); + + expect(result.messages[0].content.text).toContain('Blob'); + expect(result.messages[1].content.resource.uri).toContain('blob/5'); + }); + + it('should reject invalid resource type', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + expect(() => handler({ resourceType: 'Invalid', resourceId: '1' })).toThrow( + 'Invalid resourceType' + ); + }); + + it('should reject invalid resource ID', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + expect(() => handler({ resourceType: 'Text', resourceId: '-1' })).toThrow( + 'Invalid resourceId' + ); + expect(() => handler({ resourceType: 'Text', resourceId: '0' })).toThrow( + 'Invalid resourceId' + ); + expect(() => handler({ resourceType: 'Text', resourceId: 'abc' })).toThrow( + 'Invalid resourceId' + ); + }); + + it('should include both intro text and resource messages', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Text', resourceId: '3' }); + + expect(result.messages).toHaveLength(2); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + expect(result.messages[1].role).toBe('user'); + expect(result.messages[1].content.type).toBe('resource'); + }); + }); +}); diff --git a/src/everything/__tests__/resources.test.ts b/src/everything/__tests__/resources.test.ts new file mode 100644 index 00000000..c664059b --- /dev/null +++ b/src/everything/__tests__/resources.test.ts @@ -0,0 +1,268 @@ +import { describe, it, expect, vi } from 'vitest'; +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { + textResource, + blobResource, + textResourceUri, + blobResourceUri, + RESOURCE_TYPE_TEXT, + RESOURCE_TYPE_BLOB, + RESOURCE_TYPES, + resourceTypeCompleter, + resourceIdForPromptCompleter, + resourceIdForResourceTemplateCompleter, + registerResourceTemplates, +} from '../resources/templates.js'; +import { + getSessionResourceURI, + registerSessionResource, +} from '../resources/session.js'; + +describe('Resource Templates', () => { + describe('Constants', () => { + it('should define text resource type', () => { + expect(RESOURCE_TYPE_TEXT).toBe('Text'); + }); + + it('should define blob resource type', () => { + expect(RESOURCE_TYPE_BLOB).toBe('Blob'); + }); + + it('should include both types in RESOURCE_TYPES array', () => { + expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_TEXT); + expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_BLOB); + expect(RESOURCE_TYPES).toHaveLength(2); + }); + }); + + describe('textResourceUri', () => { + it('should create URL for text resource', () => { + const uri = textResourceUri(1); + expect(uri.toString()).toBe('demo://resource/dynamic/text/1'); + }); + + it('should handle different resource IDs', () => { + expect(textResourceUri(5).toString()).toBe('demo://resource/dynamic/text/5'); + expect(textResourceUri(100).toString()).toBe('demo://resource/dynamic/text/100'); + }); + }); + + describe('blobResourceUri', () => { + it('should create URL for blob resource', () => { + const uri = blobResourceUri(1); + expect(uri.toString()).toBe('demo://resource/dynamic/blob/1'); + }); + + it('should handle different resource IDs', () => { + expect(blobResourceUri(5).toString()).toBe('demo://resource/dynamic/blob/5'); + expect(blobResourceUri(100).toString()).toBe('demo://resource/dynamic/blob/100'); + }); + }); + + describe('textResource', () => { + it('should create text resource with correct structure', () => { + const uri = textResourceUri(1); + const resource = textResource(uri, 1); + + expect(resource.uri).toBe(uri.toString()); + expect(resource.mimeType).toBe('text/plain'); + expect(resource.text).toContain('Resource 1'); + expect(resource.text).toContain('plaintext'); + }); + + it('should include timestamp in content', () => { + const uri = textResourceUri(2); + const resource = textResource(uri, 2); + + // Timestamp format varies, just check it contains time-related content + expect(resource.text).toMatch(/\d/); + }); + }); + + describe('blobResource', () => { + it('should create blob resource with correct structure', () => { + const uri = blobResourceUri(1); + const resource = blobResource(uri, 1); + + expect(resource.uri).toBe(uri.toString()); + expect(resource.mimeType).toBe('text/plain'); + expect(resource.blob).toBeDefined(); + }); + + it('should create valid base64 encoded content', () => { + const uri = blobResourceUri(3); + const resource = blobResource(uri, 3); + + // Decode and verify content + const decoded = Buffer.from(resource.blob, 'base64').toString(); + expect(decoded).toContain('Resource 3'); + expect(decoded).toContain('base64 blob'); + }); + }); + + describe('resourceTypeCompleter', () => { + it('should be defined as a completable schema', () => { + // The completer is a zod schema wrapped with completable + expect(resourceTypeCompleter).toBeDefined(); + // It should have the zod parse method + expect(typeof (resourceTypeCompleter as any).parse).toBe('function'); + }); + + it('should validate string resource types', () => { + // Test that valid strings pass validation + expect(() => (resourceTypeCompleter as any).parse('Text')).not.toThrow(); + expect(() => (resourceTypeCompleter as any).parse('Blob')).not.toThrow(); + }); + }); + + describe('resourceIdForPromptCompleter', () => { + it('should be defined as a completable schema', () => { + expect(resourceIdForPromptCompleter).toBeDefined(); + expect(typeof (resourceIdForPromptCompleter as any).parse).toBe('function'); + }); + + it('should validate string IDs', () => { + // Test that valid strings pass validation + expect(() => (resourceIdForPromptCompleter as any).parse('1')).not.toThrow(); + expect(() => (resourceIdForPromptCompleter as any).parse('100')).not.toThrow(); + }); + }); + + describe('resourceIdForResourceTemplateCompleter', () => { + it('should validate positive integer IDs', () => { + expect(resourceIdForResourceTemplateCompleter('1')).toEqual(['1']); + expect(resourceIdForResourceTemplateCompleter('50')).toEqual(['50']); + }); + + it('should reject invalid IDs', () => { + expect(resourceIdForResourceTemplateCompleter('0')).toEqual([]); + expect(resourceIdForResourceTemplateCompleter('-5')).toEqual([]); + expect(resourceIdForResourceTemplateCompleter('not-a-number')).toEqual([]); + }); + }); + + describe('registerResourceTemplates', () => { + it('should register text and blob resource templates', () => { + const registeredResources: any[] = []; + + const mockServer = { + registerResource: vi.fn((...args) => { + registeredResources.push(args); + }), + } as unknown as McpServer; + + registerResourceTemplates(mockServer); + + expect(mockServer.registerResource).toHaveBeenCalledTimes(2); + + // Check text resource registration + const textRegistration = registeredResources.find((r) => + r[0].includes('Text') + ); + expect(textRegistration).toBeDefined(); + expect(textRegistration[1]).toBeInstanceOf(ResourceTemplate); + + // Check blob resource registration + const blobRegistration = registeredResources.find((r) => + r[0].includes('Blob') + ); + expect(blobRegistration).toBeDefined(); + }); + }); +}); + +describe('Session Resources', () => { + describe('getSessionResourceURI', () => { + it('should generate correct URI for resource name', () => { + expect(getSessionResourceURI('test')).toBe('demo://resource/session/test'); + }); + + it('should handle various resource names', () => { + expect(getSessionResourceURI('my-file')).toBe('demo://resource/session/my-file'); + expect(getSessionResourceURI('document_123')).toBe( + 'demo://resource/session/document_123' + ); + }); + }); + + describe('registerSessionResource', () => { + it('should register text resource and return resource link', () => { + const registrations: any[] = []; + const mockServer = { + registerResource: vi.fn((...args) => { + registrations.push(args); + }), + } as unknown as McpServer; + + const resource = { + uri: 'demo://resource/session/test-file', + name: 'test-file', + mimeType: 'text/plain', + description: 'A test file', + }; + + const result = registerSessionResource( + mockServer, + resource, + 'text', + 'Hello, World!' + ); + + expect(result.type).toBe('resource_link'); + expect(result.uri).toBe(resource.uri); + expect(result.name).toBe(resource.name); + + expect(mockServer.registerResource).toHaveBeenCalledWith( + 'test-file', + 'demo://resource/session/test-file', + expect.objectContaining({ + mimeType: 'text/plain', + description: 'A test file', + }), + expect.any(Function) + ); + }); + + it('should register blob resource correctly', () => { + const mockServer = { + registerResource: vi.fn(), + } as unknown as McpServer; + + const resource = { + uri: 'demo://resource/session/binary-file', + name: 'binary-file', + mimeType: 'application/octet-stream', + }; + + const blobContent = Buffer.from('binary data').toString('base64'); + const result = registerSessionResource(mockServer, resource, 'blob', blobContent); + + expect(result.type).toBe('resource_link'); + expect(mockServer.registerResource).toHaveBeenCalled(); + }); + + it('should return resource handler that provides correct content', async () => { + let capturedHandler: Function | null = null; + const mockServer = { + registerResource: vi.fn((_name, _uri, _config, handler) => { + capturedHandler = handler; + }), + } as unknown as McpServer; + + const resource = { + uri: 'demo://resource/session/content-test', + name: 'content-test', + mimeType: 'text/plain', + }; + + registerSessionResource(mockServer, resource, 'text', 'Test content here'); + + expect(capturedHandler).not.toBeNull(); + + const handlerResult = await capturedHandler!(new URL(resource.uri)); + expect(handlerResult.contents).toHaveLength(1); + expect(handlerResult.contents[0].text).toBe('Test content here'); + expect(handlerResult.contents[0].mimeType).toBe('text/plain'); + }); + }); +}); diff --git a/src/everything/__tests__/tools.test.ts b/src/everything/__tests__/tools.test.ts new file mode 100644 index 00000000..d90ac05f --- /dev/null +++ b/src/everything/__tests__/tools.test.ts @@ -0,0 +1,579 @@ +import { describe, it, expect, vi } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerEchoTool, EchoSchema } from '../tools/echo.js'; +import { registerGetSumTool } from '../tools/get-sum.js'; +import { registerGetEnvTool } from '../tools/get-env.js'; +import { registerGetTinyImageTool, MCP_TINY_IMAGE } from '../tools/get-tiny-image.js'; +import { registerGetStructuredContentTool } from '../tools/get-structured-content.js'; +import { registerGetAnnotatedMessageTool } from '../tools/get-annotated-message.js'; +import { registerTriggerLongRunningOperationTool } from '../tools/trigger-long-running-operation.js'; +import { registerGetResourceLinksTool } from '../tools/get-resource-links.js'; +import { registerGetResourceReferenceTool } from '../tools/get-resource-reference.js'; + +// Helper to capture registered tool handlers +function createMockServer() { + const handlers: Map = new Map(); + const configs: Map = new Map(); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + configs.set(name, config); + }), + server: { + getClientCapabilities: vi.fn(() => ({})), + notification: vi.fn(), + }, + } as unknown as McpServer; + + return { mockServer, handlers, configs }; +} + +describe('Tools', () => { + describe('echo', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerEchoTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'echo', + expect.objectContaining({ + title: 'Echo Tool', + description: 'Echoes back the input string', + }), + expect.any(Function) + ); + }); + + it('should echo back the message', async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); + + const handler = handlers.get('echo')!; + const result = await handler({ message: 'Hello, World!' }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'Echo: Hello, World!' }], + }); + }); + + it('should handle empty message', async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); + + const handler = handlers.get('echo')!; + const result = await handler({ message: '' }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'Echo: ' }], + }); + }); + + it('should reject invalid input', async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); + + const handler = handlers.get('echo')!; + + await expect(handler({})).rejects.toThrow(); + await expect(handler({ message: 123 })).rejects.toThrow(); + }); + }); + + describe('EchoSchema', () => { + it('should validate correct input', () => { + const result = EchoSchema.parse({ message: 'test' }); + expect(result).toEqual({ message: 'test' }); + }); + + it('should reject missing message', () => { + expect(() => EchoSchema.parse({})).toThrow(); + }); + + it('should reject non-string message', () => { + expect(() => EchoSchema.parse({ message: 123 })).toThrow(); + }); + }); + + describe('get-sum', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerGetSumTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'get-sum', + expect.objectContaining({ + title: 'Get Sum Tool', + description: 'Returns the sum of two numbers', + }), + expect.any(Function) + ); + }); + + it('should calculate sum of two positive numbers', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get('get-sum')!; + const result = await handler({ a: 5, b: 3 }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'The sum of 5 and 3 is 8.' }], + }); + }); + + it('should calculate sum with negative numbers', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get('get-sum')!; + const result = await handler({ a: -5, b: 3 }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'The sum of -5 and 3 is -2.' }], + }); + }); + + it('should calculate sum with zero', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get('get-sum')!; + const result = await handler({ a: 0, b: 0 }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'The sum of 0 and 0 is 0.' }], + }); + }); + + it('should handle floating point numbers', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get('get-sum')!; + const result = await handler({ a: 1.5, b: 2.5 }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'The sum of 1.5 and 2.5 is 4.' }], + }); + }); + + it('should reject invalid input', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get('get-sum')!; + + await expect(handler({})).rejects.toThrow(); + await expect(handler({ a: 'not a number', b: 5 })).rejects.toThrow(); + await expect(handler({ a: 5 })).rejects.toThrow(); + }); + }); + + describe('get-env', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerGetEnvTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'get-env', + expect.objectContaining({ + title: 'Print Environment Tool', + description: expect.stringContaining('environment variables'), + }), + expect.any(Function) + ); + }); + + it('should return all environment variables as JSON', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetEnvTool(mockServer); + + const handler = handlers.get('get-env')!; + process.env.TEST_VAR_EVERYTHING = 'test_value'; + const result = await handler({}); + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe('text'); + + const envJson = JSON.parse(result.content[0].text); + expect(envJson.TEST_VAR_EVERYTHING).toBe('test_value'); + + delete process.env.TEST_VAR_EVERYTHING; + }); + + it('should return valid JSON', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetEnvTool(mockServer); + + const handler = handlers.get('get-env')!; + const result = await handler({}); + + expect(() => JSON.parse(result.content[0].text)).not.toThrow(); + }); + }); + + describe('get-tiny-image', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerGetTinyImageTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'get-tiny-image', + expect.objectContaining({ + title: 'Get Tiny Image Tool', + description: 'Returns a tiny MCP logo image.', + }), + expect.any(Function) + ); + }); + + it('should return image content with text descriptions', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetTinyImageTool(mockServer); + + const handler = handlers.get('get-tiny-image')!; + const result = await handler({}); + + expect(result.content).toHaveLength(3); + expect(result.content[0]).toEqual({ + type: 'text', + text: "Here's the image you requested:", + }); + expect(result.content[1]).toEqual({ + type: 'image', + data: MCP_TINY_IMAGE, + mimeType: 'image/png', + }); + expect(result.content[2]).toEqual({ + type: 'text', + text: 'The image above is the MCP logo.', + }); + }); + + it('should return valid base64 image data', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetTinyImageTool(mockServer); + + const handler = handlers.get('get-tiny-image')!; + const result = await handler({}); + + const imageContent = result.content[1]; + expect(imageContent.type).toBe('image'); + expect(imageContent.mimeType).toBe('image/png'); + // Verify it's valid base64 + expect(() => Buffer.from(imageContent.data, 'base64')).not.toThrow(); + }); + }); + + describe('get-structured-content', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'get-structured-content', + expect.objectContaining({ + title: 'Get Structured Content Tool', + description: expect.stringContaining('structured content'), + }), + expect.any(Function) + ); + }); + + it('should return weather for New York', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'New York' }); + + expect(result.structuredContent).toEqual({ + temperature: 33, + conditions: 'Cloudy', + humidity: 82, + }); + expect(result.content[0].type).toBe('text'); + expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent); + }); + + it('should return weather for Chicago', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'Chicago' }); + + expect(result.structuredContent).toEqual({ + temperature: 36, + conditions: 'Light rain / drizzle', + humidity: 82, + }); + }); + + it('should return weather for Los Angeles', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'Los Angeles' }); + + expect(result.structuredContent).toEqual({ + temperature: 73, + conditions: 'Sunny / Clear', + humidity: 48, + }); + }); + }); + + describe('get-annotated-message', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'get-annotated-message', + expect.objectContaining({ + title: 'Get Annotated Message Tool', + description: expect.stringContaining('annotations'), + }), + expect.any(Function) + ); + }); + + it('should return error message with high priority', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'error', includeImage: false }); + + expect(result.content).toHaveLength(1); + expect(result.content[0].text).toBe('Error: Operation failed'); + expect(result.content[0].annotations).toEqual({ + priority: 1.0, + audience: ['user', 'assistant'], + }); + }); + + it('should return success message with medium priority', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'success', includeImage: false }); + + expect(result.content[0].text).toBe('Operation completed successfully'); + expect(result.content[0].annotations.priority).toBe(0.7); + expect(result.content[0].annotations.audience).toEqual(['user']); + }); + + it('should return debug message with low priority', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'debug', includeImage: false }); + + expect(result.content[0].text).toContain('Debug:'); + expect(result.content[0].annotations.priority).toBe(0.3); + expect(result.content[0].annotations.audience).toEqual(['assistant']); + }); + + it('should include annotated image when requested', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'success', includeImage: true }); + + expect(result.content).toHaveLength(2); + expect(result.content[1].type).toBe('image'); + expect(result.content[1].annotations).toEqual({ + priority: 0.5, + audience: ['user'], + }); + }); + }); + + describe('trigger-long-running-operation', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerTriggerLongRunningOperationTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'trigger-long-running-operation', + expect.objectContaining({ + title: 'Trigger Long Running Operation Tool', + description: expect.stringContaining('long running operation'), + }), + expect.any(Function) + ); + }); + + it('should complete operation and return result', async () => { + const { mockServer, handlers } = createMockServer(); + registerTriggerLongRunningOperationTool(mockServer); + + const handler = handlers.get('trigger-long-running-operation')!; + // Use very short duration for test + const result = await handler( + { duration: 0.1, steps: 2 }, + { _meta: {}, requestId: 'test-123' } + ); + + expect(result.content[0].text).toContain('Long running operation completed'); + expect(result.content[0].text).toContain('Duration: 0.1 seconds'); + expect(result.content[0].text).toContain('Steps: 2'); + }, 10000); + + it('should send progress notifications when progressToken provided', async () => { + const { mockServer, handlers } = createMockServer(); + registerTriggerLongRunningOperationTool(mockServer); + + const handler = handlers.get('trigger-long-running-operation')!; + await handler( + { duration: 0.1, steps: 2 }, + { _meta: { progressToken: 'token-123' }, requestId: 'test-456', sessionId: 'session-1' } + ); + + expect(mockServer.server.notification).toHaveBeenCalledTimes(2); + expect(mockServer.server.notification).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'notifications/progress', + params: expect.objectContaining({ + progressToken: 'token-123', + }), + }), + expect.any(Object) + ); + }, 10000); + }); + + describe('get-resource-links', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerGetResourceLinksTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'get-resource-links', + expect.objectContaining({ + title: 'Get Resource Links Tool', + description: expect.stringContaining('resource links'), + }), + expect.any(Function) + ); + }); + + it('should return specified number of resource links', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); + + const handler = handlers.get('get-resource-links')!; + const result = await handler({ count: 3 }); + + // 1 intro text + 3 resource links + expect(result.content).toHaveLength(4); + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('3 resource links'); + + // Check resource links + for (let i = 1; i < 4; i++) { + expect(result.content[i].type).toBe('resource_link'); + expect(result.content[i].uri).toBeDefined(); + expect(result.content[i].name).toBeDefined(); + } + }); + + it('should alternate between text and blob resources', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); + + const handler = handlers.get('get-resource-links')!; + const result = await handler({ count: 4 }); + + // Odd IDs (1, 3) are blob, even IDs (2, 4) are text + expect(result.content[1].name).toContain('Blob'); + expect(result.content[2].name).toContain('Text'); + expect(result.content[3].name).toContain('Blob'); + expect(result.content[4].name).toContain('Text'); + }); + + it('should use default count of 3', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); + + const handler = handlers.get('get-resource-links')!; + const result = await handler({}); + + // 1 intro text + 3 resource links (default) + expect(result.content).toHaveLength(4); + }); + }); + + describe('get-resource-reference', () => { + it('should register with correct name and config', () => { + const { mockServer } = createMockServer(); + registerGetResourceReferenceTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'get-resource-reference', + expect.objectContaining({ + title: 'Get Resource Reference Tool', + description: expect.stringContaining('resource reference'), + }), + expect.any(Function) + ); + }); + + it('should return text resource reference', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); + + const handler = handlers.get('get-resource-reference')!; + const result = await handler({ resourceType: 'Text', resourceId: 1 }); + + expect(result.content).toHaveLength(3); + expect(result.content[0].text).toContain('Resource 1'); + expect(result.content[1].type).toBe('resource'); + expect(result.content[1].resource.uri).toContain('text/1'); + expect(result.content[2].text).toContain('URI'); + }); + + it('should return blob resource reference', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); + + const handler = handlers.get('get-resource-reference')!; + const result = await handler({ resourceType: 'Blob', resourceId: 5 }); + + expect(result.content[1].resource.uri).toContain('blob/5'); + }); + + it('should reject invalid resource type', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); + + const handler = handlers.get('get-resource-reference')!; + await expect(handler({ resourceType: 'Invalid', resourceId: 1 })).rejects.toThrow( + 'Invalid resourceType' + ); + }); + + it('should reject invalid resource ID', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); + + const handler = handlers.get('get-resource-reference')!; + await expect(handler({ resourceType: 'Text', resourceId: -1 })).rejects.toThrow( + 'Invalid resourceId' + ); + await expect(handler({ resourceType: 'Text', resourceId: 0 })).rejects.toThrow( + 'Invalid resourceId' + ); + await expect(handler({ resourceType: 'Text', resourceId: 1.5 })).rejects.toThrow( + 'Invalid resourceId' + ); + }); + }); +}); diff --git a/src/everything/package.json b/src/everything/package.json index 73a1678a..86b96bea 100644 --- a/src/everything/package.json +++ b/src/everything/package.json @@ -26,7 +26,8 @@ "start:sse": "node dist/index.js sse", "start:streamableHttp": "node dist/index.js streamableHttp", "prettier:fix": "prettier --write .", - "prettier:check": "prettier --check ." + "prettier:check": "prettier --check .", + "test": "vitest run --coverage" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", @@ -39,8 +40,10 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", + "@vitest/coverage-v8": "^2.1.8", "shx": "^0.3.4", "typescript": "^5.6.2", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "vitest": "^2.1.8" } } diff --git a/src/everything/vitest.config.ts b/src/everything/vitest.config.ts new file mode 100644 index 00000000..d414ec8f --- /dev/null +++ b/src/everything/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['**/__tests__/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['**/*.ts'], + exclude: ['**/__tests__/**', '**/dist/**'], + }, + }, +});