Why I Don't Like Imperative Code (And What I Prefer Instead, Especially with MCP)

Why I Don't Like Imperative Code (And What I Prefer Instead, Especially with MCP) blog post fallback blur image Why I Don't Like Imperative Code (And What I Prefer Instead, Especially with MCP) blog post main image
Stephen CollinsJul 25, 2025
5 mins

Key Points

How does MCP relate to imperative vs. functional programming?

MCP is all about *protocols*—defining the structure, context, and semantics of a workflow up front, much like declarative or functional programming. It avoids the brittle, step-by-step micromanagement of imperative code, and instead lets you compose reliable, predictable chains of tools and actions.

Why is imperative code so dangerous in LLM automation?

LLM workflows with hidden state and implicit steps become black boxes fast. Imperative code multiplies that chaos. Protocol-driven design (like MCP) keeps things explicit, inspectable, and testable—even for AI.

What's the future of code organization in AI-first apps?

Code will become more declarative and protocol-driven, using intent graphs, MCP, and similar standards to describe *what* needs to be done, not *how* to do it line by line. This shift is already underway.

I’ve been writing code for over a decade, and I’ll just say it: Imperative code isn’t just suboptimal—it’s actively hostile to modern LLM workflows, especially when you want reliable, composable automation.

Imperative programming has its place. But as soon as you need reliability, modularity, or AI involvement (think: chaining LLM tools, orchestrating API calls, or building anything with MCP), imperative code gets in the way. Here’s why I now favor declarative, functional—and increasingly, protocol-driven—approaches like MCP and intent graphs.


The Problem with Imperative Code

Imperative code is a recipe for cognitive overload and hidden bugs, but it gets even worse as soon as you try to automate or integrate with LLMs.

It’s the classic “step-by-step” directions:

// Imperative
const users = [];
for (let i = 0; i < data.length; i++) {
  if (data[i].age > 18) {
    users.push({
      name: data[i].name,
      age: data[i].age
    });
  }
}

With imperative code, you’re forced to track:

  • Mutable state (what’s in users at each point?)
  • Side effects (are we mutating anything else? Did we forget a corner case?)
  • Every tiny step (was that off-by-one bug here or there?)

Multiply this by every handler in an LLM workflow and you have a brittle, untestable mess.


Functional Code: A Bridge to Protocols

Here’s a declarative refactor:

const users = data
  .filter(user => user.age > 18)
  .map(user => ({
    name: user.name,
    age: user.age
  }));

No mutation, no hidden steps, just a clear specification of what you want.

This isn’t just cleaner code—it’s the same philosophy behind MCP, intent graphs, and protocol-driven automation:

  • Explicit structure (“Filter, then map” becomes “intent, then tool”)
  • Predictable behavior (no surprises)
  • Composable building blocks (each transformation is a protocol step)

LLM Automation: Where Imperative Fails, MCP Wins

Let’s go deeper. Say you’re building an LLM-powered user onboarding system. With imperative code, the workflow is a spaghetti mess of:

  • Fetch user
  • Validate fields
  • Create session
  • Send notification

Each step mutates state, with bugs lurking at every jump.

With MCP or an intent graph, you declare the workflow as a protocol:

{
  "intent": "user_onboarding",
  "steps": [
    { "tool": "fetch_user", "params": { "username": "{{username}}" } },
    { "tool": "validate_fields" },
    { "tool": "create_session" },
    { "tool": "send_notification" }
  ]
}

No surprises. You can inspect, modify, test, or even simulate the workflow. Each node (step) is deterministic and self-contained.


Real Example: Authentication Workflow, Imperative vs. Protocol

Imperative:

function authenticateUser(username, password) {
  let user = null;
  let isValid = false;
  // ...
  // (a bunch of stateful, error-prone logic)
}

Functional:

const authenticateUser = (username, password) => 
  findUser(username)
    .filter(user => verifyPassword(user, password))
    .map(createSession)
    .getOrElse(null);

MCP / Intent Graph (declarative protocol):

{
  "intent": "authenticate_user",
  "inputs": { "username": "bob", "password": "secret" },
  "steps": [
    { "tool": "find_user", "args": { "username": "{{username}}" } },
    { "tool": "verify_password", "args": { "password": "{{password}}" } },
    { "tool": "create_session" }
  ]
}

Every step is visible, testable, and even portable between languages, tools, or clouds.


Why This Matters in the LLM Era

  • Debuggability: Imperative code hides bugs in call stacks and hidden mutations. Protocol-driven flows (MCP) surface every step.
  • Testing: Want to test an LLM workflow? With MCP, you can mock or simulate each protocol step—no side effects, no mysterious state.
  • Maintenance: Need to add an audit log or tweak notifications? With protocols, you just add a node; with imperative code, you rewrite the universe.

When Imperative Is Useful

Sometimes you need to go low-level—tight loops, custom memory management, or raw system hooks. Fine. But for workflows, APIs, or anything with LLMs in the loop? Protocol-driven, declarative approaches win every time.


Cognitive Load and Team Scaling

Imperative code scales cognitive pain as your system grows. Protocol-driven code (intent graphs, MCP flows) reduces it, making workflows self-documenting and team-friendly.


Practical Tips for Going Protocol-Driven

  1. Start Small: Define your workflows as JSON, YAML, or intent graphs—before you write a single imperative handler.
  2. Embrace Immutability: Protocols work best when steps don’t mutate global state.
  3. Extract Pure Steps: Each tool/node should be pure and reusable—think microservices, but lighter.
  4. Adopt Standards: Use MCP, intent-kit, or other open standards to make your workflows portable and interoperable.

The Bottom Line

Imperative code isn’t just “old-school”—it’s a liability in the age of AI, automation, and protocol-driven systems like MCP.

When I write anything these days, I ask: Can this be a protocol? Can this be modeled as an intent graph or MCP workflow? If yes, I go that route. The results: fewer bugs, easier scaling, and future-proofed automation.

What’s your take? Have you started building protocol-driven workflows yet?


Want to see how this works in practice? Check out my other posts on intent-kit and building expert LLM systems.


TL;DR:

The future is declarative, protocol-driven, and AI-native. If you’re still writing imperative code for your workflows, you’re fighting the tide.