daknode-mcp 0.3.1__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.
- daknode_mcp-0.3.3/PKG-INFO +265 -0
- daknode_mcp-0.3.3/README.md +254 -0
- daknode_mcp-0.3.3/calendar_models.py +55 -0
- daknode_mcp-0.3.3/daknode_mcp.egg-info/PKG-INFO +265 -0
- {daknode_mcp-0.3.1 → daknode_mcp-0.3.3}/daknode_mcp.egg-info/SOURCES.txt +12 -1
- {daknode_mcp-0.3.1 → daknode_mcp-0.3.3}/daknode_mcp.egg-info/entry_points.txt +1 -0
- daknode_mcp-0.3.3/daknode_mcp.egg-info/requires.txt +2 -0
- daknode_mcp-0.3.3/event_source.py +114 -0
- daknode_mcp-0.3.3/events.py +268 -0
- daknode_mcp-0.3.3/identity.py +89 -0
- {daknode_mcp-0.3.1 → daknode_mcp-0.3.3}/pyproject.toml +4 -3
- daknode_mcp-0.3.3/remote.py +310 -0
- daknode_mcp-0.3.3/server.py +458 -0
- daknode_mcp-0.3.3/tests/test_write_tools.py +95 -0
- daknode_mcp-0.3.1/PKG-INFO +0 -79
- daknode_mcp-0.3.1/README.md +0 -68
- daknode_mcp-0.3.1/daknode_mcp.egg-info/PKG-INFO +0 -79
- daknode_mcp-0.3.1/daknode_mcp.egg-info/requires.txt +0 -2
- daknode_mcp-0.3.1/server.py +0 -229
- {daknode_mcp-0.3.1 → daknode_mcp-0.3.3}/__init__.py +0 -0
- {daknode_mcp-0.3.1 → daknode_mcp-0.3.3}/daknode_mcp.egg-info/dependency_links.txt +0 -0
- {daknode_mcp-0.3.1 → daknode_mcp-0.3.3}/daknode_mcp.egg-info/top_level.txt +0 -0
- {daknode_mcp-0.3.1 → daknode_mcp-0.3.3}/setup.cfg +0 -0
|
@@ -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
|