Developer reference

AlphaCreek MCP — SEC filing retrieval

Hosted Model Context Protocol server for navigation-first reads of ingested US SEC filings (10-K, 10-Q, 20-F, 6-K, 8-K). JSON-RPC over Streamable HTTP; read-only tools; OAuth for identity. This page reflects the current server implementation.

Overview

Agents discover filings, load a table of contents, read selected navigation nodes, and cite passages using CITATION_URL lines returned with content. Stable filing identity is artifact_document_id (Mongo _id string, typically TICKER_FORM_ACCESSION).

Initialize response — On initialize, the server returns instructions (multi-line workflow guidance), protocolVersion: "2025-03-26", and serverInfo: { name: "alphacreek-mcp", version: "1.0.0" }.

Endpoint and protocol

Base URL: https://mcp.alphacreek.ai/mcp

Transport is Streamable HTTP with JSON-RPC 2.0 payloads (e.g. initialize, tools/list, tools/call). OAuth discovery is served from the same MCP origin; tool calls require an authenticated user.

1POST https://mcp.alphacreek.ai/mcp
2Content-Type: application/json
3
4{
5  "jsonrpc": "2.0",
6  "id": 1,
7  "method": "tools/call",
8  "params": {
9    "name": "list_filings",
10    "arguments": { "ticker": "MSFT", "document_type": "10-Q", "limit": 5 }
11  }
12}

Authentication

Tool execution requires OAuth (authorization code with PKCE, dynamic client registration, and refresh tokens as exposed by the AlphaCreek auth stack). Step-by-step client configuration lives on /mcp-docs.

Recommended agent workflow

The server embeds the following guidance in initialize.instructions(wording may evolve with releases):

1AlphaCreek MCP provides SEC filing navigation and source text.
21. Choose the filing period first. Multiple fiscal periods may be available per ticker (several 10-K/20-F and 10-Q/6-K filings). Call list_filings when you need a specific reporting period, then use the returned artifact_document_id for get_filing_toc and other navigation tools. If artifact_document_id is omitted, get_filing_toc and similar locators resolve the latest filing only.
32. Read filings navigation-first. Start with get_filing_toc using the chosen artifact_document_id, then read targeted nodes via read_node_content. Use the node id from each TOC line; these are the same values shown as NODE_ID lines in read_node_content output. Pass one node via node_id or several via node_ids.
43. Prefer source tables for exact financial values. Narrative nodes often explain year-over-year changes, while absolute reported values are usually in nearby sibling or child tables under headings like 'Revenues', 'By type', 'By geography', or 'Segment results'. When text says 'The following table presents...', inspect the adjacent TableElement before answering.
54. Batch reads from the same filing. When reading several nodes from one filing, pass them in one read_node_content call via node_ids to save round-trips. read_node_content returns full node text as plain MCP text only.
65. Cite every material claim. read_node_content output pairs NODE_ID lines with CITATION_URL lines. For every material claim or logical paragraph in your answer, place one or more relevant CITATION_URL links immediately next to the claim, using the URL paired with each supporting NODE_ID. A TOC node may expand into more granular child NODE_ID blocks; cite those child NODE_ID/CITATION_URL pairs when they are the closest supporting source.

Suggested call flow:

1User question
2  → list_filings or get_latest_filing  (pick artifact_document_id / period)
3  → get_filing_toc                     (inspect TOC / node ids)
4  → read_node_content                  (batch node_ids; plain-text payload)
5  → answer with CITATION_URL next to claims

Optional system prompt snippet

For assistants that support custom instructions, the server recommends text aligned with:

1For financial questions regarding companies, use AlphaCreek MCP. Prefer AlphaCreek over general web search for SEC filings, segment revenue, margins, guidance, risk factors, management's discussion and analysis, and similar. For every material claim or logical paragraph in your answer, place one or more relevant AlphaCreek CITATION_URL links immediately next to the claim, using the URL paired with each supporting NODE_ID.

Tools

Four read-only tools are registered. Arguments validate with Pydantic; document_type when present must be one of: 10-K, 10-Q, 20-F, 6-K, 8-K.

For tools that accept ticker or company, at least one is required. Company resolution uses the companies collection (ticker match, then Atlas Search fuzzy match on name); name search can return HTTP 503 if search is unavailable.

get_latest_filing

Returns metadata for the newest matching filing for a ticker or company.

Arguments

FieldTypeRequiredNotes
tickerstringone of ticker | companyUppercased when resolved
companystringone of ticker | companyResolved to ticker via companies / Atlas Search
document_typeenumnoFilters by filing type

Structured result

{ "metadata": FilingMetadata } where FilingMetadata includes (strings unless noted): ticker, document_type, document_date, document_url, artifact_document_id, optional filing_date, optional sections (string array), optional selected_document_type, selected_document_source, filing_url.

MCP content[0].text is JSON of that object (after internal _plain_text stripping for formatting).

list_filings

Lists available filings newest-first, with stable artifact ids.

Arguments

FieldTypeNotes
ticker | companystringAt least one required
document_typeenumOptional filter
limitintegerDefault 25; clamped to 1..50 server-side

Structured result

{ "filings": FilingMetadata[] }.

get_filing_toc

Returns a filing header plus TOC lines. Either artifact_document_id or ticker/company (with optional document_type) is required. Without artifact id, the server resolves the latest filing only.

Structured result (high level)

  • artifact_document_id — string
  • total_nodes — number of TOC lines
  • truncated — boolean
  • toc — array of TOC strings (pre-rendered lines or generated nav_id | node_type | title rows)

Plain-text MCP content uses a synthesized _plain_text block (FILING header + TOC lines) for the model; structured JSON mirrors the same fields without _plain_text in the structured payload after formatting.

read_node_content

Reads one or more navigation nodes for a single artifact. Arguments accept node_id and/or node_ids; Pydantic also accepts aliases nav_id / nav_ids (same semantics). artifact_document_id is required. Node ids match TOC / tree ids; citation URLs normalize ids with an sp- prefix when building reader links.

structuredContent: For read_node_content only, the server omits structuredContent on the tool result and returns plain text only in content[0].text (see tools/call handler in the MCP server).

Plain-text shape (illustrative)

1ARTIFACT_DOCUMENT_ID: MSFT_10-Q_000119312524000123
2
3NOT_FOUND_NODE_IDS: 9.9.9
4
5NODE_ID: 2.1
6TITLE: Management's Discussion and Analysis
7NODE_ID: sp-2.1.1
8CITATION_URL: https://www.alphacreek.ai/sec/reader?doc_id=MSFT_10-Q_000119312524000123&node_id=sp-2.1.1
9CONTENT_START
10…passage text…
11CONTENT_END

Per-node reads apply a combined character budget of 60,000 for rendered segments; oversized reads set truncated and remaining_chars on internal segment payloads before flattening to plain text.

Limits and batching

  • read_node_content: up to 60,000 characters of rendered segment text per request (combined across requested nodes as implemented in navigation helpers).
  • list_filings: limit clamped to 50 maximum.
  • Prefer batching multiple node_ids in one read_node_content call for the same filing to reduce round-trips.

HTTP errors (tool layer)

