@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.
@@ -0,0 +1,126 @@
1
+ import z from "zod";
2
+ import { DEFAULT_TRACE_PAGE_SIZE, MAX_SESSION_PAGE_SIZE } from "./constants.js";
3
+ import { requireIdentifier } from "./identifiers.js";
4
+ import { fetchAllPages } from "./pagination.js";
5
+ import { resolveProjectIdentifier } from "./projectUtils.js";
6
+ import { getResponseData } from "./responseUtils.js";
7
+ import { jsonResponse } from "./toolResults.js";
8
+ // ---------------------------------------------------------------------------
9
+ // Tool descriptions
10
+ // ---------------------------------------------------------------------------
11
+ const LIST_SESSIONS_DESCRIPTION = `List sessions for a project.
12
+
13
+ Sessions represent conversation flows grouped across traces.
14
+
15
+ Example usage:
16
+ Show me the last 10 sessions for project "default"
17
+
18
+ Expected return:
19
+ Array of session objects ordered by the requested sort order.`;
20
+ const GET_SESSION_DESCRIPTION = `Get a single session by GlobalID or user-provided session_id.
21
+
22
+ Example usage:
23
+ Show me session "chat-123"
24
+
25
+ Expected return:
26
+ A session object and, optionally, its annotations.`;
27
+ // ---------------------------------------------------------------------------
28
+ // Helpers
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * Fetch all annotations for a single session, paginating internally.
32
+ */
33
+ async function fetchSessionAnnotations({ client, projectIdentifier, sessionId, }) {
34
+ const normalizedProjectIdentifier = requireIdentifier({
35
+ identifier: projectIdentifier,
36
+ label: "projectIdentifier",
37
+ });
38
+ return fetchAllPages({
39
+ limit: Infinity,
40
+ fetchPage: async (cursor, pageSize) => {
41
+ const query = {
42
+ session_ids: [sessionId],
43
+ limit: pageSize,
44
+ };
45
+ if (cursor) {
46
+ query.cursor = cursor;
47
+ }
48
+ const response = await client.GET("/v1/projects/{project_identifier}/session_annotations", {
49
+ params: {
50
+ path: { project_identifier: normalizedProjectIdentifier },
51
+ query,
52
+ },
53
+ });
54
+ const data = getResponseData({
55
+ response,
56
+ errorPrefix: `Failed to fetch annotations for session "${sessionId}"`,
57
+ });
58
+ return { data: data.data, nextCursor: data.next_cursor || undefined };
59
+ },
60
+ });
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Tool registration
64
+ // ---------------------------------------------------------------------------
65
+ /**
66
+ * Register session-related MCP tools on the given server.
67
+ */
68
+ export const initializeSessionTools = ({ client, server, defaultProject, }) => {
69
+ server.tool("list-sessions", LIST_SESSIONS_DESCRIPTION, {
70
+ project_identifier: z.string().optional(),
71
+ limit: z
72
+ .number()
73
+ .min(1)
74
+ .max(MAX_SESSION_PAGE_SIZE)
75
+ .default(DEFAULT_TRACE_PAGE_SIZE),
76
+ order: z.enum(["asc", "desc"]).default("desc").optional(),
77
+ }, async ({ project_identifier, limit, order = "desc" }) => {
78
+ const normalizedProjectIdentifier = resolveProjectIdentifier({
79
+ projectIdentifier: project_identifier,
80
+ defaultProjectIdentifier: defaultProject,
81
+ });
82
+ const sessions = await fetchAllPages({
83
+ limit,
84
+ fetchPage: async (cursor, pageSize) => {
85
+ const response = await client.GET("/v1/projects/{project_identifier}/sessions", {
86
+ params: {
87
+ path: { project_identifier: normalizedProjectIdentifier },
88
+ query: { cursor, limit: pageSize, order },
89
+ },
90
+ });
91
+ const data = getResponseData({
92
+ response,
93
+ errorPrefix: `Failed to fetch sessions for project "${normalizedProjectIdentifier}"`,
94
+ });
95
+ return { data: data.data, nextCursor: data.next_cursor || undefined };
96
+ },
97
+ });
98
+ return jsonResponse(sessions);
99
+ });
100
+ server.tool("get-session", GET_SESSION_DESCRIPTION, {
101
+ session_identifier: z.string(),
102
+ include_annotations: z.boolean().default(false).optional(),
103
+ }, async ({ session_identifier, include_annotations = false }) => {
104
+ const response = await client.GET("/v1/sessions/{session_identifier}", {
105
+ params: {
106
+ path: { session_identifier },
107
+ },
108
+ });
109
+ const session = getResponseData({
110
+ response,
111
+ errorPrefix: `Failed to fetch session "${session_identifier}"`,
112
+ }).data;
113
+ if (!include_annotations) {
114
+ return jsonResponse(session);
115
+ }
116
+ const annotations = await fetchSessionAnnotations({
117
+ client,
118
+ projectIdentifier: session.project_id,
119
+ sessionId: session.session_id,
120
+ });
121
+ return jsonResponse({
122
+ session,
123
+ annotations,
124
+ });
125
+ });
126
+ };
@@ -1,4 +1,12 @@
1
1
  import z from "zod";
2
+ import { DEFAULT_PAGE_SIZE, MAX_SPAN_QUERY_LIMIT } from "./constants.js";
3
+ import { resolveProjectIdentifier } from "./projectUtils.js";
4
+ import { getResponseData } from "./responseUtils.js";
5
+ import { attachAnnotationsToSpans, extractSpanIds, fetchProjectSpans, fetchSpanAnnotations, } from "./spanUtils.js";
6
+ import { jsonResponse } from "./toolResults.js";
7
+ // ---------------------------------------------------------------------------
8
+ // Tool descriptions
9
+ // ---------------------------------------------------------------------------
2
10
  const GET_SPANS_DESCRIPTION = `Get spans from a project with filtering criteria.
3
11
 
4
12
  Spans represent individual operations or units of work within a trace. They contain timing information,
@@ -59,89 +67,111 @@ Expected return:
59
67
  ],
60
68
  "nextCursor": "cursor_for_pagination"
61
69
  }`;
62
- export const initializeSpanTools = ({ client, server, }) => {
70
+ // ---------------------------------------------------------------------------
71
+ // Tool registration
72
+ // ---------------------------------------------------------------------------
73
+ /**
74
+ * Register span-related MCP tools on the given server.
75
+ */
76
+ export const initializeSpanTools = ({ client, server, defaultProject, }) => {
63
77
  server.tool("get-spans", GET_SPANS_DESCRIPTION, {
64
- projectName: z.string(),
65
- startTime: z.string().optional(),
66
- endTime: z.string().optional(),
67
- traceIds: z.array(z.string()).optional(),
78
+ project_identifier: z.string().optional(),
79
+ start_time: z.string().optional(),
80
+ end_time: z.string().optional(),
81
+ trace_ids: z.array(z.string()).optional(),
82
+ parent_id: z.string().nullable().optional(),
83
+ names: z.array(z.string()).optional(),
84
+ span_kinds: z.array(z.string()).optional(),
85
+ status_codes: z.array(z.enum(["OK", "ERROR", "UNSET"])).optional(),
68
86
  cursor: z.string().optional(),
69
- limit: z.number().min(1).max(1000).default(100).optional(),
70
- }, async ({ projectName, startTime, endTime, traceIds, cursor, limit = 100, }) => {
71
- const params = {
72
- limit,
73
- };
74
- if (cursor) {
75
- params.cursor = cursor;
76
- }
77
- if (startTime) {
78
- params.start_time = startTime;
79
- }
80
- if (endTime) {
81
- params.end_time = endTime;
82
- }
83
- if (traceIds) {
84
- params.trace_id = traceIds;
85
- }
86
- const response = await client.GET("/v1/projects/{project_identifier}/spans", {
87
- params: {
88
- path: {
89
- project_identifier: projectName,
90
- },
91
- query: params,
87
+ limit: z
88
+ .number()
89
+ .min(1)
90
+ .max(MAX_SPAN_QUERY_LIMIT)
91
+ .default(DEFAULT_PAGE_SIZE)
92
+ .optional(),
93
+ include_annotations: z.boolean().default(false).optional(),
94
+ }, async ({ project_identifier, start_time, end_time, trace_ids, parent_id, names, span_kinds, status_codes, cursor, limit = DEFAULT_PAGE_SIZE, include_annotations = false, }) => {
95
+ const resolvedProjectIdentifier = resolveProjectIdentifier({
96
+ projectIdentifier: project_identifier,
97
+ defaultProjectIdentifier: defaultProject,
98
+ });
99
+ const response = await fetchProjectSpans({
100
+ client,
101
+ projectIdentifier: resolvedProjectIdentifier,
102
+ filters: {
103
+ cursor,
104
+ limit,
105
+ startTime: start_time,
106
+ endTime: end_time,
107
+ traceIds: trace_ids,
108
+ parentId: parent_id,
109
+ names,
110
+ spanKinds: span_kinds,
111
+ statusCodes: status_codes,
92
112
  },
113
+ totalLimit: limit,
114
+ });
115
+ const spans = include_annotations
116
+ ? attachAnnotationsToSpans({
117
+ spans: response.spans,
118
+ annotations: await fetchSpanAnnotations({
119
+ client,
120
+ projectIdentifier: resolvedProjectIdentifier,
121
+ spanIds: extractSpanIds(response.spans),
122
+ }),
123
+ })
124
+ : response.spans;
125
+ return jsonResponse({
126
+ spans,
127
+ nextCursor: response.nextCursor,
93
128
  });
94
- return {
95
- content: [
96
- {
97
- type: "text",
98
- text: JSON.stringify({
99
- spans: response.data?.data ?? [],
100
- nextCursor: response.data?.next_cursor ?? null,
101
- }, null, 2),
102
- },
103
- ],
104
- };
105
129
  });
106
130
  server.tool("get-span-annotations", GET_SPAN_ANNOTATIONS_DESCRIPTION, {
107
- projectName: z.string(),
108
- spanIds: z.array(z.string()),
109
- includeAnnotationNames: z.array(z.string()).optional(),
110
- excludeAnnotationNames: z.array(z.string()).optional(),
131
+ project_identifier: z.string().optional(),
132
+ span_ids: z.array(z.string()),
133
+ include_annotation_names: z.array(z.string()).optional(),
134
+ exclude_annotation_names: z.array(z.string()).optional(),
111
135
  cursor: z.string().optional(),
112
- limit: z.number().min(1).max(1000).default(100).optional(),
113
- }, async ({ projectName, spanIds, includeAnnotationNames, excludeAnnotationNames, cursor, limit = 100, }) => {
136
+ limit: z
137
+ .number()
138
+ .min(1)
139
+ .max(MAX_SPAN_QUERY_LIMIT)
140
+ .default(DEFAULT_PAGE_SIZE)
141
+ .optional(),
142
+ }, async ({ project_identifier, span_ids, include_annotation_names, exclude_annotation_names, cursor, limit = DEFAULT_PAGE_SIZE, }) => {
143
+ const resolvedProjectIdentifier = resolveProjectIdentifier({
144
+ projectIdentifier: project_identifier,
145
+ defaultProjectIdentifier: defaultProject,
146
+ });
114
147
  const params = {
115
- span_ids: spanIds,
148
+ span_ids,
116
149
  limit,
117
150
  };
118
151
  if (cursor) {
119
152
  params.cursor = cursor;
120
153
  }
121
- if (includeAnnotationNames) {
122
- params.include_annotation_names = includeAnnotationNames;
154
+ if (include_annotation_names) {
155
+ params.include_annotation_names = include_annotation_names;
123
156
  }
124
- if (excludeAnnotationNames) {
125
- params.exclude_annotation_names = excludeAnnotationNames;
157
+ if (exclude_annotation_names) {
158
+ params.exclude_annotation_names = exclude_annotation_names;
126
159
  }
127
160
  const response = await client.GET("/v1/projects/{project_identifier}/span_annotations", {
128
161
  params: {
129
162
  path: {
130
- project_identifier: projectName,
163
+ project_identifier: resolvedProjectIdentifier,
131
164
  },
132
165
  query: params,
133
166
  },
134
167
  });
135
- return {
136
- content: [
137
- {
138
- type: "text",
139
- text: JSON.stringify({
140
- annotations: response.data?.data ?? [],
141
- nextCursor: response.data?.next_cursor ?? null,
142
- }, null, 2),
143
- },
144
- ],
145
- };
168
+ const data = getResponseData({
169
+ response,
170
+ errorPrefix: `Failed to fetch span annotations for project "${resolvedProjectIdentifier}"`,
171
+ });
172
+ return jsonResponse({
173
+ annotations: data.data,
174
+ nextCursor: data.next_cursor || null,
175
+ });
146
176
  });
147
177
  };
@@ -0,0 +1,232 @@
1
+ import { getSpans, } from "@arizeai/phoenix-client/spans";
2
+ import { ANNOTATION_CHUNK_SIZE, ANNOTATION_PAGE_SIZE, DEFAULT_PAGE_SIZE, MAX_CONCURRENT_ANNOTATION_REQUESTS, MAX_SPAN_QUERY_LIMIT, MS_PER_MINUTE, } from "./constants.js";
3
+ import { requireIdentifier } from "./identifiers.js";
4
+ import { getResponseData } from "./responseUtils.js";
5
+ /**
6
+ * Extract span IDs from an array of spans, filtering out any without context.
7
+ */
8
+ export function extractSpanIds(spans) {
9
+ return spans
10
+ .map((span) => span.context?.span_id)
11
+ .filter((spanId) => Boolean(spanId));
12
+ }
13
+ /**
14
+ * Translate MCP span filters into the phoenix-client `getSpans` parameter shape.
15
+ *
16
+ * The MCP layer uses plural field names (`names`, `spanKinds`, `statusCodes`)
17
+ * while phoenix-client uses singular (`name`, `spanKind`, `statusCode`).
18
+ */
19
+ export function buildProjectSpansRequest({ cursor, limit, startTime, endTime, traceIds, parentId, names, spanKinds, statusCodes, }) {
20
+ const request = {};
21
+ if (cursor) {
22
+ request.cursor = cursor;
23
+ }
24
+ if (limit !== undefined) {
25
+ request.limit = limit;
26
+ }
27
+ if (startTime) {
28
+ request.startTime = startTime;
29
+ }
30
+ if (endTime) {
31
+ request.endTime = endTime;
32
+ }
33
+ if (traceIds && traceIds.length > 0) {
34
+ request.traceIds = traceIds;
35
+ }
36
+ if (parentId !== undefined) {
37
+ request.parentId = parentId;
38
+ }
39
+ if (names && names.length > 0) {
40
+ request.name = names;
41
+ }
42
+ if (spanKinds && spanKinds.length > 0) {
43
+ request.spanKind = spanKinds;
44
+ }
45
+ if (statusCodes && statusCodes.length > 0) {
46
+ request.statusCode = statusCodes;
47
+ }
48
+ return request;
49
+ }
50
+ /**
51
+ * Resolve the lower-bound start time for trace and span listing tools.
52
+ *
53
+ * An explicit `since` ISO timestamp takes precedence. When only `lastNMinutes`
54
+ * is provided the start time is computed relative to `now`.
55
+ *
56
+ * @returns An ISO 8601 timestamp string, or `undefined` if no time constraint was given.
57
+ */
58
+ export function resolveStartTime({ since, lastNMinutes, now = new Date(), }) {
59
+ if (since) {
60
+ return since;
61
+ }
62
+ if (lastNMinutes === undefined) {
63
+ return undefined;
64
+ }
65
+ return new Date(now.getTime() - lastNMinutes * MS_PER_MINUTE).toISOString();
66
+ }
67
+ /**
68
+ * Fetch spans for a project, paginating internally until `totalLimit` is
69
+ * reached or all matching spans have been retrieved.
70
+ *
71
+ * @param options.client - The Phoenix REST client.
72
+ * @param options.projectIdentifier - Project name or Relay GlobalID.
73
+ * @param options.filters - Span filter criteria.
74
+ * @param options.totalLimit - Cap on the total number of spans returned.
75
+ * @returns The collected spans and an optional cursor for further pagination.
76
+ */
77
+ export async function fetchProjectSpans({ client, projectIdentifier, filters, totalLimit, }) {
78
+ const normalizedProjectIdentifier = requireIdentifier({
79
+ identifier: projectIdentifier,
80
+ label: "projectIdentifier",
81
+ });
82
+ const collectedSpans = [];
83
+ let cursor = filters.cursor;
84
+ const pageLimit = Math.min(totalLimit || filters.limit || DEFAULT_PAGE_SIZE, MAX_SPAN_QUERY_LIMIT);
85
+ do {
86
+ const response = await getSpans({
87
+ client,
88
+ project: { project: normalizedProjectIdentifier },
89
+ ...buildProjectSpansRequest({
90
+ ...filters,
91
+ cursor,
92
+ limit: pageLimit,
93
+ }),
94
+ });
95
+ collectedSpans.push(...response.spans);
96
+ cursor = response.nextCursor || undefined;
97
+ if (totalLimit !== undefined && collectedSpans.length >= totalLimit) {
98
+ return {
99
+ spans: collectedSpans.slice(0, totalLimit),
100
+ nextCursor: cursor || null,
101
+ };
102
+ }
103
+ } while (cursor);
104
+ return {
105
+ spans: collectedSpans,
106
+ nextCursor: cursor || null,
107
+ };
108
+ }
109
+ /**
110
+ * Split an array into chunks of at most `size` elements.
111
+ */
112
+ function chunkArray(values, size) {
113
+ const chunks = [];
114
+ for (let index = 0; index < values.length; index += size) {
115
+ chunks.push(values.slice(index, index + size));
116
+ }
117
+ return chunks;
118
+ }
119
+ /**
120
+ * Fetch all span annotation pages for a single chunk of span IDs.
121
+ *
122
+ * Paginates internally until every annotation for the given span IDs
123
+ * has been collected.
124
+ */
125
+ async function fetchSpanAnnotationsForChunk({ client, projectIdentifier, spanIds, includeAnnotationNames, excludeAnnotationNames, pageLimit, }) {
126
+ const annotations = [];
127
+ let cursor;
128
+ do {
129
+ const query = {
130
+ span_ids: spanIds,
131
+ limit: pageLimit,
132
+ };
133
+ if (cursor) {
134
+ query.cursor = cursor;
135
+ }
136
+ if (includeAnnotationNames && includeAnnotationNames.length > 0) {
137
+ query.include_annotation_names = includeAnnotationNames;
138
+ }
139
+ if (excludeAnnotationNames && excludeAnnotationNames.length > 0) {
140
+ query.exclude_annotation_names = excludeAnnotationNames;
141
+ }
142
+ const response = await client.GET("/v1/projects/{project_identifier}/span_annotations", {
143
+ params: {
144
+ path: {
145
+ project_identifier: projectIdentifier,
146
+ },
147
+ query,
148
+ },
149
+ });
150
+ const data = getResponseData({
151
+ response,
152
+ errorPrefix: `Failed to fetch span annotations for project "${projectIdentifier}"`,
153
+ });
154
+ annotations.push(...data.data);
155
+ cursor = data.next_cursor || undefined;
156
+ } while (cursor);
157
+ return annotations;
158
+ }
159
+ /**
160
+ * Fetch span annotations in chunks to avoid overwhelming the annotations
161
+ * endpoint with very large span-ID lists.
162
+ *
163
+ * Span IDs are split into chunks of {@link ANNOTATION_CHUNK_SIZE} and
164
+ * fetched with a concurrency cap of {@link MAX_CONCURRENT_ANNOTATION_REQUESTS}.
165
+ *
166
+ * @param options.client - The Phoenix REST client.
167
+ * @param options.projectIdentifier - Project name or Relay GlobalID.
168
+ * @param options.spanIds - Span IDs to fetch annotations for.
169
+ * @param options.includeAnnotationNames - Only return annotations with these names.
170
+ * @param options.excludeAnnotationNames - Exclude annotations with these names.
171
+ * @param options.pageLimit - Per-page limit for annotation pagination.
172
+ * @param options.maxConcurrent - Maximum concurrent chunk requests.
173
+ */
174
+ export async function fetchSpanAnnotations({ client, projectIdentifier, spanIds, includeAnnotationNames, excludeAnnotationNames, pageLimit = ANNOTATION_PAGE_SIZE, maxConcurrent = MAX_CONCURRENT_ANNOTATION_REQUESTS, }) {
175
+ if (spanIds.length === 0) {
176
+ return [];
177
+ }
178
+ const normalizedProjectIdentifier = requireIdentifier({
179
+ identifier: projectIdentifier,
180
+ label: "projectIdentifier",
181
+ });
182
+ const uniqueSpanIds = Array.from(new Set(spanIds));
183
+ const chunks = chunkArray(uniqueSpanIds, ANNOTATION_CHUNK_SIZE);
184
+ const allAnnotations = [];
185
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += maxConcurrent) {
186
+ const batch = chunks.slice(chunkIndex, chunkIndex + maxConcurrent);
187
+ const batchResults = await Promise.all(batch.map((spanIdsChunk) => fetchSpanAnnotationsForChunk({
188
+ client,
189
+ projectIdentifier: normalizedProjectIdentifier,
190
+ spanIds: spanIdsChunk,
191
+ includeAnnotationNames,
192
+ excludeAnnotationNames,
193
+ pageLimit,
194
+ })));
195
+ for (const batchResult of batchResults) {
196
+ allAnnotations.push(...batchResult);
197
+ }
198
+ }
199
+ return allAnnotations;
200
+ }
201
+ /**
202
+ * Merge annotations onto their corresponding spans by `span_id`.
203
+ *
204
+ * Spans that have no matching annotations are returned unmodified.
205
+ * Spans with annotations gain an `annotations` property containing
206
+ * the full list.
207
+ */
208
+ export function attachAnnotationsToSpans({ spans, annotations, }) {
209
+ const annotationsBySpanId = new Map();
210
+ for (const annotation of annotations) {
211
+ const spanAnnotations = annotationsBySpanId.get(annotation.span_id);
212
+ if (spanAnnotations) {
213
+ spanAnnotations.push(annotation);
214
+ }
215
+ else {
216
+ annotationsBySpanId.set(annotation.span_id, [annotation]);
217
+ }
218
+ }
219
+ return spans.map((span) => {
220
+ const spanId = span.context?.span_id;
221
+ const spanAnnotations = spanId
222
+ ? annotationsBySpanId.get(spanId)
223
+ : undefined;
224
+ if (!spanAnnotations || spanAnnotations.length === 0) {
225
+ return span;
226
+ }
227
+ return {
228
+ ...span,
229
+ annotations: spanAnnotations,
230
+ };
231
+ });
232
+ }
@@ -12,10 +12,16 @@ or best practices.
12
12
 
13
13
  Expected return:
14
14
  Expert guidance about how to use and integrate Phoenix`;
15
+ /** Cached RunLLM MCP client, lazily created on first use. */
16
+ let runLLMClient = null;
15
17
  /**
16
- * Creates an MCP client connected to the RunLLM server via HTTP
18
+ * Return a cached MCP client connected to the RunLLM server.
19
+ * Creates and connects the client on first call; subsequent calls reuse it.
17
20
  */
18
- async function createRunLLMClient() {
21
+ async function getRunLLMClient() {
22
+ if (runLLMClient) {
23
+ return runLLMClient;
24
+ }
19
25
  const transport = new StreamableHTTPClientTransport(new URL("https://mcp.runllm.com/mcp"), {
20
26
  requestInit: {
21
27
  headers: {
@@ -28,21 +34,18 @@ async function createRunLLMClient() {
28
34
  version: "1.0.0",
29
35
  });
30
36
  await client.connect(transport);
37
+ runLLMClient = client;
31
38
  return client;
32
39
  }
33
40
  /**
34
- * Calls the chat tool on the RunLLM MCP server
41
+ * Calls the search tool on the RunLLM MCP server.
35
42
  */
36
43
  export async function callRunLLMQuery({ query, }) {
37
- const client = await createRunLLMClient();
38
- // Call the chat tool with the user's question
44
+ const client = await getRunLLMClient();
39
45
  const result = await client.callTool({
40
46
  name: "search",
41
- arguments: {
42
- query: query,
43
- },
47
+ arguments: { query },
44
48
  });
45
- // There's usually only one content item, but we'll handle multiple for safety
46
49
  if (result.content && Array.isArray(result.content)) {
47
50
  const textContent = result.content
48
51
  .filter((item) => item.type === "text")
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Wrap an arbitrary payload as a JSON-formatted MCP text content response.
3
+ *
4
+ * @param payload - The value to serialize. Will be pretty-printed with 2-space indentation.
5
+ * @returns An MCP tool result containing one text content block.
6
+ */
7
+ export function jsonResponse(payload) {
8
+ return {
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: JSON.stringify(payload, null, 2),
13
+ },
14
+ ],
15
+ };
16
+ }
17
+ /**
18
+ * Wrap a plain string as an MCP text content response.
19
+ *
20
+ * @param text - The plain text to return.
21
+ * @returns An MCP tool result containing one text content block.
22
+ */
23
+ export function textResponse(text) {
24
+ return {
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text,
29
+ },
30
+ ],
31
+ };
32
+ }