Skip to content

MCP tools

Give an agent the tools of a Model Context Protocol (MCP) server that runs as its own service.

The Model Context Protocol (MCP) is a standard way for a program to offer tools to AI agents. Instead of writing the tools into your agent, you point the agent at an MCP server: it connects, asks what tools the server has, and offers them to the model like any other. When the model calls one, the call goes over the network to the server and the result comes back. The server is its own process, so it can be written in any language, run anywhere, and be shared by this agent, an IDE and any other client. Here it runs as a Docker service.

Use it when

  • The tools already exist as an MCP server, yours or someone else's.
  • One tool implementation should serve several clients: this agent, an IDE, another application.
  • Tools should be discovered at run time and not hard-coded into the agent.

Look elsewhere when

  • The tools are plain functions that only this agent uses: tool_calling is simpler, with nothing to run beside it.
agent  ──── HTTP / MCP ────▶  service: days_between · add_days · weekday
(this process)                (its own container, started by Docker Compose)
   ▲                                   │
   └──────────── tool result ◀─────────┘

What it shows

  • A real service boundary. The server (service/server.py) is a separate process with its own Dockerfile and docker-compose.yml. The agent reaches it over the network, so the tools are discovered across a wire, and the server can be written in anything, run anywhere, and shared
  • The address is a dependency. McpDeps.server_url (read from MCP_SERVER_URL, defaulting to port 8000 on this machine, which the Docker service does not use: it publishes on a random free port) feeds a toolset the agent builds per run (@agent.toolset), so the same agent can use a staging server or a test double. Pointing it at a different MCP server changes nothing else: the tools are discovered, not coded
  • Server errors the model can fix: a malformed date raises in the tool, and the model sees Error calling tool 'days_between': 'next tuesday' is not a date. Use the form YYYY-MM-DD and retries with a correct one
  • A server that isn't there: run_mcp raises McpServerUnavailable saying where it looked and how to start it, instead of a bare connection error
  • Tested for real. The offline tests run the same server as a local subprocess and talk to it over HTTP; the release check builds the Docker image, waits for its healthcheck, finds the port Docker chose, and runs the live tests and a recorded run against the container

See it run: sample_run.md is a recorded run against a real model: what each agent was asked, which tools it called, and what it returned.

Running it

docker compose -f examples/mcp_tools/service/docker-compose.yml up -d --wait
PORT=$(docker compose -f examples/mcp_tools/service/docker-compose.yml port mcp-server 8000 | cut -d: -f2)
MCP_SERVER_URL=http://127.0.0.1:$PORT/mcp uv run python -m examples.mcp_tools.agent
docker compose -f examples/mcp_tools/service/docker-compose.yml down

To use it in your project, add_agent.py copies the agent into agent/agents/ and the service into services/<name>/, and tells you how to start it:

uv run python scripts/add_agent.py mcp_tools --name calendar

To adapt it, replace the tools in service/server.py (or point MCP_SERVER_URL at an MCP server you already have) and rewrite the agent's instructions. Dates are a good fit for a tool: models are unreliable at calendar arithmetic, and a tool makes it exact.

Source

All of it is in examples/mcp_tools/.

"""MCP tools: give an agent the tools of a Model Context Protocol server running as a service.

Use this pattern when:
- The tools already exist as an MCP server (yours, or someone else's) and the agent should use them
- One tool implementation should serve several clients (this agent, an IDE, another application)
- Tools should be discovered at run time rather than hard-coded into the agent

How it works: the server is its own process (`service/server.py`, started by Docker Compose). The
agent connects to it by URL, lists its tools, and exposes them to the model like any other. When the
model calls one, the call goes over the network to the server and the result comes back. A tool that
raises becomes an error the model sees and can correct (here, a malformed date).

The server's address is a dependency (`McpDeps.server_url`, read from MCP_SERVER_URL), so the same agent
can talk to the local service, a staging server or a test double. The toolset is built per run from it.
Pointing at a different MCP server changes nothing else: its tools are discovered, not coded.

If the server isn't running, `run_mcp` raises `McpServerUnavailable` saying where it looked.
"""

from __future__ import annotations

import os
from dataclasses import dataclass, field

from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
from pydantic_ai.capabilities import RaiseContentFilterError
from pydantic_ai.mcp import MCPToolset
from pydantic_ai.usage import UsageLimits

