@langwatch/mcp-server 0.8.0 → 0.10.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +7 -0
  3. package/dist/archive-project-HZBRPR2S.js +19 -0
  4. package/dist/{archive-scenario-YKGLJ4MZ.js → archive-scenario-P5RRITRM.js} +3 -3
  5. package/dist/{archive-suite-QFNDO3Y3.js → archive-suite-NUUFD2EM.js} +3 -3
  6. package/dist/{assign-prompt-tag-Y2B4CBBT.js → assign-prompt-tag-FKCCZPML.js} +2 -2
  7. package/dist/{chunk-YH2IBTEL.js → chunk-42ZHHHIJ.js} +14 -3
  8. package/dist/chunk-ADSJ2FDA.js +44 -0
  9. package/dist/{chunk-L6QENXYA.js → chunk-BB7EOKHR.js} +2 -2
  10. package/dist/chunk-BBJ5WAJB.js +24 -0
  11. package/dist/{chunk-PZUWCWOS.js → chunk-BYWYM2RP.js} +2 -2
  12. package/dist/{chunk-NACSR6B7.js → chunk-CCM4ZFGF.js} +2 -2
  13. package/dist/{chunk-FBRKHHJV.js → chunk-CHKCEZBP.js} +2 -2
  14. package/dist/{chunk-B57MZ4WI.js → chunk-FVDAZ4XG.js} +763 -90
  15. package/dist/{chunk-PO7FM7IX.js → chunk-HKY3GW45.js} +2 -2
  16. package/dist/{chunk-KZNBCKWG.js → chunk-JHOSVOHT.js} +2 -2
  17. package/dist/{chunk-HDJDFKZR.js → chunk-R7C7OUVG.js} +2 -2
  18. package/dist/{chunk-OVQF3D5R.js → chunk-TROIWSEQ.js} +2 -2
  19. package/dist/{chunk-CPUPGDNA.js → chunk-X2SEURT2.js} +2 -2
  20. package/dist/{chunk-YI3OG4PI.js → chunk-XC62KJIT.js} +3 -3
  21. package/dist/{create-agent-YRSDH7P7.js → create-agent-K5Q7BFGZ.js} +3 -3
  22. package/dist/{create-annotation-FYQNKMVJ.js → create-annotation-KN3GE3DK.js} +3 -3
  23. package/dist/create-api-key-OQI5PUNQ.js +26 -0
  24. package/dist/{create-dashboard-CORBR4YQ.js → create-dashboard-G4QA5AXP.js} +3 -3
  25. package/dist/{create-dataset-ZC63T6BB.js → create-dataset-WQSC37N7.js} +3 -3
  26. package/dist/{create-dataset-records-ILVNVKVR.js → create-dataset-records-B4RL3FLC.js} +3 -3
  27. package/dist/{create-evaluator-2AU3PXFB.js → create-evaluator-YFFQIR7E.js} +3 -3
  28. package/dist/create-mcp-server.d.ts +3 -1
  29. package/dist/create-mcp-server.js +2 -1
  30. package/dist/create-project-CUVQ67VN.js +44 -0
  31. package/dist/{create-prompt-NKRJ47YY.js → create-prompt-NGXKSP4X.js} +2 -2
  32. package/dist/{create-prompt-tag-S4JX3ULL.js → create-prompt-tag-IUKYPVHH.js} +2 -2
  33. package/dist/{create-scenario-BPRF75KF.js → create-scenario-FNBXALH3.js} +3 -3
  34. package/dist/{create-suite-4YN7D6K2.js → create-suite-DJEPD4ZB.js} +3 -3
  35. package/dist/{delete-agent-Y72R2YQA.js → delete-agent-TJI5XBBM.js} +3 -3
  36. package/dist/{delete-annotation-UHWDMHSH.js → delete-annotation-KRP53TUU.js} +3 -3
  37. package/dist/{delete-dashboard-LPWT36KN.js → delete-dashboard-N5SZJIR3.js} +3 -3
  38. package/dist/{delete-dataset-GGPQRHKX.js → delete-dataset-KDUOHL2P.js} +3 -3
  39. package/dist/{delete-dataset-records-LTG5LT26.js → delete-dataset-records-DM4W3PAW.js} +3 -3
  40. package/dist/{delete-evaluator-F7QLXDHU.js → delete-evaluator-7ZATLJOA.js} +3 -3
  41. package/dist/{delete-prompt-tag-B5UKZL45.js → delete-prompt-tag-I6HNMSVJ.js} +2 -2
  42. package/dist/{delete-workflow-4DGUDIDE.js → delete-workflow-QEO74JQR.js} +3 -3
  43. package/dist/{get-agent-YEVICUX4.js → get-agent-3KDWCP5N.js} +3 -3
  44. package/dist/{get-analytics-3W5CMU3F.js → get-analytics-GPVJX7SF.js} +2 -2
  45. package/dist/{get-dashboard-NCQHN6MW.js → get-dashboard-YPTIE5YY.js} +3 -3
  46. package/dist/{get-dataset-KBK6PH36.js → get-dataset-6DQFYITM.js} +3 -3
  47. package/dist/{get-evaluator-QDUL5LXR.js → get-evaluator-PCM6XQAU.js} +3 -3
  48. package/dist/get-project-JCMWXVLU.js +26 -0
  49. package/dist/{get-prompt-XCSSNRKR.js → get-prompt-5XAGWWBG.js} +2 -2
  50. package/dist/{get-scenario-FNRKOY7E.js → get-scenario-7XBCDZWX.js} +3 -3
  51. package/dist/{get-simulation-run-WOEKUF7K.js → get-simulation-run-AV24FJWI.js} +3 -3
  52. package/dist/{get-suite-U5AOD5TC.js → get-suite-4DBVHFEM.js} +3 -3
  53. package/dist/{get-trace-MUWEJM2Z.js → get-trace-MRBDS3HJ.js} +2 -2
  54. package/dist/{get-workflow-SOA7MT7L.js → get-workflow-UR4Y6UFX.js} +3 -3
  55. package/dist/{http-server-7DMOCB75.js → http-server-XUQLIFE7.js} +5 -3
  56. package/dist/index.js +1175 -222
  57. package/dist/{langwatch-api-annotations-Q57NDUVR.js → langwatch-api-annotations-ZJDRB4JH.js} +3 -3
  58. package/dist/{langwatch-api-dashboards-VBWWXV3Y.js → langwatch-api-dashboards-WARLI6NF.js} +3 -3
  59. package/dist/{langwatch-api-monitors-L3IY7FDT.js → langwatch-api-monitors-4XKXHYCG.js} +2 -2
  60. package/dist/{langwatch-api-secrets-S5HHF3XR.js → langwatch-api-secrets-6SHK5G7Q.js} +2 -2
  61. package/dist/{langwatch-api-triggers-ME33QE26.js → langwatch-api-triggers-5SX6R2WU.js} +2 -2
  62. package/dist/{langwatch-api-workflows-4CJQAYP2.js → langwatch-api-workflows-NLJENNXE.js} +3 -3
  63. package/dist/{list-agents-3HGIBJFM.js → list-agents-NECO7MPI.js} +3 -3
  64. package/dist/{list-annotations-KWXYVEPI.js → list-annotations-3OJEO774.js} +3 -3
  65. package/dist/list-api-keys-AWW7ROLB.js +37 -0
  66. package/dist/{list-dashboards-VQ2LH666.js → list-dashboards-DVPVVVKW.js} +3 -3
  67. package/dist/{list-dataset-records-RNRGROJP.js → list-dataset-records-I4BYORF5.js} +3 -3
  68. package/dist/{list-datasets-I4A2LT2S.js → list-datasets-WUAE5HGX.js} +3 -3
  69. package/dist/{list-evaluators-AKKRJ5MX.js → list-evaluators-B6OSVAM2.js} +3 -3
  70. package/dist/{list-model-providers-DVFEHFHZ.js → list-model-providers-NL6IP4UZ.js} +3 -3
  71. package/dist/list-projects-5FHMJ4PQ.js +42 -0
  72. package/dist/{list-prompt-tags-PL5OISUA.js → list-prompt-tags-Z2DD6FLQ.js} +2 -2
  73. package/dist/{list-prompts-7VTFWE2Z.js → list-prompts-VNXKHK7T.js} +2 -2
  74. package/dist/{list-scenarios-5BZWZ6ZN.js → list-scenarios-CFZHXL76.js} +3 -3
  75. package/dist/{list-simulation-runs-WCZ6RLPE.js → list-simulation-runs-AM2BXLBW.js} +3 -3
  76. package/dist/{list-suites-RRQ2JX5Y.js → list-suites-ZDOHAAXF.js} +3 -3
  77. package/dist/{list-workflows-UQB5FCX5.js → list-workflows-WBRBKLIT.js} +3 -3
  78. package/dist/{rename-prompt-tag-TURD6KYU.js → rename-prompt-tag-A4RJUK7Z.js} +2 -2
  79. package/dist/revoke-api-key-ZSPSPF5G.js +15 -0
  80. package/dist/{run-agent-BEJ4K4CM.js → run-agent-JFNDY7MR.js} +3 -3
  81. package/dist/{run-suite-U7V432G2.js → run-suite-TH7LO5TJ.js} +3 -3
  82. package/dist/{run-workflow-5WX7DK5T.js → run-workflow-CK3JP5U2.js} +3 -3
  83. package/dist/{search-traces-ZCHNOMTX.js → search-traces-3BBVYPC3.js} +2 -2
  84. package/dist/{set-model-provider-6CFHMCNE.js → set-model-provider-WOC5FNMV.js} +3 -3
  85. package/dist/{update-agent-UE6UWLZP.js → update-agent-YMEV5MTL.js} +3 -3
  86. package/dist/{update-dataset-4XQZZE4S.js → update-dataset-EYT5POFM.js} +3 -3
  87. package/dist/{update-dataset-record-GNNM5TKM.js → update-dataset-record-VFF5JTX4.js} +3 -3
  88. package/dist/{update-evaluator-LHVDXIZD.js → update-evaluator-6IJHFG2E.js} +3 -3
  89. package/dist/update-project-OIT4WPFI.js +24 -0
  90. package/dist/{update-prompt-3XEHECJ7.js → update-prompt-ZU2DD6KM.js} +2 -2
  91. package/dist/{update-scenario-DFPREYCD.js → update-scenario-3VUTPDRU.js} +3 -3
  92. package/dist/{update-suite-RATU5THO.js → update-suite-C6JFDQHL.js} +3 -3
  93. package/package.json +2 -2
  94. package/pnpm-workspace.yaml +7 -0
  95. package/pyproject.toml +3 -0
  96. package/src/__tests__/experiment-list-tools.unit.test.ts +157 -0
  97. package/src/__tests__/experiment-status.unit.test.ts +194 -0
  98. package/src/__tests__/get-experiment-results.unit.test.ts +313 -0
  99. package/src/__tests__/project-api-key-tools.unit.test.ts +451 -0
  100. package/src/create-mcp-server.d.ts +3 -1
  101. package/src/create-mcp-server.ts +263 -13
  102. package/src/http-server.ts +5 -0
  103. package/src/langwatch-api-api-keys.ts +54 -0
  104. package/src/langwatch-api-projects.ts +82 -0
  105. package/src/langwatch-api.ts +17 -3
  106. package/src/tools/archive-project.ts +14 -0
  107. package/src/tools/create-api-key.ts +29 -0
  108. package/src/tools/create-project.ts +45 -0
  109. package/src/tools/experiment-run-status.ts +36 -0
  110. package/src/tools/get-experiment-results.ts +306 -0
  111. package/src/tools/get-project.ts +20 -0
  112. package/src/tools/list-api-keys.ts +33 -0
  113. package/src/tools/list-experiment-runs.ts +142 -0
  114. package/src/tools/list-experiments.ts +95 -0
  115. package/src/tools/list-projects.ts +41 -0
  116. package/src/tools/revoke-api-key.ts +9 -0
  117. package/src/tools/run-experiment.ts +182 -0
  118. package/src/tools/update-project.ts +22 -0
  119. package/uv.lock +7 -3
  120. package/dist/run-evaluation-AJ7WZNQD.js +0 -64
  121. package/src/tools/run-evaluation.ts +0 -87
