use super::{
schemas::{ToolSchema, object_schema, safe_output_schema},
tools::{
approvals::approvals_create_schema,
order_groups::{
bracket_order_preview_schema, live_bracket_order_submit_schema,
paper_bracket_order_submit_schema,
},
order_preview::order_preview_schema,
orders_live::{
live_order_cancel_schema, live_order_modify_schema, live_order_submit_schema,
},
orders_paper::{
paper_order_cancel_schema, paper_order_modify_schema, paper_order_submit_schema,
},
},
};
use crate::internal::auth::{
ACCOUNTS_READ, AUDIT_EXPORT, AUDIT_READ, CALENDAR_READ, CURRENCY_READ, FUNDAMENTALS_READ,
HEALTH_READ, MARKETDATA_DEPTH_READ, MARKETDATA_READ, NEWS_READ, OPTIONS_READ, ORDERS_READ,
PORTFOLIO_READ, POSITIONS_READ, RISK_READ, SCANNER_READ, ScopeSet, TRANSFERS_READ,
};
use crate::internal::domain::{ErrorCode, GatewayError};
use std::sync::OnceLock;
static BASE_BROKER_TOOL_SCHEMAS: OnceLock<Vec<ToolSchema>> = OnceLock::new();
pub const FORBIDDEN_TOOL_NAMES: &[&str] = &[
"ibkr_order_intent_validate",
"ibkr_order_preview_explain",
"ibkr_order_submit",
"ibkr_order_cancel",
"ibkr_order_modify",
"ibkr_order_approve",
];
#[must_use]
pub fn broker_tool_schemas() -> Vec<ToolSchema> {
broker_tool_schemas_ref().to_vec()
}
#[must_use]
pub fn broker_tool_schemas_ref() -> &'static [ToolSchema] {
base_broker_tool_schemas()
}
#[must_use]
pub fn broker_tool_schemas_with_live(live_enabled: bool) -> Vec<ToolSchema> {
let mut tools = broker_tool_schemas();
if live_enabled {
tools.push(live_order_submit_schema());
tools.push(live_order_cancel_schema());
tools.push(live_order_modify_schema());
tools.push(live_bracket_order_submit_schema());
}
tools
}
#[must_use]
pub fn local_tool_schemas() -> Vec<ToolSchema> {
let mut tools = broker_tool_schemas();
tools.push(order_preview_schema());
tools.push(bracket_order_preview_schema());
tools.push(paper_order_submit_schema());
tools.push(paper_order_cancel_schema());
tools.push(paper_order_modify_schema());
tools.push(paper_bracket_order_submit_schema());
tools.push(live_order_submit_schema());
tools.push(live_order_cancel_schema());
tools.push(live_order_modify_schema());
tools.push(live_bracket_order_submit_schema());
tools.push(approvals_create_schema());
tools
}
#[must_use]
pub fn local_tool_schemas_for_scopes(scopes: &ScopeSet) -> Vec<ToolSchema> {
local_tool_schemas()
.into_iter()
.filter(|tool| scopes.contains(&tool.scope))
.collect()
}
#[must_use]
pub fn broker_tool_schema_count() -> usize {
broker_tool_schemas_ref().len()
}
#[must_use]
pub fn find_broker_tool_schema(name: &str) -> Option<&'static ToolSchema> {
broker_tool_schemas_ref()
.iter()
.find(|tool| tool.name == name)
}
#[must_use]
pub fn find_local_tool_schema(name: &str) -> Option<ToolSchema> {
if let Some(tool) = find_broker_tool_schema(name) {
return Some(tool.clone());
}
local_tool_schemas()
.into_iter()
.find(|tool| tool.name == name)
}
#[must_use]
pub fn find_broker_tool_schema_with_live(name: &str, live_enabled: bool) -> Option<ToolSchema> {
if let Some(tool) = find_broker_tool_schema(name) {
return Some(tool.clone());
}
if !live_enabled {
return None;
}
broker_tool_schemas_with_live(true)
.into_iter()
.find(|tool| tool.name == name)
}
fn base_broker_tool_schemas() -> &'static [ToolSchema] {
BASE_BROKER_TOOL_SCHEMAS.get_or_init(|| {
vec![
tool("ibkr_health", HEALTH_READ, &[]),
tool("ibkr_backend_status", HEALTH_READ, &[]),
tool("ibkr_session_requirements", HEALTH_READ, &[]),
tool("ibkr_session_renew", HEALTH_READ, &[]),
tool("ibkr_kill_switch_status", HEALTH_READ, &[]),
tool("ibkr_accounts_list", ACCOUNTS_READ, &[]),
tool("ibkr_account_metadata", ACCOUNTS_READ, &["account_id"]),
tool("ibkr_account_summary", PORTFOLIO_READ, &["account_id"]),
tool("ibkr_pnl_daily", PORTFOLIO_READ, &["account_id"]),
tool("ibkr_pnl_realtime", PORTFOLIO_READ, &["account_id"]),
tool("ibkr_positions_list", POSITIONS_READ, &["account_id"]),
tool("ibkr_portfolio_snapshot", PORTFOLIO_READ, &["account_id"]),
tool("ibkr_contracts_search", MARKETDATA_READ, &["query"]),
tool("ibkr_contract_resolve", MARKETDATA_READ, &["symbol"]),
tool("ibkr_market_snapshot", MARKETDATA_READ, &["contract_id"]),
tool(
"ibkr_historical_bars",
MARKETDATA_READ,
&["contract_id", "duration", "bar_size"],
),
tool("ibkr_options_chain", OPTIONS_READ, &["symbol"]),
tool("ibkr_option_greeks", OPTIONS_READ, &["contract_id"]),
tool("ibkr_market_depth", MARKETDATA_DEPTH_READ, &["contract_id"]),
tool("ibkr_scanner_run", SCANNER_READ, &["scanner_code"]),
tool("ibkr_news_list", NEWS_READ, &["symbol"]),
tool("ibkr_news_article", NEWS_READ, &["article_id"]),
tool("ibkr_fundamentals_get", FUNDAMENTALS_READ, &["symbol"]),
tool("ibkr_market_session", CALENDAR_READ, &["exchange"]),
tool("ibkr_market_holidays", CALENDAR_READ, &["exchange"]),
tool("ibkr_currency_rate", CURRENCY_READ, &["base", "quote"]),
tool("ibkr_transfer_history", TRANSFERS_READ, &["account_id"]),
tool("ibkr_orders_list", ORDERS_READ, &["account_id"]),
tool("ibkr_orders_history", ORDERS_READ, &["account_id"]),
tool(
"ibkr_order_status",
ORDERS_READ,
&["account_id", "broker_order_id"],
),
tool("ibkr_executions_list", ORDERS_READ, &["account_id"]),
tool("ibkr_limits_status", RISK_READ, &["account_id"]),
tool("ibkr_audit_tail", AUDIT_READ, &["limit"]),
tool("ibkr_audit_export", AUDIT_EXPORT, &["limit"]),
]
})
}
#[must_use]
pub fn is_forbidden_tool_name(name: &str) -> bool {
FORBIDDEN_TOOL_NAMES.contains(&name)
}
pub fn refuse_forbidden_tool(name: &str) -> GatewayError {
GatewayError::new(
ErrorCode::ReadonlyWriteForbidden,
format!("MCP tool {name} is forbidden; use explicit preview, paper, or live-gated tools"),
false,
Some("Use a later feature spec for preview or trading".to_string()),
)
}
#[must_use]
pub const fn audit_scope() -> &'static str {
AUDIT_READ
}
fn tool(name: &str, scope: &str, required: &[&str]) -> ToolSchema {
ToolSchema {
name: name.to_string(),
scope: scope.to_string(),
input_schema: object_schema(required),
output_schema: safe_output_schema(),
}
}