Model Context Protocol (MCP)
AuthProxy embeds an MCP server so that AI assistants (Claude, ChatGPT, custom agents) can authenticate, discover capabilities and call platform tools through a single JSON-RPC 2.0 endpoint.
The MCP gateway sits in front of TrexWallet, Chat, Payments, Markets and any custom Core tools — every call goes through the same authentication, rate-limit and audit pipeline as the regular REST API.
Endpoint: POST /mcp
Wire format: JSON-RPC 2.0
Protocol version reported by initialize: 2024-11-05
MCP login produces a programmatic session that intentionally differs from the browser cookie session.
It does not set a browser-compatible sid cookie, and modules may refuse side-effecting browser-only flows on Mcp* sessions.
For PWA debugging or reproducing user-reported bugs use the challenge-response browser flow (/auth/v1/login_options + /auth/v1/login).
Quick start
- Generate an Ed25519 key pair for your AI agent.
- Register the public key for a CRM user via
POST /auth/v1/keys/register_key(or the admin UI at/ProxyAdmin/UserKeys). - Set the
McpAccessflag on the key viaPOST /auth/v1/keys/update_key({ "keyId": "...", "mcpAccess": true }) or the admin UI. - Call
POST /auth/v1/mcp_loginto obtain a session. - Call
POST /mcpwithAuthorization: Bearer <sessionId>.
For unauthenticated public flows (anonymous chat invites, payment-order pages) skip steps 1–4 and use Capability mode.
Authentication
mcp_login uses HTTP headers only — there is no JSON body.
The login level is selected by the combination of headers you send.
| Login type | Required headers | Access |
|---|---|---|
McpBasic | X-PublicKey | Limited read on a curated tool list. Convenient bootstrap, no proof-of-possession. |
McpVerified | X-PublicKey + X-Timestamp + X-Signature | Full personal access. Ed25519 signature proves possession of the private key. |
McpApp | X-PublicKey + X-App-Id | Delegated app access scoped to a single registered application. |
Detailed semantics, threat model and tool eligibility per type: see MCP Access Model.
Headers
| Header | Format | When |
|---|---|---|
X-PublicKey | base64url, 32 bytes (Ed25519 public key) | Always |
X-Timestamp | Unix seconds (integer as string) | McpVerified |
X-Signature | base64url Ed25519 signature, 64 bytes | McpVerified |
X-App-Id | TimeTick of a registered user_app | McpApp |
What you sign for McpVerified
The signature is over the 8-byte little-endian representation of the Unix timestamp (int64), not the decimal string. Sign exactly the same 8 bytes that AuthProxy will reconstruct from X-Timestamp.
Validation rules:
- Timestamp must be within ±5 minutes of server UTC time.
- The public key must be present in the
user_keytable for the corresponding user. - The key must have the
McpAccessflag set. - The user must not be blocked.
Login example — Node.js
import { ed25519 } from '@noble/curves/ed25519';
const privateKey = /* Uint8Array(32) */;
const publicKey = ed25519.getPublicKey(privateKey);
const timestamp = BigInt(Math.floor(Date.now() / 1000));
const tsBytes = new Uint8Array(8);
new DataView(tsBytes.buffer).setBigInt64(0, timestamp, true); // little-endian
const signature = ed25519.sign(tsBytes, privateKey);
const toBase64Url = (bytes) =>
Buffer.from(bytes).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const res = await fetch('https://authproxy.example.com/auth/v1/mcp_login', {
method: 'POST',
headers: {
'X-PublicKey': toBase64Url(publicKey),
'X-Timestamp': timestamp.toString(),
'X-Signature': toBase64Url(signature),
},
});
const { result } = await res.json();
// result.sessionId, result.loginType, result.expiresUtc, result.appId
Login example — Python
import base64, struct, time, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
private_key = Ed25519PrivateKey.from_private_bytes(secret_32_bytes)
public_key = private_key.public_key().public_bytes_raw()
ts = int(time.time())
ts_bytes = struct.pack('<q', ts) # 8-byte little-endian int64
sig = private_key.sign(ts_bytes)
res = requests.post(
'https://authproxy.example.com/auth/v1/mcp_login',
headers={
'X-PublicKey': b64url(public_key),
'X-Timestamp': str(ts),
'X-Signature': b64url(sig),
},
)
result = res.json()['result']
session_id = result['sessionId']
Login example — McpBasic (read-only bootstrap)
curl -X POST https://authproxy.example.com/auth/v1/mcp_login \
-H "X-PublicKey: <base64url-public-key>"
Login example — McpApp (delegated app)
curl -X POST https://authproxy.example.com/auth/v1/mcp_login \
-H "X-PublicKey: <base64url-public-key>" \
-H "X-App-Id: <user_app timetick>"
The user_app must have the McpEnabled flag, and the calling key must belong to the app owner or be the explicit pubkey configured for that app.
Login response
{
"result": {
"sessionId": "abc123…",
"loginType": "McpVerified",
"expiresUtc": "2026-05-13T15:00:00Z",
"appId": null
}
}
Use sessionId as a Bearer token on every subsequent /mcp call:
Authorization: Bearer abc123…
Errors are returned in the standard ApiResponse envelope ({ "error": { "code": …, "message": … } }). Common cases:
| HTTP | error.message | Reason |
|---|---|---|
| 400 | Invalid input format | Malformed key, signature, timestamp or app id |
| 401 | Session not found | Public key not found in the runtime key cache (only UserKey rows are eligible) |
| 401 | Account not found | Key resolved but the owning apguser row is missing |
| 401 | Access denied | Key lacks the McpAccess flag, user blocked, or app not approved / key not authorised for app |
| 401 | Challenge expired | Timestamp outside ±5 minute window |
| 401 | Invalid signature | Signature does not verify against X-PublicKey |
| 429 | Access rate limit | Per-key quota exhausted (see Rate limits) |
JSON-RPC over /mcp
After login, send JSON-RPC 2.0 requests to POST /mcp with the Bearer token.
The server supports the standard MCP method set: initialize, notifications/initialized, tools/list, tools/call, resources/list, resources/read.
initialize
Always call initialize first. It does not require authentication, lets the client negotiate the protocol version, and returns server capabilities.
curl -X POST https://authproxy.example.com/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "my-agent", "version": "1.0.0" }
}
}'
Response includes a Mcp-Session-Id response header (used by transports that want it) and a JSON body:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": { "tools": {}, "resources": {} },
"serverInfo": { "name": "ItBuild AuthProxy MCP", "version": "1.0.0" }
}
}
After initialize, send the standard notifications/initialized notification — the server replies with HTTP 200 and no body.
tools/list
Returns the catalogue of tools available to the current authentication context. Without authentication you receive the full advertised catalogue (so the client can plan a login flow); with authentication the list is filtered to what the session is actually allowed to call.
curl -X POST https://authproxy.example.com/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <sessionId>" \
-d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }'
Each tool has an MCP-standard shape:
{
"name": "wallet_get_balance",
"description": "Get wallet balance (free, locked in/out, orders) by currency or all",
"inputSchema": {
"type": "object",
"properties": {
"currency": { "type": "string", "description": "Currency code …" }
}
}
}
tools/call
curl -X POST https://authproxy.example.com/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <sessionId>" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "wallet_get_balance",
"arguments": { "currency": "USDT" }
}
}'
Successful response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"result\":[{\"currency\":\"USDT\",\"freeBalance\":1000.5}]}"
}
]
}
}
The tool result is the raw backend JSON wrapped in MCP's content[] array. The platform never re-shapes the underlying response — clients see exactly what the REST endpoint returned.
When the underlying REST call returns an ApiResponse error, MCP marks the result with isError: true:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "{\"error\":{\"code\":-11001,\"message\":\"Gate error\"}}" }
],
"isError": true
}
}
JSON-RPC level errors (auth, unknown tool, validation) are returned via the standard error field:
error.code | Meaning |
|---|---|
-32601 | Method not found |
-32602 | Invalid params (missing tool name, unknown tool, invalid capability code) |
-32001 | Unauthorized / forbidden for this session |
-32002 | Resource URI not found |
-32000 | Tool execution error / capability module unavailable |
resources/list and resources/read
Resources expose read-only views of platform state and use stable itbuild://… URIs.
curl -X POST https://authproxy.example.com/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <sessionId>" \
-d '{ "jsonrpc": "2.0", "id": 4, "method": "resources/list" }'
Read a resource:
curl -X POST https://authproxy.example.com/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <sessionId>" \
-d '{
"jsonrpc": "2.0",
"id": 5,
"method": "resources/read",
"params": { "uri": "itbuild://wallet/info" }
}'
The response carries contents[].text and mimeType. Resources also honour the isError: true flag for backend errors.
Tool catalogue (built-in)
AuthProxy ships built-in tools for the platform modules. The exact list visible to a session depends on its login type and the access model.
| Group | Examples |
|---|---|
| AuthProxy | sessions, login_log, user_keys, get_usr_apps, get_app_info, list_app_access, revoke_app_access, close_sessions |
| Wallet (TrexWallet) | wallet_get_info, wallet_get_balance, wallet_get_history_transactions, wallet_get_tx_info, wallet_list_addresses, wallet_internal_transfer, wallet_create_withdraw |
| Markets | market_get_rates, market_swap_info, market_get_trade_info, market_create_swap, get_candles, assets, tokens_networks |
| Trading rooms | room_list, room_create, room_close, room_join |
| Orders | market_get_orders, market_create_order |
| Payment orders (merchant) | payorder_info, payorder_terminals, payorder_payments, payorder_payment, payorder_create |
| Payment gate (capability) | payment_order_info, payment_terminals, payment_pay_card, payment_pay_p2p, payment_cancel_p2p, payment_pay_crypto, payment_confirm_crypto, payment_cancel_crypto |
| Chat (session mode) | chat_list, chat_info, chat_members, chat_messages, chat_unread, chat_pinned, chat_files, chat_file_info, chat_read_text_file, chat_friends, chat_invites, chat_send_message, chat_mark_read, chat_create, chat_create_private, chat_edit_message, chat_soft_delete_message, chat_hard_delete_message, chat_archive_chat, chat_leave_chat, chat_hard_delete_chat, chat_add_member |
| Chat (anonymous capability) | chat_anon_* family |
The authoritative list (with parameters) is what tools/list returns at runtime — it always reflects the deployed version.
Custom tools (Core projects)
Customer Core projects extend the catalogue through appsettings.json:
{
"Config": {
"McpTools": {
"core_get_orders": {
"endpoint": "http://core:80/api/v1/orders/list",
"method": "GET",
"description": "List orders for the current user",
"appScope": false,
"restricted": ["McpBasic"],
"inputSchema": {
"type": "object",
"properties": {
"limit": { "type": "integer", "description": "Max rows (default 50)" }
}
}
}
},
"McpResources": {
"itbuild://core/profile": {
"endpoint": "http://core:80/api/v1/profile",
"name": "Customer profile",
"description": "Profile snapshot for the authenticated user",
"mimeType": "application/json"
}
}
}
}
Fields:
| Field | Purpose |
|---|---|
endpoint | Backend URL the gateway forwards the call to. AuthProxy injects X-Crm, X-Scopes, X-KeyType, X-Project and (for McpApp) X-MCP-App-Id automatically. |
method | GET or POST. For GET all arguments become query parameters; for POST they form the JSON body. |
description | Free-form description shown to MCP clients. |
inputSchema | JSON Schema for arguments (rendered to clients via tools/list). |
restricted | Login types denied for this tool (e.g. ["McpBasic"]). See Access Model. |
appScope | true to allow McpApp sessions to call the tool. Default false. |
queryParams | Names of arguments that should travel as query string instead of JSON body for POST requests. |
bodyRootProperty | Name of an argument whose value replaces the entire request body (useful when the backend expects a raw value). |
capabilityModule | If set ("chat" or "wallet") the tool is only callable in Capability mode. |
capabilityCodeBodyProperty | When in capability mode, automatically inject the capability code into this body field. |
Resources (McpResources) accept the same endpoint, description, mimeType, restricted, appScope, sessionScope, capabilityModule fields plus an optional fixed queryString.
Capability mode
For public, code-based flows (anonymous chat invites, public payment-order pages) AuthProxy supports sessionless MCP calls — there is no mcp_login.
Send the capability code with the request:
- Query string:
POST /mcp?code=<capability_code> - Header:
X-MCP-Code: <capability_code> - JSON-RPC argument:
params.arguments.codeorparams.code
For chat-family codes you must also identify the device:
- Header:
X-Device-Guid: <uuid> - Query string:
device_guid=<uuid> - JSON-RPC argument:
params.arguments.device_guid
Example — list anonymous chat tools and read a message:
curl -X POST "https://authproxy.example.com/mcp?code=<invite_code>" \
-H "Content-Type: application/json" \
-H "X-Device-Guid: <uuid>" \
-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'
Capability mode exposes only the tools/resources whose capabilityModule matches the resolved code family (chat for invite codes, wallet for payment-order codes). Session-scoped tools are not visible.
Rate limits
mcp_login and /mcp calls are rate-limited per public key.
The active quota is configured in admin settings (McpApiRPSLimit).
When the quota is exhausted the server returns HTTP 429 with:
X-RateLimit: s=m;l=10;r=0;t=3
Retry-After: 3
X-RateLimit parts: s = scope (m for MCP), l = window limit, r = remaining, t = seconds until reset. Wait Retry-After seconds before retrying.
Security checklist for AI agent integrations
- Generate the Ed25519 key pair on the agent host. Never share the private key.
- Use
McpAccess-flagged keys exclusively for MCP — keep them separate from browser/PWA keys. - Prefer
McpVerifiedoverMcpBasicfor any flow that mutates state;McpBasicis intentionally limited. - Use
McpAppfor third-party AI integrations so that the operator can revoke a singleuser_appwithout disturbing other agents. - Treat
sessionIdlike any other bearer token. Rotate by callingmcp_loginagain orclose_sessions. - Keep clocks synchronised —
McpVerifiedrejects timestamps outside ±5 minutes. - Pay attention to
isError: trueon tool responses; the JSON-RPC envelope isresulteven when the underlying call failed.
Related
- MCP Access Model — what each login type can call and why.
- Reverse Proxy — how
/mcpfits into the AuthProxy routing pipeline. - Session Management — common session lifecycle for browser and MCP clients.
- MCP specification — upstream protocol reference.
- JSON-RPC 2.0 — wire format used by
/mcp.