from agent.config import settings
from agent.logging import agent_label, configure_logging, get_logger
from agent.runs import Flow, RunResult

logger = get_logger(__name__)
LABEL = agent_label(__name__)  # names this agent's run spans in Logfire traces

# Each tool call is a model round trip, so a few questions in one need headroom.
USAGE_LIMITS = UsageLimits(
    request_limit=12, total_tokens_limit=100_000, cost_limit=settings.cost_limit
)

# The server's own port, for a server you start yourself on it. The Docker service publishes on a random
# free host port instead, so set MCP_SERVER_URL to the address `docker compose port` shows (see service/).
DEFAULT_SERVER_URL = "http://127.0.0.1:8000/mcp"


def server_url_from_env() -> str:
    return os.environ.get("MCP_SERVER_URL", DEFAULT_SERVER_URL)


# --- Dependencies ---
@dataclass
class McpDeps:
    """Runtime dependencies for the MCP agent."""

    server_url: str = field(default_factory=server_url_from_env)


class McpServerUnavailable(Exception):
    """The MCP server could not be reached."""


# --- Output type ---
class Answer(BaseModel):
    # `result` is the conventional output field in these examples; the generated
    # eval starter reads it when present (see evals/helpers.py).
    result: str


# --- Agent ---
mcp_agent: Agent[McpDeps, Answer] = Agent(
    settings.model,
    name=LABEL,
    output_type=Answer,
    deps_type=McpDeps,
    capabilities=[RaiseContentFilterError()],
    instructions=(
        "You answer questions about dates and the calendar. Use the tools for every calculation "
        "and never work out dates yourself. Dates are written YYYY-MM-DD."
    ),
)


@mcp_agent.toolset(per_run_step=False)
def calendar_tools(ctx: RunContext[McpDeps]) -> MCPToolset:
    """The MCP server's tools, connected to the address in deps. Built once per run."""
    return MCPToolset(ctx.deps.server_url)


async def run_mcp(user_input: str, deps: McpDeps | None = None) -> RunResult[Answer]:
    """Answer a calendar question using the MCP server's tools.

    Returns:
        A RunResult: `.output` is the `Answer`; the MCP tool calls and results are in
        `.all_messages()`.

    Raises:
        McpServerUnavailable: When the server can't be reached.
    """
    if deps is None:
        deps = McpDeps()
    logger.info("Running MCP agent", extra={"user_input": user_input, "server": deps.server_url})
    flow = Flow(USAGE_LIMITS)
    try:
        result = await flow.run(mcp_agent, user_input, deps=deps)
    except RuntimeError as exc:
        # fastmcp reports a refused or unreachable connection as a RuntimeError; anything else is a bug.
        if "failed to connect" not in str(exc):
            raise
        raise McpServerUnavailable(
            f"No MCP server at {deps.server_url}. Start it with "
            "`docker compose -f examples/mcp_tools/service/docker-compose.yml up -d --wait`, "
            "or set MCP_SERVER_URL."
        ) from exc
    return flow.finish(result.output)


if __name__ == "__main__":
    import asyncio

    configure_logging()
    question = (
        "How many days are there from 2024-02-10 to 2024-03-01, and what weekday is 2024-12-25?"
    )
    print(asyncio.run(run_mcp(question)).output)
title = "MCP tools"
pattern = "mcp_tools"
summary = "Give an agent the tools of a Model Context Protocol server running as its own service: discovered at run time, called over the network, with server errors the model can correct."
smoke_input = "How many days are there from 2024-02-10 to 2024-03-01?"
# A real model must use the server's tool rather than count the days itself (checked by the release check).
expected_tools = ["days_between"]

# The agent is only a client and needs nothing beyond the template. The tests start the server
# locally, which needs fastmcp's server half; that is not part of using the agent, so add_agent.py
# does not install it into your project.
test_dependencies = ["fastmcp-slim[server]>=4.0,<5"]

# The server runs as a docker-compose service (service/). The release check starts it, finds the
# port Docker chose, and passes the address to the example in MCP_SERVER_URL.
services = ["mcp-server"]

[service.mcp-server]
port = 8000
env = "MCP_SERVER_URL"
url = "http://{address}/mcp"

