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:
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:
- 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.
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.
- 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.
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
- Point the MCP server at a project with Crashlytics data and pick an iOS app.
- Find an issue whose sample event has a stack deeper than 20 frames:
crashlytics_get_report(appId: "<appId>", report: "topIssues", pageSize: 10)
- Fetch that issue's sample event:
crashlytics_batch_get_events(appId: "<appId>", names: ["<sampleEvent from step 2>"])
- 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/<projectNumber>/apps/<appId>/events:batchGet?names=<eventName>
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):
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 () -> () (<compiler-generated>)
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.
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
framesarray.The cap lives in
formatFramesinsrc/mcp/tools/crashlytics/events.ts:maxFramesis a default parameter that no call site ever overrides, and neithercrashlytics_list_eventsnorcrashlytics_batch_get_eventsexposes it in its ZodinputSchema. So an agent that needs frame 25 has no way to request it — its only recourse is to abandon the MCP server and callevents:batchGetover raw HTTP.Request: add an optional
maxFrames(orframeLimit) parameter tocrashlytics_list_eventsandcrashlytics_batch_get_events, plumbed through toformatFrames, 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/toTextand are equally unreachable from a client, so they may be worth addressing in the same change:symbol,fileandlineare printed. The API'slibrary,address,offset,ownerandblamedare discarded for every frame — they survive only on the single top-levelblameFrame. Withoutlibraryandaddress, frames like-[UIApplication _run]carry no binary attribution and manual symbolication is impossible.breadcrumbsare 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.FATAL/ANRevents, all non-crashed threads are discarded viathreads.filter((t) => t.crashed || t.blamed). Deadlocks and main-thread hangs cannot be diagnosed without the other threads' stacks.logsare 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
crashlytics_list_events,crashlytics_batch_get_events(both route throughformatFrames)firebase-tools version: 15.29.0 (current
latest), launched by the official Firebase Claude Code plugin asnpx -y firebase-tools@latest mcp.Steps to Reproduce
errors(orthreads) 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:
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, withlibrary/addressunavailable for every frame):Note that
blameFramecarrieslibrary,addressandoffsetwhile none of the stack frames do.