MCP Server Integration

Building on Examples 01–04, this example demonstrates connecting a Dragon agent to remote Model Context Protocol (MCP) servers. The agent can discover and invoke tools exposed by any MCP-compatible server (e.g., data stores, compute clusters, external APIs) alongside locally registered tools. Tool calls are automatically routed based on a namespace prefix.

Prerequisites: Read 04_memory.py first.

What you’ll learn:

  • How to connect to one or more MCP servers via HTTP/SSE

  • How to configure authentication tokens and timeouts

  • How tool names are scoped (local tools vs. MCP tools via alias__tool_name)

  • How the ToolDispatcher routes calls to the correct server

  • How to mix local and remote tools in the same agent

Architecture:

The agent’s ToolDispatcher maintains connections to multiple MCP servers and routes tool calls transparently: calls without a prefix go to local tools; calls like datastore__query route to the MCP server aliased datastore.

Main Code

Below is the complete example:

Listing 32 05_mcp_tools.py: Agent with MCP server tools
  1"""05 — MCP Server Integration.
  2
  3**Prerequisites:** Read ``02_multi_agent_dag.py`` first.
  4
  5**What's new:**
  6
  7* **MCPServerConfig** — connect an agent to a remote MCP tool server
  8* **Scoped tool names** — MCP tools appear as ``alias__tool_name``
  9  (e.g. ``jupyter__create_notebook``) to avoid collisions with local tools
 10* **Mixed local + MCP tools** — the reporter agent has both local tools
 11  (``format_results_table``) and remote MCP tools (Jupyter operations)
 12* **Auto-discovery** — MCP tool schemas are fetched at connect time and
 13  merged into the LLM's tool list
 14* **Token from file** — bearer token read from ``token.txt``, never on CLI
 15* **Graceful fallback** — if no MCP URL is provided, reporter uses
 16  local tools only
 17
 18Architecture::
 19
 20    planner_agent ──► runner_agent ──► analyzer_agent ──┐
 21         │                │                             ├──► reporter_agent
 22         └────────────────┴─────────────────────────────┘      │
 23                                                         MCP: jupyter__*
 24
 25Usage::
 26
 27    # Without MCP (local tools only):
 28    dragon 05_mcp_tools.py
 29
 30    # With MCP (Jupyter notebook creation):
 31    echo '<your-token>' > token.txt
 32    dragon 05_mcp_tools.py <mcp_server_url>
 33"""
 34
 35import asyncio
 36import os
 37import sys
 38from typing import Any
 39
 40import dragon
 41import multiprocessing as mp
 42
 43from dragon.ai.agent.core import create_sub_agent
 44from dragon.ai.agent.config import (
 45    AgentConfig,
 46    MCPServerConfig,       # ← NEW
 47    OrchestratorConfig,
 48    Pipeline,
 49    PipelineNode,
 50)
 51from dragon import Policy, System, Batch
 52from dragon.ai.agent.tools import ToolRegistry
 53from dragon.ai.agent.orchestrator import DAGOrchestrator
 54from dragon.native.event import Event
 55from dragon.native.machine import Node
 56from dragon.native.process import Process
 57from dragon.native.queue import Queue
 58
 59from dragon.ai.inference import (
 60    BatchingConfig,
 61    HardwareConfig,
 62    InferenceConfig,
 63    ModelConfig,
 64    Inference,
 65)
 66
 67from tools import (
 68    propose_experiment,
 69    launch_experiment,
 70    check_progress,
 71    collect_results,
 72    analyze_convergence,
 73    format_results_table,
 74)
 75from tools.runner import cleanup_experiment_state
 76
 77
 78# ===========================================================================
 79# User-configurable constants
 80# ===========================================================================
 81
 82MODEL_NAME = "/path/to/your/model"
 83HF_TOKEN = ""
 84
 85
 86# ===========================================================================
 87# Inference Pipeline Configuration
 88# ===========================================================================
 89
 90INFERENCE_CONFIG = InferenceConfig(
 91    model=ModelConfig(
 92        model_name=MODEL_NAME,
 93        hf_token=HF_TOKEN,
 94        tp_size=2,
 95        max_tokens=8192,
 96        max_model_len=32768,
 97    ),
 98    hardware=HardwareConfig(
 99        num_nodes=1,
100        num_gpus=2,
101        num_inf_workers_per_cpu=1,
102    ),
103    batching=BatchingConfig(
104        batch_wait_seconds=0.1,
105        max_batch_size=32,
106    ),
107)
108
109
110# ===========================================================================
111# Tool registries
112# ===========================================================================
113
114planner_registry = ToolRegistry()
115planner_registry.register(propose_experiment)
116
117runner_registry = ToolRegistry()
118runner_registry.register(launch_experiment)
119runner_registry.register(check_progress)
120runner_registry.register(collect_results)
121
122analyzer_registry = ToolRegistry()
123analyzer_registry.register(analyze_convergence)
124
125reporter_registry = ToolRegistry()
126reporter_registry.register(format_results_table)
127
128
129# ===========================================================================
130# DAG pipeline
131# ===========================================================================
132
133pipeline = Pipeline(nodes=[
134    PipelineNode(
135        agent_id="planner_agent",
136        task_description=(
137            "You are a scientific experiment planner.  The user wants to study "
138            "Monte Carlo convergence for estimating π.\n\n"
139            "Propose an experiment plan by calling propose_experiment with:\n"
140            "  - description, sample_sizes, convergence_target, methodology\n\n"
141            "Report the approved plan verbatim as your final answer."
142        ),
143        depends_on=[],
144    ),
145    PipelineNode(
146        agent_id="runner_agent",
147        task_description=(
148            "You manage parallel Monte Carlo simulations on an HPC cluster.\n\n"
149            "Tools:\n"
150            "  1. launch_experiment(sample_sizes, seeds) — launches ALL "
151            "simulations in parallel.\n"
152            "  2. check_progress() — shows done/running/pending status.\n"
153            "  3. collect_results() — call ONLY when all_done=true.\n\n"
154            "STRICT workflow:\n"
155            "  1. Call launch_experiment.\n"
156            "  2. Call check_progress until all_done=true.\n"
157            "  3. Call collect_results — REQUIRED before final answer.\n"
158            "  4. Report collect_results output verbatim."
159        ),
160        depends_on=["planner_agent"],
161    ),
162    PipelineNode(
163        agent_id="analyzer_agent",
164        task_description=(
165            "Call analyze_convergence with a list of dicts (keys: "
166            "'n_samples', 'absolute_error').  Report all metrics verbatim."
167        ),
168        depends_on=["runner_agent"],
169    ),
170    PipelineNode(
171        agent_id="reporter_agent",
172        task_description=(
173            "Write a structured report with the experiment results.\n\n"
174            "STRICT workflow — follow every step IN ORDER:\n\n"
175            "  STEP 1.  Call format_results_table to get a Markdown table.\n\n"
176            "  STEP 2.  Check your available tools.  If you have tools that\n"
177            "           can manage Jupyter kernels and notebook files, do\n"
178            "           BOTH sub-steps:\n"
179            "             a. Create a Jupyter kernel.\n"
180            "             b. Create a notebook file named\n"
181            "                'monte_carlo_convergence_report' with the\n"
182            "                COMPLETE report as code_content.\n"
183            "           If no such tools are available, skip Step 2.\n\n"
184            "  STEP 3.  Give your final answer with the SAME complete\n"
185            "           report text.  COPY-PASTE the actual table.\n\n"
186            "Never invent data — use only data from upstream agents."
187        ),
188        depends_on=["planner_agent", "runner_agent", "analyzer_agent"],
189    ),
190])
191
192
193# ===========================================================================
194# Helpers
195# ===========================================================================
196
197def _make_agent_kwargs(agent_id, name, role, registry, inference_queue,
198                       mcp_servers=None,
199                       max_tool_call_iterations=20):
200    """Build kwargs for create_sub_agent().
201
202    NEW in this example: ``mcp_servers`` — list of MCPServerConfig objects.
203    The agent connects to each MCP server in its own process at startup.
204    """
205    return {
206        "config": AgentConfig(
207            agent_id=agent_id, name=name, role=role,
208            inference_queue=inference_queue,
209            max_tool_call_iterations=max_tool_call_iterations,
210        ),
211        "tool_registry": registry,
212        "mcp_servers": mcp_servers,    # ← NEW: passed to create_sub_agent
213        "shutdown_event": Event(),
214        "reply_queue": Queue(),
215    }
216
217
218def _start_agents(specs, policies):
219    procs = []
220    for spec, policy in zip(specs, policies):
221        p = Process(target=create_sub_agent, kwargs=spec, policy=policy)
222        p.start()
223        procs.append(p)
224    queues = {}
225    for spec in specs:
226        aid = spec["config"].agent_id
227        queues[aid] = spec["reply_queue"].get()
228        print(f"[startup] Agent '{aid}' ready.", flush=True)
229    return procs, queues
230
231
232# ===========================================================================
233# Main
234# ===========================================================================
235
236async def main():
237    input_queue = Queue()
238
239    print("[startup] Initializing inference pipeline...", flush=True)
240
241    inference_pipeline = None
242    try:
243        inference_pipeline = Inference(INFERENCE_CONFIG, input_queue)
244        inference_pipeline.initialize()
245    except Exception as exc:
246        import traceback
247        print(f"\n[FATAL] Inference pipeline failed to initialize: {exc}", flush=True)
248        traceback.print_exc()
249        if inference_pipeline is not None:
250            inference_pipeline.destroy()
251        return
252    print("[startup] Inference pipeline ready.\n", flush=True)
253
254    my_alloc = System()
255    node_list = my_alloc.nodes
256    compute_host = (
257        Node(node_list[1]).hostname if len(node_list) > 1
258        else Node(node_list[0]).hostname
259    )
260    compute_policy = Policy(
261        placement=Policy.Placement.HOST_NAME,
262        host_name=compute_host,
263    )
264
265    # -------------------------------------------------------------------
266    # ← NEW: MCP server setup for the reporter agent
267    #
268    # Pass the MCP URL as a command-line argument:
269    #   dragon 05_mcp_tools.py <mcp_url>
270    #
271    # The bearer token is read from token.txt in CWD.
272    # -------------------------------------------------------------------
273    mcp_url = sys.argv[1] if len(sys.argv) >= 2 else None
274
275    TOKEN_FILE = os.path.join(os.getcwd(), "token.txt")
276    mcp_token = None
277    if mcp_url:
278        try:
279            with open(TOKEN_FILE, "rb") as f:
280                raw = f.read()
281            mcp_token = raw.decode("utf-8").strip().strip("\ufeff")
282            print(f"[startup] MCP token loaded from {TOKEN_FILE}", flush=True)
283        except FileNotFoundError:
284            print(
285                f"[startup] WARNING: {TOKEN_FILE} not found — "
286                "MCP connection will have no auth token.",
287                flush=True,
288            )
289
290    reporter_mcp_servers = None
291    if mcp_url:
292        reporter_mcp_servers = [
293            MCPServerConfig(url=mcp_url, alias="jupyter", token=mcp_token),
294        ]
295        print(f"[startup] Reporter will connect to MCP: {mcp_url}", flush=True)
296    else:
297        print(
298            "[startup] No MCP URL provided — reporter uses local tools only.\n"
299            "  To enable MCP, run:\n"
300            "    echo '<token>' > token.txt\n"
301            "    dragon 05_mcp_tools.py <mcp_url>",
302            flush=True,
303        )
304
305    procs, agent_specs = [], []
306    try:
307        agent_specs = [
308            _make_agent_kwargs(
309                "planner_agent", "Experiment Planner",
310                "You are an experiment planner for Monte Carlo convergence "
311                "studies on an HPC cluster.",
312                planner_registry, input_queue,
313            ),
314            _make_agent_kwargs(
315                "runner_agent", "Parallel Simulation Runner",
316                "You manage parallel Monte Carlo simulations.\n"
317                "You MUST call all three tools in order: "
318                "launch_experiment → check_progress → collect_results.\n"
319                "NEVER give a final answer without calling collect_results first.",
320                runner_registry, input_queue,
321                max_tool_call_iterations=60,
322            ),
323            _make_agent_kwargs(
324                "analyzer_agent", "Convergence Analyzer",
325                "Analyse convergence.  Call analyze_convergence with "
326                "n_samples + absolute_error dicts.  Report verbatim.",
327                analyzer_registry, input_queue,
328            ),
329            _make_agent_kwargs(
330                "reporter_agent", "Report Writer",
331                "Write a structured report with Markdown tables.  "
332                "If you have Jupyter tools, also create a notebook.  "
333                "Always include tool output verbatim.",
334                reporter_registry, input_queue,
335                # ← NEW: attach MCP servers to this agent
336                mcp_servers=reporter_mcp_servers,
337            ),
338        ]
339        policies = [None, compute_policy, None, None]
340
341        procs, queues = _start_agents(agent_specs, policies)
342        for spec in agent_specs:
343            spec["config"].input_queue = queues[spec["config"].agent_id]
344
345        orchestrator = DAGOrchestrator(
346            config=OrchestratorConfig(
347                agents=[s["config"] for s in agent_specs],
348                poll_interval=0.5,
349                poll_timeout=14400.0,
350            ),
351            pipeline=pipeline,
352        )
353
354        user_input = (
355            "Design and run a Monte Carlo convergence experiment for π.  "
356            "Monitor progress and report which nodes ran what."
357            + (
358                "  Save the final report to a Jupyter notebook on the cluster."
359                if mcp_url else ""
360            )
361        )
362
363        batch = Batch()
364        try:
365            print("=" * 60, flush=True)
366            print("Dragon AI — 05 MCP Tools Integration", flush=True)
367            print("=" * 60, flush=True)
368            print(f"Request: {user_input}\n", flush=True)
369
370            result = orchestrator.run(
371                user_input=user_input,
372                batch=batch,
373            )
374
375            print("\n" + "=" * 60, flush=True)
376            print("FINAL RESULT", flush=True)
377            print("=" * 60, flush=True)
378            print(result, flush=True)
379
380        except Exception as exc:
381            import traceback
382            print(f"\n[error] Pipeline failed: {exc}", flush=True)
383            traceback.print_exc()
384        finally:
385            orchestrator.destroy()
386            batch.join()
387
388    except Exception as exc:
389        import traceback
390        print(f"\n[error] Fatal: {exc}", flush=True)
391        traceback.print_exc()
392    finally:
393        cleanup_experiment_state()
394        for spec in agent_specs:
395            try:
396                spec["shutdown_event"].set()
397            except Exception:
398                pass
399        for p in procs:
400            try:
401                p.join()
402            except Exception:
403                pass
404        print("\n[teardown] All agents stopped.", flush=True)
405        try:
406            inference_pipeline.destroy()
407        except Exception:
408            pass
409        print("[teardown] Inference pipeline stopped.", flush=True)
410
411
412if __name__ == "__main__":
413    mp.set_start_method("dragon")
414    asyncio.run(main())

