Skip to content

Custom Tools

Building custom MCP tools for specific use cases.

Overview

Extend the TradingView Screener MCP server with custom tools tailored to your specific needs.

Basic Tool Structure

Tool Definition

typescript
interface MCPTool {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, any>;
    required?: string[];
  };
}

Tool Handler

typescript
async function handleToolCall(
  name: string,
  args: Record<string, any>
): Promise<ToolResult> {
  // Implement tool logic
  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify(result),
      },
    ],
  };
}

Example: Portfolio Analyzer

Tool Definition

typescript
const portfolioAnalyzerTool: MCPTool = {
  name: 'analyze_portfolio',
  description: 'Analyze a portfolio of stocks',
  inputSchema: {
    type: 'object',
    properties: {
      symbols: {
        type: 'array',
        items: { type: 'string' },
        description: 'Stock symbols to analyze',
      },
      metrics: {
        type: 'array',
        items: {
          type: 'string',
          enum: ['valuation', 'growth', 'profitability', 'technical'],
        },
        description: 'Metrics to include',
      },
    },
    required: ['symbols'],
  },
};

Tool Implementation

typescript
import { StockScreener, StockField } from 'tradingview-screener';

async function analyzePortfolio(
  symbols: string[],
  metrics: string[]
): Promise<any> {
  const screener = new StockScreener();

  // Filter for specific symbols
  screener.where(StockField.NAME.isin(symbols));

  // Select fields based on requested metrics
  const fields = [StockField.NAME, StockField.PRICE];

  if (metrics.includes('valuation')) {
    fields.push(
      StockField.PRICE_TO_EARNINGS_RATIO_TTM,
      StockField.PRICE_TO_BOOK_MRQ,
      StockField.PRICE_SALES_CURRENT
    );
  }

  if (metrics.includes('growth')) {
    fields.push(
      StockField.REVENUE_TTM_YOY_GROWTH,
      StockField.EARNINGS_PER_SHARE_DILUTED_TTM
    );
  }

  if (metrics.includes('profitability')) {
    fields.push(
      StockField.NET_INCOME_TTM,
      StockField.REVENUE_TTM
    );
  }

  if (metrics.includes('technical')) {
    fields.push(
      StockField.RSI,
      StockField.ATR
    );
  }

  screener.select(...fields);

  const results = await screener.get();

  return {
    portfolio: results.data,
    summary: {
      totalStocks: results.data.length,
      metrics: metrics,
    },
  };
}

Register Tool

typescript
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    portfolioAnalyzerTool,
    // ... other tools
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === 'analyze_portfolio') {
    const result = await analyzePortfolio(args.symbols, args.metrics || []);

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(result, null, 2),
        },
      ],
    };
  }

  // ... other tools
});

Example: Sector Comparison

Tool Definition

typescript
const sectorComparisonTool: MCPTool = {
  name: 'compare_sectors',
  description: 'Compare performance across sectors',
  inputSchema: {
    type: 'object',
    properties: {
      sectors: {
        type: 'array',
        items: { type: 'string' },
        description: 'Sectors to compare',
      },
      metric: {
        type: 'string',
        enum: ['price_change', 'volume', 'market_cap', 'pe_ratio'],
        description: 'Metric to compare',
      },
      period: {
        type: 'string',
        enum: ['1D', '1W', '1M', '3M', '1Y'],
        description: 'Time period',
      },
    },
    required: ['sectors', 'metric'],
  },
};

Tool Implementation

typescript
async function compareSectors(
  sectors: string[],
  metric: string,
  period: string = '1D'
): Promise<any> {
  const results = [];

  for (const sector of sectors) {
    const screener = new StockScreener();

    // Note: SECTOR field filtering requires additional implementation
    // This example shows metric selection only

    // Select appropriate field based on metric
    switch (metric) {
      case 'price_change':
        screener.select(StockField.CHANGE_PERCENT);
        break;
      case 'volume':
        screener.select(StockField.VOLUME);
        break;
      case 'market_cap':
        screener.select(StockField.MARKET_CAPITALIZATION);
        break;
      case 'pe_ratio':
        screener.select(StockField.PRICE_TO_EARNINGS_RATIO_TTM);
        break;
    }

    const data = await screener.get();

    // Calculate sector average
    const values = data.data.map(row => row[metric]);
    const average = values.reduce((a, b) => a + b, 0) / values.length;

    results.push({
      sector,
      average,
      count: data.totalCount,
    });
  }

  // Sort by average
  results.sort((a, b) => b.average - a.average);

  return {
    comparison: results,
    metric,
    period,
  };
}

Example: Alert System

Tool Definition

