opik-mcp 2.0.0 → 2.0.2

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,6 +1,7 @@
1
1
  import { runWithRequestContext } from '../utils/request-context.js';
2
2
  import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import config from '../config.js';
4
+ import { DEPRECATION_DESCRIPTION_SUFFIX, DEPRECATION_RESPONSE_BLOCK, } from '../utils/deprecation.js';
4
5
  const MISSING_API_KEY_MESSAGE = [
5
6
  'This Opik MCP request requires an API key.',
6
7
  'Set OPIK_API_KEY in the environment where the server runs,',
@@ -11,7 +12,7 @@ const MISSING_API_KEY_MESSAGE = [
11
12
  function inferAnnotations(name) {
12
13
  const readPrefixes = ['get-', 'list-', 'search-', 'read-'];
13
14
  const mutatePrefixes = ['create-', 'delete-', 'update-', 'add-', 'save-'];
14
- if (readPrefixes.some(prefix => name.startsWith(prefix))) {
15
+ if (readPrefixes.some((prefix) => name.startsWith(prefix))) {
15
16
  return {
16
17
  readOnlyHint: true,
17
18
  destructiveHint: false,
@@ -19,7 +20,7 @@ function inferAnnotations(name) {
19
20
  openWorldHint: false,
20
21
  };
21
22
  }
22
- if (mutatePrefixes.some(prefix => name.startsWith(prefix))) {
23
+ if (mutatePrefixes.some((prefix) => name.startsWith(prefix))) {
23
24
  return {
24
25
  readOnlyHint: false,
25
26
  destructiveHint: name.startsWith('delete-'),
@@ -33,7 +34,7 @@ function withRequestContext(handler, requiresApiKey = true) {
33
34
  return (...args) => {
34
35
  const extra = [...args]
35
36
  .reverse()
36
- .find(arg => arg && typeof arg === 'object' && 'authInfo' in arg);
37
+ .find((arg) => arg && typeof arg === 'object' && 'authInfo' in arg);
37
38
  const authInfo = extra?.authInfo;
38
39
  const context = {
39
40
  apiKey: authInfo?.token,
@@ -52,8 +53,32 @@ function withRequestContext(handler, requiresApiKey = true) {
52
53
  return runWithRequestContext(context, () => handler(...args));
53
54
  };
54
55
  }
56
+ /**
57
+ * Append a deprecation notice to every tool response. The block is
58
+ * pushed at the END of ``content`` (not the front) so existing tests
59
+ * that read ``content[0]`` keep passing and the LLM still surfaces it
60
+ * via recency. Defensive: only mutates results whose shape matches the
61
+ * MCP tool-call protocol ({ content: [...] }); anything else is
62
+ * returned untouched.
63
+ */
64
+ function withDeprecationNotice(handler) {
65
+ return async (...args) => {
66
+ const result = await handler(...args);
67
+ if (result &&
68
+ typeof result === 'object' &&
69
+ Array.isArray(result.content)) {
70
+ const typed = result;
71
+ return {
72
+ ...result,
73
+ content: [...typed.content, DEPRECATION_RESPONSE_BLOCK],
74
+ };
75
+ }
76
+ return result;
77
+ };
78
+ }
55
79
  export function registerTool(server, name, description, inputSchema, handler, options = {}) {
56
- const wrappedHandler = withRequestContext(handler, options.requiresApiKey !== false);
80
+ const taggedDescription = description + DEPRECATION_DESCRIPTION_SUFFIX;
81
+ const wrappedHandler = withDeprecationNotice(withRequestContext(handler, options.requiresApiKey !== false));
57
82
  if (typeof server.registerTool === 'function') {
58
83
  const inferredAnnotations = inferAnnotations(name);
59
84
  const mergedAnnotations = {
@@ -62,15 +87,17 @@ export function registerTool(server, name, description, inputSchema, handler, op
62
87
  };
63
88
  server.registerTool(name, {
64
89
  ...(options.title && { title: options.title }),
65
- description,
90
+ description: taggedDescription,
66
91
  inputSchema,
67
- ...(Object.keys(mergedAnnotations).length > 0 && { annotations: mergedAnnotations }),
92
+ ...(Object.keys(mergedAnnotations).length > 0 && {
93
+ annotations: mergedAnnotations,
94
+ }),
68
95
  ...(options.outputSchema && { outputSchema: options.outputSchema }),
69
96
  ...(options._meta && { _meta: options._meta }),
70
97
  }, wrappedHandler);
71
98
  return;
72
99
  }
73
- server.tool(name, description, inputSchema, wrappedHandler);
100
+ server.tool(name, taggedDescription, inputSchema, wrappedHandler);
74
101
  }
75
102
  export function registerResource(server, name, uri, description, readCallback) {
76
103
  const wrappedReadCallback = withRequestContext(readCallback);
@@ -84,7 +111,9 @@ export function registerResource(server, name, uri, description, readCallback) {
84
111
  }
85
112
  export function registerResourceTemplate(server, name, uriTemplate, description, readCallback, listCallback) {
86
113
  const wrappedReadCallback = withRequestContext(readCallback);
87
- const wrappedListCallback = listCallback ? withRequestContext(listCallback) : undefined;
114
+ const wrappedListCallback = listCallback
115
+ ? withRequestContext(listCallback)
116
+ : undefined;
88
117
  const template = new ResourceTemplate(uriTemplate, {
89
118
  list: wrappedListCallback,
90
119
  });
@@ -1,6 +1,17 @@
1
1
  import { z } from 'zod';
2
- export const pageSchema = z.number().int().min(1).default(1).describe('1-based page number.');
3
- export const sizeSchema = (defaultSize, max = 100) => z.number().int().min(1).max(max).default(defaultSize).describe(`Page size (1-${max}).`);
2
+ export const pageSchema = z
3
+ .number()
4
+ .int()
5
+ .min(1)
6
+ .default(1)
7
+ .describe('1-based page number.');
8
+ export const sizeSchema = (defaultSize, max = 100) => z
9
+ .number()
10
+ .int()
11
+ .min(1)
12
+ .max(max)
13
+ .default(defaultSize)
14
+ .describe(`Page size (1-${max}).`);
4
15
  export const workspaceNameSchema = z
5
16
  .string()
6
17
  .min(1)
@@ -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';
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Single source of truth for the deprecation message surfaced by every
3
+ * channel (MCP ``instructions`` field, stderr banner, README, package
4
+ * description, future tool-result injection). Keep all wording here so
5
+ * the four channels never drift apart.
6
+ *
7
+ * The companion `legacy/typescript/DEPRECATED.md` declares the same EOL
8
+ * date — update both together if it changes.
9
+ */
10
+ export const SUNSET_DATE = '2026-11-15';
11
+ export const MIGRATION_URL = 'https://github.com/comet-ml/opik-mcp/blob/main/legacy/typescript/MIGRATION.md';
12
+ const ONE_LINE = `[DEPRECATED] opik-mcp (npm) is deprecated. Migrate to: ` +
13
+ `uvx opik-mcp@latest — sunset ${SUNSET_DATE}.`;
14
+ const SHORT = `The TypeScript Opik MCP server is deprecated and will stop ` +
15
+ `serving requests on ${SUNSET_DATE}. ` +
16
+ `Install the supported Python build with: uvx opik-mcp@latest`;
17
+ const FULL = `\
18
+ ============================================================
19
+ ⚠️ opik-mcp (npm, TypeScript) is DEPRECATED
20
+ ============================================================
21
+
22
+ This server stops serving MCP requests on ${SUNSET_DATE}.
23
+
24
+ Migrate now — in your MCP client config, replace:
25
+
26
+ npx -y opik-mcp
27
+
28
+ with:
29
+
30
+ uvx opik-mcp@latest
31
+
32
+ The tool surface, env vars and transports have changed. Migration
33
+ guide: ${MIGRATION_URL}
34
+
35
+ ============================================================`;
36
+ export const DEPRECATION_NOTICE = {
37
+ oneLine: ONE_LINE,
38
+ short: SHORT,
39
+ full: FULL,
40
+ sunsetDate: SUNSET_DATE,
41
+ migrationUrl: MIGRATION_URL,
42
+ };
43
+ /**
44
+ * Short suffix appended to every tool ``description`` so the MCP host's
45
+ * tool palette shows the deprecation label inline. Kept brief — palette
46
+ * UIs in Claude / Cursor / VS Code truncate long descriptions. Not
47
+ * applied to resources/prompts: their descriptions surface less
48
+ * prominently and the stderr banner + ``instructions`` field already
49
+ * cover those surfaces.
50
+ */
51
+ export const DEPRECATION_DESCRIPTION_SUFFIX = ` ⚠️ DEPRECATED — migrate to \`uvx opik-mcp@latest\` by ${SUNSET_DATE}`;
52
+ /**
53
+ * Content block appended to every tool response. The host LLM reads
54
+ * tool outputs on every call, so this is the channel that survives
55
+ * sessions started before the upgrade and never reads logs/READMEs.
56
+ * Phrased as a server-to-assistant notice so the model surfaces it
57
+ * to the user instead of treating it as part of the tool payload.
58
+ */
59
+ export const DEPRECATION_RESPONSE_BLOCK = {
60
+ type: 'text',
61
+ text: `[opik-mcp server notice] This TypeScript MCP server (npm) is ` +
62
+ `DEPRECATED and stops serving requests on ${SUNSET_DATE}. Please ` +
63
+ `tell the user to replace \`npx -y opik-mcp\` with ` +
64
+ `\`uvx opik-mcp@latest\` in their MCP client config. Migration ` +
65
+ `guide: ${MIGRATION_URL}`,
66
+ };
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "opik-mcp",
3
- "version": "2.0.0",
4
- "description": "MCP server to interact with Opik - Enables automated prompt optimization",
3
+ "version": "2.0.2",
4
+ "description": "[DEPRECATED — use `uvx opik-mcp@latest` instead, sunset 2026-11-15] MCP server to interact with Opik - Enables automated prompt optimization",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opik-mcp": "./build/cli.js"
@@ -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",
@@ -78,7 +80,9 @@
78
80
  {
79
81
  "useESM": true,
80
82
  "diagnostics": {
81
- "ignoreCodes": [151002]
83
+ "ignoreCodes": [
84
+ 151002
85
+ ]
82
86
  }
83
87
  }
84
88
  ]