Skip to content

feat: add get_console_logs tool for browser console output#454

Merged
shivammittal274 merged 2 commits intomainfrom
feat/console-tool
Mar 16, 2026
Merged

feat: add get_console_logs tool for browser console output#454
shivammittal274 merged 2 commits intomainfrom
feat/console-tool

Conversation

@shivammittal274
Copy link
Contributor

Summary

  • Adds get_console_logs MCP tool that surfaces browser console errors, warnings, and logs per page
  • Captures Runtime.consoleAPICalled, Runtime.exceptionThrown, and Log.entryAdded CDP events in per-page FIFO ring buffers (500 entries max)
  • Adds session-aware CDP event dispatching (onSessionEvent) to CdpBackend for O(1) routing — matches how Puppeteer/Playwright internally route session events via Map lookup
  • Tool supports filtering by level hierarchy (error ⊃ warning ⊃ info ⊃ debug), text search, limit, and optional buffer clear

New files

  • apps/server/src/browser/console-collector.tsConsoleCollector class with per-page buffers, event handlers, arg serialization
  • apps/server/src/tools/console.tsget_console_logs tool definition

Modified files

  • apps/server/src/browser/backends/cdp.ts — session-aware event dispatching in handleMessage
  • apps/server/src/browser/backends/types.tsonSessionEvent on CdpBackend interface
  • apps/server/src/browser/browser.tsConsoleCollector wiring, Log.enable(), getConsoleLogs()/clearConsoleLogs()
  • apps/server/src/tools/registry.ts — registered in Observation category
  • packages/shared/src/constants/limits.tsCONSOLE_BUFFER_MAX_ENTRIES, CONSOLE_DEFAULT_LIMIT, CONSOLE_MAX_LIMIT

Design decisions

  • Per-page collection (not global) — consistent with all other tools taking a page param
  • O(1) session routing via single handler + Map lookup, matching Puppeteer/Playwright internals
  • Buffer clears on main-frame navigation — simpler than Playwright's navigation marks or Chrome DevTools MCP's navigation slices; can add preservation as follow-up
  • No execution context dedup — BrowserOS controls the browser environment so duplicate contexts are unlikely

Test plan

  • Empty page returns No console output for page 1.
  • console.log/warn/error/debug/info all captured correctly
  • Log.entryAdded captured (favicon 404 from browser)
  • Runtime.exceptionThrown captured (uncaught throw new Error)
  • level: "error" returns only errors
  • search: "warning" text filter works
  • limit: 2 returns most recent entries with "showing 2 of 6" header
  • clear: true empties buffer (6 → 0)
  • Full page navigation clears buffer (2 → 0)

Captures Runtime.consoleAPICalled, Runtime.exceptionThrown, and
Log.entryAdded CDP events per page with a FIFO ring buffer (500 entries).

- ConsoleCollector: per-page buffers with O(1) session routing via Map lookup
- Session-aware CDP event dispatching (onSessionEvent) in CdpBackend
- Log.enable() added alongside Runtime.enable() in attachToPage
- Single tool with level hierarchy, text search, limit, and clear params
- Buffer clears on main-frame navigation, cleaned up on page close
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR adds a get_console_logs MCP tool that captures and surfaces browser console output (logs, warnings, errors, uncaught exceptions, and browser-level log entries) per page via CDP. The architecture is clean — a ConsoleCollector class maintains per-page FIFO ring buffers, with session-to-page routing handled in O(1) via a Map lookup hooked into a new onSessionEvent dispatcher on CdpBackend.

Key changes:

  • ConsoleCollector registers three CDP event sources (Runtime.consoleAPICalled, Runtime.exceptionThrown, Log.entryAdded) and auto-clears buffers on main-frame navigation via Page.frameNavigated
  • attachToPage in browser.ts now passes pageId down so the collector can be attached at the right time; detach() is called in listPages cleanup and closePage
  • The get_console_logs tool supports level hierarchy filtering, text search, entry limit, and an optional buffer clear

Issues found:

  • clear: true wipes the full buffer regardless of filters — calling getConsoleLogs({ level: 'error', clear: true }) silently discards all info/warning/debug entries that were never returned to the caller
  • detachedFromTarget does not call consoleCollector.detach() — stale session mappings persist in sessionToPage / pageToSession until the next attachToPage call
  • onSessionEvent cleanup functions are never stored or called — all four handler registrations in the ConsoleCollector constructor discard the returned unsubscribe functions, creating a resource-leak pattern
  • Object serialization falls back to CDP descriptionconsole.log({a: 1}) produces "Object" rather than any meaningful representation

