smartoption-mcp 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smartoption_mcp-0.1.1/PKG-INFO +192 -0
- smartoption_mcp-0.1.1/README.md +170 -0
- smartoption_mcp-0.1.1/pyproject.toml +37 -0
- smartoption_mcp-0.1.1/setup.cfg +4 -0
- smartoption_mcp-0.1.1/smartoption_mcp/__init__.py +0 -0
- smartoption_mcp-0.1.1/smartoption_mcp/client.py +166 -0
- smartoption_mcp-0.1.1/smartoption_mcp/diff.py +105 -0
- smartoption_mcp-0.1.1/smartoption_mcp/server.py +436 -0
- smartoption_mcp-0.1.1/smartoption_mcp.egg-info/PKG-INFO +192 -0
- smartoption_mcp-0.1.1/smartoption_mcp.egg-info/SOURCES.txt +12 -0
- smartoption_mcp-0.1.1/smartoption_mcp.egg-info/dependency_links.txt +1 -0
- smartoption_mcp-0.1.1/smartoption_mcp.egg-info/entry_points.txt +2 -0
- smartoption_mcp-0.1.1/smartoption_mcp.egg-info/requires.txt +2 -0
- smartoption_mcp-0.1.1/smartoption_mcp.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartoption-mcp
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: MCP server exposing the smartoption-ai customer auto-trade API as tools for AI agents (Claude Desktop / Claude Code / any MCP client).
|
|
5
|
+
Author-email: SmartOption <service.smartoption@gmail.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/SmartOption/smartoption-ai
|
|
8
|
+
Project-URL: Repository, https://github.com/SmartOption/smartoption-ai
|
|
9
|
+
Project-URL: Issues, https://github.com/SmartOption/smartoption-ai/issues
|
|
10
|
+
Keywords: mcp,smartoption,claude,auto-trade,copy-trading
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: mcp>=1.2.0
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
|
|
23
|
+
# smartoption-mcp
|
|
24
|
+
|
|
25
|
+
MCP server that exposes the smartoption-ai **customer auto-trade API** as tools
|
|
26
|
+
for AI agents (Claude Desktop, Claude Code, anything that speaks MCP).
|
|
27
|
+
|
|
28
|
+
Phase 1 is **read-only**: 5 tools covering copy rules, virtual lots, agent
|
|
29
|
+
status, parsed signal history (大单/喊单历史), and copy-run logs. Write
|
|
30
|
+
operations (creating/modifying rules, placing orders) will be added in
|
|
31
|
+
Phase 2 once the read path is battle-tested.
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Claude / Agent
|
|
37
|
+
│ MCP stdio
|
|
38
|
+
▼
|
|
39
|
+
smartoption-mcp ──HTTP + Bearer JWT──▶ backend /api/customer/auto-trade/*
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The server is a thin wrapper: each tool maps 1:1 (or close) to a customer
|
|
43
|
+
API endpoint, authenticated with a user JWT loaded from env at startup.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
### End-user (recommended)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install smartoption-mcp
|
|
51
|
+
# or, in an isolated venv:
|
|
52
|
+
python3.11 -m venv ~/.smartoption-mcp && ~/.smartoption-mcp/bin/pip install smartoption-mcp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`smartoption-mcp` is published on [PyPI](https://pypi.org/project/smartoption-mcp/).
|
|
56
|
+
This installs the `smartoption-mcp` CLI entry point. Upgrade with
|
|
57
|
+
`pip install -U smartoption-mcp`.
|
|
58
|
+
|
|
59
|
+
### Local dev (from this repo)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd mcp-server
|
|
63
|
+
python3.11 -m venv .venv
|
|
64
|
+
source .venv/bin/activate
|
|
65
|
+
pip install -e .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configure auth
|
|
69
|
+
|
|
70
|
+
**Recommended: long-lived API token** (1-year expiry, revocable any time).
|
|
71
|
+
Log into [portal.smartoption.ai](https://portal.smartoption.ai) → 个人中心
|
|
72
|
+
→ "API Token(用于 MCP / 脚本)" → 起个名字 → 复制生成的 token。
|
|
73
|
+
管理页面随时可以吊销。
|
|
74
|
+
|
|
75
|
+
**Fallback: session JWT** — if the API tokens page isn't deployed yet,
|
|
76
|
+
open DevTools → Application → Local Storage → copy the `access_token`
|
|
77
|
+
field. Expires in ~24h so you'll re-paste daily.
|
|
78
|
+
|
|
79
|
+
Either way, the token goes into `SMARTOPTION_JWT`. Set
|
|
80
|
+
`SMARTOPTION_API_BASE=https://api.smartoption.ai`.
|
|
81
|
+
|
|
82
|
+
Smoke test from a shell:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
export SMARTOPTION_API_BASE="https://api.smartoption.ai"
|
|
86
|
+
export SMARTOPTION_JWT="eyJ..." # no "Bearer " prefix
|
|
87
|
+
.venv/bin/python -c "from smartoption_mcp.client import SmartoptionClient; \
|
|
88
|
+
print(SmartoptionClient().list_agents())"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Register with Claude Code / Claude Desktop
|
|
92
|
+
|
|
93
|
+
**Claude Code** (this repo's primary host) — edit `~/.claude.json` and add
|
|
94
|
+
under the top-level `mcpServers` key:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"smartoption": {
|
|
100
|
+
"command": "smartoption-mcp",
|
|
101
|
+
"env": {
|
|
102
|
+
"SMARTOPTION_API_BASE": "https://api.smartoption.ai",
|
|
103
|
+
"SMARTOPTION_JWT": "eyJ..."
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Restart Claude Code (or start a new session). The 5 `smartoption` tools
|
|
111
|
+
should appear in the tool list.
|
|
112
|
+
|
|
113
|
+
**Claude Desktop** — same JSON shape, but the file lives at
|
|
114
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`. Restart
|
|
115
|
+
the app after editing.
|
|
116
|
+
|
|
117
|
+
Once registered, you should be able to ask things like:
|
|
118
|
+
|
|
119
|
+
- "查一下最近一周苹果相关的喊单"
|
|
120
|
+
- "我现在的虚拟仓有哪些标的?"
|
|
121
|
+
- "我的跟单 agent 在线吗?最近一次心跳是什么时候?"
|
|
122
|
+
- "今天 agent 跑了几条信号,有没有被跳过的?"
|
|
123
|
+
|
|
124
|
+
## Tools
|
|
125
|
+
|
|
126
|
+
### Read (Phase 1)
|
|
127
|
+
|
|
128
|
+
| Tool | Endpoint | Purpose |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `list_copy_rules` | `GET /copy-rules` | Current copy-trading rules |
|
|
131
|
+
| `list_virtual_lots` | `GET /virtual-ledger` | Open virtual lots + qty_by_key |
|
|
132
|
+
| `get_agent_status` | `GET /agents` | Per-user agent online status |
|
|
133
|
+
| `query_signal_history` | `GET /signal-history` | Search parsed alerts (大单) |
|
|
134
|
+
| `list_run_logs` | `GET /copy-run-logs` | Per-signal execution outcomes |
|
|
135
|
+
|
|
136
|
+
### Write (Phase 2)
|
|
137
|
+
|
|
138
|
+
| Tool | Endpoint | Purpose |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `update_copy_rules` | `PUT /copy-rules` | Replace full copy-trading settings (rules + toggles) |
|
|
141
|
+
| `ensure_agent` | `POST /agents/ensure` | Create / provision the user's agent k8s pod |
|
|
142
|
+
| `refresh_broker_snapshot` | `POST /broker-account-snapshot/refresh` | Ask agent to push fresh broker snapshot |
|
|
143
|
+
|
|
144
|
+
### Phase 2 extension (broker config + agent ops + signal detail + quant)
|
|
145
|
+
|
|
146
|
+
| Tool | Endpoint | Purpose |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `list_broker_configurations` | `GET /broker-configurations` | All saved brokers + active (no creds returned) |
|
|
149
|
+
| `get_active_broker` | `GET /broker-configurations` | Just the active broker name + display name |
|
|
150
|
+
| `set_active_broker` | `PUT /active-broker` | Switch active broker — dry-run gated |
|
|
151
|
+
| `test_broker_connection` | `POST /broker-configurations/test-{ib,tiger,futu,moomoo}` | Verify creds against broker; does NOT save |
|
|
152
|
+
| `restart_agent_client` | `POST /agents/<id>/restart-client` | Restart in-pod agent process — dry-run gated |
|
|
153
|
+
| `stop_agent` | `POST /agents/<id>/stop` | Scale agent deployment to 0 — dry-run gated |
|
|
154
|
+
| `get_agent_logs` | `GET /agents/<id>/logs` | Tail recent agent-pod stdout |
|
|
155
|
+
| `get_signal_detail` | `GET /signal-history/<id>` | Full SignalForwardRecord for one signal |
|
|
156
|
+
| `get_signal_chain` | `GET /signal-history/<id>/chain-slots` | Paired/chained signal context (ROLL_UP pairs etc.) |
|
|
157
|
+
| `list_virtual_followers` | `GET /api/customer/virtual-followers` | User's virtual-follower accounts + equity summary |
|
|
158
|
+
| `list_quant_strategies` | `GET /api/customer/quant-strategies/list` | Available / subscribed quant strategies |
|
|
159
|
+
| `get_quant_strategy_snapshot` | `GET /api/customer/quant-strategies/<id>/snapshot` | One strategy's positions + canonical valuation |
|
|
160
|
+
|
|
161
|
+
**Confirmation model.** Destructive writes use a `confirmed: bool` flag,
|
|
162
|
+
defaulting to `False` for **dry-run**:
|
|
163
|
+
|
|
164
|
+
- `update_copy_rules(new_settings, confirmed=False)` → server fetches
|
|
165
|
+
current settings, returns a structured diff (top-level toggle changes +
|
|
166
|
+
rules added / removed / modified, keyed by `rule_id`). Nothing is
|
|
167
|
+
written. The model is instructed (via docstring) to show the diff to
|
|
168
|
+
the user and only call again with `confirmed=True` after approval.
|
|
169
|
+
- `ensure_agent(force_redeploy=True, confirmed=False)` → returns a
|
|
170
|
+
description of the redeploy without doing it. Default `force_redeploy=False`
|
|
171
|
+
is idempotent and skips the dry-run gate.
|
|
172
|
+
- `refresh_broker_snapshot` → no gate (non-destructive async request).
|
|
173
|
+
|
|
174
|
+
The host (Claude Code / Desktop) also shows tool-call arguments to the
|
|
175
|
+
user before each call, so `confirmed=True` is always visible in the
|
|
176
|
+
approval UI — the in-tool `confirmed` flag is a second belt on top of
|
|
177
|
+
that suspenders.
|
|
178
|
+
|
|
179
|
+
**Validation.** `update_copy_rules` rejects partial settings: the proposed
|
|
180
|
+
doc must contain `auto_buy_enabled`, `auto_sell_enabled`,
|
|
181
|
+
`use_all_matched_rules`, and `rules`. The model is expected to start from
|
|
182
|
+
`list_copy_rules`, mutate locally, and pass the full doc back —
|
|
183
|
+
preserving every rule's `rule_id` so the diff stays stable.
|
|
184
|
+
|
|
185
|
+
## Roadmap
|
|
186
|
+
|
|
187
|
+
- **Phase 3 (✅ Phase 3a done)**: long-lived API tokens managed from the
|
|
188
|
+
customer portal — eliminates daily JWT re-paste, no protocol changes.
|
|
189
|
+
Tokens are still pasted into env; Phase 3b would be full MCP OAuth 2.1
|
|
190
|
+
with a remote HTTP MCP server so other users can connect from their own
|
|
191
|
+
Claude install without copy-paste. Deferred until there's a real
|
|
192
|
+
multi-user use case.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# smartoption-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that exposes the smartoption-ai **customer auto-trade API** as tools
|
|
4
|
+
for AI agents (Claude Desktop, Claude Code, anything that speaks MCP).
|
|
5
|
+
|
|
6
|
+
Phase 1 is **read-only**: 5 tools covering copy rules, virtual lots, agent
|
|
7
|
+
status, parsed signal history (大单/喊单历史), and copy-run logs. Write
|
|
8
|
+
operations (creating/modifying rules, placing orders) will be added in
|
|
9
|
+
Phase 2 once the read path is battle-tested.
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Claude / Agent
|
|
15
|
+
│ MCP stdio
|
|
16
|
+
▼
|
|
17
|
+
smartoption-mcp ──HTTP + Bearer JWT──▶ backend /api/customer/auto-trade/*
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The server is a thin wrapper: each tool maps 1:1 (or close) to a customer
|
|
21
|
+
API endpoint, authenticated with a user JWT loaded from env at startup.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
### End-user (recommended)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install smartoption-mcp
|
|
29
|
+
# or, in an isolated venv:
|
|
30
|
+
python3.11 -m venv ~/.smartoption-mcp && ~/.smartoption-mcp/bin/pip install smartoption-mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`smartoption-mcp` is published on [PyPI](https://pypi.org/project/smartoption-mcp/).
|
|
34
|
+
This installs the `smartoption-mcp` CLI entry point. Upgrade with
|
|
35
|
+
`pip install -U smartoption-mcp`.
|
|
36
|
+
|
|
37
|
+
### Local dev (from this repo)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd mcp-server
|
|
41
|
+
python3.11 -m venv .venv
|
|
42
|
+
source .venv/bin/activate
|
|
43
|
+
pip install -e .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configure auth
|
|
47
|
+
|
|
48
|
+
**Recommended: long-lived API token** (1-year expiry, revocable any time).
|
|
49
|
+
Log into [portal.smartoption.ai](https://portal.smartoption.ai) → 个人中心
|
|
50
|
+
→ "API Token(用于 MCP / 脚本)" → 起个名字 → 复制生成的 token。
|
|
51
|
+
管理页面随时可以吊销。
|
|
52
|
+
|
|
53
|
+
**Fallback: session JWT** — if the API tokens page isn't deployed yet,
|
|
54
|
+
open DevTools → Application → Local Storage → copy the `access_token`
|
|
55
|
+
field. Expires in ~24h so you'll re-paste daily.
|
|
56
|
+
|
|
57
|
+
Either way, the token goes into `SMARTOPTION_JWT`. Set
|
|
58
|
+
`SMARTOPTION_API_BASE=https://api.smartoption.ai`.
|
|
59
|
+
|
|
60
|
+
Smoke test from a shell:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export SMARTOPTION_API_BASE="https://api.smartoption.ai"
|
|
64
|
+
export SMARTOPTION_JWT="eyJ..." # no "Bearer " prefix
|
|
65
|
+
.venv/bin/python -c "from smartoption_mcp.client import SmartoptionClient; \
|
|
66
|
+
print(SmartoptionClient().list_agents())"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Register with Claude Code / Claude Desktop
|
|
70
|
+
|
|
71
|
+
**Claude Code** (this repo's primary host) — edit `~/.claude.json` and add
|
|
72
|
+
under the top-level `mcpServers` key:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"smartoption": {
|
|
78
|
+
"command": "smartoption-mcp",
|
|
79
|
+
"env": {
|
|
80
|
+
"SMARTOPTION_API_BASE": "https://api.smartoption.ai",
|
|
81
|
+
"SMARTOPTION_JWT": "eyJ..."
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Restart Claude Code (or start a new session). The 5 `smartoption` tools
|
|
89
|
+
should appear in the tool list.
|
|
90
|
+
|
|
91
|
+
**Claude Desktop** — same JSON shape, but the file lives at
|
|
92
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`. Restart
|
|
93
|
+
the app after editing.
|
|
94
|
+
|
|
95
|
+
Once registered, you should be able to ask things like:
|
|
96
|
+
|
|
97
|
+
- "查一下最近一周苹果相关的喊单"
|
|
98
|
+
- "我现在的虚拟仓有哪些标的?"
|
|
99
|
+
- "我的跟单 agent 在线吗?最近一次心跳是什么时候?"
|
|
100
|
+
- "今天 agent 跑了几条信号,有没有被跳过的?"
|
|
101
|
+
|
|
102
|
+
## Tools
|
|
103
|
+
|
|
104
|
+
### Read (Phase 1)
|
|
105
|
+
|
|
106
|
+
| Tool | Endpoint | Purpose |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `list_copy_rules` | `GET /copy-rules` | Current copy-trading rules |
|
|
109
|
+
| `list_virtual_lots` | `GET /virtual-ledger` | Open virtual lots + qty_by_key |
|
|
110
|
+
| `get_agent_status` | `GET /agents` | Per-user agent online status |
|
|
111
|
+
| `query_signal_history` | `GET /signal-history` | Search parsed alerts (大单) |
|
|
112
|
+
| `list_run_logs` | `GET /copy-run-logs` | Per-signal execution outcomes |
|
|
113
|
+
|
|
114
|
+
### Write (Phase 2)
|
|
115
|
+
|
|
116
|
+
| Tool | Endpoint | Purpose |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `update_copy_rules` | `PUT /copy-rules` | Replace full copy-trading settings (rules + toggles) |
|
|
119
|
+
| `ensure_agent` | `POST /agents/ensure` | Create / provision the user's agent k8s pod |
|
|
120
|
+
| `refresh_broker_snapshot` | `POST /broker-account-snapshot/refresh` | Ask agent to push fresh broker snapshot |
|
|
121
|
+
|
|
122
|
+
### Phase 2 extension (broker config + agent ops + signal detail + quant)
|
|
123
|
+
|
|
124
|
+
| Tool | Endpoint | Purpose |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `list_broker_configurations` | `GET /broker-configurations` | All saved brokers + active (no creds returned) |
|
|
127
|
+
| `get_active_broker` | `GET /broker-configurations` | Just the active broker name + display name |
|
|
128
|
+
| `set_active_broker` | `PUT /active-broker` | Switch active broker — dry-run gated |
|
|
129
|
+
| `test_broker_connection` | `POST /broker-configurations/test-{ib,tiger,futu,moomoo}` | Verify creds against broker; does NOT save |
|
|
130
|
+
| `restart_agent_client` | `POST /agents/<id>/restart-client` | Restart in-pod agent process — dry-run gated |
|
|
131
|
+
| `stop_agent` | `POST /agents/<id>/stop` | Scale agent deployment to 0 — dry-run gated |
|
|
132
|
+
| `get_agent_logs` | `GET /agents/<id>/logs` | Tail recent agent-pod stdout |
|
|
133
|
+
| `get_signal_detail` | `GET /signal-history/<id>` | Full SignalForwardRecord for one signal |
|
|
134
|
+
| `get_signal_chain` | `GET /signal-history/<id>/chain-slots` | Paired/chained signal context (ROLL_UP pairs etc.) |
|
|
135
|
+
| `list_virtual_followers` | `GET /api/customer/virtual-followers` | User's virtual-follower accounts + equity summary |
|
|
136
|
+
| `list_quant_strategies` | `GET /api/customer/quant-strategies/list` | Available / subscribed quant strategies |
|
|
137
|
+
| `get_quant_strategy_snapshot` | `GET /api/customer/quant-strategies/<id>/snapshot` | One strategy's positions + canonical valuation |
|
|
138
|
+
|
|
139
|
+
**Confirmation model.** Destructive writes use a `confirmed: bool` flag,
|
|
140
|
+
defaulting to `False` for **dry-run**:
|
|
141
|
+
|
|
142
|
+
- `update_copy_rules(new_settings, confirmed=False)` → server fetches
|
|
143
|
+
current settings, returns a structured diff (top-level toggle changes +
|
|
144
|
+
rules added / removed / modified, keyed by `rule_id`). Nothing is
|
|
145
|
+
written. The model is instructed (via docstring) to show the diff to
|
|
146
|
+
the user and only call again with `confirmed=True` after approval.
|
|
147
|
+
- `ensure_agent(force_redeploy=True, confirmed=False)` → returns a
|
|
148
|
+
description of the redeploy without doing it. Default `force_redeploy=False`
|
|
149
|
+
is idempotent and skips the dry-run gate.
|
|
150
|
+
- `refresh_broker_snapshot` → no gate (non-destructive async request).
|
|
151
|
+
|
|
152
|
+
The host (Claude Code / Desktop) also shows tool-call arguments to the
|
|
153
|
+
user before each call, so `confirmed=True` is always visible in the
|
|
154
|
+
approval UI — the in-tool `confirmed` flag is a second belt on top of
|
|
155
|
+
that suspenders.
|
|
156
|
+
|
|
157
|
+
**Validation.** `update_copy_rules` rejects partial settings: the proposed
|
|
158
|
+
doc must contain `auto_buy_enabled`, `auto_sell_enabled`,
|
|
159
|
+
`use_all_matched_rules`, and `rules`. The model is expected to start from
|
|
160
|
+
`list_copy_rules`, mutate locally, and pass the full doc back —
|
|
161
|
+
preserving every rule's `rule_id` so the diff stays stable.
|
|
162
|
+
|
|
163
|
+
## Roadmap
|
|
164
|
+
|
|
165
|
+
- **Phase 3 (✅ Phase 3a done)**: long-lived API tokens managed from the
|
|
166
|
+
customer portal — eliminates daily JWT re-paste, no protocol changes.
|
|
167
|
+
Tokens are still pasted into env; Phase 3b would be full MCP OAuth 2.1
|
|
168
|
+
with a remote HTTP MCP server so other users can connect from their own
|
|
169
|
+
Claude install without copy-paste. Deferred until there's a real
|
|
170
|
+
multi-user use case.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "smartoption-mcp"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "MCP server exposing the smartoption-ai customer auto-trade API as tools for AI agents (Claude Desktop / Claude Code / any MCP client)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "Proprietary" }
|
|
8
|
+
authors = [{ name = "SmartOption", email = "service.smartoption@gmail.com" }]
|
|
9
|
+
keywords = ["mcp", "smartoption", "claude", "auto-trade", "copy-trading"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"mcp>=1.2.0",
|
|
21
|
+
"httpx>=0.27.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/SmartOption/smartoption-ai"
|
|
26
|
+
Repository = "https://github.com/SmartOption/smartoption-ai"
|
|
27
|
+
Issues = "https://github.com/SmartOption/smartoption-ai/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
smartoption-mcp = "smartoption_mcp.server:main"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["setuptools>=68"]
|
|
34
|
+
build-backend = "setuptools.build_meta"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["smartoption_mcp*"]
|
|
File without changes
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Thin HTTP client for the smartoption-ai customer auto-trade API.
|
|
2
|
+
|
|
3
|
+
All endpoints under /api/customer/auto-trade are user-JWT scoped: the
|
|
4
|
+
authenticated user's id is derived from the token, so we never pass user_id
|
|
5
|
+
explicitly. The JWT comes from env (SMARTOPTION_JWT) at server startup.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE = "https://api.smartoption.ai"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SmartoptionClient:
|
|
19
|
+
def __init__(self, base_url: str | None = None, jwt: str | None = None, timeout: float = 20.0):
|
|
20
|
+
self.base_url = (base_url or os.environ.get("SMARTOPTION_API_BASE") or DEFAULT_BASE).rstrip("/")
|
|
21
|
+
self.jwt = jwt or os.environ.get("SMARTOPTION_JWT") or ""
|
|
22
|
+
if not self.jwt:
|
|
23
|
+
raise RuntimeError("SMARTOPTION_JWT env var is required")
|
|
24
|
+
self._client = httpx.Client(
|
|
25
|
+
base_url=self.base_url,
|
|
26
|
+
timeout=timeout,
|
|
27
|
+
headers={"Authorization": f"Bearer {self.jwt}"},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def close(self) -> None:
|
|
31
|
+
self._client.close()
|
|
32
|
+
|
|
33
|
+
def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
34
|
+
clean = {k: v for k, v in (params or {}).items() if v is not None and v != ""}
|
|
35
|
+
r = self._client.get(path, params=clean)
|
|
36
|
+
r.raise_for_status()
|
|
37
|
+
return self._unwrap(r.json())
|
|
38
|
+
|
|
39
|
+
def _post(self, path: str, json_body: dict[str, Any] | None = None) -> Any:
|
|
40
|
+
r = self._client.post(path, json=json_body or {})
|
|
41
|
+
r.raise_for_status()
|
|
42
|
+
return self._unwrap(r.json())
|
|
43
|
+
|
|
44
|
+
def _put(self, path: str, json_body: dict[str, Any] | None = None) -> Any:
|
|
45
|
+
r = self._client.put(path, json=json_body or {})
|
|
46
|
+
r.raise_for_status()
|
|
47
|
+
return self._unwrap(r.json())
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _unwrap(body: Any) -> Any:
|
|
51
|
+
# ResponseFormatter wraps payloads as {success, data, message}
|
|
52
|
+
if isinstance(body, dict) and "data" in body:
|
|
53
|
+
return body.get("data")
|
|
54
|
+
return body
|
|
55
|
+
|
|
56
|
+
def list_copy_rules(self) -> Any:
|
|
57
|
+
return self._get("/api/customer/auto-trade/copy-rules")
|
|
58
|
+
|
|
59
|
+
def list_virtual_lots(self) -> Any:
|
|
60
|
+
return self._get("/api/customer/auto-trade/virtual-ledger")
|
|
61
|
+
|
|
62
|
+
def list_agents(self) -> Any:
|
|
63
|
+
return self._get("/api/customer/auto-trade/agents")
|
|
64
|
+
|
|
65
|
+
def query_signal_history(
|
|
66
|
+
self,
|
|
67
|
+
page: int = 1,
|
|
68
|
+
per_page: int = 20,
|
|
69
|
+
raw_content_search: str | None = None,
|
|
70
|
+
message_source_ids: list[str] | None = None,
|
|
71
|
+
created_at_from: str | None = None,
|
|
72
|
+
created_at_to: str | None = None,
|
|
73
|
+
) -> Any:
|
|
74
|
+
return self._get(
|
|
75
|
+
"/api/customer/auto-trade/signal-history",
|
|
76
|
+
{
|
|
77
|
+
"page": page,
|
|
78
|
+
"per_page": per_page,
|
|
79
|
+
"raw_content_search": raw_content_search,
|
|
80
|
+
"message_source_ids": ",".join(message_source_ids) if message_source_ids else None,
|
|
81
|
+
"created_at_from": created_at_from,
|
|
82
|
+
"created_at_to": created_at_to,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def put_copy_rules(self, new_settings: dict[str, Any]) -> Any:
|
|
87
|
+
return self._put("/api/customer/auto-trade/copy-rules", new_settings)
|
|
88
|
+
|
|
89
|
+
def ensure_agent(
|
|
90
|
+
self,
|
|
91
|
+
label: str | None = None,
|
|
92
|
+
provision_k8s: bool = True,
|
|
93
|
+
force_redeploy: bool = False,
|
|
94
|
+
) -> Any:
|
|
95
|
+
body: dict[str, Any] = {"provision_k8s": provision_k8s, "force_redeploy": force_redeploy}
|
|
96
|
+
if label:
|
|
97
|
+
body["label"] = label
|
|
98
|
+
return self._post("/api/customer/auto-trade/agents/ensure", body)
|
|
99
|
+
|
|
100
|
+
def refresh_broker_snapshot(self, agent_id: str | None = None) -> Any:
|
|
101
|
+
body = {"agent_id": agent_id} if agent_id else {}
|
|
102
|
+
return self._post("/api/customer/auto-trade/broker-account-snapshot/refresh", body)
|
|
103
|
+
|
|
104
|
+
# ---- Phase 2: broker configuration ----
|
|
105
|
+
def list_broker_configurations(self) -> Any:
|
|
106
|
+
return self._get("/api/customer/auto-trade/broker-configurations")
|
|
107
|
+
|
|
108
|
+
def set_active_broker(self, broker_name: str | None) -> Any:
|
|
109
|
+
return self._put("/api/customer/auto-trade/active-broker", {"active_broker": broker_name})
|
|
110
|
+
|
|
111
|
+
def test_broker_connection(self, broker_name: str, credentials: dict[str, Any]) -> Any:
|
|
112
|
+
slug = broker_name.lower().strip()
|
|
113
|
+
if slug not in {"ib", "tiger", "futu", "moomoo"}:
|
|
114
|
+
raise ValueError(f"unsupported broker for connection test: {broker_name}")
|
|
115
|
+
return self._post(f"/api/customer/auto-trade/broker-configurations/test-{slug}", credentials)
|
|
116
|
+
|
|
117
|
+
# ---- Phase 2: agent ops ----
|
|
118
|
+
def restart_agent_client(self, agent_id: str) -> Any:
|
|
119
|
+
return self._post(f"/api/customer/auto-trade/agents/{agent_id}/restart-client")
|
|
120
|
+
|
|
121
|
+
def stop_agent(self, agent_id: str) -> Any:
|
|
122
|
+
return self._post(f"/api/customer/auto-trade/agents/{agent_id}/stop")
|
|
123
|
+
|
|
124
|
+
def get_agent_logs(self, agent_id: str, lines: int = 200) -> Any:
|
|
125
|
+
return self._get(f"/api/customer/auto-trade/agents/{agent_id}/logs", {"lines": lines})
|
|
126
|
+
|
|
127
|
+
# ---- Phase 2: signal detail ----
|
|
128
|
+
def get_signal_detail(self, record_id: str) -> Any:
|
|
129
|
+
return self._get(f"/api/customer/auto-trade/signal-history/{record_id}")
|
|
130
|
+
|
|
131
|
+
def get_signal_chain_slots(self, record_id: str) -> Any:
|
|
132
|
+
return self._get(f"/api/customer/auto-trade/signal-history/{record_id}/chain-slots")
|
|
133
|
+
|
|
134
|
+
# ---- Phase 2: virtual followers ----
|
|
135
|
+
def list_virtual_followers(self) -> Any:
|
|
136
|
+
return self._get("/api/customer/virtual-followers")
|
|
137
|
+
|
|
138
|
+
# ---- Phase 2: quant strategies ----
|
|
139
|
+
def list_quant_strategies(self) -> Any:
|
|
140
|
+
return self._get("/api/customer/quant-strategies/list")
|
|
141
|
+
|
|
142
|
+
def get_quant_strategy_snapshot(self, strategy_id: str) -> Any:
|
|
143
|
+
return self._get(f"/api/customer/quant-strategies/{strategy_id}/snapshot")
|
|
144
|
+
|
|
145
|
+
def list_run_logs(
|
|
146
|
+
self,
|
|
147
|
+
page: int = 1,
|
|
148
|
+
per_page: int = 30,
|
|
149
|
+
signal_id: str | None = None,
|
|
150
|
+
call_content: str | None = None,
|
|
151
|
+
message_source_ids: list[str] | None = None,
|
|
152
|
+
created_after: str | None = None,
|
|
153
|
+
created_before: str | None = None,
|
|
154
|
+
) -> Any:
|
|
155
|
+
return self._get(
|
|
156
|
+
"/api/customer/auto-trade/copy-run-logs",
|
|
157
|
+
{
|
|
158
|
+
"page": page,
|
|
159
|
+
"per_page": per_page,
|
|
160
|
+
"signal_id": signal_id,
|
|
161
|
+
"call_content": call_content,
|
|
162
|
+
"message_source_ids": ",".join(message_source_ids) if message_source_ids else None,
|
|
163
|
+
"created_after": created_after,
|
|
164
|
+
"created_before": created_before,
|
|
165
|
+
},
|
|
166
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Structured diff for copy-trading settings dicts.
|
|
2
|
+
|
|
3
|
+
Goal: give the model (and the user reading via the model) a compact,
|
|
4
|
+
unambiguous view of what would change between current and proposed
|
|
5
|
+
settings. Diffs by rule_id where possible so reordering rules doesn't
|
|
6
|
+
produce noisy output.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_SCALAR_KEYS = (
|
|
15
|
+
"auto_buy_enabled",
|
|
16
|
+
"auto_sell_enabled",
|
|
17
|
+
"use_all_matched_rules",
|
|
18
|
+
"active_time_start",
|
|
19
|
+
"active_time_end",
|
|
20
|
+
"active_weekdays",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def diff_copy_settings(current: dict[str, Any], proposed: dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
changes: list[dict[str, Any]] = []
|
|
26
|
+
|
|
27
|
+
for key in _SCALAR_KEYS:
|
|
28
|
+
cv = current.get(key)
|
|
29
|
+
pv = proposed.get(key)
|
|
30
|
+
if cv != pv:
|
|
31
|
+
changes.append({"path": key, "before": cv, "after": pv})
|
|
32
|
+
|
|
33
|
+
cur_rules = {r.get("rule_id"): r for r in (current.get("rules") or []) if isinstance(r, dict)}
|
|
34
|
+
new_rules = {r.get("rule_id"): r for r in (proposed.get("rules") or []) if isinstance(r, dict)}
|
|
35
|
+
|
|
36
|
+
added_rules = [r for rid, r in new_rules.items() if rid and rid not in cur_rules]
|
|
37
|
+
removed_rules = [r for rid, r in cur_rules.items() if rid and rid not in new_rules]
|
|
38
|
+
|
|
39
|
+
# Rules in proposed without rule_id are treated as new (frontend assigns
|
|
40
|
+
# uuid server-side, so this is the normal "add a new rule" path).
|
|
41
|
+
added_rules += [r for r in (proposed.get("rules") or []) if isinstance(r, dict) and not r.get("rule_id")]
|
|
42
|
+
|
|
43
|
+
modified_rules: list[dict[str, Any]] = []
|
|
44
|
+
for rid, new_rule in new_rules.items():
|
|
45
|
+
if not rid or rid not in cur_rules:
|
|
46
|
+
continue
|
|
47
|
+
cur_rule = cur_rules[rid]
|
|
48
|
+
rule_changes = _shallow_field_diff(cur_rule, new_rule)
|
|
49
|
+
if rule_changes:
|
|
50
|
+
modified_rules.append(
|
|
51
|
+
{
|
|
52
|
+
"rule_id": rid,
|
|
53
|
+
"name": new_rule.get("name") or cur_rule.get("name"),
|
|
54
|
+
"fields": rule_changes,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"top_level_changes": changes,
|
|
60
|
+
"rules_added": [
|
|
61
|
+
{"rule_id": r.get("rule_id"), "name": r.get("name"), "enabled": r.get("enabled")}
|
|
62
|
+
for r in added_rules
|
|
63
|
+
],
|
|
64
|
+
"rules_removed": [
|
|
65
|
+
{"rule_id": r.get("rule_id"), "name": r.get("name")} for r in removed_rules
|
|
66
|
+
],
|
|
67
|
+
"rules_modified": modified_rules,
|
|
68
|
+
"summary": {
|
|
69
|
+
"top_level_changed": len(changes),
|
|
70
|
+
"rules_added": len(added_rules),
|
|
71
|
+
"rules_removed": len(removed_rules),
|
|
72
|
+
"rules_modified": len(modified_rules),
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _shallow_field_diff(a: dict[str, Any], b: dict[str, Any]) -> list[dict[str, Any]]:
|
|
78
|
+
out: list[dict[str, Any]] = []
|
|
79
|
+
keys = set(a.keys()) | set(b.keys())
|
|
80
|
+
# These are server-set echo fields; ignore.
|
|
81
|
+
keys.discard("updated_at")
|
|
82
|
+
for k in sorted(keys):
|
|
83
|
+
if a.get(k) != b.get(k):
|
|
84
|
+
out.append({"field": k, "before": a.get(k), "after": b.get(k)})
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
REQUIRED_TOP_LEVEL_KEYS = ("auto_buy_enabled", "auto_sell_enabled", "use_all_matched_rules", "rules")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate_full_settings(proposed: dict[str, Any]) -> list[str]:
|
|
92
|
+
"""Returns a list of validation errors. Empty list = OK."""
|
|
93
|
+
errors: list[str] = []
|
|
94
|
+
if not isinstance(proposed, dict):
|
|
95
|
+
return ["new_settings must be an object"]
|
|
96
|
+
for k in REQUIRED_TOP_LEVEL_KEYS:
|
|
97
|
+
if k not in proposed:
|
|
98
|
+
errors.append(
|
|
99
|
+
f"missing required top-level key '{k}'. You must pass the FULL settings doc "
|
|
100
|
+
f"(start from list_copy_rules and mutate locally), not a partial patch."
|
|
101
|
+
)
|
|
102
|
+
rules = proposed.get("rules")
|
|
103
|
+
if "rules" in proposed and not isinstance(rules, list):
|
|
104
|
+
errors.append("'rules' must be a list")
|
|
105
|
+
return errors
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""MCP server exposing read-only smartoption-ai customer auto-trade tools.
|
|
2
|
+
|
|
3
|
+
Phase 1 (read-only): 5 tools — copy rules, virtual lots, agent status,
|
|
4
|
+
signal history (大单/喊单历史), and copy-run logs. Write operations
|
|
5
|
+
(create/modify copy rules, place orders) are intentionally not exposed yet.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from mcp.server.fastmcp import FastMCP
|
|
14
|
+
|
|
15
|
+
from .client import SmartoptionClient
|
|
16
|
+
from .diff import diff_copy_settings, validate_full_settings
|
|
17
|
+
|
|
18
|
+
mcp = FastMCP("smartoption")
|
|
19
|
+
_client: SmartoptionClient | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _c() -> SmartoptionClient:
|
|
23
|
+
global _client
|
|
24
|
+
if _client is None:
|
|
25
|
+
_client = SmartoptionClient()
|
|
26
|
+
return _client
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _dump(obj: Any) -> str:
|
|
30
|
+
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
def list_copy_rules() -> str:
|
|
35
|
+
"""List the current user's copy-trading rules and global settings.
|
|
36
|
+
|
|
37
|
+
Returns the same payload the customer frontend uses to render the
|
|
38
|
+
CopyRules panel: enabled rules, source channel filters, sizing,
|
|
39
|
+
premium controls, ROLL_UP options, etc.
|
|
40
|
+
"""
|
|
41
|
+
return _dump(_c().list_copy_rules())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@mcp.tool()
|
|
45
|
+
def list_virtual_lots() -> str:
|
|
46
|
+
"""List the user's virtual-ledger open lots (CopyTradingVirtualLot rows).
|
|
47
|
+
|
|
48
|
+
Useful to answer "what virtual positions am I currently holding?" and
|
|
49
|
+
"what's the unrealized pnl on my paper-traded copies?". Includes a
|
|
50
|
+
qty_by_key map for quick aggregation by position_key.
|
|
51
|
+
"""
|
|
52
|
+
return _dump(_c().list_virtual_lots())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@mcp.tool()
|
|
56
|
+
def get_agent_status() -> str:
|
|
57
|
+
"""Return the user's auto-trade agent(s): online/offline, last heartbeat,
|
|
58
|
+
active broker, k8s pod state. Use this to answer "is my copy-trade
|
|
59
|
+
bot running?" / "did it disconnect?".
|
|
60
|
+
"""
|
|
61
|
+
return _dump(_c().list_agents())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
def query_signal_history(
|
|
66
|
+
raw_content_search: str | None = None,
|
|
67
|
+
message_source_ids: list[str] | None = None,
|
|
68
|
+
created_at_from: str | None = None,
|
|
69
|
+
created_at_to: str | None = None,
|
|
70
|
+
page: int = 1,
|
|
71
|
+
per_page: int = 20,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Search parsed trader-alert signals (a.k.a. 历史喊单 / 大单).
|
|
74
|
+
|
|
75
|
+
Each row is a SignalForwardRecord with the original raw alert text and
|
|
76
|
+
the LLM-parsed structured signal (action, instrument_kind, option_legs,
|
|
77
|
+
size_relative, etc.).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
raw_content_search: free-text substring match on the raw alert (e.g.
|
|
81
|
+
"AAPL", "苹果", a strike, an expiry). Server-side ILIKE match.
|
|
82
|
+
message_source_ids: optional list of MessageSource ObjectIds to scope
|
|
83
|
+
to particular Discord channels / X-API accounts / etc.
|
|
84
|
+
created_at_from / created_at_to: ISO 8601 timestamps to bound the
|
|
85
|
+
search window. Prefer narrow windows (a few days) for relevance.
|
|
86
|
+
page / per_page: standard pagination; per_page capped server-side at 100.
|
|
87
|
+
"""
|
|
88
|
+
return _dump(
|
|
89
|
+
_c().query_signal_history(
|
|
90
|
+
page=page,
|
|
91
|
+
per_page=per_page,
|
|
92
|
+
raw_content_search=raw_content_search,
|
|
93
|
+
message_source_ids=message_source_ids,
|
|
94
|
+
created_at_from=created_at_from,
|
|
95
|
+
created_at_to=created_at_to,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@mcp.tool()
|
|
101
|
+
def list_run_logs(
|
|
102
|
+
signal_id: str | None = None,
|
|
103
|
+
call_content: str | None = None,
|
|
104
|
+
message_source_ids: list[str] | None = None,
|
|
105
|
+
created_after: str | None = None,
|
|
106
|
+
created_before: str | None = None,
|
|
107
|
+
page: int = 1,
|
|
108
|
+
per_page: int = 30,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""List CopyTradingRunLog rows — the agent's per-signal execution outcomes.
|
|
111
|
+
|
|
112
|
+
Use this to answer "did my agent act on signal X?", "why was it skipped?",
|
|
113
|
+
"show me today's executions". Each row carries the matched rule, the
|
|
114
|
+
submitted broker orders (with canonical view), and an outcome code such
|
|
115
|
+
as `buy_open_submitted`, `roll_up_close_skipped_no_rule`, `premium_blocked`.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
signal_id: filter to a specific upstream signal id.
|
|
119
|
+
call_content: substring match against the original alert text.
|
|
120
|
+
message_source_ids: scope to specific source channels.
|
|
121
|
+
created_after / created_before: ISO 8601 timestamps.
|
|
122
|
+
"""
|
|
123
|
+
return _dump(
|
|
124
|
+
_c().list_run_logs(
|
|
125
|
+
page=page,
|
|
126
|
+
per_page=per_page,
|
|
127
|
+
signal_id=signal_id,
|
|
128
|
+
call_content=call_content,
|
|
129
|
+
message_source_ids=message_source_ids,
|
|
130
|
+
created_after=created_after,
|
|
131
|
+
created_before=created_before,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@mcp.tool()
|
|
137
|
+
def update_copy_rules(new_settings: dict, confirmed: bool = False) -> str:
|
|
138
|
+
"""Update the user's full copy-trading settings (rules + global toggles).
|
|
139
|
+
|
|
140
|
+
⚠️ This is a **destructive write**. The customer backend PUT endpoint
|
|
141
|
+
replaces the entire settings document, so `new_settings` MUST be the
|
|
142
|
+
complete doc (start from `list_copy_rules`, mutate locally — never
|
|
143
|
+
construct from scratch or pass a partial patch).
|
|
144
|
+
|
|
145
|
+
Two-phase usage (required):
|
|
146
|
+
1. Call with `confirmed=False` (the default). The server fetches the
|
|
147
|
+
current settings, computes a structured diff vs `new_settings`,
|
|
148
|
+
and returns it WITHOUT writing. You MUST present this diff to the
|
|
149
|
+
user in natural language and get explicit approval.
|
|
150
|
+
2. Only after the user approves, call again with the same
|
|
151
|
+
`new_settings` and `confirmed=True` to actually persist.
|
|
152
|
+
|
|
153
|
+
Required top-level keys in new_settings: auto_buy_enabled,
|
|
154
|
+
auto_sell_enabled, use_all_matched_rules, rules. Optional:
|
|
155
|
+
active_time_start, active_time_end, active_weekdays.
|
|
156
|
+
|
|
157
|
+
Rule changes diff by `rule_id`, so preserve `rule_id` on every rule
|
|
158
|
+
you keep — otherwise the rule will look removed-and-re-added.
|
|
159
|
+
"""
|
|
160
|
+
errors = validate_full_settings(new_settings)
|
|
161
|
+
if errors:
|
|
162
|
+
return _dump({"ok": False, "errors": errors})
|
|
163
|
+
|
|
164
|
+
if not confirmed:
|
|
165
|
+
current = _c().list_copy_rules() or {}
|
|
166
|
+
diff = diff_copy_settings(current, new_settings)
|
|
167
|
+
return _dump(
|
|
168
|
+
{
|
|
169
|
+
"ok": True,
|
|
170
|
+
"mode": "dry_run",
|
|
171
|
+
"diff": diff,
|
|
172
|
+
"next_step": (
|
|
173
|
+
"Show this diff to the user. If they approve, call "
|
|
174
|
+
"update_copy_rules again with the same new_settings and confirmed=True."
|
|
175
|
+
),
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
result = _c().put_copy_rules(new_settings)
|
|
180
|
+
return _dump({"ok": True, "mode": "applied", "result": result})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@mcp.tool()
|
|
184
|
+
def ensure_agent(
|
|
185
|
+
label: str | None = None,
|
|
186
|
+
force_redeploy: bool = False,
|
|
187
|
+
confirmed: bool = False,
|
|
188
|
+
) -> str:
|
|
189
|
+
"""Ensure the user has an auto-trade agent pod provisioned in k8s.
|
|
190
|
+
|
|
191
|
+
Default (label=None, force_redeploy=False) is **idempotent**: if an
|
|
192
|
+
agent already exists it just returns its state; if not, it creates
|
|
193
|
+
one and provisions the k8s deployment. This default mode does NOT
|
|
194
|
+
require `confirmed=True`.
|
|
195
|
+
|
|
196
|
+
`force_redeploy=True` is **destructive** — it tears down and rebuilds
|
|
197
|
+
the user's agent pod (existing in-flight orders / heartbeats will be
|
|
198
|
+
interrupted). For this mode you MUST first call with
|
|
199
|
+
`confirmed=False` (returns a description of what will happen) and
|
|
200
|
+
then with `confirmed=True` after user approval.
|
|
201
|
+
"""
|
|
202
|
+
if force_redeploy and not confirmed:
|
|
203
|
+
return _dump(
|
|
204
|
+
{
|
|
205
|
+
"ok": True,
|
|
206
|
+
"mode": "dry_run",
|
|
207
|
+
"action": "force_redeploy_agent",
|
|
208
|
+
"warning": (
|
|
209
|
+
"This will tear down and rebuild the user's auto-trade agent k8s pod. "
|
|
210
|
+
"In-flight broker connections and heartbeats will reset. "
|
|
211
|
+
"Confirm with the user before calling again with confirmed=True."
|
|
212
|
+
),
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
result = _c().ensure_agent(label=label, force_redeploy=force_redeploy)
|
|
216
|
+
return _dump({"ok": True, "mode": "applied", "result": result})
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@mcp.tool()
|
|
220
|
+
def refresh_broker_snapshot(agent_id: str | None = None) -> str:
|
|
221
|
+
"""Ask the user's auto-trade agent to push a fresh broker account
|
|
222
|
+
snapshot (positions + balances) up to the backend.
|
|
223
|
+
|
|
224
|
+
Non-destructive: just an async signal to the agent. Safe to call
|
|
225
|
+
without confirmation. If `agent_id` is omitted, uses the user's first
|
|
226
|
+
enabled agent.
|
|
227
|
+
"""
|
|
228
|
+
return _dump(_c().refresh_broker_snapshot(agent_id=agent_id))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# =========================================================================
|
|
232
|
+
# Phase 2 tools — broker config, agent ops, signal detail, quant strategies
|
|
233
|
+
# =========================================================================
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@mcp.tool()
|
|
237
|
+
def list_broker_configurations() -> str:
|
|
238
|
+
"""List the user's broker configurations + which broker is currently active.
|
|
239
|
+
|
|
240
|
+
Returns the same payload the customer "券商配置" page renders: which
|
|
241
|
+
brokers have credentials saved (configured), which one is active
|
|
242
|
+
(`active_broker`), and per-broker metadata. Credentials themselves
|
|
243
|
+
are NOT returned (server-side scrub).
|
|
244
|
+
|
|
245
|
+
Use this to answer "我配了哪些券商" / "现在在用哪个 broker".
|
|
246
|
+
"""
|
|
247
|
+
return _dump(_c().list_broker_configurations())
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@mcp.tool()
|
|
251
|
+
def get_active_broker() -> str:
|
|
252
|
+
"""Return just the currently active broker name (and display name).
|
|
253
|
+
|
|
254
|
+
Thin wrapper over `list_broker_configurations` that narrows to the
|
|
255
|
+
`active_broker` / `active_broker_display_name` / `active_broker_configured`
|
|
256
|
+
fields. Use when the caller only needs "which broker is in use right now".
|
|
257
|
+
"""
|
|
258
|
+
data = _c().list_broker_configurations() or {}
|
|
259
|
+
return _dump(
|
|
260
|
+
{
|
|
261
|
+
"active_broker": data.get("active_broker"),
|
|
262
|
+
"active_broker_display_name": data.get("active_broker_display_name"),
|
|
263
|
+
"active_broker_configured": data.get("active_broker_configured"),
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@mcp.tool()
|
|
269
|
+
def set_active_broker(broker_name: str | None, confirmed: bool = False) -> str:
|
|
270
|
+
"""Switch the user's active broker (or clear it with broker_name=None).
|
|
271
|
+
|
|
272
|
+
⚠️ Destructive: changes which broker the auto-trade agent will route
|
|
273
|
+
orders through. In-flight orders on the previous broker are NOT
|
|
274
|
+
cancelled, but new copy-trade signals will go to the new broker.
|
|
275
|
+
|
|
276
|
+
Two-phase usage:
|
|
277
|
+
1. Call with confirmed=False to get a dry-run summary (current vs
|
|
278
|
+
target broker).
|
|
279
|
+
2. After user approval, call with confirmed=True.
|
|
280
|
+
|
|
281
|
+
`broker_name` must already have credentials configured (use
|
|
282
|
+
`list_broker_configurations` to check); pass None to clear active.
|
|
283
|
+
"""
|
|
284
|
+
if not confirmed:
|
|
285
|
+
current = _c().list_broker_configurations() or {}
|
|
286
|
+
return _dump(
|
|
287
|
+
{
|
|
288
|
+
"ok": True,
|
|
289
|
+
"mode": "dry_run",
|
|
290
|
+
"current_active_broker": current.get("active_broker"),
|
|
291
|
+
"target_active_broker": broker_name,
|
|
292
|
+
"next_step": "Call again with confirmed=True after user approval.",
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
return _dump({"ok": True, "mode": "applied", "result": _c().set_active_broker(broker_name)})
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@mcp.tool()
|
|
299
|
+
def test_broker_connection(broker_name: str, credentials: dict) -> str:
|
|
300
|
+
"""Run the broker connection-test workflow for IB / Tiger / Futu / Moomoo.
|
|
301
|
+
|
|
302
|
+
Spawns a one-shot pod (or equivalent dry connection) against the
|
|
303
|
+
submitted `credentials` and returns the broker-side login + auth
|
|
304
|
+
result. Does NOT save the credentials — purely a verification call.
|
|
305
|
+
|
|
306
|
+
`credentials` shape is broker-specific; see the customer "券商配置"
|
|
307
|
+
form for the exact keys per broker (e.g. IB needs trading_mode +
|
|
308
|
+
login_id + login_password).
|
|
309
|
+
"""
|
|
310
|
+
return _dump(_c().test_broker_connection(broker_name, credentials))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@mcp.tool()
|
|
314
|
+
def restart_agent_client(agent_id: str, confirmed: bool = False) -> str:
|
|
315
|
+
"""Restart the agent-client process inside a user's auto-trade pod.
|
|
316
|
+
|
|
317
|
+
⚠️ Destructive: in-flight socket connections to the gateway reset,
|
|
318
|
+
pending broker order-status polls are interrupted briefly (re-issued
|
|
319
|
+
on restart). Use when the agent looks stuck or after pushing a
|
|
320
|
+
config change that needs a process restart.
|
|
321
|
+
|
|
322
|
+
Two-phase: call with confirmed=False to get a dry-run, then with
|
|
323
|
+
confirmed=True to actually restart.
|
|
324
|
+
"""
|
|
325
|
+
if not confirmed:
|
|
326
|
+
return _dump(
|
|
327
|
+
{
|
|
328
|
+
"ok": True,
|
|
329
|
+
"mode": "dry_run",
|
|
330
|
+
"action": "restart_agent_client",
|
|
331
|
+
"agent_id": agent_id,
|
|
332
|
+
"next_step": "Call again with confirmed=True after user approval.",
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
return _dump({"ok": True, "mode": "applied", "result": _c().restart_agent_client(agent_id)})
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@mcp.tool()
|
|
339
|
+
def stop_agent(agent_id: str, confirmed: bool = False) -> str:
|
|
340
|
+
"""Stop the user's auto-trade agent (scale deployment to 0 replicas).
|
|
341
|
+
|
|
342
|
+
⚠️ Destructive: the agent will stop receiving trading_signal events
|
|
343
|
+
and stop polling broker order status until restarted (via
|
|
344
|
+
`ensure_agent` or in the UI). Use when the user wants to pause
|
|
345
|
+
copy-trading without deleting their config.
|
|
346
|
+
|
|
347
|
+
Two-phase: confirmed=False for dry-run, confirmed=True to apply.
|
|
348
|
+
"""
|
|
349
|
+
if not confirmed:
|
|
350
|
+
return _dump(
|
|
351
|
+
{
|
|
352
|
+
"ok": True,
|
|
353
|
+
"mode": "dry_run",
|
|
354
|
+
"action": "stop_agent",
|
|
355
|
+
"agent_id": agent_id,
|
|
356
|
+
"next_step": "Call again with confirmed=True after user approval.",
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
return _dump({"ok": True, "mode": "applied", "result": _c().stop_agent(agent_id)})
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@mcp.tool()
|
|
363
|
+
def get_agent_logs(agent_id: str, lines: int = 200) -> str:
|
|
364
|
+
"""Fetch recent stdout logs from the user's auto-trade agent pod.
|
|
365
|
+
|
|
366
|
+
`lines` is the tail length (default 200, server may cap). Useful for
|
|
367
|
+
"why didn't my agent act on this signal" / "is the agent crashing".
|
|
368
|
+
"""
|
|
369
|
+
return _dump(_c().get_agent_logs(agent_id, lines=lines))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@mcp.tool()
|
|
373
|
+
def get_signal_detail(record_id: str) -> str:
|
|
374
|
+
"""Fetch the full SignalForwardRecord for one signal id.
|
|
375
|
+
|
|
376
|
+
Includes the raw alert text, the LLM-parsed structured signal, the
|
|
377
|
+
upstream message_source metadata, and any backfill/normalize traces.
|
|
378
|
+
Use this after `query_signal_history` returns a row id that the user
|
|
379
|
+
wants to drill into.
|
|
380
|
+
"""
|
|
381
|
+
return _dump(_c().get_signal_detail(record_id))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@mcp.tool()
|
|
385
|
+
def get_signal_chain(record_id: str) -> str:
|
|
386
|
+
"""Return the chain-slot context (the same signal across the trader's
|
|
387
|
+
recent signal chain) for one SignalForwardRecord id.
|
|
388
|
+
|
|
389
|
+
Used by the frontend to show "this signal is part of a ROLL_UP pair"
|
|
390
|
+
/ "previous open from same trader" / paired-leg relationships. Pairs
|
|
391
|
+
nicely with ROLL_UP debugging.
|
|
392
|
+
"""
|
|
393
|
+
return _dump(_c().get_signal_chain_slots(record_id))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@mcp.tool()
|
|
397
|
+
def list_virtual_followers() -> str:
|
|
398
|
+
"""List the user's virtual-follower accounts.
|
|
399
|
+
|
|
400
|
+
Each VirtualFollower mirrors a trader's signals into a sandboxed
|
|
401
|
+
VirtualAccount (no real broker), tracked end-to-end through the
|
|
402
|
+
canonical valuation pipeline. Returns id, label, the followed
|
|
403
|
+
message-source, and current equity summary.
|
|
404
|
+
"""
|
|
405
|
+
return _dump(_c().list_virtual_followers())
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@mcp.tool()
|
|
409
|
+
def list_quant_strategies() -> str:
|
|
410
|
+
"""List quant strategies available / subscribed by the user.
|
|
411
|
+
|
|
412
|
+
Returns id, name, description, visibility (public/private), and the
|
|
413
|
+
user's subscription state. Pairs with `get_quant_strategy_snapshot`
|
|
414
|
+
for per-strategy 持仓 + 业绩 detail.
|
|
415
|
+
"""
|
|
416
|
+
return _dump(_c().list_quant_strategies())
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@mcp.tool()
|
|
420
|
+
def get_quant_strategy_snapshot(strategy_id: str) -> str:
|
|
421
|
+
"""Fetch one quant strategy's current positions + performance snapshot.
|
|
422
|
+
|
|
423
|
+
Backed by the canonical virtual-account valuation
|
|
424
|
+
(compute_virtual_account_totals), so the numbers match what the
|
|
425
|
+
`AIStrategyDetail.vue` page renders: total_value, cash_balance,
|
|
426
|
+
positions_detail, win-rate / realized PnL metrics.
|
|
427
|
+
"""
|
|
428
|
+
return _dump(_c().get_quant_strategy_snapshot(strategy_id))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def main() -> None:
|
|
432
|
+
mcp.run()
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
if __name__ == "__main__":
|
|
436
|
+
main()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartoption-mcp
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: MCP server exposing the smartoption-ai customer auto-trade API as tools for AI agents (Claude Desktop / Claude Code / any MCP client).
|
|
5
|
+
Author-email: SmartOption <service.smartoption@gmail.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/SmartOption/smartoption-ai
|
|
8
|
+
Project-URL: Repository, https://github.com/SmartOption/smartoption-ai
|
|
9
|
+
Project-URL: Issues, https://github.com/SmartOption/smartoption-ai/issues
|
|
10
|
+
Keywords: mcp,smartoption,claude,auto-trade,copy-trading
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: mcp>=1.2.0
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
|
|
23
|
+
# smartoption-mcp
|
|
24
|
+
|
|
25
|
+
MCP server that exposes the smartoption-ai **customer auto-trade API** as tools
|
|
26
|
+
for AI agents (Claude Desktop, Claude Code, anything that speaks MCP).
|
|
27
|
+
|
|
28
|
+
Phase 1 is **read-only**: 5 tools covering copy rules, virtual lots, agent
|
|
29
|
+
status, parsed signal history (大单/喊单历史), and copy-run logs. Write
|
|
30
|
+
operations (creating/modifying rules, placing orders) will be added in
|
|
31
|
+
Phase 2 once the read path is battle-tested.
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Claude / Agent
|
|
37
|
+
│ MCP stdio
|
|
38
|
+
▼
|
|
39
|
+
smartoption-mcp ──HTTP + Bearer JWT──▶ backend /api/customer/auto-trade/*
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The server is a thin wrapper: each tool maps 1:1 (or close) to a customer
|
|
43
|
+
API endpoint, authenticated with a user JWT loaded from env at startup.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
### End-user (recommended)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install smartoption-mcp
|
|
51
|
+
# or, in an isolated venv:
|
|
52
|
+
python3.11 -m venv ~/.smartoption-mcp && ~/.smartoption-mcp/bin/pip install smartoption-mcp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`smartoption-mcp` is published on [PyPI](https://pypi.org/project/smartoption-mcp/).
|
|
56
|
+
This installs the `smartoption-mcp` CLI entry point. Upgrade with
|
|
57
|
+
`pip install -U smartoption-mcp`.
|
|
58
|
+
|
|
59
|
+
### Local dev (from this repo)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd mcp-server
|
|
63
|
+
python3.11 -m venv .venv
|
|
64
|
+
source .venv/bin/activate
|
|
65
|
+
pip install -e .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configure auth
|
|
69
|
+
|
|
70
|
+
**Recommended: long-lived API token** (1-year expiry, revocable any time).
|
|
71
|
+
Log into [portal.smartoption.ai](https://portal.smartoption.ai) → 个人中心
|
|
72
|
+
→ "API Token(用于 MCP / 脚本)" → 起个名字 → 复制生成的 token。
|
|
73
|
+
管理页面随时可以吊销。
|
|
74
|
+
|
|
75
|
+
**Fallback: session JWT** — if the API tokens page isn't deployed yet,
|
|
76
|
+
open DevTools → Application → Local Storage → copy the `access_token`
|
|
77
|
+
field. Expires in ~24h so you'll re-paste daily.
|
|
78
|
+
|
|
79
|
+
Either way, the token goes into `SMARTOPTION_JWT`. Set
|
|
80
|
+
`SMARTOPTION_API_BASE=https://api.smartoption.ai`.
|
|
81
|
+
|
|
82
|
+
Smoke test from a shell:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
export SMARTOPTION_API_BASE="https://api.smartoption.ai"
|
|
86
|
+
export SMARTOPTION_JWT="eyJ..." # no "Bearer " prefix
|
|
87
|
+
.venv/bin/python -c "from smartoption_mcp.client import SmartoptionClient; \
|
|
88
|
+
print(SmartoptionClient().list_agents())"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Register with Claude Code / Claude Desktop
|
|
92
|
+
|
|
93
|
+
**Claude Code** (this repo's primary host) — edit `~/.claude.json` and add
|
|
94
|
+
under the top-level `mcpServers` key:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"smartoption": {
|
|
100
|
+
"command": "smartoption-mcp",
|
|
101
|
+
"env": {
|
|
102
|
+
"SMARTOPTION_API_BASE": "https://api.smartoption.ai",
|
|
103
|
+
"SMARTOPTION_JWT": "eyJ..."
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Restart Claude Code (or start a new session). The 5 `smartoption` tools
|
|
111
|
+
should appear in the tool list.
|
|
112
|
+
|
|
113
|
+
**Claude Desktop** — same JSON shape, but the file lives at
|
|
114
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`. Restart
|
|
115
|
+
the app after editing.
|
|
116
|
+
|
|
117
|
+
Once registered, you should be able to ask things like:
|
|
118
|
+
|
|
119
|
+
- "查一下最近一周苹果相关的喊单"
|
|
120
|
+
- "我现在的虚拟仓有哪些标的?"
|
|
121
|
+
- "我的跟单 agent 在线吗?最近一次心跳是什么时候?"
|
|
122
|
+
- "今天 agent 跑了几条信号,有没有被跳过的?"
|
|
123
|
+
|
|
124
|
+
## Tools
|
|
125
|
+
|
|
126
|
+
### Read (Phase 1)
|
|
127
|
+
|
|
128
|
+
| Tool | Endpoint | Purpose |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `list_copy_rules` | `GET /copy-rules` | Current copy-trading rules |
|
|
131
|
+
| `list_virtual_lots` | `GET /virtual-ledger` | Open virtual lots + qty_by_key |
|
|
132
|
+
| `get_agent_status` | `GET /agents` | Per-user agent online status |
|
|
133
|
+
| `query_signal_history` | `GET /signal-history` | Search parsed alerts (大单) |
|
|
134
|
+
| `list_run_logs` | `GET /copy-run-logs` | Per-signal execution outcomes |
|
|
135
|
+
|
|
136
|
+
### Write (Phase 2)
|
|
137
|
+
|
|
138
|
+
| Tool | Endpoint | Purpose |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `update_copy_rules` | `PUT /copy-rules` | Replace full copy-trading settings (rules + toggles) |
|
|
141
|
+
| `ensure_agent` | `POST /agents/ensure` | Create / provision the user's agent k8s pod |
|
|
142
|
+
| `refresh_broker_snapshot` | `POST /broker-account-snapshot/refresh` | Ask agent to push fresh broker snapshot |
|
|
143
|
+
|
|
144
|
+
### Phase 2 extension (broker config + agent ops + signal detail + quant)
|
|
145
|
+
|
|
146
|
+
| Tool | Endpoint | Purpose |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `list_broker_configurations` | `GET /broker-configurations` | All saved brokers + active (no creds returned) |
|
|
149
|
+
| `get_active_broker` | `GET /broker-configurations` | Just the active broker name + display name |
|
|
150
|
+
| `set_active_broker` | `PUT /active-broker` | Switch active broker — dry-run gated |
|
|
151
|
+
| `test_broker_connection` | `POST /broker-configurations/test-{ib,tiger,futu,moomoo}` | Verify creds against broker; does NOT save |
|
|
152
|
+
| `restart_agent_client` | `POST /agents/<id>/restart-client` | Restart in-pod agent process — dry-run gated |
|
|
153
|
+
| `stop_agent` | `POST /agents/<id>/stop` | Scale agent deployment to 0 — dry-run gated |
|
|
154
|
+
| `get_agent_logs` | `GET /agents/<id>/logs` | Tail recent agent-pod stdout |
|
|
155
|
+
| `get_signal_detail` | `GET /signal-history/<id>` | Full SignalForwardRecord for one signal |
|
|
156
|
+
| `get_signal_chain` | `GET /signal-history/<id>/chain-slots` | Paired/chained signal context (ROLL_UP pairs etc.) |
|
|
157
|
+
| `list_virtual_followers` | `GET /api/customer/virtual-followers` | User's virtual-follower accounts + equity summary |
|
|
158
|
+
| `list_quant_strategies` | `GET /api/customer/quant-strategies/list` | Available / subscribed quant strategies |
|
|
159
|
+
| `get_quant_strategy_snapshot` | `GET /api/customer/quant-strategies/<id>/snapshot` | One strategy's positions + canonical valuation |
|
|
160
|
+
|
|
161
|
+
**Confirmation model.** Destructive writes use a `confirmed: bool` flag,
|
|
162
|
+
defaulting to `False` for **dry-run**:
|
|
163
|
+
|
|
164
|
+
- `update_copy_rules(new_settings, confirmed=False)` → server fetches
|
|
165
|
+
current settings, returns a structured diff (top-level toggle changes +
|
|
166
|
+
rules added / removed / modified, keyed by `rule_id`). Nothing is
|
|
167
|
+
written. The model is instructed (via docstring) to show the diff to
|
|
168
|
+
the user and only call again with `confirmed=True` after approval.
|
|
169
|
+
- `ensure_agent(force_redeploy=True, confirmed=False)` → returns a
|
|
170
|
+
description of the redeploy without doing it. Default `force_redeploy=False`
|
|
171
|
+
is idempotent and skips the dry-run gate.
|
|
172
|
+
- `refresh_broker_snapshot` → no gate (non-destructive async request).
|
|
173
|
+
|
|
174
|
+
The host (Claude Code / Desktop) also shows tool-call arguments to the
|
|
175
|
+
user before each call, so `confirmed=True` is always visible in the
|
|
176
|
+
approval UI — the in-tool `confirmed` flag is a second belt on top of
|
|
177
|
+
that suspenders.
|
|
178
|
+
|
|
179
|
+
**Validation.** `update_copy_rules` rejects partial settings: the proposed
|
|
180
|
+
doc must contain `auto_buy_enabled`, `auto_sell_enabled`,
|
|
181
|
+
`use_all_matched_rules`, and `rules`. The model is expected to start from
|
|
182
|
+
`list_copy_rules`, mutate locally, and pass the full doc back —
|
|
183
|
+
preserving every rule's `rule_id` so the diff stays stable.
|
|
184
|
+
|
|
185
|
+
## Roadmap
|
|
186
|
+
|
|
187
|
+
- **Phase 3 (✅ Phase 3a done)**: long-lived API tokens managed from the
|
|
188
|
+
customer portal — eliminates daily JWT re-paste, no protocol changes.
|
|
189
|
+
Tokens are still pasted into env; Phase 3b would be full MCP OAuth 2.1
|
|
190
|
+
with a remote HTTP MCP server so other users can connect from their own
|
|
191
|
+
Claude install without copy-paste. Deferred until there's a real
|
|
192
|
+
multi-user use case.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
smartoption_mcp/__init__.py
|
|
4
|
+
smartoption_mcp/client.py
|
|
5
|
+
smartoption_mcp/diff.py
|
|
6
|
+
smartoption_mcp/server.py
|
|
7
|
+
smartoption_mcp.egg-info/PKG-INFO
|
|
8
|
+
smartoption_mcp.egg-info/SOURCES.txt
|
|
9
|
+
smartoption_mcp.egg-info/dependency_links.txt
|
|
10
|
+
smartoption_mcp.egg-info/entry_points.txt
|
|
11
|
+
smartoption_mcp.egg-info/requires.txt
|
|
12
|
+
smartoption_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
smartoption_mcp
|