mailsocket-mcp 0.2.2__tar.gz → 0.2.3__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.2.3
4
4
  Summary: MCP server exposing mailsocket inboxes + wait-for-OTP to AI agents.
5
5
  Author: mailsocket
6
6
  License-Expression: MIT
@@ -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 |
43
+ | `wait_for_link(inbox_id, timeout?=60, since?)` | the extracted magic link (returned, *not* followed) + subject/from/id/received_at |
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,18 @@ 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
+ But if you reuse an existing inbox (e.g. to request a second OTP/link), pass
57
+ `since` to `wait_for_otp`/`wait_for_link`: otherwise the wait can match the
58
+ OLDEST retained message instead of the new one you just triggered. Pass
59
+ either the time right before you triggered the new email (ISO8601 or unix
60
+ seconds), or the `id` of the last message you saw (`msg_...`, meaning "only
61
+ messages after that one") — both tools return `id`/`received_at` on the
62
+ matched message so you can chain `since=<id>` into the next call on that same
63
+ inbox.
64
+
53
65
  ## Requirements
54
66
 
55
67
  - Python 3.10+
@@ -183,6 +195,7 @@ remote-transport tests talk to a local fake upstream on `127.0.0.1`.
183
195
 
184
196
  ## Changelog
185
197
 
198
+ - **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
199
  - **0.2.2** — depends on `mailsocket>=0.1.3` (stricter `_seg` id validation in the underlying Python SDK).
187
200
 
188
201
  <!-- 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 |
16
+ | `wait_for_link(inbox_id, timeout?=60, since?)` | the extracted magic link (returned, *not* followed) + subject/from/id/received_at |
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,18 @@ 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
+ But if you reuse an existing inbox (e.g. to request a second OTP/link), pass
30
+ `since` to `wait_for_otp`/`wait_for_link`: otherwise the wait can match the
31
+ OLDEST retained message instead of the new one you just triggered. Pass
32
+ either the time right before you triggered the new email (ISO8601 or unix
33
+ seconds), or the `id` of the last message you saw (`msg_...`, meaning "only
34
+ messages after that one") — both tools return `id`/`received_at` on the
35
+ matched message so you can chain `since=<id>` into the next call on that same
36
+ inbox.
37
+
26
38
  ## Requirements
27
39
 
28
40
  - Python 3.10+
@@ -156,6 +168,7 @@ remote-transport tests talk to a local fake upstream on `127.0.0.1`.
156
168
 
157
169
  ## Changelog
158
170
 
171
+ - **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
172
  - **0.2.2** — depends on `mailsocket>=0.1.3` (stricter `_seg` id validation in the underlying Python SDK).
160
173
 
161
174
  <!-- 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.2.3"
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,35 @@ 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). A fresh inbox straight from
485
+ ``create_inbox`` needs nothing here — the default already matches
486
+ anything in it. When REUSING an inbox for a second code, pass either the
487
+ time just before you triggered this email, or the ``id`` of the last
488
+ message you saw, so you get the NEW code instead of the oldest one still
489
+ retained in the inbox.
490
+
480
491
  Side effects: none beyond the long-poll HTTP call(s) needed to satisfy the
481
492
  wait (``openWorldHint`` — it talks to the live mailsocket API and blocks
482
493
  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.
494
+ score, the subject/from of the matching message, and its ``id`` /
495
+ ``received_at`` (chain ``since=<id>`` into the next call on this inbox).
496
+ Raises a clean tool error (``WaitTimeout``) if nothing matching arrives
497
+ before the deadline.
485
498
  """
499
+ kwargs = {} if since is None else {"since": since}
486
500
  result = _get_client().wait_for_otp(
487
501
  inbox_id,
488
502
  timeout=_clamp_timeout(timeout),
489
503
  min_confidence=min_confidence,
504
+ **kwargs,
490
505
  )
491
506
  return {
492
507
  "otp": result.otp,
493
508
  "confidence": result.confidence,
494
- **_ctx(result, ("subject", "from")),
509
+ **_ctx(result, ("subject", "from", "id", "received_at")),
495
510
  }
496
511
 
497
512
 
@@ -502,6 +517,7 @@ def wait_for_otp(
502
517
  def wait_for_link(
503
518
  inbox_id: str,
504
519
  timeout: float = DEFAULT_WAIT_TIMEOUT,
520
+ since: str | int | float | None = None,
505
521
  ) -> dict:
506
522
  """Block (bounded) until a magic link arrives in ``inbox_id`` and return it.
507
523
 
@@ -518,15 +534,28 @@ def wait_for_link(
518
534
  return slightly after the ceiling in practice. On a timeout just call it
519
535
  again.
520
536
 
537
+ ``since`` (optional) only matches messages that arrived after this point:
538
+ an ISO8601 timestamp, unix seconds, or a message id (``msg_...``, meaning
539
+ "only messages after that one"; an unknown id falls back to the oldest
540
+ message, so prefer a timestamp if unsure). A fresh inbox straight from
541
+ ``create_inbox`` needs nothing here — the default already matches
542
+ anything in it. When REUSING an inbox for a second link, pass either the
543
+ time just before you triggered this email, or the ``id`` of the last
544
+ message you saw, so you get the NEW link instead of the oldest one still
545
+ retained in the inbox.
546
+
521
547
  Side effects: none beyond the long-poll HTTP call(s) needed to satisfy the
522
548
  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.
549
+ for up to ``timeout`` seconds). Returns the magic link, the subject/from
550
+ of the matching message, and its ``id`` / ``received_at`` (chain
551
+ ``since=<id>`` into the next call on this inbox). Raises a clean tool
552
+ error (``WaitTimeout``) if no link arrives before the deadline.
525
553
  """
526
- result = _get_client().wait_for_link(inbox_id, timeout=_clamp_timeout(timeout))
554
+ kwargs = {} if since is None else {"since": since}
555
+ result = _get_client().wait_for_link(inbox_id, timeout=_clamp_timeout(timeout), **kwargs)
527
556
  return {
528
557
  "magic_link": result.magic_link,
529
- **_ctx(result, ("subject", "from")),
558
+ **_ctx(result, ("subject", "from", "id", "received_at")),
530
559
  }
531
560
 
532
561
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailsocket-mcp
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: MCP server exposing mailsocket inboxes + wait-for-OTP to AI agents.
5
5
  Author: mailsocket
6
6
  License-Expression: MIT
@@ -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 |
43
+ | `wait_for_link(inbox_id, timeout?=60, since?)` | the extracted magic link (returned, *not* followed) + subject/from/id/received_at |
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,18 @@ 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
+ But if you reuse an existing inbox (e.g. to request a second OTP/link), pass
57
+ `since` to `wait_for_otp`/`wait_for_link`: otherwise the wait can match the
58
+ OLDEST retained message instead of the new one you just triggered. Pass
59
+ either the time right before you triggered the new email (ISO8601 or unix
60
+ seconds), or the `id` of the last message you saw (`msg_...`, meaning "only
61
+ messages after that one") — both tools return `id`/`received_at` on the
62
+ matched message so you can chain `since=<id>` into the next call on that same
63
+ inbox.
64
+
53
65
  ## Requirements
54
66
 
55
67
  - Python 3.10+
@@ -183,6 +195,7 @@ remote-transport tests talk to a local fake upstream on `127.0.0.1`.
183
195
 
184
196
  ## Changelog
185
197
 
198
+ - **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
199
  - **0.2.2** — depends on `mailsocket>=0.1.3` (stricter `_seg` id validation in the underlying Python SDK).
187
200
 
188
201
  <!-- 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.2.2"
7
+ version = "0.2.3"
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"
@@ -62,13 +62,13 @@ class FakeClient:
62
62
  return {"id": "inbox_1", "address": "x@in.inboxpipe.net"}
63
63
 
64
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))
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
70
  def wait_for_link(self, inbox_id, *, timeout=60.0, since=0):
71
- self.wait_link_calls.append((inbox_id, timeout))
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 == 0 # absent -> SDK default unchanged
240
241
 
241
242
 
242
243
  def test_wait_for_otp_timeout_is_clamped(fake_client):
@@ -244,14 +245,44 @@ 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_result_carries_id_and_received_at(fake_client):
267
+ fake_client.wait_otp = FakeWaitResult(
268
+ otp="123456",
269
+ confidence=0.95,
270
+ message={
271
+ "subject": "Your code",
272
+ "from": "Acme <no-reply@acme.test>",
273
+ "id": "msg_999",
274
+ "received_at": "2024-01-01T00:00:00Z",
275
+ },
276
+ )
277
+
278
+ result = call_tool("wait_for_otp", {"inbox_id": "inbox_abc"})
279
+
280
+ assert result.is_error is False
281
+ text = result.content[0].text
282
+ assert '"msg_999"' in text
283
+ assert '"2024-01-01T00:00:00Z"' in text
284
+
285
+
255
286
  def test_wait_for_otp_wait_timeout_is_clean_error(fake_client):
256
287
  fake_client.wait_otp = WaitTimeout("No matching message arrived within 60s.")
257
288
 
@@ -274,6 +305,38 @@ def test_wait_for_link_returns_magic_link(fake_client):
274
305
 
275
306
  assert result.is_error is False
276
307
  assert "https://example.test/go?token=abc" in result.content[0].text
308
+ inbox_id, timeout, since = fake_client.wait_link_calls[0]
309
+ assert inbox_id == "inbox_abc"
310
+ assert since == 0 # absent -> SDK default unchanged
311
+
312
+
313
+ def test_wait_for_link_since_passed_through(fake_client):
314
+ fake_client.wait_link = FakeWaitResult(magic_link="https://example.test/go?token=abc")
315
+
316
+ call_tool("wait_for_link", {"inbox_id": "inbox_abc", "since": "2024-01-01T00:00:00Z"})
317
+
318
+ inbox_id, _, since = fake_client.wait_link_calls[0]
319
+ assert inbox_id == "inbox_abc"
320
+ assert since == "2024-01-01T00:00:00Z"
321
+
322
+
323
+ def test_wait_for_link_result_carries_id_and_received_at(fake_client):
324
+ fake_client.wait_link = FakeWaitResult(
325
+ magic_link="https://example.test/go?token=abc",
326
+ message={
327
+ "subject": "Sign in",
328
+ "from": "Acme <no-reply@acme.test>",
329
+ "id": "msg_777",
330
+ "received_at": "2024-02-02T00:00:00Z",
331
+ },
332
+ )
333
+
334
+ result = call_tool("wait_for_link", {"inbox_id": "inbox_abc"})
335
+
336
+ assert result.is_error is False
337
+ text = result.content[0].text
338
+ assert '"msg_777"' in text
339
+ assert '"2024-02-02T00:00:00Z"' in text
277
340
 
278
341
 
279
342
  # -- create_inbox / list_messages / delete -----------------------------------
@@ -365,13 +428,13 @@ def test_version_exported():
365
428
 
366
429
 
367
430
  def test_metadata_consistency_across_files():
368
- """Versions match everywhere: MCP 0.2.2 (pyproject, server.json, __version__)
431
+ """Versions match everywhere: MCP 0.2.3 (pyproject, server.json, __version__)
369
432
  and its Python SDK sibling 0.1.3 (pyproject, __version__, user agent)."""
370
433
  import tomllib
371
434
  from pathlib import Path
372
435
 
373
436
  root = Path(__file__).resolve().parents[1]
374
- expected = "0.2.2"
437
+ expected = "0.2.3"
375
438
  expected_sdk = "0.1.3"
376
439
 
377
440
  pyproject = tomllib.loads((root / "pyproject.toml").read_text())
File without changes
File without changes