firebase / firebase/firebase-tools

[MCP] Crashlytics stack traces are capped at 20 frames with no way to request more

Open
#11,027 0 comments 0 reactions 1 assignee Claimed by @maxl0rd View on GitHub
api: crashlytics api: mcp
Dominant language
TypeScript
Stars
4.5k
Forks
1.3k
Avg merge
1d 12h
Merged PRs (30d)
84

Description

## Summary

The Crashlytics event tools truncate stack traces to 20 frames, and there is no way for an MCP client to ask for more. For deep native/framework stacks this cuts off exactly the part needed to debug, even though the Crashlytics API returns the complete `frames` array.

The cap lives in `formatFrames` in `src/mcp/tools/crashlytics/events.ts`:

```ts
function formatFrames(origFrames, maxFrames = 20) {
const frames = origFrames || [];
const shouldTruncate = frames.length > maxFrames;
const framesToFormat = shouldTruncate ? frames.slice(0, maxFrames - 1) : frames;
// ...
if (shouldTruncate) formatted.push("... frames omitted ...");
return formatted;
}
```

`maxFrames` is a default parameter that no call site ever overrides, and neither `crashlytics_list_events` nor `crashlytics_batch_get_events` exposes it in its Zod `inputSchema`. So an agent that needs frame 25 has no way to request it — its only recourse is to abandon the MCP server and call `events:batchGet` over raw HTTP.

**Request:** add an optional `maxFrames` (or `frameLimit`) parameter to `crashlytics_list_events` and `crashlytics_batch_get_events`, plumbed through to `formatFrames`, so a client can raise the limit when it needs the full trace. Keeping 20 as the default preserves today's context-friendly behaviour.

### Related truncation in the same function

These are all in `formatFrames`/`toText` and are equally unreachable from a client, so they may be worth addressing in the same change:

1. **Per-frame fields are dropped even when no truncation happens.** Only `symbol`, `file` and `line` are printed. The API's `library`, `address`, `offset`, `owner` and `blamed` are discarded for every frame — they survive only on the single top-level `blameFrame`. Without `library` and `address`, frames like `-[UIApplication _run]` carry no binary attribution and manual symbolication is impossible.
2. **`breadcrumbs` are capped at the last 10** (`slice(-10)`). Ten breadcrumbs covers only a few seconds of app activity, so the user actions leading up to an error are routinely lost. In the event below the ten returned spanned 38 seconds.
3. **For `FATAL`/`ANR` events, all non-crashed threads are discarded** via `threads.filter((t) => t.crashed || t.blamed)`. Deadlocks and main-thread hangs cannot be diagnosed without the other threads' stacks.
4. **`logs` are capped at the last 100** (`slice(logs.length - 100)`) — the most defensible of the four, but likewise not adjustable.

A single optional "verbosity"/limits parameter covering these would work as well as four separate ones; the key point is that a client currently cannot opt into the complete data.

## Bug Info

- **Affected Tool(s):** `crashlytics_list_events`, `crashlytics_batch_get_events` (both route through `formatFrames`)
- **MCP Client:** Claude Code 2.1.259
- **Operating System:** macOS 26.6.2 (build 25G83)

firebase-tools version: **15.29.0** (current `latest`), launched by the official Firebase Claude Code plugin as `npx -y firebase-tools@latest mcp`.

### Steps to Reproduce

1. Point the MCP server at a project with Crashlytics data and pick an iOS app.
2. Find an issue whose sample event has a stack deeper than 20 frames:
```
crashlytics_get_report(appId: "", report: "topIssues", pageSize: 10)
```
3. Fetch that issue's sample event:
```
crashlytics_batch_get_events(appId: "", names: [""])
```
4. Look at the `errors` (or `threads`) block in the response. It stops after 19 frames and ends with a literal `... frames omitted ...` line. There is no parameter on either tool that raises the limit.

For comparison, the same event fetched directly from the REST endpoint the tool wraps returns the full array:

```
GET https://firebasecrashlytics.googleapis.com/v1alpha/projects//apps//events:batchGet?names=
```

so the data is present in the API response and is being dropped by the MCP formatting layer.

#### Sample response showing the format

An abridged real response (a 20-frame iOS non-fatal, so right at the boundary — the last frame arrives unsymbolicated and prints as a bare `at`, with `library`/`address` unavailable for every frame):

```yaml
blameFrame: |
line: '109'
file: AppDelegate.swift
symbol: Logger.logPromiseKitEvent(_:)
offset: '109'
address: '5829372'
library: MyApp
owner: DEVELOPER
blamed: true
errors: |-
Error: Non-fatal: Error
at -[FIRCrashlytics recordError:userInfo:]
at Logger.logPromiseKitEvent(_:) (AppDelegate.swift:109)
at closure #1 in CatchMixin.cauterize() (Catchable.swift:191)
at thunk for @escaping @callee_guaranteed @Sendable () -> () ()
at _dispatch_call_block_and_release
at _dispatch_client_callout
at _dispatch_main_queue_drain
at __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
at __CFRunLoopRun
at CFRunLoopRunSpecific
at GSEventRunModal
at -[UIApplication _run]
at UIApplicationMain
at main (AppDelegate.swift)
at
threads: |-
Thread:
at -[FIRCrashlytics recordError:userInfo:]
...
```

Note that `blameFrame` carries `library`, `address` and `offset` while none of the stack frames do.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.