opik-mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import fs from 'fs';
3
+ // Import configuration
4
+ import configImport from './config.js';
5
+ const config = configImport;
6
+ // Setup file-based logging
7
+ const logFile = '/tmp/opik-mcp.log';
8
+ // Define logging functions
9
+ function logToFile(message) {
10
+ try {
11
+ const timestamp = new Date().toISOString();
12
+ fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`);
13
+ }
14
+ catch (error) {
15
+ // Silently fail if we can't write to the log file
16
+ }
17
+ }
18
+ /**
19
+ * Create a configured MCP server instance
20
+ * This function is used by both the CLI and the original index.ts
21
+ */
22
+ export function createMcpServer() {
23
+ logToFile('Creating MCP server');
24
+ // Import all the handlers and capabilities from index.js
25
+ // Requires a refactor of index.js to export these as separate modules
26
+ // For now, create a minimal configuration
27
+ const server = new McpServer({
28
+ name: config.mcpName || 'Opik MCP',
29
+ version: config.mcpVersion || '0.0.1',
30
+ }, {
31
+ capabilities: {
32
+ // Minimal capabilities for demo
33
+ mcp__get_server_info: {
34
+ name: 'get_server_info',
35
+ description: 'Get information about the Opik server configuration',
36
+ parameter_schema: {
37
+ type: 'object',
38
+ additionalProperties: false,
39
+ properties: {
40
+ random_string: {
41
+ type: 'string',
42
+ description: 'Dummy parameter for no-parameter tools',
43
+ },
44
+ },
45
+ },
46
+ handler: async () => {
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: `# Opik MCP Server
52
+
53
+ Server Name: ${config.mcpName || 'Opik MCP'}
54
+ Version: ${config.mcpVersion || '0.0.1'}
55
+ API Base URL: ${config.apiBaseUrl || 'Not configured'}
56
+ Self-hosted: ${config.isSelfHosted ? 'Yes' : 'No'}
57
+ Workspace: ${config.workspaceName || 'None'}
58
+
59
+ This is a minimal configuration for demo purposes.`,
60
+ },
61
+ ],
62
+ };
63
+ },
64
+ },
65
+ },
66
+ });
67
+ return server;
68
+ }
69
+ /**
70
+ * Start the MCP server with the provided transport
71
+ */
72
+ export async function startServerWithTransport(transport) {
73
+ logToFile('Starting server with provided transport');
74
+ const server = createMcpServer();
75
+ // Add explicit error handlers to the transport
76
+ transport.onerror = error => {
77
+ logToFile(`Transport error: ${error.message}`);
78
+ console.error(`Transport error: ${error.message}`);
79
+ };
80
+ transport.onclose = () => {
81
+ logToFile('Transport connection closed');
82
+ console.log('Transport connection closed');
83
+ };
84
+ try {
85
+ // Connect server to transport
86
+ await server.connect(transport);
87
+ logToFile('Opik MCP Server successfully connected and running');
88
+ console.log('Opik MCP Server successfully connected and running');
89
+ return server;
90
+ }
91
+ catch (error) {
92
+ logToFile(`Error in server connection: ${error?.message || error}`);
93
+ console.error(`Error in server connection: ${error?.message || error}`);
94
+ throw error;
95
+ }
96
+ }
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Test client for Opik API
3
+ * This helps test API calls and view the responses
4
+ */
5
+ // Load environment variables first
6
+ import './utils/env.js';
7
+ import config from './config.js';
8
+ // Simple test client for MCP
9
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
10
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
11
+ /**
12
+ * Make an API request to the Opik API
13
+ */
14
+ async function makeApiRequest(path, options = {}) {
15
+ // Prepare headers based on configuration
16
+ // According to the documentation:
17
+ // - authorization header should NOT include "Bearer" prefix
18
+ // - Comet-Workspace header should be included for cloud installations
19
+ const API_HEADERS = {
20
+ Accept: 'application/json',
21
+ 'Content-Type': 'application/json',
22
+ authorization: config.apiKey,
23
+ };
24
+ // Add workspace header for cloud version (and on-premise installations of Comet platform)
25
+ if (config.workspaceName) {
26
+ API_HEADERS['Comet-Workspace'] = config.workspaceName;
27
+ if (config.debugMode) {
28
+ console.log(`Using workspace: ${config.workspaceName}`);
29
+ }
30
+ }
31
+ const url = `${config.apiBaseUrl}${path}`;
32
+ console.log(`Making API request to: ${url}`);
33
+ if (config.debugMode) {
34
+ console.log('Headers:', JSON.stringify(API_HEADERS, null, 2));
35
+ }
36
+ try {
37
+ const response = await fetch(url, {
38
+ ...options,
39
+ headers: {
40
+ ...API_HEADERS,
41
+ ...options.headers,
42
+ },
43
+ });
44
+ // Get the response body text
45
+ const responseText = await response.text();
46
+ let responseData = null;
47
+ // Try to parse the response as JSON
48
+ try {
49
+ responseData = JSON.parse(responseText);
50
+ }
51
+ catch (e) {
52
+ // If it's not valid JSON, use the raw text
53
+ responseData = responseText;
54
+ }
55
+ if (!response.ok) {
56
+ return {
57
+ data: null,
58
+ error: `HTTP error! status: ${response.status} ${JSON.stringify(responseData)}`,
59
+ };
60
+ }
61
+ return {
62
+ data: responseData,
63
+ error: null,
64
+ };
65
+ }
66
+ catch (error) {
67
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
68
+ console.error('Error making API request:', error);
69
+ return {
70
+ data: null,
71
+ error: errorMessage,
72
+ };
73
+ }
74
+ }
75
+ /**
76
+ * Test functions for different API endpoints
77
+ */
78
+ const api = {
79
+ // Simple test endpoint
80
+ async testConnection() {
81
+ // Test with the projects endpoint which we know works
82
+ return makeApiRequest('/v1/private/projects');
83
+ },
84
+ // Workspaces
85
+ async listWorkspaces() {
86
+ return makeApiRequest('/v1/private/workspaces');
87
+ },
88
+ // Projects
89
+ async listProjects(page = 1, size = 10) {
90
+ return makeApiRequest(`/v1/private/projects?page=${page}&size=${size}`);
91
+ },
92
+ async getProject(projectId) {
93
+ return makeApiRequest(`/v1/private/projects/${projectId}`);
94
+ },
95
+ // Traces
96
+ async listTraces(page = 1, size = 10, projectId) {
97
+ let url = `/v1/private/traces?page=${page}&size=${size}`;
98
+ if (projectId)
99
+ url += `&project_id=${projectId}`;
100
+ return makeApiRequest(url);
101
+ },
102
+ async getTrace(traceId) {
103
+ return makeApiRequest(`/v1/private/traces/${traceId}`);
104
+ },
105
+ async getTraceStats(projectId) {
106
+ let url = `/v1/private/traces/stats`;
107
+ if (projectId)
108
+ url += `?project_id=${projectId}`;
109
+ return makeApiRequest(url);
110
+ },
111
+ // Prompts
112
+ async listPrompts(page = 1, size = 10) {
113
+ return makeApiRequest(`/v1/private/prompts?page=${page}&size=${size}`);
114
+ },
115
+ // Metrics
116
+ async getMetrics(metricName, projectId) {
117
+ const params = [];
118
+ if (metricName)
119
+ params.push(`metric_name=${metricName}`);
120
+ if (projectId)
121
+ params.push(`project_id=${projectId}`);
122
+ let url = `/v1/private/metrics`;
123
+ if (params.length > 0) {
124
+ url += `?${params.join('&')}`;
125
+ }
126
+ return makeApiRequest(url);
127
+ },
128
+ };
129
+ /**
130
+ * Find workspaces and projects with traces
131
+ */
132
+ async function findWorkspacesAndProjects() {
133
+ const results = {
134
+ availableWorkspaces: [],
135
+ projectsWithTraces: [],
136
+ };
137
+ try {
138
+ // Try the predefined workspace first (from config)
139
+ if (config.workspaceName) {
140
+ console.log(`Using predefined workspace: ${config.workspaceName}`);
141
+ // List projects in this workspace
142
+ const projectsResponse = await api.listProjects();
143
+ if (projectsResponse.data && projectsResponse.data.content) {
144
+ // Check each project for traces
145
+ for (const project of projectsResponse.data.content) {
146
+ const tracesResponse = await api.listTraces(1, 1, project.id);
147
+ if (tracesResponse.data && tracesResponse.data.total > 0) {
148
+ results.projectsWithTraces.push({
149
+ workspaceName: config.workspaceName,
150
+ projectId: project.id,
151
+ projectName: project.name,
152
+ traceCount: tracesResponse.data.total,
153
+ });
154
+ }
155
+ }
156
+ }
157
+ }
158
+ else {
159
+ // If no workspace is defined, try to discover available workspaces
160
+ console.log('No predefined workspace, attempting to discover workspaces...');
161
+ // This endpoint may not exist, but we can try
162
+ const workspacesResponse = await api.listWorkspaces();
163
+ if (workspacesResponse.data) {
164
+ results.availableWorkspaces = workspacesResponse.data;
165
+ // Try each workspace
166
+ for (const workspace of results.availableWorkspaces) {
167
+ // Temporarily set workspace for API calls
168
+ const originalWorkspace = config.workspaceName;
169
+ config.workspaceName = workspace.name;
170
+ // List projects in this workspace
171
+ const projectsResponse = await api.listProjects();
172
+ if (projectsResponse.data && projectsResponse.data.content) {
173
+ // Check each project for traces
174
+ for (const project of projectsResponse.data.content) {
175
+ const tracesResponse = await api.listTraces(1, 1, project.id);
176
+ if (tracesResponse.data && tracesResponse.data.total > 0) {
177
+ results.projectsWithTraces.push({
178
+ workspaceName: workspace.name,
179
+ projectId: project.id,
180
+ projectName: project.name,
181
+ traceCount: tracesResponse.data.total,
182
+ });
183
+ }
184
+ }
185
+ }
186
+ // Restore original workspace setting
187
+ config.workspaceName = originalWorkspace;
188
+ }
189
+ }
190
+ else {
191
+ // Try with "default" workspace
192
+ const originalWorkspace = config.workspaceName;
193
+ config.workspaceName = 'default';
194
+ // List projects in default workspace
195
+ const projectsResponse = await api.listProjects();
196
+ if (projectsResponse.data && projectsResponse.data.content) {
197
+ // Check each project for traces
198
+ for (const project of projectsResponse.data.content) {
199
+ const tracesResponse = await api.listTraces(1, 1, project.id);
200
+ if (tracesResponse.data && tracesResponse.data.total > 0) {
201
+ results.projectsWithTraces.push({
202
+ workspaceName: 'default',
203
+ projectId: project.id,
204
+ projectName: project.name,
205
+ traceCount: tracesResponse.data.total,
206
+ });
207
+ }
208
+ }
209
+ }
210
+ // Restore original workspace setting
211
+ config.workspaceName = originalWorkspace;
212
+ }
213
+ }
214
+ }
215
+ catch (error) {
216
+ console.error('Error finding workspaces and projects:', error);
217
+ }
218
+ return results;
219
+ }
220
+ /**
221
+ * Run tests for all API endpoints
222
+ */
223
+ async function runApiTests() {
224
+ console.log('šŸ” Testing Opik API with the following configuration:');
225
+ console.log(`- API Base URL: ${config.apiBaseUrl}`);
226
+ console.log(`- Self-hosted: ${config.isSelfHosted ? 'Yes' : 'No'}`);
227
+ console.log(`- Workspace: ${config.workspaceName || 'None'}`);
228
+ // Set debug mode for this run
229
+ config.debugMode = true;
230
+ console.log(`- Debug mode: ${config.debugMode ? 'Enabled' : 'Disabled'}`);
231
+ console.log('\n');
232
+ try {
233
+ // Find workspaces and projects with traces
234
+ console.log('šŸ”Ž FINDING WORKSPACES AND PROJECTS WITH TRACES');
235
+ const discovery = await findWorkspacesAndProjects();
236
+ if (discovery.projectsWithTraces.length > 0) {
237
+ console.log(`\nFound ${discovery.projectsWithTraces.length} projects with traces:`);
238
+ discovery.projectsWithTraces.forEach((project, index) => {
239
+ console.log(`${index + 1}. Workspace: ${project.workspaceName}, Project: ${project.projectName} (${project.projectId}), Traces: ${project.traceCount}`);
240
+ });
241
+ // Look for the 'Therapist Chat' project first
242
+ let testProject = discovery.projectsWithTraces.find(p => p.projectName === 'Therapist Chat');
243
+ // If not found, use the first project with traces
244
+ if (!testProject) {
245
+ testProject = discovery.projectsWithTraces[0];
246
+ }
247
+ console.log(`\nUsing project "${testProject.projectName}" in workspace "${testProject.workspaceName}" for testing`);
248
+ // Set the workspace for testing
249
+ const originalWorkspace = config.workspaceName;
250
+ config.workspaceName = testProject.workspaceName;
251
+ // Test basic connection first
252
+ console.log('\nšŸ”Œ TESTING CONNECTION');
253
+ const connectionTest = await api.testConnection();
254
+ if (connectionTest.data) {
255
+ console.log('Connection successful');
256
+ if (connectionTest.data.total) {
257
+ console.log(`Found ${connectionTest.data.total} projects`);
258
+ }
259
+ console.log(JSON.stringify(connectionTest.data, null, 2));
260
+ }
261
+ else {
262
+ console.log(`Connection failed: ${connectionTest.error}`);
263
+ }
264
+ console.log('\n');
265
+ // Continue with other tests only if connection was successful
266
+ if (connectionTest.error) {
267
+ console.error('Cannot continue tests due to connection issues');
268
+ return;
269
+ }
270
+ // Test traces for the selected project
271
+ console.log('\nšŸ” TESTING TRACES API');
272
+ console.log(`Using project ID: ${testProject.projectId} for traces`);
273
+ const tracesResponse = await api.listTraces(1, 10, testProject.projectId);
274
+ if (tracesResponse.data) {
275
+ console.log(`Found ${tracesResponse.data.total} traces`);
276
+ console.log(JSON.stringify(tracesResponse.data, null, 2));
277
+ // If there are traces, get details for the first one
278
+ if (tracesResponse.data.content && tracesResponse.data.content.length > 0) {
279
+ const traceId = tracesResponse.data.content[0].id;
280
+ console.log(`\nGetting details for trace: ${traceId}`);
281
+ const traceDetail = await api.getTrace(traceId);
282
+ console.log(JSON.stringify(traceDetail.data, null, 2));
283
+ }
284
+ }
285
+ else {
286
+ console.error('Error fetching traces:', tracesResponse.error);
287
+ }
288
+ // Restore original workspace setting
289
+ config.workspaceName = originalWorkspace;
290
+ }
291
+ else {
292
+ console.log('\nNo projects with traces found. Continuing with general tests...');
293
+ // Default test flow...
294
+ // Test Projects API
295
+ console.log('šŸ“ TESTING PROJECTS API');
296
+ const projectsResponse = await api.listProjects();
297
+ let firstProjectId = null;
298
+ if (projectsResponse.data) {
299
+ console.log(`Found ${projectsResponse.data.total} projects`);
300
+ console.log(JSON.stringify(projectsResponse.data, null, 2));
301
+ // If there are projects, get details for the first one
302
+ if (projectsResponse.data.content && projectsResponse.data.content.length > 0) {
303
+ firstProjectId = projectsResponse.data.content[0].id;
304
+ console.log(`\nGetting details for project: ${firstProjectId}`);
305
+ const projectDetail = await api.getProject(firstProjectId);
306
+ console.log(JSON.stringify(projectDetail.data, null, 2));
307
+ }
308
+ }
309
+ else {
310
+ console.error('Error fetching projects:', projectsResponse.error);
311
+ }
312
+ // Test Trace Stats API
313
+ console.log('\nšŸ“Š TESTING TRACE STATS API');
314
+ if (firstProjectId) {
315
+ console.log(`Using project ID: ${firstProjectId} for trace stats`);
316
+ const traceStatsResponse = await api.getTraceStats(firstProjectId);
317
+ if (traceStatsResponse.data) {
318
+ console.log('Trace statistics:');
319
+ console.log(JSON.stringify(traceStatsResponse.data, null, 2));
320
+ }
321
+ else {
322
+ console.error('Error fetching trace stats:', traceStatsResponse.error);
323
+ }
324
+ }
325
+ else {
326
+ console.log('No projects available to test trace stats API');
327
+ }
328
+ // Test Prompts API
329
+ console.log('\nšŸ“ TESTING PROMPTS API');
330
+ const promptsResponse = await api.listPrompts();
331
+ if (promptsResponse.data) {
332
+ console.log(`Found ${promptsResponse.data.total} prompts`);
333
+ console.log(JSON.stringify(promptsResponse.data, null, 2));
334
+ }
335
+ else {
336
+ console.error('Error fetching prompts:', promptsResponse.error);
337
+ }
338
+ // Test Metrics API
339
+ console.log('\nšŸ“ˆ TESTING METRICS API');
340
+ const metricsResponse = await api.getMetrics();
341
+ if (metricsResponse.data) {
342
+ console.log('Metrics:');
343
+ console.log(JSON.stringify(metricsResponse.data, null, 2));
344
+ }
345
+ else {
346
+ console.error('Error fetching metrics:', metricsResponse.error);
347
+ }
348
+ // Test with specific 'Therapist Chat' project
349
+ console.log('\n🧠 TESTING WITH THERAPIST CHAT PROJECT');
350
+ const therapistChatProjectId = '0194fdd8-de46-73c4-b0ac-381cec5fbf5c';
351
+ // Get project details
352
+ console.log(`\nGetting details for Therapist Chat project: ${therapistChatProjectId}`);
353
+ const therapistChatProject = await api.getProject(therapistChatProjectId);
354
+ if (therapistChatProject.data) {
355
+ console.log(JSON.stringify(therapistChatProject.data, null, 2));
356
+ // Get traces for this project
357
+ console.log('\nGetting traces for Therapist Chat project:');
358
+ const therapistChatTraces = await api.listTraces(1, 10, therapistChatProjectId);
359
+ if (therapistChatTraces.data) {
360
+ console.log(`Found ${therapistChatTraces.data.total} traces`);
361
+ console.log(JSON.stringify(therapistChatTraces.data, null, 2));
362
+ // Get details for first trace if available
363
+ if (therapistChatTraces.data.content && therapistChatTraces.data.content.length > 0) {
364
+ const traceId = therapistChatTraces.data.content[0].id;
365
+ console.log(`\nGetting details for trace: ${traceId}`);
366
+ const traceDetail = await api.getTrace(traceId);
367
+ console.log(JSON.stringify(traceDetail.data, null, 2));
368
+ }
369
+ }
370
+ else {
371
+ console.error('Error fetching Therapist Chat traces:', therapistChatTraces.error);
372
+ }
373
+ }
374
+ else {
375
+ console.error('Error fetching Therapist Chat project:', therapistChatProject.error);
376
+ }
377
+ }
378
+ }
379
+ catch (err) {
380
+ console.error('Error running API tests:', err);
381
+ }
382
+ }
383
+ async function main() {
384
+ console.log('Starting MCP test client...');
385
+ // Create a transport that runs our server
386
+ const transport = new StdioClientTransport({
387
+ command: 'node',
388
+ args: ['build/index.js', '--debug', 'true'],
389
+ });
390
+ // Add event handlers for lifecycle events
391
+ transport.onerror = (error) => {
392
+ console.error('Transport error:', error);
393
+ };
394
+ transport.onclose = () => {
395
+ console.log('Transport connection closed');
396
+ };
397
+ // Create the client
398
+ const client = new Client({
399
+ name: 'test-client',
400
+ version: '1.0.0',
401
+ }, {
402
+ capabilities: {
403
+ tools: {}, // We're interested in tools
404
+ },
405
+ });
406
+ try {
407
+ // Connect to the server
408
+ console.log('Connecting to MCP server...');
409
+ await client.connect(transport);
410
+ console.log('Connected successfully!');
411
+ // List available tools
412
+ console.log('Requesting tool list...');
413
+ const tools = await client.listTools();
414
+ console.log('Available tools:');
415
+ console.log(JSON.stringify(tools, null, 2));
416
+ // Close the connection
417
+ await client.close();
418
+ console.log('Connection closed.');
419
+ }
420
+ catch (error) {
421
+ console.error('Error:', error);
422
+ }
423
+ }
424
+ // ESM-compatible entry point detection
425
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
426
+ // Run the tests when this file is executed directly
427
+ if (isMainModule) {
428
+ runApiTests().then(() => {
429
+ console.log('\nāœ… API tests completed');
430
+ });
431
+ }
432
+ main().catch(error => {
433
+ console.error('Fatal error:', error);
434
+ process.exit(1);
435
+ });
436
+ export { api, makeApiRequest };