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:
+60
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user