DEV Community

Cover image for Thirty Lines Between a Question and an API
Jaroslav Šmarda
Jaroslav Šmarda

Posted on Originally published at Medium

Thirty Lines Between a Question and an API

Building a tiny MCP server, and who does what

At the end of the last part I promised to look at the division of labour: what the model does in seconds, and what still takes a person who knows
where to look. I found the best way to look at it was to build something.

Because there was one question I'd been carrying for months, and was a little embarrassed to ask. I've called APIs for most of my working life. I've
used MCP servers for this whole series. But how does one become the other?

"How do you get from an API to an MCP server?"

You wrap it. An MCP server is a small program that sits in front of an API and translates. On one side it speaks to me; on the other it speaks to
the API.

"That's all?"

That's most of it. It does three things. It tells me what tools it has. When I ask for one, it turns my request into a real API call. And it hands
the answer back.


A menu for a new kind of guest

The picture that finally made it click for me was a restaurant.

The kitchen is the data: prices, filings, everything EODHD has. The guest is a program that wants something from it. Between them stands the waiter,
who takes the order to the kitchen and brings the plate back. That waiter is the API. And the menu is the list of what the API can do, written in its
own precise language: this address, these parameters, this format.

For thirty years, the guests were programs written by people like me. We read the menu once, carefully, and wrote the order into our code.

And now the guest is me. I can't read the API's menu the way your code does, and I shouldn't hold its keys. So someone writes me a menu of my own:
a short list of dishes, each with a plain-language description, and a waiter who knows how to turn my order into the kitchen's language.

That menu, and that waiter, are the MCP server. Same kitchen, same API. Just a menu translated for a new kind of guest.


Thirty lines

Here is the whole server. One tool, one API call:

"""A minimal MCP server: one tool, wrapped around one EODHD API call."""
import os


import requests
from mcp.server.mcpserver import MCPServer


mcp = MCPServer("mini-eodhd")




@mcp.tool()
def get_close_price(ticker: str, date: str) -> str:
    """Return the raw closing price of a stock on one trading day.


    ticker: symbol with exchange suffix, e.g. AAPL.US
    date: trading day as YYYY-MM-DD
    The price is the raw close, not adjusted for later splits.
    """
    r = requests.get(
        f"https://eodhd.com/api/eod/{ticker}",
        params={"api_token": os.environ["EODHD_API_KEY"],
                "fmt": "json", "from": date, "to": date},
        timeout=30,
    )
    r.raise_for_status()
    rows = r.json()
    if not rows:
        return f"No trading data for {ticker} on {date} (weekend or holiday?)"
    return f"{ticker} closed at {rows[0]['close']} on {date}"




if __name__ == "__main__":
    mcp.run()
Enter fullscreen mode Exit fullscreen mode

If you've written Python, nothing here is new. A function, a web request, a JSON answer. The only unusual line is @mcp.tool(), and that one line is
what turns an ordinary function into a dish on the menu.

"So what do you actually see of all this?"

Three things. The name, get_close_price. The docstring, word for word — that's my menu description, so it's the most important text in the file.
And the two parameters with their types, which the library turns into a little form: ticker, a string, required; date, a string, required.

"And the rest?"

The rest is the waiter's business. I never see the URL, never see the code, and never see the API key. It comes from the computer's environment,
outside the conversation entirely.

That last point surprised me most. The docstring isn't a comment anymore. For thirty years I wrote docstrings for the next programmer, who might read
them. Here, the docstring is read every single time, by the guest, and it decides what gets ordered. Raw close, not adjusted for later splits isn't
a note for posterity. It's an instruction to the reader that matters most.


Two loud mistakes

It didn't work the first time. Twice. And both failures were the best part of the evening.

The first mistake was Claude's. It wrote the server from what it remembered of the official library, with a line that began from mcp.server.fastmcp
import FastMCP
. Before I'd even run it, Claude tested it in its own workspace, and the library answered at once:

No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer.

It even pointed to the migration guide.

"You got it wrong."

I did. I wrote the version I'd learned, and the library had moved on. That's the risk from Part 3: an answer from memory sounds just as sure as a
current one. The difference here is that code doesn't accept sounding sure. It either imports or it doesn't.

One line changed, and the server started.

The second mistake was mine, following Claude's instructions. To register the server, I typed a command that passed my API key with --env and then
gave the server its name. Claude Code replied:

Invalid environment variable format: mini-eodhd, environment variables should be added as: -e KEY1=value1 -e KEY2=value2.

