notebooklm-mcp-cli 0.7.2__tar.gz → 0.7.4__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.
Files changed (209) hide show
  1. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/CHANGELOG.md +26 -5
  2. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/CLAUDE.md +12 -2
  3. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/PKG-INFO +1 -1
  4. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/desktop-extension/manifest.json +1 -1
  5. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/CLI_GUIDE.md +31 -0
  6. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/MCP_GUIDE.md +4 -2
  7. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/pyproject.toml +1 -1
  8. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/__init__.py +1 -1
  9. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/ai_docs.py +2 -2
  10. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/research.py +2 -4
  11. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/base.py +91 -1
  12. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/errors.py +27 -0
  13. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/AGENTS_SECTION.md +1 -1
  14. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/SKILL.md +1 -1
  15. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/server.py +4 -49
  16. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/_utils.py +6 -59
  17. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/downloads.py +2 -40
  18. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/research.py +6 -2
  19. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/sources.py +4 -111
  20. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/downloads.py +0 -71
  21. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/research.py +19 -1
  22. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/sources.py +26 -263
  23. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/env_sanitize.py +6 -2
  24. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_base.py +15 -0
  25. notebooklm_mcp_cli-0.7.4/tests/core/test_rpc_errors.py +83 -0
  26. notebooklm_mcp_cli-0.7.4/tests/core/test_rpc_overrides.py +43 -0
  27. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_research.py +76 -0
  28. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_sources.py +78 -0
  29. notebooklm_mcp_cli-0.7.2/docs/CHATGPT_CONNECTOR_SETUP.md +0 -162
  30. notebooklm_mcp_cli-0.7.2/tests/test_e2e_chatgpt_upload_polling.py +0 -68
  31. notebooklm_mcp_cli-0.7.2/tests/test_mcp_chatgpt_file_download.py +0 -130
  32. notebooklm_mcp_cli-0.7.2/tests/test_mcp_chatgpt_file_upload.py +0 -93
  33. notebooklm_mcp_cli-0.7.2/tools/run-chatgpt-tunnel.ps1 +0 -38
  34. notebooklm_mcp_cli-0.7.2/tools/run-cloudflare-chatgpt.ps1 +0 -34
  35. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.github/workflows/lint-test.yml +0 -0
  36. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.github/workflows/publish.yml +0 -0
  37. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.github/workflows/release-gate.yml +0 -0
  38. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.github/workflows/version-check.yml +0 -0
  39. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.gitignore +0 -0
  40. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/AGENTS.md +0 -0
  41. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/CONTRIBUTING.md +0 -0
  42. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/GEMINI.md +0 -0
  43. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/LICENSE +0 -0
  44. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/README.md +0 -0
  45. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/desktop-extension/run_server.py +0 -0
  46. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/API_REFERENCE.md +0 -0
  47. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/AUTHENTICATION.md +0 -0
  48. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/FILE_UPLOAD_IMPLEMENTATION.md +0 -0
  49. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/GETTING_STARTED.md +0 -0
  50. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/KNOWN_ISSUES.md +0 -0
  51. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/MCP_CLI_TEST_PLAN.md +0 -0
  52. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/MULTI_USER_ANALYSIS.md +0 -0
  53. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/WSL_SETUP.md +0 -0
  54. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/media/header.jpg +0 -0
  55. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/run_log.txt +0 -0
  56. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/scripts/build_mcpb.py +0 -0
  57. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/scripts/inject_cookies_and_inspect.py +0 -0
  58. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/scripts/inspect_upload_dom.py +0 -0
  59. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/scripts/test_label_rpcs.py +0 -0
  60. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/__init__.py +0 -0
  61. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/__init__.py +0 -0
  62. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/alias.py +0 -0
  63. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/batch.py +0 -0
  64. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/chat.py +0 -0
  65. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/config.py +0 -0
  66. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/cross.py +0 -0
  67. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/doctor.py +0 -0
  68. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/download.py +0 -0
  69. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/export.py +0 -0
  70. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/label.py +0 -0
  71. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/note.py +0 -0
  72. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/notebook.py +0 -0
  73. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/pipeline.py +0 -0
  74. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/repl.py +0 -0
  75. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/setup.py +0 -0
  76. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/share.py +0 -0
  77. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/skill.py +0 -0
  78. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/source.py +0 -0
  79. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/studio.py +0 -0
  80. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/tag.py +0 -0
  81. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/verbs.py +0 -0
  82. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/formatters.py +0 -0
  83. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/main.py +0 -0
  84. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/utils.py +0 -0
  85. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/__init__.py +0 -0
  86. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/alias.py +0 -0
  87. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/auth.py +0 -0
  88. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/client.py +0 -0
  89. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/constants.py +0 -0
  90. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/conversation.py +0 -0
  91. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/data_types.py +0 -0
  92. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/download.py +0 -0
  93. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/exceptions.py +0 -0
  94. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/exports.py +0 -0
  95. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/labels.py +0 -0
  96. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/models.py +0 -0
  97. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/notebooks.py +0 -0
  98. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/notes.py +0 -0
  99. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/research.py +0 -0
  100. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/retry.py +0 -0
  101. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/sharing.py +0 -0
  102. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/sources.py +0 -0
  103. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/studio.py +0 -0
  104. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/utils.py +0 -0
  105. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/command_reference.md +0 -0
  106. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/studio-prompt-examples.md +0 -0
  107. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/studio-prompting-guide.md +0 -0
  108. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/troubleshooting.md +0 -0
  109. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/workflows.md +0 -0
  110. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/__init__.py +0 -0
  111. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/__init__.py +0 -0
  112. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/auth.py +0 -0
  113. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/batch.py +0 -0
  114. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/chat.py +0 -0
  115. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/cross_notebook.py +0 -0
  116. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/exports.py +0 -0
  117. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/labels.py +0 -0
  118. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/notebooks.py +0 -0
  119. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/notes.py +0 -0
  120. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/pipeline.py +0 -0
  121. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/server.py +0 -0
  122. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/sharing.py +0 -0
  123. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/smart_select.py +0 -0
  124. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/studio.py +0 -0
  125. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/studio_advanced.py +0 -0
  126. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/__init__.py +0 -0
  127. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/_compat.py +0 -0
  128. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/auth.py +0 -0
  129. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/batch.py +0 -0
  130. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/chat.py +0 -0
  131. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/cross_notebook.py +0 -0
  132. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/errors.py +0 -0
  133. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/exports.py +0 -0
  134. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/labels.py +0 -0
  135. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/notebooks.py +0 -0
  136. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/notes.py +0 -0
  137. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/pipeline.py +0 -0
  138. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/sharing.py +0 -0
  139. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/smart_select.py +0 -0
  140. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/studio.py +0 -0
  141. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/__init__.py +0 -0
  142. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/auth_browser.py +0 -0
  143. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/browser.py +0 -0
  144. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/cdp.py +0 -0
  145. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/config.py +0 -0
  146. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/io_encoding.py +0 -0
  147. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/wsl.py +0 -0
  148. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_formatters.py +0 -0
  149. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_login.py +0 -0
  150. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_repl.py +0 -0
  151. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_setup_github_copilot.py +0 -0
  152. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_setup_opencode.py +0 -0
  153. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_source_add.py +0 -0
  154. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_studio_cli.py +0 -0
  155. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_verbs_defaults.py +0 -0
  156. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_verbs_parity.py +0 -0
  157. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_alias.py +0 -0
  158. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_auth_check.py +0 -0
  159. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_auth_guard.py +0 -0
  160. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_conversation.py +0 -0
  161. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_data_types.py +0 -0
  162. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_download.py +0 -0
  163. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_errors.py +0 -0
  164. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_notebooks.py +0 -0
  165. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_notes.py +0 -0
  166. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_research.py +0 -0
  167. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_retry.py +0 -0
  168. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_sharing.py +0 -0
  169. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_source_reconciliation.py +0 -0
  170. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_sources.py +0 -0
  171. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_studio.py +0 -0
  172. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_thread_safety.py +0 -0
  173. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_url_source_fallback.py +0 -0
  174. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_utils.py +0 -0
  175. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/manual/test_fastmcp_stdio.py +0 -0
  176. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/manual/test_revise_slide_deck.py +0 -0
  177. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/__init__.py +0 -0
  178. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_auth_health.py +0 -0
  179. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_auth_service.py +0 -0
  180. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_batch.py +0 -0
  181. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_chat.py +0 -0
  182. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_cross_notebook.py +0 -0
  183. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_downloads.py +0 -0
  184. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_errors.py +0 -0
  185. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_exports.py +0 -0
  186. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_notebooks.py +0 -0
  187. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_notes.py +0 -0
  188. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_pipeline.py +0 -0
  189. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_sharing.py +0 -0
  190. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_smart_select.py +0 -0
  191. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_studio.py +0 -0
  192. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_api_client.py +0 -0
  193. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_auth_browser.py +0 -0
  194. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_auth_migration.py +0 -0
  195. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_cdp_proxy_bypass.py +0 -0
  196. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_cdp_url_check.py +0 -0
  197. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_coerce_list.py +0 -0
  198. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_cookie_parsing.py +0 -0
  199. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_download_integration.py +0 -0
  200. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_e2e.py +0 -0
  201. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_file_upload.py +0 -0
  202. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_io_encoding_windows.py +0 -0
  203. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_mcp_auth_studio_failures.py +0 -0
  204. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_mcp_e2e.py +0 -0
  205. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_mcp_file_upload.py +0 -0
  206. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_mcp_studio.py +0 -0
  207. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_profile_aware_headless_auth.py +0 -0
  208. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_research_polling.py +0 -0
  209. {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_snap.py +0 -0
@@ -7,21 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.7.2] - 2026-06-08
10
+ ## [0.7.4] - 2026-06-15
11
11
 
