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