mcp-server-gpt-image
Version:
MCP server for OpenAI GPT Image-1 and Responses API with dual-mode support, real-time streaming, intelligent caching, and automatic image optimization
298 lines • 13 kB
JavaScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { ListToolsResultSchema, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
// Mock all dependencies before importing
vi.mock('./tools/image-generation', () => ({
generateImage: vi.fn(),
editImage: vi.fn(),
}));
vi.mock('./tools/image-generation-streaming', () => ({
generateImageWithStreaming: vi.fn(),
}));
vi.mock('./utils/cache', () => ({
imageCache: {
clear: vi.fn(),
getCacheStats: vi.fn(),
get: vi.fn(),
set: vi.fn(),
},
}));
// Set environment variable for tests
process.env.OPENAI_API_KEY = 'test-api-key';
// Now import after mocks are set up
import { createMCPServer } from './server';
import { generateImage, editImage } from './tools/image-generation';
import { generateImageWithStreaming } from './tools/image-generation-streaming';
import { imageCache } from './utils/cache';
describe('MCP Server Integration Tests', () => {
let server;
let client;
let clientTransport;
let serverTransport;
beforeEach(async () => {
vi.clearAllMocks();
server = createMCPServer();
// Create linked transport pair
[clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
// Create client
client = new Client({
name: 'test-client',
version: '1.0.0',
});
// Connect both client and server
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
});
afterEach(async () => {
vi.restoreAllMocks();
await client.close();
});
describe('ListToolsRequest', () => {
it('should list all available tools', async () => {
const response = await client.request({
method: 'tools/list',
params: {},
}, ListToolsResultSchema);
expect(response.tools).toHaveLength(7);
const toolNames = response.tools.map((tool) => tool.name);
expect(toolNames).toContain('generate_image');
expect(toolNames).toContain('edit_image');
expect(toolNames).toContain('clear_cache');
expect(toolNames).toContain('cache_stats');
expect(toolNames).toContain('list_conversations');
expect(toolNames).toContain('get_conversation');
expect(toolNames).toContain('clear_conversation');
});
it('should include proper tool descriptions and schemas', async () => {
const response = await client.request({
method: 'tools/list',
params: {},
}, ListToolsResultSchema);
const generateTool = response.tools.find((t) => t.name === 'generate_image');
expect(generateTool).toBeDefined();
expect(generateTool.description).toContain('GPT Image-1');
expect(generateTool.inputSchema).toHaveProperty('type', 'object');
expect(generateTool.inputSchema).toHaveProperty('properties');
expect(generateTool.inputSchema).toHaveProperty('required', ['prompt']);
});
});
describe('CallToolRequest - generate_image', () => {
it('should generate image successfully', async () => {
const mockResult = {
images: ['aGVsbG8gd29ybGQ='], // Valid base64: "hello world"
revised_prompt: 'A beautiful sunset over the ocean',
};
vi.mocked(generateImage).mockResolvedValue(mockResult);
const response = await client.request({
method: 'tools/call',
params: {
name: 'generate_image',
arguments: {
prompt: 'A sunset',
size: '1024x1024',
quality: 'high',
format: 'png',
},
},
}, CallToolResultSchema);
expect(response.content).toBeInstanceOf(Array);
expect(response.content[0].type).toBe('text');
expect(response.content[0].text).toContain('Generated 1 image(s) successfully');
expect(response.content[1].type).toBe('image');
expect(response.content[1].data).toBe('aGVsbG8gd29ybGQ=');
expect(response.content[1].mimeType).toBe('image/png');
});
it('should handle multiple images', async () => {
const mockResult = {
images: ['aW1hZ2Ux', 'aW1hZ2Uy', 'aW1hZ2Uz'], // Valid base64
};
vi.mocked(generateImage).mockResolvedValue(mockResult);
const response = await client.request({
method: 'tools/call',
params: {
name: 'generate_image',
arguments: {
prompt: 'Multiple images',
n: 3,
},
},
}, CallToolResultSchema);
expect(response.content).toHaveLength(4); // 1 text + 3 images
expect(response.content[0].text).toContain('Generated 3 image(s)');
});
it('should handle streaming requests', async () => {
const mockStreamEvents = [
{ type: 'progress', data: { message: 'Starting...', percentage: 0 } },
{ type: 'partial', data: { partialImage: 'partial1', index: 0 } },
{ type: 'complete', data: { finalImage: 'ZmluYWwtaW1hZ2U=', revisedPrompt: 'Enhanced prompt' } },
];
vi.mocked(generateImageWithStreaming).mockImplementation(async function* () {
for (const event of mockStreamEvents) {
yield event;
}
});
const response = await client.request({
method: 'tools/call',
params: {
name: 'generate_image',
arguments: {
prompt: 'Streaming test',
stream: true,
partialImages: 2,
},
},
}, CallToolResultSchema);
expect(response.content[0].text).toContain('Generated image with streaming successfully');
expect(response.content[0].text).toContain('Revised prompt: Enhanced prompt');
expect(response.content[0].text).toContain('Streaming events: 3');
expect(response.content[1].data).toBe('ZmluYWwtaW1hZ2U='); // 'final-image' in base64
});
it('should validate input parameters', async () => {
await expect(client.request({
method: 'tools/call',
params: {
name: 'generate_image',
arguments: {
// Missing required 'prompt' field
size: '1024x1024',
},
},
}, CallToolResultSchema)).rejects.toMatchObject({
code: ErrorCode.InvalidParams,
message: expect.stringContaining('Invalid parameters'),
});
});
it('should handle generation errors', async () => {
vi.mocked(generateImage).mockRejectedValue(new Error('API rate limit exceeded'));
await expect(client.request({
method: 'tools/call',
params: {
name: 'generate_image',
arguments: {
prompt: 'Test error handling',
},
},
}, CallToolResultSchema)).rejects.toMatchObject({
code: ErrorCode.InternalError,
message: expect.stringContaining('API rate limit exceeded'),
});
});
});
describe('CallToolRequest - edit_image', () => {
it('should edit image successfully', async () => {
const mockResult = {
images: ['ZWRpdGVkLWltYWdlLWRhdGE='], // Valid base64
revised_prompt: 'Added rainbow to sunset',
};
vi.mocked(editImage).mockResolvedValue(mockResult);
const response = await client.request({
method: 'tools/call',
params: {
name: 'edit_image',
arguments: {
prompt: 'Add rainbow',
images: ['b3JpZ2luYWwtaW1hZ2UtZGF0YQ=='], // Valid base64
format: 'jpeg',
},
},
}, CallToolResultSchema);
expect(response.content[0].text).toContain('Edited image(s) successfully');
expect(response.content[0].text).toContain('Revised prompt: Added rainbow to sunset');
expect(response.content[1].type).toBe('image');
expect(response.content[1].mimeType).toBe('image/jpeg');
});
it('should validate edit input parameters', async () => {
await expect(client.request({
method: 'tools/call',
params: {
name: 'edit_image',
arguments: {
prompt: 'Edit without images',
// Missing required 'images' field
},
},
}, CallToolResultSchema)).rejects.toMatchObject({
code: ErrorCode.InvalidParams,
});
});
});
describe('CallToolRequest - cache operations', () => {
it('should clear cache successfully', async () => {
vi.mocked(imageCache.clear).mockResolvedValue(undefined);
const response = await client.request({
method: 'tools/call',
params: {
name: 'clear_cache',
arguments: {},
},
}, CallToolResultSchema);
expect(response.content[0].text).toBe('Image cache cleared successfully.');
expect(imageCache.clear).toHaveBeenCalled();
});
it('should return cache statistics', async () => {
vi.mocked(imageCache.getCacheStats).mockReturnValue({
memoryEntries: 10,
estimatedDiskUsage: '5.0 MB',
});
const response = await client.request({
method: 'tools/call',
params: {
name: 'cache_stats',
arguments: {},
},
}, CallToolResultSchema);
expect(response.content[0].text).toContain('Memory entries: 10');
expect(response.content[0].text).toContain('Estimated disk usage: 5.0 MB');
});
});
describe('CallToolRequest - error handling', () => {
it('should handle unknown tool names', async () => {
await expect(client.request({
method: 'tools/call',
params: {
name: 'unknown_tool',
arguments: {},
},
}, CallToolResultSchema)).rejects.toMatchObject({
code: ErrorCode.MethodNotFound,
message: expect.stringContaining('Unknown tool: unknown_tool'),
});
});
it('should handle Zod validation errors with details', async () => {
await expect(client.request({
method: 'tools/call',
params: {
name: 'generate_image',
arguments: {
prompt: 'Test',
size: 'invalid-size', // Invalid enum value
n: 10, // Exceeds max value
},
},
}, CallToolResultSchema)).rejects.toMatchObject({
code: ErrorCode.InvalidParams,
message: expect.stringContaining('Invalid parameters'),
});
});
});
describe('Server instance creation', () => {
it('should create server with correct configuration', () => {
const server = createMCPServer();
expect(server).toBeDefined();
// Server name and version are private, but we can test that the server was created
expect(server).toBeInstanceOf(Object);
expect(typeof server.connect).toBe('function');
expect(typeof server.setRequestHandler).toBe('function');
});
it('should have tools capability', () => {
const server = createMCPServer();
const capabilities = server.getCapabilities();
expect(capabilities).toHaveProperty('tools', {});
});
});
});
//# sourceMappingURL=server.test.js.map