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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailsocket-mcp
3
- Version: 0.2.2
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.1.3
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.2.2"
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, and the subject/from of the matching message. Raises a clean tool
484
- error (``WaitTimeout``) if nothing matching arrives before the deadline.
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). Raises a clean tool error (``WaitTimeout``)
524
- if no link arrives before the deadline.
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
- result = _get_client().wait_for_link(inbox_id, timeout=_clamp_timeout(timeout))
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.2.2
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.1.3
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 -->
@@ -1,4 +1,4 @@
1
- mailsocket>=0.1.3
1
+ mailsocket>=0.2.0
2
2
  mcp<3.0,>=2.0
3
3
 
4
4
  [dev]
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mailsocket-mcp"
7
- version = "0.2.2"
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.1.3", # published on PyPI as `mailsocket` (0.1.3 adds strict _seg validation)
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=0):
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=0):
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.2.2 (pyproject, server.json, __version__)
369
- and its Python SDK sibling 0.1.3 (pyproject, __version__, user agent)."""
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.2.2"
375
- expected_sdk = "0.1.3"
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