@langwatch/mcp-server 0.10.1 → 2.0.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/.python-version +1 -0
- package/.release-please-shim +1 -1
- package/CHANGELOG.md +75 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +64 -18
- package/dist/{archive-project-KAZFHR4A.js → archive-project-4EQGLBJJ.js} +5 -4
- package/dist/{archive-scenario-6M2IB7Y2.js → archive-scenario-W6BXYDZ3.js} +5 -4
- package/dist/{archive-suite-NXZ3YDS3.js → archive-suite-4T6BSRS7.js} +5 -4
- package/dist/{assign-prompt-tag-WPUJO55U.js → assign-prompt-tag-VLXUWEKU.js} +4 -3
- package/dist/chunk-37SMPMQS.js +12 -0
- package/dist/{chunk-HRFQAFPR.js → chunk-4QC7SA2L.js} +221 -134
- package/dist/{chunk-XRQNXN2Z.js → chunk-5CCZFUXS.js} +2 -2
- package/dist/{chunk-ITN2QGHF.js → chunk-77BNWQ3U.js} +2 -2
- package/dist/{chunk-B6UV3JVI.js → chunk-7RHG56OH.js} +2 -2
- package/dist/{chunk-CAY57CKF.js → chunk-A2LEVYQ5.js} +78 -5
- package/dist/{chunk-U2QIW2SW.js → chunk-AQJO2EI5.js} +2 -2
- package/dist/{chunk-GWEW5XON.js → chunk-CDYQOY5U.js} +2 -2
- package/dist/{chunk-LTEJDL4V.js → chunk-J42FWRJ2.js} +2 -2
- package/dist/chunk-SAS5OB7F.js +58 -0
- package/dist/{chunk-AGGE6NPR.js → chunk-SFB4MU6H.js} +2 -2
- package/dist/{chunk-COCNCXVJ.js → chunk-TJCXFNDT.js} +2 -2
- package/dist/chunk-TOXJQDIZ.js +432 -0
- package/dist/chunk-UJPXFA26.js +267 -0
- package/dist/{chunk-KOO5733S.js → chunk-WT4PO4A3.js} +8 -2
- package/dist/{chunk-RDUXCCFR.js → chunk-YX3A3CFM.js} +2 -2
- package/dist/{chunk-UKRCZJJC.js → chunk-YZLYHLLI.js} +2 -2
- package/dist/{chunk-ESGZVPPC.js → chunk-ZWIL4EFL.js} +2 -2
- package/dist/config.js +2 -1
- package/dist/{create-agent-BI7UTJ6A.js → create-agent-7XHO6UIS.js} +5 -4
- package/dist/{create-annotation-SNM2J6S4.js → create-annotation-YQXRQC6A.js} +5 -4
- package/dist/{create-api-key-23MXYC2L.js → create-api-key-YUS36KBU.js} +5 -4
- package/dist/{create-dashboard-CPOOOIKK.js → create-dashboard-4Z5JO5LB.js} +5 -4
- package/dist/{create-dataset-BVXPVVY2.js → create-dataset-E6NMSVX7.js} +5 -4
- package/dist/{create-dataset-records-S7VFIIMR.js → create-dataset-records-SKHPRD2K.js} +5 -4
- package/dist/{create-evaluator-WJAAQOUE.js → create-evaluator-JXF5PF33.js} +5 -4
- package/dist/create-mcp-server.js +4 -3
- package/dist/{create-project-BF7LMZH5.js → create-project-CS2CB57Y.js} +5 -4
- package/dist/{create-prompt-LZ3QHSU4.js → create-prompt-F5RMX3L3.js} +4 -3
- package/dist/{create-prompt-tag-IF4J24QM.js → create-prompt-tag-GMRWTEFR.js} +4 -3
- package/dist/{create-scenario-BK4E5DEU.js → create-scenario-JDR2VB2H.js} +5 -4
- package/dist/{create-suite-HEQPNLND.js → create-suite-LZS6QIAF.js} +5 -4
- package/dist/{delete-agent-Y4T2G5PT.js → delete-agent-O3EEH4S5.js} +5 -4
- package/dist/{delete-annotation-KYDMQTZG.js → delete-annotation-XP6FQM7A.js} +5 -4
- package/dist/{delete-dashboard-3RW647WJ.js → delete-dashboard-RFUNYCNP.js} +5 -4
- package/dist/{delete-dataset-SZAHFHHS.js → delete-dataset-3MJFRDLP.js} +5 -4
- package/dist/{delete-dataset-records-ISHKGEBH.js → delete-dataset-records-WPMU5GL5.js} +5 -4
- package/dist/{delete-evaluator-VKIG5I3S.js → delete-evaluator-33SP2JFX.js} +5 -4
- package/dist/{delete-prompt-tag-VYYL4GTF.js → delete-prompt-tag-KNMZVSPB.js} +4 -3
- package/dist/{delete-workflow-SHNPYSWQ.js → delete-workflow-3NUXO46U.js} +5 -4
- package/dist/discover-evaluator-schema-XTAD4X7D.js +2191 -0
- package/dist/{discover-scenario-schema-MEEEVND7.js → discover-scenario-schema-FE64QK2D.js} +3 -1
- package/dist/{discover-schema-3T52ORPB.js → discover-schema-L6QDPON3.js} +3 -1
- package/dist/{get-agent-IZSP7AR5.js → get-agent-Y7MWZ65F.js} +5 -4
- package/dist/{get-analytics-EDFAYYIQ.js → get-analytics-TETCSOK6.js} +4 -3
- package/dist/{get-dashboard-WSAO6IQ4.js → get-dashboard-KHBEGZGL.js} +5 -4
- package/dist/{get-dataset-BEVNXQNF.js → get-dataset-RCMU5VAH.js} +5 -4
- package/dist/{get-evaluator-HCVXNPZR.js → get-evaluator-ADMKVWYZ.js} +5 -4
- package/dist/{get-project-H4V3GYRQ.js → get-project-RAK7URQJ.js} +5 -4
- package/dist/{get-prompt-EWRJVVGB.js → get-prompt-OY5B7TRI.js} +4 -3
- package/dist/{get-scenario-24YUJS5W.js → get-scenario-ZTAJJL5T.js} +5 -4
- package/dist/{get-simulation-run-UX2W6F6I.js → get-simulation-run-T2ZQWYH2.js} +5 -4
- package/dist/{get-suite-B65ZKZJA.js → get-suite-J7JVWO6O.js} +5 -4
- package/dist/{get-trace-ZR7MIITH.js → get-trace-K7U234Q4.js} +8 -10
- package/dist/{get-workflow-YROHUQJW.js → get-workflow-L5L2PSK3.js} +5 -4
- package/dist/http-security-EU3CHTY7.js +26 -0
- package/dist/http-server-YXHFPPGA.js +24706 -0
- package/dist/index.js +23280 -3135
- package/dist/{langwatch-api-annotations-OPEXNTPF.js → langwatch-api-annotations-FDAS6NQ7.js} +5 -4
- package/dist/{langwatch-api-dashboards-NPQM6QJI.js → langwatch-api-dashboards-JTVCS7ZJ.js} +5 -4
- package/dist/{langwatch-api-monitors-3L25OOOS.js → langwatch-api-monitors-3J7UIQME.js} +4 -3
- package/dist/{langwatch-api-secrets-ZFURUA62.js → langwatch-api-secrets-BUBSNCNO.js} +4 -3
- package/dist/{langwatch-api-triggers-XGXXT7DD.js → langwatch-api-triggers-EDFUCJN6.js} +4 -3
- package/dist/langwatch-api-workflows-NBJX5KOG.js +16 -0
- package/dist/{list-agents-6ZNOV7FK.js → list-agents-KFHKGPXV.js} +5 -4
- package/dist/{list-annotations-6P3IWILH.js → list-annotations-ZTVP4EEL.js} +5 -4
- package/dist/{list-api-keys-JJI62KD2.js → list-api-keys-CWD4UQT5.js} +5 -4
- package/dist/{list-dashboards-G2WJOXWB.js → list-dashboards-PWG6P6KR.js} +5 -4
- package/dist/{list-dataset-records-GWWVOCII.js → list-dataset-records-IOE7LQXS.js} +5 -4
- package/dist/{list-datasets-HBTH33X4.js → list-datasets-ZT4JYUDT.js} +5 -4
- package/dist/{list-evaluators-K5OGB6TX.js → list-evaluators-SJHC7VBH.js} +5 -4
- package/dist/{list-model-providers-JMOGZLSO.js → list-model-providers-PPD2WPD2.js} +5 -4
- package/dist/{list-projects-OXJC2NW5.js → list-projects-K7QAIX2I.js} +5 -4
- package/dist/{list-prompt-tags-J2HPHVAZ.js → list-prompt-tags-FOO6TNHB.js} +4 -3
- package/dist/{list-prompts-C7GR72XL.js → list-prompts-SC2HAUFG.js} +4 -3
- package/dist/{list-scenarios-JEPGIIDO.js → list-scenarios-F2WYNRIE.js} +5 -4
- package/dist/{list-simulation-runs-DRSAGPEC.js → list-simulation-runs-KF5THG5H.js} +5 -4
- package/dist/{list-suites-POTZ3ZT6.js → list-suites-CRY4VWW6.js} +5 -4
- package/dist/{list-workflows-QQZQIOHB.js → list-workflows-QSJWURCK.js} +5 -4
- package/dist/{rename-prompt-tag-4AAV2BS4.js → rename-prompt-tag-EKWU5QF3.js} +4 -3
- package/dist/report-issue-XYVAPAYJ.js +412 -0
- package/dist/{revoke-api-key-TONB4ADQ.js → revoke-api-key-FTRTZMOF.js} +5 -4
- package/dist/{run-agent-7QE3YEH7.js → run-agent-6SM4XNA7.js} +5 -4
- package/dist/{run-suite-Q66OSPO4.js → run-suite-NKZCJ2IN.js} +5 -4
- package/dist/{run-workflow-RWX2D752.js → run-workflow-QPXC627O.js} +5 -4
- package/dist/{search-traces-M5E5VB4X.js → search-traces-A6HFKPLJ.js} +10 -3
- package/dist/{set-model-provider-XENT5D2P.js → set-model-provider-3WKT63ZN.js} +5 -4
- package/dist/{update-agent-GML25EE3.js → update-agent-SMAEHLXH.js} +5 -4
- package/dist/{update-dataset-R4MM6NNY.js → update-dataset-BDUK7TUO.js} +5 -4
- package/dist/{update-dataset-record-FN4BL5P4.js → update-dataset-record-4ZSDRBSW.js} +5 -4
- package/dist/{update-evaluator-LH2RDDB5.js → update-evaluator-LS6TIBY7.js} +5 -4
- package/dist/{update-project-H53IHRBB.js → update-project-LKVYXENH.js} +5 -4
- package/dist/{update-prompt-M33OTTAJ.js → update-prompt-ODCRQRN6.js} +4 -3
- package/dist/{update-scenario-FKTQRAFQ.js → update-scenario-NEXCAQI7.js} +5 -4
- package/dist/{update-suite-JJTNOTXN.js → update-suite-SNO3GS7T.js} +5 -4
- package/package.json +20 -18
- package/pyproject.toml +10 -2
- package/src/__tests__/all-tools.integration.test.ts +56 -120
- package/src/__tests__/config.unit.test.ts +33 -0
- package/src/__tests__/docker.integration.test.ts +91 -7
- package/src/__tests__/documentation-fetch-security.integration.test.ts +182 -0
- package/src/__tests__/feature-map-tool-drift.unit.test.ts +93 -0
- package/src/__tests__/http-security-origin.unit.test.ts +97 -0
- package/src/__tests__/http-security-sessions.unit.test.ts +209 -0
- package/src/__tests__/http-security-verifier.unit.test.ts +248 -0
- package/src/__tests__/http-server-allocation.integration.test.ts +279 -0
- package/src/__tests__/http-server-origin.integration.test.ts +169 -0
- package/src/__tests__/http-server-session-auth.integration.test.ts +341 -0
- package/src/__tests__/http-transport.integration.test.ts +85 -87
- package/src/__tests__/langwatch-api.unit.test.ts +206 -0
- package/src/__tests__/public-http-request-security.integration.test.ts +118 -0
- package/src/__tests__/report-issue.integration.test.ts +147 -0
- package/src/__tests__/support/http-server-harness.ts +138 -0
- package/src/__tests__/tools.unit.test.ts +24 -0
- package/src/config.ts +17 -2
- package/src/create-mcp-server.ts +43 -29
- package/src/documentation-fetch.ts +111 -0
- package/src/http-security.ts +492 -0
- package/src/http-server.ts +536 -152
- package/src/index.ts +49 -2
- package/src/langwatch-api-agents.ts +2 -2
- package/src/langwatch-api.ts +138 -8
- package/src/public-http-request.ts +242 -0
- package/src/tools/discover-evaluator-schema.ts +2 -2
- package/src/tools/get-trace.ts +2 -12
- package/src/tools/report-issue.ts +150 -0
- package/src/tools/search-traces.ts +4 -0
- package/src/utils/format-evaluations.ts +20 -0
- package/tsconfig.json +8 -1
- package/uv.lock +261 -207
- package/dist/chunk-IUP5IHBD.js +0 -71
- package/dist/discover-evaluator-schema-H23XCLNE.js +0 -1402
- package/dist/http-server-GXVEDA7G.js +0 -226
- package/dist/langwatch-api-workflows-CXCTTCXT.js +0 -15
- package/pnpm-workspace.yaml +0 -17
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
updateSuite
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-YZLYHLLI.js";
|
|
4
|
+
import "./chunk-A2LEVYQ5.js";
|
|
5
|
+
import "./chunk-WT4PO4A3.js";
|
|
6
|
+
import "./chunk-SAS5OB7F.js";
|
|
6
7
|
|
|
7
8
|
// src/tools/update-suite.ts
|
|
8
9
|
async function handleUpdateSuite(params) {
|
|
@@ -36,4 +37,4 @@ async function handleUpdateSuite(params) {
|
|
|
36
37
|
export {
|
|
37
38
|
handleUpdateSuite
|
|
38
39
|
};
|
|
39
|
-
//# sourceMappingURL=update-suite-
|
|
40
|
+
//# sourceMappingURL=update-suite-SNO3GS7T.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@langwatch/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "An MCP server for Langwatch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,36 +36,38 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@ai-sdk/anthropic": "^3.0.44",
|
|
39
|
-
"@anthropic-ai/claude-code": "
|
|
39
|
+
"@anthropic-ai/claude-code": "2.1.202",
|
|
40
40
|
"@eslint/js": "^10.0.1",
|
|
41
41
|
"@langwatch/scenario": "^0.4.6",
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
42
|
+
"@opentelemetry/context-async-hooks": "^2.1.0",
|
|
43
|
+
"@opentelemetry/sdk-node": "^0.219.0",
|
|
44
|
+
"@types/debug": "^4.1.13",
|
|
44
45
|
"@types/express": "^5.0.6",
|
|
45
46
|
"@types/node": "^25.6.0",
|
|
46
47
|
"@types/yargs": "^17.0.33",
|
|
47
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
48
|
+
"@typescript/native-preview": "7.0.0-dev.20260705.1",
|
|
48
49
|
"ai": "^6.0.161",
|
|
50
|
+
"chalk": "^5.6.2",
|
|
51
|
+
"date-fns": "^4.1.0",
|
|
49
52
|
"dotenv": "^17.2.2",
|
|
50
|
-
"esbuild": "^0.
|
|
53
|
+
"esbuild": "^0.28.1",
|
|
51
54
|
"eslint": "^10.2.1",
|
|
55
|
+
"express": "^5.2.1",
|
|
56
|
+
"node-pty": "^1.0.0",
|
|
52
57
|
"tsup": "^8.1.0",
|
|
53
58
|
"tsx": "^4.20.5",
|
|
54
59
|
"typescript": "^5.0.0",
|
|
55
|
-
"typescript-eslint": "^8.55.0"
|
|
60
|
+
"typescript-eslint": "^8.55.0",
|
|
61
|
+
"vitest": "^4.1.8",
|
|
62
|
+
"yargs": "^18.0.0",
|
|
63
|
+
"zod-validation-error": "^5.0.0",
|
|
64
|
+
"@langwatch/redaction": "0.0.1",
|
|
65
|
+
"@langwatch/ssrf": "0.0.1",
|
|
66
|
+
"@langwatch/handled-error": "0.1.0"
|
|
56
67
|
},
|
|
57
68
|
"dependencies": {
|
|
58
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
59
|
-
"
|
|
60
|
-
"@opentelemetry/sdk-node": "^0.217.0",
|
|
61
|
-
"chalk": "^5.6.2",
|
|
62
|
-
"date-fns": "^4.1.0",
|
|
63
|
-
"express": "^5.2.1",
|
|
64
|
-
"node-pty": "^1.0.0",
|
|
65
|
-
"vitest": "^3.2.4",
|
|
66
|
-
"yargs": "^17.7.2",
|
|
67
|
-
"zod": "^4.3.6",
|
|
68
|
-
"zod-validation-error": "^5.0.0"
|
|
69
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
70
|
+
"zod": "^4.3.6"
|
|
69
71
|
},
|
|
70
72
|
"bin": {
|
|
71
73
|
"langwatch-mcp-server": "./dist/index.js"
|
package/pyproject.toml
CHANGED
|
@@ -7,7 +7,7 @@ requires-python = ">=3.11"
|
|
|
7
7
|
dependencies = [
|
|
8
8
|
"ipykernel>=6.30.1",
|
|
9
9
|
"langwatch",
|
|
10
|
-
"litellm>=1.
|
|
10
|
+
"litellm>=1.84.0",
|
|
11
11
|
"mcp[cli]>=1.13.1",
|
|
12
12
|
"pandas>=2.3.2",
|
|
13
13
|
"tenacity>=9.1.2",
|
|
@@ -16,5 +16,13 @@ dependencies = [
|
|
|
16
16
|
[tool.uv]
|
|
17
17
|
exclude-newer = "7 days"
|
|
18
18
|
|
|
19
|
+
# Transitive-dependency security pins. Keep these even when the importing
|
|
20
|
+
# package isn't a direct dependency, so a future re-resolution can't silently
|
|
21
|
+
# pull a vulnerable version back in.
|
|
22
|
+
constraint-dependencies = [
|
|
23
|
+
"python-dotenv>=1.2.2", # security: python-dotenv advisory (#934)
|
|
24
|
+
"python-liquid>=2.2.1", # security: python-liquid advisory (#1528)
|
|
25
|
+
]
|
|
26
|
+
|
|
19
27
|
[tool.uv.sources]
|
|
20
|
-
langwatch = { path = "
|
|
28
|
+
langwatch = { path = "../../sdks/python", editable = true }
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createServer, type Server } from "http";
|
|
2
2
|
import { afterAll, beforeAll, describe, expect, it, vi, beforeEach } from "vitest";
|
|
3
3
|
import { initConfig } from "../config.js";
|
|
4
|
+
import {
|
|
5
|
+
fetchDocumentation,
|
|
6
|
+
resolveDocumentationUrl,
|
|
7
|
+
} from "../documentation-fetch.js";
|
|
4
8
|
|
|
5
9
|
// --- Canned responses for every API endpoint ---
|
|
6
10
|
|
|
@@ -14,6 +18,14 @@ const CANNED_TRACES_SEARCH = {
|
|
|
14
18
|
output: { value: "I am fine, thank you!" },
|
|
15
19
|
timestamps: { started_at: 1700000000000 },
|
|
16
20
|
metadata: { user_id: "user-42", thread_id: "thread-1" },
|
|
21
|
+
evaluations: [
|
|
22
|
+
{
|
|
23
|
+
evaluator_id: "eval-1",
|
|
24
|
+
name: "Faithfulness",
|
|
25
|
+
score: 0.95,
|
|
26
|
+
passed: true,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
17
29
|
},
|
|
18
30
|
],
|
|
19
31
|
pagination: { totalHits: 1 },
|
|
@@ -616,7 +628,6 @@ function createMockServer(): Server {
|
|
|
616
628
|
describe("All MCP tools integration", () => {
|
|
617
629
|
let server: Server;
|
|
618
630
|
let port: number;
|
|
619
|
-
let originalFetch: typeof globalThis.fetch;
|
|
620
631
|
|
|
621
632
|
beforeAll(async () => {
|
|
622
633
|
server = createMockServer();
|
|
@@ -631,94 +642,41 @@ describe("All MCP tools integration", () => {
|
|
|
631
642
|
resolve();
|
|
632
643
|
});
|
|
633
644
|
});
|
|
634
|
-
originalFetch = globalThis.fetch;
|
|
635
645
|
});
|
|
636
646
|
|
|
637
647
|
afterAll(async () => {
|
|
638
648
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
639
|
-
globalThis.fetch = originalFetch;
|
|
640
649
|
});
|
|
641
650
|
|
|
642
651
|
// =====================
|
|
643
652
|
// 1. fetch_langwatch_docs
|
|
644
653
|
// =====================
|
|
645
654
|
describe("fetch_langwatch_docs", () => {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
text: () =>
|
|
651
|
-
Promise.resolve(
|
|
652
|
-
"# LangWatch Docs\nWelcome to LangWatch documentation.",
|
|
653
|
-
),
|
|
654
|
-
});
|
|
655
|
-
globalThis.fetch = mockFetch;
|
|
656
|
-
|
|
657
|
-
// The tool is inlined in index.ts; call the same logic directly
|
|
658
|
-
const url = "https://langwatch.ai/docs/llms.txt";
|
|
659
|
-
const response = await fetch(url);
|
|
660
|
-
const text = await response.text();
|
|
661
|
-
|
|
662
|
-
expect(text).toContain("LangWatch");
|
|
663
|
-
expect(mockFetch).toHaveBeenCalledWith(url);
|
|
664
|
-
|
|
665
|
-
globalThis.fetch = originalFetch;
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
describe("when fetching a specific doc page", () => {
|
|
670
|
-
it("appends .md extension when missing", async () => {
|
|
671
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
672
|
-
text: () => Promise.resolve("# Integration Guide"),
|
|
673
|
-
});
|
|
674
|
-
globalThis.fetch = mockFetch;
|
|
675
|
-
|
|
676
|
-
let urlToFetch = "https://langwatch.ai/docs/integration";
|
|
677
|
-
if (
|
|
678
|
-
!urlToFetch.endsWith(".md") &&
|
|
679
|
-
!urlToFetch.endsWith(".txt")
|
|
680
|
-
) {
|
|
681
|
-
urlToFetch += ".md";
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const response = await fetch(urlToFetch);
|
|
685
|
-
const text = await response.text();
|
|
686
|
-
|
|
687
|
-
expect(text).toContain("Integration Guide");
|
|
688
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
689
|
-
"https://langwatch.ai/docs/integration.md",
|
|
690
|
-
);
|
|
691
|
-
|
|
692
|
-
globalThis.fetch = originalFetch;
|
|
693
|
-
});
|
|
655
|
+
it("resolves the trusted docs index", () => {
|
|
656
|
+
expect(resolveDocumentationUrl("langwatch").toString()).toBe(
|
|
657
|
+
"https://langwatch.ai/docs/llms.txt",
|
|
658
|
+
);
|
|
694
659
|
});
|
|
695
660
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
});
|
|
701
|
-
globalThis.fetch = mockFetch;
|
|
702
|
-
|
|
703
|
-
let urlToFetch: string | undefined = "/getting-started";
|
|
704
|
-
if (urlToFetch && !urlToFetch.endsWith(".md") && !urlToFetch.endsWith(".txt")) {
|
|
705
|
-
urlToFetch += ".md";
|
|
706
|
-
}
|
|
707
|
-
if (!urlToFetch!.startsWith("http")) {
|
|
708
|
-
if (!urlToFetch!.startsWith("/")) {
|
|
709
|
-
urlToFetch = "/" + urlToFetch;
|
|
710
|
-
}
|
|
711
|
-
urlToFetch = "https://langwatch.ai/docs" + urlToFetch;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
await fetch(urlToFetch);
|
|
661
|
+
it("fetches trusted docs without following redirects", async () => {
|
|
662
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
663
|
+
new Response("# Integration Guide"),
|
|
664
|
+
);
|
|
715
665
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
666
|
+
const text = await fetchDocumentation(
|
|
667
|
+
"langwatch",
|
|
668
|
+
"integration",
|
|
669
|
+
mockFetch,
|
|
670
|
+
);
|
|
719
671
|
|
|
720
|
-
|
|
721
|
-
|
|
672
|
+
expect(text).toContain("Integration Guide");
|
|
673
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
674
|
+
new URL("https://langwatch.ai/docs/integration.md"),
|
|
675
|
+
{
|
|
676
|
+
redirect: "error",
|
|
677
|
+
signal: expect.any(AbortSignal),
|
|
678
|
+
},
|
|
679
|
+
);
|
|
722
680
|
});
|
|
723
681
|
});
|
|
724
682
|
|
|
@@ -726,53 +684,16 @@ describe("All MCP tools integration", () => {
|
|
|
726
684
|
// 2. fetch_scenario_docs
|
|
727
685
|
// =====================
|
|
728
686
|
describe("fetch_scenario_docs", () => {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
Promise.resolve(
|
|
734
|
-
"# Scenario Testing\nLearn how to test agents.",
|
|
735
|
-
),
|
|
736
|
-
});
|
|
737
|
-
globalThis.fetch = mockFetch;
|
|
738
|
-
|
|
739
|
-
const url = "https://langwatch.ai/scenario/llms.txt";
|
|
740
|
-
const response = await fetch(url);
|
|
741
|
-
const text = await response.text();
|
|
742
|
-
|
|
743
|
-
expect(text).toContain("Scenario Testing");
|
|
744
|
-
expect(mockFetch).toHaveBeenCalledWith(url);
|
|
745
|
-
|
|
746
|
-
globalThis.fetch = originalFetch;
|
|
747
|
-
});
|
|
687
|
+
it("resolves the trusted Scenario docs index", () => {
|
|
688
|
+
expect(resolveDocumentationUrl("scenario").toString()).toBe(
|
|
689
|
+
"https://langwatch.ai/scenario/llms.txt",
|
|
690
|
+
);
|
|
748
691
|
});
|
|
749
692
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
});
|
|
755
|
-
globalThis.fetch = mockFetch;
|
|
756
|
-
|
|
757
|
-
let urlToFetch: string | undefined = "setup";
|
|
758
|
-
if (urlToFetch && !urlToFetch.endsWith(".md") && !urlToFetch.endsWith(".txt")) {
|
|
759
|
-
urlToFetch += ".md";
|
|
760
|
-
}
|
|
761
|
-
if (!urlToFetch!.startsWith("http")) {
|
|
762
|
-
if (!urlToFetch!.startsWith("/")) {
|
|
763
|
-
urlToFetch = "/" + urlToFetch;
|
|
764
|
-
}
|
|
765
|
-
urlToFetch = "https://langwatch.ai/scenario" + urlToFetch;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
await fetch(urlToFetch);
|
|
769
|
-
|
|
770
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
771
|
-
"https://langwatch.ai/scenario/setup.md",
|
|
772
|
-
);
|
|
773
|
-
|
|
774
|
-
globalThis.fetch = originalFetch;
|
|
775
|
-
});
|
|
693
|
+
it("normalizes a relative Scenario docs path", () => {
|
|
694
|
+
expect(resolveDocumentationUrl("scenario", "setup").toString()).toBe(
|
|
695
|
+
"https://langwatch.ai/scenario/setup.md",
|
|
696
|
+
);
|
|
776
697
|
});
|
|
777
698
|
});
|
|
778
699
|
|
|
@@ -976,6 +897,21 @@ describe("All MCP tools integration", () => {
|
|
|
976
897
|
});
|
|
977
898
|
});
|
|
978
899
|
});
|
|
900
|
+
|
|
901
|
+
describe("given a trace has evaluation results", () => {
|
|
902
|
+
/** @scenario Agent searches traces and sees evaluation results without a follow-up call */
|
|
903
|
+
it("includes evaluation status in the digest without a follow-up call", async () => {
|
|
904
|
+
const { handleSearchTraces } = await import(
|
|
905
|
+
"../tools/search-traces.js"
|
|
906
|
+
);
|
|
907
|
+
const result = await handleSearchTraces({
|
|
908
|
+
startDate: "24h",
|
|
909
|
+
endDate: "now",
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
expect(result).toContain("- **Faithfulness**: PASSED (score: 0.95)");
|
|
913
|
+
});
|
|
914
|
+
});
|
|
979
915
|
});
|
|
980
916
|
|
|
981
917
|
// =====================
|
|
@@ -58,6 +58,39 @@ describe("config", () => {
|
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
describe("when the endpoint carries a trailing slash", () => {
|
|
62
|
+
it("strips it from a CLI endpoint", () => {
|
|
63
|
+
initConfig({ endpoint: "https://cli.example.com/" });
|
|
64
|
+
expect(getConfig().endpoint).toBe("https://cli.example.com");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("trims whitespace around a CLI endpoint", () => {
|
|
68
|
+
initConfig({ endpoint: " https://cli.example.com/// " });
|
|
69
|
+
expect(getConfig().endpoint).toBe("https://cli.example.com");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("strips repeated slashes from the env var", () => {
|
|
73
|
+
process.env.LANGWATCH_ENDPOINT = "http://localhost:5560///";
|
|
74
|
+
initConfig({});
|
|
75
|
+
expect(getConfig().endpoint).toBe("http://localhost:5560");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("keeps the built request path free of a double slash", () => {
|
|
79
|
+
initConfig({ endpoint: "https://app.langwatch.ai/" });
|
|
80
|
+
expect(`${getConfig().endpoint}/api/bug-reports`).toBe(
|
|
81
|
+
"https://app.langwatch.ai/api/bug-reports",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("when the endpoint is blank", () => {
|
|
87
|
+
it("falls back to the default rather than an empty base url", () => {
|
|
88
|
+
process.env.LANGWATCH_ENDPOINT = " ";
|
|
89
|
+
initConfig({});
|
|
90
|
+
expect(getConfig().endpoint).toBe("https://app.langwatch.ai");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
61
94
|
describe("when no args or env vars are provided", () => {
|
|
62
95
|
it("defaults endpoint to https://app.langwatch.ai", () => {
|
|
63
96
|
initConfig({});
|
|
@@ -4,7 +4,16 @@ import { execSync } from "child_process";
|
|
|
4
4
|
const IMAGE_NAME = "langwatch-mcp-server-test";
|
|
5
5
|
const CONTAINER_NAME = "langwatch-mcp-server-test-container";
|
|
6
6
|
const HOST_PORT = 13099; // unlikely to conflict
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HTTP mode verifies the bearer against the LangWatch API before creating a
|
|
10
|
+
* session, so the authenticated paths need a key the API actually accepts.
|
|
11
|
+
* Without one, this file still covers startup, CORS, and rejection.
|
|
12
|
+
*/
|
|
13
|
+
const BEARER_TOKEN = process.env.LANGWATCH_API_KEY;
|
|
14
|
+
|
|
15
|
+
/** A syntactically fine token that the LangWatch API does not know. */
|
|
16
|
+
const BOGUS_TOKEN = "totally-not-a-real-key";
|
|
8
17
|
|
|
9
18
|
/** Standard headers required by the MCP Streamable HTTP protocol */
|
|
10
19
|
const MCP_POST_HEADERS = {
|
|
@@ -20,7 +29,7 @@ describe("Docker container", () => {
|
|
|
20
29
|
// Build from repo root (mcp-server needs langevals/ for build)
|
|
21
30
|
const repoRoot = process.cwd().replace(/\/mcp-server.*$/, "");
|
|
22
31
|
execSync(
|
|
23
|
-
`docker build -t ${IMAGE_NAME} -f mcp
|
|
32
|
+
`docker build -t ${IMAGE_NAME} -f mcp/typescript/Dockerfile .`,
|
|
24
33
|
{
|
|
25
34
|
cwd: repoRoot,
|
|
26
35
|
stdio: "pipe",
|
|
@@ -85,11 +94,86 @@ describe("Docker container", () => {
|
|
|
85
94
|
expect(body.status).toBe("ok");
|
|
86
95
|
});
|
|
87
96
|
|
|
88
|
-
it("
|
|
97
|
+
it("does not answer with a wildcard CORS origin", async () => {
|
|
89
98
|
if (!containerRunning) return;
|
|
90
99
|
|
|
91
100
|
const res = await fetch(`http://localhost:${HOST_PORT}/health`);
|
|
92
|
-
expect(res.headers.get("access-control-allow-origin")).toBe("*");
|
|
101
|
+
expect(res.headers.get("access-control-allow-origin")).not.toBe("*");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("reflects an allowed origin and rejects an unlisted one", async () => {
|
|
105
|
+
if (!containerRunning) return;
|
|
106
|
+
|
|
107
|
+
const allowed = await fetch(`http://localhost:${HOST_PORT}/health`, {
|
|
108
|
+
headers: { Origin: "http://localhost:5173" },
|
|
109
|
+
});
|
|
110
|
+
expect(allowed.status).toBe(200);
|
|
111
|
+
expect(allowed.headers.get("access-control-allow-origin")).toBe(
|
|
112
|
+
"http://localhost:5173"
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const rejected = await fetch(`http://localhost:${HOST_PORT}/health`, {
|
|
116
|
+
headers: { Origin: "https://attacker.example" },
|
|
117
|
+
});
|
|
118
|
+
expect(rejected.status).toBe(403);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("rejects a bearer the LangWatch API does not recognise", async () => {
|
|
122
|
+
if (!containerRunning) return;
|
|
123
|
+
|
|
124
|
+
const res = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
...MCP_POST_HEADERS,
|
|
128
|
+
Authorization: `Bearer ${BOGUS_TOKEN}`,
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
jsonrpc: "2.0",
|
|
132
|
+
id: 1,
|
|
133
|
+
method: "initialize",
|
|
134
|
+
params: {
|
|
135
|
+
protocolVersion: "2025-03-26",
|
|
136
|
+
capabilities: {},
|
|
137
|
+
clientInfo: { name: "docker-test", version: "1.0.0" },
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(res.status).toBe(401);
|
|
143
|
+
expect(res.headers.get("mcp-session-id")).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("rejects a session id presented without a bearer token", async () => {
|
|
147
|
+
if (!containerRunning || !BEARER_TOKEN) return;
|
|
148
|
+
|
|
149
|
+
const initRes = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
...MCP_POST_HEADERS,
|
|
153
|
+
Authorization: `Bearer ${BEARER_TOKEN}`,
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
jsonrpc: "2.0",
|
|
157
|
+
id: 1,
|
|
158
|
+
method: "initialize",
|
|
159
|
+
params: {
|
|
160
|
+
protocolVersion: "2025-03-26",
|
|
161
|
+
capabilities: {},
|
|
162
|
+
clientInfo: { name: "docker-test", version: "1.0.0" },
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
const sessionId = initRes.headers.get("mcp-session-id");
|
|
167
|
+
expect(sessionId).toBeTruthy();
|
|
168
|
+
await initRes.text();
|
|
169
|
+
|
|
170
|
+
const res = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { ...MCP_POST_HEADERS, "mcp-session-id": sessionId! },
|
|
173
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(res.status).toBe(401);
|
|
93
177
|
});
|
|
94
178
|
|
|
95
179
|
it("returns 401 on initialize without Bearer token", async () => {
|
|
@@ -116,7 +200,7 @@ describe("Docker container", () => {
|
|
|
116
200
|
});
|
|
117
201
|
|
|
118
202
|
it("MCP initialize works with Bearer token", async () => {
|
|
119
|
-
if (!containerRunning) return;
|
|
203
|
+
if (!containerRunning || !BEARER_TOKEN) return;
|
|
120
204
|
|
|
121
205
|
const res = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
|
|
122
206
|
method: "POST",
|
|
@@ -146,7 +230,7 @@ describe("Docker container", () => {
|
|
|
146
230
|
});
|
|
147
231
|
|
|
148
232
|
it("lists tools after initialization with Bearer token", async () => {
|
|
149
|
-
if (!containerRunning) return;
|
|
233
|
+
if (!containerRunning || !BEARER_TOKEN) return;
|
|
150
234
|
|
|
151
235
|
// Initialize a session
|
|
152
236
|
const initRes = await fetch(`http://localhost:${HOST_PORT}/mcp`, {
|
|
@@ -205,7 +289,7 @@ describe("Docker container", () => {
|
|
|
205
289
|
});
|
|
206
290
|
|
|
207
291
|
it("legacy SSE endpoint responds with Bearer token", async () => {
|
|
208
|
-
if (!containerRunning) return;
|
|
292
|
+
if (!containerRunning || !BEARER_TOKEN) return;
|
|
209
293
|
|
|
210
294
|
const controller = new AbortController();
|
|
211
295
|
const timeout = setTimeout(() => controller.abort(), 5000);
|