Skip to main content
Glama
dubeyanant

paper-broker-mcp

by dubeyanant
README.md
# paper-broker-mcp

An MCP server for a simulated stock trading account, plus a small agent loop that talks to it. You say "buy 10 Reliance", the agent looks up the stock, previews the order, reads it back and asks "Shall I place it?". Only after you say yes does anything get placed.

This is the backend for the voice trading agent on my portfolio: **https://www.anantdubey.com/work/voice-trading-agent**. The idea comes from my Ideathon 2026 entry. This repo is a from-scratch rebuild done in public and shares no code with the original.

Prices are simulated and no real money is involved. It isn't connected to any broker or exchange, and nothing here is investment advice.

## How it fits together

```
 browser mic ──speech-to-text──┐                ┌── text-to-speech ──> speaker
                               v                │
                     runAgentTurn(messages, state)
                               │
                 model (Claude, Gemini, or offline rules)
                               │  tool calls
                               v
             MCP client ══ in-memory transport ══ MCP server
                                                   │
                                                   v
                                      PaperBroker (plain JSON state)
                                      prices = f(symbol, time)
```

The same MCP server also runs over stdio, so Claude Desktop or any other MCP client can use it directly.

## Tools

| Tool | What it does |
| --- | --- |
| `search_instruments(query)` | Turns a spoken name ("state bank", "L and T", "infy") into an NSE symbol. |
| `get_quote(symbol)` | Last price, previous close, change, day high and low. Always marked `simulated: true`. |
| `get_funds()` | Cash, cash blocked by open buy orders, available cash, holdings value, P&L. |
| `get_positions()` | Holdings with average price, current price and P&L. |
| `get_orders()` | Recent orders, newest first. |
| `preview_order(symbol, side, quantity, order_type, limit_price?)` | Checks an order and returns `preview_id`, a one-line `summary`, `estimated_value` and `expires_in_seconds: 60`. Changes nothing else. |
| `place_order(preview_id)` | Places a previewed order. Fails if the preview doesn't exist or has expired. |
| `cancel_order(order_id)` | Cancels an open limit order. |

Each tool returns a short text line (written to be read aloud) and the same data as structured JSON.

## Preview, then confirm

There is no tool that places an order from raw parameters. An order can only come from a preview, and a preview only lives for 60 seconds. That gives two layers of protection:

1. **The server.** `place_order` takes nothing but a `preview_id`. The model can't invent an order the user never heard, and a stale preview can't be replayed. Its description says to call it only after the user has explicitly confirmed the preview.
2. **The agent loop.** `runAgentTurn` only lets `place_order` through for the preview that was pending *before* the user's latest message. A preview created during the current turn is refused with `NOT_CONFIRMED`, whatever the model decides, so the user always hears the preview and answers before anything is placed. If the user says anything other than a confirmation, the pending preview is dropped.

## Quickstart

### Claude Desktop

Add this to `claude_desktop_config.json` and restart Claude Desktop:

```json
{
  "mcpServers": {
    "paper-broker": {
      "command": "npx",
      "args": ["-y", "github:dubeyanant/paper-broker-mcp"]
    }
  }
}
```

The account lives in memory and resets when the server restarts. To keep it, add `"--state", "/path/to/paper-account.json"` to `args`. The file is created on the first change.

### Terminal chat

```sh
git clone https://github.com/dubeyanant/paper-broker-mcp
cd paper-broker-mcp
npm install
npm run chat
```

Tool calls are printed dimmed above each reply. Things to try: `buy 10 reliance`, `yes`, `sell 5 TCS at 4,000`, `buy twenty shares of infosys`, `price of HDFC bank`, `how is Tata Motors doing`, `show my portfolio`, `how much cash do I have`, `my orders`, `cancel my last order`. Add `-- --provider rules` to force the offline parser.

The chat script and tests run the TypeScript sources directly, which needs Node 22.18 or newer. The built package in `dist/` runs on Node 20.

### From code

```ts
import { runAgentTurn } from "paper-broker-mcp/agent";

const first = await runAgentTurn({
  messages: [{ role: "user", content: "buy 10 reliance" }],
  state: null, // fresh account with 10 lakh rupees of virtual cash
});
first.reply; // "Buy 10 shares of Reliance at market, about 29,425 rupees. Shall I place it?"
first.events; // [{ type: "tool_call", name: "search_instruments", args: {...} }, { type: "tool_result", ... }, ...]

const second = await runAgentTurn({
  messages: [...first.messages, { role: "user", content: "yes" }],
  state: first.state, // plain JSON, safe to store anywhere between turns
});
second.reply; // "Done. Bought 10 shares of Reliance at 2,942 rupees 45 paise."
```

