server.ts•5.47 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { ExaClient } from './client.js';
import { getToolDefinitions, handleToolCall } from './tools/index.js';
import { log } from './utils/logger.js';
/**
* Main server class for Exa MCP integration
* @class ExaServer
*/
export class ExaServer {
private client: ExaClient;
private server: Server;
private enabledTools: string[];
private debug: boolean;
/**
* Creates a new ExaServer instance
* @param {string} apiKey - Exa API key for authentication
* @param {string[]} enabledTools - List of enabled tool IDs
* @param {boolean} debug - Enable debug logging
*/
constructor(apiKey: string, enabledTools?: string[], debug: boolean = false) {
this.client = new ExaClient(apiKey);
this.enabledTools = enabledTools || [
'web_search_exa',
'company_research_exa',
'crawling_exa',
'linkedin_search_exa',
'deep_researcher_start',
'deep_researcher_check'
];
this.debug = debug;
this.server = new Server(
{
name: 'exa-search-server',
version: '2.0.3',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
if (this.debug) {
log(`ExaServer initialized with ${this.enabledTools.length} tools`);
}
}
/**
* Sets up MCP request handlers for tools
* @private
*/
private setupHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = getToolDefinitions(this.enabledTools);
if (this.debug) {
log(`Listing ${tools.length} available tools`);
}
return { tools };
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
if (!this.enabledTools.includes(toolName)) {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown or disabled tool: ${toolName}`
);
}
if (this.debug) {
log(`Executing tool: ${toolName}`);
}
try {
return await handleToolCall(this.client, toolName, request.params.arguments);
} catch (error) {
if (this.debug) {
log(`Tool execution failed: ${error}`);
}
throw error;
}
});
}
/**
* Configures error handling and graceful shutdown
* @private
*/
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error('[MCP Error]', error);
if (this.debug) {
log(`Server error: ${error}`);
}
};
process.on('SIGINT', async () => {
if (this.debug) {
log('Shutting down server...');
}
await this.server.close();
process.exit(0);
});
}
/**
* Returns the underlying MCP server instance
* @returns {Server} MCP server instance
*/
getServer(): Server {
return this.server;
}
}
/**
* Factory function for creating standalone server instances
* Used by HTTP transport for session-based connections
* @param {string} apiKey - Exa API key for authentication
* @param {string[]} enabledTools - List of enabled tool IDs
* @param {boolean} debug - Enable debug logging
* @returns {Server} Configured MCP server instance
*/
export function createStandaloneServer(apiKey: string, enabledTools?: string[], debug: boolean = false): Server {
const server = new Server(
{
name: "exa-search-server",
version: "2.0.3",
},
{
capabilities: {
tools: {},
},
},
);
const client = new ExaClient(apiKey);
const tools = enabledTools || [
'web_search_exa',
'company_research_exa',
'crawling_exa',
'linkedin_search_exa',
'deep_researcher_start',
'deep_researcher_check'
];
// Set up handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
const toolDefs = getToolDefinitions(tools);
if (debug) {
log(`Listing ${toolDefs.length} available tools`);
}
return { tools: toolDefs };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
if (!tools.includes(toolName)) {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown or disabled tool: ${toolName}`
);
}
if (debug) {
log(`Executing tool: ${toolName}`);
}
return await handleToolCall(client, toolName, request.params.arguments);
});
return server;
}