@langwatch/mcp-server 0.10.1 → 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.
Files changed (122) hide show
  1. package/.release-please-shim +1 -1
  2. package/CHANGELOG.md +54 -0
  3. package/README.md +9 -17
  4. package/dist/{archive-project-KAZFHR4A.js → archive-project-MK3673A7.js} +4 -3
  5. package/dist/{archive-scenario-6M2IB7Y2.js → archive-scenario-KATSXRIN.js} +4 -3
  6. package/dist/{archive-suite-NXZ3YDS3.js → archive-suite-C3KX6IIZ.js} +4 -3
  7. package/dist/{assign-prompt-tag-WPUJO55U.js → assign-prompt-tag-E3C5W6PI.js} +3 -2
  8. package/dist/chunk-37SMPMQS.js +12 -0
  9. package/dist/{chunk-CAY57CKF.js → chunk-5UWY24RP.js} +77 -4
  10. package/dist/chunk-BJ7B4R26.js +42 -0
  11. package/dist/chunk-CE6QMZOP.js +432 -0
  12. package/dist/{chunk-AGGE6NPR.js → chunk-DAIQKS27.js} +2 -2
  13. package/dist/{chunk-GWEW5XON.js → chunk-EU22L52K.js} +2 -2
  14. package/dist/{chunk-ITN2QGHF.js → chunk-J45SVWPR.js} +2 -2
  15. package/dist/{chunk-LTEJDL4V.js → chunk-O3LWVN67.js} +2 -2
  16. package/dist/{chunk-RDUXCCFR.js → chunk-RYRB2YVJ.js} +2 -2
  17. package/dist/{chunk-XRQNXN2Z.js → chunk-T7WJVS2N.js} +2 -2
  18. package/dist/{chunk-U2QIW2SW.js → chunk-UXXA4D6K.js} +2 -2
  19. package/dist/{chunk-HRFQAFPR.js → chunk-VYPSKRPN.js} +220 -133
  20. package/dist/{chunk-UKRCZJJC.js → chunk-WDH5P5RF.js} +2 -2
  21. package/dist/{chunk-COCNCXVJ.js → chunk-WWMDHZZV.js} +2 -2
  22. package/dist/{chunk-B6UV3JVI.js → chunk-XY4STOBE.js} +2 -2
  23. package/dist/{chunk-ESGZVPPC.js → chunk-Y3HDZ6OW.js} +2 -2
  24. package/dist/config.js +1 -0
  25. package/dist/{create-agent-BI7UTJ6A.js → create-agent-7W57J7MT.js} +4 -3
  26. package/dist/{create-annotation-SNM2J6S4.js → create-annotation-W2XELBCQ.js} +4 -3
  27. package/dist/{create-api-key-23MXYC2L.js → create-api-key-MKRXKFJM.js} +4 -3
  28. package/dist/{create-dashboard-CPOOOIKK.js → create-dashboard-QGL7IODC.js} +4 -3
  29. package/dist/{create-dataset-BVXPVVY2.js → create-dataset-3PH7A4HG.js} +4 -3
  30. package/dist/{create-dataset-records-S7VFIIMR.js → create-dataset-records-4DZS3VRL.js} +4 -3
  31. package/dist/{create-evaluator-WJAAQOUE.js → create-evaluator-YWH6NC4M.js} +4 -3
  32. package/dist/create-mcp-server.js +3 -2
  33. package/dist/{create-project-BF7LMZH5.js → create-project-BOBQ32C5.js} +4 -3
  34. package/dist/{create-prompt-LZ3QHSU4.js → create-prompt-FLCFGITE.js} +3 -2
  35. package/dist/{create-prompt-tag-IF4J24QM.js → create-prompt-tag-EMPNIKEA.js} +3 -2
  36. package/dist/{create-scenario-BK4E5DEU.js → create-scenario-BSBIHUL6.js} +4 -3
  37. package/dist/{create-suite-HEQPNLND.js → create-suite-TWF6BBR4.js} +4 -3
  38. package/dist/{delete-agent-Y4T2G5PT.js → delete-agent-7MSYNY5E.js} +4 -3
  39. package/dist/{delete-annotation-KYDMQTZG.js → delete-annotation-ZT5IGN24.js} +4 -3
  40. package/dist/{delete-dashboard-3RW647WJ.js → delete-dashboard-BTIY5S3L.js} +4 -3
  41. package/dist/{delete-dataset-SZAHFHHS.js → delete-dataset-U45HQHPR.js} +4 -3
  42. package/dist/{delete-dataset-records-ISHKGEBH.js → delete-dataset-records-R43IMAWB.js} +4 -3
  43. package/dist/{delete-evaluator-VKIG5I3S.js → delete-evaluator-U6JMQ6C7.js} +4 -3
  44. package/dist/{delete-prompt-tag-VYYL4GTF.js → delete-prompt-tag-4EV4TDHB.js} +3 -2
  45. package/dist/{delete-workflow-SHNPYSWQ.js → delete-workflow-5QYALXEL.js} +4 -3
  46. package/dist/discover-evaluator-schema-MRGHVNTT.js +2175 -0
  47. package/dist/{discover-scenario-schema-MEEEVND7.js → discover-scenario-schema-UPBTCWPG.js} +3 -1
  48. package/dist/{discover-schema-3T52ORPB.js → discover-schema-ICGXPHUW.js} +3 -1
  49. package/dist/{get-agent-IZSP7AR5.js → get-agent-7QJVPH72.js} +4 -3
  50. package/dist/{get-analytics-EDFAYYIQ.js → get-analytics-PVERWOA5.js} +3 -2
  51. package/dist/{get-dashboard-WSAO6IQ4.js → get-dashboard-ZZPC3WMD.js} +4 -3
  52. package/dist/{get-dataset-BEVNXQNF.js → get-dataset-25TB2LQH.js} +4 -3
  53. package/dist/{get-evaluator-HCVXNPZR.js → get-evaluator-3FTRQKEC.js} +4 -3
  54. package/dist/{get-project-H4V3GYRQ.js → get-project-5OAXYC57.js} +4 -3
  55. package/dist/{get-prompt-EWRJVVGB.js → get-prompt-I753LZKZ.js} +3 -2
  56. package/dist/{get-scenario-24YUJS5W.js → get-scenario-DSSTR6FI.js} +4 -3
  57. package/dist/{get-simulation-run-UX2W6F6I.js → get-simulation-run-RQPMS2MC.js} +4 -3
  58. package/dist/{get-suite-B65ZKZJA.js → get-suite-GTOCY5PL.js} +4 -3
  59. package/dist/{get-trace-ZR7MIITH.js → get-trace-O5VT3NAC.js} +7 -9
  60. package/dist/{get-workflow-YROHUQJW.js → get-workflow-3EZDOEIF.js} +4 -3
  61. package/dist/http-server-2ACJ3CXW.js +24366 -0
  62. package/dist/index.js +20935 -1751
  63. package/dist/{langwatch-api-annotations-OPEXNTPF.js → langwatch-api-annotations-E65MI3WZ.js} +4 -3
  64. package/dist/{langwatch-api-dashboards-NPQM6QJI.js → langwatch-api-dashboards-QCDECRCY.js} +4 -3
  65. package/dist/{langwatch-api-monitors-3L25OOOS.js → langwatch-api-monitors-UIO2KTDV.js} +3 -2
  66. package/dist/{langwatch-api-secrets-ZFURUA62.js → langwatch-api-secrets-4T2EKSJ7.js} +3 -2
  67. package/dist/{langwatch-api-triggers-XGXXT7DD.js → langwatch-api-triggers-MPOEKPUY.js} +3 -2
  68. package/dist/{langwatch-api-workflows-CXCTTCXT.js → langwatch-api-workflows-5HRZ7HR4.js} +4 -3
  69. package/dist/{list-agents-6ZNOV7FK.js → list-agents-TIJTP566.js} +4 -3
  70. package/dist/{list-annotations-6P3IWILH.js → list-annotations-CGX5BT7E.js} +4 -3
  71. package/dist/{list-api-keys-JJI62KD2.js → list-api-keys-JLWJPFRX.js} +4 -3
  72. package/dist/{list-dashboards-G2WJOXWB.js → list-dashboards-RNIWX3TX.js} +4 -3
  73. package/dist/{list-dataset-records-GWWVOCII.js → list-dataset-records-B5MHZR66.js} +4 -3
  74. package/dist/{list-datasets-HBTH33X4.js → list-datasets-BVCAMQCP.js} +4 -3
  75. package/dist/{list-evaluators-K5OGB6TX.js → list-evaluators-65FBTL2U.js} +4 -3
  76. package/dist/{list-model-providers-JMOGZLSO.js → list-model-providers-GRK3I3VV.js} +4 -3
  77. package/dist/{list-projects-OXJC2NW5.js → list-projects-JLMBFMW3.js} +4 -3
  78. package/dist/{list-prompt-tags-J2HPHVAZ.js → list-prompt-tags-UHNWDGQI.js} +3 -2
  79. package/dist/{list-prompts-C7GR72XL.js → list-prompts-7254B3U7.js} +3 -2
  80. package/dist/{list-scenarios-JEPGIIDO.js → list-scenarios-46YNICKM.js} +4 -3
  81. package/dist/{list-simulation-runs-DRSAGPEC.js → list-simulation-runs-LGNEFNPN.js} +4 -3
  82. package/dist/{list-suites-POTZ3ZT6.js → list-suites-5VK4HXEK.js} +4 -3
  83. package/dist/{list-workflows-QQZQIOHB.js → list-workflows-MLQDM6CP.js} +4 -3
  84. package/dist/{rename-prompt-tag-4AAV2BS4.js → rename-prompt-tag-RHQMNEQL.js} +3 -2
  85. package/dist/report-issue-DXJ6TPTC.js +412 -0
  86. package/dist/{revoke-api-key-TONB4ADQ.js → revoke-api-key-3Y35EMBT.js} +4 -3
  87. package/dist/{run-agent-7QE3YEH7.js → run-agent-RBSIFKRT.js} +4 -3
  88. package/dist/{run-suite-Q66OSPO4.js → run-suite-K3DNQ3SE.js} +4 -3
  89. package/dist/{run-workflow-RWX2D752.js → run-workflow-5LVBZYLS.js} +4 -3
  90. package/dist/{search-traces-M5E5VB4X.js → search-traces-U4HLOCZ2.js} +9 -2
  91. package/dist/{set-model-provider-XENT5D2P.js → set-model-provider-V3M5CDIY.js} +4 -3
  92. package/dist/{update-agent-GML25EE3.js → update-agent-47RZIHS7.js} +4 -3
  93. package/dist/{update-dataset-R4MM6NNY.js → update-dataset-G6QJJGS5.js} +4 -3
  94. package/dist/{update-dataset-record-FN4BL5P4.js → update-dataset-record-RMWLSWCS.js} +4 -3
  95. package/dist/{update-evaluator-LH2RDDB5.js → update-evaluator-SFRRI5Q3.js} +4 -3
  96. package/dist/{update-project-H53IHRBB.js → update-project-PI5DWBVK.js} +4 -3
  97. package/dist/{update-prompt-M33OTTAJ.js → update-prompt-RQFTEO7Y.js} +3 -2
  98. package/dist/{update-scenario-FKTQRAFQ.js → update-scenario-G3IU2UIB.js} +4 -3
  99. package/dist/{update-suite-JJTNOTXN.js → update-suite-TRGGLNR7.js} +4 -3
  100. package/package.json +20 -18
  101. package/pnpm-workspace.yaml +20 -3
  102. package/pyproject.toml +9 -1
  103. package/src/__tests__/all-tools.integration.test.ts +56 -120
  104. package/src/__tests__/documentation-fetch-security.integration.test.ts +176 -0
  105. package/src/__tests__/langwatch-api.unit.test.ts +206 -0
  106. package/src/__tests__/public-http-request-security.integration.test.ts +118 -0
  107. package/src/__tests__/report-issue.integration.test.ts +147 -0
  108. package/src/__tests__/tools.unit.test.ts +24 -0
  109. package/src/create-mcp-server.ts +43 -29
  110. package/src/documentation-fetch.ts +111 -0
  111. package/src/langwatch-api-agents.ts +2 -2
  112. package/src/langwatch-api.ts +138 -8
  113. package/src/public-http-request.ts +242 -0
  114. package/src/tools/get-trace.ts +2 -12
  115. package/src/tools/report-issue.ts +150 -0
  116. package/src/tools/search-traces.ts +4 -0
  117. package/src/utils/format-evaluations.ts +20 -0
  118. package/tsconfig.json +8 -1
  119. package/uv.lock +259 -205
  120. package/dist/chunk-IUP5IHBD.js +0 -71
  121. package/dist/discover-evaluator-schema-H23XCLNE.js +0 -1402
  122. package/dist/http-server-GXVEDA7G.js +0 -226
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langwatch/mcp-server",
3
- "version": "0.10.1",
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": "^2.1.119",
39
+ "@anthropic-ai/claude-code": "2.1.202",
40
40
  "@eslint/js": "^10.0.1",
