@langwatch/mcp-server 0.6.0 → 0.7.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 (258) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/Dockerfile +22 -0
  3. package/README.md +51 -8
  4. package/dist/{archive-scenario-YFD5THOR.js → archive-scenario-WJ2A7ETN.js} +4 -4
  5. package/dist/archive-suite-4JOM4SSL.js +15 -0
  6. package/dist/archive-suite-4JOM4SSL.js.map +1 -0
  7. package/dist/assign-prompt-tag-GL7EAAF3.js +19 -0
  8. package/dist/assign-prompt-tag-GL7EAAF3.js.map +1 -0
  9. package/dist/{chunk-6U4TCGFC.js → chunk-2GT7IFWK.js} +2 -2
  10. package/dist/{chunk-LLRQIF52.js → chunk-2WPLXEKF.js} +49 -5
  11. package/dist/chunk-2WPLXEKF.js.map +1 -0
  12. package/dist/chunk-4AUB2BIA.js +71 -0
  13. package/dist/chunk-4AUB2BIA.js.map +1 -0
  14. package/dist/chunk-BWWY5KEF.js +71 -0
  15. package/dist/chunk-BWWY5KEF.js.map +1 -0
  16. package/dist/{chunk-IX6QJKAD.js → chunk-CCWCIJJ3.js} +2 -2
  17. package/dist/chunk-E5JYYU3J.js +1567 -0
  18. package/dist/chunk-E5JYYU3J.js.map +1 -0
  19. package/dist/chunk-EGH5RPXM.js +25 -0
  20. package/dist/chunk-EGH5RPXM.js.map +1 -0
  21. package/dist/chunk-EUQDVOSU.js +39 -0
  22. package/dist/chunk-EUQDVOSU.js.map +1 -0
  23. package/dist/{chunk-5UOPNRXW.js → chunk-HYMIWO3U.js} +10 -3
  24. package/dist/{chunk-5UOPNRXW.js.map → chunk-HYMIWO3U.js.map} +1 -1
  25. package/dist/chunk-IE4M5OGE.js +42 -0
  26. package/dist/chunk-IE4M5OGE.js.map +1 -0
  27. package/dist/chunk-LUMEZEHL.js +48 -0
  28. package/dist/chunk-LUMEZEHL.js.map +1 -0
  29. package/dist/chunk-NUFPTHKU.js +9 -0
  30. package/dist/chunk-NUFPTHKU.js.map +1 -0
  31. package/dist/chunk-WQG2DFHX.js +86 -0
  32. package/dist/chunk-WQG2DFHX.js.map +1 -0
  33. package/dist/chunk-WVI5CJ6S.js +21 -0
  34. package/dist/chunk-WVI5CJ6S.js.map +1 -0
  35. package/dist/chunk-ZVS4UKN2.js +35 -0
  36. package/dist/chunk-ZVS4UKN2.js.map +1 -0
  37. package/dist/config.d.ts +17 -0
  38. package/dist/config.js +13 -0
  39. package/dist/create-agent-MBOOT3XK.js +23 -0
  40. package/dist/create-agent-MBOOT3XK.js.map +1 -0
  41. package/dist/create-annotation-XMV6A7ED.js +24 -0
  42. package/dist/create-annotation-XMV6A7ED.js.map +1 -0
  43. package/dist/create-dashboard-VRKLFVZJ.js +18 -0
  44. package/dist/create-dashboard-VRKLFVZJ.js.map +1 -0
  45. package/dist/create-dataset-7HQPZCRE.js +22 -0
  46. package/dist/create-dataset-7HQPZCRE.js.map +1 -0
  47. package/dist/create-dataset-records-2NIVFYTV.js +22 -0
  48. package/dist/create-dataset-records-2NIVFYTV.js.map +1 -0
  49. package/dist/{create-evaluator-E5X5ZP3B.js → create-evaluator-WYFRFLRC.js} +4 -4
  50. package/dist/create-mcp-server.d.ts +8 -0
  51. package/dist/create-mcp-server.js +8 -0
  52. package/dist/create-mcp-server.js.map +1 -0
  53. package/dist/{create-prompt-7Z35MIL6.js → create-prompt-IEN7O6JT.js} +7 -4
  54. package/dist/create-prompt-IEN7O6JT.js.map +1 -0
  55. package/dist/create-prompt-tag-CAEZ45VO.js +17 -0
  56. package/dist/create-prompt-tag-CAEZ45VO.js.map +1 -0
  57. package/dist/{create-scenario-DIMPJRPY.js → create-scenario-6YHSMGLP.js} +4 -4
  58. package/dist/create-suite-H5XOFDE5.js +38 -0
  59. package/dist/create-suite-H5XOFDE5.js.map +1 -0
  60. package/dist/delete-agent-JKTNBWAA.js +18 -0
  61. package/dist/delete-agent-JKTNBWAA.js.map +1 -0
  62. package/dist/delete-annotation-ACMDV6QO.js +17 -0
  63. package/dist/delete-annotation-ACMDV6QO.js.map +1 -0
  64. package/dist/delete-dashboard-VJSR7CRU.js +18 -0
  65. package/dist/delete-dashboard-VJSR7CRU.js.map +1 -0
  66. package/dist/delete-dataset-RTTTWP4D.js +20 -0
  67. package/dist/delete-dataset-RTTTWP4D.js.map +1 -0
  68. package/dist/delete-dataset-records-KNQTIPQK.js +21 -0
  69. package/dist/delete-dataset-records-KNQTIPQK.js.map +1 -0
  70. package/dist/delete-evaluator-7FVC5WM4.js +15 -0
  71. package/dist/delete-evaluator-7FVC5WM4.js.map +1 -0
  72. package/dist/delete-prompt-tag-ZMZUAGFW.js +17 -0
  73. package/dist/delete-prompt-tag-ZMZUAGFW.js.map +1 -0
  74. package/dist/delete-workflow-EYQ3T4RQ.js +17 -0
  75. package/dist/delete-workflow-EYQ3T4RQ.js.map +1 -0
  76. package/dist/get-agent-E3QSTVKG.js +28 -0
  77. package/dist/get-agent-E3QSTVKG.js.map +1 -0
  78. package/dist/{get-analytics-4YJW4S5L.js → get-analytics-2OHV763I.js} +22 -5
  79. package/dist/get-analytics-2OHV763I.js.map +1 -0
  80. package/dist/get-dashboard-PC7JDHH6.js +29 -0
  81. package/dist/get-dashboard-PC7JDHH6.js.map +1 -0
  82. package/dist/get-dataset-H5IK5FVU.js +48 -0
  83. package/dist/get-dataset-H5IK5FVU.js.map +1 -0
  84. package/dist/{get-evaluator-WDEH2F7M.js → get-evaluator-UWVUPYTM.js} +4 -4
  85. package/dist/{get-prompt-F6PDVC76.js → get-prompt-WZUOLLQC.js} +4 -4
  86. package/dist/get-prompt-WZUOLLQC.js.map +1 -0
  87. package/dist/{get-scenario-H24ZYNT5.js → get-scenario-Q6LCNUYN.js} +4 -4
  88. package/dist/get-simulation-run-Z2XKO256.js +56 -0
  89. package/dist/get-simulation-run-Z2XKO256.js.map +1 -0
  90. package/dist/get-suite-BXK5TN7P.js +44 -0
  91. package/dist/get-suite-BXK5TN7P.js.map +1 -0
  92. package/dist/{get-trace-27USKGO7.js → get-trace-4EJAYVBG.js} +3 -3
  93. package/dist/get-workflow-O34QJLHD.js +25 -0
  94. package/dist/get-workflow-O34QJLHD.js.map +1 -0
  95. package/dist/http-server-ISHF7NJX.js +224 -0
  96. package/dist/http-server-ISHF7NJX.js.map +1 -0
  97. package/dist/index.js +61109 -31519
  98. package/dist/index.js.map +1 -1
  99. package/dist/langwatch-api-annotations-ERBG27RF.js +17 -0
  100. package/dist/langwatch-api-annotations-ERBG27RF.js.map +1 -0
  101. package/dist/langwatch-api-dashboards-G6TR7VVO.js +17 -0
  102. package/dist/langwatch-api-dashboards-G6TR7VVO.js.map +1 -0
  103. package/dist/langwatch-api-monitors-WSXZLS2B.js +52 -0
  104. package/dist/langwatch-api-monitors-WSXZLS2B.js.map +1 -0
  105. package/dist/langwatch-api-secrets-RR5DKG2X.js +40 -0
  106. package/dist/langwatch-api-secrets-RR5DKG2X.js.map +1 -0
  107. package/dist/langwatch-api-triggers-2RTNSWQY.js +30 -0
  108. package/dist/langwatch-api-triggers-2RTNSWQY.js.map +1 -0
  109. package/dist/langwatch-api-workflows-VJRIIDZ7.js +15 -0
  110. package/dist/langwatch-api-workflows-VJRIIDZ7.js.map +1 -0
  111. package/dist/list-agents-AW3P6FJV.js +32 -0
  112. package/dist/list-agents-AW3P6FJV.js.map +1 -0
  113. package/dist/list-annotations-QNCGWPQ3.js +33 -0
  114. package/dist/list-annotations-QNCGWPQ3.js.map +1 -0
  115. package/dist/list-dashboards-R7RK2IXL.js +32 -0
  116. package/dist/list-dashboards-R7RK2IXL.js.map +1 -0
  117. package/dist/list-dataset-records-WVCSQP7P.js +39 -0
  118. package/dist/list-dataset-records-WVCSQP7P.js.map +1 -0
  119. package/dist/list-datasets-KEXRLDEU.js +60 -0
  120. package/dist/list-datasets-KEXRLDEU.js.map +1 -0
  121. package/dist/{list-evaluators-KRGI72EH.js → list-evaluators-7R2LB4JH.js} +4 -4
  122. package/dist/{list-model-providers-A5YCFTPI.js → list-model-providers-OBYJCKBX.js} +4 -4
  123. package/dist/list-prompt-tags-6LY3ZPYI.js +23 -0
  124. package/dist/list-prompt-tags-6LY3ZPYI.js.map +1 -0
  125. package/dist/{list-prompts-LKJSE7XN.js → list-prompts-VWEZ4CDE.js} +3 -3
  126. package/dist/{list-scenarios-ZK5CMGC4.js → list-scenarios-TXD4W2ZJ.js} +4 -4
  127. package/dist/list-simulation-runs-RHPOLMFI.js +43 -0
  128. package/dist/list-simulation-runs-RHPOLMFI.js.map +1 -0
  129. package/dist/list-suites-FSGBX27Z.js +42 -0
  130. package/dist/list-suites-FSGBX27Z.js.map +1 -0
  131. package/dist/list-workflows-VRGEZ3EN.js +35 -0
  132. package/dist/list-workflows-VRGEZ3EN.js.map +1 -0
  133. package/dist/rename-prompt-tag-VOJJLX4M.js +18 -0
  134. package/dist/rename-prompt-tag-VOJJLX4M.js.map +1 -0
  135. package/dist/run-agent-KTCC7TB7.js +36 -0
  136. package/dist/run-agent-KTCC7TB7.js.map +1 -0
  137. package/dist/run-evaluation-2X3V3Q6L.js +64 -0
  138. package/dist/run-evaluation-2X3V3Q6L.js.map +1 -0
  139. package/dist/run-suite-BTMHGI2D.js +29 -0
  140. package/dist/run-suite-BTMHGI2D.js.map +1 -0
  141. package/dist/run-workflow-IGAYGKJL.js +36 -0
  142. package/dist/run-workflow-IGAYGKJL.js.map +1 -0
  143. package/dist/{search-traces-SOKAAMAR.js → search-traces-VYFUIMXH.js} +3 -3
  144. package/dist/{set-model-provider-7MGULZDH.js → set-model-provider-W5IUI326.js} +4 -4
  145. package/dist/update-agent-4BB7EYLR.js +20 -0
  146. package/dist/update-agent-4BB7EYLR.js.map +1 -0
  147. package/dist/update-dataset-CXBKZ4W3.js +22 -0
  148. package/dist/update-dataset-CXBKZ4W3.js.map +1 -0
  149. package/dist/update-dataset-record-HOHAMHXG.js +19 -0
  150. package/dist/update-dataset-record-HOHAMHXG.js.map +1 -0
  151. package/dist/{update-evaluator-A3XINFLJ.js → update-evaluator-IVOR6FTR.js} +4 -4
  152. package/dist/{update-prompt-IW7X2UQM.js → update-prompt-OWQAKE7K.js} +5 -3
  153. package/dist/update-prompt-OWQAKE7K.js.map +1 -0
  154. package/dist/{update-scenario-ZT7TOBFR.js → update-scenario-GDFVILPN.js} +4 -4
  155. package/dist/update-suite-BXYUYSPT.js +39 -0
  156. package/dist/update-suite-BXYUYSPT.js.map +1 -0
  157. package/package.json +17 -3
  158. package/src/__tests__/all-tools.integration.test.ts +344 -0
  159. package/src/__tests__/config.unit.test.ts +81 -1
  160. package/src/__tests__/datasets.integration.test.ts +603 -0
  161. package/src/__tests__/datasets.unit.test.ts +240 -0
  162. package/src/__tests__/docker.integration.test.ts +234 -0
  163. package/src/__tests__/http-transport.integration.test.ts +397 -0
  164. package/src/__tests__/langwatch-api.unit.test.ts +174 -0
  165. package/src/__tests__/tools.unit.test.ts +177 -0
  166. package/src/config.d.ts +15 -0
  167. package/src/config.ts +83 -7
  168. package/src/create-mcp-server.d.ts +6 -0
  169. package/src/create-mcp-server.ts +1799 -0
  170. package/src/http-server.ts +329 -0
  171. package/src/index.ts +22 -593
  172. package/src/langwatch-api-agents.ts +108 -0
  173. package/src/langwatch-api-annotations.ts +49 -0
  174. package/src/langwatch-api-dashboards.ts +48 -0
  175. package/src/langwatch-api-datasets.ts +191 -0
  176. package/src/langwatch-api-evaluators.ts +10 -0
  177. package/src/langwatch-api-monitors.ts +82 -0
  178. package/src/langwatch-api-secrets.ts +48 -0
  179. package/src/langwatch-api-simulation-runs.ts +51 -0
  180. package/src/langwatch-api-suites.ts +118 -0
  181. package/src/langwatch-api-triggers.ts +48 -0
  182. package/src/langwatch-api-workflows.ts +41 -0
  183. package/src/langwatch-api.ts +60 -2
  184. package/src/schemas/create-dataset.ts +48 -0
  185. package/src/schemas/index.ts +1 -0
  186. package/src/tools/archive-suite.ts +12 -0
  187. package/src/tools/assign-prompt-tag.ts +16 -0
  188. package/src/tools/create-agent.ts +15 -0
  189. package/src/tools/create-annotation.ts +18 -0
  190. package/src/tools/create-dashboard.ts +7 -0
  191. package/src/tools/create-dataset-records.ts +23 -0
  192. package/src/tools/create-dataset.ts +22 -0
  193. package/src/tools/create-prompt-tag.ts +12 -0
  194. package/src/tools/create-prompt.ts +4 -0
  195. package/src/tools/create-suite.ts +42 -0
  196. package/src/tools/delete-agent.ts +7 -0
  197. package/src/tools/delete-annotation.ts +7 -0
  198. package/src/tools/delete-dashboard.ts +7 -0
  199. package/src/tools/delete-dataset-records.ts +22 -0
  200. package/src/tools/delete-dataset.ts +20 -0
  201. package/src/tools/delete-evaluator.ts +12 -0
  202. package/src/tools/delete-prompt-tag.ts +12 -0
  203. package/src/tools/delete-workflow.ts +7 -0
  204. package/src/tools/format-dataset-mutation.ts +25 -0
  205. package/src/tools/get-agent.ts +21 -0
  206. package/src/tools/get-analytics.ts +30 -3
  207. package/src/tools/get-dashboard.ts +22 -0
  208. package/src/tools/get-dataset.ts +63 -0
  209. package/src/tools/get-prompt.ts +2 -1
  210. package/src/tools/get-simulation-run.ts +60 -0
  211. package/src/tools/get-suite.ts +47 -0
  212. package/src/tools/get-workflow.ts +17 -0
  213. package/src/tools/list-agents.ts +27 -0
  214. package/src/tools/list-annotations.ts +33 -0
  215. package/src/tools/list-dashboards.ts +27 -0
  216. package/src/tools/list-dataset-records.ts +46 -0
  217. package/src/tools/list-datasets.ts +80 -0
  218. package/src/tools/list-prompt-tags.ts +21 -0
  219. package/src/tools/list-simulation-runs.ts +49 -0
  220. package/src/tools/list-suites.ts +43 -0
  221. package/src/tools/list-workflows.ts +31 -0
  222. package/src/tools/rename-prompt-tag.ts +14 -0
  223. package/src/tools/run-agent.ts +36 -0
  224. package/src/tools/run-evaluation.ts +87 -0
  225. package/src/tools/run-suite.ts +28 -0
  226. package/src/tools/run-workflow.ts +36 -0
  227. package/src/tools/update-agent.ts +13 -0
  228. package/src/tools/update-dataset-record.ts +21 -0
  229. package/src/tools/update-dataset.ts +23 -0
  230. package/src/tools/update-prompt.ts +3 -0
  231. package/src/tools/update-suite.ts +44 -0
  232. package/src/utils/escape-markdown.ts +12 -0
  233. package/tsup.config.ts +5 -1
  234. package/dist/chunk-AAQNA53E.js +0 -28
  235. package/dist/chunk-AAQNA53E.js.map +0 -1
  236. package/dist/chunk-LLRQIF52.js.map +0 -1
  237. package/dist/config-FIQWQRUB.js +0 -11
  238. package/dist/create-prompt-7Z35MIL6.js.map +0 -1
  239. package/dist/get-analytics-4YJW4S5L.js.map +0 -1
  240. package/dist/get-prompt-F6PDVC76.js.map +0 -1
  241. package/dist/update-prompt-IW7X2UQM.js.map +0 -1
  242. /package/dist/{archive-scenario-YFD5THOR.js.map → archive-scenario-WJ2A7ETN.js.map} +0 -0
  243. /package/dist/{chunk-6U4TCGFC.js.map → chunk-2GT7IFWK.js.map} +0 -0
  244. /package/dist/{chunk-IX6QJKAD.js.map → chunk-CCWCIJJ3.js.map} +0 -0
  245. /package/dist/{config-FIQWQRUB.js.map → config.js.map} +0 -0
  246. /package/dist/{create-evaluator-E5X5ZP3B.js.map → create-evaluator-WYFRFLRC.js.map} +0 -0
  247. /package/dist/{create-scenario-DIMPJRPY.js.map → create-scenario-6YHSMGLP.js.map} +0 -0
  248. /package/dist/{get-evaluator-WDEH2F7M.js.map → get-evaluator-UWVUPYTM.js.map} +0 -0
  249. /package/dist/{get-scenario-H24ZYNT5.js.map → get-scenario-Q6LCNUYN.js.map} +0 -0
  250. /package/dist/{get-trace-27USKGO7.js.map → get-trace-4EJAYVBG.js.map} +0 -0
  251. /package/dist/{list-evaluators-KRGI72EH.js.map → list-evaluators-7R2LB4JH.js.map} +0 -0
  252. /package/dist/{list-model-providers-A5YCFTPI.js.map → list-model-providers-OBYJCKBX.js.map} +0 -0
  253. /package/dist/{list-prompts-LKJSE7XN.js.map → list-prompts-VWEZ4CDE.js.map} +0 -0
  254. /package/dist/{list-scenarios-ZK5CMGC4.js.map → list-scenarios-TXD4W2ZJ.js.map} +0 -0
  255. /package/dist/{search-traces-SOKAAMAR.js.map → search-traces-VYFUIMXH.js.map} +0 -0
  256. /package/dist/{set-model-provider-7MGULZDH.js.map → set-model-provider-W5IUI326.js.map} +0 -0
  257. /package/dist/{update-evaluator-A3XINFLJ.js.map → update-evaluator-IVOR6FTR.js.map} +0 -0
  258. /package/dist/{update-scenario-ZT7TOBFR.js.map → update-scenario-GDFVILPN.js.map} +0 -0
