Joplin via Data API + Mail attachments

Notes server rewritten to use Joplin CLI Data API (joplin-cli sync
clients on host, ports 41184/41185). Clean fast search, proper
resource handling. New tools: list_note_resources, read_resource
(attachments as inline image/document/text).

Mail server: read_mail now lists attachments; new read_attachment
tool returns images inline, PDFs/docs as EmbeddedResource, text directly.

Tests: 54 total. Notes now real (notebooks, list, create+read).
Mail attachment listing + fetch tested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Stefan Lohmaier
2026-06-12 12:26:26 +02:00
parent 38cf147eec
commit c06e6d6b4c
4 changed files with 290 additions and 52 deletions
+60 -2
View File
@@ -167,6 +167,39 @@ class TestMail:
assert "From:" in body
assert "Subject:" in body
def test_attachment_listing_and_fetch(self):
token = get_token(self.PORT)
# Find a mail with an attachment by scanning Rechnung search results
text = tool_call(self.PORT, token, "search_mail",
{"query": "Rechnung", "limit": 10, "account": "d370128_0-slohmaier"})
if "No results" in text:
pytest.skip("No mails found")
# Read each result, look for one with attachments
found_att = None
for block in text.split("\n\n"):
acc = fld = k = None
for line in block.split("\n"):
if "Account:" in line:
parts = line.strip().split(", ")
acc = parts[0].split(": ")[1]
fld = parts[1].split(": ")[1]
k = parts[2].split(": ")[1]
if not k:
continue
body = tool_call(self.PORT, token, "read_mail", {"account": acc, "folder": fld, "key": k})
if "Anhaenge" in body:
found_att = (acc, fld, k)
break
if not found_att:
pytest.skip("No mail with attachment found")
acc, fld, k = found_att
result = mcp_call(self.PORT, token, "tools/call", {
"name": "read_attachment",
"arguments": {"account": acc, "folder": fld, "key": k, "attachment_index": 1},
})
content = result["result"]["content"][0]
assert content["type"] in ("image", "resource", "text")
# ============================================================
# Calendar Tests (CRUD on calendar-test)
@@ -354,12 +387,37 @@ class TestFiles:
# ============================================================
# Notes Tests (Joplin — likely no token configured)
# Notes Tests (Joplin Data API)
# ============================================================
class TestNotes:
PORT = SERVERS["notes"]
TEST_NOTEBOOK = "Inbox"
def test_list_notebooks(self):
text = tool_call(self.PORT, get_token(self.PORT), "list_notebooks")
assert len(text) > 0 # Either notebooks or error about token
assert "id:" in text and "Keine" not in text
def test_list_notes(self):
text = tool_call(self.PORT, get_token(self.PORT), "list_notes", {"limit": 5})
assert "id:" in text or "Keine Notizen" in text
def test_create_search_read_note(self):
token = get_token(self.PORT)
tag = f"mcptest-{int(time.time())}"
# Create
result = tool_call(self.PORT, token, "create_note", {
"notebook": self.TEST_NOTEBOOK,
"title": f"Test Note {tag}",
"body": f"Automatischer Test {tag}\n\nInhalt.",
})
assert "erstellt" in result.lower(), f"Create failed: {result}"
note_id = result.split("id: ")[1].rstrip(")").strip()
# Read back (search FTS index may lag, so verify via read)
body = tool_call(self.PORT, token, "read_note", {"note_id": note_id})
assert tag in body, f"Note content mismatch: {body[:200]}"
# Verify it appears in the notebook listing
listing = tool_call(self.PORT, token, "list_notes", {"notebook": self.TEST_NOTEBOOK})
assert note_id in listing or f"Test Note {tag}" in listing