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.
- package/README.md +134 -196
- package/build/cli.js +20 -2
- package/build/config.js +25 -9
- package/build/debug-log.js +5 -5
- package/build/index.js +51 -20
- 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 +37 -8
- 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/deprecation.js +66 -0
- 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 +8 -4
- package/LICENSE +0 -203
|
@@ -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
|
|
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 && {
|
|
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,
|
|
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
|
|
114
|
+
const wrappedListCallback = listCallback
|
|
115
|
+
? withRequestContext(listCallback)
|
|
116
|
+
: undefined;
|
|
88
117
|
const template = new ResourceTemplate(uriTemplate, {
|
|
89
118
|
list: wrappedListCallback,
|
|
90
119
|
});
|
package/build/tools/schema.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export const pageSchema = z
|
|
3
|
-
|
|
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)
|
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';
|
|
@@ -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
|
+
};
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opik-mcp",
|
|
3
|
-
"version": "2.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": "^
|
|
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": [
|
|
83
|
+
"ignoreCodes": [
|
|
84
|
+
151002
|
|
85
|
+
]
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
]
|