catalora-mcp

catalora-mcp

An MCP server for trading agents that provides eight tools to trade under enforceable limits, local paper trading, and comprehensive risk management.

Category
访问服务器

README

catalora-mcp

Eight tools that let an AI agent trade under limits it cannot talk its way around.

npm CI MCP License: MIT

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, never 0. "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/trace are 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

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.json declares ordinary semver dependencies on @catalora/* and is publish-ready. Until those land on npm, pnpm-workspace.yaml redirects 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

MIT

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选