@langwatch/mcp-server 0.10.0 → 1.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/.release-please-shim +1 -1
- package/CHANGELOG.md +62 -0
- package/README.md +9 -17
- package/dist/{archive-project-HZBRPR2S.js → archive-project-MK3673A7.js} +4 -3
- package/dist/{archive-scenario-P5RRITRM.js → archive-scenario-KATSXRIN.js} +4 -3
- package/dist/{archive-suite-NUUFD2EM.js → archive-suite-C3KX6IIZ.js} +4 -3
- package/dist/{assign-prompt-tag-FKCCZPML.js → assign-prompt-tag-E3C5W6PI.js} +3 -2
- package/dist/chunk-37SMPMQS.js +12 -0
- package/dist/{chunk-42ZHHHIJ.js → chunk-5UWY24RP.js} +79 -4
- package/dist/chunk-BJ7B4R26.js +42 -0
- package/dist/chunk-CE6QMZOP.js +432 -0
- package/dist/{chunk-X2SEURT2.js → chunk-DAIQKS27.js} +2 -2
- package/dist/{chunk-R7C7OUVG.js → chunk-EU22L52K.js} +2 -2
- package/dist/{chunk-TROIWSEQ.js → chunk-J45SVWPR.js} +2 -2
- package/dist/{chunk-CCM4ZFGF.js → chunk-O3LWVN67.js} +2 -2
- package/dist/{chunk-BB7EOKHR.js → chunk-RYRB2YVJ.js} +2 -2
- package/dist/{chunk-HKY3GW45.js → chunk-T7WJVS2N.js} +2 -2
- package/dist/{chunk-JHOSVOHT.js → chunk-UXXA4D6K.js} +2 -2
- package/dist/{chunk-FVDAZ4XG.js → chunk-VYPSKRPN.js} +219 -137
- package/dist/{chunk-CHKCEZBP.js → chunk-WDH5P5RF.js} +2 -2
- package/dist/{chunk-BBJ5WAJB.js → chunk-WWMDHZZV.js} +2 -2
- package/dist/{chunk-ADSJ2FDA.js → chunk-XY4STOBE.js} +2 -2
- package/dist/{chunk-BYWYM2RP.js → chunk-Y3HDZ6OW.js} +2 -2
- package/dist/config.js +1 -0
- package/dist/{create-agent-K5Q7BFGZ.js → create-agent-7W57J7MT.js} +4 -3
- package/dist/{create-annotation-KN3GE3DK.js → create-annotation-W2XELBCQ.js} +4 -3
- package/dist/{create-api-key-OQI5PUNQ.js → create-api-key-MKRXKFJM.js} +4 -3
- package/dist/{create-dashboard-G4QA5AXP.js → create-dashboard-QGL7IODC.js} +4 -3
- package/dist/{create-dataset-WQSC37N7.js → create-dataset-3PH7A4HG.js} +4 -3
- package/dist/{create-dataset-records-B4RL3FLC.js → create-dataset-records-4DZS3VRL.js} +4 -3
- package/dist/{create-evaluator-YFFQIR7E.js → create-evaluator-YWH6NC4M.js} +4 -3
- package/dist/create-mcp-server.js +3 -2
- package/dist/{create-project-CUVQ67VN.js → create-project-BOBQ32C5.js} +4 -3
- package/dist/{create-prompt-NGXKSP4X.js → create-prompt-FLCFGITE.js} +3 -2
- package/dist/{create-prompt-tag-IUKYPVHH.js → create-prompt-tag-EMPNIKEA.js} +3 -2
- package/dist/{create-scenario-FNBXALH3.js → create-scenario-BSBIHUL6.js} +4 -3
- package/dist/{create-suite-DJEPD4ZB.js → create-suite-TWF6BBR4.js} +4 -3
- package/dist/{delete-agent-TJI5XBBM.js → delete-agent-7MSYNY5E.js} +4 -3
- package/dist/{delete-annotation-KRP53TUU.js → delete-annotation-ZT5IGN24.js} +4 -3
- package/dist/{delete-dashboard-N5SZJIR3.js → delete-dashboard-BTIY5S3L.js} +4 -3
- package/dist/{delete-dataset-KDUOHL2P.js → delete-dataset-U45HQHPR.js} +4 -3
- package/dist/{delete-dataset-records-DM4W3PAW.js → delete-dataset-records-R43IMAWB.js} +4 -3
- package/dist/{delete-evaluator-7ZATLJOA.js → delete-evaluator-U6JMQ6C7.js} +4 -3
- package/dist/{delete-prompt-tag-I6HNMSVJ.js → delete-prompt-tag-4EV4TDHB.js} +3 -2
- package/dist/{delete-workflow-QEO74JQR.js → delete-workflow-5QYALXEL.js} +4 -3
- package/dist/discover-evaluator-schema-MRGHVNTT.js +2175 -0
- package/dist/{discover-scenario-schema-MEEEVND7.js → discover-scenario-schema-UPBTCWPG.js} +3 -1
- package/dist/{discover-schema-3T52ORPB.js → discover-schema-ICGXPHUW.js} +3 -1
- package/dist/{get-agent-3KDWCP5N.js → get-agent-7QJVPH72.js} +4 -3
- package/dist/{get-analytics-GPVJX7SF.js → get-analytics-PVERWOA5.js} +3 -2
- package/dist/{get-dashboard-YPTIE5YY.js → get-dashboard-ZZPC3WMD.js} +4 -3
- package/dist/{get-dataset-6DQFYITM.js → get-dataset-25TB2LQH.js} +4 -3
- package/dist/{get-evaluator-PCM6XQAU.js → get-evaluator-3FTRQKEC.js} +4 -3
- package/dist/{get-project-JCMWXVLU.js → get-project-5OAXYC57.js} +4 -3
- package/dist/{get-prompt-5XAGWWBG.js → get-prompt-I753LZKZ.js} +3 -2
- package/dist/{get-scenario-7XBCDZWX.js → get-scenario-DSSTR6FI.js} +4 -3
- package/dist/{get-simulation-run-AV24FJWI.js → get-simulation-run-RQPMS2MC.js} +4 -3
- package/dist/{get-suite-4DBVHFEM.js → get-suite-GTOCY5PL.js} +4 -3
- package/dist/{get-trace-MRBDS3HJ.js → get-trace-O5VT3NAC.js} +7 -9
- package/dist/{get-workflow-UR4Y6UFX.js → get-workflow-3EZDOEIF.js} +4 -3
- package/dist/http-server-2ACJ3CXW.js +24366 -0
- package/dist/index.js +23374 -3140
- package/dist/{langwatch-api-annotations-ZJDRB4JH.js → langwatch-api-annotations-E65MI3WZ.js} +4 -3
- package/dist/{langwatch-api-dashboards-WARLI6NF.js → langwatch-api-dashboards-QCDECRCY.js} +4 -3
- package/dist/{langwatch-api-monitors-4XKXHYCG.js → langwatch-api-monitors-UIO2KTDV.js} +3 -2
- package/dist/{langwatch-api-secrets-6SHK5G7Q.js → langwatch-api-secrets-4T2EKSJ7.js} +3 -2
- package/dist/{langwatch-api-triggers-5SX6R2WU.js → langwatch-api-triggers-MPOEKPUY.js} +3 -2
- package/dist/{langwatch-api-workflows-NLJENNXE.js → langwatch-api-workflows-5HRZ7HR4.js} +4 -3
- package/dist/{list-agents-NECO7MPI.js → list-agents-TIJTP566.js} +4 -3
- package/dist/{list-annotations-3OJEO774.js → list-annotations-CGX5BT7E.js} +4 -3
- package/dist/{list-api-keys-AWW7ROLB.js → list-api-keys-JLWJPFRX.js} +4 -3
- package/dist/{list-dashboards-DVPVVVKW.js → list-dashboards-RNIWX3TX.js} +4 -3
- package/dist/{list-dataset-records-I4BYORF5.js → list-dataset-records-B5MHZR66.js} +4 -3
- package/dist/{list-datasets-WUAE5HGX.js → list-datasets-BVCAMQCP.js} +4 -3
- package/dist/{list-evaluators-B6OSVAM2.js → list-evaluators-65FBTL2U.js} +4 -3
- package/dist/{list-model-providers-NL6IP4UZ.js → list-model-providers-GRK3I3VV.js} +4 -3
- package/dist/{list-projects-5FHMJ4PQ.js → list-projects-JLMBFMW3.js} +4 -3
- package/dist/{list-prompt-tags-Z2DD6FLQ.js → list-prompt-tags-UHNWDGQI.js} +3 -2
- package/dist/{list-prompts-VNXKHK7T.js → list-prompts-7254B3U7.js} +3 -2
- package/dist/{list-scenarios-CFZHXL76.js → list-scenarios-46YNICKM.js} +4 -3
- package/dist/{list-simulation-runs-AM2BXLBW.js → list-simulation-runs-LGNEFNPN.js} +4 -3
- package/dist/{list-suites-ZDOHAAXF.js → list-suites-5VK4HXEK.js} +4 -3
- package/dist/{list-workflows-WBRBKLIT.js → list-workflows-MLQDM6CP.js} +4 -3
- package/dist/{rename-prompt-tag-A4RJUK7Z.js → rename-prompt-tag-RHQMNEQL.js} +3 -2
- package/dist/report-issue-DXJ6TPTC.js +412 -0
- package/dist/{revoke-api-key-ZSPSPF5G.js → revoke-api-key-3Y35EMBT.js} +4 -3
- package/dist/{run-agent-JFNDY7MR.js → run-agent-RBSIFKRT.js} +4 -3
- package/dist/{run-suite-TH7LO5TJ.js → run-suite-K3DNQ3SE.js} +4 -3
- package/dist/{run-workflow-CK3JP5U2.js → run-workflow-5LVBZYLS.js} +4 -3
- package/dist/{search-traces-3BBVYPC3.js → search-traces-U4HLOCZ2.js} +9 -2
- package/dist/{set-model-provider-WOC5FNMV.js → set-model-provider-V3M5CDIY.js} +4 -3
- package/dist/{update-agent-YMEV5MTL.js → update-agent-47RZIHS7.js} +4 -3
- package/dist/{update-dataset-EYT5POFM.js → update-dataset-G6QJJGS5.js} +4 -3
- package/dist/{update-dataset-record-VFF5JTX4.js → update-dataset-record-RMWLSWCS.js} +4 -3
- package/dist/{update-evaluator-6IJHFG2E.js → update-evaluator-SFRRI5Q3.js} +4 -3
- package/dist/{update-project-OIT4WPFI.js → update-project-PI5DWBVK.js} +4 -3
- package/dist/{update-prompt-ZU2DD6KM.js → update-prompt-RQFTEO7Y.js} +3 -2
- package/dist/{update-scenario-3VUTPDRU.js → update-scenario-G3IU2UIB.js} +4 -3
- package/dist/{update-suite-C6JFDQHL.js → update-suite-TRGGLNR7.js} +4 -3
- package/package.json +20 -18
- package/pnpm-workspace.yaml +27 -2
- package/pyproject.toml +9 -1
- package/src/__tests__/all-tools.integration.test.ts +56 -120
- package/src/__tests__/documentation-fetch-security.integration.test.ts +176 -0
- 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__/tools.unit.test.ts +24 -0
- package/src/create-mcp-server.ts +43 -29
- package/src/documentation-fetch.ts +111 -0
- 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/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 +259 -205
- package/dist/chunk-XC62KJIT.js +0 -71
- package/dist/discover-evaluator-schema-H23XCLNE.js +0 -1402
- package/dist/http-server-XUQLIFE7.js +0 -226
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@langwatch/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.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/handled-error": "0.1.0",
|
|
65
|
+
"@langwatch/redaction": "0.0.1",
|
|
66
|
+
"@langwatch/ssrf": "0.0.1"
|
|
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/pnpm-workspace.yaml
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
# Standalone workspace: mcp-server is installed on its own in CI, the
|
|
2
|
+
# release workflow, and its Dockerfile. Shared source-only contract packages
|
|
3
|
+
# are bundled by tsup for publish.
|
|
4
|
+
packages:
|
|
5
|
+
- .
|
|
6
|
+
- ../packages/handled-error
|
|
7
|
+
- ../langwatch/packages/redaction
|
|
8
|
+
- ../langwatch/packages/ssrf
|
|
9
|
+
|
|
1
10
|
minimumReleaseAge: 10080
|
|
2
11
|
|
|
3
12
|
overrides:
|
|
4
|
-
|
|
5
|
-
|
|
13
|
+
# Security: protobufjs code execution + prototype-pollution + DoS + Any-expansion DoS + property shadowing
|
|
14
|
+
# (GHSA-xq3m-2v4x-88gg, GHSA-66ff-xgx4-vchm, GHSA-75px-5xx7-5xc7,
|
|
15
|
+
# GHSA-jvwf-75h9-cwgg, GHSA-685m-2w69-288q, GHSA-wcpc-wj8m-hjx6, GHSA-f38q-mgvj-vph7)
|
|
16
|
+
"protobufjs": ">=7.5.6 <8.0.0 || >=8.6.0"
|
|
17
|
+
# Security: langsmith untrusted manifest deserialization (GHSA-3644-q5cj-c5c7).
|
|
18
|
+
# langsmith is only a transitive of the @langwatch/scenario devDependency
|
|
19
|
+
# (via langchain / @langchain/core); pinned to a known-safe version.
|
|
20
|
+
"langsmith": "0.6.0"
|
|
21
|
+
# Security: esbuild Deno binary integrity RCE + dev-server file read (GHSA-gv7w-rqvm-qjhr / GHSA-g7r4-m6w7-qqqr)
|
|
22
|
+
"esbuild@<0.28.1": ">=0.28.1"
|
|
23
|
+
# Security: vite dev-server file read / middleware path bypass
|
|
24
|
+
"vite@>=7.0.0 <7.3.5": ">=7.3.5 <8"
|
|
25
|
+
# Security: hono request smuggling / path confusion
|
|
26
|
+
"hono@<4.12.25": ">=4.12.25"
|
|
27
|
+
# Security: fast-uri path traversal + host confusion
|
|
6
28
|
"fast-uri@<3.1.2": "3.1.2"
|
|
29
|
+
liquidjs: ">=10.26.0"
|
|
30
|
+
# Security: uuid missing buffer bounds check in v3/v5/v6 (GHSA-w5hq-g745-h8pq)
|
|
31
|
+
"uuid@<11.1.1": ">=11.1.1 <12"
|
|
7
32
|
|
|
8
33
|
onlyBuiltDependencies:
|
|
9
34
|
- node-pty
|
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
28
|
langwatch = { path = "../python-sdk", 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
|
// =====================
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createServer, type Server } from "node:http";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { initConfig } from "../config.js";
|
|
5
|
+
import { fetchDocumentation, resolveDocumentationUrl } from "../documentation-fetch.js";
|
|
6
|
+
|
|
7
|
+
const MCP_HEADERS = {
|
|
8
|
+
Accept: "application/json, text/event-stream",
|
|
9
|
+
Authorization: "Bearer security-regression-test",
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("MCP documentation fetch security", () => {
|
|
14
|
+
describe("URL validation", () => {
|
|
15
|
+
/** @scenario A documentation tool accepts its own trusted HTTPS pages */
|
|
16
|
+
it.each([
|
|
17
|
+
["langwatch" as const, undefined, "https://langwatch.ai/docs/llms.txt"],
|
|
18
|
+
["scenario" as const, undefined, "https://langwatch.ai/scenario/llms.txt"],
|
|
19
|
+
["langwatch" as const, "observability/tracing", "https://langwatch.ai/docs/observability/tracing.md"],
|
|
20
|
+
["scenario" as const, "/scenario/guides/quickstart", "https://langwatch.ai/scenario/guides/quickstart.md"],
|
|
21
|
+
[
|
|
22
|
+
"langwatch" as const,
|
|
23
|
+
"https://langwatch.ai/docs/llms.txt?format=raw",
|
|
24
|
+
"https://langwatch.ai/docs/llms.txt?format=raw",
|
|
25
|
+
],
|
|
26
|
+
])("resolves trusted %s documentation URL %s", (kind, input, expected) => {
|
|
27
|
+
expect(resolveDocumentationUrl(kind, input).toString()).toBe(expected);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** @scenario A documentation tool rejects non-HTTPS and untrusted hosts */
|
|
31
|
+
/** @scenario A documentation tool rejects a trusted host outside its namespace */
|
|
32
|
+
it.each([
|
|
33
|
+
["langwatch" as const, "http://169.254.169.254/latest/meta-data/"],
|
|
34
|
+
["langwatch" as const, "https://langwatch.ai.attacker.example/docs/x"],
|
|
35
|
+
["langwatch" as const, "https://langwatch.ai@127.0.0.1/docs/x"],
|
|
36
|
+
["langwatch" as const, "https://langwatch.ai:444/docs/x"],
|
|
37
|
+
["langwatch" as const, "file:///etc/passwd"],
|
|
38
|
+
["langwatch" as const, "https://langwatch.ai/scenario/llms.txt"],
|
|
39
|
+
["langwatch" as const, "https://langwatch.ai/docs-evil/page.md"],
|
|
40
|
+
["scenario" as const, "https://langwatch.ai/docs/llms.txt"],
|
|
41
|
+
["scenario" as const, "https://langwatch.ai/scenario-evil/page.md"],
|
|
42
|
+
["langwatch" as const, "https://langwatch.ai/docs/../scenario/llms.txt"],
|
|
43
|
+
])("rejects untrusted %s documentation URL %s", (kind, input) => {
|
|
44
|
+
expect(() => resolveDocumentationUrl(kind, input)).toThrow(/trusted LangWatch documentation URL/);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** @scenario Documentation fetches do not follow redirects */
|
|
49
|
+
it("disables redirects and bounds trusted documentation fetches", async () => {
|
|
50
|
+
let receivedRedirectMode: RequestRedirect | undefined;
|
|
51
|
+
let receivedSignal: AbortSignal | null | undefined;
|
|
52
|
+
const fetchForTest: typeof fetch = async (_input, init) => {
|
|
53
|
+
receivedRedirectMode = init?.redirect;
|
|
54
|
+
receivedSignal = init?.signal;
|
|
55
|
+
return new Response("# documentation");
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const text = await fetchDocumentation("langwatch", "https://langwatch.ai/docs/llms.txt", fetchForTest);
|
|
59
|
+
|
|
60
|
+
expect(text).toBe("# documentation");
|
|
61
|
+
expect(receivedRedirectMode).toBe("error");
|
|
62
|
+
expect(receivedSignal).toBeInstanceOf(AbortSignal);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("rejects non-document responses from the trusted origin", async () => {
|
|
66
|
+
const fetchForTest: typeof fetch = async () =>
|
|
67
|
+
new Response("<html>unexpected</html>", {
|
|
68
|
+
headers: { "Content-Type": "text/html" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await expect(fetchDocumentation("langwatch", "https://langwatch.ai/docs/llms.txt", fetchForTest)).rejects.toThrow(
|
|
72
|
+
/unexpected content type/
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("HTTP tool transport", () => {
|
|
77
|
+
let mcpServer: Server;
|
|
78
|
+
let targetServer: Server;
|
|
79
|
+
let mcpPort: number;
|
|
80
|
+
let targetPort: number;
|
|
81
|
+
let sessionId: string;
|
|
82
|
+
let targetHits = 0;
|
|
83
|
+
let rpcId = 10;
|
|
84
|
+
|
|
85
|
+
beforeAll(async () => {
|
|
86
|
+
targetServer = createServer((_request, response) => {
|
|
87
|
+
targetHits += 1;
|
|
88
|
+
response.end("internal-only response");
|
|
89
|
+
});
|
|
90
|
+
await new Promise<void>((resolve) => {
|
|
91
|
+
targetServer.listen(0, "127.0.0.1", resolve);
|
|
92
|
+
});
|
|
93
|
+
const targetAddress = targetServer.address();
|
|
94
|
+
targetPort = typeof targetAddress === "object" && targetAddress ? targetAddress.port : 0;
|
|
95
|
+
|
|
96
|
+
initConfig({ endpoint: "https://app.langwatch.ai" });
|
|
97
|
+
const { startHttpServer } = await import("../http-server.js");
|
|
98
|
+
const started = await startHttpServer({ port: 0 });
|
|
99
|
+
mcpServer = started.server;
|
|
100
|
+
mcpPort = started.port;
|
|
101
|
+
|
|
102
|
+
const initializeResponse = await fetch(`http://127.0.0.1:${mcpPort}/mcp`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: MCP_HEADERS,
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
jsonrpc: "2.0",
|
|
107
|
+
id: 1,
|
|
108
|
+
method: "initialize",
|
|
109
|
+
params: {
|
|
110
|
+
protocolVersion: "2025-03-26",
|
|
111
|
+
capabilities: {},
|
|
112
|
+
clientInfo: { name: "security-test", version: "1.0.0" },
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
sessionId = initializeResponse.headers.get("mcp-session-id") ?? "";
|
|
117
|
+
expect(sessionId).not.toBe("");
|
|
118
|
+
|
|
119
|
+
await fetch(`http://127.0.0.1:${mcpPort}/mcp`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
jsonrpc: "2.0",
|
|
124
|
+
method: "notifications/initialized",
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterAll(async () => {
|
|
130
|
+
await Promise.all([
|
|
131
|
+
new Promise<void>((resolve, reject) => {
|
|
132
|
+
mcpServer.close((error) => (error ? reject(error) : resolve()));
|
|
133
|
+
}),
|
|
134
|
+
new Promise<void>((resolve, reject) => {
|
|
135
|
+
targetServer.close((error) => (error ? reject(error) : resolve()));
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
async function callDocumentationTool(
|
|
141
|
+
name: "fetch_langwatch_docs" | "fetch_scenario_docs",
|
|
142
|
+
url: string
|
|
143
|
+
): Promise<string> {
|
|
144
|
+
const response = await fetch(`http://127.0.0.1:${mcpPort}/mcp`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
jsonrpc: "2.0",
|
|
149
|
+
id: rpcId++,
|
|
150
|
+
method: "tools/call",
|
|
151
|
+
params: { name, arguments: { url } },
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(response.status).toBe(200);
|
|
156
|
+
return response.text();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
it.each(["fetch_langwatch_docs" as const, "fetch_scenario_docs" as const])(
|
|
160
|
+
"blocks %s from reaching a loopback HTTP server",
|
|
161
|
+
async (tool) => {
|
|
162
|
+
const hitsBefore = targetHits;
|
|
163
|
+
const body = await callDocumentationTool(tool, `http://127.0.0.1:${targetPort}/secrets`);
|
|
164
|
+
|
|
165
|
+
expect(body).toContain("trusted LangWatch documentation URL");
|
|
166
|
+
expect(targetHits).toBe(hitsBefore);
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
it("blocks cross-namespace fetches through the real MCP handler", async () => {
|
|
171
|
+
const body = await callDocumentationTool("fetch_langwatch_docs", "https://langwatch.ai/scenario/llms.txt");
|
|
172
|
+
|
|
173
|
+
expect(body).toContain("trusted LangWatch documentation URL");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|