DEV Community

Cover image for From Prompt to MCP Tool in 5 Seconds
Thomas Hansen
Thomas Hansen

Posted on Originally published at hyperlambda.dev

From Prompt to MCP Tool in 5 Seconds

Building an MCP tool, as the industry currently understands the job: install an SDK, write a server, hand-author a tool schema, wire the handler, host the process, deploy it, restart it, reconnect the client.

Here is the same job on Magic, the MIT-licensed backend platform I work on.

You write a sentence.

That is the whole procedure, and this article is about why — because the interesting part is not that it is fast. It is that there is no second step to be fast at.

The measured run

I did this on the cloudlet serving my website while writing the article. The generator reports its own execution time, so these numbers are server-measured, not my stopwatch.

I wanted a tool that did not exist: a daily trend over the anonymous submissions to our carbon calculator — how many people submitted each day, and how heavy their average footprint was.

Attempt Seconds (measured) Outcome
First generation 2.88 Saved, then threw a 500 on invocation
Regenerated 1.83 Live and correct
Regenerated for a better description 3.03 Live, correct, properly documented

Three generations, 7.74 seconds of compute, one live tool. It is public and read-only, so you can call it yourself right now:

curl "https://hyperlambda.dev/magic/modules/mcp-demo/footprint-trend?days=365"
Enter fullscreen mode Exit fullscreen mode
[{"day":"2026-08-09","submissions":2,"average_total_kg":16188.0},
 {"day":"2026-08-03","submissions":1,"average_total_kg":8772.0}]
Enter fullscreen mode Exit fullscreen mode

I will come back to that failed first attempt, because the honest part of this story is more useful than the fast part.

There is no second step

The file the generator saved is this. The comment is abbreviated here; everything else is verbatim.

// Returns the daily trend of anonymous carbon footprint submissions, one entry per day [...]
.arguments
   days:int
validators.default:x:@.arguments
   days:int:7
