Skip to content
Quick Guides
Quick Guides

Agent discovery — MCP-style for agents calling agents

Query a live registry of agents at runtime. Filter by tag and health, hand the result to an LLM as a tool list, let it pick.

The control plane keeps a live registry of every reasoner and skill across your fleet. Query it at runtime, filter by tag and health, and hand the result to the LLM as a tool list. New agent comes online → the LLM finds it on the next call.

This is the MCP-style path for AgentField agents calling AgentField agents: discover the live capability list, then call the chosen target with app.call("node.function"). If the capability should cross an organization or network boundary, use external agent discovery as the public opt-in layer.

from agentfield import Agent
from agentfield.tool_calling import ToolCallConfig

app = Agent(node_id="orchestrator")

@app.reasoner()
async def smart_route(question: str) -> dict:
    # Inspect the live registry — health-aware
    caps = app.discover(
        tags=["public"],
        health_status="active",
        include_input_schema=True,
    )
    # caps.json.total_agents tells you how many are online right now

    # Hand the registry to the LLM as a tool list. It picks and calls.
    result = await app.ai(
        system="Pick the best available agent to answer the user's question.",
        user=question,
        tools="discover",
    )

    # Trace shows exactly which tools the LLM invoked
    for call in result.trace.calls:
        print(f"  {call.tool_name} ({call.latency_ms:.0f}ms)")

    return {"answer": result.text}

# Large registries — send names+descriptions first, hydrate full schemas on demand
@app.reasoner()
async def lazy_route(question: str) -> dict:
    return await app.ai(
        user=question,
        tools=ToolCallConfig(
            schema_hydration="lazy",
            max_candidate_tools=50,    # show 50 tools to the LLM
            max_hydrated_tools=10,     # full schemas only for the 10 it picks
        ),
    )

app.run()
import { Agent } from "@agentfield/sdk";

const agent = new Agent({ nodeId: "orchestrator" });

agent.reasoner("smart_route", async (ctx) => {
  // Inspect the live registry — health-aware
  const caps = await agent.discover({
    tags: ["public"],
    healthStatus: "active",
    includeInputSchema: true,
  });
  // caps.json?.totalAgents tells you how many are online right now

  // Hand the registry to the LLM as a tool list. It picks and calls.
  const result = await ctx.aiWithTools(ctx.input.question, {
    system: "Pick the best available agent to answer the user's question.",
    tools: "discover",
  });

  // Trace shows exactly which tools the LLM invoked
  for (const call of result.trace.calls) {
    console.log(`  ${call.toolName} (${call.latencyMs.toFixed(0)}ms)`);
  }

  return { answer: result.text };
});

agent.serve();
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/Agent-Field/agentfield/sdk/go/agent"
    "github.com/Agent-Field/agentfield/sdk/go/ai"
)

func main() {
    a, err := agent.New(agent.Config{
        NodeID:        "orchestrator",
        Version:       "1.0.0",
        AgentFieldURL: "http://localhost:8080",
    })
    if err != nil {
        log.Fatal(err)
    }

    a.RegisterReasoner("smart_route", func(ctx context.Context, input map[string]any) (any, error) {
        question := fmt.Sprintf("%v", input["question"])

        // Hand the live registry to the LLM as a tool list. It picks and calls.
        // Discovery filters scope which agents become callable tools.
        resp, trace, err := a.AIWithTools(ctx, question,
            ai.ToolCallConfig{
                SystemPrompt: "Pick the best available agent to answer the user's question.",
                MaxTurns:     10,
            },
            agent.WithTags([]string{"public"}),
            agent.WithHealthStatus("active"),
            agent.WithDiscoveryInputSchema(true),
        )
        if err != nil {
            return nil, err
        }

        // Trace shows exactly which tools the LLM invoked
        for _, call := range trace.Calls {
            fmt.Printf("  %s (%.0fms)\n", call.ToolName, call.LatencyMs)
        }

        return map[string]any{"answer": resp.Text()}, nil
    })

    a.Serve(context.Background())
}

The same registry powers the control plane UI. The Agent nodes page shows every registered agent, its tags, its reasoners, and whether it's online — which is exactly the data feeding app.discover():

Agent nodes page — code-forge expanded showing all 17 reasoners as endpoints, the same live registry that app.discover() queries

What this gives you

  • The tool list is generated from the live registry on every call — no static config.
  • Lazy hydration keeps context windows manageable when you have hundreds of agents.
  • The trace records every dispatch so routing decisions are debuggable.

Next