// API Portal — three auth options, all stored in localStorage: // 1. Graphite key (signed up via /signup, used against our /chat etc.) // 2. Anthropic API key (BYO — pay your own Anthropic bill) // 3. OpenAI API key (BYO — pay your own OpenAI bill) // // Chat in the portal UI works with the graphite key (server-side agent // loop calls Claude with our key + MCP tools). For BYO Anthropic/OpenAI // users we surface MCP config snippets — they run their own LLM client // (Claude Code, Codex CLI, Cursor, etc.) and connect graphite as a tool. // // All API URLs come from window.__ENV.CENTRAL_API_URL — no hardcoded // hostnames here. const PROVIDERS = { graphite: { id: 'graphite', label: 'Graphite key', storage: 'graphite.key', prefix: 'sk-' }, anthropic: { id: 'anthropic', label: 'Anthropic key', storage: 'graphite.anthropicKey', prefix: 'sk-ant-' }, openai: { id: 'openai', label: 'OpenAI key', storage: 'graphite.openaiKey', prefix: 'sk-' }, }; const ACTIVE_PROVIDER_KEY = 'graphite.activeProvider'; function loadKey(provider) { try { return localStorage.getItem(PROVIDERS[provider].storage) || ''; } catch { return ''; } } function saveKey(provider, k) { try { localStorage.setItem(PROVIDERS[provider].storage, k); } catch { } } function clearStoredKey(provider) { try { localStorage.removeItem(PROVIDERS[provider].storage); } catch { } } function loadActiveProvider() { try { return localStorage.getItem(ACTIVE_PROVIDER_KEY) || 'graphite'; } catch { return 'graphite'; } } function saveActiveProvider(p) { try { localStorage.setItem(ACTIVE_PROVIDER_KEY, p); } catch { } } // Graphite endpoints — built relative to CENTRAL_API_URL at call time so // env.js changes don't require a rebuild. function apiUrl(path) { const base = (window.__ENV && window.__ENV.CENTRAL_API_URL || '').replace(/\/$/, ''); return base + path; } async function callGraphite(path, { key, method = 'GET', body } = {}) { try { const res = await fetch(apiUrl(path), { method, headers: { ...(key ? { 'X-API-Key': key } : {}), ...(body ? { 'Content-Type': 'application/json' } : {}), }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { let detail = `${res.status}`; try { const j = await res.json(); if (j.detail) detail = `${res.status}: ${j.detail}`; } catch { } return { ok: false, error: detail }; } return { ok: true, data: await res.json() }; } catch (e) { return { ok: false, error: e.message || 'Network error' }; } } function useCopy() { const [copied, setCopied] = React.useState(false); const copy = React.useCallback((text) => { navigator.clipboard?.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1400); }, []); return [copied, copy]; } // ──────────────────────── main ──────────────────────── window.Portal = function Portal({ navigate }) { // One key per provider, all persisted independently. const [keys, setKeys] = React.useState({ graphite: loadKey('graphite'), anthropic: loadKey('anthropic'), openai: loadKey('openai'), }); const [active, setActive] = React.useState(loadActiveProvider()); function setKey(provider, value) { setKeys((k) => ({ ...k, [provider]: value })); if (value) saveKey(provider, value); else clearStoredKey(provider); } function pickProvider(provider) { setActive(provider); saveActiveProvider(provider); } const activeKey = keys[active]; return (

Use the graph without writing code.

Pick how you want to authenticate, ask the graph questions, and copy code samples — all from this page. Talks to {(window.__ENV && window.__ENV.CENTRAL_API_URL) || ''} directly.

); }; // ──────────────────────── MCP setup guide ──────────────────────── // // Walks the user through wiring the Graphite MCP server into their own // Claude Code / Claude Desktop / Cursor / Codex CLI. The MCP server lives // in this repo at sn43_owner/mcp_server/ and exposes 7 tools that any // MCP-compatible client can call. Once set up, the user's Claude can // answer questions like "what's NVDA's exposure to TSMC" by calling // these tools, without us proxying any LLM traffic. const MCP_TOOLS = [ { name: 'search_entities', desc: 'Free-text search across entity names, tickers, sectors, descriptions.' }, { name: 'get_entity', desc: 'Fetch the full record for a specific entity by ID (e.g. company:NVDA).' }, { name: 'get_relationships', desc: 'List every edge attached to an entity (employs, supplies, depends_on, …).' }, { name: 'get_facts', desc: 'Retrieve the typed fact log for an entity (revenue, headcount, etc).' }, { name: 'find_path', desc: 'Shortest path between two entities through the relationship graph.' }, { name: 'exposure_analysis', desc: '1st + 2nd-degree neighborhood with sector breakdown — supply-chain risk view.' }, { name: 'compare_entities', desc: 'Shared connections + path distance + direct relationships between two entities.' }, ]; const SAMPLE_PROMPTS = [ "What's NVIDIA's supply-chain exposure to TSMC?", "Find the shortest path from company:AAPL to company:ASML.", "Compare Microsoft and Google — who do they share board members with?", "Which companies are most affected by the EU AI Act?", "List every revenue-from edge for NVDA.", ]; function McpSetup({ active, apiKey }) { const apiBase = ((window.__ENV && window.__ENV.CENTRAL_API_URL) || '').replace(/\/$/, ''); const k = (active === 'graphite' && apiKey) ? apiKey : 'YOUR_GRAPHITE_KEY'; const [copiedConfig, copyConfig] = useCopy(); // Same config works for Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json), // Claude Code (~/.claude/mcp.json), Cursor (~/.cursor/mcp.json), and // Codex CLI (~/.codex/config.json) — the JSON shape is identical, only // the file path differs. const config = `{ "mcpServers": { "graphite": { "command": "graphite-mcp", "env": { "CENTRAL_SERVER_URL": "${apiBase}", "CUSTOMER_API_KEY": "${k}" } } } }`; return (