StatusTypical cause
400Missing required locator (e.g. ticker/company), invalid document_type, missing node ids
404No filings for ticker/type; filing or navigation not found; company name not resolved to ticker
503Company name Atlas Search unavailable (check index / mapping)

Runnable example: Python + OpenAI

Uses the official mcp Streamable HTTP client, your AlphaCreek MCP API key as Authorization: Bearer …, and OpenAI's client.responses.create with hosted tools mapped from tools/list. The marketing page shows an illustrative sketch; this is the full script you can save and run.

Install and run

  1. Python 3.10+ recommended.
  2. pip install mcp openai
  3. Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and OPENAI_API_KEY.
  4. Optional: ALPHACREEK_MCP_URL (default https://mcp.alphacreek.ai/mcp), OPENAI_MODEL (default gpt-5.4-mini in the script).
  5. Save the script below as python_openai.py (same content as project_ff/alphacreek_mcp/examples/python_openai.py in the AlphaCreek repo) and run python python_openai.py.

Claude / ChatGPT setup and OAuth-oriented flows are documented on /mcp-docs.

Script

Python
1#!/usr/bin/env python3
2"""OpenAI + AlphaCreek MCP: one question → tools → print answer.
3
4Needs: ALPHACREEK_API_KEY, OPENAI_API_KEY.
5
6Canonical runnable example for the MCP developer docs and the marketing-site full script
7(alphacreek-ui src/sections/docs/mcp-openai-python-example.ts, export MCP_OPENAI_PYTHON_FULL_SCRIPT).
8Update that embedded string when you change this file.
9"""
10
11import asyncio
12import json
13import os
14import sys
15from typing import Any
16
17import mcp.types as T
18from mcp import ClientSession
19from mcp.client.streamable_http import streamablehttp_client
20from openai import OpenAI
21from openai.types.shared.reasoning import Reasoning
22
23MCP_URL = os.environ.get("ALPHACREEK_MCP_URL", "https://mcp.alphacreek.ai/mcp").strip()
24MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.4-mini").strip()
25QUESTION = (
26    "What are NVIDIA’s biggest risks in the latest 10-K? "
27)
28SYSTEM = "Answer SEC questions using only tools; ground every claim in tool output."
29
30
31def _env(name: str) -> str:
32    v = (os.environ.get(name) or "").strip()
33    if not v:
34        sys.exit(f"Missing {name}")
35    return v
36
37
38def _tool_text(r: T.CallToolResult) -> str:
39    parts = [b.text for b in (r.content or []) if isinstance(b, T.TextContent) and b.text]
40    if parts:
41        return "\n".join(parts)
42    d: dict[str, Any] = {}
43    if isinstance(r.structuredContent, dict):
44        d["structuredContent"] = r.structuredContent
45    if r.isError:
46        d["isError"] = True
47    return json.dumps(d or {"isError": r.isError}, ensure_ascii=True)
48
49
50def _to_fc_tools(tools: list[T.Tool]) -> list[dict[str, Any]]:
51    return [
52        {
53            "type": "function",
54            "name": t.name,
55            "description": ((t.description or "").strip() or None),
56            "parameters": t.inputSchema if isinstance(t.inputSchema, dict) else {"type": "object", "properties": {}},
57            "strict": False,
58        }
59        for t in tools
60    ]
61
62
63def _split_output(items: list[Any]) -> tuple[list[Any], str]:
64    calls, texts = [], []
65    for it in items or []:
66        if getattr(it, "type", None) == "function_call":
67            calls.append(it)
68        elif getattr(it, "type", None) == "message":
69            for b in getattr(it, "content", None) or []:
70                if getattr(b, "type", None) == "output_text":
71                    tx = getattr(b, "text", "") or ""
72                    if tx:
73                        texts.append(tx)
74    return calls, "\n".join(texts).strip()
75
76
77async def main() -> None:
78    oai = OpenAI(api_key=_env("OPENAI_API_KEY"))
79    reasoning = Reasoning(effort="low")  # type: ignore[arg-type]
80    hdrs = {"Authorization": f"Bearer {_env('ALPHACREEK_API_KEY')}"}
81    async with streamablehttp_client(
82        MCP_URL, headers=hdrs, timeout=120, sse_read_timeout=300, terminate_on_close=True
83    ) as (read, write, _):
84        async with ClientSession(read, write, client_info=T.Implementation(name="demo", version="0")) as s:
85            await s.initialize()
86            fcs = _to_fc_tools((await s.list_tools()).tools)
87            if not fcs:
88                sys.exit("MCP returned no tools")
89            prev: str | None = None
90            nxt: list[dict[str, Any]] = []
91            for _ in range(8):
92                common = dict(model=MODEL, tools=fcs, tool_choice="auto", temperature=1, reasoning=reasoning)
93                if prev is None:
94                    resp = await asyncio.to_thread(
95                        oai.responses.create,
96                        instructions=SYSTEM,
97                        input=[{"type": "message", "role": "user", "content": QUESTION}],
98                        **common,
99                    )
100                else:
101                    resp = await asyncio.to_thread(
102                        oai.responses.create, previous_response_id=prev, input=nxt, **common
103                    )
104                if getattr(resp, "error", None):
105                    sys.exit(f"OpenAI error: {resp.error}")
106                prev = resp.id
107                calls, text = _split_output(list(resp.output or []))
108                if not calls:
109                    if not text:
110                        sys.exit("No text and no tool calls")
111                    print(text)
112                    return
113                nxt = []
114                for c in calls:
115                    try:
116                        args = json.loads(c.arguments or "{}")
117                    except json.JSONDecodeError:
118                        args = {}
119                    if not isinstance(args, dict):
120                        args = {}
121                    raw = await s.call_tool(c.name, args)
122                    nxt.append(
123                        {"type": "function_call_output", "call_id": c.call_id, "output": _tool_text(raw)}
124                    )
125            sys.exit("Too many tool rounds")
126
127
128if __name__ == "__main__":
129    asyncio.run(main())
130

Runnable example: JavaScript + OpenAI

Uses native fetch (Node.js 18+) for Streamable HTTP MCP, your AlphaCreek MCP API key as Authorization: Bearer …, and OpenAI's client.responses.create with function tools mapped from tools/list (same flow as the Python example: previous_response_id plus function_call_output items on follow-up turns). The marketing page sketch is illustrative; this is the full runnable file.

Install and run

  1. Node.js 18+ (requires global fetch).
  2. npm install openai in an empty folder (or add to an existing project).
  3. Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and OPENAI_API_KEY.
  4. Optional: ALPHACREEK_MCP_URL, OPENAI_MODEL, QUESTION, MCP_MAX_TOOL_ROUNDS.
  5. Save the script as javascript_openai.mjs (same content as project_ff/alphacreek_mcp/examples/javascript_openai.mjs) and run node javascript_openai.mjs.

ChatGPT / OAuth-oriented setup is documented on /mcp-docs.

Script

JavaScript
1#!/usr/bin/env node
2/**
3 * OpenAI Responses API + AlphaCreek MCP (Node.js 18+): one question → tools → stdout.
4 *
5 * Requires global fetch (Node 18+). MCP uses JSON-RPC POST with SSE responses.
6 *
7 * Needs: ALPHACREEK_API_KEY, OPENAI_API_KEY
8 *
9 * Canonical runnable example for the MCP developer docs and the marketing-site full script
10 * (alphacreek-ui src/sections/docs/mcp-openai-javascript-example.ts → MCP_OPENAI_JS_FULL_SCRIPT).
11 */
12
13import OpenAI from 'openai';
14
15const MCP_URL = (process.env.ALPHACREEK_MCP_URL || 'https://mcp.alphacreek.ai/mcp').trim();
16const MODEL = (process.env.OPENAI_MODEL || 'gpt-5.4-mini').trim();
17const MAX_TOOL_ROUNDS = Number.parseInt(process.env.MCP_MAX_TOOL_ROUNDS || '8', 10);
18
19const QUESTION =
20  process.env.QUESTION || "What are NVIDIA’s biggest risks in the latest 10-K? ";
21
22const SYSTEM = 'Answer SEC questions using only tools; ground every claim in tool output.';
23
24function requireEnv(name) {
25  const v = process.env[name];
26  const s = typeof v === 'string' ? v.trim() : '';
27  if (!s) {
28    console.error('Missing env: ' + name);
29    process.exit(1);
30  }
31  return s;
32}
33
34class StreamableHttpMcpClient {
35  constructor(baseUrl, bearerToken) {
36    this.baseUrl = baseUrl;
37    this.bearerToken = bearerToken;
38    /** @type {string | null} */
39    this.sessionId = null;
40    this.msgId = 0;
41  }
42
43  /**
44   * @param {string} method
45   * @param {unknown} params
46   */
47  async rpc(method, params) {
48    const id = ++this.msgId;
49    /** @type {Record<string, string>} */
50    const headers = {
51      'Content-Type': 'application/json',
52      Accept: 'application/json, text/event-stream',
53      Authorization: 'Bearer ' + this.bearerToken,
54    };
55    const sid = this.sessionId;
56    if (sid) headers['mcp-session-id'] = sid;
57
58    const res = await fetch(this.baseUrl, {
59      method: 'POST',
60      headers,
61      body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
62    });
63    const nh = res.headers.get('mcp-session-id');
64    if (nh) this.sessionId = nh;
65
66    if (!res.ok) {
67      const body = await res.text();
68      throw new Error('MCP HTTP ' + res.status + ': ' + body.slice(0, 600));
69    }
70
71    const ct = res.headers.get('content-type') || '';
72    if (ct.includes('text/event-stream')) {
73      const parsed = this.parseSSE(await res.text(), id);
74      if (parsed == null) throw new Error('MCP SSE: no matching jsonrpc envelope');
75      return parsed;
76    }
77    const json = await res.json();
78    if (json.error) throw new Error(json.error.message || 'MCP JSON-RPC error');
79    return json.result;
80  }
81
82  /**
83   * @param {string} text
84   * @param {number} targetId
85   */
86  parseSSE(text, targetId) {
87    const blocks = text.split('\n\n');
88    for (const block of blocks) {
89      for (const line of block.split('\n')) {
90        if (!line.startsWith('data:')) continue;
91        try {
92          const obj = JSON.parse(line.slice(5).trim());
93          if (obj.id === targetId) {
94            if (obj.error) throw new Error(obj.error.message || 'MCP SSE error');
95            return obj.result;
96          }
97        } catch (e) {
98          if (e instanceof SyntaxError) continue;
99          throw e;
100        }
101      }
102    }
103    return null;
104  }
105
106  async connect() {
107    await this.rpc('initialize', {
108      protocolVersion: '2025-03-26',
109      capabilities: {},
110      clientInfo: { name: 'alphacreek-js-demo', version: '0' },
111    });
112  }
113
114  async listTools() {
115    const result = await this.rpc('tools/list', {});
116    return result?.tools ?? [];
117  }
118
119  /**
120   * @param {string} name
121   * @param {Record<string, unknown>} args
122   */
123  async callTool(name, args) {
124    return this.rpc('tools/call', { name, arguments: args || {} });
125  }
126}
127
128/** @param {unknown[]} tools */
129function toOpenAiFcTools(tools) {
130  return tools.map((t) => {
131    const tt = /** @type {{ name: string; description?: string; inputSchema?: object; input_schema?: object }} */ (t);
132    const params = tt.inputSchema || tt.input_schema || { type: 'object', properties: {} };
133    const desc = (tt.description || '').trim();
134    return {
135      type: 'function',
136      name: tt.name,
137      description: desc || null,
138      parameters: params,
139      strict: false,
140    };
141  });
142}
143
144/**
145 * @param {unknown[]} items
146 * @returns {{ calls: unknown[]; text: string }}
147 */
148function splitOutput(items) {
149  const calls = [];
150  const texts = [];
151  for (const it of items || []) {
152    const o = /** @type {{ type?: string; content?: unknown[] }} */ (it);
153    if (o.type === 'function_call') calls.push(it);
154    else if (o.type === 'message') {
155      for (const b of o.content || []) {
156        const bb = /** @type {{ type?: string; text?: string }} */ (b);
157        if (bb.type === 'output_text' && bb.text) texts.push(bb.text);
158      }
159    }
160  }
161  return { calls, text: texts.join('\n').trim() };
162}
163
164/** @param {unknown} raw */
165function toolResultToString(raw) {
166  if (!raw || typeof raw !== 'object') return JSON.stringify(raw ?? null);
167  const r = /** @type {{ content?: unknown[]; structuredContent?: unknown; isError?: boolean }} */ (raw);
168  if (Array.isArray(r.content)) {
169    const parts = r.content.map((c) => {
170      if (typeof c === 'string') return c;
171      if (c && typeof c === 'object' && typeof /** @type {{ text?: string }} */ (c).text === 'string') {
172        return /** @type {{ text: string }} */ (c).text;
173      }
174      return JSON.stringify(c);
175    });
176    if (parts.some(Boolean)) return parts.join('\n');
177  }
178  const d = {};
179  if (r.structuredContent && typeof r.structuredContent === 'object') d.structuredContent = r.structuredContent;
180  if (r.isError) d.isError = true;
181  return JSON.stringify(Object.keys(d).length ? d : { isError: !!r.isError });
182}
183
184async function main() {
185  const openaiKey = requireEnv('OPENAI_API_KEY');
186  const creekKey = requireEnv('ALPHACREEK_API_KEY');
187
188  const mcp = new StreamableHttpMcpClient(MCP_URL, creekKey);
189  await mcp.connect();
190  const mcpTools = await mcp.listTools();
191  const fcs = toOpenAiFcTools(mcpTools);
192  if (!fcs.length) {
193    console.error('MCP returned no tools');
194    process.exit(1);
195  }
196
197  const client = new OpenAI({ apiKey: openaiKey });
198
199  /** @type {string | null} */
200  let prev = null;
201  /** @type {unknown[]} */
202  let nxt = [];
203
204  const reasoning = { effort: 'low' };
205
206  for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
207    /** @type {Record<string, unknown>} */
208    const common = {
209      model: MODEL,
210      tools: fcs,
211      tool_choice: 'auto',
212      temperature: 1,
213      reasoning,
214    };
215
216    /** @type {Record<string, unknown>} */
217    const body =
218      prev == null
219        ? {
220            ...common,
221            instructions: SYSTEM,
222            input: [{ type: 'message', role: 'user', content: QUESTION }],
223          }
224        : {
225            ...common,
226            previous_response_id: prev,
227            input: nxt,
228          };
229
230    const resp = await client.responses.create(/** @type {any} */ (body));
231
232    if (resp?.error) {
233      console.error('OpenAI error: ' + JSON.stringify(resp.error));
234      process.exit(1);
235    }
236
237    prev = resp.id;
238    const out = splitOutput(/** @type {unknown[]} */ (resp.output || []));
239    if (!out.calls.length) {
240      if (!out.text) {
241        console.error('No text and no tool calls');
242        process.exit(1);
243      }
244      console.log(out.text);
245      return;
246    }
247
248    nxt = [];
249    for (const c of out.calls) {
250      const fc = /** @type {{ name: string; call_id: string; arguments?: string }} */ (c);
251      let args = {};
252      try {
253        args = JSON.parse(fc.arguments || '{}');
254      } catch {
255        args = {};
256      }
257      if (!args || typeof args !== 'object' || Array.isArray(args)) args = {};
258      const raw = await mcp.callTool(fc.name, /** @type {Record<string, unknown>} */ (args));
259      nxt.push({
260        type: 'function_call_output',
261        call_id: fc.call_id,
262        output: toolResultToString(raw),
263      });
264    }
265  }
266
267  console.error('Too many tool rounds');
268  process.exit(1);
269}
270
271main().catch((err) => {
272  console.error(err);
273  process.exit(1);
274});
275
276