41
41
  "@langwatch/scenario": "^0.4.6",
42
- "@types/debug": "^4.1.12",
43
- "@types/eslint__js": "^8.42.3",
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.20250911.1",
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.25.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.26.0",
59
- "@opentelemetry/context-async-hooks": "^2.1.0",
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"
@@ -1,17 +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
- # Security: protobufjs code execution + prototype-pollution + DoS
13
+ # Security: protobufjs code execution + prototype-pollution + DoS + Any-expansion DoS + property shadowing
5
14
  # (GHSA-xq3m-2v4x-88gg, GHSA-66ff-xgx4-vchm, GHSA-75px-5xx7-5xc7,
6
- # GHSA-jvwf-75h9-cwgg, GHSA-685m-2w69-288q)
7
- "protobufjs": ">=7.5.6 <8.0.0 || >=8.0.2"
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"
8
17
  # Security: langsmith untrusted manifest deserialization (GHSA-3644-q5cj-c5c7).
9
18
  # langsmith is only a transitive of the @langwatch/scenario devDependency
10
19
  # (via langchain / @langchain/core); pinned to a known-safe version.
11
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"
12
27
  # Security: fast-uri path traversal + host confusion
13
28
  "fast-uri@<3.1.2": "3.1.2"
14
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"
15
32
 
