> ## Documentation Index
> Fetch the complete documentation index at: https://dbhub.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# How DBHub Adopts the New MCP Spec 2026-07-28

> Upgrading DBHub to the MCP 2026-07-28 spec revision: the stateless core, cacheable tool lists, header-based routing — with before/after code for every change, plus what we evaluated and skipped.

*July 28, 2026*

MCP shipped its [2026-07-28 revision](https://blog.modelcontextprotocol.io/posts/2026-07-28/): the `initialize` handshake is gone, requests are self-contained, and the protocol becomes request/response. We adopted it alongside the TypeScript SDK v2 that implements it. The upgrade ships in [DBHub 1.0.0](/blog/dbhub-1-0-0) — 2026-07-28 clients get the new protocol on the same `/mcp` endpoint that keeps serving 2025-era clients.

This post shows each change with before/after code, and what we evaluated but did not build.

## 1. The stateless core

DBHub's HTTP transport has always been stateless — no session affinity, safe behind a round-robin load balancer. The 2025-era protocol assumed a session, so we faked statelessness: a fresh transport and server instance per request, session management disabled by hand.

**Before:**

```typescript theme={null}
app.post("/mcp", async (req, res) => {
  try {
    // In stateless mode, create a new instance of transport and server for each request
    // to ensure complete isolation. A single instance would cause request ID collisions
    // when multiple clients connect concurrently.
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined, // Disable session management for stateless mode
      enableJsonResponse: true // Use JSON responses (SSE not supported in stateless mode)
    });
    const server = createServer();

    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error("Error handling request:", error);
    if (!res.headersSent) {
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});
```

**After** (SDK v2's `createMcpHandler` — one factory, both protocol eras):

```typescript theme={null}
const mcpHandler = createMcpHandler(createServer, { onerror: logMcpError("handler") });
const mcpNodeHandler = toNodeHandler(mcpHandler, { onerror: logMcpError("adapter") });

app.all("/mcp", (req, res) => {
  void mcpNodeHandler(req, res, req.body);
});
```

On 2026-07-28, each request carries protocol version, client identity, and capabilities in `_meta` — no `initialize` exchange, no `Mcp-Session-Id`. The per-request model we hand-rolled is now the native serving model; 2025-era clients are handled by the SDK's built-in stateless fallback. stdio got the same treatment: `server.connect(new StdioServerTransport())` became `serveStdio(createServer)`.

**Result:** \~25 lines of session-faking deleted. Running N replicas behind any load balancer is now spec-guaranteed instead of workaround-dependent.

## 2. Cacheable `tools/list`

DBHub's tool list is fixed per configuration, yet every 2025-era client re-fetches it each session — the protocol had no way to say "this doesn't change." The 2026-07-28 revision adds `ttlMs` / `cacheScope` fields on cacheable results. Our entire change:

**Before:**

```typescript theme={null}
const server = new McpServer({
  name: SERVER_NAME,
  version: SERVER_VERSION,
});
```

**After:**

```typescript theme={null}
const server = new McpServer(
  { name: SERVER_NAME, version: SERVER_VERSION },
  {
    cacheHints: {
      "tools/list": { ttlMs: TOOLS_LIST_CACHE_TTL_MS, cacheScope: "private" },
    },
  }
);
```

On the wire:

```json theme={null}
{
  "result": {
    "tools": [ { "name": "execute_sql", ... }, { "name": "search_objects", ... } ],
    "ttlMs": 300000,
    "cacheScope": "private"
  }
}
```

**Result:** clients skip re-fetching the tool list for five minutes at a time. Not "forever" because DBHub hot-reloads its TOML config and the tool list can change at runtime — the TTL bounds staleness.

## 3. Header-based routing

2026-07-28 clients send `Mcp-Method` and `Mcp-Name` headers on every Streamable HTTP request, so a gateway in front of DBHub can tell a write-capable `execute_sql` call from a `search_objects` call without parsing JSON bodies. Our change was one CORS line (the headers come from the SDK):

**Before:**

```typescript theme={null}
res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
```

**After:**

```typescript theme={null}
res.header('Access-Control-Allow-Headers',
  'Content-Type, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name');
```

**Result:** an nginx front can throttle tool execution with no MCP awareness:

```nginx theme={null}
# Throttle tool execution; leave schema exploration unthrottled
map $http_mcp_name $mcp_write_limit {
    "execute_sql"  $binary_remote_addr;
    default        "";
}
limit_req_zone $mcp_write_limit zone=mcp_writes:10m rate=10r/m;
```

## 4. Evaluated and skipped: MRTR write confirmation

Multi-round-trip requests (MRTR) replace server-initiated elicitation: a tool returns `resultType: "input_required"` and the client retries with answers — which finally works over stateless HTTP. The obvious application for a database tool: a confirmation prompt before writes.

We designed it and didn't ship it:

1. **Confirmation is the host's job.** DBHub already annotates write-capable tools with `destructiveHint: true`, and hosts like Claude Code gate destructive calls behind their own approval UI. A server-side prompt double-confirms.
2. **The fallback is weak.** Clients without elicitation degrade to a `confirm: true` argument, which models learn to set preemptively.
3. **It required new config.** Every design added TOML knobs on top of the existing `readonly` boolean, creating conflicting semantics.

The evaluation did trigger a cleanup that needed no new config. Read-only enforcement used to re-interpret the `readonly` boolean in four places. It now flows through one pipeline: every statement is classified (`read | dml | ddl | admin | unknown`, dialect-aware), and the config compiles into a class→verdict policy.

**Before** (one of four scattered interpretations):

```typescript theme={null}
const isReadonly = toolConfig?.readonly === true;
if (isReadonly && !areAllStatementsReadOnly(sql, connector.id)) {
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
}
```

**After** (all four sites consume one policy):

```typescript theme={null}
const policy = policyFromReadonly(toolConfig?.readonly);
if (sqlVerdict(policy, sql, connector.id) === "deny") {
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
}
```

The verdict set can grow a `confirm` member later — when elicitation-capable clients are the norm, MRTR write confirmation slots into the two gate sites without touching config. Until then, `readonly = true/false` remains the entire configuration vocabulary.

## 5. Compatibility

* **2025-era clients are unaffected.** Same `/mcp` endpoint, same `initialize` handshake, served by the SDK's stateless fallback. stdio clients see no difference.
* **One caveat for hand-rolled HTTP clients:** the legacy path now answers with spec-standard SSE framing (`event: message` / `data: {...}`) instead of plain JSON bodies. MCP clients already accept both; a raw `curl` script needs to read the `data:` line.
* **No config migration.** No TOML key is added or changed.

## Takeaway

For DBHub, adoption was mostly deletion — the spec absorbed our workaround — plus two small opt-ins (cache hints, CORS headers). Everything here ships in [DBHub 1.0.0](/blog/dbhub-1-0-0). The source is at [github.com/bytebase/dbhub](https://github.com/bytebase/dbhub); the adoption is a readable series of commits on `main`.
