keep-mcp 2.2.1__tar.gz → 2.2.2__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.
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/PKG-INFO +1 -1
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/pyproject.toml +1 -1
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/src/server/cli.py +20 -19
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/src/server/keep_api.py +6 -3
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/tests/test_checklist_validation.py +3 -2
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/tests/test_cli.py +23 -16
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/tests/test_keep_client.py +15 -5
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.agents/skills/keep-mcp-evidence/SKILL.md +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.agents/skills/keep-mcp-evidence/references/cli-runs.md +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.agents/skills/keep-mcp-evidence/references/evidence-template.md +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.agents/skills/keep-mcp-evidence/references/scenarios.md +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.example.env +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.github/pull_request_template.md +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.github/workflows/ci.yml +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.github/workflows/release.yml +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.gitignore +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/.python-version +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/CONTRIBUTING.md +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/LICENSE +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/Makefile +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/README.md +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/scripts/smoke_test.py +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/src/server/__init__.py +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/src/server/__main__.py +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/tests/conftest.py +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/tests/test_keep_api.py +0 -0
- {keep_mcp-2.2.1 → keep_mcp-2.2.2}/tests/test_main_module.py +0 -0
|
@@ -13,6 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
import gkeepapi
|
|
15
15
|
from mcp.server import MCPServer
|
|
16
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
16
17
|
|
|
17
18
|
from .keep_api import (
|
|
18
19
|
KEEP_MCP_LABEL,
|
|
@@ -34,13 +35,13 @@ def _get_note_or_raise(note_id: str):
|
|
|
34
35
|
keep = get_client()
|
|
35
36
|
note = keep.get(note_id)
|
|
36
37
|
if not note:
|
|
37
|
-
raise
|
|
38
|
+
raise ToolError(f"Note with ID {note_id} not found")
|
|
38
39
|
return keep, note
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def _ensure_modifiable(note):
|
|
42
43
|
if not can_modify_note(note):
|
|
43
|
-
raise
|
|
44
|
+
raise ToolError(
|
|
44
45
|
f"Note with ID {note.id} cannot be modified "
|
|
45
46
|
"(missing keep-mcp label and UNSAFE_MODE is not enabled)"
|
|
46
47
|
)
|
|
@@ -55,7 +56,7 @@ def _normalize_colors(colors: list[str] | None):
|
|
|
55
56
|
try:
|
|
56
57
|
normalized_colors.append(gkeepapi.node.ColorValue(color))
|
|
57
58
|
except ValueError as exc:
|
|
58
|
-
raise
|
|
59
|
+
raise ToolError(f"Invalid color '{color}'") from exc
|
|
59
60
|
|
|
60
61
|
return normalized_colors
|
|
61
62
|
|
|
@@ -66,7 +67,7 @@ def _parse_utc(value: str | None, param: str) -> datetime | None:
|
|
|
66
67
|
try:
|
|
67
68
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
68
69
|
except ValueError as exc:
|
|
69
|
-
raise
|
|
70
|
+
raise ToolError(
|
|
70
71
|
f"Invalid {param} '{value}': expected ISO 8601, "
|
|
71
72
|
"e.g. 2026-07-29 or 2026-07-29T12:00:00Z"
|
|
72
73
|
) from exc
|
|
@@ -199,7 +200,7 @@ def create_list(title: str | None = None, items: list[dict[str, Any]] | None = N
|
|
|
199
200
|
if items:
|
|
200
201
|
for item in items:
|
|
201
202
|
if not isinstance(item.get("checked", False), bool):
|
|
202
|
-
raise
|
|
203
|
+
raise ToolError("Each item's checked field must be a boolean")
|
|
203
204
|
formatted_items = [
|
|
204
205
|
(item.get("text", ""), item.get("checked", False)) for item in items
|
|
205
206
|
]
|
|
@@ -223,7 +224,7 @@ def add_list_item(note_id: str, text: str, checked: bool = False) -> str:
|
|
|
223
224
|
_ensure_modifiable(note)
|
|
224
225
|
|
|
225
226
|
if not isinstance(note, gkeepapi.node.List):
|
|
226
|
-
raise
|
|
227
|
+
raise ToolError(f"Note with ID {note_id} is not a list")
|
|
227
228
|
|
|
228
229
|
item = note.add(text=text, checked=checked)
|
|
229
230
|
keep.sync()
|
|
@@ -238,11 +239,11 @@ def update_list_item(note_id: str, item_id: str, text: str | None = None, checke
|
|
|
238
239
|
_ensure_modifiable(note)
|
|
239
240
|
|
|
240
241
|
if not isinstance(note, gkeepapi.node.List):
|
|
241
|
-
raise
|
|
242
|
+
raise ToolError(f"Note with ID {note_id} is not a list")
|
|
242
243
|
|
|
243
244
|
item = note.get(item_id)
|
|
244
245
|
if not item:
|
|
245
|
-
raise
|
|
246
|
+
raise ToolError(f"List item with ID {item_id} not found")
|
|
246
247
|
|
|
247
248
|
if text is not None:
|
|
248
249
|
item.text = text
|
|
@@ -261,11 +262,11 @@ def delete_list_item(note_id: str, item_id: str) -> str:
|
|
|
261
262
|
_ensure_modifiable(note)
|
|
262
263
|
|
|
263
264
|
if not isinstance(note, gkeepapi.node.List):
|
|
264
|
-
raise
|
|
265
|
+
raise ToolError(f"Note with ID {note_id} is not a list")
|
|
265
266
|
|
|
266
267
|
item = note.get(item_id)
|
|
267
268
|
if not item:
|
|
268
|
-
raise
|
|
269
|
+
raise ToolError(f"List item with ID {item_id} not found")
|
|
269
270
|
|
|
270
271
|
item.delete()
|
|
271
272
|
keep.sync()
|
|
@@ -280,7 +281,7 @@ def update_note(note_id: str, title: str | None = None, text: str | None = None)
|
|
|
280
281
|
_ensure_modifiable(note)
|
|
281
282
|
|
|
282
283
|
if text is not None and isinstance(note, gkeepapi.node.List):
|
|
283
|
-
raise
|
|
284
|
+
raise ToolError("Cannot replace checklist text: use the checklist item tools")
|
|
284
285
|
|
|
285
286
|
if title is not None:
|
|
286
287
|
note.title = title
|
|
@@ -301,7 +302,7 @@ def set_note_color(note_id: str, color: str) -> str:
|
|
|
301
302
|
try:
|
|
302
303
|
note.color = gkeepapi.node.ColorValue(color)
|
|
303
304
|
except ValueError as exc:
|
|
304
|
-
raise
|
|
305
|
+
raise ToolError(f"Invalid color '{color}'") from exc
|
|
305
306
|
|
|
306
307
|
keep.sync()
|
|
307
308
|
return json.dumps(serialize_note(note))
|
|
@@ -393,10 +394,10 @@ def delete_label(label_id: str) -> str:
|
|
|
393
394
|
keep = get_client()
|
|
394
395
|
label = keep.getLabel(label_id)
|
|
395
396
|
if not label:
|
|
396
|
-
raise
|
|
397
|
+
raise ToolError(f"Label with ID {label_id} not found")
|
|
397
398
|
if not is_unsafe_mode():
|
|
398
399
|
if label.name == KEEP_MCP_LABEL:
|
|
399
|
-
raise
|
|
400
|
+
raise ToolError(
|
|
400
401
|
f"Cannot delete the '{KEEP_MCP_LABEL}' label in safe mode: all notes managed "
|
|
401
402
|
"by this server would become permanently unmodifiable. Set UNSAFE_MODE=true to override."
|
|
402
403
|
)
|
|
@@ -405,7 +406,7 @@ def delete_label(label_id: str) -> str:
|
|
|
405
406
|
if any(lb.id == label_id for lb in n.labels.all()) and not has_keep_mcp_label(n)
|
|
406
407
|
]
|
|
407
408
|
if unmanaged:
|
|
408
|
-
raise
|
|
409
|
+
raise ToolError(
|
|
409
410
|
f"Cannot delete label '{label.name}' in safe mode: it is attached to "
|
|
410
411
|
f"{len(unmanaged)} unmanaged note(s). Deleting it would silently modify "
|
|
411
412
|
"those notes. Set UNSAFE_MODE=true to override."
|
|
@@ -424,7 +425,7 @@ def add_label_to_note(note_id: str, label_id: str) -> str:
|
|
|
424
425
|
|
|
425
426
|
label = keep.getLabel(label_id)
|
|
426
427
|
if not label:
|
|
427
|
-
raise
|
|
428
|
+
raise ToolError(f"Label with ID {label_id} not found")
|
|
428
429
|
|
|
429
430
|
note.labels.add(label)
|
|
430
431
|
keep.sync()
|
|
@@ -440,9 +441,9 @@ def remove_label_from_note(note_id: str, label_id: str) -> str:
|
|
|
440
441
|
|
|
441
442
|
label = keep.getLabel(label_id)
|
|
442
443
|
if not label:
|
|
443
|
-
raise
|
|
444
|
+
raise ToolError(f"Label with ID {label_id} not found")
|
|
444
445
|
if label.name == KEEP_MCP_LABEL and not is_unsafe_mode():
|
|
445
|
-
raise
|
|
446
|
+
raise ToolError(
|
|
446
447
|
f"Cannot remove the '{KEEP_MCP_LABEL}' label in safe mode: the note would "
|
|
447
448
|
"become permanently unmodifiable. Set UNSAFE_MODE=true to override."
|
|
448
449
|
)
|
|
@@ -519,7 +520,7 @@ def download_media(note_id: str, dest_dir: str, blob_id: str | None = None) -> s
|
|
|
519
520
|
|
|
520
521
|
blobs = [blob for blob in note.blobs if blob_id is None or blob.id == blob_id]
|
|
521
522
|
if blob_id is not None and not blobs:
|
|
522
|
-
raise
|
|
523
|
+
raise ToolError(f"Blob with ID {blob_id} not found on note {note_id}")
|
|
523
524
|
|
|
524
525
|
os.makedirs(dest_dir, exist_ok=True)
|
|
525
526
|
|
|
@@ -5,6 +5,7 @@ from threading import RLock
|
|
|
5
5
|
import gkeepapi
|
|
6
6
|
import requests
|
|
7
7
|
from dotenv import load_dotenv
|
|
8
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
8
9
|
|
|
9
10
|
KEEP_MCP_LABEL = "keep-mcp"
|
|
10
11
|
|
|
@@ -20,6 +21,8 @@ def keep_operation(func):
|
|
|
20
21
|
with _client_lock:
|
|
21
22
|
try:
|
|
22
23
|
return func(*args, **kwargs)
|
|
24
|
+
except ToolError:
|
|
25
|
+
raise
|
|
23
26
|
except Exception:
|
|
24
27
|
# A failed operation can leave unsaved or prematurely cleaned
|
|
25
28
|
# mutations in memory. Reload remote state on the next call.
|
|
@@ -51,7 +54,7 @@ def get_client():
|
|
|
51
54
|
master_token = os.getenv('GOOGLE_MASTER_TOKEN')
|
|
52
55
|
|
|
53
56
|
if not email or not master_token:
|
|
54
|
-
raise
|
|
57
|
+
raise ToolError("Missing Google Keep credentials. Please set GOOGLE_EMAIL and GOOGLE_MASTER_TOKEN environment variables.")
|
|
55
58
|
|
|
56
59
|
# Initialize the Keep API
|
|
57
60
|
keep = gkeepapi.Keep()
|
|
@@ -60,7 +63,7 @@ def get_client():
|
|
|
60
63
|
try:
|
|
61
64
|
keep.authenticate(email, master_token)
|
|
62
65
|
except requests.exceptions.JSONDecodeError as exc:
|
|
63
|
-
raise
|
|
66
|
+
raise ToolError(
|
|
64
67
|
"Google Keep API returned a non-JSON response during authentication. "
|
|
65
68
|
"This usually means the unofficial Keep API (notes/v1) is inaccessible "
|
|
66
69
|
"from this environment (HTTP 403/4xx). "
|
|
@@ -68,7 +71,7 @@ def get_client():
|
|
|
68
71
|
"is reachable from this network."
|
|
69
72
|
) from exc
|
|
70
73
|
except gkeepapi.exception.LoginException as exc:
|
|
71
|
-
raise
|
|
74
|
+
raise ToolError(
|
|
72
75
|
f"Google Keep login failed: {exc}. "
|
|
73
76
|
"Verify that GOOGLE_EMAIL and GOOGLE_MASTER_TOKEN are correct."
|
|
74
77
|
) from exc
|
|
@@ -5,6 +5,7 @@ from unittest.mock import Mock
|
|
|
5
5
|
|
|
6
6
|
import gkeepapi
|
|
7
7
|
import pytest
|
|
8
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
8
9
|
|
|
9
10
|
from server import cli
|
|
10
11
|
|
|
@@ -20,7 +21,7 @@ def offline_keep(monkeypatch):
|
|
|
20
21
|
|
|
21
22
|
@pytest.mark.parametrize("checked", ["false", "true", 0, 1, None])
|
|
22
23
|
def test_create_list_rejects_non_boolean_checked_without_creating_note(offline_keep, checked):
|
|
23
|
-
with pytest.raises(
|
|
24
|
+
with pytest.raises(ToolError, match="checked.*boolean"):
|
|
24
25
|
cli.create_list("Tasks", [{"text": "Buy milk", "checked": checked}])
|
|
25
26
|
assert list(offline_keep.all()) == []
|
|
26
27
|
offline_keep.sync.assert_not_called()
|
|
@@ -40,7 +41,7 @@ def test_create_list_preserves_boolean_checked_and_default(offline_keep):
|
|
|
40
41
|
def test_update_list_text_does_not_partially_change_title(offline_keep):
|
|
41
42
|
note = offline_keep.createList("Original", [("Buy milk", False)])
|
|
42
43
|
note.labels.add(offline_keep.createLabel("keep-mcp"))
|
|
43
|
-
with pytest.raises(
|
|
44
|
+
with pytest.raises(ToolError, match="checklist.*item"):
|
|
44
45
|
cli.update_note(note.id, title="Changed", text="Replacement")
|
|
45
46
|
assert note.title == "Original"
|
|
46
47
|
assert [item.text for item in note.items] == ["Buy milk"]
|
|
@@ -3,7 +3,9 @@ import re
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from types import SimpleNamespace
|
|
5
5
|
|
|
6
|
+
import anyio
|
|
6
7
|
import pytest
|
|
8
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
7
9
|
|
|
8
10
|
from server import cli
|
|
9
11
|
|
|
@@ -253,7 +255,7 @@ def test_find_date_filter_func(keep):
|
|
|
253
255
|
|
|
254
256
|
|
|
255
257
|
def test_find_invalid_date_raises(keep):
|
|
256
|
-
with pytest.raises(
|
|
258
|
+
with pytest.raises(ToolError, match="created_after"):
|
|
257
259
|
cli.find(created_after="not-a-date")
|
|
258
260
|
|
|
259
261
|
|
|
@@ -268,7 +270,7 @@ def test_find_invalid_color_raises(keep, monkeypatch):
|
|
|
268
270
|
raise ValueError("bad")
|
|
269
271
|
|
|
270
272
|
monkeypatch.setattr(cli.gkeepapi.node, "ColorValue", bad_color)
|
|
271
|
-
with pytest.raises(
|
|
273
|
+
with pytest.raises(ToolError, match="Invalid color 'invalid'"):
|
|
272
274
|
cli.find(colors=["invalid"])
|
|
273
275
|
|
|
274
276
|
|
|
@@ -310,8 +312,13 @@ def test_update_note_updates_fields(keep):
|
|
|
310
312
|
assert data["text"] == "changed"
|
|
311
313
|
|
|
312
314
|
|
|
315
|
+
def test_anticipated_errors_reach_the_client(keep):
|
|
316
|
+
with pytest.raises(ToolError, match="Note with ID missing not found"):
|
|
317
|
+
anyio.run(cli.mcp.call_tool, "get_note", {"note_id": "missing"})
|
|
318
|
+
|
|
319
|
+
|
|
313
320
|
def test_update_note_not_found_raises(keep):
|
|
314
|
-
with pytest.raises(
|
|
321
|
+
with pytest.raises(ToolError, match="not found"):
|
|
315
322
|
cli.update_note("missing", title="x")
|
|
316
323
|
|
|
317
324
|
|
|
@@ -325,12 +332,12 @@ def test_list_item_roundtrip(keep):
|
|
|
325
332
|
|
|
326
333
|
|
|
327
334
|
def test_update_list_item_missing_item_raises(keep):
|
|
328
|
-
with pytest.raises(
|
|
335
|
+
with pytest.raises(ToolError, match="not found"):
|
|
329
336
|
cli.update_list_item("list1", "missing", text="x")
|
|
330
337
|
|
|
331
338
|
|
|
332
339
|
def test_delete_list_item_paths(keep):
|
|
333
|
-
with pytest.raises(
|
|
340
|
+
with pytest.raises(ToolError, match="not found"):
|
|
334
341
|
cli.delete_list_item("list1", "missing")
|
|
335
342
|
|
|
336
343
|
new_item_id = json.loads(cli.add_list_item("list1", "task"))["item_id"]
|
|
@@ -339,11 +346,11 @@ def test_delete_list_item_paths(keep):
|
|
|
339
346
|
|
|
340
347
|
|
|
341
348
|
def test_list_item_requires_list_type(keep):
|
|
342
|
-
with pytest.raises(
|
|
349
|
+
with pytest.raises(ToolError, match="not a list"):
|
|
343
350
|
cli.add_list_item("n1", "x")
|
|
344
|
-
with pytest.raises(
|
|
351
|
+
with pytest.raises(ToolError, match="not a list"):
|
|
345
352
|
cli.update_list_item("n1", "i1", text="x")
|
|
346
|
-
with pytest.raises(
|
|
353
|
+
with pytest.raises(ToolError, match="not a list"):
|
|
347
354
|
cli.delete_list_item("n1", "i1")
|
|
348
355
|
|
|
349
356
|
|
|
@@ -352,7 +359,7 @@ def test_set_note_color_validates(keep, monkeypatch):
|
|
|
352
359
|
raise ValueError("bad")
|
|
353
360
|
|
|
354
361
|
monkeypatch.setattr(cli.gkeepapi.node, "ColorValue", bad_color)
|
|
355
|
-
with pytest.raises(
|
|
362
|
+
with pytest.raises(ToolError, match="Invalid color"):
|
|
356
363
|
cli.set_note_color("n1", "invalid")
|
|
357
364
|
|
|
358
365
|
|
|
@@ -378,17 +385,17 @@ def test_label_crud_and_missing_label_errors(keep):
|
|
|
378
385
|
message = json.loads(cli.delete_label(created["id"]))
|
|
379
386
|
assert "marked for deletion" in message["message"]
|
|
380
387
|
|
|
381
|
-
with pytest.raises(
|
|
388
|
+
with pytest.raises(ToolError, match="Label with ID bad not found"):
|
|
382
389
|
cli.delete_label("bad")
|
|
383
|
-
with pytest.raises(
|
|
390
|
+
with pytest.raises(ToolError, match="Label with ID bad not found"):
|
|
384
391
|
cli.add_label_to_note("n1", "bad")
|
|
385
|
-
with pytest.raises(
|
|
392
|
+
with pytest.raises(ToolError, match="Label with ID bad not found"):
|
|
386
393
|
cli.remove_label_from_note("n1", "bad")
|
|
387
394
|
|
|
388
395
|
|
|
389
396
|
def test_delete_label_safe_mode_guards(keep, monkeypatch):
|
|
390
397
|
# Deleting the keep-mcp label is blocked in safe mode.
|
|
391
|
-
with pytest.raises(
|
|
398
|
+
with pytest.raises(ToolError, match="keep-mcp"):
|
|
392
399
|
cli.delete_label("l1")
|
|
393
400
|
|
|
394
401
|
# Deleting a label that is on an unmanaged note is also blocked in safe mode.
|
|
@@ -397,7 +404,7 @@ def test_delete_label_safe_mode_guards(keep, monkeypatch):
|
|
|
397
404
|
keep._labels["shared"] = shared
|
|
398
405
|
unmanaged.labels.add(shared)
|
|
399
406
|
keep.notes["unmanaged"] = unmanaged
|
|
400
|
-
with pytest.raises(
|
|
407
|
+
with pytest.raises(ToolError, match="unmanaged"):
|
|
401
408
|
cli.delete_label("shared")
|
|
402
409
|
|
|
403
410
|
# UNSAFE_MODE bypasses both guards.
|
|
@@ -441,14 +448,14 @@ def test_download_media_writes_files(keep, monkeypatch, tmp_path):
|
|
|
441
448
|
|
|
442
449
|
|
|
443
450
|
def test_download_media_unknown_blob(keep, tmp_path):
|
|
444
|
-
with pytest.raises(
|
|
451
|
+
with pytest.raises(ToolError, match="Blob with ID nope not found"):
|
|
445
452
|
cli.download_media("n1", str(tmp_path), blob_id="nope")
|
|
446
453
|
|
|
447
454
|
|
|
448
455
|
def test_modification_guard_blocks_when_unlabeled(keep, monkeypatch):
|
|
449
456
|
keep.notes["n1"].labels = DummyLabels()
|
|
450
457
|
monkeypatch.setattr(cli, "can_modify_note", lambda _: False)
|
|
451
|
-
with pytest.raises(
|
|
458
|
+
with pytest.raises(ToolError, match="cannot be modified"):
|
|
452
459
|
cli.update_note("n1", title="x")
|
|
453
460
|
|
|
454
461
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
2
3
|
|
|
3
4
|
from server import keep_api
|
|
4
5
|
|
|
@@ -38,12 +39,21 @@ def test_get_client_raises_when_missing_credentials(monkeypatch):
|
|
|
38
39
|
monkeypatch.setattr(keep_api, "load_dotenv", lambda: None)
|
|
39
40
|
monkeypatch.setattr(keep_api.os, "getenv", lambda _key: None)
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
with pytest.raises(ToolError, match="Missing Google Keep credentials"):
|
|
42
43
|
keep_api.get_client()
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_tool_error_keeps_the_client(monkeypatch):
|
|
47
|
+
client = object()
|
|
48
|
+
monkeypatch.setattr(keep_api, "_keep_client", client)
|
|
49
|
+
|
|
50
|
+
@keep_api.keep_operation
|
|
51
|
+
def reject():
|
|
52
|
+
raise ToolError("rejected")
|
|
53
|
+
|
|
54
|
+
with pytest.raises(ToolError):
|
|
55
|
+
reject()
|
|
56
|
+
assert keep_api._keep_client is client
|
|
47
57
|
|
|
48
58
|
|
|
49
59
|
def test_cached_client_refreshes_external_changes(monkeypatch):
|
|
File without changes
|
|
File without changes
|
{keep_mcp-2.2.1 → keep_mcp-2.2.2}/.agents/skills/keep-mcp-evidence/references/evidence-template.md
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|