feat(dashboard): surface full per-MCP catalog detail; fix pip-install doc (#48520)
The dashboard MCP catalog only showed name/description/transport and a non-clickable source. Users couldn't see what an entry connects to or runs before installing — the exact detail the docs trust model tells them to vet. - /api/mcp/catalog now returns transport target (url, or command+args), auth_type, git install source/ref + bootstrap commands, default-enabled tool hint, and post-install guidance per entry. - McpPage renders the endpoint URL (http) or command+args (stdio), the git install source/ref, a collapsible bootstrap-commands list, setup notes, and the source as a clickable link when it's a URL. - Docs: drop the 'uv pip install -e .[mcp]' quick-start step (Hermes does not support pip installs; MCP ships with the standard install) and note the dashboard now surfaces this detail. - Strengthen the catalog endpoint test to assert the new inspection fields.
This commit is contained in:
parent
4af16b5da2
commit
c37fdec2d9
@ -7593,17 +7593,35 @@ async def list_mcp_catalog(profile: Optional[str] = None):
|
|||||||
}
|
}
|
||||||
for entry in catalog_entries:
|
for entry in catalog_entries:
|
||||||
auth = entry.auth
|
auth = entry.auth
|
||||||
|
transport = entry.transport
|
||||||
|
install = entry.install
|
||||||
entries.append({
|
entries.append({
|
||||||
"name": entry.name,
|
"name": entry.name,
|
||||||
"description": entry.description,
|
"description": entry.description,
|
||||||
"source": entry.source,
|
"source": entry.source,
|
||||||
"transport": entry.transport.type,
|
"transport": transport.type,
|
||||||
"auth_type": getattr(auth, "type", "none"),
|
"auth_type": getattr(auth, "type", "none"),
|
||||||
# Env vars the user must supply (names + prompts only, never values).
|
# Env vars the user must supply (names + prompts only, never values).
|
||||||
"required_env": [
|
"required_env": [
|
||||||
{"name": e.name, "prompt": e.prompt, "required": e.required}
|
{"name": e.name, "prompt": e.prompt, "required": e.required}
|
||||||
for e in getattr(auth, "env", []) or []
|
for e in getattr(auth, "env", []) or []
|
||||||
],
|
],
|
||||||
|
# Transport details so the UI can show exactly what connects/runs.
|
||||||
|
# The trust model (docs: user-guide/features/mcp) tells users to
|
||||||
|
# inspect command/args/url and the install bootstrap before
|
||||||
|
# installing — surface them rather than hiding them in the repo.
|
||||||
|
"command": transport.command,
|
||||||
|
"args": list(transport.args or []),
|
||||||
|
"url": transport.url,
|
||||||
|
# Git bootstrap (present only for entries that clone + build).
|
||||||
|
"install_url": install.url if install else None,
|
||||||
|
"install_ref": install.ref if install else None,
|
||||||
|
"bootstrap": list(install.bootstrap) if install else [],
|
||||||
|
# Default tool pre-selection hint and post-install guidance.
|
||||||
|
"default_enabled": list(entry.tools.default_enabled)
|
||||||
|
if entry.tools.default_enabled is not None
|
||||||
|
else None,
|
||||||
|
"post_install": entry.post_install or "",
|
||||||
"needs_install": entry.install is not None,
|
"needs_install": entry.install is not None,
|
||||||
"installed": installed_state.get(entry.name, (False, False))[0],
|
"installed": installed_state.get(entry.name, (False, False))[0],
|
||||||
"enabled": installed_state.get(entry.name, (False, False))[1],
|
"enabled": installed_state.get(entry.name, (False, False))[1],
|
||||||
|
|||||||
@ -94,9 +94,31 @@ class TestMcpEndpoints:
|
|||||||
body = r.json()
|
body = r.json()
|
||||||
assert "entries" in body and "diagnostics" in body
|
assert "entries" in body and "diagnostics" in body
|
||||||
# The shipped optional-mcps/ catalog has at least one entry; each must
|
# The shipped optional-mcps/ catalog has at least one entry; each must
|
||||||
# carry the install/enabled status fields the UI relies on.
|
# carry the install/enabled status fields plus the inspection detail
|
||||||
|
# the dashboard renders (transport target, install source, guidance) so
|
||||||
|
# users can vet an entry before installing.
|
||||||
for e in body["entries"]:
|
for e in body["entries"]:
|
||||||
assert {"name", "transport", "installed", "enabled", "needs_install"} <= set(e)
|
assert {
|
||||||
|
"name",
|
||||||
|
"transport",
|
||||||
|
"auth_type",
|
||||||
|
"installed",
|
||||||
|
"enabled",
|
||||||
|
"needs_install",
|
||||||
|
"command",
|
||||||
|
"args",
|
||||||
|
"url",
|
||||||
|
"install_url",
|
||||||
|
"install_ref",
|
||||||
|
"bootstrap",
|
||||||
|
"default_enabled",
|
||||||
|
"post_install",
|
||||||
|
} <= set(e)
|
||||||
|
# http entries expose a url; stdio entries expose a command.
|
||||||
|
if e["transport"] == "http":
|
||||||
|
assert e["url"]
|
||||||
|
elif e["transport"] == "stdio":
|
||||||
|
assert e["command"]
|
||||||
|
|
||||||
def test_catalog_install_unknown_404(self):
|
def test_catalog_install_unknown_404(self):
|
||||||
r = self.client.post("/api/mcp/catalog/install", json={"name": "no-such-mcp-xyz"})
|
r = self.client.post("/api/mcp/catalog/install", json={"name": "no-such-mcp-xyz"})
|
||||||
|
|||||||
@ -1301,6 +1301,17 @@ export interface McpCatalogEntry {
|
|||||||
transport: "http" | "stdio";
|
transport: "http" | "stdio";
|
||||||
auth_type: "api_key" | "oauth" | "none";
|
auth_type: "api_key" | "oauth" | "none";
|
||||||
required_env: Array<{ name: string; prompt: string; required: boolean }>;
|
required_env: Array<{ name: string; prompt: string; required: boolean }>;
|
||||||
|
// Transport details — what actually connects (http) or runs (stdio).
|
||||||
|
command: string | null;
|
||||||
|
args: string[];
|
||||||
|
url: string | null;
|
||||||
|
// Git bootstrap (only set for entries that clone + build locally).
|
||||||
|
install_url: string | null;
|
||||||
|
install_ref: string | null;
|
||||||
|
bootstrap: string[];
|
||||||
|
// Default tool pre-selection (null = all tools pre-checked) + guidance text.
|
||||||
|
default_enabled: string[] | null;
|
||||||
|
post_install: string;
|
||||||
needs_install: boolean;
|
needs_install: boolean;
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@ -26,6 +26,10 @@ import { cn, themedBody } from "@/lib/utils";
|
|||||||
|
|
||||||
type Transport = "http" | "stdio";
|
type Transport = "http" | "stdio";
|
||||||
|
|
||||||
|
function isHttpUrl(value: string): boolean {
|
||||||
|
return /^https?:\/\//i.test(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
function truncateText(value: string, maxLength: number): string {
|
function truncateText(value: string, maxLength: number): string {
|
||||||
return value.length > maxLength ? value.slice(0, maxLength) + "..." : value;
|
return value.length > maxLength ? value.slice(0, maxLength) + "..." : value;
|
||||||
}
|
}
|
||||||
@ -707,9 +711,21 @@ export default function McpPage() {
|
|||||||
>
|
>
|
||||||
{entry.transport}
|
{entry.transport}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge tone="outline">
|
<Badge tone="outline">auth: {entry.auth_type}</Badge>
|
||||||
{entry.source === "official" ? "official" : entry.source}
|
{isHttpUrl(entry.source) ? (
|
||||||
</Badge>
|
<a
|
||||||
|
href={entry.source}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-primary underline underline-offset-2 hover:opacity-80"
|
||||||
|
>
|
||||||
|
source ↗
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
entry.source && (
|
||||||
|
<Badge tone="outline">{entry.source}</Badge>
|
||||||
|
)
|
||||||
|
)}
|
||||||
{entry.installed && (
|
{entry.installed && (
|
||||||
<Badge tone="success">Installed</Badge>
|
<Badge tone="success">Installed</Badge>
|
||||||
)}
|
)}
|
||||||
@ -722,6 +738,67 @@ export default function McpPage() {
|
|||||||
{entry.description}
|
{entry.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{/* Connection detail: what the agent actually talks to. */}
|
||||||
|
{entry.transport === "http" && entry.url && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
<span className="font-medium">Endpoint:</span>{" "}
|
||||||
|
<code className="font-mono">{entry.url}</code>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{entry.transport === "stdio" && entry.command && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground break-all">
|
||||||
|
<span className="font-medium">Runs:</span>{" "}
|
||||||
|
<code className="font-mono">
|
||||||
|
{[entry.command, ...entry.args].join(" ")}
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{/* Git bootstrap — surfaced so users see what gets cloned/run
|
||||||
|
before they install (matches the docs trust model). */}
|
||||||
|
{entry.install_url && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground break-all">
|
||||||
|
<span className="font-medium">Installs from:</span>{" "}
|
||||||
|
{isHttpUrl(entry.install_url) ? (
|
||||||
|
<a
|
||||||
|
href={entry.install_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary underline underline-offset-2 hover:opacity-80"
|
||||||
|
>
|
||||||
|
{entry.install_url}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<code className="font-mono">{entry.install_url}</code>
|
||||||
|
)}
|
||||||
|
{entry.install_ref && (
|
||||||
|
<span> @ {entry.install_ref}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{entry.bootstrap.length > 0 && (
|
||||||
|
<details className="mt-1 text-xs text-muted-foreground">
|
||||||
|
<summary className="cursor-pointer select-none">
|
||||||
|
Bootstrap commands ({entry.bootstrap.length})
|
||||||
|
</summary>
|
||||||
|
<ul className="mt-1 ml-3 list-disc space-y-0.5">
|
||||||
|
{entry.bootstrap.map((cmd, i) => (
|
||||||
|
<li key={`${entry.name}-bs-${i}`} className="break-all">
|
||||||
|
<code className="font-mono">{cmd}</code>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
{entry.post_install && (
|
||||||
|
<details className="mt-1 text-xs text-muted-foreground">
|
||||||
|
<summary className="cursor-pointer select-none">
|
||||||
|
Setup notes
|
||||||
|
</summary>
|
||||||
|
<p className="mt-1 whitespace-pre-wrap">
|
||||||
|
{entry.post_install.trim()}
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
{entryDiags.map((d, i) => (
|
{entryDiags.map((d, i) => (
|
||||||
<p
|
<p
|
||||||
key={`${entry.name}-diag-${i}`}
|
key={`${entry.name}-diag-${i}`}
|
||||||
|
|||||||
@ -20,12 +20,7 @@ If you have ever wanted Hermes to use a tool that already exists somewhere else,
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
1. Install MCP support (already included if you used the standard install script):
|
1. MCP support ships with the standard install — no extra step needed.
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/.hermes/hermes-agent
|
|
||||||
uv pip install -e ".[mcp]"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Add an MCP server to `~/.hermes/config.yaml`:
|
2. Add an MCP server to `~/.hermes/config.yaml`:
|
||||||
|
|
||||||
@ -132,7 +127,12 @@ the hermes-agent repo, so Nous has reviewed each entry before it shipped —
|
|||||||
Manifests live at
|
Manifests live at
|
||||||
[`optional-mcps/<name>/manifest.yaml`](https://github.com/NousResearch/hermes-agent/tree/main/optional-mcps)
|
[`optional-mcps/<name>/manifest.yaml`](https://github.com/NousResearch/hermes-agent/tree/main/optional-mcps)
|
||||||
on GitHub. The picker also prints the manifest's `source:` URL at install
|
on GitHub. The picker also prints the manifest's `source:` URL at install
|
||||||
time so you can quickly verify the upstream repo.
|
time so you can quickly verify the upstream repo. The web dashboard's MCP
|
||||||
|
page surfaces the same detail per catalog entry — transport, auth type, the
|
||||||
|
endpoint URL (HTTP) or command + args (stdio), the git install source/ref and
|
||||||
|
bootstrap commands, and setup notes — with the `source:` rendered as a
|
||||||
|
clickable link, so you can inspect exactly what an entry connects to or runs
|
||||||
|
before clicking Install.
|
||||||
|
|
||||||
### Manifest version compatibility
|
### Manifest version compatibility
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user