AI Engineer World's Fair 2025
MCPs are Boring (or: Why we are losing the Sparkle of LLMs)
Read the talk
MCPs are Boring (or: Why we are losing the Sparkle of LLMs)
A weather lookup, a CRM query and a JavaScript sandbox show how generated code can reduce model round trips and turn one-off answers into reusable software.
From a talk by Manuel Odendahl
Before you start: Basic familiarity with LLM tool calling, SQL joins and JavaScript functions will help you follow the examples.
What happens when you ask for the weather?
Ask an LLM for the weather in San Francisco. Instead of writing an API integration yourself, you want to describe the request and let the application do the work. That promise is the starting point for Manuel Odendahl’s critique of tool calling. Known as ProgramWithAi on Twitter and wesen on GitHub, Odendahl brings a background spanning Common Lisp, embedded systems, search, databases and backend engineering. He reports using LLMs for coding since 2022 and adopting them for essentially all his coding after ChatGPT appeared.
The basic mechanism is language generation. Models have learned about tools from descriptions, literature and code, with reinforcement further shaping their use. A model emits structured language; the host maps that output to a function, an API request or an MCP server. The returned text becomes material for another inference. It does not need a rigid response schema merely for the model to interpret it, which is part of the appeal: a person can ask for information without writing all the parsing code themselves.
For the San Francisco request, the loop is straightforward:
- The application supplies tool descriptions and argument schemas in the model’s context.
- The model selects the current-weather tool and produces its arguments.
- Deterministic code calls the weather API and returns the conditions, perhaps as JSON.
- A new inference turns that response into an answer for the user.
The software executes the action; the model chooses and describes it. Once several useful tools are connected, that ordinary plumbing produces a remarkably flexible interface.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
The model becomes a data courier
MCP makes these connections reusable across applications. A host can connect to tools for GitHub, Blender, local files or even a room-temperature sensor. But a larger inventory also creates more decisions. In Odendahl’s hypothetical collection of 100 tools, both a weather function and an internet-search function might appear to answer the San Francisco question. The model can choose poorly, confuse schemas or supply the wrong parameters.
Arguments have another cost: the model must generate them. Suppose a document fragment is already in the conversation and the next task is to find similar material. Passing that fragment to a search tool can require the model to emit the whole thing again. The user waits while generation performs what is effectively a copy operation. Repeating San Francisco as an argument is the same mechanism on a smaller scale.
Results create the reverse problem. If the user only wants wind speed, a weather response may still include dates and information about the sun and moon. Odendahl illustrates the waste as 2,000 tokens processed to recover a single wind-speed value. That is an illustrative estimate, not a measured token count for a specified API response. The expensive boundary is the model’s context: data crosses it even when ordinary code could select the relevant field first.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
One email address, an entire CRM response
Now ask a CRM for OpenAI’s contact information. In the example, the available Get CRM Companies tool returns all 36 companies so the model can extract OpenAI’s email address. A narrower Get CRM Company tool seems like the obvious repair, until a misspelled company name fails to match. Add another option for contact information, such as contact=true, and the interface starts accumulating special cases.
Continue covering every possible request and the tool begins to resemble a query language such as GraphQL—with another syntax the model can get wrong. Yet the broad response has a real advantage: once the company records are in context, the user can ask an unanticipated question such as which companies are in San Francisco.
| Interface | What it buys | What it costs |
|---|---|---|
| All company records | Flexible follow-up questions | Large model inputs |
| One company by name | Smaller responses | Matching sensitivity |
| Specialized flags | Targeted fields | More interface cases |
| General query language | Expressive selection | More query construction |
Odendahl’s illustrative bulk-query scenario uses 20,000 tokens, costs about $0.50 and takes five minutes. Those figures express the proposed tradeoff; the talk does not establish model, pricing or measurement conditions. The design problem is how to retain flexible questions without sending every record through inference.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Let tools use context the application already has
One way to avoid regenerating information is to make existing application context available to the tool. Odendahl proposes several forms:
- Chat history: Include history as tool-call metadata. A tool could ignore it, reuse arguments from previous calls or derive a search query from the conversation.
- Persistent memory: A weather tool could read a stored location of San Francisco instead of requiring the model to repeat it.
- Attachments: Pass files, their paths or their metadata so the tool can work with the original material.
These proposals move information through the application instead of requiring the model to reconstruct it as output tokens.
That access has security implications, and applications do not all represent context alike. A drawing interface may have images that do not fit a conventional attachment model; a graph-RAG application may need to convey graph structure. A universal protocol for all these forms can lead back toward the ontologies, semantic-web triples and XML schemas of earlier interoperability efforts. The easy local extension becomes a difficult shared contract.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Approval should not be the only intervention
A more immediate improvement is to let the user edit a proposed call. Before execution, a small interface could expose attachments and arguments. If the model misspells OpenAI—or decides to search for Oracle—the user could correct the request directly instead of approving it or starting another conversation turn. Approval-only interfaces and permanently static tool inventories are application choices, not MCP requirements: the March 2025 tools specification leaves interaction patterns to implementations and supports notifications when tools change.
The same intervention belongs on the return path. Let the user trim a large response before it enters the model’s context. Odendahl contrasts manually trimming 30 pages with an illustrative ten-minute, $0.50 model pass. A person who can see the relevant records may finish the selection faster than a model can regenerate an explanation of them.
The overdue-invoice example makes the proposed workflow concrete:
- The user asks for all customers with overdue invoices.
- The model proposes a SQL-like CRM query and filter.
- The host offers approve, reject and edit actions before the query executes.
- The user inspects the returned records and edits what will be sent onward.
- If the query was wrong, the user returns to its arguments, changes them and runs it again.
The important distinction is between a proposed call, an executed query and the result admitted into context. Each is a separate opportunity for intervention. A query that returns thousands of unwanted records should not force the user to rewind the entire agent run. This is a local repair loop; Odendahl leaves the broader problem of editing conversation context outside the talk’s scope.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Ask for the code that performs the action
There is a larger opportunity behind these interface changes. A language model can write a poem about Oracle or imitate a terminal because its output is not confined to function arguments. It can also write programs. Odendahl praises Sonnet 4 for generating whole applications and working with recently introduced APIs. His references to thousands of APIs and knowledge of work only months old express his experience of that capability, rather than results from a named evaluation. The question is why an application should expose that coding ability through only a fixed menu of function schemas.
A SQL query is already code. A file-editing tool can write code that invokes other tools, evaluates more code or runs another agent. Given a database schema, a model can generate a Get Company Contact function at the moment it is needed. Odendahl even speculates that a local three-billion-parameter model could handle that task. The capability need not exist as a separately designed tool before the conversation begins.
The resulting instruction is simple: ask the model to write the code to do the task. Odendahl points to Voyager and recalls a second paper approximately; the likely match is Executable Code Actions Elicit Better LLM Agents. Both are relevant to treating generated programs as agent actions rather than restricting each action to a single predefined call.
His own workflow began with a manual version of that loop. ChatGPT generated shell scripts, small applications or SQL queries; he ran them and pasted the results back. The human was the tool caller. He describes formal tool calling as still feeling closer to GPT-3.5-era intelligence than the coding abilities he now sees. That is a subjective comparison, but it motivates a concrete simplification: replace the copying with an evaluator.
An eval tool lets the model compose actions in code. Bash is one possible execution language. Coding agents already use grep, find and ls, and Odendahl points to their fallback to sed when a dedicated file-editing tool fails. A shell program can combine existing utilities without introducing a new model-facing schema for every combination.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Discover the schema, then let SQL count
Consider the question: how many orders did John Smith place last month? In this example, the model initially receives only a SQL evaluation tool, not the database structure. It first discovers the tables, finds customers and orders, and inspects their schemas. A customer name, an order’s customer identifier and a date field provide enough information to construct the join and aggregation.
For a SQLite version of that workflow, suppose inspection reveals customers(id, name) and orders(id, customer_id, order_date), with dates stored as ISO-formatted text. The discovery and answer queries can take this shape:
sql
SELECT name
FROM sqlite_master
WHERE type = 'table';
PRAGMA table_info(customers);
PRAGMA table_info(orders);
SELECT COUNT(*) AS order_count
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
WHERE c.name = 'John Smith'
AND o.order_date >= date('now', 'start of month', '-1 month')
AND o.order_date < date('now', 'start of month');
Here, last month means the previous calendar month. The database performs the join, filtering and count; only the count needs to return to the model. The schema and query remain inspectable, and the same query can be reused.
A specialized orders-for-customer tool can fail at several different boundaries. It might expect separate first and last names. A retry might then fail because the date argument needs a different format, such as a relative offset. Once the request succeeds, it might return invoice items that the model must aggregate itself. Odendahl estimates 5,000 tokens for that failure-prone path versus roughly 500 for the SQL approach. These are illustrative estimates, but the mechanism is precise: SQL performs deterministic arithmetic on selected rows instead of asking the model to add values from a large response.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Keep the useful code
Once the query works, save it. Odendahl proposes storing a customer-order calculation as a SQL view so later requests can select the relevant value without rediscovering the tables and joins. Exploration may require several attempts; the durable result can be one reusable abstraction. He describes consolidating a sequence of queries into a view, although the screenshots shown at this point are not the intended view example.
A collection of reusable functions and views is a library. Instead of exposing every GitHub operation as a separate tool, the evaluator could have access to a preconfigured GitHub library. Authentication belongs in that environment, so the generated program does not need to manage OAuth tokens. The model can compose library operations using the same coding patterns it already knows.
Odendahl claims models handle a library with a very large function surface more effectively than a comparable inventory of tool definitions. He acknowledges improved tool calling in Sonnet 4 and GPT-4.1, while reporting continued parameter mistakes. In contrast, he finds that pointing models at function declarations in his own header files often provides enough information to use those libraries. The practical distinction is between selecting one schema-bound action and writing a program against an API.
For a CRM, import the existing functionality and generate the missing composition. A get-company-info operation may simply call the company-list function, loop over the results and select the requested fields. There is no need to publish each such composition as a new MCP tool before the model can use it. The generated function can become another reusable part of the library.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Generate an interface for the person, too
Generated code is useful to more than the model. The next step is to build interfaces for the people using the tools. A JavaScript interpreter with library access still leaves the user with a poor experience if the only visible interaction is a function call and a small JSON editor. Odendahl proposes giving the host an easy way to render generated interfaces.
Return to the editable overdue-invoice query. The model could emit a UI description language that the host renders into controls suited to the arguments. Instead of changing raw JSON, the user could adjust sliders, choose dropdown values, attach a file or refuse access to memories. Preferences could be saved for subsequent calls.
The output side could provide a scrollable, filterable view of results. The user could remove information the model should not see, add relevant material or return to the previous inputs and run the tool again. The interface makes context selection an explicit part of the workflow rather than an invisible consequence of whatever the tool happens to return.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
A JavaScript evaluator with a database and a server
Odendahl’s prototype makes the execution environment concrete. It is an MCP implementation written in Go, with a JavaScript sandbox he calls Sandbox.js. The environment loads a SQLite library and a web server that exposes a handler-registration function. Evaluated JavaScript can therefore query data and register HTTP handlers. The interface supports executing JavaScript directly or loading an existing JavaScript file, including one written in Cursor.
Using Sonnet 4 with deliberately aggressive prototype prompting, the company lookup begins like the earlier SQL example: inspect the database. But the generated JavaScript can filter the table listing for company-related tables before returning anything. It can also fetch the relevant schemas inside the same evaluation. Odendahl reports saving two tool calls by combining table filtering and schema retrieval in code. The underlying database query operation has not changed; the model can now compose several uses of it before crossing back into inference.
The generated program can continue into querying during that evaluation. The narrated sequence includes an initial result containing ten companies and a subsequent call for all companies. Even the response formatting is rough: ordinary Go-style output appears rather than carefully serialized JSON, yet the model can interpret it.
Odendahl reports a query response in two or three seconds and describes it as one tool call rather than 15. The timing and call-count boundary are unclear because the preceding sequence includes a follow-up query; these numbers should not be read as the cost of the entire demonstration. What the example makes visible is the architectural change: discovery, filtering and querying can happen within generated code instead of requiring a model turn between each operation.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
From a query to a usable CRM
The next request is to save the useful code as a global function. Sonnet goes further than requested. Odendahl reports that the save-function step produced 15 functions in two tool calls, with one call needed to correct a syntax error. The resulting operations include retrieving company information, looking up a company by ID and searching companies. He attributes the error to the prototype’s prompting.
Those functions can now become HTTP endpoints. Because the evaluator has access to handler registration, the model can connect the generated functions to REST routes. It then generates a website, registering separate handlers for JavaScript, CSS and HTML. The existing REST endpoints supply the data. At this point, the user has an interface to work in rather than another answer to read in chat.
This changes what the model is being asked to deliver. A fixed-tool agent performs operations through capabilities supplied beforehand. Here, the model helps construct the capabilities and the interface around them. Odendahl describes the resulting CRM as supporting company edits and new records stored in the database. Once generated, that interface can be used without continued LLM interaction or a separate coding environment such as Cursor. A single kind of capability—evaluation—underpins the workflow, but building the application involves a sequence of requests and calls.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Code that creates the next capability
The final step is to think recursively. Ask for code that performs a task, then ask for code that creates or packages that capability. The JavaScript sandbox is not merely an interpreter with some libraries loaded: it can create libraries that later evaluations load and reuse. Generated APIs can also serve other systems, extending the usefulness of the work beyond the original conversation.
Language produces code; that code can produce more code, invoke another model or expose another interface. Eventually the chain makes something happen outside the conversation. Keeping those intermediate artifacts matters because they are where a one-off request becomes durable software.
The LLM writes code that does the work. Calling the surrounding system an agent does not remove the engineering. The database still aggregates records, the server still handles requests and the interface still lets a person complete a task. Odendahl’s closing appeal to recover the sparkle of LLMs is an invitation to build these successive capabilities: let generated software become the material for the next piece of software, instead of ending every interaction at a predefined function call.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Resources
From the talk
A Minecraft agent that generates executable skills, improves them through environmental feedback and stores them for reuse.
Introduces CodeAct, which represents agent actions as executable Python so tools can be composed and revised using execution feedback.
Further reading
Historical specification covering tool discovery, invocation, changing tool inventories and application-controlled interaction patterns.
Updates since the talk
- Code execution with MCPArticle
A November 2025 explanation of combining MCP with generated code to load tools selectively, filter intermediate data and save reusable functions.
Read the transcript and explore slides
Loading transcript and slides…