finnhub-mcp-server
by freddyhaddad
README.md
# finnhub-mcp-server
A small, production-shaped **remote MCP server** that lets Claude.ai query live stock-market data from the
[Finnhub](https://finnhub.io) REST API.
It is the reference pattern for "connect a third-party REST API to Claude":
```
Claude.ai ──MCP over HTTPS──▶ finnhub-mcp-server ──REST + X-Finnhub-Token header──▶ Finnhub
```
- **Upstream auth**: Finnhub API key sent in a request header. The key lives only in a server-side environment variable.
- **Downstream auth**: Claude must present a bearer token. Constant-time comparison. Refuses to boot without one unless you opt out explicitly.
- **Transport**: MCP Streamable HTTP, stateless. Works with Claude.ai custom connectors, Claude Desktop, Claude Code, and any MCP client.
- **Hosting**: one-click on Railway or Render. Health endpoint included.
- **Tests**: 18 end-to-end tests run a real MCP client against the server with a mocked Finnhub. No key or network needed.
## Tools
| Tool | What it does |
|---|---|
| `get_quote` | Current price, change, open, high, low, previous close for a ticker |
| `get_company_profile` | Name, exchange, country, currency, industry, market cap, IPO date, website |
| `search_symbol` | Find tickers by company name or partial symbol, optionally per exchange |
| `get_company_news` | Recent articles for a ticker, default last 7 days, capped and trimmed for the model |
| `get_market_news` | General, forex, crypto, or merger headlines |
Every tool is declared read-only and returns both human-readable text and `structuredContent` JSON.
## Quick start (local)
```bash
git clone https://github.com/freddyhaddad/finnhub-mcp-server.git
cd finnhub-mcp-server
npm install
cp .env.example .env
# edit .env: paste your Finnhub key, and generate a token with: openssl rand -hex 32
npm run dev
```
Then in another terminal:
```bash
npm run smoke # lists tools and calls each one for AAPL
npm run smoke -- NVDA # any ticker
```
Requires Node 22.6 or newer (runs TypeScript directly, no build step for dev).
## Run the tests
```bash
npm test
```
The suite spins up a fake Finnhub, starts the server, and drives it with the official MCP client. It checks
that the API key travels in the `X-Finnhub-Token` header, that requests without a bearer token get `401`,
that bad input never reaches Finnhub, and that upstream failures come back as clean tool errors without
leaking the key.
## Deploy
### Railway
1. Fork or push this repo to GitHub.
2. In Railway: **New Project → Deploy from GitHub repo**. `railway.json` sets the start command and health check.
3. Under **Variables** add `FINNHUB_API_KEY` and `MCP_AUTH_TOKEN`. Railway sets `PORT` for you.
4. Under **Settings → Networking**, generate a public domain. Your MCP URL is `https://<domain>/mcp`.
### Render
1. **New → Blueprint**, point it at the repo. `render.yaml` defines the service and auto-generates `MCP_AUTH_TOKEN`.
2. Add `FINNHUB_API_KEY` when prompted.
3. Copy the generated token from the service's environment page. Your MCP URL is `https://<service>.onrender.com/mcp`.
Optional hardening: set `ALLOWED_HOSTS` to your public hostname so the server rejects requests with any other
`Host` header (DNS-rebinding protection). If you put a firewall in front, Anthropic's egress range is
`160.79.104.0/21`.
## Connect it to Claude
### Claude.ai (web, desktop, mobile)
Settings → Connectors → **Add custom connector**. Paste the MCP URL.
For the bearer token, use the **request headers** option and add `Authorization: Bearer <your MCP_AUTH_TOKEN>`.
Static request-header auth on custom connectors is in beta and is configured by an organization admin. If
your account does not show the option yet, two fallbacks:
- **Claude Desktop or Claude Code** send headers today (see below).
- Run with `ALLOW_UNAUTHENTICATED=true` behind an unguessable URL. Not recommended for anything beyond a demo,
since anyone with the URL can spend your Finnhub quota. Adding OAuth is the proper next step and is a
natural Phase 2.
### Claude Code
```bash
claude mcp add --transport http finnhub https://<domain>/mcp \
--header "Authorization: Bearer <your MCP_AUTH_TOKEN>"
```
Then ask: *"What's NVDA trading at, and what's the latest news on it?"*
## Configuration
| Variable | Required | Default | Purpose |
|---|---|---|---|
| `FINNHUB_API_KEY` | yes | | Finnhub key, sent as `X-Finnhub-Token` |
| `MCP_AUTH_TOKEN` | yes* | | Bearer token Claude must send. Min 16 chars |
| `PORT` | no | `3000` | Listen port. Hosts inject this |
| `ALLOWED_HOSTS` | no | | Comma-separated hostnames to accept |
| `RATE_LIMIT_PER_MINUTE` | no | `60` | Per-IP limit on `/mcp` |
| `UPSTREAM_TIMEOUT_MS` | no | `10000` | Finnhub request timeout |
| `FINNHUB_BASE_URL` | no | `https://finnhub.io/api/v1` | Override for tests or proxies |
| `ALLOW_UNAUTHENTICATED` | no | `false` | *Set to `true` to run without `MCP_AUTH_TOKEN` |
## Security notes
- The Finnhub key never appears in tool output, error messages, or logs.
- Tool inputs are validated with Zod before any upstream call. Tickers are restricted to `[A-Z0-9.\-:]`, dates to `YYYY-MM-DD`, list sizes to 1–20.
- User input only ever reaches Finnhub through URL-encoded query parameters. It can never change which endpoint is called.
- Upstream `401`/`403`/`429`/`5xx` are mapped to short, safe messages. Upstream bodies are never forwarded on failure.
- Request bodies are capped at 100 KB. Responses are trimmed (news summaries to 300 chars, lists to 20 items) so Claude's context stays small.
- `GET` and `DELETE` on `/mcp` return `405`. The server is stateless: nothing is stored between requests.
- Rotate either secret by changing the environment variable and redeploying. Revoke Claude's access by deleting the connector.
## Adapting this to another API
The whole point of the pattern is that swapping Finnhub for any other key-authenticated REST API is mechanical:
1. Replace `src/finnhub.ts` with a client for the new API. Keep the header-auth and error-mapping shape.
2. Replace the tool definitions in `src/server.ts`. One `registerTool` per endpoint.
3. Update `test/mock-finnhub.ts` fixtures and the assertions in `test/e2e.test.ts`.
`src/app.ts`, `src/auth.ts`, and `src/config.ts` do not need to change.
## Project layout
```
src/
index.ts entrypoint: load config, listen
app.ts Express app: health, rate limit, bearer auth, /mcp handler
auth.ts bearer-token and host-allowlist middleware
config.ts environment parsing and startup guards
finnhub.ts Finnhub REST client (header auth, timeouts, sanitised errors)
server.ts MCP tool definitions
test/
mock-finnhub.ts fake upstream used by the tests
e2e.test.ts end-to-end suite using the official MCP client
scripts/
smoke.ts manual check against a running server
railway.json, render.yaml one-click deploy configs
```
## License
MIT. Built by [Frederic Haddad](https://frederic.ai).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues