@langwatch/mcp-server 0.6.1 → 0.8.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 (241) hide show
  1. package/.release-please-shim +1 -0
  2. package/CHANGELOG.md +89 -0
  3. package/README.md +51 -8
  4. package/dist/{archive-scenario-YFD5THOR.js → archive-scenario-YKGLJ4MZ.js} +4 -4
  5. package/dist/archive-suite-QFNDO3Y3.js +15 -0
  6. package/dist/assign-prompt-tag-Y2B4CBBT.js +19 -0
  7. package/dist/chunk-B57MZ4WI.js +1573 -0
  8. package/dist/{chunk-IX6QJKAD.js → chunk-CPUPGDNA.js} +2 -2
  9. package/dist/chunk-FBRKHHJV.js +48 -0
  10. package/dist/{chunk-5UOPNRXW.js → chunk-HDJDFKZR.js} +10 -3
  11. package/dist/chunk-KOO5733S.js +72 -0
  12. package/dist/chunk-KZNBCKWG.js +39 -0
  13. package/dist/chunk-L6QENXYA.js +42 -0
  14. package/dist/{chunk-6U4TCGFC.js → chunk-NACSR6B7.js} +2 -2
  15. package/dist/chunk-NUFPTHKU.js +9 -0
  16. package/dist/chunk-OVQF3D5R.js +86 -0
  17. package/dist/chunk-PO7FM7IX.js +35 -0
  18. package/dist/chunk-PZUWCWOS.js +25 -0
  19. package/dist/chunk-WVI5CJ6S.js +21 -0
  20. package/dist/{chunk-LLRQIF52.js → chunk-YH2IBTEL.js} +54 -6
  21. package/dist/chunk-YI3OG4PI.js +71 -0
  22. package/dist/config.d.ts +17 -0
  23. package/dist/config.js +13 -0
  24. package/dist/create-agent-YRSDH7P7.js +23 -0
  25. package/dist/create-annotation-FYQNKMVJ.js +24 -0
  26. package/dist/create-dashboard-CORBR4YQ.js +18 -0
  27. package/dist/create-dataset-ZC63T6BB.js +22 -0
  28. package/dist/create-dataset-records-ILVNVKVR.js +22 -0
  29. package/dist/{create-evaluator-E5X5ZP3B.js → create-evaluator-2AU3PXFB.js} +4 -4
  30. package/dist/create-mcp-server.d.ts +8 -0
  31. package/dist/create-mcp-server.js +8 -0
  32. package/dist/{create-prompt-7Z35MIL6.js → create-prompt-NKRJ47YY.js} +7 -4
  33. package/dist/create-prompt-tag-S4JX3ULL.js +17 -0
  34. package/dist/{create-scenario-DIMPJRPY.js → create-scenario-BPRF75KF.js} +4 -4
  35. package/dist/create-suite-4YN7D6K2.js +38 -0
  36. package/dist/delete-agent-Y72R2YQA.js +18 -0
  37. package/dist/delete-annotation-UHWDMHSH.js +17 -0
  38. package/dist/delete-dashboard-LPWT36KN.js +18 -0
  39. package/dist/delete-dataset-GGPQRHKX.js +20 -0
  40. package/dist/delete-dataset-records-LTG5LT26.js +21 -0
  41. package/dist/delete-evaluator-F7QLXDHU.js +15 -0
  42. package/dist/delete-prompt-tag-B5UKZL45.js +17 -0
  43. package/dist/delete-workflow-4DGUDIDE.js +17 -0
  44. package/dist/get-agent-YEVICUX4.js +28 -0
  45. package/dist/{get-analytics-4YJW4S5L.js → get-analytics-3W5CMU3F.js} +22 -5
  46. package/dist/get-dashboard-NCQHN6MW.js +29 -0
  47. package/dist/get-dataset-KBK6PH36.js +48 -0
  48. package/dist/{get-evaluator-WDEH2F7M.js → get-evaluator-QDUL5LXR.js} +4 -4
  49. package/dist/{get-prompt-F6PDVC76.js → get-prompt-XCSSNRKR.js} +4 -4
  50. package/dist/{get-scenario-H24ZYNT5.js → get-scenario-FNRKOY7E.js} +4 -4
  51. package/dist/get-simulation-run-WOEKUF7K.js +56 -0
  52. package/dist/get-suite-U5AOD5TC.js +44 -0
  53. package/dist/{get-trace-27USKGO7.js → get-trace-MUWEJM2Z.js} +3 -3
  54. package/dist/get-workflow-SOA7MT7L.js +25 -0
  55. package/dist/http-server-7DMOCB75.js +224 -0
  56. package/dist/index.js +61640 -31868
  57. package/dist/langwatch-api-annotations-Q57NDUVR.js +17 -0
  58. package/dist/langwatch-api-dashboards-VBWWXV3Y.js +17 -0
  59. package/dist/langwatch-api-monitors-L3IY7FDT.js +52 -0
  60. package/dist/langwatch-api-secrets-S5HHF3XR.js +40 -0
  61. package/dist/langwatch-api-triggers-ME33QE26.js +30 -0
  62. package/dist/langwatch-api-workflows-4CJQAYP2.js +15 -0
  63. package/dist/list-agents-3HGIBJFM.js +32 -0
  64. package/dist/list-annotations-KWXYVEPI.js +33 -0
  65. package/dist/list-dashboards-VQ2LH666.js +32 -0
  66. package/dist/list-dataset-records-RNRGROJP.js +39 -0
  67. package/dist/list-datasets-I4A2LT2S.js +60 -0
  68. package/dist/{list-evaluators-KRGI72EH.js → list-evaluators-AKKRJ5MX.js} +4 -4
  69. package/dist/{list-model-providers-A5YCFTPI.js → list-model-providers-DVFEHFHZ.js} +4 -4
  70. package/dist/list-prompt-tags-PL5OISUA.js +23 -0
  71. package/dist/{list-prompts-LKJSE7XN.js → list-prompts-7VTFWE2Z.js} +3 -3
  72. package/dist/{list-scenarios-ZK5CMGC4.js → list-scenarios-5BZWZ6ZN.js} +4 -4
  73. package/dist/list-simulation-runs-WCZ6RLPE.js +43 -0
  74. package/dist/list-suites-RRQ2JX5Y.js +42 -0
  75. package/dist/list-workflows-UQB5FCX5.js +35 -0
  76. package/dist/rename-prompt-tag-TURD6KYU.js +18 -0
  77. package/dist/run-agent-BEJ4K4CM.js +36 -0
  78. package/dist/run-evaluation-AJ7WZNQD.js +64 -0
  79. package/dist/run-suite-U7V432G2.js +29 -0
  80. package/dist/run-workflow-5WX7DK5T.js +36 -0
  81. package/dist/{search-traces-SOKAAMAR.js → search-traces-ZCHNOMTX.js} +3 -3
  82. package/dist/{set-model-provider-7MGULZDH.js → set-model-provider-6CFHMCNE.js} +4 -4
  83. package/dist/update-agent-UE6UWLZP.js +20 -0
  84. package/dist/update-dataset-4XQZZE4S.js +22 -0
  85. package/dist/update-dataset-record-GNNM5TKM.js +19 -0
  86. package/dist/{update-evaluator-A3XINFLJ.js → update-evaluator-LHVDXIZD.js} +4 -4
  87. package/dist/{update-prompt-IW7X2UQM.js → update-prompt-3XEHECJ7.js} +5 -3
  88. package/dist/{update-scenario-ZT7TOBFR.js → update-scenario-DFPREYCD.js} +4 -4
  89. package/dist/update-suite-RATU5THO.js +39 -0
  90. package/package.json +22 -8
  91. package/src/__tests__/all-tools.integration.test.ts +353 -0
  92. package/src/__tests__/config.unit.test.ts +81 -1
  93. package/src/__tests__/datasets.integration.test.ts +620 -0
  94. package/src/__tests__/datasets.unit.test.ts +244 -0
  95. package/src/__tests__/docker.integration.test.ts +234 -0
  96. package/src/__tests__/http-transport.integration.test.ts +397 -0
  97. package/src/__tests__/langwatch-api.unit.test.ts +174 -0
  98. package/src/__tests__/scenario-tools.unit.test.ts +3 -0
  99. package/src/__tests__/tools.unit.test.ts +177 -0
  100. package/src/config.d.ts +15 -0
  101. package/src/config.ts +85 -7
  102. package/src/create-mcp-server.d.ts +6 -0
  103. package/src/create-mcp-server.ts +1799 -0
  104. package/src/http-server.ts +329 -0
  105. package/src/index.ts +22 -593
  106. package/src/langwatch-api-agents.ts +108 -0
  107. package/src/langwatch-api-annotations.ts +49 -0
  108. package/src/langwatch-api-dashboards.ts +48 -0
  109. package/src/langwatch-api-datasets.ts +191 -0
  110. package/src/langwatch-api-evaluators.ts +10 -0
  111. package/src/langwatch-api-monitors.ts +82 -0
  112. package/src/langwatch-api-secrets.ts +48 -0
  113. package/src/langwatch-api-simulation-runs.ts +51 -0
  114. package/src/langwatch-api-suites.ts +118 -0
  115. package/src/langwatch-api-triggers.ts +48 -0
  116. package/src/langwatch-api-workflows.ts +41 -0
  117. package/src/langwatch-api.ts +65 -3
  118. package/src/schemas/create-dataset.ts +48 -0
  119. package/src/schemas/index.ts +1 -0
  120. package/src/tools/archive-suite.ts +12 -0
  121. package/src/tools/assign-prompt-tag.ts +16 -0
  122. package/src/tools/create-agent.ts +15 -0
  123. package/src/tools/create-annotation.ts +18 -0
  124. package/src/tools/create-dashboard.ts +7 -0
  125. package/src/tools/create-dataset-records.ts +23 -0
  126. package/src/tools/create-dataset.ts +22 -0
  127. package/src/tools/create-prompt-tag.ts +12 -0
  128. package/src/tools/create-prompt.ts +4 -0
  129. package/src/tools/create-suite.ts +42 -0
  130. package/src/tools/delete-agent.ts +7 -0
  131. package/src/tools/delete-annotation.ts +7 -0
  132. package/src/tools/delete-dashboard.ts +7 -0
  133. package/src/tools/delete-dataset-records.ts +22 -0
  134. package/src/tools/delete-dataset.ts +20 -0
  135. package/src/tools/delete-evaluator.ts +12 -0
  136. package/src/tools/delete-prompt-tag.ts +12 -0
  137. package/src/tools/delete-workflow.ts +7 -0
  138. package/src/tools/format-dataset-mutation.ts +25 -0
  139. package/src/tools/get-agent.ts +21 -0
  140. package/src/tools/get-analytics.ts +30 -3
  141. package/src/tools/get-dashboard.ts +22 -0
  142. package/src/tools/get-dataset.ts +63 -0
  143. package/src/tools/get-prompt.ts +2 -1
  144. package/src/tools/get-simulation-run.ts +60 -0
  145. package/src/tools/get-suite.ts +47 -0
  146. package/src/tools/get-workflow.ts +17 -0
  147. package/src/tools/list-agents.ts +27 -0
  148. package/src/tools/list-annotations.ts +33 -0
  149. package/src/tools/list-dashboards.ts +27 -0
  150. package/src/tools/list-dataset-records.ts +46 -0
  151. package/src/tools/list-datasets.ts +80 -0
  152. package/src/tools/list-prompt-tags.ts +21 -0
  153. package/src/tools/list-simulation-runs.ts +49 -0
  154. package/src/tools/list-suites.ts +43 -0
  155. package/src/tools/list-workflows.ts +31 -0
  156. package/src/tools/rename-prompt-tag.ts +14 -0
  157. package/src/tools/run-agent.ts +36 -0
  158. package/src/tools/run-evaluation.ts +87 -0
  159. package/src/tools/run-suite.ts +28 -0
  160. package/src/tools/run-workflow.ts +36 -0
  161. package/src/tools/update-agent.ts +13 -0
  162. package/src/tools/update-dataset-record.ts +21 -0
  163. package/src/tools/update-dataset.ts +23 -0
  164. package/src/tools/update-prompt.ts +3 -0
  165. package/src/tools/update-suite.ts +44 -0
  166. package/src/utils/escape-markdown.ts +12 -0
  167. package/uv.lock +188 -530
  168. package/build.js +0 -12
  169. package/dist/archive-scenario-YFD5THOR.js.map +0 -1
  170. package/dist/chunk-5UOPNRXW.js.map +0 -1
  171. package/dist/chunk-6U4TCGFC.js.map +0 -1
  172. package/dist/chunk-AAQNA53E.js +0 -28
  173. package/dist/chunk-AAQNA53E.js.map +0 -1
  174. package/dist/chunk-IX6QJKAD.js.map +0 -1
  175. package/dist/chunk-LLRQIF52.js.map +0 -1
  176. package/dist/chunk-ZXKLPC2E.js.map +0 -1
  177. package/dist/config-FIQWQRUB.js +0 -11
  178. package/dist/config-FIQWQRUB.js.map +0 -1
  179. package/dist/create-evaluator-E5X5ZP3B.js.map +0 -1
  180. package/dist/create-prompt-7Z35MIL6.js.map +0 -1
  181. package/dist/create-scenario-DIMPJRPY.js.map +0 -1
  182. package/dist/discover-evaluator-schema-H23XCLNE.js.map +0 -1
  183. package/dist/discover-scenario-schema-MEEEVND7.js.map +0 -1
  184. package/dist/discover-schema-3T52ORPB.js.map +0 -1
  185. package/dist/get-analytics-4YJW4S5L.js.map +0 -1
  186. package/dist/get-evaluator-WDEH2F7M.js.map +0 -1
  187. package/dist/get-prompt-F6PDVC76.js.map +0 -1
  188. package/dist/get-scenario-H24ZYNT5.js.map +0 -1
  189. package/dist/get-trace-27USKGO7.js.map +0 -1
  190. package/dist/index.js.map +0 -1
  191. package/dist/list-evaluators-KRGI72EH.js.map +0 -1
  192. package/dist/list-model-providers-A5YCFTPI.js.map +0 -1
  193. package/dist/list-prompts-LKJSE7XN.js.map +0 -1
  194. package/dist/list-scenarios-ZK5CMGC4.js.map +0 -1
  195. package/dist/search-traces-SOKAAMAR.js.map +0 -1
  196. package/dist/set-model-provider-7MGULZDH.js.map +0 -1
  197. package/dist/update-evaluator-A3XINFLJ.js.map +0 -1
  198. package/dist/update-prompt-IW7X2UQM.js.map +0 -1
  199. package/dist/update-scenario-ZT7TOBFR.js.map +0 -1
  200. package/tests/evaluations.ipynb +0 -649
  201. package/tests/fixtures/azure/azure_openai_stream_bot_expected.py +0 -102
  202. package/tests/fixtures/azure/azure_openai_stream_bot_input.py +0 -78
  203. package/tests/fixtures/dspy/dspy_bot_expected.py +0 -61
  204. package/tests/fixtures/dspy/dspy_bot_input.py +0 -53
  205. package/tests/fixtures/fastapi/fastapi_app_expected.py +0 -68
  206. package/tests/fixtures/fastapi/fastapi_app_input.py +0 -60
  207. package/tests/fixtures/fastapi/prompt_management_fastapi_expected.py +0 -114
  208. package/tests/fixtures/fastapi/prompt_management_fastapi_input.py +0 -88
  209. package/tests/fixtures/haystack/haystack_bot_expected.py +0 -141
  210. package/tests/fixtures/haystack/haystack_bot_input.py +0 -69
  211. package/tests/fixtures/langchain/langchain_bot_expected.py +0 -53
  212. package/tests/fixtures/langchain/langchain_bot_input.py +0 -45
  213. package/tests/fixtures/langchain/langchain_bot_with_memory_expected.py +0 -69
  214. package/tests/fixtures/langchain/langchain_bot_with_memory_input.py +0 -61
  215. package/tests/fixtures/langchain/langchain_rag_bot_expected.py +0 -97
  216. package/tests/fixtures/langchain/langchain_rag_bot_input.py +0 -77
  217. package/tests/fixtures/langchain/langchain_rag_bot_vertex_ai_expected.py +0 -116
  218. package/tests/fixtures/langchain/langchain_rag_bot_vertex_ai_input.py +0 -81
  219. package/tests/fixtures/langchain/langgraph_rag_bot_with_threads_expected.py +0 -331
  220. package/tests/fixtures/langchain/langgraph_rag_bot_with_threads_input.py +0 -106
  221. package/tests/fixtures/litellm/litellm_bot_expected.py +0 -40
  222. package/tests/fixtures/litellm/litellm_bot_input.py +0 -35
  223. package/tests/fixtures/openai/openai_bot_expected.py +0 -43
  224. package/tests/fixtures/openai/openai_bot_function_call_expected.py +0 -91
  225. package/tests/fixtures/openai/openai_bot_function_call_input.py +0 -82
  226. package/tests/fixtures/openai/openai_bot_input.py +0 -36
  227. package/tests/fixtures/openai/openai_bot_rag_expected.py +0 -73
  228. package/tests/fixtures/openai/openai_bot_rag_input.py +0 -51
  229. package/tests/fixtures/opentelemetry/openinference_dspy_bot_expected.py +0 -63
  230. package/tests/fixtures/opentelemetry/openinference_dspy_bot_input.py +0 -58
  231. package/tests/fixtures/opentelemetry/openinference_langchain_bot_expected.py +0 -53
  232. package/tests/fixtures/opentelemetry/openinference_langchain_bot_input.py +0 -52
  233. package/tests/fixtures/opentelemetry/openinference_openai_bot_expected.py +0 -49
  234. package/tests/fixtures/opentelemetry/openinference_openai_bot_input.py +0 -41
  235. package/tests/fixtures/opentelemetry/openllmetry_openai_bot_expected.py +0 -44
  236. package/tests/fixtures/opentelemetry/openllmetry_openai_bot_input.py +0 -40
  237. package/tests/fixtures/strands/strands_bot_expected.py +0 -84
  238. package/tests/fixtures/strands/strands_bot_input.py +0 -52
  239. package/tests/scenario-openai.test.ts +0 -160
  240. package/tsup.config.ts +0 -10
  241. package/vitest.config.js +0 -7
