real-browser-cli 0.16.4__tar.gz → 0.16.9__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 (251) hide show
  1. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/CLAUDE.md +1 -1
  2. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/PKG-INFO +116 -2
  3. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/README.md +112 -0
  4. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/client/core.py +11 -8
  5. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/client/messages.py +2 -2
  6. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/extract.py +25 -19
  7. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/navigate.py +8 -6
  8. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/page.py +5 -4
  9. real_browser_cli-0.16.9/browser_cli/mcp/__init__.py +5 -0
  10. real_browser_cli-0.16.9/browser_cli/mcp/naming.py +39 -0
  11. real_browser_cli-0.16.9/browser_cli/mcp/serialization.py +19 -0
  12. real_browser_cli-0.16.9/browser_cli/mcp/server.py +314 -0
  13. real_browser_cli-0.16.9/browser_cli/mcp/targets.py +16 -0
  14. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/remote/pool.py +23 -0
  15. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/remote/registry.py +26 -0
  16. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/dom.py +25 -23
  17. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/navigation.py +35 -21
  18. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/tabs.py +17 -1
  19. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/serve/security.py +30 -1
  20. real_browser_cli-0.16.9/browser_cli/url_match.py +138 -0
  21. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/manifest.json +1 -1
  22. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/classes/JobManager.ts +10 -0
  23. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/dom.ts +2 -2
  24. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/perf.ts +15 -11
  25. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/windows.ts +29 -2
  26. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/test/jobs.test.ts +16 -1
  27. real_browser_cli-0.16.9/extension/test/windows.test.ts +69 -0
  28. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/package-lock.json +1385 -292
  29. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/package.json +6 -6
  30. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/tsconfig.json +2 -2
  31. real_browser_cli-0.16.9/package-lock.json +917 -0
  32. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/package.json +3 -3
  33. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/pyproject.toml +5 -1
  34. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/conftest.py +12 -0
  35. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_api.py +14 -3
  36. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_cli.py +52 -13
  37. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_client.py +19 -17
  38. real_browser_cli-0.16.9/tests/test_mcp.py +340 -0
  39. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_new_feature_commands.py +44 -5
  40. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_remote_pool.py +33 -0
  41. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_remote_registry.py +17 -0
  42. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_serve_security.py +17 -0
  43. real_browser_cli-0.16.9/tests/test_url_match.py +84 -0
  44. real_browser_cli-0.16.9/uv.lock +1582 -0
  45. real_browser_cli-0.16.4/package-lock.json +0 -556
  46. real_browser_cli-0.16.4/uv.lock +0 -690
  47. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/.claude/settings.local.json +0 -0
  48. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/.gitea/workflows/package-extension.yml +0 -0
  49. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/.gitea/workflows/publish.yml +0 -0
  50. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/.gitea/workflows/testing.yml +0 -0
  51. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/.gitignore +0 -0
  52. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/.python-version +0 -0
  53. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/LICENSE +0 -0
  54. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/PRIVACY.md +0 -0
  55. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/__init__.py +0 -0
  56. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/async_sdk.py +0 -0
  57. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/auth/__init__.py +0 -0
  58. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/auth/agent.py +0 -0
  59. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/auth/keys.py +0 -0
  60. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/auth/pq.py +0 -0
  61. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/auth/server_identity.py +0 -0
  62. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/auth/signing.py +0 -0
  63. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/cli.py +0 -0
  64. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/client/__init__.py +0 -0
  65. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/client/auth.py +0 -0
  66. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/client/targets.py +0 -0
  67. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/command_security.py +0 -0
  68. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/__init__.py +0 -0
  69. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/auth.py +0 -0
  70. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/clients.py +0 -0
  71. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/completion.py +0 -0
  72. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/doctor.py +0 -0
  73. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/dom.py +0 -0
  74. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/events.py +0 -0
  75. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/extension.py +0 -0
  76. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/groups.py +0 -0
  77. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/install.py +0 -0
  78. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/perf.py +0 -0
  79. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/raw.py +0 -0
  80. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/remote.py +0 -0
  81. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/__init__.py +0 -0
  82. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/common.py +0 -0
  83. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/labels.py +0 -0
  84. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/tabs_tree.py +0 -0
  85. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/windows_tree.py +0 -0
  86. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/script.py +0 -0
  87. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/search.py +0 -0
  88. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/serve.py +0 -0
  89. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/serve_http.py +0 -0
  90. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/session.py +0 -0
  91. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/storage.py +0 -0
  92. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/tabs.py +0 -0
  93. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/watch.py +0 -0
  94. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/windows.py +0 -0
  95. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/commands/workspace.py +0 -0
  96. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/compat/__init__.py +0 -0
  97. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/compat/auth.py +0 -0
  98. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/compat/commands.py +0 -0
  99. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/constants.py +0 -0
  100. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/endpoints.py +0 -0
  101. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/errors.py +0 -0
  102. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/framing.py +0 -0
  103. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/local_transport.py +0 -0
  104. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/markdown/__init__.py +0 -0
  105. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/markdown/html.py +0 -0
  106. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/markdown/render.py +0 -0
  107. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/models.py +0 -0
  108. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/native/__init__.py +0 -0
  109. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/native/host.py +0 -0
  110. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/native/local_server.py +0 -0
  111. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/native/protocol.py +0 -0
  112. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/platform.py +0 -0
  113. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/registry.py +0 -0
  114. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/remote/__init__.py +0 -0
  115. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/remote/auth.py +0 -0
  116. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/remote/known_hosts.py +0 -0
  117. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/remote/socket.py +0 -0
  118. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/remote/transport.py +0 -0
  119. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/__init__.py +0 -0
  120. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/base.py +0 -0
  121. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/browser_data.py +0 -0
  122. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/decorators.py +0 -0
  123. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/extension.py +0 -0
  124. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/factories.py +0 -0
  125. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/groups.py +0 -0
  126. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/perf.py +0 -0
  127. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/routing.py +0 -0
  128. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/session.py +0 -0
  129. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/windows.py +0 -0
  130. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/sdk/workflow_decorators.py +0 -0
  131. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/search/__init__.py +0 -0
  132. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/search/engines.py +0 -0
  133. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/serve/__init__.py +0 -0
  134. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/serve/auth.py +0 -0
  135. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/serve/challenge.py +0 -0
  136. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/serve/control.py +0 -0
  137. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/serve/logging.py +0 -0
  138. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/serve/proxy.py +0 -0
  139. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/serve/runtime.py +0 -0
  140. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/transport/__init__.py +0 -0
  141. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/transport/binary.py +0 -0
  142. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/transport/codecs.py +0 -0
  143. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/browser_cli/version_manager.py +0 -0
  144. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/examples/demo.py +0 -0
  145. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/examples/demo.sh +0 -0
  146. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/content.js +0 -0
  147. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/icon.svg +0 -0
  148. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/icons/icon-128.png +0 -0
  149. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/icons/icon-16.png +0 -0
  150. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/icons/icon-32.png +0 -0
  151. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/icons/icon-48.png +0 -0
  152. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/browser-api.ts +0 -0
  153. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/classes/CommandGroup.ts +0 -0
  154. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/classes/CommandRegistry.ts +0 -0
  155. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/classes/NativeConnection.ts +0 -0
  156. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/autosave.ts +0 -0
  157. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/browser-data.ts +0 -0
  158. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/extension.ts +0 -0
  159. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/groups.ts +0 -0
  160. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/injected.ts +0 -0
  161. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/navigation.ts +0 -0
  162. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/session-snapshot.ts +0 -0
  163. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/session.ts +0 -0
  164. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/tabs-query.ts +0 -0
  165. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/commands/tabs.ts +0 -0
  166. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/dispatch.ts +0 -0
  167. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/dom-ops.ts +0 -0
  168. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/extract.ts +0 -0
  169. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/markdown/code.ts +0 -0
  170. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/markdown/index.ts +0 -0
  171. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/markdown/renderer.ts +0 -0
  172. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/markdown/root.ts +0 -0
  173. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/markdown/utils.ts +0 -0
  174. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content/page-info.ts +0 -0
  175. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/content-dispatch.ts +0 -0
  176. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/core/errors.ts +0 -0
  177. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/core/group-helpers.ts +0 -0
  178. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/core/index.ts +0 -0
  179. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/core/scripting.ts +0 -0
  180. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/core/storage.ts +0 -0
  181. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/core/tab-groups.ts +0 -0
  182. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/core/tab-helpers.ts +0 -0
  183. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/core/throttle.ts +0 -0
  184. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/index.ts +0 -0
  185. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/types/command-args.ts +0 -0
  186. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/types/index.ts +0 -0
  187. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/types/jobs.ts +0 -0
  188. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/types/json.ts +0 -0
  189. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/types/messages.ts +0 -0
  190. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/types/session.ts +0 -0
  191. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/types/tabs.ts +0 -0
  192. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/src/types/webextension.ts +0 -0
  193. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/test/autosave.test.ts +0 -0
  194. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/test/browser-api.test.ts +0 -0
  195. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/test/chrome-mock.ts +0 -0
  196. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/test/clients.test.ts +0 -0
  197. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/test/navigation.test.ts +0 -0
  198. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/test/paging.test.ts +0 -0
  199. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/extension/test/url-match.test.ts +0 -0
  200. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/justfile +0 -0
  201. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/.claude/settings.local.json +0 -0
  202. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/.gitignore +0 -0
  203. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/README.md +0 -0
  204. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/credentials/BrowserCliApi.credentials.ts +0 -0
  205. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/BrowserCli.node.ts +0 -0
  206. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/browserCli.svg +0 -0
  207. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/protocol.ts +0 -0
  208. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/request.ts +0 -0
  209. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/serveClient.ts +0 -0
  210. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/scripts/copy-assets.mjs +0 -0
  211. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/scripts/run-tests.mjs +0 -0
  212. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/test/protocol.test.ts +0 -0
  213. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/test/request.test.ts +0 -0
  214. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/test-dist/protocol.test.mjs +0 -0
  215. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/test-dist/request.test.mjs +0 -0
  216. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/scripts/gen_extension_key.py +0 -0
  217. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/scripts/package_extension.py +0 -0
  218. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/scripts/package_verified_crx.sh +0 -0
  219. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/scripts/setup_verified_crx_key.sh +0 -0
  220. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/shell.nix +0 -0
  221. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_auth.py +0 -0
  222. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_commands_cli.py +0 -0
  223. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_compat.py +0 -0
  224. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_dom.py +0 -0
  225. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_extension_error_page_handling.py +0 -0
  226. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_extension_packaging.py +0 -0
  227. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_extract.py +0 -0
  228. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_framing.py +0 -0
  229. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_groups.py +0 -0
  230. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_known_hosts.py +0 -0
  231. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_markdown_security.py +0 -0
  232. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_native_host.py +0 -0
  233. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_nav.py +0 -0
  234. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_page.py +0 -0
  235. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_perf.py +0 -0
  236. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_performance_integration.py +0 -0
  237. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_platform.py +0 -0
  238. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_refactor_boundaries.py +0 -0
  239. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_registry.py +0 -0
  240. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_remote_protocol_matrix.py +0 -0
  241. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_rendering.py +0 -0
  242. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_serve.py +0 -0
  243. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_session.py +0 -0
  244. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_storage.py +0 -0
  245. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_tabs.py +0 -0
  246. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_transport.py +0 -0
  247. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tests/test_windows.py +0 -0
  248. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/tsconfig.json +0 -0
  249. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/webstore-assets/screenshots/screenshot-1.png +0 -0
  250. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/webstore-assets/screenshots/screenshot-2.png +0 -0
  251. {real_browser_cli-0.16.4 → real_browser_cli-0.16.9}/webstore-assets/screenshots/screenshot-3.png +0 -0
