@langwatch/mcp-server 1.0.0 → 2.1.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/.python-version +1 -0
- package/CHANGELOG.md +110 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +86 -4
- package/dist/{archive-project-MK3673A7.js → archive-project-W3IKI7UL.js} +5 -5
- package/dist/archive-run-plan-474OKHHU.js +16 -0
- package/dist/{archive-scenario-KATSXRIN.js → archive-scenario-V5UNYK26.js} +5 -5
- package/dist/archive-test-suite-O7FLDMXK.js +16 -0
- package/dist/{assign-prompt-tag-E3C5W6PI.js → assign-prompt-tag-Q46WRB6P.js} +4 -4
- package/dist/chunk-56OGTS24.js +44 -0
- package/dist/chunk-6DJDEXET.js +63 -0
- package/dist/chunk-7E4FZIYJ.js +15 -0
- package/dist/{chunk-J45SVWPR.js → chunk-AN4OUA4D.js} +2 -2
- package/dist/chunk-CKNAH5TG.js +74 -0
- package/dist/{chunk-CE6QMZOP.js → chunk-EYXMLE5X.js} +86 -11
- package/dist/{chunk-XY4STOBE.js → chunk-G4ZAZTZW.js} +2 -2
- package/dist/{chunk-5UWY24RP.js → chunk-GELJ2MQR.js} +12 -3
- package/dist/chunk-HV4DWKBI.js +46 -0
- package/dist/chunk-JR6DJZOU.js +49 -0
- package/dist/{chunk-DAIQKS27.js → chunk-JTQJRNFJ.js} +2 -2
- package/dist/{chunk-WWMDHZZV.js → chunk-K37ZNPXK.js} +2 -2
- package/dist/{chunk-EU22L52K.js → chunk-OJLYYJWI.js} +2 -2
- package/dist/{chunk-Y3HDZ6OW.js → chunk-QAMP3CGR.js} +2 -2
- package/dist/{chunk-VYPSKRPN.js → chunk-QRO35SOM.js} +347 -146
- package/dist/{chunk-O3LWVN67.js → chunk-RVAE2MEA.js} +2 -2
- package/dist/{chunk-UXXA4D6K.js → chunk-S4GVX36W.js} +2 -2
- package/dist/{chunk-BJ7B4R26.js → chunk-SAS5OB7F.js} +18 -2
- package/dist/{chunk-RYRB2YVJ.js → chunk-SR6QPRIE.js} +2 -2
- package/dist/chunk-UF6XIGYR.js +58 -0
- package/dist/{chunk-T7WJVS2N.js → chunk-UFOYFYI7.js} +2 -2
- package/dist/chunk-UJPXFA26.js +267 -0
- package/dist/{chunk-KOO5733S.js → chunk-WT4PO4A3.js} +8 -2
- package/dist/config.js +2 -2
- package/dist/{create-agent-7W57J7MT.js → create-agent-JFJ2K6X3.js} +5 -5
- package/dist/{create-annotation-W2XELBCQ.js → create-annotation-EUXXQ6NS.js} +5 -5
- package/dist/{create-api-key-MKRXKFJM.js → create-api-key-BGBB76JX.js} +5 -5
- package/dist/{create-dashboard-QGL7IODC.js → create-dashboard-Z76AIPLS.js} +5 -5
- package/dist/{create-dataset-3PH7A4HG.js → create-dataset-FD5VAXU4.js} +5 -5
- package/dist/{create-dataset-records-4DZS3VRL.js → create-dataset-records-WQUXXOHI.js} +5 -5
- package/dist/{create-evaluator-YWH6NC4M.js → create-evaluator-6NV33Z52.js} +5 -5
- package/dist/create-mcp-server.js +9 -4
- package/dist/{create-project-BOBQ32C5.js → create-project-ZQ7TFIO2.js} +5 -5
- package/dist/{create-prompt-FLCFGITE.js → create-prompt-BS6QRCUU.js} +4 -4
- package/dist/{create-prompt-tag-EMPNIKEA.js → create-prompt-tag-FCQ5ELSQ.js} +4 -4
- package/dist/{create-scenario-BSBIHUL6.js → create-scenario-DRPTPXU3.js} +12 -5
- package/dist/create-test-suite-ZIZCCFJ4.js +37 -0
- package/dist/{delete-agent-7MSYNY5E.js → delete-agent-SA4JMUZF.js} +5 -5
- package/dist/{delete-annotation-ZT5IGN24.js → delete-annotation-X3KKK7QY.js} +5 -5
- package/dist/{delete-dashboard-BTIY5S3L.js → delete-dashboard-LYMRPF27.js} +5 -5
- package/dist/{delete-dataset-U45HQHPR.js → delete-dataset-W6EZ44HY.js} +5 -5
- package/dist/{delete-dataset-records-R43IMAWB.js → delete-dataset-records-7CNOTALB.js} +5 -5
- package/dist/{delete-evaluator-U6JMQ6C7.js → delete-evaluator-RKYC4TNB.js} +5 -5
- package/dist/{delete-prompt-tag-4EV4TDHB.js → delete-prompt-tag-QB3OCZUV.js} +4 -4
- package/dist/{delete-workflow-5QYALXEL.js → delete-workflow-NG2VXYYM.js} +5 -5
- package/dist/{discover-evaluator-schema-MRGHVNTT.js → discover-evaluator-schema-ESSHQGPY.js} +58 -321
- package/dist/{discover-scenario-schema-UPBTCWPG.js → discover-scenario-schema-W7KBXA4J.js} +54 -2
- package/dist/{discover-schema-ICGXPHUW.js → discover-schema-L6QDPON3.js} +2 -2
- package/dist/get-agent-7THF7HCU.js +66 -0
- package/dist/{get-analytics-PVERWOA5.js → get-analytics-R6AZD3AW.js} +4 -4
- package/dist/{get-dashboard-ZZPC3WMD.js → get-dashboard-P4WWVHFU.js} +5 -5
- package/dist/{get-dataset-25TB2LQH.js → get-dataset-AKTYSQDI.js} +5 -5
- package/dist/{get-evaluator-3FTRQKEC.js → get-evaluator-RZ44UIN6.js} +5 -5
- package/dist/{get-project-5OAXYC57.js → get-project-QSQEAB33.js} +5 -5
- package/dist/get-prompt-GVKXF5NL.js +100 -0
- package/dist/get-run-plan-NY7J5GRE.js +61 -0
- package/dist/{get-scenario-DSSTR6FI.js → get-scenario-Q3LLQE4T.js} +9 -5
- package/dist/{get-simulation-run-RQPMS2MC.js → get-simulation-run-WINEYDMK.js} +14 -5
- package/dist/get-test-suite-4HKA7OKS.js +49 -0
- package/dist/{get-trace-O5VT3NAC.js → get-trace-RCUV7N4O.js} +4 -4
- package/dist/{get-workflow-3EZDOEIF.js → get-workflow-EEOC7Y45.js} +5 -5
- package/dist/http-security-EU3CHTY7.js +26 -0
- package/dist/{http-server-2ACJ3CXW.js → http-server-WCDUARTH.js} +1038 -693
- package/dist/index.js +6437 -4020
- package/dist/{langwatch-api-annotations-E65MI3WZ.js → langwatch-api-annotations-6DHVZWUK.js} +5 -5
- package/dist/{langwatch-api-dashboards-QCDECRCY.js → langwatch-api-dashboards-2KJL2M42.js} +5 -5
- package/dist/{langwatch-api-monitors-UIO2KTDV.js → langwatch-api-monitors-6ISFJ7WX.js} +4 -4
- package/dist/{langwatch-api-secrets-4T2EKSJ7.js → langwatch-api-secrets-OEBD6GBU.js} +4 -4
- package/dist/{langwatch-api-triggers-MPOEKPUY.js → langwatch-api-triggers-5E6DDGX6.js} +4 -4
- package/dist/langwatch-api-workflows-6UJL5U4Z.js +16 -0
- package/dist/list-agents-AMVDQPQL.js +43 -0
- package/dist/{list-annotations-CGX5BT7E.js → list-annotations-6PR5NLIC.js} +5 -5
- package/dist/{list-api-keys-JLWJPFRX.js → list-api-keys-GVPNBIHQ.js} +5 -5
- package/dist/{list-dashboards-RNIWX3TX.js → list-dashboards-Z3BZKDGF.js} +5 -5
- package/dist/{list-dataset-records-B5MHZR66.js → list-dataset-records-O67HZ35Y.js} +5 -5
- package/dist/{list-datasets-BVCAMQCP.js → list-datasets-WXZL6UMP.js} +5 -5
- package/dist/{list-evaluators-65FBTL2U.js → list-evaluators-PAX72WCN.js} +5 -5
- package/dist/{list-model-providers-GRK3I3VV.js → list-model-providers-VVLZMLCX.js} +5 -5
- package/dist/{list-projects-JLMBFMW3.js → list-projects-AQVI6SUK.js} +5 -5
- package/dist/{list-prompt-tags-UHNWDGQI.js → list-prompt-tags-WZFBJOHG.js} +4 -4
- package/dist/{list-prompts-7254B3U7.js → list-prompts-BQOHRHYT.js} +4 -4
- package/dist/list-run-plans-UBAU2DGH.js +46 -0
- package/dist/{list-scenarios-46YNICKM.js → list-scenarios-MS4IUIVM.js} +10 -7
- package/dist/{list-simulation-runs-LGNEFNPN.js → list-simulation-runs-D6YF4ZOU.js} +13 -7
- package/dist/list-test-suites-L7R2O6DR.js +35 -0
- package/dist/{list-workflows-MLQDM6CP.js → list-workflows-6MV4XJHF.js} +5 -5
- package/dist/{rename-prompt-tag-RHQMNEQL.js → rename-prompt-tag-K5LQ4CY6.js} +4 -4
- package/dist/rename-test-suite-NTYHAO5X.js +21 -0
- package/dist/report-issue-RHZZ33IS.js +885 -0
- package/dist/rerun-run-plan-MUDWJBE4.js +22 -0
- package/dist/{revoke-api-key-3Y35EMBT.js → revoke-api-key-6LAZ3QZZ.js} +5 -5
- package/dist/run-agent-CAGWGTYT.js +72 -0
- package/dist/run-plan-T4ZYOZGA.js +39 -0
- package/dist/run-test-suite-L4MZ7G4D.js +26 -0
- package/dist/{run-workflow-5LVBZYLS.js → run-workflow-DFIS23AP.js} +5 -5
- package/dist/{search-traces-U4HLOCZ2.js → search-traces-4XTJYXIK.js} +4 -4
- package/dist/{set-model-provider-V3M5CDIY.js → set-model-provider-XRNMFOVR.js} +5 -5
- package/dist/{update-agent-47RZIHS7.js → update-agent-DUWNYBNA.js} +5 -5
- package/dist/{update-dataset-G6QJJGS5.js → update-dataset-YSOMDCOB.js} +5 -5
- package/dist/{update-dataset-record-RMWLSWCS.js → update-dataset-record-DEA2ITYR.js} +5 -5
- package/dist/{update-evaluator-SFRRI5Q3.js → update-evaluator-NH7Q5GWV.js} +5 -5
- package/dist/{update-project-PI5DWBVK.js → update-project-Q26FBSYH.js} +5 -5
- package/dist/update-prompt-ZQQ2C3KU.js +143 -0
- package/dist/{update-scenario-G3IU2UIB.js → update-scenario-WUX34JAR.js} +12 -5
- package/package.json +8 -9
- package/pyproject.toml +5 -5
- package/src/__tests__/agent-tools.unit.test.ts +257 -0
- package/src/__tests__/all-tools.integration.test.ts +321 -75
- package/src/__tests__/config.unit.test.ts +33 -0
- package/src/__tests__/docker.integration.test.ts +91 -7
- package/src/__tests__/documentation-fetch-security.integration.test.ts +8 -2
- package/src/__tests__/feature-map-tool-drift.unit.test.ts +93 -0
- package/src/__tests__/http-security-origin.unit.test.ts +97 -0
- package/src/__tests__/http-security-sessions.unit.test.ts +209 -0
- package/src/__tests__/http-security-verifier.unit.test.ts +248 -0
- package/src/__tests__/http-server-allocation.integration.test.ts +279 -0
- package/src/__tests__/http-server-origin.integration.test.ts +169 -0
- package/src/__tests__/http-server-session-auth.integration.test.ts +341 -0
- package/src/__tests__/http-transport.integration.test.ts +85 -87
- package/src/__tests__/integration.integration.test.ts +7 -11
- package/src/__tests__/prompt-response-types.unit.test.ts +51 -0
- package/src/__tests__/prompts.integration.test.ts +736 -0
- package/src/__tests__/run-plan-tools.unit.test.ts +565 -0
- package/src/__tests__/scenario-tools.unit.test.ts +19 -0
- package/src/__tests__/simulation-run-tools.unit.test.ts +116 -0
- package/src/__tests__/support/http-server-harness.ts +138 -0
- package/src/__tests__/test-suite-tools.unit.test.ts +615 -0
- package/src/__tests__/{tools.unit.test.ts → tools.integration.test.ts} +31 -59
- package/src/config.ts +17 -2
- package/src/create-mcp-server.ts +334 -55
- package/src/http-security.ts +492 -0
- package/src/http-server.ts +536 -152
- package/src/index.ts +49 -2
- package/src/langwatch-api-agents.ts +196 -12
- package/src/langwatch-api-run-plans.ts +157 -0
- package/src/langwatch-api-scenarios.ts +9 -0
- package/src/langwatch-api-simulation-runs.ts +5 -0
- package/src/langwatch-api-test-suites.ts +135 -0
- package/src/langwatch-api.ts +62 -12
- package/src/schemas/index.ts +5 -0
- package/src/schemas/run-plan.ts +92 -0
- package/src/schemas/suite-fields.ts +149 -0
- package/src/tools/archive-run-plan.ts +12 -0
- package/src/tools/archive-test-suite.ts +12 -0
- package/src/tools/create-scenario.ts +8 -0
- package/src/tools/create-test-suite.ts +43 -0
- package/src/tools/discover-evaluator-schema.ts +2 -2
- package/src/tools/discover-scenario-schema.ts +54 -0
- package/src/tools/format-run-plan.ts +68 -0
- package/src/tools/format-scenario.ts +14 -0
- package/src/tools/format-suite-details.ts +96 -0
- package/src/tools/get-agent.ts +45 -3
- package/src/tools/get-prompt.ts +108 -24
- package/src/tools/get-run-plan.ts +62 -0
- package/src/tools/get-scenario.ts +3 -0
- package/src/tools/get-simulation-run.ts +9 -0
- package/src/tools/get-test-suite.ts +49 -0
- package/src/tools/list-agents.ts +20 -3
- package/src/tools/list-run-plans.ts +47 -0
- package/src/tools/list-scenarios.ts +12 -2
- package/src/tools/list-simulation-runs.ts +6 -2
- package/src/tools/list-test-suites.ts +35 -0
- package/src/tools/rename-test-suite.ts +18 -0
- package/src/tools/rerun-run-plan.ts +21 -0
- package/src/tools/run-agent.ts +54 -6
- package/src/tools/run-plan.ts +49 -0
- package/src/tools/run-test-suite.ts +30 -0
- package/src/tools/test-agent.ts +18 -0
- package/src/tools/update-prompt.ts +195 -10
- package/src/tools/update-scenario.ts +8 -0
- package/src/tools/update-test-suite.ts +45 -0
- package/tsconfig.json +7 -2
- package/uv.lock +418 -298
- package/dist/archive-suite-C3KX6IIZ.js +0 -16
- package/dist/chunk-WDH5P5RF.js +0 -48
- package/dist/create-suite-TWF6BBR4.js +0 -39
- package/dist/get-agent-7QJVPH72.js +0 -29
- package/dist/get-prompt-I753LZKZ.js +0 -46
- package/dist/get-suite-GTOCY5PL.js +0 -45
- package/dist/langwatch-api-workflows-5HRZ7HR4.js +0 -16
- package/dist/list-agents-TIJTP566.js +0 -33
- package/dist/list-suites-5VK4HXEK.js +0 -43
- package/dist/report-issue-DXJ6TPTC.js +0 -412
- package/dist/run-agent-RBSIFKRT.js +0 -37
- package/dist/run-suite-K3DNQ3SE.js +0 -30
- package/dist/update-prompt-RQFTEO7Y.js +0 -25
- package/dist/update-suite-TRGGLNR7.js +0 -40
- package/pnpm-workspace.yaml +0 -34
- package/src/langwatch-api-suites.ts +0 -118
- package/src/tools/archive-suite.ts +0 -12
- package/src/tools/create-suite.ts +0 -42
- package/src/tools/get-suite.ts +0 -47
- package/src/tools/list-suites.ts +0 -43
- package/src/tools/run-suite.ts +0 -28
- package/src/tools/update-suite.ts +0 -44
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session id must authorize nothing on its own, and credentials must never
|
|
3
|
+
* be accepted from the query string.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { initConfig } from "../config.js";
|
|
9
|
+
import {
|
|
10
|
+
countingVerifier,
|
|
11
|
+
MCP_POST_HEADERS,
|
|
12
|
+
openSession,
|
|
13
|
+
OTHER_VALID_KEY,
|
|
14
|
+
readSessionId,
|
|
15
|
+
revocableVerifier,
|
|
16
|
+
startHarness,
|
|
17
|
+
toolsListBody,
|
|
18
|
+
VALID_KEY,
|
|
19
|
+
initializeBody,
|
|
20
|
+
type Harness,
|
|
21
|
+
} from "./support/http-server-harness.js";
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
initConfig({ endpoint: "https://app.langwatch.ai" });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("Credentials in query parameters", () => {
|
|
28
|
+
let harness: Harness;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
const { verifier } = countingVerifier([VALID_KEY]);
|
|
32
|
+
harness = await startHarness({ port: 0, apiKeyVerifier: verifier });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
await harness.close();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not accept an API key from the SSE query string", async () => {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
|
|
42
|
+
const response = await fetch(`${harness.baseUrl}/sse?apiKey=${VALID_KEY}`, {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(response.status).toBe(401);
|
|
47
|
+
controller.abort();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does not accept an API key from the query string on /mcp", async () => {
|
|
51
|
+
const response = await fetch(`${harness.baseUrl}/mcp?apiKey=${VALID_KEY}`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: MCP_POST_HEADERS,
|
|
54
|
+
body: initializeBody(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(response.status).toBe(401);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects a message posted with only a session id in the query string", async () => {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const sseResponse = await fetch(`${harness.baseUrl}/sse`, {
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
headers: { Authorization: `Bearer ${VALID_KEY}` },
|
|
65
|
+
});
|
|
66
|
+
expect(sseResponse.status).toBe(200);
|
|
67
|
+
|
|
68
|
+
const sessionId = await readSessionId(sseResponse);
|
|
69
|
+
|
|
70
|
+
const response = await fetch(
|
|
71
|
+
`${harness.baseUrl}/messages?sessionId=${sessionId}`,
|
|
72
|
+
{
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: toolsListBody(),
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(response.status).toBe(401);
|
|
80
|
+
controller.abort();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("Session id is not a credential", () => {
|
|
85
|
+
let harness: Harness;
|
|
86
|
+
|
|
87
|
+
beforeEach(async () => {
|
|
88
|
+
const { verifier } = countingVerifier([VALID_KEY, OTHER_VALID_KEY]);
|
|
89
|
+
harness = await startHarness({ port: 0, apiKeyVerifier: verifier });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(async () => {
|
|
93
|
+
await harness.close();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("rejects tools/list carrying only a valid session id", async () => {
|
|
97
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl });
|
|
98
|
+
|
|
99
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { ...MCP_POST_HEADERS, "mcp-session-id": sessionId },
|
|
102
|
+
body: toolsListBody(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(response.status).toBe(401);
|
|
106
|
+
const body = await response.json();
|
|
107
|
+
expect(body.error).toContain("Authorization");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("rejects GET /mcp carrying only a valid session id", async () => {
|
|
111
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl });
|
|
112
|
+
|
|
113
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
114
|
+
method: "GET",
|
|
115
|
+
headers: { Accept: "text/event-stream", "mcp-session-id": sessionId },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(response.status).toBe(401);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("rejects DELETE /mcp carrying only a valid session id", async () => {
|
|
122
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl });
|
|
123
|
+
|
|
124
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
headers: { "mcp-session-id": sessionId },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(response.status).toBe(401);
|
|
130
|
+
|
|
131
|
+
// The session survived the unauthorized delete attempt.
|
|
132
|
+
const authorized = await fetch(`${harness.baseUrl}/mcp`, {
|
|
133
|
+
method: "DELETE",
|
|
134
|
+
headers: {
|
|
135
|
+
"mcp-session-id": sessionId,
|
|
136
|
+
Authorization: `Bearer ${VALID_KEY}`,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
expect(authorized.status).toBe(200);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("accepts a lowercase bearer scheme, which RFC 7235 allows", async () => {
|
|
143
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl });
|
|
144
|
+
|
|
145
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
...MCP_POST_HEADERS,
|
|
149
|
+
"mcp-session-id": sessionId,
|
|
150
|
+
Authorization: `bearer ${VALID_KEY}`,
|
|
151
|
+
},
|
|
152
|
+
body: toolsListBody(),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(response.status).toBe(200);
|
|
156
|
+
await response.text();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("tolerates extra spaces between the scheme and the token", async () => {
|
|
160
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl });
|
|
161
|
+
|
|
162
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: {
|
|
165
|
+
...MCP_POST_HEADERS,
|
|
166
|
+
"mcp-session-id": sessionId,
|
|
167
|
+
Authorization: `Bearer ${VALID_KEY}`,
|
|
168
|
+
},
|
|
169
|
+
body: toolsListBody(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(response.status).toBe(200);
|
|
173
|
+
await response.text();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("rejects an Authorization header carrying no token", async () => {
|
|
177
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl });
|
|
178
|
+
|
|
179
|
+
for (const header of ["Bearer", `Basic ${VALID_KEY}`, `Bearer${VALID_KEY}`]) {
|
|
180
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: {
|
|
183
|
+
...MCP_POST_HEADERS,
|
|
184
|
+
"mcp-session-id": sessionId,
|
|
185
|
+
Authorization: header,
|
|
186
|
+
},
|
|
187
|
+
body: toolsListBody(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(response.status).toBe(401);
|
|
191
|
+
await response.text();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("does not reveal whether another key's session exists", async () => {
|
|
196
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl, apiKey: VALID_KEY });
|
|
197
|
+
|
|
198
|
+
const someoneElses = await fetch(`${harness.baseUrl}/mcp`, {
|
|
199
|
+
method: "DELETE",
|
|
200
|
+
headers: {
|
|
201
|
+
"mcp-session-id": sessionId,
|
|
202
|
+
Authorization: `Bearer ${OTHER_VALID_KEY}`,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const neverExisted = await fetch(`${harness.baseUrl}/mcp`, {
|
|
207
|
+
method: "DELETE",
|
|
208
|
+
headers: {
|
|
209
|
+
"mcp-session-id": "00000000-0000-0000-0000-000000000000",
|
|
210
|
+
Authorization: `Bearer ${OTHER_VALID_KEY}`,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Same status for "not yours" and "does not exist", so the response is not
|
|
215
|
+
// an existence oracle for session ids.
|
|
216
|
+
expect(someoneElses.status).toBe(404);
|
|
217
|
+
expect(neverExisted.status).toBe(404);
|
|
218
|
+
expect(await someoneElses.json()).toEqual(await neverExisted.json());
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("rejects a different valid key reusing someone else's session", async () => {
|
|
222
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl, apiKey: VALID_KEY });
|
|
223
|
+
|
|
224
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: {
|
|
227
|
+
...MCP_POST_HEADERS,
|
|
228
|
+
"mcp-session-id": sessionId,
|
|
229
|
+
Authorization: `Bearer ${OTHER_VALID_KEY}`,
|
|
230
|
+
},
|
|
231
|
+
body: toolsListBody(),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(response.status).toBe(401);
|
|
235
|
+
const body = await response.json();
|
|
236
|
+
expect(body.error).toContain("does not match session");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("still serves the session to the key that created it", async () => {
|
|
240
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl });
|
|
241
|
+
|
|
242
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: {
|
|
245
|
+
...MCP_POST_HEADERS,
|
|
246
|
+
"mcp-session-id": sessionId,
|
|
247
|
+
Authorization: `Bearer ${VALID_KEY}`,
|
|
248
|
+
},
|
|
249
|
+
body: toolsListBody(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(response.status).toBe(200);
|
|
253
|
+
const text = await response.text();
|
|
254
|
+
expect(text).toContain("fetch_langwatch_docs");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("answers 401 rather than 400 for a session id it does not know", async () => {
|
|
258
|
+
const response = await fetch(`${harness.baseUrl}/mcp`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: {
|
|
261
|
+
...MCP_POST_HEADERS,
|
|
262
|
+
"mcp-session-id": "00000000-0000-0000-0000-000000000000",
|
|
263
|
+
Authorization: `Bearer ${VALID_KEY}`,
|
|
264
|
+
},
|
|
265
|
+
body: toolsListBody(),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(response.status).toBe(401);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("Revoking a key ends its sessions", () => {
|
|
273
|
+
it("stops serving a Streamable HTTP session once its key stops verifying", async () => {
|
|
274
|
+
const { verifier, revoke } = revocableVerifier([VALID_KEY]);
|
|
275
|
+
const harness = await startHarness({ port: 0, apiKeyVerifier: verifier });
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const sessionId = await openSession({ baseUrl: harness.baseUrl });
|
|
279
|
+
const call = () =>
|
|
280
|
+
fetch(`${harness.baseUrl}/mcp`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: {
|
|
283
|
+
...MCP_POST_HEADERS,
|
|
284
|
+
"mcp-session-id": sessionId,
|
|
285
|
+
Authorization: `Bearer ${VALID_KEY}`,
|
|
286
|
+
},
|
|
287
|
+
body: toolsListBody(),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const before = await call();
|
|
291
|
+
expect(before.status).toBe(200);
|
|
292
|
+
await before.text();
|
|
293
|
+
|
|
294
|
+
revoke(VALID_KEY);
|
|
295
|
+
|
|
296
|
+
const after = await call();
|
|
297
|
+
expect(after.status).toBe(401);
|
|
298
|
+
await after.text();
|
|
299
|
+
} finally {
|
|
300
|
+
await harness.close();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("stops serving an SSE session once its key stops verifying", async () => {
|
|
305
|
+
const { verifier, revoke } = revocableVerifier([VALID_KEY]);
|
|
306
|
+
const harness = await startHarness({ port: 0, apiKeyVerifier: verifier });
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const sse = await fetch(`${harness.baseUrl}/sse`, {
|
|
311
|
+
signal: controller.signal,
|
|
312
|
+
headers: { Authorization: `Bearer ${VALID_KEY}` },
|
|
313
|
+
});
|
|
314
|
+
expect(sse.status).toBe(200);
|
|
315
|
+
|
|
316
|
+
const sessionId = await readSessionId(sse);
|
|
317
|
+
const post = () =>
|
|
318
|
+
fetch(`${harness.baseUrl}/messages?sessionId=${sessionId}`, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: {
|
|
321
|
+
"Content-Type": "application/json",
|
|
322
|
+
Authorization: `Bearer ${VALID_KEY}`,
|
|
323
|
+
},
|
|
324
|
+
body: toolsListBody(),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const before = await post();
|
|
328
|
+
expect(before.status).toBeLessThan(400);
|
|
329
|
+
await before.text();
|
|
330
|
+
|
|
331
|
+
revoke(VALID_KEY);
|
|
332
|
+
|
|
333
|
+
const after = await post();
|
|
334
|
+
expect(after.status).toBe(401);
|
|
335
|
+
await after.text();
|
|
336
|
+
} finally {
|
|
337
|
+
controller.abort();
|
|
338
|
+
await harness.close();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
2
|
import { initConfig } from "../config.js";
|
|
3
|
+
import type { ApiKeyVerifier } from "../http-security.js";
|
|
3
4
|
import type { Server } from "http";
|
|
4
5
|
|
|
5
6
|
/** Standard headers required by the MCP Streamable HTTP protocol for POST requests */
|
|
@@ -10,6 +11,18 @@ const MCP_POST_HEADERS = {
|
|
|
10
11
|
|
|
11
12
|
const BEARER_TOKEN = "test-session-key";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Stands in for the LangWatch API. The standalone server has no database, so
|
|
16
|
+
* it asks the API whether a key is real; here one key is.
|
|
17
|
+
*/
|
|
18
|
+
function stubVerifier(validKeys: string[]): ApiKeyVerifier {
|
|
19
|
+
return {
|
|
20
|
+
verify: async (apiKey: string) => validKeys.includes(apiKey),
|
|
21
|
+
sweep: () => undefined,
|
|
22
|
+
clear: () => undefined,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
/** Helper to create auth + MCP headers */
|
|
14
27
|
function mcpHeaders({
|
|
15
28
|
sessionId,
|
|
@@ -41,6 +54,7 @@ function initializeBody() {
|
|
|
41
54
|
describe("HTTP transport", () => {
|
|
42
55
|
let httpServer: Server;
|
|
43
56
|
let port: number;
|
|
57
|
+
let baseUrl: string;
|
|
44
58
|
|
|
45
59
|
beforeAll(async () => {
|
|
46
60
|
// Initialize with no API key -- HTTP mode relies on per-session Bearer tokens
|
|
@@ -49,9 +63,13 @@ describe("HTTP transport", () => {
|
|
|
49
63
|
});
|
|
50
64
|
|
|
51
65
|
const { startHttpServer } = await import("../http-server.js");
|
|
52
|
-
const result = await startHttpServer({
|
|
66
|
+
const result = await startHttpServer({
|
|
67
|
+
port: 0,
|
|
68
|
+
apiKeyVerifier: stubVerifier([BEARER_TOKEN, "my-langwatch-api-key"]),
|
|
69
|
+
});
|
|
53
70
|
httpServer = result.server;
|
|
54
71
|
port = result.port;
|
|
72
|
+
baseUrl = `http://127.0.0.1:${port}`;
|
|
55
73
|
});
|
|
56
74
|
|
|
57
75
|
afterAll(async () => {
|
|
@@ -62,7 +80,7 @@ describe("HTTP transport", () => {
|
|
|
62
80
|
|
|
63
81
|
describe("/health endpoint", () => {
|
|
64
82
|
it("returns ok status without authentication", async () => {
|
|
65
|
-
const response = await fetch(
|
|
83
|
+
const response = await fetch(`${baseUrl}/health`);
|
|
66
84
|
const body = await response.json();
|
|
67
85
|
|
|
68
86
|
expect(response.status).toBe(200);
|
|
@@ -71,18 +89,30 @@ describe("HTTP transport", () => {
|
|
|
71
89
|
});
|
|
72
90
|
|
|
73
91
|
describe("CORS headers", () => {
|
|
74
|
-
it("
|
|
75
|
-
const response = await fetch(
|
|
92
|
+
it("sends no Access-Control-Allow-Origin when the request has no Origin", async () => {
|
|
93
|
+
const response = await fetch(`${baseUrl}/health`);
|
|
94
|
+
|
|
95
|
+
expect(response.status).toBe(200);
|
|
96
|
+
expect(response.headers.get("access-control-allow-origin")).toBeNull();
|
|
97
|
+
});
|
|
76
98
|
|
|
77
|
-
|
|
99
|
+
it("reflects an allowed origin rather than answering with a wildcard", async () => {
|
|
100
|
+
const response = await fetch(`${baseUrl}/health`, {
|
|
101
|
+
headers: { Origin: "http://localhost:5173" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(response.headers.get("access-control-allow-origin")).toBe(
|
|
105
|
+
"http://localhost:5173"
|
|
106
|
+
);
|
|
107
|
+
expect(response.headers.get("vary")).toContain("Origin");
|
|
78
108
|
});
|
|
79
109
|
|
|
80
110
|
it("responds to OPTIONS preflight requests", async () => {
|
|
81
|
-
const response = await fetch(
|
|
111
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
82
112
|
method: "OPTIONS",
|
|
83
113
|
});
|
|
84
114
|
|
|
85
|
-
expect(response.status).toBe(
|
|
115
|
+
expect(response.status).toBe(204);
|
|
86
116
|
expect(response.headers.get("access-control-allow-methods")).toContain(
|
|
87
117
|
"POST"
|
|
88
118
|
);
|
|
@@ -92,7 +122,7 @@ describe("HTTP transport", () => {
|
|
|
92
122
|
});
|
|
93
123
|
|
|
94
124
|
it("includes Authorization in allowed headers for CORS", async () => {
|
|
95
|
-
const response = await fetch(
|
|
125
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
96
126
|
method: "OPTIONS",
|
|
97
127
|
});
|
|
98
128
|
|
|
@@ -105,7 +135,7 @@ describe("HTTP transport", () => {
|
|
|
105
135
|
describe("/mcp endpoint (Streamable HTTP)", () => {
|
|
106
136
|
describe("when no Bearer token is provided", () => {
|
|
107
137
|
it("returns 401 on initialize request", async () => {
|
|
108
|
-
const response = await fetch(
|
|
138
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
109
139
|
method: "POST",
|
|
110
140
|
headers: MCP_POST_HEADERS,
|
|
111
141
|
body: initializeBody(),
|
|
@@ -118,7 +148,7 @@ describe("HTTP transport", () => {
|
|
|
118
148
|
});
|
|
119
149
|
|
|
120
150
|
it("rejects non-initialize POST without session ID", async () => {
|
|
121
|
-
const response = await fetch(
|
|
151
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
122
152
|
method: "POST",
|
|
123
153
|
headers: {
|
|
124
154
|
...MCP_POST_HEADERS,
|
|
@@ -135,7 +165,7 @@ describe("HTTP transport", () => {
|
|
|
135
165
|
});
|
|
136
166
|
|
|
137
167
|
it("accepts initialize request with Bearer token and returns session ID", async () => {
|
|
138
|
-
const response = await fetch(
|
|
168
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
139
169
|
method: "POST",
|
|
140
170
|
headers: mcpHeaders(),
|
|
141
171
|
body: initializeBody(),
|
|
@@ -147,7 +177,7 @@ describe("HTTP transport", () => {
|
|
|
147
177
|
});
|
|
148
178
|
|
|
149
179
|
it("lists all tools after initialization", async () => {
|
|
150
|
-
const initResponse = await fetch(
|
|
180
|
+
const initResponse = await fetch(`${baseUrl}/mcp`, {
|
|
151
181
|
method: "POST",
|
|
152
182
|
headers: mcpHeaders(),
|
|
153
183
|
body: initializeBody(),
|
|
@@ -156,7 +186,7 @@ describe("HTTP transport", () => {
|
|
|
156
186
|
const sessionId = initResponse.headers.get("mcp-session-id")!;
|
|
157
187
|
|
|
158
188
|
// Send initialized notification
|
|
159
|
-
await fetch(
|
|
189
|
+
await fetch(`${baseUrl}/mcp`, {
|
|
160
190
|
method: "POST",
|
|
161
191
|
headers: mcpHeaders({ sessionId }),
|
|
162
192
|
body: JSON.stringify({
|
|
@@ -166,7 +196,7 @@ describe("HTTP transport", () => {
|
|
|
166
196
|
});
|
|
167
197
|
|
|
168
198
|
// List tools
|
|
169
|
-
const toolsResponse = await fetch(
|
|
199
|
+
const toolsResponse = await fetch(`${baseUrl}/mcp`, {
|
|
170
200
|
method: "POST",
|
|
171
201
|
headers: mcpHeaders({ sessionId }),
|
|
172
202
|
body: JSON.stringify({
|
|
@@ -187,7 +217,7 @@ describe("HTTP transport", () => {
|
|
|
187
217
|
|
|
188
218
|
describe("DELETE /mcp", () => {
|
|
189
219
|
it("closes an existing session", async () => {
|
|
190
|
-
const initResponse = await fetch(
|
|
220
|
+
const initResponse = await fetch(`${baseUrl}/mcp`, {
|
|
191
221
|
method: "POST",
|
|
192
222
|
headers: mcpHeaders(),
|
|
193
223
|
body: initializeBody(),
|
|
@@ -195,18 +225,24 @@ describe("HTTP transport", () => {
|
|
|
195
225
|
|
|
196
226
|
const sessionId = initResponse.headers.get("mcp-session-id")!;
|
|
197
227
|
|
|
198
|
-
const deleteResponse = await fetch(
|
|
228
|
+
const deleteResponse = await fetch(`${baseUrl}/mcp`, {
|
|
199
229
|
method: "DELETE",
|
|
200
|
-
headers: {
|
|
230
|
+
headers: {
|
|
231
|
+
"mcp-session-id": sessionId,
|
|
232
|
+
Authorization: `Bearer ${BEARER_TOKEN}`,
|
|
233
|
+
},
|
|
201
234
|
});
|
|
202
235
|
|
|
203
236
|
expect(deleteResponse.status).toBe(200);
|
|
204
237
|
});
|
|
205
238
|
|
|
206
239
|
it("returns 404 for unknown session", async () => {
|
|
207
|
-
const response = await fetch(
|
|
240
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
208
241
|
method: "DELETE",
|
|
209
|
-
headers: {
|
|
242
|
+
headers: {
|
|
243
|
+
"mcp-session-id": "nonexistent-session-id",
|
|
244
|
+
Authorization: `Bearer ${BEARER_TOKEN}`,
|
|
245
|
+
},
|
|
210
246
|
});
|
|
211
247
|
|
|
212
248
|
expect(response.status).toBe(404);
|
|
@@ -217,7 +253,7 @@ describe("HTTP transport", () => {
|
|
|
217
253
|
describe("/.well-known/oauth-authorization-server", () => {
|
|
218
254
|
it("returns OAuth metadata with token endpoint", async () => {
|
|
219
255
|
const response = await fetch(
|
|
220
|
-
|
|
256
|
+
`${baseUrl}/.well-known/oauth-authorization-server`
|
|
221
257
|
);
|
|
222
258
|
const body = await response.json();
|
|
223
259
|
|
|
@@ -229,14 +265,11 @@ describe("HTTP transport", () => {
|
|
|
229
265
|
|
|
230
266
|
describe("/oauth/token", () => {
|
|
231
267
|
it("returns 400 for unsupported grant type", async () => {
|
|
232
|
-
const response = await fetch(
|
|
233
|
-
|
|
234
|
-
{
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
body: "grant_type=authorization_code&client_secret=test-key",
|
|
238
|
-
}
|
|
239
|
-
);
|
|
268
|
+
const response = await fetch(`${baseUrl}/oauth/token`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
271
|
+
body: "grant_type=authorization_code&client_secret=test-key",
|
|
272
|
+
});
|
|
240
273
|
const body = await response.json();
|
|
241
274
|
|
|
242
275
|
expect(response.status).toBe(400);
|
|
@@ -244,14 +277,11 @@ describe("HTTP transport", () => {
|
|
|
244
277
|
});
|
|
245
278
|
|
|
246
279
|
it("returns 400 when client_secret is missing", async () => {
|
|
247
|
-
const response = await fetch(
|
|
248
|
-
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
body: "grant_type=client_credentials",
|
|
253
|
-
}
|
|
254
|
-
);
|
|
280
|
+
const response = await fetch(`${baseUrl}/oauth/token`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
283
|
+
body: "grant_type=client_credentials",
|
|
284
|
+
});
|
|
255
285
|
const body = await response.json();
|
|
256
286
|
|
|
257
287
|
expect(response.status).toBe(400);
|
|
@@ -259,14 +289,11 @@ describe("HTTP transport", () => {
|
|
|
259
289
|
});
|
|
260
290
|
|
|
261
291
|
it("issues an access token for valid client credentials", async () => {
|
|
262
|
-
const response = await fetch(
|
|
263
|
-
|
|
264
|
-
{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
body: "grant_type=client_credentials&client_secret=my-langwatch-api-key",
|
|
268
|
-
}
|
|
269
|
-
);
|
|
292
|
+
const response = await fetch(`${baseUrl}/oauth/token`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
295
|
+
body: "grant_type=client_credentials&client_secret=my-langwatch-api-key",
|
|
296
|
+
});
|
|
270
297
|
const body = await response.json();
|
|
271
298
|
|
|
272
299
|
expect(response.status).toBe(200);
|
|
@@ -277,20 +304,17 @@ describe("HTTP transport", () => {
|
|
|
277
304
|
|
|
278
305
|
it("issued token works for MCP initialize request", async () => {
|
|
279
306
|
// Get OAuth token
|
|
280
|
-
const tokenResponse = await fetch(
|
|
281
|
-
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
body: `grant_type=client_credentials&client_secret=${BEARER_TOKEN}`,
|
|
286
|
-
}
|
|
287
|
-
);
|
|
307
|
+
const tokenResponse = await fetch(`${baseUrl}/oauth/token`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
310
|
+
body: `grant_type=client_credentials&client_secret=${BEARER_TOKEN}`,
|
|
311
|
+
});
|
|
288
312
|
expect(tokenResponse.status).toBe(200);
|
|
289
313
|
const { access_token } = await tokenResponse.json();
|
|
290
314
|
expect(access_token).toBeTruthy();
|
|
291
315
|
|
|
292
316
|
// Use OAuth token for MCP initialize
|
|
293
|
-
const response = await fetch(
|
|
317
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
294
318
|
method: "POST",
|
|
295
319
|
headers: {
|
|
296
320
|
...MCP_POST_HEADERS,
|
|
@@ -307,20 +331,17 @@ describe("HTTP transport", () => {
|
|
|
307
331
|
const controller = new AbortController();
|
|
308
332
|
|
|
309
333
|
// Get OAuth token
|
|
310
|
-
const tokenResponse = await fetch(
|
|
311
|
-
|
|
312
|
-
{
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
body: `grant_type=client_credentials&client_secret=${BEARER_TOKEN}`,
|
|
316
|
-
}
|
|
317
|
-
);
|
|
334
|
+
const tokenResponse = await fetch(`${baseUrl}/oauth/token`, {
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
337
|
+
body: `grant_type=client_credentials&client_secret=${BEARER_TOKEN}`,
|
|
338
|
+
});
|
|
318
339
|
expect(tokenResponse.status).toBe(200);
|
|
319
340
|
const { access_token } = await tokenResponse.json();
|
|
320
341
|
expect(access_token).toBeTruthy();
|
|
321
342
|
|
|
322
343
|
// Use OAuth token for SSE
|
|
323
|
-
const response = await fetch(
|
|
344
|
+
const response = await fetch(`${baseUrl}/sse`, {
|
|
324
345
|
signal: controller.signal,
|
|
325
346
|
headers: { Authorization: `Bearer ${access_token}` },
|
|
326
347
|
});
|
|
@@ -339,7 +360,7 @@ describe("HTTP transport", () => {
|
|
|
339
360
|
it("returns 401 without authorization", async () => {
|
|
340
361
|
const controller = new AbortController();
|
|
341
362
|
|
|
342
|
-
const response = await fetch(
|
|
363
|
+
const response = await fetch(`${baseUrl}/sse`, {
|
|
343
364
|
signal: controller.signal,
|
|
344
365
|
});
|
|
345
366
|
|
|
@@ -350,7 +371,7 @@ describe("HTTP transport", () => {
|
|
|
350
371
|
it("establishes SSE connection with Bearer token in header", async () => {
|
|
351
372
|
const controller = new AbortController();
|
|
352
373
|
|
|
353
|
-
const response = await fetch(
|
|
374
|
+
const response = await fetch(`${baseUrl}/sse`, {
|
|
354
375
|
signal: controller.signal,
|
|
355
376
|
headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
|
|
356
377
|
});
|
|
@@ -370,28 +391,5 @@ describe("HTTP transport", () => {
|
|
|
370
391
|
|
|
371
392
|
controller.abort();
|
|
372
393
|
});
|
|
373
|
-
|
|
374
|
-
it("establishes SSE connection with apiKey query parameter", async () => {
|
|
375
|
-
const controller = new AbortController();
|
|
376
|
-
|
|
377
|
-
const response = await fetch(
|
|
378
|
-
`http://localhost:${port}/sse?apiKey=${BEARER_TOKEN}`,
|
|
379
|
-
{ signal: controller.signal }
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
expect(response.status).toBe(200);
|
|
383
|
-
expect(response.headers.get("content-type")).toContain(
|
|
384
|
-
"text/event-stream"
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
const reader = response.body!.getReader();
|
|
388
|
-
const decoder = new TextDecoder();
|
|
389
|
-
const { value } = await reader.read();
|
|
390
|
-
const text = decoder.decode(value);
|
|
391
|
-
|
|
392
|
-
expect(text).toContain("event: endpoint");
|
|
393
|
-
|
|
394
|
-
controller.abort();
|
|
395
|
-
});
|
|
396
394
|
});
|
|
397
395
|
});
|