Skip to content

Architecture ​

How the Server Works ​

When an MCP client connects (VS Code, Claude Desktop, Cursor), the server does three things:

  1. Sends instructions (~16K tokens) — a text blob the LLM reads to know which tools to combine for which questions
  2. Registers 300+ tools — each with a name, description, and parameter schema the LLM can call
  3. Registers 36 prompts — step-by-step analysis templates the user can invoke (e.g. /follow_the_money, /drug_safety)

The instructions and tools stay in the model's context for the entire session. Every question the user asks has access to them.

File Structure ​

src/
  server.ts                 # Entry point — auto-discovers API modules, parses CLI flags
  server/
    instructions.ts         # Builds the instructions string from module metadata
    curated-guides.ts       # Code Mode guide + analysis rules (hand-authored)
    prompts.ts              # 36 cross-cutting analysis prompts (module-aware)
    prompt-filter.ts        # Drops prompt steps that name tools which aren't loaded
    module-selection.ts     # --modules / --domains / --hide-unconfigured
    tool-registry.ts        # One validated entry point for calling any tool by name
    discovery.ts            # Discovery mode: find_tools + call_tool
    serve-tool.ts           # Wraps each tool: meta.sources + response size budget
    response-budget.ts      # Trims oversized results, keeping them valid JSON
    sources.ts              # Adds the upstream requests behind a result to meta.sources
    reference-resources.ts  # govdata://{module}/reference MCP resources
    http-auth.ts            # Bearer-token access control for the HTTP transport
    logger.ts               # Server logger passed to FastMCP
  shared/
    client.ts               # createClient() — HTTP client with cache, retry, rate-limit
    disk-cache.ts           # Disk-backed TTL cache (one file per response, LRU-swept)
    request-context.ts      # Records which upstream requests produced a result
    types.ts                # Domain, QuestionType, RouteHint, ModuleMeta, ApiModule
    response.ts             # Standardized JSON response helpers
    html.ts                 # htmlToText(): HTML → plain text, tags stripped and entities decoded once
    url.ts                  # isHostOrSubdomain(): exact host matching for URLs taken from API data
    geo.ts                  # State names, USPS codes and FIPS codes
    query-escape.ts         # Escaping for SoQL/OData filters built from parameters
    env.ts                  # Optional settings; ignores .env.example placeholders
    version.ts              # Package version and default User-Agent
    enum-utils.ts           # Zod enum helpers for tool parameters
    sandbox.ts              # WASM (QuickJS) sandbox for code mode
  apis/
    fred/                   # One folder per API (42 total)
      sdk.ts                # Typed API client (no MCP dependency — usable standalone)
      meta.ts               # Identity, auth, domains, crossRef routing hints
      tools.ts              # MCP tool definitions (Zod schemas → SDK calls)
      prompts.ts            # Module-specific prompts (optional, 17 modules have these)
      index.ts              # Barrel: combines meta + tools + prompts + clearCache
    treasury/
      ...

What Each File Does ​

apis/{name}/sdk.ts — API Client ​

A standalone typed HTTP client for one government API. Uses createClient() which provides disk-backed caching, retry, rate limiting, and auth injection. Has zero MCP or Zod dependencies — you can import it in any TypeScript project without running the MCP server.

apis/{name}/meta.ts — Module Metadata ​

Describes the module to the server. Key fields:

FieldPurpose
name, displayNameIdentity (e.g. "fred", "FRED (Federal Reserve Economic Data)")
descriptionOne-line summary shown in the instructions
workflowTool call sequence guidance (e.g. "fred_search → fred_series_data")
tipsUsage tips (rate limits, parameter gotchas, popular values)
domainsTopic areas this module serves (e.g. ["economy", "housing"])
crossRefRouting hints — maps question types to specific tools/parameters
auth?API key config — omit for keyless APIs
reference?Lookup tables exposed as MCP resources

apis/{name}/tools.ts — MCP Tools ​

Exports an array of FastMCP tool definitions. Each tool has a Zod parameter schema, delegates to the SDK, and returns JSON.stringify({ summary, ...data }).

apis/{name}/prompts.ts — Module Prompts (Optional) ​

17 modules export guided analysis prompts. These appear in the client's prompt picker (e.g. VS Code's / menu).

server.ts — Auto-Discovery ​

Scans apis/ at startup, imports each folder's index.ts, and:

  1. Selects modules (--modules, --domains, --hide-unconfigured; unknown names are an error)
  2. Registers the tools — every tool in full mode, or find_tools and call_tool in their place in discovery mode (code_mode and clear_cache are always there)
  3. Registers prompts (per-module + cross-cutting), minus steps that need unloaded tools
  4. Registers each module's reference data as a govdata://{module}/reference resource
  5. Builds the instructions string from module metadata
  6. Validates API keys and logs which are missing

