@langwatch/mcp-server 0.6.1 → 0.8.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 (241) hide show
  1. package/.release-please-shim +1 -0
  2. package/CHANGELOG.md +89 -0
  3. package/README.md +51 -8
  4. package/dist/{archive-scenario-YFD5THOR.js → archive-scenario-YKGLJ4MZ.js} +4 -4
  5. package/dist/archive-suite-QFNDO3Y3.js +15 -0
  6. package/dist/assign-prompt-tag-Y2B4CBBT.js +19 -0
  7. package/dist/chunk-B57MZ4WI.js +1573 -0
  8. package/dist/{chunk-IX6QJKAD.js → chunk-CPUPGDNA.js} +2 -2
  9. package/dist/chunk-FBRKHHJV.js +48 -0
  10. package/dist/{chunk-5UOPNRXW.js → chunk-HDJDFKZR.js} +10 -3
  11. package/dist/chunk-KOO5733S.js +72 -0
  12. package/dist/chunk-KZNBCKWG.js +39 -0
  13. package/dist/chunk-L6QENXYA.js +42 -0
  14. package/dist/{chunk-6U4TCGFC.js → chunk-NACSR6B7.js} +2 -2
  15. package/dist/chunk-NUFPTHKU.js +9 -0
  16. package/dist/chunk-OVQF3D5R.js +86 -0
  17. package/dist/chunk-PO7FM7IX.js +35 -0
  18. package/dist/chunk-PZUWCWOS.js +25 -0
  19. package/dist/chunk-WVI5CJ6S.js +21 -0
  20. package/dist/{chunk-LLRQIF52.js → chunk-YH2IBTEL.js} +54 -6
  21. package/dist/chunk-YI3OG4PI.js +71 -0
  22. package/dist/config.d.ts +17 -0
  23. package/dist/config.js +13 -0
  24. package/dist/create-agent-YRSDH7P7.js +23 -0
  25. package/dist/create-annotation-FYQNKMVJ.js +24 -0
  26. package/dist/create-dashboard-CORBR4YQ.js +18 -0
  27. package/dist/create-dataset-ZC63T6BB.js +22 -0
  28. package/dist/create-dataset-records-ILVNVKVR.js +22 -0
  29. package/dist/{create-evaluator-E5X5ZP3B.js → create-evaluator-2AU3PXFB.js} +4 -4
  30. package/dist/create-mcp-server.d.ts +8 -0
  31. package/dist/create-mcp-server.js +8 -0
  32. package/dist/{create-prompt-7Z35MIL6.js → create-prompt-NKRJ47YY.js} +7 -4
  33. package/dist/create-prompt-tag-S4JX3ULL.js +17 -0
  34. package/dist/{create-scenario-DIMPJRPY.js → create-scenario-BPRF75KF.js} +4 -4
  35. package/dist/create-suite-4YN7D6K2.js +38 -0
  36. package/dist/delete-agent-Y72R2YQA.js +18 -0
  37. package/dist/delete-annotation-UHWDMHSH.js +17 -0
  38. package/dist/delete-dashboard-LPWT36KN.js +18 -0
  39. package/dist/delete-dataset-GGPQRHKX.js +20 -0
  40. package/dist/delete-dataset-records-LTG5LT26.js +21 -0
  41. package/dist/delete-evaluator-F7QLXDHU.js +15 -0
  42. package/dist/delete-prompt-tag-B5UKZL45.js +17 -0
  43. package/dist/delete-workflow-4DGUDIDE.js +17 -0
  44. package/dist/get-agent-YEVICUX4.js +28 -0
  45. package/dist/{get-analytics-4YJW4S5L.js → get-analytics-3W5CMU3F.js} +22 -5
  46. package/dist/get-dashboard-NCQHN6MW.js +29 -0
  47. package/dist/get-dataset-KBK6PH36.js +48 -0
  48. package/dist/{get-evaluator-WDEH2F7M.js → get-evaluator-QDUL5LXR.js} +4 -4
  49. package/dist/{get-prompt-F6PDVC76.js → get-prompt-XCSSNRKR.js} +4 -4
  50. package/dist/{get-scenario-H24ZYNT5.js → get-scenario-FNRKOY7E.js} +4 -4
  51. package/dist/get-simulation-run-WOEKUF7K.js +56 -0
  52. package/dist/get-suite-U5AOD5TC.js +44 -0
  53. package/dist/{get-trace-27USKGO7.js → get-trace-MUWEJM2Z.js} +3 -3
  54. package/dist/get-workflow-SOA7MT7L.js +25 -0
  55. package/dist/http-server-7DMOCB75.js +224 -0
  56. package/dist/index.js +61640 -31868
  57. package/dist/langwatch-api-annotations-Q57NDUVR.js +17 -0
  58. package/dist/langwatch-api-dashboards-VBWWXV3Y.js +17 -0
  59. package/dist/langwatch-api-monitors-L3IY7FDT.js +52 -0
  60. package/dist/langwatch-api-secrets-S5HHF3XR.js +40 -0
  61. package/dist/langwatch-api-triggers-ME33QE26.js +30 -0
  62. package/dist/langwatch-api-workflows-4CJQAYP2.js +15 -0
  63. package/dist/list-agents-3HGIBJFM.js +32 -0
  64. package/dist/list-annotations-KWXYVEPI.js +33 -0
  65. package/dist/list-dashboards-VQ2LH666.js +32 -0
  66. package/dist/list-dataset-records-RNRGROJP.js +39 -0
  67. package/dist/list-datasets-I4A2LT2S.js +60 -0
  68. package/dist/{list-evaluators-KRGI72EH.js → list-evaluators-AKKRJ5MX.js} +4 -4
  69. package/dist/{list-model-providers-A5YCFTPI.js → list-model-providers-DVFEHFHZ.js} +4 -4
  70. package/dist/list-prompt-tags-PL5OISUA.js +23 -0
  71. package/dist/{list-prompts-LKJSE7XN.js → list-prompts-7VTFWE2Z.js} +3 -3
  72. package/dist/{list-scenarios-ZK5CMGC4.js → list-scenarios-5BZWZ6ZN.js} +4 -4
  73. package/dist/list-simulation-runs-WCZ6RLPE.js +43 -0
  74. package/dist/list-suites-RRQ2JX5Y.js +42 -0
  75. package/dist/list-workflows-UQB5FCX5.js +35 -0
  76. package/dist/rename-prompt-tag-TURD6KYU.js +18 -0
  77. package/dist/run-agent-BEJ4K4CM.js +36 -0
  78. package/dist/run-evaluation-AJ7WZNQD.js +64 -0
  79. package/dist/run-suite-U7V432G2.js +29 -0
  80. package/dist/run-workflow-5WX7DK5T.js +36 -0
  81. package/dist/{search-traces-SOKAAMAR.js → search-traces-ZCHNOMTX.js} +3 -3
  82. package/dist/{set-model-provider-7MGULZDH.js → set-model-provider-6CFHMCNE.js} +4 -4
  83. package/dist/update-agent-UE6UWLZP.js +20 -0
  84. package/dist/update-dataset-4XQZZE4S.js +22 -0
  85. package/dist/update-dataset-record-GNNM5TKM.js +19 -0
  86. package/dist/{update-evaluator-A3XINFLJ.js → update-evaluator-LHVDXIZD.js} +4 -4
  87. package/dist/{update-prompt-IW7X2UQM.js → update-prompt-3XEHECJ7.js} +5 -3
  88. package/dist/{update-scenario-ZT7TOBFR.js → update-scenario-DFPREYCD.js} +4 -4
  89. package/dist/update-suite-RATU5THO.js +39 -0
  90. package/package.json +22 -8
  91. package/src/__tests__/all-tools.integration.test.ts +353 -0
  92. package/src/__tests__/config.unit.test.ts +81 -1
  93. package/src/__tests__/datasets.integration.test.ts +620 -0
  94. package/src/__tests__/datasets.unit.test.ts +244 -0
  95. package/src/__tests__/docker.integration.test.ts +234 -0
  96. package/src/__tests__/http-transport.integration.test.ts +397 -0
  97. package/src/__tests__/langwatch-api.unit.test.ts +174 -0
  98. package/src/__tests__/scenario-tools.unit.test.ts +3 -0
  99. package/src/__tests__/tools.unit.test.ts +177 -0
  100. package/src/config.d.ts +15 -0
  101. package/src/config.ts +85 -7
  102. package/src/create-mcp-server.d.ts +6 -0
  103. package/src/create-mcp-server.ts +1799 -0
  104. package/src/http-server.ts +329 -0
  105. package/src/index.ts +22 -593
  106. package/src/langwatch-api-agents.ts +108 -0
  107. package/src/langwatch-api-annotations.ts +49 -0
  108. package/src/langwatch-api-dashboards.ts +48 -0
  109. package/src/langwatch-api-datasets.ts +191 -0
  110. package/src/langwatch-api-evaluators.ts +10 -0
  111. package/src/langwatch-api-monitors.ts +82 -0
  112. package/src/langwatch-api-secrets.ts +48 -0
  113. package/src/langwatch-api-simulation-runs.ts +51 -0
  114. package/src/langwatch-api-suites.ts +118 -0
  115. package/src/langwatch-api-triggers.ts +48 -0
  116. package/src/langwatch-api-workflows.ts +41 -0
  117. package/src/langwatch-api.ts +65 -3
  118. package/src/schemas/create-dataset.ts +48 -0
  119. package/src/schemas/index.ts +1 -0
  120. package/src/tools/archive-suite.ts +12 -0
  121. package/src/tools/assign-prompt-tag.ts +16 -0
  122. package/src/tools/create-agent.ts +15 -0
  123. package/src/tools/create-annotation.ts +18 -0
  124. package/src/tools/create-dashboard.ts +7 -0
  125. package/src/tools/create-dataset-records.ts +23 -0
  126. package/src/tools/create-dataset.ts +22 -0
  127. package/src/tools/create-prompt-tag.ts +12 -0
  128. package/src/tools/create-prompt.ts +4 -0
  129. package/src/tools/create-suite.ts +42 -0
  130. package/src/tools/delete-agent.ts +7 -0
  131. package/src/tools/delete-annotation.ts +7 -0
  132. package/src/tools/delete-dashboard.ts +7 -0
  133. package/src/tools/delete-dataset-records.ts +22 -0
  134. package/src/tools/delete-dataset.ts +20 -0
  135. package/src/tools/delete-evaluator.ts +12 -0
  136. package/src/tools/delete-prompt-tag.ts +12 -0
  137. package/src/tools/delete-workflow.ts +7 -0
  138. package/src/tools/format-dataset-mutation.ts +25 -0
  139. package/src/tools/get-agent.ts +21 -0
  140. package/src/tools/get-analytics.ts +30 -3
  141. package/src/tools/get-dashboard.ts +22 -0
  142. package/src/tools/get-dataset.ts +63 -0
  143. package/src/tools/get-prompt.ts +2 -1
  144. package/src/tools/get-simulation-run.ts +60 -0
  145. package/src/tools/get-suite.ts +47 -0
  146. package/src/tools/get-workflow.ts +17 -0
  147. package/src/tools/list-agents.ts +27 -0
  148. package/src/tools/list-annotations.ts +33 -0
  149. package/src/tools/list-dashboards.ts +27 -0
  150. package/src/tools/list-dataset-records.ts +46 -0
  151. package/src/tools/list-datasets.ts +80 -0
  152. package/src/tools/list-prompt-tags.ts +21 -0
  153. package/src/tools/list-simulation-runs.ts +49 -0
  154. package/src/tools/list-suites.ts +43 -0
  155. package/src/tools/list-workflows.ts +31 -0
  156. package/src/tools/rename-prompt-tag.ts +14 -0
  157. package/src/tools/run-agent.ts +36 -0
  158. package/src/tools/run-evaluation.ts +87 -0
  159. package/src/tools/run-suite.ts +28 -0
  160. package/src/tools/run-workflow.ts +36 -0
  161. package/src/tools/update-agent.ts +13 -0
  162. package/src/tools/update-dataset-record.ts +21 -0
  163. package/src/tools/update-dataset.ts +23 -0
  164. package/src/tools/update-prompt.ts +3 -0
  165. package/src/tools/update-suite.ts +44 -0
  166. package/src/utils/escape-markdown.ts +12 -0
  167. package/uv.lock +188 -530
  168. package/build.js +0 -12
  169. package/dist/archive-scenario-YFD5THOR.js.map +0 -1
  170. package/dist/chunk-5UOPNRXW.js.map +0 -1
  171. package/dist/chunk-6U4TCGFC.js.map +0 -1
  172. package/dist/chunk-AAQNA53E.js +0 -28
  173. package/dist/chunk-AAQNA53E.js.map +0 -1
  174. package/dist/chunk-IX6QJKAD.js.map +0 -1
  175. package/dist/chunk-LLRQIF52.js.map +0 -1
  176. package/dist/chunk-ZXKLPC2E.js.map +0 -1
  177. package/dist/config-FIQWQRUB.js +0 -11
  178. package/dist/config-FIQWQRUB.js.map +0 -1
  179. package/dist/create-evaluator-E5X5ZP3B.js.map +0 -1
  180. package/dist/create-prompt-7Z35MIL6.js.map +0 -1
  181. package/dist/create-scenario-DIMPJRPY.js.map +0 -1
  182. package/dist/discover-evaluator-schema-H23XCLNE.js.map +0 -1
  183. package/dist/discover-scenario-schema-MEEEVND7.js.map +0 -1
  184. package/dist/discover-schema-3T52ORPB.js.map +0 -1
  185. package/dist/get-analytics-4YJW4S5L.js.map +0 -1
  186. package/dist/get-evaluator-WDEH2F7M.js.map +0 -1
  187. package/dist/get-prompt-F6PDVC76.js.map +0 -1
  188. package/dist/get-scenario-H24ZYNT5.js.map +0 -1
  189. package/dist/get-trace-27USKGO7.js.map +0 -1
  190. package/dist/index.js.map +0 -1
  191. package/dist/list-evaluators-KRGI72EH.js.map +0 -1
  192. package/dist/list-model-providers-A5YCFTPI.js.map +0 -1
  193. package/dist/list-prompts-LKJSE7XN.js.map +0 -1
  194. package/dist/list-scenarios-ZK5CMGC4.js.map +0 -1
  195. package/dist/search-traces-SOKAAMAR.js.map +0 -1
  196. package/dist/set-model-provider-7MGULZDH.js.map +0 -1
  197. package/dist/update-evaluator-A3XINFLJ.js.map +0 -1
  198. package/dist/update-prompt-IW7X2UQM.js.map +0 -1
  199. package/dist/update-scenario-ZT7TOBFR.js.map +0 -1
  200. package/tests/evaluations.ipynb +0 -649
  201. package/tests/fixtures/azure/azure_openai_stream_bot_expected.py +0 -102
  202. package/tests/fixtures/azure/azure_openai_stream_bot_input.py +0 -78
  203. package/tests/fixtures/dspy/dspy_bot_expected.py +0 -61
  204. package/tests/fixtures/dspy/dspy_bot_input.py +0 -53
  205. package/tests/fixtures/fastapi/fastapi_app_expected.py +0 -68
  206. package/tests/fixtures/fastapi/fastapi_app_input.py +0 -60
  207. package/tests/fixtures/fastapi/prompt_management_fastapi_expected.py +0 -114
  208. package/tests/fixtures/fastapi/prompt_management_fastapi_input.py +0 -88
  209. package/tests/fixtures/haystack/haystack_bot_expected.py +0 -141
  210. package/tests/fixtures/haystack/haystack_bot_input.py +0 -69
  211. package/tests/fixtures/langchain/langchain_bot_expected.py +0 -53
  212. package/tests/fixtures/langchain/langchain_bot_input.py +0 -45
  213. package/tests/fixtures/langchain/langchain_bot_with_memory_expected.py +0 -69
  214. package/tests/fixtures/langchain/langchain_bot_with_memory_input.py +0 -61
  215. package/tests/fixtures/langchain/langchain_rag_bot_expected.py +0 -97
  216. package/tests/fixtures/langchain/langchain_rag_bot_input.py +0 -77
  217. package/tests/fixtures/langchain/langchain_rag_bot_vertex_ai_expected.py +0 -116
  218. package/tests/fixtures/langchain/langchain_rag_bot_vertex_ai_input.py +0 -81
  219. package/tests/fixtures/langchain/langgraph_rag_bot_with_threads_expected.py +0 -331
  220. package/tests/fixtures/langchain/langgraph_rag_bot_with_threads_input.py +0 -106
  221. package/tests/fixtures/litellm/litellm_bot_expected.py +0 -40
  222. package/tests/fixtures/litellm/litellm_bot_input.py +0 -35
  223. package/tests/fixtures/openai/openai_bot_expected.py +0 -43
  224. package/tests/fixtures/openai/openai_bot_function_call_expected.py +0 -91
  225. package/tests/fixtures/openai/openai_bot_function_call_input.py +0 -82
  226. package/tests/fixtures/openai/openai_bot_input.py +0 -36
  227. package/tests/fixtures/openai/openai_bot_rag_expected.py +0 -73
  228. package/tests/fixtures/openai/openai_bot_rag_input.py +0 -51
  229. package/tests/fixtures/opentelemetry/openinference_dspy_bot_expected.py +0 -63
  230. package/tests/fixtures/opentelemetry/openinference_dspy_bot_input.py +0 -58
  231. package/tests/fixtures/opentelemetry/openinference_langchain_bot_expected.py +0 -53
  232. package/tests/fixtures/opentelemetry/openinference_langchain_bot_input.py +0 -52
  233. package/tests/fixtures/opentelemetry/openinference_openai_bot_expected.py +0 -49
  234. package/tests/fixtures/opentelemetry/openinference_openai_bot_input.py +0 -41
  235. package/tests/fixtures/opentelemetry/openllmetry_openai_bot_expected.py +0 -44
  236. package/tests/fixtures/opentelemetry/openllmetry_openai_bot_input.py +0 -40
  237. package/tests/fixtures/strands/strands_bot_expected.py +0 -84
  238. package/tests/fixtures/strands/strands_bot_input.py +0 -52
  239. package/tests/scenario-openai.test.ts +0 -160
  240. package/tsup.config.ts +0 -10
  241. package/vitest.config.js +0 -7
