From ce6b647fe7e620031c3dd9d16a8e8c08ba13b4b1 Mon Sep 17 00:00:00 2001 From: huige66631 <1030089807@qq.com> Date: Wed, 9 Sep 2026 16:13:33 +0800 Subject: [PATCH] feat(mcp): detect OAuth-protected remote MCP servers (#2363) * feat(mcp): surface OAuth-required server tests * fix(mcp): show connection failure details in status cards --------- Co-authored-by: RockChinQ --- docs/assets/pr-2363/oauth-required-state.png | Bin 0 -> 75030 bytes src/langbot/pkg/api/http/service/mcp.py | 25 +++-- src/langbot/pkg/provider/tools/loaders/mcp.py | 57 ++++++++++- .../pkg/provider/tools/loaders/mcp_stdio.py | 1 + .../api/service/test_mcp_service.py | 70 ++++++++++++++ .../provider/test_mcp_remote_transport.py | 80 +++++++++++++++- web/playwright.config.ts | 2 +- .../home/mcp/components/mcp-form/MCPForm.tsx | 90 ++++++++++++++---- web/src/app/infra/entities/api/index.ts | 1 + web/src/i18n/locales/en-US.ts | 9 ++ web/src/i18n/locales/es-ES.ts | 9 ++ web/src/i18n/locales/ja-JP.ts | 9 ++ web/src/i18n/locales/ru-RU.ts | 9 ++ web/src/i18n/locales/th-TH.ts | 9 ++ web/src/i18n/locales/vi-VN.ts | 9 ++ web/src/i18n/locales/zh-Hans.ts | 8 ++ web/src/i18n/locales/zh-Hant.ts | 8 ++ web/tests/e2e/mcp-oauth-required.spec.ts | 74 ++++++++++++++ 18 files changed, 433 insertions(+), 37 deletions(-) create mode 100644 docs/assets/pr-2363/oauth-required-state.png create mode 100644 web/tests/e2e/mcp-oauth-required.spec.ts diff --git a/docs/assets/pr-2363/oauth-required-state.png b/docs/assets/pr-2363/oauth-required-state.png new file mode 100644 index 0000000000000000000000000000000000000000..a969c3b9feabb33d6e611bd02fa7b736764dd31c GIT binary patch literal 75030 zcmcHhWk8hA_XdnFqM!m&0s;aer8G!LOE*Y^#7g(lAh{qVCDPI$CEc~ef^>Js(#=v! zv+RHM^ZoswH_zMWezUW4-}l*>Gjrz5oa;Ip_EAL^?7O%_5GdBg(rQa{B48Oq({6+l6(1|K zB@Kgz;;N=n@N7~N^ZumhQ`MMayJ?LZKFXtKGc)9~UkD_9Ou_Y&>dkXC%8wF_C@^%J zZ^R%uU{x^l($LI!n=qn5A^7_9L;28je^S|QzhY#ES zE3FU^05Ja*{UtvDaQ~}()*cM-Cz+q0fBf$|;$^UexF9wLr~eb51{M^@f0bB1FIZ9K z=jhjWtck~<3`|T+yU=u0?UR4E1p}cU9qjEB?u!ZZ<{muw3 zgz2n5>M!TEc8bbEB*ix-z_ z$+rMPK%ivY4HDASM0fLdtN=SXS1)uS&HVK*9(;DRLOA5)<(aqnWBi|B9zRxnFVASe zi?)Z>gO-Tf|K7pg2^MK-uK)2+27voLLKSt|@~<9{MU$0uNgCVs*uP!;L<~p3>3`b8 zx!Y`Mq~~Nuwma3<)xG}TFI(hQbz9U)R(;-mx2yjkzPa`ECV;w0reW2mnw)BW-F*S6R|<0Z;W0Aw&2#MXZ_1rjU8nms`E>rBSCV+k5Qs7-_9k%IWLUXXVZ0V zkANeuF_LTOHnugZ{DA4-FZgpcJ!&hpH{0){p-eeU>&V5szut&0rLxxgruK;E{&}Ff z%1QsRKxmg|vB}l(PnqU%Dd+bxmj*L~MNS6a6fGZtn8)FyYd})Fp57!;;ew?U%O|iS z9fxszaM;T{iY^hOt4P-t8h6=GwX~4TZvp=XH7mMGA7J2B+wKenk`>ftV=jCRjr}M_ z^o3syQZK+#5^k$l#y8#_HuT!P`jh#+lNDdO_zP+ZrV5=vLNQ(dk#)o!=Nhh)sk7C?jlY8L@|KY$x#FS|fz6%U#ZR z$T+HcgrZ~1JVt)K1WCG1#DK5c*M5a~|FcWLsO&5Upp1_a33)T0ALXnN-f+7q2NP2| z@dZ^KooVc}UQ4gh9$h zHJMDy)gK%?g$+23=z2z^$rFQ=Uf+XYGU|2UeMPGGpGP%_#vdzr%43$9E${b&p`#@n zum>}1rX_iOlpKch?>#3YLOsN~RV`f*k|v9SWdBX2H{KTh$}0HBHijpZ=8GIYO!tGP zS-(yUx`sx0_&JPPHbt^FW3Yvj?)oiHq@vqFxu#M2A6tD*294huiSR6hYxNJu6BbJg zckuS*eYgKK_B`HX;KCI@)7MOW$Z(d&rd4;L@OvNguP!qQ^e64wiMf z`PZlwy>Tuy_4K6wwLi;N*CofW_-6^2dRvxM_#o$$^2&2J3Yy-e;nH-dqM>g~>E!+z z)6fA1uGg@eU9mO|jeO~ByHuI7#TTp`rJqk0|Fm}K@NpV}+_R*Zj^g_A=&Z)Y)5Bu6 zl7!m;5F<_qK24%F{U*)P8uKqQG0>FZSCXepNXVLEaMI&O*^Ah_3-kNUoN5~I`Z#`2 zch5T$U27h^1WVzzjK#ALoY#qLPN>B~yD{vq!Aw}~HN)%0jr3>8_M~S(Zm!$0r0wxu zf!Bz(KwD=x*R1}7phbw=e#_;(5XVvG(9qD-ln%+$t_ow((?KGb!lA{HKn z6aWy3_A>^p251!}AaFL8y=AX%Zv_NaaY)fl)OPL=HgD_3hNaW(QC5$55V^0=4UMH*7|Xil}@?R!|xH^?rLnjY(xBRS^$UU<#3=hF_>Cu~$$5_mGBfxp;HdDGF z@+P_DSVj{CBr)(8e_rzKm;- znzsAy`3wrNEe5@e2B!@VA-AVX7j_36ph&U5`zOOb5cy}!vSE8QQ>-XzQO6^)85O^U z$=(OLfo<~Q8twd`I1kmOPi0nkt(K0xBk{CiXUuP)=l9#SI$)c3pO*OuoA8H32R)GK z>7MRzY$KPFHyYn(4QncY;sbX-C9kqbYU}F?I!*{UeJqYq9Lb|PK*v0Dot+W zW1(hBP0nUm7)EESCUb0}VMc>e`O>NV-fKZgKYcff*6-SJ8?j!-@)WqC`9)8Ikzs@* z!uqW5$bHkXUygdl{Js){@ zhVngXF??sKaywP-CyySJEL&GCz+Kytig+s=rj2sl@)o_}EiQ|(6V&4$JumQXN%s?4 zc3JFge_|hQh$?Gz9_WAZ6DxB*FF*gNYzj^al#(04%n;JQO#{8H=3uD{jb4A6OeRA8 z*_h=Jj#YoYG(W#&F_SBxMMk#0Wie;{i|e?n-aO)teS7W5jDyB^`!!9{a`l^Sd#7t+obxx}#|>FBVYBu+~iE zU=cMBTE(A>*>^!wE4Y|{$^SXM9l)> za_?;G0mB6xNnd1Q!ivIxu9o9Ke`@gN`JSKKc?(tHeC@zoul$H6gVdvk%9_?zp9{ro z1JuN;NU9Nq7L32S$2SMkbc-3Eywid_;h{~-R=BQ+tMsVswzEfk$%z{z>&RSz`L3G2 z5+VSR7#p)_Gm-M}@#PoLq;b)Gm4QY@DeKpRP#bYj6!Ivu<$Rh?(4+I)>O{Ws^-5r5 zKBMf_A^NbBh3zUYyuK#YeB_+RH4$|)y`@e2`^;TO_~pt4&1K`#x-_PlC@zRnv^PxU zx@%++PTgxXvq+oiH}TPLeK@dV>FC^@QN+%jafnj(2VC4#QAdZ&(vsBd;>#PtKG8|6%3)aeD*QX(#%bFUYSZ=W$&&HV%P`*dVhnQ_eX9@Q}_7cDbITMt~c zsAiZ~8~yxMB5>VWQTmnYC=KCh!&2Yhsjwi(IT5}2Cm4!8sn+`5mZ|i#(bIX6URRX0 zwwBRL=c$dXNq(j{mLg+FnylWXiM69xl7g2OkA$y2b=u#%W;GxFgLq#D@HOkQkV!rK z*`KWjIvx%p0$s;${IaU7XJaWIjvQKB2vfvY87EknUAL>a*$zs^pnt_2kP%o5dG#Zt zVT^o{b^?1zsJ1&ruls>T_rzmeGJ5AH+sUV&>v@2Z-9;t1U}g4~*Mi=dfy%C?`ArmQ zq$AK&hPPd(9tx5gql-;Yer|u6-CViwtbyo!Yt6UivY(m+E3--t)a?(4(2Lej(el#L z^AqxmewG29^85AkeKDiPKcctMqBk`Bpu4NXdKe+vxON^Gk^t}>-HROTtnH@pKzCm% zZckyj-m5bmCf%OT+)W8H3Rpay4-7f(<}E#gMh+@5ue*x&Ki!`j2BJPEWiB1gAGRpd zxw~9mkE3FGg9Y<8jCmh<7#>@D=2BxX3B1%>1%0HYy*e$H(S` z8~k4^z%5qEb6T?iR-9>(^k#z7`*4wr?iLzzwxTDejDD9)CjbvCpB6z`HWPnLbwbZ7$#aan#Sb` z+K}ccR`*w1-wpUZukdoi@w+Kp?y-D>w8;YdEzl@j2-<0TA$Qixb=#3 zMRl36An_yLjJs`vlyNt{+IwPD4^Q>mN(s1swbO5{A*3D63yI>ar0ie}8pAyks(Ntn z-ORIDiGk#ASmbAIaq56m+;*CVfjF>9VKZ4s^9;S;0Xlg$;)qY~hf@Xd5h-JaW{ z{^8;4TbP)B)rQA-#e=88f7mlE(Msf;W}vdSUQXKb!+z%I?l$&l7S*hKv@h}_q=rGq zuc&bds{u0gf!*MK*N9>N&TEZwcLvTU3b(g4#Ru#NzlGW;tlX92bbOooLkhw(4W77- zok^>4GkoWhqiq)jNo-k7_Z^ry&`_yz3)-8Wo)+=G|MbbrX?Of8TQZcODPmCr2o2S` z#_}$5TgTrwc8TbujcCO9#TGHd%^oizevqwU85o?yBayiolbxEc=CD9ueUx4Cdfg<) zvUEs~G$}($o&81H6Zif1QDY6-UM3|}V4=@?#z^i9xD-buv38}iVIn}M8lyNz7wICQ z>0g@4x1QmBXKm41Ss7;Gb^R84J(Po3U8Ri|M@JP5o7@B0Wzbs5!f}hF-BRID+fKbvq)&NUaU z?Me$BK+*u*QSwLL7YSIyswq;P)XgDx$oO1V*hQ!9EK!P$g>NL@^wh{Dw}Hy+NVu&h zUp!(uS6)F6ua4L3WO*%yPRKpZuDHSvdM;~{7x|l--80q1JieDZ_$KGWZa!Hb7RV$_ z)+ALW1CM>`EyA6YT8}HJ{M1};$)u%4k@#}>4{LQkNKVZ>m*e>jCFP3uaarvM$Agj< zygufovJ@X|WrCtC7v1lI!O>mzdP`|dN7~CCz_#s7R5{P)T9;xHJ2De-T0PSG7`GW% zi1-_OLt7j@AV0u1ukNHLGbQxP8?0}VdP_F+j}q{JelW*c6%TNZtyrz^wIrSIqTL2l z&C>cNx4>~3YJv~gylm!n=Z(DDnu(1yTF{g8(shGx&Iki_UvX24_dGe{0Xbotjzf37&Fks*mW1Nm#=*(!U zcJ;P2PV`*-sGA(p0StRbET*JwKFIwdRAAB>5Ass^&%pGMUTW?tjDH&W(@aV+hd)#I zvvC7Ayy0}>jJMJxs!q8$^>R`}b!#K7*-Pui`=ySKj-{n^u#uKldg|?nfXh4!En3h1 z-}j2V@!meSfBMK8#P!Bh`$bQUeO1Pv`@rH66bgwb_8JG6sIZ%DhwJ;yHW{MzRp~jgYWx>I@$#qEs!W$;~R(O)IyGf=^u_ zohE*kZaS};(3A?Ixsso_11E|b8L{zeD^?@8l|wCmdi8+`KTg_9MpJAH==G?Y<7R)M z?cQxuEAK^jj)wKam!*EZOirpMtB;5T1!n`B^c@ajV&ijHc>Iz9XVrtItLEyhjWq%+ zhTj$Ns19i(pNU@%8UTSBuP-O#f~l#igOVy`_qx0v$iiEGsPhQU2clxWPQ6QYh(~I+ zM-8;d&Q#zPr*5vlvzxDh-Cd6yi6C8Fb#;4xt6iQyt5C^QM0<^vu0iYgd<)0|M;xHS9AQJY`{qjLo=dJ6FHCw_A7VT7QO6vl@5s^6`{ zQKd~wzv^8tz{|E41FUZ$5uK%JqVcX+%FC@<=eo`YZ+0Ix@tAnx^uYY~ou!=bUOY(u zAUT0vHbX+NI5;@QX^)^NG27`9JUnxUbADKSP3_ySfiyh2`B_;jL;SEW7jrP;Vl8d$ zs28iU=s17Q?v3y5{q*9Zu-)v(8(?W&_tcbI`_}@RFDs&dJCd%gufM;w)xBh*%~4jP zk2UL^r7>i`q#SLKo^CMP;_^Ij>3Ms{bJZvspUk7rCaz%pIa_I4%ED3y;ZKf-9F`9J zwamB_On{cY#YSjpVPW6EEmMng%Poj_mx7FptoFtn@&W_EN%Z0i3gWgmkW}1?YK^_` z@HuLsyF(%!o7~Vdz09H>aMenS`P8qWLrkXvpes&OEg?*LFUFxmmPC2Z1_YKl*nncn zB|4002kq(VQ$O{WO>bMJfEJy`p}w)B4ULDSaGyiKzC#_`+oOQSJ}0wn+srGJv+nXX z#U=;v6S=rmD^GbMP@UJq(C2qWKhr(e`mp(y^)5?!A4ki3q_fv}fWt02^iodsv8&>a zd0^A8ETO#D(qlUob1^mu7}@COb5$B?5gn&Az_R<*A6Y?u^N3v0UO800PGN0T3S+E{w)Ih;U?&=-e;Go0~Tch|3J1B z>;uOqkhdvY;|hnBu?q=q({~kzW@Sz;EJ5AnXGsYk$cTOo6??II8VUp1d_a+-{6B*u z9B3}dzDn8~v3pt@R^gK+QHzL)h_cTCjaq#Cn7mt6zGQTk_GW;+R|bBXQCL(@J4fq8 zCl7y`P2^`~^kj%onIk<@Iy5)F39yxEiRVoQ4jU%D966L!zcB-Wb#a=UX+Dv!cJMj* zEh_FeP+4$YY_0oMaXUalGx1ZKKnyg=;I&J2=d>?BcRQM@uqYwV_cH&*l_B4e4_0Ws zxb}1shTJ@A%Gfs+Xcqiza%r6k`XRU!^-2QQTHLI9N) z!e|)0STZ7M8t9jOTs_Q}NFwJ>;8YxG=C*=~inl3PIH!b{1ATSr?t z(598I=^f8Z`P(Or&gntnPmI?eSpSpxSLg}fqTzEEKbQK5mY$<5JVs@zy{++pH(ooE z>&-lw8&92V-QY|VO4%!?zJHz(z8jj zmB+^=?ZURhU}c<$Bs+N!{r})bPLujufZ}(<-wWr`L+M+5;{qOdABArfHyyshUjr_t zuW$duAU~9w!$sRJ@cj#mQiqNYs#=GaZVusoI*RJ5JT@v5uu~T#p=7i_Tnol_kLT72w|(@AP<~OFOmye+gR; zK6^*c{}1yVMZ4nvrUD?;AMhV41bF!ux%_uIK7Q=~pE}!U|0fUd|Luccadv~yz97>N zvV_K~#>W@u=Vbtph`6{vb8}r?UDstzSb_qp9xwkkZvh>;o}NNZPEIaPPD+*U$-|_| zF%8ML#RG-yTNpP+KA4(nxVX4DyvKA!?M@x~W3W@bPb_Cn#_|*-Wuqb3eiGsZS7L4Z$Vj^-|=Tv_`B`3g99Z11P3`5jB0K5 ztR9{?D+|C}dj2%{ym$hi6PaTB{~&+ggNz_c}xOCP($W%vMV-D2a!XJqA~wT<^J;xOHk;sz)< zE-y~R*nm743&2bhI)(p-DvpiRwOb#^0S0|d;G;l=kY_Ea07T9H3^*6Igt#5JabSPg zhSt)`&mAp-%!`-YQt~%Rl>9XyP?^V)ThgYjgclZQsVGN$%h1E~$8tC$#ctPOmiXbz zff=3DOw@6~QH?KefI}2^r~q>Jq{$C%>#|wZ2$O~L=%7W%TCIok*2_$fI;oT z127KCY7>xw{eHoqy=w*LGWtm5Z?P{GO!w?)uTWGOH1>b7fSKqVU^w-259QxkF{O>* z+pBk@13AF{iHY@s%suM^i)&p`$ml3E2e_ANTPh0$;a*hgCoXRRejZ&4nFP^b?C@IQ z8tCb5Zt(%~u0}N)6pH5C1F;($m!>Gag1;?2OyK~w7HN8Ue!-t{*-a9T<&fKswgxzcI1&uqm>kxGI1zw=Tn{OB1R`=>+3M)EJVUv*U0!Erva! zuI6soR99$Kz~!vR(XoZgl`l3Ul@5dL#Mt1>hGoq=k@<G7P46W}kot_Z$G zCJ2js@cL5ZoZlF;i9(FK{x-}^hUIm<>>v;A1MbDab>^xvys-H#x-6B z+Do@FxindgKnmo=)+cU=0}oa{n7kJg6Y!qfD;aIH2k_-+zW}~qBjRyzlClx8vt^bw znud@Y?db0Q%OSbZ^lc%C_SyP)bm;|TxZbm^J~2^SXGUd7_(>MaXtcSaB0m@>O+m4A zx}|^CbLZKvb;@LodQMoLbs+`L{^SPqh6NRx_(B${DI}|@AAhoDh5{BOq&f&&8Xubx zk2&1hUlNv<+Vk?(wm_LkmLyB-67L7Z0f4rEF->hKG`0X+4u4{Cachf|LMv7jz5FC& zwQX(r6}V-Ar>y}>2c28EoWY>PQJk@82GlkE(pGh(E4!Jii;JyQ)<_jTRSOzJP(>Co zA4qp)<16qtQ`2W^+_YyuJ=;$JdE1>%zdlJg3i{b_T}kZwq|s|@G#e3F91?Nc-HL?2 z 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 literal 0 HcmV?d00001 diff --git a/src/langbot/pkg/api/http/service/mcp.py b/src/langbot/pkg/api/http/service/mcp.py index 09bc185f7..67e8e14ae 100644 --- a/src/langbot/pkg/api/http/service/mcp.py +++ b/src/langbot/pkg/api/http/service/mcp.py @@ -446,15 +446,19 @@ class MCPService: persisted_session = runtime_mcp_session async def _refresh_and_report() -> None: - needs_start = persisted_session.status == MCPSessionStatus.ERROR or persisted_session.session is None - if needs_start: - await persisted_session.start() - else: - try: - await persisted_session.refresh() - except Exception: + try: + needs_start = ( + persisted_session.status == MCPSessionStatus.ERROR or persisted_session.session is None + ) + if needs_start: await persisted_session.start() - ctx.metadata['runtime_info'] = persisted_session.get_runtime_info_dict() + else: + try: + await persisted_session.refresh() + except Exception: + await persisted_session.start() + finally: + ctx.metadata['runtime_info'] = persisted_session.get_runtime_info_dict() coroutine = _refresh_and_report() else: @@ -471,8 +475,11 @@ class MCPService: async def _run_and_cleanup() -> None: try: await test_session.start() - ctx.metadata['runtime_info'] = test_session.get_runtime_info_dict() finally: + # start() raises for a failed connection. Preserve the + # terminal runtime state so the UI can render actionable + # failure phases such as OAuth-required. + ctx.metadata['runtime_info'] = test_session.get_runtime_info_dict() try: await test_session.shutdown() except Exception as exc: diff --git a/src/langbot/pkg/provider/tools/loaders/mcp.py b/src/langbot/pkg/provider/tools/loaders/mcp.py index 2084bd154..6cde880a3 100644 --- a/src/langbot/pkg/provider/tools/loaders/mcp.py +++ b/src/langbot/pkg/provider/tools/loaders/mcp.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import enum import json import math @@ -206,6 +207,13 @@ class MCPSessionStatus(enum.Enum): ERROR = 'error' +@dataclasses.dataclass(frozen=True) +class MCPOAuthChallenge: + """Bearer challenge metadata returned by an OAuth-protected MCP server.""" + + resource_metadata_url: str | None + + class _TransportReconnect(Exception): """Internal signal: the Box stdio WS transport dropped but the managed process is still alive. Triggers a lightweight transport reconnect that @@ -265,6 +273,7 @@ class RuntimeMCPSession: _ready_event: asyncio.Event error_message: str | None = None + _public_error_code: str = 'runtime_error' error_phase: MCPSessionErrorPhase | None = None @@ -510,6 +519,13 @@ class RuntimeMCPSession: await self._init_streamable_http_server() return except Exception as e: + if self._extract_oauth_challenge(e) is not None: + self.error_phase = MCPSessionErrorPhase.OAUTH_REQUIRED + self.ap.logger.info( + f'MCP server {self.server_name}: remote server requires OAuth authorization; ' + 'not falling back to SSE' + ) + raise if not self._should_fallback_to_sse(e): self.ap.logger.info( f'MCP server {self.server_name}: Streamable HTTP transport failed ' @@ -630,6 +646,7 @@ class RuntimeMCPSession: except Exception as e: self.status = MCPSessionStatus.ERROR self.error_message = str(e) + self._public_error_code = self._classify_public_error(e) self.ap.logger.error(f'Error in MCP session lifecycle {self.server_name}: {e}\n{traceback.format_exc()}') # Do NOT set _ready_event here — let _lifecycle_loop_with_retry # handle retries first. It will set the event when all retries @@ -752,6 +769,11 @@ class RuntimeMCPSession: except Exception as e: if self._shutdown_event.is_set(): return # Shutdown requested, don't retry + if self.error_phase == MCPSessionErrorPhase.OAUTH_REQUIRED: + self.retry_count = attempt + 1 + self.status = MCPSessionStatus.ERROR + self._ready_event.set() + return if self.error_phase == MCPSessionErrorPhase.BOX_UNAVAILABLE: box_service = getattr(self.ap, 'box_service', None) if box_service is not None and getattr(box_service, 'enabled', True): @@ -832,6 +854,39 @@ class RuntimeMCPSession: else: yield exc + @staticmethod + def _classify_public_error(exc: BaseException) -> str: + """Expose a safe category without transport URLs, headers, or arguments.""" + for leaf in RuntimeMCPSession._iter_exception_leaves(exc): + if isinstance(leaf, httpx.HTTPStatusError): + return f'http_{leaf.response.status_code}' + if isinstance(leaf, (httpx.TimeoutException, TimeoutError)): + return 'connection_timeout' + if isinstance(leaf, httpx.ConnectError): + return 'connection_unreachable' + return 'runtime_error' + + @staticmethod + def _extract_oauth_challenge(exc: BaseException) -> MCPOAuthChallenge | None: + """Extract an OAuth Bearer challenge from a remote MCP connection failure.""" + for leaf in RuntimeMCPSession._iter_exception_leaves(exc): + if not isinstance(leaf, httpx.HTTPStatusError) or leaf.response.status_code != 401: + continue + for header in leaf.response.headers.get_list('www-authenticate'): + bearer_match = re.search(r'(?:^|,)\s*Bearer(?:\s|,|$)', header, flags=re.IGNORECASE) + if bearer_match is None: + continue + metadata_match = re.search( + r'(?:^|,)\s*resource_metadata\s*=\s*(?:"([^"]+)"|([^,\s]+))', + header[bearer_match.end() :], + flags=re.IGNORECASE, + ) + if metadata_match is None: + continue + resource_metadata_url = metadata_match.group(1) or metadata_match.group(2) + return MCPOAuthChallenge(resource_metadata_url=resource_metadata_url) + return None + @staticmethod def _should_fallback_to_sse(exc: BaseException) -> bool: """Whether a Streamable HTTP failure matches legacy-SSE fallback. @@ -1374,7 +1429,7 @@ class RuntimeMCPSession: # environment values. Detailed diagnostics belong in AUDIT_VIEW # logs; resource-list responses expose only a stable status. 'error_message': 'MCP runtime failed' if self.error_message else None, - 'error_code': 'runtime_error' if self.error_message else None, + 'error_code': self._public_error_code if self.error_message else None, 'error_phase': self.error_phase.value if self.error_phase else None, 'retry_count': self.retry_count, 'tool_count': len(self.get_tools()), diff --git a/src/langbot/pkg/provider/tools/loaders/mcp_stdio.py b/src/langbot/pkg/provider/tools/loaders/mcp_stdio.py index 110fff431..48aa9a227 100644 --- a/src/langbot/pkg/provider/tools/loaders/mcp_stdio.py +++ b/src/langbot/pkg/provider/tools/loaders/mcp_stdio.py @@ -52,6 +52,7 @@ class MCPSessionErrorPhase(enum.Enum): MCP_INIT = 'mcp_init' RUNTIME = 'runtime' TOOL_CALL = 'tool_call' + OAUTH_REQUIRED = 'oauth_required' # Stdio MCP refused because Box is disabled in config or currently # unavailable. Not transient — retries would be pointless. The frontend # uses this phase to render a localized actionable message instead of diff --git a/tests/unit_tests/api/service/test_mcp_service.py b/tests/unit_tests/api/service/test_mcp_service.py index 31c531e98..5cddde1e4 100644 --- a/tests/unit_tests/api/service/test_mcp_service.py +++ b/tests/unit_tests/api/service/test_mcp_service.py @@ -1009,6 +1009,37 @@ class TestMCPServiceTestMCPServer: # Verify - returns task ID assert task_id == 123 + @pytest.mark.parametrize('refresh_first', [False, True]) + async def test_persisted_test_preserves_failure_details(self, refresh_first): + from langbot.pkg.provider.tools.loaders.mcp import MCPSessionStatus + + runtime_info = {'status': 'error', 'error_message': 'HTTP 403: access denied'} + session = SimpleNamespace( + status=MCPSessionStatus.CONNECTED if refresh_first else MCPSessionStatus.ERROR, + session=object(), + refresh=AsyncMock(side_effect=RuntimeError('refresh failed')), + start=AsyncMock(side_effect=RuntimeError('Connection failed, please check URL')), + get_runtime_info_dict=Mock(return_value=runtime_info), + ) + captured = {} + + def create_user_task(coroutine, **kwargs): + captured.update(coroutine=coroutine, context=kwargs['context']) + return SimpleNamespace(id=123) + + ap = SimpleNamespace( + tool_mgr=SimpleNamespace(mcp_tool_loader=SimpleNamespace(get_session=Mock(return_value=session))), + task_mgr=SimpleNamespace(create_user_task=Mock(side_effect=create_user_task)), + ) + service = _service(ap) + service._require_server = AsyncMock(return_value=(_CONTEXT, {'name': 'existing-server'})) + await service.test_mcp_server(_CONTEXT, 'existing-server', {}) + with pytest.raises(RuntimeError, match='Connection failed'): + await captured['coroutine'] + assert captured['context'].metadata['runtime_info'] == runtime_info + session.start.assert_awaited_once() + assert session.refresh.await_count == int(refresh_first) + async def test_test_mcp_server_not_found_raises(self): """Raises ValueError when server not found.""" # Setup @@ -1052,6 +1083,45 @@ class TestMCPServiceTestMCPServer: ap.tool_mgr.mcp_tool_loader.load_mcp_server.assert_called_once() assert task_id == 456 + async def test_transient_test_preserves_runtime_info_after_connection_failure(self): + runtime_info = { + 'status': 'error', + 'error_phase': 'oauth_required', + 'retry_count': 1, + } + mock_session = SimpleNamespace( + server_name='oauth-server', + start=AsyncMock(side_effect=RuntimeError('connection failed')), + get_runtime_info_dict=Mock(return_value=runtime_info), + shutdown=AsyncMock(), + ) + ap = SimpleNamespace( + tool_mgr=SimpleNamespace( + mcp_tool_loader=SimpleNamespace(load_mcp_server=AsyncMock(return_value=mock_session)) + ) + ) + captured: dict = {} + + def create_user_task(coroutine, **kwargs): + captured['coroutine'] = coroutine + captured['context'] = kwargs['context'] + return SimpleNamespace(id=457) + + ap.task_mgr = SimpleNamespace(create_user_task=Mock(side_effect=create_user_task)) + service = _service(ap) + + task_id = await service.test_mcp_server( + _CONTEXT, + '_', + {'name': 'OAuth server', 'mode': 'remote', 'enable': True, 'extra_args': {}}, + ) + + assert task_id == 457 + with pytest.raises(RuntimeError, match='connection failed'): + await captured['coroutine'] + assert captured['context'].metadata['runtime_info'] == runtime_info + mock_session.shutdown.assert_awaited_once_with() + async def test_rejected_transient_test_session_is_shut_down(self): ap = SimpleNamespace() mock_session = MagicMock() diff --git a/tests/unit_tests/provider/test_mcp_remote_transport.py b/tests/unit_tests/provider/test_mcp_remote_transport.py index 7abe8afea..91a142134 100644 --- a/tests/unit_tests/provider/test_mcp_remote_transport.py +++ b/tests/unit_tests/provider/test_mcp_remote_transport.py @@ -13,7 +13,15 @@ from aiohttp import web from mcp import types as mcp_types from langbot.pkg.api.http.context import ExecutionContext -from langbot.pkg.provider.tools.loaders.mcp import MCPToolCallTimeoutError, RuntimeMCPSession +from langbot.pkg.provider.tools.loaders.mcp import MCPSessionStatus, MCPToolCallTimeoutError, RuntimeMCPSession +from langbot.pkg.provider.tools.loaders.mcp_stdio import MCPSessionErrorPhase + + +TEST_EXECUTION_CONTEXT = ExecutionContext( + instance_uuid='instance-a', + workspace_uuid='workspace-a', + placement_generation=1, +) TEST_EXECUTION_CONTEXT = ExecutionContext( @@ -24,8 +32,9 @@ TEST_EXECUTION_CONTEXT = ExecutionContext( class _TransportProbe: - def __init__(self, streamable_status: int | None) -> None: + def __init__(self, streamable_status: int | None, streamable_headers: dict[str, str] | None = None) -> None: self.streamable_status = streamable_status + self.streamable_headers = streamable_headers or {} self.streamable_posts = 0 self.streamable_messages: list[str] = [] self.sse_gets = 0 @@ -93,7 +102,7 @@ class _TransportProbe: } ) return web.Response(status=202) - return web.Response(status=self.streamable_status) + return web.Response(status=self.streamable_status, headers=self.streamable_headers) self.sse_gets += 1 response = web.StreamResponse( @@ -136,8 +145,8 @@ class _TransportProbe: @asynccontextmanager -async def _transport_server(streamable_status: int | None): - probe = _TransportProbe(streamable_status) +async def _transport_server(streamable_status: int | None, streamable_headers: dict[str, str] | None = None): + probe = _TransportProbe(streamable_status, streamable_headers) application = web.Application() application.router.add_route('*', '/mcp', probe.handle_mcp_endpoint) application.router.add_post('/messages', probe.handle_sse_message) @@ -265,6 +274,45 @@ async def test_remote_transport_real_non_compatibility_error_does_not_fallback(s await _close_session(session) +def test_remote_transport_extracts_oauth_resource_metadata_from_bearer_challenge(): + request = httpx.Request('POST', 'https://mcp.example/mcp') + response = httpx.Response( + 401, + headers={ + 'WWW-Authenticate': ( + 'Basic realm="MCP", Bearer resource_metadata="https://mcp.example/.well-known/oauth-protected-resource"' + ) + }, + request=request, + ) + + with pytest.raises(httpx.HTTPStatusError) as exc_info: + response.raise_for_status() + + challenge = RuntimeMCPSession._extract_oauth_challenge(exc_info.value) + + assert challenge is not None + assert challenge.resource_metadata_url == 'https://mcp.example/.well-known/oauth-protected-resource' + + +@pytest.mark.asyncio +async def test_remote_transport_oauth_challenge_sets_non_retryable_authorization_state(): + headers = { + 'WWW-Authenticate': 'Bearer resource_metadata="https://mcp.example/.well-known/oauth-protected-resource"' + } + async with _transport_server(401, headers) as (probe, url): + session = _session(url) + + await session._lifecycle_loop_with_retry() + + assert session.status == MCPSessionStatus.ERROR + assert session.error_phase == MCPSessionErrorPhase.OAUTH_REQUIRED + assert session.retry_count == 1 + assert session._ready_event.is_set() + assert probe.streamable_posts == 1 + assert probe.sse_gets == 0 + + @pytest.mark.asyncio async def test_remote_transport_real_timeout_does_not_fallback(): async with _transport_server(None) as (probe, url): @@ -313,3 +361,25 @@ async def test_remote_transport_external_cancellation_is_not_converted_to_sse_fa finally: probe.release_streamable_request.set() await _close_session(session) + + +@pytest.mark.parametrize( + ('error', 'expected'), + [ + (httpx.ConnectError('secret host'), 'connection_unreachable'), + (httpx.ReadTimeout('secret URL'), 'connection_timeout'), + (TimeoutError('secret command'), 'connection_timeout'), + (RuntimeError('secret environment'), 'runtime_error'), + ( + httpx.HTTPStatusError( + 'secret response', + request=httpx.Request('POST', 'https://example.test/?token=secret'), + response=httpx.Response(403), + ), + 'http_403', + ), + ], +) +def test_public_error_category_does_not_expose_exception_details(error, expected): + grouped = ExceptionGroup('secret outer exception', [error]) + assert RuntimeMCPSession._classify_public_error(grouped) == expected diff --git a/web/playwright.config.ts b/web/playwright.config.ts index e15c6ef9e..90990a759 100644 --- a/web/playwright.config.ts +++ b/web/playwright.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ }, ], webServer: { - command: 'pnpm exec vite --host 127.0.0.1 --port 4173', + command: 'corepack pnpm@8.9.2 exec vite --host 127.0.0.1 --port 4173', url: 'http://127.0.0.1:4173', reuseExistingServer: !process.env.CI, timeout: 120_000, diff --git a/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx b/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx index d1e4140a4..d7f71261a 100644 --- a/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx +++ b/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx @@ -8,7 +8,14 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; -import { Braces, Loader2, Trash2, Wrench, XCircle } from 'lucide-react'; +import { + Braces, + Loader2, + ShieldAlert, + Trash2, + Wrench, + XCircle, +} from 'lucide-react'; import { Resolver, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; @@ -101,7 +108,7 @@ function StatusDisplay({
- {t('mcp.connectionFailed')} + {t('mcp.connectionFailedStatus')}
@@ -117,15 +124,41 @@ function StatusDisplay({ ); } + if (runtimeInfo.error_phase === 'oauth_required') { + return ( +
+
+ + + {t('mcp.oauthAuthorizationRequired')} + +
+
+ {t('mcp.oauthAuthorizationRequiredSuggestion')} +
+
+ ); + } + + const httpStatus = runtimeInfo.error_code?.match(/^http_(\d{3})$/)?.[1]; + const errorDetail = + runtimeInfo.error_code === 'connection_unreachable' + ? t('mcp.connectionUnreachable') + : runtimeInfo.error_code === 'connection_timeout' + ? t('mcp.connectionTimeout') + : httpStatus + ? t('mcp.connectionHttpError', { status: httpStatus }) + : runtimeInfo.error_message || t('mcp.unknownError'); + return (
- {t('mcp.connectionFailed')} + {t('mcp.connectionFailedStatus')}
- {runtimeInfo.error_message && ( -
- {runtimeInfo.error_message} + {errorDetail && ( +
+ {errorDetail}
)}
@@ -835,15 +868,31 @@ const MCPForm = forwardRef(function MCPForm( async function testMcp() { setMcpTesting(true); + const showConnectionFailure = ( + message: string, + info?: MCPServerRuntimeInfo, + ) => { + toast.error(t('mcp.connectionFailedStatus')); + setRuntimeInfo({ + tool_count: 0, + tools: [], + resource_count: 0, + resources: [], + ...info, + status: MCPSessionStatus.ERROR, + error_message: info?.error_message || message, + }); + }; + try { const mode = form.getValues('mode'); if (mode === 'stdio' && !mcpStdioEnabled) { - toast.error(t('mcp.stdioDisabledByPolicy')); + showConnectionFailure(t('mcp.stdioDisabledByPolicy')); setMcpTesting(false); return; } if (mode === 'stdio' && !boxAvailable) { - toast.error(t('mcp.stdioBlockedByBoxToast')); + showConnectionFailure(t('mcp.stdioBlockedByBoxToast')); setMcpTesting(false); return; } @@ -914,15 +963,9 @@ const MCPForm = forwardRef(function MCPForm( if (taskResp.runtime.exception) { const errorMsg = taskResp.runtime.exception || t('mcp.unknownError'); - toast.error(`${t('mcp.testError')}: ${errorMsg}`); - setRuntimeInfo({ - status: MCPSessionStatus.ERROR, - error_message: errorMsg, - tool_count: 0, - tools: [], - resource_count: 0, - resources: [], - }); + const runtimeInfoFromTest = taskResp.task_context?.metadata + ?.runtime_info as MCPServerRuntimeInfo | undefined; + showConnectionFailure(errorMsg, runtimeInfoFromTest); if (shouldTestPersistedServer) { await onPersistedTestComplete?.(serverName); } @@ -949,14 +992,19 @@ const MCPForm = forwardRef(function MCPForm( clearInterval(interval); setMcpTesting(false); const errorMsg = - (err as CustomApiError).msg || t('mcp.getTaskFailed'); - toast.error(`${t('mcp.testError')}: ${errorMsg}`); + (err as CustomApiError).msg || + (err as Error).message || + t('mcp.getTaskFailed'); + showConnectionFailure(errorMsg); } }, 1000); } catch (err) { setMcpTesting(false); - const errorMsg = (err as Error).message || t('mcp.unknownError'); - toast.error(`${t('mcp.testError')}: ${errorMsg}`); + const errorMsg = + (err as CustomApiError).msg || + (err as Error).message || + t('mcp.unknownError'); + showConnectionFailure(errorMsg); } } diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index 7c66f6be1..997ffc489 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -586,6 +586,7 @@ export enum MCPSessionStatus { } export interface MCPServerRuntimeInfo { + error_code?: string; status: MCPSessionStatus; error_message?: string; /** Stage at which the session failed. Frontends key off this to render diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 3fb93b85a..e5192ecad 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -872,6 +872,15 @@ const enUS = { connectionSuccess: 'Connection successful', connectionFailed: 'Connection failed, please check URL', connectionFailedStatus: 'Connection Failed', + connectionUnreachable: + 'Cannot reach the MCP server. Check that it is running and accessible.', + connectionTimeout: + 'The MCP server did not respond in time. Check the service or increase the timeout.', + connectionHttpError: + 'The MCP server returned HTTP {{status}}. Check its access requirements and server logs.', + oauthAuthorizationRequired: 'OAuth authorization required', + oauthAuthorizationRequiredSuggestion: + 'This MCP server requires OAuth sign-in. OAuth sign-in is not available yet; add an Authorization header manually if the server supports it.', boxDisabledStdioRefused: 'Stdio MCP servers require the Box sandbox, which is disabled in config (box.enabled = false).', boxUnavailableStdioRefused: diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 54918de07..9f8f97d99 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -892,6 +892,15 @@ const esES = { connectionSuccess: 'Conexión exitosa', connectionFailed: 'Error de conexión, por favor verifica la URL', connectionFailedStatus: 'Conexión fallida', + connectionUnreachable: + 'No se puede acceder al servidor MCP. Compruebe que esté iniciado y accesible.', + connectionTimeout: + 'El servidor MCP no respondió a tiempo. Compruebe el servicio o aumente el tiempo de espera.', + connectionHttpError: + 'El servidor MCP devolvió HTTP {{status}}. Compruebe los requisitos de acceso y los registros del servidor.', + oauthAuthorizationRequired: 'Se requiere autorización OAuth', + oauthAuthorizationRequiredSuggestion: + 'Este servidor MCP requiere inicio de sesión con OAuth. Aún no está disponible; agregue manualmente un encabezado Authorization si el servidor lo permite.', boxDisabledStdioRefused: 'Los servidores MCP en modo stdio requieren el sandbox de Box, desactivado en la configuración (box.enabled = false).', boxUnavailableStdioRefused: diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 7a9803d7a..37fe6f6b2 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -880,6 +880,15 @@ const jaJP = { connectionSuccess: '接続に成功しました', connectionFailed: '接続に失敗しました,URLを確認してください', connectionFailedStatus: '接続失敗', + connectionUnreachable: + 'MCP サーバーに接続できません。起動状態とネットワークを確認してください。', + connectionTimeout: + 'MCP サーバーの応答がタイムアウトしました。サービスを確認するか、待機時間を延長してください。', + connectionHttpError: + 'MCP サーバーが HTTP {{status}} を返しました。アクセス要件とサーバーログを確認してください。', + oauthAuthorizationRequired: 'OAuth 認可が必要です', + oauthAuthorizationRequiredSuggestion: + 'この MCP サーバーには OAuth ログインが必要です。現在は OAuth ログインに対応していません。サーバーが許可している場合は、Authorization ヘッダーを手動で追加してください。', boxDisabledStdioRefused: 'Stdio モードの MCP サーバーは Box サンドボックスを必要としますが、設定で無効化されています(box.enabled = false)。', boxUnavailableStdioRefused: diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index 22d8f04f3..261c8c9e3 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -885,6 +885,15 @@ const ruRU = { connectionSuccess: 'Подключение успешно', connectionFailed: 'Не удалось подключиться, проверьте URL', connectionFailedStatus: 'Ошибка подключения', + connectionUnreachable: + 'Сервер MCP недоступен. Проверьте, запущен ли он и доступен ли по сети.', + connectionTimeout: + 'Время ожидания ответа MCP истекло. Проверьте сервис или увеличьте тайм-аут.', + connectionHttpError: + 'Сервер MCP вернул HTTP {{status}}. Проверьте требования доступа и журналы сервера.', + oauthAuthorizationRequired: 'Требуется авторизация OAuth', + oauthAuthorizationRequiredSuggestion: + 'Для этого MCP-сервера требуется вход через OAuth. OAuth-вход пока не поддерживается; если сервер это позволяет, добавьте заголовок Authorization вручную.', boxDisabledStdioRefused: 'MCP-серверы в режиме stdio требуют песочницу Box, которая отключена в конфигурации (box.enabled = false).', boxUnavailableStdioRefused: diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index 44b01f883..2621d770a 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -863,6 +863,15 @@ const thTH = { connectionSuccess: 'เชื่อมต่อสำเร็จ', connectionFailed: 'เชื่อมต่อล้มเหลว กรุณาตรวจสอบ URL', connectionFailedStatus: 'เชื่อมต่อล้มเหลว', + connectionUnreachable: + 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ MCP ได้ โปรดตรวจสอบว่าบริการทำงานและเข้าถึงได้', + connectionTimeout: + 'เซิร์ฟเวอร์ MCP ไม่ตอบกลับภายในเวลาที่กำหนด โปรดตรวจสอบบริการหรือเพิ่มเวลารอ', + connectionHttpError: + 'เซิร์ฟเวอร์ MCP ส่งคืน HTTP {{status}} โปรดตรวจสอบข้อกำหนดการเข้าถึงและบันทึกของเซิร์ฟเวอร์', + oauthAuthorizationRequired: 'ต้องมีการอนุญาต OAuth', + oauthAuthorizationRequiredSuggestion: + 'MCP server นี้ต้องเข้าสู่ระบบด้วย OAuth ซึ่งยังไม่รองรับในขณะนี้ หาก server อนุญาต คุณสามารถเพิ่ม Authorization header ด้วยตนเองได้', boxDisabledStdioRefused: 'MCP server แบบ stdio ต้องใช้ Sandbox Box ซึ่งถูกปิดใช้งานในการตั้งค่า (box.enabled = false)', boxUnavailableStdioRefused: diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index ad7b483e8..6e81af16f 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -878,6 +878,15 @@ const viVN = { connectionSuccess: 'Kết nối thành công', connectionFailed: 'Kết nối thất bại, vui lòng kiểm tra URL', connectionFailedStatus: 'Kết nối thất bại', + connectionUnreachable: + 'Không thể kết nối tới máy chủ MCP. Hãy kiểm tra dịch vụ và kết nối mạng.', + connectionTimeout: + 'Máy chủ MCP không phản hồi kịp thời. Hãy kiểm tra dịch vụ hoặc tăng thời gian chờ.', + connectionHttpError: + 'Máy chủ MCP trả về HTTP {{status}}. Hãy kiểm tra yêu cầu truy cập và nhật ký máy chủ.', + oauthAuthorizationRequired: 'Yêu cầu ủy quyền OAuth', + oauthAuthorizationRequiredSuggestion: + 'MCP server này yêu cầu đăng nhập OAuth. Hiện chưa hỗ trợ đăng nhập OAuth; hãy thêm thủ công tiêu đề Authorization nếu server cho phép.', boxDisabledStdioRefused: 'MCP server ở chế độ stdio cần Sandbox Box, hiện đã bị tắt trong cấu hình (box.enabled = false).', boxUnavailableStdioRefused: diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 84b7775a0..e91090f4d 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -837,6 +837,14 @@ const zhHans = { connectionSuccess: '连接成功', connectionFailed: '连接失败,请检查URL', connectionFailedStatus: '连接失败', + connectionUnreachable: + '无法连接到 MCP 服务器,请确认服务已启动且网络可达。', + connectionTimeout: 'MCP 服务器响应超时,请检查服务状态或增加超时时间。', + connectionHttpError: + 'MCP 服务器返回 HTTP {{status}},请检查访问要求和服务器日志。', + oauthAuthorizationRequired: '需要 OAuth 授权', + oauthAuthorizationRequiredSuggestion: + '此 MCP 服务器需要 OAuth 登录。当前尚不支持 OAuth 登录;如果服务器允许,可以手动添加 Authorization 请求头。', boxDisabledStdioRefused: 'Stdio 模式的 MCP 服务器依赖 Box 沙箱,目前已在配置中禁用(box.enabled = false)。', boxUnavailableStdioRefused: diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 52f30996b..1920a8ba4 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -839,6 +839,14 @@ const zhHant = { connectionSuccess: '連接成功', connectionFailed: '連接失敗,請檢查URL', connectionFailedStatus: '連接失敗', + connectionUnreachable: + '無法連接到 MCP 伺服器,請確認服務已啟動且網路可達。', + connectionTimeout: 'MCP 伺服器回應逾時,請檢查服務狀態或增加逾時時間。', + connectionHttpError: + 'MCP 伺服器回傳 HTTP {{status}},請檢查存取要求和伺服器日誌。', + oauthAuthorizationRequired: '需要 OAuth 授權', + oauthAuthorizationRequiredSuggestion: + '此 MCP 伺服器需要 OAuth 登入。目前尚不支援 OAuth 登入;如果伺服器允許,可以手動新增 Authorization 請求標頭。', boxDisabledStdioRefused: 'Stdio 模式的 MCP 伺服器依賴 Box 沙箱,目前已在設定中停用(box.enabled = false)。', boxUnavailableStdioRefused: diff --git a/web/tests/e2e/mcp-oauth-required.spec.ts b/web/tests/e2e/mcp-oauth-required.spec.ts new file mode 100644 index 000000000..61dc5fa5c --- /dev/null +++ b/web/tests/e2e/mcp-oauth-required.spec.ts @@ -0,0 +1,74 @@ +import { expect, test } from '@playwright/test'; + +import { installLangBotApiMocks } from './fixtures/langbot-api'; + +function ok(data: unknown) { + return { + code: 0, + message: 'ok', + data, + timestamp: Date.now(), + }; +} + +test('shows an actionable OAuth-required state after a transient MCP test', async ({ + page, +}, testInfo) => { + await installLangBotApiMocks(page, { authenticated: true }); + + await page.route('**/api/v1/mcp/servers/_/test', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(ok({ task_id: 2363 })), + }); + }); + await page.route('**/api/v1/system/tasks/2363', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify( + ok({ + runtime: { + done: true, + exception: 'Connection failed', + state: 'error', + }, + task_context: { + current_action: 'Testing MCP server', + log: '', + metadata: { + runtime_info: { + status: 'error', + error_phase: 'oauth_required', + retry_count: 1, + tool_count: 0, + tools: [], + resource_count: 0, + resources: [], + }, + }, + }, + }), + ), + }); + }); + + await page.goto('/home/mcp?id=new'); + await page.locator('input[name="name"]').fill('oauth-protected-mcp'); + await page + .locator('input[name="url"]') + .fill('https://mcp.example.test/protected'); + await page.getByRole('button', { name: /^Test$/ }).click(); + + await expect(page.getByText('OAuth authorization required')).toBeVisible(); + await expect( + page.getByText( + 'This MCP server requires OAuth sign-in. OAuth sign-in is not available yet; add an Authorization header manually if the server supports it.', + ), + ).toBeVisible(); + await page.screenshot({ + path: testInfo.outputPath('oauth-required.png'), + fullPage: true, + }); +});