[entrypoint]
deps = "McpDeps"
run = "run_mcp"
"""The calendar server, the agent that uses it over HTTP, and what happens when it is down — offline.

The tests that need the server half of fastmcp (`test_dependencies` in example.toml) run it as a
local subprocess (see conftest.py) and are skipped without it; the release check runs them in the
example's own environment.
"""

import importlib.util
import runpy
from pathlib import Path

import pytest
from pydantic_ai.messages import (
    ModelResponse,
    RetryPromptPart,
    ToolCallPart,
    ToolReturnPart,
)
from pydantic_ai.models.function import AgentInfo, FunctionModel

from examples.mcp_tools import agent as module
from examples.mcp_tools.agent import (
    DEFAULT_SERVER_URL,
    McpDeps,
    McpServerUnavailable,
    mcp_agent,
    run_mcp,
    server_url_from_env,
)

SERVER_PATH = Path(__file__).parent / "service" / "server.py"


@pytest.fixture(scope="module")
def calendar():
    """The server module, loaded by path (it is a standalone service, not part of the package)."""
    try:
        spec = importlib.util.spec_from_file_location("calendar_server", SERVER_PATH)
        loaded = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(loaded)
    except ImportError:
        pytest.skip("needs fastmcp-slim[server]")
    return loaded


# --- The server's tools, called directly (the @server.tool decorator leaves them ordinary functions) ---


def test_days_between_counts_across_a_leap_february(calendar):
    assert calendar.days_between("2024-02-10", "2024-03-01") == 20  # 2024 has a Feb 29
    assert calendar.days_between("2023-02-10", "2023-03-01") == 19


def test_days_between_is_zero_for_the_same_day_and_negative_going_backwards(calendar):
    assert calendar.days_between("2025-01-15", "2025-01-15") == 0
    assert calendar.days_between("2025-03-01", "2025-01-15") == -45


def test_add_days_crosses_month_boundaries_and_can_go_backwards(calendar):
    assert calendar.add_days("2025-01-15", 45) == "2025-03-01"
    assert calendar.add_days("2025-03-01", -45) == "2025-01-15"
    assert calendar.add_days("2025-12-31", 1) == "2026-01-01"


def test_weekday_names_the_day(calendar):
    assert calendar.weekday("2024-12-25") == "Wednesday"
    assert calendar.weekday("2000-01-01") == "Saturday"


def test_surrounding_whitespace_is_tolerated(calendar):
    assert calendar.parse_date(" 2025-03-01 ").isoformat() == "2025-03-01"


@pytest.mark.parametrize("bad", ["next tuesday", "2025-13-01", "2025-02-30", "", "03/01/2025"])
def test_a_malformed_date_raises_an_error_that_says_how_to_fix_it(calendar, bad):
    with pytest.raises(ValueError, match=r"Use the form YYYY-MM-DD"):
        calendar.parse_date(bad)


# --- Serving ---


def test_main_serves_over_http_on_the_configured_address(calendar, monkeypatch):
    served = {}
    monkeypatch.setattr(
        calendar.server, "run", lambda **kwargs: served.update(kwargs), raising=False
    )
    monkeypatch.setenv(
        "MCP_HOST", "0.0.0.0"
    )  # what the container sets, so the port can be published
    monkeypatch.setenv("MCP_PORT", "9123")
    calendar.main()
    assert served == {"transport": "http", "host": "0.0.0.0", "port": 9123}


def test_main_defaults_to_localhost_port_8000(calendar, monkeypatch):
    served = {}
    monkeypatch.setattr(
        calendar.server, "run", lambda **kwargs: served.update(kwargs), raising=False
    )
    monkeypatch.delenv("MCP_HOST", raising=False)
    monkeypatch.delenv("MCP_PORT", raising=False)
    calendar.main()
    assert served == {"transport": "http", "host": "127.0.0.1", "port": 8000}


def test_running_the_file_as_a_script_starts_the_server(calendar, monkeypatch):
    import fastmcp

    started = []
    monkeypatch.setattr(fastmcp.FastMCP, "run", lambda self, **kwargs: started.append(kwargs))
    runpy.run_path(str(SERVER_PATH), run_name="__main__")
    assert started and started[0]["transport"] == "http"


# --- The server, over real MCP and a real network connection ---


async def test_the_server_advertises_exactly_these_tools_with_descriptions(server_url):
    from fastmcp import Client

    async with Client(server_url) as client:
        tools = {tool.name: tool for tool in await client.list_tools()}
    assert set(tools) == {"days_between", "add_days", "weekday"}
    assert all(
        tool.description for tool in tools.values()
    )  # the model chooses tools by description


