@langwatch/mcp-server 0.6.1 → 0.7.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/CHANGELOG.md +23 -0
- package/Dockerfile +22 -0
- package/README.md +51 -8
- package/dist/{archive-scenario-YFD5THOR.js → archive-scenario-WJ2A7ETN.js} +4 -4
- package/dist/archive-suite-4JOM4SSL.js +15 -0
- package/dist/archive-suite-4JOM4SSL.js.map +1 -0
- package/dist/assign-prompt-tag-GL7EAAF3.js +19 -0
- package/dist/assign-prompt-tag-GL7EAAF3.js.map +1 -0
- package/dist/{chunk-6U4TCGFC.js → chunk-2GT7IFWK.js} +2 -2
- package/dist/{chunk-LLRQIF52.js → chunk-2WPLXEKF.js} +49 -5
- package/dist/chunk-2WPLXEKF.js.map +1 -0
- package/dist/chunk-4AUB2BIA.js +71 -0
- package/dist/chunk-4AUB2BIA.js.map +1 -0
- package/dist/chunk-BWWY5KEF.js +71 -0
- package/dist/chunk-BWWY5KEF.js.map +1 -0
- package/dist/{chunk-IX6QJKAD.js → chunk-CCWCIJJ3.js} +2 -2
- package/dist/chunk-E5JYYU3J.js +1567 -0
- package/dist/chunk-E5JYYU3J.js.map +1 -0
- package/dist/chunk-EGH5RPXM.js +25 -0
- package/dist/chunk-EGH5RPXM.js.map +1 -0
- package/dist/chunk-EUQDVOSU.js +39 -0
- package/dist/chunk-EUQDVOSU.js.map +1 -0
- package/dist/{chunk-5UOPNRXW.js → chunk-HYMIWO3U.js} +10 -3
- package/dist/{chunk-5UOPNRXW.js.map → chunk-HYMIWO3U.js.map} +1 -1
- package/dist/chunk-IE4M5OGE.js +42 -0
- package/dist/chunk-IE4M5OGE.js.map +1 -0
- package/dist/chunk-LUMEZEHL.js +48 -0
- package/dist/chunk-LUMEZEHL.js.map +1 -0
- package/dist/chunk-NUFPTHKU.js +9 -0
- package/dist/chunk-NUFPTHKU.js.map +1 -0
- package/dist/chunk-WQG2DFHX.js +86 -0
- package/dist/chunk-WQG2DFHX.js.map +1 -0
- package/dist/chunk-WVI5CJ6S.js +21 -0
- package/dist/chunk-WVI5CJ6S.js.map +1 -0
- package/dist/chunk-ZVS4UKN2.js +35 -0
- package/dist/chunk-ZVS4UKN2.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +13 -0
- package/dist/create-agent-MBOOT3XK.js +23 -0
- package/dist/create-agent-MBOOT3XK.js.map +1 -0
- package/dist/create-annotation-XMV6A7ED.js +24 -0
- package/dist/create-annotation-XMV6A7ED.js.map +1 -0
- package/dist/create-dashboard-VRKLFVZJ.js +18 -0
- package/dist/create-dashboard-VRKLFVZJ.js.map +1 -0
- package/dist/create-dataset-7HQPZCRE.js +22 -0
- package/dist/create-dataset-7HQPZCRE.js.map +1 -0
- package/dist/create-dataset-records-2NIVFYTV.js +22 -0
- package/dist/create-dataset-records-2NIVFYTV.js.map +1 -0
- package/dist/{create-evaluator-E5X5ZP3B.js → create-evaluator-WYFRFLRC.js} +4 -4
- package/dist/create-mcp-server.d.ts +8 -0
- package/dist/create-mcp-server.js +8 -0
- package/dist/create-mcp-server.js.map +1 -0
- package/dist/{create-prompt-7Z35MIL6.js → create-prompt-IEN7O6JT.js} +7 -4
- package/dist/create-prompt-IEN7O6JT.js.map +1 -0
- package/dist/create-prompt-tag-CAEZ45VO.js +17 -0
- package/dist/create-prompt-tag-CAEZ45VO.js.map +1 -0
- package/dist/{create-scenario-DIMPJRPY.js → create-scenario-6YHSMGLP.js} +4 -4
- package/dist/create-suite-H5XOFDE5.js +38 -0
- package/dist/create-suite-H5XOFDE5.js.map +1 -0
- package/dist/delete-agent-JKTNBWAA.js +18 -0
- package/dist/delete-agent-JKTNBWAA.js.map +1 -0
- package/dist/delete-annotation-ACMDV6QO.js +17 -0
- package/dist/delete-annotation-ACMDV6QO.js.map +1 -0
- package/dist/delete-dashboard-VJSR7CRU.js +18 -0
- package/dist/delete-dashboard-VJSR7CRU.js.map +1 -0
- package/dist/delete-dataset-RTTTWP4D.js +20 -0
- package/dist/delete-dataset-RTTTWP4D.js.map +1 -0
- package/dist/delete-dataset-records-KNQTIPQK.js +21 -0
- package/dist/delete-dataset-records-KNQTIPQK.js.map +1 -0
- package/dist/delete-evaluator-7FVC5WM4.js +15 -0
- package/dist/delete-evaluator-7FVC5WM4.js.map +1 -0
- package/dist/delete-prompt-tag-ZMZUAGFW.js +17 -0
- package/dist/delete-prompt-tag-ZMZUAGFW.js.map +1 -0
- package/dist/delete-workflow-EYQ3T4RQ.js +17 -0
- package/dist/delete-workflow-EYQ3T4RQ.js.map +1 -0
- package/dist/get-agent-E3QSTVKG.js +28 -0
- package/dist/get-agent-E3QSTVKG.js.map +1 -0
- package/dist/{get-analytics-4YJW4S5L.js → get-analytics-2OHV763I.js} +22 -5
- package/dist/get-analytics-2OHV763I.js.map +1 -0
- package/dist/get-dashboard-PC7JDHH6.js +29 -0
- package/dist/get-dashboard-PC7JDHH6.js.map +1 -0
- package/dist/get-dataset-H5IK5FVU.js +48 -0
- package/dist/get-dataset-H5IK5FVU.js.map +1 -0
- package/dist/{get-evaluator-WDEH2F7M.js → get-evaluator-UWVUPYTM.js} +4 -4
- package/dist/{get-prompt-F6PDVC76.js → get-prompt-WZUOLLQC.js} +4 -4
- package/dist/get-prompt-WZUOLLQC.js.map +1 -0
- package/dist/{get-scenario-H24ZYNT5.js → get-scenario-Q6LCNUYN.js} +4 -4
- package/dist/get-simulation-run-Z2XKO256.js +56 -0
- package/dist/get-simulation-run-Z2XKO256.js.map +1 -0
- package/dist/get-suite-BXK5TN7P.js +44 -0
- package/dist/get-suite-BXK5TN7P.js.map +1 -0
- package/dist/{get-trace-27USKGO7.js → get-trace-4EJAYVBG.js} +3 -3
- package/dist/get-workflow-O34QJLHD.js +25 -0
- package/dist/get-workflow-O34QJLHD.js.map +1 -0
- package/dist/http-server-ISHF7NJX.js +224 -0
- package/dist/http-server-ISHF7NJX.js.map +1 -0
- package/dist/index.js +61109 -31519
- package/dist/index.js.map +1 -1
- package/dist/langwatch-api-annotations-ERBG27RF.js +17 -0
- package/dist/langwatch-api-annotations-ERBG27RF.js.map +1 -0
- package/dist/langwatch-api-dashboards-G6TR7VVO.js +17 -0
- package/dist/langwatch-api-dashboards-G6TR7VVO.js.map +1 -0
- package/dist/langwatch-api-monitors-WSXZLS2B.js +52 -0
- package/dist/langwatch-api-monitors-WSXZLS2B.js.map +1 -0
- package/dist/langwatch-api-secrets-RR5DKG2X.js +40 -0
- package/dist/langwatch-api-secrets-RR5DKG2X.js.map +1 -0
- package/dist/langwatch-api-triggers-2RTNSWQY.js +30 -0
- package/dist/langwatch-api-triggers-2RTNSWQY.js.map +1 -0
- package/dist/langwatch-api-workflows-VJRIIDZ7.js +15 -0
- package/dist/langwatch-api-workflows-VJRIIDZ7.js.map +1 -0
- package/dist/list-agents-AW3P6FJV.js +32 -0
- package/dist/list-agents-AW3P6FJV.js.map +1 -0
- package/dist/list-annotations-QNCGWPQ3.js +33 -0
- package/dist/list-annotations-QNCGWPQ3.js.map +1 -0
- package/dist/list-dashboards-R7RK2IXL.js +32 -0
- package/dist/list-dashboards-R7RK2IXL.js.map +1 -0
- package/dist/list-dataset-records-WVCSQP7P.js +39 -0
- package/dist/list-dataset-records-WVCSQP7P.js.map +1 -0
- package/dist/list-datasets-KEXRLDEU.js +60 -0
- package/dist/list-datasets-KEXRLDEU.js.map +1 -0
- package/dist/{list-evaluators-KRGI72EH.js → list-evaluators-7R2LB4JH.js} +4 -4
- package/dist/{list-model-providers-A5YCFTPI.js → list-model-providers-OBYJCKBX.js} +4 -4
- package/dist/list-prompt-tags-6LY3ZPYI.js +23 -0
- package/dist/list-prompt-tags-6LY3ZPYI.js.map +1 -0
- package/dist/{list-prompts-LKJSE7XN.js → list-prompts-VWEZ4CDE.js} +3 -3
- package/dist/{list-scenarios-ZK5CMGC4.js → list-scenarios-TXD4W2ZJ.js} +4 -4
- package/dist/list-simulation-runs-RHPOLMFI.js +43 -0
- package/dist/list-simulation-runs-RHPOLMFI.js.map +1 -0
- package/dist/list-suites-FSGBX27Z.js +42 -0
- package/dist/list-suites-FSGBX27Z.js.map +1 -0
- package/dist/list-workflows-VRGEZ3EN.js +35 -0
- package/dist/list-workflows-VRGEZ3EN.js.map +1 -0
- package/dist/rename-prompt-tag-VOJJLX4M.js +18 -0
- package/dist/rename-prompt-tag-VOJJLX4M.js.map +1 -0
- package/dist/run-agent-KTCC7TB7.js +36 -0
- package/dist/run-agent-KTCC7TB7.js.map +1 -0
- package/dist/run-evaluation-2X3V3Q6L.js +64 -0
- package/dist/run-evaluation-2X3V3Q6L.js.map +1 -0
- package/dist/run-suite-BTMHGI2D.js +29 -0
- package/dist/run-suite-BTMHGI2D.js.map +1 -0
- package/dist/run-workflow-IGAYGKJL.js +36 -0
- package/dist/run-workflow-IGAYGKJL.js.map +1 -0
- package/dist/{search-traces-SOKAAMAR.js → search-traces-VYFUIMXH.js} +3 -3
- package/dist/{set-model-provider-7MGULZDH.js → set-model-provider-W5IUI326.js} +4 -4
- package/dist/update-agent-4BB7EYLR.js +20 -0
- package/dist/update-agent-4BB7EYLR.js.map +1 -0
- package/dist/update-dataset-CXBKZ4W3.js +22 -0
- package/dist/update-dataset-CXBKZ4W3.js.map +1 -0
- package/dist/update-dataset-record-HOHAMHXG.js +19 -0
- package/dist/update-dataset-record-HOHAMHXG.js.map +1 -0
- package/dist/{update-evaluator-A3XINFLJ.js → update-evaluator-IVOR6FTR.js} +4 -4
- package/dist/{update-prompt-IW7X2UQM.js → update-prompt-OWQAKE7K.js} +5 -3
- package/dist/update-prompt-OWQAKE7K.js.map +1 -0
- package/dist/{update-scenario-ZT7TOBFR.js → update-scenario-GDFVILPN.js} +4 -4
- package/dist/update-suite-BXYUYSPT.js +39 -0
- package/dist/update-suite-BXYUYSPT.js.map +1 -0
- package/package.json +17 -3
- package/src/__tests__/all-tools.integration.test.ts +344 -0
- package/src/__tests__/config.unit.test.ts +81 -1
- package/src/__tests__/datasets.integration.test.ts +603 -0
- package/src/__tests__/datasets.unit.test.ts +240 -0
- package/src/__tests__/docker.integration.test.ts +234 -0
- package/src/__tests__/http-transport.integration.test.ts +397 -0
- package/src/__tests__/langwatch-api.unit.test.ts +174 -0
- package/src/__tests__/tools.unit.test.ts +177 -0
- package/src/config.d.ts +15 -0
- package/src/config.ts +83 -7
- package/src/create-mcp-server.d.ts +6 -0
- package/src/create-mcp-server.ts +1799 -0
- package/src/http-server.ts +329 -0
- package/src/index.ts +22 -593
- package/src/langwatch-api-agents.ts +108 -0
- package/src/langwatch-api-annotations.ts +49 -0
- package/src/langwatch-api-dashboards.ts +48 -0
- package/src/langwatch-api-datasets.ts +191 -0
- package/src/langwatch-api-evaluators.ts +10 -0
- package/src/langwatch-api-monitors.ts +82 -0
- package/src/langwatch-api-secrets.ts +48 -0
- package/src/langwatch-api-simulation-runs.ts +51 -0
- package/src/langwatch-api-suites.ts +118 -0
- package/src/langwatch-api-triggers.ts +48 -0
- package/src/langwatch-api-workflows.ts +41 -0
- package/src/langwatch-api.ts +60 -2
- package/src/schemas/create-dataset.ts +48 -0
- package/src/schemas/index.ts +1 -0
- package/src/tools/archive-suite.ts +12 -0
- package/src/tools/assign-prompt-tag.ts +16 -0
- package/src/tools/create-agent.ts +15 -0
- package/src/tools/create-annotation.ts +18 -0
- package/src/tools/create-dashboard.ts +7 -0
- package/src/tools/create-dataset-records.ts +23 -0
- package/src/tools/create-dataset.ts +22 -0
- package/src/tools/create-prompt-tag.ts +12 -0
- package/src/tools/create-prompt.ts +4 -0
- package/src/tools/create-suite.ts +42 -0
- package/src/tools/delete-agent.ts +7 -0
- package/src/tools/delete-annotation.ts +7 -0
- package/src/tools/delete-dashboard.ts +7 -0
- package/src/tools/delete-dataset-records.ts +22 -0
- package/src/tools/delete-dataset.ts +20 -0
- package/src/tools/delete-evaluator.ts +12 -0
- package/src/tools/delete-prompt-tag.ts +12 -0
- package/src/tools/delete-workflow.ts +7 -0
- package/src/tools/format-dataset-mutation.ts +25 -0
- package/src/tools/get-agent.ts +21 -0
- package/src/tools/get-analytics.ts +30 -3
- package/src/tools/get-dashboard.ts +22 -0
- package/src/tools/get-dataset.ts +63 -0
- package/src/tools/get-prompt.ts +2 -1
- package/src/tools/get-simulation-run.ts +60 -0
- package/src/tools/get-suite.ts +47 -0
- package/src/tools/get-workflow.ts +17 -0
- package/src/tools/list-agents.ts +27 -0
- package/src/tools/list-annotations.ts +33 -0
- package/src/tools/list-dashboards.ts +27 -0
- package/src/tools/list-dataset-records.ts +46 -0
- package/src/tools/list-datasets.ts +80 -0
- package/src/tools/list-prompt-tags.ts +21 -0
- package/src/tools/list-simulation-runs.ts +49 -0
- package/src/tools/list-suites.ts +43 -0
- package/src/tools/list-workflows.ts +31 -0
- package/src/tools/rename-prompt-tag.ts +14 -0
- package/src/tools/run-agent.ts +36 -0
- package/src/tools/run-evaluation.ts +87 -0
- package/src/tools/run-suite.ts +28 -0
- package/src/tools/run-workflow.ts +36 -0
- package/src/tools/update-agent.ts +13 -0
- package/src/tools/update-dataset-record.ts +21 -0
- package/src/tools/update-dataset.ts +23 -0
- package/src/tools/update-prompt.ts +3 -0
- package/src/tools/update-suite.ts +44 -0
- package/src/utils/escape-markdown.ts +12 -0
- package/tsup.config.ts +5 -1
- package/dist/chunk-AAQNA53E.js +0 -28
- package/dist/chunk-AAQNA53E.js.map +0 -1
- package/dist/chunk-LLRQIF52.js.map +0 -1
- package/dist/config-FIQWQRUB.js +0 -11
- package/dist/create-prompt-7Z35MIL6.js.map +0 -1
- package/dist/get-analytics-4YJW4S5L.js.map +0 -1
- package/dist/get-prompt-F6PDVC76.js.map +0 -1
- package/dist/update-prompt-IW7X2UQM.js.map +0 -1
- /package/dist/{archive-scenario-YFD5THOR.js.map → archive-scenario-WJ2A7ETN.js.map} +0 -0
- /package/dist/{chunk-6U4TCGFC.js.map → chunk-2GT7IFWK.js.map} +0 -0
- /package/dist/{chunk-IX6QJKAD.js.map → chunk-CCWCIJJ3.js.map} +0 -0
- /package/dist/{config-FIQWQRUB.js.map → config.js.map} +0 -0
- /package/dist/{create-evaluator-E5X5ZP3B.js.map → create-evaluator-WYFRFLRC.js.map} +0 -0
- /package/dist/{create-scenario-DIMPJRPY.js.map → create-scenario-6YHSMGLP.js.map} +0 -0
- /package/dist/{get-evaluator-WDEH2F7M.js.map → get-evaluator-UWVUPYTM.js.map} +0 -0
- /package/dist/{get-scenario-H24ZYNT5.js.map → get-scenario-Q6LCNUYN.js.map} +0 -0
- /package/dist/{get-trace-27USKGO7.js.map → get-trace-4EJAYVBG.js.map} +0 -0
- /package/dist/{list-evaluators-KRGI72EH.js.map → list-evaluators-7R2LB4JH.js.map} +0 -0
- /package/dist/{list-model-providers-A5YCFTPI.js.map → list-model-providers-OBYJCKBX.js.map} +0 -0
- /package/dist/{list-prompts-LKJSE7XN.js.map → list-prompts-VWEZ4CDE.js.map} +0 -0
- /package/dist/{list-scenarios-ZK5CMGC4.js.map → list-scenarios-TXD4W2ZJ.js.map} +0 -0
- /package/dist/{search-traces-SOKAAMAR.js.map → search-traces-VYFUIMXH.js.map} +0 -0
- /package/dist/{set-model-provider-7MGULZDH.js.map → set-model-provider-W5IUI326.js.map} +0 -0
- /package/dist/{update-evaluator-A3XINFLJ.js.map → update-evaluator-IVOR6FTR.js.map} +0 -0
- /package/dist/{update-scenario-ZT7TOBFR.js.map → update-scenario-GDFVILPN.js.map} +0 -0
|
@@ -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
|
+
}
|