@langwatch/mcp-server 1.0.0 → 2.1.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 (204) hide show
  1. package/.python-version +1 -0
  2. package/CHANGELOG.md +110 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +86 -4
  5. package/dist/{archive-project-MK3673A7.js → archive-project-W3IKI7UL.js} +5 -5
  6. package/dist/archive-run-plan-474OKHHU.js +16 -0
  7. package/dist/{archive-scenario-KATSXRIN.js → archive-scenario-V5UNYK26.js} +5 -5
  8. package/dist/archive-test-suite-O7FLDMXK.js +16 -0
  9. package/dist/{assign-prompt-tag-E3C5W6PI.js → assign-prompt-tag-Q46WRB6P.js} +4 -4
  10. package/dist/chunk-56OGTS24.js +44 -0
  11. package/dist/chunk-6DJDEXET.js +63 -0
  12. package/dist/chunk-7E4FZIYJ.js +15 -0
  13. package/dist/{chunk-J45SVWPR.js → chunk-AN4OUA4D.js} +2 -2
  14. package/dist/chunk-CKNAH5TG.js +74 -0
  15. package/dist/{chunk-CE6QMZOP.js → chunk-EYXMLE5X.js} +86 -11
  16. package/dist/{chunk-XY4STOBE.js → chunk-G4ZAZTZW.js} +2 -2
  17. package/dist/{chunk-5UWY24RP.js → chunk-GELJ2MQR.js} +12 -3
  18. package/dist/chunk-HV4DWKBI.js +46 -0
  19. package/dist/chunk-JR6DJZOU.js +49 -0
  20. package/dist/{chunk-DAIQKS27.js → chunk-JTQJRNFJ.js} +2 -2
  21. package/dist/{chunk-WWMDHZZV.js → chunk-K37ZNPXK.js} +2 -2
  22. package/dist/{chunk-EU22L52K.js → chunk-OJLYYJWI.js} +2 -2
  23. package/dist/{chunk-Y3HDZ6OW.js → chunk-QAMP3CGR.js} +2 -2
  24. package/dist/{chunk-VYPSKRPN.js → chunk-QRO35SOM.js} +347 -146
  25. package/dist/{chunk-O3LWVN67.js → chunk-RVAE2MEA.js} +2 -2
  26. package/dist/{chunk-UXXA4D6K.js → chunk-S4GVX36W.js} +2 -2
  27. package/dist/{chunk-BJ7B4R26.js → chunk-SAS5OB7F.js} +18 -2
  28. package/dist/{chunk-RYRB2YVJ.js → chunk-SR6QPRIE.js} +2 -2
  29. package/dist/chunk-UF6XIGYR.js +58 -0
  30. package/dist/{chunk-T7WJVS2N.js → chunk-UFOYFYI7.js} +2 -2
  31. package/dist/chunk-UJPXFA26.js +267 -0
  32. package/dist/{chunk-KOO5733S.js → chunk-WT4PO4A3.js} +8 -2
  33. package/dist/config.js +2 -2
  34. package/dist/{create-agent-7W57J7MT.js → create-agent-JFJ2K6X3.js} +5 -5
  35. package/dist/{create-annotation-W2XELBCQ.js → create-annotation-EUXXQ6NS.js} +5 -5
  36. package/dist/{create-api-key-MKRXKFJM.js → create-api-key-BGBB76JX.js} +5 -5
  37. package/dist/{create-dashboard-QGL7IODC.js → create-dashboard-Z76AIPLS.js} +5 -5
  38. package/dist/{create-dataset-3PH7A4HG.js → create-dataset-FD5VAXU4.js} +5 -5
  39. package/dist/{create-dataset-records-4DZS3VRL.js → create-dataset-records-WQUXXOHI.js} +5 -5
  40. package/dist/{create-evaluator-YWH6NC4M.js → create-evaluator-6NV33Z52.js} +5 -5
  41. package/dist/create-mcp-server.js +9 -4
  42. package/dist/{create-project-BOBQ32C5.js → create-project-ZQ7TFIO2.js} +5 -5
  43. package/dist/{create-prompt-FLCFGITE.js → create-prompt-BS6QRCUU.js} +4 -4
  44. package/dist/{create-prompt-tag-EMPNIKEA.js → create-prompt-tag-FCQ5ELSQ.js} +4 -4
  45. package/dist/{create-scenario-BSBIHUL6.js → create-scenario-DRPTPXU3.js} +12 -5
  46. package/dist/create-test-suite-ZIZCCFJ4.js +37 -0
  47. package/dist/{delete-agent-7MSYNY5E.js → delete-agent-SA4JMUZF.js} +5 -5
  48. package/dist/{delete-annotation-ZT5IGN24.js → delete-annotation-X3KKK7QY.js} +5 -5
  49. package/dist/{delete-dashboard-BTIY5S3L.js → delete-dashboard-LYMRPF27.js} +5 -5
  50. package/dist/{delete-dataset-U45HQHPR.js → delete-dataset-W6EZ44HY.js} +5 -5
  51. package/dist/{delete-dataset-records-R43IMAWB.js → delete-dataset-records-7CNOTALB.js} +5 -5
  52. package/dist/{delete-evaluator-U6JMQ6C7.js → delete-evaluator-RKYC4TNB.js} +5 -5
  53. package/dist/{delete-prompt-tag-4EV4TDHB.js → delete-prompt-tag-QB3OCZUV.js} +4 -4
  54. package/dist/{delete-workflow-5QYALXEL.js → delete-workflow-NG2VXYYM.js} +5 -5
  55. package/dist/{discover-evaluator-schema-MRGHVNTT.js → discover-evaluator-schema-ESSHQGPY.js} +58 -321
  56. package/dist/{discover-scenario-schema-UPBTCWPG.js → discover-scenario-schema-W7KBXA4J.js} +54 -2
  57. package/dist/{discover-schema-ICGXPHUW.js → discover-schema-L6QDPON3.js} +2 -2
  58. package/dist/get-agent-7THF7HCU.js +66 -0
  59. package/dist/{get-analytics-PVERWOA5.js → get-analytics-R6AZD3AW.js} +4 -4
  60. package/dist/{get-dashboard-ZZPC3WMD.js → get-dashboard-P4WWVHFU.js} +5 -5
  61. package/dist/{get-dataset-25TB2LQH.js → get-dataset-AKTYSQDI.js} +5 -5
  62. package/dist/{get-evaluator-3FTRQKEC.js → get-evaluator-RZ44UIN6.js} +5 -5
  63. package/dist/{get-project-5OAXYC57.js → get-project-QSQEAB33.js} +5 -5
  64. package/dist/get-prompt-GVKXF5NL.js +100 -0
  65. package/dist/get-run-plan-NY7J5GRE.js +61 -0
  66. package/dist/{get-scenario-DSSTR6FI.js → get-scenario-Q3LLQE4T.js} +9 -5
  67. package/dist/{get-simulation-run-RQPMS2MC.js → get-simulation-run-WINEYDMK.js} +14 -5
  68. package/dist/get-test-suite-4HKA7OKS.js +49 -0
  69. package/dist/{get-trace-O5VT3NAC.js → get-trace-RCUV7N4O.js} +4 -4
  70. package/dist/{get-workflow-3EZDOEIF.js → get-workflow-EEOC7Y45.js} +5 -5
  71. package/dist/http-security-EU3CHTY7.js +26 -0
  72. package/dist/{http-server-2ACJ3CXW.js → http-server-WCDUARTH.js} +1038 -693
  73. package/dist/index.js +6437 -4020
  74. package/dist/{langwatch-api-annotations-E65MI3WZ.js → langwatch-api-annotations-6DHVZWUK.js} +5 -5
  75. package/dist/{langwatch-api-dashboards-QCDECRCY.js → langwatch-api-dashboards-2KJL2M42.js} +5 -5
  76. package/dist/{langwatch-api-monitors-UIO2KTDV.js → langwatch-api-monitors-6ISFJ7WX.js} +4 -4
  77. package/dist/{langwatch-api-secrets-4T2EKSJ7.js → langwatch-api-secrets-OEBD6GBU.js} +4 -4
  78. package/dist/{langwatch-api-triggers-MPOEKPUY.js → langwatch-api-triggers-5E6DDGX6.js} +4 -4
  79. package/dist/langwatch-api-workflows-6UJL5U4Z.js +16 -0
  80. package/dist/list-agents-AMVDQPQL.js +43 -0
  81. package/dist/{list-annotations-CGX5BT7E.js → list-annotations-6PR5NLIC.js} +5 -5
  82. package/dist/{list-api-keys-JLWJPFRX.js → list-api-keys-GVPNBIHQ.js} +5 -5
  83. package/dist/{list-dashboards-RNIWX3TX.js → list-dashboards-Z3BZKDGF.js} +5 -5
  84. package/dist/{list-dataset-records-B5MHZR66.js → list-dataset-records-O67HZ35Y.js} +5 -5
  85. package/dist/{list-datasets-BVCAMQCP.js → list-datasets-WXZL6UMP.js} +5 -5
  86. package/dist/{list-evaluators-65FBTL2U.js → list-evaluators-PAX72WCN.js} +5 -5
  87. package/dist/{list-model-providers-GRK3I3VV.js → list-model-providers-VVLZMLCX.js} +5 -5
  88. package/dist/{list-projects-JLMBFMW3.js → list-projects-AQVI6SUK.js} +5 -5
  89. package/dist/{list-prompt-tags-UHNWDGQI.js → list-prompt-tags-WZFBJOHG.js} +4 -4
  90. package/dist/{list-prompts-7254B3U7.js → list-prompts-BQOHRHYT.js} +4 -4
  91. package/dist/list-run-plans-UBAU2DGH.js +46 -0
  92. package/dist/{list-scenarios-46YNICKM.js → list-scenarios-MS4IUIVM.js} +10 -7
  93. package/dist/{list-simulation-runs-LGNEFNPN.js → list-simulation-runs-D6YF4ZOU.js} +13 -7
  94. package/dist/list-test-suites-L7R2O6DR.js +35 -0
  95. package/dist/{list-workflows-MLQDM6CP.js → list-workflows-6MV4XJHF.js} +5 -5
  96. package/dist/{rename-prompt-tag-RHQMNEQL.js → rename-prompt-tag-K5LQ4CY6.js} +4 -4
  97. package/dist/rename-test-suite-NTYHAO5X.js +21 -0
  98. package/dist/report-issue-RHZZ33IS.js +885 -0
  99. package/dist/rerun-run-plan-MUDWJBE4.js +22 -0
  100. package/dist/{revoke-api-key-3Y35EMBT.js → revoke-api-key-6LAZ3QZZ.js} +5 -5
  101. package/dist/run-agent-CAGWGTYT.js +72 -0
  102. package/dist/run-plan-T4ZYOZGA.js +39 -0
  103. package/dist/run-test-suite-L4MZ7G4D.js +26 -0
  104. package/dist/{run-workflow-5LVBZYLS.js → run-workflow-DFIS23AP.js} +5 -5
  105. package/dist/{search-traces-U4HLOCZ2.js → search-traces-4XTJYXIK.js} +4 -4
  106. package/dist/{set-model-provider-V3M5CDIY.js → set-model-provider-XRNMFOVR.js} +5 -5
  107. package/dist/{update-agent-47RZIHS7.js → update-agent-DUWNYBNA.js} +5 -5
  108. package/dist/{update-dataset-G6QJJGS5.js → update-dataset-YSOMDCOB.js} +5 -5
  109. package/dist/{update-dataset-record-RMWLSWCS.js → update-dataset-record-DEA2ITYR.js} +5 -5
  110. package/dist/{update-evaluator-SFRRI5Q3.js → update-evaluator-NH7Q5GWV.js} +5 -5
  111. package/dist/{update-project-PI5DWBVK.js → update-project-Q26FBSYH.js} +5 -5
  112. package/dist/update-prompt-ZQQ2C3KU.js +143 -0
  113. package/dist/{update-scenario-G3IU2UIB.js → update-scenario-WUX34JAR.js} +12 -5
  114. package/package.json +8 -9
  115. package/pyproject.toml +5 -5
  116. package/src/__tests__/agent-tools.unit.test.ts +257 -0
  117. package/src/__tests__/all-tools.integration.test.ts +321 -75
  118. package/src/__tests__/config.unit.test.ts +33 -0
  119. package/src/__tests__/docker.integration.test.ts +91 -7
  120. package/src/__tests__/documentation-fetch-security.integration.test.ts +8 -2
  121. package/src/__tests__/feature-map-tool-drift.unit.test.ts +93 -0
  122. package/src/__tests__/http-security-origin.unit.test.ts +97 -0
  123. package/src/__tests__/http-security-sessions.unit.test.ts +209 -0
  124. package/src/__tests__/http-security-verifier.unit.test.ts +248 -0
  125. package/src/__tests__/http-server-allocation.integration.test.ts +279 -0
  126. package/src/__tests__/http-server-origin.integration.test.ts +169 -0
  127. package/src/__tests__/http-server-session-auth.integration.test.ts +341 -0
  128. package/src/__tests__/http-transport.integration.test.ts +85 -87
  129. package/src/__tests__/integration.integration.test.ts +7 -11
  130. package/src/__tests__/prompt-response-types.unit.test.ts +51 -0
  131. package/src/__tests__/prompts.integration.test.ts +736 -0
  132. package/src/__tests__/run-plan-tools.unit.test.ts +565 -0
  133. package/src/__tests__/scenario-tools.unit.test.ts +19 -0
  134. package/src/__tests__/simulation-run-tools.unit.test.ts +116 -0
  135. package/src/__tests__/support/http-server-harness.ts +138 -0
  136. package/src/__tests__/test-suite-tools.unit.test.ts +615 -0
  137. package/src/__tests__/{tools.unit.test.ts → tools.integration.test.ts} +31 -59
  138. package/src/config.ts +17 -2
  139. package/src/create-mcp-server.ts +334 -55
  140. package/src/http-security.ts +492 -0
  141. package/src/http-server.ts +536 -152
  142. package/src/index.ts +49 -2
  143. package/src/langwatch-api-agents.ts +196 -12
  144. package/src/langwatch-api-run-plans.ts +157 -0
  145. package/src/langwatch-api-scenarios.ts +9 -0
  146. package/src/langwatch-api-simulation-runs.ts +5 -0
  147. package/src/langwatch-api-test-suites.ts +135 -0
  148. package/src/langwatch-api.ts +62 -12
  149. package/src/schemas/index.ts +5 -0
  150. package/src/schemas/run-plan.ts +92 -0
  151. package/src/schemas/suite-fields.ts +149 -0
  152. package/src/tools/archive-run-plan.ts +12 -0
  153. package/src/tools/archive-test-suite.ts +12 -0
  154. package/src/tools/create-scenario.ts +8 -0
  155. package/src/tools/create-test-suite.ts +43 -0
  156. package/src/tools/discover-evaluator-schema.ts +2 -2
  157. package/src/tools/discover-scenario-schema.ts +54 -0
  158. package/src/tools/format-run-plan.ts +68 -0
  159. package/src/tools/format-scenario.ts +14 -0
  160. package/src/tools/format-suite-details.ts +96 -0
  161. package/src/tools/get-agent.ts +45 -3
  162. package/src/tools/get-prompt.ts +108 -24
  163. package/src/tools/get-run-plan.ts +62 -0
  164. package/src/tools/get-scenario.ts +3 -0
  165. package/src/tools/get-simulation-run.ts +9 -0
  166. package/src/tools/get-test-suite.ts +49 -0
  167. package/src/tools/list-agents.ts +20 -3
  168. package/src/tools/list-run-plans.ts +47 -0
  169. package/src/tools/list-scenarios.ts +12 -2
  170. package/src/tools/list-simulation-runs.ts +6 -2
  171. package/src/tools/list-test-suites.ts +35 -0
  172. package/src/tools/rename-test-suite.ts +18 -0
  173. package/src/tools/rerun-run-plan.ts +21 -0
  174. package/src/tools/run-agent.ts +54 -6
  175. package/src/tools/run-plan.ts +49 -0
  176. package/src/tools/run-test-suite.ts +30 -0
  177. package/src/tools/test-agent.ts +18 -0
  178. package/src/tools/update-prompt.ts +195 -10
  179. package/src/tools/update-scenario.ts +8 -0
  180. package/src/tools/update-test-suite.ts +45 -0
  181. package/tsconfig.json +7 -2
  182. package/uv.lock +418 -298
  183. package/dist/archive-suite-C3KX6IIZ.js +0 -16
  184. package/dist/chunk-WDH5P5RF.js +0 -48
  185. package/dist/create-suite-TWF6BBR4.js +0 -39
  186. package/dist/get-agent-7QJVPH72.js +0 -29
  187. package/dist/get-prompt-I753LZKZ.js +0 -46
  188. package/dist/get-suite-GTOCY5PL.js +0 -45
  189. package/dist/langwatch-api-workflows-5HRZ7HR4.js +0 -16
  190. package/dist/list-agents-TIJTP566.js +0 -33
  191. package/dist/list-suites-5VK4HXEK.js +0 -43
  192. package/dist/report-issue-DXJ6TPTC.js +0 -412
  193. package/dist/run-agent-RBSIFKRT.js +0 -37
  194. package/dist/run-suite-K3DNQ3SE.js +0 -30
  195. package/dist/update-prompt-RQFTEO7Y.js +0 -25
  196. package/dist/update-suite-TRGGLNR7.js +0 -40
  197. package/pnpm-workspace.yaml +0 -34
  198. package/src/langwatch-api-suites.ts +0 -118
  199. package/src/tools/archive-suite.ts +0 -12
  200. package/src/tools/create-suite.ts +0 -42
  201. package/src/tools/get-suite.ts +0 -47
  202. package/src/tools/list-suites.ts +0 -43
  203. package/src/tools/run-suite.ts +0 -28
  204. package/src/tools/update-suite.ts +0 -44
