Skip to main content
Glama
TylerFlar

claude-fidelity-mcp

by TylerFlar

fidelity_get_positions

Retrieve complete list of positions across all Fidelity accounts. Displays stock ticker, quantity, price, and value for each holding to assess your portfolio.

Instructions

Get all positions (holdings) across all Fidelity accounts. Returns account details with stock ticker, quantity, price, and value for each holding.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • src/index.ts:170-199 (registration)
    Registration of the 'fidelity_get_positions' tool on the MCP server with description, empty schema, and handler calling getPositions().
    // ─── Get Positions ──────────────────────────────────────────────────────────────
    
    server.tool(
      "fidelity_get_positions",
      "Get all positions (holdings) across all Fidelity accounts. Returns account details with stock ticker, quantity, price, and value for each holding.",
      {},
      async () => {
        try {
          const positions = await getPositions();
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(positions, null, 2),
              },
            ],
          };
        } catch (e) {
          return {
            content: [
              {
                type: "text",
                text: `Failed to get positions: ${e instanceof Error ? e.message : String(e)}`,
              },
            ],
            isError: true,
          };
        }
      }
    );
  • Main getPositions() function - tries CSV download first, falls back to page scraping.
    export async function getPositions(): Promise<Account[]> {
      // Try CSV download first, fall back to page scraping
      try {
        const csvResult = await getPositionsViaCsv();
        if (csvResult.length > 0) return csvResult;
      } catch {
        // CSV download failed, try scraping
      }
    
      return await getPositionsViaScrape();
    }
  • getPositionsViaCsv() - Downloads positions CSV from Fidelity by interacting with the UI, parsing the CSV with parsePositionsCsv().
    async function getPositionsViaCsv(): Promise<Account[]> {
      const page = await getPage();
    
      await page.goto(POSITIONS_URL, { waitUntil: "domcontentloaded" });
      await waitForLoadingCompleteDouble(page, 150000);
    
      // Set up download listener BEFORE clicking
      const downloadPromise = page.waitForEvent("download", { timeout: 20000 });
    
      // Open the Available Actions kebab menu, then click Download
      // Strategy 1: Use the known kebab menu item ID
      let clicked = false;
    
      try {
        // Click "Available Actions" button to open menu
        const actionsBtn = page.locator(
          "button[aria-label='Available Actions'], button:has-text('Available Actions')"
        );
        await actionsBtn.first().click({ timeout: 5000 });
        await page.waitForTimeout(500);
    
        // Click the Download menu item by its ID pattern
        const downloadBtn = page.locator(
          "#kebabmenuitem-download, button:has-text('Download')[id*='kebab']"
        );
        await downloadBtn.first().click({ timeout: 5000 });
        clicked = true;
      } catch {
        // Strategy 2: Try role-based selectors
        try {
          const actionsBtn = page.getByRole("button", { name: "Available Actions" });
          await actionsBtn.click({ timeout: 5000 });
          await page.waitForTimeout(500);
          const downloadItem = page.getByRole("menuitem", { name: "Download" });
          await downloadItem.click({ timeout: 5000 });
          clicked = true;
        } catch {
          // Strategy 3: Try label-based
          try {
            const downloadBtn = page.getByLabel("Download Positions");
            await downloadBtn.click({ timeout: 5000 });
            clicked = true;
          } catch {
            // Strategy 4: Any element with download text
            try {
              const btn = page.locator("[id*='download'], [aria-label*='ownload']");
              await btn.first().click({ timeout: 5000 });
              clicked = true;
            } catch {
              throw new Error("Could not find download button.");
            }
          }
        }
      }
    
      if (!clicked) {
        throw new Error("Failed to click download.");
      }
    
      const download = await downloadPromise;
      const dlPath = await download.path();
    
      if (!dlPath) {
        throw new Error("Downloaded CSV path is null.");
      }
    
      const fs = await import("fs");
      const csvContent = fs.readFileSync(dlPath, "utf-8");
    
      // Delete temp file
      try {
        fs.unlinkSync(dlPath);
      } catch {
        // Cleanup optional
      }
    
      if (!csvContent || csvContent.trim().length === 0) {
        throw new Error("Downloaded CSV is empty.");
      }
    
      return parsePositionsCsv(csvContent);
    }
  • getPositionsViaScrape() - Scrapes positions table directly from the DOM as fallback when CSV download fails.
    async function getPositionsViaScrape(): Promise<Account[]> {
      const page = await getPage();
    
      if (!page.url().includes("positions")) {
        await page.goto(POSITIONS_URL, { waitUntil: "domcontentloaded" });
        await waitForLoadingCompleteDouble(page, 60000);
      }
    
      // Scrape directly from the positions table (Table 0 with 304 rows)
      const data = await page.evaluate(() => {
        const results: {
          accountNumber: string;
          accountName: string;
          symbol: string;
          lastPrice: string;
          lastPriceChange: string;
          currentValue: string;
          quantity: string;
          costBasis: string;
          gainLoss: string;
          gainLossPct: string;
        }[] = [];
    
        // Find all table rows
        const rows = document.querySelectorAll("tr, [role='row']");
        let currentAccount = "";
        let currentAccountName = "";
    
        for (const row of rows) {
          const cells = row.querySelectorAll("td, th, [role='cell'], [role='gridcell']");
          const text = (row.textContent ?? "").trim();
    
          // Account header rows typically have account name and number
          // Look for patterns like "Individual (Z23385543)" or account numbers
          const acctMatch = text.match(/(Z?\d{6,})/);
    
          // Check if this is an account header row (fewer cells, contains account info)
          if (cells.length <= 3 && acctMatch) {
            currentAccount = acctMatch[1];
            // Try to get account name (text before the account number)
            const nameMatch = text.match(/^(.+?)(?:\s*[-–(]?\s*(?:Z?\d{6,}))/);
            currentAccountName = nameMatch ? nameMatch[1].trim() : currentAccount;
            continue;
          }
    
          // Position rows have many cells (symbol, price, quantity, etc.)
          if (cells.length >= 5 && currentAccount) {
            const cellTexts: string[] = [];
            cells.forEach((c) => cellTexts.push((c.textContent ?? "").trim()));
    
            // First cell is usually the symbol
            const symbol = cellTexts[0]?.replace(/\s+/g, " ").split(" ")[0] ?? "";
    
            // Skip non-ticker rows
            if (!symbol || symbol.length > 10 || !/^[A-Z0-9.*]+$/i.test(symbol)) continue;
            if (symbol === "Symbol" || symbol === "Total") continue;
    
            results.push({
              accountNumber: currentAccount,
              accountName: currentAccountName,
              symbol: symbol.toUpperCase(),
              lastPrice: cellTexts[1] ?? "",
              lastPriceChange: cellTexts[2] ?? "",
              currentValue: cellTexts[7] ?? "",
              quantity: cellTexts[9] ?? "",
              costBasis: cellTexts[10] ?? "",
              gainLoss: cellTexts[5] ?? "",
              gainLossPct: cellTexts[6] ?? "",
            });
          }
        }
    
        return results;
      });
    
      const accountMap = new Map<string, Account>();
    
      const cleanNum = (s: string): number => {
        if (!s) return 0;
        const cleaned = s.replace(/[$,%]/g, "").trim();
        if (cleaned === "" || cleaned === "n/a" || cleaned === "--") return 0;
        const n = parseFloat(cleaned);
        return isNaN(n) ? 0 : n;
      };
    
      for (const row of data) {
        if (!accountMap.has(row.accountNumber)) {
          accountMap.set(row.accountNumber, {
            accountNumber: row.accountNumber,
            accountName: row.accountName,
            nickname: row.accountName,
            balance: 0,
            withdrawalBalance: 0,
            stocks: [],
          });
        }
    
        const account = accountMap.get(row.accountNumber)!;
        const ticker = row.symbol;
    
        if (
          ticker === "SPAXX" ||
          ticker === "FCASH" ||
          ticker === "FDRXX" ||
          ticker === "FZFXX"
        ) {
          const val = cleanNum(row.currentValue) || cleanNum(row.quantity);
          account.balance += val;
          account.withdrawalBalance += val;
        }
    
        account.stocks.push({
          ticker,
          description: "",
          quantity: cleanNum(row.quantity),
          lastPrice: cleanNum(row.lastPrice),
          lastPriceChange: cleanNum(row.lastPriceChange),
          currentValue: cleanNum(row.currentValue),
        });
      }
    
      return Array.from(accountMap.values());
    }
  • The Account type definition that getPositions() returns, containing accountNumber, accountName, nickname, balance, withdrawalBalance, and stocks array.
    export interface Account {
      accountNumber: string;
      accountName: string;
      nickname: string;
      balance: number;
      withdrawalBalance: number;
      stocks: Stock[];
    }
Behavior3/5

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

Without annotations, the description carries the full burden. It states the tool returns account details with specific fields, implying a read operation. However, it does not explicitly confirm read-only behavior, mention authentication requirements, or describe error states (e.g., empty accounts). The description is adequate but lacks deeper behavioral context.

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?

The description is two sentences with no wasted words. The first sentence clearly states the action and scope, and the second lists the output details. It is efficiently structured and easy to parse.

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

Completeness4/5

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

Given the tool has no parameters and no output schema, the description provides sufficient information about what is returned (account details, ticker, quantity, price, value). It does not mention limits, sorting, or read-only nature, but the scope is well-defined and likely sufficient for an agent to understand the output.

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

Parameters4/5

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

The tool has no parameters, so the description adds no parameter semantics. Baseline score of 4 is appropriate since no parameters need explanation.

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?

The description clearly states the tool retrieves all positions across all Fidelity accounts and lists the returned fields (ticker, quantity, price, value). It uses a specific verb ('Get') and resource ('positions'), and distinguishes from sibling tools like fidelity_get_accounts by focusing on individual holdings.

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

Usage Guidelines2/5

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

No guidance is provided on when to use this tool versus alternatives such as fidelity_get_accounts or fidelity_get_quote. The description does not mention scenarios where this tool is inappropriate or any preconditions (e.g., authentication).

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/TylerFlar/claude-fidelity-mcp'

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