opik-mcp 2.0.0 → 2.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.
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { buildTraceFilters, callSdk, getOpikApi, getRequestOptions, resolveProjectIdentifier, } from '../utils/opik-sdk.js';
3
3
  import { registerTool } from './registration.js';
4
- import { isoDateSchema, pageSchema, sizeSchema, workspaceNameSchema } from './schema.js';
4
+ import { isoDateSchema, pageSchema, sizeSchema, workspaceNameSchema, } from './schema.js';
5
5
  export const loadTraceTools = (server, options = {}) => {
6
6
  const { includeCoreTools = true, includeExpertActions = true } = options;
7
7
  if (includeCoreTools) {
@@ -18,7 +18,7 @@ export const loadTraceTools = (server, options = {}) => {
18
18
  .describe('Optional project name (alternative to projectId).'),
19
19
  workspaceName: workspaceNameSchema,
20
20
  }, async (args) => {
21
- const { page = 1, size = 10, projectId, projectName, workspaceName } = args;
21
+ const { page = 1, size = 10, projectId, projectName, workspaceName, } = args;
22
22
  const resolved = await resolveProjectIdentifier(projectId, projectName, workspaceName);
23
23
  if (resolved.error) {
24
24
  return {
@@ -30,11 +30,18 @@ export const loadTraceTools = (server, options = {}) => {
30
30
  page,
31
31
  size,
32
32
  ...(resolved.projectId && { projectId: resolved.projectId }),
33
- ...(resolved.projectName && { projectName: resolved.projectName }),
33
+ ...(resolved.projectName && {
34
+ projectName: resolved.projectName,
35
+ }),
34
36
  }, getRequestOptions(workspaceName)));
35
37
  if (!response.data) {
36
38
  return {
37
- content: [{ type: 'text', text: response.error || 'Failed to fetch traces' }],
39
+ content: [
40
+ {
41
+ type: 'text',
42
+ text: response.error || 'Failed to fetch traces',
43
+ },
44
+ ],
38
45
  };
39
46
  }
40
47
  return {
@@ -67,7 +74,9 @@ export const loadTraceTools = (server, options = {}) => {
67
74
  const response = await callSdk(() => api.traces.getTraceById(traceId, getRequestOptions(workspaceName)));
68
75
  if (!response.data) {
69
76
  return {
70
- content: [{ type: 'text', text: response.error || 'Failed to fetch trace' }],
77
+ content: [
78
+ { type: 'text', text: response.error || 'Failed to fetch trace' },
79
+ ],
71
80
  };
72
81
  }
73
82
  const formattedResponse = { ...response.data };
@@ -126,12 +135,19 @@ export const loadTraceTools = (server, options = {}) => {
126
135
  const api = getOpikApi();
127
136
  const response = await callSdk(() => api.traces.getTraceStats({
128
137
  ...(resolved.projectId && { projectId: resolved.projectId }),
129
- ...(resolved.projectName && { projectName: resolved.projectName }),
138
+ ...(resolved.projectName && {
139
+ projectName: resolved.projectName,
140
+ }),
130
141
  ...(filters && { filters }),
131
142
  }, getRequestOptions(workspaceName)));
132
143
  if (!response.data) {
133
144
  return {
134
- content: [{ type: 'text', text: response.error || 'Failed to fetch trace statistics' }],
145
+ content: [
146
+ {
147
+ type: 'text',
148
+ text: response.error || 'Failed to fetch trace statistics',
149
+ },
150
+ ],
135
151
  };
136
152
  }
137
153
  return {
@@ -156,8 +172,14 @@ export const loadTraceTools = (server, options = {}) => {
156
172
  },
157
173
  });
158
174
  registerTool(server, 'get-trace-threads', 'List trace threads (conversation/session groupings) or fetch one thread by ID.', {
159
- projectId: z.string().optional().describe('Optional project ID filter.'),
160
- projectName: z.string().optional().describe('Optional project name filter.'),
175
+ projectId: z
176
+ .string()
177
+ .optional()
178
+ .describe('Optional project ID filter.'),
179
+ projectName: z
180
+ .string()
181
+ .optional()
182
+ .describe('Optional project name filter.'),
161
183
  page: pageSchema,
162
184
  size: sizeSchema(10),
163
185
  threadId: z
@@ -178,17 +200,26 @@ export const loadTraceTools = (server, options = {}) => {
178
200
  ? await callSdk(() => api.traces.getTraceThread({
179
201
  threadId,
180
202
  ...(resolved.projectId && { projectId: resolved.projectId }),
181
- ...(resolved.projectName && { projectName: resolved.projectName }),
203
+ ...(resolved.projectName && {
204
+ projectName: resolved.projectName,
205
+ }),
182
206
  }, getRequestOptions(workspaceName)))
183
207
  : await callSdk(() => api.traces.getTraceThreads({
184
208
  page: page || 1,
185
209
  size: size || 10,
186
210
  ...(resolved.projectId && { projectId: resolved.projectId }),
187
- ...(resolved.projectName && { projectName: resolved.projectName }),
211
+ ...(resolved.projectName && {
212
+ projectName: resolved.projectName,
213
+ }),
188
214
  }, getRequestOptions(workspaceName)));
189
215
  if (!response.data) {
190
216
  return {
191
- content: [{ type: 'text', text: response.error || 'Failed to fetch trace threads' }],
217
+ content: [
218
+ {
219
+ type: 'text',
220
+ text: response.error || 'Failed to fetch trace threads',
221
+ },
222
+ ],
192
223
  };
193
224
  }
194
225
  return {
@@ -217,8 +248,14 @@ export const loadTraceTools = (server, options = {}) => {
217
248
  }
218
249
  if (includeExpertActions) {
219
250
  registerTool(server, 'search-traces', 'Search traces with optional text query, structured filters, and sorting.', {
220
- projectId: z.string().optional().describe('Optional project ID to constrain search.'),
221
- projectName: z.string().optional().describe('Optional project name to constrain search.'),
251
+ projectId: z
252
+ .string()
253
+ .optional()
254
+ .describe('Optional project ID to constrain search.'),
255
+ projectName: z
256
+ .string()
257
+ .optional()
258
+ .describe('Optional project name to constrain search.'),
222
259
  query: z
223
260
  .string()
224
261
  .optional()
@@ -233,7 +270,11 @@ export const loadTraceTools = (server, options = {}) => {
233
270
  .enum(['created_at', 'duration', 'name', 'status'])
234
271
  .optional()
235
272
  .describe('Optional sort field.'),
236
- sortOrder: z.enum(['asc', 'desc']).optional().default('desc').describe('Sort direction.'),
273
+ sortOrder: z
274
+ .enum(['asc', 'desc'])
275
+ .optional()
276
+ .default('desc')
277
+ .describe('Sort direction.'),
237
278
  workspaceName: workspaceNameSchema,
238
279
  }, async (args) => {
239
280
  const { projectId, projectName, query, filters, page, size, sortBy, sortOrder, workspaceName, } = args;
@@ -250,13 +291,20 @@ export const loadTraceTools = (server, options = {}) => {
250
291
  page: page || 1,
251
292
  size: size || 10,
252
293
  ...(resolved.projectId && { projectId: resolved.projectId }),
253
- ...(resolved.projectName && { projectName: resolved.projectName }),
294
+ ...(resolved.projectName && {
295
+ projectName: resolved.projectName,
296
+ }),
254
297
  ...(sdkFilters && { filters: sdkFilters }),
255
298
  ...(sorting && { sorting }),
256
299
  }, getRequestOptions(workspaceName)));
257
300
  if (!response.data) {
258
301
  return {
259
- content: [{ type: 'text', text: response.error || 'Failed to search traces' }],
302
+ content: [
303
+ {
304
+ type: 'text',
305
+ text: response.error || 'Failed to search traces',
306
+ },
307
+ ],
260
308
  };
261
309
  }
262
310
  return {
@@ -289,7 +337,10 @@ export const loadTraceTools = (server, options = {}) => {
289
337
  .min(1)
290
338
  .describe('Feedback metric name, e.g. relevance, accuracy, helpfulness.'),
291
339
  value: z.number().finite().describe('Numeric score value.'),
292
- reason: z.string().optional().describe('Optional reason for this score.'),
340
+ reason: z
341
+ .string()
342
+ .optional()
343
+ .describe('Optional reason for this score.'),
293
344
  source: z
294
345
  .enum(['ui', 'sdk', 'online_scoring'])
295
346
  .optional()
@@ -23,7 +23,7 @@ function parseCsvEnv(value) {
23
23
  }
24
24
  return value
25
25
  .split(',')
26
- .map(part => part.trim())
26
+ .map((part) => part.trim())
27
27
  .filter(Boolean);
28
28
  }
29
29
  function isAccessLogEnabled() {
@@ -32,7 +32,10 @@ function isAccessLogEnabled() {
32
32
  return false;
33
33
  }
34
34
  const normalized = value.trim().toLowerCase();
35
- return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
35
+ return (normalized === '1' ||
36
+ normalized === 'true' ||
37
+ normalized === 'yes' ||
38
+ normalized === 'on');
36
39
  }
37
40
  function formatAccessLogLine(req, res, durationMs, hasAuthHeader) {
38
41
  const timestamp = new Date().toISOString();
@@ -54,7 +57,9 @@ function getMcpMethodFromRequest(req) {
54
57
  if (!method) {
55
58
  return {};
56
59
  }
57
- const toolName = method === 'tools/call' && body.params && typeof body.params.name === 'string'
60
+ const toolName = method === 'tools/call' &&
61
+ body.params &&
62
+ typeof body.params.name === 'string'
58
63
  ? body.params.name
59
64
  : undefined;
60
65
  return { method, toolName };
@@ -120,7 +125,7 @@ export class StreamableHttpTransport {
120
125
  this.port = options.port || 3001;
121
126
  this.host = options.host || process.env.STREAMABLE_HTTP_HOST || '127.0.0.1';
122
127
  this.app = createMcpExpressApp({ host: this.host });
123
- this.mcpTransport.onerror = error => {
128
+ this.mcpTransport.onerror = (error) => {
124
129
  logToFile(`Streamable HTTP transport error: ${error instanceof Error ? error.stack || error.message : String(error)}`);
125
130
  };
126
131
  const allowedOrigins = parseCsvEnv(process.env.STREAMABLE_HTTP_CORS_ORIGINS);
@@ -128,7 +133,12 @@ export class StreamableHttpTransport {
128
133
  this.app.use(cors({
129
134
  origin: allowedOrigins,
130
135
  methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
131
- allowedHeaders: ['content-type', 'authorization', 'x-api-key', 'comet-workspace'],
136
+ allowedHeaders: [
137
+ 'content-type',
138
+ 'authorization',
139
+ 'x-api-key',
140
+ 'comet-workspace',
141
+ ],
132
142
  credentials: false,
133
143
  }));
134
144
  }
@@ -151,7 +161,10 @@ export class StreamableHttpTransport {
151
161
  const response = { status: 'ok' };
152
162
  res.json(response);
153
163
  });
154
- this.app.get(['/.well-known/oauth-protected-resource', '/.well-known/oauth-protected-resource/mcp'], (req, res) => {
164
+ this.app.get([
165
+ '/.well-known/oauth-protected-resource',
166
+ '/.well-known/oauth-protected-resource/mcp',
167
+ ], (req, res) => {
155
168
  const baseUrl = getBaseUrl(req);
156
169
  const metadata = {
157
170
  resource: `${baseUrl}/mcp`,
@@ -166,7 +179,10 @@ export class StreamableHttpTransport {
166
179
  error: 'unsupported_auth_flow',
167
180
  message: 'OAuth authorization server endpoints are not implemented; authenticate with Authorization: Bearer <OPIK_API_KEY>.',
168
181
  };
169
- this.app.get(['/.well-known/oauth-authorization-server', '/.well-known/openid-configuration'], (_req, res) => {
182
+ this.app.get([
183
+ '/.well-known/oauth-authorization-server',
184
+ '/.well-known/openid-configuration',
185
+ ], (_req, res) => {
170
186
  res.status(404).json(oauthNotSupportedResponse);
171
187
  });
172
188
  this.app.post('/register', (_req, res) => {
@@ -175,7 +191,8 @@ export class StreamableHttpTransport {
175
191
  this.app.all('/mcp', async (req, res) => {
176
192
  try {
177
193
  const { method, toolName } = getMcpMethodFromRequest(req);
178
- if (isRemoteAuthRequired() && !isMethodAllowedWithoutAuth(method || '', toolName)) {
194
+ if (isRemoteAuthRequired() &&
195
+ !isMethodAllowedWithoutAuth(method || '', toolName)) {
179
196
  const auth = authenticateRemoteRequest(req.headers);
180
197
  if (!auth.ok) {
181
198
  const errorResponse = {
@@ -282,7 +299,7 @@ export class StreamableHttpTransport {
282
299
  await this.mcpTransport.close();
283
300
  return new Promise((resolve, reject) => {
284
301
  if (this.server) {
285
- this.server.close(err => {
302
+ this.server.close((err) => {
286
303
  if (err) {
287
304
  reject(err);
288
305
  return;
@@ -132,7 +132,14 @@ export const opikCapabilities = {
132
132
  'Filter by trace type',
133
133
  'Basic text search in trace names',
134
134
  ],
135
- filterOptions: ['project_id', 'project_name', 'start_date', 'end_date', 'name', 'type'],
135
+ filterOptions: [
136
+ 'project_id',
137
+ 'project_name',
138
+ 'start_date',
139
+ 'end_date',
140
+ 'name',
141
+ 'type',
142
+ ],
136
143
  schema: {
137
144
  trace: {
138
145
  id: 'string',
@@ -320,11 +327,11 @@ export function getCapabilitiesDescription(config) {
320
327
  }
321
328
  description += `${key.charAt(0).toUpperCase() + key.slice(1)}:\n`;
322
329
  description += 'Features:\n';
323
- cap.features.forEach(feature => {
330
+ cap.features.forEach((feature) => {
324
331
  description += `- ${feature}\n`;
325
332
  });
326
333
  description += '\nLimitations:\n';
327
- cap.limitations.forEach(limitation => {
334
+ cap.limitations.forEach((limitation) => {
328
335
  description += `- ${limitation}\n`;
329
336
  });
330
337
  description += '\n';
@@ -391,7 +391,7 @@ export function getExampleForTask(task) {
391
391
  const keyWords = key.split('-');
392
392
  const taskWords = normalizedTask.split(/\s+/);
393
393
  // Check if all key words are in the task
394
- const allWordsMatch = keyWords.every(word => taskWords.some(taskWord => taskWord.includes(word) || word.includes(taskWord)));
394
+ const allWordsMatch = keyWords.every((word) => taskWords.some((taskWord) => taskWord.includes(word) || word.includes(taskWord)));
395
395
  if (allWordsMatch) {
396
396
  return example;
397
397
  }
@@ -410,5 +410,5 @@ export function getExampleForTask(task) {
410
410
  * @returns Array of task titles
411
411
  */
412
412
  export function getAllExampleTasks() {
413
- return Object.values(examples).map(example => example.title);
413
+ return Object.values(examples).map((example) => example.title);
414
414
  }
@@ -8,7 +8,10 @@ function getEffectiveApiKey() {
8
8
  }
9
9
  function getEffectiveWorkspaceName() {
10
10
  const context = getRequestContext();
11
- return context?.workspaceName || config.workspaceName || config.mcpDefaultWorkspace || 'default';
11
+ return (context?.workspaceName ||
12
+ config.workspaceName ||
13
+ config.mcpDefaultWorkspace ||
14
+ 'default');
12
15
  }
13
16
  function getOpikClient() {
14
17
  const apiKey = getEffectiveApiKey();
@@ -51,7 +54,9 @@ export async function resolveProjectIdentifier(projectId, projectName, workspace
51
54
  page: 1,
52
55
  size: 1,
53
56
  }, getRequestOptions(workspaceName)));
54
- if (!response.data || !response.data.content || response.data.content.length === 0) {
57
+ if (!response.data ||
58
+ !response.data.content ||
59
+ response.data.content.length === 0) {
55
60
  return { error: response.error || 'No projects found' };
56
61
  }
57
62
  return { projectId: response.data.content[0].id };
@@ -62,7 +62,9 @@ export function isMethodAllowedWithoutAuth(method, toolName) {
62
62
  if (ONBOARDING_ALLOWED_NO_AUTH_METHODS.has(method)) {
63
63
  return true;
64
64
  }
65
- if (method === 'tools/call' && toolName && ONBOARDING_SAFE_TOOLS.has(toolName)) {
65
+ if (method === 'tools/call' &&
66
+ toolName &&
67
+ ONBOARDING_SAFE_TOOLS.has(toolName)) {
66
68
  return true;
67
69
  }
68
70
  return false;
@@ -157,7 +159,11 @@ export async function validateRemoteAuth(context) {
157
159
  valid: false,
158
160
  expiresAt: now + INVALID_CACHE_TTL_MS,
159
161
  });
160
- return { ok: false, status: 401, message: 'Invalid API key or workspace.' };
162
+ return {
163
+ ok: false,
164
+ status: 401,
165
+ message: 'Invalid API key or workspace.',
166
+ };
161
167
  }
162
168
  return {
163
169
  ok: false,
@@ -13,7 +13,13 @@ export function getTracingInfo(topic) {
13
13
  return {
14
14
  title: 'Opik Tracing Capabilities',
15
15
  description: 'Opik provides comprehensive tracing capabilities to help you understand and analyze your LLM applications. Traces capture the full context of LLM interactions, including inputs, outputs, and metadata.',
16
- availableTopics: ['traces', 'spans', 'feedback', 'search', 'visualization'],
16
+ availableTopics: [
17
+ 'traces',
18
+ 'spans',
19
+ 'feedback',
20
+ 'search',
21
+ 'visualization',
22
+ ],
17
23
  };
18
24
  }
19
25
  // Return information based on the requested topic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opik-mcp",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "MCP server to interact with Opik - Enables automated prompt optimization",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,12 +50,14 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@jest/globals": "^30.2.0",
53
+ "@smithery/cli": "^4.1.8",
53
54
  "@types/cors": "^2.8.19",
54
55
  "@types/express": "^5.0.6",
55
56
  "@types/jest": "^30.0.0",
56
- "@types/node": "^22.13.9",
57
+ "@types/node": "^25.3.3",
57
58
  "@typescript-eslint/eslint-plugin": "^7.4.0",
58
59
  "@typescript-eslint/parser": "^7.4.0",
60
+ "esbuild": "^0.27.3",
59
61
  "eslint": "^8.57.0",
60
62
  "eslint-config-prettier": "^10.1.8",
61
63
  "eslint-plugin-prettier": "^5.5.5",