typescript
const alertTool: MCPTool = {
  name: 'create_alert',
  description: 'Create a price or indicator alert',
  inputSchema: {
    type: 'object',
    properties: {
      symbol: {
        type: 'string',
        description: 'Stock symbol',
      },
      condition: {
        type: 'object',
        properties: {
          field: { type: 'string' },
          operator: { type: 'string' },
          value: { type: 'number' },
        },
        description: 'Alert condition',
      },
      notification: {
        type: 'object',
        properties: {
          type: {
            type: 'string',
            enum: ['email', 'webhook', 'console'],
          },
          target: { type: 'string' },
        },
      },
    },
    required: ['symbol', 'condition'],
  },
};

Tool Implementation

typescript
interface Alert {
  id: string;
  symbol: string;
  condition: {
    field: string;
    operator: string;
    value: number;
  };
  notification: {
    type: string;
    target: string;
  };
  active: boolean;
}

const alerts: Map<string, Alert> = new Map();

async function createAlert(
  symbol: string,
  condition: any,
  notification: any
): Promise<string> {
  const alertId = `alert_${Date.now()}_${Math.random().toString(36).slice(2)}`;

  const alert: Alert = {
    id: alertId,
    symbol,
    condition,
    notification: notification || { type: 'console', target: '' },
    active: true,
  };

  alerts.set(alertId, alert);

  // Start monitoring (in background)
  monitorAlert(alert);

  return alertId;
}

async function monitorAlert(alert: Alert): Promise<void> {
  const screener = new StockScreener();

  screener.where(StockField.NAME.eq(alert.symbol));
  screener.select(StockField[alert.condition.field]);

  for await (const data of screener.stream({ interval: 60000 })) {
    if (!alert.active || !data) continue;

    const row = data.data[0];
    if (!row) continue;

    const value = row[alert.condition.field];
    const triggered = checkCondition(
      value,
      alert.condition.operator,
      alert.condition.value
    );

    if (triggered) {
      await sendNotification(alert);
      alert.active = false; // Trigger once
      break;
    }
  }
}

function checkCondition(
  value: number,
  operator: string,
  target: number
): boolean {
  switch (operator) {
    case 'greater':
      return value > target;
    case 'less':
      return value < target;
    case 'equal':
      return value === target;
    default:
      return false;
  }
}

async function sendNotification(alert: Alert): Promise<void> {
  const message = `Alert triggered for ${alert.symbol}: ${alert.condition.field} ${alert.condition.operator} ${alert.condition.value}`;

  switch (alert.notification.type) {
    case 'email':
      // Send email
      console.log('Email:', message);
      break;
    case 'webhook':
      // Call webhook
      await fetch(alert.notification.target, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message, alert }),
      });
      break;
    case 'console':
    default:
      console.log(message);
      break;
  }
}

Example: Backtesting Tool

Tool Definition

typescript
const backtestTool: MCPTool = {
  name: 'backtest_strategy',
  description: 'Backtest a trading strategy',
  inputSchema: {
    type: 'object',
    properties: {
      strategy: {
        type: 'object',
        properties: {
          entry: {
            type: 'object',
            description: 'Entry conditions',
          },
          exit: {
            type: 'object',
            description: 'Exit conditions',
          },
          universe: {
            type: 'object',
            description: 'Stock universe filters',
          },
        },
      },
      period: {
        type: 'object',
        properties: {
          start: { type: 'string' },
          end: { type: 'string' },
        },
      },
      capital: {
        type: 'number',
        description: 'Starting capital',
      },
    },
    required: ['strategy'],
  },
};

Tool Implementation

typescript
async function backtestStrategy(
  strategy: any,
  period: any,
  capital: number = 100000
): Promise<any> {
  const screener = new StockScreener();

  // Apply universe filters
  for (const [field, condition] of Object.entries(strategy.universe || {})) {
    const fieldObj = StockField[field];
    const { operator, value } = condition as any;

    switch (operator) {
      case 'greater':
        screener.where(fieldObj.gt(value));
        break;
      case 'less':
        screener.where(fieldObj.lt(value));
        break;
      // ... other operators
    }
  }

  // Get candidate stocks
  const candidates = await screener.get();

  // Simulate trades
  const trades = [];
  let currentCapital = capital;

  for (const stock of candidates.data) {
    // Check entry conditions
    const shouldEnter = evaluateConditions(stock, strategy.entry);

    if (shouldEnter) {
      // Simulate entry
      const entryPrice = stock.close;
      const shares = Math.floor(currentCapital * 0.1 / entryPrice); // 10% position

      // Check exit conditions (simplified)
      const exitPrice = entryPrice * 1.05; // 5% profit target
      const profit = (exitPrice - entryPrice) * shares;

      trades.push({
        symbol: stock.symbol,
        entry: entryPrice,
        exit: exitPrice,
        shares,
        profit,
      });

      currentCapital += profit;
    }
  }

  const totalProfit = currentCapital - capital;
  const returnPercent = (totalProfit / capital) * 100;

  return {
    performance: {
      startingCapital: capital,
      endingCapital: currentCapital,
      totalProfit,
      returnPercent,
    },
    trades: trades.length,
    winners: trades.filter(t => t.profit > 0).length,
    losers: trades.filter(t => t.profit < 0).length,
    tradeHistory: trades,
  };
}