Use with your own Claude

Wire the Graphite MCP server into Claude Code, Claude Desktop, or Cursor. Your subscription LLM does the reasoning; Graphite just answers graph queries it asks for. Quota stays on your account.

One-time setup — installs the open-source graphite-mcp Python package. Adds a graphite-mcp command to your PATH that the MCP client launches as a subprocess.

{active === 'graphite' && apiKey ? 'You already have one selected — the config below has it pre-filled.' : 'Use the Graphite tab above to issue or paste a key. Without it, the MCP server can\'t authenticate to api.graphite-ai.net.'}

Drop this into the right config file for your client. Replace /absolute/path/to/sn43_owner with where you cloned the repo in step 1.

  • Claude Code~/.claude/mcp.json
  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
  • Cursor~/.cursor/mcp.json
  • Codex CLI~/.codex/config.json
{config}

After restart, Claude can call the graph as tools. Sample prompts:

{SAMPLE_PROMPTS.map((p, i) => "{p}")}

Available tools · {MCP_TOOLS.length} exposed

{MCP_TOOLS.map((t) => (
{t.name} {t.desc}
))}
); } function McpStep({ n, title, children }) { return (
{n}

{title}

{children}
); } function CodeBlock({ code }) { const [copied, copy] = useCopy(); return (
{code}
); } // ──────────────────────── auth tabs ──────────────────────── function AuthTabs({ active, onPick }) { return (
{Object.values(PROVIDERS).map((p) => ( ))}
); } function AuthPanel({ active, keys, setKey }) { if (active === 'graphite') return setKey('graphite', v)} />; if (active === 'anthropic') return setKey('anthropic', v)} />; if (active === 'openai') return setKey('openai', v)} />; return null; } // ── graphite: signup or paste key ── function GraphiteAuth({ currentKey, setKey }) { const [email, setEmail] = React.useState(''); const [keyDraft, setKeyDraft] = React.useState(''); const [issued, setIssued] = React.useState(''); const [issueState, setIssueState] = React.useState('idle'); async function requestKey(e) { e.preventDefault(); if (!email) return; setIssueState('loading'); const r = await callGraphite('/api/v1/signup', { method: 'POST', body: { email } }); if (r.ok && r.data?.key) { setIssued(r.data.key); setIssueState('done'); } else { setIssueState('error'); alert("Couldn't issue a key: " + (r.error || 'unknown error')); } } function adoptIssued() { setKey(issued); setIssued(''); setIssueState('idle'); } function commitDraft() { if (keyDraft.trim()) { setKey(keyDraft.trim()); setKeyDraft(''); } } return (
{issueState === 'done' ? (
Save this key — it won't be shown again.
) : (
setEmail(e.target.value)} className="portal-input" />
By requesting a key you agree to fair-use. Keys are bound to your email and revocable.
)}
{currentKey ? (
) : (
setKeyDraft(e.target.value)} className="portal-input mono" />
Stored only in your browser — never sent to graphite servers from this page.
)}
); } // ── BYO Anthropic/OpenAI ── const BYO_COPY = { anthropic: { title: 'Connect Anthropic', sub: 'Bring your own Anthropic API key. Pay-per-token on YOUR Anthropic account, not Graphite. Works with Claude Code + the Graphite MCP server.', placeholder: 'sk-ant-…', docs: 'https://console.anthropic.com/account/keys', docsLabel: 'Get an Anthropic key', note: 'This is the paid API, not the Claude Pro chat subscription. Subscriptions can\'t be used from a third-party site — only from Anthropic\'s own apps (claude.ai, Claude Code).', }, openai: { title: 'Connect OpenAI', sub: 'Bring your own OpenAI API key. Pay-per-token on YOUR OpenAI account, not Graphite. Works with Codex CLI + the Graphite MCP server.', placeholder: 'sk-…', docs: 'https://platform.openai.com/api-keys', docsLabel: 'Get an OpenAI key', note: 'This is the paid API, not the ChatGPT Pro subscription. Subscriptions can\'t be used from a third-party site — only from OpenAI\'s own apps (chatgpt.com, Codex CLI).', }, }; function ByoKeyAuth({ provider, currentKey, setKey }) { const copy = BYO_COPY[provider]; const [draft, setDraft] = React.useState(''); function commit() { if (draft.trim()) { setKey(draft.trim()); setDraft(''); } } return (
{currentKey ? (
{copy.note}
) : (
setDraft(e.target.value)} className="portal-input mono" autoComplete="off" />
{copy.note} {copy.docsLabel} →
)}
); } function maskKey(k) { if (!k || k.length < 12) return k || ''; return k.slice(0, 7) + '…' + k.slice(-4); } // ──────────────────────── shared bits ──────────────────────── function PortalCard({ title, subtitle, children }) { return (

