From e0e991126f125d51d2b0c6952ab39a662faa07fe Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Mon, 4 May 2026 17:42:47 -0700 Subject: [PATCH] feat: add searchable MCP tool inventory --- api/routes.py | 184 ++++++++++++++++++ docs/pr-media/697/mcp-tools-search-filter.png | Bin 0 -> 65004 bytes static/i18n.js | 64 ++++++ static/index.html | 8 + static/panels.js | 56 +++++- static/style.css | 6 + tests/test_issue697_mcp_tool_inventory.py | 136 +++++++++++++ 7 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 docs/pr-media/697/mcp-tools-search-filter.png create mode 100644 tests/test_issue697_mcp_tool_inventory.py diff --git a/api/routes.py b/api/routes.py index 9cbb7b16..8cb81380 100644 --- a/api/routes.py +++ b/api/routes.py @@ -2820,6 +2820,10 @@ def handle_get(handler, parsed) -> bool: if parsed.path == "/api/mcp/servers": return _handle_mcp_servers_list(handler) + # ── MCP Tools (GET) ── + if parsed.path == "/api/mcp/tools": + return _handle_mcp_tools_list(handler) + # ── Checkpoints / Rollback (GET) ── if parsed.path == "/api/rollback/list": qs = parse_qs(parsed.query) @@ -7548,6 +7552,186 @@ def _server_summary(name, cfg, runtime_status=None): return out +def _mcp_safe_display_text(value, *, limit: int) -> str: + """Return redacted, bounded MCP text safe for WebUI inventory rows.""" + if not isinstance(value, str): + value = "" if value is None else str(value) + value = _redact_text(value).strip() + value = re.sub(r"Authorization:\s*Bearer\s+\S+", "[REDACTED CREDENTIAL]", value, flags=re.I) + if len(value) > limit: + value = value[: max(0, limit - 1)].rstrip() + "…" + return value + + +def _mcp_schema_type(schema) -> str: + """Return a compact, non-sensitive display type for a JSON schema node.""" + if not isinstance(schema, dict): + return "unknown" + typ = schema.get("type") + if isinstance(typ, list): + typ = "/".join(str(t) for t in typ if t) + if isinstance(typ, str) and typ: + return typ + for composite in ("anyOf", "oneOf", "allOf"): + if isinstance(schema.get(composite), list) and schema[composite]: + return composite + if "enum" in schema: + return "enum" + return "unknown" + + +def _mcp_schema_summary(schema, *, limit: int = 12) -> list[dict]: + """Summarize an MCP input schema without exposing raw defaults/examples. + + The WebUI only needs searchable/displayable argument hints. Returning raw + JSON Schema can overexpose server-provided defaults, examples, enums, or + vendor extensions, so this strips each parameter down to name/type/required + and a redacted description. + """ + if not isinstance(schema, dict): + return [] + properties = schema.get("properties") + if not isinstance(properties, dict): + return [] + required = schema.get("required") + required_names = set(required) if isinstance(required, list) else set() + out = [] + for name, prop in properties.items(): + if len(out) >= limit: + break + if not isinstance(name, str): + continue + prop = prop if isinstance(prop, dict) else {} + desc = prop.get("description", "") + if not isinstance(desc, str): + desc = "" + desc = _mcp_safe_display_text(desc, limit=180) + out.append({ + "name": name, + "type": _mcp_schema_type(prop), + "required": name in required_names, + "description": desc, + }) + return out + + +def _mcp_tool_schema_from_payload(tool): + if not isinstance(tool, dict): + return {} + for key in ("parameters", "inputSchema", "input_schema", "schema"): + value = tool.get(key) + if isinstance(value, dict): + if key == "schema" and isinstance(value.get("parameters"), dict): + return value["parameters"] + return value + return {} + + +def _mcp_tool_summary(name, tool, server_summary): + """Return a safe global inventory row for one MCP tool.""" + server_summary = server_summary if isinstance(server_summary, dict) else {} + if isinstance(tool, str): + tool = {"name": tool} + elif not isinstance(tool, dict): + tool = {} + tool_name = str(tool.get("name") or name or "") + description = tool.get("description") or "" + if not isinstance(description, str): + description = str(description) + description = _mcp_safe_display_text(description, limit=360) + return { + "name": tool_name, + "server": str(server_summary.get("name") or ""), + "description": description, + "active": bool(server_summary.get("active")), + "enabled": bool(server_summary.get("enabled")), + "status": server_summary.get("status") or "unknown", + "schema_summary": _mcp_schema_summary(_mcp_tool_schema_from_payload(tool)), + } + + +def _mcp_tools_from_runtime_status(runtime_by_name, server_summaries): + """Read detailed MCP tool payloads from runtime status when available.""" + tools = [] + if not isinstance(runtime_by_name, dict): + return tools + for server_name, runtime in runtime_by_name.items(): + if not isinstance(runtime, dict): + continue + raw_tools = runtime.get("tools") + if not isinstance(raw_tools, list): + raw_tools = runtime.get("tool_schemas") + if not isinstance(raw_tools, list): + continue + server_summary = server_summaries.get(str(server_name), {"name": str(server_name)}) + for index, tool in enumerate(raw_tools): + fallback_name = f"{server_name}:{index}" + summary = _mcp_tool_summary(fallback_name, tool, server_summary) + if summary["name"]: + tools.append(summary) + return tools + + +def _mcp_tools_from_registry(server_summaries): + """Read already-registered MCP tool schemas without probing MCP servers.""" + try: + from tools.registry import registry + except Exception: + return [] + tools = [] + try: + names = registry.get_all_tool_names() + except Exception: + return [] + for tool_name in names: + try: + toolset = registry.get_toolset_for_tool(tool_name) + except Exception: + continue + if not isinstance(toolset, str) or not toolset.startswith("mcp-"): + continue + server_name = toolset[len("mcp-"):] + schema = registry.get_schema(tool_name) or {} + server_summary = server_summaries.get(server_name, { + "name": server_name, + "enabled": True, + "active": False, + "status": "configured", + }) + tools.append(_mcp_tool_summary(tool_name, schema, server_summary)) + return tools + + +def _handle_mcp_tools_list(handler): + """List known MCP tools from already-available runtime inventory only.""" + cfg = get_config() + servers = cfg.get("mcp_servers", {}) + if not isinstance(servers, dict): + servers = {} + runtime = _mcp_runtime_status_by_name() + server_summaries = { + str(name): _server_summary(str(name), scfg, runtime.get(str(name))) + for name, scfg in servers.items() + } + tools = _mcp_tools_from_runtime_status(runtime, server_summaries) + source = "mcp_runtime_status" + if not tools: + tools = _mcp_tools_from_registry(server_summaries) + source = "tool_registry" if tools else "none" + tools.sort(key=lambda row: (row.get("server", ""), row.get("name", ""))) + unavailable_servers = [ + summary["name"] for summary in server_summaries.values() + if summary.get("enabled") and not summary.get("active") + ] + return j(handler, { + "tools": tools, + "total": len(tools), + "source": source, + "inventory_scope": "already_known_runtime_only", + "unavailable_servers": unavailable_servers, + }) + + def _handle_mcp_servers_list(handler): """List configured MCP servers with safe, read-only runtime visibility.""" cfg = get_config() diff --git a/docs/pr-media/697/mcp-tools-search-filter.png b/docs/pr-media/697/mcp-tools-search-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..3d681893f994e62f460fd0506da4135f7e504793 GIT binary patch literal 65004 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�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

