@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.
- package/CHANGELOG.md +30 -0
- package/Dockerfile +22 -0
- package/README.md +51 -8
- package/dist/{archive-scenario-YFD5THOR.js → archive-scenario-WJ2A7ETN.js} +4 -4
- package/dist/archive-suite-4JOM4SSL.js +15 -0
- package/dist/archive-suite-4JOM4SSL.js.map +1 -0
- package/dist/assign-prompt-tag-GL7EAAF3.js +19 -0
- package/dist/assign-prompt-tag-GL7EAAF3.js.map +1 -0
- package/dist/{chunk-6U4TCGFC.js → chunk-2GT7IFWK.js} +2 -2
- package/dist/{chunk-LLRQIF52.js → chunk-2WPLXEKF.js} +49 -5
- package/dist/chunk-2WPLXEKF.js.map +1 -0
- package/dist/chunk-4AUB2BIA.js +71 -0
- package/dist/chunk-4AUB2BIA.js.map +1 -0
- package/dist/chunk-BWWY5KEF.js +71 -0
- package/dist/chunk-BWWY5KEF.js.map +1 -0
- package/dist/{chunk-IX6QJKAD.js → chunk-CCWCIJJ3.js} +2 -2
- package/dist/chunk-E5JYYU3J.js +1567 -0
- package/dist/chunk-E5JYYU3J.js.map +1 -0
- package/dist/chunk-EGH5RPXM.js +25 -0
- package/dist/chunk-EGH5RPXM.js.map +1 -0
- package/dist/chunk-EUQDVOSU.js +39 -0
- package/dist/chunk-EUQDVOSU.js.map +1 -0
- package/dist/{chunk-5UOPNRXW.js → chunk-HYMIWO3U.js} +10 -3
- package/dist/{chunk-5UOPNRXW.js.map → chunk-HYMIWO3U.js.map} +1 -1
- package/dist/chunk-IE4M5OGE.js +42 -0
- package/dist/chunk-IE4M5OGE.js.map +1 -0
- package/dist/chunk-LUMEZEHL.js +48 -0
- package/dist/chunk-LUMEZEHL.js.map +1 -0
- package/dist/chunk-NUFPTHKU.js +9 -0
- package/dist/chunk-NUFPTHKU.js.map +1 -0
- package/dist/chunk-WQG2DFHX.js +86 -0
- package/dist/chunk-WQG2DFHX.js.map +1 -0
- package/dist/chunk-WVI5CJ6S.js +21 -0
- package/dist/chunk-WVI5CJ6S.js.map +1 -0
- package/dist/chunk-ZVS4UKN2.js +35 -0
- package/dist/chunk-ZVS4UKN2.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +13 -0
- package/dist/create-agent-MBOOT3XK.js +23 -0
- package/dist/create-agent-MBOOT3XK.js.map +1 -0
- package/dist/create-annotation-XMV6A7ED.js +24 -0
- package/dist/create-annotation-XMV6A7ED.js.map +1 -0
- package/dist/create-dashboard-VRKLFVZJ.js +18 -0
- package/dist/create-dashboard-VRKLFVZJ.js.map +1 -0
- package/dist/create-dataset-7HQPZCRE.js +22 -0
- package/dist/create-dataset-7HQPZCRE.js.map +1 -0
- package/dist/create-dataset-records-2NIVFYTV.js +22 -0
- package/dist/create-dataset-records-2NIVFYTV.js.map +1 -0
- package/dist/{create-evaluator-E5X5ZP3B.js → create-evaluator-WYFRFLRC.js} +4 -4
- package/dist/create-mcp-server.d.ts +8 -0
- package/dist/create-mcp-server.js +8 -0
- package/dist/create-mcp-server.js.map +1 -0
- package/dist/{create-prompt-7Z35MIL6.js → create-prompt-IEN7O6JT.js} +7 -4
- package/dist/create-prompt-IEN7O6JT.js.map +1 -0
- package/dist/create-prompt-tag-CAEZ45VO.js +17 -0
- package/dist/create-prompt-tag-CAEZ45VO.js.map +1 -0
- package/dist/{create-scenario-DIMPJRPY.js → create-scenario-6YHSMGLP.js} +4 -4
- package/dist/create-suite-H5XOFDE5.js +38 -0
- package/dist/create-suite-H5XOFDE5.js.map +1 -0
- package/dist/delete-agent-JKTNBWAA.js +18 -0
- package/dist/delete-agent-JKTNBWAA.js.map +1 -0
- package/dist/delete-annotation-ACMDV6QO.js +17 -0
- package/dist/delete-annotation-ACMDV6QO.js.map +1 -0
- package/dist/delete-dashboard-VJSR7CRU.js +18 -0
- package/dist/delete-dashboard-VJSR7CRU.js.map +1 -0
- package/dist/delete-dataset-RTTTWP4D.js +20 -0
- package/dist/delete-dataset-RTTTWP4D.js.map +1 -0
- package/dist/delete-dataset-records-KNQTIPQK.js +21 -0
- package/dist/delete-dataset-records-KNQTIPQK.js.map +1 -0
- package/dist/delete-evaluator-7FVC5WM4.js +15 -0
- package/dist/delete-evaluator-7FVC5WM4.js.map +1 -0
- package/dist/delete-prompt-tag-ZMZUAGFW.js +17 -0
- package/dist/delete-prompt-tag-ZMZUAGFW.js.map +1 -0
- package/dist/delete-workflow-EYQ3T4RQ.js +17 -0
- package/dist/delete-workflow-EYQ3T4RQ.js.map +1 -0
- package/dist/get-agent-E3QSTVKG.js +28 -0
- package/dist/get-agent-E3QSTVKG.js.map +1 -0
- package/dist/{get-analytics-4YJW4S5L.js → get-analytics-2OHV763I.js} +22 -5
- package/dist/get-analytics-2OHV763I.js.map +1 -0
- package/dist/get-dashboard-PC7JDHH6.js +29 -0
- package/dist/get-dashboard-PC7JDHH6.js.map +1 -0
- package/dist/get-dataset-H5IK5FVU.js +48 -0
- package/dist/get-dataset-H5IK5FVU.js.map +1 -0
- package/dist/{get-evaluator-WDEH2F7M.js → get-evaluator-UWVUPYTM.js} +4 -4
- package/dist/{get-prompt-F6PDVC76.js → get-prompt-WZUOLLQC.js} +4 -4
- package/dist/get-prompt-WZUOLLQC.js.map +1 -0
- package/dist/{get-scenario-H24ZYNT5.js → get-scenario-Q6LCNUYN.js} +4 -4
- package/dist/get-simulation-run-Z2XKO256.js +56 -0
- package/dist/get-simulation-run-Z2XKO256.js.map +1 -0
- package/dist/get-suite-BXK5TN7P.js +44 -0
- package/dist/get-suite-BXK5TN7P.js.map +1 -0
- package/dist/{get-trace-27USKGO7.js → get-trace-4EJAYVBG.js} +3 -3
- package/dist/get-workflow-O34QJLHD.js +25 -0
- package/dist/get-workflow-O34QJLHD.js.map +1 -0
- package/dist/http-server-ISHF7NJX.js +224 -0
- package/dist/http-server-ISHF7NJX.js.map +1 -0
- package/dist/index.js +61109 -31519
- package/dist/index.js.map +1 -1
- package/dist/langwatch-api-annotations-ERBG27RF.js +17 -0
- package/dist/langwatch-api-annotations-ERBG27RF.js.map +1 -0
- package/dist/langwatch-api-dashboards-G6TR7VVO.js +17 -0
- package/dist/langwatch-api-dashboards-G6TR7VVO.js.map +1 -0
- package/dist/langwatch-api-monitors-WSXZLS2B.js +52 -0
- package/dist/langwatch-api-monitors-WSXZLS2B.js.map +1 -0
- package/dist/langwatch-api-secrets-RR5DKG2X.js +40 -0
- package/dist/langwatch-api-secrets-RR5DKG2X.js.map +1 -0
- package/dist/langwatch-api-triggers-2RTNSWQY.js +30 -0
- package/dist/langwatch-api-triggers-2RTNSWQY.js.map +1 -0
- package/dist/langwatch-api-workflows-VJRIIDZ7.js +15 -0
- package/dist/langwatch-api-workflows-VJRIIDZ7.js.map +1 -0
- package/dist/list-agents-AW3P6FJV.js +32 -0
- package/dist/list-agents-AW3P6FJV.js.map +1 -0
- package/dist/list-annotations-QNCGWPQ3.js +33 -0
- package/dist/list-annotations-QNCGWPQ3.js.map +1 -0
- package/dist/list-dashboards-R7RK2IXL.js +32 -0
- package/dist/list-dashboards-R7RK2IXL.js.map +1 -0
- package/dist/list-dataset-records-WVCSQP7P.js +39 -0
- package/dist/list-dataset-records-WVCSQP7P.js.map +1 -0
- package/dist/list-datasets-KEXRLDEU.js +60 -0
- package/dist/list-datasets-KEXRLDEU.js.map +1 -0
- package/dist/{list-evaluators-KRGI72EH.js → list-evaluators-7R2LB4JH.js} +4 -4
- package/dist/{list-model-providers-A5YCFTPI.js → list-model-providers-OBYJCKBX.js} +4 -4
- package/dist/list-prompt-tags-6LY3ZPYI.js +23 -0
- package/dist/list-prompt-tags-6LY3ZPYI.js.map +1 -0
- package/dist/{list-prompts-LKJSE7XN.js → list-prompts-VWEZ4CDE.js} +3 -3
- package/dist/{list-scenarios-ZK5CMGC4.js → list-scenarios-TXD4W2ZJ.js} +4 -4
- package/dist/list-simulation-runs-RHPOLMFI.js +43 -0
- package/dist/list-simulation-runs-RHPOLMFI.js.map +1 -0
- package/dist/list-suites-FSGBX27Z.js +42 -0
- package/dist/list-suites-FSGBX27Z.js.map +1 -0
- package/dist/list-workflows-VRGEZ3EN.js +35 -0
- package/dist/list-workflows-VRGEZ3EN.js.map +1 -0
- package/dist/rename-prompt-tag-VOJJLX4M.js +18 -0
- package/dist/rename-prompt-tag-VOJJLX4M.js.map +1 -0
- package/dist/run-agent-KTCC7TB7.js +36 -0
- package/dist/run-agent-KTCC7TB7.js.map +1 -0
- package/dist/run-evaluation-2X3V3Q6L.js +64 -0
- package/dist/run-evaluation-2X3V3Q6L.js.map +1 -0
- package/dist/run-suite-BTMHGI2D.js +29 -0
- package/dist/run-suite-BTMHGI2D.js.map +1 -0
- package/dist/run-workflow-IGAYGKJL.js +36 -0
- package/dist/run-workflow-IGAYGKJL.js.map +1 -0
- package/dist/{search-traces-SOKAAMAR.js → search-traces-VYFUIMXH.js} +3 -3
- package/dist/{set-model-provider-7MGULZDH.js → set-model-provider-W5IUI326.js} +4 -4
- package/dist/update-agent-4BB7EYLR.js +20 -0
- package/dist/update-agent-4BB7EYLR.js.map +1 -0
- package/dist/update-dataset-CXBKZ4W3.js +22 -0
- package/dist/update-dataset-CXBKZ4W3.js.map +1 -0
- package/dist/update-dataset-record-HOHAMHXG.js +19 -0
- package/dist/update-dataset-record-HOHAMHXG.js.map +1 -0
- package/dist/{update-evaluator-A3XINFLJ.js → update-evaluator-IVOR6FTR.js} +4 -4
- package/dist/{update-prompt-IW7X2UQM.js → update-prompt-OWQAKE7K.js} +5 -3
- package/dist/update-prompt-OWQAKE7K.js.map +1 -0
- package/dist/{update-scenario-ZT7TOBFR.js → update-scenario-GDFVILPN.js} +4 -4
- package/dist/update-suite-BXYUYSPT.js +39 -0
- package/dist/update-suite-BXYUYSPT.js.map +1 -0
- package/package.json +17 -3
- package/src/__tests__/all-tools.integration.test.ts +344 -0
- package/src/__tests__/config.unit.test.ts +81 -1
- package/src/__tests__/datasets.integration.test.ts +603 -0
- package/src/__tests__/datasets.unit.test.ts +240 -0
- package/src/__tests__/docker.integration.test.ts +234 -0
- package/src/__tests__/http-transport.integration.test.ts +397 -0
- package/src/__tests__/langwatch-api.unit.test.ts +174 -0
- package/src/__tests__/tools.unit.test.ts +177 -0
- package/src/config.d.ts +15 -0
- package/src/config.ts +83 -7
- package/src/create-mcp-server.d.ts +6 -0
- package/src/create-mcp-server.ts +1799 -0
- package/src/http-server.ts +329 -0
- package/src/index.ts +22 -593
- package/src/langwatch-api-agents.ts +108 -0
- package/src/langwatch-api-annotations.ts +49 -0
- package/src/langwatch-api-dashboards.ts +48 -0
- package/src/langwatch-api-datasets.ts +191 -0
- package/src/langwatch-api-evaluators.ts +10 -0
- package/src/langwatch-api-monitors.ts +82 -0
- package/src/langwatch-api-secrets.ts +48 -0
- package/src/langwatch-api-simulation-runs.ts +51 -0
- package/src/langwatch-api-suites.ts +118 -0
- package/src/langwatch-api-triggers.ts +48 -0
- package/src/langwatch-api-workflows.ts +41 -0
- package/src/langwatch-api.ts +60 -2
- package/src/schemas/create-dataset.ts +48 -0
- package/src/schemas/index.ts +1 -0
- package/src/tools/archive-suite.ts +12 -0
- package/src/tools/assign-prompt-tag.ts +16 -0
- package/src/tools/create-agent.ts +15 -0
- package/src/tools/create-annotation.ts +18 -0
- package/src/tools/create-dashboard.ts +7 -0
- package/src/tools/create-dataset-records.ts +23 -0
- package/src/tools/create-dataset.ts +22 -0
- package/src/tools/create-prompt-tag.ts +12 -0
- package/src/tools/create-prompt.ts +4 -0
- package/src/tools/create-suite.ts +42 -0
- package/src/tools/delete-agent.ts +7 -0
- package/src/tools/delete-annotation.ts +7 -0
- package/src/tools/delete-dashboard.ts +7 -0
- package/src/tools/delete-dataset-records.ts +22 -0
- package/src/tools/delete-dataset.ts +20 -0
- package/src/tools/delete-evaluator.ts +12 -0
- package/src/tools/delete-prompt-tag.ts +12 -0
- package/src/tools/delete-workflow.ts +7 -0
- package/src/tools/format-dataset-mutation.ts +25 -0
- package/src/tools/get-agent.ts +21 -0
- package/src/tools/get-analytics.ts +30 -3
- package/src/tools/get-dashboard.ts +22 -0
- package/src/tools/get-dataset.ts +63 -0
- package/src/tools/get-prompt.ts +2 -1
- package/src/tools/get-simulation-run.ts +60 -0
- package/src/tools/get-suite.ts +47 -0
- package/src/tools/get-workflow.ts +17 -0
- package/src/tools/list-agents.ts +27 -0
- package/src/tools/list-annotations.ts +33 -0
- package/src/tools/list-dashboards.ts +27 -0
- package/src/tools/list-dataset-records.ts +46 -0
- package/src/tools/list-datasets.ts +80 -0
- package/src/tools/list-prompt-tags.ts +21 -0
- package/src/tools/list-simulation-runs.ts +49 -0
- package/src/tools/list-suites.ts +43 -0
- package/src/tools/list-workflows.ts +31 -0
- package/src/tools/rename-prompt-tag.ts +14 -0
- package/src/tools/run-agent.ts +36 -0
- package/src/tools/run-evaluation.ts +87 -0
- package/src/tools/run-suite.ts +28 -0
- package/src/tools/run-workflow.ts +36 -0
- package/src/tools/update-agent.ts +13 -0
- package/src/tools/update-dataset-record.ts +21 -0
- package/src/tools/update-dataset.ts +23 -0
- package/src/tools/update-prompt.ts +3 -0
- package/src/tools/update-suite.ts +44 -0
- package/src/utils/escape-markdown.ts +12 -0
- package/tsup.config.ts +5 -1
- package/dist/chunk-AAQNA53E.js +0 -28
- package/dist/chunk-AAQNA53E.js.map +0 -1
- package/dist/chunk-LLRQIF52.js.map +0 -1
- package/dist/config-FIQWQRUB.js +0 -11
- package/dist/create-prompt-7Z35MIL6.js.map +0 -1
- package/dist/get-analytics-4YJW4S5L.js.map +0 -1
- package/dist/get-prompt-F6PDVC76.js.map +0 -1
- package/dist/update-prompt-IW7X2UQM.js.map +0 -1
- /package/dist/{archive-scenario-YFD5THOR.js.map → archive-scenario-WJ2A7ETN.js.map} +0 -0
- /package/dist/{chunk-6U4TCGFC.js.map → chunk-2GT7IFWK.js.map} +0 -0
- /package/dist/{chunk-IX6QJKAD.js.map → chunk-CCWCIJJ3.js.map} +0 -0
- /package/dist/{config-FIQWQRUB.js.map → config.js.map} +0 -0
- /package/dist/{create-evaluator-E5X5ZP3B.js.map → create-evaluator-WYFRFLRC.js.map} +0 -0
- /package/dist/{create-scenario-DIMPJRPY.js.map → create-scenario-6YHSMGLP.js.map} +0 -0
- /package/dist/{get-evaluator-WDEH2F7M.js.map → get-evaluator-UWVUPYTM.js.map} +0 -0
- /package/dist/{get-scenario-H24ZYNT5.js.map → get-scenario-Q6LCNUYN.js.map} +0 -0
- /package/dist/{get-trace-27USKGO7.js.map → get-trace-4EJAYVBG.js.map} +0 -0
- /package/dist/{list-evaluators-KRGI72EH.js.map → list-evaluators-7R2LB4JH.js.map} +0 -0
- /package/dist/{list-model-providers-A5YCFTPI.js.map → list-model-providers-OBYJCKBX.js.map} +0 -0
- /package/dist/{list-prompts-LKJSE7XN.js.map → list-prompts-VWEZ4CDE.js.map} +0 -0
- /package/dist/{list-scenarios-ZK5CMGC4.js.map → list-scenarios-TXD4W2ZJ.js.map} +0 -0
- /package/dist/{search-traces-SOKAAMAR.js.map → search-traces-VYFUIMXH.js.map} +0 -0
- /package/dist/{set-model-provider-7MGULZDH.js.map → set-model-provider-W5IUI326.js.map} +0 -0
- /package/dist/{update-evaluator-A3XINFLJ.js.map → update-evaluator-IVOR6FTR.js.map} +0 -0
- /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");
|