@@ -3,7 +3,7 @@
3
3
  ## Memory laden
4
4
  Beim Start dieser Session: Memory-Datei lesen für vollständigen Codebase-Kontext:
5
5
  ```
6
- /home/daniel156161/Dokumente/Nodes - Obsidian/AI-Worker/03 Bereiche/Open Source & Side Projects/browser-cli.md
6
+ 03 Bereiche/Open Source & Side Projects/browser-cli.md
7
7
  ```
8
8
 
9
9
  Memory nach Änderungen aktualisieren (neue Commands, Architektur-Änderungen, neue Patterns, Version-Bumps).
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: real-browser-cli
3
- Version: 0.16.4
3
+ Version: 0.16.9
4
4
  Summary: Control your real running browser from the terminal or Python SDK
5
5
  Project-URL: Homepage, https://git.yiprawr.dev/Automatisation/browser-cli
6
6
  Project-URL: Repository, https://git.yiprawr.dev/Automatisation/browser-cli
@@ -90,6 +90,8 @@ Requires-Dist: questionary>=2
90
90
  Requires-Dist: rich>=13
91
91
  Provides-Extra: fast
92
92
  Requires-Dist: zstandard>=0.22; extra == 'fast'
93
+ Provides-Extra: mcp
94
+ Requires-Dist: mcp<3,>=2; extra == 'mcp'
93
95
  Description-Content-Type: text/markdown
