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.
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/CHANGELOG.md +26 -5
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/CLAUDE.md +12 -2
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/PKG-INFO +1 -1
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/desktop-extension/manifest.json +1 -1
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/CLI_GUIDE.md +31 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/MCP_GUIDE.md +4 -2
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/pyproject.toml +1 -1
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/__init__.py +1 -1
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/ai_docs.py +2 -2
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/research.py +2 -4
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/base.py +91 -1
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/errors.py +27 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/AGENTS_SECTION.md +1 -1
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/SKILL.md +1 -1
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/server.py +4 -49
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/_utils.py +6 -59
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/downloads.py +2 -40
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/research.py +6 -2
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/sources.py +4 -111
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/downloads.py +0 -71
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/research.py +19 -1
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/sources.py +26 -263
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/env_sanitize.py +6 -2
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_base.py +15 -0
- notebooklm_mcp_cli-0.7.4/tests/core/test_rpc_errors.py +83 -0
- notebooklm_mcp_cli-0.7.4/tests/core/test_rpc_overrides.py +43 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_research.py +76 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_sources.py +78 -0
- notebooklm_mcp_cli-0.7.2/docs/CHATGPT_CONNECTOR_SETUP.md +0 -162
- notebooklm_mcp_cli-0.7.2/tests/test_e2e_chatgpt_upload_polling.py +0 -68
- notebooklm_mcp_cli-0.7.2/tests/test_mcp_chatgpt_file_download.py +0 -130
- notebooklm_mcp_cli-0.7.2/tests/test_mcp_chatgpt_file_upload.py +0 -93
- notebooklm_mcp_cli-0.7.2/tools/run-chatgpt-tunnel.ps1 +0 -38
- notebooklm_mcp_cli-0.7.2/tools/run-cloudflare-chatgpt.ps1 +0 -34
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.github/workflows/lint-test.yml +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.github/workflows/publish.yml +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.github/workflows/release-gate.yml +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.github/workflows/version-check.yml +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/.gitignore +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/AGENTS.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/CONTRIBUTING.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/GEMINI.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/LICENSE +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/README.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/desktop-extension/run_server.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/API_REFERENCE.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/AUTHENTICATION.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/FILE_UPLOAD_IMPLEMENTATION.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/GETTING_STARTED.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/KNOWN_ISSUES.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/MCP_CLI_TEST_PLAN.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/MULTI_USER_ANALYSIS.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/WSL_SETUP.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/docs/media/header.jpg +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/run_log.txt +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/scripts/build_mcpb.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/scripts/inject_cookies_and_inspect.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/scripts/inspect_upload_dom.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/scripts/test_label_rpcs.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/__init__.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/__init__.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/alias.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/batch.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/chat.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/config.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/cross.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/doctor.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/download.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/export.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/label.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/note.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/notebook.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/pipeline.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/repl.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/setup.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/share.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/skill.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/source.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/studio.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/tag.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/verbs.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/formatters.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/main.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/utils.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/__init__.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/alias.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/auth.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/client.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/constants.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/conversation.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/data_types.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/download.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/exceptions.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/exports.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/labels.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/models.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/notebooks.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/notes.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/research.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/retry.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/sharing.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/sources.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/studio.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/core/utils.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/command_reference.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/studio-prompt-examples.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/studio-prompting-guide.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/troubleshooting.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/data/references/workflows.md +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/__init__.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/__init__.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/auth.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/batch.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/chat.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/cross_notebook.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/exports.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/labels.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/notebooks.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/notes.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/pipeline.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/server.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/sharing.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/smart_select.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/studio.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/studio_advanced.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/__init__.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/_compat.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/auth.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/batch.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/chat.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/cross_notebook.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/errors.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/exports.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/labels.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/notebooks.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/notes.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/pipeline.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/sharing.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/smart_select.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/services/studio.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/__init__.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/auth_browser.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/browser.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/cdp.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/config.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/io_encoding.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/utils/wsl.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_formatters.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_login.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_repl.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_setup_github_copilot.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_setup_opencode.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_source_add.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_studio_cli.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_verbs_defaults.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/cli/test_verbs_parity.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_alias.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_auth_check.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_auth_guard.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_conversation.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_data_types.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_download.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_errors.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_notebooks.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_notes.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_research.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_retry.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_sharing.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_source_reconciliation.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_sources.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_studio.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_thread_safety.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_url_source_fallback.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/core/test_utils.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/manual/test_fastmcp_stdio.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/manual/test_revise_slide_deck.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/__init__.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_auth_health.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_auth_service.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_batch.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_chat.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_cross_notebook.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_downloads.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_errors.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_exports.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_notebooks.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_notes.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_pipeline.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_sharing.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_smart_select.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/services/test_studio.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_api_client.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_auth_browser.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_auth_migration.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_cdp_proxy_bypass.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_cdp_url_check.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_coerce_list.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_cookie_parsing.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_download_integration.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_e2e.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_file_upload.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_io_encoding_windows.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_mcp_auth_studio_failures.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_mcp_e2e.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_mcp_file_upload.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_mcp_studio.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_profile_aware_headless_auth.py +0 -0
- {notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/tests/test_research_polling.py +0 -0
- {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.
|
|
10
|
+
## [0.7.4] - 2026-06-15
|
|
11
11
|
|
|
12
12
|
### Added
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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,
|
|
228
|
-
|
|
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")
|
|
@@ -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 (
|
|
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
|
{notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/cli/commands/research.py
RENAMED
|
@@ -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=
|
|
152
|
-
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,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nlm-skill
|
|
3
|
-
version: "0.7.
|
|
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
|
|
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
|
|
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
|
|
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,
|
{notebooklm_mcp_cli-0.7.2 → notebooklm_mcp_cli-0.7.4}/src/notebooklm_tools/mcp/tools/_utils.py
RENAMED
|
@@ -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]
|
|
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)
|
|
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
|
|
271
|
-
|
|
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
|