Confidence Score: 3/5

  • Functional but has a data-loss risk with clear: true + filters and stale session state after target detach.
  • The core CDP wiring and session-routing logic is solid and the happy path works. However, the clear: true + filter combination can silently discard unread log entries, the detachedFromTarget handler leaves stale collector state, and the unsubscribed event handlers are a resource-leak pattern. None of these are crash-level bugs, but the data-loss scenario with clear: true is a real correctness concern that callers may not anticipate.
  • console-collector.ts (clear semantics and handler leaks) and browser.ts (detachedFromTarget cleanup)

Important Files Changed

Filename Overview
packages/browseros-agent/apps/server/src/browser/console-collector.ts New file implementing per-page console buffering with O(1) session routing. Three issues: (1) clear: true wipes the entire buffer regardless of active filters, potentially discarding non-returned entries; (2) all four onSessionEvent cleanup functions are discarded (resource leak); (3) object serialization falls back to the unhelpful CDP description field (e.g., "Object").
packages/browseros-agent/apps/server/src/browser/browser.ts Wires ConsoleCollector into Browser lifecycle. The detachedFromTarget handler removes the session from this.sessions but does not call consoleCollector.detach(), leaving stale session mappings in the collector until the next attachToPage call.
packages/browseros-agent/apps/server/src/browser/backends/cdp.ts Adds onSessionEvent method and session-aware dispatch in handleMessage. Clean implementation that mirrors Puppeteer/Playwright's internal routing pattern. No issues found.
packages/browseros-agent/apps/server/src/browser/backends/types.ts Adds onSessionEvent signature to the CdpBackend interface. Straightforward and correct.
packages/browseros-agent/apps/server/src/tools/console.ts New get_console_logs MCP tool with well-structured Zod schema, filtering, and formatted text output. Minor: the clear parameter description does not clarify that it clears the full buffer (not just the filtered entries), which may mislead callers.
packages/browseros-agent/apps/server/src/tools/registry.ts Registers get_console_logs in the Observation category and updates comment count to 9. No issues.
packages/browseros-agent/packages/shared/src/constants/limits.ts Adds three new constants: CONSOLE_BUFFER_MAX_ENTRIES (500), CONSOLE_DEFAULT_LIMIT (50), CONSOLE_MAX_LIMIT (200). Reasonable values, no issues.

Sequence Diagram

sequenceDiagram
    participant Tool as get_console_logs tool
    participant Browser
    participant CC as ConsoleCollector
    participant CDP as CdpBackend
    participant Chrome as Chrome (CDP)

    Note over CDP,Chrome: Per-session CDP event flow
    Chrome-->>CDP: Runtime.consoleAPICalled {sessionId}
    CDP->>CC: onSessionEvent handler(params, sessionId)
    CC->>CC: sessionToPage.get(sessionId) → pageId
    CC->>CC: addEntry(pageId, entry) into FIFO buffer

    Note over Tool,CC: Tool invocation
    Tool->>Browser: getConsoleLogs(page, opts)
    Browser->>Browser: resolveSession(page) → attachToPage if needed
    Browser->>CC: getLogs(pageId, opts)
    CC->>CC: filter by level + search, slice(-limit)
    CC-->>Browser: { entries, totalCount }
    Browser-->>Tool: result
    Tool-->>Tool: format text + response.data(...)

    Note over Browser,CC: Lifecycle hooks
    Browser->>CC: attach(pageId, sessionId) on new session
    Browser->>CC: detach(pageId) on tab close / listPages cleanup
Loading

Comments Outside Diff (1)

  1. packages/browseros-agent/apps/server/src/browser/browser.ts, line 109-118 (link)

    detachedFromTarget does not clean up the console collector

    When a target detaches (tab crash, process swap), the session is removed from this.sessions but this.consoleCollector.detach() is never called. This leaves stale entries in sessionToPage and pageToSession inside the collector until the next resolveSessionattachToPage cycle calls attach() and overwrites them.

    Between the detach event and the next resolveSession call, any CDP event that happens to arrive on the dead session ID would still match sessionToPage and incorrectly be appended to the page's buffer.

    Fix by calling detach() from the detachedFromTarget handler:

    this.cdp.Target.on('detachedFromTarget', (params) => {
      if (params.sessionId) {
        for (const [targetId, sid] of this.sessions) {
          if (sid === params.sessionId) {
            this.sessions.delete(targetId)
            // Find and clean up the corresponding page in the collector
            for (const [pageId, info] of this.pages) {
              if (info.targetId === targetId) {
                this.consoleCollector.detach(pageId)
                break
              }
            }
            break
          }
        }
      }
    })
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/browseros-agent/apps/server/src/browser/browser.ts
    Line: 109-118
    
    Comment:
    **`detachedFromTarget` does not clean up the console collector**
    
    When a target detaches (tab crash, process swap), the session is removed from `this.sessions` but `this.consoleCollector.detach()` is never called. This leaves stale entries in `sessionToPage` and `pageToSession` inside the collector until the next `resolveSession``attachToPage` cycle calls `attach()` and overwrites them.
    
    Between the detach event and the next `resolveSession` call, any CDP event that happens to arrive on the dead session ID would still match `sessionToPage` and incorrectly be appended to the page's buffer.
    
    Fix by calling `detach()` from the `detachedFromTarget` handler:
    
    ```ts
    this.cdp.Target.on('detachedFromTarget', (params) => {
      if (params.sessionId) {
        for (const [targetId, sid] of this.sessions) {
          if (sid === params.sessionId) {
            this.sessions.delete(targetId)
            // Find and clean up the corresponding page in the collector
            for (const [pageId, info] of this.pages) {
              if (info.targetId === targetId) {
                this.consoleCollector.detach(pageId)
                break
              }
            }
            break
          }
        }
      }
    })
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/browser/console-collector.ts
Line: 149-151

Comment:
**`clear: true` silently discards non-matching buffer entries**

When `level` or `search` filters are active, `opts?.clear` still wipes the entire buffer — not just the returned entries. For example, calling `getConsoleLogs({ level: 'error', clear: true })` will drop all `info`, `warning`, and `debug` entries that were never surfaced to the caller.

This could silently discard valuable debugging data when the tool is used with narrow filters. Consider either (a) clearing only the visible entries, or (b) documenting this explicitly in the tool description and the `clear` parameter so callers are aware.

```suggestion
    if (opts?.clear) {
      this.buffers.set(pageId, [])
    }
