catalora-mcp
An MCP server for trading agents that provides eight tools to trade under enforceable limits, local paper trading, and comprehensive risk management.
README
catalora-mcp
Eight tools that let an AI agent trade under limits it cannot talk its way around.
An MCP server for trading agents. Works with Claude, Codex, Cursor, or anything else that speaks the Model Context Protocol.
{
"mcpServers": {
"catalora": {
"command": "npx",
"args": ["-y", "@catalora/mcp"],
"env": {
"CATALORA_MODE": "local",
"CATALORA_MANDATE": "./mandate.json",
"CATALORA_JOURNAL": "./fills.jsonl",
"CATALORA_MARKS": "./marks.json",
"CATALORA_STARTING_EQUITY": "100000"
}
}
}
}
Drop that into claude_desktop_config.json, .mcp.json for Claude Code, or the equivalent
for Codex or Cursor. Ready-made copies are in examples/.
The eight tools
Deliberately small. An agent handed forty tools reasons worse than one handed eight — it spends its planning budget choosing between tools instead of choosing between trades.
| Tool | Does | Why it earns a slot |
|---|---|---|
get_mandate |
Ticker whitelist, leverage ceiling, drawdown cap, position and order limits | The agent knows its boundaries before it plans, not after it is refused |
get_session_state |
open / pre / post / closed / halted, plus hours to next open | The input almost no agent has, and the largest avoidable loss there is |
get_quote |
Reference price with a staleness flag | Tells the agent when a price is a last print rather than a live one |
get_portfolio |
NAV, cash, positions, headroom against every limit | Full state in one call, instead of inferring from remembered orders |
get_risk_budget |
Drawdown remaining, notional remaining, NAV floor | Lets the agent brake before it is stopped |
submit_order |
Checks the mandate, then executes. Refuses on breach | The enforcement point |
close_position |
Full or partial exit | Always available, even in a symbol the mandate no longer permits |
get_track_record |
Realised P&L, drawdown, Sharpe, hit rate, time in market | What the agent benchmarks against |
A ninth tool needs a written argument for why the agent reasons better with it, not why
the data is useful. test/tools.test.ts fails the build at nine. A documented promise that
nothing enforces erodes through ordinary, well-meaning pull requests.
Resources and prompts — capability without a ninth tool
The obvious way to add capability would be to break the eight-tool cap. But a tool is for doing something, and a resource is something you read that does not change when you read it. Modelling them correctly means the surface grows and the promise holds.
Resources — attach to a conversation once, up front:
| URI | What |
|---|---|
catalora://mandate |
The limits as prose, written for a system prompt |
catalora://mandate.json |
The same, machine-readable |
catalora://portfolio |
NAV, positions, and headroom against every limit |
catalora://record |
Full track record, provenance first |
catalora://record/monthly |
P&L by month — the view a lucky month cannot hide in |
catalora://record/attribution |
How much of the record is one ticker |
catalora://record/drawdowns |
Every drawdown, its depth, and whether it recovered |
catalora://calendar |
Closures, half-days, and the next five sessions |
Resource templates — parameterised, read any instance:
| Template | What |
|---|---|
catalora://session/{ticker} |
Session state and whether the price is live |
catalora://auction/{ticker} |
Auction times and the MOC/MOO cutoffs |
catalora://luld/{ticker}/{price} |
Limit up/down bounds at a reference price |
catalora://settlement/{date} |
When a trade on that date settles |
Attaching the mandate costs nothing and means the model plans inside its limits rather than discovering them by being refused.
Prompts — checklists the operator invokes by name:
pre_trade_check— session, mandate, room, quote staleness, in that order. Stops at the first step that says stop, and never submits.daily_review— what moved, what it cost, what tomorrow's calendar looks like. Explicitly refuses to congratulate you on one good day.risk_report— every limit ranked by how close it is to binding, tightest first.position_sizing— works a size back from the drawdown budget rather than from NAV, because the budget is what is actually scarce. Says "there is no room for this" when that is the honest answer, instead of shrinking it to something pointless.postmortem— separates a good decision with a bad outcome from a bad decision, because those need opposite responses and conflating them is how a process gets changed for no reason. One page, no consolation.record_critique— reads the record as a sceptic. Ends with the strongest argument that it is luck, and is told not to soften it.
A REST shim over the same eight tools
For agents that are not MCP clients — a Python script, a cron job, a custom runtime.
curl -s localhost:8787/tools
curl -s -X POST localhost:8787/tools/submit_order \
-d '{"ticker":"NVDA","side":"buy","quantity":100}'
curl -s "localhost:8787/resources/catalora%3A%2F%2Fmandate"
It calls exactly the same callTool the MCP server does, so the two surfaces cannot drift.
A refusal comes back 200 with ok: false — the request was correct and the answer is no;
a 4xx would tell the caller to fix a request that has nothing wrong with it.
Binds to 127.0.0.1 and has no authentication. That is only safe because of the binding.
The kill switch
CATALORA_READ_ONLY=1
submit_order and close_position refuse. Everything that reads keeps working, because an
operator who has just pulled this switch will want to see where the book stands.
It fails closed: anything but an explicit 0, false, no or empty counts as on. An
operator who sets CATALORA_READ_ONLY=yes and gets live trading because the parser wanted
"true" has been failed by the tool at the exact moment they were relying on it.
It disables exits too. That is deliberate — "stop everything, now" is a different intent from "stop opening", and it needs its own way to say it.
Auditing what the agent tried
CATALORA_AUDIT=./audit.jsonl
Off unless set. A track record says what an agent traded; the audit log says what it tried, including the refusals — and an agent that submitted forty breaching orders before finding a compliant one has told you something its P&L never will.
Arguments are recorded; results are not. A result can contain the whole portfolio, and the arguments are the part that reflects what the agent chose.
Refusals are answers, not errors
This is the part that decides whether an agent recovers or spins.
REFUSED — 1 mandate violation:
[POSITION_SIZE_EXCEEDED] The resulting NVDA position would be $300,000.00, 30.0% of
$1,000,000.00 NAV, above the 25.0% per-position cap ($250,000.00). At $100.00 the
largest permissible order is 500 units.
The largest order that would pass every constraint is 500 units of NVDA (submitted 3000).
An agent that gets "mandate breach" resubmits the same order. An agent that gets the
constraint, the actual value, the limit, and the exact quantity that would work fixes it on
the next turn.
Every response carries both prose and structure: the model reasons over the sentence and
computes with structuredContent.
When no smaller order would help, it says so — "Do not retry it smaller."
Local mode, and what it is not
Today this server runs entirely on your machine. The mandate is a JSON file, fills go to an append-only journal, and the track record is computed from that journal.
submit_order journals a paper fill. It does not route anywhere. Every receipt says so:
PAPER FILL — journalled locally at the reference price. Nothing was sent to a venue
and no capital moved. Assume zero slippage and infinite liquidity, because that is
what was simulated.
A locally computed track record is self-reported. It carries
provenance: "local-journal" and every response leads with the unverified notice. There is
no code path here that converts it into a verified one, and there never will be — if there
were, "verified" would stop meaning anything.
That is not a limitation to work around. It is the honest description of what a local record is: the operator controls the input file.
What this server refuses to fabricate
- A price it does not have. No marks file entry means an error, not a guess. An agent cannot tell a made-up quote from a real one.
- A mandate. No mandate file means the server refuses to start the agent. An agent with no declared limits is exactly what this exists to prevent.
- A statistic it cannot compute.
null, never0. "Could not measure" and "measured zero" are different facts. - Slippage. A marks file is a list of prices, not a book.
Chain mode
Not implemented. The CATALORA contracts ship through Robinhood, and until their ABIs are frozen there is nothing to write against.
The stub is real, though, and it throws rather than falling back to local mode. An operator who asked for settled execution and silently got paper fills would not find out until it mattered.
The Driver seam exists on day one for that reason: LocalDriver today, ChainDriver when
the contracts land, same tool handlers, same schemas, same responses, one env var to switch.
No chain type or contract ABI may leak past that interface into a tool handler. Retrofitting
a seam after eight handlers have grown their own I/O is the rewrite this design avoids.
Configuration
From the environment only. Never a CLI flag — flags land in shell history, and one of these values will eventually be a key that can move money.
| Variable | Default | Meaning |
|---|---|---|
CATALORA_MODE |
local |
local or chain |
CATALORA_MANDATE |
./mandate.json |
Path to the mandate |
CATALORA_JOURNAL |
./fills.jsonl |
Path to the append-only fill journal |
CATALORA_MARKS |
— | JSON map of { "TICKER": price }, your quote source |
CATALORA_STARTING_EQUITY |
— | Capital base. Without it, drawdown and Sharpe are unmeasured |
CATALORA_AGENT_KEY |
— | Chain mode only. Never logged, never echoed |
CATALORA_NETWORK |
local |
Chain mode only |
CATALORA_AUDIT |
— | Append-only log of every tool call. Off unless set |
CATALORA_READ_ONLY |
— | Refuse every order-placing tool. Fails closed |
CATALORA_LOG_LEVEL |
info |
debug · info · warn · error · silent |
Everything is validated at startup rather than at the first tool call. An agent that discovers a missing key three tool calls into a plan has already wasted the plan.
The agent key is absent from any logged or returned config, not masked. A masked secret still leaks its length and prefix, and a field that exists at all invites someone to widen it.
stdout is the protocol stream
The single most important rule in this repository.
A stray console.log anywhere in the process writes into the JSON-RPC stream and corrupts it.
The failure mode is not a helpful parse error — it is a client that appears to hang, or one
that works right up until something logs mid-tool-call. This cost real debugging time on a
previous MCP server.
So:
- Everything here logs to stderr.
console.log/info/debug/warn/error/traceare redirected to stderr at startup, so even a dependency that logs cannot corrupt the stream.- A test spawns the real server, exercises all eight tools over a real stdio transport, and asserts that every byte of stdout parses as a JSON-RPC message. CI runs the same check against the built binary.
Getting started
mkdir my-agent && cd my-agent
curl -O https://raw.githubusercontent.com/cataloraxyz/catalora-mcp/main/examples/mandate.json
curl -O https://raw.githubusercontent.com/cataloraxyz/catalora-mcp/main/examples/marks.json
Edit mandate.json to the limits you actually want, point your MCP client at the directory,
and ask your agent:
Check the session state for NVDA, then tell me what your mandate allows.
A good first sign that it is wired up correctly: ask it to buy far more than the mandate permits and watch it come back with the exact size that would have worked.
Built on
@catalora/market-session— session state@catalora/mandate— the enforcement rules@catalora/track-record— performance math
All three are zero-dependency and usable on their own. The MCP server is a thin surface over them, which is why the interesting logic is tested there rather than here.
Development
pnpm install
pnpm test # 147 tests, including a real end-to-end MCP session
pnpm typecheck
pnpm lint
pnpm build
pnpm dev # run from source over stdio
Note on dependencies.
package.jsondeclares ordinary semver dependencies on@catalora/*and is publish-ready. Until those land on npm,pnpm-workspace.yamlredirects them to sibling checkouts, and CI checks the sibling repos out to build them. Both blocks are commented and have a removal trigger. This shape is what broke CI across a previous repo fleet, so it is scoped and temporary on purpose — the fix is publishing, not more scaffolding.
See CONTRIBUTING.md.
Part of CATALORA
CATALORA is the allocation layer for AI agents trading tokenized stocks: agents trade under a mandate a contract enforces, build a track record nobody can fake, and raise outside capital against it.
This server is the integration surface. Local mode works with none of that — no account, no key, no contract — and the same eight tools point at the contracts when they ship.
agents ──▶ @catalora/mcp ─┐
│ @catalora/mandate what an agent may do
├──▶ @catalora/market-session when it may do it
humans ──▶ @catalora/cli ─┘ @catalora/track-record what it actually did
two surfaces three zero-dependency libraries
The eight tools are a thin shell over the three libraries below. Nothing this server does is unavailable to a program that imports them directly.
| Package | Repo | The question it answers |
|---|---|---|
@catalora/mandate |
agent-mandate | What is this agent allowed to do? |
@catalora/market-session |
market-session | Is this ticker tradeable right now, and if not, when? |
@catalora/track-record |
track-record | What did it actually do, and who says so? |
@catalora/mcp |
catalora-mcp | The three libraries as eight tools, for any MCP client ← you are here |
@catalora/cli |
catalora-cli | The same three, from a terminal |
Web catalora.xyz · dApp dapp.catalora.xyz · Docs docs.catalora.xyz · Source github.com/cataloraxyz
License
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。