Runnable example: Python + Anthropic (Claude)

Uses the official mcp Streamable HTTP client, your AlphaCreek MCP API key as Authorization: Bearer …, and Anthropic's client.messages.create with tools mapped from tools/list (input_schema comes from each tool's JSON Schema). The marketing page shows an illustrative sketch; this is the full script you can save and run.

Install and run

  1. Python 3.10+ recommended.
  2. pip install mcp anthropic
  3. Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and ANTHROPIC_API_KEY.
  4. Optional: ALPHACREEK_MCP_URL (default https://mcp.alphacreek.ai/mcp), ANTHROPIC_MODEL (default claude-sonnet-4-6), ANTHROPIC_MAX_TOKENS (default 8192).
  5. Save the script below as python_anthropic.py (same content as project_ff/alphacreek_mcp/examples/python_anthropic.py in the AlphaCreek repo) and run python python_anthropic.py.

Claude / ChatGPT setup and OAuth-oriented flows are documented on /mcp-docs.

Script

Python
1#!/usr/bin/env python3
2"""Anthropic Claude + AlphaCreek MCP: one question → tools → print answer.
3
4Needs: ALPHACREEK_API_KEY, ANTHROPIC_API_KEY.
5
6Canonical runnable example for the MCP developer docs and the marketing-site full script
7(alphacreek-ui src/sections/docs/mcp-anthropic-python-example.ts, export MCP_ANTHROPIC_PYTHON_FULL_SCRIPT).
8Update that embedded string when you change this file.
9"""
10
11import asyncio
12import functools
13import json
14import os
15import sys
16from typing import Any
17
18import mcp.types as T
19from anthropic import Anthropic
20from mcp import ClientSession
21from mcp.client.streamable_http import streamablehttp_client
22
23MCP_URL = os.environ.get("ALPHACREEK_MCP_URL", "https://mcp.alphacreek.ai/mcp").strip()
24MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-6").strip()
25MAX_TOKENS = int(os.environ.get("ANTHROPIC_MAX_TOKENS", "8192"))
26QUESTION = (
27    "What are NVIDIA’s biggest risks in the latest 10-K? "
28)
29SYSTEM = "Answer SEC questions using only tools; ground every claim in tool output."
30
31
32def _env(name: str) -> str:
33    v = (os.environ.get(name) or "").strip()
34    if not v:
35        sys.exit(f"Missing {name}")
36    return v
37
38
39def _tool_text(r: T.CallToolResult) -> str:
40    parts = [b.text for b in (r.content or []) if isinstance(b, T.TextContent) and b.text]
41    if parts:
42        return "\n".join(parts)
43    d: dict[str, Any] = {}
44    if isinstance(r.structuredContent, dict):
45        d["structuredContent"] = r.structuredContent
46    if r.isError:
47        d["isError"] = True
48    return json.dumps(d or {"isError": r.isError}, ensure_ascii=True)
49
50
51def _to_anthropic_tools(tools: list[T.Tool]) -> list[dict[str, Any]]:
52    return [
53        {
54            "name": t.name,
55            "description": ((t.description or "").strip() or "(MCP tool)"),
56            "input_schema": t.inputSchema
57            if isinstance(t.inputSchema, dict)
58            else {"type": "object", "properties": {}},
59        }
60        for t in tools
61    ]
62
63
64def _assistant_text(message: Any) -> str:
65    parts: list[str] = []
66    for b in getattr(message, "content", None) or []:
67        if getattr(b, "type", None) == "text":
68            tx = (getattr(b, "text", None) or "").strip()
69            if tx:
70                parts.append(tx)
71    return "\n".join(parts).strip()
72
73
74async def main() -> None:
75    ac = Anthropic(api_key=_env("ANTHROPIC_API_KEY"))
76    hdrs = {"Authorization": f"Bearer {_env('ALPHACREEK_API_KEY')}"}
77    async with streamablehttp_client(
78        MCP_URL, headers=hdrs, timeout=120, sse_read_timeout=300, terminate_on_close=True
79    ) as (read, write, _):
80        async with ClientSession(read, write, client_info=T.Implementation(name="demo", version="0")) as s:
81            await s.initialize()
82            atools = _to_anthropic_tools((await s.list_tools()).tools)
83            if not atools:
84                sys.exit("MCP returned no tools")
85            messages: list[dict[str, Any]] = [{"role": "user", "content": QUESTION}]
86            for _ in range(8):
87                call = functools.partial(
88                    ac.messages.create,
89                    model=MODEL,
90                    max_tokens=MAX_TOKENS,
91                    system=SYSTEM,
92                    messages=messages,
93                    tools=atools,
94                    tool_choice={"type": "auto"},
95                )
96                resp = await asyncio.to_thread(call)
97                reason = getattr(resp, "stop_reason", None)
98
99                if reason == "end_turn":
100                    text = _assistant_text(resp)
101                    if text:
102                        print(text)
103                        return
104                    sys.exit("No assistant text")
105
106                if reason != "tool_use":
107                    text = _assistant_text(resp)
108                    if text:
109                        print(text)
110                        return
111                    sys.exit(f"Stopped with {reason!r} and no usable text")
112
113                messages.append({"role": "assistant", "content": resp.content})
114                tool_results: list[dict[str, Any]] = []
115                for b in resp.content or []:
116                    if getattr(b, "type", None) != "tool_use":
117                        continue
118                    name = getattr(b, "name", "") or ""
119                    tool_id = getattr(b, "id", "") or ""
120                    raw_in = getattr(b, "input", None)
121                    args = raw_in if isinstance(raw_in, dict) else {}
122                    raw = await s.call_tool(name, args)
123                    tool_results.append(
124                        {
125                            "type": "tool_result",
126                            "tool_use_id": tool_id,
127                            "content": _tool_text(raw),
128                        }
129                    )
130                if not tool_results:
131                    sys.exit("stop_reason was tool_use but no tool_use blocks were found")
132                messages.append({"role": "user", "content": tool_results})
133            sys.exit("Too many tool rounds")
134
135
136if __name__ == "__main__":
137    asyncio.run(main())
138

Runnable example: JavaScript + Anthropic (Claude)

Uses native fetch (Node.js 18+) to speak Streamable HTTP MCP (initialize, tools/list, tools/call with Authorization: Bearer …, session id header when returned), maps MCP tool schemas into Anthropic input_schema, and loops on stop_reason === "tool_use" until end_turn. The marketing page sketch is illustrative; this is the full runnable file.

Install and run

  1. Node.js 18+ (requires global fetch).
  2. npm install @anthropic-ai/sdk in an empty folder (or use your package manager).
  3. Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and ANTHROPIC_API_KEY.
  4. Optional: ALPHACREEK_MCP_URL (default https://mcp.alphacreek.ai/mcp), ANTHROPIC_MODEL, ANTHROPIC_MAX_TOKENS (default 8192), QUESTION, MCP_MAX_TOOL_ROUNDS.
  5. Save the script below as javascript_anthropic.mjs (same content as project_ff/alphacreek_mcp/examples/javascript_anthropic.mjs) and run node javascript_anthropic.mjs.

Claude connectors and OAuth-centric setup are documented on /mcp-docs.

Script

JavaScript
1#!/usr/bin/env node
2/**
3 * Anthropic Claude + AlphaCreek MCP (Node.js 18+): one question → tools → stdout.
4 *
5 * Requires global fetch (Node 18+). MCP uses JSON-RPC POST with SSE responses.
6 *
7 * Needs: ALPHACREEK_API_KEY, ANTHROPIC_API_KEY
8 *
9 * Canonical runnable example for the MCP developer docs and the marketing-site full script
10 * (alphacreek-ui src/sections/docs/mcp-anthropic-javascript-example.ts → MCP_ANTHROPIC_JS_FULL_SCRIPT).
11 */
12
13import Anthropic from '@anthropic-ai/sdk';
14
15const MCP_URL = (process.env.ALPHACREEK_MCP_URL || 'https://mcp.alphacreek.ai/mcp').trim();
16const MODEL = (process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-6').trim();
17const MAX_TOKENS = Number.parseInt(process.env.ANTHROPIC_MAX_TOKENS || '8192', 10);
18const MAX_TOOL_ROUNDS = Number.parseInt(process.env.MCP_MAX_TOOL_ROUNDS || '8', 10);
19
20const QUESTION =
21  process.env.QUESTION || "What are NVIDIA’s biggest risks in the latest 10-K? ";
22
23const SYSTEM = 'Answer SEC questions using only tools; ground every claim in tool output.';
24
25function requireEnv(name) {
26  const v = process.env[name];
27  const s = typeof v === 'string' ? v.trim() : '';
28  if (!s) {
29    console.error('Missing env: ' + name);
30    process.exit(1);
31  }
32  return s;
33}
34
35class StreamableHttpMcpClient {
36  constructor(baseUrl, bearerToken) {
37    this.baseUrl = baseUrl;
38    this.bearerToken = bearerToken;
39    /** @type {string | null} */
40    this.sessionId = null;
41    this.msgId = 0;
42  }
43
44  /**
45   * @param {string} method
46   * @param {unknown} params
47   */
48  async rpc(method, params) {
49    const id = ++this.msgId;
50    /** @type {Record<string, string>} */
51    const headers = {
52      'Content-Type': 'application/json',
53      Accept: 'application/json, text/event-stream',
54      Authorization: 'Bearer ' + this.bearerToken,
55    };
56    const sid = this.sessionId;
57    if (sid) headers['mcp-session-id'] = sid;
58
59    const res = await fetch(this.baseUrl, {
60      method: 'POST',
61      headers,
62      body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
63    });
64    const nh = res.headers.get('mcp-session-id');
65    if (nh) this.sessionId = nh;
66
67    if (!res.ok) {
68      const body = await res.text();
69      throw new Error('MCP HTTP ' + res.status + ': ' + body.slice(0, 600));
70    }
71
72    const ct = res.headers.get('content-type') || '';
73    if (ct.includes('text/event-stream')) {
74      const parsed = this.parseSSE(await res.text(), id);
75      if (parsed == null) throw new Error('MCP SSE: no matching jsonrpc envelope');
76      return parsed;
77    }
78    const json = await res.json();
79    if (json.error) throw new Error(json.error.message || 'MCP JSON-RPC error');
80    return json.result;
81  }
82
83  /**
84   * @param {string} text
85   * @param {number} targetId
86   */
87  parseSSE(text, targetId) {
88    const blocks = text.split('\n\n');
89    for (const block of blocks) {
90      for (const line of block.split('\n')) {
91        if (!line.startsWith('data:')) continue;
92        try {
93          const obj = JSON.parse(line.slice(5).trim());
94          if (obj.id === targetId) {
95            if (obj.error) throw new Error(obj.error.message || 'MCP SSE error');
96            return obj.result;
97          }
98        } catch (e) {
99          if (e instanceof SyntaxError) continue;
100          throw e;
101        }
102      }
103    }
104    return null;
105  }
106
107  async connect() {
108    await this.rpc('initialize', {
109      protocolVersion: '2025-03-26',
110      capabilities: {},
111      clientInfo: { name: 'alphacreek-js-demo', version: '0' },
112    });
113  }
114
115  async listTools() {
116    const result = await this.rpc('tools/list', {});
117    return result?.tools ?? [];
118  }
119
120  /**
121   * @param {string} name
122   * @param {Record<string, unknown>} args
123   */
124  async callTool(name, args) {
125    return this.rpc('tools/call', { name, arguments: args || {} });
126  }
127}
128
129/** @param {unknown[]} tools */
130function toAnthropicTools(tools) {
131  return tools.map((t) => {
132    const tt = /** @type {{ name: string; description?: string; inputSchema?: object; input_schema?: object }} */ (t);
133    const schema = tt.inputSchema || tt.input_schema || { type: 'object', properties: {} };
134    return {
135      name: tt.name,
136      description: ((tt.description || '').trim()) || '(MCP tool)',
137      input_schema: schema,
138    };
139  });
140}
141
142/** @param {unknown} raw */
143function toolResultToString(raw) {
144  if (!raw || typeof raw !== 'object') return JSON.stringify(raw ?? null);
145  const r = /** @type {{ content?: unknown[] }} */ (raw);
146  if (!Array.isArray(r.content)) return JSON.stringify(raw);
147  const parts = r.content.map((c) => {
148    if (typeof c === 'string') return c;
149    if (c && typeof c === 'object' && typeof /** @type {{ text?: string }} */ (c).text === 'string') {
150      return /** @type {{ text: string }} */ (c).text;
151    }
152    return JSON.stringify(c);
153  });
154  return parts.join('\n');
155}
156
157async function main() {
158  const anthropicKey = requireEnv('ANTHROPIC_API_KEY');
159  const creekKey = requireEnv('ALPHACREEK_API_KEY');
160
161  const mcp = new StreamableHttpMcpClient(MCP_URL, creekKey);
162  await mcp.connect();
163  const mcpTools = await mcp.listTools();
164  const tools = toAnthropicTools(mcpTools);
165  if (!tools.length) {
166    console.error('MCP returned no tools');
167    process.exit(1);
168  }
169
170  const anthropic = new Anthropic({ apiKey: anthropicKey });
171
172  const messages = [{ role: 'user', content: QUESTION }];
173
174  for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
175    const res = await anthropic.messages.create({
176      model: MODEL,
177      max_tokens: MAX_TOKENS,
178      system: SYSTEM,
179      messages,
180      tools,
181    });
182
183    if (res.stop_reason === 'end_turn') {
184      const text = res.content
185        .filter((/** @type {{ type: string }} */ b) => b.type === 'text')
186        .map((/** @type {{ type: string; text?: string }} */ b) => b.text || '')
187        .join('\n')
188        .trim();
189      if (!text) {
190        console.error('No assistant text');
191        process.exit(1);
192      }
193      console.log(text);
194      return;
195    }
196
197    if (res.stop_reason === 'tool_use') {
198      messages.push({ role: 'assistant', content: res.content });
199      const toolResults = [];
200      for (const block of res.content) {
201        if (block.type !== 'tool_use') continue;
202        const tu = /** @type {{ type: 'tool_use'; id: string; name: string; input: Record<string, unknown> }} */ (block);
203        const raw = await mcp.callTool(tu.name, tu.input || {});
204        toolResults.push({
205          type: 'tool_result',
206          tool_use_id: tu.id,
207          content: toolResultToString(raw),
208        });
209      }
210      if (!toolResults.length) {
211        console.error('stop_reason was tool_use but no tool_use blocks');
212        process.exit(1);
213      }
214      messages.push({ role: 'user', content: toolResults });
215      continue;
216    }
217
218    const fallback = res.content
219      .filter((/** @type {{ type: string }} */ b) => b.type === 'text')
220      .map((/** @type {{ type: string; text?: string }} */ b) => b.text || '')
221      .join('\n')
222      .trim();
223    if (fallback) {
224      console.log(fallback);
225      return;
226    }
227    console.error('Unexpected stop_reason: ' + res.stop_reason);
228    process.exit(1);
229  }
230
231  console.error('Too many tool rounds');
232  process.exit(1);
233}
234
235main().catch((err) => {
236  console.error(err);
237  process.exit(1);
238});
239

Runnable example: Python + Gemini

Uses the official mcp Streamable HTTP client, your AlphaCreek MCP API key as Authorization: Bearer …, and Google's google-genai Client.models.generate_content with FunctionDeclarationpayloads mapped from tools/list (JSON Schema becomes parameters_json_schema). The marketing page shows an illustrative sketch; this is the full script you can save and run.

Install and run

  1. Python 3.10+ recommended.
  2. pip install mcp google-genai
  3. Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and a Gemini key as GEMINI_API_KEY or GOOGLE_API_KEY.
  4. Optional: ALPHACREEK_MCP_URL (default https://mcp.alphacreek.ai/mcp), GEMINI_MODEL (default gemini-3-flash-preview), GEMINI_MAX_OUTPUT_TOKENS (default 8192).
  5. Save the script below as python_gemini.py (same content as project_ff/alphacreek_mcp/examples/python_gemini.py in the AlphaCreek repo) and run python python_gemini.py.

Claude / ChatGPT setup and OAuth-oriented flows are documented on /mcp-docs.

Script

Python
1#!/usr/bin/env python3
2"""Google Gemini + AlphaCreek MCP: one question → tools → print answer.
3
4Needs: ALPHACREEK_API_KEY and GEMINI_API_KEY or GOOGLE_API_KEY.
5
6Canonical runnable example for the MCP developer docs and the marketing-site full script
7(alphacreek-ui src/sections/docs/mcp-gemini-python-example.ts, export MCP_GEMINI_PYTHON_FULL_SCRIPT).
8Update that embedded string when you change this file.
9"""
10
11import asyncio
12import functools
13import json
14import os
15import sys
16from typing import Any
17
18import mcp.types as T
19from google import genai
20from google.genai import types
21from mcp import ClientSession
22from mcp.client.streamable_http import streamablehttp_client
23
24MCP_URL = os.environ.get("ALPHACREEK_MCP_URL", "https://mcp.alphacreek.ai/mcp").strip()
25MODEL = os.environ.get("GEMINI_MODEL", "gemini-3-flash-preview").strip()
26MAX_OUTPUT_TOKENS = int(os.environ.get("GEMINI_MAX_OUTPUT_TOKENS", "8192"))
27QUESTION = (
28    "What are NVIDIA’s biggest risks in the latest 10-K? "
29)
30SYSTEM = "Answer SEC questions using only tools; ground every claim in tool output."
31
32
33def _env(name: str) -> str:
34    v = (os.environ.get(name) or "").strip()
35    if not v:
36        sys.exit(f"Missing {name}")
37    return v
38
39
40def _gemini_api_key() -> str:
41    for name in ("GEMINI_API_KEY", "GOOGLE_API_KEY"):
42        v = (os.environ.get(name) or "").strip()
43        if v:
44            return v
45    sys.exit("Missing GEMINI_API_KEY or GOOGLE_API_KEY")
46
47
48def _tool_text(r: T.CallToolResult) -> str:
49    parts = [b.text for b in (r.content or []) if isinstance(b, T.TextContent) and b.text]
50    if parts:
51        return "\n".join(parts)
52    d: dict[str, Any] = {}
53    if isinstance(r.structuredContent, dict):
54        d["structuredContent"] = r.structuredContent
55    if r.isError:
56        d["isError"] = True
57    return json.dumps(d or {"isError": r.isError}, ensure_ascii=True)
58
59
60def _to_gemini_tool(tools: list[T.Tool]) -> types.Tool:
61    decls: list[types.FunctionDeclaration] = []
62    for t in tools:
63        schema = t.inputSchema if isinstance(t.inputSchema, dict) else {"type": "object", "properties": {}}
64        decls.append(
65            types.FunctionDeclaration(
66                name=t.name,
67                description=((t.description or "").strip() or "(MCP tool)"),
68                parameters_json_schema=schema,
69            )
70        )
71    return types.Tool(function_declarations=decls)
72
73
74def _fc_args(fc: types.FunctionCall) -> dict[str, Any]:
75    a = fc.args
76    if a is None:
77        return {}
78    if isinstance(a, dict):
79        return dict(a)
80    return dict(a)
81
82
83def _parts_text(parts: list[types.Part] | None) -> str:
84    out: list[str] = []
85    for p in parts or []:
86        if p.text:
87            tx = p.text.strip()
88            if tx:
89                out.append(tx)
90    return "\n".join(out).strip()
91
92
93async def main() -> None:
94    client = genai.Client(api_key=_gemini_api_key())
95    hdrs = {"Authorization": f"Bearer {_env('ALPHACREEK_API_KEY')}"}
96    async with streamablehttp_client(
97        MCP_URL, headers=hdrs, timeout=120, sse_read_timeout=300, terminate_on_close=True
98    ) as (read, write, _):
99        async with ClientSession(read, write, client_info=T.Implementation(name="demo", version="0")) as s:
100            await s.initialize()
101            mcp_tools = (await s.list_tools()).tools
102            gtool = _to_gemini_tool(mcp_tools)
103            if not gtool.function_declarations:
104                sys.exit("MCP returned no tools")
105
106            config = types.GenerateContentConfig(
107                system_instruction=SYSTEM,
108                tools=[gtool],
109                tool_config=types.ToolConfig(
110                    function_calling_config=types.FunctionCallingConfig(
111                        mode=types.FunctionCallingConfigMode.AUTO,
112                    )
113                ),
114                max_output_tokens=MAX_OUTPUT_TOKENS,
115                temperature=1.0,
116                automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
117            )
118
119            history: list[types.Content] = [
120                types.Content(role="user", parts=[types.Part(text=QUESTION)]),
121            ]
122
123            for _ in range(8):
124                call = functools.partial(
125                    client.models.generate_content,
126                    model=MODEL,
127                    contents=history,
128                    config=config,
129                )
130                resp = await asyncio.to_thread(call)
131
132                if resp.prompt_feedback and resp.prompt_feedback.block_reason:
133                    sys.exit(f"Prompt blocked: {resp.prompt_feedback.block_reason}")
134
135                cands = resp.candidates or []
136                if not cands:
137                    sys.exit("Gemini returned no candidates")
138
139                cand = cands[0]
140                parts = list(cand.content.parts) if cand.content and cand.content.parts else []
141                fcs = [p.function_call for p in parts if p.function_call]
142
143                if not fcs:
144                    text = _parts_text(parts)
145                    if text:
146                        print(text)
147                        return
148                    sys.exit("No function calls and no model text")
149
150                history.append(types.Content(role="model", parts=parts))
151                fr_parts: list[types.Part] = []
152                for fc in fcs:
153                    if not fc.name:
154                        continue
155                    raw = await s.call_tool(fc.name, _fc_args(fc))
156                    fr_parts.append(
157                        types.Part(
158                            function_response=types.FunctionResponse(
159                                name=fc.name,
160                                response={"result": _tool_text(raw)},
161                            )
162                        )
163                    )
164                if not fr_parts:
165                    sys.exit("Model issued no usable function calls")
166                history.append(types.Content(role="user", parts=fr_parts))
167
168            sys.exit("Too many tool rounds")
169
170
171if __name__ == "__main__":
172    asyncio.run(main())
173

Runnable example: JavaScript + Gemini

Uses native fetch (Node.js 20+; matches current @google/genai package engines) for Streamable HTTP MCP, your AlphaCreek MCP API key as Authorization: Bearer …, and the @google/genai GoogleGenAI client with models.generateContent, functionDeclarations from tools/list, and automaticFunctionCalling set to { disable: true } so your code runs MCP tools/call manually (same pattern as the Python example). The marketing sketch is illustrative; this is the full runnable file.

Install @google/genai ^1.52.0 or newer (as in project_ff/alphacreek_mcp/examples/package.json). Default Gemini 3 models return a thoughtSignature on functionCall parts; it must be echoed on the next request. Older SDK releases could strip it when serializing history and return HTTP 400 on a later tool round. See Google's Thought signatures documentation.

Install and run

  1. Node.js 20+ (global fetch; aligns with @google/genai engines).
  2. From project_ff/alphacreek_mcp/examples, run npm install (committed package.json), or elsewhere run npm install @google/genai@^1.52.0.
  3. Export ALPHACREEK_API_KEY and either GEMINI_API_KEY or GOOGLE_API_KEY.
  4. Optional: ALPHACREEK_MCP_URL, GEMINI_MODEL, GEMINI_MAX_OUTPUT_TOKENS, QUESTION, MCP_MAX_TOOL_ROUNDS.
  5. Save the script as javascript_gemini.mjs (same content as project_ff/alphacreek_mcp/examples/javascript_gemini.mjs) and run node javascript_gemini.mjs.

OAuth-oriented MCP setup is documented on /mcp-docs.

Script

JavaScript
1#!/usr/bin/env node
2/**
3 * Google Gemini + AlphaCreek MCP (Node.js 20+): one question → tools → stdout.
4 *
5 * Requires global fetch and @google/genai ^1.52 (Gemini 3 tool calls need thoughtSignature round-tripped).
6 * MCP uses JSON-RPC POST with SSE responses.
7 *
8 * Needs: ALPHACREEK_API_KEY and GEMINI_API_KEY or GOOGLE_API_KEY
9 *
10 * Canonical runnable example for the MCP developer docs and the marketing-site full script
11 * (alphacreek-ui src/sections/docs/mcp-gemini-javascript-example.ts → MCP_GEMINI_JS_FULL_SCRIPT).
12 */
13
14import { GoogleGenAI } from '@google/genai';
15
16const MCP_URL = (process.env.ALPHACREEK_MCP_URL || 'https://mcp.alphacreek.ai/mcp').trim();
17const MODEL = (process.env.GEMINI_MODEL || 'gemini-3-flash-preview').trim();
18const MAX_OUTPUT_TOKENS = Number.parseInt(process.env.GEMINI_MAX_OUTPUT_TOKENS || '8192', 10);
19const MAX_TOOL_ROUNDS = Number.parseInt(process.env.MCP_MAX_TOOL_ROUNDS || '8', 10);
20
21const QUESTION =
22  process.env.QUESTION || "What are NVIDIA’s biggest risks in the latest 10-K? ";
23
24const SYSTEM = 'Answer SEC questions using only tools; ground every claim in tool output.';
25
26function requireEnv(name) {
27  const v = process.env[name];
28  const s = typeof v === 'string' ? v.trim() : '';
29  if (!s) {
30    console.error('Missing env: ' + name);
31    process.exit(1);
32  }
33  return s;
34}
35
36function geminiApiKey() {
37  const g = process.env.GEMINI_API_KEY;
38  const o = process.env.GOOGLE_API_KEY;
39  const a = typeof g === 'string' ? g.trim() : '';
40  if (a) return a;
41  const b = typeof o === 'string' ? o.trim() : '';
42  if (b) return b;
43  console.error('Missing GEMINI_API_KEY or GOOGLE_API_KEY');
44  process.exit(1);
45  return '';
46}
47
48class StreamableHttpMcpClient {
49  constructor(baseUrl, bearerToken) {
50    this.baseUrl = baseUrl;
51    this.bearerToken = bearerToken;
52    /** @type {string | null} */
53    this.sessionId = null;
54    this.msgId = 0;
55  }
56
57  /**
58   * @param {string} method
59   * @param {unknown} params
60   */
61  async rpc(method, params) {
62    const id = ++this.msgId;
63    /** @type {Record<string, string>} */
64    const headers = {
65      'Content-Type': 'application/json',
66      Accept: 'application/json, text/event-stream',
67      Authorization: 'Bearer ' + this.bearerToken,
68    };
69    const sid = this.sessionId;
70    if (sid) headers['mcp-session-id'] = sid;
71
72    const res = await fetch(this.baseUrl, {
73      method: 'POST',
74      headers,
75      body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
76    });
77    const nh = res.headers.get('mcp-session-id');
78    if (nh) this.sessionId = nh;
79
80    if (!res.ok) {
81      const body = await res.text();
82      throw new Error('MCP HTTP ' + res.status + ': ' + body.slice(0, 600));
83    }
84
85    const ct = res.headers.get('content-type') || '';
86    if (ct.includes('text/event-stream')) {
87      const parsed = this.parseSSE(await res.text(), id);
88      if (parsed == null) throw new Error('MCP SSE: no matching jsonrpc envelope');
89      return parsed;
90    }
91    const json = await res.json();
92    if (json.error) throw new Error(json.error.message || 'MCP JSON-RPC error');
93    return json.result;
94  }
95
96  /**
97   * @param {string} text
98   * @param {number} targetId
99   */
100  parseSSE(text, targetId) {
101    const blocks = text.split('\n\n');
102    for (const block of blocks) {
103      for (const line of block.split('\n')) {
104        if (!line.startsWith('data:')) continue;
105        try {
106          const obj = JSON.parse(line.slice(5).trim());
107          if (obj.id === targetId) {
108            if (obj.error) throw new Error(obj.error.message || 'MCP SSE error');
109            return obj.result;
110          }
111        } catch (e) {
112          if (e instanceof SyntaxError) continue;
113          throw e;
114        }
115      }
116    }
117    return null;
118  }
119
120  async connect() {
121    await this.rpc('initialize', {
122      protocolVersion: '2025-03-26',
123      capabilities: {},
124      clientInfo: { name: 'alphacreek-js-demo', version: '0' },
125    });
126  }
127
128  async listTools() {
129    const result = await this.rpc('tools/list', {});
130    return result?.tools ?? [];
131  }
132
133  /**
134   * @param {string} name
135   * @param {Record<string, unknown>} args
136   */
137  async callTool(name, args) {
138    return this.rpc('tools/call', { name, arguments: args || {} });
139  }
140}
141
142/** @param {unknown[]} tools */
143function toGeminiDeclarations(tools) {
144  return tools.map((t) => {
145    const tt = /** @type {{ name: string; description?: string; inputSchema?: object; input_schema?: object }} */ (t);
146    const schema = tt.inputSchema || tt.input_schema || { type: 'object', properties: {} };
147    return {
148      name: tt.name,
149      description: ((tt.description || '').trim()) || '(MCP tool)',
150      parameters: schema,
151    };
152  });
153}
154
155/** @param {unknown} raw */
156function toolResultToString(raw) {
157  if (!raw || typeof raw !== 'object') return JSON.stringify(raw ?? null);
158  const r = /** @type {{ content?: unknown[]; structuredContent?: unknown; isError?: boolean }} */ (raw);
159  if (Array.isArray(r.content)) {
160    const parts = r.content.map((c) => {
161      if (typeof c === 'string') return c;
162      if (c && typeof c === 'object' && typeof /** @type {{ text?: string }} */ (c).text === 'string') {
163        return /** @type {{ text: string }} */ (c).text;
164      }
165      return JSON.stringify(c);
166    });
167    if (parts.some(Boolean)) return parts.join('\n');
168  }
169  const d = {};
170  if (r.structuredContent && typeof r.structuredContent === 'object') d.structuredContent = r.structuredContent;
171  if (r.isError) d.isError = true;
172  return JSON.stringify(Object.keys(d).length ? d : { isError: !!r.isError });
173}
174
175/** @param {any[] | undefined} parts */
176function partsText(parts) {
177  const out = [];
178  for (const p of parts || []) {
179    if (p.text && String(p.text).trim()) out.push(String(p.text).trim());
180  }
181  return out.join('\n').trim();
182}
183
184/** @param {any} fc */
185function fcArgs(fc) {
186  const a = fc?.args;
187  if (!a || typeof a !== 'object' || Array.isArray(a)) return {};
188  return /** @type {Record<string, unknown>} */ ({ ...a });
189}
190
191async function main() {
192  const creekKey = requireEnv('ALPHACREEK_API_KEY');
193  const genaiKey = geminiApiKey();
194
195  const mcp = new StreamableHttpMcpClient(MCP_URL, creekKey);
196  await mcp.connect();
197  const mcpTools = await mcp.listTools();
198  const functionDeclarations = toGeminiDeclarations(mcpTools);
199  if (!functionDeclarations.length) {
200    console.error('MCP returned no tools');
201    process.exit(1);
202  }
203
204  const ai = new GoogleGenAI({ apiKey: genaiKey });
205
206  /** @type {any[]} */
207  const history = [{ role: 'user', parts: [{ text: QUESTION }] }];
208
209  for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
210    const resp = await ai.models.generateContent({
211      model: MODEL,
212      contents: history,
213      config: {
214        systemInstruction: SYSTEM,
215        tools: [{ functionDeclarations }],
216        toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
217        maxOutputTokens: MAX_OUTPUT_TOKENS,
218        temperature: 1,
219        automaticFunctionCalling: { disable: true },
220      },
221    });
222
223    if (resp.promptFeedback?.blockReason) {
224      console.error('Prompt blocked: ' + String(resp.promptFeedback.blockReason));
225      process.exit(1);
226    }
227
228    const cands = resp.candidates || [];
229    if (!cands.length) {
230      console.error('Gemini returned no candidates');
231      process.exit(1);
232    }
233
234    const cand = cands[0];
235    const parts = cand.content?.parts ? [...cand.content.parts] : [];
236    const fcs = parts.map((p) => p.functionCall).filter(Boolean);
237
238    if (!fcs.length) {
239      const text = partsText(parts);
240      if (!text) {
241        console.error('No function calls and no model text');
242        process.exit(1);
243      }
244      console.log(text);
245      return;
246    }
247
248    history.push({ role: 'model', parts });
249
250    const frParts = [];
251    for (const fc of fcs) {
252      if (!fc?.name) continue;
253      const raw = await mcp.callTool(fc.name, fcArgs(fc));
254      frParts.push({
255        functionResponse: {
256          name: fc.name,
257          response: { result: toolResultToString(raw) },
258        },
259      });
260    }
261    if (!frParts.length) {
262      console.error('Model issued no usable function calls');
263      process.exit(1);
264    }
265    history.push({ role: 'user', parts: frParts });
266  }
267
268  console.error('Too many tool rounds');
269  process.exit(1);
270}
271
272main().catch((err) => {
273  console.error(err);
274  process.exit(1);
275});
276
277

Example traces (illustrative)

Compare risk factors across periods

  1. list_filings with document_type: "10-K" and a suitable limit to enumerate artifacts.
  2. get_filing_toc per artifact_document_id.
  3. read_node_content with Risk Factors node ids for each period.
  4. Compose the answer; place each claim beside the matching CITATION_URL.

Latest quarterly segment discussion

  1. get_latest_filing with document_type: "10-Q" or pick a specific quarter via list_filings.
  2. get_filing_toc, then read_node_content on MD&A / segment nodes; pull adjacent table nodes when narrative references numbers.