Three FastMCP servers that expose the OpenWealth APIs as MCP tools, so an AI assistant (Claude, Cursor, any MCP client) can read custody data, manage orders, and onboard customers through a typed, audited interface instead of raw HTTP.
| Server | API | Console script | Tools |
|---|---|---|---|
| Custody | Custody Services v3.2.0 — customers, accounts, positions, transactions | openwealth-custody-mcp |
10 (read-only) |
| Trading | Order Placement v3.0.1 — orders, executions, quotes, subscriptions | openwealth-trading-mcp |
18 (full lifecycle) |
| Customer | Customer Management v2.0.6 — customers, persons, contacts, addresses, documents, KYC | openwealth-customer-mcp |
26 (full lifecycle) |
All three ship in one Python package and speak MCP over stdio. OpenAPI specs are vendored from OpenWealth and served as sanitized MCP resources (server URLs and auth schemes are stripped before the spec is exposed to the LLM).
⚠️ Trading places real orders.create_orderis a financially consequential tool. Read Safety model before pointing it at anything other than the sandbox.
pip install openwealth-mcpRequires Python ≥ 3.11 and a bearer token for an OpenWealth endpoint (sandbox or custodian).
To install from source for development:
git clone https://github.com/OpenWealth/OpenWealth-MCP.git
cd OpenWealth-MCP
pip install -e ".[dev]" # or: uv sync --extra devConfigure each server separately — each needs its own base URL and token.
Claude Desktop / Claude Code (claude_desktop_config.json):
{
"mcpServers": {
"openwealth-custody": {
"command": "openwealth-custody-mcp",
"env": {
"OPENWEALTH_CUSTODY_BASE_URL": "https://api.openwealth.synpulse8.com/api/custody-services/v3",
"OPENWEALTH_BEARER_TOKEN": "<jwt>"
}
},
"openwealth-trading": {
"command": "openwealth-trading-mcp",
"env": {
"OPENWEALTH_TRADING_BASE_URL": "https://<host>/api/trading-services/v1",
"OPENWEALTH_BEARER_TOKEN": "<jwt>"
}
},
"openwealth-customer": {
"command": "openwealth-customer-mcp",
"env": {
"OPENWEALTH_CUSTOMER_MANAGEMENT_BASE_URL": "https://api.openwealth.synpulse8.com/api/customer-management/v2",
"OPENWEALTH_BEARER_TOKEN": "<jwt>"
}
}
}
}Cursor (.cursor/mcp.json, or Settings → MCP for a global server):
{
"mcpServers": {
"openwealth-custody": {
"command": "openwealth-custody-mcp",
"env": {
"OPENWEALTH_CUSTODY_BASE_URL": "https://api.openwealth.synpulse8.com/api/custody-services/v3",
"OPENWEALTH_BEARER_TOKEN": "<jwt>"
}
},
"openwealth-trading": {
"command": "openwealth-trading-mcp",
"env": {
"OPENWEALTH_TRADING_BASE_URL": "https://<host>/api/trading-services/v1",
"OPENWEALTH_BEARER_TOKEN": "<jwt>"
}
},
"openwealth-customer": {
"command": "openwealth-customer-mcp",
"env": {
"OPENWEALTH_CUSTOMER_MANAGEMENT_BASE_URL": "https://api.openwealth.synpulse8.com/api/customer-management/v2",
"OPENWEALTH_BEARER_TOKEN": "<jwt>"
}
}
}
}Then run MCP: Restart Servers from the command palette and check that the server shows as connected. Configure only the domain you have access to.
Sandbox JWTs often expire in about 60 seconds — refresh from the portal and
restart the server. Supply the token without the Bearer prefix; the
client adds it.
Full client wiring, WSL specifics and troubleshooting live in
docs/mcp-clients.md and
docs/local-dev.md.
All settings are environment variables prefixed OPENWEALTH_. A .env file in
the working directory is loaded automatically (and is gitignored — never commit
one). Host and credentials are never hard-coded.
| Variable | Default | Description |
|---|---|---|
OPENWEALTH_CUSTODY_BASE_URL |
— | Custody API base URL including the path prefix, no trailing slash. Required when running the Custody server. https:// is added if the scheme is missing. |
OPENWEALTH_TRADING_BASE_URL |
— | Trading API base URL including the path prefix, no trailing slash. Required when running the Trading server. |
OPENWEALTH_CUSTOMER_MANAGEMENT_BASE_URL |
— | Customer Management API base URL including the path prefix, no trailing slash. Required when running the Customer Management server. |
OPENWEALTH_BEARER_TOKEN |
— | Access token without the Bearer prefix. Required unless OPENWEALTH_AUTH_HEADER is set. |
OPENWEALTH_AUTH_HEADER |
— | Full Authorization header value; overrides OPENWEALTH_BEARER_TOKEN. |
OPENWEALTH_CORRELATION_ID |
generated | Fixed correlation id; otherwise a UUID per request. |
OPENWEALTH_TIMEOUT_SECONDS |
120 |
Read/write timeout in seconds (max 300). |
OPENWEALTH_CONNECT_TIMEOUT_SECONDS |
10 |
Connect timeout in seconds (max 120). |
OPENWEALTH_MAX_RETRIES |
2 |
Retries for transient failures (0–5). See Safety model. |
OPENWEALTH_VERIFY_TLS |
true |
Set to false only against a sandbox with a self-signed certificate. |
OPENWEALTH_LOG_LEVEL |
INFO |
DEBUG, INFO, WARNING, or ERROR. JSON logs on stderr. |
OPENWEALTH_LOG_FILE |
— | Optional log file path (logs to both file and stderr). |
Startup fails fast if neither OPENWEALTH_BEARER_TOKEN nor OPENWEALTH_AUTH_HEADER is set.
The URL for each server is validated at startup — a missing URL prints a clear error and exits.
Transport is stdio only. HTTP transport and JWKS ingress are out of scope in favour of a smaller surface.
Every response is JSON with status_code, correlation_id and data
(plus next_cursor on paginated GETs). Errors return
{"error": true, "error_code": "...", "message": "...", "retryable": bool} — never a stack trace.
| Tool | operationId | Endpoint |
|---|---|---|
get_customers |
getCustomers |
GET /customers |
get_customers_by_customer_id |
getCustomersByCustomerId |
GET /customers/{customerId} |
get_customer_accounts_by_customer_id |
getCustomerAccountsByCustomerId |
GET /customers/{customerId}/accounts |
get_customer_account_by_id |
getCustomerAccountById |
GET /customers/{customerId}/accounts/{accountId} |
get_customer_position_by_customer_id |
getCustomerPositionByCustomerId |
GET /customers/{customerId}/positions |
get_customer_position_by_id |
getCustomerPositionById |
GET /customers/{customerId}/positions/{positionId} |
get_account_position_by_account_id |
getAccountPositionByAccountId |
GET /accounts/{accountId}/positions |
get_account_position_by_id |
getAccountPositionById |
GET /accounts/{accountId}/positions/{positionId} |
get_transaction_by_customer_id |
getTransactionByCustomerId |
GET /customers/{customerId}/transactions |
get_transaction_by_transaction_id |
getTransactionByTransactionId |
GET /customers/{customerId}/transactions/{transactionId} |
Listing positions and transactions requires a date (YYYY-MM-DD) — the
OpenAPI spec makes it mandatory.
| Tool | operationId | Endpoint |
|---|---|---|
list_customers |
listCustomers |
GET /customers |
get_customer |
getCustomer |
GET /customers/{customerId} |
list_accounts |
listAccounts |
GET /accounts |
get_account |
getAccount |
GET /accounts/{accountId} |
list_orders |
listOrders |
GET /orders |
get_order |
getOrder |
GET /orders/{orderId} |
create_order |
createOrder |
POST /orders |
cancel_order |
actionCancelOrder |
POST /orders/{orderId}/actions/cancel |
list_order_executions |
listOrderExecutions |
GET /orders/{orderId}/executions |
get_order_execution |
getOrderExecution |
GET /orders/{orderId}/executions/{executionId} |
list_order_states |
listOrderStates |
GET /orders/{orderId}/states |
create_quote |
createQuote |
POST /quotes |
list_event_subscriptions |
listEventSubscriptions |
GET /event-subscriptions |
get_event_subscription |
getEventSubscription |
GET /event-subscriptions/{eventSubscriptionId} |
create_event_subscription |
createEventSubscription |
POST /event-subscriptions |
update_event_subscription |
updateEventSubscription |
PUT /event-subscriptions/{eventSubscriptionId} |
delete_event_subscription |
deleteEventSubscription |
DELETE /event-subscriptions/{eventSubscriptionId} |
list_event_subscription_notifications |
listEventSubscriptionEventNotifications |
GET /event-subscriptions/{eventSubscriptionId}/event-notifications |
| Tool | operationId | Endpoint |
|---|---|---|
get_customers |
getCustomers |
GET /customers |
create_customer |
postCustomer |
POST /customer-details |
get_customer |
getCustomerByCustomerId |
GET /customers/{customerId} |
get_customer_details |
getCustomerDetailsByCustomerId |
GET /customers/{customerId}/customer-details |
get_persons |
getPersons |
GET /customers/{customerId}/persons |
create_person |
postPerson |
POST /customers/{customerId}/person-details |
get_person |
getPersonByPersonId |
GET /customers/{customerId}/persons/{personId} |
get_person_details |
getPersonDetailsByPersonId |
GET /customers/{customerId}/person-details/{personId} |
get_contacts |
getContactDetailsByCustomerId |
GET /customers/{customerId}/persons/{personId}/contacts |
create_contact |
postContactDetailsByCustomerId |
POST /customers/{customerId}/persons/{personId}/contacts |
get_contact |
getContactDetailsByContactDetailID |
GET /customers/{customerId}/persons/{personId}/contacts/{contactId} |
update_contact |
putContactDetailByContactDetailId |
PUT /customers/{customerId}/persons/{personId}/contacts/{contactId} |
delete_contact |
deleteContactDetailsByContactDetailID |
DELETE /customers/{customerId}/persons/{personId}/contacts/{contactId} |
get_addresses |
getAddressByCustomerId |
GET /customers/{customerId}/persons/{personId}/addresses |
create_address |
postAddressByCustomerId |
POST /customers/{customerId}/persons/{personId}/addresses |
get_address |
getAddressByAddressId |
GET /customers/{customerId}/persons/{personId}/addresses/{addressId} |
update_address |
putAddressByCustomerId |
PUT /customers/{customerId}/persons/{personId}/addresses/{addressId} |
get_documents |
getDocumentsByCustomerId |
GET /customers/{customerId}/documents |
create_document |
postDocumentByCustomerId |
POST /customers/{customerId}/document-details |
get_document |
getDocumentByDocumentId |
GET /customers/{customerId}/documents/{documentId} |
get_document_details |
getDocumentDetailsByDocumentId |
GET /customers/{customerId}/documents/{documentId}/document-details |
get_kyc |
getKycByCustomerId |
GET /customers/{customerId}/persons/{personId}/kyc |
create_kyc |
postKyc |
POST /customers/{customerId}/persons/{personId}/kyc |
create_prospect_precheck |
postProspect |
POST /prospect-precheck |
get_prospect_precheck |
getPreCheck |
GET /prospect-precheck/{temporaryId} |
get_status |
getStatusByTemporaryId |
GET /status/{temporaryId} |
customer, person, document, kyc) — records that are hard to reverse and carry KYC/AML implications. Write tools on softer, mutable data (contact, address, prospect_precheck) are not marked because they can be freely updated or deleted without compliance impact. All Customer Management write tools are still never retried regardless of the
| URI | Content |
|---|---|
openwealth://specs/custody |
Custody tool/endpoint table (Markdown) |
openwealth://specs/custody.yaml |
Custody OpenAPI schema reference (server URLs and auth stripped) |
openwealth://specs/trading |
Trading tool/endpoint table with financial-impact notes |
openwealth://specs/trading.yaml |
Trading OpenAPI schema reference (server URLs and auth stripped) |
openwealth://specs/customer |
Customer Management tool/endpoint table with write-impact notes |
openwealth://specs/customer.yaml |
Customer Management OpenAPI schema reference (server URLs and auth stripped) |
create_orderis never retried. A duplicate order is a financial error, so a failed POST fails loudly rather than silently placing a second one.- Customer Management write tools are never retried.
create_customer,create_person,create_contact,create_address,create_document,create_kycandcreate_prospect_precheckare not retried to avoid duplicate records at the custody bank. - Idempotent writes are retried on 5xx:
cancel_order,create_quote,update_contact,update_address,PUTandDELETE. GETs retry on 408, 429 and 5xx with exponential backoff and ±20% jitter. TheRetry-Afterheader is honoured. - Tool annotations (
readOnlyHint,destructiveHint,idempotentHint) are set per tool, so MCP clients can require confirmation for writes. - Tokens are never tool arguments. They come from the environment only, and are never echoed in tool results or logs.
- Custody is read-only by design. Only the Trading and Customer servers can mutate state.
Always confirm order details with a human before calling create_order.
With uv (recommended):
uv sync --extra dev
uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy
uv run pytest -qWith pip:
pip install -e ".[dev]"
ruff check src tests
ruff format --check src tests
mypy
pytest -qThe same four gates run in CI on every push and pull request across Python 3.11,
3.12 and 3.13. pre-commit install wires ruff and mypy into your commits.
See CONTRIBUTING.md for full contribution guidelines.
| Where | What |
|---|---|
CHANGELOG.md |
Release history |
CONTRIBUTING.md |
Dev setup and contribution guidelines |
docs/mcp-clients.md |
MCP client configuration (Claude, Cursor, WSL) |
docs/local-dev.md |
Local development setup |
docs/release.md |
Release and publishing process |
specs/ |
Vendored OpenAPI definitions |
SECURITY.md |
Vulnerability reporting and secret handling |
Custody is read-only (GET). Trading covers the full order lifecycle (GET, POST, PUT, DELETE). Customer Management covers the full customer onboarding and lifecycle (GET, POST, PUT, DELETE). HTTP transport, JWKS ingress and a central MCP gateway are out of scope.