AltaForge MCP Fake-Mode Standard¶
- Status: Draft (for review)
- Date: 2026-06-28
- Scope:
altaforge.mcpframework + 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):
- Per-request (only if the instance permits it — see safety):
- HTTP header
X-AltaForge-Fake: 1— idiomatic; joins the existingX-AltaForge-*wire contract inaltaforge/mcp/headers.py, set byAltaForgeMcpClientand readable server-side the same way agent/user identity headers already are. - Query param
?fake=1on the MCP endpoint — for callers that can't set headers (a browser hitting a URL, a checkbox that just appends a param). - Instance default — env
ALTAFORGE_FAKE_DATA=1boots 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 unlessALTAFORGE_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 earlierALTAFORGE_FAKE_DATA_FORCE/ALTAFORGE_FAKE_DATA_ALLOW_REQUESTpair — 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.py →
migrations/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; healthdata_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, calltools/list+ a representativetools/call, assert real rows come back over MCP with no backend creds present.
Rollout¶
- Framework change on
altaforge-librariesbranchfeat/mcp-fake-mode-standard→ PR intodev(the org standard; the submodule we already track). - Bump the
client-verticalscopesubmodule to the new framework sha. - Add
mcp/_testdata/snapshot + afakesmodule per server; register fakes. - CI: extend
mcp-tests.ymlwith 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)
vsanalyticscomputes aggregates at request time from a deterministic per-category synthetic profile, honoring the category arguments.