The option had swallowed the name, thinking it was another variable. Swap two words, run again, done.

In Part 3 I complained that the new world fails quietly: a list that's simply shorter, a lookup that answers the wrong question. Building the server,
I was back in the old world, where every mistake stops you, says where, and says why. I'd forgotten how much I liked it.


One question, end to end

With the server running, I asked it in plain English: what was Apple's closing price on October 16, 2026?

Here is everything that happened between my sentence and the answer.

  1. Claude read the menu: one tool, get_close_price, with its description and its two-field form.
  2. Claude decided the tool fit the question, and filled in the form: ticker = AAPL.US, date = 2026-10-16.
  3. Claude Code passed that order to my server, the little program on my desk.
  4. The server added the API key from my computer's environment and sent a real request to EODHD.
  5. EODHD sent back a line of JSON. The server pulled out one number and returned a sentence: AAPL.US closed at 343.46 on 2026-10-16.
  6. Claude wrote the answer, and added the warning from my docstring: this is the raw close, not adjusted for later splits.

$343.46. I checked it against EODHD's own server afterwards; it matched.

"Where in those six steps could it go wrong?"

Steps three to five are ordinary code. Same order in, same answer out, every time. Steps one, two and six are mine. That's where the choices are:
reading "Apple" as AAPL.US, reading "October 16" as a date in the right format, deciding what to say about the result.

So the uncertainty I wrote about in Part 3 has an exact address. It doesn't live in the server, or the API, or the data. It lives in the two
translations at either end: from my sentence into the form, and from the result back into a sentence.

Then I asked about the next day, a Saturday. My server did exactly what I'd told it to: No trading data for AAPL.US on 2026-10-17 (weekend or
holiday?).
Claude's answer went further. It said Saturday, explained that markets were closed, and gave Friday's close as the last one before it.

As far as I can tell, I asked that question in a fresh session, which means Claude went back to the tool a second time, for Friday, on its own.
Nobody told it to. That's the part of the work I didn't write.

Who did what

Looking back at the evening, the division of labour is surprisingly clean.

I decided which API to call. I decided which number counts: the raw close, not the adjusted one, which is a choice with consequences, as anyone who
read about stock splits in my other series knows. I decided what the tool says when there's nothing to say. I decided that the key stays on my
computer, and that the tool can only read, never change anything.

And I did the rest in seconds: understood "Apple", found the right date format, picked the tool, noticed that a Saturday needs a different answer,
and went back for the Friday.

That's the split I expected after Part 5, in a form small enough to see whole. The machine is fast at everything inside the menu. The menu itself,
what's on it, what each dish means, what's not allowed, comes from someone who knows the kitchen.

Which also means that whoever writes the menu decides what the AI may do. EODHD's server offers dozens of tools, and every one of them only reads.
There is no tool to change or delete anything, so there's nothing for a wrong guess to break. That's not the model being careful. That's the menu
being careful.


Final Thoughts

My other series has carried a tagline from the start: Python for programmers, prompts for data analysts. Until this week I thought of those as two
kinds of reader. Building this server, I saw them as two halves of one job.

The programmer writes the menu. Thirty lines: one function, one docstring, one decision about which number is the right one. The analyst asks a
question in a sentence and never sees any of it. Neither half is more important. Without the menu, the model has nothing to order. Without the model,
the menu is just another API that only programmers can read.

And because MCP is a standard, the menu is written once. The same thirty lines would work in any app that speaks MCP, the way a plug fits any socket
built to the same shape. That's what EODHD did for its whole catalogue, and what Arhan did for his. It's what I did, on a much smaller scale, for one
question about one price.

The challenge from Part 1 still stands: find a task where checking costs more than producing, and a prompt is still the right choice. This week moved
it a little. When you write the tool yourself, part of the checking moves into the code, where it's done once. The docstring that says raw close,
not adjusted
is a check written down before anyone asks.

So far in this series I've treated the prompt as a sentence. Next time I want to look at what else can go into one: instructions, examples, files,
the menu itself. I've been typing prompts for months, and I still don't know the full answer.

New to this series? Start with Part 1: Is That All This Is? — A Programmer Meets the Prompt. Part 4 explains where MCP servers run; Part 5 looks at the plain fetch that needs no server at all. Why raw and adjusted prices differ is covered in
Every Split Leaves a Fingerprint, part of my other series, Unlock Real-Time Market
Intelligence with EODHD and Claude
.

Top comments (0)