agent-discover exposes 1 action-based MCP tool (registry) plus dynamically proxied tools from active servers. Keeping the tool count to one minimizes prompt overhead — every action lives behind the same dispatcher.
Unified server-registry tool. Actions: find_tool, find_tools, get_schema, proxy_call, list, install, uninstall, activate, deactivate, browse, status.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
action |
string | yes | One of the actions listed above |
query |
string | no | [find_tool/list/browse] Intent or FTS search query |
intents |
string[] | no | [find_tools] Array of intent strings — one per tool you need to discover in the batch |
auto_activate |
boolean | no | [find_tool/find_tools] When false, do not expose proxied tools to the host. Use proxy_call to invoke them instead. Default true. |
call_as |
string | no | [get_schema/proxy_call] Fully-qualified mcp__server__tool name returned by find_tool |
server |
string | no | [proxy_call] Server name (alternative to call_as) |
tool |
string | no | [proxy_call] Tool name (alternative to call_as) |
arguments |
object | no | [proxy_call] Arguments to pass to the proxied tool |
limit |
number | no | [find_tool/find_tools/browse] Max results per query |
source |
string | no | [list/install] Filter by or specify source (local, registry, smithery, manual) |
installed_only |
boolean | no | [list] Only show installed servers |
name |
string | no | [install/uninstall/activate/deactivate] Server name |
command |
string | no | [install] Command to start server (required for manual install) |
args |
string[] | no | [install] Command arguments |
env |
object | no | [install] Environment variables |
description |
string | no | [install] Server description |
tags |
string[] | no | [install] Tags for search/filtering |
cursor |
string | no | [browse] Pagination cursor from previous response |
Single-call tool discovery. The agent describes what it wants in natural language, agent-discover ranks the entire cross-server tool catalog with hybrid BM25 + semantic retrieval, and returns the best match with a confidence label and the args the agent needs to invoke it. Replaces the multi-step list → activate → call dance.
find_tool example (single intent):
{
"name": "registry",
"arguments": {
"action": "find_tool",
"query": "post a message to a slack channel",
"auto_activate": false
}
}Returns:
{
"found": true,
"confidence": "high",
"score": 0.87,
"call_as": "mcp__slack__post_message",
"server": "slack",
"tool": "post_message",
"description": "Post a message to a Slack channel or thread.",
"required_args": [
{ "name": "channel", "type": "string", "description": "Channel ID or name." },
{ "name": "text", "type": "string", "description": "Message body." }
],
"optional_count": 1,
"next_step": "invoke call_as directly",
"other_matches": [
{
"call_as": "mcp__slack__list_channels",
"tool": "list_channels",
"description": "...",
"score": 0.42
}
]
}When the top-result score falls below 0.25 the response is { found: false, top_score, hint } instead — the no-match path.
find_tools example (batch — N intents in one round-trip):
{
"name": "registry",
"arguments": {
"action": "find_tools",
"intents": ["recent sentry errors for the web project", "create a linear issue"],
"auto_activate": false
}
}Returns one result per intent in the same shape as find_tool.
get_schema example (full input_schema for a tool you've already discovered):
{
"name": "registry",
"arguments": { "action": "get_schema", "call_as": "mcp__slack__post_message" }
}Use this only when the compact required_args summary isn't enough (conditional / polymorphic args). Most tools can be invoked from the find_tool response alone.
proxy_call example (invoke a discovered tool through agent-discover):
{
"name": "registry",
"arguments": {
"action": "proxy_call",
"call_as": "mcp__slack__post_message",
"arguments": { "channel": "#releases", "text": "deploy finished" }
}
}proxy_call lets the agent invoke a tool without the host having to load that tool into its catalog. Combined with find_tool({auto_activate: false}), this keeps the host MCP surface area at exactly 5 agent-discover actions regardless of how many tools the registered child servers actually expose. Critical for very large catalogs (~1k+ tools) where firing notifications/tools/list_changed would flood the host with thousands of schemas.
If the proxied tool call fails, the response includes a did_you_mean array with similarly-named tools so the agent can recover from a wrong-tool selection in one extra turn.
Example (list):
{
"name": "registry",
"arguments": { "action": "list", "query": "filesystem", "installed_only": true }
}Example (install from registry):
{
"name": "registry",
"arguments": { "action": "install", "name": "filesystem", "source": "registry" }
}Example (install manually):
{
"name": "registry",
"arguments": {
"action": "install",
"name": "my-server",
"command": "node",
"args": ["/path/to/server.js"],
"description": "My custom MCP server"
}
}Example (uninstall):
{
"name": "registry",
"arguments": { "action": "uninstall", "name": "filesystem" }
}Example (browse):
{
"name": "registry",
"arguments": { "action": "browse", "query": "database", "limit": 5 }
}Example (status):
{
"name": "registry",
"arguments": { "action": "status" }
}Activation starts the server as a child process and exposes its tools through agent-discover. Proxied tools appear as serverName__toolName. Secrets stored for this server are automatically merged into the process environment.
Deactivation stops the server and removes its proxied tools.
Example (activate):
{
"name": "registry",
"arguments": { "action": "activate", "name": "filesystem" }
}Example (deactivate):
{
"name": "registry",
"arguments": { "action": "deactivate", "name": "filesystem" }
}Activation and deactivation trigger an MCP tools/list_changed notification, so MCP clients refresh their tool list automatically.
When a server is activated, its tools are exposed with a namespaced name: serverName__toolName. These tools accept the same parameters as the original server defines and proxy calls through to the child process.
Example: If filesystem is active and provides read_file, you can call the tool filesystem__read_file with the original tool's parameters.
Each proxied tool call is automatically metered -- latency, success/failure, and call count are recorded in the metrics table.
The dashboard server exposes a REST API for programmatic access. All responses include Access-Control-Allow-Origin: * for CORS. CORS preflight (OPTIONS) is handled for all routes.
Health check endpoint.
Response:
{
"status": "ok",
"version": "1.1.0",
"uptime": 3600
}Probe the host for installed package managers. The dashboard uses this to warn when a tool needed for an install (npx, uvx, docker) is missing.
Each value is the result of spawning <tool> --version with a 5-second timeout. Uses shell: true so Windows .cmd/.bat shims (npx.cmd, uvx.cmd) resolve correctly.
Response:
{
"npx": true,
"uvx": false,
"docker": false,
"uv": false
}List registered servers. Supports query parameters for filtering.
Query parameters:
| Name | Description |
|---|---|
query |
FTS search query |
source |
Filter by source type |
installed |
Set to "true" for installed only |
Response: Array of server objects with active status merged from proxy. Each server includes health_status, last_health_check, and error_count fields.
Get a single server by ID, including its discovered tools.
Response:
{
"id": 1,
"name": "filesystem",
"description": "...",
"active": true,
"health_status": "healthy",
"error_count": 0,
"tools": [
{ "id": 1, "server_id": 1, "name": "read_file", "description": "...", "input_schema": {} }
]
}Errors: Returns 404 if the server ID does not exist.
Register a new server.
Request body:
{
"name": "my-server",
"command": "node",
"args": ["/path/to/server.js"],
"description": "My server",
"tags": ["custom"]
}Response: 201 Created with the server object.
Update an existing server's configuration.
Request body (all fields optional):
{
"description": "Updated description",
"command": "node",
"args": ["server.js", "--verbose"],
"env": { "API_KEY": "..." },
"tags": ["updated"]
}Accepted fields: description, command, args, env, tags.
Response: The updated server object.
Errors:
404if the server ID does not exist.
Remove a server. Deactivates it first if active.
Response:
{ "status": "deleted" }Errors: Returns 404 if the server ID does not exist.
Activate a server by ID. Secrets are merged into the server's environment on activation.
Response:
{ "status": "activated", "tool_count": 5 }Errors:
404if the server ID does not exist.400if the server has no command configured.- Returns
{ "status": "already_active" }if already running.
Deactivate a running server.
Response:
{ "status": "deactivated" }Errors:
404if the server ID does not exist.- Returns
{ "status": "not_active" }if the server is not currently running.
List all secrets for a server. Values are masked (first 4 characters visible, rest replaced with ****).
Response:
[
{
"key": "API_KEY",
"masked_value": "sk-t****",
"updated_at": "2025-01-15T10:30:00"
}
]Errors: Returns 404 if the server ID does not exist.
Set (create or update) a secret for a server. The secret value is stored and will be injected as an environment variable when the server is activated.
Request body:
{
"value": "sk-test-1234567890"
}Response:
{ "status": "set", "key": "API_KEY" }Errors:
404if the server ID does not exist.422ifvalueis missing or empty.
Delete a secret for a server.
Response:
{ "status": "deleted", "key": "API_KEY" }Errors: Returns 404 if the server ID does not exist.
Run a health check on a server. For active servers, checks the tool list via the proxy. For inactive servers with a command, attempts a quick activate/deactivate cycle (5-second timeout). Updates health_status, last_health_check, and error_count in the database.
Response:
{
"status": "healthy",
"latency_ms": 42
}or on failure:
{
"status": "unhealthy",
"latency_ms": 5001,
"error": "Health check timed out"
}Errors: Returns 404 if the server ID does not exist.
Get per-tool metrics for a specific server.
Response:
[
{
"tool_name": "read_file",
"call_count": 42,
"error_count": 1,
"avg_latency_ms": 150,
"last_called_at": "2025-01-15T12:00:00"
}
]Errors: Returns 404 if the server ID does not exist.
Global metrics overview across all servers with recorded activity.
Response:
[
{
"server_name": "filesystem",
"total_calls": 100,
"total_errors": 2,
"avg_latency_ms": 120
}
]Federated search across the official MCP registry, npm, and PyPI, merged into a single result. The official registry is the primary source; npm and PyPI augment best-effort and never block the response.
Same-source version duplicates are collapsed by name (highest semver wins). Cross-source name collisions are kept distinct via a <source>:<name> dedupe key, so e.g. mcp-server-sqlite (npm) and mcp-server-sqlite (PyPI) both appear.
Query parameters:
| Name | Description |
|---|---|
query |
Search term |
limit |
Max results (default 20) |
cursor |
Pagination cursor |
Response: Marketplace result with servers array and next_cursor. Each server entry's packages[].runtime is one of:
| Runtime | Source | Install command (default) |
|---|---|---|
node |
Official registry node entries, npm fallback | npx -y <pkg> |
python |
PyPI curated list / scrape | uvx <pkg> |
docker |
Official registry docker entries | docker run -i --rm <image> |
streamable-http |
Official registry, remote MCP servers | (no spawn — direct HTTP) |
sse |
Official registry, remote MCP servers | (no spawn — direct SSE) |
Reset a server's error count to 0.
Response:
{ "status": "reset" }Call a tool on an active server via REST (proxy). Generates a log entry.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
tool |
string | yes | Tool name (e.g. search_read) |
args |
object | no | Arguments to pass to the tool |
Response: The tool's MCP result (content array).
Return recent call log entries (newest first).
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
limit |
number | 100 | Max entries (up to 500) |
offset |
number | 0 | Skip first N entries |
Response:
{
"entries": [
{
"id": 3,
"timestamp": "2026-04-12T08:26:35.123Z",
"server": "lastloop-odoo",
"tool": "search_read",
"args": { "model": "res.partner" },
"response": "[{\"id\": 1, \"name\": \"...\"}]",
"latency_ms": 414,
"success": true
}
],
"total": 3
}Clear all log entries.
Response:
{ "status": "cleared" }Show active server summary.
Response:
{
"active_count": 2,
"servers": [{ "name": "filesystem", "tool_count": 5, "tools": ["read_file", "..."] }]
}Connect to ws://localhost:3424 to receive real-time state updates.
On connect, the server sends a full state snapshot:
{
"type": "state",
"version": "1.1.0",
"servers": [
{
"id": 1,
"name": "filesystem",
"active": true,
"health_status": "healthy",
"error_count": 0,
"tools": [{ "id": 1, "name": "read_file", "description": "..." }]
}
],
"active": [
{
"name": "filesystem",
"tools": [{ "name": "read_file", "description": "..." }]
}
]
}The server polls the database every 2 seconds. When changes are detected (via a fingerprint based on server count, max ID, last update time, and tool count), a full state message is re-sent to all connected clients whose fingerprint is stale.
| Message | Description |
|---|---|
{ "type": "refresh" } |
Request a full state resend |
| Message | Description |
|---|---|
type: "state" |
Full state snapshot |
type: "log_entry" |
Real-time tool call log entry (see GET /api/logs for schema). entry.kind is call / ping / resource-read / prompt-get / notification / progress. |
type: "notification" |
Mirrors a log entry with kind: "notification" — { serverName, method, params, ts }. |
type: "progress" |
Mirrors a log entry with kind: "progress" — { serverName, payload: { token, progress, total, message }, ts }. |
type: "error" |
Error message (invalid JSON, etc.) |
The 14 new tester endpoints under /api/servers/:id/* and /api/transient/:handle/* (also mirrored as GET /api/roots, GET /api/logs/notifications, GET /api/logs/progress) are documented in DASHBOARD.md → Test Panel and the top-level README.md endpoint table. All are gated on loopback access unless AGENT_DISCOVER_ALLOW_REMOTE_TEST=1 is set.
- Max payload size: 4096 bytes
- Max connections: 50
- Ping interval: 30 seconds (clients must respond to pings)