Skip to main content
Glama
Investbrain MCP

Investbrain MCP

Investbrain MCP

Remote Model Context Protocol server for a self-hosted Investbrain portfolio tracker. It runs on Cloudflare Workers (free plan) and calls your Investbrain /api routes with the Sanctum token your MCP client sends on each request.

The Worker does not store that token.

Keywords: MCP, Model Context Protocol, Investbrain, portfolio tracker, holdings, transactions, dividends, market data, Laravel Sanctum, Cloudflare Workers, self-hosted.

Tools

One tool per Investbrain API route under auth:sanctum. Update and delete tools are marked destructive in the MCP tool annotations and in the description.

Tool

Method

Path

investbrain_get_me

GET

/api/me

investbrain_list_portfolios

GET

/api/portfolio

investbrain_get_portfolio

GET

/api/portfolio/{portfolio}

investbrain_create_portfolio

POST

/api/portfolio

investbrain_update_portfolio

PUT

/api/portfolio/{portfolio}

investbrain_delete_portfolio

DELETE

/api/portfolio/{portfolio}

investbrain_list_transactions

GET

/api/transaction

investbrain_get_transaction

GET

/api/transaction/{transaction}

investbrain_create_transaction

POST

/api/transaction

investbrain_update_transaction

PUT

/api/transaction/{transaction}

investbrain_delete_transaction

DELETE

/api/transaction/{transaction}

investbrain_list_holdings

GET

/api/holding

investbrain_get_holding

GET

/api/holding/{portfolio}/{symbol}

investbrain_update_holding

PUT

/api/holding/{portfolio}/{symbol}

investbrain_list_dividends

GET

/api/dividend

investbrain_get_dividend

GET

/api/dividend/{dividend}

investbrain_create_dividend

POST

/api/dividend

investbrain_update_dividend

PUT

/api/dividend/{dividend}

investbrain_delete_dividend

DELETE

/api/dividend/{dividend}

investbrain_get_market_data

GET

/api/market-data/{symbol}

Portfolio, transaction, and holding list tools accept Investbrain's filter query: page, itemsPerPage (default 15, or -1 for every row), search, sortBy, and sortDesc. Portfolio search matches title and notes. Transaction and holding search match the symbol. investbrain_list_portfolios can also filter with holdings_symbol (filter[holdings.symbol]).

investbrain_list_dividends uses a different query. It accepts page, itemsPerPage (1–100, default 15; -1 is not valid), search (symbol, max 255 characters), sort (comma-separated columns symbol, date, dividend_amount, source, created_at, updated_at, with - for descending, for example -date), symbol (filter[symbol], one ticker or a comma-separated list), source (filter[source]: sync or manual), and date (filter[date], exact YYYY-MM-DD). Default order is date descending, then id ascending. The list is paginated (data, links, meta). A single dividend is not wrapped.

A dividend date is the inclusive entitlement date. For B3 that is data-com, not the payment date and not Yahoo's ex-date. Shares bought or sold on that date count. dividend_amount is the gross per-share amount in the trading currency. The app does not withhold tax. One row exists per symbol per date, so same-day proventos (dividendo, JCP, and others) are summed into that amount. Create and update store source=manual. source and dividend_amount_base sent on a write are ignored. Manual rows survive the Yahoo sync. A delete is remembered, so sync does not recreate that symbol and date. Each write recalculates dividends_earned. A duplicate symbol and date, or another validation failure, is a 422. The tool returns Investbrain's message and field errors.

Portfolio, transaction, and dividend ids are UUIDs. Create and update fields follow Investbrain's form requests: portfolio title is 5–255 characters; a transaction is BUY (requires cost_basis) or SELL (requires sale_price); quantity must be greater than 0; a sell cannot exceed shares owned before that date; holding update only sets reinvest_dividends. A dividend create requires symbol, date, and dividend_amount greater than 0. A dividend update sends any subset of those fields.

Laravel registers PUT and PATCH for each update action. These tools send PUT.

Related MCP server: IB MCP Server