data.connect:billionair
   data.select:"select date(created) as day, count(*) as submissions, avg(total_kg) as average_total_kg from footprints where created >= date('now', '-' || @days || ' day') and total_kg > 0 group by day order by day desc"
      days:x:@.arguments/*/days
   return-nodes:x:@data.select/*
Enter fullscreen mode Exit fullscreen mode

Saving that file is the last thing that happens. There is no build, no restart, no registration, no export.

The reason is that Magic's MCP server has no tool registry. It does not keep a list of tools that somebody has to remember to update. When a client asks what tools exist, the server enumerates the endpoints that exist, right then, and describes them. The catalogue is not a copy of reality that can drift out of date — it is a query against reality.

Which means a tool cannot be added to the tool list. It can only be created, after which the list already contains it.

A Magic cloudlet connected as an MCP connector, listing the tools it publishes to the agent

The same is true in the other direction, and this is the part I find quietly elegant: calling a tool runs the endpoint. Not a copy of the endpoint, not a proxy in front of it — the endpoint, through the same invocation path an HTTP request takes, carrying the caller's own identity. So the role check that guards it from a browser is the role check that guards it from an agent, because it is the same check. There was never a second one to keep in sync.

Your prompt is the tool description

Now the mechanic that makes one sentence sufficient.

A language model deciding whether to call a tool reads two things: the tool's description, and a description of each argument. In every other stack, those are a third artifact — written by hand, next to the code and the schema, and stale within a month.

Magic publishes an endpoint's file comment as the tool description, and the comment above each argument as that argument's description. And the generator writes the file comment from your prompt.

So the sentence you typed is not merely instructions to a code generator that get thrown away afterwards. It survives, in the file, as the documentation your agent reads. Here is what came back out the other end, from this cloudlet's live specification:

"/magic/modules/mcp-demo/footprint-trend": {
  "get": {
    "operationId": "get_mcp-demo_footprint-trend",
    "description": "Returns the daily trend of anonymous carbon footprint submissions, one entry per day, so a caller can see how many people submitted and how heavy their average footprint was [...]",
    "parameters": [{ "name": "days", "in": "query", "schema": { "type": "integer" } }]
  }
}
Enter fullscreen mode Exit fullscreen mode

I never wrote a schema. I wrote days in a sentence, said it was an integer, and the type came along for the ride.

The mapping from Hyperlambda's types to JSON Schema is boring on purpose:

Hyperlambda JSON Schema
short, int, long integer
decimal, double, float number
bool boolean
date string, format date-time
guid string, format uuid
everything else string

So write the prompt like a docstring

Here is a mistake I made in front of you, and it is the most useful paragraph in this article.

My second attempt worked perfectly, and its description read:

"Use a single SQL statement that does all date arithmetic inside the database itself: group rows by the date part of the 'created' column [...] Do not perform any date arithmetic outside of SQL."

Every word of that is true, and every word of it is useless to the model that has to decide whether to call the tool. I had written a work order for a compiler and published it as documentation. An agent reading that learns nothing about when this tool is the right answer — it learns how I wanted the SQL written.

So I regenerated a third time, with the same requirements in a different order: what the tool returns first, who would want it, what the argument controls, and the implementation constraint last. That is the 3.03-second run in the table, and it is the version now serving.

The rule that falls out of this is short. The prompt becomes the docstring, so write it as one. Lead with what the tool does and when to use it. Name every argument and its type. Put implementation notes at the end, where they belong — after the sentence a model actually needs.

Nobody writes tool definitions on this platform. But somebody still has to write the first sentence well, and that somebody is you.

The honest edges

The first generation was wrong. It compiled, it verified, it saved, and it threw a 500 the moment I called it — it had tried to subtract dates in a way the runtime found ambiguous. The verifier proves that every instruction in generated code exists and is real; it does not prove the code does what you meant. That still requires running it, which is why I ran it.

Note the fix, though: I did not open the file. I changed one clause in the prompt and regenerated, 1.83 seconds. When the build path is that short, it is also the repair path — hand-editing generated code is not the workflow here, and does not need to be.

Your client may not notice immediately. The server's catalogue is current the instant the file lands, but most MCP clients fetch the tool list once, when they connect, and cache it. The tool is live, callable, and in the catalogue — your agent just will not see it until you reconnect the connector. This trips people up regularly, and it is a client behaviour, not a server one.

Two smaller ones. Some clients enforce a 64-character limit on tool names; Magic does not truncate, so a very deeply nested module path could produce a name a strict client dislikes. And the transport is plain request/response JSON — a spec-compliant subset, with no SSE stream and no server-initiated messages.

One cloudlet, several servers

A last detail worth knowing. The MCP endpoint takes an optional path, which narrows the catalogue to one module subtree — point a client at ?path=/modules/crm/ and it sees the CRM tools and nothing else. The narrowing is confined inside modules/ and normalised at both ends, so /modules/crm cannot leak into a sibling like /modules/crm-archive.

Combine that with the catalogue being assembled per caller — Magic reads the roles on the authenticated ticket and lists only endpoints that caller may execute — and one backend serves as many different, differently-scoped MCP servers as you have audiences for. I made that argument properly in the OpenAPI article, so I will not repeat it here.

Try it

One command:

curl -fsSL https://hyperlambda.dev/docker-compose.yaml | docker compose -f - up
Enter fullscreen mode Exit fullscreen mode

Open localhost:5555, point it at localhost:4444, log in with root / root, and describe a tool you wish you had. Then connect an agent and look for it.

Magic is MIT-licensed and open source. The repository is at github.com/polterguy/magic, with documentation at docs.ainiro.io.

The five seconds in the title is the round number. The measured ones were 2.88, 1.83 and 3.03 — and honestly, the seconds are the least interesting thing about it. What matters is that when they elapsed, there was nothing left to do.

Related reading

Top comments (0)