SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

web: page Integrations (MCP, Claude Code, Cursor, Codex, Skills) + composant IntCopyBlock

- sous-routes /integrations{,/mcp,/claude-code,/cursor,/codex,/skills}, hero avec session de démo, table des 14 outils, 3 prompts d'exemple
- blocs de config copiables en un clic (claude mcp add, .mcp.json, .cursor/mcp.json, config.toml Codex, mcpServers générique), clé API optionnelle injectée localement (jamais persistée)
- bascule tarball (disponible) / npm (après publication), boutons de téléchargement (.tgz, skills .zip), deeplink « Add to Cursor » (best effort, base64 JSON)
- tokens theme.css uniquement, aucun CDN ; e2e Playwright e2e-integrations.mjs (25 assertions vertes sur vite preview)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 20 days ago (Sep 4, 2026) parent 1040d52

4 changed files +598 −3

added hfmarketdata/web/src/components/IntCopyBlock.jsx +66 −0
@@ -0,0 +1,66 @@
1 +// Copy-in-one-click code block for the Integrations pages. Local to this area (web-core will ship a
2 +// shared CodeBlock later); uses only theme.css tokens, no external deps, no CDN.
3 +import React, { useCallback, useEffect, useState } from 'react'
4 +
5 +export function useCopy(timeoutMs = 1600) {
6 + const [copied, setCopied] = useState(false)
7 + useEffect(() => {
8 + if (!copied) return undefined
9 + const t = setTimeout(() => setCopied(false), timeoutMs)
10 + return () => clearTimeout(t)
11 + }, [copied, timeoutMs])
12 + const copy = useCallback(async (text) => {
13 + try {
14 + if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text)
15 + else {
16 + const ta = document.createElement('textarea')
17 + ta.value = text
18 + ta.setAttribute('readonly', '')
19 + ta.style.position = 'fixed'
20 + ta.style.opacity = '0'
21 + document.body.appendChild(ta)
22 + ta.select()
23 + document.execCommand('copy')
24 + document.body.removeChild(ta)
25 + }
26 + setCopied(true)
27 + return true
28 + } catch {
29 + return false
30 + }
31 + }, [])
32 + return { copied, copy }
33 +}
34 +
35 +/**
36 + * <IntCopyBlock code="…" lang="bash" title="Terminal" />
37 + * - `title` renders a small header with the language/filename and the copy button.
38 + * - `lines` (array) may be used instead of `code`.
39 + * - `note` renders a muted caption under the block.
40 + */
41 +export default function IntCopyBlock({ code, lines, lang = 'text', title, note, compact = false }) {
42 + const text = code ?? (lines || []).join('\n')
43 + const { copied, copy } = useCopy()
44 + return (
45 + <figure className={`int-copy${compact ? ' compact' : ''}`}>
46 + <div className="int-copy-head">
47 + <span className="int-copy-title mono">{title || lang}</span>
48 + <button type="button" className={`int-copy-btn${copied ? ' ok' : ''}`} onClick={() => copy(text)} aria-live="polite" aria-label={copied ? 'Copied' : 'Copy to clipboard'}>
49 + {copied ? 'Copied' : 'Copy'}
50 + </button>
51 + </div>
52 + <pre className="int-copy-pre" data-lang={lang}><code>{text}</code></pre>
53 + {note ? <figcaption className="int-copy-note muted">{note}</figcaption> : null}
54 + </figure>
55 + )
56 +}
57 +
58 +/** Inline copyable chip, e.g. for a single command or an env var. */
59 +export function IntCopyChip({ text, label }) {
60 + const { copied, copy } = useCopy()
61 + return (
62 + <button type="button" className={`int-chip mono${copied ? ' ok' : ''}`} onClick={() => copy(text)} title="Copy">
63 + {label || text}<span className="int-chip-ico" aria-hidden="true">{copied ? ' ✓' : ' ⧉'}</span>
64 + </button>
65 + )
66 +}
modified hfmarketdata/web/src/pages/integrations/Integrations.jsx +354 −3
@@ -1,5 +1,356 @@
1 −import React from 'react'
2 −// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
1 +// Integrations — MCP server, Claude Code / Cursor / Codex setup, skills pack. Owner: mcp-skills.
2 +// Sub-routes: /integrations, /integrations/mcp, /claude-code, /cursor, /codex, /skills.
3 +import React, { createContext, useContext, useMemo, useState } from 'react'
4 +import { Link, NavLink, Route, Routes } from 'react-router-dom'
5 +import IntCopyBlock, { IntCopyChip } from '../../components/IntCopyBlock.jsx'
6 +import { PUBLIC_BASE, CONTACT_EMAIL } from '../../app/api.js'
7 +import './integrations.css'
8 +
9 +// ---------------------------------------------------------------------------------------------
10 +// Constants (bump MCP_VERSION together with mcp/package.json and the tarball in public/downloads)
11 +// ---------------------------------------------------------------------------------------------
12 +export const MCP_VERSION = '1.0.0'
13 +export const TARBALL_URL = `${PUBLIC_BASE}/downloads/hfmarketdata-mcp-${MCP_VERSION}.tgz`
14 +export const SKILLS_ZIP_URL = `${PUBLIC_BASE}/downloads/hfmarketdata-skills.zip`
15 +const KEY_PLACEHOLDER = 'hfmd_live_YOUR_KEY'
16 +
17 +const TOOLS = [
18 + ['search_symbols', 'Find tickers, futures roots or optionable underlyings', '/v1/{asset}/tickers · /v1/futures/roots · /v1/options/tickers'],
19 + ['get_bars', 'OHLCV 1min…1day for one or many symbols; vendor continuous; individual contracts (ESZ25)', '/v1/bars/… · /v1/futures/contract/{symbol}/bars'],
20 + ['get_futures_contracts', 'Every expiration of a root: expiry, FND, status, volume, OI', '/v1/futures/{root}/contracts'],
21 + ['get_futures_chain', 'Listed contracts as of a date, front month first', '/v1/futures/{root}/chain'],
22 + ['get_continuous', 'Server-built continuous series: roll, adjust, depth (roll dates in meta)', '/v1/futures/{root}/continuous'],
23 + ['get_term_structure', 'The curve on a date — spreads, OI, contango / backwardation', '/v1/futures/{root}/term-structure'],
24 + ['get_options_chain', 'EOD chain with IV + Greeks, filters by expiry / strikes / side', '/v1/options/chain/{t} · /expirations/{t}'],
25 + ['get_coverage', 'What exists for a contract or a ticker’s fundamentals, gaps included', '/v1/futures/contract/{s}/coverage · /v1/fundamentals/{t}/coverage'],
26 + ['get_financial_statements', 'Income / balance / cash-flow, annual or quarterly, point-in-time; XBRL facts', '/v1/fundamentals/{t}/statements · /facts/{concept}'],
27 + ['get_ratios', 'Latest P/E, P/B, EV/EBITDA, FCF yield, ROE, margins, leverage', '/v1/fundamentals/{t}/ratios'],
28 + ['get_ratios_daily', 'Daily ratio history with the fundamentals known that day', '/v1/fundamentals/{t}/ratios/daily'],
29 + ['screen_fundamentals', 'filters="pe<15,roe>0.15" · sort="fcf_yield:desc" · as_of', '/v1/fundamentals/screener'],
30 + ['get_filings', '10-K / 10-Q / 8-K / 4 / 13F… with URLs', '/v1/fundamentals/{t}/filings'],
31 + ['subscribe_filings', 'Bounded listen on the live filings websocket (N messages or T seconds)', 'wss://…/v1/stream'],
32 +]
33 +
34 +const PROMPTS = [
35 + ['Plot me the CL term structure', 'get_term_structure → ASCII/inline chart, contango or backwardation, annualised roll yield per leg.'],
36 + ['Compare ESZ25 and ESH26 over 30 days', 'get_coverage ×2 → get_bars(asset=futures_contract) ×2 → returns, calendar spread trend, volume migration.'],
37 + ['Screen US stocks with PE<15 and FCF yield>6% and show their last 3 quarters', 'screen_fundamentals → get_financial_statements per hit, with filed_at dates.'],
38 +]
39 +
40 +const SKILLS = [
41 + ['hfmd-data-analysis', 'Bars → pandas → CAGR, vol, Sharpe, drawdowns, monthly table, chart', 'fetch_bars.py · analyze.py'],
42 + ['hfmd-quick-backtest', 'MA-crossover template with honest walk-forward, costs, benchmark, per-block parameters', 'ma_crossover.py'],
43 + ['hfmd-continuous-futures', 'Build/compare continuous series (roll × adjust × depth) vs vendor; local stitcher', 'continuous_compare.py · stitch_local.py'],
44 + ['hfmd-fundamentals-screen', 'Screener → ratios → last quarters, with point-in-time notes', 'screen.py'],
45 + ['hfmd-term-structure', 'Curve, contango/backwardation verdict, roll yield, two-date comparison, chart', 'term_structure.py'],
46 +]
47 +
48 +// ---------------------------------------------------------------------------------------------
49 +// Config generators (pure functions — also unit-testable)
50 +// ---------------------------------------------------------------------------------------------
51 +export function serverConfig(mode, key) {
52 + const cfg = mode === 'npm' ? { command: 'npx', args: ['-y', 'hfmarketdata-mcp'] } : { command: 'hfmarketdata-mcp', args: [] }
53 + if (key) cfg.env = { HFMD_API_KEY: key }
54 + return cfg
55 +}
56 +export function mcpJson(mode, key, wrapper = 'mcpServers') {
57 + return JSON.stringify({ [wrapper]: { hfmarketdata: serverConfig(mode, key) } }, null, 2)
58 +}
59 +export function claudeAddCommand(mode, key, scope) {
60 + const env = key ? ` -e HFMD_API_KEY=${key}` : ''
61 + const sc = scope ? ` --scope ${scope}` : ''
62 + return mode === 'npm' ? `claude mcp add hfmarketdata${sc}${env} -- npx -y hfmarketdata-mcp` : `claude mcp add hfmarketdata${sc}${env} -- hfmarketdata-mcp`
63 +}
64 +export function codexToml(mode, key) {
65 + const c = serverConfig(mode, key)
66 + const lines = ['[mcp_servers.hfmarketdata]', `command = "${c.command}"`, `args = [${c.args.map((a) => `"${a}"`).join(', ')}]`]
67 + if (key) lines.push(`env = { HFMD_API_KEY = "${key}" }`)
68 + return lines.join('\n')
69 +}
70 +function b64(s) {
71 + const bytes = new TextEncoder().encode(s)
72 + let bin = ''
73 + bytes.forEach((b) => { bin += String.fromCharCode(b) })
74 + return btoa(bin)
75 +}
76 +export function cursorDeeplink(mode, key) {
77 + return `cursor://anysphere.cursor-deeplink/mcp/install?name=hfmarketdata&config=${encodeURIComponent(b64(JSON.stringify(serverConfig(mode, key))))}`
78 +}
79 +export const installCommand = (mode) => (mode === 'npm' ? 'npm i -g hfmarketdata-mcp' : `npm i -g ${TARBALL_URL}`)
80 +
81 +// ---------------------------------------------------------------------------------------------
82 +// Shared state: install mode + optional key (never persisted, never sent anywhere)
83 +// ---------------------------------------------------------------------------------------------
84 +const Ctx = createContext(null)
85 +const useInt = () => useContext(Ctx)
86 +
87 +function InstallControls({ showKey = true }) {
88 + const { mode, setMode, key, setKey } = useInt()
89 + return (
90 + <div>
91 + <div className="int-toggle" role="tablist" aria-label="Install source">
92 + <button type="button" role="tab" aria-selected={mode === 'tarball'} className={mode === 'tarball' ? 'on' : ''} onClick={() => setMode('tarball')}>Tarball (available now)</button>
93 + <button type="button" role="tab" aria-selected={mode === 'npm'} className={mode === 'npm' ? 'on' : ''} onClick={() => setMode('npm')}>npm registry (npx)</button>
94 + </div>
95 + {showKey && (
96 + <div className="int-field">
97 + <label htmlFor="int-key">API key (optional)</label>
98 + <input id="int-key" value={key} onChange={(e) => setKey(e.target.value.trim())} placeholder={KEY_PLACEHOLDER} autoComplete="off" spellCheck="false" />
99 + <span className="hint">Stays in this tab — only used to fill the snippets. No key = keyless mode (30 req/h).</span>
100 + </div>
101 + )}
102 + {mode === 'npm' && <div className="int-callout warn">The npm package is being published; until it resolves on the registry, use the tarball tab (same binary, same version {MCP_VERSION}).</div>}
103 + </div>
104 + )
105 +}
106 +
107 +const SUBNAV = [
108 + ['/integrations', 'Overview'], ['/integrations/mcp', 'MCP server'], ['/integrations/claude-code', 'Claude Code'],
109 + ['/integrations/cursor', 'Cursor'], ['/integrations/codex', 'Codex'], ['/integrations/skills', 'Skills pack'],
110 +]
111 +
3 112 export default function Integrations() {
4 − return <main className="page"><h1>Integrations</h1><p className="muted">Coming soon.</p></main>
113 + const [mode, setMode] = useState('tarball')
114 + const [key, setKey] = useState('')
115 + const value = useMemo(() => ({ mode, setMode, key, setKey, keyOrPlaceholder: key || KEY_PLACEHOLDER }), [mode, key])
116 + return (
117 + <Ctx.Provider value={value}>
118 + <main className="page int">
119 + <Hero />
120 + <nav className="int-subnav" aria-label="Integrations">
121 + {SUBNAV.map(([to, label]) => <NavLink key={to} to={to} end={to === '/integrations'} className={({ isActive }) => (isActive ? 'active' : '')}>{label}</NavLink>)}
122 + </nav>
123 + <Routes>
124 + <Route index element={<Overview />} />
125 + <Route path="mcp" element={<McpPage />} />
126 + <Route path="claude-code" element={<ClaudeCodePage />} />
127 + <Route path="cursor" element={<CursorPage />} />
128 + <Route path="codex" element={<CodexPage />} />
129 + <Route path="skills" element={<SkillsPage />} />
130 + <Route path="*" element={<Overview />} />
131 + </Routes>
132 + </main>
133 + </Ctx.Provider>
134 + )
135 +}
136 +
137 +// ---------------------------------------------------------------------------------------------
138 +function Hero() {
139 + const { mode, key } = useInt()
140 + return (
141 + <section className="int-hero">
142 + <div>
143 + <div className="int-kicker">Integrations · MCP · Skills</div>
144 + <h1>Market data where your agent already works.</h1>
145 + <p className="lead">
146 + One MCP server gives Claude Code, Cursor, Codex and Claude Desktop 14 read-only tools over the whole HF Market Data API —
147 + bars since 2007, individual futures contracts and custom continuous series, term structures, options with Greeks, point-in-time SEC fundamentals
148 + and a live filings stream. Plus five Claude skills with ready-to-run Python.
149 + </p>
150 + <div className="int-actions">
151 + <Link to="/integrations/claude-code" className="btn btn-primary">Set up in Claude Code</Link>
152 + <a href={cursorDeeplink(mode, key)} className="btn" title="Best effort: opens Cursor’s MCP install dialog (Cursor ≥ 1.0)">Add to Cursor</a>
153 + <a href={TARBALL_URL} className="btn btn-ghost" download>Download MCP tarball</a>
154 + <a href={SKILLS_ZIP_URL} className="btn btn-ghost" download>Download skills (.zip)</a>
155 + </div>
156 + <div className="int-badges">
157 + <span className="int-badge ok">hfmarketdata-mcp v{MCP_VERSION}</span>
158 + <span className="int-badge">14 tools · 2 resources · 3 prompts</span>
159 + <span className="int-badge">Node ≥ 20 · zero heavy deps</span>
160 + <span className="int-badge">MIT</span>
161 + <span className="int-badge">Keyless works · free key = 120 req/min</span>
162 + </div>
163 + </div>
164 + <DemoTerminal />
165 + </section>
166 + )
167 +}
168 +
169 +function DemoTerminal() {
170 + return (
171 + <div className="int-demo" aria-label="Example session">
172 + <div className="int-demo-bar"><i /><i /><i /></div>
173 + <pre>
174 + <span className="u">› Plot me the CL term structure</span>{'\n\n'}
175 + <span className="t">⏺ get_term_structure(root="CL")</span>{'\n'}
176 + <span className="a"> CLX25 64.12 · CLZ25 63.71 · CLF26 63.35 · CLG26 63.04 …</span>{'\n'}
177 + <span className="g"> ▇▆▅▅▄▄▃▃▃▂▂▂ backwardation (front −1.7 % to 6th)</span>{'\n'}
178 + <span className="a"> Roll yield for a long front-month ≈ +7.6 %/yr; OI concentrates in X25/Z25.</span>{'\n\n'}
179 + <span className="u">› Compare ESZ25 and ESH26 over 30 days</span>{'\n\n'}
180 + <span className="t">⏺ get_coverage(kind="futures_contract", symbol="ESZ25") … get_bars(asset="futures_contract", …)</span>{'\n'}
181 + <span className="a"> Spread H26−Z25: 61.5 → 58.0 pts (−3.5); volume 96 % still in Z25;{'\n'} 1 missing session (2025-09-01) left as null.</span>{'\n\n'}
182 + <span className="u">› Screen US stocks with PE&lt;15 and FCF yield&gt;6 % …</span>{'\n'}
183 + <span className="t">⏺ screen_fundamentals(filters="pe&lt;15,fcf_yield&gt;0.06", sort="fcf_yield:desc")</span>
184 + </pre>
185 + </div>
186 + )
187 +}
188 +
189 +// ---------------------------------------------------------------------------------------------
190 +function Overview() {
191 + return (
192 + <>
193 + <section className="int-grid">
194 + <Link to="/integrations/mcp" className="card int-card"><h3><span className="int-logo">MCP</span>MCP server</h3><p>What the 14 tools do, how outputs are bounded, resources and prompts, install without npm.</p><span className="go">Read →</span></Link>
195 + <Link to="/integrations/claude-code" className="card int-card"><h3><span className="int-logo g">CC</span>Claude Code</h3><p>One <code>claude mcp add</code> command, or a shared <code>.mcp.json</code> for your project.</p><span className="go">Set up →</span></Link>
196 + <Link to="/integrations/cursor" className="card int-card"><h3><span className="int-logo b">Cu</span>Cursor</h3><p><code>.cursor/mcp.json</code> snippet and a one-click deeplink.</p><span className="go">Set up →</span></Link>
197 + <Link to="/integrations/codex" className="card int-card"><h3><span className="int-logo w">Cx</span>Codex</h3><p><code>~/.codex/config.toml</code> block, ready to paste.</p><span className="go">Set up →</span></Link>
198 + <Link to="/integrations/skills" className="card int-card"><h3><span className="int-logo">Sk</span>Skills pack</h3><p>Five Claude skills: analysis, walk-forward backtest, continuous futures, fundamentals screen, term structure.</p><span className="go">Download →</span></Link>
199 + </section>
200 +
201 + <section className="int-section">
202 + <h2>Three prompts to try first</h2>
203 + <div className="int-prompts">
204 + {PROMPTS.map(([q, how]) => <div key={q} className="card int-prompt"><q>{q}</q><small>{how}</small><IntCopyChip text={q} label="Copy prompt" /></div>)}
205 + </div>
206 + </section>
207 +
208 + <section className="int-section">
209 + <h2>How it behaves</h2>
210 + <ul>
211 + <li><strong>Bounded outputs.</strong> Compact <code>{'{columns, rows}'}</code> tables; above 200 rows the tool returns count, first/last rows, numeric ranges and a hint to narrow — an agent cannot flood its context by accident.</li>
212 + <li><strong>Quota-aware.</strong> Every result ends with the remaining requests/rows from <code>X-RateLimit-*</code>; a 429 explains <code>Retry-After</code> and how to get a key.</li>
213 + <li><strong>Honest data.</strong> Missing values stay <code>null</code>; coverage and gaps are first-class (<code>get_coverage</code>). Intraday timestamps are US/Eastern, daily bars are dates.</li>
214 + <li><strong>Read-only.</strong> No tool writes anything. The API key is read from <code>HFMD_API_KEY</code> and never logged or echoed.</li>
215 + </ul>
216 + </section>
217 + </>
218 + )
219 +}
220 +
221 +// ---------------------------------------------------------------------------------------------
222 +function McpPage() {
223 + const { mode, keyOrPlaceholder } = useInt()
224 + return (
225 + <>
226 + <section className="int-section">
227 + <h2>hfmarketdata-mcp</h2>
228 + <p>TypeScript, stdio transport, built on the official <code>@modelcontextprotocol/sdk</code> with strict zod input schemas. Native <code>fetch</code>, no heavy dependencies, Node ≥ 20 (Node ≥ 22 for the websocket tool).</p>
229 + <InstallControls showKey={false} />
230 + <IntCopyBlock lang="bash" title="Install & check" lines={[installCommand(mode), 'hfmarketdata-mcp --selftest', '# OK https://www.hfmarketdata.io (312 ms, keyless) — datasets: stock, etf, futures, futures_contracts, crypto, index, fx, options']} />
231 + <div className="int-dl">
232 + <div className="card"><strong>MCP server tarball</strong><span className="mono">hfmarketdata-mcp-{MCP_VERSION}.tgz</span><span className="size">≈ 26 kB · npm pack output · sha512 in the npm metadata</span><a className="btn" href={TARBALL_URL} download>Download .tgz</a></div>
233 + <div className="card"><strong>Source & README</strong><span>Directory <code>mcp/</code> of the hfmarketdata repository (MIT).</span><span className="size">README covers Claude Code, Cursor, Codex, Claude Desktop, development and release.</span><a className="btn btn-ghost" href={`mailto:${CONTACT_EMAIL}?subject=hfmarketdata-mcp`}>Ask for access</a></div>
234 + </div>
235 + </section>
236 +
237 + <section className="int-section">
238 + <h2>Environment variables</h2>
239 + <div className="int-table"><table>
240 + <thead><tr><th>Variable</th><th>Default</th><th>Purpose</th></tr></thead>
241 + <tbody>
242 + <tr><td><code>HFMD_API_KEY</code></td><td><em>(unset = keyless)</em></td><td>Bearer key from your <Link to="/dashboard">dashboard</Link>. Keyless: 30 requests/hour per IP, 5 000 rows/request. Free account: 120 req/min, 50 000 rows/request.</td></tr>
243 + <tr><td><code>HFMD_BASE_URL</code></td><td><code>{PUBLIC_BASE}</code></td><td>Point the server at a staging or self-hosted API. The websocket URL is derived (<code>wss://…/v1/stream</code>).</td></tr>
244 + </tbody>
245 + </table></div>
246 + </section>
247 +
248 + <section className="int-section">
249 + <h2>Tools</h2>
250 + <div className="int-table"><table>
251 + <thead><tr><th>Tool</th><th>What it does</th><th>Endpoint(s)</th></tr></thead>
252 + <tbody>{TOOLS.map(([n, d, e]) => <tr key={n}><td><code>{n}</code></td><td>{d}</td><td className="mono muted" style={{ fontSize: 12 }}>{e}</td></tr>)}</tbody>
253 + </table></div>
254 + <p><strong>Resources:</strong> <code>hfmarketdata://status</code> (dataset inventory) · <code>hfmarketdata://limits</code> (tiers + live headroom). <strong>Prompts:</strong> <code>term-structure-analysis</code>, <code>compare-contracts</code>, <code>fundamentals-snapshot</code>.</p>
255 + <div className="int-callout">The futures v2 and fundamentals endpoints roll out progressively on the API. Until an endpoint is live, the corresponding tool returns a clear <code>404 NOT_FOUND</code> message pointing to <code>hfmarketdata://status</code> — nothing is fabricated.</div>
256 + </section>
257 +
258 + <section className="int-section">
259 + <h2>Generic MCP client config</h2>
260 + <p>Any client that reads the standard <code>mcpServers</code> JSON (Claude Desktop, Windsurf, Zed, Continue…):</p>
261 + <InstallControls />
262 + <IntCopyBlock lang="json" title="mcpServers" code={mcpJson(mode, keyOrPlaceholder)} note="Claude Desktop: Settings → Developer → Edit Config (claude_desktop_config.json)." />
263 + <h3>Example prompts</h3>
264 + <div className="int-prompts">{PROMPTS.map(([q, how]) => <div key={q} className="card int-prompt"><q>{q}</q><small>{how}</small><IntCopyChip text={q} label="Copy prompt" /></div>)}</div>
265 + </section>
266 + </>
267 + )
268 +}
269 +
270 +// ---------------------------------------------------------------------------------------------
271 +function ClaudeCodePage() {
272 + const { mode, keyOrPlaceholder } = useInt()
273 + return (
274 + <section className="int-section">
275 + <h2>Claude Code</h2>
276 + <p>Two ways: a one-liner for your user scope, or a <code>.mcp.json</code> committed with the project so the whole team gets the tools.</p>
277 + <InstallControls />
278 + <ol className="int-steps">
279 + <li><strong>Install the server</strong><IntCopyBlock compact lang="bash" title="Terminal" code={installCommand(mode)} note={mode === 'npm' ? 'With npx you can skip this step — Claude Code downloads the package on first use.' : 'Installs the hfmarketdata-mcp binary globally from the tarball served by this site.'} /></li>
280 + <li><strong>Register it</strong><IntCopyBlock compact lang="bash" title="Terminal" code={claudeAddCommand(mode, keyOrPlaceholder, 'user')} note="Drop the -e flag for keyless mode. Use --scope project to write .mcp.json instead." /></li>
281 + <li><strong>Or commit a project config</strong><IntCopyBlock compact lang="json" title=".mcp.json" code={mcpJson(mode, keyOrPlaceholder)} note="Prefer ${HFMD_API_KEY} expansion in shared files rather than a literal key: Claude Code expands environment variables in .mcp.json." /></li>
282 + <li><strong>Check & use</strong><IntCopyBlock compact lang="text" title="Inside Claude Code" lines={['/mcp # hfmarketdata should show as connected', 'Plot me the CL term structure', 'Compare ESZ25 and ESH26 over 30 days', 'Screen US stocks with PE<15 and FCF yield>6% and show their last 3 quarters']} /></li>
283 + </ol>
284 + <div className="int-callout">Pair it with the <Link to="/integrations/skills">skills pack</Link>: the MCP tools answer questions live, the skills give Claude Code reproducible Python (pandas) workflows for backtests and charts.</div>
285 + </section>
286 + )
287 +}
288 +
289 +function CursorPage() {
290 + const { mode, key, keyOrPlaceholder } = useInt()
291 + const link = cursorDeeplink(mode, key)
292 + return (
293 + <section className="int-section">
294 + <h2>Cursor</h2>
295 + <p>Cursor reads <code>.cursor/mcp.json</code> in the project (shared) or <code>~/.cursor/mcp.json</code> (global). Tools appear in Agent mode.</p>
296 + <InstallControls />
297 + <div className="int-actions">
298 + <a href={link} className="btn btn-primary" title="Opens Cursor’s MCP install dialog">Add to Cursor (one click)</a>
299 + <IntCopyChip text={link} label="Copy deeplink" />
300 + </div>
301 + <p className="muted" style={{ fontSize: 13 }}>Best effort: the deeplink (<code>cursor://anysphere.cursor-deeplink/mcp/install?name=…&config=&lt;base64 JSON&gt;</code>) is supported by Cursor ≥ 1.0; if nothing happens, paste the JSON below.</p>
302 + <ol className="int-steps">
303 + <li><strong>Install the server</strong><IntCopyBlock compact lang="bash" title="Terminal" code={installCommand(mode)} /></li>
304 + <li><strong>Add the config</strong><IntCopyBlock compact lang="json" title=".cursor/mcp.json" code={mcpJson(mode, keyOrPlaceholder)} note="Cursor → Settings → MCP → the server should list 14 tools. Toggle it on for the Agent." /></li>
305 + <li><strong>Try</strong><IntCopyBlock compact lang="text" title="Agent chat" lines={PROMPTS.map(([q]) => q)} /></li>
306 + </ol>
307 + </section>
308 + )
309 +}
310 +
311 +function CodexPage() {
312 + const { mode, keyOrPlaceholder } = useInt()
313 + return (
314 + <section className="int-section">
315 + <h2>Codex</h2>
316 + <p>OpenAI Codex CLI reads MCP servers from <code>~/.codex/config.toml</code>. Add a table for <code>hfmarketdata</code>:</p>
317 + <InstallControls />
318 + <ol className="int-steps">
319 + <li><strong>Install the server</strong><IntCopyBlock compact lang="bash" title="Terminal" code={installCommand(mode)} /></li>
320 + <li><strong>Add to the config</strong><IntCopyBlock compact lang="toml" title="~/.codex/config.toml" code={codexToml(mode, keyOrPlaceholder)} note="Remove the env line for keyless mode. Restart codex; `/mcp` lists the connected servers." /></li>
321 + <li><strong>Try</strong><IntCopyBlock compact lang="text" title="codex" lines={PROMPTS.map(([q]) => q)} /></li>
322 + </ol>
323 + </section>
324 + )
325 +}
326 +
327 +// ---------------------------------------------------------------------------------------------
328 +function SkillsPage() {
329 + return (
330 + <>
331 + <section className="int-section">
332 + <h2>Skills pack for Claude Code / Claude</h2>
333 + <p>Five folders, each a <code>SKILL.md</code> (when to use, step-by-step, examples, gotchas) plus small Python scripts on <code>requests</code> + <code>pandas</code> (charts with <code>matplotlib</code>). No other dependency; every script prints the exact date range it received and never fills gaps.</p>
334 + <div className="int-dl">
335 + <div className="card"><strong>hfmarketdata-skills.zip</strong><span className="size">≈ 42 kB · 5 skills · 18 files · deterministic build</span><a className="btn btn-primary" href={SKILLS_ZIP_URL} download>Download skills</a></div>
336 + <div className="card"><strong>Install</strong><span className="size">Personal (all projects) or project scope (commit it with your repo)</span>
337 + <IntCopyBlock compact lang="bash" title="Terminal" lines={[`curl -LO ${SKILLS_ZIP_URL}`, 'unzip -o hfmarketdata-skills.zip -d ~/.claude/skills/ # all projects', '# or: unzip -o hfmarketdata-skills.zip -d .claude/skills/ # this project only', 'pip install requests pandas matplotlib', 'export HFMD_API_KEY=hfmd_live_… # optional']} />
338 + </div>
339 + </div>
340 + <div className="int-table"><table>
341 + <thead><tr><th>Skill</th><th>What it does</th><th>Scripts</th></tr></thead>
342 + <tbody>{SKILLS.map(([n, d, s]) => <tr key={n}><td><code>{n}</code></td><td>{d}</td><td className="mono muted" style={{ fontSize: 12 }}>{s}</td></tr>)}</tbody>
343 + </table></div>
344 + </section>
345 + <section className="int-section">
346 + <h2>Example</h2>
347 + <IntCopyBlock lang="bash" title="hfmd-quick-backtest" lines={[
348 + 'python3 ~/.claude/skills/hfmd-quick-backtest/scripts/ma_crossover.py --asset etf --ticker SPY --start 2010-01-01 \\',
349 + ' --grid 10,20,50 --grid-slow 100,150,200 --train-years 3 --test-years 1 --cost-bps 5 --plot spy_wf.png',
350 + '# OUT-OF-SAMPLE 2013-01-04 → 2026-08-07 (3,418 bars, 14 blocks): strategy Sharpe 0.82 vs buy&hold 0.91, max DD −23 % vs −34 %',
351 + ]} note="Parameters are chosen on the preceding training window only, applied to the next bar — the numbers shown are out-of-sample." />
352 + <p>Ask in plain words instead: “backtest a 20/100 moving-average cross on CL with a walk-forward” — Claude Code picks the skill.</p>
353 + </section>
354 + </>
355 + )
5 356 }
added hfmarketdata/web/src/pages/integrations/e2e-integrations.mjs +88 −0
@@ -0,0 +1,88 @@
1 +#!/usr/bin/env node
2 +// End-to-end check of the Integrations pages against a running build (vite preview or the API serving dist/).
3 +// node src/pages/integrations/e2e-integrations.mjs [http://localhost:4173]
4 +// Uses the Playwright already in devDependencies; prints one line per assertion, exits 1 on failure.
5 +import { chromium } from '@playwright/test'
6 +
7 +const base = process.argv[2] || 'http://localhost:4173'
8 +let failures = 0
9 +const ok = (cond, msg) => { console.log(`${cond ? 'PASS' : 'FAIL'} ${msg}`); if (!cond) failures++ }
10 +
11 +const browser = await chromium.launch()
12 +const ctx = await browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'] })
13 +const page = await ctx.newPage()
14 +// Page errors are always failures. Console errors are failures unless they come from the API
15 +// (/v1/*), which is absent behind `vite preview` (the shell's AuthProvider probes /v1/me).
16 +const errors = []
17 +const apiNoise = []
18 +page.on('pageerror', (e) => errors.push(String(e)))
19 +page.on('console', (m) => {
20 + if (m.type() !== 'error') return
21 + const url = m.location()?.url || ''
22 + if (url.includes('/v1/')) apiNoise.push(url)
23 + else errors.push(`${m.text()} @ ${url}`)
24 +})
25 +
26 +// Overview
27 +await page.goto(`${base}/integrations`, { waitUntil: 'networkidle' })
28 +ok((await page.locator('h1').first().textContent()).includes('Market data where your agent already works'), 'overview: hero title')
29 +ok((await page.locator('.int-subnav a').count()) === 6, 'overview: 6 sub-navigation links')
30 +ok((await page.locator('.int-card').count()) === 5, 'overview: 5 integration cards')
31 +ok((await page.locator('.int-prompt').count()) === 3, 'overview: 3 example prompts')
32 +const heroCursor = await page.locator('.int-hero a', { hasText: 'Add to Cursor' }).getAttribute('href')
33 +ok(heroCursor.startsWith('cursor://anysphere.cursor-deeplink/mcp/install?name=hfmarketdata&config='), 'overview: Add to Cursor deeplink')
34 +
35 +// MCP page
36 +await page.goto(`${base}/integrations/mcp`, { waitUntil: 'networkidle' })
37 +ok((await page.locator('table').nth(1).locator('tbody tr').count()) === 14, 'mcp: 14 tools in the table')
38 +ok((await page.locator('a[href$="hfmarketdata-mcp-1.0.0.tgz"]').count()) >= 1, 'mcp: tarball download link')
39 +ok((await page.locator('.int-copy-pre').first().textContent()).includes('npm i -g https://www.hfmarketdata.io/downloads/hfmarketdata-mcp-1.0.0.tgz'), 'mcp: tarball install command by default')
40 +
41 +// Claude Code: key substitution + mode toggle + copy button
42 +await page.goto(`${base}/integrations/claude-code`, { waitUntil: 'networkidle' })
43 +const pre = (i) => page.locator('.int-copy-pre').nth(i).textContent()
44 +ok((await pre(1)).includes('claude mcp add hfmarketdata --scope user -e HFMD_API_KEY=hfmd_live_YOUR_KEY -- hfmarketdata-mcp'), 'claude-code: add command (tarball mode, placeholder key)')
45 +await page.fill('#int-key', 'hfmd_live_abc123')
46 +ok((await pre(1)).includes('-e HFMD_API_KEY=hfmd_live_abc123 -- hfmarketdata-mcp'), 'claude-code: typed key flows into the command')
47 +ok((await pre(2)).includes('"HFMD_API_KEY": "hfmd_live_abc123"'), 'claude-code: typed key flows into .mcp.json')
48 +await page.click('.int-toggle button:has-text("npm registry")')
49 +ok((await pre(1)).includes('-- npx -y hfmarketdata-mcp'), 'claude-code: npm mode switches to npx')
50 +ok((await pre(2)).includes('"command": "npx"'), 'claude-code: npm mode in .mcp.json')
51 +ok((await page.locator('.int-callout.warn').count()) === 1, 'claude-code: npm mode shows the "being published" notice')
52 +await page.locator('.int-copy-btn').nth(1).click()
53 +const clip = await page.evaluate(() => navigator.clipboard.readText())
54 +ok(clip.includes('claude mcp add hfmarketdata') && clip.includes('npx -y hfmarketdata-mcp'), 'claude-code: copy button writes the command to the clipboard')
55 +ok((await page.locator('.int-copy-btn').nth(1).textContent()) === 'Copied', 'claude-code: copy button shows "Copied"')
56 +
57 +// Cursor: deeplink decodes to a valid config
58 +await page.goto(`${base}/integrations/cursor`, { waitUntil: 'networkidle' })
59 +await page.fill('#int-key', 'hfmd_live_xyz')
60 +const href = await page.locator('a.btn-primary', { hasText: 'Add to Cursor' }).getAttribute('href')
61 +const b64 = decodeURIComponent(href.split('config=')[1])
62 +const cfg = JSON.parse(Buffer.from(b64, 'base64').toString('utf8'))
63 +ok(cfg.command === 'hfmarketdata-mcp' && cfg.env?.HFMD_API_KEY === 'hfmd_live_xyz', `cursor: deeplink config decodes (${JSON.stringify(cfg)})`)
64 +ok((await page.locator('.int-copy-title', { hasText: '.cursor/mcp.json' }).count()) === 1, 'cursor: .cursor/mcp.json block')
65 +
66 +// Codex
67 +await page.goto(`${base}/integrations/codex`, { waitUntil: 'networkidle' })
68 +const toml = await page.locator('.int-copy-pre').nth(1).textContent()
69 +ok(toml.startsWith('[mcp_servers.hfmarketdata]') && toml.includes('command = "hfmarketdata-mcp"') && toml.includes('env = { HFMD_API_KEY = "hfmd_live_YOUR_KEY" }'), 'codex: TOML block')
70 +
71 +// Skills
72 +await page.goto(`${base}/integrations/skills`, { waitUntil: 'networkidle' })
73 +ok((await page.locator('a[href$="hfmarketdata-skills.zip"]').count()) >= 1, 'skills: zip download link')
74 +ok((await page.locator('table tbody tr').count()) === 5, 'skills: 5 skills listed')
75 +
76 +// Unknown sub-route falls back to overview; downloads are served
77 +await page.goto(`${base}/integrations/nope`, { waitUntil: 'networkidle' })
78 +ok((await page.locator('.int-card').count()) === 5, 'fallback: unknown sub-route shows the overview')
79 +for (const f of ['hfmarketdata-mcp-1.0.0.tgz', 'hfmarketdata-skills.zip']) {
80 + const r = await page.request.get(`${base}/downloads/${f}`)
81 + const ct = r.headers()['content-type'] || ''
82 + ok(r.status() === 200 && !ct.includes('text/html'), `downloads: /downloads/${f} → ${r.status()} ${ct} ${(await r.body()).length} bytes`)
83 +}
84 +ok(errors.length === 0, `no console/page errors${errors.length ? ': ' + errors.join(' | ') : ''}${apiNoise.length ? ` (ignored ${apiNoise.length} API-absent responses on /v1/*)` : ''}`)
85 +
86 +await browser.close()
87 +console.log(failures ? `\n${failures} failure(s)` : '\nall green')
88 +process.exit(failures ? 1 : 0)
added hfmarketdata/web/src/pages/integrations/integrations.css +90 −0
@@ -0,0 +1,90 @@
1 +/* Integrations area — uses the shell tokens from src/app/theme.css only. Owner: mcp-skills. */
2 +.int-hero { display: grid; grid-template-columns: 1.4fr 1fr; gap: 28px; align-items: start; margin-bottom: 28px; }
3 +.int-hero h1 { font-size: 34px; line-height: 1.15; margin: 0 0 10px; letter-spacing: -0.01em; }
4 +.int-hero p.lead { color: var(--fg-1); font-size: 16px; margin: 0 0 18px; max-width: 60ch; }
5 +.int-kicker { display: inline-flex; gap: 8px; align-items: center; font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); margin-bottom: 10px; }
6 +.int-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 6px 0 4px; }
7 +.int-actions .btn { text-decoration: none; }
8 +.int-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
9 +.int-badge { font-size: 12px; padding: 3px 9px; border-radius: 999px; border: 1px solid var(--line-2); color: var(--fg-1); background: var(--bg-1); }
10 +.int-badge.ok { border-color: color-mix(in srgb, var(--accent) 45%, transparent); color: var(--accent); }
11 +
12 +.int-demo { border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-1); overflow: hidden; }
13 +.int-demo-bar { display: flex; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--line); }
14 +.int-demo-bar i { width: 10px; height: 10px; border-radius: 50%; background: var(--line-2); display: inline-block; }
15 +.int-demo pre { margin: 0; border: 0; border-radius: 0; background: transparent; font-size: 12.5px; line-height: 1.55; padding: 14px 16px; min-height: 250px; }
16 +.int-demo .u { color: var(--accent-2); }
17 +.int-demo .t { color: var(--fg-2); }
18 +.int-demo .a { color: var(--fg); }
19 +.int-demo .g { color: var(--accent); }
20 +
21 +.int-subnav { display: flex; gap: 6px; flex-wrap: wrap; border-bottom: 1px solid var(--line); padding-bottom: 10px; margin: 8px 0 26px; }
22 +.int-subnav a { padding: 7px 12px; border-radius: 6px; color: var(--fg-1); font-size: 14px; }
23 +.int-subnav a.active, .int-subnav a:hover { background: var(--bg-2); color: var(--fg); text-decoration: none; }
24 +
25 +.int-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin: 18px 0 28px; }
26 +.int-card { display: flex; flex-direction: column; gap: 8px; text-decoration: none; color: inherit; transition: border-color 0.15s; }
27 +.int-card:hover { border-color: var(--fg-2); text-decoration: none; }
28 +.int-card h3 { margin: 0; font-size: 16px; display: flex; align-items: center; gap: 8px; }
29 +.int-card p { margin: 0; color: var(--fg-1); font-size: 14px; }
30 +.int-card .go { color: var(--accent-2); font-size: 13px; margin-top: auto; }
31 +.int-logo { width: 26px; height: 26px; border-radius: 7px; display: inline-grid; place-items: center; font-family: var(--mono); font-size: 12px; font-weight: 700; color: #05140c; background: var(--accent); }
32 +.int-logo.b { background: var(--accent-2); color: #041a2a; }
33 +.int-logo.w { background: var(--warn); color: #2a1c00; }
34 +.int-logo.g { background: var(--fg-1); color: var(--bg); }
35 +
36 +.int-section { margin: 0 0 34px; }
37 +.int-section h2 { font-size: 22px; margin: 0 0 6px; }
38 +.int-section h3 { font-size: 16px; margin: 22px 0 8px; }
39 +.int-section p { color: var(--fg-1); max-width: 78ch; }
40 +.int-steps { counter-reset: s; list-style: none; padding: 0; margin: 12px 0; display: grid; gap: 14px; }
41 +.int-steps li { position: relative; padding-left: 40px; }
42 +.int-steps li::before { counter-increment: s; content: counter(s); position: absolute; left: 0; top: 2px; width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; background: var(--bg-2); border: 1px solid var(--line-2); color: var(--fg); font-size: 13px; font-weight: 600; }
43 +.int-steps li > strong { display: block; margin-bottom: 6px; }
44 +
45 +.int-toggle { display: inline-flex; border: 1px solid var(--line-2); border-radius: 8px; overflow: hidden; margin: 6px 0 12px; }
46 +.int-toggle button { font: inherit; font-size: 13px; padding: 6px 12px; background: var(--bg-1); color: var(--fg-1); border: 0; cursor: pointer; }
47 +.int-toggle button + button { border-left: 1px solid var(--line-2); }
48 +.int-toggle button.on { background: var(--bg-2); color: var(--fg); }
49 +
50 +.int-field { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 6px 0 14px; }
51 +.int-field label { font-size: 13px; color: var(--fg-1); }
52 +.int-field input { min-width: 280px; font-family: var(--mono); font-size: 13px; }
53 +.int-field .hint { font-size: 12px; color: var(--fg-2); }
54 +
55 +.int-copy { margin: 10px 0 16px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-1); overflow: hidden; }
56 +.int-copy-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px 6px 12px; border-bottom: 1px solid var(--line); background: var(--bg-2); }
57 +.int-copy-title { font-size: 12px; color: var(--fg-2); }
58 +.int-copy-btn { font: inherit; font-size: 12px; padding: 3px 10px; border-radius: 6px; border: 1px solid var(--line-2); background: var(--bg-1); color: var(--fg-1); cursor: pointer; }
59 +.int-copy-btn:hover { border-color: var(--fg-2); color: var(--fg); }
60 +.int-copy-btn.ok { border-color: var(--accent); color: var(--accent); }
61 +.int-copy-pre { margin: 0; border: 0; border-radius: 0; background: transparent; font-size: 13px; line-height: 1.55; padding: 12px 14px; }
62 +.int-copy.compact .int-copy-pre { padding: 8px 12px; }
63 +.int-copy-note { padding: 6px 12px 10px; font-size: 12.5px; }
64 +.int-chip { font: inherit; font-size: 12.5px; padding: 3px 9px; border-radius: 6px; border: 1px solid var(--line-2); background: var(--bg-1); color: var(--fg); cursor: pointer; }
65 +.int-chip:hover { border-color: var(--fg-2); }
66 +.int-chip.ok { border-color: var(--accent); color: var(--accent); }
67 +.int-chip-ico { color: var(--fg-2); }
68 +
69 +.int-table { margin: 12px 0 18px; overflow-x: auto; }
70 +.int-table table { font-size: 13.5px; }
71 +.int-table td:first-child { white-space: nowrap; }
72 +.int-table code { white-space: nowrap; }
73 +
74 +.int-prompts { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin: 10px 0 18px; }
75 +.int-prompt { padding: 14px; display: flex; flex-direction: column; gap: 8px; }
76 +.int-prompt q { font-style: italic; color: var(--fg); quotes: '“' '”'; }
77 +.int-prompt small { color: var(--fg-2); }
78 +
79 +.int-callout { border-left: 3px solid var(--accent-2); padding: 10px 14px; background: var(--bg-1); border-radius: 0 var(--radius) var(--radius) 0; margin: 14px 0; color: var(--fg-1); font-size: 14px; }
80 +.int-callout.warn { border-left-color: var(--warn); }
81 +.int-dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin: 14px 0 22px; }
82 +.int-dl .card { display: flex; flex-direction: column; gap: 6px; }
83 +.int-dl .card .size { color: var(--fg-2); font-size: 12.5px; }
84 +.int-dl .btn { align-self: flex-start; margin-top: 4px; }
85 +
86 +@media (max-width: 860px) {
87 + .int-hero { grid-template-columns: 1fr; }
88 + .int-hero h1 { font-size: 28px; }
89 + .int-field input { min-width: 0; width: 100%; }
90 +}
91