Skip to content

Commit f450227

Browse files
committed
feat(mcp): support discord approvals
- add Discord approval manager plumbing and CLI/server flags - gate write-capable Schwab tools behind reviewer approval or override - surface progress keepalives so long-running approvals don’t timeout - document Discord bot setup steps and expand test coverage
1 parent f38519c commit f450227

28 files changed

+2012
-121
lines changed

README.md

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ the MCP [python-sdk](https://github.com/modelcontextprotocol/python-sdk).
1414
- Access order and transaction history
1515
- Comprehensive order building and placement capabilities
1616
- Advanced order strategies (OCO, trigger-based, and bracket orders)
17-
- Modify account state with special tools (requires `--jesus-take-the-wheel` flag)
17+
- Modify account state with special tools gated behind Discord approvals (bypass with `--jesus-take-the-wheel`)
1818
- Designed to integrate with Large Language Models (LLMs)
1919

2020
## Installation
@@ -57,24 +57,61 @@ After authentication, you can run the server:
5757

5858
```bash
5959
# Run the server with default token path
60-
uv run schwab-mcp server --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET --callback-url YOUR_CALLBACK_URL
60+
uv run schwab-mcp server \
61+
--client-id YOUR_CLIENT_ID \
62+
--client-secret YOUR_CLIENT_SECRET \
63+
--callback-url YOUR_CALLBACK_URL \
64+
--discord-token YOUR_DISCORD_BOT_TOKEN \
65+
--discord-channel-id YOUR_APPROVAL_CHANNEL_ID \
66+
--discord-approver DISCORD_USER_ID [--discord-approver ANOTHER_USER_ID ...]
6167

6268
# Run with a custom token path
63-
uv run schwab-mcp server --token-path /path/to/token.json --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET --callback-url YOUR_CALLBACK_URL
69+
uv run schwab-mcp server \
70+
--token-path /path/to/token.json \
71+
--client-id YOUR_CLIENT_ID \
72+
--client-secret YOUR_CLIENT_SECRET \
73+
--callback-url YOUR_CALLBACK_URL \
74+
--discord-token YOUR_DISCORD_BOT_TOKEN \
75+
--discord-channel-id YOUR_APPROVAL_CHANNEL_ID
76+
77+
# Run with account modification tools auto-approved (no Discord prompt)
78+
uv run schwab-mcp server \
79+
--jesus-take-the-wheel \
80+
--client-id YOUR_CLIENT_ID \
81+
--client-secret YOUR_CLIENT_SECRET \
82+
--callback-url YOUR_CALLBACK_URL
6483

65-
# Run with account modification tools enabled
66-
uv run schwab-mcp server --jesus-take-the-wheel --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET --callback-url YOUR_CALLBACK_URL
6784
```
6885

6986
Token age is validated - if older than 5 days, you will be prompted to re-authenticate.
7087

88+
Discord-related flags can also be provided via environment variables:
89+
- `SCHWAB_MCP_DISCORD_TOKEN`
90+
- `SCHWAB_MCP_DISCORD_CHANNEL_ID`
91+
- `SCHWAB_MCP_DISCORD_TIMEOUT` (optional, defaults to 600 seconds)
92+
- `SCHWAB_MCP_DISCORD_APPROVERS` (optional comma-separated list of Discord user IDs)
93+
94+
### Discord approval setup
95+
96+
1. Create or open your application at <https://discord.com/developers/applications>, add a **Bot**, reset the token, and paste it into `SCHWAB_MCP_DISCORD_TOKEN`.
97+
2. In the **Installation** tab (left sidebar) set *Install Link* to `None` so the default authorization link is disabled.
98+
3. Open the **Bot** tab, toggle **Public Bot** off.
99+
4. Use **OAuth2 → URL Generator** to build the invite link by selecting the `bot` scope. When the permissions matrix appears, grant the bot:
100+
- View Channel
101+
- Send Messages
102+
- Embed Links
103+
- Add Reactions
104+
- Read Message History
105+
- Manage Messages
106+
5. Copy the generated URL (the permissions integer reflects the boxes you checked), open it while logged into the Discord account that owns the approval server, and authorize the bot into the channel where approvals should post.
107+
71108
> **WARNING**: Using the `--jesus-take-the-wheel` flag enables tools that can modify your account state. Use with caution as this allows LLMs to cancel orders and potentially perform other actions that change account state.
72109
73110
## Available Tools
74111

75112
The server exposes the following MCP tools:
76113

77-
> **Note**: Tools 28-35 require the `--jesus-take-the-wheel` flag as they can modify account state by placing orders.
114+
> **Note**: Tools 27-39 will pause for Discord approval before executing. Pass `--jesus-take-the-wheel` to bypass the approval workflow entirely.
78115
79116
### Date and Market Information
80117
1. `get_datetime` - Get the current datetime in ISO format

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ requires-python = ">=3.10"
1010
dependencies = [
1111
"anyio>=4.11.0",
1212
"click>=8.3.0",
13+
"discord.py>=2.4.0",
1314
"httpx>=0.27.2",
1415
"mcp>=1.17.0",
1516
"platformdirs>=4.5.0",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import annotations
2+
3+
from schwab_mcp.approvals.base import (
4+
ApprovalDecision,
5+
ApprovalManager,
6+
ApprovalRequest,
7+
NoOpApprovalManager,
8+
)
9+
from schwab_mcp.approvals.discord import (
10+
DiscordApprovalManager,
11+
DiscordApprovalSettings,
12+
)
13+
14+
__all__ = [
15+
"ApprovalDecision",
16+
"ApprovalManager",
17+
"ApprovalRequest",
18+
"NoOpApprovalManager",
19+
"DiscordApprovalManager",
20+
"DiscordApprovalSettings",
21+
]

src/schwab_mcp/approvals/base.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
import abc
4+
from dataclasses import dataclass
5+
from enum import Enum
6+
from typing import Mapping
7+
8+
9+
class ApprovalDecision(str, Enum):
10+
"""Decision returned by an approval workflow."""
11+
12+
APPROVED = "approved"
13+
DENIED = "denied"
14+
EXPIRED = "expired"
15+
16+
17+
@dataclass(slots=True, frozen=True)
18+
class ApprovalRequest:
19+
"""Details about a write tool invocation requiring approval."""
20+
21+
id: str
22+
tool_name: str
23+
request_id: str
24+
client_id: str | None
25+
arguments: Mapping[str, str]
26+
27+
28+
class ApprovalManager(abc.ABC):
29+
"""Interface for asynchronous approval backends."""
30+
31+
async def start(self) -> None:
32+
"""Perform any startup/connection work."""
33+
34+
async def stop(self) -> None:
35+
"""Clean up resources."""
36+
37+
@abc.abstractmethod
38+
async def require(self, request: ApprovalRequest) -> ApprovalDecision:
39+
"""Require approval for the provided request."""
40+
41+
42+
class NoOpApprovalManager(ApprovalManager):
43+
"""Approval manager that always approves requests."""
44+
45+
async def require(self, request: ApprovalRequest) -> ApprovalDecision: # noqa: ARG002
46+
return ApprovalDecision.APPROVED
47+
48+
49+
__all__ = [
50+
"ApprovalDecision",
51+
"ApprovalManager",
52+
"ApprovalRequest",
53+
"NoOpApprovalManager",
54+
]

0 commit comments

Comments
 (0)