The Model Context Protocol (MCP) lets Claude call external tools defined by your application. Instead of pasting data into a chat window or writing one-off scripts to query your system, you define tools with typed schemas, start an MCP server, and Claude can call them directly — in Claude Code, Claude Desktop, or any MCP-compatible client.
The Trader project uses this to expose its Forex trading engine as Claude tools: query your OANDA account, review open trades, run a backtest against historical data, and — when explicitly enabled — place or close orders.
How MCP Works
MCP is JSON-RPC 2.0 over stdio. The client (Claude) sends requests; the server (your app) responds. The protocol defines a small set of methods:
initialize— handshake, agree on protocol versiontools/list— return the list of available tools with their input schemastools/call— call a named tool with provided argumentsresources/list,resources/read— expose readable artifacts (files, reports)prompts/list— expose reusable prompt templates
Claude uses the tool descriptions and schemas to decide which tool to call and how to format the arguments. Clear descriptions and precise schemas produce better results than vague ones.
Server Structure
Trader’s MCP server lives in api/mcp/ and is split across three files:
server.go — the dispatcher. It reads from stdin, parses JSON-RPC
requests, and routes to the appropriate handler. Notifications (requests
without an id) are handled separately and never replied to, per the
JSON-RPC spec.
func (s *Server) handleRequest(req *Request) *Response {
switch req.Method {
case "initialize":
return s.handleInitialize(req)
case "tools/list":
return s.handleToolsList(req)
case "tools/call":
return s.handleToolsCall(req)
case "resources/list":
return s.handleResourcesList(req)
case "resources/read":
return s.handleResourcesRead(req)
}
return errorResponse(req.ID, -32601, "method not found")
}
tools.go — tool definitions and handlers. Each tool has a name,
description, and a JSON Schema for its input parameters. The description
is what Claude reads to decide when to call the tool — write it like
documentation for a human who knows nothing about your internals.
resources.go — exposes readable artifacts. Trader uses this for
backtest .org report files and YAML config files, letting Claude read
past results or configs without a separate tool call.
Defining a Tool
A read-only tool looks like this:
Tool{
Name: "run_backtest",
Description: "Run a backtest using a named YAML config file. " +
"Returns a summary including net P/L, win rate, trade count, " +
"and max drawdown. Does not require OANDA credentials.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"config": map[string]any{
"type": "string",
"description": "Path to the YAML config file, relative to testdata/configs/",
},
},
"required": []string{"config"},
},
},
The handler calls the actual backtest logic and returns the result as a string. Claude receives it and can reason over it, compare it to other runs, or present a summary.
Write-Gating
Write tools — place_order, close_trade, update_stop — are only
registered when the server is started with --enable-write:
func (s *Server) buildTools() []Tool {
tools := readOnlyTools()
if s.EnableWrite {
tools = append(tools, writeTools()...)
}
return tools
}
This means the default MCP session is read-only. You can query your account
balance, review positions, and run backtests without any risk of accidental
order placement. When you actually want to trade, you restart with
--enable-write and Claude knows the write tools are available.
The pattern is worth adopting for any MCP server that has side effects: split tools into read and write, gate the writes explicitly, and make the default safe.
Transport
Stdio is the standard MCP transport for local tools. The server reads a line from stdin, processes it, writes a line to stdout. This makes it trivial to test outside Claude:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | trader mcp
It also means the server has no network exposure by default — it only communicates with the process that spawned it, which is the MCP client.
Connecting to Claude Code
Add the server to .claude/settings.json or the global Claude Code config:
{
"mcpServers": {
"trader": {
"command": "trader",
"args": ["mcp"],
"env": {
"OANDA_TOKEN": "${OANDA_TOKEN}"
}
}
}
}
Once connected, Claude Code can call run_backtest, get_account_summary,
and the other tools in any conversation. You can ask “which of my configs
has the best Sharpe ratio?” and Claude will call run_backtest for each
config and compare the results.
What Makes a Good MCP Tool
A few things that improve tool quality in practice:
One responsibility per tool. get_account_summary returns account state.
list_open_trades returns positions. Combining them into one tool makes the
response harder for Claude to parse and reason over.
Descriptions that explain when to call the tool, not just what it does. “Returns account balance” is less useful than “Returns current balance, NAV, margin used, and unrealized P/L. Call this before placing an order to verify available margin.”
Return structured data as formatted text. Claude handles plain text and markdown better than raw JSON in tool responses. Format numbers, add labels, and use tables for lists of items.
Fail with useful error messages. If OANDA is not configured and a tool requires it, return an error response explaining that — not a Go panic or a blank response.