daknode-mcp 0.1.0__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.1.0/PKG-INFO +77 -0
- daknode_mcp-0.1.0/README.md +66 -0
- daknode_mcp-0.1.0/__init__.py +1 -0
- daknode_mcp-0.1.0/daknode_mcp.egg-info/PKG-INFO +77 -0
- daknode_mcp-0.1.0/daknode_mcp.egg-info/SOURCES.txt +12 -0
- daknode_mcp-0.1.0/daknode_mcp.egg-info/dependency_links.txt +1 -0
- daknode_mcp-0.1.0/daknode_mcp.egg-info/entry_points.txt +2 -0
- daknode_mcp-0.1.0/daknode_mcp.egg-info/requires.txt +2 -0
- daknode_mcp-0.1.0/daknode_mcp.egg-info/top_level.txt +1 -0
- daknode_mcp-0.1.0/pyproject.toml +28 -0
- daknode_mcp-0.1.0/server.py +229 -0
- daknode_mcp-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: daknode-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DakNode's email API as MCP tools for AI agents, over stdio.
|
|
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.2,>=0.1
|
|
10
|
+
Requires-Dist: mcp<3,>=2.2
|
|
11
|
+
|
|
12
|
+
# DakNode MCP server
|
|
13
|
+
|
|
14
|
+
DakNode's API as MCP tools, over **stdio**. The client launches it as a
|
|
15
|
+
subprocess; there is no hosted endpoint and no infrastructure.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Python 3.10+. With [uv](https://docs.astral.sh/uv/), nothing needs installing
|
|
20
|
+
up front — the client starts it with `uvx`. Otherwise `pipx install daknode-mcp`.
|
|
21
|
+
|
|
22
|
+
Get an API key with the two-request signup at <https://daknode.com/developers>.
|
|
23
|
+
|
|
24
|
+
## Configure
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"daknode": {
|
|
30
|
+
"command": "uvx",
|
|
31
|
+
"args": ["daknode-mcp"],
|
|
32
|
+
"env": { "DAKNODE_API_KEY": "…" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
With `pipx`, use `"command": "daknode-mcp"` and no `args`. From a repository
|
|
39
|
+
checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.
|
|
40
|
+
|
|
41
|
+
The key comes from the environment and **never from a command-line argument**:
|
|
42
|
+
argv is visible to every process through `ps`, and shells record it in history.
|
|
43
|
+
The server refuses to start without it, so a misconfiguration is obvious at
|
|
44
|
+
launch rather than looking like a broken tool.
|
|
45
|
+
|
|
46
|
+
## Tools
|
|
47
|
+
|
|
48
|
+
`create_inbox` · `list_threads` · `list_messages` · `get_thread` · `get_message`
|
|
49
|
+
· `search` · `send`
|
|
50
|
+
|
|
51
|
+
**Every tool is 1:1 with a published API operation.** That is `architecture.md`
|
|
52
|
+
§0 consequence 4, and it is the boundary this server exists inside: DakNode
|
|
53
|
+
exposes operations, the agent decides which to call. A tool like
|
|
54
|
+
`reply_to_latest` or `auto_respond` would cross that line — DakNode would be
|
|
55
|
+
choosing which message to answer — **while looking like a convenience**.
|
|
56
|
+
`tests/test_mcp_server.py` enforces it rather than leaving it to review.
|
|
57
|
+
|
|
58
|
+
## What is deliberately absent
|
|
59
|
+
|
|
60
|
+
**Erasure.** It is irreversible, and an agent that can erase a data subject
|
|
61
|
+
through a tool call is a liability no convenience justifies. It stays an
|
|
62
|
+
explicit API call a human integrates.
|
|
63
|
+
|
|
64
|
+
**Console and operator APIs.** Not a product surface.
|
|
65
|
+
|
|
66
|
+
## Notes for whoever uses this
|
|
67
|
+
|
|
68
|
+
`send` without `thread_id` opens a **new** conversation in the recipient's
|
|
69
|
+
client. Pass `thread_id` to reply into an existing one — the server then derives
|
|
70
|
+
`In-Reply-To` and `References` from that thread's latest received message.
|
|
71
|
+
|
|
72
|
+
`search` returns identifiers and scores, **not content**. Resolve them with
|
|
73
|
+
`get_message` or `get_thread`.
|
|
74
|
+
|
|
75
|
+
`body_text` is already cleaned: quoted history and signatures are stripped.
|
|
76
|
+
Check `risk_level` before feeding content to a model — `high` means
|
|
77
|
+
prompt-injection patterns were detected in mail from the internet.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# DakNode MCP server
|
|
2
|
+
|
|
3
|
+
DakNode's API as MCP tools, over **stdio**. The client launches it as a
|
|
4
|
+
subprocess; there is no hosted endpoint and no infrastructure.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
Python 3.10+. With [uv](https://docs.astral.sh/uv/), nothing needs installing
|
|
9
|
+
up front — the client starts it with `uvx`. Otherwise `pipx install daknode-mcp`.
|
|
10
|
+
|
|
11
|
+
Get an API key with the two-request signup at <https://daknode.com/developers>.
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"daknode": {
|
|
19
|
+
"command": "uvx",
|
|
20
|
+
"args": ["daknode-mcp"],
|
|
21
|
+
"env": { "DAKNODE_API_KEY": "…" }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With `pipx`, use `"command": "daknode-mcp"` and no `args`. From a repository
|
|
28
|
+
checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.
|
|
29
|
+
|
|
30
|
+
The key comes from the environment and **never from a command-line argument**:
|
|
31
|
+
argv is visible to every process through `ps`, and shells record it in history.
|
|
32
|
+
The server refuses to start without it, so a misconfiguration is obvious at
|
|
33
|
+
launch rather than looking like a broken tool.
|
|
34
|
+
|
|
35
|
+
## Tools
|
|
36
|
+
|
|
37
|
+
`create_inbox` · `list_threads` · `list_messages` · `get_thread` · `get_message`
|
|
38
|
+
· `search` · `send`
|
|
39
|
+
|
|
40
|
+
**Every tool is 1:1 with a published API operation.** That is `architecture.md`
|
|
41
|
+
§0 consequence 4, and it is the boundary this server exists inside: DakNode
|
|
42
|
+
exposes operations, the agent decides which to call. A tool like
|
|
43
|
+
`reply_to_latest` or `auto_respond` would cross that line — DakNode would be
|
|
44
|
+
choosing which message to answer — **while looking like a convenience**.
|
|
45
|
+
`tests/test_mcp_server.py` enforces it rather than leaving it to review.
|
|
46
|
+
|
|
47
|
+
## What is deliberately absent
|
|
48
|
+
|
|
49
|
+
**Erasure.** It is irreversible, and an agent that can erase a data subject
|
|
50
|
+
through a tool call is a liability no convenience justifies. It stays an
|
|
51
|
+
explicit API call a human integrates.
|
|
52
|
+
|
|
53
|
+
**Console and operator APIs.** Not a product surface.
|
|
54
|
+
|
|
55
|
+
## Notes for whoever uses this
|
|
56
|
+
|
|
57
|
+
`send` without `thread_id` opens a **new** conversation in the recipient's
|
|
58
|
+
client. Pass `thread_id` to reply into an existing one — the server then derives
|
|
59
|
+
`In-Reply-To` and `References` from that thread's latest received message.
|
|
60
|
+
|
|
61
|
+
`search` returns identifiers and scores, **not content**. Resolve them with
|
|
62
|
+
`get_message` or `get_thread`.
|
|
63
|
+
|
|
64
|
+
`body_text` is already cleaned: quoted history and signatures are stripped.
|
|
65
|
+
Check `risk_level` before feeding content to a model — `high` means
|
|
66
|
+
prompt-injection patterns were detected in mail from the internet.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""DakNode MCP server. Spec: `.agent/specs/17_agent_toolkit.md#mcp`."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: daknode-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DakNode's email API as MCP tools for AI agents, over stdio.
|
|
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.2,>=0.1
|
|
10
|
+
Requires-Dist: mcp<3,>=2.2
|
|
11
|
+
|
|
12
|
+
# DakNode MCP server
|
|
13
|
+
|
|
14
|
+
DakNode's API as MCP tools, over **stdio**. The client launches it as a
|
|
15
|
+
subprocess; there is no hosted endpoint and no infrastructure.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Python 3.10+. With [uv](https://docs.astral.sh/uv/), nothing needs installing
|
|
20
|
+
up front — the client starts it with `uvx`. Otherwise `pipx install daknode-mcp`.
|
|
21
|
+
|
|
22
|
+
Get an API key with the two-request signup at <https://daknode.com/developers>.
|
|
23
|
+
|
|
24
|
+
## Configure
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"daknode": {
|
|
30
|
+
"command": "uvx",
|
|
31
|
+
"args": ["daknode-mcp"],
|
|
32
|
+
"env": { "DAKNODE_API_KEY": "…" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
With `pipx`, use `"command": "daknode-mcp"` and no `args`. From a repository
|
|
39
|
+
checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.
|
|
40
|
+
|
|
41
|
+
The key comes from the environment and **never from a command-line argument**:
|
|
42
|
+
argv is visible to every process through `ps`, and shells record it in history.
|
|
43
|
+
The server refuses to start without it, so a misconfiguration is obvious at
|
|
44
|
+
launch rather than looking like a broken tool.
|
|
45
|
+
|
|
46
|
+
## Tools
|
|
47
|
+
|
|
48
|
+
`create_inbox` · `list_threads` · `list_messages` · `get_thread` · `get_message`
|
|
49
|
+
· `search` · `send`
|
|
50
|
+
|
|
51
|
+
**Every tool is 1:1 with a published API operation.** That is `architecture.md`
|
|
52
|
+
§0 consequence 4, and it is the boundary this server exists inside: DakNode
|
|
53
|
+
exposes operations, the agent decides which to call. A tool like
|
|
54
|
+
`reply_to_latest` or `auto_respond` would cross that line — DakNode would be
|
|
55
|
+
choosing which message to answer — **while looking like a convenience**.
|
|
56
|
+
`tests/test_mcp_server.py` enforces it rather than leaving it to review.
|
|
57
|
+
|
|
58
|
+
## What is deliberately absent
|
|
59
|
+
|
|
60
|
+
**Erasure.** It is irreversible, and an agent that can erase a data subject
|
|
61
|
+
through a tool call is a liability no convenience justifies. It stays an
|
|
62
|
+
explicit API call a human integrates.
|
|
63
|
+
|
|
64
|
+
**Console and operator APIs.** Not a product surface.
|
|
65
|
+
|
|
66
|
+
## Notes for whoever uses this
|
|
67
|
+
|
|
68
|
+
`send` without `thread_id` opens a **new** conversation in the recipient's
|
|
69
|
+
client. Pass `thread_id` to reply into an existing one — the server then derives
|
|
70
|
+
`In-Reply-To` and `References` from that thread's latest received message.
|
|
71
|
+
|
|
72
|
+
`search` returns identifiers and scores, **not content**. Resolve them with
|
|
73
|
+
`get_message` or `get_thread`.
|
|
74
|
+
|
|
75
|
+
`body_text` is already cleaned: quoted history and signatures are stripped.
|
|
76
|
+
Check `risk_level` before feeding content to a model — `high` means
|
|
77
|
+
prompt-injection patterns were detected in mail from the internet.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
__init__.py
|
|
3
|
+
pyproject.toml
|
|
4
|
+
server.py
|
|
5
|
+
./__init__.py
|
|
6
|
+
./server.py
|
|
7
|
+
daknode_mcp.egg-info/PKG-INFO
|
|
8
|
+
daknode_mcp.egg-info/SOURCES.txt
|
|
9
|
+
daknode_mcp.egg-info/dependency_links.txt
|
|
10
|
+
daknode_mcp.egg-info/entry_points.txt
|
|
11
|
+
daknode_mcp.egg-info/requires.txt
|
|
12
|
+
daknode_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
daknode_mcp
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# PUB-PYPI — `.agent/specs/pypi_publishing.md`. The published import package is
|
|
2
|
+
# `daknode_mcp`, mapped onto this directory so the repository keeps importing
|
|
3
|
+
# it as `mcp_server` (tests, local runs) without a second copy of the code.
|
|
4
|
+
[build-system]
|
|
5
|
+
requires = ["setuptools>=69"]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "daknode-mcp"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
description = "DakNode's email API as MCP tools for AI agents, over stdio."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"daknode>=0.1,<0.2",
|
|
16
|
+
"mcp>=2.2,<3",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://daknode.com"
|
|
21
|
+
Documentation = "https://daknode.com/developers"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
daknode-mcp = "daknode_mcp.server:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = { "daknode_mcp" = "." }
|
|
28
|
+
packages = ["daknode_mcp"]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""DakNode MCP server — DakNode's API as tools, over stdio.
|
|
2
|
+
|
|
3
|
+
Spec: `.agent/specs/17_agent_toolkit.md#mcp`.
|
|
4
|
+
Boundary: `.agent/architecture.md` §0, "Where MCP, SDKs and a CLI sit".
|
|
5
|
+
|
|
6
|
+
**Every tool is 1:1 with a published API operation and composes no decision.**
|
|
7
|
+
That is `architecture.md` §0 consequence 4, and it is the whole boundary here.
|
|
8
|
+
`create_inbox`, `list_threads`, `send` are inside the line. A tool called
|
|
9
|
+
`reply_to_latest` or `auto_respond` would be outside it — DakNode would be
|
|
10
|
+
choosing which message to answer, **while looking like a convenience**. That is
|
|
11
|
+
how this boundary erodes: not by someone adding an agent framework, which the
|
|
12
|
+
existing rule catches loudly, but by someone adding a helpful-sounding tool.
|
|
13
|
+
|
|
14
|
+
If a tool cannot be named after an `operationId`, it does not belong here.
|
|
15
|
+
`tests/test_mcp_server.py` enforces that rather than leaving it to review.
|
|
16
|
+
|
|
17
|
+
This wraps the **generated SDK** rather than building requests itself: a second
|
|
18
|
+
HTTP client would drift from the first, and the SDK is already regenerated from
|
|
19
|
+
the spec and guarded against drift.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
_SDK = Path(__file__).resolve().parents[1] / "sdk" / "python"
|
|
30
|
+
if str(_SDK) not in sys.path: # pragma: no cover - import plumbing
|
|
31
|
+
sys.path.insert(0, str(_SDK))
|
|
32
|
+
|
|
33
|
+
from mcp.server.mcpserver import MCPServer # noqa: E402
|
|
34
|
+
|
|
35
|
+
DEFAULT_BASE_URL = "https://api.daknode.com"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _version() -> str:
|
|
39
|
+
"""The installed `daknode-mcp` version, so a client can report what it runs.
|
|
40
|
+
|
|
41
|
+
Read from package metadata rather than written here a second time; a copy
|
|
42
|
+
run from the repository, not installed, reports itself as such.
|
|
43
|
+
"""
|
|
44
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
return version("daknode-mcp")
|
|
48
|
+
except PackageNotFoundError:
|
|
49
|
+
return "0.0.0+checkout"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MissingApiKey(RuntimeError):
|
|
53
|
+
"""No credential. Raised at STARTUP, not at first tool call.
|
|
54
|
+
|
|
55
|
+
A server that starts happily and then fails every call looks like a broken
|
|
56
|
+
tool rather than a missing setting, and the person debugging it is an agent.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _client():
|
|
61
|
+
"""Build the SDK client from the environment.
|
|
62
|
+
|
|
63
|
+
The key comes from `DAKNODE_API_KEY` and **never from argv**: command-line
|
|
64
|
+
arguments are visible to every process on the machine through `ps`, and
|
|
65
|
+
shells record them in history.
|
|
66
|
+
"""
|
|
67
|
+
key = os.environ.get("DAKNODE_API_KEY", "").strip()
|
|
68
|
+
if not key:
|
|
69
|
+
raise MissingApiKey(
|
|
70
|
+
"DAKNODE_API_KEY is not set. Put it in the MCP client's environment "
|
|
71
|
+
"configuration for this server."
|
|
72
|
+
)
|
|
73
|
+
from daknode import AuthenticatedClient
|
|
74
|
+
|
|
75
|
+
return AuthenticatedClient(
|
|
76
|
+
base_url=os.environ.get("DAKNODE_BASE_URL", DEFAULT_BASE_URL).rstrip("/"),
|
|
77
|
+
token=key,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _result(response) -> Any:
|
|
82
|
+
"""Return the parsed body, or a readable failure.
|
|
83
|
+
|
|
84
|
+
Never raises to the agent: an MCP tool that throws gives the model a stack
|
|
85
|
+
trace, which it cannot act on and may repeat back to a user.
|
|
86
|
+
"""
|
|
87
|
+
parsed = getattr(response, "parsed", None)
|
|
88
|
+
if parsed is not None:
|
|
89
|
+
return parsed.to_dict() if hasattr(parsed, "to_dict") else parsed
|
|
90
|
+
return {
|
|
91
|
+
"status": getattr(response, "status_code", None),
|
|
92
|
+
"error": (getattr(response, "content", b"") or b"").decode("utf-8", "replace")[:500],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build_server() -> MCPServer:
|
|
97
|
+
"""Register one tool per published operation.
|
|
98
|
+
|
|
99
|
+
**Deliberately NOT exposed:** erasure. It is irreversible, and an agent that
|
|
100
|
+
can erase a data subject through a tool call is a liability no convenience
|
|
101
|
+
justifies (spec §5). It stays an explicit API call a human integrates.
|
|
102
|
+
Console, operator and provider-callback surfaces are absent for the same
|
|
103
|
+
reason the SDK omits them.
|
|
104
|
+
"""
|
|
105
|
+
server = MCPServer(
|
|
106
|
+
name="daknode",
|
|
107
|
+
title="DakNode",
|
|
108
|
+
version=_version(),
|
|
109
|
+
website_url="https://daknode.com",
|
|
110
|
+
description=(
|
|
111
|
+
"Email for software agents: provision an inbox, read threads, "
|
|
112
|
+
"search, and send."
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@server.tool()
|
|
117
|
+
def create_inbox() -> Any:
|
|
118
|
+
"""Create a new agent inbox and return its id and address.
|
|
119
|
+
|
|
120
|
+
The same address both sends and receives. Inbound mail arrives by
|
|
121
|
+
webhook; use `list_threads` and `get_thread` to read history.
|
|
122
|
+
"""
|
|
123
|
+
from daknode.api.inboxes import create_inbox as op
|
|
124
|
+
from daknode.models import InboxCreateRequest
|
|
125
|
+
|
|
126
|
+
with _client() as c:
|
|
127
|
+
return _result(op.sync_detailed(client=c, body=InboxCreateRequest()))
|
|
128
|
+
|
|
129
|
+
@server.tool()
|
|
130
|
+
def list_threads(inbox_id: str, limit: int = 25, cursor: str | None = None) -> Any:
|
|
131
|
+
"""List conversations in an inbox, most recently active first."""
|
|
132
|
+
from daknode.api.messages import list_threads as op
|
|
133
|
+
from daknode.types import UNSET
|
|
134
|
+
|
|
135
|
+
with _client() as c:
|
|
136
|
+
return _result(op.sync_detailed(
|
|
137
|
+
inbox_id=inbox_id, client=c, limit=limit,
|
|
138
|
+
cursor=cursor if cursor else UNSET,
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
@server.tool()
|
|
142
|
+
def list_messages(inbox_id: str, limit: int = 25, cursor: str | None = None) -> Any:
|
|
143
|
+
"""List messages in an inbox, newest first."""
|
|
144
|
+
from daknode.api.messages import list_messages as op
|
|
145
|
+
from daknode.types import UNSET
|
|
146
|
+
|
|
147
|
+
with _client() as c:
|
|
148
|
+
return _result(op.sync_detailed(
|
|
149
|
+
inbox_id=inbox_id, client=c, limit=limit,
|
|
150
|
+
cursor=cursor if cursor else UNSET,
|
|
151
|
+
))
|
|
152
|
+
|
|
153
|
+
@server.tool()
|
|
154
|
+
def get_thread(thread_id: str) -> Any:
|
|
155
|
+
"""Fetch one conversation with both sides, oldest first."""
|
|
156
|
+
from daknode.api.messages import get_thread as op
|
|
157
|
+
|
|
158
|
+
with _client() as c:
|
|
159
|
+
return _result(op.sync_detailed(thread_id=thread_id, client=c))
|
|
160
|
+
|
|
161
|
+
@server.tool()
|
|
162
|
+
def get_message(message_id: str) -> Any:
|
|
163
|
+
"""Fetch one message by id.
|
|
164
|
+
|
|
165
|
+
`body_text` is already cleaned: quoted history and signatures are
|
|
166
|
+
stripped, so there is no reply chain to parse out of it. Check
|
|
167
|
+
`risk_level` before feeding the content to a model — `high` means
|
|
168
|
+
prompt-injection patterns were detected in mail from the internet.
|
|
169
|
+
"""
|
|
170
|
+
from daknode.api.messages import get_message as op
|
|
171
|
+
|
|
172
|
+
with _client() as c:
|
|
173
|
+
return _result(op.sync_detailed(message_id=message_id, client=c))
|
|
174
|
+
|
|
175
|
+
@server.tool()
|
|
176
|
+
def search(query: str, top_k: int = 10) -> Any:
|
|
177
|
+
"""Semantic search over this caller's own mail.
|
|
178
|
+
|
|
179
|
+
Returns identifiers and scores, not content — resolve them with
|
|
180
|
+
`get_message` or `get_thread`.
|
|
181
|
+
"""
|
|
182
|
+
from daknode.api.search import search as op
|
|
183
|
+
from daknode.models import SearchRequest
|
|
184
|
+
|
|
185
|
+
with _client() as c:
|
|
186
|
+
return _result(op.sync_detailed(
|
|
187
|
+
client=c, body=SearchRequest(query=query, top_k=top_k)
|
|
188
|
+
))
|
|
189
|
+
|
|
190
|
+
@server.tool()
|
|
191
|
+
def send(
|
|
192
|
+
inbox_id: str,
|
|
193
|
+
to: str,
|
|
194
|
+
subject: str,
|
|
195
|
+
body_text: str,
|
|
196
|
+
thread_id: str | None = None,
|
|
197
|
+
) -> Any:
|
|
198
|
+
"""Send a message.
|
|
199
|
+
|
|
200
|
+
Pass `thread_id` to reply into a conversation: the server derives
|
|
201
|
+
`In-Reply-To` and `References` from that thread's latest received
|
|
202
|
+
message. Without it the message opens a NEW conversation in the
|
|
203
|
+
recipient's client — it does not continue one.
|
|
204
|
+
"""
|
|
205
|
+
from daknode.api.send import send as op
|
|
206
|
+
from daknode.models import SendRequest
|
|
207
|
+
from daknode.types import UNSET
|
|
208
|
+
|
|
209
|
+
with _client() as c:
|
|
210
|
+
return _result(op.sync_detailed(client=c, body=SendRequest(
|
|
211
|
+
inbox_id=inbox_id, to=to, subject=subject, body_text=body_text,
|
|
212
|
+
thread_id=thread_id if thread_id else UNSET,
|
|
213
|
+
)))
|
|
214
|
+
|
|
215
|
+
return server
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main() -> int: # pragma: no cover - process entry point
|
|
219
|
+
try:
|
|
220
|
+
_client()
|
|
221
|
+
except MissingApiKey as exc:
|
|
222
|
+
print(f"daknode-mcp: {exc}", file=sys.stderr)
|
|
223
|
+
return 2
|
|
224
|
+
build_server().run()
|
|
225
|
+
return 0
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__": # pragma: no cover
|
|
229
|
+
raise SystemExit(main())
|