@@ -0,0 +1,306 @@
1
+ import { LangWatchApiError, makeRequest } from "../langwatch-api.js";
2
+ import { deriveRunStatus, isTerminalStatus } from "./experiment-run-status.js";
3
+
4
+ interface DatasetEntry {
5
+ index: number;
6
+ targetId?: string | null;
7
+ entry: Record<string, unknown>;
8
+ predicted?: Record<string, unknown>;
9
+ cost?: number | null;
10
+ duration?: number | null;
11
+ error?: string | null;
12
+ traceId?: string | null;
13
+ }
14
+
15
+ interface EvaluationItem {
16
+ evaluator: string;
17
+ name?: string | null;
18
+ index: number;
19
+ targetId?: string | null;
20
+ status: "processed" | "skipped" | "error";
21
+ score?: number | null;
22
+ label?: string | null;
23
+ passed?: boolean | null;
24
+ details?: string | null;
25
+ inputs?: Record<string, unknown> | null;
26
+ }
27
+
28
+ interface EvaluationRunResults {
29
+ experimentId: string;
30
+ runId: string;
31
+ projectId: string;
32
+ progress?: number | null;
33
+ total?: number | null;
34
+ dataset: DatasetEntry[];
35
+ evaluations: EvaluationItem[];
36
+ timestamps: {
37
+ createdAt: number;
38
+ updatedAt: number;
39
+ finishedAt?: number | null;
40
+ stoppedAt?: number | null;
41
+ };
42
+ }
43
+
44
+ const DEFAULT_ROW_CAP = 50;
45
+
46
+ const rowKey = (index: number, targetId?: string | null): string =>
47
+ `${index}:${targetId ?? ""}`;
48
+
49
+ const summarizeEntry = (entry: Record<string, unknown>): string => {
50
+ const candidates = ["input", "question", "query", "prompt", "user"];
51
+ for (const key of candidates) {
52
+ const value = entry[key];
53
+ if (typeof value === "string" && value.length > 0) {
54
+ return value.length > 80 ? `${value.slice(0, 77)}...` : value;
55
+ }
56
+ }
57
+ return "";
58
+ };
59
+
60
+ const isFailedEvaluation = (e: EvaluationItem): boolean =>
61
+ e.status === "error" || e.passed === false;
62
+
63
+ const isFailedRow = ({
64
+ entry,
65
+ evaluations,
66
+ }: {
67
+ entry: DatasetEntry;
68
+ evaluations: EvaluationItem[];
69
+ }): boolean => Boolean(entry.error) || evaluations.some(isFailedEvaluation);
70
+
71
+ export async function handleExperimentResults(params: {
72
+ runId: string;
73
+ experimentSlug?: string;
74
+ filter?: "all" | "failed";
75
+ evaluator?: string;
76
+ limit?: number;
77
+ }): Promise<string> {
78
+ const filter = params.filter ?? "all";
79
+ const evaluatorFilter = params.evaluator?.trim();
80
+ const limit =
81
+ typeof params.limit === "number" && params.limit > 0
82
+ ? Math.min(params.limit, DEFAULT_ROW_CAP)
83
+ : DEFAULT_ROW_CAP;
84
+
85
+ const search = new URLSearchParams();
86
+ if (params.experimentSlug) search.set("experimentSlug", params.experimentSlug);
87
+ const qs = search.toString() ? `?${search.toString()}` : "";
88
+
89
+ let results: EvaluationRunResults | null;
90
+ try {
91
+ results = (await makeRequest(
92
+ "GET",
93
+ `/api/experiments/runs/${encodeURIComponent(params.runId)}/results${qs}`,
94
+ )) as EvaluationRunResults;
95
+ } catch (error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ const status =
98
+ error instanceof LangWatchApiError
99
+ ? error.status
100
+ : error && typeof error === "object" &&
101
+ "status" in error &&
102
+ typeof (error as { status?: unknown }).status === "number"
103
+ ? (error as { status: number }).status
104
+ : undefined;
105
+ if (
106
+ status === 404 ||
107
+ (status === undefined && /404|not found/i.test(message))
108
+ ) {
109
+ return [
110
+ `# Evaluation Results: ${params.runId}`,
111
+ "",
112
+ "**Status**: not found",
113
+ "",
114
+ `Could not load results for run \`${params.runId}\`. The run id may be incorrect, or its run state may have expired (Redis keeps it for 24h).`,
115
+ "",
116
+ "> Pass `experimentSlug` to load results for runs older than 24h. Discover the slug with `platform_experiment_list`, then `platform_experiment_list_runs` for the run ids.",
117
+ ].join("\n");
118
+ }
119
+ throw error;
120
+ }
121
+
122
+ if (!results) {
123
+ return [
124
+ `# Evaluation Results: ${params.runId}`,
125
+ "",
126
+ "**Status**: not found",
127
+ "",
128
+ `Could not load results for run \`${params.runId}\`.`,
129
+ "",
130
+ "> Pass `experimentSlug` if the run is older than 24h. Discover the slug with `platform_experiment_list`, then `platform_experiment_list_runs` to confirm the run id.",
131
+ ].join("\n");
132
+ }
133
+
134
+ // Partial results are served even while the run is still in progress.
135
+ // Rows land in ClickHouse incrementally, so a "running" or "interrupted"
136
+ // run can still expose every row recorded so far. A run only fails to set
137
+ // finished_at/stopped_at when the SDK process dies before flushing, but the
138
+ // rows it did log are still useful, so we never gate on a terminal status.
139
+ const runStatus = deriveRunStatus(results.timestamps);
140
+ const partial = !isTerminalStatus(runStatus);
141
+
142
+ // Group evaluations by target-scoped row key, applying evaluator filter.
143
+ const evaluationsByRow = new Map<string, EvaluationItem[]>();
144
+ for (const evaluation of results.evaluations) {
145
+ if (evaluatorFilter && evaluation.evaluator !== evaluatorFilter) continue;
146
+ const key = rowKey(evaluation.index, evaluation.targetId);
147
+ const list = evaluationsByRow.get(key) ?? [];
148
+ list.push(evaluation);
149
+ evaluationsByRow.set(key, list);
150
+ }
151
+
152
+ const evaluatorNames = Array.from(
153
+ new Set(
154
+ (evaluatorFilter
155
+ ? results.evaluations.filter((e) => e.evaluator === evaluatorFilter)
156
+ : results.evaluations
157
+ ).map((e) => e.evaluator),
158
+ ),
159
+ );
160
+
161
+ let rows = results.dataset.map((entry) => ({
162
+ entry,
163
+ evaluations: evaluationsByRow.get(rowKey(entry.index, entry.targetId)) ?? [],
164
+ }));
165
+
166
+ if (filter === "failed") {
167
+ rows = rows.filter((r) =>
168
+ isFailedRow({ entry: r.entry, evaluations: r.evaluations }),
169
+ );
170
+ }
171
+
172
+ const totalMatching = rows.length;
173
+ const rowsForSummary = rows;
174
+ const truncated = rows.length > limit;
175
+ rows = rows.slice(0, limit);
176
+
177
+ // Per-evaluator stats across the filtered rows (before truncation),
178
+ // so the summary matches the displayed subset when filter="failed".
179
+ const evaluatorAverages = new Map<
180
+ string,
181
+ { sum: number; count: number; passed: number; failed: number; errored: number }
182
+ >();
183
+ for (const r of rowsForSummary) {
184
+ for (const e of r.evaluations) {
185
+ const stats =
186
+ evaluatorAverages.get(e.evaluator) ?? {
187
+ sum: 0,
188
+ count: 0,
189
+ passed: 0,
190
+ failed: 0,
191
+ errored: 0,
192
+ };
193
+ if (typeof e.score === "number") {
194
+ stats.sum += e.score;
195
+ stats.count += 1;
196
+ }
197
+ if (e.status === "error") stats.errored += 1;
198
+ else if (e.passed === true) stats.passed += 1;
199
+ else if (e.passed === false) stats.failed += 1;
200
+ evaluatorAverages.set(e.evaluator, stats);
201
+ }
202
+ }
203
+
204
+ const lines: string[] = [];
205
+ lines.push(`# Evaluation Results: ${results.runId}`);
206
+ lines.push("");
207
+ lines.push(`**Experiment**: ${results.experimentId}`);
208
+ lines.push(`**Status**: ${runStatus}`);
209
+ if (typeof results.total === "number" && results.total > 0) {
210
+ lines.push(
211
+ `**Progress**: ${results.progress ?? results.dataset.length}/${results.total} rows`,
212
+ );
213
+ }
214
+ lines.push(`**Total rows**: ${results.dataset.length}`);
215
+ lines.push(`**Total evaluations**: ${results.evaluations.length}`);
216
+ if (filter === "failed") {
217
+ lines.push(`**Filter**: failed only`);
218
+ }
219
+ if (evaluatorFilter) {
220
+ lines.push(`**Evaluator filter**: ${evaluatorFilter}`);
221
+ }
222
+ if (partial) {
223
+ lines.push("");
224
+ lines.push(
225
+ runStatus === "interrupted"
226
+ ? "> These are partial results. The run never sent a finished/stopped marker and has had no updates recently, so it likely was interrupted before completing. The rows below are everything recorded so far."
227
+ : "> These are partial results. The run is still in progress, so more rows may appear on a later call.",
228
+ );
229
+ }
230
+ lines.push("");
231
+
232
+ if (evaluatorAverages.size > 0) {
233
+ lines.push("## Evaluator Summary");
234
+ lines.push("");
235
+ lines.push("| Evaluator | Avg Score | Passed | Failed | Errored |");
236
+ lines.push("| --- | --- | --- | --- | --- |");
237
+ for (const [name, stats] of evaluatorAverages) {
238
+ const avg =
239
+ stats.count > 0 ? (stats.sum / stats.count).toFixed(3) : "—";
240
+ lines.push(
241
+ `| ${name} | ${avg} | ${stats.passed} | ${stats.failed} | ${stats.errored} |`,
242
+ );
243
+ }
244
+ lines.push("");
245
+ }
246
+
247
+ if (rows.length === 0) {
248
+ if (filter === "failed") {
249
+ lines.push("_No rows matched the filter._");
250
+ } else if (runStatus === "running") {
251
+ lines.push(
252
+ "_No rows recorded yet. The run is still in progress; call again shortly._",
253
+ );
254
+ } else if (runStatus === "interrupted") {
255
+ lines.push("_No rows were recorded before the run was interrupted._");
256
+ } else {
257
+ lines.push("_No rows recorded for this run._");
258
+ }
259
+ return lines.join("\n");
260
+ }
261
+
262
+ lines.push(
263
+ `## Rows (${rows.length}${truncated ? ` of ${totalMatching}` : ""})`,
264
+ );
265
+ lines.push("");
266
+
267
+ for (const { entry, evaluations } of rows) {
268
+ const summary = summarizeEntry(entry.entry);
269
+ lines.push(`### Row #${entry.index}${summary ? ` — ${summary}` : ""}`);
270
+ if (entry.error) {
271
+ lines.push(`- **Error**: ${entry.error}`);
272
+ }
273
+ if (entry.traceId) {
274
+ lines.push(`- **Trace ID**: \`${entry.traceId}\``);
275
+ }
276
+ if (evaluations.length === 0) {
277
+ lines.push(
278
+ evaluatorNames.length === 0
279
+ ? "- _No evaluations recorded_"
280
+ : "- _No evaluations recorded for this row_",
281
+ );
282
+ }
283
+ for (const e of evaluations) {
284
+ const parts: string[] = [`**${e.evaluator}**`];
285
+ if (e.status === "error") parts.push("status=error");
286
+ else if (typeof e.score === "number") parts.push(`score=${e.score.toFixed(3)}`);
287
+ if (typeof e.passed === "boolean") {
288
+ parts.push(`passed=${e.passed ? "yes" : "no"}`);
289
+ }
290
+ if (e.label) parts.push(`label=${e.label}`);
291
+ lines.push(`- ${parts.join(" · ")}`);
292
+ if (e.details) {
293
+ lines.push(` - details: ${e.details}`);
294
+ }
295
+ }
296
+ lines.push("");
297
+ }
298
+
299
+ if (truncated) {
300
+ lines.push(
301
+ `> Output truncated to ${limit} rows of ${totalMatching} matching to protect the agent's context window. Pass \`limit\` to expand or filter further.`,
302
+ );
303
+ }
304
+
305
+ return lines.join("\n");
306
+ }
@@ -0,0 +1,20 @@
1
+ import { getProject as apiGetProject } from "../langwatch-api-projects.js";
2
+
3
+ export async function handleGetProject(params: {
4
+ id: string;
5
+ }): Promise<string> {
6
+ const project = await apiGetProject(params.id);
7
+
8
+ const lines: string[] = [];
9
+ lines.push(`# ${project.name}\n`);
10
+ lines.push(`**ID**: ${project.id}`);
11
+ lines.push(`**Slug**: ${project.slug}`);
12
+ lines.push(`**Language**: ${project.language}`);
13
+ lines.push(`**Framework**: ${project.framework}`);
14
+ lines.push(`**Team ID**: ${project.teamId}`);
15
+ lines.push(`**PII Redaction**: ${project.piiRedactionLevel}`);
16
+ lines.push(`**Created**: ${project.createdAt}`);
17
+ lines.push(`**Updated**: ${project.updatedAt}`);
18
+
19
+ return lines.join("\n");
20
+ }
@@ -0,0 +1,33 @@
1
+ import { listApiKeys as apiListApiKeys } from "../langwatch-api-api-keys.js";
2
+
3
+ export async function handleListApiKeys(): Promise<string> {
4
+ const result = await apiListApiKeys();
5
+ const keys = result.data;
6
+
7
+ if (!Array.isArray(keys) || keys.length === 0) {
8
+ return "No API keys found.\n\n> Tip: Use `platform_create_api_key` to create a new key.";
9
+ }
10
+
11
+ const lines: string[] = [];
12
+ lines.push(`# API Keys (${keys.length} total)\n`);
13
+
14
+ for (const k of keys) {
15
+ const status = k.revokedAt ? "REVOKED" : k.expiresAt && new Date(k.expiresAt) < new Date() ? "EXPIRED" : "ACTIVE";
16
+ lines.push(`## ${k.name}`);
17
+ lines.push(`**ID**: ${k.id}`);
18
+ lines.push(`**Status**: ${status}`);
19
+ if (k.description) lines.push(`**Description**: ${k.description}`);
20
+ if (k.expiresAt) lines.push(`**Expires**: ${k.expiresAt}`);
21
+ if (k.lastUsedAt) lines.push(`**Last Used**: ${k.lastUsedAt}`);
22
+ lines.push(`**Created**: ${k.createdAt}`);
23
+ if (k.roleBindings.length > 0) {
24
+ const bindings = k.roleBindings
25
+ .map((rb) => `${rb.role} on ${rb.scopeType}:${rb.scopeId}`)
26
+ .join(", ");
27
+ lines.push(`**Bindings**: ${bindings}`);
28
+ }
29
+ lines.push("");
30
+ }
31
+
32
+ return lines.join("\n");
33
+ }
@@ -0,0 +1,142 @@
1
+ import { makeRequest } from "../langwatch-api.js";
2
+
3
+ interface RunSummaryEvaluator {
4
+ name: string;
5
+ averageScore: number | null;
6
+ averagePassed?: number;
7
+ }
8
+
9
+ interface RunSummary {
10
+ evaluations: Record<string, RunSummaryEvaluator>;
11
+ }
12
+
13
+ interface ExperimentRunSummaryEntry {
14
+ experimentId: string;
15
+ runId: string;
16
+ workflowVersion: { id: string; version: string } | null;
17
+ timestamps: {
18
+ createdAt: number;
19
+ updatedAt: number;
20
+ finishedAt?: number | null;
21
+ stoppedAt?: number | null;
22
+ };
23
+ progress?: number | null;
24
+ total?: number | null;
25
+ summary: RunSummary;
26
+ }
27
+
28
+ interface ExperimentRunsListResponse {
29
+ experimentId: string;
30
+ experimentSlug: string;
31
+ runs: ExperimentRunSummaryEntry[];
32
+ pagination: {
33
+ page: number;
34
+ pageSize: number;
35
+ totalHits: number;
36
+ hasMore: boolean;
37
+ };
38
+ }
39
+
40
+ const DEFAULT_LIMIT = 25;
41
+ const MAX_LIMIT = 100;
42
+
43
+ const formatEpoch = (epoch: number | null | undefined): string => {
44
+ if (!epoch || !Number.isFinite(epoch)) return "—";
45
+ return new Date(epoch).toISOString().replace("T", " ").slice(0, 19) + " UTC";
46
+ };
47
+
48
+ const passRate = (evaluations: RunSummary["evaluations"]): string => {
49
+ const entries = Object.values(evaluations ?? {});
50
+ if (entries.length === 0) return "—";
51
+ const passed = entries.filter((e) => typeof e.averagePassed === "number");
52
+ if (passed.length === 0) {
53
+ const scored = entries.filter((e) => typeof e.averageScore === "number");
54
+ if (scored.length === 0) return "—";
55
+ const avg =
56
+ scored.reduce((sum, e) => sum + (e.averageScore ?? 0), 0) / scored.length;
57
+ return `${avg.toFixed(2)} avg`;
58
+ }
59
+ const avg =
60
+ passed.reduce((sum, e) => sum + (e.averagePassed ?? 0), 0) / passed.length;
61
+ return `${(avg * 100).toFixed(0)}% pass`;
62
+ };
63
+
64
+ const runStatus = (run: ExperimentRunSummaryEntry): string => {
65
+ if (run.timestamps.stoppedAt) return "stopped";
66
+ if (run.timestamps.finishedAt) return "completed";
67
+ return "running";
68
+ };
69
+
70
+ export async function handleExperimentListRuns(params: {
71
+ experimentSlug: string;
72
+ limit?: number;
73
+ }): Promise<string> {
74
+ const requested =
75
+ typeof params.limit === "number" && params.limit > 0
76
+ ? params.limit
77
+ : DEFAULT_LIMIT;
78
+ const effectiveLimit = Math.min(requested, MAX_LIMIT);
79
+
80
+ const search = new URLSearchParams();
81
+ search.set("experimentSlug", params.experimentSlug);
82
+ search.set("pageSize", String(effectiveLimit));
83
+
84
+ let result: ExperimentRunsListResponse;
85
+ try {
86
+ result = (await makeRequest(
87
+ "GET",
88
+ `/api/experiments/runs?${search.toString()}`,
89
+ )) as ExperimentRunsListResponse;
90
+ } catch (error) {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ if (/404|not found/i.test(message)) {
93
+ return [
94
+ `# Evaluation Runs: ${params.experimentSlug}`,
95
+ "",
96
+ "**Status**: experiment not found",
97
+ "",
98
+ `No experiment with slug \`${params.experimentSlug}\` exists in this project.`,
99
+ "",
100
+ "> Run `platform_experiment_list` to discover available experiment slugs.",
101
+ ].join("\n");
102
+ }
103
+ throw error;
104
+ }
105
+
106
+ if (result.runs.length === 0) {
107
+ return [
108
+ `# Evaluation Runs: ${result.experimentSlug}`,
109
+ "",
110
+ "_This experiment has no runs yet._",
111
+ "",
112
+ `> Trigger a run with \`platform_run_experiment\` (slug: \`${result.experimentSlug}\`) to populate this list.`,
113
+ ].join("\n");
114
+ }
115
+
116
+ const lines: string[] = [];
117
+ lines.push(`# Evaluation Runs: ${result.experimentSlug}`);
118
+ lines.push("");
119
+ lines.push(
120
+ `Showing ${result.runs.length} of ${result.pagination.totalHits} run${result.pagination.totalHits === 1 ? "" : "s"}.`,
121
+ );
122
+ if (result.pagination.hasMore) {
123
+ lines.push(
124
+ `> More runs exist beyond this limit. Re-run with a higher \`limit\` (max ${MAX_LIMIT}) if needed.`,
125
+ );
126
+ }
127
+ lines.push("");
128
+ lines.push("| Run ID | Status | Started | Finished | Result |");
129
+ lines.push("|--------|--------|---------|----------|--------|");
130
+
131
+ for (const run of result.runs) {
132
+ lines.push(
133
+ `| \`${run.runId}\` | ${runStatus(run)} | ${formatEpoch(run.timestamps.createdAt)} | ${formatEpoch(run.timestamps.finishedAt ?? null)} | ${passRate(run.summary?.evaluations ?? {})} |`,
134
+ );
135
+ }
136
+
137
+ lines.push("");
138
+ lines.push(
139
+ "> Use `platform_experiment_status <runId>` for live progress, or `platform_experiment_results <runId>` for per-row results.",
140
+ );
141
+ return lines.join("\n");
142
+ }
@@ -0,0 +1,95 @@
1
+ import { makeRequest } from "../langwatch-api.js";
2
+
3
+ interface ExperimentSummary {
4
+ id: string;
5
+ slug: string;
6
+ name: string | null;
7
+ type: string;
8
+ workflowId: string | null;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ runsCount: number;
12
+ lastRunAt: string | null;
13
+ }
14
+
15
+ interface ExperimentListResponse {
16
+ experiments: ExperimentSummary[];
17
+ pagination: {
18
+ page: number;
19
+ pageSize: number;
20
+ totalHits: number;
21
+ hasMore: boolean;
22
+ };
23
+ }
24
+
25
+ const DEFAULT_LIMIT = 25;
26
+ const MAX_LIMIT = 100;
27
+
28
+ const formatTimestamp = (iso: string | null): string => {
29
+ if (!iso) return "—";
30
+ const d = new Date(iso);
31
+ if (Number.isNaN(d.getTime())) return iso;
32
+ return d.toISOString().replace("T", " ").slice(0, 19) + " UTC";
33
+ };
34
+
35
+ const escapeMarkdownTableCell = (value: string): string =>
36
+ value.replace(/[\\|]/g, (c) => `\\${c}`).replace(/\n/g, " ");
37
+
38
+ const formatInlineCode = (value: string): string =>
39
+ `\`${value.replace(/[\\`|]/g, (c) => `\\${c}`).replace(/\n/g, " ")}\``;
40
+
41
+ export async function handleExperimentList(params: {
42
+ limit?: number;
43
+ }): Promise<string> {
44
+ const requested =
45
+ typeof params.limit === "number" && params.limit > 0
46
+ ? params.limit
47
+ : DEFAULT_LIMIT;
48
+ const effectiveLimit = Math.min(requested, MAX_LIMIT);
49
+
50
+ const search = new URLSearchParams();
51
+ search.set("pageSize", String(effectiveLimit));
52
+
53
+ const result = (await makeRequest(
54
+ "GET",
55
+ `/api/experiments?${search.toString()}`,
56
+ )) as ExperimentListResponse;
57
+
58
+ if (result.experiments.length === 0) {
59
+ return [
60
+ "# Experiments",
61
+ "",
62
+ "_No experiments found in this project._",
63
+ "",
64
+ "> Create an experiment in the LangWatch dashboard, then re-run this tool to discover its slug.",
65
+ ].join("\n");
66
+ }
67
+
68
+ const lines: string[] = [];
69
+ lines.push("# Experiments");
70
+ lines.push("");
71
+ lines.push(
72
+ `Showing ${result.experiments.length} of ${result.pagination.totalHits} experiment${result.pagination.totalHits === 1 ? "" : "s"}.`,
73
+ );
74
+ if (result.pagination.hasMore) {
75
+ lines.push(
76
+ `> More experiments exist beyond this limit. Re-run with a higher \`limit\` (max ${MAX_LIMIT}) if needed.`,
77
+ );
78
+ }
79
+ lines.push("");
80
+ lines.push("| Slug | Name | Runs | Last Run |");
81
+ lines.push("|------|------|------|----------|");
82
+
83
+ for (const exp of result.experiments) {
84
+ const name = escapeMarkdownTableCell(exp.name ?? exp.slug);
85
+ lines.push(
86
+ `| ${formatInlineCode(exp.slug)} | ${name} | ${exp.runsCount} | ${formatTimestamp(exp.lastRunAt)} |`,
87
+ );
88
+ }
89
+
90
+ lines.push("");
91
+ lines.push(
92
+ "> Use `platform_experiment_list_runs` with one of these slugs to see its runs.",
93
+ );
94
+ return lines.join("\n");
95
+ }
@@ -0,0 +1,41 @@
1
+ import { listProjects as apiListProjects } from "../langwatch-api-projects.js";
2
+
3
+ export async function handleListProjects(params?: {
4
+ page?: number;
5
+ limit?: number;
6
+ }): Promise<string> {
7
+ const result = await apiListProjects({
8
+ page: params?.page,
9
+ limit: params?.limit ?? 100,
10
+ });
11
+ const projects = result.data;
12
+
13
+ if (!Array.isArray(projects) || projects.length === 0) {
14
+ return "No projects found in this organization.\n\n> Tip: Use `platform_create_project` to create your first project.";
15
+ }
16
+
17
+ const lines: string[] = [];
18
+ lines.push(`# Projects (${result.pagination.total} total)\n`);
19
+
20
+ for (const p of projects) {
21
+ lines.push(`## ${p.name}`);
22
+ lines.push(`**ID**: ${p.id}`);
23
+ lines.push(`**Slug**: ${p.slug}`);
24
+ lines.push(`**Language**: ${p.language}`);
25
+ lines.push(`**Framework**: ${p.framework}`);
26
+ lines.push(`**Updated**: ${p.updatedAt}`);
27
+ lines.push("");
28
+ }
29
+
30
+ if (result.pagination.totalPages > 1) {
31
+ lines.push(
32
+ `> Showing page ${result.pagination.page} of ${result.pagination.totalPages}. Use \`page\` parameter to navigate.`,
33
+ );
34
+ }
35
+
36
+ lines.push(
37
+ "> Use `platform_get_project` with the ID to see full project details.",
38
+ );
39
+
40
+ return lines.join("\n");
41
+ }
@@ -0,0 +1,9 @@
1
+ import { revokeApiKey as apiRevokeApiKey } from "../langwatch-api-api-keys.js";
2
+
3
+ export async function handleRevokeApiKey(params: {
4
+ id: string;
5
+ }): Promise<string> {
6
+ await apiRevokeApiKey(params.id);
7
+
8
+ return `API key ${params.id} revoked successfully.`;
9
+ }