`state` is plain JSON, so a web route can send it to the browser and get it back on the next request. Validate before trusting it: `runAgentTurn` and `PaperBroker.fromState` both reject malformed state.

The broker and server are available on their own too:

```ts
import { PaperBroker, createServer } from "paper-broker-mcp";

const broker = PaperBroker.fromState(savedState); // or null
const server = createServer(broker); // an McpServer; connect it to any transport
// ...after tool calls:
const next = broker.toState();
```

## API keys

Copy `.env.example` to `.env`. Every value is optional.

| Variable | Default | Notes |
| --- | --- | --- |
| `ANTHROPIC_API_KEY` | | Uses Claude when set. |
| `ANTHROPIC_MODEL` | `claude-haiku-5-5` | |
| `GEMINI_API_KEY` | | Uses Gemini when set and there is no Anthropic key. |
| `GEMINI_MODEL` | `gemini-3.8-flash` | |

Either key works. With neither, the agent uses a built-in rules parser that makes no network calls and drives the same MCP tools, so the events look the same. If a model call fails before any tool has run, the turn is answered by the rules parser and the result includes an `error` field.

## How the simulation works

- **Universe.** Twenty NSE large caps, each with spoken aliases ("RIL", "tata consultancy", "airtel", "L and T"). Equity delivery only.
- **Prices.** A pure function of symbol and time: a base price times a sum of slow sine waves (periods from 47 minutes to 71 days, with per-symbol phases) plus a little hashed noise that changes once a second, rounded to the 0.05 tick. Nothing is stored, so any process computes the same price for the same second. The market never closes; "previous close" is the price just before midnight IST.
- **Account.** Starts with 10 lakh rupees of virtual cash. No brokerage, taxes or slippage.
- **Rules.** Quantity 1 to 10,000. Buys need enough available cash. No short selling. Limit prices must be within 20% of the last price. Market orders fill at the last price. Limit orders rest, block cash (buys) or shares (sells), and fill at the limit price the first time the price path touches it. Open orders are checked whenever the account is used, by walking the price path since the order was placed.

## Development

```sh
npm test           # node:test, runs the TypeScript sources directly
npm run typecheck  # src, tests and examples
npm run build      # compile src to dist/
```

`dist/` is committed on purpose so the package can be installed straight from GitHub without a build step. Run `npm run build` and commit the result after changing anything in `src/`.

## Disclaimer

Simulated prices, no real money, not investment advice. This project is not affiliated with any broker or exchange. Stock names and symbols are used only to make the demo recognisable.

## License

MIT. See [LICENSE](LICENSE).

TDQS

A3.9/5.0

Scored across 8 tools

Disambiguation5/5

Each tool targets a distinct resource+action pair: search_instruments resolves names to symbols, get_quote prices a single stock, get_funds/get_positions/get_orders read distinct account views, and cancel_order/preview_order/place_order cover separate order lifecycle steps. The preview_order/place_order split is explicitly documented as a two-step gate with a preview_id handshake, so misselection risk is low. No two tools appear to do the same thing.

Naming Consistency5/5

Every tool is snake_case verb_noun (search_instruments, get_quote, get_funds, get_positions, get_orders, preview_order, place_order, cancel_order). The verb choices are meaningful and predictable: get_* for reads, preview/place/cancel_* for lifecycle actions. No mixed conventions or vague verbs.

Tool Count5/5

Eight tools is well-scoped for a paper-trading broker: three read-only account views, one instrument lookup, one quote, and a three-step order lifecycle. Each tool earns its place and there is no filler or redundant surface.

Completeness4/5

The surface covers the full retail trading loop: discovery (search_instruments), pricing (get_quote), account state (funds/positions/orders), and order creation and cancellation with a preview gate. Minor gaps remain, such as no modify-order operation, no single-order status lookup by id, and no market-hours/status signal, but agents can work around these using get_orders and cancel-then-replace.

Maintenance

ActivityMaintained
ResponsivenessNo issues