12
12
  ### Added
13
13
 
14
- - **ChatGPT File Parameter Upload Bridge (PR #220)** Enables direct upload of files provided by ChatGPT App/Action clients by resolving and downloading from the client's temporary HTTPS URL before uploading to Google NotebookLM. Thanks to **@insane66613**!
15
- - **ChatGPT Artifact Download Bridge (PR #220)** — Allows copying generated artifacts to the local server's public directory and returning public download links when the server runs behind secure tunnels. Exposes a new `/artifacts/{filename}` route in the MCP HTTP server. Thanks to **@insane66613**!
16
- - **Transient Source Content Polling** (`poll_source_content` service function) — Automatically handles Google NotebookLM indexing states (processing, indexing, try again) via robust polling with exponential backoff when fetching raw source contents immediately after creation.
14
+ - **`research_status`: `auto_import` parameter** Pass `auto_import=True` and the tool automatically imports discovered sources on completion. No separate `research_import` call needed. When `False` (default), the response includes a `next_action` hint pointing you to the exact `research_import` call to make. Fixes the empty-notebook-shell problem reported in #231.
15
+
16
+ ### Fixed
17
+
18
+ - **`source_list_drive` timeouts on large notebooks (Fixes #232)** — Drive-source freshness checks were made one at a time, in sequence. A notebook with 50 Drive sources took ~30 seconds and routinely hit the 30-second timeout. Freshness checks now run in parallel (8 workers). Measured on a live 44-source notebook: 29s sequential → 3.1s parallel, zero rate-limit errors. If one source's freshness check fails, that source reports `stale: null` instead of failing the whole list.
19
+ - **`research_status` default timeout too short for deep research** — The MCP default was 5 minutes; deep research frequently runs longer. Default `max_wait` bumped to 15 minutes (900s), poll interval stays 30s. The CLI `--auto-import` flow is now consistent: same 15-minute / 30-second cadence instead of the old mode-specific 10-minute / 10-second split.
20
+ - **RPC drift detection silent on empty responses (PR #233, follow-up)** — The `RPCDriftError` doc said "instead of returning silently" but the code still returned `None` silently when the response had no `wrb.fr` chunks to compare against. Clarified the behavior in `CLAUDE.md` and added the MCP server restart requirement to the `NOTEBOOKLM_RPC_OVERRIDES` hot-patch instructions.
21
+
22
+ ### Community
23
+
24
+ - **RPC resilience (PR #233)** — Runtime RPC-ID overrides via `NOTEBOOKLM_RPC_OVERRIDES`, loud `RPCDriftError` on rotation detection, and exponential-backoff retry for `RESOURCE_EXHAUSTED` (code 8) throttling. Thanks to **@Grobiou** for the contribution!
25
+
26
+ ## [0.7.3] - 2026-06-09
27
+
28
+ ### Added
29
+
30
+ - **Scripting & Automation section in CLI_GUIDE.md (PR #226)** — Documents how to reliably parse `--json` output in automation scripts using a JSON parser rather than string splitting. Thanks to **@JanaGK2** for the first contribution!
31
+
32
+ ### Fixed
33
+
34
+ - **Reverted ChatGPT file bridge (PR #220)** — The ChatGPT file upload/download bridge introduced an SSRF vulnerability (no hostname validation on downloaded URLs), a disk leak (every `source_get_content` call wrote files to disk with no cleanup), and unintended behavioral changes to `source_get_content` and `download_artifact` for all users. The feature has been reverted. A future release will re-introduce the polling improvements (`wait`/`poll_interval`) cleanly, and the ChatGPT integration may return in a hardened form.
35
+ - **Tightened `no_proxy` sanitization** — The v0.7.2 sanitizer stripped all `no_proxy` entries containing `:`, incorrectly removing legitimate IPv6 loopback addresses (`::1`) and `host:port` entries. The fix now targets only CIDR-style IPv6 entries (e.g., `fe11::/16`) that crash httpx's URL parser, preserving all other valid entries.
36
+
37
+ ## [0.7.2] - 2026-06-08
17
38
 
18
39
  ### Fixed
19
40
 
20
- - **Sanitize `no_proxy` environment variable (PR #221)** — Fixed package import crashes on Windows systems by cleaning up the `no_proxy` environment variable on initialization before `httpx` parses it. Thanks to **@insane66613**!
21
41
  - **Modern Chrome Cookie DB path detection (PR #222)** — Improved the SQLite cookie database location search logic for Google Chrome to work correctly with modern Chrome setups. Thanks to **@insane66613**!
22
42
  - **Profile-aware headless refresh (PR #223)** — Fixed headless login refresh to use the default/active profile configured in the CLI configuration file instead of falling back. Thanks to **@insane66613**!
23
43
  - **`AuthHealthChecker` API fallback passed wrong cookie format, causing false `stale` on semi-stale sessions (PR #225)** — The API fallback flattened `profile.cookies` to a dict, dropping domain-specific duplicates and omitting `session_id` / `build_label`. The API probe now passes `profile.cookies` unchanged with all session fields, matching `nlm login --check`. Thanks to **@insane66613**!
24
44
  - **`server_info`, `refresh_auth`, and `studio_create` disagreed on semi-stale auth (PR #225, Fixes #224)** — These three MCP paths each ran independent auth checks, so `server_info` could report `stale` and `studio_create` refuse to run while `notebook_list` and CLI tools worked fine. All MCP auth gates now share `credentials_are_usable()` (`AuthHealthChecker` + live API confirmation), eliminating split-brain results. Thanks to **@insane66613**!
45
+ - **Sanitize `no_proxy` environment variable (PR #221)** — Fixed package import crashes on Windows systems caused by CIDR-style IPv6 entries in `no_proxy` that httpx cannot parse. Thanks to **@insane66613**!
25
46
 
26
47
  ## [0.7.1] - 2026-06-06
27
48
 
@@ -78,6 +78,16 @@ save_auth_tokens(cookies=<cookie_header>)
78
78
  | `NOTEBOOKLM_SESSION_ID` | No | (DEPRECATED - auto-extracted) |
79
79
  | `NOTEBOOKLM_BL` | No | Override for build label / bl URL param (auto-extracted from page) |
80
80
  | `NOTEBOOKLM_HL` | No | Interface language and default artifact language (default: `en`) |
81
+ | `NOTEBOOKLM_RPC_OVERRIDES` | No | Hot-patch rotated batchexecute RPC method IDs without a release. JSON object mapping `BaseClient` RPC attribute names to new IDs, e.g. `{"RPC_LIST_NOTEBOOKS": "abc123"}` |
82
+
83
+ ### Resilience: rotated RPC IDs
84
+
85
+ NotebookLM's internal API uses short RPC "method IDs" (e.g. `wXbhsf`) that Google rotates without notice. When one rotates, calls using the old ID fail. The client now:
86
+
87
+ - **Detects drift loudly**: raises `RPCDriftError` (instead of returning silently) when the server responds with **other** `wrb.fr` RPC IDs than the one requested. An empty response still returns silently (no comparison points), so use `--debug` to inspect in that case.
88
+ - **Discovers the new ID**: run with `--debug` to log `RPC IDs in response: [...]` — the new ID for your call appears there.
89
+ - **Hot-patches without a release**: set `NOTEBOOKLM_RPC_OVERRIDES='{"RPC_LIST_NOTEBOOKS": "<new_id>"}'` (use the `RPC_*` attribute name from `core/base.py`) to override the ID for the current session. **Restart the MCP server for the override to take effect** — the env var is read once at client init, not per call. The CLI (`nlm`) picks it up on the next invocation automatically.
90
+ - **Auto-retries throttling**: `RESOURCE_EXHAUSTED` (RPC error code 8) responses are retried with exponential backoff.
81
91
 
82
92
  ### Token Expiration
83
93
 
@@ -157,8 +167,8 @@ src/notebooklm_tools/
157
167
  | `source_rename` | Rename a source in a notebook |
158
168
  | `source_delete` | Delete a source from notebook (REQUIRES confirmation) |
159
169
  | `research_start` | Start Web or Drive research to discover sources |
160
- | `research_status` | Check research progress and get results |
161
- | `research_import` | Import discovered sources into notebook |
170
+ | `research_status` | Check research progress (default: 900s wait, 30s poll). Pass `auto_import=True` to automatically import sources on completion — no separate `research_import` call needed. |
171
+ | `research_import` | Import discovered sources into notebook (manual, if `auto_import` not used) |
162
172
  | `studio_create` | Generate unified content (audio, video, infographic, slides, etc.) |
163
173
  | `download_artifact` | Download any artifact (audio, video, pdf, markdown, json). Supports `wait`, `wait_timeout`, `poll_interval` params and returns `download_url` when MCP HTTP transport is active. |
164
174
  | `export_artifact` | Export Data Tables to Google Sheets or Reports to Google Docs |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notebooklm-mcp-cli
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: Unified CLI and MCP server for Google NotebookLM
5
5
  Project-URL: Homepage, https://github.com/jacob-bd/notebooklm-mcp-cli
6
6
  Project-URL: Repository, https://github.com/jacob-bd/notebooklm-mcp-cli
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.2",
3
3
  "name": "notebooklm-mcp",
4
4
  "display_name": "NotebookLM MCP Server",
5
- "version": "0.7.2",
5
+ "version": "0.7.3",
6
6
  "description": "Connect Claude to Google NotebookLM. Create podcasts, research topics, generate quizzes, flashcards, mind maps, and more from your notebooks.",
7
7
  "long_description": "This extension connects Claude Desktop to Google NotebookLM via the Model Context Protocol. It provides 29 tools for managing notebooks, sources, and generating AI content.\n\n**Prerequisites:**\n1. Install the package: `pip install notebooklm-mcp-cli` or `uv tool install notebooklm-mcp-cli`\n2. Authenticate: Run `nlm login` to set up your Google account\n\n**Features:**\n- Create and manage notebooks\n- Add sources (URLs, YouTube, Google Drive, text, files)\n- Generate podcasts (Audio Overview)\n- Create quizzes, flashcards, mind maps, reports\n- Research topics with web or Drive search",
8
8
  "author": {
@@ -367,3 +367,34 @@ nlm download audio ai <artifact-id> --output podcast.mp3
367
367
  - Run `nlm login profile list` to see all profiles with their associated email addresses
368
368
  - Run `nlm doctor` to diagnose installation, auth, or config issues
369
369
  - Use `nlm setup add <client>` to quickly configure MCP for your AI tool
370
+
371
+ ---
372
+
373
+ ## Scripting & Automation
374
+
375
+ ### Getting IDs for piping
376
+
377
+ Use `--quiet` when you only need IDs — it outputs one ID per line, no parsing required:
378
+
379
+ ```bash
380
+ # Grab the first notebook ID in a shell script
381
+ notebook_id=$(nlm notebook list --quiet | head -1)
382
+ nlm notebook query "$notebook_id" "Summarize key points"
383
+ ```
384
+
385
+ ### Parsing `--json` output
386
+
387
+ When you need more than just IDs (title, source count, etc.), use `--json`. It emits
388
+ clean JSON to stdout — always parse it properly rather than splitting strings:
389
+
390
+ ```python
391
+ import json
392
+ import subprocess
393
+
394
+ result = subprocess.run(
395
+ ["nlm", "notebook", "list", "--json"],
396
+ capture_output=True, text=True
397
+ )
398
+ notebooks = json.loads(result.stdout)
399
+ notebook_id = notebooks[0]["id"] # UUID like "abc12345-..."
400
+ ```
@@ -224,8 +224,10 @@ tag(action="select", query="ai research") # Find notebooks by tag match
224
224
 
225
225
  ```
226
226
  1. research_start(query="AI trends 2026", mode="deep")
227
- 2. research_status(notebook_id, max_wait=300) # wait for completion
228
- 3. research_import(notebook_id, task_id, cited_only=True, timeout=600) # optional cited subset
227
+ 2. research_status(notebook_id, auto_import=True) # waits up to 15 min, imports automatically
228
+ # Or review sources first, then import manually:
229
+ 2a. research_status(notebook_id) # waits up to 15 min, returns next_action hint
230
+ 2b. research_import(notebook_id, task_id, cited_only=True, timeout=600) # optional cited subset
229
231
  4. studio_create(notebook_id, artifact_type="audio", confirm=True)
230
232
  5. studio_status(notebook_id) # poll until complete
231
233
  6. download_artifact(notebook_id, artifact_type="audio", output_path="podcast.mp3")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "notebooklm-mcp-cli"
7
- version = "0.7.2"
7
+ version = "0.7.4"
8
8
  description = "Unified CLI and MCP server for Google NotebookLM"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -6,7 +6,7 @@ from typing import Any
6
6
 
7
7
  import notebooklm_tools.utils.env_sanitize as _env_sanitize # noqa: F401
8
8
 
9
- __version__ = "0.7.2"
9
+ __version__ = "0.7.4"
10
10
 
11
11
  __all__ = ["NotebookLMClient", "__version__"]
12
12
 
@@ -304,10 +304,10 @@ nlm research start "query" --notebook-id <id> # Fast web (def
304
304
  nlm research start "query" --notebook-id <id> --mode deep # Deep web (~5min)
305
305
  nlm research start "query" --notebook-id <id> --source drive # Fast drive
306
306
  nlm research start "query" --notebook-id <id> --force # Override pending
307
- nlm research start "query" --notebook-id <id> --auto-import # Auto wait and import
307
+ nlm research start "query" --notebook-id <id> --auto-import # Auto wait (15min) and import
308
308
 
309
309
  # Check progress
310
- nlm research status <notebook-id> # Poll until done (5min max)
310
+ nlm research status <notebook-id> # Poll until done (15min max, 30s interval)
311
311
  nlm research status <notebook-id> --max-wait 0 # Single check
312
312
  nlm research status <notebook-id> --task-id <tid> # Specific task
313
313
  nlm research status <notebook-id> --full # Full details
@@ -141,15 +141,13 @@ def start_research(
141
141
  ) as progress:
142
142
  progress.add_task("Gathering sources...", total=None)
143
143
 
144
- # Use a larger timeout for deep mode
145
- max_wait = 600 if mode == "deep" else 120
146
144
  with get_client(profile) as import_client:
147
145
  poll_res = research_service.poll_research(
148
146
  import_client,
149
147
  notebook_id,
150
148
  task_id=result["task_id"],
151
- poll_interval=10,
152
- max_wait=max_wait,
149
+ poll_interval=30,
150
+ max_wait=900,
153
151
  )
154
152
 
155
153
  _display_research_status(poll_res, compact=True)
@@ -25,7 +25,7 @@ from notebooklm_tools.utils.config import get_base_url
25
25
  from . import constants
26
26
  from .data_types import ConversationTurn
27
27
  from .errors import ClientAuthenticationError as AuthenticationError
28
- from .errors import ResourceExhaustedError, RPCError
28
+ from .errors import ResourceExhaustedError, RPCDriftError, RPCError
29
29
  from .retry import DEFAULT_BASE_DELAY, DEFAULT_MAX_DELAY, DEFAULT_MAX_RETRIES, is_retryable_error
30
30
  from .utils import (
31
31
  RPC_NAMES,
@@ -60,6 +60,28 @@ def _safe_int_env(name: str, default: int) -> int:
60
60
  return value
61
61
 
62
62
 
63
+ def load_rpc_overrides() -> dict[str, str]:
64
+ """Load runtime RPC-ID overrides from NOTEBOOKLM_RPC_OVERRIDES.
65
+
66
+ Lets users hot-patch rotated batchexecute method IDs without a release.
67
+ The value is a JSON object mapping BaseClient RPC attribute names to new
68
+ IDs, e.g. '{"RPC_LIST_NOTEBOOKS": "abc123"}'. Returns {} if unset, empty,
69
+ or malformed (a warning is logged on malformed input).
70
+ """
71
+ raw = os.environ.get("NOTEBOOKLM_RPC_OVERRIDES", "").strip()
72
+ if not raw:
73
+ return {}
74
+ try:
75
+ data = json.loads(raw)
76
+ except json.JSONDecodeError as e:
77
+ logger.warning("Ignoring malformed NOTEBOOKLM_RPC_OVERRIDES: %s", e)
78
+ return {}
79
+ if not isinstance(data, dict):
80
+ logger.warning("Ignoring NOTEBOOKLM_RPC_OVERRIDES: expected a JSON object")
81
+ return {}
82
+ return {str(k): str(v) for k, v in data.items()}
83
+
84
+
63
85
  def _extract_user_message(detail_data: Any, _depth: int = 0) -> str:
64
86
  """Recursively extract human-readable strings from a protobuf detail payload.
65
87
 
@@ -370,6 +392,9 @@ class BaseClient:
370
392
  # It is never held during network I/O.
371
393
  self._state_lock = threading.Lock()
372
394
 
395
+ # Apply any runtime RPC-ID overrides (hot-patch for rotated method IDs).
396
+ self._apply_rpc_overrides()
397
+
373
398
  # Only refresh CSRF token if not provided - tokens actually last hours/days, not minutes
374
399
  # The retry logic in _call_rpc() handles expired tokens gracefully
375
400
  if not self.csrf_token:
@@ -387,6 +412,21 @@ class BaseClient:
387
412
  self._client.close()
388
413
  self._client = None
389
414
 
415
+ def _apply_rpc_overrides(self) -> None:
416
+ """Apply NOTEBOOKLM_RPC_OVERRIDES as instance attributes.
417
+
418
+ Each entry shadows the matching class-level RPC_* constant on this
419
+ instance only. Unknown keys (not an existing RPC_* attribute) are
420
+ logged and ignored so a typo can never silently disable a tool.
421
+ """
422
+ for name, new_id in load_rpc_overrides().items():
423
+ if not name.startswith("RPC_") or not hasattr(type(self), name):
424
+ logger.warning("Ignoring unknown RPC override: %s", name)
425
+ continue
426
+ old_id = getattr(type(self), name)
427
+ setattr(self, name, new_id)
428
+ logger.warning("RPC override applied: %s %s -> %s", name, old_id, new_id)
429
+
390
430
  # =========================================================================
391
431
  # Cookie Handling
392
432
  # =========================================================================
@@ -647,8 +687,31 @@ class BaseClient:
647
687
  except json.JSONDecodeError:
648
688
  return result_str
649
689
  return result_str
690
+ present = self._extract_present_rpc_ids(parsed_response)
691
+ if present and rpc_id not in present:
692
+ raise RPCDriftError(rpc_id, present)
650
693
  return None
651
694
 
695
+ def _extract_present_rpc_ids(self, parsed_response: list) -> list[str]:
696
+ """Return the rpc_ids of every wrb.fr chunk in a parsed response.
697
+
698
+ Used for drift diagnostics: when the expected rpc_id is missing, this
699
+ reveals which IDs the server actually returned so the user can set
700
+ NOTEBOOKLM_RPC_OVERRIDES.
701
+ """
702
+ present: list[str] = []
703
+ for chunk in parsed_response:
704
+ if isinstance(chunk, list):
705
+ for item in chunk:
706
+ if (
707
+ isinstance(item, list)
708
+ and len(item) >= 2
709
+ and item[0] == "wrb.fr"
710
+ and isinstance(item[1], str)
711
+ ):
712
+ present.append(item[1])
713
+ return present
714
+
652
715
  def _call_rpc(
653
716
  self,
654
717
  rpc_id: str,
@@ -714,6 +777,8 @@ class BaseClient:
714
777
 
715
778
  # Check for RPC-level errors (soft auth failure)
716
779
  parsed = self._parse_response(response.text)
780
+ if logger.isEnabledFor(logging.DEBUG):
781
+ logger.debug("RPC IDs in response: %s", self._extract_present_rpc_ids(parsed))
717
782
  result = self._extract_rpc_result(parsed, rpc_id)
718
783
 
719
784
  # Enhanced debug logging for extracted result
@@ -764,6 +829,31 @@ class BaseClient:
764
829
  # Fall through to auth recovery below
765
830
  pass
766
831
 
832
+ except ResourceExhaustedError:
833
+ # RPC-level rate limit (HTTP 200, error code 8). Back off and retry.
834
+ if _server_retry < DEFAULT_MAX_RETRIES:
835
+ import time as _time
836
+
837
+ delay = min(DEFAULT_BASE_DELAY * (2**_server_retry), DEFAULT_MAX_DELAY)
838
+ logger.warning(
839
+ "RPC rate limit (RESOURCE_EXHAUSTED) on %s, attempt %d/%d, retrying in %.1fs...",
840
+ rpc_id,
841
+ _server_retry + 1,
842
+ DEFAULT_MAX_RETRIES + 1,
843
+ delay,
844
+ )
845
+ _time.sleep(delay)
846
+ return self._call_rpc(
847
+ rpc_id,
848
+ params,
849
+ path,
850
+ timeout,
851
+ _retry,
852
+ _deep_retry,
853
+ _server_retry=_server_retry + 1,
854
+ )
855
+ raise
856
+
767
857
  except AuthenticationError:
768
858
  # RPC Error 16 - fall through to auth recovery below
769
859
  pass
@@ -142,3 +142,30 @@ class ResourceExhaustedError(RPCError):
142
142
 
143
143
  def __init__(self, message: str, detail_type: str = "", detail_data=None):
144
144
  super().__init__(message, error_code=8, detail_type=detail_type, detail_data=detail_data)
145
+
146
+
147
+ class RPCDriftError(NotebookLMError):
148
+ """Raised when the response contains other RPC IDs but not the one requested.
149
+
150
+ The usual cause is Google rotating the batchexecute method ID. The message
151
+ names the missing id, lists the ids the server *did* return, and points the
152
+ user at NOTEBOOKLM_RPC_OVERRIDES to hot-patch it without a release.
153
+
154
+ Deliberately extends NotebookLMError directly, NOT RPCError: drift must
155
+ bypass the service-layer ``except RPCError`` handlers so its actionable
156
+ NOTEBOOKLM_RPC_OVERRIDES guidance reaches the user verbatim instead of
157
+ being reformatted into a generic service error. (Contrast with
158
+ ResourceExhaustedError, which subclasses RPCError on purpose so existing
159
+ handlers still catch it.)
160
+ """
161
+
162
+ def __init__(self, rpc_id: str, present_ids: list[str] | None = None):
163
+ present = ", ".join(present_ids) if present_ids else "(none)"
164
+ super().__init__(
165
+ f"No result for RPC '{rpc_id}' — the method ID may have been rotated by Google. "
166
+ f"RPC IDs present in the response: {present}. "
167
+ f'Hot-patch it by setting NOTEBOOKLM_RPC_OVERRIDES=\'{{"<ATTR_NAME>": "<new_id>"}}\' '
168
+ f"(run with --debug to inspect the response)."
169
+ )
170
+ self.rpc_id = rpc_id
171
+ self.present_ids = present_ids or []
@@ -1,5 +1,5 @@
1
1
  <!-- nlm-skill-start -->
2
- <!-- nlm-version: 0.7.2 -->
2
+ <!-- nlm-version: 0.7.3 -->
3
3
  ## NLM - NotebookLM CLI Expert
4
4
 
5
5
  **Triggers:** "nlm", "notebooklm", "notebook lm", "podcast", "audio overview", "research"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: nlm-skill
3
- version: "0.7.2"
3
+ version: "0.7.3"
4
4
  description: "Expert guide for the NotebookLM CLI (`nlm`) and MCP server - interfaces for Google NotebookLM. Use this skill when users want to interact with NotebookLM programmatically, including: creating/managing notebooks, adding sources (URLs, YouTube, text, Google Drive), generating content (podcasts, reports, quizzes, flashcards, mind maps, slides, infographics, videos, data tables), conducting research, chatting with sources, or automating NotebookLM workflows. Triggers on mentions of \"nlm\", \"notebooklm\", \"notebook lm\", \"podcast generation\", \"audio overview\", or any NotebookLM-related automation task."
5
5
  ---
6
6
 
@@ -20,23 +20,18 @@ Tool Modules:
20
20
  import argparse
21
21
  import logging
22
22
  import os
23
- from pathlib import Path
24
23
 
25
24
  from fastmcp import FastMCP
26
- from starlette.middleware import Middleware
27
- from starlette.middleware.base import BaseHTTPMiddleware
28
25
  from starlette.requests import Request
29
- from starlette.responses import FileResponse, JSONResponse, Response
26
+ from starlette.responses import JSONResponse
30
27
 
31
28
  from notebooklm_tools import __version__
32
29
 
33
- from .tools._utils import PUBLIC_DIR, reset_mcp_base_url, set_mcp_base_url
34
-
35
30
  _FALSY = frozenset({"false", "0", "no", "off"})
36
31
 
37
32
 
38
33
  def _env_bool(name: str, default: bool = False) -> bool:
39
- """Read a boolean from an environment variable. Unset/empty → *default*; otherwise ``false|0|no|off`` (case-insensitive) → False, anything else → True."""
34
+ """Read a boolean from an environment variable. Unset/empty *default*; otherwise ``false|0|no|off`` (case-insensitive) False, anything else True."""
40
35
  raw = os.environ.get(name, "")
41
36
  if not raw:
42
37
  return default
@@ -78,46 +73,6 @@ async def health_check(request: Request) -> JSONResponse:
78
73
  )
79
74
 
80
75
 
81
- class BaseUrlMiddleware(BaseHTTPMiddleware):
82
- async def dispatch(self, request: Request, call_next):
83
- host = request.headers.get(
84
- "x-forwarded-host", request.headers.get("host", "127.0.0.1:8000")
85
- )
86
- proto = request.headers.get("x-forwarded-proto", "http")
87
- base_url = f"{proto}://{host}"
88
-
89
- token = set_mcp_base_url(base_url)
90
- try:
91
- return await call_next(request)
92
- finally:
93
- reset_mcp_base_url(token)
94
-
95
-
96
- # Ensure FastMCP applies the HTTP middleware to the actual served Starlette app.
97
- _original_http_app = mcp.http_app
98
-
99
-
100
- def _http_app_with_base_url(*args, middleware=None, **kwargs):
101
- merged_middleware = [Middleware(BaseUrlMiddleware), *(middleware or [])]
102
- return _original_http_app(*args, middleware=merged_middleware, **kwargs)
103
-
104
-
105
- mcp.http_app = _http_app_with_base_url
106
-
107
-
108
- # Custom route to serve public artifacts
109
- @mcp.custom_route("/artifacts/{filename}", methods=["GET"])
110
- async def get_public_artifact(request: Request) -> Response:
111
- """Serve downloaded artifacts and source files via the public URL."""
112
- filename = request.path_params["filename"]
113
- # Clean the path to prevent directory traversal
114
- safe_name = Path(filename).name
115
- filepath = PUBLIC_DIR / safe_name
116
- if not filepath.exists() or not filepath.is_file():
117
- return Response("File not found", status_code=404)
118
- return FileResponse(str(filepath))
119
-
120
-
121
76
  def _register_tools() -> None:
122
77
  """Import and register all tools from the modular tools package."""
123
78
  # Import all tool modules to populate the registry
@@ -214,7 +169,7 @@ Examples:
214
169
  default=_env_bool("NOTEBOOKLM_MCP_STATELESS", default=True),
215
170
  help=(
216
171
  "Stateless HTTP sessions (default: true). Avoids MCP SDK double-response crash "
217
- "(python-sdk#2416). NOTE: this affects the MCP HTTP transport layer only — "
172
+ "(python-sdk#2416). NOTE: this affects the MCP HTTP transport layer only "
218
173
  "it does NOT control the in-process conversation history cache. To bound the "
219
174
  "conversation cache (e.g. for long-lived servers), set "
220
175
  "NOTEBOOKLM_CONVERSATION_MAX_TURNS / NOTEBOOKLM_CONVERSATION_MAX_CONVS."
@@ -286,7 +241,7 @@ Examples:
286
241
  if not _env_bool("NOTEBOOKLM_ALLOW_EXTERNAL_BIND"):
287
242
  print(
288
243
  f"SECURITY ERROR: Refusing to bind to non-loopback address '{args.host}'.\n"
289
- "There is no built-in authentication — binding externally exposes\n"
244
+ "There is no built-in authentication binding externally exposes\n"
290
245
  "your Google cookies to anyone on the network.\n\n"
291
246
  "To override, set: NOTEBOOKLM_ALLOW_EXTERNAL_BIND=1",
292
247
  file=sys.stderr,
@@ -7,64 +7,17 @@ import logging
7
7
  import os
8
8
  import threading
9
9
  from collections.abc import Awaitable, Callable
10
- from contextvars import ContextVar
11
- from pathlib import Path
12
10
  from typing import Any, ParamSpec, TypeAlias, TypeVar, cast
13
11
 
14
12
  from notebooklm_tools.core.client import NotebookLMClient
15
13
  from notebooklm_tools.core.utils import extract_cookies_from_chrome_export
16
14
  from notebooklm_tools.services.auth import load_cached_tokens
17
- from notebooklm_tools.utils.config import get_data_dir
18
-
19
- mcp_base_url: ContextVar[str] = ContextVar("mcp_base_url", default="")
20
- _last_mcp_base_url = ""
21
- _base_url_lock = threading.Lock()
22
- PUBLIC_DIR: Path = get_data_dir() / "public_artifacts"
23
-
24
-
25
- def set_mcp_base_url(base_url: str) -> object:
26
- """Store the current public MCP base URL for tool-generated links."""
27
- global _last_mcp_base_url
28
- normalized = base_url.rstrip("/")
29
- with _base_url_lock:
30
- _last_mcp_base_url = normalized
31
- return mcp_base_url.set(normalized)
32
-
33
-
34
- def reset_mcp_base_url(token: object) -> None:
35
- """Reset the request-local base URL without clearing the process fallback."""
36
- mcp_base_url.reset(token)
37
-
38
-
39
- def get_mcp_base_url() -> str:
40
- """Return the request-local base URL or the last seen HTTP base URL.
41
-
42
- FastMCP may execute sync tools outside the ASGI context where the middleware
43
- set the ContextVar. The process fallback preserves the tunnel host captured
44
- from the current or most recent HTTP request.
45
- """
46
- current = mcp_base_url.get()
47
- if current:
48
- return current
49
- with _base_url_lock:
50
- return _last_mcp_base_url
51
-
52
15
 
53
16
  # MCP request/response logger
54
17
  mcp_logger = logging.getLogger("notebooklm_tools.mcp")
55
18
 
56
19
  # Parameters that must never appear in log output
57
- _SENSITIVE_PARAMS = frozenset(
58
- {
59
- "cookies",
60
- "csrf_token",
61
- "session_id",
62
- "request_body",
63
- "download_url",
64
- "download_link",
65
- "file",
66
- }
67
- )
20
+ _SENSITIVE_PARAMS = frozenset({"cookies", "csrf_token", "session_id", "request_body"})
68
21
  P = ParamSpec("P")
69
22
  R = TypeVar("R")
70
23
  T = TypeVar("T")
@@ -197,13 +150,10 @@ def get_mcp_instance() -> Any:
197
150
 
198
151
 
199
152
  # Registry for tools - allows registration without immediate mcp dependency
200
- _tool_registry: list[tuple[str, Callable[..., Any], dict[str, Any] | None]] = []
153
+ _tool_registry: list[tuple[str, Callable[..., Any]]] = []
201
154
 
202
155
 
203
- def logged_tool(
204
- *,
205
- meta: dict[str, Any] | None = None,
206
- ) -> Callable[[Callable[P, Any]], Callable[P, Any]]:
156
+ def logged_tool() -> Callable[[Callable[P, Any]], Callable[P, Any]]:
207
157
  """Decorator that adds MCP request/response logging to a tool.
208
158
 
209
159
  Decorated tools are added to the internal registry for later MCP server
@@ -259,7 +209,7 @@ def logged_tool(
259
209
  wrapper = sync_wrapper
260
210
 
261
211
  # Store for later registration
262
- _tool_registry.append((func.__name__, cast(Callable[..., Any], wrapper), meta))
212
+ _tool_registry.append((func.__name__, cast(Callable[..., Any], wrapper)))
263
213
  return wrapper
264
214
 
265
215
  return decorator
@@ -267,11 +217,8 @@ def logged_tool(
267
217
 
268
218
  def register_all_tools(mcp: Any) -> None:
269
219
  """Register all collected tools with the MCP instance."""
270
- for _, wrapper, meta in _tool_registry:
271
- if meta:
272
- mcp.tool(meta=meta)(wrapper)
273
- else:
274
- mcp.tool()(wrapper)
220
+ for _, wrapper in _tool_registry:
221
+ mcp.tool()(wrapper)
275
222
 
276
223
 
277
224
  # Essential cookies for NotebookLM API authentication