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.
- package/README.md +119 -196
- package/build/cli.js +13 -2
- package/build/config.js +25 -9
- package/build/debug-log.js +5 -5
- package/build/index.js +45 -19
- package/build/prompts/core-prompts.js +1 -1
- package/build/resources/opik-resources.js +55 -10
- package/build/test-client.js +8 -5
- package/build/tools/capabilities.js +2 -2
- package/build/tools/dataset.js +60 -13
- package/build/tools/metrics.js +4 -2
- package/build/tools/project.js +10 -2
- package/build/tools/prompt.js +49 -13
- package/build/tools/registration.js +9 -5
- package/build/tools/schema.js +13 -2
- package/build/tools/trace.js +69 -18
- package/build/transports/streamable-http-transport.js +26 -9
- package/build/utils/capabilities.js +10 -3
- package/build/utils/examples.js +2 -2
- package/build/utils/opik-sdk.js +7 -2
- package/build/utils/remote-auth.js +8 -2
- package/build/utils/tracing-info.js +7 -1
- package/package.json +4 -2
package/build/tools/trace.js
CHANGED
|
@@ -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 && {
|
|
33
|
+
...(resolved.projectName && {
|
|
34
|
+
projectName: resolved.projectName,
|
|
35
|
+
}),
|
|
34
36
|
}, getRequestOptions(workspaceName)));
|
|
35
37
|
if (!response.data) {
|
|
36
38
|
return {
|
|
37
|
-
content: [
|
|
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: [
|
|
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 && {
|
|
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: [
|
|
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
|
|
160
|
-
|
|
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 && {
|
|
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 && {
|
|
211
|
+
...(resolved.projectName && {
|
|
212
|
+
projectName: resolved.projectName,
|
|
213
|
+
}),
|
|
188
214
|
}, getRequestOptions(workspaceName)));
|
|
189
215
|
if (!response.data) {
|
|
190
216
|
return {
|
|
191
|
-
content: [
|
|
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
|
|
221
|
-
|
|
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
|
|
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 && {
|
|
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: [
|
|
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
|
|
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' ||
|
|
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' &&
|
|
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: [
|
|
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([
|
|
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([
|
|
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() &&
|
|
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: [
|
|
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';
|
package/build/utils/examples.js
CHANGED
|
@@ -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
|
}
|
package/build/utils/opik-sdk.js
CHANGED
|
@@ -8,7 +8,10 @@ function getEffectiveApiKey() {
|
|
|
8
8
|
}
|
|
9
9
|
function getEffectiveWorkspaceName() {
|
|
10
10
|
const context = getRequestContext();
|
|
11
|
-
return context?.workspaceName ||
|
|
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 ||
|
|
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' &&
|
|
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 {
|
|
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: [
|
|
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.
|
|
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": "^
|
|
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",
|