@@ -0,0 +1,244 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ vi.mock("../langwatch-api-datasets.js", () => ({
4
+ listDatasets: vi.fn(),
5
+ getDataset: vi.fn(),
6
+ }));
7
+
8
+ import { listDatasets, getDataset } from "../langwatch-api-datasets.js";
9
+
10
+ import { handleListDatasets } from "../tools/list-datasets.js";
11
+ import { handleGetDataset } from "../tools/get-dataset.js";
12
+ import { formatDatasetResponse } from "../tools/get-dataset.js";
13
+ import { createDatasetSchema } from "../schemas/create-dataset.js";
14
+
15
+ const mockListDatasets = vi.mocked(listDatasets);
16
+ const mockGetDataset = vi.mocked(getDataset);
17
+
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ // ── formatDatasetResponse ────────────────────────────────────────
23
+
24
+ describe("formatDatasetResponse()", () => {
25
+ const sampleDataset = {
26
+ id: "ds_abc123",
27
+ name: "My Dataset",
28
+ slug: "my-dataset",
29
+ columnTypes: [
30
+ { name: "input", type: "string" },
31
+ { name: "output", type: "string" },
32
+ ],
33
+ createdAt: "2025-01-01T00:00:00.000Z",
34
+ updatedAt: "2025-01-02T00:00:00.000Z",
35
+ data: [
36
+ { id: "rec-1", entry: { input: "hello", output: "world" } },
37
+ { id: "rec-2", entry: { input: "foo", output: "bar" } },
38
+ ],
39
+ };
40
+
41
+ describe("when given a dataset with columns and records", () => {
42
+ let result: string;
43
+
44
+ beforeEach(() => {
45
+ result = formatDatasetResponse(sampleDataset);
46
+ });
47
+
48
+ it("includes the dataset name in the heading", () => {
49
+ expect(result).toContain("# Dataset: My Dataset");
50
+ });
51
+
52
+ it("includes the slug", () => {
53
+ expect(result).toContain("my-dataset");
54
+ });
55
+
56
+ /** @scenario "formatDatasetResponse renders column table and record entries as markdown" */
57
+ it("includes a column table", () => {
58
+ expect(result).toContain("input");
59
+ expect(result).toContain("output");
60
+ expect(result).toContain("string");
61
+ });
62
+
63
+ it("includes record entries", () => {
64
+ expect(result).toContain("hello");
65
+ expect(result).toContain("world");
66
+ expect(result).toContain("foo");
67
+ expect(result).toContain("bar");
68
+ });
69
+
70
+ it("includes record IDs", () => {
71
+ expect(result).toContain("rec-1");
72
+ expect(result).toContain("rec-2");
73
+ });
74
+ });
75
+
76
+ describe("when given a dataset with no records", () => {
77
+ it("indicates no records", () => {
78
+ const result = formatDatasetResponse({
79
+ ...sampleDataset,
80
+ data: [],
81
+ });
82
+ expect(result).toContain("No records");
83
+ });
84
+ });
85
+ });
86
+
87
+ // ── handleListDatasets ──────────────────────────────────────────
88
+
89
+ describe("handleListDatasets()", () => {
90
+ const sampleListResponse = {
91
+ data: [
92
+ {
93
+ id: "ds_abc123",
94
+ name: "User Feedback",
95
+ slug: "user-feedback",
96
+ columnTypes: [
97
+ { name: "input", type: "string" },
98
+ { name: "output", type: "string" },
99
+ ],
100
+ recordCount: 25,
101
+ createdAt: "2025-01-01T00:00:00.000Z",
102
+ updatedAt: "2025-01-02T00:00:00.000Z",
103
+ },
104
+ ],
105
+ pagination: { total: 1, page: 1, limit: 50, totalPages: 1 },
106
+ };
107
+
108
+ describe("when format is digest (default)", () => {
109
+ it("returns markdown containing dataset names", async () => {
110
+ mockListDatasets.mockResolvedValue(sampleListResponse);
111
+ const result = await handleListDatasets();
112
+ expect(result).toContain("User Feedback");
113
+ expect(result).toContain("user-feedback");
114
+ });
115
+ });
116
+
117
+ describe("when format is json", () => {
118
+ it("returns valid parseable JSON containing all datasets and total", async () => {
119
+ mockListDatasets.mockResolvedValue(sampleListResponse);
120
+ const result = await handleListDatasets({ format: "json" });
121
+ const parsed = JSON.parse(result);
122
+ expect(parsed.data).toEqual(sampleListResponse.data);
123
+ expect(parsed.total).toBe(sampleListResponse.pagination.total);
124
+ });
125
+ });
126
+
127
+ describe("when no datasets exist", () => {
128
+ it("returns a no-datasets message in digest mode", async () => {
129
+ mockListDatasets.mockResolvedValue({ data: [], pagination: { total: 0, page: 1, limit: 50, totalPages: 0 } });
130
+ const result = await handleListDatasets();
131
+ expect(result).toContain("No datasets found");
132
+ });
133
+ });
134
+ });
135
+
136
+ // ── handleGetDataset ────────────────────────────────────────────
137
+
138
+ describe("handleGetDataset()", () => {
139
+ const sampleDataset = {
140
+ id: "ds_abc123",
141
+ name: "My Dataset",
142
+ slug: "my-dataset",
143
+ columnTypes: [
144
+ { name: "input", type: "string" },
145
+ { name: "output", type: "string" },
146
+ ],
147
+ createdAt: "2025-01-01T00:00:00.000Z",
148
+ updatedAt: "2025-01-02T00:00:00.000Z",
149
+ data: [
150
+ { id: "rec-1", entry: { input: "hello", output: "world" } },
151
+ ],
152
+ };
153
+
154
+ describe("when format is digest (default)", () => {
155
+ it("returns markdown containing dataset name", async () => {
156
+ mockGetDataset.mockResolvedValue(sampleDataset);
157
+ const result = await handleGetDataset({ slugOrId: "my-dataset" });
158
+ expect(result).toContain("# Dataset: My Dataset");
159
+ });
160
+ });
161
+
162
+ describe("when format is json", () => {
163
+ it("returns valid parseable JSON matching the dataset structure", async () => {
164
+ mockGetDataset.mockResolvedValue(sampleDataset);
165
+ const result = await handleGetDataset({ slugOrId: "my-dataset", format: "json" });
166
+ expect(JSON.parse(result)).toEqual(sampleDataset);
167
+ });
168
+ });
169
+ });
170
+
171
+ // ── platform_create_dataset schema validation ────────────────────
172
+
173
+ describe("platform_create_dataset schema", () => {
174
+ describe("when input has no name", () => {
175
+ /** @scenario "platform_create_dataset schema rejects input without a name" */
176
+ it("rejects the input with a validation error", () => {
177
+ const result = createDatasetSchema.safeParse({});
178
+ expect(result.success).toBe(false);
179
+ });
180
+ });
181
+
182
+ describe("when input has an empty name", () => {
183
+ it("rejects the input with a validation error", () => {
184
+ const result = createDatasetSchema.safeParse({ name: "" });
185
+ expect(result.success).toBe(false);
186
+ });
187
+ });
188
+
189
+ describe("when input has a valid name", () => {
190
+ it("accepts the input", () => {
191
+ const result = createDatasetSchema.safeParse({ name: "Test" });
192
+ expect(result.success).toBe(true);
193
+ });
194
+ });
195
+ });
196
+
197
+ // ── Tool Registration ────────────────────────────────────────────
198
+
199
+ describe("MCP server dataset tool registration", () => {
200
+ describe("when the MCP server is created", () => {
201
+ /** @scenario "All dataset tools are registered in the MCP server" */
202
+ it("registers all 8 dataset tools", async () => {
203
+ const { createMcpServer } = await import("../create-mcp-server.js");
204
+ const server = createMcpServer();
205
+ // Access registered tools via the internal _registeredTools object
206
+ const registeredTools = (server as unknown as { _registeredTools: Record<string, unknown> })._registeredTools;
207
+ const toolNames = Object.keys(registeredTools);
208
+
209
+ expect(toolNames).toContain("platform_list_datasets");
210
+ expect(toolNames).toContain("platform_get_dataset");
211
+ expect(toolNames).toContain("platform_create_dataset");
212
+ expect(toolNames).toContain("platform_update_dataset");
213
+ expect(toolNames).toContain("platform_delete_dataset");
214
+ expect(toolNames).toContain("platform_create_dataset_records");
215
+ expect(toolNames).toContain("platform_update_dataset_record");
216
+ expect(toolNames).toContain("platform_delete_dataset_records");
217
+ });
218
+ });
219
+ });
220
+
221
+ // ── API Key Requirement ──────────────────────────────────────────
222
+
223
+ describe("dataset tools API key requirement", () => {
224
+ describe("when no API key is configured", () => {
225
+ /** @scenario "Dataset tools require an API key" */
226
+ it("requireApiKey throws when apiKey is empty", async () => {
227
+ const savedKey = process.env.LANGWATCH_API_KEY;
228
+ delete process.env.LANGWATCH_API_KEY;
229
+
230
+ try {
231
+ const { initConfig, requireApiKey } = await import("../config.js");
232
+ initConfig({ apiKey: "", endpoint: "http://localhost:0" });
233
+
234
+ expect(() => requireApiKey()).toThrow(
235
+ "LANGWATCH_API_KEY is required",
236
+ );
237
+ } finally {
238
+ if (savedKey !== undefined) {
239
+ process.env.LANGWATCH_API_KEY = savedKey;
240
+ }
241
+ }
242
+ });
243
+ });
244
+ });
@@ -0,0 +1,234 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { execSync } from "child_process";
3
+
4
+ const IMAGE_NAME = "langwatch-mcp-server-test";
5
+ const CONTAINER_NAME = "langwatch-mcp-server-test-container";
6
+ const HOST_PORT = 13099; // unlikely to conflict
7
+ const BEARER_TOKEN = "test-docker-api-key";
8
+
9
+ /** Standard headers required by the MCP Streamable HTTP protocol */
10
+ const MCP_POST_HEADERS = {
11
+ "Content-Type": "application/json",
12
+ Accept: "application/json, text/event-stream",
13
+ };
14
+
15
+ describe("Docker container", () => {
16
+ let containerRunning = false;
17
+
18
+ beforeAll(async () => {
19
+ try {
20
+ // Build from repo root (mcp-server needs langevals/ for build)
21
+ const repoRoot = process.cwd().replace(/\/mcp-server.*$/, "");
22
+ execSync(
23
+ `docker build -t ${IMAGE_NAME} -f mcp-server/Dockerfile .`,
24
+ {
25
+ cwd: repoRoot,
26
+ stdio: "pipe",
27
+ timeout: 180_000,
28
+ }
29
+ );
30
+
31
+ // Stop any previous container
32
+ execSync(`docker rm -f ${CONTAINER_NAME} 2>/dev/null || true`, {
33
+ stdio: "pipe",
34
+ });
35
+
36
+ // Start the container WITHOUT LANGWATCH_API_KEY -- clients bring their own
37
+ execSync(
38
+ `docker run -d --name ${CONTAINER_NAME} -p ${HOST_PORT}:3000 ${IMAGE_NAME}`,
39
+ { stdio: "pipe" }
40
+ );
41
+
42
+ // Wait for the server to be ready
43
+ let retries = 0;
44
+ while (retries < 20) {
45
+ try {
46
+ const res = await fetch(`http://localhost:${HOST_PORT}/health`);
47
+ if (res.ok) {
48
+ containerRunning = true;
49
+ break;
50
+ }
51
+ } catch {
52
+ // not ready yet
53
+ }
54
+ await new Promise((r) => setTimeout(r, 500));
55
+ retries++;
56
+ }
57
+
58
+ if (!containerRunning) {
59
+ const logs = execSync(`docker logs ${CONTAINER_NAME}`, {
60
+ encoding: "utf8",
61
+ });
62
+ throw new Error(`Container failed to start. Logs:\n${logs}`);
63
+ }
64
+ } catch (error) {
65
+ console.error("Docker setup failed:", error);
66
+ // Clean up on failure
67
+ execSync(`docker rm -f ${CONTAINER_NAME} 2>/dev/null || true`, {
68
+ stdio: "pipe",
69
+ });
70
+ }
71
+ }, 180_000); // 3 min for build + start
72
+
73
+ afterAll(() => {
74
+ execSync(`docker rm -f ${CONTAINER_NAME} 2>/dev/null || true`, {
75
+ stdio: "pipe",
76
+ });
77
+ });
78
+
79
+ it("health endpoint responds without authentication", async () => {
80
+ if (!containerRunning) return;
81
+
82
+ const res = await fetch(`http://localhost:${HOST_PORT}/health`);
83
+ expect(res.status).toBe(200);
84
+ const body = await res.json();
85
+ expect(body.status).toBe("ok");
86
+ });
87
+
88
+ it("CORS headers are present", async () => {
89
+ if (!containerRunning) return;
90
+
91
+ const res = await fetch(`http://localhost:${HOST_PORT}/health`);
92
+ expect(res.headers.get("access-control-allow-origin")).toBe("*");
93
+ });
94
+
95
+ it("returns 401 on initialize without Bearer token", async () => {
96
+ if (!containerRunning) return;
97
+
98
+ const res = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
99
+ method: "POST",
100
+ headers: MCP_POST_HEADERS,
101
+ body: JSON.stringify({
102
+ jsonrpc: "2.0",
103
+ id: 1,
104
+ method: "initialize",
105
+ params: {
106
+ protocolVersion: "2025-03-26",
107
+ capabilities: {},
108
+ clientInfo: { name: "docker-test", version: "1.0.0" },
109
+ },
110
+ }),
111
+ });
112
+
113
+ expect(res.status).toBe(401);
114
+ const body = await res.json();
115
+ expect(body.error).toContain("Authorization");
116
+ });
117
+
118
+ it("MCP initialize works with Bearer token", async () => {
119
+ if (!containerRunning) return;
120
+
121
+ const res = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
122
+ method: "POST",
123
+ headers: {
124
+ ...MCP_POST_HEADERS,
125
+ Authorization: `Bearer ${BEARER_TOKEN}`,
126
+ },
127
+ body: JSON.stringify({
128
+ jsonrpc: "2.0",
129
+ id: 1,
130
+ method: "initialize",
131
+ params: {
132
+ protocolVersion: "2025-03-26",
133
+ capabilities: {},
134
+ clientInfo: { name: "docker-test", version: "1.0.0" },
135
+ },
136
+ }),
137
+ });
138
+
139
+ expect(res.status).toBe(200);
140
+ const sessionId = res.headers.get("mcp-session-id");
141
+ expect(sessionId).toBeTruthy();
142
+
143
+ // Parse SSE response for the initialize result
144
+ const text = await res.text();
145
+ expect(text).toContain("serverInfo");
146
+ });
147
+
148
+ it("lists tools after initialization with Bearer token", async () => {
149
+ if (!containerRunning) return;
150
+
151
+ // Initialize a session
152
+ const initRes = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
153
+ method: "POST",
154
+ headers: {
155
+ ...MCP_POST_HEADERS,
156
+ Authorization: `Bearer ${BEARER_TOKEN}`,
157
+ },
158
+ body: JSON.stringify({
159
+ jsonrpc: "2.0",
160
+ id: 1,
161
+ method: "initialize",
162
+ params: {
163
+ protocolVersion: "2025-03-26",
164
+ capabilities: {},
165
+ clientInfo: { name: "docker-test", version: "1.0.0" },
166
+ },
167
+ }),
168
+ });
169
+
170
+ const sessionId = initRes.headers.get("mcp-session-id");
171
+
172
+ // Send initialized notification
173
+ await fetch(`http://localhost:${HOST_PORT}/mcp`, {
174
+ method: "POST",
175
+ headers: {
176
+ ...MCP_POST_HEADERS,
177
+ "mcp-session-id": sessionId!,
178
+ Authorization: `Bearer ${BEARER_TOKEN}`,
179
+ },
180
+ body: JSON.stringify({
181
+ jsonrpc: "2.0",
182
+ method: "notifications/initialized",
183
+ }),
184
+ });
185
+
186
+ // List tools
187
+ const toolsRes = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
188
+ method: "POST",
189
+ headers: {
190
+ ...MCP_POST_HEADERS,
191
+ "mcp-session-id": sessionId!,
192
+ Authorization: `Bearer ${BEARER_TOKEN}`,
193
+ },
194
+ body: JSON.stringify({
195
+ jsonrpc: "2.0",
196
+ id: 2,
197
+ method: "tools/list",
198
+ }),
199
+ });
200
+
201
+ expect(toolsRes.status).toBe(200);
202
+ const toolsText = await toolsRes.text();
203
+ expect(toolsText).toContain("fetch_langwatch_docs");
204
+ expect(toolsText).toContain("search_traces");
205
+ });
206
+
207
+ it("legacy SSE endpoint responds with Bearer token", async () => {
208
+ if (!containerRunning) return;
209
+
210
+ const controller = new AbortController();
211
+ const timeout = setTimeout(() => controller.abort(), 5000);
212
+
213
+ try {
214
+ const res = await fetch(`http://localhost:${HOST_PORT}/sse`, {
215
+ signal: controller.signal,
216
+ headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
217
+ });
218
+ expect(res.status).toBe(200);
219
+ expect(res.headers.get("content-type")).toContain("text/event-stream");
220
+ } catch (error: any) {
221
+ if (error.name !== "AbortError") throw error;
222
+ // AbortError is expected -- SSE stays open
223
+ } finally {
224
+ clearTimeout(timeout);
225
+ }
226
+ });
227
+
228
+ it("legacy SSE endpoint returns 401 without token", async () => {
229
+ if (!containerRunning) return;
230
+
231
+ const res = await fetch(`http://localhost:${HOST_PORT}/sse`);
232
+ expect(res.status).toBe(401);
233
+ });
234
+ });