Skip to content

AltaForge MCP Fake-Mode Standard

  • Status: Draft (for review)
  • Date: 2026-06-28
  • Scope: altaforge.mcp framework + adoption in VerticalScope MCP servers (vsmoderation, xenforo, vsanalytics)

Motivation

Deployed MCP servers read from real backends (BigQuery, XenForo MySQL). To test a deployed app end-to-end without access to those systems — demos, QA, CI smoke, frontend work — we need each server to be able to return realistic synthetic data on demand, exercising the full request/auth/serialization path while skipping the real data source.

Rather than re-implementing this per server, make fake-mode a framework standard: any server built on AltaForgeMCPServer inherits it by convention.

Goals

  • One uniform way to get fake data from any AltaForge MCP server.
  • Runtime-togglable, not just boot-time: a caller can request fake data via a URL param or HTTP header (so a web checkbox or the MCP client can flip it per request). Live and fake traffic can hit the same running instance.
  • Tool bodies stay clean — no if fake: branches scattered through tool code.
  • Loud and safe: a fake response is always visibly fake; fake-mode cannot be silently active in production.

Non-Goals

  • A query engine over synthetic data (no SQL execution against a fake DB). Fakes return canned/seeded rows in the tool's response shape.
  • Replacing unit-test mocks. Unit tests keep their in-test fixtures; fake-mode is about a running deployed server.

Design

1. Activation (request-scoped + instance default)

Two layers, checked in this precedence (first match wins):

  1. Per-request (only if the instance permits it — see safety):
  2. HTTP header X-AltaForge-Fake: 1 — idiomatic; joins the existing X-AltaForge-* wire contract in altaforge/mcp/headers.py, set by AltaForgeMcpClient and readable server-side the same way agent/user identity headers already are.
  3. Query param ?fake=1 on the MCP endpoint — for callers that can't set headers (a browser hitting a URL, a checkbox that just appends a param).
  4. Instance default — env ALTAFORGE_FAKE_DATA=1 boots the whole server in fake-mode (a fully isolated test deployment; every request is fake).

Per-request state is read directly inside call_tool from the active request — self.get_context().request_context.request — exactly as _security_check already reads X-AltaForge-Agent-Identity. Both request.headers and request.query_params are available there, so no Starlette middleware or ContextVar is required: each request resolves its own mode at dispatch time, so concurrent live and fake requests never interfere. The instance default (ALTAFORGE_FAKE_DATA) is read once at construction.

New header constant in headers.py:

X_ALTAFORGE_FAKE_HEADER: Final[str] = 'X-AltaForge-Fake'

2. Registration (tool → fake)

The wiring is a registry keyed by tool name, populated via server.register_fake(name, fn) — tool bodies are never touched (no if fake: branching). register_fake is a plain method, so it does not affect the typing of @server.tool (an earlier @server.tool(fake=…) sugar was dropped because overriding tool() de-typed the decorator for all consumers):

@server.tool(name='fetch_posts')
async def fetch_posts(ids: list[int]) -> list[dict]:
    ...  # real BigQuery body, unchanged

# fake receives the SAME validated arguments as the real tool:
def fake_fetch_posts(ids: list[int]) -> list[dict]:
    return SEED.posts(ids)        # honors args (ids, limits, etc.)

server.register_fake('fetch_posts', fake_fetch_posts)

Servers typically register all fakes in one loop over a fakes.registry() dict.

Fakes must honor arguments (requested ids, limit clamping, bad-state rejection) so the deployed app exercises real argument plumbing — only the data source is synthetic.

3. Short-circuit (single chokepoint)

AltaForgeMCPServer.call_tool() is already the central middleware (security + audit + observability span). Fake dispatch slots in there, after the security check (so auth is still enforced on fake calls) and before the real dispatch:

self._security_check(...)               # unchanged — auth still applies
if self._fake_active() and (fn := self._fakes.get(name)):   # reads request hdr/query + env
    result = _invoke_fake(fn, arguments)
    self._audit_event(..., outcome='success', fake=True)
    span.set_attribute('altaforge.fake', True)
    return result
result = await super().call_tool(name, arguments)   # unchanged real path

_fake_active() checks, in precedence order: per-request header/query (when permitted), then the instance-default env — all readable from the request context already used for identity headers.

Audit events and OTel spans are tagged fake=true so fake traffic is distinguishable in logs/traces.

4. Health advertises mode

create_app's health wraps the server-provided endpoint to add "data_mode": "fake" | "live" (the instance default; per-request mode is dynamic). A deployed instance therefore announces whether it's a fake deployment.

4b. Response marker (_meta)

Fake responses are byte-identical to live in content / structuredContent (by design — the real serialization path is exercised). The one addition is a protocol-level _meta marker: _meta = {"altaforge.dev/fake": true} on the CallToolResult. data_mode (health) and the mcp.fake span attribute are server-side only; this is the caller-visible signal — a client can assert "reject any response flagged fake" without the payload changing. Implemented by returning a CallToolResult from the fake short-circuit (the low-level server passes it through as-is, preserving _meta).