94
96
 
95
97
  # browser-cli
@@ -164,6 +166,11 @@ For better remote-response compression, install the optional `fast` extra:
164
166
  uv tool install "real-browser-cli[fast]"
165
167
  ```
166
168
 
169
+ To expose the conservative MCP tool surface, install the optional `mcp` extra:
170
+ ```sh
171
+ uv tool install "real-browser-cli[mcp]"
172
+ ```
173
+
167
174
  To upgrade later:
168
175
 
169
176
  ```sh
@@ -235,6 +242,104 @@ browser-cli/
235
242
 
236
243
  ---
237
244
 
245
+ ## Stateless MCP server
246
+ The optional MCP adapter exposes a small, typed subset of the Python SDK for
247
+ MCP hosts such as Claude Desktop, Claude Code, Cursor, or VS Code. It controls
248
+ the same real browser; it does not launch a headless browser or duplicate the
249
+ browser command implementation.
250
+
251
+ Install and run the local stdio server:
252
+ ```sh
253
+ uv tool install "real-browser-cli[mcp]"
254
+ browser-cli-mcp
255
+ ```
256
+
257
+ Example MCP host configuration:
258
+ ```json
259
+ {
260
+ "mcpServers": {
261
+ "browser-cli": {
262
+ "command": "browser-cli-mcp"
263
+ }
264
+ }
265
+ }
266
+ ```
267
+
268
+ The server is stateless at the MCP layer. Every tool call creates a fresh
269
+ `BrowserCLI` SDK client, and browser state remains in the real browser. Pass
270
+ `browser`, `remote`, and `key` on a tool call when a specific local profile or
271
+ authenticated browser-cli remote is required.
272
+
273
+ `browser_navigate`, `browser_tabs_close`, and `browser_screenshot` take an
274
+ optional `tab_id` and act on the active tab when it is omitted, so a caller
275
+ does not need a preceding `browser_tabs_list` round trip. `browser_tabs_close`
276
+ reports the tab it closed.
277
+
278
+ `browser_tabs_open` does not duplicate pages. It takes over an already-open tab
279
+ whose URL is nearly the same and reports `reused` next to the tab metadata, so
280
+ an agent that re-opens a page keeps working in the tab the user already has.
281
+ The `reuse` argument selects the match:
282
+
283
+ | `reuse` | Matches |
284
+ |---------|---------|
285
+ | `similar` (default) | Same site and page, ignoring fragment, trailing slash, query order, `www.` and default ports — plus any tab of the site when the requested URL is its root |
286
+ | `exact` | Only a character-for-character identical URL |
287
+ | `origin` | Any tab of the same site, whatever its path |
288
+ | `none` | Never reuse, always open a new tab |
289
+
290
+ A reused tab is only navigated when it is not already on the requested page, so
291
+ no reload throws away page state.
292
+
293
+ Available tools:
294
+ - `browser_tabs_list`, `browser_tabs_open`, `browser_tabs_close`
295
+ - `browser_navigate`, `browser_page_info`
296
+ - `browser_extract_text`, `browser_extract_markdown`
297
+ - `browser_dom_query`, `browser_dom_click`, `browser_dom_type`
298
+ - `browser_screenshot`
299
+
300
+ Generic JavaScript evaluation, raw browser commands, storage writes, and
301
+ session import are intentionally not exposed.
302
+
303
+ ### Pinning one browser and naming tools
304
+ Set `BROWSER_CLI_PROFILE` to pin every call from an MCP server to one browser,
305
+ so tools do not have to pass `browser` and listing tools do not fan out across
306
+ all connected browsers:
307
+ ```json
308
+ {
309
+ "mcpServers": {
310
+ "browser-cli-testing": {
311
+ "command": "browser-cli-mcp",
312
+ "env": {
313
+ "BROWSER_CLI_PROFILE": "testing",
314
+ "BROWSER_CLI_MCP_TOOL_PREFIX": ""
315
+ }
316
+ }
317
+ }
318
+ }
319
+ ```
320
+ `BROWSER_CLI_REMOTE` and `BROWSER_CLI_KEY` pin an authenticated remote the same
321
+ way. Explicit `browser`, `remote`, and `key` tool arguments still win.
322
+
323
+ Tool names carry a `browser_` prefix by default so they stay unambiguous in
324
+ hosts that expose raw MCP tool names. Hosts that already prefix tools with the
325
+ server name produce stutter such as `browser_cli_testing_browser_tabs_list`;
326
+ setting `BROWSER_CLI_MCP_TOOL_PREFIX` to an empty string drops the built-in
327
+ prefix and yields `browser_cli_testing_tabs_list`. Any other value replaces the
328
+ prefix.
329
+
330
+ For local development and testing, Streamable HTTP is also available:
331
+ ```sh
332
+ browser-cli-mcp --transport streamable-http --port 8000
333
+ # endpoint: http://127.0.0.1:8000/mcp
334
+ ```
335
+
336
+ HTTP uses stateless JSON responses and intentionally refuses non-loopback bind
337
+ addresses because this MCP endpoint has no independent authentication. For a
338
+ browser on another machine, keep MCP local and pass an authenticated
339
+ browser-cli `remote` target to each tool call.
340
+
341
+ ---
342
+
238
343
  ## CLI reference
