Skip to content

Commit b818a19

Browse files
rodrigopazTechRodrigo Cristobal Paz Jimenezjacob-bd
authored
feat: Add support for native NotebookLM collections
* feat: add support for native NotebookLM collections * fix: resolve collection PR checks --------- Co-authored-by: Rodrigo Cristobal Paz Jimenez <rodrigo.paz@telecommx.gob.mx> Co-authored-by: jacob-bd <47641890+jacob-bd@users.noreply.github.com>
1 parent 062a6c9 commit b818a19

7 files changed

Lines changed: 367 additions & 0 deletions

File tree

src/notebooklm_tools/core/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Any
1313

1414
from . import constants
15+
from .collections import CollectionsMixin
1516
from .conversation import ConversationMixin
1617
from .download import DownloadMixin
1718

@@ -51,6 +52,7 @@ class NotebookLMClient(
5152
NotebookMixin,
5253
NotesMixin,
5354
LabelsMixin,
55+
CollectionsMixin,
5456
):
5557
"""Client for NotebookLM MCP internal API.
5658
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""CollectionsMixin - Native NotebookLM collections management operations."""
2+
3+
import logging
4+
import time
5+
6+
from .base import BaseClient
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class CollectionsMixin(BaseClient):
12+
"""Mixin for native NotebookLM collections management operations."""
13+
14+
def _get_collections_header(self) -> list:
15+
"""Returns the standard header for collections RPC operations."""
16+
return [2, None, [1], [1, None, None, None, None, None, None, None, None, None, [1, 3]]]
17+
18+
def list_collections(self) -> list[dict]:
19+
"""Lists all collections by temporarily creating a hidden one and deleting it.
20+
21+
Returns:
22+
List of collections, where each collection is a dict with:
23+
- id: str
24+
- name: str
25+
- notebook_ids: list[str]
26+
- emoji: str
27+
"""
28+
header = self._get_collections_header()
29+
temp_name = f"_temp_list_query_{int(time.time())}"
30+
31+
# 1. Create a temporary collection to force Google to return the list
32+
params_create = [header, None, None, None, None, [[temp_name], None, []], 3]
33+
34+
result = self._call_rpc("agX4Bc", params_create)
35+
36+
collections = []
37+
temp_id = None
38+
39+
if result and len(result) > 2 and isinstance(result[2], list):
40+
for col in result[2]:
41+
if not isinstance(col, list) or len(col) < 3:
42+
continue
43+
44+
col_name = col[0] or ""
45+
col_notebooks = col[1] or []
46+
col_id = col[2] or ""
47+
col_emoji = col[3] if len(col) > 3 else ""
48+
49+
if col_name == temp_name:
50+
temp_id = col_id
51+
continue
52+
53+
collections.append(
54+
{
55+
"id": col_id,
56+
"name": col_name,
57+
"notebook_ids": col_notebooks,
58+
"emoji": col_emoji,
59+
}
60+
)
61+
62+
# 2. Delete the temporary collection in the background
63+
if temp_id:
64+
try:
65+
self.delete_collection(temp_id)
66+
except Exception as e:
67+
logger.warning(f"Failed to cleanup temporary listing collection {temp_id}: {e}")
68+
69+
return collections
70+
71+
def create_collection(self, name: str, notebook_ids: list[str] = None) -> dict:
72+
"""Creates a new collection with the given name and notebooks.
73+
74+
Args:
75+
name: Name of the collection
76+
notebook_ids: List of notebook UUIDs to include
77+
78+
Returns:
79+
The created collection dict.
80+
"""
81+
header = self._get_collections_header()
82+
notebooks = notebook_ids or []
83+
84+
params = [header, None, None, None, None, [[name], None, notebooks], 3]
85+
86+
result = self._call_rpc("agX4Bc", params)
87+
88+
# Find the newly created collection in the returned list
89+
if result and len(result) > 2 and isinstance(result[2], list):
90+
for col in result[2]:
91+
if isinstance(col, list) and len(col) >= 3 and col[0] == name:
92+
return {
93+
"id": col[2],
94+
"name": col[0],
95+
"notebook_ids": col[1] or [],
96+
"emoji": col[3] if len(col) > 3 else "",
97+
}
98+
99+
raise RuntimeError("Failed to confirm collection creation on Google backend")
100+
101+
def edit_collection(
102+
self, collection_id: str, name: str = None, notebook_ids: list[str] = None
103+
) -> bool:
104+
"""Edits an existing collection's name and/or notebooks.
105+
106+
Args:
107+
collection_id: UUID of the collection to edit
108+
name: New name (optional)
109+
notebook_ids: New complete list of notebook IDs (optional)
110+
"""
111+
header = self._get_collections_header()
112+
113+
# Format: [[None, None, None, [[notebook_ids]]], [[new_name]]]
114+
notebooks_payload = None
115+
if notebook_ids is not None:
116+
notebooks_payload = [None, None, None, [notebook_ids]]
117+
118+
name_payload = None
119+
if name is not None:
120+
name_payload = [[name]]
121+
122+
edit_payload = [notebooks_payload, name_payload]
123+
124+
params = [header, None, collection_id, edit_payload, 3]
125+
126+
result = self._call_rpc("le8sX", params)
127+
return result == [] or result is not None
128+
129+
def set_collection_emoji(self, collection_id: str, emoji: str) -> bool:
130+
"""Sets or clears the emoji marker on a collection (pass "" to clear)."""
131+
header = self._get_collections_header()
132+
params = [header, None, collection_id, [[[None, emoji]]], 3]
133+
result = self._call_rpc("le8sX", params)
134+
return result == [] or result is not None
135+
136+
def delete_collection(self, collection_id: str) -> bool:
137+
"""Permanently deletes a collection. Notebooks are not deleted."""
138+
header = self._get_collections_header()
139+
params = [header, None, [collection_id], 3]
140+
result = self._call_rpc("GyzE7e", params)
141+
return result == [] or result is not None