async def test_a_tool_call_over_the_network_returns_the_result(server_url):
    from fastmcp import Client

    async with Client(server_url) as client:
        result = await client.call_tool(
            "days_between", {"start": "2024-02-10", "end": "2024-03-01"}
        )
    assert result.data == 20


async def test_a_bad_argument_comes_back_over_the_network_as_an_error_naming_the_fix(server_url):
    from fastmcp import Client
    from fastmcp.exceptions import ToolError

    async with Client(server_url) as client:
        with pytest.raises(ToolError, match="YYYY-MM-DD"):
            await client.call_tool("weekday", {"day": "next tuesday"})


# --- The agent, using the server over HTTP, with a scripted model ---


def scripted(*steps: dict):
    """Follow `steps`: a tool call ({"tool", "args"}) or the final output ({"output"})."""
    remaining = list(steps)

    def model_fn(messages, info: AgentInfo) -> ModelResponse:
        step = remaining.pop(0)
        if "tool" in step:
            return ModelResponse(parts=[ToolCallPart(step["tool"], step["args"])])
        return ModelResponse(parts=[ToolCallPart(info.output_tools[0].name, step["output"])])

    return FunctionModel(model_fn)


def tool_returns(result) -> list[ToolReturnPart]:
    return [
        p
        for m in result.all_messages()
        for p in m.parts
        if isinstance(p, ToolReturnPart) and p.tool_name != "final_result"
    ]


def retries(result) -> list[RetryPromptPart]:
    return [p for m in result.all_messages() for p in m.parts if isinstance(p, RetryPromptPart)]


async def test_the_model_sees_the_servers_tools_and_calls_one_over_the_network(server_url):
    seen_tools: list[str] = []

    def model_fn(messages, info: AgentInfo) -> ModelResponse:
        if not seen_tools:
            seen_tools.extend(tool.name for tool in info.function_tools)
            args = {"start": "2024-02-10", "end": "2024-03-01"}
            return ModelResponse(parts=[ToolCallPart("days_between", args)])
        return ModelResponse(parts=[ToolCallPart(info.output_tools[0].name, {"result": "20 days"})])

    with mcp_agent.override(model=FunctionModel(model_fn)):
        result = await run_mcp("How many days?", McpDeps(server_url))

    assert set(seen_tools) == {"days_between", "add_days", "weekday"}  # discovered, not hard-coded
    assert [str(r.content) for r in tool_returns(result)] == ["20"]  # computed by the service
    assert result.output.result == "20 days"
    assert [step.agent for step in result.steps] == ["mcp_tools"]


async def test_a_server_error_is_shown_to_the_model_which_then_corrects_its_call(server_url):
    model = scripted(
        {"tool": "weekday", "args": {"day": "next tuesday"}},
        {"tool": "weekday", "args": {"day": "2025-03-04"}},
        {"output": {"result": "Tuesday"}},
    )
    with mcp_agent.override(model=model):
        result = await run_mcp("What weekday is next tuesday?", McpDeps(server_url))

    assert len(retries(result)) == 1 and "YYYY-MM-DD" in str(retries(result)[0].content)
    assert [str(r.content) for r in tool_returns(result)] == ["Tuesday"]


async def test_several_tools_can_be_used_in_one_run(server_url):
    model = scripted(
        {"tool": "add_days", "args": {"start": "2025-01-15", "days": 45}},
        {"tool": "weekday", "args": {"day": "2025-03-01"}},
        {"output": {"result": "2025-03-01 is a Saturday"}},
    )
    with mcp_agent.override(model=model):
        result = await run_mcp("45 days after 2025-01-15, and the weekday?", McpDeps(server_url))
    assert [str(r.content) for r in tool_returns(result)] == ["2025-03-01", "Saturday"]


# --- The address comes from deps ---


def test_the_server_address_defaults_to_the_local_service(monkeypatch):
    monkeypatch.delenv("MCP_SERVER_URL", raising=False)
    assert server_url_from_env() == DEFAULT_SERVER_URL == "http://127.0.0.1:8000/mcp"
    assert McpDeps().server_url == DEFAULT_SERVER_URL


