MCP Agents setup¶
This page is for an agent you build or run yourself: a program that calls the OHLCX MCP server without a person in an editor approving each step. For an editor or a chat client, see Cursor setup and Claude setup.
How this page was checked
The Claude Agent SDK and Messages API sections follow Anthropic's documentation as published on 2026-10-09. The OHLCX side was read from the server's code. The examples were not run against an OHLCX host for this page: where a step says "check your version", confirm it in your own setup before you rely on it.
Not the assistant inside OHLCX
OHLCX has its own in-app assistant, described under Agents and In-app assistant. It runs inside the app with its own tools and does not use MCP. This page is about the opposite direction: an external agent that reaches OHLCX through the MCP server, with the tools in the tool reference.
Choose a transport¶
| Stdio | HTTP | |
|---|---|---|
| How | The agent starts php artisan mcp:start ohlcx in a checkout |
The agent sends POST requests to /mcp/ohlcx on a running OHLCX app |
| Signed-in user | None | The owner of the bearer token |
| Tools | 4: ping, run-support-agent, search-knowledge-base, get-knowledge-base-article |
Everything that user is offered |
| Use it for | A knowledge base agent, or a smoke test of a checkout | Anything that reads or changes a user's strategies, accounts or orders |
An agent that works with a user's data needs HTTP.
Authenticate a headless agent¶
The HTTP endpoint requires a signed-in user. An agent signs in by sending an OHLCX API token on every request:
Authorization: Bearer <token>
How to get a token is on Claude setup. The server has no OAuth sign-in: a request without a valid token answers 401 with WWW-Authenticate: Bearer realm="mcp", error="invalid_token".
What a token is¶
- One token is one user. The agent acts as that user and sees that user's strategies and linked brokerage accounts.
- Tokens carry no scopes that the MCP server reads. Any valid token reaches every tool its user is offered, reads and writes alike. There is no read-only token.
- By default tokens do not expire. A token works until it is revoked.
What follows from that:
- Read-only has to be enforced in your agent, by the tools you allow. See Restrict an agent to read-only tools. The server will not do it for you.
- Treat the token like the user's password. Read it from an environment variable or a secret store at run time. Never write it into the repository, a prompt, a log or a shared configuration file.
- Give each agent its own token, so that you can revoke one agent without stopping the others. Revoke with
POST /api/auth/logout-device, sent with that token as the bearer header. - Do not run an agent with an admin's token unless it needs the admin tools. An admin's token also reaches user administration and billing.
- One agent, one user. Do not put one user's token behind a service that several people use: each of them would act on that user's brokerage accounts.
export OHLCX_MCP_TOKEN="<token>"
The server's rules for agents¶
These are the server's own rules. An unattended agent has to keep them without a person watching.
- No tool places, changes or cancels a broker order directly. A strategy does: one that is deployed, switched on, real (not demo), with orders on and at least one order account that can receive orders. See What leads to broker orders.
- Confirm with the user before switching a strategy or its orders on, changing its settings, or deleting anything.
- Confirm before setting order accounts.
set-strategy-accountsdecides which brokerage accounts receive a strategy's orders. It never switches orders on. - Retain before changing a deployed strategy. A deployed strategy refuses changes with "Retain the strategy before changing it." Call
retain-strategy, make the change, thendeploy-strategy. Switching things off always works. See the deployed lock. - Read
ohlcx://strategy-settingsbefore creating a strategy or changing its settings. - A refusal is an answer, not an obstacle. Do not reach the same result through another tool. See Errors and refusals.
Reads and writes¶
The server sends a hint with each tool, in the tool's annotations: readOnlyHint for a tool that changes nothing, destructiveHint for a write, idempotentHint where repeating a call changes nothing more. Some writes carry no hint: treat a tool without readOnlyHint: true as a write. The tool reference lists every tool's kind.
| Agent | Allow |
|---|---|
| Unattended | Tools of kind Read only |
| With a person who confirms | Reads freely. Each write only after that person has seen the tool and its arguments and said yes |
The hints are the server's description of its tools. They are a sound basis for an allow-list. They are not an enforcement: enforce in your agent.
Claude Agent SDK¶
The Agent SDK runs Claude's agent loop in your own process and connects to MCP servers you configure.
Install it (Node.js 18 or later, or Python 3.10 or later) and set your Anthropic API key:
npm install @anthropic-ai/claude-agent-sdk
pip install claude-agent-sdk
export ANTHROPIC_API_KEY="<your Anthropic API key>"
Connect over HTTP¶
Pass the server in mcpServers (TypeScript) or mcp_servers (Python), with "type": "http" and the token in headers. MCP tools are named mcp__<server>__<tool>, so with the server named ohlcx the tool list-strategies is mcp__ohlcx__list-strategies.
Restrict an agent to read-only tools¶
The SDK allows and denies tools by name. It has no switch for "tools with a read-only hint", so build the allow-list from the tool reference: every tool whose kind is Read. 81 of the 147 tools are.
run-trading-agent is not one of them. It runs an assistant that can change the user's settings and preferences and, for an admin, a user's credits and billing package. Keep it off a read-only allow-list. run-support-agent only reads and is marked read-only.
Two options do the work together:
allowedToolslists the tools that run without asking.permissionMode: "dontAsk"denies every call that would otherwise ask. Without it, a tool that is not on the list is still available and falls through to the permission mode.
Never use bypassPermissions for this: allowedTools does not constrain that mode, and every tool would be approved.
import { query } from "@anthropic-ai/claude-agent-sdk";
const readOnly = [
"mcp__ohlcx__list-strategies",
"mcp__ohlcx__get-strategy",
"mcp__ohlcx__get-strategy-statistics",
"mcp__ohlcx__list-trades",
"mcp__ohlcx__list-accounts",
"mcp__ohlcx__get-accounts-balances",
"mcp__ohlcx__list-orders",
"mcp__ohlcx__get-ticker-bars"
];
for await (const message of query({
prompt: "Summarise how my strategies did this month.",
options: {
mcpServers: {
ohlcx: {
type: "http",
url: "https://ohlcx.example.com/mcp/ohlcx",
headers: {
Authorization: `Bearer ${process.env.OHLCX_MCP_TOKEN}`
}
}
},
allowedTools: readOnly,
permissionMode: "dontAsk"
}
})) {
if (message.type === "system" && message.subtype === "init") {
const unavailable = message.mcp_servers.filter(
(s) => s.status === "failed" || s.status === "needs-auth"
);
if (unavailable.length > 0) {
throw new Error("OHLCX MCP server is not available");
}
}
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
The same in Python:
import asyncio
import os
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, SystemMessage, query
READ_ONLY = [
"mcp__ohlcx__list-strategies",
"mcp__ohlcx__get-strategy",
"mcp__ohlcx__get-strategy-statistics",
"mcp__ohlcx__list-trades",
"mcp__ohlcx__list-accounts",
"mcp__ohlcx__get-accounts-balances",
"mcp__ohlcx__list-orders",
"mcp__ohlcx__get-ticker-bars",
]
async def main():
options = ClaudeAgentOptions(
mcp_servers={
"ohlcx": {
"type": "http",
"url": "https://ohlcx.example.com/mcp/ohlcx",
"headers": {"Authorization": f"Bearer {os.environ['OHLCX_MCP_TOKEN']}"},
}
},
allowed_tools=READ_ONLY,
permission_mode="dontAsk",
)
async for message in query(
prompt="Summarise how my strategies did this month.", options=options
):
if isinstance(message, SystemMessage) and message.subtype == "init":
unavailable = [
s
for s in message.data.get("mcp_servers", [])
if s.get("status") in ("failed", "needs-auth")
]
if unavailable:
raise RuntimeError("OHLCX MCP server is not available")
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
What this agent does, step by step:
- It connects with the user's token. The first message, the
initmessage, reports each server's status.failedorneeds-authmeans the token or the host is wrong: stop, do not continue on built-in tools. A status ofpendingis not a failure. - Claude reads the server's instructions and calls read tools:
list-strategies, thenget-strategy-statisticsfor each one. - If Claude tries a tool that is not on the list, for example
set-strategy-flag, the call is denied and Claude is told so. Nothing reaches OHLCX. - The agent prints the result. Nothing was changed, and nothing could have been.
The SDK also has its own built-in tools (files, shell). dontAsk denies those that would ask, but some reads run without asking. To take tools away entirely, name them in disallowedTools. See Anthropic's permissions page.
A longer list can be kept short with a pattern after the server prefix, for example mcp__ohlcx__list-*. Check each pattern against the tool reference first: a name pattern is not a kind. get- and list- tools are reads in this server; set-, update-, create-, delete-, clear-, deploy-, retain- and run-backtest tools are not.
A write that a person confirms¶
For a write, a person has to see the call and agree. In the SDK that is the canUseTool callback. It runs only for calls that nothing earlier approved, so:
- keep the write tools out of
allowedTools. A tool on that list never reaches the callback; - set
permissionMode: "default"explicitly; - show the person the tool and its arguments, with accounts by their masked labels, and wait.
import { query } from "@anthropic-ai/claude-agent-sdk";
// Replace with your application's own confirmation: a dialog, a chat message, a ticket.
async function askUser(question: string): Promise<boolean> {
throw new Error("Connect this to your own confirmation step");
}
for await (const message of query({
prompt: "Send strategy 7's orders to my account ending 678.",
options: {
mcpServers: {
ohlcx: {
type: "http",
url: "https://ohlcx.example.com/mcp/ohlcx",
headers: {
Authorization: `Bearer ${process.env.OHLCX_MCP_TOKEN}`
}
}
},
allowedTools: [
"mcp__ohlcx__get-strategy",
"mcp__ohlcx__list-strategy-accounts"
],
permissionMode: "default",
canUseTool: async (toolName, input) => {
const approved = await askUser(
`Allow ${toolName} with ${JSON.stringify(input)}?`
);
if (approved) {
return { behavior: "allow", updatedInput: input };
}
return { behavior: "deny", message: "User declined" };
}
}
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
What happens:
- Claude calls
get-strategyandlist-strategy-accounts. Both are reads on the allow-list and run at once. - Claude decides to call
set-strategy-accountswith{"strategy_id": 7, "mode": "selected", "account_ids": [4]}. The tool is not on the allow-list, so the SDK callscanUseTool. - Your application shows the person the call, naming account 4 by its masked label
*****678, and waits. - On yes, the call runs. On no, Claude is told "User declined" and nothing is sent.
- If the strategy is deployed, the server refuses with "Retain the strategy before changing it." Claude then needs
retain-strategy, which is another write and another confirmation.
Setting the accounts does not switch orders on. Switching orders on (set-strategy-flag) and deploying (deploy-strategy) are separate writes, each with its own confirmation. The whole sequence is on Strategies and order routing.
In Python the callback is can_use_tool, and it returns PermissionResultAllow(updated_input=...) or PermissionResultDeny(message=...). Anthropic's example for it uses a streaming prompt and a hook that keeps the stream open: follow Handle approvals and user input for the current form.
Messages API: the MCP connector¶
The Messages API can connect to a remote MCP server itself, with the beta header anthropic-beta: mcp-client-2025-11-20. Whether the OHLCX server qualifies:
| Requirement of the connector | OHLCX server |
|---|---|
Publicly exposed over HTTP; the URL must start with https:// |
Yes, if your OHLCX host is on the public internet with HTTPS. A local or private host does not qualify |
| Streamable HTTP or SSE transport | Yes: Streamable HTTP at /mcp/ohlcx |
| Only tool calls are supported | The tools work. The three prompts and the ohlcx://strategy-settings resource are not available through the connector |
authorization_token, documented as an OAuth token |
The OHLCX server expects its own API token as a bearer header. Check your version: this page did not verify that the connector accepts an OHLCX token in authorization_token |
The connector takes the server in mcp_servers and a toolset in tools. To allow only chosen tools, set enabled to false by default and enable tools by name:
{
"model": "claude-opus-5-5",
"max_tokens": 1000,
"messages": [{ "role": "user", "content": "List my strategies." }],
"mcp_servers": [
{
"type": "url",
"url": "https://ohlcx.example.com/mcp/ohlcx",
"name": "ohlcx",
"authorization_token": "<token>"
}
],
"tools": [
{
"type": "mcp_toolset",
"mcp_server_name": "ohlcx",
"default_config": { "enabled": false },
"configs": {
"list-strategies": { "enabled": true },
"get-strategy": { "enabled": true },
"list-accounts": { "enabled": true }
}
}
]
}
With the connector, Anthropic's servers call the tool during the request. Your code does not see a call before it runs, so there is no place for a person to confirm one. Use the connector for read-only agents, with an allow-list as above. For a write, enable that one tool only in a request your application sends after the user has confirmed, or use the Agent SDK.
Because the resource is not available through the connector, an agent that creates strategies or changes settings this way has no settings reference. Prefer the Agent SDK for that work.
Any other MCP client¶
The server speaks the Model Context Protocol, so any conforming client can use it. In terms of the specification:
| Item | Value |
|---|---|
| Transport | Streamable HTTP: JSON-RPC messages sent with POST to /mcp/ohlcx. GET answers 405 |
| Local transport | stdio: php artisan mcp:start ohlcx |
| Protocol revisions | 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 |
| Authorization | Authorization: Bearer <token> on every request. No OAuth discovery |
| Capabilities | Tools, resources and prompts. The lists do not change during a session |
| Session | When the server returns an MCP-Session-Id header, send it back on later requests |
Three things a client must get right:
- Follow
nextCursorontools/list. The server returns the tool list in pages of 15 by default (at most 50 per page). A client that reads only the first page sees 15 tools. Request the next page with thecursorit was given until there is none. - Read each tool's
annotationsfromtools/listand build your allow-list fromreadOnlyHint. This is where a client can restrict by annotation rather than by name. - Treat a tool result with
isError: trueas a sentence to act on, not as a transport failure. See Errors and refusals.
Refusals and limits¶
- An agent must tell a refusal from a fault. A refusal names what stands in the way; a fault says only that something could not be done. What to do with each kind is on Errors and refusals.
- Retry only the "try again" kind, with a pause and a small fixed number of attempts. Never retry a refusal unchanged.
- List tools page or cap their answers. An agent that needs everything has to follow pages and cursors: see Limits and paging.
list-strategies,list-signalsandlist-conditionscan be up to a minute behind changes made in the OHLCX app.- Account ids come from
list-accounts. Show people masked labels. See Accounts and identifiers.
Checklist before you leave an agent unattended¶
- It uses its own token, read from the environment, for a user who is not an admin.
- Its allow-list holds only tools of kind Read, and calls outside the list are denied, not asked.
- It stops when the server is unavailable or the token is refused.
- It does not retry refusals.
- Nothing it can call switches a strategy or its orders on, sets order accounts, changes settings, deploys, retains or deletes.