@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,240 @@
|
|
|
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
|
+
it("includes a column table", () => {
|
|
57
|
+
expect(result).toContain("input");
|
|
58
|
+
expect(result).toContain("output");
|
|
59
|
+
expect(result).toContain("string");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("includes record entries", () => {
|
|
63
|
+
expect(result).toContain("hello");
|
|
64
|
+
expect(result).toContain("world");
|
|
65
|
+
expect(result).toContain("foo");
|
|
66
|
+
expect(result).toContain("bar");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("includes record IDs", () => {
|
|
70
|
+
expect(result).toContain("rec-1");
|
|
71
|
+
expect(result).toContain("rec-2");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("when given a dataset with no records", () => {
|
|
76
|
+
it("indicates no records", () => {
|
|
77
|
+
const result = formatDatasetResponse({
|
|
78
|
+
...sampleDataset,
|
|
79
|
+
data: [],
|
|
80
|
+
});
|
|
81
|
+
expect(result).toContain("No records");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── handleListDatasets ──────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
describe("handleListDatasets()", () => {
|
|
89
|
+
const sampleListResponse = {
|
|
90
|
+
data: [
|
|
91
|
+
{
|
|
92
|
+
id: "ds_abc123",
|
|
93
|
+
name: "User Feedback",
|
|
94
|
+
slug: "user-feedback",
|
|
95
|
+
columnTypes: [
|
|
96
|
+
{ name: "input", type: "string" },
|
|
97
|
+
{ name: "output", type: "string" },
|
|
98
|
+
],
|
|
99
|
+
recordCount: 25,
|
|
100
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
101
|
+
updatedAt: "2025-01-02T00:00:00.000Z",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
pagination: { total: 1, page: 1, limit: 50, totalPages: 1 },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
describe("when format is digest (default)", () => {
|
|
108
|
+
it("returns markdown containing dataset names", async () => {
|
|
109
|
+
mockListDatasets.mockResolvedValue(sampleListResponse);
|
|
110
|
+
const result = await handleListDatasets();
|
|
111
|
+
expect(result).toContain("User Feedback");
|
|
112
|
+
expect(result).toContain("user-feedback");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("when format is json", () => {
|
|
117
|
+
it("returns valid parseable JSON containing all datasets and total", async () => {
|
|
118
|
+
mockListDatasets.mockResolvedValue(sampleListResponse);
|
|
119
|
+
const result = await handleListDatasets({ format: "json" });
|
|
120
|
+
const parsed = JSON.parse(result);
|
|
121
|
+
expect(parsed.data).toEqual(sampleListResponse.data);
|
|
122
|
+
expect(parsed.total).toBe(sampleListResponse.pagination.total);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("when no datasets exist", () => {
|
|
127
|
+
it("returns a no-datasets message in digest mode", async () => {
|
|
128
|
+
mockListDatasets.mockResolvedValue({ data: [], pagination: { total: 0, page: 1, limit: 50, totalPages: 0 } });
|
|
129
|
+
const result = await handleListDatasets();
|
|
130
|
+
expect(result).toContain("No datasets found");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ── handleGetDataset ────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe("handleGetDataset()", () => {
|
|
138
|
+
const sampleDataset = {
|
|
139
|
+
id: "ds_abc123",
|
|
140
|
+
name: "My Dataset",
|
|
141
|
+
slug: "my-dataset",
|
|
142
|
+
columnTypes: [
|
|
143
|
+
{ name: "input", type: "string" },
|
|
144
|
+
{ name: "output", type: "string" },
|
|
145
|
+
],
|
|
146
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
147
|
+
updatedAt: "2025-01-02T00:00:00.000Z",
|
|
148
|
+
data: [
|
|
149
|
+
{ id: "rec-1", entry: { input: "hello", output: "world" } },
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
describe("when format is digest (default)", () => {
|
|
154
|
+
it("returns markdown containing dataset name", async () => {
|
|
155
|
+
mockGetDataset.mockResolvedValue(sampleDataset);
|
|
156
|
+
const result = await handleGetDataset({ slugOrId: "my-dataset" });
|
|
157
|
+
expect(result).toContain("# Dataset: My Dataset");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("when format is json", () => {
|
|
162
|
+
it("returns valid parseable JSON matching the dataset structure", async () => {
|
|
163
|
+
mockGetDataset.mockResolvedValue(sampleDataset);
|
|
164
|
+
const result = await handleGetDataset({ slugOrId: "my-dataset", format: "json" });
|
|
165
|
+
expect(JSON.parse(result)).toEqual(sampleDataset);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── platform_create_dataset schema validation ────────────────────
|
|
171
|
+
|
|
172
|
+
describe("platform_create_dataset schema", () => {
|
|
173
|
+
describe("when input has no name", () => {
|
|
174
|
+
it("rejects the input with a validation error", () => {
|
|
175
|
+
const result = createDatasetSchema.safeParse({});
|
|
176
|
+
expect(result.success).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("when input has an empty name", () => {
|
|
181
|
+
it("rejects the input with a validation error", () => {
|
|
182
|
+
const result = createDatasetSchema.safeParse({ name: "" });
|
|
183
|
+
expect(result.success).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("when input has a valid name", () => {
|
|
188
|
+
it("accepts the input", () => {
|
|
189
|
+
const result = createDatasetSchema.safeParse({ name: "Test" });
|
|
190
|
+
expect(result.success).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── Tool Registration ────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
describe("MCP server dataset tool registration", () => {
|
|
198
|
+
describe("when the MCP server is created", () => {
|
|
199
|
+
it("registers all 8 dataset tools", async () => {
|
|
200
|
+
const { createMcpServer } = await import("../create-mcp-server.js");
|
|
201
|
+
const server = createMcpServer();
|
|
202
|
+
// Access registered tools via the internal _registeredTools object
|
|
203
|
+
const registeredTools = (server as unknown as { _registeredTools: Record<string, unknown> })._registeredTools;
|
|
204
|
+
const toolNames = Object.keys(registeredTools);
|
|
205
|
+
|
|
206
|
+
expect(toolNames).toContain("platform_list_datasets");
|
|
207
|
+
expect(toolNames).toContain("platform_get_dataset");
|
|
208
|
+
expect(toolNames).toContain("platform_create_dataset");
|
|
209
|
+
expect(toolNames).toContain("platform_update_dataset");
|
|
210
|
+
expect(toolNames).toContain("platform_delete_dataset");
|
|
211
|
+
expect(toolNames).toContain("platform_create_dataset_records");
|
|
212
|
+
expect(toolNames).toContain("platform_update_dataset_record");
|
|
213
|
+
expect(toolNames).toContain("platform_delete_dataset_records");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── API Key Requirement ──────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
describe("dataset tools API key requirement", () => {
|
|
221
|
+
describe("when no API key is configured", () => {
|
|
222
|
+
it("requireApiKey throws when apiKey is empty", async () => {
|
|
223
|
+
const savedKey = process.env.LANGWATCH_API_KEY;
|
|
224
|
+
delete process.env.LANGWATCH_API_KEY;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const { initConfig, requireApiKey } = await import("../config.js");
|
|
228
|
+
initConfig({ apiKey: "", endpoint: "http://localhost:0" });
|
|
229
|
+
|
|
230
|
+
expect(() => requireApiKey()).toThrow(
|
|
231
|
+
"LANGWATCH_API_KEY is required",
|
|
232
|
+
);
|
|
233
|
+
} finally {
|
|
234
|
+
if (savedKey !== undefined) {
|
|
235
|
+
process.env.LANGWATCH_API_KEY = savedKey;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -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
|
+
});
|