{title}

{subtitle &&

{subtitle}

}
{children}
); } function KeyDisplay({ value }) { const [copied, copy] = useCopy(); return (
{value}
); } // ──────────────────────── status row ──────────────────────── function StatusRow() { // Uses /health (public) for live entity/rel/fact counts. The previous // design also showed per-key usage but that endpoint isn't exposed // publicly on central — chat quota lives at /chat/quota and is // user-facing only when chatting. const [counts, setCounts] = React.useState(null); React.useEffect(() => { let alive = true; function refresh() { fetch(apiUrl('/health')) .then((r) => r.ok ? r.json() : null) .then((h) => { if (alive && h) setCounts(h); }) .catch(() => { }); } refresh(); const id = setInterval(refresh, 60000); return () => { alive = false; clearInterval(id); }; }, []); const fmt = (n) => n == null ? '—' : Number(n).toLocaleString(); return (

Live network status

Auto-refreshes every minute.

); } function Counter({ label, value, accent }) { return (
{value}
{label}
); } // ──────────────────────── chat panel ──────────────────────── function ChatPanel({ active, apiKey }) { const [messages, setMessages] = React.useState([ { role: 'system', content: "Ask the graph anything. The server-side LLM uses MCP tools — search, relationships, path-finding, exposure analysis — to ground every answer in real graph data." }, ]); const [input, setInput] = React.useState(''); const [busy, setBusy] = React.useState(false); const scrollRef = React.useRef(null); const suggestions = [ "What's NVIDIA's exposure to TSMC?", "Who sits on Apple's board?", "Show the supply-chain path from ASML to AAPL", "Which companies are most affected by EU AI Act?", ]; React.useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages]); // Chat from this page only works with the graphite key. With Anthropic // or OpenAI keys, you run your own LLM client via the MCP server (see // QuickStart below) — we don't proxy 3rd-party LLM calls. const chatEnabled = active === 'graphite' && !!apiKey; const chatBlockedReason = active !== 'graphite' ? `Chat from this page uses the Graphite-managed LLM. With your ${active === 'anthropic' ? 'Anthropic' : 'OpenAI'} key, run a local LLM client (Claude Code / Codex CLI / Cursor) using the MCP config below — quota stays on your account.` : !apiKey ? 'Add a Graphite key above to start chatting (free tier: 2 chat queries).' : null; async function send(text) { const q = (text ?? input).trim(); if (!q || busy || !chatEnabled) return; setInput(''); setMessages((m) => [...m, { role: 'user', content: q }]); setBusy(true); const r = await callGraphite('/api/v1/chat', { key: apiKey, method: 'POST', body: { question: q }, }); if (r.ok) { setMessages((m) => [...m, { role: 'assistant', content: r.data?.answer || JSON.stringify(r.data, null, 2), tools: r.data?.tool_calls, sources: r.data?.sources, }]); } else { setMessages((m) => [...m, { role: 'assistant', error: true, content: `Couldn't reach the API: ${r.error}` }]); } setBusy(false); } return (

Chat with the graph

Natural-language queries grounded by the same MCP tools an agent would call. 2 free queries lifetime per Graphite key.

