Skip to main content
Glama

show_portfolio

Displays current stock holdings with P&L, optional risk metrics, and FX return decomposition for non-USD reporting.

Instructions

Current holdings derived from the user's transaction log: ticker, shares, avg cost, current price, market value, P&L (absolute and %), plus an optional risk summary (Sharpe, volatility, drawdown) from snapshot history and FX return decomposition when a non-USD display currency is requested. For the daily check-in, prefer get_brief which already includes this data plus weights, news, earnings, and macro context in one call.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
currencyNoDisplay currency for FX return decomposition (e.g. KRW, JPY). When provided and not USD, returns fx_decomposition per holding showing how much of P&L came from price vs exchange rate movement.

Implementation Reference

  • The handler function that executes show_portfolio logic. It reads all transactions and prices from the DB, aggregates holdings (ticker, shares, avg price, cost basis, market value, P&L), optionally builds a risk summary (Sharpe, Sortino, volatility, drawdown) via buildRiskMetrics, and optionally computes FX return decomposition when a non-USD currency is requested.
    async ({ currency }) => {
      const repo = getRepository();
      const db = getDb();
      const txns = db.select().from(transactions).all();
      const priceMap = new Map(
        db
          .select()
          .from(prices)
          .all()
          .map((p) => [p.ticker, p]),
      );
    
      const holdingsMap = aggregateHoldings(txns);
      const holdings = [...holdingsMap.entries()].map(([ticker, h]) => {
        const p = priceMap.get(ticker);
        const avgPrice = h.costShares > 0 ? h.totalCost / h.costShares : null;
        const costBasis = avgPrice != null ? avgPrice * h.costShares : 0;
        const marketValue = p ? p.current_price * h.shares : null;
        return {
          ticker,
          shares: h.shares,
          avgPrice,
          costBasis,
          currentPrice: p?.current_price ?? null,
          marketValue,
          pnl: marketValue != null ? marketValue - costBasis : null,
          pnlPct:
            marketValue != null && costBasis > 0
              ? ((marketValue - costBasis) / costBasis) * 100
              : null,
          name: p?.name ?? null,
          syncedAt: p?.synced_at ?? null,
        };
      });
    
      let riskSummary: {
        sharpe: number;
        sortino: number | null;
        annualized_return_pct: number;
        annualized_vol_pct: number;
        max_drawdown_pct: number;
        trading_days: number;
        risk_free_rate_pct: number;
        note: string;
      } | null = null;
    
      const riskResult = await buildRiskMetrics(repo, null, { fred_key: getFredKey() });
      if (riskResult.ok) {
        const m = riskResult.metrics;
        riskSummary = {
          sharpe: m.ratios.sharpe,
          sortino: m.ratios.sortino,
          annualized_return_pct: m.returns.annualized_pct,
          annualized_vol_pct: m.volatility.annualized_pct,
          max_drawdown_pct: m.drawdown.max_pct,
          trading_days: m.period.trading_days,
          risk_free_rate_pct: m.ratios.risk_free_rate_pct,
          note: sharpeNoteText(m.ratios.sharpe),
        };
      }
    
      type FxRow = { ticker: string; price_return_pct: number; fx_return_pct: number; total_return_pct: number };
      let fxDecomposition: FxRow[] | null = null;
      const cur = currency?.toUpperCase();
      if (cur && cur !== 'USD') {
        const fxRows: FxRow[] = [];
        const cachedFxRates = db.select().from(fxRates).all();
    
        const getLatestFxPerUsd = (c: string): number | null => {
          const entries = cachedFxRates
            .filter((r) => r.currency === c)
            .sort((a, b) => b.date.localeCompare(a.date));
          return entries[0]?.rate_to_usd ?? null;
        };
    
        const curFxPerUsd = getLatestFxPerUsd(cur);
        if (curFxPerUsd != null) {
          for (const [ticker, h] of holdingsMap.entries()) {
            const p = priceMap.get(ticker);
            if (!p) continue;
            const avgCostUsd = h.costShares > 0 ? h.totalCost / h.costShares : null;
            if (!avgCostUsd) continue;
    
            const buyTxns = repo.transactions.getAll(ticker).filter((t) => t.type === 'buy');
            if (buyTxns.length === 0) continue;
    
            let totalNotional = 0;
            let weightedFxSum = 0;
            for (const txn of buyTxns) {
              const fxRecord = repo.fx.getRateOnOrBefore(txn.date, cur);
              if (!fxRecord) continue;
              const notional = txn.shares * txn.price;
              totalNotional += notional;
              weightedFxSum += notional * fxRecord.rate_to_usd;
            }
            if (totalNotional === 0) continue;
    
            const avgBuyFxPerUsd = weightedFxSum / totalNotional;
            const priceRetPct = (p.current_price / avgCostUsd - 1) * 100;
            const fxRetPct = (curFxPerUsd / avgBuyFxPerUsd - 1) * 100;
            const totalRetPct = ((1 + priceRetPct / 100) * (1 + fxRetPct / 100) - 1) * 100;
    
            fxRows.push({
              ticker,
              price_return_pct: Math.round(priceRetPct * 100) / 100,
              fx_return_pct: Math.round(fxRetPct * 100) / 100,
              total_return_pct: Math.round(totalRetPct * 100) / 100,
            });
          }
          if (fxRows.length > 0) fxDecomposition = fxRows;
        }
      }
    
      return ok({
        holdings,
        risk_summary: riskSummary,
        fx_decomposition: fxDecomposition
          ? { rows: fxDecomposition, note: fxDecompNoteText(cur!) }
          : null,
      });
    },
  • Input schema for show_porttool: optional 'currency' string parameter (Zod schema) for FX return decomposition (e.g. KRW, JPY). When provided and not USD, returns fx_decomposition showing price vs exchange rate return.
    {
      currency: z
        .string()
        .optional()
        .describe(
          'Display currency for FX return decomposition (e.g. KRW, JPY). When provided and not USD, returns fx_decomposition per holding showing how much of P&L came from price vs exchange rate movement.',
        ),
    },
  • The registration function registerPortfolioTools calls server.tool('show_portfolio', ...) to register the tool on the MCP server. Called from apps/mcp/src/index.ts line 19.
    export function registerPortfolioTools(server: McpServer): void {
      server.tool(
        'show_portfolio',
  • Where registerPortfolioTools is called to register the show_portfolio (and other portfolio) tools on the MCP server instance.
    registerPortfolioTools(server);
  • Imports used by the show_portfolio handler: aggregateHoldings, getDb, getRepository, getFinnhubKey, getFredKey, buildRiskMetrics, fxDecompNoteText, sharpeNoteText, and the 'ok' response helper.
    import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
    import { assembleBriefData } from '@firma/brief';
    import { buildRiskMetrics, ensureTodaySnapshot } from '@firma/portfolio';
    import { fxDecompNoteText, localDate, sharpeNoteText } from '@firma/utils';
    import { asc, eq } from 'drizzle-orm';
    import { z } from 'zod';
    import {
      aggregateHoldings,
      balanceEntries,
      flowEntries,
      fxRates,
      getDb,
      getFinnhubKey,
      getFredKey,
      getRepository,
      portfolioSnapshots,
      prices,
      recordBriefCallAndCheckCta,
      transactions,
    } from '../db.ts';
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, but description thoroughly explains what data is included and optional features (risk summary, FX decomposition). Lacks explicit statement of being read-only, but that is implied by 'show' and description of derived data.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, front-loaded with core data, followed by usage guidance. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema, but description explains return values comprehensively. Covers optional risk summary and FX decomposition, making it complete for a read tool with one optional parameter.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description covers 100% of parameters. Description adds context beyond schema, explaining that currency triggers FX return decomposition and what that includes.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states it shows current holdings from transaction log with specific fields listed (ticker, shares, avg cost, etc.). Distinguishes from sibling get_brief by noting that get_brief includes additional data.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly recommends using get_brief for daily check-in, providing context on when to use this tool vs the alternative. Also implies not for risk-only or transaction history.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

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/evan-moon/firma'

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