def test_the_server_address_can_be_set_in_the_environment(monkeypatch):
    monkeypatch.setenv("MCP_SERVER_URL", "http://staging.internal:9000/mcp")
    assert McpDeps().server_url == "http://staging.internal:9000/mcp"
    assert McpDeps("http://elsewhere/mcp").server_url == "http://elsewhere/mcp"  # deps win


# --- When the server is not there ---


async def test_an_unreachable_server_is_reported_with_where_it_looked_and_how_to_start_it():
    dead = McpDeps("http://127.0.0.1:1/mcp")  # nothing listens on port 1
    with (
        mcp_agent.override(model=scripted({"output": {"result": "never"}})),
        pytest.raises(McpServerUnavailable) as failure,
    ):
        await run_mcp("How many days?", dead)
    message = str(failure.value)
    assert "http://127.0.0.1:1/mcp" in message and "docker compose" in message
    assert isinstance(failure.value.__cause__, RuntimeError)  # the original error is kept


async def test_other_runtime_errors_are_not_mistaken_for_a_missing_server(monkeypatch):
    def broken(url: str):
        raise RuntimeError("the tool schema was invalid")

    monkeypatch.setattr(module, "MCPToolset", broken)
    with (
        mcp_agent.override(model=scripted({"output": {"result": "never"}})),
        pytest.raises(RuntimeError, match="tool schema was invalid") as failure,
    ):
        await run_mcp("How many days?", McpDeps("http://127.0.0.1:1/mcp"))
    assert not isinstance(failure.value, McpServerUnavailable)  # passed through unchanged
"""Live check: the model uses the tools of an MCP server running as a Docker service. `-m eval`.

The release check (scripts/release_check.py) builds and starts the service, then runs this with its
address in MCP_SERVER_URL. To run it by hand: start the service, then set the variable:

    docker compose -f examples/mcp_tools/service/docker-compose.yml up -d --wait
    export MCP_SERVER_URL=http://127.0.0.1:$(docker compose -f examples/mcp_tools/service/docker-compose.yml port mcp-server 8000 | cut -d: -f2)/mcp
"""

import os

import pytest

from evals.trace import traced_run
from examples.live_support import assert_every_agent_ran, run_as_script
from examples.mcp_tools import agent as module

pytestmark = pytest.mark.eval

if not os.environ.get("MCP_SERVER_URL"):
    pytest.skip("needs the calendar service running (MCP_SERVER_URL)", allow_module_level=True)


@pytest.fixture(scope="module")
async def leap_days():
    return await traced_run(
        module.run_mcp, "How many days are there from 2024-02-10 to 2024-03-01?"
    )


@pytest.fixture(scope="module")
async def forward():
    return await traced_run(module.run_mcp, "What date is 45 days after 2025-01-15?")


@pytest.fixture(scope="module")
async def day_name():
    return await traced_run(module.run_mcp, "What day of the week is 2024-12-25?")


async def test_a_calendar_question_is_answered_with_the_services_exact_result(leap_days):
    assert "days_between" in leap_days.tools_called  # it used the tool rather than counting itself
    assert (
        "20" in leap_days.result.output.result
    )  # 2024 is a leap year: 19 would be the naive answer


async def test_date_arithmetic_comes_from_the_tool(forward):
    assert "add_days" in forward.tools_called
    assert "2025-03-01" in forward.result.output.result


async def test_a_weekday_question_uses_the_weekday_tool(day_name):
    assert "weekday" in day_name.tools_called
    assert "wednesday" in day_name.result.output.result.lower()


async def test_the_tool_results_in_the_history_are_the_services_not_the_models(
    leap_days, forward, day_name
):
    """The tool-return parts came back over the network from the service; the answers must agree."""
    for traced, expected in ((leap_days, "20"), (forward, "2025-03-01"), (day_name, "Wednesday")):
        returns = [
            str(part.content)
            for message in traced.result.all_messages()
            for part in message.parts
            if type(part).__name__ == "ToolReturnPart" and part.tool_name != "final_result"
        ]
        assert expected in returns


async def test_the_agent_talks_to_the_service_not_to_something_in_process(leap_days):
    """The address is the one Docker published, so the calls really crossed a network boundary."""
    assert module.McpDeps().server_url == os.environ["MCP_SERVER_URL"]
    assert module.McpDeps().server_url.startswith("http://127.0.0.1:")