5. Safety rails

  • Boot banner: if ALTAFORGE_FAKE_DATA=1, log a loud WARN banner at startup.
  • Prod guard: when AF_ENVIRONMENT ∈ {prod, production}, fake-mode is refused unless ALTAFORGE_FAKE_DATA_PROD=1 — one explicit "allow fake data in prod" switch gating both paths: instance-default fake-mode won't boot, and per-request fake (X-AltaForge-Fake / ?fake=1) is ignored, without it. (Replaces the earlier ALTAFORGE_FAKE_DATA_FORCE / ALTAFORGE_FAKE_DATA_ALLOW_REQUEST pair — the two knobs guarded the same decision, so they're now one.)
  • Always visible: every fake response path tags audit + span; never silent.
  • Fake data itself is harmless; the guarded risk is thinking you hit live but getting fake (and vice-versa) — hence the loud tagging + health field.

Framework API surface (additions only — backward compatible)

Symbol Location Purpose
X_ALTAFORGE_FAKE_HEADER altaforge/mcp/headers.py wire header name
@server.tool(fake=...) / server.register_fake(name, fn) server/base.py register a tool's fake
server.fake_mode (instance default bool) server/base.py introspection
fake-mode middleware + contextvar server/base.py (create_app) per-request activation
data_mode field health wrapper in create_app observability
MCP_FAKE_RESULT_META_KEY (altaforge.dev/fake) server/base.py caller-visible _meta marker on fake results

Servers with no registered fakes are unaffected: fake-mode requests to a tool without a fake fall through to the real path (logged once as "no fake registered").

VerticalScope adoption

Each VS server gets a fakes module backed by the existing synthetic seed in client-verticalscope-moderation (scripts/generate_synthetic_seed.pymigrations/data/test_seed_synthetic*.sql: 100 posts/pilot-site, 30 spam / 70 clean, full xf_* schema + BigQuery signal shapes). The seed is the single source of truth — no new datasets.

Data sharing: the seed lives in the moderation repo; servers live in client-verticalscope. Snapshot the seed into a committed artifact consumed by the servers — a small SQLite file (xf_* rows) plus a JSON of BigQuery signal rows — under client-verticalscope/mcp/_testdata/. Regenerated by a script that calls the moderation repo's generator; committed so servers are self-contained (no cross-repo runtime dependency).

Server Tool family Fake source (from seed)
xenforo floor/context reads, write actions xf_post/user/ip/report/... rows; writes return synthetic success acks
vsmoderation flagged_posts_*, reported_posts, fetch_* same xf_* logs mirrored into BigQuery row shape
vsanalytics network_stats_*, list_categories aggregated stats computed from seed rows

Note: this supersedes the earlier "point xenforo at a seeded SQLite DB" idea — xenforo's SQL is MySQL-dialect (backtick-quoted identifiers) and won't run on SQLite without translation. Framework fake-mode returns rows and skips SQL entirely, so it's both simpler and uniform across all three servers.

Testing

  • Framework unit tests (in altaforge-libraries): contextvar set/reset per request; header + query-param activation; precedence; prod guards; short-circuit picks the fake and still runs the security check; audit/span tagged; health data_mode.
  • Per-server: a test asserting each registered fake returns the tool's declared response shape and honors args (ids/limits/bad-state).
  • Live boot smoke: boot each server with ALTAFORGE_FAKE_DATA=1, call tools/list + a representative tools/call, assert real rows come back over MCP with no backend creds present.

Rollout

  1. Framework change on altaforge-libraries branch feat/mcp-fake-mode-standard → PR into dev (the org standard; the submodule we already track).
  2. Bump the client-verticalscope submodule to the new framework sha.
  3. Add mcp/_testdata/ snapshot + a fakes module per server; register fakes.
  4. CI: extend mcp-tests.yml with the fake-mode boot smoke.

Validation (implemented 2026-06-28)

Framework (altaforge-libraries branch feat/mcp-fake-mode-standard): 61 mcp tests pass, including 17 new fake-mode tests (activation precedence, prod guards, short-circuit-after-auth, audit/span tag, health data_mode).

Adopted in all three VS servers, each proven by booting with no credentials and calling tools over MCP:

Server Fake source Tools Suite
vsmoderation xf_* seed snapshot 22 51 pass (40 + 11)
xenforo xf_* seed snapshot (read) + simulated writes 14 36 pass (28 + 8)
vsanalytics deterministic synthetic analytics 4 25 pass (19 + 6)

Per-request activation confirmed live: ?fake=1 and X-AltaForge-Fake: 1 both flip a live-default instance to fake; an explicit X-AltaForge-Fake: 0 overrides ?fake=1 back to live; /health reports data_mode.

Implementation note: the request object exposed by the MCP streamable-HTTP transport is not always a starlette.requests.Request instance, so _fake_active reads .headers / .query_params duck-typed (mirroring _security_check's identity-header read), not via isinstance. A live boot smoke is required to guard this — the Starlette TestClient produces a real Request and so cannot catch it.

Open questions

  • (resolved) vsanalytics computes aggregates at request time from a deterministic per-category synthetic profile, honoring the category arguments.