Skip to content

Commit 1b81241

Browse files
authored
Merge pull request modelcontextprotocol#22 from jkoelker/jk/auth
fix(auth): align max token age with reauth warning
2 parents bd9d7f5 + f706fce commit 1b81241

File tree

6 files changed

+65
-11
lines changed

6 files changed

+65
-11
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ build-backend = "hatchling.build"
2828
[dependency-groups]
2929
dev = [
3030
"mcp[cli]>=1.17.0",
31-
"pyright>=1.1.406",
31+
"pyright>=1.1.407",
3232
"pytest>=8.4.2",
3333
"ruff>=0.14.0",
3434
]

src/schwab_mcp/auth.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
from schwab_mcp import tokens
1414

15+
16+
DEFAULT_MAX_TOKEN_AGE_SECONDS = 5 * 24 * 60 * 60
17+
1518
if TYPE_CHECKING:
1619
from multiprocessing import Process as ProcessType, Queue as QueueType
1720
else: # pragma: no cover - runtime fallback for multiprocess
@@ -29,7 +32,7 @@ def easy_client(
2932
token_manager: tokens.Manager,
3033
asyncio: bool = False,
3134
enforce_enums: bool = True,
32-
max_token_age: int | None = 60 * 60 * 24 * 13 // 2,
35+
max_token_age: int | None = DEFAULT_MAX_TOKEN_AGE_SECONDS,
3336
callback_timeout: float = 300.0,
3437
interactive: bool = True,
3538
requested_browser: str | None = None,

src/schwab_mcp/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616

1717
APP_NAME = "schwab-mcp"
18+
TOKEN_MAX_AGE_SECONDS = schwab_auth.DEFAULT_MAX_TOKEN_AGE_SECONDS
1819

1920

2021
@click.group()
@@ -68,6 +69,7 @@ def auth(
6869
client_secret=client_secret,
6970
callback_url=callback_url,
7071
token_manager=token_manager,
72+
max_token_age=TOKEN_MAX_AGE_SECONDS,
7173
)
7274

7375
# If we get here, the authentication was successful
@@ -162,6 +164,7 @@ def server(
162164
asyncio=True,
163165
interactive=False,
164166
enforce_enums=False,
167+
max_token_age=TOKEN_MAX_AGE_SECONDS,
165168
)
166169

167170
if not isinstance(client, AsyncClient):
@@ -180,7 +183,7 @@ def server(
180183
return 1
181184

182185
# Check token age
183-
if client.token_age() > 5 * 86400:
186+
if client.token_age() >= TOKEN_MAX_AGE_SECONDS:
184187
send_error_response(
185188
"Token is older than 5 days. Please run 'schwab-mcp auth' to re-authenticate.",
186189
code=401,

tests/test_cli_auth.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from click.testing import CliRunner
6+
7+
from schwab_mcp import cli
8+
9+
10+
def test_auth_command_uses_max_token_age(monkeypatch, tmp_path):
11+
captured: dict[str, Any] = {}
12+
13+
class DummyManager:
14+
def __init__(self, path: str) -> None:
15+
self.path = path
16+
captured["token_path"] = path
17+
18+
def fake_easy_client(**kwargs):
19+
captured["easy_client_kwargs"] = kwargs
20+
return object()
21+
22+
monkeypatch.setattr(cli.tokens, "Manager", DummyManager)
23+
monkeypatch.setattr(cli.schwab_auth, "easy_client", fake_easy_client)
24+
25+
runner = CliRunner()
26+
token_file = tmp_path / "token.yaml"
27+
result = runner.invoke(
28+
cli.cli,
29+
[
30+
"auth",
31+
"--token-path",
32+
str(token_file),
33+
"--client-id",
34+
"cid",
35+
"--client-secret",
36+
"secret",
37+
],
38+
catch_exceptions=False,
39+
)
40+
41+
assert result.exit_code == 0
42+
assert captured["token_path"] == str(token_file)
43+
assert captured["easy_client_kwargs"]["max_token_age"] == cli.TOKEN_MAX_AGE_SECONDS

tests/test_cli_server_write_modes.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from click.testing import CliRunner
4+
from typing import Any
45

56
from schwab_mcp import cli
67
from schwab_mcp.approvals import ApprovalDecision, ApprovalManager, ApprovalRequest, NoOpApprovalManager
@@ -34,11 +35,12 @@ def authorized_user_ids(users):
3435
return frozenset(int(value) for value in users)
3536

3637

37-
def _patch_common(monkeypatch, captured):
38+
def _patch_common(monkeypatch, captured: dict[str, Any]) -> None:
3839
monkeypatch.setattr(cli, "AsyncClient", FakeAsyncClient)
3940

4041
def fake_easy_client(**_kwargs):
4142
captured["easy_client_called"] = True
43+
captured["easy_client_kwargs"] = _kwargs
4244
return FakeAsyncClient()
4345

4446
monkeypatch.setattr(cli.schwab_auth, "easy_client", fake_easy_client)
@@ -58,7 +60,7 @@ async def run(self):
5860

5961

6062
def test_server_defaults_to_read_only(monkeypatch):
61-
captured: dict[str, object] = {}
63+
captured: dict[str, Any] = {}
6264
_patch_common(monkeypatch, captured)
6365

6466
runner = CliRunner()
@@ -77,10 +79,11 @@ def test_server_defaults_to_read_only(monkeypatch):
7779
assert result.exit_code == 0
7880
assert captured["allow_write"] is False
7981
assert isinstance(captured["approval_manager"], NoOpApprovalManager)
82+
assert captured["easy_client_kwargs"]["max_token_age"] == cli.TOKEN_MAX_AGE_SECONDS
8083

8184

8285
def test_server_enables_write_mode_when_flag_set(monkeypatch):
83-
captured: dict[str, object] = {}
86+
captured: dict[str, Any] = {}
8487
_patch_common(monkeypatch, captured)
8588

8689
runner = CliRunner()
@@ -100,10 +103,11 @@ def test_server_enables_write_mode_when_flag_set(monkeypatch):
100103
assert result.exit_code == 0
101104
assert captured["allow_write"] is True
102105
assert isinstance(captured["approval_manager"], NoOpApprovalManager)
106+
assert captured["easy_client_kwargs"]["max_token_age"] == cli.TOKEN_MAX_AGE_SECONDS
103107

104108

105109
def test_server_enables_write_mode_with_discord(monkeypatch):
106-
captured: dict[str, object] = {}
110+
captured: dict[str, Any] = {}
107111
_patch_common(monkeypatch, captured)
108112
monkeypatch.setattr(cli, "DiscordApprovalManager", DummyDiscordApprovalManager)
109113

@@ -129,3 +133,4 @@ def test_server_enables_write_mode_with_discord(monkeypatch):
129133
assert result.exit_code == 0
130134
assert captured["allow_write"] is True
131135
assert isinstance(captured["approval_manager"], DummyDiscordApprovalManager)
136+
assert captured["easy_client_kwargs"]["max_token_age"] == cli.TOKEN_MAX_AGE_SECONDS

uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)