import { describe, expect, it } from 'vitest'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { createServer } from '../src/server.js'; import { RATE_HEADERS, bars, clientWith } from './helpers.js'; async function connected(routes: Parameters[0]) { const { client: http, calls } = clientWith(routes); const { server } = createServer({ client: http }); const [a, b] = InMemoryTransport.createLinkedPair(); await server.connect(a); const mcp = new Client({ name: 'test', version: '0.0.0' }); await mcp.connect(b); return { mcp, server, calls }; } describe('MCP server (in-memory transport)', () => { it('lists 14 tools, 2 resources and 3 prompts with JSON schemas', async () => { const { mcp, server } = await connected([]); const tools = await mcp.listTools(); expect(tools.tools.map((t) => t.name)).toHaveLength(14); const getBars = tools.tools.find((t) => t.name === 'get_bars')!; expect(getBars.inputSchema.type).toBe('object'); expect(Object.keys((getBars.inputSchema as any).properties)).toContain('timeframe'); expect((getBars.inputSchema as any).required).toEqual(['asset', 'symbol']); const resources = await mcp.listResources(); expect(resources.resources.map((r) => r.uri).sort()).toEqual(['hfmarketdata://limits', 'hfmarketdata://status']); const prompts = await mcp.listPrompts(); expect(prompts.prompts.map((p) => p.name).sort()).toEqual(['compare-contracts', 'fundamentals-snapshot', 'term-structure-analysis']); await server.close(); }); it('calls a tool end-to-end and returns text content', async () => { const { mcp, server, calls } = await connected([{ match: '/v1/bars/stock/AAPL', body: { count: 2, data: bars(2) }, headers: RATE_HEADERS }]); const res = await mcp.callTool({ name: 'get_bars', arguments: { asset: 'stock', symbol: 'AAPL', timeframe: '1d', limit: 2 } }); expect(res.isError).toBeFalsy(); const text = (res.content as any)[0].text as string; expect(text).toContain('"row_count":2'); expect(calls[0]).toContain('timeframe=1day'); await server.close(); }); it('returns isError for API failures and for bad arguments', async () => { const { mcp, server } = await connected([{ match: '/v1/futures/ZZ/chain', status: 404, body: { error: { code: 'ROOT_NOT_FOUND', message: 'nope' } } }]); const bad = await mcp.callTool({ name: 'get_futures_chain', arguments: { root: 'ZZ' } }); expect(bad.isError).toBe(true); expect((bad.content as any)[0].text).toContain('ROOT_NOT_FOUND'); const invalid = await mcp.callTool({ name: 'get_futures_chain', arguments: { root: 'ZZ', depth: 999 } }); expect(invalid.isError).toBe(true); await server.close(); }); it('reads both resources', async () => { const { mcp, server } = await connected([{ match: '/v1/status', body: { datasets: { stock: { '1day': { adj_splitdiv: 7664 } } } } }]); const status = await mcp.readResource({ uri: 'hfmarketdata://status' }); expect((status.contents[0] as any).text).toContain('7664'); const limits = await mcp.readResource({ uri: 'hfmarketdata://limits' }); const text = (limits.contents[0] as any).text as string; expect(text).toContain('keyless'); expect(text).toContain('"requests": 120'); expect(text).toContain('unavailable (404'); await server.close(); }); it('renders prompts with arguments', async () => { const { mcp, server } = await connected([]); const p = await mcp.getPrompt({ name: 'compare-contracts', arguments: { symbol_a: 'esz25', symbol_b: 'esh26', days: '45' } }); const text = (p.messages[0].content as any).text as string; expect(text).toContain('ESZ25'); expect(text).toContain('45 days'); const ts = await mcp.getPrompt({ name: 'term-structure-analysis', arguments: { root: 'cl' } }); expect((ts.messages[0].content as any).text).toContain('get_term_structure(root="CL")'); const fs = await mcp.getPrompt({ name: 'fundamentals-snapshot', arguments: { ticker: 'aapl' } }); expect((fs.messages[0].content as any).text).toContain('get_ratios(ticker="AAPL")'); await server.close(); }); });