daknode-mcp 0.3.4__tar.gz → 0.3.5__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.4 → daknode_mcp-0.3.5}/PKG-INFO +12 -5
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/README.md +8 -3
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/daknode_mcp.egg-info/PKG-INFO +12 -5
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/daknode_mcp.egg-info/SOURCES.txt +1 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/pyproject.toml +4 -2
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/server.py +12 -10
- daknode_mcp-0.3.5/tests/test_local_discovery.py +148 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/__init__.py +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/calendar_models.py +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/daknode_mcp.egg-info/dependency_links.txt +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/daknode_mcp.egg-info/entry_points.txt +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/daknode_mcp.egg-info/requires.txt +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/daknode_mcp.egg-info/top_level.txt +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/event_source.py +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/events.py +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/identity.py +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/remote.py +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/setup.cfg +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/tests/test_calendar_scheduling.py +0 -0
- {daknode_mcp-0.3.4 → daknode_mcp-0.3.5}/tests/test_write_tools.py +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: daknode-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: DakNode's email and calendar API tools over local and authenticated remote MCP.
|
|
5
5
|
Project-URL: Homepage, https://daknode.com
|
|
6
|
-
Project-URL: Documentation, https://daknode.com/
|
|
6
|
+
Project-URL: Documentation, https://daknode.com/integrations/mcp/
|
|
7
|
+
Project-URL: Security, https://daknode.com/security/reporting/
|
|
8
|
+
Keywords: daknode,mcp,email,calendar,developer-tools
|
|
7
9
|
Requires-Python: >=3.10
|
|
8
10
|
Description-Content-Type: text/markdown
|
|
9
11
|
Requires-Dist: daknode<0.4,>=0.3.9
|
|
@@ -13,7 +15,7 @@ Requires-Dist: mcp<3,>=2.2
|
|
|
13
15
|
|
|
14
16
|
<!-- mcp-name: io.github.Subodh103/daknode -->
|
|
15
17
|
|
|
16
|
-
DakNode MCP 0.3.
|
|
18
|
+
DakNode MCP 0.3.5 exposes 21 email and calendar API tools over **stdio**,
|
|
17
19
|
including availability, reliable booking, reminders and recurring-event edits.
|
|
18
20
|
An authenticated hosted endpoint is also available at
|
|
19
21
|
`https://api.daknode.com/mcp`; local installation does not connect to it or
|
|
@@ -45,8 +47,13 @@ checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.
|
|
|
45
47
|
|
|
46
48
|
The key comes from the environment and **never from a command-line argument**:
|
|
47
49
|
argv is visible to every process through `ps`, and shells record it in history.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
Version 0.3.5 supports
|
|
51
|
+
credential-free local `initialize`, `tools/list` and empty `prompts/list`, so a
|
|
52
|
+
client can inspect static schemas before setup. Every operation still requires credentials and fails with structured
|
|
53
|
+
`authentication_required` setup guidance if the key is missing; no API request
|
|
54
|
+
is sent. Configure the key privately, then restart/reconnect the client.
|
|
55
|
+
Hosted MCP authentication and permission-filtered discovery are unchanged:
|
|
56
|
+
this is not anonymous access to the remote endpoint or to customer data.
|
|
50
57
|
|
|
51
58
|
## Tools
|
|
52
59
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- mcp-name: io.github.Subodh103/daknode -->
|
|
4
4
|
|
|
5
|
-
DakNode MCP 0.3.
|
|
5
|
+
DakNode MCP 0.3.5 exposes 21 email and calendar API tools over **stdio**,
|
|
6
6
|
including availability, reliable booking, reminders and recurring-event edits.
|
|
7
7
|
An authenticated hosted endpoint is also available at
|
|
8
8
|
`https://api.daknode.com/mcp`; local installation does not connect to it or
|
|
@@ -34,8 +34,13 @@ checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.
|
|
|
34
34
|
|
|
35
35
|
The key comes from the environment and **never from a command-line argument**:
|
|
36
36
|
argv is visible to every process through `ps`, and shells record it in history.
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
Version 0.3.5 supports
|
|
38
|
+
credential-free local `initialize`, `tools/list` and empty `prompts/list`, so a
|
|
39
|
+
client can inspect static schemas before setup. Every operation still requires credentials and fails with structured
|
|
40
|
+
`authentication_required` setup guidance if the key is missing; no API request
|
|
41
|
+
is sent. Configure the key privately, then restart/reconnect the client.
|
|
42
|
+
Hosted MCP authentication and permission-filtered discovery are unchanged:
|
|
43
|
+
this is not anonymous access to the remote endpoint or to customer data.
|
|
39
44
|
|
|
40
45
|
## Tools
|
|
41
46
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: daknode-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: DakNode's email and calendar API tools over local and authenticated remote MCP.
|
|
5
5
|
Project-URL: Homepage, https://daknode.com
|
|
6
|
-
Project-URL: Documentation, https://daknode.com/
|
|
6
|
+
Project-URL: Documentation, https://daknode.com/integrations/mcp/
|
|
7
|
+
Project-URL: Security, https://daknode.com/security/reporting/
|
|
8
|
+
Keywords: daknode,mcp,email,calendar,developer-tools
|
|
7
9
|
Requires-Python: >=3.10
|
|
8
10
|
Description-Content-Type: text/markdown
|
|
9
11
|
Requires-Dist: daknode<0.4,>=0.3.9
|
|
@@ -13,7 +15,7 @@ Requires-Dist: mcp<3,>=2.2
|
|
|
13
15
|
|
|
14
16
|
<!-- mcp-name: io.github.Subodh103/daknode -->
|
|
15
17
|
|
|
16
|
-
DakNode MCP 0.3.
|
|
18
|
+
DakNode MCP 0.3.5 exposes 21 email and calendar API tools over **stdio**,
|
|
17
19
|
including availability, reliable booking, reminders and recurring-event edits.
|
|
18
20
|
An authenticated hosted endpoint is also available at
|
|
19
21
|
`https://api.daknode.com/mcp`; local installation does not connect to it or
|
|
@@ -45,8 +47,13 @@ checkout, `"command": "python", "args": ["-m", "mcp_server.server"]`.
|
|
|
45
47
|
|
|
46
48
|
The key comes from the environment and **never from a command-line argument**:
|
|
47
49
|
argv is visible to every process through `ps`, and shells record it in history.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
Version 0.3.5 supports
|
|
51
|
+
credential-free local `initialize`, `tools/list` and empty `prompts/list`, so a
|
|
52
|
+
client can inspect static schemas before setup. Every operation still requires credentials and fails with structured
|
|
53
|
+
`authentication_required` setup guidance if the key is missing; no API request
|
|
54
|
+
is sent. Configure the key privately, then restart/reconnect the client.
|
|
55
|
+
Hosted MCP authentication and permission-filtered discovery are unchanged:
|
|
56
|
+
this is not anonymous access to the remote endpoint or to customer data.
|
|
50
57
|
|
|
51
58
|
## Tools
|
|
52
59
|
|
|
@@ -7,10 +7,11 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "daknode-mcp"
|
|
10
|
-
version = "0.3.
|
|
10
|
+
version = "0.3.5"
|
|
11
11
|
description = "DakNode's email and calendar API tools over local and authenticated remote MCP."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
|
+
keywords = ["daknode", "mcp", "email", "calendar", "developer-tools"]
|
|
14
15
|
dependencies = [
|
|
15
16
|
"daknode>=0.3.9,<0.4",
|
|
16
17
|
"mcp>=2.2,<3",
|
|
@@ -18,7 +19,8 @@ dependencies = [
|
|
|
18
19
|
|
|
19
20
|
[project.urls]
|
|
20
21
|
Homepage = "https://daknode.com"
|
|
21
|
-
Documentation = "https://daknode.com/
|
|
22
|
+
Documentation = "https://daknode.com/integrations/mcp/"
|
|
23
|
+
Security = "https://daknode.com/security/reporting/"
|
|
22
24
|
|
|
23
25
|
[project.scripts]
|
|
24
26
|
daknode-mcp = "daknode_mcp.server:main"
|
|
@@ -172,11 +172,7 @@ def _version() -> str:
|
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
class MissingApiKey(RuntimeError):
|
|
175
|
-
"""No credential.
|
|
176
|
-
|
|
177
|
-
A server that starts happily and then fails every call looks like a broken
|
|
178
|
-
tool rather than a missing setting, and the person debugging it is an agent.
|
|
179
|
-
"""
|
|
175
|
+
"""No operation credential. Static local discovery needs no SDK client."""
|
|
180
176
|
|
|
181
177
|
|
|
182
178
|
def _client():
|
|
@@ -228,6 +224,15 @@ def _execute(operation, **kwargs):
|
|
|
228
224
|
try:
|
|
229
225
|
with _client() as client:
|
|
230
226
|
return _result(operation(client=client, **kwargs))
|
|
227
|
+
except MissingApiKey:
|
|
228
|
+
return _failure(
|
|
229
|
+
"authentication_required",
|
|
230
|
+
"Configure DAKNODE_API_KEY privately in your MCP client's environment, "
|
|
231
|
+
"then restart or reconnect. Create an account at "
|
|
232
|
+
"https://console.daknode.com/sign-up or read "
|
|
233
|
+
"https://daknode.com/developers/. Never paste the key into chat. "
|
|
234
|
+
"No API operation was performed.",
|
|
235
|
+
)
|
|
231
236
|
except httpx.TimeoutException:
|
|
232
237
|
return _failure("timeout", "API timeout; write outcome may be unknown. Reuse the same idempotency key for any send retry.", retryable=True)
|
|
233
238
|
except httpx.RequestError:
|
|
@@ -545,11 +550,8 @@ def build_server() -> MCPServer:
|
|
|
545
550
|
|
|
546
551
|
|
|
547
552
|
def main() -> int: # pragma: no cover - process entry point
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
except MissingApiKey as exc:
|
|
551
|
-
print(f"daknode-mcp: {exc}", file=sys.stderr)
|
|
552
|
-
return 2
|
|
553
|
+
# Local discovery exposes static schemas only. Each operation still calls
|
|
554
|
+
# _client(), which refuses absent credentials before constructing transport.
|
|
553
555
|
build_server().run()
|
|
554
556
|
return 0
|
|
555
557
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Credential-free local metadata; operations and remote access stay protected."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from mcp import ClientSession, StdioServerParameters
|
|
10
|
+
from mcp.client.stdio import stdio_client
|
|
11
|
+
|
|
12
|
+
from mcp_server import server
|
|
13
|
+
|
|
14
|
+
ID = "11111111-1111-4111-8111-111111111111"
|
|
15
|
+
IDS = {"inbox_id": ID, "event_id": ID}
|
|
16
|
+
WINDOW = {"start": "2030-11-04T09:00:00Z", "end": "2030-11-04T17:00:00Z"}
|
|
17
|
+
EVENT = {"title": "Synthetic test", "timezone": "UTC", "starts_at": WINDOW["start"], "ends_at": WINDOW["end"]}
|
|
18
|
+
ARGS = {
|
|
19
|
+
"create_inbox": {"alias": "synthetic"},
|
|
20
|
+
"list_threads": {"inbox_id": ID}, "list_messages": {"inbox_id": ID},
|
|
21
|
+
"get_thread": {"thread_id": ID}, "get_message": {"message_id": ID},
|
|
22
|
+
"search": {"query": "private-input-sentinel"},
|
|
23
|
+
"send": {"inbox_id": ID, "to": "synthetic@example.test", "subject": "Synthetic", "body_text": "No send", "idempotency_key": "synthetic-operation"},
|
|
24
|
+
"get_calendar_event": IDS,
|
|
25
|
+
"create_calendar_event": {"inbox_id": ID, "event": EVENT, "idempotency_key": "synthetic-operation"},
|
|
26
|
+
"update_calendar_event": {**IDS, "revision": 1, "patch": {"description": "Synthetic"}},
|
|
27
|
+
"cancel_calendar_event": {**IDS, "revision": 1},
|
|
28
|
+
"respond_calendar_event": {**IDS, "revision": 1, "response": "accepted"},
|
|
29
|
+
"send_calendar_invitations": {**IDS, "revision": 1, "idempotency_key": "synthetic-operation"},
|
|
30
|
+
"list_calendar_events": {"inbox_id": ID},
|
|
31
|
+
"get_calendar_agenda": {"inbox_id": ID, **WINDOW},
|
|
32
|
+
"get_calendar_free_busy": {"inbox_id": ID, "request": WINDOW},
|
|
33
|
+
"get_calendar_availability": {"inbox_id": ID, "request": {**WINDOW, "timezone": "UTC", "working_hours": [{"weekday": 0, "start": "09:00", "end": "17:00"}], "duration_minutes": 30, "step_minutes": 15}},
|
|
34
|
+
"book_calendar_event": {"inbox_id": ID, "booking": {"event": EVENT}, "idempotency_key": "synthetic-operation"},
|
|
35
|
+
"get_calendar_reminders": IDS,
|
|
36
|
+
"set_calendar_reminders": {**IDS, "revision": 1, "minutes_before_start": [15]},
|
|
37
|
+
"edit_calendar_series": {**IDS, "revision": 1, "edit": {"scope": "series", "changes": {"description": "Synthetic"}}, "idempotency_key": "synthetic-operation"},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def assert_setup_refusal(result):
|
|
42
|
+
assert result.is_error, result
|
|
43
|
+
payload = json.loads(result.content[0].text)
|
|
44
|
+
assert payload["error"]["code"] == "authentication_required"
|
|
45
|
+
assert "https://console.daknode.com/sign-up" in payload["error"]["message"]
|
|
46
|
+
assert "privately" in payload["error"]["message"]
|
|
47
|
+
assert "private-input-sentinel" not in result.content[0].text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_no_new_tool_or_fake_prompt_is_introduced(monkeypatch):
|
|
51
|
+
monkeypatch.delenv("DAKNODE_API_KEY", raising=False)
|
|
52
|
+
def forbidden(*args, **kwargs):
|
|
53
|
+
pytest.fail("Discovery constructed an authenticated client")
|
|
54
|
+
monkeypatch.setattr(server, "_client", forbidden)
|
|
55
|
+
async def run():
|
|
56
|
+
adapter = server.build_server()
|
|
57
|
+
assert {tool.name for tool in await adapter.list_tools()} == set(ARGS) == server.TOOL_NAMES
|
|
58
|
+
assert await adapter.list_prompts() == []
|
|
59
|
+
asyncio.run(run())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.parametrize("name", sorted(ARGS))
|
|
63
|
+
@pytest.mark.parametrize("key", [None, "", " \t\n"])
|
|
64
|
+
def test_every_valid_operation_refuses_missing_credentials_before_transport(monkeypatch, name, key):
|
|
65
|
+
if key is None:
|
|
66
|
+
monkeypatch.delenv("DAKNODE_API_KEY", raising=False)
|
|
67
|
+
else:
|
|
68
|
+
monkeypatch.setenv("DAKNODE_API_KEY", key)
|
|
69
|
+
def forbidden(*args, **kwargs):
|
|
70
|
+
pytest.fail("No key must mean no HTTP transport")
|
|
71
|
+
monkeypatch.setattr(server, "BoundedTransport", forbidden)
|
|
72
|
+
result = asyncio.run(server.build_server().call_tool(name, ARGS[name]))
|
|
73
|
+
assert_setup_refusal(result)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_main_does_not_construct_a_client_or_write_protocol_noise(monkeypatch, capsys):
|
|
77
|
+
monkeypatch.delenv("DAKNODE_API_KEY", raising=False)
|
|
78
|
+
ran = []
|
|
79
|
+
class Adapter:
|
|
80
|
+
def run(self):
|
|
81
|
+
ran.append(True)
|
|
82
|
+
monkeypatch.setattr(server, "build_server", Adapter)
|
|
83
|
+
monkeypatch.setattr(server, "_client", lambda: pytest.fail("Startup must not build an operation client"))
|
|
84
|
+
assert server.main() == 0
|
|
85
|
+
assert ran == [True]
|
|
86
|
+
captured = capsys.readouterr()
|
|
87
|
+
assert captured.out == captured.err == ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_actual_stdio_initialize_discovery_and_refusal_without_key(monkeypatch):
|
|
91
|
+
monkeypatch.delenv("DAKNODE_API_KEY", raising=False)
|
|
92
|
+
# A real child process, not the already-installed public0.3.4 entry point.
|
|
93
|
+
# Deny sockets so this can never hit a real API even if the guard regresses.
|
|
94
|
+
bootstrap = (
|
|
95
|
+
"import runpy,socket; "
|
|
96
|
+
"socket.socket.connect=lambda *a,**k: (_ for _ in ()).throw(RuntimeError('Network forbidden in discovery test')); "
|
|
97
|
+
"runpy.run_module('mcp_server.server',run_name='__main__')"
|
|
98
|
+
)
|
|
99
|
+
params = StdioServerParameters(command=sys.executable, args=["-c", bootstrap],
|
|
100
|
+
cwd=str(Path(__file__).resolve().parents[2]), env={"PATH": os.defpath})
|
|
101
|
+
async def run():
|
|
102
|
+
async with stdio_client(params) as (reader, writer):
|
|
103
|
+
async with ClientSession(reader, writer) as client:
|
|
104
|
+
await client.initialize()
|
|
105
|
+
tools = await client.list_tools()
|
|
106
|
+
assert {tool.name for tool in tools.tools} == server.TOOL_NAMES
|
|
107
|
+
assert (await client.list_prompts()).prompts == []
|
|
108
|
+
for name in ("list_messages", "send", "book_calendar_event"):
|
|
109
|
+
assert_setup_refusal(await client.call_tool(name, ARGS[name]))
|
|
110
|
+
assert len((await client.list_tools()).tools) == 21
|
|
111
|
+
asyncio.run(run())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.parametrize("status", [200, 403])
|
|
115
|
+
def test_actual_stdio_first_read_uses_authenticated_sdk_and_preserves_refusal(status):
|
|
116
|
+
"""Real MCP wire and generated client, synthetic HTTP only, no sockets."""
|
|
117
|
+
bootstrap = f'''
|
|
118
|
+
import httpx, socket
|
|
119
|
+
from mcp_server import server
|
|
120
|
+
socket.socket.connect = lambda *a, **k: (_ for _ in ()).throw(RuntimeError("Network forbidden"))
|
|
121
|
+
def handle(request):
|
|
122
|
+
assert request.method == "GET"
|
|
123
|
+
assert str(request.url) == "https://api.daknode.com/v1/inboxes/{ID}/messages?limit=5"
|
|
124
|
+
assert request.headers["authorization"] == "Bearer synthetic-onboarding-only"
|
|
125
|
+
assert request.headers["accept-encoding"] == "identity"
|
|
126
|
+
return httpx.Response({status}, json={{"items": [], "has_more": False, "next_cursor": None}} if {status} == 200 else {{"error": {{"code": "forbidden"}}}})
|
|
127
|
+
server.BoundedTransport = lambda: httpx.MockTransport(handle)
|
|
128
|
+
server.main()
|
|
129
|
+
'''
|
|
130
|
+
params = StdioServerParameters(command=sys.executable, args=["-c", bootstrap],
|
|
131
|
+
cwd=str(Path(__file__).resolve().parents[2]),
|
|
132
|
+
env={"PATH": os.defpath, "DAKNODE_API_KEY": "synthetic-onboarding-only"})
|
|
133
|
+
|
|
134
|
+
async def run():
|
|
135
|
+
async with stdio_client(params) as (reader, writer):
|
|
136
|
+
async with ClientSession(reader, writer) as client:
|
|
137
|
+
await client.initialize()
|
|
138
|
+
assert "list_messages" in {tool.name for tool in (await client.list_tools()).tools}
|
|
139
|
+
result = await client.call_tool("list_messages", {"inbox_id": ID, "limit": 5})
|
|
140
|
+
text = " ".join(block.text for block in result.content if hasattr(block, "text"))
|
|
141
|
+
assert "synthetic-onboarding-only" not in text
|
|
142
|
+
if status == 200:
|
|
143
|
+
assert not result.is_error
|
|
144
|
+
assert json.loads(text)["items"] == []
|
|
145
|
+
else:
|
|
146
|
+
assert result.is_error
|
|
147
|
+
assert "403" in text
|
|
148
|
+
asyncio.run(run())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|