@arizeai/phoenix-mcp 3.1.5 → 4.0.0
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 +20 -2
- package/build/annotationConfigTools.js +43 -0
- package/build/client.js +22 -0
- package/build/config.js +47 -0
- package/build/constants.js +61 -0
- package/build/datasetTools.js +123 -102
- package/build/datasetUtils.js +59 -0
- package/build/experimentTools.js +59 -78
- package/build/identifiers.js +77 -0
- package/build/index.js +16 -14
- package/build/pagination.js +25 -0
- package/build/projectTools.js +57 -19
- package/build/projectUtils.js +14 -0
- package/build/promptSchemas.js +26 -14
- package/build/promptTools.js +184 -303
- package/build/responseUtils.js +16 -0
- package/build/sessionTools.js +126 -0
- package/build/spanTools.js +92 -62
- package/build/spanUtils.js +232 -0
- package/build/supportTools.js +12 -9
- package/build/toolResults.js +32 -0
- package/build/traceTools.js +141 -0
- package/build/traceUtils.js +57 -0
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ Phoenix MCP Server is an implementation of the Model Context Protocol for the Ar
|
|
|
31
31
|
You can use Phoenix MCP Server for:
|
|
32
32
|
|
|
33
33
|
- **Projects Management**: List and explore projects that organize your observability data
|
|
34
|
-
- **Spans & Annotations**: Retrieve spans and
|
|
34
|
+
- **Traces, Spans & Annotations**: Retrieve traces, spans, and annotation configs for analysis and debugging
|
|
35
|
+
- **Sessions**: Explore conversation flows and session-level annotations
|
|
35
36
|
- **Prompts Management**: Create, list, update, and iterate on prompts
|
|
36
37
|
- **Datasets**: Explore datasets and synthesize new examples
|
|
37
38
|
- **Experiments**: Pull experiment results and visualize them with the help of an LLM
|
|
@@ -103,10 +104,27 @@ pnpm inspect
|
|
|
103
104
|
When developing, the server requires the following environment variables:
|
|
104
105
|
|
|
105
106
|
- `PHOENIX_API_KEY`: Your Phoenix API key
|
|
106
|
-
- `
|
|
107
|
+
- `PHOENIX_HOST`: The base URL for Phoenix
|
|
108
|
+
- `PHOENIX_PROJECT`: Optional default project for project-scoped tools
|
|
109
|
+
- `PHOENIX_CLIENT_HEADERS`: Optional JSON-encoded request headers
|
|
107
110
|
|
|
108
111
|
Make sure to set these in a `.env` file. See `.env.example`.
|
|
109
112
|
|
|
113
|
+
## Tool Coverage
|
|
114
|
+
|
|
115
|
+
The MCP server now covers the main operational Phoenix workflows:
|
|
116
|
+
|
|
117
|
+
- `list-projects`, `get-project`
|
|
118
|
+
- `list-traces`, `get-trace`
|
|
119
|
+
- `get-spans`, `get-span-annotations`
|
|
120
|
+
- `list-sessions`, `get-session`
|
|
121
|
+
- `list-annotation-configs`
|
|
122
|
+
- `list-datasets`, `get-dataset`, `get-dataset-examples`, `get-dataset-experiments`, `add-dataset-examples`
|
|
123
|
+
- `list-experiments-for-dataset`, `get-experiment-by-id`
|
|
124
|
+
- `list-prompts`, `get-prompt`, legacy prompt getter aliases, prompt version/tag tools, `upsert-prompt`
|
|
125
|
+
|
|
126
|
+
For Phoenix documentation search, use the separate Phoenix Docs MCP server instead of this package.
|
|
127
|
+
|
|
110
128
|
## Community
|
|
111
129
|
|
|
112
130
|
Join our community to connect with thousands of AI builders:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { MAX_LIST_LIMIT } from "./constants.js";
|
|
3
|
+
import { fetchAllPages } from "./pagination.js";
|
|
4
|
+
import { getResponseData } from "./responseUtils.js";
|
|
5
|
+
import { jsonResponse } from "./toolResults.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Tool descriptions
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const LIST_ANNOTATION_CONFIGS_DESCRIPTION = `List Phoenix annotation configs.
|
|
10
|
+
|
|
11
|
+
Annotation configs define the available human or automated labels, scores, and freeform annotation types.
|
|
12
|
+
|
|
13
|
+
Example usage:
|
|
14
|
+
Show me all annotation configs
|
|
15
|
+
|
|
16
|
+
Expected return:
|
|
17
|
+
Array of annotation config objects.`;
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Tool registration
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Register annotation-config-related MCP tools on the given server.
|
|
23
|
+
*/
|
|
24
|
+
export const initializeAnnotationConfigTools = ({ client, server, }) => {
|
|
25
|
+
server.tool("list-annotation-configs", LIST_ANNOTATION_CONFIGS_DESCRIPTION, {
|
|
26
|
+
limit: z.number().min(1).max(MAX_LIST_LIMIT).default(100).optional(),
|
|
27
|
+
}, async ({ limit = 100 }) => {
|
|
28
|
+
const configs = await fetchAllPages({
|
|
29
|
+
limit,
|
|
30
|
+
fetchPage: async (cursor, pageSize) => {
|
|
31
|
+
const response = await client.GET("/v1/annotation_configs", {
|
|
32
|
+
params: { query: { cursor, limit: pageSize } },
|
|
33
|
+
});
|
|
34
|
+
const data = getResponseData({
|
|
35
|
+
response,
|
|
36
|
+
errorPrefix: "Failed to fetch annotation configs",
|
|
37
|
+
});
|
|
38
|
+
return { data: data.data, nextCursor: data.next_cursor || undefined };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return jsonResponse(configs);
|
|
42
|
+
});
|
|
43
|
+
};
|
package/build/client.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createClient } from "@arizeai/phoenix-client";
|
|
2
|
+
/**
|
|
3
|
+
* Create a Phoenix REST client for MCP tool handlers.
|
|
4
|
+
*
|
|
5
|
+
* The MCP package sends both bearer and `api_key` auth headers because Phoenix
|
|
6
|
+
* deployments may rely on either convention.
|
|
7
|
+
*/
|
|
8
|
+
export function createPhoenixClient({ config, }) {
|
|
9
|
+
const headers = {
|
|
10
|
+
...(config.headers || {}),
|
|
11
|
+
};
|
|
12
|
+
if (config.apiKey) {
|
|
13
|
+
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
14
|
+
headers.api_key = config.apiKey;
|
|
15
|
+
}
|
|
16
|
+
return createClient({
|
|
17
|
+
options: {
|
|
18
|
+
baseUrl: config.baseUrl,
|
|
19
|
+
headers,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
package/build/config.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DEFAULT_PHOENIX_BASE_URL, ENV_PHOENIX_API_KEY, ENV_PHOENIX_CLIENT_HEADERS, ENV_PHOENIX_HOST, ENV_PHOENIX_PROJECT, getHeadersFromEnvironment, getStrFromEnvironment, } from "@arizeai/phoenix-config";
|
|
2
|
+
export const DEFAULT_PHOENIX_ENDPOINT = DEFAULT_PHOENIX_BASE_URL;
|
|
3
|
+
/**
|
|
4
|
+
* Load Phoenix MCP configuration from environment variables.
|
|
5
|
+
*/
|
|
6
|
+
export function loadConfigFromEnvironment() {
|
|
7
|
+
const baseUrl = getStrFromEnvironment(ENV_PHOENIX_HOST);
|
|
8
|
+
const apiKey = getStrFromEnvironment(ENV_PHOENIX_API_KEY);
|
|
9
|
+
const headers = getHeadersFromEnvironment(ENV_PHOENIX_CLIENT_HEADERS);
|
|
10
|
+
const project = getStrFromEnvironment(ENV_PHOENIX_PROJECT);
|
|
11
|
+
return {
|
|
12
|
+
baseUrl: baseUrl || DEFAULT_PHOENIX_ENDPOINT,
|
|
13
|
+
apiKey: apiKey || undefined,
|
|
14
|
+
headers: headers || undefined,
|
|
15
|
+
project: project || undefined,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Extract only the string-valued command-line options, ignoring bare boolean
|
|
20
|
+
* flags that `minimist` produces when a flag is used without a value.
|
|
21
|
+
*/
|
|
22
|
+
function getStringCommandLineOptions(commandLineOptions) {
|
|
23
|
+
return {
|
|
24
|
+
...(typeof commandLineOptions.baseUrl === "string"
|
|
25
|
+
? { baseUrl: commandLineOptions.baseUrl }
|
|
26
|
+
: {}),
|
|
27
|
+
...(typeof commandLineOptions.apiKey === "string"
|
|
28
|
+
? { apiKey: commandLineOptions.apiKey }
|
|
29
|
+
: {}),
|
|
30
|
+
...(typeof commandLineOptions.project === "string"
|
|
31
|
+
? { project: commandLineOptions.project }
|
|
32
|
+
: {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Merge environment-derived Phoenix MCP configuration with command-line overrides.
|
|
37
|
+
*
|
|
38
|
+
* Only string command-line values are treated as overrides so that bare flags
|
|
39
|
+
* parsed by `minimist` do not replace valid environment defaults with boolean `true`.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveConfig({ commandLineOptions, }) {
|
|
42
|
+
const envConfig = loadConfigFromEnvironment();
|
|
43
|
+
return {
|
|
44
|
+
...envConfig,
|
|
45
|
+
...getStringCommandLineOptions(commandLineOptions),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Pagination
|
|
3
|
+
// ============================================================
|
|
4
|
+
/** Default number of items fetched per API page request. */
|
|
5
|
+
export const DEFAULT_PAGE_SIZE = 100;
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Span queries
|
|
8
|
+
// ============================================================
|
|
9
|
+
/** Maximum number of spans that a single query may return. */
|
|
10
|
+
export const MAX_SPAN_QUERY_LIMIT = 1000;
|
|
11
|
+
// ============================================================
|
|
12
|
+
// List queries (datasets, experiments, projects, configs)
|
|
13
|
+
// ============================================================
|
|
14
|
+
/** Upper bound for the `limit` parameter on list endpoints. */
|
|
15
|
+
export const MAX_LIST_LIMIT = 500;
|
|
16
|
+
// ============================================================
|
|
17
|
+
// Annotation fetching
|
|
18
|
+
// ============================================================
|
|
19
|
+
/** Number of span IDs included in each annotation chunk request. */
|
|
20
|
+
export const ANNOTATION_CHUNK_SIZE = 100;
|
|
21
|
+
/** Maximum number of annotation chunk requests executed concurrently. */
|
|
22
|
+
export const MAX_CONCURRENT_ANNOTATION_REQUESTS = 5;
|
|
23
|
+
/** Page size used when exhausting annotation pages within a single chunk. */
|
|
24
|
+
export const ANNOTATION_PAGE_SIZE = 1000;
|
|
25
|
+
// ============================================================
|
|
26
|
+
// Trace queries
|
|
27
|
+
// ============================================================
|
|
28
|
+
/** Default number of traces returned by the list-traces tool. */
|
|
29
|
+
export const DEFAULT_TRACE_PAGE_SIZE = 10;
|
|
30
|
+
/** Maximum number of traces the list-traces tool may return. */
|
|
31
|
+
export const MAX_TRACE_PAGE_SIZE = 100;
|
|
32
|
+
// ============================================================
|
|
33
|
+
// Session queries
|
|
34
|
+
// ============================================================
|
|
35
|
+
/** Maximum number of sessions the list-sessions tool may return. */
|
|
36
|
+
export const MAX_SESSION_PAGE_SIZE = 100;
|
|
37
|
+
// ============================================================
|
|
38
|
+
// Prompt defaults
|
|
39
|
+
// ============================================================
|
|
40
|
+
/** Default model provider when creating a prompt version. */
|
|
41
|
+
export const DEFAULT_MODEL_PROVIDER = "OPENAI";
|
|
42
|
+
/** Default model name when creating a prompt version. */
|
|
43
|
+
export const DEFAULT_MODEL_NAME = "gpt-4";
|
|
44
|
+
/** Default sampling temperature when creating a prompt version. */
|
|
45
|
+
export const DEFAULT_TEMPERATURE = 0.7;
|
|
46
|
+
/**
|
|
47
|
+
* Default `max_tokens` for Anthropic prompt versions.
|
|
48
|
+
*
|
|
49
|
+
* Anthropic models require an explicit `max_tokens` invocation parameter.
|
|
50
|
+
*/
|
|
51
|
+
export const ANTHROPIC_DEFAULT_MAX_TOKENS = 1000;
|
|
52
|
+
// ============================================================
|
|
53
|
+
// Time
|
|
54
|
+
// ============================================================
|
|
55
|
+
/** Number of milliseconds in one minute. */
|
|
56
|
+
export const MS_PER_MINUTE = 60_000;
|
|
57
|
+
// ============================================================
|
|
58
|
+
// MCP metadata
|
|
59
|
+
// ============================================================
|
|
60
|
+
/** Provenance tag applied to dataset examples created through the MCP server. */
|
|
61
|
+
export const MCP_SYNTHETIC_SOURCE = "Synthetic Example added via MCP";
|
package/build/datasetTools.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
|
+
import { MAX_LIST_LIMIT, MCP_SYNTHETIC_SOURCE } from "./constants.js";
|
|
3
|
+
import { resolveDatasetId } from "./datasetUtils.js";
|
|
4
|
+
import { fetchAllPages } from "./pagination.js";
|
|
5
|
+
import { getResponseData } from "./responseUtils.js";
|
|
6
|
+
import { jsonResponse } from "./toolResults.js";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Tool descriptions
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
2
10
|
const LIST_DATASETS_DESCRIPTION = `Get a list of all datasets.
|
|
3
11
|
|
|
4
|
-
Datasets are collections of 'dataset examples' that each example includes an input,
|
|
12
|
+
Datasets are collections of 'dataset examples' that each example includes an input,
|
|
5
13
|
(expected) output, and optional metadata. They are primarily used as inputs for experiments.
|
|
6
14
|
|
|
7
15
|
Example usage:
|
|
@@ -21,52 +29,23 @@ Expected return:
|
|
|
21
29
|
]`;
|
|
22
30
|
const GET_DATASET_EXAMPLES_DESCRIPTION = `Get examples from a dataset.
|
|
23
31
|
|
|
24
|
-
Dataset examples are an array of objects that each include an input,
|
|
25
|
-
(expected) output, and optional metadata. These examples are typically used to represent
|
|
26
|
-
input to an application or model (e.g. prompt template variables, a code file, or image)
|
|
32
|
+
Dataset examples are an array of objects that each include an input,
|
|
33
|
+
(expected) output, and optional metadata. These examples are typically used to represent
|
|
34
|
+
input to an application or model (e.g. prompt template variables, a code file, or image)
|
|
27
35
|
and used to test or benchmark changes.
|
|
28
36
|
|
|
29
37
|
Example usage:
|
|
30
38
|
Show me all examples from dataset RGF0YXNldDox
|
|
31
39
|
|
|
32
40
|
Expected return:
|
|
33
|
-
Object containing dataset ID, version ID, and array of examples
|
|
34
|
-
Example: {
|
|
35
|
-
"dataset_id": "datasetid1234",
|
|
36
|
-
"version_id": "datasetversionid1234",
|
|
37
|
-
"examples": [
|
|
38
|
-
{
|
|
39
|
-
"id": "exampleid1234",
|
|
40
|
-
"input": {
|
|
41
|
-
"text": "Sample input text"
|
|
42
|
-
},
|
|
43
|
-
"output": {
|
|
44
|
-
"text": "Expected output text"
|
|
45
|
-
},
|
|
46
|
-
"metadata": {},
|
|
47
|
-
"updated_at": "YYYY-MM-DDTHH:mm:ssZ"
|
|
48
|
-
}
|
|
49
|
-
]
|
|
50
|
-
}`;
|
|
41
|
+
Object containing dataset ID, version ID, and array of examples.`;
|
|
51
42
|
const GET_DATASET_EXPERIMENTS_DESCRIPTION = `List experiments run on a dataset.
|
|
52
43
|
|
|
53
44
|
Example usage:
|
|
54
45
|
Show me all experiments run on dataset RGF0YXNldDox
|
|
55
46
|
|
|
56
47
|
Expected return:
|
|
57
|
-
Array of experiment objects with metadata
|
|
58
|
-
Example: [
|
|
59
|
-
{
|
|
60
|
-
"id": "experimentid1234",
|
|
61
|
-
"dataset_id": "datasetid1234",
|
|
62
|
-
"dataset_version_id": "datasetversionid1234",
|
|
63
|
-
"repetitions": 1,
|
|
64
|
-
"metadata": {},
|
|
65
|
-
"project_name": "Experiment-abc123",
|
|
66
|
-
"created_at": "YYYY-MM-DDTHH:mm:ssZ",
|
|
67
|
-
"updated_at": "YYYY-MM-DDTHH:mm:ssZ"
|
|
68
|
-
}
|
|
69
|
-
]`;
|
|
48
|
+
Array of experiment objects with metadata.`;
|
|
70
49
|
const ADD_DATASET_EXAMPLES_DESCRIPTION = `Add examples to an existing dataset.
|
|
71
50
|
|
|
72
51
|
This tool adds one or more examples to an existing dataset. Each example includes an input,
|
|
@@ -79,107 +58,149 @@ Example usage:
|
|
|
79
58
|
Look at the analyze "my-dataset" and augment them with new examples to cover relevant edge cases
|
|
80
59
|
|
|
81
60
|
Expected return:
|
|
82
|
-
Confirmation of successful addition of examples to the dataset
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
61
|
+
Confirmation of successful addition of examples to the dataset.`;
|
|
62
|
+
const GET_DATASET_DESCRIPTION = `Get dataset metadata by name or ID.
|
|
63
|
+
|
|
64
|
+
Example usage:
|
|
65
|
+
Show me the dataset "my-dataset"
|
|
66
|
+
|
|
67
|
+
Expected return:
|
|
68
|
+
A dataset object with metadata and version information.`;
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Shared schema
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
const datasetSelectorSchema = z
|
|
73
|
+
.object({
|
|
74
|
+
dataset_id: z.string().optional(),
|
|
75
|
+
dataset_name: z.string().optional(),
|
|
76
|
+
})
|
|
77
|
+
.refine(({ dataset_id, dataset_name }) => Boolean(dataset_id || dataset_name), { message: "Provide dataset_id or dataset_name" });
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Tool registration
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
/**
|
|
82
|
+
* Register dataset-related MCP tools on the given server.
|
|
83
|
+
*/
|
|
87
84
|
export const initializeDatasetTools = ({ client, server, }) => {
|
|
88
85
|
server.tool("list-datasets", LIST_DATASETS_DESCRIPTION, {
|
|
89
|
-
limit: z.number().min(1).max(
|
|
86
|
+
limit: z.number().min(1).max(MAX_LIST_LIMIT).default(100),
|
|
90
87
|
}, async ({ limit }) => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
const datasets = await fetchAllPages({
|
|
89
|
+
limit,
|
|
90
|
+
fetchPage: async (cursor, pageSize) => {
|
|
91
|
+
const response = await client.GET("/v1/datasets", {
|
|
92
|
+
params: { query: { cursor, limit: pageSize } },
|
|
93
|
+
});
|
|
94
|
+
const data = getResponseData({
|
|
95
|
+
response,
|
|
96
|
+
errorPrefix: "Failed to fetch datasets",
|
|
97
|
+
});
|
|
98
|
+
return { data: data.data, nextCursor: data.next_cursor || undefined };
|
|
94
99
|
},
|
|
95
100
|
});
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
101
|
+
return jsonResponse(datasets);
|
|
102
|
+
});
|
|
103
|
+
server.tool("get-dataset", GET_DATASET_DESCRIPTION, datasetSelectorSchema.shape, async ({ dataset_id, dataset_name }) => {
|
|
104
|
+
const resolvedId = await resolveDatasetId({
|
|
105
|
+
client,
|
|
106
|
+
datasetId: dataset_id,
|
|
107
|
+
datasetName: dataset_name,
|
|
108
|
+
});
|
|
109
|
+
const response = await client.GET("/v1/datasets/{id}", {
|
|
110
|
+
params: { path: { id: resolvedId } },
|
|
111
|
+
});
|
|
112
|
+
const dataset = getResponseData({
|
|
113
|
+
response,
|
|
114
|
+
errorPrefix: `Failed to fetch dataset "${resolvedId}"`,
|
|
115
|
+
}).data;
|
|
116
|
+
return jsonResponse(dataset);
|
|
104
117
|
});
|
|
105
118
|
server.tool("get-dataset-examples", GET_DATASET_EXAMPLES_DESCRIPTION, {
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
...datasetSelectorSchema.shape,
|
|
120
|
+
version_id: z.string().optional(),
|
|
121
|
+
splits: z.array(z.string()).optional(),
|
|
122
|
+
}, async ({ dataset_id, dataset_name, version_id, splits }) => {
|
|
123
|
+
const resolvedId = await resolveDatasetId({
|
|
124
|
+
client,
|
|
125
|
+
datasetId: dataset_id,
|
|
126
|
+
datasetName: dataset_name,
|
|
127
|
+
});
|
|
108
128
|
const response = await client.GET("/v1/datasets/{id}/examples", {
|
|
109
129
|
params: {
|
|
110
|
-
path: { id:
|
|
130
|
+
path: { id: resolvedId },
|
|
131
|
+
query: { version_id, split: splits },
|
|
111
132
|
},
|
|
112
133
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
};
|
|
134
|
+
const datasetExamples = getResponseData({
|
|
135
|
+
response,
|
|
136
|
+
errorPrefix: `Failed to fetch examples for dataset "${resolvedId}"`,
|
|
137
|
+
});
|
|
138
|
+
return jsonResponse(datasetExamples);
|
|
121
139
|
});
|
|
122
140
|
server.tool("get-dataset-experiments", GET_DATASET_EXPERIMENTS_DESCRIPTION, {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
...datasetSelectorSchema.shape,
|
|
142
|
+
limit: z.number().min(1).max(MAX_LIST_LIMIT).default(100).optional(),
|
|
143
|
+
}, async ({ dataset_id, dataset_name, limit = 100 }) => {
|
|
144
|
+
const resolvedId = await resolveDatasetId({
|
|
145
|
+
client,
|
|
146
|
+
datasetId: dataset_id,
|
|
147
|
+
datasetName: dataset_name,
|
|
148
|
+
});
|
|
149
|
+
const experiments = await fetchAllPages({
|
|
150
|
+
limit,
|
|
151
|
+
fetchPage: async (cursor, pageSize) => {
|
|
152
|
+
const response = await client.GET("/v1/datasets/{dataset_id}/experiments", {
|
|
153
|
+
params: {
|
|
154
|
+
path: { dataset_id: resolvedId },
|
|
155
|
+
query: { cursor, limit: pageSize },
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const data = getResponseData({
|
|
159
|
+
response,
|
|
160
|
+
errorPrefix: `Failed to fetch experiments for dataset "${resolvedId}"`,
|
|
161
|
+
});
|
|
162
|
+
return { data: data.data, nextCursor: data.next_cursor || undefined };
|
|
128
163
|
},
|
|
129
164
|
});
|
|
130
|
-
return
|
|
131
|
-
content: [
|
|
132
|
-
{
|
|
133
|
-
type: "text",
|
|
134
|
-
text: JSON.stringify(response.data, null, 2),
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
};
|
|
165
|
+
return jsonResponse(experiments);
|
|
138
166
|
});
|
|
139
167
|
server.tool("add-dataset-examples", ADD_DATASET_EXAMPLES_DESCRIPTION, {
|
|
140
|
-
|
|
168
|
+
dataset_name: z.string(),
|
|
141
169
|
examples: z.array(z.object({
|
|
142
|
-
input: z.record(z.string(), z.
|
|
143
|
-
output: z.record(z.string(), z.
|
|
144
|
-
metadata: z.record(z.string(), z.
|
|
170
|
+
input: z.record(z.string(), z.unknown()),
|
|
171
|
+
output: z.record(z.string(), z.unknown()),
|
|
172
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
145
173
|
})),
|
|
146
|
-
}, async ({
|
|
147
|
-
// Add MCP metadata to each example
|
|
174
|
+
}, async ({ dataset_name, examples }) => {
|
|
148
175
|
const examplesWithMetadata = examples.map((example) => ({
|
|
149
176
|
...example,
|
|
150
177
|
metadata: {
|
|
151
178
|
...example.metadata,
|
|
152
|
-
source:
|
|
179
|
+
source: MCP_SYNTHETIC_SOURCE,
|
|
153
180
|
},
|
|
154
181
|
}));
|
|
155
182
|
const response = await client.POST("/v1/datasets/upload", {
|
|
156
183
|
body: {
|
|
157
184
|
action: "append",
|
|
158
|
-
name:
|
|
185
|
+
name: dataset_name,
|
|
159
186
|
inputs: examplesWithMetadata.map((e) => e.input),
|
|
160
187
|
outputs: examplesWithMetadata.map((e) => e.output),
|
|
161
188
|
metadata: examplesWithMetadata.map((e) => e.metadata),
|
|
162
189
|
},
|
|
163
|
-
params: {
|
|
164
|
-
query: {
|
|
165
|
-
sync: true,
|
|
166
|
-
},
|
|
167
|
-
},
|
|
190
|
+
params: { query: { sync: true } },
|
|
168
191
|
});
|
|
169
|
-
|
|
192
|
+
const uploadResponse = getResponseData({
|
|
193
|
+
response,
|
|
194
|
+
errorPrefix: `Failed to add examples to dataset "${dataset_name}"`,
|
|
195
|
+
});
|
|
196
|
+
const uploadData = uploadResponse?.data;
|
|
197
|
+
if (!uploadData?.dataset_id) {
|
|
170
198
|
throw new Error("Failed to add examples to dataset: No dataset ID received");
|
|
171
199
|
}
|
|
172
|
-
return {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
dataset_name: datasetName,
|
|
178
|
-
dataset_id: response.data.data.dataset_id,
|
|
179
|
-
message: "Successfully added examples to dataset",
|
|
180
|
-
}, null, 2),
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
};
|
|
200
|
+
return jsonResponse({
|
|
201
|
+
dataset_name,
|
|
202
|
+
dataset_id: uploadData.dataset_id,
|
|
203
|
+
message: "Successfully added examples to dataset",
|
|
204
|
+
});
|
|
184
205
|
});
|
|
185
206
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getRelayGlobalIdIfType, requireIdentifier } from "./identifiers.js";
|
|
2
|
+
import { getResponseData } from "./responseUtils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Determine whether a dataset identifier is already a Phoenix Relay GlobalID.
|
|
5
|
+
*/
|
|
6
|
+
export function isPhoenixDatasetId(identifier) {
|
|
7
|
+
return (getRelayGlobalIdIfType({
|
|
8
|
+
identifier,
|
|
9
|
+
expectedTypeName: "Dataset",
|
|
10
|
+
}) !== null);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a dataset name or Relay GlobalID to the dataset's canonical ID.
|
|
14
|
+
*
|
|
15
|
+
* When `datasetId` is provided and is a valid Relay GlobalID, it is returned
|
|
16
|
+
* directly without an API call. Otherwise `datasetName` is looked up via the
|
|
17
|
+
* datasets list endpoint.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveDatasetId({ client, datasetId, datasetName, }) {
|
|
20
|
+
// Prefer datasetId when provided
|
|
21
|
+
if (datasetId) {
|
|
22
|
+
const normalizedId = requireIdentifier({
|
|
23
|
+
identifier: datasetId,
|
|
24
|
+
label: "datasetId",
|
|
25
|
+
});
|
|
26
|
+
const relayId = getRelayGlobalIdIfType({
|
|
27
|
+
identifier: normalizedId,
|
|
28
|
+
expectedTypeName: "Dataset",
|
|
29
|
+
});
|
|
30
|
+
if (relayId) {
|
|
31
|
+
return relayId;
|
|
32
|
+
}
|
|
33
|
+
// datasetId might be a name if caller used the wrong field — fall through
|
|
34
|
+
}
|
|
35
|
+
const nameToResolve = datasetName || datasetId;
|
|
36
|
+
if (!nameToResolve?.trim()) {
|
|
37
|
+
throw new Error("datasetName or datasetId is required");
|
|
38
|
+
}
|
|
39
|
+
const normalizedName = requireIdentifier({
|
|
40
|
+
identifier: nameToResolve,
|
|
41
|
+
label: "datasetName",
|
|
42
|
+
});
|
|
43
|
+
const response = await client.GET("/v1/datasets", {
|
|
44
|
+
params: {
|
|
45
|
+
query: {
|
|
46
|
+
name: normalizedName,
|
|
47
|
+
limit: 1,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const data = getResponseData({
|
|
52
|
+
response,
|
|
53
|
+
errorPrefix: `Failed to resolve dataset "${normalizedName}"`,
|
|
54
|
+
});
|
|
55
|
+
if (data.data.length === 0) {
|
|
56
|
+
throw new Error(`Dataset not found: "${normalizedName}"`);
|
|
57
|
+
}
|
|
58
|
+
return data.data[0].id;
|
|
59
|
+
}
|