// 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 (
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 (