Back to Insights

How to Build a Financial Data MCP Server

Architecture decisions, tool design patterns, and what we learned shipping an MCP server that delivers pre-analyzed market context to AI agents in production.

Tutorial · Building with Financial Data · Published April 4, 2026
How to Build a Financial Data MCP Server - Architecture, Tools, and Production Lessons

01 Why Build One

Building an MCP server for financial data solves a problem that every AI agent team encounters independently. Each agent that needs market data builds its own pipeline: HTTP clients, schema parsers, error handling, rate limit management, and response formatting. The work is identical across teams, but the implementations diverge.

The Model Context Protocol standardizes this interface. Instead of writing HTTP clients, agents call tools. Instead of parsing arbitrary JSON schemas, agents receive structured responses with consistent fields. The starting point is three lines of setup.

Getting started

npm install @modelcontextprotocol/sdk zod
// server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer(
{ name: 'my-financial-data', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
// Register tools here (see section 03)
const transport = new StdioServerTransport();
await server.connect(transport);

This gives you a running MCP server that any compatible client can connect to. The interesting part is what tools you register, how you structure responses, and how much processing to do before the agent sees the data.

02 Architecture Decisions

Four architectural decisions shape every financial data MCP server before a single line of business logic is written. Getting these wrong early means rewriting the transport layer later.

Decision Option A Option B

Transport

stdio

Local process, zero network. Good for desktop clients like Claude Desktop.

Streamable HTTP

Remote deployment, serverless-compatible. Required for multi-tenant or cloud-hosted servers.

Tool count

Few large tools

1-2 tools that accept complex parameter objects. Less tool selection overhead for the LLM.

Many focused tools

5-10 tools with clear, single responsibilities. Easier for the LLM to select the right one.

Response format

JSON

Machine-parseable, consistent schema. Higher token cost per field due to key names and syntax.

Markdown

Token-efficient, human-readable. Harder to parse programmatically. Dual format (both) is possible.

Auth

API key (Bearer)

Simple. One header. Works everywhere. Sufficient for most server-to-server use cases.

OAuth 2.0

Scoped access, token rotation. Needed for multi-user platforms. Adds setup complexity.

There is no universally correct column. A desktop-only tool for personal use can use stdio and a single monolithic tool. A cloud-hosted API serving thousands of agents needs Streamable HTTP, focused tools, and API key auth. The decisions compound. Choosing HTTP transport means you need session management. Choosing many tools means you need clear naming conventions so the LLM picks the right one.

03 Connecting to an MCP Server

Before building a server, it helps to understand how clients connect to one. Every MCP-capable client uses a configuration file that registers servers by name, URL, and authentication method. The structure is nearly identical across clients.

Remote server (Streamable HTTP)

// .cursor/mcp.json, .claude/settings.json, or equivalent
{
"mcpServers": {
"my-financial-data": {
"url": "https://your-server.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}

Local server (stdio)

// claude_desktop_config.json or similar
{
"mcpServers": {
"my-financial-data": {
"command": "node",
"args": ["./my-mcp-server/dist/index.js"],
"env": {
"API_KEY": "YOUR_API_KEY"
}
}
}
}

Once registered, the agent discovers the server's tools automatically. No SDK installation on the client side. The MCP protocol handles tool discovery, parameter validation, and response formatting. The agent calls tools by name and receives structured responses.

Agents do not need to know the implementation details of your server. A prompt like "List the available tools from my-financial-data and fetch the latest Bitcoin context" is enough. The agent reads the tool metadata, picks the right tool, and passes valid arguments without the user specifying technical details.

The rest of this guide covers building the server that sits behind this configuration.

04 Tool Design Patterns

The tool definition is the contract between your server and every agent that connects to it. The name, description, and parameter schema determine whether the LLM selects the right tool and passes valid arguments. A poorly described tool gets called with wrong parameters. A poorly named tool gets confused with other tools.

The @modelcontextprotocol/sdk provides server.registerTool() for tool registration. Each tool gets a name, metadata, a parameter schema (Zod-based), and a handler function.

Example: a Bitcoin price tool with context

import { z } from 'zod';
server.registerTool(
'get_btc_price',
{
title: 'Get Bitcoin Price',
description:
'Current BTC price with 24h change, 7d trend direction, ' +
'and volume relative to 30-day median.',
inputSchema: {
currency: z.enum(['usd', 'eur'])
.default('usd')
.describe('Quote currency'),
},
},
async ({ currency }) => {
// Fetch from your data source
const price = await fetchBtcPrice(currency);
const history = await fetchPriceHistory(7);
const volume = await fetchVolume24h();
const medianVol = computeMedian(await fetchVolumeHistory(30));
// Pre-compute the context an agent needs
const trend = history[6] > history[0] ? 'rising' : 'falling';
const change24h = ((price - history[6]) / history[6] * 100);
const volRatio = (volume / medianVol).toFixed(1);
const text = [
`BTC ${price.toLocaleString()} (${currency.toUpperCase()})`,
`24h: ${change24h > 0 ? '+' : ''}${change24h.toFixed(1)}%`,
`7d trend: ${trend}`,
`Volume: ${volRatio}x 30d median`,
].join('\n');
return {
content: [{ type: 'text', text }],
};
}
);

The tool fetches raw data, computes the trend and volume context, and returns a pre-formatted string the agent reads directly. The agent does not parse JSON or compute a 7-day trend from an array of prices. That computation happened server-side.

Three naming patterns matter. First, verb_noun naming (get_btc_price, list_metrics) tells the LLM what the tool does without reading the description. Second, the description includes concrete output details (trend direction, volume ratio) so the LLM knows what it will receive. Third, parameter descriptions include defaults and enum constraints. An LLM seeing z.enum(['usd', 'eur']) cannot invent a currency the tool does not support.

05 The 5-Tool Pattern

In production, PreReason settled on 5 data tools after testing configurations ranging from 2 (one list, one get) to 12 (one per data category). For a while it also had two tools that got an agent a key (request_access and check_access); they were removed on 1 October 2026, and the server has 7 tools today. The five data tools split cleanly into two categories: discovery and retrieval.

Discovery tools let agents explore what is available without consuming data. list_briefings returns all 33 live briefings with their tier requirements and descriptions. list_metrics returns all 293 individual metrics, filterable by category. These tools cost nothing to call and help the agent decide what to request.

Retrieval tools return analyzed data. get_context returns a full market briefing with trend analysis and confidence scores, and regime classification in the regime briefing. get_metric returns a single metric with signal interpretation and percentile ranking. These tools require an API key and count against rate limits. get_health is the fifth tool: a zero-auth health check that agents use to verify connectivity before making authenticated calls. Full tool documentation is in the API reference.

Two tools was too few. A single "get_data" tool with a complex parameter object confused agents about which parameter combinations were valid.

Twelve tools was too many. Agents spent reasoning tokens deciding between "get_bitcoin_price" and "get_bitcoin_context" and "get_bitcoin_full" when the distinction was not obvious from the names alone.

The discovery/retrieval split resolved both problems. Agents call a list tool first, see what is available, then call the specific retrieval tool. Two-step, predictable, no ambiguity.

06 Production Lessons

Three lessons from running a financial data MCP server in production were not obvious from reading the specification.

Data staleness needs to be visible, not hidden. Financial data has different update frequencies. Bitcoin price updates every few minutes. Federal Reserve balance sheet data updates weekly. If an agent receives both in a single response without timestamps, it cannot distinguish a 2-minute-old price from a 4-day-old liquidity number. Every data point in a response should carry its own timestamp and data age indicator.

Tool scope determines cache boundaries. A monolithic tool that returns "everything about Bitcoin" invalidates its cache when any component changes. A focused tool that returns one metric with one interpretation caches at the metric level. The second approach means most cache entries survive individual data updates instead of being discarded wholesale.

Fragile: monolithic tool

get_everything(category: "bitcoin")
// Returns: price + trends + regime +
// on-chain + cross-asset + macro
// Cache: invalidated on ANY update
// Scope: unclear what agent receives
// Tokens: ~4,000 per call

Agent cannot request only what it needs.

Resilient: focused tools

get_metric(metric: "bitcoin-price")
get_context(briefing: "btc.quick-check")
// Returns: exactly what was requested
// Cache: per-metric, per-briefing
// Scope: explicit from the tool name
// Tokens: 200-400 per call

Agent requests precisely what it needs.

07 Testing MCP Tools

MCP Inspector is the fastest way to validate tool behavior interactively. It connects to a running server and lets you call each tool with different parameters, inspect the response schema, and verify error handling. For financial data, test with edge cases: metrics that have not updated today, briefings that require a tier the test key does not have, and date ranges that span data gaps.

For get_metric, integration tests verify structured JSON fields: signal, trend, and confidence. Do not assert that Bitcoin's price is exactly $84,200. Market data changes. The schema should not.

For get_context, the contract is different. PreReason's primary output is markdown briefings, not JSON. An agent calling get_context with briefing: "cross.regime" receives pre-analyzed market context as natural language: regime state, trend interpretation, confidence levels, and cross-asset signals. The agent reads the briefing directly. No JSON destructuring, no field mapping. Both formats (JSON and markdown) are available via the format parameter. In our comprehension test the markdown cost fewer tokens and the JSON was read more accurately. The evidence page measures what matters most: not whether the schema is valid, but whether agents make better decisions when market context arrives pre-analyzed.

Clarity, delivered.

One call. The full picture. Ready when you are.

Start Free View docs

Free tier. No credit card.

Related Insights

Context Engineering for Crypto Agents

The pipeline problem that MCP servers solve for AI agents

Best Bitcoin APIs for AI Agents

Data sources to connect behind your MCP server

Real-Time Market Context

What AI agents actually need beyond raw price data