239
344
  During source development, commands are usually run as `uv run browser-cli [--browser ALIAS] <command>`. After tool installation, use `browser-cli ...` directly. Add `--remote HOST[:PORT]` and optionally `--key PATH` to target a browser exposed by `browser-cli serve`.
240
345
 
@@ -250,6 +355,12 @@ browser-cli nav open https://example.com --focus # bring opened tab/w
250
355
  browser-cli nav open https://example.com --window work # into a named window
251
356
  browser-cli nav open https://example.com --group research # into a tab group (name or ID)
252
357
 
358
+ # Reuse an open tab instead of duplicating a page
359
+ browser-cli nav open https://example.com/app --reuse # nearly identical URL (fragment, trailing slash, query order, www)
360
+ browser-cli nav open https://example.com/app --reuse-exact # only a character-for-character match
361
+ browser-cli nav open https://example.com/app --reuse-domain # any tab of the same site
362
+ browser-cli nav open https://example.com/app --reuse-title "App" # tab whose title contains the text
363
+
253
364
  # Reload
254
365
  browser-cli nav reload # reload active tab
255
366
  browser-cli nav reload 1234 # reload tab by ID
@@ -462,6 +573,9 @@ b.nav.search("google", "python asyncio")
462
573
  tabs = b.tabs.list() # list[Tab]; in multi-browser mode each tab.browser is set
463
574
  tab = b.tabs.open("https://example.com") # returns a bound Tab object
464
575
  tab = b.tabs.open("https://example.com", wait=True, timeout=10)
576
+ tab = b.tabs.open("https://example.com/app", reuse=True) # reuse a tab with a nearly identical URL
577
+ tab = b.tabs.open("https://example.com/app", reuse="origin") # reuse any tab of that site
578
+ existing = b.nav.reusable_tab("https://example.com/app", reuse=True) # Tab or None, without touching the browser
465
579
  active = b.tabs.active() # active Tab object
466
580
  tab = b.tabs.get(1234) # tab by ID
467
581
  tab = b.tabs.first("github") # first matching tab or None
@@ -70,6 +70,11 @@ For better remote-response compression, install the optional `fast` extra:
70
70
  uv tool install "real-browser-cli[fast]"
71
71
  ```
72
72
 
73
+ To expose the conservative MCP tool surface, install the optional `mcp` extra:
74
+ ```sh
75
+ uv tool install "real-browser-cli[mcp]"
76
+ ```
77
+
73
78
  To upgrade later:
74
79
 
75
80
  ```sh
