#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import Database from "better-sqlite3";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
// Dynamic import for @blockrun/llm (ESM export path is broken)
let LLMClient: any;
const loadLLM = async () => {
try {
// Try normal import first (in case package is fixed)
const llm = await import("@blockrun/llm");
LLMClient = llm.LLMClient;
} catch {
// Fall back to direct file path if exports are broken
const { createRequire } = await import("module");
const require = createRequire(import.meta.url);
const llmPath = require.resolve("@blockrun/llm");
// The CJS file is index.cjs in the same directory
const cjsPath = llmPath.replace("index.js", "index.cjs");
const llm = require(cjsPath);
LLMClient = llm.LLMClient;
}
};
// ============================================================================
// Constants
// ============================================================================
const VERSION = "0.1.0";
const BASE_CHAIN_ID = 8453;
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const WETH_ADDRESS = "0x4200000000000000000000000000000000000006";
// Risk limits (hardcoded for safety)
const RISK_LIMITS = {
maxPositionSize: 0.15, // 15% max per position
maxTotalExposure: 0.50, // 50% max total
maxDailyLoss: 0.05, // 5% daily loss limit
minCashReserve: 0.50, // 50% min cash
stopLossPercent: 0.15, // 15% stop loss
};
// ============================================================================
// Database Setup
// ============================================================================
function getDataDir(): string {
const dataDir = path.join(os.homedir(), ".blockrun", "alpha");
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
return dataDir;
}
function initDatabase(): Database.Database {
const dbPath = path.join(getDataDir(), "alpha.db");
const db = new Database(dbPath);
// Create tables
db.exec(`
-- Portfolio positions
CREATE TABLE IF NOT EXISTS positions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT NOT NULL,
amount REAL NOT NULL,
entry_price REAL NOT NULL,
entry_time TEXT NOT NULL,
notes TEXT
);
-- Trade history
CREATE TABLE IF NOT EXISTS trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT NOT NULL,
side TEXT NOT NULL,
amount REAL NOT NULL,
price REAL NOT NULL,
timestamp TEXT NOT NULL,
tx_hash TEXT,
pnl REAL,
notes TEXT
);
-- Trade memory for vector search
CREATE TABLE IF NOT EXISTS trade_memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
symbol TEXT NOT NULL,
action TEXT NOT NULL,
reasoning TEXT NOT NULL,
outcome TEXT,
pnl_percent REAL,
tags TEXT,
embedding BLOB
);
-- Daily PnL tracking
CREATE TABLE IF NOT EXISTS daily_pnl (
date TEXT PRIMARY KEY,
starting_value REAL NOT NULL,
ending_value REAL,
pnl_percent REAL
);
`);
return db;
}
// ============================================================================
// Technical Analysis Functions
// ============================================================================
function calculateEMA(prices: number[], period: number): number[] {
const k = 2 / (period + 1);
const ema: number[] = [prices[0]];
for (let i = 1; i < prices.length; i++) {
ema.push(prices[i] * k + ema[i - 1] * (1 - k));
}
return ema;
}
function calculateRSI(prices: number[], period: number = 14): number {
if (prices.length < period + 1) return 50;
let gains = 0, losses = 0;
for (let i = prices.length - period; i < prices.length; i++) {
const change = prices[i] - prices[i - 1];
if (change > 0) gains += change;
else losses -= change;
}
const avgGain = gains / period;
const avgLoss = losses / period;
if (avgLoss === 0) return 100;
const rs = avgGain / avgLoss;
return 100 - (100 / (1 + rs));
}
function calculateMACD(prices: number[]): { value: number; signal: number; histogram: number } {
if (prices.length < 26) {
return { value: 0, signal: 0, histogram: 0 };
}
const ema12 = calculateEMA(prices, 12);
const ema26 = calculateEMA(prices, 26);
const macdLine = ema12.map((v, i) => v - ema26[i]);
const signalLine = calculateEMA(macdLine.slice(-9), 9);
const value = macdLine[macdLine.length - 1];
const signal = signalLine[signalLine.length - 1];
return {
value,
signal,
histogram: value - signal
};
}
// ============================================================================
// API Functions
// ============================================================================
// Map common symbols to CoinGecko IDs
const COINGECKO_IDS: Record<string, string> = {
"BTC": "bitcoin",
"ETH": "ethereum",
"SOL": "solana",
"AVAX": "avalanche-2",
"MATIC": "matic-network",
"LINK": "chainlink",
"UNI": "uniswap",
"AAVE": "aave",
"ARB": "arbitrum",
"OP": "optimism",
"PEPE": "pepe",
"DOGE": "dogecoin",
"SHIB": "shiba-inu",
};
async function fetchCoinGeckoPrices(symbol: string, days: number = 7): Promise<number[]> {
const coinId = COINGECKO_IDS[symbol.toUpperCase()] || symbol.toLowerCase();
const url = `https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=${days}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`CoinGecko API error: ${response.status} - Try using full coin ID`);
}
const data = await response.json();
return data.prices.map((p: number[]) => p[1]); // [timestamp, price]
}
async function fetchDexScreenerToken(tokenAddress: string): Promise<any> {
const url = `https://api.dexscreener.com/latest/dex/tokens/${tokenAddress}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`DexScreener API error: ${response.status}`);
}
return response.json();
}
async function fetchDexScreenerSearch(query: string): Promise<any> {
const url = `https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(query)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`DexScreener API error: ${response.status}`);
}
return response.json();
}
async function fetch0xQuote(
sellToken: string,
buyToken: string,
sellAmount: string,
chainId: number = BASE_CHAIN_ID
): Promise<any> {
const url = `https://api.0x.org/swap/v1/quote?sellToken=${sellToken}&buyToken=${buyToken}&sellAmount=${sellAmount}&chainId=${chainId}`;
const response = await fetch(url, {
headers: {
"0x-api-key": process.env.ZEROX_API_KEY || "",
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(`0x API error: ${response.status} - ${error}`);
}
return response.json();
}
// ============================================================================
// Wallet Functions
// ============================================================================
function getWalletPath(): string {
return path.join(os.homedir(), ".blockrun", "wallet.json");
}
function loadWallet(): { address: string; privateKey: string } | null {
const walletPath = getWalletPath();
if (!fs.existsSync(walletPath)) {
return null;
}
const data = JSON.parse(fs.readFileSync(walletPath, "utf-8"));
return data;
}
// BlockRun LLM Client (lazy initialized)
let blockrunClient: any = null;
function getBlockRunClient(): any {
if (blockrunClient) return blockrunClient;
// Try to load private key from session file (same as blockrun-mcp)
const sessionPath = path.join(os.homedir(), ".blockrun", ".session");
if (fs.existsSync(sessionPath)) {
const privateKey = fs.readFileSync(sessionPath, "utf-8").trim();
if (privateKey.startsWith("0x")) {
blockrunClient = new LLMClient({ privateKey });
return blockrunClient;
}
}
return null;
}
// ============================================================================
// Simple Vector Similarity (cosine similarity without external deps)
// ============================================================================
function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) return 0;
let dotProduct = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
// Simple text to vector (bag of words style - MVP, replace with real embeddings later)
function textToVector(text: string): number[] {
const words = text.toLowerCase().split(/\s+/);
const vector = new Array(384).fill(0);
for (const word of words) {
let hash = 0;
for (let i = 0; i < word.length; i++) {
hash = ((hash << 5) - hash) + word.charCodeAt(i);
hash = hash & hash;
}
const index = Math.abs(hash) % 384;
vector[index] += 1;
}
// Normalize
const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
if (norm > 0) {
for (let i = 0; i < vector.length; i++) {
vector[i] /= norm;
}
}
return vector;
}
// ============================================================================
// MCP Server Setup
// ============================================================================
const server = new McpServer({
name: "alpha",
version: VERSION,
});
const db = initDatabase();
// ============================================================================
// Tool: alpha_signal
// ============================================================================
server.tool(
"alpha_signal",
"Get technical analysis signals (RSI, MACD, EMA) for a cryptocurrency",
{
symbol: z.string().describe("Trading symbol (e.g., 'ETH', 'BTC')"),
days: z.number().optional().describe("Days of history for analysis: 1, 7, 14, 30 (default: 7)"),
},
async ({ symbol, days = 7 }) => {
try {
const prices = await fetchCoinGeckoPrices(symbol, days);
const rsi = calculateRSI(prices);
const macd = calculateMACD(prices);
const ema9 = calculateEMA(prices, 9);
const ema21 = calculateEMA(prices, 21);
const ema50 = calculateEMA(prices, 50);
const currentPrice = prices[prices.length - 1];
const ema9Current = ema9[ema9.length - 1];
const ema21Current = ema21[ema21.length - 1];
const ema50Current = ema50[ema50.length - 1];
// Generate recommendation
let bullishSignals = 0;
let bearishSignals = 0;
// RSI
if (rsi < 30) bullishSignals += 2;
else if (rsi < 40) bullishSignals += 1;
else if (rsi > 70) bearishSignals += 2;
else if (rsi > 60) bearishSignals += 1;
// MACD
if (macd.histogram > 0 && macd.value > macd.signal) bullishSignals += 1;
if (macd.histogram < 0 && macd.value < macd.signal) bearishSignals += 1;
// EMA alignment
if (ema9Current > ema21Current && ema21Current > ema50Current) bullishSignals += 2;
if (ema9Current < ema21Current && ema21Current < ema50Current) bearishSignals += 2;
// Price vs EMAs
if (currentPrice > ema9Current && currentPrice > ema21Current) bullishSignals += 1;
if (currentPrice < ema9Current && currentPrice < ema21Current) bearishSignals += 1;
const totalSignals = bullishSignals + bearishSignals;
const confidence = totalSignals > 0 ? Math.max(bullishSignals, bearishSignals) / totalSignals : 0.5;
let recommendation = "NEUTRAL";
if (bullishSignals > bearishSignals + 2) recommendation = "STRONG_BUY";
else if (bullishSignals > bearishSignals) recommendation = "BUY";
else if (bearishSignals > bullishSignals + 2) recommendation = "STRONG_SELL";
else if (bearishSignals > bullishSignals) recommendation = "SELL";
const result = {
symbol: symbol.toUpperCase(),
days,
price: currentPrice,
indicators: {
rsi: Math.round(rsi * 100) / 100,
macd: {
value: Math.round(macd.value * 1000) / 1000,
signal: Math.round(macd.signal * 1000) / 1000,
histogram: Math.round(macd.histogram * 1000) / 1000,
},
ema: {
ema9: Math.round(ema9Current * 100) / 100,
ema21: Math.round(ema21Current * 100) / 100,
ema50: Math.round(ema50Current * 100) / 100,
}
},
signals: {
bullish: bullishSignals,
bearish: bearishSignals,
},
recommendation,
confidence: Math.round(confidence * 100) / 100,
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_dex
// ============================================================================
server.tool(
"alpha_dex",
"Get DEX market data for a token (price, volume, liquidity)",
{
token: z.string().describe("Token address or symbol (e.g., 'ETH', '0x...')"),
},
async ({ token }) => {
try {
let data;
// Check if it's an address or symbol
if (token.startsWith("0x")) {
data = await fetchDexScreenerToken(token);
} else {
data = await fetchDexScreenerSearch(token);
}
if (!data.pairs || data.pairs.length === 0) {
return {
content: [{ type: "text", text: `No DEX data found for ${token}` }],
};
}
// Get the most liquid pair (prioritize Base/Ethereum, but fall back to any chain)
let pairs = data.pairs
.filter((p: any) => p.chainId === "base" || p.chainId === "ethereum")
.sort((a: any, b: any) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0));
// Fall back to all pairs if no Base/Ethereum pairs found
if (pairs.length === 0) {
pairs = data.pairs.sort((a: any, b: any) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0));
}
if (pairs.length === 0) {
return {
content: [{ type: "text", text: `No DEX pairs found for ${token}` }],
};
}
const topPair = pairs[0];
const result = {
token: topPair.baseToken?.symbol || token,
address: topPair.baseToken?.address,
chain: topPair.chainId,
price: parseFloat(topPair.priceUsd || "0"),
priceChange: {
h1: topPair.priceChange?.h1 || 0,
h24: topPair.priceChange?.h24 || 0,
},
volume24h: topPair.volume?.h24 || 0,
liquidity: topPair.liquidity?.usd || 0,
fdv: topPair.fdv || 0,
dex: topPair.dexId,
pairAddress: topPair.pairAddress,
topPairs: pairs.slice(0, 5).map((p: any) => ({
dex: p.dexId,
chain: p.chainId,
liquidity: p.liquidity?.usd || 0,
volume24h: p.volume?.h24 || 0,
})),
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_sentiment
// ============================================================================
server.tool(
"alpha_sentiment",
"Get market sentiment from X/Twitter via Grok (requires BlockRun wallet with USDC)",
{
query: z.string().describe("Search query (e.g., 'ETH sentiment', '@VitalikButerin', 'crypto news')"),
max_results: z.number().optional().describe("Max results to return (default: 10)"),
},
async ({ query, max_results = 10 }) => {
try {
const client = getBlockRunClient();
if (!client) {
return {
content: [{
type: "text",
text: `Sentiment analysis requires BlockRun wallet.\n\nSetup:\n1. Run: claude mcp add blockrun npx @blockrun/mcp\n2. Run: blockrun_setup to create wallet\n3. Fund wallet with USDC on Base\n\nAlternatively, use alpha_signal for free technical analysis.`
}],
};
}
// Call Grok with Twitter search enabled
const response = await client.chat("xai/grok-3", query, {
system: `You are a crypto market sentiment analyst. Analyze X/Twitter posts about the query.
Return a JSON object with:
- sentiment: "bullish" | "bearish" | "neutral"
- score: -1.0 to 1.0
- summary: brief analysis (2-3 sentences)
- key_mentions: array of notable accounts/posts
- trending_topics: related trending topics
Max results to analyze: ${max_results}`,
search: true,
} as any);
// Try to parse as JSON, otherwise return raw response
let parsed;
try {
// Extract JSON from response if wrapped in markdown
const jsonMatch = response.match(/```json\n?([\s\S]*?)\n?```/) ||
response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
}
} catch {
// Return raw response if not JSON
parsed = { raw_response: response };
}
return {
content: [{
type: "text",
text: JSON.stringify({
query,
source: "X/Twitter via Grok",
...parsed,
}, null, 2)
}],
};
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
// Check for insufficient balance
if (msg.includes("402") || msg.includes("balance") || msg.includes("insufficient")) {
return {
content: [{
type: "text",
text: `Insufficient USDC balance. Fund your wallet:\n1. Run: blockrun_wallet to get address\n2. Send USDC on Base network\n\nError: ${msg}`
}],
isError: true,
};
}
return {
content: [{ type: "text", text: `Error: ${msg}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_swap
// ============================================================================
server.tool(
"alpha_swap",
"Execute a token swap on Base chain via 0x aggregator",
{
fromToken: z.string().describe("Token to sell (e.g., 'USDC', 'ETH', or address)"),
toToken: z.string().describe("Token to buy (e.g., 'ETH', 'USDC', or address)"),
amount: z.string().describe("Amount to sell (in token units, e.g., '100' for 100 USDC)"),
slippage: z.number().optional().describe("Slippage tolerance in percent (default: 0.5)"),
},
async ({ fromToken, toToken, amount, slippage = 0.5 }) => {
try {
const wallet = loadWallet();
if (!wallet) {
return {
content: [{
type: "text",
text: "No wallet found. Please setup BlockRun wallet first:\n1. Run: claude mcp add blockrun npx @blockrun/mcp\n2. Use blockrun_setup to create wallet"
}],
};
}
// Resolve token addresses
const tokenMap: Record<string, string> = {
"USDC": USDC_ADDRESS,
"ETH": WETH_ADDRESS,
"WETH": WETH_ADDRESS,
};
const sellToken = tokenMap[fromToken.toUpperCase()] || fromToken;
const buyToken = tokenMap[toToken.toUpperCase()] || toToken;
// Convert amount to wei (assuming 6 decimals for USDC, 18 for ETH)
const decimals = fromToken.toUpperCase() === "USDC" ? 6 : 18;
const sellAmount = (parseFloat(amount) * Math.pow(10, decimals)).toString();
// Get quote from 0x
const quote = await fetch0xQuote(sellToken, buyToken, sellAmount);
// For now, just return the quote (actual execution requires signing)
const result = {
status: "quote_ready",
from: {
token: fromToken,
amount: amount,
address: sellToken,
},
to: {
token: toToken,
amount: (parseFloat(quote.buyAmount) / Math.pow(10, toToken.toUpperCase() === "USDC" ? 6 : 18)).toFixed(6),
address: buyToken,
},
price: quote.price,
gas: quote.estimatedGas,
slippage: `${slippage}%`,
note: "To execute, approve and sign the transaction. Execution coming in next version.",
quote: {
to: quote.to,
data: quote.data?.slice(0, 100) + "...",
value: quote.value,
}
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_portfolio
// ============================================================================
server.tool(
"alpha_portfolio",
"Manage and view portfolio positions",
{
action: z.enum(["status", "add", "remove", "history"]).describe("Action to perform"),
symbol: z.string().optional().describe("Token symbol (for add/remove)"),
amount: z.number().optional().describe("Amount (for add/remove)"),
price: z.number().optional().describe("Entry price (for add)"),
},
async ({ action, symbol, amount, price }) => {
try {
if (action === "status") {
const positions = db.prepare("SELECT * FROM positions").all();
const trades = db.prepare("SELECT * FROM trades ORDER BY timestamp DESC LIMIT 10").all();
// Calculate total value (simplified - would need real-time prices)
let totalValue = 0;
const positionsWithValue = positions.map((p: any) => {
const value = p.amount * p.entry_price; // Simplified
totalValue += value;
return {
...p,
currentValue: value,
pnl: 0, // Would calculate with real prices
};
});
return {
content: [{
type: "text",
text: JSON.stringify({
totalValue,
positions: positionsWithValue,
recentTrades: trades,
riskLimits: RISK_LIMITS,
}, null, 2)
}],
};
}
if (action === "add" && symbol && amount && price) {
db.prepare(
"INSERT INTO positions (symbol, amount, entry_price, entry_time) VALUES (?, ?, ?, ?)"
).run(symbol.toUpperCase(), amount, price, new Date().toISOString());
return {
content: [{ type: "text", text: `Added position: ${amount} ${symbol} at $${price}` }],
};
}
if (action === "remove" && symbol) {
const result = db.prepare("DELETE FROM positions WHERE symbol = ?").run(symbol.toUpperCase());
return {
content: [{ type: "text", text: `Removed ${result.changes} position(s) for ${symbol}` }],
};
}
if (action === "history") {
const trades = db.prepare("SELECT * FROM trades ORDER BY timestamp DESC LIMIT 50").all();
return {
content: [{ type: "text", text: JSON.stringify(trades, null, 2) }],
};
}
return {
content: [{ type: "text", text: "Invalid action or missing parameters" }],
isError: true,
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_memory
// ============================================================================
server.tool(
"alpha_memory",
"Store and retrieve trading decisions using semantic search",
{
action: z.enum(["find", "store", "stats"]).describe("Action: find similar trades, store new trade, or get stats"),
query: z.string().optional().describe("Search query for finding similar trades"),
trade: z.object({
symbol: z.string(),
action: z.string(),
reasoning: z.string(),
outcome: z.string().optional(),
pnl_percent: z.number().optional(),
tags: z.array(z.string()).optional(),
}).optional().describe("Trade data to store"),
},
async ({ action, query, trade }) => {
try {
if (action === "find" && query) {
const queryVector = textToVector(query);
const memories = db.prepare("SELECT * FROM trade_memory").all() as any[];
const results = memories
.map((m) => {
const embedding = m.embedding ? JSON.parse(m.embedding) : textToVector(m.reasoning);
const similarity = cosineSimilarity(queryVector, embedding);
return { ...m, similarity };
})
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 5);
return {
content: [{
type: "text",
text: JSON.stringify({
query,
similar_trades: results.map(r => ({
id: r.id,
date: r.timestamp,
symbol: r.symbol,
action: r.action,
reasoning: r.reasoning,
outcome: r.outcome,
pnl: r.pnl_percent ? `${r.pnl_percent}%` : null,
similarity: Math.round(r.similarity * 100) / 100,
}))
}, null, 2)
}],
};
}
if (action === "store" && trade) {
const embedding = textToVector(trade.reasoning);
db.prepare(`
INSERT INTO trade_memory (timestamp, symbol, action, reasoning, outcome, pnl_percent, tags, embedding)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
new Date().toISOString(),
trade.symbol.toUpperCase(),
trade.action,
trade.reasoning,
trade.outcome || null,
trade.pnl_percent || null,
trade.tags ? JSON.stringify(trade.tags) : null,
JSON.stringify(embedding)
);
return {
content: [{ type: "text", text: `Stored trade memory for ${trade.symbol}: ${trade.action}` }],
};
}
if (action === "stats") {
const total = db.prepare("SELECT COUNT(*) as count FROM trade_memory").get() as any;
const wins = db.prepare("SELECT COUNT(*) as count FROM trade_memory WHERE pnl_percent > 0").get() as any;
const losses = db.prepare("SELECT COUNT(*) as count FROM trade_memory WHERE pnl_percent < 0").get() as any;
const avgPnl = db.prepare("SELECT AVG(pnl_percent) as avg FROM trade_memory WHERE pnl_percent IS NOT NULL").get() as any;
return {
content: [{
type: "text",
text: JSON.stringify({
total_trades: total.count,
wins: wins.count,
losses: losses.count,
win_rate: total.count > 0 ? `${Math.round((wins.count / total.count) * 100)}%` : "N/A",
avg_pnl: avgPnl.avg ? `${Math.round(avgPnl.avg * 100) / 100}%` : "N/A",
}, null, 2)
}],
};
}
return {
content: [{ type: "text", text: "Invalid action or missing parameters" }],
isError: true,
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_risk
// ============================================================================
server.tool(
"alpha_risk",
"Check if a proposed trade passes risk management rules",
{
action: z.enum(["check", "limits"]).describe("Action: check a trade or view limits"),
proposed_trade: z.object({
symbol: z.string(),
side: z.enum(["buy", "sell"]),
amount_usd: z.number(),
}).optional().describe("Proposed trade to check"),
},
async ({ action, proposed_trade }) => {
try {
if (action === "limits") {
return {
content: [{
type: "text",
text: JSON.stringify({
risk_limits: {
max_position_size: `${RISK_LIMITS.maxPositionSize * 100}% of portfolio`,
max_total_exposure: `${RISK_LIMITS.maxTotalExposure * 100}% of portfolio`,
max_daily_loss: `${RISK_LIMITS.maxDailyLoss * 100}% triggers trading halt`,
min_cash_reserve: `${RISK_LIMITS.minCashReserve * 100}% must remain in cash`,
stop_loss: `${RISK_LIMITS.stopLossPercent * 100}% per position`,
}
}, null, 2)
}],
};
}
if (action === "check" && proposed_trade) {
// Get current portfolio state
const positions = db.prepare("SELECT * FROM positions").all() as any[];
let totalValue = 0;
positions.forEach((p: any) => {
totalValue += p.amount * p.entry_price;
});
// Assume some cash balance (would come from wallet in real implementation)
const estimatedCash = 1000; // Placeholder
const portfolioTotal = totalValue + estimatedCash;
const issues: string[] = [];
let approved = true;
// Check position size
const positionPercent = proposed_trade.amount_usd / portfolioTotal;
if (positionPercent > RISK_LIMITS.maxPositionSize) {
issues.push(`Position size ${(positionPercent * 100).toFixed(1)}% exceeds ${RISK_LIMITS.maxPositionSize * 100}% limit`);
approved = false;
}
// Check total exposure
const newExposure = (totalValue + proposed_trade.amount_usd) / portfolioTotal;
if (newExposure > RISK_LIMITS.maxTotalExposure) {
issues.push(`Total exposure would be ${(newExposure * 100).toFixed(1)}%, exceeds ${RISK_LIMITS.maxTotalExposure * 100}% limit`);
approved = false;
}
// Check cash reserve
const remainingCash = estimatedCash - proposed_trade.amount_usd;
const cashPercent = remainingCash / portfolioTotal;
if (cashPercent < RISK_LIMITS.minCashReserve) {
issues.push(`Cash would drop to ${(cashPercent * 100).toFixed(1)}%, below ${RISK_LIMITS.minCashReserve * 100}% minimum`);
approved = false;
}
return {
content: [{
type: "text",
text: JSON.stringify({
approved,
proposed_trade,
portfolio: {
total_value: portfolioTotal,
current_exposure: `${((totalValue / portfolioTotal) * 100).toFixed(1)}%`,
cash: estimatedCash,
},
issues: issues.length > 0 ? issues : ["All risk checks passed"],
recommendation: approved ? "Trade approved" : "Trade rejected - adjust size or wait",
}, null, 2)
}],
};
}
return {
content: [{ type: "text", text: "Invalid action or missing parameters" }],
isError: true,
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Start Server
// ============================================================================
async function main() {
// Load @blockrun/llm dynamically (ESM export is broken)
await loadLLM();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`@blockrun/alpha MCP server v${VERSION} running`);
}
main().catch(console.error);