@langwatch/mcp-server 2.1.0 → 2.2.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/.gitignore +7 -0
- package/.npmignore +41 -0
- package/CHANGELOG.md +192 -201
- package/CONTRIBUTING.md +10 -0
- package/README.md +70 -68
- package/dist/add-dashboard-widget-4ZC5UN73.js +50 -0
- package/dist/add-dashboard-widget-ACC6QYBG.js +49 -0
- package/dist/archive-project-KWFM3I5Z.js +19 -0
- package/dist/{archive-project-W3IKI7UL.js → archive-project-SPVHXK6A.js} +4 -4
- package/dist/{archive-run-plan-474OKHHU.js → archive-run-plan-65QN3XRX.js} +4 -4
- package/dist/archive-run-plan-Z4SYE6BP.js +15 -0
- package/dist/{archive-scenario-V5UNYK26.js → archive-scenario-A6QV5LOW.js} +4 -4
- package/dist/archive-scenario-FXEYQNEH.js +19 -0
- package/dist/archive-test-suite-5ADQ3KPQ.js +15 -0
- package/dist/{archive-test-suite-O7FLDMXK.js → archive-test-suite-NKZDZY3D.js} +4 -4
- package/dist/assign-prompt-tag-5IUZXJ2T.js +23 -0
- package/dist/{assign-prompt-tag-Q46WRB6P.js → assign-prompt-tag-HBZLBIFD.js} +8 -4
- package/dist/{chunk-OJLYYJWI.js → chunk-3OCG3QPU.js} +7 -10
- package/dist/chunk-3YLN3ATJ.js +241 -0
- package/dist/chunk-5FNCQRCX.js +91 -0
- package/dist/{chunk-UF6XIGYR.js → chunk-6YZUOD34.js} +2 -5
- package/dist/{chunk-WVI5CJ6S.js → chunk-7AQLPVFL.js} +2 -2
- package/dist/chunk-7VNSRCXO.js +129 -0
- package/dist/{chunk-G4ZAZTZW.js → chunk-B4J3PJLX.js} +11 -10
- package/dist/{chunk-ZXKLPC2E.js → chunk-BIIKE3EI.js} +1 -1
- package/dist/{chunk-37SMPMQS.js → chunk-DMM3JQ42.js} +7 -2
- package/dist/{chunk-NUFPTHKU.js → chunk-FOKVY3BK.js} +1 -1
- package/dist/{chunk-WT4PO4A3.js → chunk-H6WGMG2T.js} +10 -7
- package/dist/{chunk-EYXMLE5X.js → chunk-HGELNIVY.js} +56 -47
- package/dist/{chunk-UJPXFA26.js → chunk-HHMNZAPD.js} +101 -74
- package/dist/{chunk-RVAE2MEA.js → chunk-I7ATIZED.js} +7 -7
- package/dist/{chunk-S4GVX36W.js → chunk-ILZDO25L.js} +7 -10
- package/dist/{chunk-QRO35SOM.js → chunk-J7PQKBLD.js} +652 -528
- package/dist/{chunk-SR6QPRIE.js → chunk-JRJPH75H.js} +7 -13
- package/dist/chunk-K2I7Z3LZ.js +28 -0
- package/dist/{chunk-UFOYFYI7.js → chunk-NKMTIHIJ.js} +6 -9
- package/dist/{chunk-HV4DWKBI.js → chunk-O7EMWEVJ.js} +2 -4
- package/dist/{chunk-56OGTS24.js → chunk-PTDR56Z3.js} +4 -11
- package/dist/{chunk-JR6DJZOU.js → chunk-RABYBFX5.js} +4 -10
- package/dist/chunk-SUPUUC4W.js +64 -0
- package/dist/{chunk-JTQJRNFJ.js → chunk-SXFC3FNH.js} +4 -4
- package/dist/{chunk-CKNAH5TG.js → chunk-U24EACC4.js} +3 -10
- package/dist/chunk-UNJM3BIL.js +21 -0
- package/dist/{chunk-QAMP3CGR.js → chunk-UTJCWXPZ.js} +4 -4
- package/dist/chunk-UWLE5T2F.js +312 -0
- package/dist/{chunk-6DJDEXET.js → chunk-VMWJD732.js} +6 -12
- package/dist/{chunk-7E4FZIYJ.js → chunk-XUIJYNRJ.js} +1 -1
- package/dist/chunk-Y34DHR5K.js +67 -0
- package/dist/chunk-YCARO42Z.js +516 -0
- package/dist/config-EAKW6F26.js +16 -0
- package/dist/config.cjs +109 -0
- package/dist/config.js +5 -4
- package/dist/{create-agent-JFJ2K6X3.js → create-agent-AYZ3LB3V.js} +4 -4
- package/dist/create-agent-S5ZBCEFT.js +23 -0
- package/dist/create-annotation-LHNCLHYP.js +29 -0
- package/dist/{create-annotation-EUXXQ6NS.js → create-annotation-VUSXULQK.js} +10 -5
- package/dist/{create-api-key-BGBB76JX.js → create-api-key-NNYKARMG.js} +5 -7
- package/dist/create-api-key-T4KSNPKB.js +24 -0
- package/dist/create-dashboard-DHVFMEU4.js +18 -0
- package/dist/{create-dashboard-Z76AIPLS.js → create-dashboard-WTKKS5Z6.js} +4 -4
- package/dist/{create-dataset-FD5VAXU4.js → create-dataset-2MC4SD4O.js} +6 -6
- package/dist/create-dataset-LUP5PVDM.js +22 -0
- package/dist/create-dataset-records-NSKBZP3G.js +20 -0
- package/dist/{create-dataset-records-WQUXXOHI.js → create-dataset-records-W4GLN2FV.js} +5 -7
- package/dist/{create-evaluator-6NV33Z52.js → create-evaluator-643G6LXL.js} +4 -4
- package/dist/create-evaluator-HMTYO5SS.js +27 -0
- package/dist/create-mcp-server.cjs +9513 -0
- package/dist/create-mcp-server.js +2647 -10
- package/dist/create-project-6N7PIYRY.js +44 -0
- package/dist/{create-project-ZQ7TFIO2.js → create-project-KAIC46UY.js} +4 -4
- package/dist/{create-prompt-BS6QRCUU.js → create-prompt-E67LXIYT.js} +5 -7
- package/dist/create-prompt-PKJ7EDRR.js +37 -0
- package/dist/create-prompt-tag-4YO3FSJ3.js +17 -0
- package/dist/{create-prompt-tag-FCQ5ELSQ.js → create-prompt-tag-OPQUEXWB.js} +3 -3
- package/dist/create-scenario-5PJBMAXO.js +33 -0
- package/dist/{create-scenario-DRPTPXU3.js → create-scenario-UEM77OE7.js} +5 -5
- package/dist/{create-test-suite-ZIZCCFJ4.js → create-test-suite-4IPXVSFQ.js} +6 -6
- package/dist/create-test-suite-SZEPVXSE.js +36 -0
- package/dist/create-trigger-5HZSCRAN.js +49 -0
- package/dist/create-trigger-PRELMT6J.js +48 -0
- package/dist/delete-agent-JUIZWAXG.js +18 -0
- package/dist/{delete-agent-SA4JMUZF.js → delete-agent-QMEK3XHK.js} +4 -4
- package/dist/delete-annotation-HY3O2OGR.js +17 -0
- package/dist/{delete-annotation-X3KKK7QY.js → delete-annotation-VKIQWUYW.js} +4 -4
- package/dist/delete-dashboard-76N66XXL.js +18 -0
- package/dist/{delete-dashboard-LYMRPF27.js → delete-dashboard-JVIVHNJM.js} +4 -4
- package/dist/delete-dataset-HQCR66MG.js +18 -0
- package/dist/{delete-dataset-W6EZ44HY.js → delete-dataset-WKRUW4FP.js} +5 -7
- package/dist/{delete-dataset-records-7CNOTALB.js → delete-dataset-records-C2IQFFVQ.js} +5 -7
- package/dist/delete-dataset-records-SDUHQLLM.js +19 -0
- package/dist/delete-evaluator-4SNSCLEE.js +16 -0
- package/dist/delete-evaluator-QP3QHTLN.js +15 -0
- package/dist/delete-prompt-tag-NZ4JQMYG.js +17 -0
- package/dist/{delete-prompt-tag-QB3OCZUV.js → delete-prompt-tag-YYQ4VMAD.js} +3 -3
- package/dist/{delete-workflow-NG2VXYYM.js → delete-workflow-5J46T73T.js} +4 -4
- package/dist/delete-workflow-JSWABL5F.js +17 -0
- package/dist/discover-evaluator-schema-UHWA5HVL.js +120 -0
- package/dist/discover-evaluator-schema-XKCLLCB3.js +118 -0
- package/dist/discover-scenario-schema-NOJY6S7E.js +137 -0
- package/dist/{discover-scenario-schema-W7KBXA4J.js → discover-scenario-schema-OOFIQKOG.js} +37 -17
- package/dist/{discover-schema-L6QDPON3.js → discover-schema-55YJBFGQ.js} +181 -186
- package/dist/discover-schema-A33SLMXW.js +442 -0
- package/dist/get-agent-LJQX7OF4.js +69 -0
- package/dist/{get-agent-7THF7HCU.js → get-agent-OTIYUFZA.js} +15 -11
- package/dist/get-analytics-DCS72353.js +80 -0
- package/dist/get-analytics-IQZKJHLW.js +79 -0
- package/dist/get-dashboard-5NYJEIX2.js +29 -0
- package/dist/{get-dashboard-P4WWVHFU.js → get-dashboard-QMTYDQAZ.js} +4 -4
- package/dist/get-dataset-ALSP7RTX.js +64 -0
- package/dist/get-dataset-X4B5JDG2.js +63 -0
- package/dist/{get-evaluator-RZ44UIN6.js → get-evaluator-ISMC4LLI.js} +4 -4
- package/dist/get-evaluator-XEUZGZCH.js +47 -0
- package/dist/{get-project-QSQEAB33.js → get-project-5VKROSDH.js} +4 -4
- package/dist/get-project-RM6FRCBK.js +26 -0
- package/dist/{get-prompt-GVKXF5NL.js → get-prompt-OWXJZHQQ.js} +53 -58
- package/dist/get-prompt-W52DY36E.js +94 -0
- package/dist/{get-run-plan-NY7J5GRE.js → get-run-plan-BE3M2AUX.js} +7 -9
- package/dist/get-run-plan-RUXQ6PAX.js +58 -0
- package/dist/get-scenario-RNM7BAX2.js +37 -0
- package/dist/{get-scenario-Q3LLQE4T.js → get-scenario-YM4ODWW6.js} +5 -5
- package/dist/get-simulation-run-6GIATQE5.js +69 -0
- package/dist/{get-simulation-run-WINEYDMK.js → get-simulation-run-LCFSMPGQ.js} +26 -22
- package/dist/{get-test-suite-4HKA7OKS.js → get-test-suite-GC2O7YH6.js} +5 -5
- package/dist/get-test-suite-P5LJ6VLF.js +48 -0
- package/dist/get-trace-KBRVV4NN.js +45 -0
- package/dist/{get-trace-RCUV7N4O.js → get-trace-SEF3FMR5.js} +11 -13
- package/dist/get-workflow-7PVED7JE.js +25 -0
- package/dist/{get-workflow-EEOC7Y45.js → get-workflow-YXULW4GC.js} +4 -4
- package/dist/{http-security-EU3CHTY7.js → http-security-DNLBNQZL.js} +2 -2
- package/dist/{http-server-WCDUARTH.js → http-server-IR4QOS43.js} +4071 -347
- package/dist/index.js +58671 -37983
- package/dist/langwatch-api-annotations-6SA4OB4C.js +17 -0
- package/dist/{langwatch-api-annotations-6DHVZWUK.js → langwatch-api-annotations-TBHLCQ5R.js} +4 -4
- package/dist/langwatch-api-dashboards-6EO2VVZW.js +17 -0
- package/dist/{langwatch-api-dashboards-2KJL2M42.js → langwatch-api-dashboards-NHU5BZD7.js} +4 -4
- package/dist/langwatch-api-monitors-CBF3FGIM.js +47 -0
- package/dist/{langwatch-api-monitors-6ISFJ7WX.js → langwatch-api-monitors-YNWA2M5Y.js} +11 -16
- package/dist/langwatch-api-secrets-ADTOJELP.js +56 -0
- package/dist/langwatch-api-secrets-RNFO4LF2.js +55 -0
- package/dist/langwatch-api-triggers-KQDQC6L5.js +22 -0
- package/dist/langwatch-api-triggers-QS7HDIVW.js +23 -0
- package/dist/{langwatch-api-workflows-6UJL5U4Z.js → langwatch-api-workflows-OKC4FXIF.js} +4 -4
- package/dist/langwatch-api-workflows-XGJE6SR5.js +15 -0
- package/dist/{list-agents-AMVDQPQL.js → list-agents-HVYWEQMI.js} +4 -4
- package/dist/list-agents-RBAOCF53.js +42 -0
- package/dist/list-annotations-HJH4NBLD.js +38 -0
- package/dist/{list-annotations-6PR5NLIC.js → list-annotations-J4UAHSUN.js} +10 -5
- package/dist/{list-api-keys-GVPNBIHQ.js → list-api-keys-7UTJRZDG.js} +10 -5
- package/dist/list-api-keys-A2PMV435.js +42 -0
- package/dist/list-dashboard-boards-OPWROHMG.js +33 -0
- package/dist/list-dashboard-boards-U2GJ7GC5.js +34 -0
- package/dist/{list-dashboards-Z3BZKDGF.js → list-dashboards-ROSYFLDQ.js} +4 -4
- package/dist/list-dashboards-RQ635ZU7.js +32 -0
- package/dist/{list-dataset-records-O67HZ35Y.js → list-dataset-records-M5TKA6LK.js} +14 -10
- package/dist/list-dataset-records-WFM7F33C.js +43 -0
- package/dist/{list-datasets-WXZL6UMP.js → list-datasets-B5X4P7BQ.js} +9 -13
- package/dist/list-datasets-THUKQHPX.js +56 -0
- package/dist/{list-evaluators-PAX72WCN.js → list-evaluators-MYH2OVCI.js} +5 -7
- package/dist/list-evaluators-NLVWF6K7.js +32 -0
- package/dist/{list-model-providers-VVLZMLCX.js → list-model-providers-3JUGP4YI.js} +4 -4
- package/dist/list-model-providers-H3XKORFO.js +35 -0
- package/dist/{list-projects-AQVI6SUK.js → list-projects-6EJ5YYEW.js} +5 -7
- package/dist/list-projects-YNNSRMLC.js +40 -0
- package/dist/{list-prompt-tags-WZFBJOHG.js → list-prompt-tags-NVFQO6WX.js} +3 -3
- package/dist/list-prompt-tags-XUQTTAH4.js +23 -0
- package/dist/{list-prompts-BQOHRHYT.js → list-prompts-HSIMMHTF.js} +4 -6
- package/dist/list-prompts-IRI6AIXO.js +30 -0
- package/dist/{list-run-plans-UBAU2DGH.js → list-run-plans-PX2XI6EK.js} +5 -5
- package/dist/list-run-plans-QALLCN2K.js +45 -0
- package/dist/{list-scenarios-MS4IUIVM.js → list-scenarios-QZRPVHS4.js} +13 -11
- package/dist/list-scenarios-SSJQEMJP.js +45 -0
- package/dist/list-simulation-runs-KNOLGMDV.js +53 -0
- package/dist/{list-simulation-runs-D6YF4ZOU.js → list-simulation-runs-OEADLLQR.js} +12 -8
- package/dist/list-test-suites-ANAQBLDG.js +34 -0
- package/dist/{list-test-suites-L7R2O6DR.js → list-test-suites-N4WVLBKK.js} +4 -4
- package/dist/list-trigger-fires-JV3WFYYG.js +19 -0
- package/dist/list-trigger-fires-WTW4DVIW.js +20 -0
- package/dist/list-triggers-SQNIG35Q.js +45 -0
- package/dist/list-triggers-V4Y356AK.js +46 -0
- package/dist/{list-workflows-6MV4XJHF.js → list-workflows-IMSX7SXU.js} +5 -7
- package/dist/list-workflows-ZHTX5LCL.js +33 -0
- package/dist/{rename-prompt-tag-K5LQ4CY6.js → rename-prompt-tag-IQI7MHOI.js} +3 -3
- package/dist/rename-prompt-tag-PA2BRIE6.js +18 -0
- package/dist/{rename-test-suite-NTYHAO5X.js → rename-test-suite-4J7UX5S2.js} +4 -4
- package/dist/rename-test-suite-NNVLMLKY.js +20 -0
- package/dist/report-issue-AS34ZMJF.js +898 -0
- package/dist/{report-issue-RHZZ33IS.js → report-issue-DCZFLGDO.js} +81 -67
- package/dist/{rerun-run-plan-MUDWJBE4.js → rerun-run-plan-TG2V4WM4.js} +5 -5
- package/dist/rerun-run-plan-ZI5OY3VA.js +21 -0
- package/dist/revoke-api-key-3J3ZA4UB.js +15 -0
- package/dist/{revoke-api-key-6LAZ3QZZ.js → revoke-api-key-SFMEFWJC.js} +4 -4
- package/dist/run-agent-W4VJP72Z.js +75 -0
- package/dist/run-agent-YNV6ZHW4.js +74 -0
- package/dist/run-plan-6C77GWG4.js +38 -0
- package/dist/{run-plan-T4ZYOZGA.js → run-plan-PAARQSI4.js} +7 -7
- package/dist/run-query-KJDH544C.js +47 -0
- package/dist/run-query-T4QW2Z5O.js +46 -0
- package/dist/{run-test-suite-L4MZ7G4D.js → run-test-suite-SQJELEZX.js} +6 -6
- package/dist/run-test-suite-STB6NUJM.js +25 -0
- package/dist/{run-workflow-DFIS23AP.js → run-workflow-D6PQNBI6.js} +4 -4
- package/dist/run-workflow-SP32SO5M.js +36 -0
- package/dist/search-traces-HCBGOG6R.js +170 -0
- package/dist/search-traces-HSTQ6WTQ.js +171 -0
- package/dist/{set-model-provider-XRNMFOVR.js → set-model-provider-GF7NXUJA.js} +4 -4
- package/dist/set-model-provider-KDQ7C4EZ.js +33 -0
- package/dist/{update-agent-DUWNYBNA.js → update-agent-DWG43T2P.js} +4 -4
- package/dist/update-agent-MXNOBS4H.js +20 -0
- package/dist/update-dashboard-widget-CJKJLCDY.js +71 -0
- package/dist/update-dashboard-widget-CYQDS2NR.js +70 -0
- package/dist/update-dataset-DQH3OHOU.js +22 -0
- package/dist/{update-dataset-YSOMDCOB.js → update-dataset-SQZ6T4WX.js} +6 -6
- package/dist/{update-dataset-record-DEA2ITYR.js → update-dataset-record-3AT4IPBC.js} +4 -4
- package/dist/update-dataset-record-7PLDGTMR.js +19 -0
- package/dist/update-evaluator-4LFTKFPT.js +24 -0
- package/dist/{update-evaluator-NH7Q5GWV.js → update-evaluator-IGTX6PX4.js} +4 -4
- package/dist/{update-project-Q26FBSYH.js → update-project-PLJIOZS6.js} +4 -4
- package/dist/update-project-QVHSJRUP.js +24 -0
- package/dist/update-prompt-GDKRYJI3.js +140 -0
- package/dist/{update-prompt-ZQQ2C3KU.js → update-prompt-MY263DYF.js} +27 -29
- package/dist/update-scenario-7KA3JDXA.js +34 -0
- package/dist/{update-scenario-WUX34JAR.js → update-scenario-MOHRQKAQ.js} +5 -5
- package/package.json +35 -39
- package/pyproject.toml +8 -8
- package/src/config.d.ts +4 -0
- package/src/config.ts +20 -29
- package/src/create-mcp-server.ts +755 -609
- package/src/documentation-fetch.ts +21 -20
- package/src/http-security.ts +166 -135
- package/src/http-server.ts +189 -224
- package/src/index.ts +14 -16
- package/src/langwatch-api-agents.ts +18 -21
- package/src/langwatch-api-annotations.ts +11 -12
- package/src/langwatch-api-api-keys.ts +8 -9
- package/src/langwatch-api-boards.ts +46 -0
- package/src/langwatch-api-dashboard-widgets.ts +112 -0
- package/src/langwatch-api-dashboards.ts +13 -10
- package/src/langwatch-api-datasets.ts +98 -32
- package/src/langwatch-api-evaluators.ts +13 -19
- package/src/langwatch-api-model-providers.ts +4 -4
- package/src/langwatch-api-monitors.ts +14 -18
- package/src/langwatch-api-projects.ts +27 -9
- package/src/langwatch-api-query.ts +114 -0
- package/src/langwatch-api-run-plans.ts +12 -29
- package/src/langwatch-api-scenarios.ts +9 -11
- package/src/langwatch-api-secrets.ts +31 -24
- package/src/langwatch-api-simulation-runs.ts +6 -8
- package/src/langwatch-api-test-suites.ts +14 -33
- package/src/langwatch-api-triggers.ts +96 -34
- package/src/langwatch-api-workflows.ts +9 -9
- package/src/langwatch-api.ts +108 -124
- package/src/public-http-request.ts +21 -12
- package/src/schemas/analytics-metrics.ts +3 -15
- package/src/schemas/create-dataset.ts +3 -4
- package/src/schemas/run-plan.ts +13 -29
- package/src/schemas/suite-fields.ts +6 -23
- package/src/schemas/triggers.ts +365 -0
- package/src/tools/add-dashboard-widget.ts +63 -0
- package/src/tools/archive-project.ts +2 -4
- package/src/tools/archive-run-plan.ts +2 -4
- package/src/tools/archive-scenario.ts +2 -4
- package/src/tools/archive-test-suite.ts +2 -4
- package/src/tools/assign-prompt-tag.ts +6 -2
- package/src/tools/create-agent.ts +1 -1
- package/src/tools/create-annotation.ts +7 -2
- package/src/tools/create-api-key.ts +4 -6
- package/src/tools/create-dashboard.ts +1 -1
- package/src/tools/create-dataset-records.ts +4 -8
- package/src/tools/create-dataset.ts +5 -7
- package/src/tools/create-evaluator.ts +3 -5
- package/src/tools/create-project.ts +2 -2
- package/src/tools/create-prompt-tag.ts +2 -4
- package/src/tools/create-prompt.ts +9 -18
- package/src/tools/create-scenario.ts +5 -7
- package/src/tools/create-test-suite.ts +4 -9
- package/src/tools/create-trigger.ts +55 -0
- package/src/tools/dashboard-exists.ts +25 -0
- package/src/tools/dashboard-widget-queries.ts +32 -0
- package/src/tools/delete-agent.ts +1 -1
- package/src/tools/delete-annotation.ts +1 -1
- package/src/tools/delete-dashboard.ts +1 -1
- package/src/tools/delete-dataset-records.ts +4 -8
- package/src/tools/delete-dataset.ts +3 -7
- package/src/tools/delete-evaluator.ts +4 -6
- package/src/tools/delete-prompt-tag.ts +2 -4
- package/src/tools/delete-workflow.ts +1 -1
- package/src/tools/discover-evaluator-schema.ts +34 -25
- package/src/tools/discover-scenario-schema.ts +41 -21
- package/src/tools/discover-schema.ts +149 -27
- package/src/tools/experiment-run-status.ts +2 -8
- package/src/tools/format-dataset-mutation.ts +5 -10
- package/src/tools/format-run-plan.ts +7 -21
- package/src/tools/format-scenario.ts +2 -4
- package/src/tools/format-suite-details.ts +6 -12
- package/src/tools/get-agent.ts +15 -6
- package/src/tools/get-analytics.ts +52 -46
- package/src/tools/get-dashboard.ts +1 -1
- package/src/tools/get-dataset.ts +34 -16
- package/src/tools/get-evaluator.ts +4 -11
- package/src/tools/get-experiment-results.ts +219 -186
- package/src/tools/get-project.ts +2 -4
- package/src/tools/get-prompt.ts +63 -84
- package/src/tools/get-run-plan.ts +4 -6
- package/src/tools/get-scenario.ts +4 -6
- package/src/tools/get-simulation-run.ts +27 -24
- package/src/tools/get-test-suite.ts +2 -5
- package/src/tools/get-trace.ts +16 -17
- package/src/tools/get-workflow.ts +1 -1
- package/src/tools/list-agents.ts +1 -1
- package/src/tools/list-annotations.ts +8 -5
- package/src/tools/list-api-keys.ts +7 -2
- package/src/tools/list-dashboard-boards.ts +28 -0
- package/src/tools/list-dashboards.ts +1 -1
- package/src/tools/list-dataset-records.ts +8 -6
- package/src/tools/list-datasets.ts +12 -23
- package/src/tools/list-evaluators.ts +4 -8
- package/src/tools/list-experiment-runs.ts +5 -9
- package/src/tools/list-experiments.ts +5 -11
- package/src/tools/list-model-providers.ts +3 -5
- package/src/tools/list-projects.ts +2 -4
- package/src/tools/list-prompt-tags.ts +3 -3
- package/src/tools/list-prompts.ts +4 -8
- package/src/tools/list-run-plans.ts +2 -2
- package/src/tools/list-scenarios.ts +14 -15
- package/src/tools/list-simulation-runs.ts +8 -4
- package/src/tools/list-test-suites.ts +1 -1
- package/src/tools/list-trigger-fires.ts +17 -0
- package/src/tools/list-triggers.ts +39 -0
- package/src/tools/list-workflows.ts +2 -4
- package/src/tools/rename-prompt-tag.ts +1 -1
- package/src/tools/rename-test-suite.ts +2 -5
- package/src/tools/report-issue.ts +5 -9
- package/src/tools/rerun-run-plan.ts +2 -5
- package/src/tools/revoke-api-key.ts +2 -4
- package/src/tools/run-agent.ts +29 -26
- package/src/tools/run-experiment.ts +36 -37
- package/src/tools/run-plan.ts +4 -7
- package/src/tools/run-query.ts +47 -0
- package/src/tools/run-test-suite.ts +4 -7
- package/src/tools/run-workflow.ts +2 -5
- package/src/tools/search-traces.ts +161 -47
- package/src/tools/set-model-provider.ts +4 -6
- package/src/tools/test-agent.ts +2 -2
- package/src/tools/update-agent.ts +1 -1
- package/src/tools/update-dashboard-widget.ts +97 -0
- package/src/tools/update-dataset-record.ts +1 -1
- package/src/tools/update-dataset.ts +5 -7
- package/src/tools/update-evaluator.ts +3 -5
- package/src/tools/update-project.ts +1 -1
- package/src/tools/update-prompt.ts +44 -59
- package/src/tools/update-scenario.ts +5 -7
- package/src/tools/update-test-suite.ts +6 -11
- package/src/utils/date-parsing.ts +1 -1
- package/src/utils/escape-markdown.ts +3 -8
- package/src/utils/format-evaluations.ts +6 -6
- package/src/utils/markdown-table.ts +58 -0
- package/src/utils/trace-id-shape.ts +20 -0
- package/tsconfig.app.json +24 -0
- package/tsconfig.json +14 -12
- package/vitest.integration.config.js +10 -0
- package/vitest.scenario.config.js +9 -0
- package/.eslintrc.cjs +0 -36
- package/dist/chunk-AN4OUA4D.js +0 -86
- package/dist/chunk-GELJ2MQR.js +0 -226
- package/dist/chunk-K37ZNPXK.js +0 -24
- package/dist/config.d.ts +0 -17
- package/dist/create-mcp-server.d.ts +0 -10
- package/dist/delete-evaluator-RKYC4TNB.js +0 -16
- package/dist/discover-evaluator-schema-ESSHQGPY.js +0 -1912
- package/dist/get-analytics-R6AZD3AW.js +0 -73
- package/dist/get-dataset-AKTYSQDI.js +0 -49
- package/dist/index.d.ts +0 -2
- package/dist/langwatch-api-secrets-OEBD6GBU.js +0 -41
- package/dist/langwatch-api-triggers-5E6DDGX6.js +0 -31
- package/dist/run-agent-CAGWGTYT.js +0 -72
- package/dist/search-traces-4XTJYXIK.js +0 -79
- package/src/__tests__/agent-tools.unit.test.ts +0 -257
- package/src/__tests__/all-tools.integration.test.ts +0 -1872
- package/src/__tests__/config.unit.test.ts +0 -202
- package/src/__tests__/datasets.integration.test.ts +0 -620
- package/src/__tests__/datasets.unit.test.ts +0 -244
- package/src/__tests__/date-parsing.unit.test.ts +0 -78
- package/src/__tests__/discover-evaluator-schema.unit.test.ts +0 -89
- package/src/__tests__/discover-schema.unit.test.ts +0 -118
- package/src/__tests__/docker.integration.test.ts +0 -318
- package/src/__tests__/documentation-fetch-security.integration.test.ts +0 -182
- package/src/__tests__/evaluator-tools.unit.test.ts +0 -262
- package/src/__tests__/experiment-list-tools.unit.test.ts +0 -157
- package/src/__tests__/experiment-status.unit.test.ts +0 -194
- package/src/__tests__/feature-map-tool-drift.unit.test.ts +0 -93
- package/src/__tests__/get-experiment-results.unit.test.ts +0 -313
- package/src/__tests__/http-security-origin.unit.test.ts +0 -97
- package/src/__tests__/http-security-sessions.unit.test.ts +0 -209
- package/src/__tests__/http-security-verifier.unit.test.ts +0 -248
- package/src/__tests__/http-server-allocation.integration.test.ts +0 -279
- package/src/__tests__/http-server-origin.integration.test.ts +0 -169
- package/src/__tests__/http-server-session-auth.integration.test.ts +0 -341
- package/src/__tests__/http-transport.integration.test.ts +0 -395
- package/src/__tests__/integration.integration.test.ts +0 -284
- package/src/__tests__/langwatch-api.unit.test.ts +0 -661
- package/src/__tests__/model-provider-tools.unit.test.ts +0 -190
- package/src/__tests__/project-api-key-tools.unit.test.ts +0 -451
- package/src/__tests__/prompt-response-types.unit.test.ts +0 -51
- package/src/__tests__/prompts.integration.test.ts +0 -736
- package/src/__tests__/public-http-request-security.integration.test.ts +0 -118
- package/src/__tests__/report-issue.integration.test.ts +0 -147
- package/src/__tests__/run-plan-tools.unit.test.ts +0 -565
- package/src/__tests__/scenario-tools.integration.test.ts +0 -286
- package/src/__tests__/scenario-tools.unit.test.ts +0 -207
- package/src/__tests__/schemas.unit.test.ts +0 -85
- package/src/__tests__/simulation-run-tools.unit.test.ts +0 -116
- package/src/__tests__/support/http-server-harness.ts +0 -138
- package/src/__tests__/test-suite-tools.unit.test.ts +0 -615
- package/src/__tests__/tools.integration.test.ts +0 -896
- package/src/schemas/filter-fields.ts +0 -119
- package/src/schemas/index.ts +0 -9
package/src/http-server.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import type { Request, RequestHandler, 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
1
|
import { randomUUID, createHash } from "node:crypto";
|
|
7
2
|
import type { Server } from "node:http";
|
|
8
3
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import express from "express";
|
|
8
|
+
import type { Request, RequestHandler, Response, NextFunction } from "express";
|
|
9
|
+
|
|
10
|
+
import { getConfig, runWithConfig } from "./config.ts";
|
|
11
|
+
import { createMcpServer } from "./create-mcp-server.ts";
|
|
11
12
|
import {
|
|
12
13
|
admitOAuthToken,
|
|
13
14
|
apiKeysMatch,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
type OAuthTokenEntry,
|
|
22
23
|
type RateLimiter,
|
|
23
24
|
type SessionStore,
|
|
24
|
-
} from "./http-security.
|
|
25
|
+
} from "./http-security.ts";
|
|
25
26
|
|
|
26
27
|
/** Idle time after which a session is closed and forgotten. */
|
|
27
28
|
const SESSION_MAX_AGE_MS = 30 * 60 * 1000;
|
|
@@ -47,10 +48,9 @@ interface ServerRuntime {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* Resolves and verifies the caller's Bearer token
|
|
51
|
-
* given
|
|
52
|
-
*
|
|
53
|
-
* null when the request must not proceed.
|
|
51
|
+
* Resolves and verifies the caller's Bearer token; when `expectedApiKey`
|
|
52
|
+
* is given, it must resolve to that same key. Sends the error response
|
|
53
|
+
* itself and resolves to null when the request must not proceed.
|
|
54
54
|
*/
|
|
55
55
|
type Authenticate = (args: {
|
|
56
56
|
req: Request;
|
|
@@ -82,16 +82,7 @@ function generateAccessToken(): string {
|
|
|
82
82
|
return createHash("sha256").update(randomUUID()).digest("hex");
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
* Client address used for rate limiting.
|
|
87
|
-
*
|
|
88
|
-
* Forwarded headers only mean something when a trusted proxy sets them, so this
|
|
89
|
-
* reads the socket peer unless proxy trust is turned on explicitly. Defaulting
|
|
90
|
-
* to the socket keeps the limits countable: a caller that reaches the port
|
|
91
|
-
* directly cannot rotate `X-Forwarded-For` to reset its own counter. Where that
|
|
92
|
-
* default is wrong, it is wrong in the strict direction, counting a whole proxy
|
|
93
|
-
* as one client rather than not counting at all.
|
|
94
|
-
*/
|
|
85
|
+
// Rate limit key: socket peer unless LANGWATCH_MCP_TRUST_PROXY=true checks X-Forwarded-For.
|
|
95
86
|
function rateLimitKey(req: Request): string {
|
|
96
87
|
if (process.env.LANGWATCH_MCP_TRUST_PROXY === "true") {
|
|
97
88
|
return req.ip ?? req.socket.remoteAddress ?? "unknown";
|
|
@@ -100,10 +91,9 @@ function rateLimitKey(req: Request): string {
|
|
|
100
91
|
}
|
|
101
92
|
|
|
102
93
|
/**
|
|
103
|
-
* Resolves a Bearer token to
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* the key is valid.
|
|
94
|
+
* Resolves a Bearer token to its API key. OAuth-issued access tokens map
|
|
95
|
+
* back to the key they were minted from; anything else is treated as a
|
|
96
|
+
* direct key. Only translates — does not validate.
|
|
107
97
|
*/
|
|
108
98
|
function resolveApiKey({
|
|
109
99
|
token,
|
|
@@ -121,13 +111,7 @@ function resolveApiKey({
|
|
|
121
111
|
return null;
|
|
122
112
|
}
|
|
123
113
|
|
|
124
|
-
function sendUnauthorized({
|
|
125
|
-
res,
|
|
126
|
-
error,
|
|
127
|
-
}: {
|
|
128
|
-
res: Response;
|
|
129
|
-
error: string;
|
|
130
|
-
}): void {
|
|
114
|
+
function sendUnauthorized({ res, error }: { res: Response; error: string }): void {
|
|
131
115
|
res.status(401).json({ error });
|
|
132
116
|
}
|
|
133
117
|
|
|
@@ -136,23 +120,13 @@ function sendUnauthorized({
|
|
|
136
120
|
* tool calls (which read config via `getConfig()`/`requireApiKey()`) see the
|
|
137
121
|
* per-session API key instead of the global one.
|
|
138
122
|
*/
|
|
139
|
-
async function handleWithSessionConfig<T>(
|
|
140
|
-
apiKey: string,
|
|
141
|
-
fn: () => Promise<T>
|
|
142
|
-
): Promise<T> {
|
|
123
|
+
async function handleWithSessionConfig<T>(apiKey: string, fn: () => Promise<T>): Promise<T> {
|
|
143
124
|
const baseConfig = getConfig();
|
|
144
125
|
return runWithConfig({ ...baseConfig, apiKey }, fn);
|
|
145
126
|
}
|
|
146
127
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
* The MCP transport specification requires servers to validate Origin on every
|
|
151
|
-
* incoming connection, because a page on an attacker's domain can point DNS at
|
|
152
|
-
* loopback and reach a server that only checks the token. Requests with no
|
|
153
|
-
* Origin header are not browser requests, so they pass: browsers always send
|
|
154
|
-
* Origin on the cross-origin requests this guards.
|
|
155
|
-
*/
|
|
128
|
+
// MCP requires origin validation to prevent DNS-rebinding attacks. Requests without
|
|
129
|
+
// Origin header pass since they're not browser requests and can't be compromised this way.
|
|
156
130
|
function createOriginMiddleware({
|
|
157
131
|
allowedOrigins,
|
|
158
132
|
}: {
|
|
@@ -171,16 +145,13 @@ function createOriginMiddleware({
|
|
|
171
145
|
// A cross-origin client cannot read a response header unless it is
|
|
172
146
|
// exposed, and the Streamable HTTP transport reads the session id off
|
|
173
147
|
// the initialize response.
|
|
174
|
-
res.header(
|
|
175
|
-
"Access-Control-Expose-Headers",
|
|
176
|
-
"Mcp-Session-Id, MCP-Protocol-Version"
|
|
177
|
-
);
|
|
148
|
+
res.header("Access-Control-Expose-Headers", "Mcp-Session-Id, MCP-Protocol-Version");
|
|
178
149
|
}
|
|
179
150
|
|
|
180
151
|
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
181
152
|
res.header(
|
|
182
153
|
"Access-Control-Allow-Headers",
|
|
183
|
-
"Content-Type, Authorization, mcp-session-id, MCP-Protocol-Version"
|
|
154
|
+
"Content-Type, Authorization, mcp-session-id, MCP-Protocol-Version",
|
|
184
155
|
);
|
|
185
156
|
res.header("X-Content-Type-Options", "nosniff");
|
|
186
157
|
res.header("X-Frame-Options", "DENY");
|
|
@@ -205,7 +176,10 @@ function createAuthenticator(runtime: ServerRuntime): Authenticate {
|
|
|
205
176
|
|
|
206
177
|
const token = readBearerToken(req);
|
|
207
178
|
if (!token) {
|
|
208
|
-
sendUnauthorized({
|
|
179
|
+
sendUnauthorized({
|
|
180
|
+
res,
|
|
181
|
+
error: "Authorization: Bearer <LANGWATCH_API_KEY> header required",
|
|
182
|
+
});
|
|
209
183
|
return null;
|
|
210
184
|
}
|
|
211
185
|
|
|
@@ -247,10 +221,7 @@ function overSessionLimit({
|
|
|
247
221
|
sessions: SessionStore<StreamableHTTPServerTransport>;
|
|
248
222
|
sseSessions: SessionStore<SSEServerTransport>;
|
|
249
223
|
}): boolean {
|
|
250
|
-
return (
|
|
251
|
-
sessions.countForKey(apiKey) + sseSessions.countForKey(apiKey) >=
|
|
252
|
-
MAX_SESSIONS_PER_KEY
|
|
253
|
-
);
|
|
224
|
+
return sessions.countForKey(apiKey) + sseSessions.countForKey(apiKey) >= MAX_SESSIONS_PER_KEY;
|
|
254
225
|
}
|
|
255
226
|
|
|
256
227
|
function sendSessionLimitReached(res: Response): void {
|
|
@@ -269,20 +240,17 @@ function registerOAuthRoutes({
|
|
|
269
240
|
}): void {
|
|
270
241
|
const { verifier, oauthTokens, oauthRateLimiter } = runtime;
|
|
271
242
|
|
|
272
|
-
app.get(
|
|
273
|
-
"
|
|
274
|
-
(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
);
|
|
243
|
+
app.get("/.well-known/oauth-authorization-server", (req: Request, res: Response) => {
|
|
244
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
245
|
+
res.json({
|
|
246
|
+
issuer: baseUrl,
|
|
247
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
248
|
+
token_endpoint_auth_methods_supported: ["client_secret_post"],
|
|
249
|
+
grant_types_supported: ["client_credentials"],
|
|
250
|
+
response_types_supported: [],
|
|
251
|
+
scopes_supported: ["mcp:tools"],
|
|
252
|
+
});
|
|
253
|
+
});
|
|
286
254
|
|
|
287
255
|
// RFC 6749 requires application/x-www-form-urlencoded on the token endpoint.
|
|
288
256
|
app.post(
|
|
@@ -310,8 +278,7 @@ function registerOAuthRoutes({
|
|
|
310
278
|
if (!clientSecret || typeof clientSecret !== "string") {
|
|
311
279
|
res.status(400).json({
|
|
312
280
|
error: "invalid_request",
|
|
313
|
-
error_description:
|
|
314
|
-
"client_secret is required (use your LangWatch API key)",
|
|
281
|
+
error_description: "client_secret is required (use your LangWatch API key)",
|
|
315
282
|
});
|
|
316
283
|
return;
|
|
317
284
|
}
|
|
@@ -343,7 +310,7 @@ function registerOAuthRoutes({
|
|
|
343
310
|
expires_in: OAUTH_TOKEN_TTL_SECONDS,
|
|
344
311
|
scope: "mcp:tools",
|
|
345
312
|
});
|
|
346
|
-
}
|
|
313
|
+
},
|
|
347
314
|
);
|
|
348
315
|
}
|
|
349
316
|
|
|
@@ -357,133 +324,145 @@ function registerStreamableHttpRoutes({
|
|
|
357
324
|
runtime: ServerRuntime;
|
|
358
325
|
authenticate: Authenticate;
|
|
359
326
|
}): void {
|
|
360
|
-
const {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
327
|
+
const route = { runtime, authenticate };
|
|
328
|
+
app.post("/mcp", (req: Request, res: Response) => handleMcpPost({ req, res, ...route }));
|
|
329
|
+
app.get("/mcp", (req: Request, res: Response) => handleMcpGet({ req, res, ...route }));
|
|
330
|
+
app.delete("/mcp", (req: Request, res: Response) => handleMcpDelete({ req, res, ...route }));
|
|
331
|
+
}
|
|
364
332
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
333
|
+
type McpRoute = {
|
|
334
|
+
req: Request;
|
|
335
|
+
res: Response;
|
|
336
|
+
runtime: ServerRuntime;
|
|
337
|
+
authenticate: Authenticate;
|
|
338
|
+
};
|
|
368
339
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
req,
|
|
372
|
-
res,
|
|
373
|
-
expectedApiKey: session.apiKey,
|
|
374
|
-
});
|
|
375
|
-
if (!apiKey) return;
|
|
340
|
+
const sessionIdOf = (req: Request): string | undefined =>
|
|
341
|
+
req.headers["mcp-session-id"] as string | undefined;
|
|
376
342
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
343
|
+
async function handleMcpPost({ req, res, runtime, authenticate }: McpRoute): Promise<void> {
|
|
344
|
+
const { sessions } = runtime;
|
|
345
|
+
const sessionId = sessionIdOf(req);
|
|
346
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
383
347
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
348
|
+
if (sessionId && session) {
|
|
349
|
+
const apiKey = await authenticate({
|
|
350
|
+
req,
|
|
351
|
+
res,
|
|
352
|
+
expectedApiKey: session.apiKey,
|
|
353
|
+
});
|
|
354
|
+
if (!apiKey) return;
|
|
387
355
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
356
|
+
sessions.touch(sessionId);
|
|
357
|
+
await handleWithSessionConfig(session.apiKey, () =>
|
|
358
|
+
session.transport.handleRequest(req, res, req.body),
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
392
362
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const transport = new StreamableHTTPServerTransport({
|
|
398
|
-
sessionIdGenerator: () => randomUUID(),
|
|
399
|
-
onsessioninitialized: (id) => {
|
|
400
|
-
reservation.commit({ sessionId: id, transport });
|
|
401
|
-
},
|
|
402
|
-
});
|
|
403
|
-
transport.onclose = () => {
|
|
404
|
-
if (transport.sessionId) sessions.remove(transport.sessionId);
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const sessionServer = createMcpServer();
|
|
408
|
-
try {
|
|
409
|
-
await handleWithSessionConfig(apiKey, () =>
|
|
410
|
-
sessionServer.connect(transport)
|
|
411
|
-
);
|
|
412
|
-
await handleWithSessionConfig(apiKey, () =>
|
|
413
|
-
transport.handleRequest(req, res, req.body)
|
|
414
|
-
);
|
|
415
|
-
} catch (error) {
|
|
416
|
-
if (transport.sessionId) sessions.remove(transport.sessionId);
|
|
417
|
-
await transport.close().catch(() => undefined);
|
|
418
|
-
throw error;
|
|
419
|
-
} finally {
|
|
420
|
-
// A no-op once the session took the slot, so this only returns it when
|
|
421
|
-
// initialization never produced one.
|
|
422
|
-
reservation.release();
|
|
423
|
-
}
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
363
|
+
if (!sessionId && isInitializeRequest(req.body)) {
|
|
364
|
+
await initializeSession({ req, res, runtime, authenticate });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
426
367
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
368
|
+
// An unknown session id gets the same answer as a missing token, so the
|
|
369
|
+
// response does not reveal whether the session exists.
|
|
370
|
+
if (sessionId) {
|
|
371
|
+
sendUnauthorized({ res, error: "Session expired or not found" });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
433
374
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
});
|
|
375
|
+
res.status(400).json({
|
|
376
|
+
error: "Invalid request, no session ID or not an initialize request",
|
|
437
377
|
});
|
|
378
|
+
}
|
|
438
379
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
380
|
+
/** Opens a session for an initialize request, holding the key's slot until it exists. */
|
|
381
|
+
async function initializeSession({ req, res, runtime, authenticate }: McpRoute): Promise<void> {
|
|
382
|
+
const { sessions, sseSessions } = runtime;
|
|
383
|
+
const apiKey = await authenticate({ req, res });
|
|
384
|
+
if (!apiKey) return;
|
|
442
385
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
expectedApiKey: session.apiKey,
|
|
448
|
-
});
|
|
449
|
-
if (!apiKey) return;
|
|
386
|
+
if (overSessionLimit({ apiKey, sessions, sseSessions })) {
|
|
387
|
+
sendSessionLimitReached(res);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
450
390
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
391
|
+
// Claimed before the first await. The session id only exists once
|
|
392
|
+
// initialize completes, so without holding the slot from here concurrent
|
|
393
|
+
// requests for one key would all pass the check above at a count of zero.
|
|
394
|
+
const reservation = sessions.reserve(apiKey);
|
|
395
|
+
const transport = new StreamableHTTPServerTransport({
|
|
396
|
+
sessionIdGenerator: () => randomUUID(),
|
|
397
|
+
onsessioninitialized: (id) => {
|
|
398
|
+
reservation.commit({ sessionId: id, transport });
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
transport.onclose = () => {
|
|
402
|
+
if (transport.sessionId) sessions.remove(transport.sessionId);
|
|
403
|
+
};
|
|
457
404
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
405
|
+
const sessionServer = createMcpServer();
|
|
406
|
+
try {
|
|
407
|
+
await handleWithSessionConfig(apiKey, () => sessionServer.connect(transport));
|
|
408
|
+
await handleWithSessionConfig(apiKey, () => transport.handleRequest(req, res, req.body));
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (transport.sessionId) sessions.remove(transport.sessionId);
|
|
411
|
+
await transport.close().catch(() => undefined);
|
|
412
|
+
throw error;
|
|
413
|
+
} finally {
|
|
414
|
+
// A no-op once the session took the slot, so this only returns it when
|
|
415
|
+
// initialization never produced one.
|
|
416
|
+
reservation.release();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
462
419
|
|
|
463
|
-
|
|
464
|
-
}
|
|
420
|
+
async function handleMcpGet({ req, res, runtime, authenticate }: McpRoute): Promise<void> {
|
|
421
|
+
const { sessions } = runtime;
|
|
422
|
+
const sessionId = sessionIdOf(req);
|
|
423
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
465
424
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
425
|
+
if (sessionId && session) {
|
|
426
|
+
const apiKey = await authenticate({
|
|
427
|
+
req,
|
|
428
|
+
res,
|
|
429
|
+
expectedApiKey: session.apiKey,
|
|
430
|
+
});
|
|
470
431
|
if (!apiKey) return;
|
|
471
432
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
apiKeysMatch({ presentedKey: apiKey, expectedKey: session.apiKey });
|
|
433
|
+
sessions.touch(sessionId);
|
|
434
|
+
await handleWithSessionConfig(session.apiKey, () => session.transport.handleRequest(req, res));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
477
437
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
438
|
+
if (sessionId) {
|
|
439
|
+
sendUnauthorized({ res, error: "Session expired or not found" });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
482
442
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
443
|
+
res.status(400).json({ error: "Invalid request, no valid session ID" });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function handleMcpDelete({ req, res, runtime, authenticate }: McpRoute): Promise<void> {
|
|
447
|
+
const { sessions } = runtime;
|
|
448
|
+
// Authenticate before looking the session up, so that a session the caller
|
|
449
|
+
// does not own is indistinguishable from one that does not exist.
|
|
450
|
+
const apiKey = await authenticate({ req, res });
|
|
451
|
+
if (!apiKey) return;
|
|
452
|
+
|
|
453
|
+
const sessionId = sessionIdOf(req);
|
|
454
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
455
|
+
const owned =
|
|
456
|
+
session !== undefined && apiKeysMatch({ presentedKey: apiKey, expectedKey: session.apiKey });
|
|
457
|
+
|
|
458
|
+
if (!sessionId || !session || !owned) {
|
|
459
|
+
res.status(404).json({ error: "Session not found" });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
sessions.remove(sessionId);
|
|
464
|
+
await session.transport.close();
|
|
465
|
+
res.status(200).json({ status: "session closed" });
|
|
487
466
|
}
|
|
488
467
|
|
|
489
468
|
/** Legacy SSE transport, kept for backwards compatibility. */
|
|
@@ -517,9 +496,7 @@ function registerSseRoutes({
|
|
|
517
496
|
});
|
|
518
497
|
|
|
519
498
|
try {
|
|
520
|
-
await handleWithSessionConfig(apiKey, () =>
|
|
521
|
-
sessionServer.connect(transport)
|
|
522
|
-
);
|
|
499
|
+
await handleWithSessionConfig(apiKey, () => sessionServer.connect(transport));
|
|
523
500
|
} catch (error) {
|
|
524
501
|
// Without this the entry holds one of the per-key slots until the reaper
|
|
525
502
|
// sweeps it, because res "close" may never fire if the stream never
|
|
@@ -533,21 +510,33 @@ function registerSseRoutes({
|
|
|
533
510
|
}
|
|
534
511
|
});
|
|
535
512
|
|
|
536
|
-
// Mounted at both /messages and /sse/messages because some clients
|
|
537
|
-
// the relative /messages URL differently.
|
|
538
|
-
|
|
539
|
-
// The session id
|
|
540
|
-
// the client its POST endpoint as a URI.
|
|
541
|
-
//
|
|
542
|
-
const handleSseMessage =
|
|
513
|
+
// Mounted at both /messages and /sse/messages because some clients
|
|
514
|
+
// resolve the relative /messages URL differently.
|
|
515
|
+
|
|
516
|
+
// The session id in the query string only identifies the session; it's
|
|
517
|
+
// how the SSE transport hands the client its POST endpoint as a URI.
|
|
518
|
+
// The Bearer token is what authorizes the request.
|
|
519
|
+
const handleSseMessage = createSseMessageHandler({ authenticate, sseSessions });
|
|
520
|
+
|
|
521
|
+
app.post("/messages", handleSseMessage);
|
|
522
|
+
app.post("/sse/messages", handleSseMessage);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function createSseMessageHandler({
|
|
526
|
+
authenticate,
|
|
527
|
+
sseSessions,
|
|
528
|
+
}: {
|
|
529
|
+
authenticate: Authenticate;
|
|
530
|
+
sseSessions: ServerRuntime["sseSessions"];
|
|
531
|
+
}) {
|
|
532
|
+
return async (req: Request, res: Response) => {
|
|
543
533
|
const apiKey = await authenticate({ req, res });
|
|
544
534
|
if (!apiKey) return;
|
|
545
535
|
|
|
546
536
|
const sessionId = req.query["sessionId"] as string | undefined;
|
|
547
537
|
const session = sessionId ? sseSessions.get(sessionId) : undefined;
|
|
548
538
|
const owned =
|
|
549
|
-
session !== undefined &&
|
|
550
|
-
apiKeysMatch({ presentedKey: apiKey, expectedKey: session.apiKey });
|
|
539
|
+
session !== undefined && apiKeysMatch({ presentedKey: apiKey, expectedKey: session.apiKey });
|
|
551
540
|
|
|
552
541
|
if (!sessionId || !session || !owned) {
|
|
553
542
|
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
@@ -556,24 +545,15 @@ function registerSseRoutes({
|
|
|
556
545
|
|
|
557
546
|
sseSessions.touch(sessionId);
|
|
558
547
|
await handleWithSessionConfig(session.apiKey, () =>
|
|
559
|
-
session.transport.handlePostMessage(req, res, req.body)
|
|
548
|
+
session.transport.handlePostMessage(req, res, req.body),
|
|
560
549
|
);
|
|
561
550
|
};
|
|
562
|
-
|
|
563
|
-
app.post("/messages", handleSseMessage);
|
|
564
|
-
app.post("/sse/messages", handleSseMessage);
|
|
565
551
|
}
|
|
566
552
|
|
|
567
553
|
/** Sweeps idle sessions, expired tokens, and stale rate limiter entries. */
|
|
568
554
|
function startReaper(runtime: ServerRuntime): NodeJS.Timeout {
|
|
569
|
-
const {
|
|
570
|
-
|
|
571
|
-
sseSessions,
|
|
572
|
-
oauthTokens,
|
|
573
|
-
verifier,
|
|
574
|
-
authFailRateLimiter,
|
|
575
|
-
oauthRateLimiter,
|
|
576
|
-
} = runtime;
|
|
555
|
+
const { sessions, sseSessions, oauthTokens, verifier, authFailRateLimiter, oauthRateLimiter } =
|
|
556
|
+
runtime;
|
|
577
557
|
|
|
578
558
|
const reaper = setInterval(() => {
|
|
579
559
|
sessions.sweep();
|
|
@@ -647,32 +627,17 @@ export interface StartedHttpServer {
|
|
|
647
627
|
allowedOrigins: string[];
|
|
648
628
|
}
|
|
649
629
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
* for the LangWatch MCP server.
|
|
653
|
-
*
|
|
654
|
-
* Every request carries `Authorization: Bearer <key>`. The key is verified
|
|
655
|
-
* against the LangWatch API before any per-session state is allocated, and
|
|
656
|
-
* re-checked on every subsequent request against the key the session was
|
|
657
|
-
* created with, so a session id on its own authorizes nothing.
|
|
658
|
-
*
|
|
659
|
-
* Endpoints:
|
|
660
|
-
* - GET /health - Health check for Kubernetes probes (no auth)
|
|
661
|
-
* - POST/GET/DELETE /mcp - Streamable HTTP transport (modern)
|
|
662
|
-
* - GET /sse - Legacy SSE transport (backwards compatibility)
|
|
663
|
-
* - POST /messages - Legacy SSE message endpoint
|
|
664
|
-
*/
|
|
630
|
+
// HTTP server with Streamable HTTP and legacy SSE transports; every request
|
|
631
|
+
// re-verifies the bearer token against the LangWatch API.
|
|
665
632
|
export async function startHttpServer({
|
|
666
633
|
port,
|
|
667
634
|
host,
|
|
668
635
|
allowedOrigins,
|
|
669
636
|
apiKeyVerifier,
|
|
670
637
|
}: StartHttpServerOptions): Promise<StartedHttpServer> {
|
|
671
|
-
const bindHost =
|
|
672
|
-
host ?? process.env.LANGWATCH_MCP_HTTP_HOST ?? DEFAULT_BIND_HOST;
|
|
638
|
+
const bindHost = host ?? process.env.LANGWATCH_MCP_HTTP_HOST ?? DEFAULT_BIND_HOST;
|
|
673
639
|
const originAllowlist =
|
|
674
|
-
allowedOrigins ??
|
|
675
|
-
parseAllowedOrigins(process.env.LANGWATCH_MCP_ALLOWED_ORIGINS);
|
|
640
|
+
allowedOrigins ?? parseAllowedOrigins(process.env.LANGWATCH_MCP_ALLOWED_ORIGINS);
|
|
676
641
|
|
|
677
642
|
const runtime = createRuntime({
|
|
678
643
|
endpoint: getConfig().endpoint,
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
2
2
|
import yargs from "yargs";
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
4
|
|
|
5
|
-
import { initConfig } from "./config.
|
|
6
|
-
import { createMcpServer } from "./create-mcp-server.
|
|
5
|
+
import { initConfig } from "./config.ts";
|
|
6
|
+
import { createMcpServer } from "./create-mcp-server.ts";
|
|
7
7
|
|
|
8
8
|
const argv = await yargs(hideBin(process.argv))
|
|
9
9
|
.option("apiKey", {
|
|
@@ -14,6 +14,10 @@ const argv = await yargs(hideBin(process.argv))
|
|
|
14
14
|
type: "string",
|
|
15
15
|
description: "LangWatch API endpoint",
|
|
16
16
|
})
|
|
17
|
+
.option("projectId", {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "LangWatch project ID",
|
|
20
|
+
})
|
|
17
21
|
.option("http", {
|
|
18
22
|
type: "boolean",
|
|
19
23
|
description: "Start HTTP/SSE server instead of stdio",
|
|
@@ -41,13 +45,12 @@ const argv = await yargs(hideBin(process.argv))
|
|
|
41
45
|
initConfig({
|
|
42
46
|
apiKey: argv.apiKey,
|
|
43
47
|
endpoint: argv.endpoint,
|
|
48
|
+
projectId: argv.projectId,
|
|
44
49
|
});
|
|
45
50
|
|
|
46
51
|
if (argv.http) {
|
|
47
|
-
const { startHttpServer } = await import("./http-server.
|
|
48
|
-
const { isLoopbackHost, parseAllowedOrigins } = await import(
|
|
49
|
-
"./http-security.js"
|
|
50
|
-
);
|
|
52
|
+
const { startHttpServer } = await import("./http-server.ts");
|
|
53
|
+
const { isLoopbackHost, parseAllowedOrigins } = await import("./http-security.ts");
|
|
51
54
|
|
|
52
55
|
const allowedOrigins =
|
|
53
56
|
argv.allowedOrigin && argv.allowedOrigin.length > 0
|
|
@@ -62,19 +65,14 @@ if (argv.http) {
|
|
|
62
65
|
|
|
63
66
|
// An IPv6 literal has to be bracketed inside a URL, and must not be
|
|
64
67
|
// double-bracketed when the operator already wrote it that way.
|
|
65
|
-
const displayHost =
|
|
66
|
-
|
|
67
|
-
console.log(
|
|
68
|
-
`LangWatch MCP server listening on http://${displayHost}:${port}/mcp`
|
|
69
|
-
);
|
|
70
|
-
console.log(
|
|
71
|
-
"Clients must provide their API key via Authorization: Bearer <key> header"
|
|
72
|
-
);
|
|
68
|
+
const displayHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
69
|
+
console.log(`LangWatch MCP server listening on http://${displayHost}:${port}/mcp`);
|
|
70
|
+
console.log("Clients must provide their API key via Authorization: Bearer <key> header");
|
|
73
71
|
if (!isLoopbackHost(host)) {
|
|
74
72
|
console.warn(
|
|
75
73
|
`Warning: bound to ${host}, so the server is reachable from other machines. ` +
|
|
76
74
|
"Make sure it sits behind a trusted network boundary and that --allowedOrigin " +
|
|
77
|
-
"lists only origins you control."
|
|
75
|
+
"lists only origins you control.",
|
|
78
76
|
);
|
|
79
77
|
}
|
|
80
78
|
if (process.env.LANGWATCH_MCP_TRUST_PROXY === "true") {
|
|
@@ -83,7 +81,7 @@ if (argv.http) {
|
|
|
83
81
|
"authentication uses the client address from X-Forwarded-For. Keep this " +
|
|
84
82
|
"on only when a trusted proxy terminates in front of the port and " +
|
|
85
83
|
"overwrites that header. Otherwise a client can supply any address there " +
|
|
86
|
-
"and the limit stops counting."
|
|
84
|
+
"and the limit stops counting.",
|
|
87
85
|
);
|
|
88
86
|
}
|
|
89
87
|
} else {
|