From Script to Tool: What It Actually Takes to Wrap Python in MCP
2026-07-26 • Tool Lab, MCP, Python, AWS • Sam Madireddy
A little while back I wanted a way to separate stocks that climbed steadily from ones that just had one good week, a five-line rule, maybe an afternoon of Python. What actually took the time, and what this post is really about, was turning that function into something callable by anything other than me: an agent, a script, a UI widget, all without writing a one-off integration for each.
That's what MCP is for. The screener is just the working example.
👉 How a decorator turns a plain Python function into an agent-callable tool
👉 What you'd use instead of FastMCP, and what you'd give up
👉 The tool's current output, rendered live below
👉 What's actually happening inside the screener: the pandas mechanics behind it
👉 Where this goes next: an on-demand endpoint, and wiring it into an agent

1. From Script to Tool: What @mcp.tool() Does
A screener like this is only useful to me until it's sitting in a Jupyter cell no one else can call. MCP (the Model Context Protocol) is what turns it into something an agent, or a website, can call the same way it'd call any other tool.
mcp = FastMCP("stock-screener")
@mcp.tool()
def screen_steady_gainers(days: int = 30, top_n: int = 20) -> dict:
"""Find non-crypto stocks that rose in EVERY week of the trailing window."""
return _screen_steady_gainers(days=days, top_n=top_n)
The decorator reads the function's type hints and docstring and generates a tool spec from them automatically: name, input schema, description. No separate config file to keep in sync. Add a parameter, and the schema an agent sees updates itself.
🎯 Why this matters: the moment a script becomes an MCP tool, it stops being "my script" and starts being infrastructure. The same forecasting-agent pattern from my predictive-agents post could call this screener as one more input to a decision, without a bespoke integration.
2. What You'd Use Instead of FastMCP
FastMCP isn't the only way to make a function callable. It's one point on a spectrum, and worth knowing what you'd trade for each:
| Approach | What you'd give up |
|---|---|
| A plain REST endpoint (FastAPI, Flask) | Nothing describes the function to a caller automatically. You write the OpenAPI schema by hand, and every client needs to already know the URL and shape. |
Framework-native tool decorators (LangChain's @tool, LlamaIndex's FunctionTool) | Same auto-schema convenience as FastMCP, but the tool only exists inside that framework's process. A different agent stack can't call it without re-wrapping the same function. |
The raw MCP Server class | Same protocol, same wire format as FastMCP, but you register handlers and write the JSON Schema by hand. |
Provider-specific function-calling schemas (tools=[...]-style definitions tied to one model API) | Neither the schema nor the transport travels outside that one integration. |
The thing MCP buys you over all four: it's a protocol, not a framework feature or a hand-maintained contract. One server process, one schema, callable by any MCP-speaking agent or client, or (via a thin HTTP shim) a website widget like the one below.
3. See It Live: Top 30-Day Steady Gainers
Here's what the tool returns right now, rendered straight into this page, ranked by total return over the trailing 30 days:
4. Inside the Screener
The rule itself is small: collapse daily closes into one closing price per ISO week, then require every week's close to beat the previous week's. No exceptions: a single down week disqualifies the ticker, even if the overall window still gained.
closes = [w["close"] for w in weekly]
weekly_gains_pct = [
(closes[i] - closes[i-1]) / closes[i-1] * 100
for i in range(1, len(closes))
]
is_steady = all(g > 0 for g in weekly_gains_pct) and total_change_pct > 0
Getting to weekly is the more interesting part. Daily closes get bucketed by ISO week and reduced to the last trading day's close in each bucket. That's a groupby on Timestamp.isocalendar(), not a manual date-math loop:
iso = close_series.index.isocalendar()
grouped = close_series.groupby([iso["year"], iso["week"]])
weekly = grouped.last()
And pulling history for ~500 tickers is one batched call to the market data source, not a loop of 500 requests:
raw = market_data.download(
tickers=tickers,
period=f"{lookback_days}d",
interval="1d",
group_by="ticker",
threads=True,
)
Each result gets wrapped in a small typed dataclass (TickerTrend) rather than passed around as a bag of dict keys. One to_dict() method is the only place that decides what the outside world actually sees.
5. What's Next
Two directions from here: wire this screener into an agent the way the demand-forecasting example did in the predictive-agents post, so "steady gainer" becomes one signal among several an agent reasons over, or expose it as a live, on-demand endpoint for anyone who wants fresher-than-daily data. Both are straightforward extensions of the same MCP server; nothing about today's version needs to be thrown away to get there.
If you'd like to bounce ideas about MCP, market data tooling, or where the two overlap, feel free to reach out.
Sam Madireddy
Contact me on LinkedIn