/** * Assemble the MCP server: 14 tools, 2 resources, 3 prompts, on top of an `HfmdClient`. * Exported so that it can be embedded (tests use an in-memory transport). */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { CallToolResult, GetPromptResult } from '@modelcontextprotocol/sdk/types.js'; import { HfmdClient, type ClientOptions } from './client.js'; import { PROMPTS } from './prompts.js'; import { RESOURCES } from './resources.js'; import { TOOLS, runTool, type ToolContext } from './tools.js'; export const SERVER_NAME = 'hfmarketdata'; export const SERVER_VERSION = '1.0.0'; export interface ServerOptions extends ClientOptions { client?: HfmdClient; toolContext?: ToolContext; } export function createServer(opts: ServerOptions = {}): { server: McpServer; client: HfmdClient } { const client = opts.client ?? new HfmdClient(opts); const server = new McpServer( { name: SERVER_NAME, version: SERVER_VERSION }, { instructions: [ 'HF Market Data: open high-frequency market data (stocks, ETFs, futures, crypto, indices, FX at 1-minute to daily since ~2007/2010; options chains with Greeks since 2010; SEC fundamentals point-in-time).', 'Use search_symbols to resolve symbols, get_bars for prices, the futures tools for contracts/curves/continuous series, and the fundamentals tools for statements, ratios, screens and filings.', 'Outputs are compact tables ({columns, rows}); results above 200 rows are summarised — narrow the range or lower `limit` to see everything.', client.keyless ? 'Running keyless (low hourly limits: 30 requests/hour per IP). Set HFMD_API_KEY for 120 requests/minute — the API key is free (account at https://www.hfmarketdata.io/signup); higher limits are granted on request by e-mail to contact@spboucher.ai, also free.' : 'A free API key is configured (120 requests/minute; higher limits on request by e-mail to contact@spboucher.ai, also free).', 'Intraday timestamps are naive US/Eastern; daily bars are dates. Missing values are null — never invent data.', ].join(' '), }, ); for (const t of TOOLS) { server.registerTool( t.name, { title: t.title, description: t.description, inputSchema: t.schema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: t.name !== 'subscribe_filings', openWorldHint: true }, }, async (args: unknown): Promise => { const { text, isError } = await runTool(t.name, client, args, opts.toolContext); return { content: [{ type: 'text', text }], isError: isError || undefined }; }, ); } for (const r of RESOURCES) { server.registerResource(r.name, r.uri, { title: r.title, description: r.description, mimeType: r.mimeType }, async (uri) => { let text: string; try { text = await r.read(client); } catch (e) { text = JSON.stringify({ error: e instanceof Error ? e.message : String(e) }); } return { contents: [{ uri: uri.href, mimeType: r.mimeType, text }] }; }); } for (const p of PROMPTS) { server.registerPrompt(p.name, { title: p.title, description: p.description, argsSchema: p.args }, (args: Record): GetPromptResult => ({ messages: [{ role: 'user' as const, content: { type: 'text' as const, text: p.build(args as never) } }], })); } return { server, client }; } export { HfmdClient, HfmdError } from './client.js'; export { TOOLS, TOOL_NAMES, runTool } from './tools.js'; export { RESOURCES } from './resources.js'; export { PROMPTS } from './prompts.js';