/**
* Test MCP Server Setup
*
* Provides utilities for setting up MCP server in integration tests.
* Mocks the Attio API while testing the full MCP protocol flow.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
ErrorCode,
McpError,
type CallToolResult,
type ReadResourceResult,
type GetPromptResult,
type Tool,
type Resource,
type Prompt,
} from '@modelcontextprotocol/sdk/types.js';
import { vi } from 'vitest';
import * as toolRegistry from '../../src/tools/index.js';
import * as resourceRegistry from '../../src/resources/index.js';
import * as promptRegistry from '../../src/prompts/index.js';
import * as attioClientModule from '../../src/attio-client.js';
import { createMockAttioClient } from './mock-attio-client.js';
/**
* Test MCP Server wrapper with convenience methods
*/
export class TestMCPServer {
public server: Server;
public mockClient: ReturnType<typeof createMockAttioClient>;
constructor(server: Server, mockClient: ReturnType<typeof createMockAttioClient>) {
this.server = server;
this.mockClient = mockClient;
}
/**
* List all available tools
*/
async listTools(): Promise<Tool[]> {
return toolRegistry.getAllTools();
}
/**
* Call a tool by name
*/
async callTool(name: string, args: Record<string, unknown> = {}): Promise<CallToolResult> {
const handler = toolRegistry.getToolHandler(name);
if (!handler) {
throw new McpError(
ErrorCode.MethodNotFound,
`Tool not found: ${name}`
);
}
try {
return await handler(args);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${errorMessage}`
);
}
}
/**
* List all available resources
*/
async listResources(): Promise<Resource[]> {
return resourceRegistry.getAllResources();
}
/**
* Read a resource by URI
*/
async readResource(uri: string): Promise<ReadResourceResult> {
const handler = resourceRegistry.getResourceHandler(uri);
if (!handler) {
throw new McpError(
ErrorCode.InvalidRequest,
`Resource not found: ${uri}`
);
}
try {
return await handler();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new McpError(
ErrorCode.InternalError,
`Resource read failed: ${errorMessage}`
);
}
}
/**
* List all available prompts
*/
async listPrompts(): Promise<Prompt[]> {
return promptRegistry.getAllPrompts();
}
/**
* Get a prompt by name
*/
async getPrompt(name: string, args: Record<string, unknown> = {}): Promise<GetPromptResult> {
const handler = promptRegistry.getPromptHandler(name);
if (!handler) {
throw new McpError(
ErrorCode.MethodNotFound,
`Prompt not found: ${name}`
);
}
try {
return await handler(args);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new McpError(
ErrorCode.InternalError,
`Prompt execution failed: ${errorMessage}`
);
}
}
/**
* Close the server
*/
async close(): Promise<void> {
await this.server.close();
}
}
/**
* Set up a test MCP server with mocked Attio client
*
* @returns TestMCPServer instance with convenience methods
*/
export async function setupTestMCPServer(): Promise<TestMCPServer> {
// Set up environment for testing
process.env.ATTIO_API_KEY = 'test-api-key';
// Create mock Attio client
const mockClient = createMockAttioClient();
// Mock the createAttioClient function to return our mock
vi.spyOn(attioClientModule, 'createAttioClient').mockReturnValue(
mockClient as any
);
// Create MCP server instance
const server = new Server(
{
name: 'heyho-mcp-test',
version: '0.1.0-test',
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);
// Set up tool handlers (same as production)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: toolRegistry.getAllTools(),
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const handler = toolRegistry.getToolHandler(name);
if (!handler) {
throw new McpError(
ErrorCode.MethodNotFound,
`Tool not found: ${name}`
);
}
try {
return await handler(args ?? {});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${errorMessage}`
);
}
});
// Set up resource handlers (same as production)
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: resourceRegistry.getAllResources(),
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
const handler = resourceRegistry.getResourceHandler(uri);
if (!handler) {
throw new McpError(
ErrorCode.InvalidRequest,
`Resource not found: ${uri}`
);
}
try {
return await handler();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new McpError(
ErrorCode.InternalError,
`Resource read failed: ${errorMessage}`
);
}
});
// Set up prompt handlers (same as production)
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: promptRegistry.getAllPrompts(),
}));
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const handler = promptRegistry.getPromptHandler(name);
if (!handler) {
throw new McpError(
ErrorCode.MethodNotFound,
`Prompt not found: ${name}`
);
}
try {
return await handler(args ?? {});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new McpError(
ErrorCode.InternalError,
`Prompt execution failed: ${errorMessage}`
);
}
});
return new TestMCPServer(server, mockClient);
}