Skip to main content
Glama

Alpaca MCP Server

by cesarvarela
index.ts5.31 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import dotenv from "dotenv"; import { z } from "zod"; import fetch from "node-fetch"; import Debug from "debug"; dotenv.config(); const debug = Debug("alpaca-mcp"); interface RequestOptions { base?: string; path: string; method?: string; params?: Record<string, unknown>; } async function request<T>({ base = process.env.ALPACA_ENDPOINT!, path, method = "GET", params = {} }: RequestOptions): Promise<T> { if (!process.env.ALPACA_API_KEY || !process.env.ALPACA_SECRET_KEY) { throw new Error("Alpaca credentials not configured. Set ALPACA_API_KEY and ALPACA_SECRET_KEY."); } const qs = new URLSearchParams(params as Record<string, string>).toString(); const url = `${base}${path}${qs ? `?${qs}` : ""}`; const res = await fetch(url, { method, headers: { "APCA-API-KEY-ID": process.env.ALPACA_API_KEY!, "APCA-API-SECRET-KEY": process.env.ALPACA_SECRET_KEY!, }, }); if (!res.ok) { const err = await res.json(); throw new Error(`${res.status} ${res.statusText} - ${JSON.stringify(err)}`); } return (await res.json()) as T; } function getBatches<T>(arr: T[], size: number): T[][] { const batches: T[][] = []; for (let i = 0; i < arr.length; i += size) batches.push(arr.slice(i, i + size)); return batches; } export { request, getBatches }; export async function getAssets({ assetClass = 'us_equity' }: { assetClass?: 'us_equity' | 'crypto' }) { try { const data = await request<any[]>({ base: process.env.ALPACA_BROKER_ENDPOINT!, path: "/v1/assets", params: { status: "active", asset_class: assetClass }, }); const assets = data.filter((a: any) => a.tradable); return { content: [{ type: "text", text: JSON.stringify(assets) }] as any }; } catch (err: any) { debug("get-assets error", err); return { content: [{ type: "text", text: `Error fetching assets: ${err.message}` }] as any, isError: true }; } } export async function getStockBars({ symbols, start, end, timeframe }: { symbols: string[]; start: string; end: string; timeframe: string }) { try { const result: { bars: Record<string, unknown> } = { bars: {} }; for (const batch of getBatches(symbols, 2000)) { let pageToken: string | undefined; do { const params: Record<string, unknown> = { timeframe, limit: 10000, start, end, symbols: batch.join(",") }; if (pageToken) params.page_token = pageToken; const resp = await request<{ bars: Record<string, unknown>; next_page_token?: string }>({ base: process.env.ALPACA_ENDPOINT!, path: "/v2/stocks/bars", params, }); Object.assign(result.bars, resp.bars); pageToken = resp.next_page_token; } while (pageToken); } return { content: [{ type: "text", text: JSON.stringify(result) }] as any }; } catch (err: any) { debug("get-stock-bars error", err); return { content: [{ type: "text", text: `Error fetching stock bars: ${err.message}` }] as any, isError: true }; } } export async function getMarketDays({ start, end }: { start: string; end: string }) { try { const days = await request<any[]>({ base: process.env.ALPACA_ENDPOINT!, path: "/v2/calendar", params: { start, end }, }); return { content: [{ type: "text", text: JSON.stringify(days) }] as any }; } catch (err: any) { debug("get-market-days error", err); return { content: [{ type: "text", text: `Error fetching market days: ${err.message}` }] as any, isError: true }; } } export async function getNews({ start, end, symbols }: { start: string; end: string; symbols: string[] }) { try { const all: any[] = []; let pageToken: string | undefined; do { const params: Record<string, unknown> = pageToken ? { page_token: pageToken } : { sort: "desc", start, end, symbols: symbols.join(","), include_content: true }; const resp = await request<{ news: any[]; next_page_token?: string }>({ base: process.env.ALPACA_ENDPOINT!, path: "/v1beta1/news", params, }); all.push(...resp.news); pageToken = resp.next_page_token; } while (pageToken); return { content: [{ type: "text", text: JSON.stringify(all) }] as any }; } catch (err: any) { debug("get-news error", err); return { content: [{ type: "text", text: `Error fetching news: ${err.message}` }] as any, isError: true }; } } const server = new McpServer({ name: "Alpaca MCP Server", version: "1.0.0", description: "Expose Alpaca API via MCP", }); server.tool( "get-assets", { assetClass: z.enum(["us_equity", "crypto"]).optional().default("us_equity") }, getAssets ); server.tool( "get-stock-bars", { symbols: z.array(z.string()), start: z.string(), end: z.string(), timeframe: z.string(), }, getStockBars ); server.tool( "get-market-days", { start: z.string(), end: z.string() }, getMarketDays ); server.tool( "get-news", { start: z.string(), end: z.string(), symbols: z.array(z.string()) }, getNews ); const transport = new StdioServerTransport(); await server.connect(transport);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cesarvarela/alpaca-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server