```
If the intent is always a full wipe, the `clear` param description in `console.ts` should say "Clears the **entire** console buffer after reading (including entries not returned by the current filter)."

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/browser/console-collector.ts
Line: 77-103

Comment:
**Event handler cleanup functions discarded**

All four `onSessionEvent()` calls return an unsubscribe function, but none of them are stored or ever called. The handlers remain registered in `CdpBackend` for its full lifetime. While `ConsoleCollector` and `Browser` currently share the same lifetime, this is a resource-leak pattern — if the design ever allows `Browser` to be re-constructed (e.g. reconnect after a CDP disconnect), the orphaned handlers would accumulate.

Store and expose the cleanup functions:

```ts
private readonly cleanupHandlers: Array<() => void> = []

constructor(cdp: CdpBackend) {
  this.cleanupHandlers.push(
    cdp.onSessionEvent('Runtime.consoleAPICalled', ...),
    cdp.onSessionEvent('Runtime.exceptionThrown', ...),
    cdp.onSessionEvent('Log.entryAdded', ...),
    cdp.onSessionEvent('Page.frameNavigated', ...),
  )
}

dispose(): void {
  for (const cleanup of this.cleanupHandlers) cleanup()
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/browser/browser.ts
Line: 109-118

Comment:
**`detachedFromTarget` does not clean up the console collector**

When a target detaches (tab crash, process swap), the session is removed from `this.sessions` but `this.consoleCollector.detach()` is never called. This leaves stale entries in `sessionToPage` and `pageToSession` inside the collector until the next `resolveSession``attachToPage` cycle calls `attach()` and overwrites them.

Between the detach event and the next `resolveSession` call, any CDP event that happens to arrive on the dead session ID would still match `sessionToPage` and incorrectly be appended to the page's buffer.

Fix by calling `detach()` from the `detachedFromTarget` handler:

```ts
this.cdp.Target.on('detachedFromTarget', (params) => {
  if (params.sessionId) {
    for (const [targetId, sid] of this.sessions) {
      if (sid === params.sessionId) {
        this.sessions.delete(targetId)
        // Find and clean up the corresponding page in the collector
        for (const [pageId, info] of this.pages) {
          if (info.targetId === targetId) {
            this.consoleCollector.detach(pageId)
            break
          }
        }
        break
      }
    }
  }
})
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/browser/console-collector.ts
Line: 211-219

Comment:
**Object serialization falls back to unhelpful `description`**

For `console.log({a: 1})` or any non-primitive, the CDP `RemoteObject` has `type: 'object'`, `value: undefined`, and `description: 'Object'` — so the serialized text is literally `"Object"`. This is likely to be confusing when an agent reads console output.

Consider attempting `JSON.stringify` on `arg.value` when it's non-null (CDP does populate `value` for objects when `returnByValue` is set), or falling back to `arg.preview?.properties` if the runtime was configured to send object previews. At a minimum, appending the type in the fallback (`[object]`) would be more informative than the generic description.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 1525698

- ConsoleCollector.attach() now updates session mapping on re-attach
  instead of early-returning, preventing silent event drops after
  target detach/re-attach (e.g. tab crash, cross-process navigation)
- Remove unused clearConsoleLogs() and ConsoleCollector.clear()
@shivammittal274
Copy link
Contributor Author

@greptile-ai review

@shivammittal274 shivammittal274 merged commit d1d2074 into main Mar 16, 2026
3 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Mar 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant