mailsocket-mcp 0.2.2__tar.gz → 0.3.0__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.
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/PKG-INFO +26 -4
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/README.md +24 -2
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp/server.py +39 -8
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp.egg-info/PKG-INFO +26 -4
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp.egg-info/requires.txt +1 -1
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/pyproject.toml +2 -2
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/tests/test_server.py +85 -11
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/LICENSE +0 -0
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp/__init__.py +0 -0
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp/remote.py +0 -0
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp.egg-info/SOURCES.txt +0 -0
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp.egg-info/dependency_links.txt +0 -0
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp.egg-info/entry_points.txt +0 -0
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/mailsocket_mcp.egg-info/top_level.txt +0 -0
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/setup.cfg +0 -0
- {mailsocket_mcp-0.2.2 → mailsocket_mcp-0.3.0}/tests/test_remote.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mailsocket-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: MCP server exposing mailsocket inboxes + wait-for-OTP to AI agents.
|
|
5
5
|
Author: mailsocket
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Communications :: Email
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: mailsocket>=0.
|
|
22
|
+
Requires-Dist: mailsocket>=0.2.0
|
|
23
23
|
Requires-Dist: mcp<3.0,>=2.0
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
@@ -39,8 +39,8 @@ no second HTTP client, no re-implemented polling.
|
|
|
39
39
|
| Tool | What the agent gets |
|
|
40
40
|
| --- | --- |
|
|
41
41
|
| `create_inbox(label?)` | a fresh inbox `id` + `address` |
|
|
42
|
-
| `wait_for_otp(inbox_id, timeout?=60, min_confidence?=0)` | **the headline** — blocks (bounded: requested deadline clamped to 120s locally, 55s on the remote server) and returns the OTP string + confidence + subject/from |
|
|
43
|
-
| `wait_for_link(inbox_id, timeout?=60)` | the extracted magic link (returned, *not* followed) |
|
|
42
|
+
| `wait_for_otp(inbox_id, timeout?=60, min_confidence?=0, since?)` | **the headline** — blocks (bounded: requested deadline clamped to 120s locally, 55s on the remote server) and returns the OTP string + confidence + subject/from/id/received_at. Default `since` omitted: only messages arriving after this call are matched. |
|
|
43
|
+
| `wait_for_link(inbox_id, timeout?=60, since?)` | the extracted magic link (returned, *not* followed) + subject/from/id/received_at. Default `since` omitted: only messages arriving after this call are matched. |
|
|
44
44
|
| `list_inboxes(limit?, cursor?)` | one page of inboxes owned by the key (server default page size 25) + `next_cursor`/`has_more` |
|
|
45
45
|
| `list_messages(inbox_id, has_otp?, subject_contains?, sender?, limit?, cursor?)` | one page of messages, optionally filtered (server default page size 25) + `next_cursor`/`has_more` |
|
|
46
46
|
| `get_latest(inbox_id)` | the newest message, without blocking |
|
|
@@ -50,6 +50,21 @@ Errors from the SDK (`AuthError` / `NotFound` / `RateLimited` / `WaitTimeout`)
|
|
|
50
50
|
are turned into clean MCP tool errors — no stack traces, and the API key is
|
|
51
51
|
never echoed back.
|
|
52
52
|
|
|
53
|
+
### Reusing an inbox
|
|
54
|
+
|
|
55
|
+
`create_inbox` needs no `since` — a fresh inbox only ever has new messages.
|
|
56
|
+
By default, `wait_for_otp`/`wait_for_link` omit `since` entirely, so the
|
|
57
|
+
server matches only messages arriving AFTER the call (the request start
|
|
58
|
+
time). But if you reuse an existing inbox (e.g. to request a second
|
|
59
|
+
OTP/link), pass `since` explicitly: otherwise a call with no `since` will
|
|
60
|
+
correctly ignore anything already sitting in the inbox, which is right for
|
|
61
|
+
most reuse, but if you actually WANT to catch a code that already arrived
|
|
62
|
+
before you called the tool, pass either the time right before you triggered
|
|
63
|
+
the new email (ISO8601 or unix seconds), or the `id` of the last message you
|
|
64
|
+
saw (`msg_...`, meaning "only messages after that one") — both tools return
|
|
65
|
+
`id`/`received_at` on the matched message so you can chain `since=<id>` into
|
|
66
|
+
the next call on that same inbox.
|
|
67
|
+
|
|
53
68
|
## Requirements
|
|
54
69
|
|
|
55
70
|
- Python 3.10+
|
|
@@ -183,6 +198,13 @@ remote-transport tests talk to a local fake upstream on `127.0.0.1`.
|
|
|
183
198
|
|
|
184
199
|
## Changelog
|
|
185
200
|
|
|
201
|
+
- **0.3.0** — BEHAVIOUR CHANGE (via `mailsocket>=0.2.0`): `wait_for_otp` /
|
|
202
|
+
`wait_for_link` now omit `since` by default instead of forwarding `0`
|
|
203
|
+
("any message already in the inbox"). The old default could return a
|
|
204
|
+
STALE OTP/link already sitting in a reused inbox; the new default matches
|
|
205
|
+
only messages arriving after the call. Pass `since=0` explicitly to keep
|
|
206
|
+
the old behaviour.
|
|
207
|
+
- **0.2.3** — `wait_for_otp` / `wait_for_link` accept an optional `since` (ISO8601, unix seconds or a message id) so a reused inbox returns the NEW code; results include the message `id` and `received_at`.
|
|
186
208
|
- **0.2.2** — depends on `mailsocket>=0.1.3` (stricter `_seg` id validation in the underlying Python SDK).
|
|
187
209
|
|
|
188
210
|
<!-- mcp-name: app.mailsocket/mailsocket-mcp -->
|
|
@@ -12,8 +12,8 @@ no second HTTP client, no re-implemented polling.
|
|
|
12
12
|
| Tool | What the agent gets |
|
|
13
13
|
| --- | --- |
|
|
14
14
|
| `create_inbox(label?)` | a fresh inbox `id` + `address` |
|
|
15
|
-
| `wait_for_otp(inbox_id, timeout?=60, min_confidence?=0)` | **the headline** — blocks (bounded: requested deadline clamped to 120s locally, 55s on the remote server) and returns the OTP string + confidence + subject/from |
|
|
16
|
-
| `wait_for_link(inbox_id, timeout?=60)` | the extracted magic link (returned, *not* followed) |
|
|
15
|
+
| `wait_for_otp(inbox_id, timeout?=60, min_confidence?=0, since?)` | **the headline** — blocks (bounded: requested deadline clamped to 120s locally, 55s on the remote server) and returns the OTP string + confidence + subject/from/id/received_at. Default `since` omitted: only messages arriving after this call are matched. |
|
|
16
|
+
| `wait_for_link(inbox_id, timeout?=60, since?)` | the extracted magic link (returned, *not* followed) + subject/from/id/received_at. Default `since` omitted: only messages arriving after this call are matched. |
|
|
17
17
|
| `list_inboxes(limit?, cursor?)` | one page of inboxes owned by the key (server default page size 25) + `next_cursor`/`has_more` |
|
|
18
18
|
| `list_messages(inbox_id, has_otp?, subject_contains?, sender?, limit?, cursor?)` | one page of messages, optionally filtered (server default page size 25) + `next_cursor`/`has_more` |
|
|
19
19
|
| `get_latest(inbox_id)` | the newest message, without blocking |
|
|
@@ -23,6 +23,21 @@ Errors from the SDK (`AuthError` / `NotFound` / `RateLimited` / `WaitTimeout`)
|
|
|
23
23
|
are turned into clean MCP tool errors — no stack traces, and the API key is
|
|
24
24
|
never echoed back.
|
|
25
25
|
|
|
26
|
+
### Reusing an inbox
|
|
27
|
+
|
|
28
|
+
`create_inbox` needs no `since` — a fresh inbox only ever has new messages.
|
|
29
|
+
By default, `wait_for_otp`/`wait_for_link` omit `since` entirely, so the
|
|
30
|
+
server matches only messages arriving AFTER the call (the request start
|
|
31
|
+
time). But if you reuse an existing inbox (e.g. to request a second
|
|
32
|
+
OTP/link), pass `since` explicitly: otherwise a call with no `since` will
|
|
33
|
+
correctly ignore anything already sitting in the inbox, which is right for
|
|
34
|
+
most reuse, but if you actually WANT to catch a code that already arrived
|
|
35
|
+
before you called the tool, pass either the time right before you triggered
|
|
36
|
+
the new email (ISO8601 or unix seconds), or the `id` of the last message you
|
|
37
|
+
saw (`msg_...`, meaning "only messages after that one") — both tools return
|
|
38
|
+
`id`/`received_at` on the matched message so you can chain `since=<id>` into
|
|
39
|
+
the next call on that same inbox.
|
|
40
|
+
|
|
26
41
|
## Requirements
|
|
27
42
|
|
|
28
43
|
- Python 3.10+
|
|
@@ -156,6 +171,13 @@ remote-transport tests talk to a local fake upstream on `127.0.0.1`.
|
|
|
156
171
|
|
|
157
172
|
## Changelog
|
|
158
173
|
|
|
174
|
+
- **0.3.0** — BEHAVIOUR CHANGE (via `mailsocket>=0.2.0`): `wait_for_otp` /
|
|
175
|
+
`wait_for_link` now omit `since` by default instead of forwarding `0`
|
|
176
|
+
("any message already in the inbox"). The old default could return a
|
|
177
|
+
STALE OTP/link already sitting in a reused inbox; the new default matches
|
|
178
|
+
only messages arriving after the call. Pass `since=0` explicitly to keep
|
|
179
|
+
the old behaviour.
|
|
180
|
+
- **0.2.3** — `wait_for_otp` / `wait_for_link` accept an optional `since` (ISO8601, unix seconds or a message id) so a reused inbox returns the NEW code; results include the message `id` and `received_at`.
|
|
159
181
|
- **0.2.2** — depends on `mailsocket>=0.1.3` (stricter `_seg` id validation in the underlying Python SDK).
|
|
160
182
|
|
|
161
183
|
<!-- mcp-name: app.mailsocket/mailsocket-mcp -->
|
|
@@ -52,7 +52,7 @@ __all__ = [
|
|
|
52
52
|
# Canonical version, re-exported as ``mailsocket_mcp.__version__`` (see
|
|
53
53
|
# __init__.py). Defined here, not in __init__.py, because __init__.py imports
|
|
54
54
|
# this module at package-load time and a circular self-import would break.
|
|
55
|
-
__version__ = "0.
|
|
55
|
+
__version__ = "0.3.0"
|
|
56
56
|
|
|
57
57
|
# The headline wait is bounded: an agent can request up to the transport's
|
|
58
58
|
# ceiling as the overall deadline (clamped), though transport overhead can push
|
|
@@ -461,6 +461,7 @@ def wait_for_otp(
|
|
|
461
461
|
inbox_id: str,
|
|
462
462
|
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
463
463
|
min_confidence: float = 0.0,
|
|
464
|
+
since: str | int | float | None = None,
|
|
464
465
|
) -> dict:
|
|
465
466
|
"""Block (bounded) until an OTP arrives in ``inbox_id`` and return it.
|
|
466
467
|
|
|
@@ -477,21 +478,36 @@ def wait_for_otp(
|
|
|
477
478
|
slightly after the ceiling in practice. On a timeout just call it again.
|
|
478
479
|
``min_confidence`` filters out low-confidence OTP extractions.
|
|
479
480
|
|
|
481
|
+
``since`` (optional) only matches messages that arrived after this point:
|
|
482
|
+
an ISO8601 timestamp, unix seconds, or a message id (``msg_...``, meaning
|
|
483
|
+
"only messages after that one"; an unknown id falls back to the oldest
|
|
484
|
+
message, so prefer a timestamp if unsure). Omitted (the default), the
|
|
485
|
+
server matches only messages that arrive during this call — the right
|
|
486
|
+
choice for a fresh inbox from ``create_inbox``. When REUSING an inbox for
|
|
487
|
+
a second code and you want to catch one that already arrived before this
|
|
488
|
+
call, pass either the time just before you triggered this email, or the
|
|
489
|
+
``id`` of the last message you saw, so you get the NEW code instead of
|
|
490
|
+
missing it.
|
|
491
|
+
|
|
480
492
|
Side effects: none beyond the long-poll HTTP call(s) needed to satisfy the
|
|
481
493
|
wait (``openWorldHint`` — it talks to the live mailsocket API and blocks
|
|
482
494
|
for up to ``timeout`` seconds). Returns the OTP string, a confidence
|
|
483
|
-
score,
|
|
484
|
-
|
|
495
|
+
score, the subject/from of the matching message, and its ``id`` /
|
|
496
|
+
``received_at`` (chain ``since=<id>`` into the next call on this inbox).
|
|
497
|
+
Raises a clean tool error (``WaitTimeout``) if nothing matching arrives
|
|
498
|
+
before the deadline.
|
|
485
499
|
"""
|
|
500
|
+
kwargs = {} if since is None else {"since": since}
|
|
486
501
|
result = _get_client().wait_for_otp(
|
|
487
502
|
inbox_id,
|
|
488
503
|
timeout=_clamp_timeout(timeout),
|
|
489
504
|
min_confidence=min_confidence,
|
|
505
|
+
**kwargs,
|
|
490
506
|
)
|
|
491
507
|
return {
|
|
492
508
|
"otp": result.otp,
|
|
493
509
|
"confidence": result.confidence,
|
|
494
|
-
**_ctx(result, ("subject", "from")),
|
|
510
|
+
**_ctx(result, ("subject", "from", "id", "received_at")),
|
|
495
511
|
}
|
|
496
512
|
|
|
497
513
|
|
|
@@ -502,6 +518,7 @@ def wait_for_otp(
|
|
|
502
518
|
def wait_for_link(
|
|
503
519
|
inbox_id: str,
|
|
504
520
|
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
521
|
+
since: str | int | float | None = None,
|
|
505
522
|
) -> dict:
|
|
506
523
|
"""Block (bounded) until a magic link arrives in ``inbox_id`` and return it.
|
|
507
524
|
|
|
@@ -518,15 +535,29 @@ def wait_for_link(
|
|
|
518
535
|
return slightly after the ceiling in practice. On a timeout just call it
|
|
519
536
|
again.
|
|
520
537
|
|
|
538
|
+
``since`` (optional) only matches messages that arrived after this point:
|
|
539
|
+
an ISO8601 timestamp, unix seconds, or a message id (``msg_...``, meaning
|
|
540
|
+
"only messages after that one"; an unknown id falls back to the oldest
|
|
541
|
+
message, so prefer a timestamp if unsure). Omitted (the default), the
|
|
542
|
+
server matches only messages that arrive during this call — the right
|
|
543
|
+
choice for a fresh inbox from ``create_inbox``. When REUSING an inbox for
|
|
544
|
+
a second link and you want to catch one that already arrived before this
|
|
545
|
+
call, pass either the time just before you triggered this email, or the
|
|
546
|
+
``id`` of the last message you saw, so you get the NEW link instead of
|
|
547
|
+
missing it.
|
|
548
|
+
|
|
521
549
|
Side effects: none beyond the long-poll HTTP call(s) needed to satisfy the
|
|
522
550
|
wait (``openWorldHint`` — it talks to the live mailsocket API and blocks
|
|
523
|
-
for up to ``timeout`` seconds).
|
|
524
|
-
|
|
551
|
+
for up to ``timeout`` seconds). Returns the magic link, the subject/from
|
|
552
|
+
of the matching message, and its ``id`` / ``received_at`` (chain
|
|
553
|
+
``since=<id>`` into the next call on this inbox). Raises a clean tool
|
|
554
|
+
error (``WaitTimeout``) if no link arrives before the deadline.
|
|
525
555
|
"""
|
|
526
|
-
|
|
556
|
+
kwargs = {} if since is None else {"since": since}
|
|
557
|
+
result = _get_client().wait_for_link(inbox_id, timeout=_clamp_timeout(timeout), **kwargs)
|
|
527
558
|
return {
|
|
528
559
|
"magic_link": result.magic_link,
|
|
529
|
-
**_ctx(result, ("subject", "from")),
|
|
560
|
+
**_ctx(result, ("subject", "from", "id", "received_at")),
|
|
530
561
|
}
|
|
531
562
|
|
|
532
563
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mailsocket-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: MCP server exposing mailsocket inboxes + wait-for-OTP to AI agents.
|
|
5
5
|
Author: mailsocket
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Communications :: Email
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: mailsocket>=0.
|
|
22
|
+
Requires-Dist: mailsocket>=0.2.0
|
|
23
23
|
Requires-Dist: mcp<3.0,>=2.0
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
@@ -39,8 +39,8 @@ no second HTTP client, no re-implemented polling.
|
|
|
39
39
|
| Tool | What the agent gets |
|
|
40
40
|
| --- | --- |
|
|
41
41
|
| `create_inbox(label?)` | a fresh inbox `id` + `address` |
|
|
42
|
-
| `wait_for_otp(inbox_id, timeout?=60, min_confidence?=0)` | **the headline** — blocks (bounded: requested deadline clamped to 120s locally, 55s on the remote server) and returns the OTP string + confidence + subject/from |
|
|
43
|
-
| `wait_for_link(inbox_id, timeout?=60)` | the extracted magic link (returned, *not* followed) |
|
|
42
|
+
| `wait_for_otp(inbox_id, timeout?=60, min_confidence?=0, since?)` | **the headline** — blocks (bounded: requested deadline clamped to 120s locally, 55s on the remote server) and returns the OTP string + confidence + subject/from/id/received_at. Default `since` omitted: only messages arriving after this call are matched. |
|
|
43
|
+
| `wait_for_link(inbox_id, timeout?=60, since?)` | the extracted magic link (returned, *not* followed) + subject/from/id/received_at. Default `since` omitted: only messages arriving after this call are matched. |
|
|
44
44
|
| `list_inboxes(limit?, cursor?)` | one page of inboxes owned by the key (server default page size 25) + `next_cursor`/`has_more` |
|
|
45
45
|
| `list_messages(inbox_id, has_otp?, subject_contains?, sender?, limit?, cursor?)` | one page of messages, optionally filtered (server default page size 25) + `next_cursor`/`has_more` |
|
|
46
46
|
| `get_latest(inbox_id)` | the newest message, without blocking |
|
|
@@ -50,6 +50,21 @@ Errors from the SDK (`AuthError` / `NotFound` / `RateLimited` / `WaitTimeout`)
|
|
|
50
50
|
are turned into clean MCP tool errors — no stack traces, and the API key is
|
|
51
51
|
never echoed back.
|
|
52
52
|
|
|
53
|
+
### Reusing an inbox
|
|
54
|
+
|
|
55
|
+
`create_inbox` needs no `since` — a fresh inbox only ever has new messages.
|
|
56
|
+
By default, `wait_for_otp`/`wait_for_link` omit `since` entirely, so the
|
|
57
|
+
server matches only messages arriving AFTER the call (the request start
|
|
58
|
+
time). But if you reuse an existing inbox (e.g. to request a second
|
|
59
|
+
OTP/link), pass `since` explicitly: otherwise a call with no `since` will
|
|
60
|
+
correctly ignore anything already sitting in the inbox, which is right for
|
|
61
|
+
most reuse, but if you actually WANT to catch a code that already arrived
|
|
62
|
+
before you called the tool, pass either the time right before you triggered
|
|
63
|
+
the new email (ISO8601 or unix seconds), or the `id` of the last message you
|
|
64
|
+
saw (`msg_...`, meaning "only messages after that one") — both tools return
|
|
65
|
+
`id`/`received_at` on the matched message so you can chain `since=<id>` into
|
|
66
|
+
the next call on that same inbox.
|
|
67
|
+
|
|
53
68
|
## Requirements
|
|
54
69
|
|
|
55
70
|
- Python 3.10+
|
|
@@ -183,6 +198,13 @@ remote-transport tests talk to a local fake upstream on `127.0.0.1`.
|
|
|
183
198
|
|
|
184
199
|
## Changelog
|
|
185
200
|
|
|
201
|
+
- **0.3.0** — BEHAVIOUR CHANGE (via `mailsocket>=0.2.0`): `wait_for_otp` /
|
|
202
|
+
`wait_for_link` now omit `since` by default instead of forwarding `0`
|
|
203
|
+
("any message already in the inbox"). The old default could return a
|
|
204
|
+
STALE OTP/link already sitting in a reused inbox; the new default matches
|
|
205
|
+
only messages arriving after the call. Pass `since=0` explicitly to keep
|
|
206
|
+
the old behaviour.
|
|
207
|
+
- **0.2.3** — `wait_for_otp` / `wait_for_link` accept an optional `since` (ISO8601, unix seconds or a message id) so a reused inbox returns the NEW code; results include the message `id` and `received_at`.
|
|
186
208
|
- **0.2.2** — depends on `mailsocket>=0.1.3` (stricter `_seg` id validation in the underlying Python SDK).
|
|
187
209
|
|
|
188
210
|
<!-- mcp-name: app.mailsocket/mailsocket-mcp -->
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mailsocket-mcp"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "MCP server exposing mailsocket inboxes + wait-for-OTP to AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Topic :: Communications :: Email",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"mailsocket>=0.
|
|
25
|
+
"mailsocket>=0.2.0", # published on PyPI as `mailsocket` (0.2.0: since defaults to omitted, not 0 — see CHANGELOG)
|
|
26
26
|
"mcp>=2.0,<3.0",
|
|
27
27
|
]
|
|
28
28
|
|
|
@@ -61,14 +61,14 @@ class FakeClient:
|
|
|
61
61
|
self.created.append(label)
|
|
62
62
|
return {"id": "inbox_1", "address": "x@in.inboxpipe.net"}
|
|
63
63
|
|
|
64
|
-
def wait_for_otp(self, inbox_id, *, timeout=60.0, min_confidence=0.0, since=
|
|
65
|
-
self.wait_otp_calls.append((inbox_id, timeout, min_confidence))
|
|
64
|
+
def wait_for_otp(self, inbox_id, *, timeout=60.0, min_confidence=0.0, since=None):
|
|
65
|
+
self.wait_otp_calls.append((inbox_id, timeout, min_confidence, since))
|
|
66
66
|
if isinstance(self.wait_otp, Exception):
|
|
67
67
|
raise self.wait_otp
|
|
68
68
|
return self.wait_otp
|
|
69
69
|
|
|
70
|
-
def wait_for_link(self, inbox_id, *, timeout=60.0, since=
|
|
71
|
-
self.wait_link_calls.append((inbox_id, timeout))
|
|
70
|
+
def wait_for_link(self, inbox_id, *, timeout=60.0, since=None):
|
|
71
|
+
self.wait_link_calls.append((inbox_id, timeout, since))
|
|
72
72
|
if isinstance(self.wait_link, Exception):
|
|
73
73
|
raise self.wait_link
|
|
74
74
|
return self.wait_link
|
|
@@ -233,10 +233,11 @@ def test_wait_for_otp_returns_otp_and_confidence(fake_client):
|
|
|
233
233
|
assert result.is_error is False
|
|
234
234
|
text = result.content[0].text
|
|
235
235
|
assert '"123456"' in text
|
|
236
|
-
inbox_id, timeout, min_conf = fake_client.wait_otp_calls[0]
|
|
236
|
+
inbox_id, timeout, min_conf, since = fake_client.wait_otp_calls[0]
|
|
237
237
|
assert inbox_id == "inbox_abc"
|
|
238
238
|
assert timeout == 60
|
|
239
239
|
assert min_conf == 0.0
|
|
240
|
+
assert since is None # absent -> SDK omits since (server uses request start time)
|
|
240
241
|
|
|
241
242
|
|
|
242
243
|
def test_wait_for_otp_timeout_is_clamped(fake_client):
|
|
@@ -244,14 +245,55 @@ def test_wait_for_otp_timeout_is_clamped(fake_client):
|
|
|
244
245
|
|
|
245
246
|
call_tool("wait_for_otp", {"inbox_id": "inbox_abc", "timeout": 9999})
|
|
246
247
|
|
|
247
|
-
_, timeout, _ = fake_client.wait_otp_calls[0]
|
|
248
|
+
_, timeout, _, _ = fake_client.wait_otp_calls[0]
|
|
248
249
|
assert timeout == MAX_WAIT_TIMEOUT
|
|
249
250
|
|
|
250
251
|
call_tool("wait_for_otp", {"inbox_id": "inbox_abc", "timeout": 0})
|
|
251
|
-
_, timeout, _ = fake_client.wait_otp_calls[1]
|
|
252
|
+
_, timeout, _, _ = fake_client.wait_otp_calls[1]
|
|
252
253
|
assert timeout == 1.0
|
|
253
254
|
|
|
254
255
|
|
|
256
|
+
def test_wait_for_otp_since_passed_through(fake_client):
|
|
257
|
+
fake_client.wait_otp = FakeWaitResult(otp="123456")
|
|
258
|
+
|
|
259
|
+
call_tool("wait_for_otp", {"inbox_id": "inbox_abc", "since": "msg_111"})
|
|
260
|
+
|
|
261
|
+
inbox_id, _, _, since = fake_client.wait_otp_calls[0]
|
|
262
|
+
assert inbox_id == "inbox_abc"
|
|
263
|
+
assert since == "msg_111"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_wait_for_otp_explicit_since_zero_passed_through(fake_client):
|
|
267
|
+
"""since=0 (include messages already in the inbox) must reach the SDK as 0,
|
|
268
|
+
not be dropped as falsy and turned into the new request-start default."""
|
|
269
|
+
fake_client.wait_otp = FakeWaitResult(otp="123456")
|
|
270
|
+
|
|
271
|
+
call_tool("wait_for_otp", {"inbox_id": "inbox_abc", "since": 0})
|
|
272
|
+
|
|
273
|
+
_, _, _, since = fake_client.wait_otp_calls[0]
|
|
274
|
+
assert since == 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_wait_for_otp_result_carries_id_and_received_at(fake_client):
|
|
278
|
+
fake_client.wait_otp = FakeWaitResult(
|
|
279
|
+
otp="123456",
|
|
280
|
+
confidence=0.95,
|
|
281
|
+
message={
|
|
282
|
+
"subject": "Your code",
|
|
283
|
+
"from": "Acme <no-reply@acme.test>",
|
|
284
|
+
"id": "msg_999",
|
|
285
|
+
"received_at": "2024-01-01T00:00:00Z",
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
result = call_tool("wait_for_otp", {"inbox_id": "inbox_abc"})
|
|
290
|
+
|
|
291
|
+
assert result.is_error is False
|
|
292
|
+
text = result.content[0].text
|
|
293
|
+
assert '"msg_999"' in text
|
|
294
|
+
assert '"2024-01-01T00:00:00Z"' in text
|
|
295
|
+
|
|
296
|
+
|
|
255
297
|
def test_wait_for_otp_wait_timeout_is_clean_error(fake_client):
|
|
256
298
|
fake_client.wait_otp = WaitTimeout("No matching message arrived within 60s.")
|
|
257
299
|
|
|
@@ -274,6 +316,38 @@ def test_wait_for_link_returns_magic_link(fake_client):
|
|
|
274
316
|
|
|
275
317
|
assert result.is_error is False
|
|
276
318
|
assert "https://example.test/go?token=abc" in result.content[0].text
|
|
319
|
+
inbox_id, timeout, since = fake_client.wait_link_calls[0]
|
|
320
|
+
assert inbox_id == "inbox_abc"
|
|
321
|
+
assert since is None # absent -> SDK omits since (server uses request start time)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_wait_for_link_since_passed_through(fake_client):
|
|
325
|
+
fake_client.wait_link = FakeWaitResult(magic_link="https://example.test/go?token=abc")
|
|
326
|
+
|
|
327
|
+
call_tool("wait_for_link", {"inbox_id": "inbox_abc", "since": "2024-01-01T00:00:00Z"})
|
|
328
|
+
|
|
329
|
+
inbox_id, _, since = fake_client.wait_link_calls[0]
|
|
330
|
+
assert inbox_id == "inbox_abc"
|
|
331
|
+
assert since == "2024-01-01T00:00:00Z"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_wait_for_link_result_carries_id_and_received_at(fake_client):
|
|
335
|
+
fake_client.wait_link = FakeWaitResult(
|
|
336
|
+
magic_link="https://example.test/go?token=abc",
|
|
337
|
+
message={
|
|
338
|
+
"subject": "Sign in",
|
|
339
|
+
"from": "Acme <no-reply@acme.test>",
|
|
340
|
+
"id": "msg_777",
|
|
341
|
+
"received_at": "2024-02-02T00:00:00Z",
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
result = call_tool("wait_for_link", {"inbox_id": "inbox_abc"})
|
|
346
|
+
|
|
347
|
+
assert result.is_error is False
|
|
348
|
+
text = result.content[0].text
|
|
349
|
+
assert '"msg_777"' in text
|
|
350
|
+
assert '"2024-02-02T00:00:00Z"' in text
|
|
277
351
|
|
|
278
352
|
|
|
279
353
|
# -- create_inbox / list_messages / delete -----------------------------------
|
|
@@ -365,14 +439,14 @@ def test_version_exported():
|
|
|
365
439
|
|
|
366
440
|
|
|
367
441
|
def test_metadata_consistency_across_files():
|
|
368
|
-
"""Versions match everywhere: MCP 0.
|
|
369
|
-
and its Python SDK sibling 0.
|
|
442
|
+
"""Versions match everywhere: MCP 0.3.0 (pyproject, server.json, __version__)
|
|
443
|
+
and its Python SDK sibling 0.2.0 (pyproject, __version__, user agent)."""
|
|
370
444
|
import tomllib
|
|
371
445
|
from pathlib import Path
|
|
372
446
|
|
|
373
447
|
root = Path(__file__).resolve().parents[1]
|
|
374
|
-
expected = "0.
|
|
375
|
-
expected_sdk = "0.
|
|
448
|
+
expected = "0.3.0"
|
|
449
|
+
expected_sdk = "0.2.0"
|
|
376
450
|
|
|
377
451
|
pyproject = tomllib.loads((root / "pyproject.toml").read_text())
|
|
378
452
|
assert pyproject["project"]["version"] == expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|