Cj%UKxz&#yUqKg zng(t%%ZV^TjmgS0+C-Mg%^#(b>yVVq(cx+wkj3iR`KbqxkWbGBrZO8^cR5*<{yf7U>*EZ;*kcWQte0xNiRsq-p*|~{ zmI&%wrxS?aL)mViUA9H(NO8vFdbhspFfm&X?}N;?nS`2v3qp?yLyEcGOC!3msHtZe z0vfLz!qxhw;!v78nQw++$3>&0<-&FjRG;&g&^@Hfmi1&!66Snj=Z`^Od3lgzj!@c$ z7?QR7d}<)bfh`!(O847a7iCJZ;T9Ae z>{2g0m1%gW-n^)+zfF{LlA^Dd;nJo|V^_~5w`6s2PiW=bk9Kc_S-fTs#318YTe5lT zmPR1A(OVv&EOxFnG!Br?oFpo;DD*5fo^h&LdDqLJN>)7H}L3B`= z>PlO{E0mSa-66mV1s}n4UR3(_ol@o+(xirCQpHV+xpgZp(Bob;^XKsV6yvJ^MP7yVHwV?S4R5^Q=Y`2AtXDD`VEcC3Zp7(nqEJFKWSel+_q$Sr~&?XLp z2@e$l5KYgtHmV01)*Z?keQWDp_wkORydQ=gqCIZAkGB$oFAVssRWpK{W<;r&o-V#A z-WS|`t3z9U;VcM)?3D1JS1l^UXB60P3%_A?7MP{LGj4VFC2`i*wuff&SYut8x zQik?pakPc7C1{thOZ)a9)gEH2#K``OyX0gx_d@xwz1!^WY~HFthHwuSRkb=7j6Lc4 zQ&(4#dcMuiT{@wc)8eUWX`YAR9k9&gycR84|L#guGltsw?(BJ7aW(t~J);}^^Eaje z{op*C2o_0e_RdAFP~Hav^4374*~m4o*a+kN=VDuKoy1XirFG}MY7vp(GA~vesB~m9hiJQiboN3U&J}Dd`snkL?0>N%US>7~-p~dTL{zlFLSaH_9>Q z3u*Oz!!!z8mb>%YFN}kCl3Odv)PJYqkQ&T?aS@4rt1kXwR!1%m)M7+Zx%pY|0$7+y zaG46xNtBwXOf_m^dkn(b8TC6ZVk4ZV3GvmlA~7^>e{X?1##G=en-`o}kBd-3a-x>0 zZ#6Z)MJ%eD&u~P#@4(#(zK0jn~lcu4(& zg6Y%#Jd+0a&%NN(4pFc>?HBGlDYU~h79Ps8LS6(P)xwxLI6#Yn3LWy@o0360;fiTL zQ&ZB`Y5cJg#oU8m5Kg8!KG=879r3Y0d(`6U)hhI)D|aG|2Fo-H8B(Y z#V@JJ9;O*Sf5IdunGcLh$5$gZKo@POtmK9_dd4cNm>?aqdR=iG>Tod>)knVEJ#Wkt z;<`Agek7ryAdwvokNpi zXXzk^({gUWx8-212+>oCYx*(dS93VNoMlKEWZKIbv*qW(5tCE6l=3c8sy%COat@k6 z*yByHzD1eOBQp6g8qSnhnBwN)=CLJ6?ym1HUSpW;F9bVAGMc*noC@JU*Eumv4S}~e z#A>VibWeNnLtFbp4=cOqVF!)qI||Q~aECN$gES9l7{vhW`+CRk*s&okux~B*g8f3n z!(zWstg+brO=denA6>LpFZh3GmN%EL!Bo_Can!ye^li$ zc||Ts2RE3Raf^zb@&u+4usW1;TV%ZvVP)NX+~khSvv1k-H7|fwg#ZmEF2mh!CH1@g zoZBYq3csP{arKQ%S2y&4Ex))i+!kyZDOdM~#9QKPcpya*#y72@ZzYhMPXaQQlH#)E z?iw2=@zi;}ZbmbHSHdnUDRUsl$r;1K0pK(YfKA76_cxcyTFx`(lS#g$7ExMHQF^rT zX6@M1~4=E`o~QKUi^{rv0`LD+b2G>%AZQb+s+v@{E`7iJM{N|?cR6Y_1V)`}K@ZsQZ&XNy4FHLdTCB3Ti7v%nX~lslX=GLpz>6 zae8IFPBW9g88dw-l2rr!qW|<1XL7 zu^taL{G36};44a&iZu6MY9%h(hnjyh5gkk@tX7Nh!co*5SSprO=ZYiL-~lwsz6FY` z7weFb+=a8Cax|@8&4W5#*gtVTtw>`WU?_1^{$Xwr@z>LDnL6J}mbV!*QLb+3Lq1=R-^2gLFQuR)IYeFPP)h%H2d-Kvp}X z;92I$@c3<;$*t+JoxP(IiA#setHFJ}N&-e+E}2_2wk6`A3SZS6e}Y^2AubNWV}Iwd zt&4jZqS8zKP20xJW0$?TkDS*%8#YXy{SNWxaVa0EyZUdjfHj(Q{R0hL0hdlAXHz1w5LtBGNDEadHo9DJ^KYxkc`#yG(>eHqm#(bB(LEC{Xz+U;N zVX{OGyvM#}`Rza_QQby(3+B9@_l3ypb;DYykGng<={H>=Otm~A;h?2uF#M+-knqwk z2#o$lo5-;Q(+__)XEJJ8p9dR@%@^Y=u{~+l?tm9D77h&I6gz3-s8h()Ggqyk(~3+f z2^L@z?U~N_*U4ym{(r-|tgGke=S4m0b%7b&_lzFZ8(QC1DtCAr2qysQ(Uh!!!@yh8 z(b>uNA7O^~?_V`bDqe&i}n8r{QH-G$asLd!n^`@f@uKI7#~L_lr}XuIP|Zuh-W38d&3|r)2gSK zlY6Df$pN6CXa*D`4hanj324Fpc-7j*c>%3Vzd9LS_Mim9e{+9~{{znZ6GTHz9UVTe z3AiD|@JTrd*4{LA-m-M!hb{d`S59Qwzlr>-Cb8(PfuCEf;^Ea zDWVk>X`&wHH3-@K7pWN%`+!4f_WH)P>o2li!Gzhk)k?+yx}Wmn!lF`a zB8iEhe$#;Yi&*Y^Qv7PnO+8{>8Nd>Mf|OKL4o+~u|(Ngzqs zVLuu>=P!Vr}<>P_+a-^8J{KW7_U6ky7z0}|40J6etY9ql_nD#Ga=YGbPiErGZYP!053C7 z)Zqyir>u|zFYXF(mLRK`hn_gM92AY^uTXHq-NTT$ON{x+E!BDaQbI;S^BkiP-*rqh zFK5xzzEP@)7Jw4Q$}EAd9EU38HJI>f6&ER#|V5*&hRX z^D}J+K(XS7KcAR`k;(!%!%O}82Wt0-1a!YTp_Cyl>QU4uiJ*N$?2g0)TjUHgcG?~? zvVJ!V{?ewGy?dihL^PnTHR!l9{5kvf?r!|K2^%BWZNXsX-Eh9uG83-(+@dS(DR?%L zmfy(hv`)kum*^qqGRW`1L`vq~=B53@s-xX%c_xc<1BP#asUuUvxG>Wj4r0yCPua6&rR6 z3=we?ao9&yNb?Pv`&wx`nOpaWuz$m+G~#Wy7#rf>-PrOLEVPB6`jbfB+bt^N5Aeb{rCu-#aTTZlAJJX#?~tr zwJ~$gHq~@MENx&~t<|=HJ!BpqzFZeq|NU6fnSPinaDBN(kvnGM8|To>8_1uqfoub# zvqCm4?WUwxtRf`)nXP?n2HXTZC8Pno>w? zKL@*rjo@a`X{ov$o1w{G>3FRJBp#JvYM5YSU?y0gupO@hkC_Id2JPedSE+9*iMiG_ zLB7sAmh(PKH?9nN?|L$C7v^EE%qKfB# zugsMdJhR0XnP|OQMpuW-%Aa*k(m&-U9Ov;iyY>~3l`!>H)}32AZLz@(59}``m3Avk z*d4YTU_}UKbP#R2&F*{jLDxTY@=#pAxA}O6!NYDDd>6nqeA81jj$>a*Z#%ebiE!1> zq)8p6Cs`^F>ViWQXYUqG`=;g|cE5L3)0IDH7_2RlKdn*pe*|-mbx})j)bq|p*tW7w zqPRBVKb;=Tqx3~Mt9FXTV4JT3x3zfC;-;v;-fXX`#U>N(X^j5HhCi+U*zbmu|DH2Z z9Gva*6EjnA3ar$AanNUL56Jj}dIVY(EybJb~Q9 z{!XZBS#m338)$;@qsH7y(K}7jO1$Nq5WA?J!bgrSQzErChrMeWn$^$WG;{(i$np1$ zG4Qgj?RPUlCWcl*YlA0;)Yc7)vJQr|%yO0BbP6cPO0=@5o37ln7~5ZdgeW+HSfRm_ zt_o$ypKvE8$&qtV;ASiK1IRX*Q9{Fo4O!pVR;n}EJK_2jpHbQ8iMGnOim{xqDP{)@yk)iGrKW7(QyRBuEn%P>UdR#cG);j-_W! zR~5H5Y-==kiOpB?Zrk6-1Rz{!>cpd`${yb&AJWJMQs|3B1FV)x1XKi=9jvga;Cq7A zhp(fX2#_m!NwR~g#;-pxH*J)wK5U#XYjw6DX|CMKa9KFU)K?X4h_ou>tr11d7RgF7 za|KSmgX^=b5(PI;SJ$I?Z3AlwxUAC+!rM3wE?3CKDt$C;pd|bt6>-n0++#hdY`WG> zwZ-2hPv1m0ZB0_6=G3uUQ`e*=r@Ll9?DW@u>?iJq*0+;6wI;B*2xSlPVwx@0!LHjL zW3QDO_v`@Jb0hx0@SZ&x`z^`X^8M!|%Rnw=B z;ZIcCDAOyT(I6g;+d+fKw`{H-OmvmDzj2_i-xGdJ{{YzK&X8=CYEBe?b(eYHiGP2N zxsq%I9rVjG&Z-)on_@P=B&~|k7`jYM*mMU;pnS^wLN$peRu2rP%LUcSO#Vg6&|%N~ zcT&b1$HNIdy&ATsOmBFu{+gV#%+3p%RF>Mg`^*bhi5brqb-^Pks7%{&pW4LX#R*ep z-GTNqULIv);~UQnn$H!IdA@ZlVxmg+Gfrs(shO=fylU%$d`h*lq16fYiG`aUXN{^^ zO{LsC$`0HCjhyPvrE}$I=ARvvVhcTQ>dKra)g4=^bsmC5lDu2pzmLtOCpajIO4fJh zZtRDBzH@H(wt@*;`)s<0YWc=mL+uH^1&u99Z^WHx+Ha$weM)}bkE=J_CmcD41+@tW z^^9KyzA3dl=G!jR5ODI*%+AX-M!j|HQB!QObF`$(75? zF=GZ^u?A8?jL>rPg&CH*qjvkggr!qMe*H?-_F2Iv@)U-ZNKEg&{M>TwRoDn?)4W^R zr?Nc{wOH!j|oNevK6!4QQB;ccSZGP zs{2N8j}jnnDZMD=<>Mb1qpo{|Z^TqCmlc!WYaR6@lm&i2MI^CDo&0AeQCel$wgJTO z?aA2u3)=5Vr9H`NEiLDZ+_71$%WGVnv+#|wS=v4EN86veIA6oM5_0x4s_G&$9#UV+ zbgZy_F_5WQ4ZAI}1HHOYTsw0a4PaYVG2-6BVW2N5T8(0Ql6$=*>Zd9k>?YlxwesA} zb=T4^uTARAsI%1__13{mcD75M)i~#N>qm|vx4Mim?aZ{jlL>xDk5%jOo6%D+@-n7- z775C>mUgE3H58O7;`eq6HLp1w`pxgd)t3rVX%ok!0?gN0Oemxm%dhpmt=gS0fiJQT zOdYZ=+?iboOW`%A{S~q!xiQAgHDhSF;3@aHlx|eLmCDMQCQs4Bb7lKASShG;;*fbz z??`k&1aU|%tUG;`ysMlfN<3Zg_HFtg;Ys`NncHMVK#*q+o#)(R#Q&E5ktav?jd1B{ zQUI1(=c-aHuWmd4yLFb~FGyb;6v=YM$#tW=E2pdMDnyG6^gnM4)HvCbxn?}y>NPMQ zad0L}w3;59ThJ&my_6j{0WgGC?dn98Ba#h0GqX0Cspo1f%HBy%+Vg!&2dX3GspXB( zN-I@Gk|myR-%FQ)QQ{;n#U|%gfAsAd0J%h0Nt`1;sLE* z8S2Znx45IK)4f>R!Cu~!4un&p!+4^CU|3MR?V4+34fBSuB^ZtvnksYXOO$i>e&wbX zGM$p}Rh>={s!%()$BdjAG1pUmj>zzW`UMMD*&Z4yhdecK_K1&ll_Zv zR6t{cYwWhQ)kAKco&?VO7E1+Q-g14l9|eQI9xut!S9xCC#nF zjonK%Ozkq+wA4h*tvFTTIIP8f`dY`H; zfJ}%1S&)CU16i1y(1_l;$%wDm0MO6tH3PBgrhS(}2S&VpvY$l7>~}ZZtsnF*jCuGE zyGo~*m&xc+OFi@J^MJM2An2RH@H4XTB*+^j&q(9d%|ZN>UaGIeqF@^JJD zLz!9sA$NsJEqTmt{za5g!} zV*{@nc&w>P6X-`&(B8)8Z>E$m!zJ7A`K=pyXhNarM+&cD+=xszu^;rGk6th?QgU*?2LvXKaHi{T6m#t7sb0W zRdePhDAa#gOcw|=x2HKYnK^8c|5_%;#xCh@3wzf_Z{2{PqROh<0{>1lSU|fle&*~3w`Mrc+Qh#8Hx&7kn zxGPJ2t)Y!+Qr>7T)7W%#J};4$F)sDDv_C!4zm>6JYlOW@*$uKdH2DK;Hp>0+5>4!G z$%Gtg5U6#rV{pr+U)Mp`2axX@%Z1X}~{ z6uP}X>D2GmVtVQf1JyeF6v)PvafJREFXVF@)KoG#0pD(J71Q_VUsmS{d>dtQ3qp6j zoMJLDH%(s`CoiH`*28CSsjfV*ZJ~7Y)W}=i{q^=jLT!PAov@NYj~<$u?o_LL(Y4n% z290I{G;+5X1Xyjt2Oqf8wPIEmKQS(`XXe}XAr8>sXdThU{0vndwBkZzO-t;%KI`N< zP6NlJq!4qiX|3^R=if$>MUJ>o|@dg^#T z$TO;@w5we%^T%AxJ#LaFyBN~)Gs?zVO75YpUm!}dN$-i}OhLPwrulVAPTX-cB=m(~ zTwJnV0th9?EGS){{GD)QW3EV~vD z$KT0Z_=Fk%C~UqI?m=5;*GvB~nc(;FVrY_h%2S&v(L;oLj4eo>S=D!pTCOM_UZU5k z4zyIz+c7knBPF(BZ63Xhr3S=^+Df1LXOx-w1(g{<1z%gi`h||_;JCzZOG1*mW~$oR z$sEVgcEfACv$~;t^XQh%g@qZ^w0%Fs=~Ci1 z5+gt27O zC?zHdD%#z2JU#ZARZLds6AUqOS|N9jz`*8(8H^)3(~3*qsX_O^qJzT1bWV4fYzm?r zSQ0<8I1O6?4}2yjRPal+(VqAXnv*&0pncpPd~l7)L$~&gzwb{u!z8&!sy4lqG&LiA zy` zZy4H-^BBx+8Vu8#sY{e@^EW#Q>pk%!l>?Qb+%V_35FD=70j>{lEu5(PO^i)yJLs}r zkESPE=g(=1v*jop`&gc(?XNMWBC zlepJ3queMFhM7tm)qD;`Czza~nx%(ECc`y}_x{o-ug##w)-}}5x~lHXYyC87^u7O}2TRj(cE1B64tc{z$#iOKdDJc$zJ)QmLVD;D=iBN-h*&Shs5N zdVc~v?la}gu!xFyqgw=@WlXL52?$x`ZuWQ7SG?!0s5c8Y(1b$=hb9`gW-rBz@zodB zGdbiKa@vzOk3Nx@-<*h`jqvM9b2L>}YR4WKwLpGO1t)sqZdfYB`+unGcW$8=~ZFG${hMfP3NPo;O9Y{Nk*1i`WN9Po(8u(i} zx;BP^HPt77e3eUg+5GvV^9f?`CtZMS_TnrW_mNdNArM>h!8KXWE0oq)iF~4~=FrY+ zgJbRnR?(yS=+kJ62MBc|R~UO2EtQ4zes;E$H7eh*mF;V;g!wC=JRy*HVRr8!?8je2-B?wg*w<04$A z>?$*l3m5MJ+<4X`d*8OX8GMEl$=cI#M8wao>>c?^od4+2gVp)AXKX!!(hNz#D=<+t z=a0d4U6fQayLmNSR?Hw%t~a3Og|eDqH4|OEZY9GYzQy)Z)MPsX+LXaBVtkt@kuCU& z0!}Ld5-QzfT?st~W@hEgoy~y#(lJl0Ja_HE^@Gy(>d-v>4G)fzf*f;U=xGZA(EAsi^1{AUF z1&sJ3V`CXZJ72k+Q93S%Uo`mKB-f5nl3KxPsoK5emB_@OLtEIWEg@rjHM~W^B3INR z<<5q!#G!3ZNQBWeH6W9m`w}$Ev#uOD*?Qc>WOt~Uz4a$3lq1(kr=etlR9Ut8UR~qY zchEf01E?<|Q_{+dby{qCGFd|(|2mbaX)pv~S`-sgC-nO_C5J^zPOT-0CtYA&ty87E z({;ppv~?Lz(7aBRJGX-7YH!<5R^hU;DXsh46C2$CA(a#R2qww^}?nxGE6x%Vvao4;krXb;+zBGup9`lRyreoGt1HRB_&%21{bYYcm#hS`v0#O8l{_ zsyw$z;fogl&X9)PX!4z_4Wo!_P+7$%QMex212cu@N<++|G19;<}@ zjz_hp0!S}R08VG&f~;rR%#(@U3r*&(I>2$jyzS=WH7GlFf#4=s2tBd8gL zgz}(WY(z-bUeV)en^w|#V-l&eLr`;A;Fg1_hnN&O3Ee<@6SU7`b9@{MFIZ88cjTwH z>Mcp0Sp~SeAo^~Zs*ScUFK=kk6vs?MU0aE(k|qpwO<1ClOTkzBA5&A45Vpas@O(u8 z5rY3`L)|_MKB8%i&F||OGuAyOi-)V}ZBYO$6zLNF zcMkRHzM-}k+G$$yB^VW6gA)FKRbTj@MZ>WG2#_Was?7HfX7W{=_=+|Ax(yIzzkBy? zXQv{7N=TRi6(bV#}_5+-3KFt$et-oRXMxVU}0QopOxhVedmg^U0LrMOn+?;`asa^DJNV1I*pQI3%h)RbHeN6=tnu=q3!xuR5~KV-0T+&0 zTQYgBjlPs_ztHgHg`o$O_fAg}dqdh@yaJ55=DBB{{hCp6)ac?@)r8_s;Nd=gG`)f3 zxVXFZ;~3>~kz2rh>~bfeM@RUQwiiI2+dDg9F)<*g?=5dYa4i6fxrQdJf6^>kUB|&R zf84JR1rQhB^VhFKX17u6o{IbEd_ZjD-At}F)8EsmsGqW;9B3{P)!$A4wZ3>k;CGq( z&><_MqN4Kb0o~~v&1&Ek!mI}~^rKjuH|2{|kI#ueehj>fB?ew`J2#S&=lHy({`oAj zp?63^F(M{vxg7x7b7>#n>%n~CG^L?hy?yQc031mF#)9}Aoa5}S=6P=n&}AFv4*)2; zIQ}1is|(xzZ*;2ve|rFmz96q6j$@tDui-uIb{(j5zWL@|3^m;Y)6XX`t$mUKxAK<6 z2MHEewz)3y8E~+3Fn%9GI3bxJK?lgN04%q6GhelzwjAC);mM{sYbtV3jWzuxtFU--K=`zm(u-=O@T<;y^jdy_yS8TwkzP|N%kJBAaX*yFBbf)Fd{TN4XW{bV#k012e$1xxB< zZaIgyP%*(M4z%P`v=rAuuq$4kkFy}rgla>!SERF(X0KD;d{8MCQ>~x!bjjs*D-Z60 zTX6e#)jL5<*Mds$T3F(@KGL4_WXS;xJ_?>eOd(RF2b;u1;IXKMa_?=0(Wcnr`ZtP= zc^ReL*3lZ){36cUsX6?h3%fx&B^kaoFdW3Lo~Q0^k-t!nEOT0D51qeJOxfelBXSus zPT_nhni5ASzzYPb){Bam^b`CKv+W4FnTgvxvafxXRNx8*Zh`(T{W9j;j7nE1P0#dw zP!xe6*-46#X64U;@Ql$v8HgwR@|}#A-p+9>9NNS8=pSjO3mni#l(#TaoHgqoRHwvq zQUn!qzrfqY;&T)NrHv!D%g2Mc{Zzlrj2Bo(ZUMfr$tpn@X^0QjuhvT;UT_d5r7tf# zYMT6XV){EaQAenmWntke(UY{*oBDPt6Rh_f7UeAp;Ft6-Ui7@_trh9TJP4x3YhU5H zIY4ec63Vd5Mk=Cr?0tKOC)i!tv`+P#GigP3SeCK*QaZ!dm*IhjcrhWQeYS>-qe_Uv zS?&Goi1#tFXV$TJ0a9#3{Vp-zGgS?dQ!YfV%F6e^=odRF2%xPT{$*ahBiLyn zlE8g7)45X{DnD9cAht|Oiv4mUfaxqd@|#QKu^=B*qd${_JMbB|FDLPhD3d&dSz7El@$HcE1sENiy}Yt=t4L#2 zIk@2U6-hlPz)-~NJ~BAEmBYEs$oHEI{n|NL^DV6ycVk^-tRo+mF=ya@oJj{|@z20s z_H(e%?$C>}vK%ptkr0(~%#A3$1B!5<;e?oyG;6A3`pc9;vGYxGSg= z%C|tmF)m-;DI&N2H1-S2Hwe!tf$dDAT5q5yBBjCnH}f^M;;5fDQ521=9U02zyE1(I zdbV-nq?&5QL(+sdQ8Y?I)RR04;mcVJdvK~fxO~To6uC@~v?(J+0H}kd#EUP#0vp3! z)R&h16fMCd=;88~JVAFkR(+hnF);Wu`L`_WOkrN;F(r1VnjB*Wh?mi5qv^v(~OPYEhML2MtOpH|38_hS{_7+SS* zlLfJm(UVc?M$D;y;*)Uw+^W`{Z$3B1aG=VE31211K6=K%)fN+mrwvs z2$%lmOzxo;F$9jBN8Q>njnJsX;VKex(E38lLEj|T0!4>SfCP@=5J;vu*BUNjCf}8# zTEo;ump~X^X0aOoLN&#H4Z>25(jE(PFH)FhzzwN@MXUkB;eBRyRMoG!DJo#uAZiQb z-+btyS6ogvbgps7HyEAHFi*;diMxSm2D@N_UZh?%5cy? zb*$}F9}Q8s@=3wZFls}FwR4r(tNoa@Ljp?W8A3?qM89|%vR?50tqg;!(dW_!IAe1r znT~{F2k{B$I}extGiPg%hkI8aD2ibSjHy@sN@r3sO zc{-1OPi0)>t&&;rZJK5TMN{#01QoFj?UaM!ZdC+Drd7*y$?4_n))otF21*|U=1Rt7 znlPgMIw_^dF<%;4ls-j645=Vb(8scnsQRUc;qZ_V%cXa=09&ko<68cz%NAP(a z46v}{<3O}-Wa-{+W07{ z4S5_3b7(oGKX!#@J{QLS|b^ki0Y)bCi*ZBeP@2% zZavEm*GhdN#C#AriFA%Z4<+3V+MrFwKW989v8nm_lkcbWGmokB{mV6HMMK?9UpXq#Ptch_yr zF{Xj&OasO_ z%TIH|&@W!u4hKc{R#fDnaDFfINQv=%R8TFy_|SFEq&_X1b&$&%o+63?ZMri%CeK~wv8`!o*y z_Y>)tlV>4o)U^0PzGi-ec)2%Q{8GS+Cjb@deu719O9paC86cgRHQv1wp(>^Hm^vrT zEe6>hzpZ{Y-6mDN%1%J|*sZ#^O$4k=Za$S3pi7(!_2i5@fQxUil>ljA}|uaZ~*uBrGpZPtVv zN6qxCdaZqVli1%}vdgCUp$;kI1s52&1v@r)1(ud_{oDiJ^SE`MaBx2I^GARA6~ZEU z3mTF|TmNlPK+^f!kr24=`XDg*cnCpRfHFxB!QUtu2fb}7CAhQbK{j-8rwfl-(mDR< z%t^X8o3j-9;v@?meEqu<4B-MB&_9Y&E^y+= z=A@%Nkvf}sUoOcB+36w){9W+qaY5RRKkpes%}wU|W=(;umBHFXOQQ14W+gNHrU#oM z_(arEFIoHh+I)HHM~rKKYew>Gc^>G(k}u;&d z)Fa?7T`SX&06S0ZtwX;P5n_<3ufoaLAZAYL#QIOX`Xa%3=R_)iB~=w;x~cR}f$M&D zdu?5JOd0X*g6BV09;;4!kBzAx&*Ag&E>gcCT#Uza+~5!1mb~}llNTWsh+_^=d!;#{_syj0@@id-kqPUxZ!iA%pXt|JocM1^(NEe5K$ir9HPmMV fXufu_@gXKE%RMY~-K_MyI8#whO}0$h?8E;84IY?z literal 0 HcmV?d00001 diff --git a/static/i18n.js b/static/i18n.js index b8241dc8..20dac616 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -77,6 +77,14 @@ const LOCALES = { mcp_tool_count: '{0} tools', mcp_enabled_yes: 'Enabled', mcp_enabled_no: 'Disabled', + mcp_tools_title: 'MCP Tools', + mcp_tools_desc: 'Search known tools across active MCP servers.', + mcp_tools_search_placeholder: 'Search tools by name, server, or description…', + mcp_tools_no_tools: 'No MCP tools are available from the active runtime inventory.', + mcp_tools_no_matches: 'No MCP tools match your search.', + mcp_tools_load_failed: 'Failed to load MCP tools.', + mcp_tools_schema_empty: 'No schema parameters.', + mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.', // PDF preview (#480) pdf_loading: 'Loading PDF {0}…', pdf_too_large: 'PDF too large for inline preview', @@ -1060,6 +1068,14 @@ const LOCALES = { mcp_tool_count: '{0} tools', mcp_enabled_yes: 'Enabled', mcp_enabled_no: 'Disabled', + mcp_tools_title: 'MCP Tools', + mcp_tools_desc: 'Search known tools across active MCP servers.', + mcp_tools_search_placeholder: 'Search tools by name, server, or description…', + mcp_tools_no_tools: 'No MCP tools are available from the active runtime inventory.', + mcp_tools_no_matches: 'No MCP tools match your search.', + mcp_tools_load_failed: 'Failed to load MCP tools.', + mcp_tools_schema_empty: 'No schema parameters.', + mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.', // PDF preview (#480) pdf_loading: 'PDF {0} を読み込み中…', pdf_too_large: 'PDF が大きすぎてインラインプレビューできません', @@ -2040,6 +2056,14 @@ const LOCALES = { mcp_tool_count: '{0} tools', mcp_enabled_yes: 'Enabled', mcp_enabled_no: 'Disabled', + mcp_tools_title: 'MCP Tools', + mcp_tools_desc: 'Search known tools across active MCP servers.', + mcp_tools_search_placeholder: 'Search tools by name, server, or description…', + mcp_tools_no_tools: 'No MCP tools are available from the active runtime inventory.', + mcp_tools_no_matches: 'No MCP tools match your search.', + mcp_tools_load_failed: 'Failed to load MCP tools.', + mcp_tools_schema_empty: 'No schema parameters.', + mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.', thinking: 'Думаю', expand_all: 'Развернуть всё', collapse_all: 'Свернуть всё', @@ -2953,6 +2977,14 @@ const LOCALES = { mcp_tool_count: '{0} tools', mcp_enabled_yes: 'Enabled', mcp_enabled_no: 'Disabled', + mcp_tools_title: 'MCP Tools', + mcp_tools_desc: 'Search known tools across active MCP servers.', + mcp_tools_search_placeholder: 'Search tools by name, server, or description…', + mcp_tools_no_tools: 'No MCP tools are available from the active runtime inventory.', + mcp_tools_no_matches: 'No MCP tools match your search.', + mcp_tools_load_failed: 'Failed to load MCP tools.', + mcp_tools_schema_empty: 'No schema parameters.', + mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.', thinking: 'Pensando', expand_all: 'Expandir todo', collapse_all: 'Contraer todo', @@ -3869,6 +3901,14 @@ const LOCALES = { mcp_tool_count: '{0} tools', mcp_enabled_yes: 'Enabled', mcp_enabled_no: 'Disabled', + mcp_tools_title: 'MCP Tools', + mcp_tools_desc: 'Search known tools across active MCP servers.', + mcp_tools_search_placeholder: 'Search tools by name, server, or description…', + mcp_tools_no_tools: 'No MCP tools are available from the active runtime inventory.', + mcp_tools_no_matches: 'No MCP tools match your search.', + mcp_tools_load_failed: 'Failed to load MCP tools.', + mcp_tools_schema_empty: 'No schema parameters.', + mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.', thinking: 'Nachdenken', expand_all: 'Alle ausklappen', collapse_all: 'Alle einklappen', @@ -4789,6 +4829,14 @@ const LOCALES = { mcp_tool_count: '{0} tools', mcp_enabled_yes: 'Enabled', mcp_enabled_no: 'Disabled', + mcp_tools_title: 'MCP Tools', + mcp_tools_desc: 'Search known tools across active MCP servers.', + mcp_tools_search_placeholder: 'Search tools by name, server, or description…', + mcp_tools_no_tools: 'No MCP tools are available from the active runtime inventory.', + mcp_tools_no_matches: 'No MCP tools match your search.', + mcp_tools_load_failed: 'Failed to load MCP tools.', + mcp_tools_schema_empty: 'No schema parameters.', + mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.', thinking: '\u601d\u8003\u8fc7\u7a0b', expand_all: '\u5168\u90e8\u5c55\u5f00', collapse_all: '\u5168\u90e8\u6298\u53e0', @@ -5704,6 +5752,14 @@ const LOCALES = { mcp_tool_count: '{0} tools', mcp_enabled_yes: 'Enabled', mcp_enabled_no: 'Disabled', + mcp_tools_title: 'MCP Tools', + mcp_tools_desc: 'Search known tools across active MCP servers.', + mcp_tools_search_placeholder: 'Search tools by name, server, or description…', + mcp_tools_no_tools: 'No MCP tools are available from the active runtime inventory.', + mcp_tools_no_matches: 'No MCP tools match your search.', + mcp_tools_load_failed: 'Failed to load MCP tools.', + mcp_tools_schema_empty: 'No schema parameters.', + mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.', thinking: '\u601d\u8003\u904e\u7a0b', expand_all: '\u5168\u90e8\u5c55\u958b', collapse_all: '\u5168\u90e8\u6298\u758a', @@ -7488,6 +7544,14 @@ const LOCALES = { mcp_tool_count: '{0} tools', mcp_enabled_yes: 'Enabled', mcp_enabled_no: 'Disabled', + mcp_tools_title: 'MCP Tools', + mcp_tools_desc: 'Search known tools across active MCP servers.', + mcp_tools_search_placeholder: 'Search tools by name, server, or description…', + mcp_tools_no_tools: 'No MCP tools are available from the active runtime inventory.', + mcp_tools_no_matches: 'No MCP tools match your search.', + mcp_tools_load_failed: 'Failed to load MCP tools.', + mcp_tools_schema_empty: 'No schema parameters.', + mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.', thinking: '생각 중', expand_all: '모두 펼치기', collapse_all: '모두 접기', diff --git a/static/index.html b/static/index.html index 64eb114a..8c217a4e 100644 --- a/static/index.html +++ b/static/index.html @@ -1029,6 +1029,14 @@