async def test_the_agent_ran(leap_days, forward, day_name):
    assert_every_agent_ran(module, leap_days.agents_ran | forward.agents_ran | day_name.agents_ran)


async def test_the_demo_script_runs():
    out = await run_as_script("examples.mcp_tools.agent")
    assert "result=" in out
# The calendar MCP server as a container image. Built and started by docker-compose.yml.
FROM python:3.13-slim

RUN pip install --no-cache-dir "fastmcp-slim[server]>=4.0,<5"

WORKDIR /app
COPY server.py .

# Listen on all interfaces inside the container; Compose publishes the port to 127.0.0.1 only.
ENV MCP_HOST=0.0.0.0 MCP_PORT=8000
EXPOSE 8000

CMD ["python", "server.py"]
# The calendar MCP server, as a service. Start it by hand with:
#     docker compose up -d --wait
# scripts/release_check.py starts and stops it for you when it checks the example.
services:
  mcp-server:
    build: .
    ports:
      # Publish on 127.0.0.1 only, on a free host port (the first field is left empty so Docker
      # picks one); `docker compose port mcp-server 8000` says which.
      - "127.0.0.1::8000"
    healthcheck:
      # Healthy once the server accepts connections, so `up --wait` returns when it is ready.
      test: ["CMD", "python", "-c", "import socket; socket.create_connection(('127.0.0.1', 8000), 2).close()"]
      interval: 1s
      timeout: 3s
      retries: 30
"""The calendar MCP server: a standalone service that an agent connects to over HTTP.

This is the *other half* of the `mcp_tools` example. It knows nothing about agents or models: it
exposes three date tools over the Model Context Protocol, and any MCP client (the agent in
../agent.py, an IDE, another application) can call them. It runs as its own process, started by
Docker Compose (see docker-compose.yml) or directly:

    uv run --with "fastmcp-slim[server]>=4.0,<5" python server.py     # http://127.0.0.1:8000/mcp

Dates are a good fit for a tool: models are unreliable at calendar arithmetic, and a tool makes it
exact. Replace these tools with your own; the agent discovers whatever the server offers.
"""

from __future__ import annotations

import os
from datetime import date, timedelta

from fastmcp import FastMCP

server = FastMCP("calendar-tools")


def parse_date(text: str) -> date:
    """A date from `YYYY-MM-DD`, or a ValueError the model can read and fix."""
    try:
        return date.fromisoformat(text.strip())
    except ValueError:
        raise ValueError(
            f"{text!r} is not a date. Use the form YYYY-MM-DD, e.g. 2025-03-01."
        ) from None


@server.tool
def days_between(start: str, end: str) -> int:
    """The number of days from `start` to `end`: negative if `end` is earlier. Dates are YYYY-MM-DD."""
    return (parse_date(end) - parse_date(start)).days


@server.tool
def add_days(start: str, days: int) -> str:
    """The date `days` days after `start` (before it, if negative), as YYYY-MM-DD."""
    return (parse_date(start) + timedelta(days=days)).isoformat()


@server.tool
def weekday(day: str) -> str:
    """The day of the week a date falls on, e.g. "Wednesday". The date is YYYY-MM-DD."""
    return parse_date(day).strftime("%A")


def main() -> None:
    """Serve over streamable HTTP. Inside a container MCP_HOST is 0.0.0.0 so the port can be published."""
    host = os.environ.get("MCP_HOST", "127.0.0.1")
    port = int(os.environ.get("MCP_PORT", "8000"))
    server.run(transport="http", host=host, port=port)


if __name__ == "__main__":
    main()

Recorded run · gemini-3.1-flash-lite · 1 step · $0.0003

Recorded 2026-10-07 with google:gemini-3.1-flash-lite · 1 step · 793 tokens · $0.0003 · 2.1 s. Model output varies between runs. Regenerate with uv run python scripts/record_example.py mcp_tools.

Input

How many days are there from 2024-02-10 to 2024-03-01?

Steps

1. mcp_tools 793 tokens · $0.0003

Prompt

How many days are there from 2024-02-10 to 2024-03-01?

What happened - called days_between({"start": "2024-02-10", "end": "2024-03-01"}) - days_between returned: 20

Output

{
  "result": "There are 20 days from 2024-02-10 to 2024-03-01."
}

Result

run_mcp(...).output

{
  "result": "There are 20 days from 2024-02-10 to 2024-03-01."
}