@@ -0,0 +1,96 @@
1
+ import type {
2
+ EvaluatorAttachmentWire,
3
+ ScenarioMapping,
4
+ SuiteField,
5
+ } from "../schemas/suite-fields.js";
6
+
7
+ /**
8
+ * The digest lines for a suite's fields, the evaluators attached to a suite
9
+ * or a plan, and the evaluator results on a finished run. Shared by every
10
+ * tool that prints one of them.
11
+ */
12
+
13
+ /** A mapping as one short path: `scenario.fields.golden_sql` or `"literal"`. */
14
+ export function describeMapping(mapping: ScenarioMapping): string {
15
+ return mapping.type === "value"
16
+ ? JSON.stringify(mapping.value)
17
+ : `${mapping.sourceId}.${mapping.path.join(".")}`;
18
+ }
19
+
20
+ export function formatSuiteFields(fields: SuiteField[] | undefined): string[] {
21
+ if (!fields || fields.length === 0) return [];
22
+ return [
23
+ "\n## Fields",
24
+ ...fields.map((field) => `- ${field.identifier} (${field.type})`),
25
+ ];
26
+ }
27
+
28
+ export function formatEvaluatorAttachments(
29
+ evaluators: EvaluatorAttachmentWire[] | undefined,
30
+ ): string[] {
31
+ if (!evaluators || evaluators.length === 0) return [];
32
+ const lines = ["\n## Evaluators"];
33
+ for (const attachment of evaluators) {
34
+ const gate = attachment.required ? "required" : "reports only";
35
+ lines.push(`- ${attachment.evaluatorId} (${gate}, attachment ${attachment.id})`);
36
+ for (const [input, mapping] of Object.entries(attachment.mappings)) {
37
+ lines.push(` - ${input}: ${describeMapping(mapping)}`);
38
+ }
39
+ }
40
+ return lines;
41
+ }
42
+
43
+ /**
44
+ * The status the platform reports while a run's evaluators have not been
45
+ * recorded yet. The conversation is over and the judge has decided, but a
46
+ * required evaluator can still turn the run red, so a caller must not read
47
+ * the verdict as final.
48
+ */
49
+ export const PENDING_EVALUATION_STATUS = "PENDING_EVALUATION";
50
+
51
+ /** How a run's status reads in a digest. */
52
+ export function formatRunStatus(status: string): string {
53
+ if (status === PENDING_EVALUATION_STATUS) return "evaluating";
54
+ if (status === "SUCCESS") return "pass";
55
+ if (status === "FAILED") return "FAIL";
56
+ return status;
57
+ }
58
+
59
+ /**
60
+ * The line that warns a caller off a run's verdict while its evaluators are
61
+ * still to be recorded, or nothing when they already are.
62
+ */
63
+ export function pendingEvaluationNote(status: string): string[] {
64
+ if (status !== PENDING_EVALUATION_STATUS) return [];
65
+ return [
66
+ "> The evaluators attached to this run have not been recorded yet. A required one may still fail it, so read the verdict again once the status changes.",
67
+ ];
68
+ }
69
+
70
+ /** One evaluator's result on a finished run. */
71
+ export interface SimulationRunEvaluation {
72
+ evaluatorId: string;
73
+ name: string;
74
+ status: "passed" | "failed" | "scored" | "skipped" | "error";
75
+ required: boolean;
76
+ passed?: boolean;
77
+ score?: number;
78
+ label?: string;
79
+ details?: string;
80
+ }
81
+
82
+ export function formatEvaluations(
83
+ evaluations: SimulationRunEvaluation[] | undefined,
84
+ ): string[] {
85
+ if (!evaluations || evaluations.length === 0) return [];
86
+ const lines = ["\n## Evaluators"];
87
+ for (const evaluation of evaluations) {
88
+ const parts: string[] = [evaluation.status];
89
+ if (evaluation.score !== undefined) parts.push(`score ${evaluation.score}`);
90
+ if (evaluation.label !== undefined) parts.push(evaluation.label);
91
+ if (evaluation.required) parts.push("required");
92
+ lines.push(`- **${evaluation.name}**: ${parts.join(", ")}`);
93
+ if (evaluation.details) lines.push(` ${evaluation.details}`);
94
+ }
95
+ return lines;
96
+ }
@@ -1,15 +1,57 @@
1
- import { getAgent as apiGetAgent } from "../langwatch-api-agents.js";
1
+ import { getAgent as apiGetAgent, type AgentParameterSpec } from "../langwatch-api-agents.js";
2
2
 
