Metadata-Version: 2.4
Name: daknode-mcp
Version: 0.3.3
Summary: DakNode's email and calendar API tools over local and authenticated remote MCP.
Project-URL: Homepage, https://daknode.com
Project-URL: Documentation, https://daknode.com/developers
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: daknode<0.4,>=0.3.8
Requires-Dist: mcp<3,>=2.2

# DakNode MCP server

<!-- mcp-name: io.github.Subodh103/daknode -->

DakNode 0.3.3 exposes email and Calendar API tools over **stdio**, with a separate authenticated
Streamable HTTP process prepared for hosting. No hosted deployment or OAuth
availability is implied by this checkout.

## Install

Python 3.10+. With [uv](https://docs.astral.sh/uv/), nothing needs installing
up front — the client starts it with `uvx`. Otherwise `pipx install daknode-mcp`.

Get an API key with the two-request signup at <https://daknode.com/developers>.

## Configure

```json
{
  "mcpServers": {
    "daknode": {
      "command": "uvx",
      "args": ["daknode-mcp"],
      "env": { "DAKNODE_API_KEY": "…" }
    }
  }
}
```

With `pipx`, use `"command": "daknode-mcp"` and no `args`. From a repository
checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.

The key comes from the environment and **never from a command-line argument**:
argv is visible to every process through `ps`, and shells record it in history.
The server refuses to start without it, so a misconfiguration is obvious at
launch rather than looking like a broken tool.

## Tools

`create_inbox` · `list_threads` · `list_messages` · `get_thread` · `get_message`
· `search` · `send`

Calendar: `get_calendar_event` · `create_calendar_event` · `update_calendar_event`
· `cancel_calendar_event` · `respond_calendar_event` · `send_calendar_invitations`.
These six tools require generated Python SDK **>=0.3.8,<0.4** and each invokes
exactly its published operation once. No Calendar deletion, exception, agenda,
or unrelated management tools are added by this release.

`get_calendar_event` requires `calendar.read`. The Calendar writes require
`calendar.write`; invitation dispatch additionally requires `messages.send`.
Create takes the explicit event body and stable caller `idempotency_key`.
Update/cancel/respond/invitation dispatch take the caller's observed positive
integer `revision`, forwarded as a strong quoted `If-Match` header. Conflicts
are refused without fetching/overwriting a newer revision. Invitation dispatch
also requires a stable `idempotency_key` and an explicit REQUEST/CANCEL/REPLY
method (REQUEST is the API default).

Saving, updating, cancelling and recording an RSVP do not send mail. Use the
separate invitation operation only after explicit approval; delivery records
describe actual state, not a promise of delivery. Calendar input models validate
the published transport fields; the API owns timezone/DST, RRULE, attendee and
organizer authority. All-day end dates are exclusive. Updates preserve omitted
fields and forward explicit nulls unchanged. No automatic write retries occur.

`create_inbox(alias)` requires the chosen address alias. `send` requires a
caller-supplied `idempotency_key`: keep it stable for retries of the same approved
message. Blank, control-character/non-ASCII or over-255-character keys are
refused before HTTP. A distinct intentional send needs a distinct key. Tool
discovery alone does not verify these write contracts; isolated operation tests
exercise the generated SDK request and response with a mocked HTTP transport.

Codex plugin, Cursor install link and canonical multi-platform skill:
https://github.com/Subodh103/daknode-integrations. These launch the same local
server; no hosted ChatGPT endpoint or public marketplace acceptance is implied.

**Every tool is 1:1 with a published API operation.** That is `architecture.md`
§0 consequence 4, and it is the boundary this server exists inside: DakNode
exposes operations, the agent decides which to call. A tool like
`reply_to_latest` or `auto_respond` would cross that line — DakNode would be
choosing which message to answer — **while looking like a convenience**.
`tests/test_mcp_server.py` enforces it rather than leaving it to review.

## What is deliberately absent

**Erasure.** It is irreversible, and an agent that can erase a data subject
through a tool call is a liability no convenience justifies. It stays an
explicit API call a human integrates.

**Console and operator APIs.** Not a product surface.

## Notes for whoever uses this

`send` without `thread_id` opens a **new** conversation in the recipient's
client. Pass `thread_id` to reply into an existing one — the server then derives
`In-Reply-To` and `References` from that thread's latest received message.

`search` returns identifiers and scores, **not content**. Resolve them with
`get_message` or `get_thread`.

`body_text` is already cleaned: quoted history and signatures are stripped.
Check `risk_level` before feeding content to a model — `high` means
prompt-injection patterns were detected in mail from the internet.

## Remote process (not deployed)

The dedicated entry point is `daknode-mcp-http` (or `python -m daknode_mcp.remote`) after installing
the MCP wheel, or `python -m mcp_server.remote` from a checkout. Keep it in its
own environment; never install MCP into the API virtualenv. The HTTP process
binds loopback port 8081. A separately managed TLS reverse proxy must publish
exactly `https://api.daknode.com/mcp`, preserving the canonical Host. The REST
resource and upstream remain `https://api.daknode.com`; there is no user-selected
upstream, redirect following, environment proxy inheritance or automatic retry.

The default verifier calls the backend's identity-only `GET /v1/mcp/identity`
on the fixed canonical resource, with the same caller credential. It returns
no inbox enumeration and maps checked permissions into tool visibility. Missing
or unavailable backend support fails closed. Inbox-scoped keys never discover
`create_inbox`. OAuth grant wiring and end-to-end client flow are still separate
acceptance gates; a passing mocked identity contract is not live OAuth evidence.

Optionally configure `DAKNODE_MCP_VERIFIER=your_adapter:verify` with deployment-owned code.
This is an import path, not a credential. The async function receives the header
credential and returns `Authority(api_key=credential, tools=frozenset(...))`, or
`None` to refuse. It must verify expiry, revocation, tenant/inbox binding and
permissions against the canonical authority on every request. It may not cache
positive revocation decisions. Its returned tool allowlist must contain only
published tool names; it is not a replacement for REST operation authorization.
For subscriptions it must also return checked `event_names`, `workspace_id`
and `inbox_id` (nullable only for workspace-scoped authority). The default
identity adapter populates these from checked permissions and binding.
The gateway verifies discovery too: an explicitly invalid verifier refuses startup, an
unavailable verifier yields 503, and refused credentials yield 401. This module
does not import the API application or open its database.

Clients may privately supply either `Authorization: Bearer <credential>` or
`x-api-key: <credential>`, never both. A header-capable client can point to the
canonical URL once deployed. Credentials in URLs or tool arguments are not an
authentication mechanism; query strings are refused and extra tool arguments
are rejected. Never paste credentials into chats, argv or public configuration.
`DAKNODE_API_KEY` is used only by local stdio, never as remote fallback.
Dedicated OAuth tokens must have `dak_oauth_` plus a canonical unpadded 32-byte
URL-safe payload (43 characters). Malformed OAuth-shaped strings are refused
before verification; a syntactically valid token still requires the backend's
active grant mapping and permission checks. Ordinary live/test handling remains
unchanged. The 401 challenge points to the backend's real canonical protected
resource metadata; the MCP process does not manufacture issuer metadata.

`DAKNODE_MCP_HOSTS` is an exact comma-separated Host-authority list (default
`api.daknode.com`); no wildcards. All Origin-bearing requests are refused unless
their exact HTTPS Origin appears in `DAKNODE_MCP_ORIGINS`. Missing Origin is
permitted for native clients; it does not bypass credential verification. No
CORS credentials, wildcard Origin or forwarded-header trust is installed.
Proxy request/header/time limits and credential-safe proxy logs are deployment
requirements. Application access logging is disabled.

`mcp_server/Dockerfile` builds this process and the generated SDK in a dedicated
image from the repository-root context, runs as UID 10001 and binds port 8081.
`GET /health` is unauthenticated dependency-free liveness only; it is not API,
OAuth or event readiness. The container must be reachable only through the
canonical TLS proxy. Outside the container the default bind is loopback;
`DAKNODE_MCP_BIND` explicitly changes it. No image build/deployment is implied.

Verified with **mcp 2.2.0**, Python 3.13, and protocol revisions **2025-11-25**
(stateless initialize + tools requests) and **2026-07-28** (per-request envelope,
`Mcp-Method`/`Mcp-Name` routing headers). SDK wire validation is retained.
Core subscriptions and native draft Events push require **2026-07-28**; ordinary
tools retain the tested 2025 compatibility path. No session IDs or unauthenticated
standalone GET stream are used. OAuth-discovery clients such as
ChatGPT/Claude require the real backend grant/consent/PKCE flow and a tested
verifier adapter before they are supported. No fake OAuth metadata is served.

Limits: 16 KiB headers, 64 KiB JSON request, 1 MiB uncompressed REST response,
2 MiB ordinary MCP response, 64 KiB per SSE frame. Compressed REST responses are refused. Verification has a
5-second deadline, request upload 10 seconds, MCP handling 25 seconds; REST
connect timeout is 5 seconds and read/write/pool timeouts 15 seconds. REST body
iteration also has a 15-second elapsed ceiling (checked between chunks). Sync
SDK calls run in SDK worker threads; cancellation cannot retract a dispatched
write or force-kill a thread, but disconnect suppresses delivery and network work
is bounded. A timeout/disconnect is not proof a send failed: retry only with its
original idempotency key. Errors expose safe codes/status and numeric retry hints,
not raw API bodies, tracebacks or credentials. HTTP results are `no-store`.

## Core subscriptions and native Events push (local wire-tested)

AgentMail's [MCP Events](https://www.agentmail.to/docs/integrations/mcp-events)
publishes mail occurrences, and [OpenAI's Events guidance](https://developers.openai.com/plugins/build/mcp-events)
describes event-triggered clients. The official [Events design sketch](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md)
is a separate draft extension with `events/list`, `events/poll`, `events/stream`
and webhook `events/subscribe`/`events/unsubscribe` methods, cursors and TTLs.

The installed official `mcp==2.2.0` source has no built-in Events extension, but
provides the public `Extension`/`MethodBinding`, lowlevel handler and notification
channels used by this custom implementation. Its built-in `ListenHandler`
serves core `subscriptions/listen`. See the official
[subscription documentation](https://py.sdk.modelcontextprotocol.io/handlers/subscriptions/)
and [v2.2.0 source](https://github.com/modelcontextprotocol/python-sdk/tree/v2.2.0/src/mcp).

Two distinct capabilities are implemented and wire-tested:

- `subscriptions/listen`: the official SDK sends the acknowledgement first,
  tags frames with the request ID, and delivers only requested catalog/resource
  cues. Resources use `daknode://inboxes/{inboxId}/calendar/changes` or
  `daknode://inboxes/{inboxId}/messages/changes`. Each listen has its own bus,
  with an eight-cue buffer, scoped resource validation and revocation checks.
  These core cues have **no replay guarantee**; clients re-listen and refetch.
- `io.modelcontextprotocol/events`: custom draft Events `events/list`,
  `events/poll`, and actual SSE `events/stream`. Discovery advertises exactly
  `poll` and `push`, never webhook. The implementation follows the Events design
  sketch fetched 2026-11-01 (draft, not a finalized core MCP primitive).
  Frames are `notifications/events/active`, `event`, `heartbeat`, and
  `terminated`; every frame is request-ID tagged. The final result closes a
  server-ended stream; client disconnect cancels without a final result.

The default verifier exposes `message.received` for `messages.read`, and
`calendar.event.created`, `.updated`, `.cancelled`, `.responded`, `.starting`,
`.ending` for `calendar.read`. Inbox-scoped credentials can subscribe only to
their own inbox. Event payloads contain identifiers and calendar revision only;
no bodies, subjects, event titles, attendees or email addresses. This is not a
claim of AgentMail's exact spam/authentication filtering policy: retained mail
availability follows DakNode's existing polling API policy.

Native delivery **internally polls the bounded REST journal**: existing
`GET /v1/inboxes/{inbox_id}/events` for messages and
`GET /v1/inboxes/{inbox_id}/calendar/changes` for calendar facts. Each request
asks for one row, so each occurrence carries the exact cursor after that row;
disconnect cannot skip later records in an already-read page. Cursors and
stable event IDs come from the API, not process memory. Clients persist cursors
and deduplicate IDs across reconnects. Expired cursors restart one bounded page
with `truncated: true`; history older than `maxAgeMs` is skipped visibly.
`maxAgeMs` defaults to five minutes and is capped at one day. Polling is normally
every two seconds, or 250 ms while a backlog remains. The API must mount both
routes and enforce key scope, permissions, revocation, deleted-inbox and erasure
filters; these are deployment/integration gates, not proved by mock tests here.

Streams reauthorize each source read and before occurrence delivery. There are
64 stream slots per process and four per credential across both protocols,
with no retained global credential or cross-tenant fan-out. Each stream ends
after five minutes (reconnect with its cursor); SDK cancellation releases the
slot. Each SSE write has a 10-second deadline and there is no unbounded event
queue. Failures send safe control metadata or close the connection, not raw
upstream bodies. Streams do not authorize any follow-on write.

`events/subscribe`/`events/unsubscribe` webhook delivery is **not implemented**.
ChatGPT's documented webhook delivery therefore remains a separate acceptance
gate: durable subscription storage, approved callback/SSRF protection, signing,
retries, refresh/expiry and real-client validation are still required. Neither
core resource cues nor push-only Events prove ChatGPT task-trigger compatibility.

For deployment, forward `/mcp` POSTs without buffering SSE, retain the canonical
Host, and allow idle timeouts of at least 60 seconds (SDK keepalives are 15 s).
The stream's five-minute lifetime is intentional. `/health` remains liveness
only. Route `/.well-known/*` and `/oauth/*` to the API OAuth implementation,
not this process. Use only the exact browser Origins actually required by tested
clients; no wildcard or automatic Origin reflection.