@@ -0,0 +1,397 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { initConfig } from "../config.js";
3
+ import type { Server } from "http";
4
+
5
+ /** Standard headers required by the MCP Streamable HTTP protocol for POST requests */
6
+ const MCP_POST_HEADERS = {
7
+ "Content-Type": "application/json",
8
+ Accept: "application/json, text/event-stream",
9
+ };
10
+
11
+ const BEARER_TOKEN = "test-session-key";
12
+
13
+ /** Helper to create auth + MCP headers */
14
+ function mcpHeaders({
15
+ sessionId,
16
+ apiKey,
17
+ }: { sessionId?: string; apiKey?: string } = {}) {
18
+ const headers: Record<string, string> = {
19
+ ...MCP_POST_HEADERS,
20
+ Authorization: `Bearer ${apiKey ?? BEARER_TOKEN}`,
21
+ };
22
+ if (sessionId) {
23
+ headers["mcp-session-id"] = sessionId;
24
+ }
25
+ return headers;
26
+ }
27
+
28
+ function initializeBody() {
29
+ return JSON.stringify({
30
+ jsonrpc: "2.0",
31
+ method: "initialize",
32
+ params: {
33
+ protocolVersion: "2025-03-26",
34
+ capabilities: {},
35
+ clientInfo: { name: "test-client", version: "1.0.0" },
36
+ },
37
+ id: 1,
38
+ });
39
+ }
40
+
41
+ describe("HTTP transport", () => {
42
+ let httpServer: Server;
43
+ let port: number;
44
+
45
+ beforeAll(async () => {
46
+ // Initialize with no API key -- HTTP mode relies on per-session Bearer tokens
47
+ initConfig({
48
+ endpoint: "https://app.langwatch.ai",
49
+ });
50
+
51
+ const { startHttpServer } = await import("../http-server.js");
52
+ const result = await startHttpServer({ port: 0 });
53
+ httpServer = result.server;
54
+ port = result.port;
55
+ });
56
+
57
+ afterAll(async () => {
58
+ await new Promise<void>((resolve) => {
59
+ httpServer.close(() => resolve());
60
+ });
61
+ });
62
+
63
+ describe("/health endpoint", () => {
64
+ it("returns ok status without authentication", async () => {
65
+ const response = await fetch(`http://localhost:${port}/health`);
66
+ const body = await response.json();
67
+
68
+ expect(response.status).toBe(200);
69
+ expect(body).toEqual({ status: "ok" });
70
+ });
71
+ });
72
+
73
+ describe("CORS headers", () => {
74
+ it("includes Access-Control-Allow-Origin on responses", async () => {
75
+ const response = await fetch(`http://localhost:${port}/health`);
76
+
77
+ expect(response.headers.get("access-control-allow-origin")).toBe("*");
78
+ });
79
+
80
+ it("responds to OPTIONS preflight requests", async () => {
81
+ const response = await fetch(`http://localhost:${port}/mcp`, {
82
+ method: "OPTIONS",
83
+ });
84
+
85
+ expect(response.status).toBe(200);
86
+ expect(response.headers.get("access-control-allow-methods")).toContain(
87
+ "POST"
88
+ );
89
+ expect(response.headers.get("access-control-allow-headers")).toContain(
90
+ "mcp-session-id"
91
+ );
92
+ });
93
+
94
+ it("includes Authorization in allowed headers for CORS", async () => {
95
+ const response = await fetch(`http://localhost:${port}/mcp`, {
96
+ method: "OPTIONS",
97
+ });
98
+
99
+ expect(response.headers.get("access-control-allow-headers")).toContain(
100
+ "Authorization"
101
+ );
102
+ });
103
+ });
104
+
105
+ describe("/mcp endpoint (Streamable HTTP)", () => {
106
+ describe("when no Bearer token is provided", () => {
107
+ it("returns 401 on initialize request", async () => {
108
+ const response = await fetch(`http://localhost:${port}/mcp`, {
109
+ method: "POST",
110
+ headers: MCP_POST_HEADERS,
111
+ body: initializeBody(),
112
+ });
113
+
114
+ expect(response.status).toBe(401);
115
+ const body = await response.json();
116
+ expect(body.error).toContain("Authorization");
117
+ });
118
+ });
119
+
120
+ it("rejects non-initialize POST without session ID", async () => {
121
+ const response = await fetch(`http://localhost:${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
+ method: "tools/list",
130
+ id: 1,
131
+ }),
132
+ });
133
+
134
+ expect(response.status).toBe(400);
135
+ });
136
+
137
+ it("accepts initialize request with Bearer token and returns session ID", async () => {
138
+ const response = await fetch(`http://localhost:${port}/mcp`, {
139
+ method: "POST",
140
+ headers: mcpHeaders(),
141
+ body: initializeBody(),
142
+ });
143
+
144
+ expect(response.status).toBe(200);
145
+ const sessionId = response.headers.get("mcp-session-id");
146
+ expect(sessionId).toBeTruthy();
147
+ });
148
+
149
+ it("lists all tools after initialization", async () => {
150
+ const initResponse = await fetch(`http://localhost:${port}/mcp`, {
151
+ method: "POST",
152
+ headers: mcpHeaders(),
153
+ body: initializeBody(),
154
+ });
155
+
156
+ const sessionId = initResponse.headers.get("mcp-session-id")!;
157
+
158
+ // Send initialized notification
159
+ await fetch(`http://localhost:${port}/mcp`, {
160
+ method: "POST",
161
+ headers: mcpHeaders({ sessionId }),
162
+ body: JSON.stringify({
163
+ jsonrpc: "2.0",
164
+ method: "notifications/initialized",
165
+ }),
166
+ });
167
+
168
+ // List tools
169
+ const toolsResponse = await fetch(`http://localhost:${port}/mcp`, {
170
+ method: "POST",
171
+ headers: mcpHeaders({ sessionId }),
172
+ body: JSON.stringify({
173
+ jsonrpc: "2.0",
174
+ method: "tools/list",
175
+ id: 2,
176
+ }),
177
+ });
178
+
179
+ expect(toolsResponse.status).toBe(200);
180
+ const text = await toolsResponse.text();
181
+ expect(text).toContain("fetch_langwatch_docs");
182
+ expect(text).toContain("discover_schema");
183
+ expect(text).toContain("search_traces");
184
+ expect(text).toContain("platform_create_prompt");
185
+ });
186
+ });
187
+
188
+ describe("DELETE /mcp", () => {
189
+ it("closes an existing session", async () => {
190
+ const initResponse = await fetch(`http://localhost:${port}/mcp`, {
191
+ method: "POST",
192
+ headers: mcpHeaders(),
193
+ body: initializeBody(),
194
+ });
195
+
196
+ const sessionId = initResponse.headers.get("mcp-session-id")!;
197
+
198
+ const deleteResponse = await fetch(`http://localhost:${port}/mcp`, {
199
+ method: "DELETE",
200
+ headers: { "mcp-session-id": sessionId },
201
+ });
202
+
203
+ expect(deleteResponse.status).toBe(200);
204
+ });
205
+
206
+ it("returns 404 for unknown session", async () => {
207
+ const response = await fetch(`http://localhost:${port}/mcp`, {
208
+ method: "DELETE",
209
+ headers: { "mcp-session-id": "nonexistent-session-id" },
210
+ });
211
+
212
+ expect(response.status).toBe(404);
213
+ });
214
+ });
215
+
216
+ describe("OAuth 2.0 endpoints", () => {
217
+ describe("/.well-known/oauth-authorization-server", () => {
218
+ it("returns OAuth metadata with token endpoint", async () => {
219
+ const response = await fetch(
220
+ `http://localhost:${port}/.well-known/oauth-authorization-server`
221
+ );
222
+ const body = await response.json();
223
+
224
+ expect(response.status).toBe(200);
225
+ expect(body.token_endpoint).toContain("/oauth/token");
226
+ expect(body.grant_types_supported).toContain("client_credentials");
227
+ });
228
+ });
229
+
230
+ describe("/oauth/token", () => {
231
+ it("returns 400 for unsupported grant type", async () => {
232
+ const response = await fetch(
233
+ `http://localhost:${port}/oauth/token`,
234
+ {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
237
+ body: "grant_type=authorization_code&client_secret=test-key",
238
+ }
239
+ );
240
+ const body = await response.json();
241
+
242
+ expect(response.status).toBe(400);
243
+ expect(body.error).toBe("unsupported_grant_type");
244
+ });
245
+
246
+ it("returns 400 when client_secret is missing", async () => {
247
+ const response = await fetch(
248
+ `http://localhost:${port}/oauth/token`,
249
+ {
250
+ method: "POST",
251
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
252
+ body: "grant_type=client_credentials",
253
+ }
254
+ );
255
+ const body = await response.json();
256
+
257
+ expect(response.status).toBe(400);
258
+ expect(body.error).toBe("invalid_request");
259
+ });
260
+
261
+ it("issues an access token for valid client credentials", async () => {
262
+ const response = await fetch(
263
+ `http://localhost:${port}/oauth/token`,
264
+ {
265
+ method: "POST",
266
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
267
+ body: "grant_type=client_credentials&client_secret=my-langwatch-api-key",
268
+ }
269
+ );
270
+ const body = await response.json();
271
+
272
+ expect(response.status).toBe(200);
273
+ expect(body.access_token).toBeTruthy();
274
+ expect(body.token_type).toBe("Bearer");
275
+ expect(body.expires_in).toBe(3600);
276
+ });
277
+
278
+ it("issued token works for MCP initialize request", async () => {
279
+ // Get OAuth token
280
+ const tokenResponse = await fetch(
281
+ `http://localhost:${port}/oauth/token`,
282
+ {
283
+ method: "POST",
284
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
285
+ body: `grant_type=client_credentials&client_secret=${BEARER_TOKEN}`,
286
+ }
287
+ );
288
+ expect(tokenResponse.status).toBe(200);
289
+ const { access_token } = await tokenResponse.json();
290
+ expect(access_token).toBeTruthy();
291
+
292
+ // Use OAuth token for MCP initialize
293
+ const response = await fetch(`http://localhost:${port}/mcp`, {
294
+ method: "POST",
295
+ headers: {
296
+ ...MCP_POST_HEADERS,
297
+ Authorization: `Bearer ${access_token}`,
298
+ },
299
+ body: initializeBody(),
300
+ });
301
+
302
+ expect(response.status).toBe(200);
303
+ expect(response.headers.get("mcp-session-id")).toBeTruthy();
304
+ });
305
+
306
+ it("issued token works for SSE connection", async () => {
307
+ const controller = new AbortController();
308
+
309
+ // Get OAuth token
310
+ const tokenResponse = await fetch(
311
+ `http://localhost:${port}/oauth/token`,
312
+ {
313
+ method: "POST",
314
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
315
+ body: `grant_type=client_credentials&client_secret=${BEARER_TOKEN}`,
316
+ }
317
+ );
318
+ expect(tokenResponse.status).toBe(200);
319
+ const { access_token } = await tokenResponse.json();
320
+ expect(access_token).toBeTruthy();
321
+
322
+ // Use OAuth token for SSE
323
+ const response = await fetch(`http://localhost:${port}/sse`, {
324
+ signal: controller.signal,
325
+ headers: { Authorization: `Bearer ${access_token}` },
326
+ });
327
+
328
+ expect(response.status).toBe(200);
329
+ expect(response.headers.get("content-type")).toContain(
330
+ "text/event-stream"
331
+ );
332
+
333
+ controller.abort();
334
+ });
335
+ });
336
+ });
337
+
338
+ describe("/sse endpoint (legacy SSE)", () => {
339
+ it("returns 401 without authorization", async () => {
340
+ const controller = new AbortController();
341
+
342
+ const response = await fetch(`http://localhost:${port}/sse`, {
343
+ signal: controller.signal,
344
+ });
345
+
346
+ expect(response.status).toBe(401);
347
+ controller.abort();
348
+ });
349
+
350
+ it("establishes SSE connection with Bearer token in header", async () => {
351
+ const controller = new AbortController();
352
+
353
+ const response = await fetch(`http://localhost:${port}/sse`, {
354
+ signal: controller.signal,
355
+ headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
356
+ });
357
+
358
+ expect(response.status).toBe(200);
359
+ expect(response.headers.get("content-type")).toContain(
360
+ "text/event-stream"
361
+ );
362
+
363
+ const reader = response.body!.getReader();
364
+ const decoder = new TextDecoder();
365
+ const { value } = await reader.read();
366
+ const text = decoder.decode(value);
367
+
368
+ expect(text).toContain("event: endpoint");
369
+ expect(text).toContain("/messages?sessionId=");
370
+
371
+ controller.abort();
372
+ });
373
+
374
+ it("establishes SSE connection with apiKey query parameter", async () => {
375
+ const controller = new AbortController();
376
+
377
+ const response = await fetch(
378
+ `http://localhost:${port}/sse?apiKey=${BEARER_TOKEN}`,
379
+ { signal: controller.signal }
380
+ );
381
+
382
+ expect(response.status).toBe(200);
383
+ expect(response.headers.get("content-type")).toContain(
384
+ "text/event-stream"
385
+ );
386
+
387
+ const reader = response.body!.getReader();
388
+ const decoder = new TextDecoder();
389
+ const { value } = await reader.read();
390
+ const text = decoder.decode(value);
391
+
392
+ expect(text).toContain("event: endpoint");
393
+
394
+ controller.abort();
395
+ });
396
+ });
397
+ });
@@ -34,6 +34,15 @@ describe("langwatch-api", () => {
34
34
  });
