MCP streamable-http transport, DealDesk live tools & library-enforcement CI¶
Two operational concerns that share a theme — how docgen talks to the accelerator MCP servers, and the CI guard that keeps docgen on the AltaForge libraries instead of raw SDKs.
1. Streamable-http MCP transport¶
The real accelerator source servers (altaml/altaforge-accelerators
mcp/{hubspot,gdrive,fellow}) are FastMCP streamable-http servers — a
session handshake plus SSE, not plain JSON-RPC over a single POST. docgen's
MCPTool has two code paths, selected by the streamable_http flag:
streamable_http |
Path | Used by |
|---|---|---|
False (default) |
plain httpx.post(server_url, json=<JSON-RPC envelope>) |
offline tests (httpx.MockTransport) |
True |
the mcp SDK's streamablehttp_client (handshake + ClientSession) |
the live accelerator servers |
mcp_sources.register_mcp_source_tools() registers the live sources with
streamable_http=True. The streamable path supports the tool_name +
arguments (MCP tools/call) shorthand only. Per-user identity forwarding
(per_user=True) requires streamable_http=True — per-user is streamable-only.
The fake DealDesk server is the local stand-in¶
docgen/src/docgen/mcp_servers/dealdesk_fake.py is a local FastMCP server that
mirrors the real servers' transport so you can exercise the whole streamable-http
path with no creds and no real servers. It is built stateless_http=True,
json_response=True:
server = FastMCP(
'dealdesk-fake',
host=DEALDESK_FAKE_HOST, # default 127.0.0.1
port=DEALDESK_FAKE_PORT, # default 8465
stateless_http=True, # no server-side session state between calls
json_response=True, # respond with a JSON body, not an SSE stream
)
stateless_http=True— each request is self-contained; the server keeps no per-session state, so docgen's "onehttpx.Clientper call" lifecycle works without a sticky session.json_response=True— the server answers with a plain JSON body instead of an SSE event stream, which is the simplest shape for the SDK client to consume.
Each tool returns a JSON string (return json.dumps(...)) matching the real
servers, ignores its arguments, and replays the bundled Meridian fixture
(docgen.fixtures.dealdesk_mcp_cassette) — the same fixtures the offline
cassette uses, so live-fake and offline-cassette runs stay consistent. It's a
stand-in, not a query engine. It also reflects the caller's identity back as
_served_for_user so you can see per-user forwarding (see
mcp-per-user-auth.md).
Wiring DealDesk to the (fake or real) tools¶
The DealDesk spec's three fetch lanes target the source connectors by tool_ref,
and call the accelerator tool by tool_name:
Source (tool_ref) |
Tool (tool_name) |
Returns |
|---|---|---|
hubspot |
deals.get_full_context |
{deal, owner, company, contacts, notes, meetings, fellow_short_codes, drive_folder_url} for the deal id |
gdrive |
gdrive.extract_folder |
{documents, skipped_files} — folder_id chained from hubspot's drive_folder_url |
fellow |
fellow.get_meeting_summary |
[{summary, action_items, decisions, participants, …}] — meeting_ids from hubspot's fellow_short_codes |
Each source name maps to a (URL env var, token env var) pair in
mcp_sources.MCP_SOURCES. A source whose URL env var is unset is skipped, so
offline/CI runs don't crash:
| Source | URL env var | Token env var |
|---|---|---|
hubspot |
MCP_HUBSPOT_URL |
MCP_HUBSPOT_TOKEN |
gdrive |
MCP_GDRIVE_URL |
MCP_GDRIVE_TOKEN |
fellow |
MCP_FELLOW_URL |
MCP_FELLOW_TOKEN |
bigquery |
MCP_BIGQUERY_URL |
MCP_BIGQUERY_TOKEN (Vertical Scope) |
Swapping fake → real is just env. Run the fake server, then point all three URLs at it (no tokens needed):
uv run python -m docgen.mcp_servers.dealdesk_fake # 127.0.0.1:8465
# apps/web/.env.local
MCP_HUBSPOT_URL=http://127.0.0.1:8465/mcp
MCP_GDRIVE_URL=http://127.0.0.1:8465/mcp
MCP_FELLOW_URL=http://127.0.0.1:8465/mcp
To go live later, point the same env vars at the real accelerator servers and
set the _TOKENs — no code change. (pnpm dev:fake in demo/ does the
fake-server + env wiring in one command; see mcp-per-user-auth.md.)
Reference¶
docgen/src/docgen/mcp_servers/dealdesk_fake.py(the fake server),docgen/src/docgen/tools/mcp.py(MCPTool,streamable_http),docgen/src/docgen/tools/mcp_sources.py(MCP_SOURCES,register_mcp_source_tools),docgen/src/docgen/authoring/specs/altaml-dealdesk-proposal.json.
2. AltaForge library-enforcement CI (warn mode)¶
Shipped in commit
a5e7d4f— "ci: AltaForge library enforcement (warn mode)."
docgen is required to consume LLM, HTTP, and observability capabilities through
the altaforge.* libraries (altaforge.llm → Claude, altaforge.core.runtime
for tracing, etc.) rather than importing the underlying SDKs directly. The
project tech-stack rule (see root CLAUDE.md) bans direct litellm /
anthropic / httpx imports in docgen/src and mandates the libraries +
gateway-by-default instead.
The workflow¶
.github/workflows/altaforge-enforce.yml is an 8-line caller of the org's
reusable workflow:
name: AltaForge enforce
on: pull_request
jobs:
altaforge-enforce:
uses: altaml/altaml-claude/.github/workflows/altaforge-enforce.yml@main
with:
paths: "docgen/src"
mode: warn
paths: "docgen/src"— only the product source is scanned (specs, fixtures, tools, mcp servers). Tests and scripts are out of scope.mode: warn— it runs on every PR and reports banned-import findings but does not fail the build. It is an advisory signal during the migration, not yet a hard gate.
Why warn (not enforce) — for now¶
Several files under docgen/src still import httpx directly today (e.g. the
web_page / web_search / file_ingest tools, the local phoenix / efs MCP
servers, the DealDesk cassette, and MCPTool itself). Flipping straight to
mode: enforce would red-X every PR until those are migrated to the
altaforge.* equivalents. Warn mode surfaces the debt on each PR so it trends
down; the intent is to graduate to mode: enforce once the existing direct
imports are cleared.
Reference¶
.github/workflows/altaforge-enforce.yml; reusable workflow:altaml/altaml-claude.github/workflows/altaforge-enforce.yml.- The banned-import rule + the
altaforge.*mandate: rootCLAUDE.md(tech-stack table, "LLM" + "NOT" columns).