Skip to main content
Glama
cataloraxyz

catalora-mcp

Official
by cataloraxyz

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.


Related MCP server: edgex-mcp

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

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    An MCP server that enables autonomous AI agents to connect to Tastytrade for market scanning, option strategies, account management, and optionally placing trades with built-in safety controls.
    9
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    MCP server for EdgeX perpetual contract trading, providing 16 AI agent tools for market data, account management, and order execution.
    17
    29
    MIT
  • A
    license
    -
    quality
    C
    maintenance
    An MCP server that lets an AI agent backtest, risk-check, and audit trading strategies, determining if a strategy is overfit or actually works.
    MIT
  • A
    license
    -
    quality
    A
    maintenance
    MCP server for AI agents to inspect and trade against a MetaTrader 4 terminal, with an offline mock mode for CI and demos.
    2
    MIT

View all related MCP servers

Related MCP Connectors

  • MCP server exposing the Backtest360 engine API as tools for AI agents.

  • An MCP Server that provides identity verification and anti-fraud tools for AI agents via deepidv.

  • Crypto transaction firewall and risk tools for MCP agents.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cataloraxyz/catalora-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server