Opik MCP Server

Official
  • src
import fs from 'fs'; // Import other modules import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; // Import custom transports import { SSEServerTransport } from './transports/sse-transport.js'; // Import environment variables loader - no console output import './utils/env.js'; // Setup file-based logging const logFile = '/tmp/opik-mcp.log'; // Import configuration import configImport from './config.js'; const config = configImport; // Define logging functions function logToFile(message: string) { // Only log if debug mode is enabled if (!config?.debugMode) return; try { const timestamp = new Date().toISOString(); fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`); } catch (error) { // Silently fail if we can't write to the log file } } // Only initialize log file if debug mode is enabled if (config.debugMode) { try { fs.writeFileSync(logFile, `Opik MCP Server Started: ${new Date().toISOString()}\n`); // Log process info logToFile(`Process ID: ${process.pid}, Node Version: ${process.version}`); logToFile(`Arguments: ${process.argv.join(' ')}`); logToFile( `Loaded configuration: API=${config.apiBaseUrl}, Workspace=${config.workspaceName || 'None'}` ); // Register error handlers process.on('uncaughtException', err => { logToFile(`UNCAUGHT EXCEPTION: ${err.message}`); logToFile(err.stack || 'No stack trace'); }); process.on('unhandledRejection', reason => { logToFile(`UNHANDLED REJECTION: ${reason}`); }); process.on('exit', code => { logToFile(`Process exiting with code ${code}`); }); } catch (error) { // Silently fail if we can't write to the log file } } // Rest of imports import { ProjectResponse, SingleProjectResponse, PromptResponse, SinglePromptResponse, TraceResponse, SingleTraceResponse, TraceStatsResponse, MetricsResponse, } from './types.js'; // Import capabilities module import { getEnabledCapabilities, getCapabilitiesDescription } from './utils/capabilities.js'; // Helper function to make requests to API with file logging const makeApiRequest = async <T>( path: string, options: RequestInit = {}, workspaceName?: string ): Promise<{ data: T | null; error: string | null }> => { // Prepare headers based on configuration const API_HEADERS: Record<string, string> = { Accept: 'application/json', 'Content-Type': 'application/json', authorization: config.apiKey, }; // Add workspace header for cloud version if (!config.isSelfHosted) { // Use provided workspace name or fall back to config const wsName = workspaceName || config.workspaceName; if (wsName) { // Note: The Opik API expects the workspace name to be the default workspace. // Project names like "Therapist Chat" are not valid workspace names. // The API will return a 400 error if a non-existent workspace is specified. const workspaceNameToUse = wsName.trim(); logToFile( `DEBUG - Workspace name before setting header: "${workspaceNameToUse}", type: ${typeof workspaceNameToUse}, length: ${workspaceNameToUse.length}` ); // Use the raw workspace name - do not encode it API_HEADERS['Comet-Workspace'] = workspaceNameToUse; logToFile(`Using workspace: ${workspaceNameToUse}`); } } const url = `${config.apiBaseUrl}${path}`; logToFile(`Making API request to: ${url}`); logToFile(`Headers: ${JSON.stringify(API_HEADERS, null, 2)}`); try { const response = await fetch(url, { ...options, headers: { ...API_HEADERS, ...options.headers, }, }); // Get response body text for better error handling const responseText = await response.text(); let responseData: any = null; // Try to parse the response as JSON try { responseData = JSON.parse(responseText); } catch (e) { // If it's not valid JSON, use the raw text responseData = responseText; } if (!response.ok) { const errorMsg = `HTTP error! status: ${response.status} ${JSON.stringify(responseData)}`; logToFile(`API Error: ${errorMsg}`); return { data: null, error: errorMsg, }; } return { data: responseData as T, error: null, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logToFile(`Error making API request: ${errorMessage}`); return { data: null, error: errorMessage, }; } }; // Create and configure server - no console output here const server = new McpServer( { name: config.mcpName, version: config.mcpVersion, }, { capabilities: { resources: {}, // Enable resources capability tools: {}, // Enable tools capability }, } ); // Add resources to the MCP server if (config.workspaceName) { // Define a workspace info resource server.resource('workspace-info', 'opik://workspace-info', async () => ({ contents: [ { uri: 'opik://workspace-info', text: JSON.stringify( { name: config.workspaceName, apiUrl: config.apiBaseUrl, selfHosted: config.isSelfHosted, }, null, 2 ), }, ], })); // Define a projects resource that provides the list of projects in the workspace server.resource('projects-list', 'opik://projects-list', async () => { try { const response = await makeApiRequest<ProjectResponse>('/v1/private/projects'); if (!response.data) { return { contents: [ { uri: 'opik://projects-list', text: `Error: ${response.error || 'Unknown error fetching projects'}`, }, ], }; } return { contents: [ { uri: 'opik://projects-list', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logToFile(`Error fetching projects resource: ${error}`); return { contents: [ { uri: 'opik://projects-list', text: `Error: Failed to fetch projects data`, }, ], }; } }); } // DO NOT send any protocol messages before server initialization // REMOVED: sendProtocolMessage("log", "Initializing Opik MCP Server"); // Conditionally enable tool categories based on configuration if (config.mcpEnablePromptTools) { // ----------- PROMPTS TOOLS ----------- server.tool( 'list-prompts', 'Get a list of Opik prompts', { page: z.number().describe('Page number for pagination'), size: z.number().describe('Number of items per page'), }, async args => { const response = await makeApiRequest<PromptResponse>( `/v1/private/prompts?page=${args.page}&size=${args.size}` ); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to fetch prompts' }], }; } return { content: [ { type: 'text', text: `Found ${response.data.total} prompts (showing page ${ response.data.page } of ${Math.ceil(response.data.total / response.data.size)})`, }, { type: 'text', text: JSON.stringify(response.data.content, null, 2), }, ], }; } ); server.tool( 'create-prompt', 'Create a new prompt', { name: z.string().describe('Name of the prompt'), }, async args => { const { name } = args; const response = await makeApiRequest<void>(`/v1/private/prompts`, { method: 'POST', body: JSON.stringify({ name }), }); return { content: [ { type: 'text', text: response.error || 'Successfully created prompt', }, ], }; } ); server.tool( 'create-prompt-version', 'Create a new version of a prompt', { name: z.string().describe('Name of the original prompt'), template: z.string().describe('Template content for the prompt version'), commit_message: z.string().describe('Commit message for the prompt version'), }, async args => { const { name, template, commit_message } = args; const response = await makeApiRequest<any>(`/v1/private/prompts/versions`, { method: 'POST', body: JSON.stringify({ name, version: { template, change_description: commit_message }, }), }); return { content: [ { type: 'text', text: response.data ? 'Successfully created prompt version' : `${response.error} ${JSON.stringify(args)}` || 'Failed to create prompt version', }, ], }; } ); server.tool( 'get-prompt-by-id', 'Get a single prompt by ID', { promptId: z.string().describe('ID of the prompt to fetch'), }, async args => { const { promptId } = args; const response = await makeApiRequest<SinglePromptResponse>( `/v1/private/prompts/${promptId}` ); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to fetch prompt' }], }; } return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } ); server.tool( 'update-prompt', 'Update a prompt', { promptId: z.string().describe('ID of the prompt to update'), name: z.string().describe('New name for the prompt'), }, async args => { const { promptId, name } = args; const response = await makeApiRequest<void>(`/v1/private/prompts/${promptId}`, { method: 'PUT', body: JSON.stringify({ name }), headers: { 'Content-Type': 'application/json', }, }); return { content: [ { type: 'text', text: !response.error ? 'Successfully updated prompt' : response.error || 'Failed to update prompt', }, ], }; } ); server.tool( 'delete-prompt', 'Delete a prompt', { promptId: z.string().describe('ID of the prompt to delete'), }, async args => { const { promptId } = args; const response = await makeApiRequest<void>(`/v1/private/prompts/${promptId}`, { method: 'DELETE', }); return { content: [ { type: 'text', text: !response.error ? 'Successfully deleted prompt' : response.error || 'Failed to delete prompt', }, ], }; } ); } // ----------- PROJECTS/WORKSPACES TOOLS ----------- if (config.mcpEnableProjectTools) { server.tool( 'list-projects', 'Get a list of projects/workspaces', { page: z.number().describe('Page number for pagination'), size: z.number().describe('Number of items per page'), sortBy: z.string().optional().describe('Sort projects by this field'), sortOrder: z.string().optional().describe('Sort order (asc or desc)'), workspaceName: z.string().optional().describe('Workspace name to use instead of the default'), }, async args => { const { page, size, sortBy, sortOrder, workspaceName } = args; // Build query string let url = `/v1/private/projects?page=${page}&size=${size}`; if (sortBy) url += `&sort_by=${sortBy}`; if (sortOrder) url += `&sort_order=${sortOrder}`; const response = await makeApiRequest<ProjectResponse>(url, {}, workspaceName); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to fetch projects' }], }; } return { content: [ { type: 'text', text: `Found ${response.data.total} projects (showing page ${ response.data.page } of ${Math.ceil(response.data.total / response.data.size)})`, }, { type: 'text', text: JSON.stringify(response.data.content, null, 2), }, ], }; } ); server.tool( 'get-project-by-id', 'Get a single project by ID', { projectId: z.string().describe('ID of the project to fetch'), workspaceName: z.string().optional().describe('Workspace name to use instead of the default'), }, async args => { const { projectId, workspaceName } = args; const response = await makeApiRequest<SingleProjectResponse>( `/v1/private/projects/${projectId}`, {}, workspaceName ); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to fetch project' }], }; } return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } ); server.tool( 'create-project', 'Create a new project/workspace', { name: z.string().describe('Name of the project'), description: z.string().optional().describe('Description of the project'), workspaceName: z.string().optional().describe('Workspace name to use instead of the default'), }, async args => { const { name, description, workspaceName } = args; const response = await makeApiRequest<void>( `/v1/private/projects`, { method: 'POST', body: JSON.stringify({ name, description }), }, workspaceName ); return { content: [ { type: 'text', text: response.error || 'Successfully created project', }, ], }; } ); server.tool( 'update-project', 'Update a project', { projectId: z.string().describe('ID of the project to update'), name: z.string().optional().describe('New project name'), workspaceName: z.string().optional().describe('Workspace name to use instead of the default'), description: z.string().optional().describe('New project description'), }, async args => { const { projectId, name, description, workspaceName } = args; // Build update data const updateData: Record<string, any> = {}; if (name !== undefined) updateData.name = name; if (description !== undefined) updateData.description = description; const response = await makeApiRequest<SingleProjectResponse>( `/v1/private/projects/${projectId}`, { method: 'PATCH', body: JSON.stringify(updateData), }, workspaceName ); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to update project' }], }; } return { content: [ { type: 'text', text: 'Project successfully updated', }, { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } ); server.tool( 'delete-project', 'Delete a project', { projectId: z.string().describe('ID of the project to delete'), workspaceName: z.string().optional().describe('Workspace name to use instead of the default'), }, async args => { const { projectId, workspaceName } = args; const response = await makeApiRequest<void>( `/v1/private/projects/${projectId}`, { method: 'DELETE', }, workspaceName ); return { content: [ { type: 'text', text: !response.error ? 'Successfully deleted project' : response.error || 'Failed to delete project', }, ], }; } ); } // ----------- TRACES TOOLS ----------- if (config.mcpEnableTraceTools) { server.tool( 'list-traces', 'Get a list of traces', { page: z.number().describe('Page number for pagination'), size: z.number().describe('Number of items per page'), projectId: z.string().optional().describe('Project ID to filter traces'), projectName: z.string().optional().describe('Project name to filter traces'), workspaceName: z.string().optional().describe('Workspace name to use instead of the default'), }, async args => { const { page, size, projectId, projectName, workspaceName } = args; let url = `/v1/private/traces?page=${page}&size=${size}`; // Add project filtering - API requires either project_id or project_name if (projectId) { url += `&project_id=${projectId}`; } else if (projectName) { url += `&project_name=${encodeURIComponent(projectName)}`; } else { // If no project specified, we need to find one for the API to work const projectsResponse = await makeApiRequest<ProjectResponse>( `/v1/private/projects?page=1&size=1`, {}, workspaceName ); if ( projectsResponse.data && projectsResponse.data.content && projectsResponse.data.content.length > 0 ) { const firstProject = projectsResponse.data.content[0]; url += `&project_id=${firstProject.id}`; logToFile( `No project specified, using first available: ${firstProject.name} (${firstProject.id})` ); } else { return { content: [ { type: 'text', text: 'Error: No project ID or name provided, and no projects found', }, ], }; } } const response = await makeApiRequest<TraceResponse>(url, {}, workspaceName); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to fetch traces' }], }; } return { content: [ { type: 'text', text: `Found ${response.data.total} traces (showing page ${ response.data.page } of ${Math.ceil(response.data.total / response.data.size)})`, }, { type: 'text', text: JSON.stringify(response.data.content, null, 2), }, ], }; } ); server.tool( 'get-trace-by-id', 'Get a single trace by ID', { traceId: z.string().describe('ID of the trace to fetch'), workspaceName: z.string().optional().describe('Workspace name to use instead of the default'), }, async args => { const { traceId, workspaceName } = args; const response = await makeApiRequest<SingleTraceResponse>( `/v1/private/traces/${traceId}`, {}, workspaceName ); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to fetch trace' }], }; } // Format the response for better readability const formattedResponse: any = { ...response.data }; // Format input/output if they're large if ( formattedResponse.input && typeof formattedResponse.input === 'object' && Object.keys(formattedResponse.input).length > 0 ) { formattedResponse.input = JSON.stringify(formattedResponse.input, null, 2); } if ( formattedResponse.output && typeof formattedResponse.output === 'object' && Object.keys(formattedResponse.output).length > 0 ) { formattedResponse.output = JSON.stringify(formattedResponse.output, null, 2); } return { content: [ { type: 'text', text: `Trace Details for ID: ${traceId}`, }, { type: 'text', text: JSON.stringify(formattedResponse, null, 2), }, ], }; } ); server.tool( 'get-trace-stats', 'Get statistics for traces', { projectId: z.string().optional().describe('Project ID to filter traces'), projectName: z.string().optional().describe('Project name to filter traces'), startDate: z.string().optional().describe('Start date in ISO format (YYYY-MM-DD)'), endDate: z.string().optional().describe('End date in ISO format (YYYY-MM-DD)'), workspaceName: z.string().optional().describe('Workspace name to use instead of the default'), }, async args => { const { projectId, projectName, startDate, endDate, workspaceName } = args; let url = `/v1/private/traces/stats`; // Build query parameters const queryParams = []; // Add project filtering - API requires either project_id or project_name if (projectId) { queryParams.push(`project_id=${projectId}`); } else if (projectName) { queryParams.push(`project_name=${encodeURIComponent(projectName)}`); } else { // If no project specified, we need to find one for the API to work const projectsResponse = await makeApiRequest<ProjectResponse>( `/v1/private/projects?page=1&size=1`, {}, workspaceName ); if ( projectsResponse.data && projectsResponse.data.content && projectsResponse.data.content.length > 0 ) { const firstProject = projectsResponse.data.content[0]; queryParams.push(`project_id=${firstProject.id}`); logToFile( `No project specified, using first available: ${firstProject.name} (${firstProject.id})` ); } else { return { content: [ { type: 'text', text: 'Error: No project ID or name provided, and no projects found', }, ], }; } } if (startDate) queryParams.push(`start_date=${startDate}`); if (endDate) queryParams.push(`end_date=${endDate}`); if (queryParams.length > 0) { url += `?${queryParams.join('&')}`; } const response = await makeApiRequest<TraceStatsResponse>(url, {}, workspaceName); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to fetch trace statistics' }], }; } return { content: [ { type: 'text', text: `Trace Statistics:`, }, { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } ); } // ----------- METRICS TOOLS ----------- if (config.mcpEnableMetricTools) { server.tool( 'get-metrics', 'Get metrics data', { metricName: z.string().optional().describe('Optional metric name to filter'), projectId: z.string().optional().describe('Optional project ID to filter metrics'), projectName: z.string().optional().describe('Optional project name to filter metrics'), startDate: z.string().optional().describe('Start date in ISO format (YYYY-MM-DD)'), endDate: z.string().optional().describe('End date in ISO format (YYYY-MM-DD)'), }, async args => { const { metricName, projectId, projectName, startDate, endDate } = args; let url = `/v1/private/metrics`; const queryParams = []; if (metricName) queryParams.push(`metric_name=${metricName}`); // Add project filtering - API requires either project_id or project_name if (projectId) { queryParams.push(`project_id=${projectId}`); } else if (projectName) { queryParams.push(`project_name=${encodeURIComponent(projectName)}`); } else { // If no project specified, we need to find one for the API to work const projectsResponse = await makeApiRequest<ProjectResponse>( `/v1/private/projects?page=1&size=1` ); if ( projectsResponse.data && projectsResponse.data.content && projectsResponse.data.content.length > 0 ) { const firstProject = projectsResponse.data.content[0]; queryParams.push(`project_id=${firstProject.id}`); logToFile( `No project specified, using first available: ${firstProject.name} (${firstProject.id})` ); } else { return { content: [ { type: 'text', text: 'Error: No project ID or name provided, and no projects found', }, ], }; } } if (startDate) queryParams.push(`start_date=${startDate}`); if (endDate) queryParams.push(`end_date=${endDate}`); if (queryParams.length > 0) { url += `?${queryParams.join('&')}`; } const response = await makeApiRequest<MetricsResponse>(url); if (!response.data) { return { content: [{ type: 'text', text: response.error || 'Failed to fetch metrics' }], }; } return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } ); } // ----------- SERVER CONFIGURATION TOOLS ----------- server.tool( 'get-server-info', 'Get information about the Opik server configuration', { random_string: z.string().optional().describe('Dummy parameter for no-parameter tools'), }, async () => { // Get capabilities based on current configuration const capabilities = getEnabledCapabilities(config); const capabilitiesDescription = getCapabilitiesDescription(config); return { content: [ { type: 'text', text: JSON.stringify( { // API configuration apiBaseUrl: config.apiBaseUrl, isSelfHosted: config.isSelfHosted, hasWorkspace: !!config.workspaceName, workspaceName: config.workspaceName || 'none', // MCP configuration mcpName: config.mcpName, mcpVersion: config.mcpVersion, mcpDefaultWorkspace: config.mcpDefaultWorkspace, enabledTools: { prompts: config.mcpEnablePromptTools, projects: config.mcpEnableProjectTools, traces: config.mcpEnableTraceTools, metrics: config.mcpEnableMetricTools, }, serverVersion: 'v1', // Capabilities information capabilities: capabilities, }, null, 2 ), }, { type: 'text', text: capabilitiesDescription, }, ], }; } ); // Add a new tool for contextual help about Opik capabilities server.tool( 'get-opik-help', "Get contextual help about Opik Comet's capabilities", { topic: z .string() .describe('The topic to get help about (prompts, projects, traces, metrics, or general)'), subtopic: z.string().optional().describe('Optional subtopic for more specific help'), }, async args => { const { topic, subtopic } = args; const capabilities = getEnabledCapabilities(config); // Normalize topic to lowercase const normalizedTopic = topic.toLowerCase(); // Check if the topic is valid if (!['prompts', 'projects', 'traces', 'metrics', 'general'].includes(normalizedTopic)) { return { content: [ { type: 'text', text: `Invalid topic: ${topic}. Valid topics are: prompts, projects, traces, metrics, general.`, }, ], }; } // Get the capabilities for the requested topic const topicCapabilities = capabilities[normalizedTopic as keyof typeof capabilities]; if (!topicCapabilities) { return { content: [ { type: 'text', text: `No information available for topic: ${topic}`, }, ], }; } // If it's a general topic request if (normalizedTopic === 'general') { return { content: [ { type: 'text', text: `Opik Comet General Information:\n\n` + `API Version: ${(topicCapabilities as any).apiVersion}\n` + `Authentication: ${(topicCapabilities as any).authentication}\n` + `Rate Limit: ${(topicCapabilities as any).rateLimit}\n` + `Supported Formats: ${(topicCapabilities as any).supportedFormats?.join(', ') || 'JSON'}`, }, ], }; } // For other topics, check if they're available const typedCapabilities = topicCapabilities as any; if (!typedCapabilities.available) { return { content: [ { type: 'text', text: `${topic} functionality is not enabled in the current configuration.`, }, ], }; } // If a subtopic is specified, provide more specific help if (subtopic) { const normalizedSubtopic = subtopic.toLowerCase(); // Handle different subtopics switch (normalizedSubtopic) { case 'features': return { content: [ { type: 'text', text: `${topic} Features:\n\n` + typedCapabilities.features.map((f: string) => `- ${f}`).join('\n'), }, ], }; case 'limitations': return { content: [ { type: 'text', text: `${topic} Limitations:\n\n` + typedCapabilities.limitations.map((l: string) => `- ${l}`).join('\n'), }, ], }; case 'examples': if (typedCapabilities.examples && typedCapabilities.examples.length > 0) { return { content: [ { type: 'text', text: `${topic} Examples:\n\n` + typedCapabilities.examples.map((e: string) => `- ${e}`).join('\n'), }, ], }; } else { return { content: [ { type: 'text', text: `No examples available for ${topic}.`, }, ], }; } case 'schema': if (typedCapabilities.schema) { return { content: [ { type: 'text', text: `${topic} Schema:\n\n` + JSON.stringify(typedCapabilities.schema, null, 2), }, ], }; } else { return { content: [ { type: 'text', text: `No schema information available for ${topic}.`, }, ], }; } default: // Check if the subtopic is a property of the capabilities if (typedCapabilities[normalizedSubtopic] !== undefined) { const value = typedCapabilities[normalizedSubtopic]; // Format the value based on its type let formattedValue = ''; if (Array.isArray(value)) { formattedValue = value.map((v: any) => `- ${v}`).join('\n'); } else if (typeof value === 'object') { formattedValue = JSON.stringify(value, null, 2); } else { formattedValue = value.toString(); } return { content: [ { type: 'text', text: `${topic} ${subtopic}:\n\n${formattedValue}`, }, ], }; } else { return { content: [ { type: 'text', text: `Invalid subtopic: ${subtopic} for topic: ${topic}`, }, ], }; } } } // If no subtopic is specified, provide general information about the topic let response = `${topic.charAt(0).toUpperCase() + topic.slice(1)} Capabilities:\n\n`; response += 'Features:\n'; typedCapabilities.features.forEach((feature: string) => { response += `- ${feature}\n`; }); response += '\nLimitations:\n'; typedCapabilities.limitations.forEach((limitation: string) => { response += `- ${limitation}\n`; }); // Add topic-specific information switch (normalizedTopic) { case 'prompts': response += `\nVersion Control: ${typedCapabilities.versionControl ? 'Supported' : 'Not Supported'}\n`; response += `Template Format: ${typedCapabilities.templateFormat}\n`; break; case 'projects': response += `\nHierarchy Support: ${typedCapabilities.hierarchySupport ? 'Supported' : 'Not Supported'}\n`; response += `Sharing Support: ${typedCapabilities.sharingSupport ? 'Supported' : 'Not Supported'}\n`; break; case 'traces': response += `\nData Retention: ${typedCapabilities.dataRetention}\n`; response += `Search Capabilities:\n`; typedCapabilities.searchCapabilities.forEach((capability: string) => { response += `- ${capability}\n`; }); break; case 'metrics': response += `\nAvailable Metrics:\n`; typedCapabilities.availableMetrics.forEach((metric: string) => { response += `- ${metric}\n`; }); response += `Custom Metrics Support: ${typedCapabilities.customMetricsSupport ? 'Supported' : 'Not Supported'}\n`; response += `Visualization Support: ${typedCapabilities.visualizationSupport ? 'Supported' : 'Not Supported'}\n`; break; } // Add examples if available if (typedCapabilities.examples && typedCapabilities.examples.length > 0) { response += `\nExamples:\n`; typedCapabilities.examples.forEach((example: string) => { response += `- ${example}\n`; }); } return { content: [ { type: 'text', text: response, }, ], }; } ); // Add a tool for providing contextual examples of how to use Opik Comet server.tool( 'get-opik-examples', "Get examples of how to use Opik Comet's API for specific tasks", { task: z .string() .describe( "The task to get examples for (e.g., 'create prompt', 'analyze traces', 'monitor costs')" ), }, async args => { const { task } = args; const normalizedTask = task.toLowerCase(); // Define example categories and their corresponding examples interface ExampleData { description: string; steps: string[]; code: string; } const examples: Record<string, ExampleData> = { // Prompt-related examples 'create prompt': { description: 'Creating a new prompt template in Opik Comet', steps: [ "1. Use the 'create-prompt' tool to create a new prompt with a name", "2. Use the 'create-prompt-version' tool to add content to the prompt", "3. Retrieve the prompt using 'get-prompt-by-id' to verify it was created", ], code: `// Example: Creating a customer service prompt const promptName = "Customer Service Greeting"; const promptTemplate = "Hello {{customer_name}}, thank you for contacting our support. How can I help you today?"; const commitMessage = "Initial version of customer service greeting"; // First create the prompt const createResult = await mcp.createPrompt({ name: promptName }); const promptId = createResult.id; // Then add content as a version await mcp.createPromptVersion({ name: promptName, template: promptTemplate, commit_message: commitMessage });`, }, 'version prompt': { description: 'Creating a new version of an existing prompt', steps: [ "1. Use the 'list-prompts' tool to find the prompt you want to version", "2. Use the 'create-prompt-version' tool to add a new version with updated content", '3. Include a descriptive commit message explaining the changes', ], code: `// Example: Creating a new version of an existing prompt const promptName = "Customer Service Greeting"; const newTemplate = "Hello {{customer_name}}, thank you for reaching out to our support team. How may I assist you today?"; const commitMessage = "Improved wording for more professional tone"; await mcp.createPromptVersion({ name: promptName, template: newTemplate, commit_message: commitMessage });`, }, // Project-related examples 'create project': { description: 'Creating a new project in Opik Comet', steps: [ "1. Use the 'create-project' tool to create a new project with a name and description", "2. Retrieve the project using 'get-project-by-id' to verify it was created", ], code: `// Example: Creating a new project const projectName = "Customer Support Bot"; const projectDescription = "AI assistant for handling customer support inquiries"; const createResult = await mcp.createProject({ name: projectName, description: projectDescription }); // The project ID will be in the response const projectId = createResult.id;`, }, 'organize traces': { description: 'Organizing traces by project', steps: [ '1. Create projects for different use cases or applications', '2. When recording traces, associate them with the appropriate project', "3. Use the 'list-traces' tool with project filtering to view traces for a specific project", ], code: `// Example: Listing traces for a specific project const projectId = "proj_12345"; const page = 1; const size = 10; const traces = await mcp.listTraces({ page: page, size: size, projectId: projectId }); // Alternatively, you can filter by project name const projectName = "Customer Support Bot"; const tracesByName = await mcp.listTraces({ page: page, size: size, projectName: projectName });`, }, // Trace-related examples 'log trace': { description: 'Logging a trace with the Opik API', steps: [ '1. Create a trace with input and output data', '2. Add spans to the trace to capture detailed steps', '3. Include LLM calls with relevant metadata', ], code: `// Example: Logging a trace with spans // Based on official Opik documentation // Python SDK example (for reference) /* from opik import Opik client = Opik(project_name="Opik client demo") # Create a trace trace = client.trace( name="my_trace", input={"user_question": "Hello, how are you?"}, output={"response": "Comment ça va?"} ) # Add a span trace.span( name="Add prompt template", input={"text": "Hello, how are you?", "prompt_template": "Translate the following text to French: {text}"}, output={"text": "Translate the following text to French: hello, how are you?"} ) # Add an LLM call trace.span( name="llm_call", type="llm", input={"prompt": "Translate the following text to French: hello, how are you?"}, output={"response": "Comment ça va?"} ) */ // JavaScript/TypeScript equivalent using the API const projectId = "proj_12345"; // Create a trace const traceData = { name: "my_trace", project_id: projectId, input: {"user_question": "Hello, how are you?"}, output: {"response": "Comment ça va?"} }; const traceResponse = await fetch("/v1/private/traces", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "YOUR_API_KEY" }, body: JSON.stringify(traceData) }); const trace = await traceResponse.json(); const traceId = trace.id; // Add spans to the trace const span1 = { trace_id: traceId, name: "Add prompt template", input: {"text": "Hello, how are you?", "prompt_template": "Translate the following text to French: {text}"}, output: {"text": "Translate the following text to French: hello, how are you?"} }; const span2 = { trace_id: traceId, name: "llm_call", type: "llm", input: {"prompt": "Translate the following text to French: hello, how are you?"}, output: {"response": "Comment ça va?"} }; await fetch("/v1/private/spans", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "YOUR_API_KEY" }, body: JSON.stringify(span1) }); await fetch("/v1/private/spans", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "YOUR_API_KEY" }, body: JSON.stringify(span2) });`, }, 'analyze traces': { description: 'Analyzing trace data to understand usage patterns', steps: [ "1. Use the 'list-traces' tool to retrieve traces for a specific project", "2. Use the 'get-trace-stats' tool to get aggregated statistics", '3. Filter by date range to analyze trends over time', ], code: `// Example: Getting trace statistics for a date range const projectId = "proj_12345"; const startDate = "2023-01-01"; const endDate = "2023-01-31"; const stats = await mcp.getTraceStats({ projectId: projectId, startDate: startDate, endDate: endDate }); // The response will include aggregated data like: // - Total trace count // - Total token usage // - Cost information // - Daily breakdowns`, }, 'view trace details': { description: 'Viewing detailed information about a specific trace', steps: [ "1. Use the 'list-traces' tool to find the trace you want to examine", "2. Use the 'get-trace-by-id' tool with the trace ID to get detailed information", '3. Analyze the input, output, and metadata to understand the interaction', ], code: `// Example: Getting detailed information about a trace const traceId = "trace_67890"; const traceDetails = await mcp.getTraceById({ traceId: traceId }); // The response will include: // - Input and output data // - Token usage // - Timestamps // - Metadata // - Cost information // - Spans (detailed steps within the trace)`, }, 'annotate trace': { description: 'Annotating a trace with feedback scores', steps: [ "1. Retrieve a trace using 'get-trace-by-id'", '2. Add feedback scores to evaluate the trace quality', '3. Use the feedback for monitoring and improvement', ], code: `// Example: Annotating a trace with feedback scores // Based on Opik documentation // Python SDK example (for reference) /* from opik import Opik client = Opik(project_name="Opik client demo") # Get an existing trace trace = client.get_trace(trace_id="trace_12345") # Add feedback scores trace.add_feedback_score(name="relevance", score=0.8) trace.add_feedback_score(name="accuracy", score=0.9) trace.add_feedback_score(name="helpfulness", score=0.7) */ // JavaScript/TypeScript equivalent using the API const traceId = "trace_12345"; // Add feedback scores to the trace const feedbackData = { scores: [ { name: "relevance", score: 0.8 }, { name: "accuracy", score: 0.9 }, { name: "helpfulness", score: 0.7 } ] }; await fetch(\`/v1/private/traces/\${traceId}/feedback\`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "YOUR_API_KEY" }, body: JSON.stringify(feedbackData) });`, }, // Metrics-related examples 'monitor costs': { description: 'Monitoring costs across projects and time periods', steps: [ "1. Use the 'get-metrics' tool with the 'cost' metric name", '2. Filter by project and date range to focus on specific usage', '3. Analyze trends to identify cost patterns', ], code: `// Example: Monitoring costs for a specific project const projectId = "proj_12345"; const metricName = "cost"; const startDate = "2023-01-01"; const endDate = "2023-01-31"; const costMetrics = await mcp.getMetrics({ metricName: metricName, projectId: projectId, startDate: startDate, endDate: endDate }); // The response will include cost data points over time`, }, 'track token usage': { description: 'Tracking token usage across different models and projects', steps: [ "1. Use the 'get-metrics' tool with token-related metric names", '2. Filter by project and date range to focus on specific usage', '3. Compare prompt tokens vs. completion tokens to optimize usage', ], code: `// Example: Tracking token usage metrics const projectId = "proj_12345"; const startDate = "2023-01-01"; const endDate = "2023-01-31"; // Get total token usage const totalTokens = await mcp.getMetrics({ metricName: "total_tokens", projectId: projectId, startDate: startDate, endDate: endDate }); // Get prompt token usage const promptTokens = await mcp.getMetrics({ metricName: "prompt_tokens", projectId: projectId, startDate: startDate, endDate: endDate }); // Get completion token usage const completionTokens = await mcp.getMetrics({ metricName: "completion_tokens", projectId: projectId, startDate: startDate, endDate: endDate });`, }, 'evaluate llm': { description: "Evaluating LLM outputs using Opik's evaluation metrics", steps: [ '1. Set up evaluation metrics for your use case', '2. Apply metrics to trace data to measure performance', '3. Analyze results to identify areas for improvement', ], code: `// Example: Evaluating LLM outputs with metrics // Based on Opik documentation // Python SDK example (for reference) /* from opik import evaluate from opik.metrics import Hallucination, AnswerRelevance, ContextPrecision # Define evaluation metrics metrics = [ Hallucination(), AnswerRelevance(), ContextPrecision() ] # Evaluate a response result = evaluate( question="What is the capital of France?", answer="Paris is the capital of France.", context=["Paris is the capital and most populous city of France."], metrics=metrics ) # Print results print(result.scores) */ // JavaScript/TypeScript equivalent using the API const evaluationData = { question: "What is the capital of France?", answer: "Paris is the capital of France.", context: ["Paris is the capital and most populous city of France."], metrics: ["hallucination", "answer_relevance", "context_precision"] }; const evaluationResponse = await fetch("/v1/private/evaluate", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "YOUR_API_KEY" }, body: JSON.stringify(evaluationData) }); const evaluationResults = await evaluationResponse.json(); // The response will include scores for each metric`, }, }; // Find the closest matching example let bestMatch: string | null = null; let bestMatchScore = 0; for (const [key /* example */] of Object.entries(examples)) { // Simple matching algorithm - check if the normalized task contains the key if (normalizedTask.includes(key)) { const score = key.length; // Longer matches are better if (score > bestMatchScore) { bestMatch = key; bestMatchScore = score; } } } // If no match found, provide a list of available examples if (!bestMatch) { return { content: [ { type: 'text', text: `No specific example found for "${task}". Available example categories include:\n\n` + Object.keys(examples) .map(key => `- ${key}`) .join('\n') + `\n\nTry asking for one of these specific tasks.`, }, ], }; } // Return the matched example const matchedExample = examples[bestMatch]; return { content: [ { type: 'text', text: `Example: ${bestMatch}\n\n` + `Description: ${matchedExample.description}\n\n` + `Steps:\n${matchedExample.steps.join('\n')}\n\n` + `Code Example:\n\`\`\`javascript\n${matchedExample.code}\n\`\`\``, }, ], }; } ); // Add a tool for providing information about Opik's tracing capabilities server.tool( 'get-opik-tracing-info', "Get information about Opik's tracing capabilities and how to use them", { topic: z .string() .optional() .describe( "Optional specific tracing topic to get information about (e.g., 'spans', 'distributed', 'multimodal', 'annotations')" ), }, async args => { const { topic } = args; // Define tracing information interface TracingInfo { name: string; description: string; key_features: string[]; use_cases: string[]; example?: string; related_topics?: string[]; } const tracingInfo: Record<string, TracingInfo> = { basic: { name: 'Basic Tracing', description: 'Core tracing functionality for recording LLM interactions with input and output data.', key_features: [ 'Record input and output for LLM calls', 'Track token usage and costs', 'Organize traces by project', 'Add metadata to traces', ], use_cases: [ 'Monitoring LLM usage in applications', 'Debugging LLM-based systems', 'Cost tracking and optimization', 'Performance monitoring', ], example: `from opik import Opik # Initialize Opik client client = Opik(project_name="My Project") # Create a trace trace = client.trace( name="simple_query", input={"question": "What is the capital of France?"}, output={"answer": "The capital of France is Paris."}, metadata={"model": "gpt-4", "temperature": 0.7} )`, related_topics: ['spans', 'annotations', 'metadata'], }, spans: { name: 'Spans', description: 'Detailed tracking of steps within a trace to capture the full flow of an LLM interaction.', key_features: [ 'Break down traces into logical steps', 'Track intermediate processing', 'Capture the full chain of operations', 'Measure performance of individual steps', ], use_cases: [ 'Debugging complex LLM pipelines', 'Performance optimization of multi-step processes', 'Visualizing the flow of information', 'Identifying bottlenecks in processing', ], example: `from opik import Opik # Initialize Opik client client = Opik(project_name="RAG Application") # Create a trace trace = client.trace( name="rag_query", input={"question": "What is the capital of France?"}, output={"answer": "The capital of France is Paris."} ) # Add spans for each step in the process trace.span( name="query_processing", input={"raw_query": "What is the capital of France?"}, output={"processed_query": "capital France"} ) trace.span( name="document_retrieval", input={"query": "capital France"}, output={"documents": ["Paris is the capital of France.", "France is a country in Europe."]} ) trace.span( name="llm_generation", type="llm", input={"prompt": "Based on these documents, answer: What is the capital of France?\\n\\nDocuments:\\n- Paris is the capital of France.\\n- France is a country in Europe."}, output={"response": "The capital of France is Paris."} )`, related_topics: ['basic', 'distributed', 'context'], }, distributed: { name: 'Distributed Tracing', description: 'Tracing across multiple services or components in a distributed system.', key_features: [ 'Track LLM interactions across service boundaries', 'Maintain context across different components', 'Visualize end-to-end flows', 'Correlate related traces', ], use_cases: [ 'Microservices architectures with LLMs', 'Complex multi-component AI systems', 'Cross-service debugging', 'End-to-end performance monitoring', ], example: `# Service 1: Initial request handler from opik import Opik, opik_context client = Opik(project_name="Distributed System") # Create a trace trace = client.trace( name="user_request", input={"user_query": "What is the capital of France?"} ) # Get trace headers to pass to the next service trace_headers = opik_context.get_distributed_trace_headers() # Pass trace_headers to Service 2 via API call, message queue, etc. # ----------------------------------------------- # Service 2: Document retrieval service from opik import Opik, opik_context client = Opik(project_name="Distributed System") # Initialize context from received headers opik_context.init_from_headers(received_headers) # This span will be automatically associated with the parent trace with client.span(name="document_retrieval") as span: # Retrieve documents documents = retrieve_documents("capital France") span.update(output={"documents": documents}) # ----------------------------------------------- # Service 3: LLM service from opik import Opik, opik_context client = Opik(project_name="Distributed System") # Initialize context from received headers opik_context.init_from_headers(received_headers) # This span will be automatically associated with the parent trace with client.span(name="llm_generation", type="llm") as span: # Generate response response = generate_llm_response(documents, "What is the capital of France?") span.update(output={"response": response}) # Back in Service 1, update the trace with the final output trace.update(output={"answer": "The capital of France is Paris."})`, related_topics: ['spans', 'context', 'opentelemetry'], }, multimodal: { name: 'Multimodal Tracing', description: 'Tracing for LLM interactions that involve multiple modalities like text, images, and audio.', key_features: [ 'Track inputs and outputs across modalities', 'Support for image, audio, and text data', 'Visualize multimodal interactions', 'Analyze performance across modalities', ], use_cases: [ 'Vision-language models (VLMs)', 'Image generation and analysis', 'Audio transcription and processing', 'Multimodal chatbots and assistants', ], example: `from opik import Opik import base64 # Initialize Opik client client = Opik(project_name="Multimodal App") # Load image as base64 with open("image.jpg", "rb") as f: image_data = base64.b64encode(f.read()).decode("utf-8") # Create a multimodal trace trace = client.trace( name="image_analysis", input={ "image": {"mime_type": "image/jpeg", "data": image_data}, "question": "What objects are in this image?" }, output={"answer": "The image contains a cat sitting on a windowsill."} ) # Add a span for the vision model trace.span( name="vision_model", type="llm", input={"image": {"mime_type": "image/jpeg", "data": image_data}}, output={"description": "A tabby cat sitting on a wooden windowsill looking outside."} ) # Add a span for the text generation trace.span( name="text_generation", type="llm", input={"prompt": "Based on this description: 'A tabby cat sitting on a wooden windowsill looking outside.', answer: What objects are in this image?"}, output={"answer": "The image contains a cat sitting on a windowsill."} )`, related_topics: ['basic', 'spans'], }, annotations: { name: 'Trace Annotations', description: 'Adding feedback scores and annotations to traces for evaluation and improvement.', key_features: [ 'Add qualitative and quantitative feedback', 'Score trace quality and performance', 'Track user satisfaction', 'Support continuous improvement', ], use_cases: [ 'Quality monitoring in production', 'User feedback collection', 'A/B testing of LLM configurations', 'Performance benchmarking', ], example: `from opik import Opik # Initialize Opik client client = Opik(project_name="Customer Support") # Get an existing trace trace = client.get_trace(trace_id="trace_12345") # Add feedback scores trace.add_feedback_score(name="relevance", score=0.8) trace.add_feedback_score(name="accuracy", score=0.9) trace.add_feedback_score(name="helpfulness", score=0.7) # Add a qualitative annotation trace.add_annotation(text="Response was helpful but could be more concise.")`, related_topics: ['basic', 'evaluation'], }, context: { name: 'Context Management', description: 'Managing trace context throughout the execution flow of an application.', key_features: [ 'Automatic context propagation', 'Access current trace and span data', 'Update traces and spans dynamically', 'Support for async and concurrent operations', ], use_cases: [ 'Complex application flows', 'Asynchronous processing', 'Middleware integration', 'Framework integration', ], example: `from opik import Opik, opik_context # Initialize Opik client client = Opik(project_name="Context Demo") # Create a trace with client.trace(name="main_process") as trace: # The trace is automatically set as the current trace # Access current trace data trace_data = opik_context.get_current_trace_data() # Create a span with client.span(name="subprocess") as span: # The span is automatically set as the current span # Access current span data span_data = opik_context.get_current_span_data() # Update the current span opik_context.update_current_span(output={"result": "Processed data"}) # Update the current trace opik_context.update_current_trace(output={"final_result": "Complete"})`, related_topics: ['distributed', 'spans'], }, opentelemetry: { name: 'OpenTelemetry Integration', description: 'Integration with the OpenTelemetry standard for distributed tracing.', key_features: [ 'Compatibility with OpenTelemetry ecosystem', 'Standard-compliant trace format', 'Integration with existing observability tools', 'Support for mixed tracing environments', ], use_cases: [ 'Enterprise observability platforms', 'Integration with existing monitoring systems', 'Standardized tracing across organizations', 'Multi-vendor observability solutions', ], example: `# OpenTelemetry integration example from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opik.integrations.opentelemetry import OpikSpanProcessor # Set up OpenTelemetry tracer_provider = TracerProvider() trace.set_tracer_provider(tracer_provider) # Set up OTLP exporter otlp_exporter = OTLPSpanExporter(endpoint="your-otlp-endpoint") tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter)) # Add Opik span processor opik_processor = OpikSpanProcessor( project_name="OpenTelemetry Demo", api_key="your-opik-api-key" ) tracer_provider.add_span_processor(opik_processor) # Now OpenTelemetry traces will also be sent to Opik tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("main_operation") as span: # This span will be captured by both OpenTelemetry and Opik span.set_attribute("operation.type", "query") # Perform operations result = process_data() span.set_attribute("operation.result", result)`, related_topics: ['distributed', 'context'], }, metadata: { name: 'Trace Metadata', description: 'Adding contextual metadata to traces for richer analysis and filtering.', key_features: [ 'Add custom metadata to traces and spans', 'Tag traces for easier filtering', 'Include environment and version information', 'Track business-specific metrics', ], use_cases: [ 'Environment-specific analysis', 'Version comparison', 'Business impact tracking', 'Custom categorization', ], example: `from opik import Opik # Initialize Opik client client = Opik(project_name="Metadata Demo") # Create a trace with rich metadata trace = client.trace( name="product_search", input={"query": "blue running shoes"}, output={"results": ["Product 1", "Product 2", "Product 3"]}, metadata={ "environment": "production", "version": "1.2.3", "user_segment": "premium", "region": "us-west", "experiment_id": "exp_a1b2c3", "business_metrics": { "conversion_rate": 0.12, "average_order_value": 85.50 } } ) # Add tags for easier filtering trace.add_tags(["search", "product", "footwear"])`, related_topics: ['basic', 'annotations'], }, }; // If a specific topic is requested, return information about that topic if (topic) { const normalizedTopic = topic.toLowerCase(); // Try exact match first if (tracingInfo[normalizedTopic]) { const topicData = tracingInfo[normalizedTopic]; return { content: [ { type: 'text', text: `# ${topicData.name}\n\n` + `**Description:** ${topicData.description}\n\n` + `**Key Features:**\n${topicData.key_features.map(f => `- ${f}`).join('\n')}\n\n` + `**Use Cases:**\n${topicData.use_cases.map(uc => `- ${uc}`).join('\n')}\n\n` + (topicData.example ? `**Example:**\n\`\`\`python\n${topicData.example}\n\`\`\`\n\n` : '') + (topicData.related_topics && topicData.related_topics.length > 0 ? `**Related Topics:** ${topicData.related_topics.map(t => `\`${t}\``).join(', ')}` : ''), }, ], }; } // Try fuzzy match const fuzzyMatches = Object.keys(tracingInfo).filter( k => k.includes(normalizedTopic) || normalizedTopic.includes(k) ); if (fuzzyMatches.length > 0) { return { content: [ { type: 'text', text: `No exact match found for "${topic}". Did you mean one of these?\n\n` + fuzzyMatches.map(m => `- ${tracingInfo[m].name}`).join('\n'), }, ], }; } // No matches return { content: [ { type: 'text', text: `No information found for tracing topic "${topic}". Available topics include:\n\n` + Object.values(tracingInfo) .map(t => `- ${t.name}`) .join('\n'), }, ], }; } // If no specific topic is requested, return an overview of all tracing capabilities return { content: [ { type: 'text', text: `# Opik Tracing Capabilities\n\n` + `Opik provides comprehensive tracing capabilities for LLM applications, allowing you to track, analyze, and improve your AI systems.\n\n` + `## Core Tracing Features\n\n` + Object.values(tracingInfo) .map( t => `### ${t.name}\n${t.description}\n\n**Key Features:**\n${t.key_features.map(f => `- ${f}`).join('\n')}\n` ) .join('\n\n') + `\n\n## Getting Started with Tracing\n\n` + `To start using Opik's tracing capabilities:\n\n` + `1. Install the Opik SDK: \`pip install opik\`\n` + `2. Configure your API key: \`opik configure\`\n` + `3. Create your first trace using the \`trace()\` method\n` + `4. Add spans to capture detailed steps in your process\n` + `5. View your traces in the Opik dashboard\n\n` + `For detailed information about a specific tracing topic, use this tool with the \`topic\` parameter.`, }, ], }; } ); // Main function to start the server export async function main() { logToFile('Starting main function'); // Create the appropriate transport based on configuration let transport; if (config.transport === 'sse') { logToFile(`Creating SSEServerTransport on port ${config.ssePort}`); transport = new SSEServerTransport({ port: config.ssePort || 3001, }); // Explicitly start the SSE transport logToFile('Starting SSE transport'); await transport.start(); } else { logToFile('Creating StdioServerTransport'); transport = new StdioServerTransport(); } // Connect the server to the transport logToFile('Connecting server to transport'); server.connect(transport); logToFile('Transport connection established'); // Log server status if (config.transport === 'sse') { logToFile(`Opik MCP Server running on SSE (port ${config.ssePort})`); } else { logToFile('Opik MCP Server running on stdio'); } logToFile('Main function completed successfully'); // Start heartbeat for keeping the process alive setInterval(() => { logToFile('Heartbeat ping'); }, 5000); } // Start the server main().catch(error => { logToFile(`Error starting server: ${error}`); process.exit(1); });