In late 2024, Anthropic released the Model Context Protocol (MCP) — an open standard that defines how AI models connect to external tools, APIs, and data sources. Instead of hardcoding API integrations into your LLM application, MCP lets you build reusable “servers” that any MCP-compatible client (Claude Desktop, Claude Code, or any SDK-based agent) can discover and use.
Think of MCP as the USB standard for AI tools: define the interface once, connect anywhere.
Table of contents
Open Table of contents
Why MCP Exists
Before MCP, every AI integration was bespoke. Claude’s tools API lets you define functions the model can call — but those definitions live in your application code. If you wanted to reuse the same database access logic across Claude Desktop, a custom agent, and a CI/CD bot, you’d copy-paste tool definitions everywhere.
MCP solves this with a client-server model:
Claude (MCP Client) ←→ MCP Server ←→ Database/API/File SystemThe MCP server exposes tools, resources, and prompts:
- Tools: Functions the AI can call (like API endpoints)
- Resources: Data the AI can read (like files or database records)
- Prompts: Reusable prompt templates
Once your MCP server is running, any MCP client can use it without knowing your implementation details.
Setup
npm install @modelcontextprotocol/sdk zodnpm install -D typescript @types/node tsx{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "dist", "strict": true }}Building a Financial Data MCP Server
Let’s build an MCP server for a fintech application — one that exposes transaction queries, account lookups, and fraud flagging as tools Claude can use.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema,} from '@modelcontextprotocol/sdk/types.js';import { z } from 'zod';
// Initialize the MCP serverconst server = new Server( { name: 'fintech-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, resources: {}, }, });Defining Tools
Tools are functions the AI model can call. They have a name, description, and JSON Schema input:
// Tool definitionsconst TOOLS = [ { name: 'get_account_summary', description: 'Get summary information for a bank account including balance, recent transactions, and status', inputSchema: { type: 'object', properties: { account_id: { type: 'string', description: 'The unique account identifier (format: ACC-XXXXXXXX)', }, include_transactions: { type: 'boolean', description: 'Whether to include last 10 transactions (default: false)', default: false, }, }, required: ['account_id'], }, }, { name: 'search_transactions', description: 'Search transactions by date range, amount, merchant, or category', inputSchema: { type: 'object', properties: { account_id: { type: 'string' }, from_date: { type: 'string', description: 'Start date in YYYY-MM-DD format', }, to_date: { type: 'string', description: 'End date in YYYY-MM-DD format', }, min_amount: { type: 'number' }, max_amount: { type: 'number' }, merchant_name: { type: 'string', description: 'Partial merchant name to search (case-insensitive)', }, category: { type: 'string', enum: ['retail', 'food', 'transport', 'utilities', 'entertainment', 'other'], }, limit: { type: 'integer', default: 20, maximum: 100, }, }, required: ['account_id'], }, }, { name: 'flag_suspicious_transaction', description: 'Flag a transaction for fraud review with a reason and severity level', inputSchema: { type: 'object', properties: { transaction_id: { type: 'string' }, reason: { type: 'string', description: 'Explanation of why this transaction is suspicious', }, severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], }, }, required: ['transaction_id', 'reason', 'severity'], }, }, { name: 'generate_spending_report', description: 'Generate a spending analysis report for an account over a time period', inputSchema: { type: 'object', properties: { account_id: { type: 'string' }, period: { type: 'string', enum: ['last_7_days', 'last_30_days', 'last_90_days', 'last_year'], }, }, required: ['account_id', 'period'], }, },];
// Register tool listing handlerserver.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS,}));Implementing Tool Handlers
// Input schemas for validationconst GetAccountSummarySchema = z.object({ account_id: z.string().regex(/^ACC-[A-Z0-9]{8}$/), include_transactions: z.boolean().default(false),});
const SearchTransactionsSchema = z.object({ account_id: z.string(), from_date: z.string().optional(), to_date: z.string().optional(), min_amount: z.number().optional(), max_amount: z.number().optional(), merchant_name: z.string().optional(), category: z.string().optional(), limit: z.number().int().max(100).default(20),});
// Tool execution handlerserver.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params;
try { switch (name) { case 'get_account_summary': { const input = GetAccountSummarySchema.parse(args); const account = await db.getAccount(input.account_id);
if (!account) { return { content: [{ type: 'text', text: `Account ${input.account_id} not found` }], isError: true, }; }
const result: any = { account_id: account.id, owner_name: account.owner_name, balance: account.balance, currency: account.currency, status: account.status, created_at: account.created_at, };
if (input.include_transactions) { result.recent_transactions = await db.getRecentTransactions(account.id, 10); }
return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }
case 'search_transactions': { const input = SearchTransactionsSchema.parse(args);
const transactions = await db.searchTransactions({ accountId: input.account_id, fromDate: input.from_date, toDate: input.to_date, minAmount: input.min_amount, maxAmount: input.max_amount, merchantName: input.merchant_name, category: input.category, limit: input.limit, });
return { content: [ { type: 'text', text: JSON.stringify({ total_found: transactions.length, transactions, }, null, 2), }, ], }; }
case 'flag_suspicious_transaction': { const input = z.object({ transaction_id: z.string(), reason: z.string().min(10), severity: z.enum(['low', 'medium', 'high', 'critical']), }).parse(args);
const flag = await db.createFraudFlag({ transactionId: input.transaction_id, reason: input.reason, severity: input.severity, flaggedBy: 'ai-agent', flaggedAt: new Date(), });
// High severity → immediate alert if (input.severity === 'critical' || input.severity === 'high') { await alertFraudTeam(flag); }
return { content: [ { type: 'text', text: JSON.stringify({ success: true, flag_id: flag.id, message: `Transaction flagged with ${input.severity} severity`, }, null, 2), }, ], }; }
case 'generate_spending_report': { const input = z.object({ account_id: z.string(), period: z.enum(['last_7_days', 'last_30_days', 'last_90_days', 'last_year']), }).parse(args);
const report = await generateReport(input.account_id, input.period);
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }], }; }
default: return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true, }; } } catch (err) { if (err instanceof z.ZodError) { return { content: [{ type: 'text', text: `Invalid input: ${err.message}` }], isError: true, }; } throw err; }});Exposing Resources
Resources are read-only data the AI can access — similar to GET endpoints:
// Register resourcesserver.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'fintech://accounts', name: 'Account List', description: 'List of all active accounts', mimeType: 'application/json', }, { uri: 'fintech://fraud-rules', name: 'Fraud Detection Rules', description: 'Current active fraud detection rules and thresholds', mimeType: 'application/json', }, ],}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params;
if (uri === 'fintech://accounts') { const accounts = await db.listActiveAccounts(); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(accounts), }, ], }; }
if (uri === 'fintech://fraud-rules') { const rules = await db.getFraudRules(); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(rules), }, ], }; }
throw new Error(`Resource not found: ${uri}`);});Starting the Server
// Connect via stdio transport (standard for local MCP servers)async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Fintech MCP Server running on stdio');}
main().catch(console.error);Connecting to Claude Desktop
Add your server to Claude Desktop’s config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{ "mcpServers": { "fintech": { "command": "node", "args": ["/path/to/your/dist/server.js"], "env": { "DATABASE_URL": "postgresql://localhost/fintech" } } }}Restart Claude Desktop. The tools become available in any conversation:
“Show me the last 30 days of transactions for account ACC-12345678 and flag any over €5000 that aren’t labeled as utilities.”
Claude will call search_transactions to get the data, analyze it, then call flag_suspicious_transaction for any matching transactions — all in a single conversation turn.
Production Considerations
Authentication: The stdio transport (process-to-process) inherits the parent process’s credentials. For HTTP transport, implement OAuth or API key authentication in your MCP server.
Error handling: MCP clients expect proper error responses. Always return isError: true for tool failures rather than throwing — this lets the AI understand the error and potentially retry or ask the user for clarification.
Logging: Use console.error() for logging in stdio-based servers (stdout is reserved for MCP protocol messages).
Rate limiting: If your tools call external APIs, implement rate limiting inside the server. The AI may call tools rapidly in agentic loops.
Idempotency: The AI may call the same tool multiple times if it’s not sure the first call succeeded. Make destructive tools idempotent where possible.
MCP is young but moving fast. The pattern — expose your backend capabilities as discoverable, documented tools that any AI client can use — is going to be standard infrastructure within the next two years. Building MCP-first today means your AI integrations work across Claude Desktop, Claude Code, and whatever AI tooling your team adopts next.
Related posts
- Multi-Agent Workflows with Claude API — Architecture Patterns That Work — how MCP servers fit into hierarchical and parallel agent orchestration architectures
- LLM API Integration Patterns — Structured Outputs, Function Calling, Streaming — tool calling and structured output patterns that underpin MCP tool definitions