@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.
Files changed (144) hide show
  1. package/.python-version +1 -0
  2. package/.release-please-shim +1 -1
  3. package/CHANGELOG.md +75 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +64 -18
  6. package/dist/{archive-project-KAZFHR4A.js → archive-project-4EQGLBJJ.js} +5 -4
  7. package/dist/{archive-scenario-6M2IB7Y2.js → archive-scenario-W6BXYDZ3.js} +5 -4
  8. package/dist/{archive-suite-NXZ3YDS3.js → archive-suite-4T6BSRS7.js} +5 -4
  9. package/dist/{assign-prompt-tag-WPUJO55U.js → assign-prompt-tag-VLXUWEKU.js} +4 -3
  10. package/dist/chunk-37SMPMQS.js +12 -0
  11. package/dist/{chunk-HRFQAFPR.js → chunk-4QC7SA2L.js} +221 -134
  12. package/dist/{chunk-XRQNXN2Z.js → chunk-5CCZFUXS.js} +2 -2
  13. package/dist/{chunk-ITN2QGHF.js → chunk-77BNWQ3U.js} +2 -2
  14. package/dist/{chunk-B6UV3JVI.js → chunk-7RHG56OH.js} +2 -2
  15. package/dist/{chunk-CAY57CKF.js → chunk-A2LEVYQ5.js} +78 -5
  16. package/dist/{chunk-U2QIW2SW.js → chunk-AQJO2EI5.js} +2 -2
  17. package/dist/{chunk-GWEW5XON.js → chunk-CDYQOY5U.js} +2 -2
  18. package/dist/{chunk-LTEJDL4V.js → chunk-J42FWRJ2.js} +2 -2
  19. package/dist/chunk-SAS5OB7F.js +58 -0
  20. package/dist/{chunk-AGGE6NPR.js → chunk-SFB4MU6H.js} +2 -2
  21. package/dist/{chunk-COCNCXVJ.js → chunk-TJCXFNDT.js} +2 -2
  22. package/dist/chunk-TOXJQDIZ.js +432 -0
  23. package/dist/chunk-UJPXFA26.js +267 -0
  24. package/dist/{chunk-KOO5733S.js → chunk-WT4PO4A3.js} +8 -2
  25. package/dist/{chunk-RDUXCCFR.js → chunk-YX3A3CFM.js} +2 -2
  26. package/dist/{chunk-UKRCZJJC.js → chunk-YZLYHLLI.js} +2 -2
  27. package/dist/{chunk-ESGZVPPC.js → chunk-ZWIL4EFL.js} +2 -2
  28. package/dist/config.js +2 -1
  29. package/dist/{create-agent-BI7UTJ6A.js → create-agent-7XHO6UIS.js} +5 -4
  30. package/dist/{create-annotation-SNM2J6S4.js → create-annotation-YQXRQC6A.js} +5 -4
  31. package/dist/{create-api-key-23MXYC2L.js → create-api-key-YUS36KBU.js} +5 -4
  32. package/dist/{create-dashboard-CPOOOIKK.js → create-dashboard-4Z5JO5LB.js} +5 -4
  33. package/dist/{create-dataset-BVXPVVY2.js → create-dataset-E6NMSVX7.js} +5 -4
  34. package/dist/{create-dataset-records-S7VFIIMR.js → create-dataset-records-SKHPRD2K.js} +5 -4
  35. package/dist/{create-evaluator-WJAAQOUE.js → create-evaluator-JXF5PF33.js} +5 -4
  36. package/dist/create-mcp-server.js +4 -3
  37. package/dist/{create-project-BF7LMZH5.js → create-project-CS2CB57Y.js} +5 -4
  38. package/dist/{create-prompt-LZ3QHSU4.js → create-prompt-F5RMX3L3.js} +4 -3
  39. package/dist/{create-prompt-tag-IF4J24QM.js → create-prompt-tag-GMRWTEFR.js} +4 -3
  40. package/dist/{create-scenario-BK4E5DEU.js → create-scenario-JDR2VB2H.js} +5 -4
  41. package/dist/{create-suite-HEQPNLND.js → create-suite-LZS6QIAF.js} +5 -4
  42. package/dist/{delete-agent-Y4T2G5PT.js → delete-agent-O3EEH4S5.js} +5 -4
  43. package/dist/{delete-annotation-KYDMQTZG.js → delete-annotation-XP6FQM7A.js} +5 -4
  44. package/dist/{delete-dashboard-3RW647WJ.js → delete-dashboard-RFUNYCNP.js} +5 -4
  45. package/dist/{delete-dataset-SZAHFHHS.js → delete-dataset-3MJFRDLP.js} +5 -4
  46. package/dist/{delete-dataset-records-ISHKGEBH.js → delete-dataset-records-WPMU5GL5.js} +5 -4
  47. package/dist/{delete-evaluator-VKIG5I3S.js → delete-evaluator-33SP2JFX.js} +5 -4
  48. package/dist/{delete-prompt-tag-VYYL4GTF.js → delete-prompt-tag-KNMZVSPB.js} +4 -3
  49. package/dist/{delete-workflow-SHNPYSWQ.js → delete-workflow-3NUXO46U.js} +5 -4
  50. package/dist/discover-evaluator-schema-XTAD4X7D.js +2191 -0
  51. package/dist/{discover-scenario-schema-MEEEVND7.js → discover-scenario-schema-FE64QK2D.js} +3 -1
  52. package/dist/{discover-schema-3T52ORPB.js → discover-schema-L6QDPON3.js} +3 -1
  53. package/dist/{get-agent-IZSP7AR5.js → get-agent-Y7MWZ65F.js} +5 -4
  54. package/dist/{get-analytics-EDFAYYIQ.js → get-analytics-TETCSOK6.js} +4 -3
  55. package/dist/{get-dashboard-WSAO6IQ4.js → get-dashboard-KHBEGZGL.js} +5 -4
  56. package/dist/{get-dataset-BEVNXQNF.js → get-dataset-RCMU5VAH.js} +5 -4
  57. package/dist/{get-evaluator-HCVXNPZR.js → get-evaluator-ADMKVWYZ.js} +5 -4
  58. package/dist/{get-project-H4V3GYRQ.js → get-project-RAK7URQJ.js} +5 -4
  59. package/dist/{get-prompt-EWRJVVGB.js → get-prompt-OY5B7TRI.js} +4 -3
  60. package/dist/{get-scenario-24YUJS5W.js → get-scenario-ZTAJJL5T.js} +5 -4
  61. package/dist/{get-simulation-run-UX2W6F6I.js → get-simulation-run-T2ZQWYH2.js} +5 -4
  62. package/dist/{get-suite-B65ZKZJA.js → get-suite-J7JVWO6O.js} +5 -4
  63. package/dist/{get-trace-ZR7MIITH.js → get-trace-K7U234Q4.js} +8 -10
  64. package/dist/{get-workflow-YROHUQJW.js → get-workflow-L5L2PSK3.js} +5 -4
  65. package/dist/http-security-EU3CHTY7.js +26 -0
  66. package/dist/http-server-YXHFPPGA.js +24706 -0
  67. package/dist/index.js +23280 -3135
  68. package/dist/{langwatch-api-annotations-OPEXNTPF.js → langwatch-api-annotations-FDAS6NQ7.js} +5 -4
  69. package/dist/{langwatch-api-dashboards-NPQM6QJI.js → langwatch-api-dashboards-JTVCS7ZJ.js} +5 -4
  70. package/dist/{langwatch-api-monitors-3L25OOOS.js → langwatch-api-monitors-3J7UIQME.js} +4 -3
  71. package/dist/{langwatch-api-secrets-ZFURUA62.js → langwatch-api-secrets-BUBSNCNO.js} +4 -3
  72. package/dist/{langwatch-api-triggers-XGXXT7DD.js → langwatch-api-triggers-EDFUCJN6.js} +4 -3
  73. package/dist/langwatch-api-workflows-NBJX5KOG.js +16 -0
  74. package/dist/{list-agents-6ZNOV7FK.js → list-agents-KFHKGPXV.js} +5 -4
  75. package/dist/{list-annotations-6P3IWILH.js → list-annotations-ZTVP4EEL.js} +5 -4
  76. package/dist/{list-api-keys-JJI62KD2.js → list-api-keys-CWD4UQT5.js} +5 -4
  77. package/dist/{list-dashboards-G2WJOXWB.js → list-dashboards-PWG6P6KR.js} +5 -4
  78. package/dist/{list-dataset-records-GWWVOCII.js → list-dataset-records-IOE7LQXS.js} +5 -4
  79. package/dist/{list-datasets-HBTH33X4.js → list-datasets-ZT4JYUDT.js} +5 -4
  80. package/dist/{list-evaluators-K5OGB6TX.js → list-evaluators-SJHC7VBH.js} +5 -4
  81. package/dist/{list-model-providers-JMOGZLSO.js → list-model-providers-PPD2WPD2.js} +5 -4
  82. package/dist/{list-projects-OXJC2NW5.js → list-projects-K7QAIX2I.js} +5 -4
  83. package/dist/{list-prompt-tags-J2HPHVAZ.js → list-prompt-tags-FOO6TNHB.js} +4 -3
  84. package/dist/{list-prompts-C7GR72XL.js → list-prompts-SC2HAUFG.js} +4 -3
  85. package/dist/{list-scenarios-JEPGIIDO.js → list-scenarios-F2WYNRIE.js} +5 -4
  86. package/dist/{list-simulation-runs-DRSAGPEC.js → list-simulation-runs-KF5THG5H.js} +5 -4
  87. package/dist/{list-suites-POTZ3ZT6.js → list-suites-CRY4VWW6.js} +5 -4
  88. package/dist/{list-workflows-QQZQIOHB.js → list-workflows-QSJWURCK.js} +5 -4
  89. package/dist/{rename-prompt-tag-4AAV2BS4.js → rename-prompt-tag-EKWU5QF3.js} +4 -3
  90. package/dist/report-issue-XYVAPAYJ.js +412 -0
  91. package/dist/{revoke-api-key-TONB4ADQ.js → revoke-api-key-FTRTZMOF.js} +5 -4
  92. package/dist/{run-agent-7QE3YEH7.js → run-agent-6SM4XNA7.js} +5 -4
  93. package/dist/{run-suite-Q66OSPO4.js → run-suite-NKZCJ2IN.js} +5 -4
  94. package/dist/{run-workflow-RWX2D752.js → run-workflow-QPXC627O.js} +5 -4
  95. package/dist/{search-traces-M5E5VB4X.js → search-traces-A6HFKPLJ.js} +10 -3
  96. package/dist/{set-model-provider-XENT5D2P.js → set-model-provider-3WKT63ZN.js} +5 -4
  97. package/dist/{update-agent-GML25EE3.js → update-agent-SMAEHLXH.js} +5 -4
  98. package/dist/{update-dataset-R4MM6NNY.js → update-dataset-BDUK7TUO.js} +5 -4
  99. package/dist/{update-dataset-record-FN4BL5P4.js → update-dataset-record-4ZSDRBSW.js} +5 -4
  100. package/dist/{update-evaluator-LH2RDDB5.js → update-evaluator-LS6TIBY7.js} +5 -4
  101. package/dist/{update-project-H53IHRBB.js → update-project-LKVYXENH.js} +5 -4
  102. package/dist/{update-prompt-M33OTTAJ.js → update-prompt-ODCRQRN6.js} +4 -3
  103. package/dist/{update-scenario-FKTQRAFQ.js → update-scenario-NEXCAQI7.js} +5 -4
  104. package/dist/{update-suite-JJTNOTXN.js → update-suite-SNO3GS7T.js} +5 -4
  105. package/package.json +20 -18
  106. package/pyproject.toml +10 -2
  107. package/src/__tests__/all-tools.integration.test.ts +56 -120
  108. package/src/__tests__/config.unit.test.ts +33 -0
  109. package/src/__tests__/docker.integration.test.ts +91 -7
  110. package/src/__tests__/documentation-fetch-security.integration.test.ts +182 -0
  111. package/src/__tests__/feature-map-tool-drift.unit.test.ts +93 -0
  112. package/src/__tests__/http-security-origin.unit.test.ts +97 -0
  113. package/src/__tests__/http-security-sessions.unit.test.ts +209 -0
  114. package/src/__tests__/http-security-verifier.unit.test.ts +248 -0
  115. package/src/__tests__/http-server-allocation.integration.test.ts +279 -0
  116. package/src/__tests__/http-server-origin.integration.test.ts +169 -0
  117. package/src/__tests__/http-server-session-auth.integration.test.ts +341 -0
  118. package/src/__tests__/http-transport.integration.test.ts +85 -87
  119. package/src/__tests__/langwatch-api.unit.test.ts +206 -0
  120. package/src/__tests__/public-http-request-security.integration.test.ts +118 -0
  121. package/src/__tests__/report-issue.integration.test.ts +147 -0
  122. package/src/__tests__/support/http-server-harness.ts +138 -0
  123. package/src/__tests__/tools.unit.test.ts +24 -0
  124. package/src/config.ts +17 -2
  125. package/src/create-mcp-server.ts +43 -29
  126. package/src/documentation-fetch.ts +111 -0
  127. package/src/http-security.ts +492 -0
  128. package/src/http-server.ts +536 -152
  129. package/src/index.ts +49 -2
  130. package/src/langwatch-api-agents.ts +2 -2
  131. package/src/langwatch-api.ts +138 -8
  132. package/src/public-http-request.ts +242 -0
  133. package/src/tools/discover-evaluator-schema.ts +2 -2
  134. package/src/tools/get-trace.ts +2 -12
  135. package/src/tools/report-issue.ts +150 -0
  136. package/src/tools/search-traces.ts +4 -0
  137. package/src/utils/format-evaluations.ts +20 -0
  138. package/tsconfig.json +8 -1
  139. package/uv.lock +261 -207
  140. package/dist/chunk-IUP5IHBD.js +0 -71
  141. package/dist/discover-evaluator-schema-H23XCLNE.js +0 -1402
  142. package/dist/http-server-GXVEDA7G.js +0 -226
  143. package/dist/langwatch-api-workflows-CXCTTCXT.js +0 -15
  144. package/pnpm-workspace.yaml +0 -17
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  updateSuite
3
- } from "./chunk-UKRCZJJC.js";
4
- import "./chunk-CAY57CKF.js";
5
- import "./chunk-KOO5733S.js";
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-JJTNOTXN.js.map
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.10.1",
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": "^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/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.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"
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
- langwatch = { path = "../python-sdk", editable = true }
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
- 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
  // =====================
@@ -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
- const BEARER_TOKEN = "test-docker-api-key";
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-server/Dockerfile .`,
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("CORS headers are present", async () => {
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);