finance-mcp-server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@finance-mcp-serverwhoami"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Server
A containerized Model Context Protocol (MCP) server with a management dashboard.
Users sign in with Google, manage their own access tokens and OAuth 2.1 clients;
admins manage users. The MCP endpoint (Streamable HTTP at /mcp) accepts both
personal access tokens and OAuth 2.1 access tokens issued by the built-in
authorization server.
Stack: Hono · Vite · React 19 · React Router · TanStack Query · Tailwind CSS 4 · Drizzle ORM · PostgreSQL · TypeScript. One process serves everything — in dev, Hono runs inside the Vite dev server; in production, Hono serves the built SPA.
Features
Google sign-in only — no passwords. Access is invitation-only: admins invite users by email;
ADMIN_EMAILSseeds admin accounts at boot.Personal access tokens — optional expiry, enable/disable, revoke/delete, copy-once display, last-access tracking. Only SHA-256 hashes are stored.
OAuth 2.1 authorization server — PKCE (S256) required, dynamic client registration (RFC 7591), discovery metadata (RFC 8414/9728), refresh token rotation with reuse detection, revocation (RFC 7009), consent screen.
Finance MCP endpoint — Streamable HTTP at
/mcpwith 10 read-only Yahoo Finance tools pluswhoami; requests use the same PAT/OAuth 2.1 authorization layer as the rest of the MCP service.Cross-market fund relationship layer — 6 further tools that answer which fund gives me exposure to this stock, sector or theme, using an offline-ingested index of disclosed fund holdings rather than keyword search over fund names. China public funds and US ETFs share one index, so asking who holds NVDA returns both, tagged with the market each fund trades in.
Client integration center — in-dashboard, copy-ready setup guides for Claude, Claude Code, Codex, Cursor, Antigravity 2 and generic MCP clients, with both OAuth and personal-token instructions.
Admin — user management (invite/enable/disable/role), all registered OAuth clients, full audit log.
Built-in chat assistant — streaming chat with the latest Anthropic (Claude Opus 4.8 / Sonnet 5 / Haiku 4.5), OpenAI (GPT-5.6 family), and Google Gemini (3.1 Pro / 3.5 Flash / 3.5 Flash-Lite / 3.6 Flash) models. API keys come from the environment (
ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY— any subset); conversations are persisted per user with a model picker per conversation.URL-driven UI — search, filters, pagination, sorting and tabs all live in URL search params, so every view is shareable and back/forward-safe.
Related MCP server: MCP Authentication Demo
Quick start (development)
Requirements: Node 22+, PostgreSQL.
cp .env.example .env # fill in DATABASE_URL, SESSION_SECRET, Google credentials, ADMIN_EMAILS
npm install
npm run dev # one process: http://localhost:5173 (follows PORT)Migrations run automatically at boot, and every ADMIN_EMAILS entry is seeded
as an active admin. Sign in with the matching Google account.
Google OAuth setup
Create an OAuth client (type "Web application") at
Google Cloud Console → Credentials
and add the redirect URI {APP_URL}/auth/google/callback
(e.g. http://localhost:5173/auth/google/callback for dev). Dev and production
share one port, so a single APP_URL and one registered redirect URI cover both.
Deployment (Docker)
cp .env.example .env # set SESSION_SECRET, Google credentials, ADMIN_EMAILS, APP_URL
docker compose up -d # app + PostgreSQL 17 with a persistent volumeThe image is a multi-stage build (node:24-alpine, non-root, healthcheck on
/healthz). The server waits for Postgres and applies migrations before
listening. Set APP_URL to the public HTTPS URL — it is the OAuth issuer.
Connecting an MCP client
The MCP endpoint is {APP_URL}/mcp (Streamable HTTP).
After signing in, open /integrations for client-specific commands,
configuration files, authentication steps and troubleshooting.
OAuth 2.1 (recommended): point the client at the URL; it discovers the authorization server via
/.well-known/oauth-protected-resource, registers itself, and sends you to the consent screen. Manage/revoke access on the OAuth Clients page.Personal access token: create one on the Access Tokens page and send it as
Authorization: Bearer mcp_….
Available MCP tools
Tool | Purpose |
| Find symbols, companies and related news |
| Current or near-current quotes for up to 50 symbols |
| Selected company, price, ownership, filing and fund modules |
| Historical/intraday OHLCV data and market events |
| Predefined screens such as gainers, losers and most active |
| Region-specific trending instruments |
| Option expiration dates and call/put chains |
| Analyst research, developments and technical outlooks |
| Related and similar instruments |
| Financial statement data over time |
| Beats/misses, estimate revisions and the next report date, in one call |
earningsAnalysis stitches together four quoteSummary modules and does the
arithmetic, so answering "did they beat, and are estimates moving up or down"
takes one call instead of four plus manual work. It reports estimate
revisions, not just a current snapshot — a single estimate says nothing about
direction.
Yahoo covers CN and HK listings through symbol suffixes (600519.SS,
0700.HK), so the tools above already span the A-share, Hong Kong and US
markets. What Yahoo does not carry is the inside of a fund: it publishes a
top-ten list for US ETFs and nothing at all for China's domestic public funds,
and it has no way to ask the question backwards. That is what the tools below
are for.
SEC EDGAR tools
For US issuers, EDGAR is the source of record that Yahoo mirrors — late, partially, and without provenance. These tools read it directly.
Tool | Purpose |
| An issuer's filing index, newest first, with direct document URLs |
| As-reported XBRL financials as a time series, with the filing behind every value |
Reach for these over the Yahoo statement tools when you need as-reported rather
than vendor-normalised figures, restatement-accurate history, or a citable
filing URL. secFinancials deduplicates each period to its latest restatement
and carries the us-gaap concept and accession number for every value.
Coverage is SEC registrants only: non-US listings and most ADRs have no CIK, and
filers predating XBRL have no company facts. Both tools say so explicitly rather
than returning an empty result. EDGAR requires a contact address in the
User-Agent on every request — set SEC_EDGAR_CONTACT_EMAIL.
Fund relationship tools
Tool | Purpose |
| Break a fund into sector/market exposure, with coverage and holdings stability |
| Reverse index — which funds hold a given stock, ranked by weight |
| Funds ranked by measured exposure to a sector or theme |
| Funds ranked by weight in positions matching symbol / sector / country / market-cap criteria |
| Substitutes for a fund, by cosine similarity of exposure vectors |
| Theme → tracking index / sector exposure / market exposure, in one call |
| Fees, size, top sectors and pairwise portfolio overlap for 2-10 funds |
| Cumulative/annualized return, max drawdown and volatility from NAV history |
Query-shape tests. The repo's SQL typechecks whatever it emits, and the tool tests mock the repo away, so a statement Postgres rejects at parse time can pass the whole suite — one did, and
fundsByStockfailed on every call in production while CI was green.repo-sql.test.tspins the shape offline;npm run test:db(withDATABASE_URLset) executes every repo query with every filter combination against a real database, inside a transaction that is always rolled back. It is skipped whenDATABASE_URLis unset.
fundsByHoldings is the tool that federated data makes possible: it goes back
to the raw positions and joins them against the enriched instrument table, so
criteria that were never precomputed — company size, country, an explicit basket
of symbols — can be combined in one query. Rank its results by
shareOfDisclosedPercent rather than matchedWeightPercent; the latter is a
share of net assets and is structurally smaller for a fund that discloses only
its largest positions.
Two numbers accompany every holdings-derived answer, and both matter:
coverage — the share of a fund's disclosed weight that could be classified. A 60% sector weight at 0.3 coverage is a much weaker claim than the same weight at 0.95.
holdings stability — how much of the previous report the fund still holds. Index funds sit near 1.0; a low score means last quarter's portfolio no longer describes the fund and its inferred exposure should be discounted.
themeToFunds reports index-tracking matches separately from holdings-derived
ones because a declared mandate does not drift, while a holdings snapshot can.
The mandate itself (跟踪标的) comes from the fund profile page and is only
populated from that field — never from 业绩比较基准, which for an active fund
is a blended benchmark and would make every active fund look like an index fund.
fundPerformance measures from cumulative NAV (累计净值) whenever the series
has it, so distributions are not read as losses — over a multi-year horizon that
difference compounds into a materially wrong number. The response states which
basis was used.
Watchlist tools
The only tools that write. They read and edit the authenticated user's own saved lists — the same rows the Watchlists page shows, so an assistant and the dashboard are never looking at different things.
Tool | Purpose |
| The user's lists, with item counts — the cheap "what exists" call |
| One list, priced: live quotes, fund NAV, notes, targets and a breadth summary |
| Add instruments or funds, with a note saying why |
| Drop items by symbol or fund code |
A list holds both kinds of thing at once: Yahoo symbols and 6-digit China fund
codes. A bare 6-digit ref is read as a fund code, since Yahoo's CN listings
always carry an exchange suffix (600519.SS), and kind overrides the guess.
Three deliberate limits on what an agent can do:
It cannot delete a list. Removing one item is a small correction; discarding a list throws away accumulated notes and targets. That lives on the dashboard, behind a confirmation.
It will not fork a list from a near-miss name. With lists already in place, an unrecognised name is an error listing the real ones unless
create: trueis passed — otherwise a single typo silently starts a parallel list, and the split only surfaces later. A user with no lists at all gets their first one made automatically; there is nothing to confuse it with.It will not guess between several lists. With more than one and no name given, the call fails and names the candidates.
Items store no prices. Values are fetched per request — Yahoo for instruments,
the local fund cache for funds — and the two are labelled basis: "market" and
basis: "nav" rather than blended: an intraday quote and a once-daily net asset
value are not the same measurement. Anything that could not be priced comes
back with available: false and a reason, so one bad symbol never costs you the
rest of the list. The summary weights every item equally; a watchlist records no
position sizes, so it cannot express a portfolio return.
Fetching a fund on demand
Naming a fund is request enough for its data. Opening an uncached fund in the
dashboard, or calling fundExposure, fundPerformance or similarFunds on
one, fetches it there and then — three requests, a couple of seconds — instead
of returning an empty result and telling you to go run a batch job.
Four things keep that safe on a request path:
One shared client per provider. The throttle is per client instance, so an on-demand fetch holding its own while a category sync held another would quietly double the request rate. Everything fetches through one per upstream.
In-flight de-duplication. Ten agents asking about the same fund at once cause one fetch.
The existing watermarks. A fund synced before is never refetched here — the test is "has this ever synced", not "does the data look useful", so a fund with one NAV point or a thin portfolio does not re-fetch on every call.
A queue ceiling. Past 8 pending fetches, callers are refused with a clear message rather than silently enqueuing an hour of scraping.
Sector classification runs against a deadline rather than to completion, so a
first touch does not wait through a serial walk of every holding. Whatever it
misses shows up as lower coverage on the exposure rows, and the tool response
says so.
Fund data ingest
The relationship tools read local tables only — no tool call ever hits an upstream data source. Populate them from the Funds page in the dashboard, or from the CLI:
npm run build
npm run ingest -- --provider=eastmoney --scope=qdii --limit=200
npm run ingest -- --provider=ishares --scope=all
npm run ingest -- --provider=eastmoney --scope=index --dry-run # counts only
npm run ingest -- --provider=eastmoney --codes=162411,270042
npm run ingest -- --provider=ishares --codes=IVV --skip-universe
npm run ingest -- --provider=eastmoney --scope=qdii --force # ignore freshnessThe job runs six steps: fund universe → per-fund profile detail → holdings → NAV history → instrument enrichment from Yahoo → recomputed exposure. The first four are the provider's; the last two are shared, which is what makes exposure vectors from different markets directly comparable.
Instrument enrichment is the join to everything else. One quoteSummary
call per instrument yields the GICS sector (which covers CN, HK and US listings
under one taxonomy) and the attributes that make a cross-source question
answerable in SQL — ISIN, country of domicile, exchange, market capitalization.
The step is watermark-driven on instruments.profile_synced_at with a 7-day
window, and the watermark is set even when Yahoo returns nothing, so an
uncovered symbol costs one request a week rather than one per run.
Market caps are stored twice: as reported, and converted to USD. The native column cannot be compared across markets — filtering "above 10 billion" over a mixed JPY/USD/CNY column ranks by currency, not by size — so every size filter uses the USD one. Rates come from Yahoo, fetched once per currency per run.
Providers. A provider is one upstream source, declared in
src/shared/funds.ts and implemented in src/server/funds/providers/:
Provider | Universe | Holdings | Cadence |
| China public funds, 6-digit codes | Quarterly report, top holdings only | re-checked weekly |
| iShares US ETFs, listing tickers | Full published portfolio | daily |
Each declares its own scopes and freshness windows, because they age
differently: re-fetching a China quarterly report every day is waste, and
caching an iShares file for a week serves stale positions as current. Adding a
third means implementing FundProvider and adding a descriptor — no change to
the pipeline, the tables, or the tools.
Disclosure conventions differ, and the tools say so. funds.holdings_completeness
records whether a fund's weights cover its whole book (full) or only its
largest positions (top_holdings). Tool responses carry a note derived from it,
and a result mixing both gets an explicit warning — ranking funds by a disclosed
sector weight across conventions measures reporting rules, not portfolios.
Freshness windows. Each fund carries a per-step watermark, and a run skips whatever is still current: profile detail for 30 days, holdings for 7, NAV for
The three differ because they age differently — a fund's profile barely moves, portfolios are disclosed quarterly (re-checked weekly so a new quarter lands within days of publication), and NAV changes every trading day. This is what makes a repeat run cheap;
--forceoverrides it.
Share classes are ingested per fund code. A fund's A/C/现汇/现钞 classes share a portfolio, so fetching holdings once per class is redundant — but deduplicating means inferring the grouping from the fund name, and there is no share-class identifier in the upstream payload. A wrong grouping writes one fund's holdings under another's code, and nothing surfaces it. Since NAV and fees genuinely differ per class and must be fetched per code anyway, the deduplication would only save about 14% of a run's requests. Correct by construction beat it.
Per-fund failures are collected rather than fatal: the summary lists them and
the process exits non-zero, while everything that succeeded is still committed.
The last failure is also written to funds.last_sync_error so the dashboard can
show which funds are failing.
Unparseable holdings are counted, not swallowed. holdingsDropped in the
summary reports rows the parser could not read. A non-zero value means an
upstream format drift — the failure mode that once made Tokyo-coded positions
(285A) vanish from QDII portfolios with no error at all.
Funds dashboard page
/funds shows what is actually cached — fund count, holdings rows, distinct
stocks, latest report date — and lists every fund with its holdings count and
cache age, filterable by category and by cached / not cached / failing. Clicking
a fund opens its stored portfolio.
Admins can start a sync per category. Picking one opens a confirmation showing
how many funds it matches, how many are already fresh, how many will actually be
fetched, the request count and an estimated duration — nothing is fetched until
that is confirmed. A run is tracked in ingest_jobs with live progress and can
be stopped; it is single-flight, since two concurrent runs would double the
request rate against hosts that already throttle.
Syncs run in the server process, so a restart interrupts one; jobs left running are marked failed at boot rather than appearing stuck forever.
Note on upstream formats. The Eastmoney and iShares endpoints are undocumented and their response shapes were implemented from their known structure, not verified against live responses. All shape knowledge is isolated in each provider's
parse.tsas pure functions with fixture-based tests, so a format change is a fixture-plus-parser fix that touches nothing else.
The theme crosswalk in src/server/funds/crosswalk.ts is the one piece that
cannot be scraped — it maps a theme onto Eastmoney board names, Yahoo GICS
sectors and index names, which is what lets a single "半导体" query return both
onshore sector funds and QDII funds holding the same sector offshore. Extend it
by adding entries there.
| whoami | Current MCP user and authorization method |
Market data is provided through the unofficial yahoo-finance2 integration and
may be delayed, unavailable, changed, or removed for delisted symbols. Tool
responses are capped at 1 MB; narrow large requests by symbol count, date range,
or summary modules.
Scripts
Command | Purpose |
| Dev server (SPA + backend, single process) |
| Production build / run |
| Checks (run in CI) |
| End-to-end smoke test against a running dev server |
| Generate a migration after editing |
Architecture
See docs/PLAN.md for the full design and CLAUDE.md
for project conventions (notably: URL search params are the source of truth for
all page-level UI state).
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceMCP server with built-in Google OAuth 2.0 authentication, enabling secure tool access via Google sign-in with automatic token lifecycle management and optional domain restriction for Google Workspace organizations.MIT
- FlicenseNot gradedqualityDmaintenanceA production-ready MCP server with OAuth 2.1 authentication, enabling authenticated MCP tool calls via Google OAuth.
- FlicenseNot gradedqualityBmaintenanceAn MCP server with HTTP/stdio support, a web admin panel for managing services, capabilities, and user permissions with Bearer token authentication, enabling relay and access control for MCP tools.
- FlicenseNot gradedqualityCmaintenanceThis MCP server implements a secure OAuth 2.1 authorization server with Google login, enabling authenticated tool execution and a user interface for MCP applications.
Related MCP Connectors
MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
MCP Hub: AI service discovery, per-user OAuth, and multi-service workflow orchestration
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/ShinChven/finance-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server