Server changes are read-only here for now. Edit config.yaml and restart Hermes for changes to take effect.
+ +
+ +
Search known tools across active MCP servers.
+ +
+
Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.
+
diff --git a/static/panels.js b/static/panels.js index d5974d1b..d3c45a1d 100644 --- a/static/panels.js +++ b/static/panels.js @@ -5101,6 +5101,60 @@ function loadMcpServers(){ }).join('')+toggleNote; }).catch(()=>{list.innerHTML=`
${esc(t('mcp_load_failed'))}
`}); } +let _mcpToolsCache=[]; +function _filterMcpToolsForSearch(tools, query){ + const q=(query||'').trim().toLowerCase(); + if(!q) return Array.isArray(tools)?tools:[]; + return (Array.isArray(tools)?tools:[]).filter(tool=>{ + const hay=[tool.name,tool.server,tool.description].map(v=>String(v||'').toLowerCase()).join(' '); + return hay.includes(q); + }); +} +function _mcpToolSchemaText(schemaSummary){ + if(!Array.isArray(schemaSummary)||!schemaSummary.length) return t('mcp_tools_schema_empty'); + return schemaSummary.map(p=>{ + const req=p.required?'*':''; + const desc=p.description?` — ${p.description}`:''; + return `${p.name}${req}: ${p.type||'unknown'}${desc}`; + }).join('\n'); +} +function _renderMcpTools(tools, query){ + const list=$('mcpToolList'); + if(!list) return; + const filtered=_filterMcpToolsForSearch(tools, query); + if(!filtered.length){ + const key=query?'mcp_tools_no_matches':'mcp_tools_no_tools'; + list.innerHTML=`
${esc(t(key))}
`; + return; + } + list.innerHTML=filtered.map(tool=>{ + const status=tool.status||'unknown'; + const statusBadge=`${esc(_mcpStatusLabel(status))}`; + const schemaText=_mcpToolSchemaText(tool.schema_summary); + return `
+
+ ${esc(tool.name)} + ${esc(tool.server||'unknown')} + ${statusBadge} +
+
${esc(tool.description||'')}
+
${esc(schemaText)}
+
`; + }).join(''); +} +function filterMcpTools(){ + const input=$('mcpToolSearch'); + _renderMcpTools(_mcpToolsCache,input?input.value:''); +} +function loadMcpTools(){ + const list=$('mcpToolList'); + if(!list) return; + list.innerHTML=`
${esc(t('loading'))}
`; + api('/api/mcp/tools').then(r=>{ + _mcpToolsCache=(r&&Array.isArray(r.tools))?r.tools:[]; + filterMcpTools(); + }).catch(()=>{list.innerHTML=`
${esc(t('mcp_tools_load_failed'))}
`}); +} function loadGatewayStatus(){ const card=$('gatewayStatusCard'); if(!card) return; @@ -5127,7 +5181,7 @@ function loadGatewayStatus(){ const _origSwitchSettings=switchSettingsSection; switchSettingsSection=function(name){ _origSwitchSettings(name); - if(name==='system'){loadMcpServers();loadGatewayStatus();} + if(name==='system'){loadMcpServers();loadMcpTools();loadGatewayStatus();} }; // ── Checkpoints / Rollback ────────────────────────────────────────────────── diff --git a/static/style.css b/static/style.css index 4e687c91..0233c165 100644 --- a/static/style.css +++ b/static/style.css @@ -2292,6 +2292,12 @@ main.main.showing-profiles > #mainProfiles{display:flex;} .mcp-status-invalid_config,.mcp-status-unknown{background:rgba(239,68,68,.12);color:#f87171;} .mcp-tool-count{color:var(--text);} .mcp-readonly-note,.mcp-restart-hint{margin-top:8px;color:var(--muted);font-size:11px;line-height:1.45;background:var(--code-bg);border:1px solid var(--border2);border-radius:6px;padding:8px 10px;} +.mcp-tool-search{width:100%;margin:0 0 8px 0;padding:8px 10px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:8px;font-size:12px;outline:none;} +.mcp-tool-search:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-bg-soft);} +.mcp-tool-row{display:flex;flex-direction:column;gap:5px;padding:9px 10px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;font-size:12px;background:var(--surface);} +.mcp-tool-name{font-weight:600;color:var(--text);overflow-wrap:anywhere;} +.mcp-tool-server{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);background:var(--code-bg);border:1px solid var(--border2);border-radius:999px;padding:2px 6px;} +.mcp-tool-schema{margin:2px 0 0 0;padding:7px 8px;white-space:pre-wrap;max-height:140px;overflow:auto;background:var(--code-bg);border:1px solid var(--border2);border-radius:6px;color:var(--muted);font-size:11px;line-height:1.45;} /* Picker grids (theme / skin / font-size): make the card chrome use tokens so all skins flip correctly. */ diff --git a/tests/test_issue697_mcp_tool_inventory.py b/tests/test_issue697_mcp_tool_inventory.py new file mode 100644 index 00000000..4dfd4ba1 --- /dev/null +++ b/tests/test_issue697_mcp_tool_inventory.py @@ -0,0 +1,136 @@ +"""Regression tests for issue #697 — searchable global MCP tool inventory.""" +import json +from unittest.mock import MagicMock, patch + +from api.routes import ( + _handle_mcp_tools_list, + _mcp_schema_summary, + _mcp_tool_summary, +) + + +def _make_handler(): + h = MagicMock() + h.path = "/api/mcp/tools" + h.command = "GET" + return h + + +def _json_payload(handler): + body = handler.wfile.write.call_args[0][0] + return json.loads(body.decode("utf-8")) + + +def _read(relative_path: str) -> str: + from pathlib import Path + + return (Path(__file__).resolve().parents[1] / relative_path).read_text(encoding="utf-8") + + +class TestMcpToolInventoryApi: + @patch("api.routes._mcp_runtime_status_by_name") + @patch("api.routes.get_config") + def test_endpoint_returns_sanitized_registered_mcp_tools(self, mock_cfg, mock_runtime): + mock_cfg.return_value = { + "mcp_servers": { + "web-reader": {"url": "http://localhost:3001/mcp", "headers": {"Authorization": "Bearer secret-token"}}, + "disabled": {"command": "disabled-cmd", "enabled": False}, + } + } + mock_runtime.return_value = { + "web-reader": { + "connected": True, + "tools": [ + { + "name": "mcp_web_reader_fetch_page", + "description": "Fetch a page without leaking Authorization: Bearer secret-token", + "parameters": { + "type": "object", + "properties": { + "url": {"type": "string", "description": "URL to fetch", "default": "https://token.example/?key=secret-token"}, + "limit": {"type": "integer", "description": "Maximum bytes"}, + }, + "required": ["url"], + }, + } + ], + }, + "disabled": {"connected": False, "tools": 0}, + } + h = _make_handler() + _handle_mcp_tools_list(h) + payload = _json_payload(h) + + assert payload["source"] == "mcp_runtime_status" + assert payload["total"] == 1 + assert payload["tools"][0]["name"] == "mcp_web_reader_fetch_page" + assert payload["tools"][0]["server"] == "web-reader" + assert payload["tools"][0]["status"] == "active" + assert payload["tools"][0]["active"] is True + assert payload["tools"][0]["enabled"] is True + assert payload["tools"][0]["schema_summary"] == [ + {"name": "url", "type": "string", "required": True, "description": "URL to fetch"}, + {"name": "limit", "type": "integer", "required": False, "description": "Maximum bytes"}, + ] + raw = json.dumps(payload) + assert "secret-token" not in raw + assert "default" not in raw + assert "Authorization" not in raw + + def test_schema_summary_uses_parameter_names_types_required_and_descriptions_only(self): + schema = { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search text", "examples": ["secret"]}, + "tags": {"type": "array", "items": {"type": "string"}, "description": "Tag filters"}, + }, + "required": ["query"], + } + assert _mcp_schema_summary(schema) == [ + {"name": "query", "type": "string", "required": True, "description": "Search text"}, + {"name": "tags", "type": "array", "required": False, "description": "Tag filters"}, + ] + + def test_tool_summary_rejects_non_dict_schema_and_redacts_description(self): + summary = _mcp_tool_summary( + "search", + {"description": "use API_KEY=super-secret", "parameters": "not-a-dict"}, + {"name": "search", "status": "configured", "enabled": True, "active": False}, + ) + assert summary["description"] != "use API_KEY=super-secret" + assert "super-secret" not in summary["description"] + assert summary["schema_summary"] == [] + + +class TestMcpToolInventoryUi: + def test_system_settings_contains_searchable_global_mcp_tool_section(self): + html = _read("static/index.html") + assert 'data-i18n="mcp_tools_title"' in html + assert 'id="mcpToolSearch"' in html + assert 'id="mcpToolList"' in html + assert 'oninput="filterMcpTools()"' in html + + def test_panels_js_loads_tools_and_filters_name_server_description(self): + js = _read("static/panels.js") + assert "function loadMcpTools" in js + assert "api('/api/mcp/tools')" in js + assert "function filterMcpTools" in js + assert "_filterMcpToolsForSearch" in js + assert "tool.name" in js + assert "tool.server" in js + assert "tool.description" in js + assert "mcp-tool-empty-state" in js + assert "mcp-tool-error-state" in js + + def test_mcp_tool_i18n_keys_are_present(self): + i18n = _read("static/i18n.js") + for key in [ + "mcp_tools_title", + "mcp_tools_desc", + "mcp_tools_search_placeholder", + "mcp_tools_no_tools", + "mcp_tools_no_matches", + "mcp_tools_load_failed", + "mcp_tools_schema_empty", + ]: + assert key in i18n