3
- export async function handleGetAgent(params: { id: string }): Promise<string> {
4
- const agent = await apiGetAgent(params.id);
3
+ export const describeParameter = ({
4
+ name,
5
+ type,
6
+ options,
7
+ default: defaultValue,
8
+ description,
9
+ required,
10
+ }: AgentParameterSpec): string => {
11
+ const parts: string[] = [type];
12
+ if (options?.length) parts.push(`one of ${options.join(", ")}`);
13
+ if (defaultValue !== undefined) parts.push(`default ${JSON.stringify(defaultValue)}`);
14
+ if (required) parts.push("required");
15
+ const suffix = description ? `: ${description}` : "";
16
+ return `- **${name}** (${parts.join(", ")})${suffix}`;
17
+ };
18
+
19
+ /**
20
+ * Handles the platform_get_agent MCP tool invocation.
21
+ *
22
+ * @see specs/mcp-server/agent-tools.feature
23
+ */
24
+ export async function handleGetAgent({ id }: { id: string }): Promise<string> {
25
+ const agent = await apiGetAgent(id);
5
26
 
6
27
  const lines: string[] = [];
7
28
  lines.push(`# ${agent.name}\n`);
8
29
  lines.push(`**ID**: ${agent.id}`);
9
30
  lines.push(`**Type**: ${agent.type}`);
31
+ if (agent.environment) lines.push(`**Environment**: ${agent.environment}`);
32
+ if (agent.status) lines.push(`**Status**: ${agent.status}`);
33
+ if (agent.owner?.name) lines.push(`**Owner**: ${agent.owner.name}`);
34
+ else if (agent.hostLabel) lines.push(`**Host**: ${agent.hostLabel}`);
35
+ if (agent.selectable === false) {
36
+ lines.push("**Run**: only its owner can run this agent");
37
+ }
38
+ if (agent.lastSeenAt) lines.push(`**Last seen**: ${agent.lastSeenAt}`);
10
39
  lines.push(`**Created**: ${agent.createdAt}`);
11
40
  lines.push(`**Updated**: ${agent.updatedAt}`);
12
41
 
42
+ if (agent.parameters && agent.parameters.length > 0) {
43
+ lines.push("\n## Parameters\n");
44
+ for (const parameter of agent.parameters) lines.push(describeParameter(parameter));
45
+ }
46
+
47
+ if (agent.instances && agent.instances.length > 0) {
48
+ lines.push(`\n## Instances (${agent.instances.length})\n`);
49
+ for (const instance of agent.instances) {
50
+ const label = instance.label ? ` (${instance.label})` : "";
51
+ lines.push(`- ${instance.hostname || instance.id}${label}, connected ${instance.connectedAt}`);
52
+ }
53
+ }
54
+
13
55
  if (agent.config && Object.keys(agent.config).length > 0) {
14
56
  lines.push("\n## Config\n");
15
57
  lines.push("```json");
@@ -1,53 +1,137 @@
1
1
  import { getPrompt as apiGetPrompt } from "../langwatch-api.js";
2
2
 
3
+ interface DeploymentTag {
4
+ name: string;
5
+ versionId?: string;
6
+ }
7
+
8
+ /**
9
+ * Normalizes one tag entry from the API. The API returns tag objects
10
+ * ({ name, versionId }); plain strings are tolerated so a shape drift can
11
+ * never crash the tool.
12
+ */
13
+ function normalizeTag(tag: unknown): DeploymentTag | undefined {
14
+ if (typeof tag === "string") return { name: tag };
15
+ if (tag && typeof tag === "object" && "name" in tag) {
16
+ const candidate = tag as { name: unknown; versionId?: unknown };
17
+ if (typeof candidate.name === "string") {
18
+ return {
19
+ name: candidate.name,
20
+ versionId:
21
+ typeof candidate.versionId === "string"
22
+ ? candidate.versionId
23
+ : undefined,
24
+ };
25
+ }
26
+ }
27
+ return undefined;
28
+ }
29
+
3
30
  /**
4
31
  * Handles the platform_get_prompt MCP tool invocation.
5
32
  *
6
- * Retrieves a specific prompt by ID or handle and formats it as
7
- * AI-readable markdown, including messages, model config, and version history.
33
+ * Retrieves a specific prompt by ID or handle. The API returns the requested
34
+ * version's data flattened to the top level (version, versionId, model,
35
+ * messages, inputs/outputs, parameters, tags). Defaults to an AI-readable
36
+ * markdown digest; `format: "json"` instead returns the raw API payload
37
+ * verbatim.
8
38
  */
9
39
  export async function handleGetPrompt(params: {
10
40
  idOrHandle: string;
11
41
  version?: number;
12
42
  tag?: string;
43
+ format?: "digest" | "json";
13
44
  }): Promise<string> {
14
- const prompt = await apiGetPrompt(params.idOrHandle, { version: params.version, tag: params.tag });
45
+ const prompt = await apiGetPrompt(params.idOrHandle, {
46
+ version: params.version,
47
+ tag: params.tag,
48
+ });
49
+
50
+ if (params.format === "json") {
51
+ return JSON.stringify(prompt, null, 2);
52
+ }
15
53
 
16
54
  const lines: string[] = [];
17
- lines.push(
18
- `# Prompt: ${prompt.name || prompt.handle || prompt.id}\n`
19
- );
55
+ lines.push(`# Prompt: ${prompt.name || prompt.handle || prompt.id}\n`);
20
56
 
21
57
  if (prompt.handle) lines.push(`**Handle**: ${prompt.handle}`);
22
58
  if (prompt.id) lines.push(`**ID**: ${prompt.id}`);
23
- if (prompt.latestVersionNumber != null)
24
- lines.push(`**Latest Version**: v${prompt.latestVersionNumber}`);
59
+ if (prompt.version != null) lines.push(`**Version**: v${prompt.version}`);
60
+ if (prompt.versionId) lines.push(`**Version ID**: ${prompt.versionId}`);
61
+ if (prompt.commitMessage) lines.push(`**Commit**: ${prompt.commitMessage}`);
62
+ if (prompt.model) lines.push(`**Model**: ${prompt.model}`);
63
+ if (prompt.temperature != null)
64
+ lines.push(`**Temperature**: ${prompt.temperature}`);
65
+ if (prompt.maxTokens != null)
66
+ lines.push(`**Max Tokens**: ${prompt.maxTokens}`);
67
+ if (prompt.responseFormat)
68
+ lines.push(`**Response Format**: ${JSON.stringify(prompt.responseFormat)}`);
69
+
70
+ // Renders a field list of any shape the API may return: an array of
71
+ // { identifier, type } entries (inputs/outputs), an object map of
72
+ // name -> value (parameters), or absent/empty (no heading at all).
73
+ const renderFieldList = ({
74
+ heading,
75
+ fields,
76
+ }: {
77
+ heading: string;
78
+ fields: unknown;
79
+ }) => {
80
+ const entries: string[] = [];
81
+ if (Array.isArray(fields)) {
82
+ for (const field of fields) {
83
+ if (field && typeof field === "object" && "identifier" in field) {
84
+ const typed = field as { identifier: unknown; type?: unknown };
85
+ entries.push(
86
+ `- **${String(typed.identifier)}**: ${String(typed.type ?? "unknown")}`
87
+ );
88
+ } else {
89
+ entries.push(`- ${JSON.stringify(field)}`);
90
+ }
91
+ }
92
+ } else if (fields && typeof fields === "object") {
93
+ for (const [key, value] of Object.entries(fields)) {
94
+ entries.push(`- **${key}**: ${JSON.stringify(value)}`);
95
+ }
96
+ }
97
+ if (entries.length === 0) return;
98
+ lines.push(`\n**${heading}**`);
99
+ lines.push(...entries);
100
+ };
25
101
 
26
- // Show model config
27
- const version = prompt.versions?.[0] ?? prompt;
28
- if (version.model) lines.push(`**Model**: ${version.model}`);
102
+ renderFieldList({ heading: "Parameters", fields: prompt.parameters });
103
+ renderFieldList({ heading: "Inputs", fields: prompt.inputs });
104
+ renderFieldList({ heading: "Outputs", fields: prompt.outputs });
29
105
 
30
- // Show messages
31
- const messages = version.messages || prompt.prompt || [];
106
+ const messages = prompt.messages;
32
107
  if (Array.isArray(messages) && messages.length > 0) {
33
108
  lines.push("\n## Messages");
34
109
  for (const msg of messages) {
35
110
  lines.push(`\n### ${msg.role}`);
36
111
  lines.push(msg.content);
37
112
  }
113
+ } else if (typeof prompt.prompt === "string" && prompt.prompt.length > 0) {
114
+ lines.push("\n## Messages");
115
+ lines.push("\n### system");
116
+ lines.push(prompt.prompt);
38
117
  }
39
118
 
40
- // Show version history
41
- if (prompt.versions && prompt.versions.length > 0) {
42
- lines.push("\n## Version History");
43
- for (const v of prompt.versions.slice(0, 10)) {
44
- const versionNum = v.version ?? "?";
45
- const commitMsg = v.commitMessage || "No message";
46
- lines.push(`- **v${versionNum}**: ${commitMsg}`);
47
- }
48
- if (prompt.versions.length > 10) {
49
- lines.push(`... and ${prompt.versions.length - 10} more versions`);
50
- }
119
+ // Deployments: every non-"latest" tag on the prompt, each shown with the
120
+ // version it currently points to. Always rendered, even when empty, so an
121
+ // empty section is never mistaken for "not checked" — and a tag pointing
122
+ // at another version is listed truthfully against that version instead of
123
+ // being implied undeployed.
124
+ lines.push("\n## Deployments");
125
+ const tags = Array.isArray(prompt.tags) ? prompt.tags : [];
126
+ for (const rawTag of tags) {
127
+ const tag = normalizeTag(rawTag);
128
+ if (!tag || tag.name === "latest") continue;
129
+ const target = tag.versionId ?? "unknown version";
130
+ const marker =
131
+ tag.versionId && tag.versionId === prompt.versionId
132
+ ? " (this version)"
133
+ : "";
134
+ lines.push(`- ${tag.name} → ${target}${marker}`);
51
135
  }
52
136
 
53
137
  return lines.join("\n");
@@ -0,0 +1,62 @@
1
+ import { getRunPlan as apiGetRunPlan } from "../langwatch-api-run-plans.js";
2
+ import { describeRunPlanScope } from "./format-run-plan.js";
3
+ import { formatEvaluatorAttachments } from "./format-suite-details.js";
4
+
5
+ /**
6
+ * Handles the platform_get_run_plan MCP tool invocation.
7
+ */
8
+ export async function handleGetRunPlan(params: {
9
+ id: string;
10
+ format?: "digest" | "json";
11
+ }): Promise<string> {
12
+ const plan = await apiGetRunPlan(params.id);
13
+
14
+ if (params.format === "json") {
15
+ return JSON.stringify(plan, null, 2);
16
+ }
17
+
18
+ const lines: string[] = [];
19
+ lines.push(`# Run Plan: ${plan.name}\n`);
20
+ lines.push(`**ID**: ${plan.id}`);
21
+ lines.push(`**Slug**: ${plan.slug}`);
22
+ lines.push(`**Covers**: ${describeRunPlanScope(plan.scope, plan.scenarioIds)}`);
23
+ lines.push(`**Repeat**: ${plan.repeatCount}x`);
24
+ lines.push(`**Simulator model**: ${plan.simulatorModel ?? "project default"}`);
25
+ lines.push(`**Judge model**: ${plan.judgeModel ?? "project default"}`);
26
+ if (plan.labels.length > 0) {
27
+ lines.push(`**Labels**: ${plan.labels.join(", ")}`);
28
+ }
29
+ if (plan.archivedAt) {
30
+ lines.push(`**Archived**: ${plan.archivedAt}`);
31
+ }
32
+ lines.push(`**Created**: ${plan.createdAt}`);
33
+ lines.push(`**Updated**: ${plan.updatedAt}`);
34
+
35
+ lines.push("\n## Targets");
36
+ for (const target of plan.targets) {
37
+ // The parameters are part of the target's identity, so a plan that
38
+ // compares one agent on two models reads as two lines that differ.
39
+ const overrides = Object.entries(target.runParameters ?? {})
40
+ .map(([name, value]) => `${name}=${String(value)}`)
41
+ .join(", ");
42
+ lines.push(
43
+ `- ${target.type}:${target.referenceId}${overrides ? ` (${overrides})` : ""}`,
44
+ );
45
+ }
46
+
47
+ if (plan.scenarioIds.length > 0) {
48
+ lines.push("\n## Scenarios");
49
+ for (const id of plan.scenarioIds) {
50
+ lines.push(`- ${id}`);
51
+ }
52
+ }
53
+
54
+ lines.push(...formatEvaluatorAttachments(plan.evaluators));
55
+
56
+ lines.push(`\n**View**: ${plan.platformUrl}`);
57
+ lines.push(
58
+ "\n> Use `platform_rerun_run_plan` to run this configuration again, or `platform_run_plan` with this name to replace it.",
59
+ );
60
+
61
+ return lines.join("\n");
62
+ }
@@ -1,4 +1,5 @@
1
1
  import { getScenario as apiGetScenario } from "../langwatch-api-scenarios.js";
2
+ import { formatScenarioFields } from "./format-scenario.js";
2
3
 
3
4
  /**
4
5
  * Handles the get_scenario MCP tool invocation.
@@ -32,5 +33,7 @@ export async function handleGetScenario(params: {
32
33
  lines.push(`\n**Labels**: ${scenario.labels.join(", ")}`);
33
34
  }
34
35
 
36
+ lines.push(...formatScenarioFields(scenario.fields));
37
+
35
38
  return lines.join("\n");
36
39
  }
@@ -1,4 +1,8 @@
1
1
  import { getSimulationRun as apiGetSimulationRun } from "../langwatch-api-simulation-runs.js";
2
+ import {
3
+ formatEvaluations,
4
+ pendingEvaluationNote,
5
+ } from "./format-suite-details.js";
2
6
 
3
7
  /**
4
8
  * Handles the platform_get_simulation_run MCP tool invocation.
@@ -19,6 +23,7 @@ export async function handleGetSimulationRun(params: {
19
23
  lines.push(`**Scenario ID**: ${run.scenarioId}`);
20
24
  lines.push(`**Batch ID**: ${run.batchRunId}`);
21
25
  lines.push(`**Status**: ${run.status}`);
26
+ lines.push(...pendingEvaluationNote(run.status));
22
27
 
23
28
  const duration = run.durationInMs > 0 ? `${(run.durationInMs / 1000).toFixed(1)}s` : "—";
24
29
  lines.push(`**Duration**: ${duration}`);
@@ -26,6 +31,9 @@ export async function handleGetSimulationRun(params: {
26
31
  lines.push(`**Cost**: $${run.totalCost.toFixed(4)}`);
27
32
  }
28
33
  lines.push(`**Started**: ${new Date(run.timestamp).toISOString()}`);
34
+ if (run.note) {
35
+ lines.push(`**Note**: ${run.note}`);
36
+ }
29
37
 
30
38
  if (run.results) {
31
39
  lines.push("\n## Results");
@@ -44,6 +52,7 @@ export async function handleGetSimulationRun(params: {
44
52
  if (run.results.error) {
45
53
  lines.push(`**Error**: ${run.results.error}`);
46
54
  }
55
+ lines.push(...formatEvaluations(run.results.evaluations));
47
56
  }
48
57
 
49
58
  if (run.messages && run.messages.length > 0) {
@@ -0,0 +1,49 @@
1
+ import { getTestSuite as apiGetTestSuite } from "../langwatch-api-test-suites.js";
2
+ import {
3
+ formatEvaluatorAttachments,
4
+ formatSuiteFields,
5
+ } from "./format-suite-details.js";
6
+
7
+ /**
8
+ * Handles the platform_get_test_suite MCP tool invocation.
9
+ */
10
+ export async function handleGetTestSuite(params: {
11
+ id: string;
12
+ format?: "digest" | "json";
13
+ }): Promise<string> {
14
+ const suite = await apiGetTestSuite(params.id);
15
+
16
+ if (params.format === "json") {
17
+ return JSON.stringify(suite, null, 2);
18
+ }
19
+
20
+ const lines: string[] = [];
21
+ lines.push(`# Test Suite: ${suite.name}\n`);
22
+ lines.push(`**ID**: ${suite.id}`);
23
+ lines.push(`**Slug**: ${suite.slug}`);
24
+ lines.push(`**Scenarios**: ${suite.scenarioCount}`);
25
+ if (suite.archivedAt) {
26
+ lines.push(`**Archived**: ${suite.archivedAt}`);
27
+ }
28
+ lines.push(`**Created**: ${suite.createdAt}`);
29
+ lines.push(`**Updated**: ${suite.updatedAt}`);
30
+
31
+ lines.push(...formatSuiteFields(suite.fields));
32
+ lines.push(...formatEvaluatorAttachments(suite.evaluators));
33
+
34
+ lines.push("\n## Scenarios");
35
+ if (suite.scenarios.length === 0) {
36
+ lines.push("None filed yet.");
37
+ } else {
38
+ for (const scenario of suite.scenarios) {
39
+ lines.push(`- ${scenario.name} (${scenario.id})`);
40
+ }
41
+ }
42
+
43
+ lines.push(`\n**View**: ${suite.platformUrl}`);
44
+ lines.push(
45
+ "\n> Use `platform_run_test_suite` to run every scenario of this suite against a target.",
46
+ );
47
+
48
+ return lines.join("\n");
49
+ }
@@ -1,11 +1,20 @@
1
- import { listAgents as apiListAgents } from "../langwatch-api-agents.js";
1
+ import { listAgents as apiListAgents, type AgentSummary } from "../langwatch-api-agents.js";
2
2
 
3
+ /** Who a personal or host-scoped agent belongs to, empty for a shared one. */
4
+ export const agentOwnerLabel = (agent: AgentSummary): string =>
5
+ agent.owner?.name ?? agent.hostLabel ?? "";
6
+
7
+ /**
8
+ * Handles the platform_list_agents MCP tool invocation.
9
+ *
10
+ * @see specs/mcp-server/agent-tools.feature
11
+ */
3
12
  export async function handleListAgents(): Promise<string> {
4
13
  const result = await apiListAgents({ limit: 100 });
5
14
  const agents = result.data;
6
15
 
7
16
  if (!Array.isArray(agents) || agents.length === 0) {
8
- return "No agents found in this project.\n\n> Tip: Use `platform_create_agent` to create your first agent.";
17
+ return "No agents found in this project.\n\n> Tip: connect one from code with `connectAgent` (TypeScript, `langwatch/agent`) or `connect_agent` (Python), or use `platform_create_agent` for an HTTP agent.";
9
18
  }
10
19
 
11
20
  const lines: string[] = [];
@@ -15,12 +24,20 @@ export async function handleListAgents(): Promise<string> {
15
24
  lines.push(`## ${a.name}`);
16
25
  lines.push(`**ID**: ${a.id}`);
17
26
  lines.push(`**Type**: ${a.type}`);
27
+ if (a.environment) lines.push(`**Environment**: ${a.environment}`);
28
+ if (a.status) lines.push(`**Status**: ${a.status}`);
29
+ if (a.instances) lines.push(`**Instances**: ${a.instances.length}`);
30
+ const owner = agentOwnerLabel(a);
31
+ if (owner) lines.push(`**Owner**: ${owner}`);
32
+ if (a.selectable === false) {
33
+ lines.push("**Run**: only its owner can run this agent");
34
+ }
18
35
  lines.push(`**Updated**: ${a.updatedAt}`);
19
36
  lines.push("");
20
37
  }
21
38
 
22
39
  lines.push(
23
- "> Use `platform_get_agent` with the ID to see full agent details including config.",
40
+ "> Use `platform_get_agent` with the ID to see full agent details including parameters, instances and config.",
24
41
  );
25
42
 
26
43
  return lines.join("\n");
@@ -0,0 +1,47 @@
1
+ import { listRunPlans as apiListRunPlans } from "../langwatch-api-run-plans.js";
2
+ import { describeRunPlanScope } from "./format-run-plan.js";
3
+
4
+ /**
5
+ * Handles the platform_list_run_plans MCP tool invocation.
6
+ */
7
+ export async function handleListRunPlans(params: {
8
+ includeArchived?: boolean;
9
+ format?: "digest" | "json";
10
+ }): Promise<string> {
11
+ const plans = await apiListRunPlans({
12
+ includeArchived: params.includeArchived,
13
+ });
14
+
15
+ if (params.format === "json") {
16
+ return JSON.stringify(plans, null, 2);
17
+ }
18
+
19
+ if (!Array.isArray(plans) || plans.length === 0) {
20
+ return "No run plans found in this project.\n\n> Tip: Use `platform_run_plan` to start one. A run plan is created the first time you run its name.";
21
+ }
22
+
23
+ const lines: string[] = [];
24
+ lines.push(`# Run Plans (${plans.length} total)\n`);
25
+
26
+ for (const plan of plans) {
27
+ lines.push(`## ${plan.name}${plan.archivedAt ? " (archived)" : ""}`);
28
+ lines.push(`**ID**: ${plan.id}`);
29
+ lines.push(`**Covers**: ${describeRunPlanScope(plan.scope, plan.scenarioIds)}`);
30
+ lines.push(
31
+ `**Targets**: ${plan.targets.length} (${plan.targets
32
+ .map((t) => `${t.type}:${t.referenceId}`)
33
+ .join(", ")})`,
34
+ );
35
+ lines.push(`**Repeat**: ${plan.repeatCount}x`);
36
+ if (plan.labels.length > 0) {
37
+ lines.push(`**Labels**: ${plan.labels.join(", ")}`);
38
+ }
39
+ lines.push("");
40
+ }
41
+
42
+ lines.push(
43
+ "> Use `platform_get_run_plan` with the ID for the full configuration, or `platform_rerun_run_plan` to run it again.",
44
+ );
45
+
46
+ return lines.join("\n");
47
+ }
@@ -7,16 +7,26 @@ import { listScenarios as apiListScenarios } from "../langwatch-api-scenarios.js
7
7
  * AI-readable digest or raw JSON.
8
8
  */
9
9
  export async function handleListScenarios(params: {
10
+ testSuiteId?: string;
10
11
  format?: "digest" | "json";
11
12
  }): Promise<string> {
12
- const scenarios = await apiListScenarios();
13
+ const all = await apiListScenarios();
14
+
15
+ // The REST list takes no test suite query, so the filter runs here on the
16
+ // testSuiteId every scenario already carries in the response.
17
+ const scenarios =
18
+ params.testSuiteId !== undefined && Array.isArray(all)
19
+ ? all.filter((s) => s.testSuiteId === params.testSuiteId)
20
+ : all;
13
21
 
14
22
  if (params.format === "json") {
15
23
  return JSON.stringify(scenarios, null, 2);
16
24
  }
17
25
 
18
26
  if (!Array.isArray(scenarios) || scenarios.length === 0) {
19
- return "No scenarios found in this project.\n\n> Tip: Use `platform_create_scenario` to create your first scenario.";
27
+ return params.testSuiteId !== undefined
28
+ ? `No scenarios found in test suite ${params.testSuiteId}.\n\n> Tip: Use \`platform_create_scenario\` with testSuiteId \`${params.testSuiteId}\` to file one there.`
29
+ : "No scenarios found in this project.\n\n> Tip: Use `platform_create_scenario` to create your first scenario.";
20
30
  }
21
31
 
22
32
  const lines: string[] = [];
@@ -1,4 +1,5 @@
1
1
  import { listSimulationRuns as apiListSimulationRuns } from "../langwatch-api-simulation-runs.js";
2
+ import { formatRunStatus } from "./format-suite-details.js";
2
3
 
3
4
  /**
4
5
  * Handles the platform_list_simulation_runs MCP tool invocation.
@@ -22,14 +23,14 @@ export async function handleListSimulationRuns(params: {
22
23
  const { runs } = result;
23
24
 
24
25
  if (runs.length === 0) {
25
- return "No simulation runs found.\n\n> Run a suite first with `platform_run_suite` to create simulation runs.";
26
+ return "No simulation runs found.\n\n> Run scenarios first with `platform_run_plan` or `platform_run_test_suite` to create simulation runs.";
26
27
  }
27
28
 
28
29
  const lines: string[] = [];
29
30
  lines.push(`# Simulation Runs (${runs.length} results${result.hasMore ? ", more available" : ""})\n`);
30
31
 
31
32
  for (const run of runs) {
32
- const statusIcon = run.status === "SUCCESS" ? "pass" : run.status === "FAILED" ? "FAIL" : run.status;
33
+ const statusIcon = formatRunStatus(run.status);
33
34
  const duration = run.durationInMs > 0 ? `${(run.durationInMs / 1000).toFixed(1)}s` : "—";
34
35
  const verdict = run.results?.verdict ?? "";
35
36
 
@@ -40,6 +41,9 @@ export async function handleListSimulationRuns(params: {
40
41
  if (run.totalCost) {
41
42
  lines.push(`**Cost**: $${run.totalCost.toFixed(4)}`);
42
43
  }
44
+ if (run.note) {
45
+ lines.push(`**Note**: ${run.note}`);
46
+ }
43
47
  lines.push("");
44
48
  }
45
49
 
@@ -0,0 +1,35 @@
1
+ import { listTestSuites as apiListTestSuites } from "../langwatch-api-test-suites.js";
2
+
3
+ /**
4
+ * Handles the platform_list_test_suites MCP tool invocation.
5
+ */
6
+ export async function handleListTestSuites(params: {
7
+ format?: "digest" | "json";
8
+ }): Promise<string> {
9
+ const suites = await apiListTestSuites();
10
+
11
+ if (params.format === "json") {
12
+ return JSON.stringify(suites, null, 2);
13
+ }
14
+
15
+ if (!Array.isArray(suites) || suites.length === 0) {
16
+ return "No test suites found in this project.\n\n> Tip: Use `platform_create_test_suite` to create one, then file scenarios in it with `testSuiteId`.";
17
+ }
18
+
19
+ const lines: string[] = [];
20
+ lines.push(`# Test Suites (${suites.length} total)\n`);
21
+
22
+ for (const suite of suites) {
23
+ lines.push(`## ${suite.name}${suite.archivedAt ? " (archived)" : ""}`);
24
+ lines.push(`**ID**: ${suite.id}`);
25
+ lines.push(`**Slug**: ${suite.slug}`);
26
+ lines.push(`**Scenarios**: ${suite.scenarioCount}`);
27
+ lines.push("");
28
+ }
29
+
30
+ lines.push(
31
+ "> Use `platform_get_test_suite` with the ID to read the scenarios, or `platform_run_test_suite` to run them against a target.",
32
+ );
33
+
34
+ return lines.join("\n");
35
+ }