Merge pull request #2802 from Hmbown/codex/harvest-2782-hf-mcp-concepts

feat(hf): add Hugging Face MCP helpers
This commit is contained in:
Hunter Bown
2026-06-05 09:37:56 -07:00
committed by GitHub
7 changed files with 361 additions and 0 deletions
+5
View File
@@ -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
+5
View File
@@ -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
+249
View File
@@ -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 <status|setup> or /hf concepts."
)),
}
}
fn usage() -> CommandResult {
CommandResult::message(
"Usage: /hf mcp <status|setup>\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::<serde_json::Value>(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));
}
}
+26
View File
@@ -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 <status|setup>|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();
+8
View File
@@ -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"
+50
View File
@@ -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 <https://huggingface.co/settings/mcp> 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: <https://huggingface.co/docs/hub/hf-mcp-server>
## Config File Location
Default path:
+18
View File
@@ -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 <https://huggingface.co/settings/mcp>. 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