Key Concepts

MCP Server Configuration:

Define one or more MCP servers in MCPServerConfig:

mcp_servers = [
    MCPServerConfig(
        url="http://datastore-service:8080/sse",
        alias="datastore",
        token="my-secret-token",
        timeout=10.0,
        max_retries=3,
    ),
    MCPServerConfig(
        url="http://compute-cluster:9090/sse",
        alias="compute",
    ),
]

Tool Naming and Routing:

  • Local tools: registered via registry.register() or @registry.tool; accessible as tool_name

  • MCP tools: auto-discovered from servers; accessible as alias__tool_name (e.g., datastore__query, compute__submit_job)

The ToolDispatcher parses the prefix and routes the call to the correct server (or local registry).

Tool Discovery:

When the agent connects to an MCP server, it auto-discovers all tools exposed by that server. No manual schema registration needed.

Installation

See Example 01 (same dependencies). For MCP servers, install compatible implementations (e.g., mcp-server-filesystem, mcp-server-postgres, etc.).

System Description

Tested on HPE Cray EX:

  • 1 compute node with 1–2 GPUs

  • MCP servers can run on same or different nodes (TCP/HTTP connectivity required)

How to Run

Step 1: Start MCP servers (in separate terminals)

Example with filesystem MCP server:

python -m mcp_server_filesystem /data/experiments

Example with custom data service:

python my_datastore_mcp_server.py --port 8080

Step 2: Edit MCP URLs in the example

Open 05_mcp_tools.py and set MCPServerConfig URLs to match your servers.

Step 3: Run the agent

dragon 05_mcp_tools.py

Example output:

$ dragon 05_mcp_tools.py
Agent 'researcher' started
MCP server 'datastore' connected (6 tools available)
MCP server 'compute' connected (4 tools available)
Task: 'Find all experiments and analyze trends'
Calling: datastore__query(filter='last_week')
→ Result: 42 experiments found
Calling: compute__analyze(experiment_ids=[...])
→ Result: Trends calculated
Calling: summarize_results(trends=...)
→ Result: Final summary written to report.json
Agent completed

Next Steps

  • 06 — Full Pipeline (all features combined: DAG + HITL + Memory + MCP + Tracing)