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).
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 claimsOptional 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
| Field | Type | Required | Notes |
|---|---|---|---|
| ticker | string | one of ticker | company | Uppercased when resolved |
| company | string | one of ticker | company | Resolved to ticker via companies / Atlas Search |
| document_type | enum | no | Filters 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
| Field | Type | Notes |
|---|---|---|
| ticker | company | string | At least one required |
| document_type | enum | Optional filter |
| limit | integer | Default 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.
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_ENDPer-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)
| Status | Typical cause |
|---|---|
| 400 | Missing required locator (e.g. ticker/company), invalid document_type, missing node ids |
| 404 | No filings for ticker/type; filing or navigation not found; company name not resolved to ticker |
| 503 | Company 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
- Python 3.10+ recommended.
- pip install mcp openai
- Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and OPENAI_API_KEY.
- Optional: ALPHACREEK_MCP_URL (default https://mcp.alphacreek.ai/mcp), OPENAI_MODEL (default gpt-5.4-mini in the script).
- 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
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())
130Runnable 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
- Node.js 18+ (requires global fetch).
- npm install openai in an empty folder (or add to an existing project).
- Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and OPENAI_API_KEY.
- Optional: ALPHACREEK_MCP_URL, OPENAI_MODEL, QUESTION, MCP_MAX_TOOL_ROUNDS.
- 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
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
276Runnable 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
- Python 3.10+ recommended.
- pip install mcp anthropic
- Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and ANTHROPIC_API_KEY.
- Optional: ALPHACREEK_MCP_URL (default https://mcp.alphacreek.ai/mcp), ANTHROPIC_MODEL (default claude-sonnet-4-6), ANTHROPIC_MAX_TOKENS (default 8192).
- 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
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())
138Runnable 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
- Node.js 18+ (requires global fetch).
- npm install @anthropic-ai/sdk in an empty folder (or use your package manager).
- Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and ANTHROPIC_API_KEY.
- Optional: ALPHACREEK_MCP_URL (default https://mcp.alphacreek.ai/mcp), ANTHROPIC_MODEL, ANTHROPIC_MAX_TOKENS (default 8192), QUESTION, MCP_MAX_TOOL_ROUNDS.
- 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
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});
239Runnable 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
- Python 3.10+ recommended.
- pip install mcp google-genai
- Export ALPHACREEK_API_KEY (Dashboard → MCP API key) and a Gemini key as GEMINI_API_KEY or GOOGLE_API_KEY.
- Optional: ALPHACREEK_MCP_URL (default https://mcp.alphacreek.ai/mcp), GEMINI_MODEL (default gemini-3-flash-preview), GEMINI_MAX_OUTPUT_TOKENS (default 8192).
- 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
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())
173Runnable 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
- Node.js 20+ (global fetch; aligns with @google/genai engines).
- From project_ff/alphacreek_mcp/examples, run npm install (committed package.json), or elsewhere run npm install @google/genai@^1.52.0.
- Export ALPHACREEK_API_KEY and either GEMINI_API_KEY or GOOGLE_API_KEY.
- Optional: ALPHACREEK_MCP_URL, GEMINI_MODEL, GEMINI_MAX_OUTPUT_TOKENS, QUESTION, MCP_MAX_TOOL_ROUNDS.
- 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
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
277Example traces (illustrative)
Compare risk factors across periods
- list_filings with document_type: "10-K" and a suitable limit to enumerate artifacts.
- get_filing_toc per artifact_document_id.
- read_node_content with Risk Factors node ids for each period.
- Compose the answer; place each claim beside the matching CITATION_URL.
Latest quarterly segment discussion
- get_latest_filing with document_type: "10-Q" or pick a specific quarter via list_filings.
- get_filing_toc, then read_node_content on MD&A / segment nodes; pull adjacent table nodes when narrative references numbers.