35
35
  }
36
36
 
37
+ function mock204Response() {
38
+ mockFetch.mockResolvedValueOnce({
39
+ ok: true,
40
+ status: 204,
41
+ text: async () => "",
42
+ json: async () => { throw new Error("No content"); },
43
+ });
44
+ }
45
+
37
46
  describe("searchTraces()", () => {
38
47
  it("sends POST to /api/traces/search with format digest by default", async () => {
39
48
  const { searchTraces } = await import("../langwatch-api.js");
@@ -259,6 +268,171 @@ describe("langwatch-api", () => {
259
268
  });
260
269
  });
261
270
 
271
+ describe("getPrompt() with tag options", () => {
272
+ describe("when called with tag option", () => {
273
+ it("appends tag query parameter", async () => {
274
+ const { getPrompt } = await import("../langwatch-api.js");
275
+ mockJsonResponse({ id: "1" });
276
+
277
+ await getPrompt("pizza-prompt", { tag: "production" });
278
+
279
+ expect(mockFetch).toHaveBeenCalledWith(
280
+ `${TEST_ENDPOINT}/api/prompts/${encodeURIComponent("pizza-prompt")}?tag=production`,
281
+ expect.objectContaining({ method: "GET" })
282
+ );
283
+ });
284
+ });
285
+
286
+ describe("when called with version option", () => {
287
+ it("appends version query parameter", async () => {
288
+ const { getPrompt } = await import("../langwatch-api.js");
289
+ mockJsonResponse({ id: "1" });
290
+
291
+ await getPrompt("pizza-prompt", { version: 2 });
292
+
293
+ expect(mockFetch).toHaveBeenCalledWith(
294
+ `${TEST_ENDPOINT}/api/prompts/${encodeURIComponent("pizza-prompt")}?version=2`,
295
+ expect.objectContaining({ method: "GET" })
296
+ );
297
+ });
298
+ });
299
+
300
+ describe("when called with no options", () => {
301
+ it("sends no query string", async () => {
302
+ const { getPrompt } = await import("../langwatch-api.js");
303
+ mockJsonResponse({ id: "1" });
304
+
305
+ await getPrompt("pizza-prompt");
306
+
307
+ expect(mockFetch).toHaveBeenCalledWith(
308
+ `${TEST_ENDPOINT}/api/prompts/${encodeURIComponent("pizza-prompt")}`,
309
+ expect.objectContaining({ method: "GET" })
310
+ );
311
+ });
312
+ });
313
+ });
314
+
315
+ describe("createPrompt() with tags", () => {
316
+ describe("when called with tags", () => {
317
+ it("includes tags in the request body", async () => {
318
+ const { createPrompt } = await import("../langwatch-api.js");
319
+ const data = {
320
+ handle: "test",
321
+ messages: [{ role: "system", content: "hi" }],
322
+ model: "openai/gpt-5-mini",
323
+ tags: ["production", "staging"],
324
+ };
325
+ mockJsonResponse({ id: "new" });
326
+
327
+ await createPrompt(data);
328
+
329
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]!.body as string);
330
+ expect(body.tags).toEqual(["production", "staging"]);
331
+ });
332
+ });
333
+ });
334
+
335
+ describe("updatePrompt() with tags", () => {
336
+ describe("when called with tags", () => {
337
+ it("includes tags in the request body", async () => {
338
+ const { updatePrompt } = await import("../langwatch-api.js");
339
+ mockJsonResponse({ id: "p1" });
340
+
341
+ await updatePrompt("p1", {
342
+ commitMessage: "tag it",
343
+ tags: ["production"],
344
+ });
345
+
346
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]!.body as string);
347
+ expect(body.tags).toEqual(["production"]);
348
+ });
349
+ });
350
+ });
351
+
352
+ describe("assignPromptTag()", () => {
353
+ it("sends PUT to /api/prompts/{id}/tags/{tag} with versionId", async () => {
354
+ const { assignPromptTag } = await import("../langwatch-api.js");
355
+ mockJsonResponse({ success: true });
356
+
357
+ await assignPromptTag({ idOrHandle: "pizza-prompt", tag: "production", versionId: "v123" });
358
+
359
+ const [url, opts] = mockFetch.mock.calls[0]!;
360
+ expect(url).toBe(`${TEST_ENDPOINT}/api/prompts/pizza-prompt/tags/production`);
361
+ expect(opts.method).toBe("PUT");
362
+ expect(JSON.parse(opts.body as string)).toEqual({ versionId: "v123" });
363
+ });
364
+ });
365
+
366
+ describe("listPromptTags()", () => {
367
+ it("sends GET to /api/prompts/tags", async () => {
368
+ const { listPromptTags } = await import("../langwatch-api.js");
369
+ const tags = [{ id: "1", name: "production" }];
370
+ mockJsonResponse(tags);
371
+
372
+ const result = await listPromptTags();
373
+
374
+ expect(mockFetch).toHaveBeenCalledWith(
375
+ `${TEST_ENDPOINT}/api/prompts/tags`,
376
+ expect.objectContaining({ method: "GET" })
377
+ );
378
+ expect(result).toEqual(tags);
379
+ });
380
+ });
381
+
382
+ describe("createPromptTag()", () => {
383
+ it("sends POST to /api/prompts/tags with name", async () => {
384
+ const { createPromptTag } = await import("../langwatch-api.js");
385
+ mockJsonResponse({ id: "t1", name: "canary" });
386
+
387
+ await createPromptTag("canary");
388
+
389
+ const [url, opts] = mockFetch.mock.calls[0]!;
390
+ expect(url).toBe(`${TEST_ENDPOINT}/api/prompts/tags`);
391
+ expect(opts.method).toBe("POST");
392
+ expect(JSON.parse(opts.body as string)).toEqual({ name: "canary" });
393
+ });
394
+ });
395
+
396
+ describe("renamePromptTag()", () => {
397
+ it("sends PUT to /api/prompts/tags/{tag} with new name", async () => {
398
+ const { renamePromptTag } = await import("../langwatch-api.js");
399
+ mockJsonResponse({ id: "t1", name: "preview" });
400
+
401
+ await renamePromptTag({ tag: "canary", name: "preview" });
402
+
403
+ const [url, opts] = mockFetch.mock.calls[0]!;
404
+ expect(url).toBe(`${TEST_ENDPOINT}/api/prompts/tags/canary`);
405
+ expect(opts.method).toBe("PUT");
406
+ expect(JSON.parse(opts.body as string)).toEqual({ name: "preview" });
407
+ });
408
+ });
409
+
410
+ describe("deletePromptTag()", () => {
411
+ it("sends DELETE to /api/prompts/tags/{tag}", async () => {
412
+ const { deletePromptTag } = await import("../langwatch-api.js");
413
+ mock204Response();
414
+
415
+ const result = await deletePromptTag("canary");
416
+
417
+ const [url, opts] = mockFetch.mock.calls[0]!;
418
+ expect(url).toBe(`${TEST_ENDPOINT}/api/prompts/tags/canary`);
419
+ expect(opts.method).toBe("DELETE");
420
+ expect(result).toBeNull();
421
+ });
422
+ });
423
+
424
+ describe("makeRequest()", () => {
425
+ describe("when response is 204 No Content", () => {
426
+ it("returns null without calling json()", async () => {
427
+ const { listPromptTags } = await import("../langwatch-api.js");
428
+ mock204Response();
429
+
430
+ const result = await listPromptTags();
431
+ expect(result).toBeNull();
432
+ });
433
+ });
434
+ });
435
+
262
436
  describe("when the API returns an error", () => {
263
437
  it("includes the status code in the error message", async () => {
264
438
  const { searchTraces } = await import("../langwatch-api.js");