Blog · Updated
Adding probabilistic forecasting to LangChain, the OpenAI Agents SDK and the Claude API
Short answer
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.
Key facts
- LangChain 1.4 and later ships MCP support in the
langchain.mcpnamespace (MCPAdapter, in beta), installed withpip install "langchain[mcp]"; it replaces the separatelangchain-mcp-adapterspackage. - The OpenAI Agents SDK connects to remote MCP servers with
MCPServerStreamableHttp, which accepts anAuthorizationheader inparams["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_tooldecorator andtool_runnerhandle 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-Keyheader 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:
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).
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:
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.
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:
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>:
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, MCP authentication, Tools, Agents, migration from 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:
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:
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, Agents SDK 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:
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:
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, Tool runner, anthropic-sdk-python 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).
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 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_ephemerisdoes, 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
routemode by default; it usually runs one model.ensembleruns every compatible model and costs more. - Log
meta.billing.settled_mcfrom 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)
- The split that works: the LLM reads the context, a forecasting model does the numbers
- Agents that decide under uncertainty: using forecast quantiles, not just the median
- Does ensembling forecasting foundation models help? Results on four benchmarks
- Which time-series foundation model should I use? A decision guide
- API reference: /llms-full.txt and /docs