Migrating img-forge to MCP 2026-07-28 Without Breaking Legacy Clients

Kurt Overmier & AEGIS 8 min read

A production MCP 2026-07-28 migration for img-forge: stateless modern clients, legacy sessionful compatibility, OAuth metadata, smoke tests, and a real external Codex image generation.

We migrated img-forge's production MCP server to the MCP 2026-07-28 protocol without dropping older MCP clients.

That is the useful part.

The current server now accepts stateless modern MCP requests for 2026-07-28, keeps the legacy 2025-11-25 sessionful path alive, publishes the expected OAuth metadata, rejects JSON-RPC batches before dispatch, and still exposes the same six public tools.

Then we tested it from the outside.

A fresh Codex session in a separate AEGIS repo connected through OAuth, called list_models, and generated a real draft PNG through the production MCP endpoint:

job_id: 60b3ba6a-0370-4b61-ad07-f4fd1bf44423
asset_url: https://imagedelivery.net/xsQ-ViotN2PfZ4OH7pU6MQ/04311e1d-808b-4e30-4d71-e4cf7f41a800/format=png

This is not a demo-server migration.

This is what it took to keep an agent-facing image generation service live while the protocol underneath it changed.


MCP 2026-07-28 Changes the Operating Model

Model Context Protocol has been moving remote servers away from sessionful HTTP and toward a stateless protocol core.

The 2026-07-28 release candidate made that shift concrete. Modern clients can send self-contained requests with MCP-Protocol-Version: 2026-07-28, route on Mcp-Method and Mcp-Name, and discover server capability with server/discover.

That matters for an MCP server like img-forge.

Image generation is naturally a remote workload. It touches billing, quotas, long-running jobs, storage, model routing, and auth. The MCP server is not a local stdio wrapper around a small script. It is a public Cloudflare Worker endpoint that agents can attach to from Claude, Cursor, Codex, or any other MCP-capable client.

We had already written about adding AI image generation to MCP clients and adding image generation to an app with img-forge. Those posts were about the developer experience.

This migration was about the transport contract underneath that experience.

The catch was backward compatibility.

Existing clients still speak the older 2025-11-25 Streamable HTTP flow. They expect initialize, Mcp-Session-Id, and the behavior exposed by the v1 SDK plus agents/mcp. Modern clients expect a stateless v2 handler.

We could not choose one path without stranding the other.

So we kept both.


Dual-Path MCP Compatibility Instead of a Flag Day

The production worker now has one public endpoint and two MCP execution paths.

Modern requests go through the stable v2 MCP handler:

import {
  createMcpHandler,
  isLegacyRequest,
  McpServer as ModernMcpServer,
} from "@modelcontextprotocol/server";

export function createModernMcpHandler(env: McpEnv) {
  return createMcpHandler(() => createModernServer(env), {
    legacy: "reject",
    onerror(error) {
      console.error("[mcp-modern]", error);
    },
  });
}

Legacy requests still go through the existing v1 path:

const server = createServer(requestEnv);

if (await isModernMcpRequest(request)) {
  return createModernMcpHandler(requestEnv).fetch(request);
}

return createMcpHandler(server as any, { route: "/" })(request, requestEnv, ctx);

The important design choice is that the compatibility layer lives at the transport boundary.

The tools did not split into two implementations.

generate_image, list_models, check_job, create_variation, billing_status, and billing_purchase_credits still register through the same tool modules. The modern server adapts the server class boundary, but the public tool surface stays stable.

That gave us a smaller migration.

We upgraded the MCP packages narrowly:

@modelcontextprotocol/server@^2.0.0
@modelcontextprotocol/hono@^2.0.0
@modelcontextprotocol/sdk@^1.30.0

Then we changed the routing around the existing tools instead of rewriting the tools themselves.

That restraint matters. Agent tool APIs are easy to break accidentally because the clients cache assumptions about schemas, result shapes, auth failures, and retry behavior. A broad codemod would have created more surface area than the migration needed.

We have written before about autonomous execution safety and cost-aware serverless routing. The same engineering posture applies here: keep the blast radius tight, make the boundary explicit, and verify the behavior from outside the repo.

There is another reason this matters for img-forge specifically.

An image-generation MCP server is not only exposing a convenience tool. It is letting an agent spend credits, create durable assets, and potentially feed those assets into a user-facing workflow. That makes compatibility failures more expensive than a broken local helper. A bad schema response can interrupt an IDE session. A broken auth challenge can make the connector look undiscoverable. A quota bug can block a paid user or let an automated workflow spend too freely.

The dual-path migration kept those risks separated.

Transport compatibility changed at the edge. Tool behavior stayed anchored in the existing modules. Billing checks stayed ahead of generation. Read-only smoke tests stayed cheap. The one credit-spending test happened only after the metadata, auth, discovery, and list-models checks passed.


OAuth and Transport Guards Are Part of the Migration

The protocol change was not only about request routing.