@@ -0,0 +1,329 @@
1
+ import express from "express";
2
+ import type { Request, Response, NextFunction } from "express";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
6
+ import { randomUUID, createHash } from "node:crypto";
7
+ import type { Server } from "node:http";
8
+
9
+ import { getConfig, runWithConfig } from "./config.js";
10
+ import { createMcpServer } from "./create-mcp-server.js";
11
+
12
+ /**
13
+ * In-memory store for OAuth access tokens.
14
+ * Maps token → API key so we can resolve Bearer tokens from either
15
+ * direct API keys or OAuth-issued access tokens.
16
+ */
17
+ const oauthTokens = new Map<string, { apiKey: string; expiresAt: number }>();
18
+
19
+ /**
20
+ * Extracts the API key from the request's Authorization header.
21
+ * Accepts both direct API keys and OAuth-issued access tokens.
22
+ * Expects the format: `Bearer <key>`.
23
+ */
24
+ function extractBearerToken(req: Request): string | null {
25
+ const authHeader = req.headers.authorization;
26
+ if (!authHeader?.startsWith("Bearer ")) return null;
27
+ const token = authHeader.slice(7) || null;
28
+ if (!token) return null;
29
+
30
+ // Check if this is an OAuth-issued access token
31
+ const oauthEntry = oauthTokens.get(token);
32
+ if (oauthEntry) {
33
+ if (Date.now() < oauthEntry.expiresAt) {
34
+ return oauthEntry.apiKey;
35
+ }
36
+ // Token expired, clean up
37
+ oauthTokens.delete(token);
38
+ return null;
39
+ }
40
+
41
+ // Otherwise treat it as a direct API key
42
+ return token;
43
+ }
44
+
45
+ /**
46
+ * Generates a deterministic but opaque access token from client credentials.
47
+ */
48
+ function generateAccessToken(): string {
49
+ return createHash("sha256").update(randomUUID()).digest("hex");
50
+ }
51
+
52
+ /**
53
+ * Per-session state: the transport and the API key provided at session creation.
54
+ */
55
+ interface SessionState {
56
+ transport: StreamableHTTPServerTransport;
57
+ apiKey: string;
58
+ }
59
+
60
+ /**
61
+ * Wraps the request handling inside `runWithConfig()` so that all downstream
62
+ * tool calls (which read config via `getConfig()`/`requireApiKey()`) see the
63
+ * per-session API key instead of the global one.
64
+ */
65
+ async function handleWithSessionConfig<T>(
66
+ apiKey: string,
67
+ fn: () => Promise<T>
68
+ ): Promise<T> {
69
+ const baseConfig = getConfig();
70
+ return runWithConfig({ ...baseConfig, apiKey }, fn);
71
+ }
72
+
73
+ /**
74
+ * Starts an Express HTTP server with Streamable HTTP and legacy SSE transports
75
+ * for the LangWatch MCP server.
76
+ *
77
+ * Each client session provides its own API key via `Authorization: Bearer <key>`.
78
+ * The key is captured on the initialize request and stored per-session. All
79
+ * subsequent requests in that session use the captured key.
80
+ *
81
+ * Endpoints:
82
+ * - GET /health - Health check for Kubernetes probes (no auth)
83
+ * - POST/GET/DELETE /mcp - Streamable HTTP transport (modern)
84
+ * - GET /sse - Legacy SSE transport (backwards compatibility)
85
+ * - POST /messages - Legacy SSE message endpoint
86
+ */
87
+ export async function startHttpServer({
88
+ port,
89
+ }: {
90
+ port: number;
91
+ }): Promise<{ server: Server; port: number }> {
92
+ const app = express();
93
+ app.set("trust proxy", true);
94
+ app.use(express.json());
95
+
96
+ // CORS middleware for cross-origin requests (ChatGPT/Claude Chat)
97
+ app.use((_req: Request, res: Response, next: NextFunction) => {
98
+ res.header("Access-Control-Allow-Origin", "*");
99
+ res.header(
100
+ "Access-Control-Allow-Methods",
101
+ "GET, POST, DELETE, OPTIONS"
102
+ );
103
+ res.header(
104
+ "Access-Control-Allow-Headers",
105
+ "Content-Type, Authorization, mcp-session-id, MCP-Protocol-Version"
106
+ );
107
+ if (_req.method === "OPTIONS") {
108
+ res.sendStatus(200);
109
+ return;
110
+ }
111
+ next();
112
+ });
113
+
114
+ // Health check endpoint for Kubernetes probes
115
+ app.get("/health", (_req: Request, res: Response) => {
116
+ res.json({ status: "ok" });
117
+ });
118
+
119
+ // --- OAuth 2.0 endpoints (for Claude Desktop and other OAuth-only clients) ---
120
+
121
+ app.get(
122
+ "/.well-known/oauth-authorization-server",
123
+ (_req: Request, res: Response) => {
124
+ const baseUrl = `${_req.protocol}://${_req.get("host")}`;
125
+ res.json({
126
+ issuer: baseUrl,
127
+ token_endpoint: `${baseUrl}/oauth/token`,
128
+ token_endpoint_auth_methods_supported: ["client_secret_post"],
129
+ grant_types_supported: ["client_credentials"],
130
+ response_types_supported: [],
131
+ scopes_supported: ["mcp:tools"],
132
+ });
133
+ }
134
+ );
135
+
136
+ // URL-encoded body parser for OAuth token endpoint (RFC 6749 requires
137
+ // application/x-www-form-urlencoded)
138
+ app.post(
139
+ "/oauth/token",
140
+ express.urlencoded({ extended: false }),
141
+ (req: Request, res: Response) => {
142
+ const grantType = req.body.grant_type;
143
+
144
+ if (grantType !== "client_credentials") {
145
+ res.status(400).json({
146
+ error: "unsupported_grant_type",
147
+ error_description:
148
+ "Only client_credentials grant type is supported",
149
+ });
150
+ return;
151
+ }
152
+
153
+ // Accept client_secret as the LangWatch API key.
154
+ // client_id is ignored — the API key identifies the project.
155
+ const clientSecret = req.body.client_secret;
156
+
157
+ if (!clientSecret || typeof clientSecret !== "string") {
158
+ res.status(400).json({
159
+ error: "invalid_request",
160
+ error_description:
161
+ "client_secret is required (use your LangWatch API key)",
162
+ });
163
+ return;
164
+ }
165
+
166
+ const expiresIn = 3600; // 1 hour
167
+ const accessToken = generateAccessToken();
168
+
169
+ oauthTokens.set(accessToken, {
170
+ apiKey: clientSecret,
171
+ expiresAt: Date.now() + expiresIn * 1000,
172
+ });
173
+
174
+ res.json({
175
+ access_token: accessToken,
176
+ token_type: "Bearer",
177
+ expires_in: expiresIn,
178
+ scope: "mcp:tools",
179
+ });
180
+ }
181
+ );
182
+
183
+ // --- Streamable HTTP transport (modern) ---
184
+
185
+ const sessions: Record<string, SessionState> = {};
186
+
187
+ app.post("/mcp", async (req: Request, res: Response) => {
188
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
189
+
190
+ if (sessionId && sessions[sessionId]) {
191
+ const session = sessions[sessionId];
192
+ await handleWithSessionConfig(session.apiKey, () =>
193
+ session.transport.handleRequest(req, res, req.body)
194
+ );
195
+ return;
196
+ }
197
+
198
+ if (!sessionId && isInitializeRequest(req.body)) {
199
+ const apiKey = extractBearerToken(req);
200
+
201
+ if (!apiKey) {
202
+ res.status(401).json({
203
+ error:
204
+ "Authorization: Bearer <LANGWATCH_API_KEY> header required",
205
+ });
206
+ return;
207
+ }
208
+
209
+ const transport = new StreamableHTTPServerTransport({
210
+ sessionIdGenerator: () => randomUUID(),
211
+ onsessioninitialized: (id) => {
212
+ sessions[id] = { transport, apiKey };
213
+ },
214
+ });
215
+ transport.onclose = () => {
216
+ if (transport.sessionId) {
217
+ delete sessions[transport.sessionId];
218
+ }
219
+ };
220
+
221
+ const sessionServer = createMcpServer();
222
+ await handleWithSessionConfig(apiKey, () =>
223
+ sessionServer.connect(transport)
224
+ );
225
+
226
+ await handleWithSessionConfig(apiKey, () =>
227
+ transport.handleRequest(req, res, req.body)
228
+ );
229
+ return;
230
+ }
231
+
232
+ res.status(400).json({
233
+ error: "Invalid request — no session ID or not an initialize request",
234
+ });
235
+ });
236
+
237
+ app.get("/mcp", async (req: Request, res: Response) => {
238
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
239
+
240
+ if (sessionId && sessions[sessionId]) {
241
+ const session = sessions[sessionId];
242
+ await handleWithSessionConfig(session.apiKey, () =>
243
+ session.transport.handleRequest(req, res)
244
+ );
245
+ } else {
246
+ res
247
+ .status(400)
248
+ .json({ error: "Invalid request — no valid session ID" });
249
+ }
250
+ });
251
+
252
+ app.delete("/mcp", async (req: Request, res: Response) => {
253
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
254
+ if (sessionId && sessions[sessionId]) {
255
+ const session = sessions[sessionId];
256
+ await session.transport.close();
257
+ delete sessions[sessionId];
258
+ res.status(200).json({ status: "session closed" });
259
+ } else {
260
+ res.status(404).json({ error: "Session not found" });
261
+ }
262
+ });
263
+
264
+ // --- Legacy SSE transport (backwards compatibility) ---
265
+
266
+ interface SseSessionState {
267
+ transport: SSEServerTransport;
268
+ apiKey: string;
269
+ }
270
+
271
+ const sseSessions: Record<string, SseSessionState> = {};
272
+
273
+ app.get("/sse", async (req: Request, res: Response) => {
274
+ const apiKey =
275
+ extractBearerToken(req) ||
276
+ (req.query["apiKey"] as string | undefined) ||
277
+ null;
278
+
279
+ if (!apiKey) {
280
+ res.status(401).json({
281
+ error:
282
+ "Authorization: Bearer <LANGWATCH_API_KEY> header or ?apiKey= query parameter required",
283
+ });
284
+ return;
285
+ }
286
+
287
+ const transport = new SSEServerTransport("/messages", res);
288
+ sseSessions[transport.sessionId] = { transport, apiKey };
289
+
290
+ const sessionServer = createMcpServer();
291
+
292
+ // Clean up when the SSE connection closes
293
+ res.on("close", () => {
294
+ delete sseSessions[transport.sessionId];
295
+ });
296
+
297
+ await handleWithSessionConfig(apiKey, () =>
298
+ sessionServer.connect(transport)
299
+ );
300
+ });
301
+
302
+ // Handle POST messages — mount at both /messages and /sse/messages
303
+ // because some clients resolve the relative /messages URL differently
304
+ const handleSseMessage = async (req: Request, res: Response) => {
305
+ const sessionId = req.query["sessionId"] as string | undefined;
306
+ if (!sessionId || !sseSessions[sessionId]) {
307
+ res.status(400).json({ error: "Invalid or missing session ID" });
308
+ return;
309
+ }
310
+
311
+ const session = sseSessions[sessionId];
312
+ await handleWithSessionConfig(session.apiKey, () =>
313
+ session.transport.handlePostMessage(req, res, req.body)
314
+ );
315
+ };
316
+
317
+ app.post("/messages", handleSseMessage);
318
+ app.post("/sse/messages", handleSseMessage);
319
+
320
+ // Start the server
321
+ return new Promise((resolve) => {
322
+ const server = app.listen(port, "0.0.0.0", () => {
323
+ const addr = server.address();
324
+ const resolvedPort =
325
+ typeof addr === "object" && addr ? addr.port : port;
326
+ resolve({ server, port: resolvedPort });
327
+ });
328
+ });
329
+ }