RSS Amplifier

Ryan’s Substack · Apr 8, 2026

How the Hotdog Is Made: MCP vs. Smart API from the code

0
Sign in to vote or save

This page did not load. You can still read it on the original site — the toolbar below keeps your place in the directory.

Last post was for the room where decisions get made and plans get drawn.

Last post was for the room where decisions get made and plans get drawn. This one is for the people who have to implement based on these decisions.

Three paths. Real code. No hand-waving.

---

The Scenario

You have a data API — let’s say it serves portfolio or product data. A customer comes to you and says “we want to query this with AI.” Your job is to figure out what to actually build.

Here’s what each path looks like under the hood.

Path 1: Dumb API + Client-Side AI

This is where most teams start. The API is unchanged. The AI lives entirely on the client.

The API endpoint (unchanged, exact-match only):

#python

# GET /api/data?skill=rust&type=featured&limit=5

@app.get(”/api/data”)

def get_data(skill: str = None, type: str = None, limit: int = 10):

results = db.query(Data)

if skill:

results = results.filter(Data.tags.contains(skill))

if type:

results = results.filter(Data.type == type)

return results.limit(limit).all()

Precise. Predictable. The API has no idea why you’re calling it.

The AI client has to compensate:

#python

# Client has to classify intent and build the query manually

async def handle_query(user_query: str):

# Step 1: classify what the user wants

intent = await llm.classify(user_query, labels=[

“find_by_skill”, “find_featured”, “find_recent”, “general_info”

])

# Step 2: extract params based on intent

if intent == “find_by_skill”:

skill = await llm.extract_entity(user_query, entity=”skill”)

return await api.get(”/api/data”, params={”skill”: skill})

elif intent == “find_featured”:

return await api.get(”/api/data”, params={”type”: “featured”})

# ... keep adding branches as requirements grow

else:

return “I don’t know how to answer that”

What’s actually happening: Two LLM calls minimum per user query (classify + extract). The routing logic is yours to maintain. Every new question type means new code. The API contributes nothing — it just returns rows.

Token cost: ~400–800 tokens per query just for the routing layer, before you even generate a response.

When this makes sense: Prototyping. Early exploration. When you don’t own the API.

When it breaks down: The moment your requirements grow past 4-5 intent types, you have a routing problem masquerading as an AI problem.

Path 2: Smart API (Intelligence Server-Side)

Same data. Different architecture. The reasoning moves into the API.

The endpoint now understands intent:

#python

# POST /api/ask

# Body: { “query”: “what has this person built for DevOps teams?” }

@app.post(”/api/ask”)

async def ask(request: AskRequest):

query = request.query

# Step 1: Use an LLM server-side to understand intent + extract structured params

intent_response = await llm.structured_extract(

prompt=f”Extract search intent from: ‘{query}’”,

schema={

“skills”: [”list of relevant skills mentioned or implied”],

“categories”: [”featured”, “exploration”, “professional”, “all”],

“audience”: [”devops”, “ml”, “backend”, “general”],

“query_type”: [”find_work”, “assess_fit”, “explore”, “general”]

}

)

# Step 2: Build a smart query from the extracted intent

results = db.smart_search(

skills=intent_response.skills,

categories=intent_response.categories,

boost_for_audience=intent_response.audience

)

# Step 3: Generate a contextual response server-side

summary = await llm.summarize(

context=results,

question=query,

persona=”helpful technical portfolio assistant”

)

return {

“summary”: summary,

“results”: results[:5],

“usage”: get_token_usage() # track what this actually cost

}

What’s actually happening: The intelligence lives in your API. The client sends a plain English question and gets a plain English answer back. No routing logic on the client. No intent classification on the client. No prompt engineering on the client.

Token cost: ~600–1,200 tokens per query — but this is the *only* LLM call. The client doesn’t touch a model at all.

The client code:

#javascript

// This is literally all the client has to do

const response = await fetch(’/api/ask’, {

method: ‘POST’,

body: JSON.stringify({ query: “what has this person built for DevOps teams?” })

});

const { summary, results, usage } = await response.json();

// summary: “Ryan has built three DevOps-relevant projects...”

// usage: { input_tokens: 847, output_tokens: 312, total_tokens: 1159 }

When this makes sense: You own the domain. You want consistent, controlled AI behavior. You want to manage cost and quality centrally. You want every client — web app, mobile app, third-party integration — to get the same smart response.

When it breaks down: When the reasoning needs to span systems you don’t control. You can’t put Jira’s intelligence in your API.

Path 3: MCP (When You Actually Need It)

Now we’re in territory where the client needs to orchestrate across multiple systems it doesn’t own.

**The tool definition (what MCP exposes):**

#json

{

“name”: “search_portfolio”,

“description”: “Search portfolio projects by skill, category, or relevance to a technical audience. Returns project summaries and metadata.”,

“inputSchema”: {

“type”: “object”,

“properties”: {

“query”: {

“type”: “string”,

“description”: “Natural language description of what you’re looking for”

},

“limit”: {

“type”: “number”,

“description”: “Max results to return (default 5)”

}

},

“required”: [”query”]

}

}

The MCP server wiring:

#python

@mcp.tool()

async def search_portfolio(query: str, limit: int = 5) -> dict:

# This just calls the smart API we already built

response = await api.post(”/api/ask”, json={”query”: query})

return response.json()

@mcp.tool()

async def get_contact_info() -> dict:

return {”email”: “ryan@brc-ai.net”, “calendar”: “brc-ai.net/contact”}

The AI agent using MCP:

#python

# The agent discovers tools at runtime and decides what to call

agent = MCPAgent(tools=mcp_client.list_tools())

result = await agent.run(

“Is Ryan available for a short-term DevOps automation project?

What has he built in that space and how do I reach him?”

)

# Agent decides: call search_portfolio(”DevOps automation projects”)

# Then: call get_contact_info()

# Then: synthesize both into a response

What’s actually happening: The model receives tool definitions, reasons about which to call, makes the calls, and synthesizes the results. The client drives the reasoning. The tools are relatively dumb — they’re just well-described endpoints.

Token cost: ~1,500–3,000 tokens per query. The model is reasoning across tool definitions, making multiple calls, and synthesizing results. This adds up fast at scale.

When this makes sense: An agent that also needs to check your Salesforce CRM, search your GitHub repos, and query a third-party availability service — all in one response. MCP lets it discover and compose all of those without custom integration code.

When it’s overkill: Anything a smart API can handle. If you own the domain, put the reasoning there.

Try It Yourself

Everything in this post is running live on my portfolio. Three scenarios, real API calls, actual token usage displayed in the browser.

abstractryan.ai/demo/mcp-vs-api

The `/api/ask` endpoint is the smart API path — open source, readable, deployed on Cloudflare Workers. The source is in the PortfolioBuilder repo if you want to dig in.

Conclusion

The architecture decision isn’t about which tool is newer or which one your customers are asking for by name. It’s about where the reasoning belongs, given what you’re building.

Get that right, and the implementation is straightforward. Get it wrong, and you’re bolting protocols onto dumb pipes and wondering why the AI doesn’t work.

Next up in this series, we will dive into ACP and how it differs from MCP and A2A by implementing a sandbox with registered agents that can be contracted for work.

Read on abstractryan.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.