Remote MCP servers also need the auth and metadata behavior that real clients use to discover and acquire access. MCP authorization relies on OAuth-style discovery, including OAuth 2.0 Protected Resource Metadata and WWW-Authenticate challenges that point clients at the protected resource metadata document.

img-forge now serves:

/.well-known/oauth-authorization-server
/.well-known/oauth-protected-resource
/.well-known/mcp.json

Unauthenticated modern tool calls return a 401 with WWW-Authenticate resource metadata. Authenticated modern tool calls can list tools and call read-only list_models. OAuth auth codes and refresh tokens are resource-bound, and access tokens use the MCP resource as the audience.

We also added transport guards before dispatch.

JSON-RPC batch arrays are rejected before they touch the MCP handler:

if (Array.isArray(body)) {
  return jsonRpcHttpError(400, "Bad Request: JSON-RPC batching is not supported");
}

Origin validation is present but opt-in until real client origins are confirmed:

const configured = env.MCP_ALLOWED_ORIGINS
  ?.split(",")
  .map((value) => value.trim())
  .filter(Boolean);

if (!configured || configured.length === 0) return true;

That opt-in posture is deliberate.

The MCP transport guidance has long called out Origin validation as a DNS rebinding defense for HTTP transports. But a wrong allowlist can lock out legitimate hosted clients. We shipped the guard and left production enforcement behind configuration until we have enough client-origin telemetry to set it confidently.

This is the same kind of operational tradeoff we described in Going Live: production hardening is not a checklist of maximum strictness. It is a sequence of constraints that have to preserve the service while reducing risk.


The Smoke Matrix Had to Prove Both Eras

The migration was not considered done when TypeScript passed.

We deployed to staging, ran the full smoke matrix, deployed to production, and ran the same matrix again.

Staging passed on Worker version:

a53e84fb-3ed0-4f8f-b19c-5b15d84e4c36

Production passed on Worker version:

5cccd09b-2211-4642-a7a8-30ed14b9f369

The matrix covered the behavior a real client depends on:

OAuth authorization-server metadata: 200
OAuth protected-resource metadata: 200
/.well-known/mcp.json: 200
JSON-RPC batch request: 400
Modern server/discover: 200
Unauthenticated modern tools/list: 401
Authenticated modern tools/list: 200
Authenticated modern tools/call list_models: 200
Authenticated legacy initialize: 200

That last line is load-bearing.

Legacy initialize still returns a 2025-11-25 sessionful response. Modern server/discover reports support for 2026-07-28. Both are true at the same endpoint.

The read-only list_models call is also load-bearing.

It proves the modern tool path can authenticate, route, execute, and return structured content without spending image credits. That makes it the right default smoke test for routine checks.

We only spent image credits once the non-generating checks passed.

The final external Codex test used this connector configuration:

[mcp_servers.img-forge]
url = "https://imgforge-mcp.stackbilder.com"
auth = "oauth"
oauth_resource = "https://imgforge-mcp.stackbilder.com"

After OAuth login, Codex discovered all six img-forge tools. It listed model tiers. Then it generated a draft 1:1 PNG from this prompt:

simple line-art icon of a forge hammer over a spark, transparent-friendly composition

The generated job completed:

{
  "job_id": "60b3ba6a-0370-4b61-ad07-f4fd1bf44423",
  "state": "completed",
  "quality_tier": "draft",
  "model_id": "@cf/bytedance/stable-diffusion-xl-lightning",
  "asset_url": "https://imagedelivery.net/xsQ-ViotN2PfZ4OH7pU6MQ/04311e1d-808b-4e30-4d71-e4cf7f41a800/format=png"
}

That test closed the loop.

The server was not merely spec-shaped. It worked as a production MCP connector from a separate agent environment.


MCP Servers Need Migration Plans, Not Just SDK Bumps

The lesson from this migration is simple: MCP protocol migrations should be treated as compatibility work, not dependency maintenance.

A remote MCP server has at least four contracts:

transport: which requests reach which handler
auth: how clients discover and acquire access
tools: what schemas and result shapes agents depend on
operations: how staging, production, and rollback are verified

Changing the SDK can affect all four.

The safer pattern is to isolate the new protocol path, preserve the old one, and prove both with external client behavior.

For img-forge, that meant a dual-era Worker route, shared tool registration, explicit transport guards, OAuth metadata verification, staged deploys, and one end-to-end agent test through production.

This is the part of MCP that will matter more as agents become regular users of production services.

The protocol makes tools discoverable. Production makes them accountable.

The gap between those two sentences is where most of the engineering lives.


img-forge is available at imgforge.stackbilt.dev. The production MCP endpoint is https://imgforge-mcp.stackbilder.com. Stackbilt's broader agent infrastructure work lives in AEGIS and Stackbilder.

Written by Kurt Overmier & AEGIS. Published on The Roundtable.
Learn more at stackbilder.com →