function evaluateConditions(stock: any, conditions: any): boolean {
  for (const [field, condition] of Object.entries(conditions)) {
    const value = stock[field];
    const { operator, target } = condition as any;

    if (!checkCondition(value, operator, target)) {
      return false;
    }
  }

  return true;
}

Example: Correlation Analyzer

Tool Definition

typescript
const correlationTool: MCPTool = {
  name: 'analyze_correlation',
  description: 'Analyze correlation between stocks or sectors',
  inputSchema: {
    type: 'object',
    properties: {
      symbols: {
        type: 'array',
        items: { type: 'string' },
        description: 'Symbols to analyze',
      },
      period: {
        type: 'string',
        enum: ['1W', '1M', '3M', '1Y'],
        description: 'Time period',
      },
    },
    required: ['symbols'],
  },
};

Tool Implementation

typescript
async function analyzeCorrelation(
  symbols: string[],
  period: string = '1M'
): Promise<any> {
  const priceData: Record<string, number[]> = {};

  // Fetch price data for each symbol
  for (const symbol of symbols) {
    const screener = new StockScreener();

    screener.where(StockField.NAME.eq(symbol));
    screener.select(StockField.PRICE, StockField.CHANGE_PERCENT);

    const results = await screener.get();
    // Note: Historical price data requires additional API implementation
    priceData[symbol] = results.data.map(row => row.price || 0);
  }

  // Calculate correlation matrix
  const correlations: Record<string, Record<string, number>> = {};

  for (let i = 0; i < symbols.length; i++) {
    for (let j = i + 1; j < symbols.length; j++) {
      const symbol1 = symbols[i];
      const symbol2 = symbols[j];

      const correlation = calculateCorrelation(
        priceData[symbol1],
        priceData[symbol2]
      );

      if (!correlations[symbol1]) correlations[symbol1] = {};
      if (!correlations[symbol2]) correlations[symbol2] = {};

      correlations[symbol1][symbol2] = correlation;
      correlations[symbol2][symbol1] = correlation;
    }
  }

  return {
    correlations,
    period,
    symbols,
  };
}

function calculateCorrelation(x: number[], y: number[]): number {
  const n = Math.min(x.length, y.length);

  const meanX = x.reduce((a, b) => a + b, 0) / n;
  const meanY = y.reduce((a, b) => a + b, 0) / n;

  let numerator = 0;
  let sumXSquared = 0;
  let sumYSquared = 0;

  for (let i = 0; i < n; i++) {
    const xDiff = x[i] - meanX;
    const yDiff = y[i] - meanY;

    numerator += xDiff * yDiff;
    sumXSquared += xDiff * xDiff;
    sumYSquared += yDiff * yDiff;
  }

  const denominator = Math.sqrt(sumXSquared * sumYSquared);

  return denominator === 0 ? 0 : numerator / denominator;
}

Tool Testing

Unit Tests

typescript
import { describe, it, expect, vi } from 'vitest';

describe('Custom Tools', () => {
  it('should analyze portfolio', async () => {
    const symbols = ['AAPL', 'GOOGL', 'MSFT'];
    const metrics = ['valuation', 'growth'];

    const result = await analyzePortfolio(symbols, metrics);

    expect(result.portfolio).toHaveLength(3);
    expect(result.summary.totalStocks).toBe(3);
  });

  it('should compare sectors', async () => {
    const sectors = ['Technology', 'Healthcare'];
    const metric = 'market_cap';

    const result = await compareSectors(sectors, metric);

    expect(result.comparison).toHaveLength(2);
    expect(result.metric).toBe('market_cap');
  });
});

Integration Tests

typescript
describe('MCP Tool Integration', () => {
  it('should handle tool call', async () => {
    const request = {
      params: {
        name: 'analyze_portfolio',
        arguments: {
          symbols: ['AAPL'],
          metrics: ['valuation'],
        },
      },
    };

    const response = await handleToolCall(
      request.params.name,
      request.params.arguments
    );

    expect(response.content[0].type).toBe('text');
    expect(JSON.parse(response.content[0].text)).toHaveProperty('portfolio');
  });
});

Best Practices

  1. Validate Inputs: Always validate tool arguments
  2. Error Handling: Provide clear error messages
  3. Documentation: Document tool behavior and parameters
  4. Performance: Cache results when appropriate
  5. Testing: Write comprehensive tests
  6. Security: Validate and sanitize user inputs

Next Steps

Released under the MIT License.