No manual wiring — drop a new folder in apis/ and it's discovered automatically.

Tool results ​

Every tool goes through serveTool(). It records the upstream requests the tool made (URLs without credentials, with fetch time and cache status) and adds them to the result as meta.sources. It then holds the result to a size budget (MAX_RESPONSE_BYTES, default 150,000): oversized results lose rows or long strings, but stay valid JSON and say what was trimmed.

The Instructions String ​

The instructions string is the most important part of the system. It's the only context the LLM is guaranteed to see, and it determines whether the model calls the right tools for a question.

What's in it ​

┌─────────────────────────────────────────────────────────┐
│  Per-module blocks (42×)                      ~8K tokens │
│  ├─ == FRED (FEDERAL RESERVE ECONOMIC DATA) ==          │
│  ├─ 800K+ economic time series: GDP, CPI...            │
│  ├─ Workflow: fred_search → fred_series_data            │
│  ├─ Popular: GDP, UNRATE, CPIAUCSL, ...                 │
│  └─ Requires FRED_API_KEY.                              │
│                                                         │
│  Cross-reference routing table (36 lines)    ~7K tokens │
│  ├─ DEBT/DEFICIT → FRED(...) + Treasury(...) + ...      │
│  ├─ DRUG INVESTIGATION → FDA(...) + NIH(...) + ...      │
│  └─ HOUSING → HUD(...) + FRED(...) + Census(...) + ...  │
│                                                         │
│  Code Mode guide                            ~450 tokens │
│  Analysis rules (8 standards)               ~250 tokens │
└─────────────────────────────────────────────────────────┘
                                        Total: ~16K tokens

Tool names aren't repeated here: clients get them, with descriptions, from tools/list.

How the routing table works ​

Each module's meta.ts declares crossRef hints — "when someone asks about X, use these specific tools from me":

typescript
// fred/meta.ts
crossRef: [
  { question: "debt/deficit", route: "fred_series_data with GDP, FYFSGDA188S (deficit as % of GDP)" },
  { question: "housing", route: "fred_series_data with MORTGAGE30US, CSUSHPINSA" },
]

// treasury/meta.ts
crossRef: [
  { question: "debt/deficit", route: "treasury_query_fiscal_data with debt_to_penny, avg_interest_rates" },
]

At startup, buildRoutingTable() collects all hints, groups by question type, and produces:

DEBT/DEFICIT → FRED(fred_series_data with GDP, FYFSGDA188S) + Treasury(treasury_query_fiscal_data with debt_to_penny) + World Bank(wb_indicator with GC.DOD.TOTL.GD.ZS)

The LLM reads this and knows exactly which tools + parameters to call. Adding a new module with crossRef hints automatically extends the routing table.

The question field is type-checked — a typo like "defecit" fails compilation.

Selective loading ​

With MODULES=fred,bls,treasury, only those 3 modules are loaded. The instructions shrink to ~2K tokens because only their per-module blocks and routing entries are generated. DOMAINS=economy,health loads every module in those domains, and HIDE_UNCONFIGURED=1 skips modules whose required key isn't set. For the smallest footprint, TOOL_MODE=discovery lists four tools (find_tools, call_tool, code_mode, clear_cache) instead of all 350.

Prompts ​

There are two kinds of prompts:

Per-module prompts (17 modules) ​

Defined in apis/{name}/prompts.ts. These are specific to one API — e.g. FRED's recession_check prompt tells the LLM which FRED series indicate a recession.

Cross-cutting prompts (36 templates) ​

Defined in server/prompts.ts. These span multiple modules — e.g. follow_the_money traces PAC money through FEC → Congress → FRED → SEC → Open Payments.

Cross-cutting prompts are module-aware: when modules are selectively loaded (MODULES=fred,bls), prompt steps that reference unloaded tools are silently removed. The prompt reads naturally with only the available tools.

Key Design Decisions ​

DecisionWhy
Co-located API foldersEach API is self-contained — sdk, meta, tools, prompts in one folder
Auto-discoveryAdding an API = create a folder. No imports or wiring in server.ts
SDK ≠ MCPAPI clients have zero MCP/Zod dependency — usable in any project
Disk cacheMCP servers restart constantly (VS Code reloads); in-memory cache is useless
Auto-generated routing tableBuilt from crossRef metadata. Adding a module updates routing automatically
Instructions over resourcesMCP resources aren't auto-read by most clients. Everything critical goes in the instructions string; resources only carry optional reference tables
JSON over markdownTools return data; the client decides how to present it

Released under the MIT License.