Quick start

  1. Fork this repository.

  2. In the Cloudflare dashboard, open Workers & Pages and connect the fork with Workers Builds. Production branch: main.

  3. In the Worker, open Settings → Builds:

    • Deploy command: pnpm run deploy

    • Build variable INVESTBRAIN_BASE_URL: public origin of your Investbrain app, for example https://investbrain.example.com

    • Optional build variable ALLOWED_HOSTNAMES: comma-separated hostnames, for example mcp.example.com, with no scheme or port

  4. A push to main deploys. Workers Builds does not copy a build variable into the Worker. pnpm run deploy does, with wrangler deploy --var. The default command npx wrangler deploy would upload the empty placeholder.

  5. Leave non-production branch builds off. Their default command does not apply that variable.

  6. Wrangler prints the workers.dev host. The MCP endpoint is https://investbrain-mcp.<account-subdomain>.workers.dev/mcp.

  7. Create an Investbrain API token (below) and put it in the MCP client. Do not add that token as a GitHub secret, a Worker secret, or a build variable.

Manual deploy

GitHub Actions can deploy the same script. The workflow runs only when you start it. A push does not start it.

  1. Create a Cloudflare API token from the "Edit Cloudflare Workers" template, and copy the account ID. Workers Builds does not need this token.

  2. In the GitHub fork, add repository secrets (Settings → Secrets and variables → Actions):

Secret

Value

CLOUDFLARE_API_TOKEN

API token from step 1

CLOUDFLARE_ACCOUNT_ID

Cloudflare account ID

INVESTBRAIN_BASE_URL

Same origin as the Workers Builds variable

ALLOWED_HOSTNAMES

Optional. Same comma-separated hostnames.

  1. Actions → Deploy → Run workflow.

Configuration

Name

Where it is set

Purpose

INVESTBRAIN_BASE_URL

Workers Builds build variable, or the GitHub Actions secret. Locally, .dev.vars.

Origin of Investbrain. http or https, no userinfo, query, or fragment. A trailing slash is stripped. A path prefix is kept.

ALLOWED_HOSTNAMES

Optional build variable or GitHub secret. Locally, .dev.vars.

Extra Host and Origin names, comma-separated. Empty keeps workers.dev and localhost only.

CLOUDFLARE_API_TOKEN

GitHub secret for the manual workflow only

Wrangler deploy. Workers Builds creates its own token.

CLOUDFLARE_ACCOUNT_ID

GitHub secret for the manual workflow only

Wrangler deploy

AUTH_FAILURE_LIMIT

wrangler.jsonc, default 10

Failed auth attempts per client IP before lockout

AUTH_FAILURE_WINDOW_SECONDS

wrangler.jsonc, default 900

Window those failures are counted in

AUTH_LOCKOUT_SECONDS

wrangler.jsonc, default 1800

How long the IP stays locked

The committed INVESTBRAIN_BASE_URL and ALLOWED_HOSTNAMES values are empty placeholders. pnpm run deploy reads those names from the environment and writes them onto the Worker with wrangler deploy --var. In Workers Builds, set them as build variables. A value saved only under Settings → Variables and Secrets is replaced on the next deploy. An empty or invalid base URL makes /mcp return 503.

Create an API token

  1. Sign in to your Investbrain instance.

  2. Open /user/api-tokens.

  3. Create a token and name it, for example MCP.

  4. Copy the plaintext token. Investbrain shows it once.

The API routes use Laravel Sanctum (auth:sanctum) and do not check token abilities. If the form lists permissions, leave them unset or pick any.

Send that token only from the MCP client:

Authorization: Bearer <investbrain-api-token>

There is no token argument on the tools. The Worker reads this header, checks it with GET /api/me, caches a successful check for 60 seconds, and forwards the same header to Investbrain.

MCP client

Streamable HTTP, endpoint path /mcp.

{
  "mcpServers": {
    "investbrain": {
      "type": "http",
      "url": "https://investbrain-mcp.<account-subdomain>.workers.dev/mcp",
      "headers": {
        "Authorization": "Bearer <investbrain-api-token>"
      }
    }
  }
}

The client must send that header on every request to /mcp, including tool calls. A missing or rejected token is HTTP 401. Repeated failures from the same IP are HTTP 429.

