Default MCP tab tools to the active tab
Requiring an explicit tab_id forced every navigate or close through a preceding tabs_list call, which costs an MCP client a full round trip just to learn the ID the browser already considers current. navigate and tabs_close now resolve the active tab when tab_id is omitted, matching the screenshot tool. Resolution is explicit rather than forwarding None into the SDK, so the acting tool knows which tab it touched; tabs_close reports it, since closing the wrong tab is not recoverable. This stays in the MCP layer: the SDK and CLI signatures are unchanged.
This commit is contained in:
@@ -26,6 +26,7 @@ class FakeClient:
|
||||
list=self.tabs_list,
|
||||
open=self.tabs_open,
|
||||
close=self.tabs_close,
|
||||
active=self.tabs_active,
|
||||
status=self.tabs_status,
|
||||
screenshot=self.tabs_screenshot,
|
||||
)
|
||||
@@ -46,6 +47,10 @@ class FakeClient:
|
||||
self.calls.append(("close", tab_id))
|
||||
return 1
|
||||
|
||||
def tabs_active(self):
|
||||
self.calls.append(("active",))
|
||||
return SimpleNamespace(id=7)
|
||||
|
||||
def tabs_status(self, tab_id):
|
||||
self.calls.append(("status", tab_id))
|
||||
return {"id": tab_id, "title": "Navigated", "url": "https://example.com/next"}
|
||||
@@ -178,6 +183,18 @@ async def test_mutating_tools_use_sdk_and_return_fresh_state(client):
|
||||
("navigate", 8, "https://example.com/next"), ("status", 8)
|
||||
]
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tab_tools_default_to_the_active_tab(client):
|
||||
navigated = await client.call_tool("browser_navigate", {"url": "https://example.com/next"})
|
||||
closed = await client.call_tool("browser_tabs_close", {})
|
||||
|
||||
assert navigated.structured_content["id"] == 7
|
||||
assert closed.structured_content == {"closed": 1, "tab_id": 7}
|
||||
assert FakeClient.instances[0].calls == [
|
||||
("active",), ("navigate", 7, "https://example.com/next"), ("status", 7)
|
||||
]
|
||||
assert FakeClient.instances[1].calls == [("active",), ("close", 7)]
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_screenshot_returns_image_content(client):
|
||||
result = await client.call_tool("browser_screenshot", {"tab_id": 7, "format": "png"})
|
||||
|
||||
@@ -43,6 +43,39 @@ def test_checkin_caps_pool_size():
|
||||
b.close()
|
||||
pool.close_all()
|
||||
|
||||
def test_checkin_caps_endpoint_buckets():
|
||||
pool.close_all()
|
||||
peers = []
|
||||
try:
|
||||
for i in range(pool._MAX_ENDPOINTS + 5):
|
||||
a, b = _socketpair()
|
||||
peers.append(b)
|
||||
pool.checkin(f"host-{i}:443", pool.PooledConnection(a, b"secret"))
|
||||
assert len(pool._POOL) <= pool._MAX_ENDPOINTS
|
||||
finally:
|
||||
for peer in peers:
|
||||
peer.close()
|
||||
pool.close_all()
|
||||
|
||||
def test_checkin_prunes_stale_endpoint_buckets():
|
||||
pool.close_all()
|
||||
old_a, old_b = _socketpair()
|
||||
old = pool.PooledConnection(old_a, b"secret")
|
||||
pool.checkin("old:443", old)
|
||||
old.last_used -= pool._MAX_IDLE_SECONDS + 1
|
||||
peers = [old_b]
|
||||
try:
|
||||
for i in range(pool._MAX_ENDPOINTS):
|
||||
a, b = _socketpair()
|
||||
peers.append(b)
|
||||
pool.checkin(f"new-{i}:443", pool.PooledConnection(a, b"secret"))
|
||||
assert "old:443" not in pool._POOL
|
||||
assert len(pool._POOL) <= pool._MAX_ENDPOINTS
|
||||
finally:
|
||||
for peer in peers:
|
||||
peer.close()
|
||||
pool.close_all()
|
||||
|
||||
def test_session_inner_message_strips_auth_fields():
|
||||
msg = {
|
||||
"id": "1", "command": "tabs.list", "args": {}, "user_agent": "browser-cli/1",
|
||||
|
||||
@@ -181,6 +181,23 @@ def test_rate_limiter_is_per_key():
|
||||
assert limiter.allow("a") is False
|
||||
assert limiter.allow("b") is False
|
||||
|
||||
def test_rate_limiter_caps_identity_buckets():
|
||||
limiter = RateLimiter(rate=0.0001, burst=1, max_buckets=3)
|
||||
for i in range(10):
|
||||
assert limiter.allow(f"key-{i}") is True
|
||||
assert len(limiter._buckets) <= 3
|
||||
|
||||
def test_rate_limiter_prunes_refilled_idle_buckets(monkeypatch):
|
||||
current = 1000.0
|
||||
monkeypatch.setattr("browser_cli.serve.security.time.monotonic", lambda: current)
|
||||
limiter = RateLimiter(rate=1, burst=2, max_buckets=2)
|
||||
assert limiter.allow("old") is True
|
||||
current += 120.0
|
||||
assert limiter.allow("a") is True
|
||||
assert limiter.allow("b") is True
|
||||
assert "old" not in limiter._buckets
|
||||
assert len(limiter._buckets) <= 2
|
||||
|
||||
# ── ServeSecurity ────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_effective_policy_prefers_per_key_override():
|
||||
|
||||
Reference in New Issue
Block a user