From 57e4a7b71af9f163c2fac883ad1c6a63294df872 Mon Sep 17 00:00:00 2001 From: idling11 <8055620+idling11@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:35:19 -0700 Subject: [PATCH] feat(hf): harvest Hugging Face MCP helpers Add /hf and /huggingface command routing for Hugging Face MCP setup/status plus a concepts explainer for provider, MCP, and Hub workflows. Document the settings-generated Hugging Face MCP configuration path and keep the slice offline: no Hub search command, no direct Hugging Face HTTP requests, and no custom URL encoding. Refs #2709 Harvested from PR #2782 by @idling11 --- CHANGELOG.md | 5 + crates/tui/CHANGELOG.md | 5 + crates/tui/src/commands/hf.rs | 249 +++++++++++++++++++++++++++++++++ crates/tui/src/commands/mod.rs | 26 ++++ crates/tui/src/localization.rs | 8 ++ docs/MCP.md | 50 +++++++ docs/PROVIDERS.md | 18 +++ 7 files changed, 361 insertions(+) create mode 100644 crates/tui/src/commands/hf.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 201e648b..90611c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 helper for future fallback routing. This preserves the requested contract without enabling silent runtime provider switches yet (#2574, #2777). Thanks @hsdbeebou for the request and @idling11 for the data-model draft. +- Added `/hf` with `/huggingface` alias for Hugging Face MCP status/setup + helpers and `/hf concepts` provider/MCP/Hub guidance. The helper points users + to Hugging Face's settings-generated MCP configuration and intentionally does + not include Hub search, direct Hugging Face HTTP requests, or upload behavior + (#2709, #2782). Thanks @idling11 for the original Hugging Face MCP draft. - Added `/sidebar` so users can toggle, show, hide, and optionally persist the TUI sidebar from the command line instead of relying on copy-hostile sidebar state during long transcript work (#2766, #2788). Thanks @mo-vic for the diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 201e648b..90611c2b 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -57,6 +57,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 helper for future fallback routing. This preserves the requested contract without enabling silent runtime provider switches yet (#2574, #2777). Thanks @hsdbeebou for the request and @idling11 for the data-model draft. +- Added `/hf` with `/huggingface` alias for Hugging Face MCP status/setup + helpers and `/hf concepts` provider/MCP/Hub guidance. The helper points users + to Hugging Face's settings-generated MCP configuration and intentionally does + not include Hub search, direct Hugging Face HTTP requests, or upload behavior + (#2709, #2782). Thanks @idling11 for the original Hugging Face MCP draft. - Added `/sidebar` so users can toggle, show, hide, and optionally persist the TUI sidebar from the command line instead of relying on copy-hostile sidebar state during long transcript work (#2766, #2788). Thanks @mo-vic for the diff --git a/crates/tui/src/commands/hf.rs b/crates/tui/src/commands/hf.rs new file mode 100644 index 00000000..0d2a7230 --- /dev/null +++ b/crates/tui/src/commands/hf.rs @@ -0,0 +1,249 @@ +//! `/hf` - Hugging Face MCP and provider concept helpers. + +use crate::mcp::{McpConfig, McpServerConfig}; +use crate::tui::app::App; + +use super::CommandResult; + +const HF_MCP_SETTINGS_URL: &str = "https://huggingface.co/settings/mcp"; +const HF_MCP_DOCS_URL: &str = "https://huggingface.co/docs/hub/hf-mcp-server"; +const HF_MCP_SERVER_URL: &str = "https://huggingface.co/mcp"; + +const HF_MCP_CONFIG_SKELETON: &str = r#"{ + "servers": { + "huggingface": { + "url": "https://huggingface.co/mcp", + "headers": { + "Authorization": "Bearer ${HF_TOKEN}" + } + } + } +}"#; + +/// Explainer shown by `/hf concepts`. +const HF_CONCEPTS: &str = "\ +CodeWhale has three distinct Hugging Face surfaces: + +1. Hugging Face provider route - chat inference + Switch the active LLM backend to Hugging Face Inference Providers. + Use: /provider huggingface + Config: provider = \"huggingface\" or [providers.huggingface] + Auth: HF_TOKEN or HUGGINGFACE_API_KEY + +2. Hugging Face MCP - Hub, docs, datasets, Spaces, and community tools + Connect CodeWhale to Hugging Face's MCP server through mcp.json. + Use: /hf mcp status or /hf mcp setup + Then: /mcp validate or restart CodeWhale so model-visible tools reload. + +3. Hugging Face Hub workflows - publish, upload, or manage repositories + Use explicit Hub tooling such as huggingface_hub or git-based flows. + CodeWhale does not upload to the Hub through /hf."; + +pub fn hf(app: &mut App, args: Option<&str>) -> CommandResult { + let raw = args.unwrap_or("").trim(); + if raw.is_empty() { + return usage(); + } + + let mut parts = raw.split_whitespace(); + let subcommand = parts.next().unwrap_or_default().to_ascii_lowercase(); + match subcommand.as_str() { + "mcp" => hf_mcp(app, parts.next()), + "concepts" | "explain" => CommandResult::message(HF_CONCEPTS), + _ => CommandResult::error(format!( + "Unknown /hf subcommand: {subcommand}. Use /hf mcp or /hf concepts." + )), + } +} + +fn usage() -> CommandResult { + CommandResult::message( + "Usage: /hf mcp \n\ + /hf concepts\n\n\ + Hugging Face MCP settings: https://huggingface.co/settings/mcp", + ) +} + +fn hf_mcp(app: &mut App, action: Option<&str>) -> CommandResult { + match action.unwrap_or("status").to_ascii_lowercase().as_str() { + "status" => hf_mcp_status(app), + "setup" => CommandResult::message(hf_mcp_setup_message(app)), + other => CommandResult::error(format!( + "Unknown /hf mcp subcommand: {other}. Use status or setup." + )), + } +} + +fn hf_mcp_status(app: &App) -> CommandResult { + match crate::mcp::load_config(&app.mcp_config_path) { + Ok(config) => { + if let Some(server_name) = configured_hf_mcp_server(&config) { + CommandResult::message(format!( + "Hugging Face MCP appears configured as `{server_name}` in {}.\n\ + Run /mcp validate or restart CodeWhale if tools are not visible yet.", + app.mcp_config_path.display() + )) + } else { + CommandResult::message(format!( + "Hugging Face MCP is not configured in {}.\n\ + Run /hf mcp setup for the settings-generated config workflow.", + app.mcp_config_path.display() + )) + } + } + Err(err) => CommandResult::error(format!( + "Could not read MCP config {}: {err}", + app.mcp_config_path.display() + )), + } +} + +fn hf_mcp_setup_message(app: &App) -> String { + format!( + "Use Hugging Face's settings-generated MCP configuration when available:\n\ + 1. Open {HF_MCP_SETTINGS_URL} while signed in.\n\ + 2. Choose your MCP client and copy the generated configuration snippet.\n\ + 3. Paste the Hugging Face server entry into {}.\n\ + 4. Restart CodeWhale, or run /mcp reload for the TUI manager snapshot.\n\n\ + CodeWhale-compatible placeholder shape:\n\n\ + ```json\n{HF_MCP_CONFIG_SKELETON}\n```\n\n\ + The placeholder is intentionally not runnable until your private MCP config has a real token value. \ + Do not commit real Hugging Face tokens.\n\n\ + Docs: {HF_MCP_DOCS_URL}\n\ + Server: {HF_MCP_SERVER_URL}", + app.mcp_config_path.display() + ) +} + +fn configured_hf_mcp_server(config: &McpConfig) -> Option<&str> { + config + .servers + .iter() + .find(|(name, server)| looks_like_hf_mcp_server(name, server)) + .map(|(name, _)| name.as_str()) +} + +fn looks_like_hf_mcp_server(name: &str, server: &McpServerConfig) -> bool { + let compact_name: String = name + .chars() + .filter(|ch| ch.is_ascii_alphanumeric()) + .flat_map(|ch| ch.to_lowercase()) + .collect(); + if matches!( + compact_name.as_str(), + "huggingface" | "huggingfacemcp" | "hfmcp" | "hfmcpserver" + ) { + return true; + } + + server.url.as_deref().is_some_and(|url| { + let url = url.to_ascii_lowercase(); + url.contains("huggingface.co/mcp") || url.contains("huggingface.co/api/mcp") + }) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use crate::config::Config; + use crate::tui::app::TuiOptions; + use tempfile::tempdir; + + use super::*; + + fn app_with_mcp_path(mcp_config_path: PathBuf) -> App { + App::new( + TuiOptions { + model: "deepseek-v4-pro".to_string(), + workspace: PathBuf::from("."), + config_path: None, + config_profile: None, + allow_shell: false, + use_alt_screen: false, + use_mouse_capture: false, + use_bracketed_paste: true, + max_subagents: 2, + skills_dir: PathBuf::from("."), + memory_path: PathBuf::from("memory.md"), + notes_path: PathBuf::from("notes.txt"), + mcp_config_path, + use_memory: false, + start_in_agent_mode: false, + skip_onboarding: true, + yolo: false, + resume_session_id: None, + initial_input: None, + }, + &Config::default(), + ) + } + + #[test] + fn hf_mcp_config_skeleton_keeps_token_placeholder_only() { + assert!(HF_MCP_CONFIG_SKELETON.contains("${HF_TOKEN}")); + assert!(!HF_MCP_CONFIG_SKELETON.contains("hf_")); + assert!(!HF_MCP_CONFIG_SKELETON.contains("Bearer hf_")); + serde_json::from_str::(HF_MCP_CONFIG_SKELETON) + .expect("skeleton should be valid JSON"); + } + + #[test] + fn hf_concepts_explains_provider_mcp_and_hub_surfaces() { + assert!(HF_CONCEPTS.contains("provider route")); + assert!(HF_CONCEPTS.contains("Hugging Face MCP")); + assert!(HF_CONCEPTS.contains("Hub workflows")); + assert!(HF_CONCEPTS.contains("/provider huggingface")); + assert!(HF_CONCEPTS.contains("/hf mcp")); + } + + #[test] + fn hf_mcp_status_detects_settings_named_server() { + let dir = tempdir().expect("tempdir"); + let path = dir.path().join("mcp.json"); + fs::write( + &path, + r#"{"mcpServers":{"hf-mcp-server":{"url":"https://huggingface.co/mcp"}}}"#, + ) + .expect("write mcp config"); + let app = app_with_mcp_path(path); + + let result = hf_mcp_status(&app); + + assert!(!result.is_error); + let message = result.message.expect("status message"); + assert!(message.contains("appears configured")); + assert!(message.contains("hf-mcp-server")); + } + + #[test] + fn hf_mcp_status_reports_missing_server_without_network() { + let dir = tempdir().expect("tempdir"); + let path = dir.path().join("mcp.json"); + fs::write(&path, r#"{"servers":{"local":{"command":"node"}}}"#).expect("write mcp config"); + let app = app_with_mcp_path(path); + + let result = hf_mcp_status(&app); + + assert!(!result.is_error); + assert!( + result + .message + .as_deref() + .unwrap_or_default() + .contains("not configured") + ); + } + + #[test] + fn hf_usage_and_setup_do_not_advertise_hub_search() { + let app = app_with_mcp_path(PathBuf::from("mcp.json")); + let usage = usage().message.expect("usage"); + let setup = hf_mcp_setup_message(&app); + + assert!(!usage.contains("/hf search")); + assert!(!setup.contains("/hf search")); + assert!(setup.contains(HF_MCP_SETTINGS_URL)); + } +} diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index dfd337fe..5cfbbdee 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -12,6 +12,7 @@ mod core; mod debug; mod feedback; mod goal; +mod hf; mod hooks; mod init; mod jobs; @@ -224,6 +225,12 @@ pub const COMMANDS: &[CommandInfo] = &[ usage: "/feedback [bug|feature|security]", description_id: MessageId::CmdFeedbackDescription, }, + CommandInfo { + name: "hf", + aliases: &["huggingface"], + usage: "/hf [mcp |concepts]", + description_id: MessageId::CmdHfDescription, + }, CommandInfo { name: "home", aliases: &["stats", "overview", "zhuye", "shouye"], @@ -577,6 +584,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult { "agent" | "daili" => agent(app, arg), "links" | "dashboard" | "api" | "lianjie" => core::deepseek_links(app), "feedback" => feedback::feedback(app, arg), + "hf" | "huggingface" => hf::hf(app, arg), "home" | "stats" | "overview" | "zhuye" | "shouye" => core::home_dashboard(app), "workspace" | "cwd" => core::workspace_switch(app, arg), "note" => note::note(app, arg), @@ -1169,6 +1177,13 @@ mod tests { .contains("right sidebar") ); assert!(COMMANDS.iter().any(|cmd| cmd.name == "links")); + let hf = COMMANDS + .iter() + .find(|cmd| cmd.name == "hf") + .expect("hf command should exist"); + assert_eq!(hf.aliases, &["huggingface"]); + assert_eq!(hf.description_id, MessageId::CmdHfDescription); + assert!(hf.description_for(Locale::En).contains("Hugging Face")); assert!(COMMANDS.iter().any(|cmd| cmd.name == "memory")); assert!(!COMMANDS.iter().any(|cmd| cmd.name == "set")); assert!(!COMMANDS.iter().any(|cmd| cmd.name == "deepseek")); @@ -1183,6 +1198,17 @@ mod tests { assert_eq!(links.aliases, &["dashboard", "api", "lianjie"]); } + #[test] + fn hf_alias_dispatches_to_concepts_helper() { + let mut app = create_test_app(); + let result = execute("/huggingface concepts", &mut app); + assert!(!result.is_error); + let message = result.message.expect("concepts message"); + assert!(message.contains("Hugging Face provider route")); + assert!(message.contains("Hugging Face MCP")); + assert!(message.contains("Hub workflows")); + } + #[test] fn rlm_slash_command_routes_to_persistent_tool_instruction() { let mut app = create_test_app(); diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index 72508193..28132c50 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -268,6 +268,7 @@ pub enum MessageId { CmdExitDescription, CmdExportDescription, CmdFeedbackDescription, + CmdHfDescription, CmdHelpDescription, CmdHomeDescription, CmdHooksDescription, @@ -595,6 +596,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::CmdExitDescription, MessageId::CmdExportDescription, MessageId::CmdFeedbackDescription, + MessageId::CmdHfDescription, MessageId::CmdHelpDescription, MessageId::CmdHomeDescription, MessageId::CmdHooksDescription, @@ -1123,6 +1125,7 @@ fn english(id: MessageId) -> &'static str { MessageId::CmdExitDescription => "Exit the application", MessageId::CmdExportDescription => "Export conversation to markdown", MessageId::CmdFeedbackDescription => "Generate a GitHub feedback URL", + MessageId::CmdHfDescription => "Inspect Hugging Face MCP setup and concepts", MessageId::CmdHelpDescription => "Show help information", MessageId::CmdHomeDescription => "Show home dashboard with stats and quick actions", MessageId::CmdHooksDescription => "List configured lifecycle hooks (read-only)", @@ -1590,6 +1593,7 @@ fn vietnamese(id: MessageId) -> Option<&'static str> { MessageId::CmdExitDescription => "Thoát ứng dụng", MessageId::CmdExportDescription => "Xuất cuộc trò chuyện sang định dạng Markdown", MessageId::CmdFeedbackDescription => "Tạo một URL để gửi phản hồi trên GitHub", + MessageId::CmdHfDescription => "Kiểm tra thiết lập và khái niệm Hugging Face MCP", MessageId::CmdHelpDescription => "Hiển thị thông tin trợ giúp", MessageId::CmdHomeDescription => { "Hiển thị bảng điều khiển trang chủ với số liệu thống kê và hành động nhanh" @@ -2142,6 +2146,7 @@ fn japanese(id: MessageId) -> Option<&'static str> { MessageId::CmdExitDescription => "アプリを終了", MessageId::CmdExportDescription => "会話を Markdown にエクスポート", MessageId::CmdFeedbackDescription => "GitHub フィードバック URL を生成", + MessageId::CmdHfDescription => "Hugging Face MCP の設定と概念を確認", MessageId::CmdHelpDescription => "ヘルプを表示", MessageId::CmdHomeDescription => "統計とクイックアクション付きのホームダッシュボードを表示", MessageId::CmdHooksDescription => { @@ -2593,6 +2598,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> { MessageId::CmdExitDescription => "退出应用", MessageId::CmdExportDescription => "将对话导出为 Markdown", MessageId::CmdFeedbackDescription => "生成 GitHub 反馈链接", + MessageId::CmdHfDescription => "检查 Hugging Face MCP 设置和概念", MessageId::CmdHelpDescription => "显示帮助信息", MessageId::CmdHomeDescription => "显示主页面板,含统计与快捷操作", MessageId::CmdHooksDescription => "列出已配置的生命周期钩子(只读)", @@ -3006,6 +3012,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> { MessageId::CmdExitDescription => "Sair do aplicativo", MessageId::CmdExportDescription => "Exportar a conversa para markdown", MessageId::CmdFeedbackDescription => "Gerar uma URL de feedback no GitHub", + MessageId::CmdHfDescription => "Inspecionar configuracao e conceitos do Hugging Face MCP", MessageId::CmdHelpDescription => "Exibir informações de ajuda", MessageId::CmdHomeDescription => "Exibir o painel inicial com estatísticas e ações rápidas", MessageId::CmdHooksDescription => { @@ -3491,6 +3498,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> { MessageId::CmdExitDescription => "Salir de la aplicación", MessageId::CmdExportDescription => "Exportar la conversación a markdown", MessageId::CmdFeedbackDescription => "Generar una URL de feedback en GitHub", + MessageId::CmdHfDescription => "Inspeccionar configuracion y conceptos de Hugging Face MCP", MessageId::CmdHelpDescription => "Mostrar información de ayuda", MessageId::CmdHomeDescription => { "Mostrar el panel inicial con estadísticas y acciones rápidas" diff --git a/docs/MCP.md b/docs/MCP.md index 3d8b58a5..c48e4615 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -61,6 +61,56 @@ manager snapshot. Config edits made from the TUI are written immediately, but the model-visible MCP tool pool is not hot-reloaded; the manager marks this as restart-required until the TUI is restarted. +## Hugging Face MCP + +Hugging Face provides a hosted MCP server for Hub resources, documentation, +datasets, Spaces, and community tools. CodeWhale does not call Hugging Face's +Hub HTTP APIs from `/hf`; it only helps you inspect and set up the MCP config +that the regular MCP manager will load. + +The recommended setup path is Hugging Face's settings-generated configuration: + +1. Visit while signed in. +2. Choose the MCP client closest to your CodeWhale config shape and copy the + generated server snippet. +3. Paste the Hugging Face server entry into your resolved MCP config file. +4. Restart CodeWhale, or run `/mcp reload` for the manager snapshot and restart + if the model-visible tool pool still needs to rebuild. + +CodeWhale reads both `servers` and `mcpServers`, so settings-generated snippets +can be adapted without changing the rest of the MCP file. A placeholder-only +shape looks like this: + +```json +{ + "servers": { + "huggingface": { + "url": "https://huggingface.co/mcp", + "headers": { + "Authorization": "Bearer ${HF_TOKEN}" + } + } + } +} +``` + +The placeholder above is not a runnable secret. Use the settings-generated +value in your private MCP config and never commit real Hugging Face tokens. + +Interactive helpers: + +```text +/hf mcp status +/hf mcp setup +/hf concepts +``` + +`/hf mcp status` checks the configured MCP file for common Hugging Face server +names or Hugging Face MCP URLs. `/hf concepts` explains the difference between +the Hugging Face provider route, Hugging Face MCP, and explicit Hub workflows. + +Official docs: + ## Config File Location Default path: diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 9b319e8a..8dcf2c54 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -130,6 +130,24 @@ endpoint. | `ollama` | `[providers.ollama]` | Optional `OLLAMA_API_KEY` | `OLLAMA_BASE_URL`; default `http://localhost:11434/v1` | `deepseek-coder:1.3b`; provider-hinted custom tags pass through | Self-hosted Ollama OpenAI-compatible route. Localhost deployments commonly omit auth. `OLLAMA_MODEL` is accepted. | | `huggingface` | `[providers.huggingface]` | `HUGGINGFACE_API_KEY`, `HF_TOKEN` | `HUGGINGFACE_BASE_URL`; default `https://router.huggingface.co/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Hugging Face Inference Providers OpenAI-compatible route. Org-prefixed model IDs pass through. | +### Hugging Face Provider vs MCP vs Hub + +CodeWhale's `huggingface` provider ID is only the OpenAI-compatible chat +inference route through Hugging Face Inference Providers. It is selected with +`/provider huggingface`, `CODEWHALE_PROVIDER=huggingface`, or +`provider = "huggingface"`. + +Hugging Face MCP is a separate external-tool route. Configure it through the +MCP config described in `docs/MCP.md`, preferably using the settings-generated +snippet from . In the TUI, `/hf mcp status` +checks whether the Hugging Face MCP server appears in the resolved MCP config, +`/hf mcp setup` prints the settings workflow and a placeholder-only shape, and +`/hf concepts` explains the provider/MCP/Hub distinction. + +Hub publishing or repository management remains explicit user action through +Hub-native tooling such as `huggingface_hub` or git. The `/hf` helper does not +upload to Hugging Face and does not perform direct Hugging Face Hub HTTP search. + ### Xiaomi MiMo Notes `xiaomi-mimo` defaults to `mimo-v2.5-pro` for long-context reasoning and coding