The client has to support custom headers. A connector that only offers OAuth cannot send this static bearer token.

/health needs no token.

Security model

  1. The Investbrain token is not a Worker secret, not a GitHub secret, and not written to Durable Object storage, logs, or tool arguments.

  2. Each request carries Authorization: Bearer <token>. The Worker copies that header to Investbrain and drops it when the request ends.

  3. Before the MCP handler runs, the Worker calls GET /api/me with the same header. A successful check is cached in that isolate for 60 seconds, keyed by the SHA-256 hash of the token, with at most 256 entries. The raw token is not stored. The cache is not shared across isolates and is not written to Durable Object storage. Investbrain 401 on the probe becomes HTTP 401 and is not cached. Other upstream failures become HTTP 502 and are not counted as bad tokens. Tool calls still request Investbrain, so an upstream 401 is a tool error even when the probe was cached. A revoked token can pass the gate until that entry expires.

  4. Missing and rejected tokens are counted per CF-Connecting-IP (Cloudflare overwrites a client-supplied value). The default is 10 failures in 15 minutes, then a 30 minute lockout. The counter is a SQLite Durable Object, which the Workers free plan includes. The in-memory cache stores the IP and the lockout expiry, not the token.

  5. Redirects are not followed, so the token is not sent to another host.

  6. INVESTBRAIN_BASE_URL is the address of your Investbrain app. It is not an API key.

Local development

Requires Node.js 22 or newer and pnpm 10.

pnpm install
cp .dev.vars.example .dev.vars
# edit INVESTBRAIN_BASE_URL
pnpm dev
pnpm test
pnpm type-check

pnpm dev serves the Worker on your machine (Wrangler prints the port, usually 8787). Point a client at http://127.0.0.1:8787/mcp with the same Authorization header. .dev.vars overrides the empty placeholders and is gitignored.

From your laptop, INVESTBRAIN_BASE_URL can be an Investbrain app that only your machine can reach. A deployed Worker cannot. The deployed URL has to be reachable from Cloudflare.

Regenerate Worker types after a wrangler.jsonc change with pnpm cf-typegen.

CI

Pull requests and pushes run pnpm install --frozen-lockfile, pnpm type-check, and pnpm test. That workflow has no secrets and does not deploy. The manual Deploy workflow repeats those checks, then runs pnpm run deploy.

Limitations

  1. Transport is stateless Streamable HTTP at /mcp. There is no SSE session. A session would have to keep the API token on the server.

  2. A successful GET /api/me check is reused for 60 seconds in that isolate. tools/call still requests Investbrain.

  3. Clients behind one public IP share the auth-failure lockout.

  4. Deleting a portfolio follows Investbrain and cascades to that portfolio's holdings and transactions.

  5. Market data is whatever that Investbrain instance has configured. Unknown symbols are a 404 from Investbrain, returned as a tool error.

  6. This uses the Workers free plan: the Worker script and one SQLite Durable Object. It does not use Workers AI, Queues, or the Rate Limiting binding.

  7. The default Host check allows workers.dev and localhost. Set ALLOWED_HOSTNAMES to add a custom hostname. Those names are also accepted as browser Origin hosts. Requests with no Origin stay valid.

  8. Dividend date is the entitlement date (B3 data-com). Yahoo's historical dividend date can differ from data-com and from the payment date. These tools do not convert one date into the other.

License

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables MCP clients to interact with Interactive Brokers' official Web API through a Client Portal Gateway, exposing brokerage account, market data, and trading functionality via authenticated HTTP endpoints.
    -
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables MCP clients to interact with Interactive Brokers' Web API for brokerage session management, account information, and trading-related endpoints. It uses the Client Portal Gateway for authentication and supports local or Docker-based deployment.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP clients to read and edit a portfolio journal, including recording buys and sells, managing buy theses and checkable claims, watchlists, tags, and DCA plans, with OAuth or API key authentication.
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP-compatible clients to interact with Interactive Brokers through its official Web API, supporting account management, portfolio reporting, market data, and trading operations.
    -