use std::{collections::HashMap, time::Duration};
use hydroxy_leptos::assistant_mcp::{AssistantMcpElicitation, AssistantMcpManager, ElicitField, McpServerInfo};
use leptos::prelude::*;
fn tools_for(id: &str) -> Vec<String> {
let names: &[&str] = match id {
"github" => &["search_repositories", "get_pull_request", "create_issue", "list_commits"],
"linear" => &["search_issues", "create_issue", "update_issue"],
"filesystem" => &["read_file", "list_directory"],
_ => &["list_items", "get_item", "search"],
};
names.iter().map(|n| (*n).to_string()).collect()
}
pub fn demo() -> AnyView {
let (servers, set_servers) = signal(vec![
McpServerInfo::connector("github", "GitHub", "Read repositories, pull requests and issues."),
McpServerInfo::connector("linear", "Linear", "Search and update issues in your workspace."),
McpServerInfo::connector("filesystem", "Filesystem", "Read files from a folder you choose."),
McpServerInfo::custom("notes", "Team notes", "https://notes.acme.dev/mcp"),
]);
let outcomes = RwSignal::new(HashMap::<String, Result<Vec<String>, String>>::new());
let (add_result, set_add_result) = signal(None::<Result<(), String>>);
// Linear fails its first attempt so Retry shows.
let linear_failed = StoredValue::new(false);
let next_custom = StoredValue::new(1_u32);
let (answers, set_answers) = signal(None::<Vec<(String, String)>>);
let on_answers = move |a: Vec<(String, String)>| set_answers.set(Some(a));
view! {
<div style="display: grid; gap: var(--space-6); max-width: 560px;">
<AssistantMcpManager
servers=servers
outcomes=outcomes
add_result=add_result
on_connect=move |id: String| {
outcomes.update(|m| {
m.remove(&id);
});
set_timeout(
move || {
let first_linear = id == "linear" && !linear_failed.get_value();
let result = if first_linear {
linear_failed.set_value(true);
Err("Authorization expired".to_string())
} else {
Ok(tools_for(&id))
};
outcomes.try_update(|m| m.insert(id, result));
},
Duration::from_millis(1200),
);
}
on_disconnect=move |id: String| {
outcomes.update(|m| {
m.remove(&id);
});
}
on_remove=move |id: String| {
set_servers.update(|v| v.retain(|s| s.id != id));
outcomes.update(|m| {
m.remove(&id);
});
}
on_add=move |(name, url): (String, String)| {
set_add_result.set(None);
set_timeout(
move || {
if url.contains("taken") {
set_add_result.try_set(Some(Err("A server at that address is already added.".to_string())));
return;
}
let n = next_custom.get_value();
next_custom.set_value(n + 1);
set_servers.try_update(|v| v.push(McpServerInfo::custom(format!("custom-{n}"), name, url)));
set_add_result.try_set(Some(Ok(())));
},
Duration::from_millis(900),
);
}
/>
<div style="display: grid; gap: var(--space-3);">
<AssistantMcpElicitation
title="Linear needs a few details"
message="To file the issue, Linear asks which repository it belongs to."
fields=vec![
ElicitField::text("repository", "Repository"),
ElicitField::choice("branch", "Branch", &["main", "develop", "release"]),
ElicitField::bool("private", "Keep the issue private"),
]
on_submit=on_answers
on_cancel=move |()| set_answers.set(None)
/>
{move || {
answers
.get()
.map(|a| {
let text = a.iter().map(|(k, v)| format!("{k}: {v}")).collect::<Vec<_>>().join(", ");
view! { <p style="margin: 0; font: var(--text-sm); color: var(--text-2);">{text}</p> }
})
}}
</div>
</div>
}
.into_any()
}
use std::collections::HashMap;
use dioxus::prelude::*;
use hydroxy_dioxus::assistant_mcp::{AssistantMcpElicitation, AssistantMcpManager, ElicitField, McpServerInfo};
async fn sleep(ms: u32) {
let _ = document::eval(&format!("await new Promise((r) => setTimeout(r, {ms}));")).await;
}
fn tools_for(id: &str) -> Vec<String> {
let names: &[&str] = match id {
"github" => &["search_repositories", "get_pull_request", "create_issue", "list_commits"],
"linear" => &["search_issues", "create_issue", "update_issue"],
"filesystem" => &["read_file", "list_directory"],
_ => &["list_items", "get_item", "search"],
};
names.iter().map(|n| (*n).to_string()).collect()
}
#[component]
pub fn AssistantMcpDemo() -> Element {
let mut servers = use_signal(|| {
vec![
McpServerInfo::connector("github", "GitHub", "Read repositories, pull requests and issues."),
McpServerInfo::connector("linear", "Linear", "Search and update issues in your workspace."),
McpServerInfo::connector("filesystem", "Filesystem", "Read files from a folder you choose."),
McpServerInfo::custom("notes", "Team notes", "https://notes.acme.dev/mcp"),
]
});
let mut outcomes = use_signal(HashMap::<String, Result<Vec<String>, String>>::new);
let mut add_result = use_signal(|| None::<Result<(), String>>);
// Linear fails its first attempt so Retry shows.
let mut linear_failed = use_signal(|| false);
let mut next_custom = use_signal(|| 1_u32);
let mut answers = use_signal(|| None::<Vec<(String, String)>>);
rsx! {
div { style: "display: grid; gap: var(--space-6); max-width: 560px;",
AssistantMcpManager {
servers,
outcomes,
add_result,
on_connect: move |id: String| {
outcomes.write().remove(&id);
spawn(async move {
sleep(1200).await;
let result = if id == "linear" && !linear_failed() {
linear_failed.set(true);
Err("Authorization expired".to_string())
} else {
Ok(tools_for(&id))
};
outcomes.write().insert(id, result);
});
},
on_disconnect: move |id: String| {
outcomes.write().remove(&id);
},
on_remove: move |id: String| {
servers.write().retain(|s| s.id != id);
outcomes.write().remove(&id);
},
on_add: move |(name, url): (String, String)| {
add_result.set(None);
spawn(async move {
sleep(900).await;
if url.contains("taken") {
add_result.set(Some(Err("A server at that address is already added.".to_string())));
return;
}
let n = next_custom();
next_custom.set(n + 1);
servers.write().push(McpServerInfo::custom(format!("custom-{n}"), name, url));
add_result.set(Some(Ok(())));
});
},
}
div { style: "display: grid; gap: var(--space-3);",
AssistantMcpElicitation {
title: "Linear needs a few details",
message: "To file the issue, Linear asks which repository it belongs to.",
fields: vec![
ElicitField::text("repository", "Repository"),
ElicitField::choice("branch", "Branch", &["main", "develop", "release"]),
ElicitField::bool("private", "Keep the issue private"),
],
on_submit: move |a: Vec<(String, String)>| answers.set(Some(a)),
on_cancel: move |()| answers.set(None),
}
if let Some(a) = answers() {
p { style: "margin: 0; font: var(--text-sm); color: var(--text-2);",
{a.iter().map(|(k, v)| format!("{k}: {v}")).collect::<Vec<_>>().join(", ")}
}
}
}
}
}
}