daknode-mcp 0.3.2__tar.gz → 0.3.4__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.
@@ -0,0 +1,289 @@
1
+ Metadata-Version: 2.4
2
+ Name: daknode-mcp
3
+ Version: 0.3.4
4
+ Summary: DakNode's email and calendar API tools over local and authenticated remote MCP.
5
+ Project-URL: Homepage, https://daknode.com
6
+ Project-URL: Documentation, https://daknode.com/developers
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: daknode<0.4,>=0.3.9
10
+ Requires-Dist: mcp<3,>=2.2
11
+
12
+ # DakNode MCP server
13
+
14
+ <!-- mcp-name: io.github.Subodh103/daknode -->
15
+
16
+ DakNode MCP 0.3.4 exposes 21 email and calendar API tools over **stdio**,
17
+ including availability, reliable booking, reminders and recurring-event edits.
18
+ An authenticated hosted endpoint is also available at
19
+ `https://api.daknode.com/mcp`; local installation does not connect to it or
20
+ authorize any action automatically.
21
+
22
+ ## Install
23
+
24
+ Python 3.10+. With [uv](https://docs.astral.sh/uv/), nothing needs installing
25
+ up front — the client starts it with `uvx`. Otherwise `pipx install daknode-mcp`.
26
+
27
+ Get an API key with the two-request signup at <https://daknode.com/developers>.
28
+
29
+ ## Configure
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "daknode": {
35
+ "command": "uvx",
36
+ "args": ["daknode-mcp"],
37
+ "env": { "DAKNODE_API_KEY": "…" }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ With `pipx`, use `"command": "daknode-mcp"` and no `args`. From a repository
44
+ checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.
45
+
46
+ The key comes from the environment and **never from a command-line argument**:
47
+ argv is visible to every process through `ps`, and shells record it in history.
48
+ The server refuses to start without it, so a misconfiguration is obvious at
49
+ launch rather than looking like a broken tool.
50
+
51
+ ## Tools
52
+
53
+ `create_inbox` · `list_threads` · `list_messages` · `get_thread` · `get_message`
54
+ · `search` · `send`
55
+
56
+ Calendar: `get_calendar_event` · `create_calendar_event` · `update_calendar_event`
57
+ · `cancel_calendar_event` · `respond_calendar_event` · `send_calendar_invitations`.
58
+ This release requires Python SDK **>=0.3.9,<0.4** and also exposes
59
+ `list_calendar_events`, `get_calendar_agenda`,
60
+ `get_calendar_free_busy`, `get_calendar_availability`, `book_calendar_event`,
61
+ `get_calendar_reminders`, `set_calendar_reminders`, and `edit_calendar_series`.
62
+ Each invokes exactly one corresponding generated SDK operation. No Calendar
63
+ deletion or unrelated management tools are added.
64
+
65
+ The list/agenda/free-busy/availability/reminder reads require `calendar.read`;
66
+ booking, reminder changes and series edits require `calendar.write`.
67
+ Free-busy takes `request: {start, end}`. Availability takes `request` with that
68
+ window plus IANA `timezone`, `working_hours: [{weekday, start, end}]` (Monday=0,
69
+ local HH:MM), `duration_minutes`, `step_minutes` and optional before/after buffer
70
+ minutes. Windows are explicit and at most 31 days. Slots are advisory and
71
+ inbox-only: external attendee calendars are unknown, never represented as free.
72
+ Booking takes `booking: {event, buffer_before_minutes?, buffer_after_minutes?}`
73
+ and a stable `idempotency_key`; only one non-recurring timed event is accepted.
74
+
75
+ Reminder writes take `revision` and `minutes_before_start`: up to five unique
76
+ integer offsets from 0 through 40320, or `[]` to clear. They configure developer
77
+ notifications, not attendee email. Series edits take `revision`, a stable retry
78
+ key, and `edit: {scope, recurrence_id?, cancelled?, changes?}` with explicit
79
+ `occurrence`, `series` or `following` scope. A following edit can return a new
80
+ future event ID; other edits return the original event. Optional patch fields
81
+ and explicit nulls retain their API meaning. No conflict triggers an automatic
82
+ revision refresh or retry.
83
+
84
+ `get_calendar_event` requires `calendar.read`. The Calendar writes require
85
+ `calendar.write`; invitation dispatch additionally requires `messages.send`.
86
+ Create takes the explicit event body and stable caller `idempotency_key`.
87
+ Update/cancel/respond/invitation dispatch take the caller's observed positive
88
+ integer `revision`, forwarded as a strong quoted `If-Match` header. Conflicts
89
+ are refused without fetching/overwriting a newer revision. Invitation dispatch
90
+ also requires a stable `idempotency_key` and an explicit REQUEST/CANCEL/REPLY
91
+ method (REQUEST is the API default).
92
+
93
+ Saving, updating, cancelling and recording an RSVP do not send mail. Use the
94
+ separate invitation operation only after explicit approval; delivery records
95
+ describe actual state, not a promise of delivery. Calendar input models validate
96
+ the published transport fields; the API owns timezone/DST, RRULE, attendee and
97
+ organizer authority. All-day end dates are exclusive. Updates preserve omitted
98
+ fields and forward explicit nulls unchanged. No automatic write retries occur.
99
+
100
+ `create_inbox(alias)` requires the chosen address alias. `send` requires a
101
+ caller-supplied `idempotency_key`: keep it stable for retries of the same approved
102
+ message. Blank, control-character/non-ASCII or over-255-character keys are
103
+ refused before HTTP. A distinct intentional send needs a distinct key. Tool
104
+ discovery alone does not verify these write contracts; isolated operation tests
105
+ exercise the generated SDK request and response with a mocked HTTP transport.
106
+
107
+ Codex plugin, Cursor install link and canonical multi-platform skill:
108
+ https://github.com/Subodh103/daknode-integrations. These launch the same local
109
+ server; no hosted ChatGPT endpoint or public marketplace acceptance is implied.
110
+
111
+ **Every tool is 1:1 with a published API operation.** That is `architecture.md`
112
+ §0 consequence 4, and it is the boundary this server exists inside: DakNode
113
+ exposes operations, the agent decides which to call. A tool like
114
+ `reply_to_latest` or `auto_respond` would cross that line — DakNode would be
115
+ choosing which message to answer — **while looking like a convenience**.
116
+ `tests/test_mcp_server.py` enforces it rather than leaving it to review.
117
+
118
+ ## What is deliberately absent
119
+
120
+ **Erasure.** It is irreversible, and an agent that can erase a data subject
121
+ through a tool call is a liability no convenience justifies. It stays an
122
+ explicit API call a human integrates.
123
+
124
+ **Console and operator APIs.** Not a product surface.
125
+
126
+ ## Notes for whoever uses this
127
+
128
+ `send` without `thread_id` opens a **new** conversation in the recipient's
129
+ client. Pass `thread_id` to reply into an existing one — the server then derives
130
+ `In-Reply-To` and `References` from that thread's latest received message.
131
+
132
+ `search` returns identifiers and scores, **not content**. Resolve them with
133
+ `get_message` or `get_thread`.
134
+
135
+ `body_text` is already cleaned: quoted history and signatures are stripped.
136
+ Check `risk_level` before feeding content to a model — `high` means
137
+ prompt-injection patterns were detected in mail from the internet.
138
+
139
+ ## Remote process (not deployed)
140
+
141
+ The dedicated entry point is `daknode-mcp-http` (or `python -m daknode_mcp.remote`) after installing
142
+ the MCP wheel, or `python -m mcp_server.remote` from a checkout. Keep it in its
143
+ own environment; never install MCP into the API virtualenv. The HTTP process
144
+ binds loopback port 8081. A separately managed TLS reverse proxy must publish
145
+ exactly `https://api.daknode.com/mcp`, preserving the canonical Host. The REST
146
+ resource and upstream remain `https://api.daknode.com`; there is no user-selected
147
+ upstream, redirect following, environment proxy inheritance or automatic retry.
148
+
149
+ The default verifier calls the backend's identity-only `GET /v1/mcp/identity`
150
+ on the fixed canonical resource, with the same caller credential. It returns
151
+ no inbox enumeration and maps checked permissions into tool visibility. Missing
152
+ or unavailable backend support fails closed. Inbox-scoped keys never discover
153
+ `create_inbox`. OAuth grant wiring and end-to-end client flow are still separate
154
+ acceptance gates; a passing mocked identity contract is not live OAuth evidence.
155
+
156
+ Optionally configure `DAKNODE_MCP_VERIFIER=your_adapter:verify` with deployment-owned code.
157
+ This is an import path, not a credential. The async function receives the header
158
+ credential and returns `Authority(api_key=credential, tools=frozenset(...))`, or
159
+ `None` to refuse. It must verify expiry, revocation, tenant/inbox binding and
160
+ permissions against the canonical authority on every request. It may not cache
161
+ positive revocation decisions. Its returned tool allowlist must contain only
162
+ published tool names; it is not a replacement for REST operation authorization.
163
+ For subscriptions it must also return checked `event_names`, `workspace_id`
164
+ and `inbox_id` (nullable only for workspace-scoped authority). The default
165
+ identity adapter populates these from checked permissions and binding.
166
+ The gateway verifies discovery too: an explicitly invalid verifier refuses startup, an
167
+ unavailable verifier yields 503, and refused credentials yield 401. This module
168
+ does not import the API application or open its database.
169
+
170
+ Clients may privately supply either `Authorization: Bearer <credential>` or
171
+ `x-api-key: <credential>`, never both. A header-capable client can point to the
172
+ canonical URL once deployed. Credentials in URLs or tool arguments are not an
173
+ authentication mechanism; query strings are refused and extra tool arguments
174
+ are rejected. Never paste credentials into chats, argv or public configuration.
175
+ `DAKNODE_API_KEY` is used only by local stdio, never as remote fallback.
176
+ Dedicated OAuth tokens must have `dak_oauth_` plus a canonical unpadded 32-byte
177
+ URL-safe payload (43 characters). Malformed OAuth-shaped strings are refused
178
+ before verification; a syntactically valid token still requires the backend's
179
+ active grant mapping and permission checks. Ordinary live/test handling remains
180
+ unchanged. The 401 challenge points to the backend's real canonical protected
181
+ resource metadata; the MCP process does not manufacture issuer metadata.
182
+
183
+ `DAKNODE_MCP_HOSTS` is an exact comma-separated Host-authority list (default
184
+ `api.daknode.com`); no wildcards. All Origin-bearing requests are refused unless
185
+ their exact HTTPS Origin appears in `DAKNODE_MCP_ORIGINS`. Missing Origin is
186
+ permitted for native clients; it does not bypass credential verification. No
187
+ CORS credentials, wildcard Origin or forwarded-header trust is installed.
188
+ Proxy request/header/time limits and credential-safe proxy logs are deployment
189
+ requirements. Application access logging is disabled.
190
+
191
+ `mcp_server/Dockerfile` builds this process and the generated SDK in a dedicated
192
+ image from the repository-root context, runs as UID 10001 and binds port 8081.
193
+ `GET /health` is unauthenticated dependency-free liveness only; it is not API,
194
+ OAuth or event readiness. The container must be reachable only through the
195
+ canonical TLS proxy. Outside the container the default bind is loopback;
196
+ `DAKNODE_MCP_BIND` explicitly changes it. No image build/deployment is implied.
197
+
198
+ Verified with **mcp 2.2.0**, Python 3.13, and protocol revisions **2025-11-25**
199
+ (stateless initialize + tools requests) and **2026-07-28** (per-request envelope,
200
+ `Mcp-Method`/`Mcp-Name` routing headers). SDK wire validation is retained.
201
+ Core subscriptions and native draft Events push require **2026-07-28**; ordinary
202
+ tools retain the tested 2025 compatibility path. No session IDs or unauthenticated
203
+ standalone GET stream are used. OAuth-discovery clients such as
204
+ ChatGPT/Claude require the real backend grant/consent/PKCE flow and a tested
205
+ verifier adapter before they are supported. No fake OAuth metadata is served.
206
+
207
+ Limits: 16 KiB headers, 64 KiB JSON request, 1 MiB uncompressed REST response,
208
+ 2 MiB ordinary MCP response, 64 KiB per SSE frame. Compressed REST responses are refused. Verification has a
209
+ 5-second deadline, request upload 10 seconds, MCP handling 25 seconds; REST
210
+ connect timeout is 5 seconds and read/write/pool timeouts 15 seconds. REST body
211
+ iteration also has a 15-second elapsed ceiling (checked between chunks). Sync
212
+ SDK calls run in SDK worker threads; cancellation cannot retract a dispatched
213
+ write or force-kill a thread, but disconnect suppresses delivery and network work
214
+ is bounded. A timeout/disconnect is not proof a send failed: retry only with its
215
+ original idempotency key. Errors expose safe codes/status and numeric retry hints,
216
+ not raw API bodies, tracebacks or credentials. HTTP results are `no-store`.
217
+
218
+ ## Core subscriptions and native Events push (local wire-tested)
219
+
220
+ AgentMail's [MCP Events](https://www.agentmail.to/docs/integrations/mcp-events)
221
+ publishes mail occurrences, and [OpenAI's Events guidance](https://developers.openai.com/plugins/build/mcp-events)
222
+ describes event-triggered clients. The official [Events design sketch](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md)
223
+ is a separate draft extension with `events/list`, `events/poll`, `events/stream`
224
+ and webhook `events/subscribe`/`events/unsubscribe` methods, cursors and TTLs.
225
+
226
+ The installed official `mcp==2.2.0` source has no built-in Events extension, but
227
+ provides the public `Extension`/`MethodBinding`, lowlevel handler and notification
228
+ channels used by this custom implementation. Its built-in `ListenHandler`
229
+ serves core `subscriptions/listen`. See the official
230
+ [subscription documentation](https://py.sdk.modelcontextprotocol.io/handlers/subscriptions/)
231
+ and [v2.2.0 source](https://github.com/modelcontextprotocol/python-sdk/tree/v2.2.0/src/mcp).
232
+
233
+ Two distinct capabilities are implemented and wire-tested:
234
+
235
+ - `subscriptions/listen`: the official SDK sends the acknowledgement first,
236
+ tags frames with the request ID, and delivers only requested catalog/resource
237
+ cues. Resources use `daknode://inboxes/{inboxId}/calendar/changes` or
238
+ `daknode://inboxes/{inboxId}/messages/changes`. Each listen has its own bus,
239
+ with an eight-cue buffer, scoped resource validation and revocation checks.
240
+ These core cues have **no replay guarantee**; clients re-listen and refetch.
241
+ - `io.modelcontextprotocol/events`: custom draft Events `events/list`,
242
+ `events/poll`, and actual SSE `events/stream`. Discovery advertises exactly
243
+ `poll` and `push`, never webhook. The implementation follows the Events design
244
+ sketch fetched 2026-11-01 (draft, not a finalized core MCP primitive).
245
+ Frames are `notifications/events/active`, `event`, `heartbeat`, and
246
+ `terminated`; every frame is request-ID tagged. The final result closes a
247
+ server-ended stream; client disconnect cancels without a final result.
248
+
249
+ The default verifier exposes `message.received` for `messages.read`, and
250
+ `calendar.event.created`, `.updated`, `.cancelled`, `.responded`, `.starting`,
251
+ `.ending`, `.reminder` for `calendar.read` in this source checkout. Inbox-scoped credentials can subscribe only to
252
+ their own inbox. Event payloads contain identifiers and calendar revision only;
253
+ no bodies, subjects, event titles, attendees or email addresses. This is not a
254
+ claim of AgentMail's exact spam/authentication filtering policy: retained mail
255
+ availability follows DakNode's existing polling API policy.
256
+
257
+ Native delivery **internally polls the bounded REST journal**: existing
258
+ `GET /v1/inboxes/{inbox_id}/events` for messages and
259
+ `GET /v1/inboxes/{inbox_id}/calendar/changes` for calendar facts. Each request
260
+ asks for one row, so each occurrence carries the exact cursor after that row;
261
+ disconnect cannot skip later records in an already-read page. Cursors and
262
+ stable event IDs come from the API, not process memory. Clients persist cursors
263
+ and deduplicate IDs across reconnects. Expired cursors restart one bounded page
264
+ with `truncated: true`; history older than `maxAgeMs` is skipped visibly.
265
+ `maxAgeMs` defaults to five minutes and is capped at one day. Polling is normally
266
+ every two seconds, or 250 ms while a backlog remains. The API must mount both
267
+ routes and enforce key scope, permissions, revocation, deleted-inbox and erasure
268
+ filters; these are deployment/integration gates, not proved by mock tests here.
269
+
270
+ Streams reauthorize each source read and before occurrence delivery. There are
271
+ 64 stream slots per process and four per credential across both protocols,
272
+ with no retained global credential or cross-tenant fan-out. Each stream ends
273
+ after five minutes (reconnect with its cursor); SDK cancellation releases the
274
+ slot. Each SSE write has a 10-second deadline and there is no unbounded event
275
+ queue. Failures send safe control metadata or close the connection, not raw
276
+ upstream bodies. Streams do not authorize any follow-on write.
277
+
278
+ `events/subscribe`/`events/unsubscribe` webhook delivery is **not implemented**.
279
+ ChatGPT's documented webhook delivery therefore remains a separate acceptance
280
+ gate: durable subscription storage, approved callback/SSRF protection, signing,
281
+ retries, refresh/expiry and real-client validation are still required. Neither
282
+ core resource cues nor push-only Events prove ChatGPT task-trigger compatibility.
283
+
284
+ For deployment, forward `/mcp` POSTs without buffering SSE, retain the canonical
285
+ Host, and allow idle timeouts of at least 60 seconds (SDK keepalives are 15 s).
286
+ The stream's five-minute lifetime is intentional. `/health` remains liveness
287
+ only. Route `/.well-known/*` and `/oauth/*` to the API OAuth implementation,
288
+ not this process. Use only the exact browser Origins actually required by tested
289
+ clients; no wildcard or automatic Origin reflection.
@@ -0,0 +1,278 @@
1
+ # DakNode MCP server
2
+
3
+ <!-- mcp-name: io.github.Subodh103/daknode -->
4
+
5
+ DakNode MCP 0.3.4 exposes 21 email and calendar API tools over **stdio**,
6
+ including availability, reliable booking, reminders and recurring-event edits.
7
+ An authenticated hosted endpoint is also available at
8
+ `https://api.daknode.com/mcp`; local installation does not connect to it or
9
+ authorize any action automatically.
10
+
11
+ ## Install
12
+
13
+ Python 3.10+. With [uv](https://docs.astral.sh/uv/), nothing needs installing
14
+ up front — the client starts it with `uvx`. Otherwise `pipx install daknode-mcp`.
15
+
16
+ Get an API key with the two-request signup at <https://daknode.com/developers>.
17
+
18
+ ## Configure
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "daknode": {
24
+ "command": "uvx",
25
+ "args": ["daknode-mcp"],
26
+ "env": { "DAKNODE_API_KEY": "…" }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ With `pipx`, use `"command": "daknode-mcp"` and no `args`. From a repository
33
+ checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.
34
+
35
+ The key comes from the environment and **never from a command-line argument**:
36
+ argv is visible to every process through `ps`, and shells record it in history.
37
+ The server refuses to start without it, so a misconfiguration is obvious at
38
+ launch rather than looking like a broken tool.
39
+
40
+ ## Tools
41
+
42
+ `create_inbox` · `list_threads` · `list_messages` · `get_thread` · `get_message`
43
+ · `search` · `send`
44
+
45
+ Calendar: `get_calendar_event` · `create_calendar_event` · `update_calendar_event`
46
+ · `cancel_calendar_event` · `respond_calendar_event` · `send_calendar_invitations`.
47
+ This release requires Python SDK **>=0.3.9,<0.4** and also exposes
48
+ `list_calendar_events`, `get_calendar_agenda`,
49
+ `get_calendar_free_busy`, `get_calendar_availability`, `book_calendar_event`,
50
+ `get_calendar_reminders`, `set_calendar_reminders`, and `edit_calendar_series`.
51
+ Each invokes exactly one corresponding generated SDK operation. No Calendar
52
+ deletion or unrelated management tools are added.
53
+
54
+ The list/agenda/free-busy/availability/reminder reads require `calendar.read`;
55
+ booking, reminder changes and series edits require `calendar.write`.
56
+ Free-busy takes `request: {start, end}`. Availability takes `request` with that
57
+ window plus IANA `timezone`, `working_hours: [{weekday, start, end}]` (Monday=0,
58
+ local HH:MM), `duration_minutes`, `step_minutes` and optional before/after buffer
59
+ minutes. Windows are explicit and at most 31 days. Slots are advisory and
60
+ inbox-only: external attendee calendars are unknown, never represented as free.
61
+ Booking takes `booking: {event, buffer_before_minutes?, buffer_after_minutes?}`
62
+ and a stable `idempotency_key`; only one non-recurring timed event is accepted.
63
+
64
+ Reminder writes take `revision` and `minutes_before_start`: up to five unique
65
+ integer offsets from 0 through 40320, or `[]` to clear. They configure developer
66
+ notifications, not attendee email. Series edits take `revision`, a stable retry
67
+ key, and `edit: {scope, recurrence_id?, cancelled?, changes?}` with explicit
68
+ `occurrence`, `series` or `following` scope. A following edit can return a new
69
+ future event ID; other edits return the original event. Optional patch fields
70
+ and explicit nulls retain their API meaning. No conflict triggers an automatic
71
+ revision refresh or retry.
72
+
73
+ `get_calendar_event` requires `calendar.read`. The Calendar writes require
74
+ `calendar.write`; invitation dispatch additionally requires `messages.send`.
75
+ Create takes the explicit event body and stable caller `idempotency_key`.
76
+ Update/cancel/respond/invitation dispatch take the caller's observed positive
77
+ integer `revision`, forwarded as a strong quoted `If-Match` header. Conflicts
78
+ are refused without fetching/overwriting a newer revision. Invitation dispatch
79
+ also requires a stable `idempotency_key` and an explicit REQUEST/CANCEL/REPLY
80
+ method (REQUEST is the API default).
81
+
82
+ Saving, updating, cancelling and recording an RSVP do not send mail. Use the
83
+ separate invitation operation only after explicit approval; delivery records
84
+ describe actual state, not a promise of delivery. Calendar input models validate
85
+ the published transport fields; the API owns timezone/DST, RRULE, attendee and
86
+ organizer authority. All-day end dates are exclusive. Updates preserve omitted
87
+ fields and forward explicit nulls unchanged. No automatic write retries occur.
88
+
89
+ `create_inbox(alias)` requires the chosen address alias. `send` requires a
90
+ caller-supplied `idempotency_key`: keep it stable for retries of the same approved
91
+ message. Blank, control-character/non-ASCII or over-255-character keys are
92
+ refused before HTTP. A distinct intentional send needs a distinct key. Tool
93
+ discovery alone does not verify these write contracts; isolated operation tests
94
+ exercise the generated SDK request and response with a mocked HTTP transport.
95
+
96
+ Codex plugin, Cursor install link and canonical multi-platform skill:
97
+ https://github.com/Subodh103/daknode-integrations. These launch the same local
98
+ server; no hosted ChatGPT endpoint or public marketplace acceptance is implied.
99
+
100
+ **Every tool is 1:1 with a published API operation.** That is `architecture.md`
101
+ §0 consequence 4, and it is the boundary this server exists inside: DakNode
102
+ exposes operations, the agent decides which to call. A tool like
103
+ `reply_to_latest` or `auto_respond` would cross that line — DakNode would be
104
+ choosing which message to answer — **while looking like a convenience**.
105
+ `tests/test_mcp_server.py` enforces it rather than leaving it to review.
106
+
107
+ ## What is deliberately absent
108
+
109
+ **Erasure.** It is irreversible, and an agent that can erase a data subject
110
+ through a tool call is a liability no convenience justifies. It stays an
111
+ explicit API call a human integrates.
112
+
113
+ **Console and operator APIs.** Not a product surface.
114
+
115
+ ## Notes for whoever uses this
116
+
117
+ `send` without `thread_id` opens a **new** conversation in the recipient's
118
+ client. Pass `thread_id` to reply into an existing one — the server then derives
119
+ `In-Reply-To` and `References` from that thread's latest received message.
120
+
121
+ `search` returns identifiers and scores, **not content**. Resolve them with
122
+ `get_message` or `get_thread`.
123
+
124
+ `body_text` is already cleaned: quoted history and signatures are stripped.
125
+ Check `risk_level` before feeding content to a model — `high` means
126
+ prompt-injection patterns were detected in mail from the internet.
127
+
128
+ ## Remote process (not deployed)
129
+
130
+ The dedicated entry point is `daknode-mcp-http` (or `python -m daknode_mcp.remote`) after installing
131
+ the MCP wheel, or `python -m mcp_server.remote` from a checkout. Keep it in its
132
+ own environment; never install MCP into the API virtualenv. The HTTP process
133
+ binds loopback port 8081. A separately managed TLS reverse proxy must publish
134
+ exactly `https://api.daknode.com/mcp`, preserving the canonical Host. The REST
135
+ resource and upstream remain `https://api.daknode.com`; there is no user-selected
136
+ upstream, redirect following, environment proxy inheritance or automatic retry.
137
+
138
+ The default verifier calls the backend's identity-only `GET /v1/mcp/identity`
139
+ on the fixed canonical resource, with the same caller credential. It returns
140
+ no inbox enumeration and maps checked permissions into tool visibility. Missing
141
+ or unavailable backend support fails closed. Inbox-scoped keys never discover
142
+ `create_inbox`. OAuth grant wiring and end-to-end client flow are still separate
143
+ acceptance gates; a passing mocked identity contract is not live OAuth evidence.
144
+
145
+ Optionally configure `DAKNODE_MCP_VERIFIER=your_adapter:verify` with deployment-owned code.
146
+ This is an import path, not a credential. The async function receives the header
147
+ credential and returns `Authority(api_key=credential, tools=frozenset(...))`, or
148
+ `None` to refuse. It must verify expiry, revocation, tenant/inbox binding and
149
+ permissions against the canonical authority on every request. It may not cache
150
+ positive revocation decisions. Its returned tool allowlist must contain only
151
+ published tool names; it is not a replacement for REST operation authorization.
152
+ For subscriptions it must also return checked `event_names`, `workspace_id`
153
+ and `inbox_id` (nullable only for workspace-scoped authority). The default
154
+ identity adapter populates these from checked permissions and binding.
155
+ The gateway verifies discovery too: an explicitly invalid verifier refuses startup, an
156
+ unavailable verifier yields 503, and refused credentials yield 401. This module
157
+ does not import the API application or open its database.
158
+
159
+ Clients may privately supply either `Authorization: Bearer <credential>` or
160
+ `x-api-key: <credential>`, never both. A header-capable client can point to the
161
+ canonical URL once deployed. Credentials in URLs or tool arguments are not an
162
+ authentication mechanism; query strings are refused and extra tool arguments
163
+ are rejected. Never paste credentials into chats, argv or public configuration.
164
+ `DAKNODE_API_KEY` is used only by local stdio, never as remote fallback.
165
+ Dedicated OAuth tokens must have `dak_oauth_` plus a canonical unpadded 32-byte
166
+ URL-safe payload (43 characters). Malformed OAuth-shaped strings are refused
167
+ before verification; a syntactically valid token still requires the backend's
168
+ active grant mapping and permission checks. Ordinary live/test handling remains
169
+ unchanged. The 401 challenge points to the backend's real canonical protected
170
+ resource metadata; the MCP process does not manufacture issuer metadata.
171
+
172
+ `DAKNODE_MCP_HOSTS` is an exact comma-separated Host-authority list (default
173
+ `api.daknode.com`); no wildcards. All Origin-bearing requests are refused unless
174
+ their exact HTTPS Origin appears in `DAKNODE_MCP_ORIGINS`. Missing Origin is
175
+ permitted for native clients; it does not bypass credential verification. No
176
+ CORS credentials, wildcard Origin or forwarded-header trust is installed.
177
+ Proxy request/header/time limits and credential-safe proxy logs are deployment
178
+ requirements. Application access logging is disabled.
179
+
180
+ `mcp_server/Dockerfile` builds this process and the generated SDK in a dedicated
181
+ image from the repository-root context, runs as UID 10001 and binds port 8081.
182
+ `GET /health` is unauthenticated dependency-free liveness only; it is not API,
183
+ OAuth or event readiness. The container must be reachable only through the
184
+ canonical TLS proxy. Outside the container the default bind is loopback;
185
+ `DAKNODE_MCP_BIND` explicitly changes it. No image build/deployment is implied.
186
+
187
+ Verified with **mcp 2.2.0**, Python 3.13, and protocol revisions **2025-11-25**
188
+ (stateless initialize + tools requests) and **2026-07-28** (per-request envelope,
189
+ `Mcp-Method`/`Mcp-Name` routing headers). SDK wire validation is retained.
190
+ Core subscriptions and native draft Events push require **2026-07-28**; ordinary
191
+ tools retain the tested 2025 compatibility path. No session IDs or unauthenticated
192
+ standalone GET stream are used. OAuth-discovery clients such as
193
+ ChatGPT/Claude require the real backend grant/consent/PKCE flow and a tested
194
+ verifier adapter before they are supported. No fake OAuth metadata is served.
195
+
196
+ Limits: 16 KiB headers, 64 KiB JSON request, 1 MiB uncompressed REST response,
197
+ 2 MiB ordinary MCP response, 64 KiB per SSE frame. Compressed REST responses are refused. Verification has a
198
+ 5-second deadline, request upload 10 seconds, MCP handling 25 seconds; REST
199
+ connect timeout is 5 seconds and read/write/pool timeouts 15 seconds. REST body
200
+ iteration also has a 15-second elapsed ceiling (checked between chunks). Sync
201
+ SDK calls run in SDK worker threads; cancellation cannot retract a dispatched
202
+ write or force-kill a thread, but disconnect suppresses delivery and network work
203
+ is bounded. A timeout/disconnect is not proof a send failed: retry only with its
204
+ original idempotency key. Errors expose safe codes/status and numeric retry hints,
205
+ not raw API bodies, tracebacks or credentials. HTTP results are `no-store`.
206
+
207
+ ## Core subscriptions and native Events push (local wire-tested)
208
+
209
+ AgentMail's [MCP Events](https://www.agentmail.to/docs/integrations/mcp-events)
210
+ publishes mail occurrences, and [OpenAI's Events guidance](https://developers.openai.com/plugins/build/mcp-events)
211
+ describes event-triggered clients. The official [Events design sketch](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md)
212
+ is a separate draft extension with `events/list`, `events/poll`, `events/stream`
213
+ and webhook `events/subscribe`/`events/unsubscribe` methods, cursors and TTLs.
214
+
215
+ The installed official `mcp==2.2.0` source has no built-in Events extension, but
216
+ provides the public `Extension`/`MethodBinding`, lowlevel handler and notification
217
+ channels used by this custom implementation. Its built-in `ListenHandler`
218
+ serves core `subscriptions/listen`. See the official
219
+ [subscription documentation](https://py.sdk.modelcontextprotocol.io/handlers/subscriptions/)
220
+ and [v2.2.0 source](https://github.com/modelcontextprotocol/python-sdk/tree/v2.2.0/src/mcp).
221
+
222
+ Two distinct capabilities are implemented and wire-tested:
223
+
224
+ - `subscriptions/listen`: the official SDK sends the acknowledgement first,
225
+ tags frames with the request ID, and delivers only requested catalog/resource
226
+ cues. Resources use `daknode://inboxes/{inboxId}/calendar/changes` or
227
+ `daknode://inboxes/{inboxId}/messages/changes`. Each listen has its own bus,
228
+ with an eight-cue buffer, scoped resource validation and revocation checks.
229
+ These core cues have **no replay guarantee**; clients re-listen and refetch.
230
+ - `io.modelcontextprotocol/events`: custom draft Events `events/list`,
231
+ `events/poll`, and actual SSE `events/stream`. Discovery advertises exactly
232
+ `poll` and `push`, never webhook. The implementation follows the Events design
233
+ sketch fetched 2026-11-01 (draft, not a finalized core MCP primitive).
234
+ Frames are `notifications/events/active`, `event`, `heartbeat`, and
235
+ `terminated`; every frame is request-ID tagged. The final result closes a
236
+ server-ended stream; client disconnect cancels without a final result.
237
+
238
+ The default verifier exposes `message.received` for `messages.read`, and
239
+ `calendar.event.created`, `.updated`, `.cancelled`, `.responded`, `.starting`,
240
+ `.ending`, `.reminder` for `calendar.read` in this source checkout. Inbox-scoped credentials can subscribe only to
241
+ their own inbox. Event payloads contain identifiers and calendar revision only;
242
+ no bodies, subjects, event titles, attendees or email addresses. This is not a
243
+ claim of AgentMail's exact spam/authentication filtering policy: retained mail
244
+ availability follows DakNode's existing polling API policy.
245
+
246
+ Native delivery **internally polls the bounded REST journal**: existing
247
+ `GET /v1/inboxes/{inbox_id}/events` for messages and
248
+ `GET /v1/inboxes/{inbox_id}/calendar/changes` for calendar facts. Each request
249
+ asks for one row, so each occurrence carries the exact cursor after that row;
250
+ disconnect cannot skip later records in an already-read page. Cursors and
251
+ stable event IDs come from the API, not process memory. Clients persist cursors
252
+ and deduplicate IDs across reconnects. Expired cursors restart one bounded page
253
+ with `truncated: true`; history older than `maxAgeMs` is skipped visibly.
254
+ `maxAgeMs` defaults to five minutes and is capped at one day. Polling is normally
255
+ every two seconds, or 250 ms while a backlog remains. The API must mount both
256
+ routes and enforce key scope, permissions, revocation, deleted-inbox and erasure
257
+ filters; these are deployment/integration gates, not proved by mock tests here.
258
+
259
+ Streams reauthorize each source read and before occurrence delivery. There are
260
+ 64 stream slots per process and four per credential across both protocols,
261
+ with no retained global credential or cross-tenant fan-out. Each stream ends
262
+ after five minutes (reconnect with its cursor); SDK cancellation releases the
263
+ slot. Each SSE write has a 10-second deadline and there is no unbounded event
264
+ queue. Failures send safe control metadata or close the connection, not raw
265
+ upstream bodies. Streams do not authorize any follow-on write.
266
+
267
+ `events/subscribe`/`events/unsubscribe` webhook delivery is **not implemented**.
268
+ ChatGPT's documented webhook delivery therefore remains a separate acceptance
269
+ gate: durable subscription storage, approved callback/SSRF protection, signing,
270
+ retries, refresh/expiry and real-client validation are still required. Neither
271
+ core resource cues nor push-only Events prove ChatGPT task-trigger compatibility.
272
+
273
+ For deployment, forward `/mcp` POSTs without buffering SSE, retain the canonical
274
+ Host, and allow idle timeouts of at least 60 seconds (SDK keepalives are 15 s).
275
+ The stream's five-minute lifetime is intentional. `/health` remains liveness
276
+ only. Route `/.well-known/*` and `/oauth/*` to the API OAuth implementation,
277
+ not this process. Use only the exact browser Origins actually required by tested
278
+ clients; no wildcard or automatic Origin reflection.