16
33
  onlyBuiltDependencies:
17
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.77.0",
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
- describe("when fetching the docs index", () => {
647
- it("returns content from the langwatch docs URL", async () => {
648
- // Intercept fetch to avoid hitting the real network
649
- const mockFetch = vi.fn().mockResolvedValue({
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
- describe("when a relative path is provided", () => {
697
- it("prepends the docs base URL", async () => {
698
- const mockFetch = vi.fn().mockResolvedValue({
699
- text: () => Promise.resolve("# Getting Started"),
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
- expect(mockFetch).toHaveBeenCalledWith(
717
- "https://langwatch.ai/docs/getting-started.md",
718
- );
666
+ const text = await fetchDocumentation(
667
+ "langwatch",
668
+ "integration",
669
+ mockFetch,
670
+ );
719
671
 
720
- globalThis.fetch = originalFetch;
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
- describe("when fetching the scenario docs index", () => {
730
- it("returns content from the scenario docs URL", async () => {
731
- const mockFetch = vi.fn().mockResolvedValue({
732
- text: () =>
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
- describe("when fetching a specific scenario doc page", () => {
751
- it("appends .md extension and prepends base URL for relative paths", async () => {
752
- const mockFetch = vi.fn().mockResolvedValue({
753
- text: () => Promise.resolve("# Setup Guide"),
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
+ });