src/notebooklm_tools/mcp/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def _register_tools() -> None:
8383
batch,
8484
chat,
8585
chats,
86+
collections,
8687
cross_notebook,
8788
downloads,
8889
exports,

src/notebooklm_tools/mcp/tool_groups.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
"organization": {
6666
"label",
6767
"tag",
68+
"collection_list",
69+
"collection_create",
70+
"collection_edit",
71+
"collection_set_emoji",
72+
"collection_delete",
6873
},
6974
"automation": {
7075
"batch",

src/notebooklm_tools/mcp/tools/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
notebook_query_status,
1111
)
1212
from .chats import chat_export, chat_get, chat_list
13+
from .collections import (
14+
collection_create,
15+
collection_delete,
16+
collection_edit,
17+
collection_list,
18+
collection_set_emoji,
19+
)
1320
from .cross_notebook import cross_notebook_query
1421
from .downloads import download_all_artifacts, download_artifact
1522
from .exports import (
@@ -69,6 +76,12 @@
6976
"notebook_create",
7077
"notebook_rename",
7178
"notebook_delete",
79+
# Collections (5)
80+
"collection_list",
81+
"collection_create",
82+
"collection_edit",
83+
"collection_set_emoji",
84+
"collection_delete",
7285
# Sources (7)
7386
"source_add",
7487
"source_list_drive",
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Collection tools - native NotebookLM collections management operations."""
2+
3+
from ...services import ServiceError
4+
from ...services import collections as collections_service
5+
from ._utils import ResultDict, error_result, get_client, logged_tool
6+
7+
8+
@logged_tool()
9+
def collection_list() -> ResultDict:
10+
"""List all native collections."""
11+
try:
12+
client = get_client()
13+
result = collections_service.list_collections(client)
14+
return {"status": "success", **result}
15+
except ServiceError as e:
16+
return error_result(e.user_message, hint=e.hint)
17+
except Exception as e:
18+
return error_result(str(e))
19+
20+
21+
@logged_tool()
22+
def collection_create(name: str, notebook_ids: list[str] = None) -> ResultDict:
23+
"""Create a new collection.
24+
25+
Args:
26+
name: Name of the collection
27+
notebook_ids: List of notebook UUIDs to include in the collection (optional)
28+
"""
29+
try:
30+
client = get_client()
31+
result = collections_service.create_collection(client, name, notebook_ids)
32+
return {"status": "success", **result}
33+
except ServiceError as e:
34+
return error_result(e.user_message, hint=e.hint)
35+
except Exception as e:
36+
return error_result(str(e))
37+
38+
39+
@logged_tool()
40+
def collection_edit(
41+
collection_id: str, name: str = None, notebook_ids: list[str] = None
42+
) -> ResultDict:
43+
"""Edit an existing collection's name and/or list of notebooks.
44+
45+
Args:
46+
collection_id: UUID of the collection
47+
name: New name for the collection (optional)
48+
notebook_ids: New complete list of notebook UUIDs to include (optional)
49+
"""
50+
try:
51+
client = get_client()
52+
result = collections_service.edit_collection(client, collection_id, name, notebook_ids)
53+
return {"status": "success", **result}
54+
except ServiceError as e:
55+
return error_result(e.user_message, hint=e.hint)
56+
except Exception as e:
57+
return error_result(str(e))
58+
59+
60+
@logged_tool()
61+
def collection_set_emoji(collection_id: str, emoji: str) -> ResultDict:
62+
"""Set or clear the emoji marker on a collection.
63+
64+
Args:
65+
collection_id: UUID of the collection
66+
emoji: Emoji character (use empty string "" to clear)
67+
"""
68+
try:
69+
client = get_client()
70+
result = collections_service.set_collection_emoji(client, collection_id, emoji)
71+
return {"status": "success", **result}
72+
except ServiceError as e:
73+
return error_result(e.user_message, hint=e.hint)
74+
except Exception as e:
75+
return error_result(str(e))
76+
77+
78+
@logged_tool()
79+
def collection_delete(collection_id: str, confirm: bool = False) -> ResultDict:
80+
"""Delete a collection permanently. Notebooks inside the collection are NOT deleted.
81+
82+
Args:
83+
collection_id: UUID of the collection
84+
confirm: Must be True after user approval
85+
"""
86+
if not confirm:
87+
return {
88+
"status": "error",
89+
"error": "Deletion not confirmed. You must ask the user to confirm "
90+
"before deleting. Set confirm=True only after user approval.",
91+
"warning": "This action is IRREVERSIBLE. The collection will be permanently deleted.",
92+
}
93+
94+
try:
95+
client = get_client()
96+
result = collections_service.delete_collection(client, collection_id)
97+
return {"status": "success", **result}
98+
except ServiceError as e:
99+
return error_result(e.user_message, hint=e.hint)
100+
except Exception as e:
101+
return error_result(str(e))

0 commit comments

Comments
 (0)