@@ -141,6 +146,104 @@ browser-cli/
141
146
 
142
147
  ---
143
148
 
149
+ ## Stateless MCP server
150
+ The optional MCP adapter exposes a small, typed subset of the Python SDK for
151
+ MCP hosts such as Claude Desktop, Claude Code, Cursor, or VS Code. It controls
152
+ the same real browser; it does not launch a headless browser or duplicate the
153
+ browser command implementation.
154
+
155
+ Install and run the local stdio server:
156
+ ```sh
157
+ uv tool install "real-browser-cli[mcp]"
158
+ browser-cli-mcp
159
+ ```
160
+
161
+ Example MCP host configuration:
162
+ ```json
163
+ {
164
+ "mcpServers": {
165
+ "browser-cli": {
166
+ "command": "browser-cli-mcp"
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ The server is stateless at the MCP layer. Every tool call creates a fresh
173
+ `BrowserCLI` SDK client, and browser state remains in the real browser. Pass
174
+ `browser`, `remote`, and `key` on a tool call when a specific local profile or
175
+ authenticated browser-cli remote is required.
176
+
177
+ `browser_navigate`, `browser_tabs_close`, and `browser_screenshot` take an
178
+ optional `tab_id` and act on the active tab when it is omitted, so a caller
179
+ does not need a preceding `browser_tabs_list` round trip. `browser_tabs_close`
180
+ reports the tab it closed.
181
+
182
+ `browser_tabs_open` does not duplicate pages. It takes over an already-open tab
183
+ whose URL is nearly the same and reports `reused` next to the tab metadata, so
184
+ an agent that re-opens a page keeps working in the tab the user already has.
185
+ The `reuse` argument selects the match:
186
+
187
+ | `reuse` | Matches |
188
+ |---------|---------|
189
+ | `similar` (default) | Same site and page, ignoring fragment, trailing slash, query order, `www.` and default ports — plus any tab of the site when the requested URL is its root |
190
+ | `exact` | Only a character-for-character identical URL |
191
+ | `origin` | Any tab of the same site, whatever its path |
192
+ | `none` | Never reuse, always open a new tab |
193
+
194
+ A reused tab is only navigated when it is not already on the requested page, so
195
+ no reload throws away page state.
196
+
197
+ Available tools:
198
+ - `browser_tabs_list`, `browser_tabs_open`, `browser_tabs_close`
199
+ - `browser_navigate`, `browser_page_info`
200
+ - `browser_extract_text`, `browser_extract_markdown`
201
+ - `browser_dom_query`, `browser_dom_click`, `browser_dom_type`
202
+ - `browser_screenshot`
203
+
204
+ Generic JavaScript evaluation, raw browser commands, storage writes, and
205
+ session import are intentionally not exposed.
206
+
207
+ ### Pinning one browser and naming tools
208
+ Set `BROWSER_CLI_PROFILE` to pin every call from an MCP server to one browser,
209
+ so tools do not have to pass `browser` and listing tools do not fan out across
210
+ all connected browsers:
211
+ ```json
212
+ {
213
+ "mcpServers": {
214
+ "browser-cli-testing": {
215
+ "command": "browser-cli-mcp",
216
+ "env": {
217
+ "BROWSER_CLI_PROFILE": "testing",
218
+ "BROWSER_CLI_MCP_TOOL_PREFIX": ""
219
+ }
220
+ }
221
+ }
222
+ }
223
+ ```
224
+ `BROWSER_CLI_REMOTE` and `BROWSER_CLI_KEY` pin an authenticated remote the same
225
+ way. Explicit `browser`, `remote`, and `key` tool arguments still win.
226
+
227
+ Tool names carry a `browser_` prefix by default so they stay unambiguous in
228
+ hosts that expose raw MCP tool names. Hosts that already prefix tools with the
229
+ server name produce stutter such as `browser_cli_testing_browser_tabs_list`;
230
+ setting `BROWSER_CLI_MCP_TOOL_PREFIX` to an empty string drops the built-in
231
+ prefix and yields `browser_cli_testing_tabs_list`. Any other value replaces the
232
+ prefix.
233
+
234
+ For local development and testing, Streamable HTTP is also available:
235
+ ```sh
236
+ browser-cli-mcp --transport streamable-http --port 8000
237
+ # endpoint: http://127.0.0.1:8000/mcp
238
+ ```
239
+
240
+ HTTP uses stateless JSON responses and intentionally refuses non-loopback bind
241
+ addresses because this MCP endpoint has no independent authentication. For a
242
+ browser on another machine, keep MCP local and pass an authenticated
243
+ browser-cli `remote` target to each tool call.
244
+
245
+ ---
246
+
144
247
  ## CLI reference
145
248
  During source development, commands are usually run as `uv run browser-cli [--browser ALIAS] <command>`. After tool installation, use `browser-cli ...` directly. Add `--remote HOST[:PORT]` and optionally `--key PATH` to target a browser exposed by `browser-cli serve`.
146
249
 
@@ -156,6 +259,12 @@ browser-cli nav open https://example.com --focus # bring opened tab/w
156
259
  browser-cli nav open https://example.com --window work # into a named window
157
260
  browser-cli nav open https://example.com --group research # into a tab group (name or ID)
158
261
 
262
+ # Reuse an open tab instead of duplicating a page
263
+ browser-cli nav open https://example.com/app --reuse # nearly identical URL (fragment, trailing slash, query order, www)
264
+ browser-cli nav open https://example.com/app --reuse-exact # only a character-for-character match
265
+ browser-cli nav open https://example.com/app --reuse-domain # any tab of the same site
266
+ browser-cli nav open https://example.com/app --reuse-title "App" # tab whose title contains the text
267
+
159
268
  # Reload
160
269
  browser-cli nav reload # reload active tab
161
270
  browser-cli nav reload 1234 # reload tab by ID
@@ -368,6 +477,9 @@ b.nav.search("google", "python asyncio")
368
477
  tabs = b.tabs.list() # list[Tab]; in multi-browser mode each tab.browser is set
369
478
  tab = b.tabs.open("https://example.com") # returns a bound Tab object
370
479
  tab = b.tabs.open("https://example.com", wait=True, timeout=10)
480
+ tab = b.tabs.open("https://example.com/app", reuse=True) # reuse a tab with a nearly identical URL
481
+ tab = b.tabs.open("https://example.com/app", reuse="origin") # reuse any tab of that site
482
+ existing = b.nav.reusable_tab("https://example.com/app", reuse=True) # Tab or None, without touching the browser
371
483
  active = b.tabs.active() # active Tab object
372
484
  tab = b.tabs.get(1234) # tab by ID
373
485
  tab = b.tabs.first("github") # first matching tab or None
@@ -151,21 +151,25 @@ def active_browser_targets(*, include_remotes: bool = True, key=None, suppress_p
151
151
  targets.extend(_remote_browser_targets(key=key, suppress_pq_warning=suppress_pq_warning))
152
152
  return targets
153
153
 
154
- def _cached_client_row(target: BrowserTarget) -> dict | None:
154
+ def _cached_client_row(target: BrowserTarget, *, scoped: bool = False) -> dict | None:
155
155
  """Build a clients row from a target's discovery data, skipping a roundtrip.
156
156
 
157
157
  Returns None when the remote didn't advertise its version (older serve), so
158
- callers fall back to an explicit ``clients.list`` query.
158
+ callers fall back to an explicit ``clients.list`` query. When *scoped* is
159
+ true, the caller already selected one remote host, so render profile-only
160
+ labels instead of adding a host group header.
159
161
  """
160
162
  if target.version is None and target.extension_version is None:
161
163
  return None
162
- return {
163
- "profile": target.display_name,
164
- "profileGroup": target.display_group,
164
+ row = {
165
+ "profile": target.profile if scoped else target.display_name,
165
166
  "name": target.browser_name or "",
166
167
  "version": target.version or "",
167
168
  "extensionVersion": target.extension_version or "",
168
169
  }
170
+ if target.display_group and not scoped:
171
+ row["profileGroup"] = target.display_group
172
+ return row
169
173
 
170
174
  def _rows_from_result(result, label: str, profile_group: str | None) -> list[dict]:
171
175
  rows = []
@@ -249,18 +253,17 @@ def collect_browser_clients(
249
253
  if targets:
250
254
  uncached = []
251
255
  for target in targets:
252
- cached = _cached_client_row(target)
256
+ cached = _cached_client_row(target, scoped=True)
253
257
  if cached is not None:
254
258
  rows.append(cached)
255
259
  else:
256
260
  uncached.append(target)
257
261
  results = _run_concurrent([
258
262
  (lambda t=t: _client_rows_async(
259
- t.display_name,
263
+ t.profile,
260
264
  profile=t.profile,
261
265
  remote=remote,
262
266
  key=key,
263
- profile_group=t.display_group,
264
267
  ))
265
268
  for t in uncached
266
269
  ])
@@ -5,8 +5,8 @@ import uuid
5
5
  from typing import Any
6
6
 
7
7
  from browser_cli import transport
8
- from browser_cli.endpoints import _normalize_endpoint
9
8
  from browser_cli.errors import BrowserNotConnected
9
+ from browser_cli.remote.registry import resolve_remote_endpoint
10
10
 
11
11
  def base_message(command: str, args: dict | None) -> dict:
12
12
  return {"id": str(uuid.uuid4()), "command": command, "args": args or {}}
@@ -14,7 +14,7 @@ def base_message(command: str, args: dict | None) -> dict:
14
14
  def requested_target(profile: str | None, remote: str | None) -> tuple[str | None, str | None]:
15
15
  requested_profile = profile or os.environ.get("BROWSER_CLI_PROFILE")
16
16
  remote_endpoint = remote or os.environ.get("BROWSER_CLI_REMOTE")
17
- return requested_profile, _normalize_endpoint(remote_endpoint) if remote_endpoint else None
17
+ return requested_profile, resolve_remote_endpoint(remote_endpoint) if remote_endpoint else None
18
18
 
19
19
  def encode_payload(msg: dict) -> bytes:
20
20
  return json.dumps(msg).encode("utf-8")
@@ -1,7 +1,7 @@
1
1
  import json
2
2
 
3
3
  import click
4
- from browser_cli.commands import client_from_ctx, handle_errors
4
+ from browser_cli.commands import client_from_ctx, handle_errors, tab_option
5
5
  from rich.console import Console
6
6
  from rich.table import Table
7
7
 
@@ -12,10 +12,11 @@ def extract_group():
12
12
  """Extract content from the active tab."""
13
13
 
14
14
  @extract_group.command("links")
15
+ @tab_option
15
16
  @handle_errors
16
- def extract_links():
17
- """Extract all links from the active tab."""
18
- links = client_from_ctx().extract.links()
17
+ def extract_links(tab_id):
18
+ """Extract all links from the active tab or --tab."""
19
+ links = client_from_ctx().extract.links(tab_id)
19
20
  if not links:
20
21
  console.print("[yellow]No links found[/yellow]")
21
22
  return
@@ -27,10 +28,11 @@ def extract_links():
27
28
  console.print(table)
28
29
 
29
30
  @extract_group.command("images")
31
+ @tab_option
30
32
  @handle_errors
31
- def extract_images():
32
- """Extract all images from the active tab."""
33
- images = client_from_ctx().extract.images()
33
+ def extract_images(tab_id):
34
+ """Extract all images from the active tab or --tab."""
35
+ images = client_from_ctx().extract.images(tab_id)
34
36
  if not images:
35
37
  console.print("[yellow]No images found[/yellow]")
36
38
  return
@@ -42,29 +44,33 @@ def extract_images():
42
44
  console.print(table)
43
45
 
44
46
  @extract_group.command("text")
47
+ @tab_option
45
48
  @handle_errors
46
- def extract_text():
47
- """Extract all visible text from the active tab."""
48
- console.print(client_from_ctx().extract.text())
49
+ def extract_text(tab_id):
50
+ """Extract all visible text from the active tab or --tab."""
51
+ console.print(client_from_ctx().extract.text(tab_id))
49
52
 
50
53
  @extract_group.command("json")
51
54
  @click.argument("selector")
55
+ @tab_option
52
56
  @handle_errors
53
- def extract_json(selector):
54
- """Parse and pretty-print JSON content inside SELECTOR."""
55
- data = client_from_ctx().extract.json(selector)
57
+ def extract_json(selector, tab_id):
58
+ """Parse and pretty-print JSON content inside SELECTOR in the active tab or --tab."""
59
+ data = client_from_ctx().extract.json(selector, tab_id)
56
60
  console.print_json(json.dumps(data))
57
61
 
58
62
  @extract_group.command("html")
63
+ @tab_option
59
64
  @handle_errors
60
- def extract_html():
61
- """Print the full HTML of the active tab to stdout."""
62
- click.echo(client_from_ctx().extract.html())
65
+ def extract_html(tab_id):
66
+ """Print the full HTML of the active tab or --tab to stdout."""
67
+ click.echo(client_from_ctx().extract.html(tab_id))
63
68
 
64
69
  @extract_group.command("markdown")
65
70
  @click.option("--selector", help="Extract only the DOM subtree matching this CSS selector.")
71
+ @tab_option
66
72
  @handle_errors
67
- def extract_markdown(selector):
68
- """Extract the page's main content as Markdown."""
69
- markdown = client_from_ctx().extract.markdown(selector)
73
+ def extract_markdown(selector, tab_id):
74
+ """Extract the page's main content as Markdown from the active tab or --tab."""
75
+ markdown = client_from_ctx().extract.markdown(selector, tab_id)
70
76
  click.echo(markdown or "", nl=not (markdown or "").endswith("\n"))
@@ -13,13 +13,14 @@ def nav_group():
13
13
  @click.option("--focus", is_flag=True, help="Bring the opened tab/window to the front")
14
14
  @click.option("--window", "window_name", default=None, help="Open in named window")
15
15
  @click.option("--group", "group_name", default=None, help="Open directly into a tab group (name or ID)")
16
- @click.option("--reuse", is_flag=True, help="Reuse an existing tab with exactly this URL")
16
+ @click.option("--reuse", is_flag=True, help="Reuse an existing tab with a nearly identical URL (ignores fragment, trailing slash, query order)")
17
+ @click.option("--reuse-exact", is_flag=True, help="Reuse an existing tab only when its URL matches character for character")
17
18
  @click.option("--reuse-domain", is_flag=True, help="Reuse an existing tab with the same domain")
18
19
  @click.option("--reuse-title", default=None, metavar="TEXT", help="Reuse an existing tab whose title contains TEXT")
19
20
  @handle_errors
20
- def cmd_open(url, focus, window_name, group_name, reuse, reuse_domain, reuse_title):
21
+ def cmd_open(url, focus, window_name, group_name, reuse, reuse_exact, reuse_domain, reuse_title):
21
22
  """Open URL in a new tab without stealing focus by default."""
22
- client_from_ctx().nav.open(url, focus=focus, window=window_name, group=group_name, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
23
+ client_from_ctx().nav.open(url, focus=focus, window=window_name, group=group_name, reuse="exact" if reuse_exact else reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
23
24
  suffix = ""
24
25
  if group_name:
25
26
  suffix = f" in group '{group_name}'"
@@ -76,13 +77,14 @@ def cmd_focus(pattern):
76
77
  @click.option("--focus", is_flag=True, help="Bring the opened tab/window to the front")
77
78
  @click.option("--window", "window_name", default=None, help="Open in named window")
78
79
  @click.option("--group", "group_name", default=None, help="Open in tab group")
79
- @click.option("--reuse", is_flag=True, help="Reuse an existing tab with exactly this URL")
80
+ @click.option("--reuse", is_flag=True, help="Reuse an existing tab with a nearly identical URL (ignores fragment, trailing slash, query order)")
81
+ @click.option("--reuse-exact", is_flag=True, help="Reuse an existing tab only when its URL matches character for character")
80
82
  @click.option("--reuse-domain", is_flag=True, help="Reuse an existing tab with the same domain")
81
83
  @click.option("--reuse-title", default=None, metavar="TEXT", help="Reuse an existing tab whose title contains TEXT")
82
84
  @handle_errors
83
- def cmd_open_wait(url, timeout, focus, window_name, group_name, reuse, reuse_domain, reuse_title):
85
+ def cmd_open_wait(url, timeout, focus, window_name, group_name, reuse, reuse_exact, reuse_domain, reuse_title):
84
86
  """Open URL in a new tab and wait until fully loaded."""
85
- tab = client_from_ctx().nav.open_wait(url, timeout=timeout, focus=focus, window=window_name, group=group_name, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
87
+ tab = client_from_ctx().nav.open_wait(url, timeout=timeout, focus=focus, window=window_name, group=group_name, reuse="exact" if reuse_exact else reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
86
88
  console.print(f"[green]Loaded:[/green] {url}" + (f" — {tab.title}" if tab.title else ""))
87
89
 
88
90
  @nav_group.command("wait")
@@ -1,5 +1,5 @@
1
1
  import click
2
- from browser_cli.commands import client_from_ctx, handle_errors
2
+ from browser_cli.commands import client_from_ctx, handle_errors, tab_option
3
3
  from rich.console import Console
4
4
  from rich.table import Table
5
5
 
@@ -10,10 +10,11 @@ def page_group():
10
10
  """Inspect current page metadata."""
11
11
 
12
12
  @page_group.command("info")
13
+ @tab_option
13
14
  @handle_errors
14
- def page_info():
15
- """Show title, URL, readyState, language, and meta tags of the active tab."""
16
- info = client_from_ctx().page.info()
15
+ def page_info(tab_id):
16
+ """Show title, URL, readyState, language, and meta tags of the active tab or --tab."""
17
+ info = client_from_ctx().page.info(tab_id)
17
18
  table = Table(show_header=False)
18
19
  table.add_column("Field", style="bold cyan", no_wrap=True)
19
20
  table.add_column("Value")
@@ -0,0 +1,5 @@
1
+ """Stateless Model Context Protocol adapter for browser-cli."""
2
+
3
+ from browser_cli.mcp.server import create_server, main
4
+
5
+ __all__ = ["create_server", "main"]
@@ -0,0 +1,39 @@
1
+ """Tool-name prefixing for the MCP surface.
2
+
3
+ Hosts differ in how they namespace MCP tools. Hosts that already prefix tool
4
+ names with the server name turn the built-in ``browser_`` prefix into stutter
5
+ (``browser_cli_testing_browser_tabs_list``), while hosts that expose raw names
6
+ need the prefix to keep ``navigate`` or ``screenshot`` unambiguous. The prefix
7
+ is therefore configurable per MCP server process.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import re
13
+
14
+ TOOL_PREFIX_ENV = "BROWSER_CLI_MCP_TOOL_PREFIX"
15
+ DEFAULT_TOOL_PREFIX = "browser_"
16
+
17
+ _VALID_PREFIX = re.compile(r"\A[a-z][a-z0-9_]*\Z")
18
+
19
+ def resolve_tool_prefix(environ: dict[str, str] | None = None) -> str:
20
+ """Return the configured tool-name prefix, defaulting to ``browser_``.
21
+
22
+ An empty value disables prefixing for hosts that namespace tools themselves.
23
+ """
24
+ configured = (environ if environ is not None else os.environ).get(TOOL_PREFIX_ENV)
25
+ if configured is None:
26
+ return DEFAULT_TOOL_PREFIX
27
+ prefix = configured.strip()
28
+ if not prefix:
29
+ return ""
30
+ if not _VALID_PREFIX.match(prefix):
31
+ raise ValueError(
32
+ f"{TOOL_PREFIX_ENV} must be lowercase letters, digits, and underscores starting "
33
+ f"with a letter, or empty to disable prefixing; got {configured!r}"
34
+ )
35
+ return prefix if prefix.endswith("_") else f"{prefix}_"
36
+
37
+ def tool_name(base: str, prefix: str) -> str:
38
+ """Apply *prefix* to a bare tool name such as ``tabs_list``."""
39
+ return f"{prefix}{base}"
@@ -0,0 +1,19 @@
1
+ """Convert browser-cli SDK models into MCP structured-output values."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import fields, is_dataclass
5
+ from typing import Any
6
+
7
+ def structured(value: Any) -> Any:
8
+ """Return JSON-compatible data without private SDK binding fields."""
9
+ if is_dataclass(value) and not isinstance(value, type):
10
+ return {
11
+ field.name: structured(getattr(value, field.name))
12
+ for field in fields(value)
13
+ if not field.name.startswith("_")
14
+ }
15
+ if isinstance(value, dict):
16
+ return {str(key): structured(item) for key, item in value.items()}
17
+ if isinstance(value, (list, tuple, set)):
18
+ return [structured(item) for item in value]
19
+ return value