{messages.map((m, i) => )} {busy && }
{chatEnabled && (
{suggestions.map((s, i) => ( ))}
)}
{ e.preventDefault(); send(); }}> setInput(e.target.value)} disabled={!chatEnabled || busy} />
); } function ChatBubble({ role, content, error, loading, tools, sources }) { if (role === 'system') return
{content}
; return (
{role === 'user' ? 'YOU' : 'GRAPH'}
{loading ? : content} {tools && tools.length > 0 && (
TOOLS USED
{tools.map((t, i) => {t.name || t})}
)} {sources && sources.length > 0 && (
{sources.length} source{sources.length === 1 ? '' : 's'} cited
)}
); } function ChatLoading() { return ; } // ──────────────────────── quick start ──────────────────────── function QuickStart({ active, apiKey }) { const [tab, setTab] = React.useState('curl'); const k = apiKey || (active === 'anthropic' ? 'ANTHROPIC_API_KEY' : active === 'openai' ? 'OPENAI_API_KEY' : 'YOUR_GRAPHITE_KEY'); const apiBase = ((window.__ENV && window.__ENV.CENTRAL_API_URL) || '').replace(/\/$/, '') + '/api/v1'; // Snippets adapt to which provider is active. Graphite snippets show // direct API calls. BYO snippets show the MCP config the user drops // into their Claude Code or Codex CLI to talk to the graph. const snippets = active === 'graphite' ? graphiteSnippets(k, apiBase) : active === 'anthropic' ? anthropicMcpSnippets(k, apiBase) : openaiMcpSnippets(k, apiBase); const tabs = active === 'graphite' ? [['curl', 'cURL'], ['python', 'Python'], ['js', 'JavaScript'], ['mcp', 'MCP config']] : [['mcp', 'MCP config'], ['cli', 'CLI usage']]; React.useEffect(() => { if (!snippets[tab]) setTab(tabs[0][0]); }, [active]); // eslint-disable-line const [copied, copy] = useCopy(); return (

Quick start

{apiKey ? 'Snippets pre-filled with your key.' : 'Add a key above and these will be ready to paste.'}

{tabs.map(([id, label]) => ( ))}
{snippets[tab]}
); } function graphiteSnippets(k, apiBase) { return { curl: `# Search for a company curl -H "X-API-Key: ${k}" \\ "${apiBase}/search?q=NVIDIA" # Get entity relationships curl -H "X-API-Key: ${k}" \\ "${apiBase}/entities/company:NVDA/relationships" # Find path between two companies curl -H "X-API-Key: ${k}" \\ "${apiBase}/graph/path?source=company:AAPL&target=company:TSM" # Exposure analysis curl -H "X-API-Key: ${k}" \\ "${apiBase}/graph/exposure?entity=company:TSM"`, python: `import requests API = "${apiBase}" headers = {"X-API-Key": "${k}"} # Search for a company r = requests.get(f"{API}/search", params={"q": "NVIDIA"}, headers=headers) print(r.json()) # Path between two companies r = requests.get(f"{API}/graph/path", params={ "source": "company:AAPL", "target": "company:TSM", }, headers=headers) for hop in r.json()["path"]: print(hop)`, js: `const API = "${apiBase}"; const headers = { "X-API-Key": "${k}" }; const search = await fetch(\`\${API}/search?q=NVIDIA\`, { headers }) .then(r => r.json()); const exposure = await fetch( \`\${API}/graph/exposure?entity=company:TSM\`, { headers } ).then(r => r.json()); console.log(exposure);`, mcp: `// ~/.cursor/mcp.json or Claude Desktop config { "mcpServers": { "graphite": { "command": "python", "args": ["-m", "mcp_server"], "env": { "CUSTOMER_API_KEY": "${k}" } } } }`, }; } function anthropicMcpSnippets(anthKey, apiBase) { return { mcp: `// ~/.claude/mcp.json (Claude Code) { "mcpServers": { "graphite": { "command": "npx", "args": ["-y", "@graphite-ai/mcp-server"], "env": { "GRAPHITE_API_BASE": "${apiBase}", "ANTHROPIC_API_KEY": "${anthKey}" } } } }`, cli: `# 1. Install Claude Code if you haven't: curl -fsSL https://claude.ai/install.sh | bash # 2. Add Graphite as an MCP server (config above) # 3. Use it from Claude Code: claude > /mcp graphite search NVIDIA > what are NVDA's supplier dependencies? # Quota stays on YOUR Anthropic account — Graphite never sees the # LLM tokens, only graph queries.`, }; } function openaiMcpSnippets(openaiKey, apiBase) { return { mcp: `// ~/.codex/config.json (Codex CLI) { "mcpServers": { "graphite": { "command": "npx", "args": ["-y", "@graphite-ai/mcp-server"], "env": { "GRAPHITE_API_BASE": "${apiBase}", "OPENAI_API_KEY": "${openaiKey}" } } } }`, cli: `# 1. Install Codex CLI if you haven't: npm install -g @openai/codex # 2. Add Graphite as an MCP server (config above) # 3. Use it from Codex CLI: codex > /mcp graphite search NVIDIA > what are NVDA's supplier dependencies? # Quota stays on YOUR OpenAI account — Graphite never sees the # LLM tokens, only graph queries.`, }; }