---
title: Adding probabilistic forecasting to LangChain, the OpenAI Agents SDK and the Claude API
description: Minimal code to give a LangChain, OpenAI Agents SDK or Claude API agent a forecasting tool, and the tool description that makes it behave.
date: 2026-10-07
updated: 2026-10-07
summary: Each of the three frameworks can reach a forecasting service two ways, through its MCP support or through a small function tool that POSTs to a REST endpoint. MCP is less code; a function tool lets you write the tool description yourself, and that description decides when the model calls the tool, whether it invents history, and whether it reports the uncertainty band. The examples use Ephemeris (https://ephemeris.cascade.industries) and read the key from EPHEMERIS_API_KEY.
tags: agents, langchain, openai-agents-sdk, claude-api, forecasting
draft: false
---

## Key facts

- LangChain 1.4 and later ships MCP support in the `langchain.mcp` namespace (`MCPAdapter`, in beta), installed with `pip install "langchain[mcp]"`; it replaces the separate `langchain-mcp-adapters` package.
- The OpenAI Agents SDK connects to remote MCP servers with `MCPServerStreamableHttp`, which accepts an `Authorization` header in `params["headers"]`.
- The Claude API calls remote MCP servers itself through the MCP connector (beta header `mcp-client-2025-11-20`); the Python SDK's `@beta_tool` decorator and `tool_runner` handle local function tools.
- The Ephemeris forecast endpoint is `POST https://ephemeris.cascade.industries/api/v1/forecast`; one request takes 1 to 64 series, a horizon of 1 to 512 steps and up to 21 quantile levels.
- Ephemeris replays a retried request that carries the same `Idempotency-Key` header without charging again; a key reused with a different body returns HTTP 409.
- One series with up to 1,024 points of context and a horizon up to 64 steps costs exactly one model's rate per model run; rates are listed on /pricing and by `GET /api/v1/models`.

## MCP or a function tool: which should I use?

Use MCP when the framework supports it and you are happy with the server's own tool descriptions; use a function tool when you want to control the description, the inputs or the output. Both reach the same forecasting pipeline.

| | MCP | Function tool over REST |
|---|---|---|
| Code you write | Connection setup only | A function of about 20 lines |
| Tools the model sees | All four: `forecast`, `list_models`, `get_balance`, `get_usage` | Only what you define |
| Tool description | Written by the server | Written by you |
| Output the model sees | The full response | Whatever you return, for example only the quantiles, models used and cost |
| Extra dependency | The framework's MCP extra | `requests` |

A *function tool* is an ordinary function the framework describes to the model with a JSON schema built from its signature and docstring. When the model asks to call it, the framework runs the function and passes the return value back to the model.

## What should the tool description say?

The description is the only instruction the model reads at the moment it decides whether and how to call the tool, so it should answer three questions: when to call, what to send, and what to report. This is the wording we use in the examples below:

```python
FORECAST_DESCRIPTION = """Forecast a numeric time series and return quantile forecasts (uncertainty bands).

Call this only when the user asks for a forecast, projection or plan for a numeric series
and has supplied its history. Do not call it speculatively or repeatedly.

Send only the history the user gave you, oldest first. Never invent, pad or interpolate values;
if points are missing, say so instead. Horizon is a number of steps at the series' frequency.

Report the band, not only the median: give the 10th-90th percentile range, say it is an 80%
interval, and mention the models used and the credits charged."""
```

Each line targets a failure we have seen in our own testing. Without "only when ... has supplied its history", models call the tool to look busy or with numbers they made up. Without "never invent, pad or interpolate", a model given a gappy series tends to fill the gaps before sending it. Without "report the band", most answers quote only the median line and drop the uncertainty, which is the part a planner needs (see [Agents that decide under uncertainty: using forecast quantiles, not just the median](/blog/agents-that-decide-under-uncertainty)).

When you connect over MCP, the server's description is used instead, and you steer the same behaviour from the system prompt or agent instructions. The examples pass `FORECAST_DESCRIPTION` there in the MCP versions.

## What does the shared REST call look like?

All three function-tool examples call the same helper. It sends one series, derives the idempotency key from the request body so an identical retry is not charged twice, and returns a compact JSON string for the model:

```python
import hashlib
import json
import os

import requests

EPHEMERIS_URL = "https://ephemeris.cascade.industries/api/v1/forecast"


def call_ephemeris(values: list[float], horizon: int, freq: str | None = None,
                   quantiles: list[float] | None = None) -> str:
    series = {"values": values}
    if freq:
        series["freq"] = freq
    body = {
        "mode": "route",
        "series": [series],
        "horizon": horizon,
        "quantiles": quantiles or [0.1, 0.5, 0.9],
    }
    digest = hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()
    response = requests.post(
        EPHEMERIS_URL,
        headers={
            "Authorization": f"Bearer {os.environ['EPHEMERIS_API_KEY']}",
            "Idempotency-Key": f"fc-{digest[:40]}",
        },
        json=body,
        timeout=120,
    )
    if not response.ok:
        return f"Forecast failed with HTTP {response.status_code}: {response.text[:500]}"
    data = response.json()
    meta = data["meta"]
    return json.dumps({
        "quantiles": data["forecasts"][0]["quantiles"],
        "models_used": meta["models_used"],
        "notes": meta.get("notes"),
        "credits_charged": int(meta["billing"]["settled_mc"]) / 1000,
    })
```

Returning the error text instead of raising lets the model explain it: a 402 means the account is out of credits and the user should top up, a 400 names the field that was wrong. Save it, together with `FORECAST_DESCRIPTION` from the previous section, as `ephemeris_tool.py`; the examples import both from there. Request and response fields are documented in [/llms-full.txt](/llms-full.txt).

## How do I add forecasting to a LangChain agent?

Pass a `@tool` function or the tools from `MCPAdapter` to `create_agent`. These examples need `pip install "langchain[mcp]" langchain-anthropic requests` and `ANTHROPIC_API_KEY` in the environment; any chat model LangChain supports works in place of `anthropic:claude-opus-5-5`.

### LangChain with a function tool

`@tool` builds the schema from the type hints and uses the docstring as the description, so the docstring carries the guidance:

```python
from langchain.agents import create_agent
from langchain.tools import tool

from ephemeris_tool import call_ephemeris


@tool
def forecast(values: list[float], horizon: int, freq: str | None = None) -> str:
    """Forecast a numeric time series and return quantile forecasts (uncertainty bands).

    Call this only when the user asks for a forecast of a numeric series and has supplied
    its history. Send only the history the user gave you, oldest first; never invent, pad
    or interpolate values. Report the 10th-90th percentile band (an 80% interval), not only
    the median, plus the models used and the credits charged.

    Args:
        values: The observed history, oldest first.
        horizon: Number of future steps to forecast, 1 to 512.
        freq: Pandas-style frequency such as "H", "D" or "W", if known.
    """
    return call_ephemeris(values, horizon, freq)


agent = create_agent(model="anthropic:claude-opus-5-5", tools=[forecast])
result = agent.invoke({"messages": [{
    "role": "user",
    "content": "Daily orders, oldest first: 120, 132, 128, 141, 150, 138, 129, 125, 137, 144, "
               "152, 160, 149, 133, 131, 140, 151, 158, 166, 154, 139. Forecast the next 7 days.",
}]})
print(result["messages"][-1].content)
```

### LangChain over MCP

`MCPAdapter` takes a URL, or a `fastmcp` `Client` when the server needs a bearer token. `auth` accepts the token string and sends it as `Authorization: Bearer <token>`:

```python
import asyncio
import os

from fastmcp.client import Client
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter

from ephemeris_tool import FORECAST_DESCRIPTION

URL = "https://ephemeris.cascade.industries/api/mcp"


async def main():
    client = Client(URL, auth=os.environ["EPHEMERIS_API_KEY"])
    async with MCPAdapter(client) as adapter:
        tools = await adapter.list_tools()
        agent = create_agent(
            model="anthropic:claude-opus-5-5",
            tools=tools,
            system_prompt=FORECAST_DESCRIPTION,
        )
        result = await agent.ainvoke({"messages": [{
            "role": "user",
            "content": "List the available forecasting models and their maximum horizons.",
        }]})
        print(result["messages"][-1].content)


asyncio.run(main())
```

The `langchain.mcp` namespace is in beta and imports with a `LangChainBetaWarning`. Code written for `langchain-mcp-adapters` and its `MultiServerMCPClient` still needs that package; LangChain's migration guide maps it to `MCPAdapter`. References: [LangChain MCP](https://docs.langchain.com/oss/python/langchain/mcp), [MCP authentication](https://docs.langchain.com/oss/python/langchain/mcp/auth), [Tools](https://docs.langchain.com/oss/python/langchain/tools), [Agents](https://docs.langchain.com/oss/python/langchain/agents), [migration from langchain-mcp-adapters](https://docs.langchain.com/oss/python/migrate/langchain-mcp-adapters).

## How do I add forecasting to an OpenAI Agents SDK agent?

Decorate a function with `@tool` and list it in `Agent(tools=...)`, or pass an `MCPServerStreamableHttp` in `Agent(mcp_servers=...)`. Install with `pip install openai-agents requests` and set `OPENAI_API_KEY`. The examples leave `model` unset, so the SDK's default model is used.

### Agents SDK with a function tool

The SDK reads the function name, docstring and type hints to build the tool:

```python
from agents import Agent, Runner
from agents.decorators import tool

from ephemeris_tool import call_ephemeris


@tool
def forecast(values: list[float], horizon: int, freq: str | None = None) -> str:
    """Forecast a numeric time series and return quantile forecasts (uncertainty bands).

    Call this only when the user asks for a forecast of a numeric series and has supplied
    its history. Send only the history the user gave you, oldest first; never invent, pad
    or interpolate values. Report the 10th-90th percentile band (an 80% interval), not only
    the median, plus the models used and the credits charged.

    Args:
        values: The observed history, oldest first.
        horizon: Number of future steps to forecast, 1 to 512.
        freq: Pandas-style frequency such as "H", "D" or "W", if known.
    """
    return call_ephemeris(values, horizon, freq)


agent = Agent(name="Planner", instructions="Help the user plan with forecasts.", tools=[forecast])
result = Runner.run_sync(agent, "Weekly sign-ups, oldest first: 410, 432, 455, 448, 470, 492, 501, "
                                "488, 515, 530, 547, 539, 560, 581, 574, 596. Forecast 8 weeks ahead.")
print(result.final_output)
```

### Agents SDK over MCP

`MCPServerStreamableHttp` opens the connection in an `async with` block and lists the server's tools for the agent. The SDK's documentation recommends an `Authorization` header over other auth options because it works with both major versions of the `mcp` package:

```python
import asyncio
import os

from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp

from ephemeris_tool import FORECAST_DESCRIPTION


async def main():
    async with MCPServerStreamableHttp(
        name="Ephemeris",
        params={
            "url": "https://ephemeris.cascade.industries/api/mcp",
            "headers": {"Authorization": f"Bearer {os.environ['EPHEMERIS_API_KEY']}"},
            "timeout": 30,
        },
        cache_tools_list=True,
    ) as server:
        agent = Agent(name="Planner", instructions=FORECAST_DESCRIPTION, mcp_servers=[server])
        result = await Runner.run(agent, "List the available forecasting models.")
        print(result.final_output)


asyncio.run(main())
```

To hide `get_usage` or other tools, pass `tool_filter=create_static_tool_filter(allowed_tool_names=["forecast", "list_models"])` (imported from `agents.mcp`). The SDK also has `HostedMCPTool`, which asks OpenAI's Responses API to call a public MCP server on the model's behalf; use it when you do not want your process to hold the connection. References: [Agents SDK MCP](https://openai.github.io/openai-agents-python/mcp/), [Agents SDK tools](https://openai.github.io/openai-agents-python/tools/).

## How do I add forecasting to the Claude API?

Use the MCP connector to let Anthropic's servers call the forecasting server, or define a function tool with `@beta_tool` and let `tool_runner` run the loop. Install with `pip install anthropic requests` and set `ANTHROPIC_API_KEY`.

### Claude API with a function tool

`@beta_tool` turns the function's type hints and docstring into a tool definition. `tool_runner` calls the API, runs any tool Claude asks for, sends the result back, and repeats until Claude finishes; `until_done()` returns the final message:

```python
import anthropic
from anthropic import beta_tool

from ephemeris_tool import call_ephemeris

client = anthropic.Anthropic()


@beta_tool
def forecast(values: list[float], horizon: int, freq: str | None = None) -> str:
    """Forecast a numeric time series and return quantile forecasts (uncertainty bands).

    Call this only when the user asks for a forecast of a numeric series and has supplied
    its history. Send only the history the user gave you, oldest first; never invent, pad
    or interpolate values. Report the 10th-90th percentile band (an 80% interval), not only
    the median, plus the models used and the credits charged.

    Args:
        values: The observed history, oldest first.
        horizon: Number of future steps to forecast, 1 to 512.
        freq: Pandas-style frequency such as "H", "D" or "W", if known.
    """
    return call_ephemeris(values, horizon, freq)


runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    max_tokens=16000,
    tools=[forecast],
    messages=[{"role": "user", "content": "Hourly CPU %, oldest first: 41, 44, 47, 52, 58, 63, 61, "
               "57, 50, 46, 43, 42, 44, 48, 53, 59, 64, 62, 56, 51, 47, 44, 42, 41. "
               "Forecast the next 12 hours."}],
)
final = runner.until_done()
print("".join(block.text for block in final.content if block.type == "text"))
```

### Claude API over the MCP connector

The connector needs no MCP client in your code: declare the server in `mcp_servers`, enable its tools with an `mcp_toolset` entry, and send the beta header. The tool calls come back as `mcp_tool_use` and `mcp_tool_result` blocks in the response:

```python
import os

import anthropic

from ephemeris_tool import FORECAST_DESCRIPTION

client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=16000,
    betas=["mcp-client-2025-11-20"],
    system=FORECAST_DESCRIPTION,
    mcp_servers=[{
        "type": "url",
        "url": "https://ephemeris.cascade.industries/api/mcp",
        "name": "ephemeris",
        "authorization_token": os.environ["EPHEMERIS_API_KEY"],
    }],
    tools=[{
        "type": "mcp_toolset",
        "mcp_server_name": "ephemeris",
        "default_config": {"enabled": False},
        "configs": {"forecast": {"enabled": True}, "list_models": {"enabled": True}},
    }],
    messages=[{"role": "user", "content": "List the available forecasting models."}],
)
print("".join(block.text for block in response.content if block.type == "text"))
```

The `default_config` and `configs` fields form an allowlist: every tool is off except `forecast` and `list_models`. Drop them to enable all four tools. The connector supports MCP tools only, needs a publicly reachable server, is not eligible for zero data retention, and is not available on Amazon Bedrock or Google Cloud. A newer beta header, `mcp-client-2026-09-15`, includes everything in `mcp-client-2025-11-20` and adds pinning of a server's tool list. References: [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector), [Tool runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner), [anthropic-sdk-python tools.md](https://github.com/anthropics/anthropic-sdk-python/blob/main/tools.md).

## What about other frameworks or no framework?

Any framework that can call a Python function can use `call_ephemeris` as written, and any that speaks MCP can use the server URL and bearer header. Setup for desktop and IDE clients (Claude Code, Cursor, VS Code, Claude Desktop, OpenClaw, Hermes Agent) is in [How to give Claude, Cursor or any AI agent a forecasting tool (MCP)](/blog/forecasting-tool-for-ai-agents-mcp).

You do not need a hosted service at all if you would rather run a model in-process: Chronos-2, TimesFM 2.5, Toto 2, TiRex-2 and FlowState are published on Hugging Face under Apache-2.0, so the body of `call_ephemeris` can load one of them instead. That suits teams that must keep data in their own network or already run GPUs. The hosted call saves serving and updating the models and gives you routing and an ensemble across them; [Which time-series foundation model should I use? A decision guide](/blog/which-forecasting-model-should-i-use) compares the choices.

## How do I keep costs predictable in an agent loop?

Agents retry, and loops can call a tool many times, so cost control belongs in the tool, not only in the prompt:

- Derive the idempotency key from the request body, as `call_ephemeris` does, so a retried identical call is replayed without a second charge.
- Return errors as text. On a 402 the model should tell the user to top up rather than retry.
- Batch: one request takes up to 64 series and 256 slots (a slot is one variate of one series), and is cheaper to reason about than 64 tool calls.
- Use `route` mode by default; it usually runs one model. `ensemble` runs every compatible model and costs more.
- Log `meta.billing.settled_mc` from each response; it is the exact cost of the call in millicredits (1 credit = 1,000 mc).

## FAQ

### Is langchain-mcp-adapters still the way to use MCP in LangChain?

Not for new code on LangChain 1.4 or later. MCP support now ships in `langchain.mcp` as `MCPAdapter` (in beta), and LangChain publishes a migration guide from `MultiServerMCPClient`.

### Why put the guidance in the tool description instead of the system prompt?

The description travels with the tool into every agent that uses it, and the model reads it at the moment it chooses the tool. With MCP you cannot edit the server's description, so the system prompt or agent instructions carry the same rules instead.

### Should the tool return the full forecast response?

Return what the model needs to answer: the quantiles, the models used, any notes and the cost. Dropping request IDs and weight revisions keeps the tool result short, which saves context in long agent runs.

### Does the Claude API MCP connector work on Amazon Bedrock or Google Cloud?

No. Anthropic's documentation lists it as a beta on the Claude API, Claude Platform on AWS and Microsoft Foundry, and not available on Amazon Bedrock or Google Cloud. Use a function tool there instead.

### Which quantiles should I ask for?

Ask for the levels that answer the question: `[0.1, 0.5, 0.9]` gives an 80% interval and a median, `[0.05, 0.5, 0.95]` a 90% interval. Up to 21 levels strictly between 0 and 1 are allowed per request.

## Related

- [How to give Claude, Cursor or any AI agent a forecasting tool (MCP)](/blog/forecasting-tool-for-ai-agents-mcp)
- [The split that works: the LLM reads the context, a forecasting model does the numbers](/blog/llm-for-context-forecaster-for-numbers)
- [Agents that decide under uncertainty: using forecast quantiles, not just the median](/blog/agents-that-decide-under-uncertainty)
- [Does ensembling forecasting foundation models help? Results on four benchmarks](/blog/does-ensembling-forecasting-models-help)
- [Which time-series foundation model should I use? A decision guide](/blog/which-forecasting-model-should-i-use)
- API reference: [/llms-full.txt](/llms-full.txt) and [/docs](/docs)
