Skip to content

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 "one httpx.Client per 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: root CLAUDE.md (tech-stack table, "LLM" + "NOT" columns).