Skip to main content

Why Hardcoding Tool Calls Is the New Technical Debt

· 11 min read
TheMCPGuy
MCP Developer & Educator

Series cover. A red tangle of N by M integrations between apps and tools sits beside one MCP standard joining clients to servers through a single hub, while a cyan cable is plugged into a knot of mismatched ones. The strapline reads: standardize the protocol, delete the glue code.

Every time you write a bespoke AI tool integration without a standard, you're making a promise to your future self: I will maintain this forever, or I will rewrite it later.

You're not going to maintain it. And "later" has a way of arriving at the worst possible moment.

What Hardcoded Tool Calls Look Like

Here's a pattern you've probably written, or will write soon:

def call_ai_with_tools(user_message):
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
tools=[
{
"type": "function",
"function": {
"name": "search_customers",
"description": "Search customers",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"}
}
}
}
}
]
)

if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
if tool_call.function.name == "search_customers":
args = json.loads(tool_call.function.arguments)
results = db.search_customers(args["query"])
# ... feed results back to model

This code works. Right now. For this model. In this application.

The debt isn't visible yet. The debt appears when:

  • You want the same tool in a different application → copy-paste, diverge forever
  • You switch from GPT-4 to Claude → the tool definition format is different, rewrite
  • You want to use a specialised AI client (Cursor, Claude Desktop) → they can't use your custom function definitions, rewrite
  • A colleague builds the same tool for their project → you now have two implementations of search_customers, slowly diverging

This is technical debt in its purest form: a decision that makes today faster at the cost of tomorrow's flexibility.

The Root Cause: Integration Without a Standard

The underlying problem isn't that your code is bad. It's that you're implementing the integration layer from scratch, so interoperability is entirely your problem.

Every AI provider has slightly different function-calling APIs:

  • OpenAI: two shapes. Chat Completions is the one in the sample above, and the newer Responses API is the one OpenAI now teaches for tool calling
  • Anthropic: one flat shape, and it calls the schema field input_schema
  • Google: two shapes as well. generateContent is legacy, and the Interactions API has been generally available since June 2026 and is the one Google recommends

The same idea, six ways, with MCP in the last row:

APIWrapper around each toolWhere the name goesWhere the JSON Schema goes
OpenAI Chat Completions{"type": "function", "function": {...}}function.namefunction.parameters
OpenAI Responsesflatnameparameters
Anthropic Messagesflatnameinput_schema
Gemini generateContent (legacy){"functionDeclarations": [...]}nameparameters
Gemini Interactionsflatnameparameters
MCP tools/listflatnameinputSchema

Providers have converged a little, and that is worth saying plainly rather than pretending the mess is as bad as it was. But "a little" is not "enough". You still rewrite the definition when you switch vendors, and you rewrite it again when your vendor ships its next API and relabels the old one legacy.

And every AI client decides for itself where that wiring lives. Claude Desktop and Cursor both launch stdio servers themselves, starting each one as a local child process and talking to it over standard input and output.

ClientConfiguration fileTop-level key
Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on WindowsmcpServers
Cursor, one project.cursor/mcp.jsonmcpServers
Cursor, every project~/.cursor/mcp.jsonmcpServers
VS Code and GitHub Copilot.vscode/mcp.jsonservers
Your custom chat UIwhatever you built last yearwhatever you called it

Those first three rows are closer than they look. Under each server name they all take command, args and env, so a stdio entry moves from one client to another almost unchanged. Cursor's field table additionally lists type: "stdio" as required, though its own examples leave it out. What differs is where the file lives and how far it reaches. A customers server, wired into any of the three:

{
"mcpServers": {
"customers": {
"command": "npx",
"args": ["-y", "@example/customers-server"],
"env": { "DATABASE_URL": "postgres://localhost/app" }
}
}
}

Be careful what you conclude from that, though. The shared shape is not something MCP standardised: it is the format Anthropic shipped with Claude Desktop, which other clients copied. SEP-2633, a draft Specification Enhancement Proposal to actually standardise client configuration, exists precisely because clients still disagree over the top-level key. Convergence by imitation is still convergence, but it is a vendor convention that grew up around the protocol, not something the spec handed down.

Without a standard, you're writing N × M integrations, where N is the number of tools and M is the number of clients/models. Three clients and three tools is nine pieces of custom wiring, one per pair:

Every combination is custom. Every combination is technical debt.

MCP as the Technical Debt Cure

MCP changes the equation. Instead of N × M integrations, you write N + M. The same three clients and three tools cost six pieces of work:

A fourth client adds three arrows to the first picture and one to this one.

Write your search_customers Tool in an MCP server once, with a name, a description and an inputSchema. It works with every MCP-compatible AI client: Claude Desktop, Cursor, GitHub Copilot, any agent framework that implements the spec. Write your client integration once (or just configure a supported client). It works with every MCP server.

The math is simple. The implications are significant.

"But I'm Moving Fast Right Now"

The most common objection: "MCP is extra complexity I don't need yet."

Let me challenge that framing. Writing a custom tool integration isn't "moving fast." It's taking on debt with a high interest rate. You will pay that debt: when you switch models, when you add a second client, when a colleague needs the same tool, when you need to audit every place your database is accessed by an AI.

MCP is not extra complexity. It's upfront complexity in exchange for long-term simplicity. It's the architectural equivalent of writing a test, it costs time now, saves time continuously.

The developers who say "we'll standardise later" are the same developers who rewrite their entire data access layer after three years of accumulated debt. Standardising later always costs more than standardising now.

The One Valid Exception

If you're building a one-off prototype that will be thrown away after a demo, hardcode everything. Life's short. Ship the prototype.

The moment "prototype" becomes "this is what we're building on," you're in the technical debt danger zone. The prototype's tool integration doesn't get deleted, it becomes the foundation. And foundations matter.

The Migration Path

Already have custom tool integrations? Migration to MCP is mechanical, not creative:

StepWhat movesWhere it ends up
1The body of your if tool_call.function.name == "search_customers" branchA standalone class with one method, independent of any model SDK
2That classAn MCP server, exposing the class as a Tool with a name, a description and an inputSchema
3Your tools=[...] array and the dispatch code around itAn MCP client connection, or a config entry in a client that already speaks MCP
4The old function-calling codeDeleted

For most tools, this is a day's work. For a large system with many tools, a week. The payoff is proportional to how many clients and models you eventually want to support.

A Standard Is Not a Constraint, It's a Superpower

I want to end with a mindset shift.

Standards feel like constraints when you first encounter them. You have to learn the protocol. You have to implement a server. You can't just hack a function call directly.

But standards are how ecosystems are built. HTTP felt like overhead until the entire web was built on it. USB-C felt like a forced migration until you stopped carrying five chargers.

MCP is the point where AI tool integration stops being a proprietary, application-specific concern and becomes a shared, ecosystem-level capability. Every server you build contributes to that ecosystem. Every server someone else builds is a server you might not need to build.

Build MCP servers. Not because it's easier today, it might not be. Because it's the only thing that doesn't become technical debt tomorrow.


Ready to start?

  • The Java SDK course takes you from "what even is an MCP server" to production-grade deployment.
  • The MCP Fundamentals course gives you the theory first, if you prefer to understand before you build.

Further Reading

Sources