diff --git a/.gitignore b/.gitignore index a28a366078c..88fd05c2e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ lib/ dev/ clean/ .gemini/ +.env diff --git a/src/crashlytics/filters.ts b/src/crashlytics/filters.ts index 414ae8ed3b9..1333096fd74 100644 --- a/src/crashlytics/filters.ts +++ b/src/crashlytics/filters.ts @@ -3,15 +3,9 @@ import { FirebaseError } from "../error"; export const ApplicationIdSchema = z .string() - .describe( - "Firebase app id. For an Android application, read the " + - "mobilesdk_app_id value specified in the google-services.json file for " + - "the current package name. For an iOS Application, read the GOOGLE_APP_ID " + - "from GoogleService-Info.plist. If neither is available, ask the user to " + - "provide the app id.", - ); + .describe("Firebase App Id. Strictly required for all API calls."); -export const IssueIdSchema = z.string().describe("Crashlytics issue id, as hexidecimal uuid"); +export const IssueIdSchema = z.string().describe("Crashlytics issue id, as hexidecimal UUID"); export const EventFilterSchema = z .object({ @@ -109,12 +103,15 @@ export function filterToUrlSearchParams(filter: EventFilter): URLSearchParams { const displayNamePattern = /^[^()]+\s+\([^()]+\)$/; // Regular expression like "xxxx (yyy)" /** - * Perform some simplistic validation on filters. + * Perform some simplistic validation on filters and fill missing values. * @param filter filters to validate * @throws FirebaseError if any of the filters are invalid. */ -export function validateEventFilters(filter: EventFilter): void { - if (!filter) return; +export function validateEventFilters(filter: EventFilter = {}): EventFilter { + if (!!filter.intervalStartTime && !filter.intervalEndTime) { + // interval.end_time is required if interval.start_time is set but the agent likes to forget it + filter.intervalEndTime = new Date().toISOString(); + } const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); if (filter.intervalStartTime && new Date(filter.intervalStartTime) < ninetyDaysAgo) { throw new FirebaseError("intervalStartTime must be less than 90 days in the past"); @@ -140,4 +137,5 @@ export function validateEventFilters(filter: EventFilter): void { } }); } + return filter; } diff --git a/src/crashlytics/reports.spec.ts b/src/crashlytics/reports.spec.ts index 2673a31fece..a6c06280cb0 100644 --- a/src/crashlytics/reports.spec.ts +++ b/src/crashlytics/reports.spec.ts @@ -140,7 +140,7 @@ describe("getReport", () => { .reply(200, mockResponse); const result = await getReport( - CrashlyticsReport.TopIssues, + CrashlyticsReport.TOP_ISSUES, appId, { issueErrorTypes: [issueType] }, pageSize, @@ -153,7 +153,7 @@ describe("getReport", () => { it("should throw a FirebaseError if the appId is invalid", async () => { const invalidAppId = "invalid-app-id"; - await expect(getReport(CrashlyticsReport.TopIssues, invalidAppId, {})).to.be.rejectedWith( + await expect(getReport(CrashlyticsReport.TOP_ISSUES, invalidAppId, {})).to.be.rejectedWith( FirebaseError, "Unable to get the projectId from the AppId.", ); diff --git a/src/crashlytics/reports.ts b/src/crashlytics/reports.ts index c299839e613..8c9caa7ffad 100644 --- a/src/crashlytics/reports.ts +++ b/src/crashlytics/reports.ts @@ -9,26 +9,30 @@ import { EventFilterSchema, filterToUrlSearchParams, } from "./filters"; +import { FirebaseError } from "../error"; const DEFAULT_PAGE_SIZE = 10; +export enum CrashlyticsReport { + TOP_ISSUES = "topIssues", + TOP_VARIANTS = "topVariants", + TOP_VERSIONS = "topVersions", + TOP_OPERATING_SYSTEMS = "topOperatingSystems", + TOP_APPLE_DEVICES = "topAppleDevices", + TOP_ANDROID_DEVICES = "topAndroidDevices", +} + +export const CrashlyticsReportSchema = z.nativeEnum(CrashlyticsReport); + export const ReportInputSchema = z.object({ appId: ApplicationIdSchema, + report: CrashlyticsReportSchema, filter: EventFilterSchema, pageSize: z.number().optional().describe("Number of rows to return").default(DEFAULT_PAGE_SIZE), }); export type ReportInput = z.infer; -export enum CrashlyticsReport { - TopIssues = "topIssues", - TopVariants = "topVariants", - TopVersions = "topVersions", - TopOperatingSystems = "topOperatingSystems", - TopAppleDevices = "topAppleDevices", - TopAndroidDevices = "topAndroidDevices", -} - /** * Returns a report for Crashlytics events. * @param report One of the supported reports in the CrashlyticsReport enum @@ -62,23 +66,26 @@ export function simplifyReport(report: Report): Report { } export async function getReport( - report: CrashlyticsReport, + reportName: CrashlyticsReport, appId: string, filter: EventFilter, pageSize = DEFAULT_PAGE_SIZE, ): Promise { + if (!reportName) { + throw new FirebaseError("Invalid Crashlytics report " + reportName); + } const requestProjectNumber = parseProjectNumber(appId); const queryParams = filterToUrlSearchParams(filter); queryParams.set("page_size", `${pageSize}`); logger.debug( - `[crashlytics] report ${report} called with appId: ${appId} filter: ${queryParams.toString()}, page_size: ${pageSize}`, + `[crashlytics] report ${reportName} called with appId: ${appId} filter: ${queryParams.toString()}, page_size: ${pageSize}`, ); const response = await CRASHLYTICS_API_CLIENT.request({ method: "GET", headers: { "Content-Type": "application/json", }, - path: `/projects/${requestProjectNumber}/apps/${appId}/reports/${report}`, + path: `/projects/${requestProjectNumber}/apps/${appId}/reports/${reportName}`, queryParams: queryParams, timeout: TIMEOUT, }); diff --git a/src/mcp/README.md b/src/mcp/README.md index 93f1412a94a..16eccc92dd7 100644 --- a/src/mcp/README.md +++ b/src/mcp/README.md @@ -159,64 +159,59 @@ For more information, visit the [official Firebase MCP server documentation](htt The Firebase MCP server provides three types of capabilities: **Tools** (functions that perform actions), **Prompts** (reusable command templates), and **Resources** (documentation files for AI models). -| Tool Name | Feature Group | Description | -| ------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| firebase_login | core | Use this to sign the user into the Firebase CLI and Firebase MCP server. This requires a Google Account, and sign in is required to create and work with Firebase Projects. | -| firebase_logout | core | Use this to sign the user out of the Firebase CLI and Firebase MCP server. | -| firebase_validate_security_rules | core | Use this to check Firebase Security Rules for Firestore, Storage, or Realtime Database for syntax and validation errors. | -| firebase_get_project | core | Use this to retrieve information about the currently active Firebase Project. | -| firebase_list_apps | core | Use this to retrieve a list of the Firebase Apps registered in the currently active Firebase project. Firebase Apps can be iOS, Android, or Web. | -| firebase_list_projects | core | Use this to retrieve a list of Firebase Projects that the signed-in user has access to. | -| firebase_get_sdk_config | core | Use this to retrieve the Firebase configuration information for a Firebase App. You must specify EITHER a platform OR the Firebase App ID for a Firebase App registered in the currently active Firebase Project. | -| firebase_create_project | core | Use this to create a new Firebase Project. | -| firebase_create_app | core | Use this to create a new Firebase App in the currently active Firebase Project. Firebase Apps can be iOS, Android, or Web. | -| firebase_create_android_sha | core | Use this to add the specified SHA certificate hash to the specified Firebase Android App. | -| firebase_get_environment | core | Use this to retrieve the current Firebase **environment** configuration for the Firebase CLI and Firebase MCP server, including current authenticated user, project directory, active Firebase Project, and more. | -| firebase_update_environment | core | Use this to update environment config for the Firebase CLI and Firebase MCP server, such as project directory, active project, active user account, accept terms of service, and more. Use `firebase_get_environment` to see the currently configured environment. | -| firebase_init | core | Use this to initialize selected Firebase services in the workspace (Cloud Firestore database, Firebase Data Connect, Firebase Realtime Database, Firebase AI Logic). All services are optional; specify only the products you want to set up. You can initialize new features into an existing project directory, but re-initializing an existing feature may overwrite configuration. To deploy the initialized features, run the `firebase deploy` command after `firebase_init` tool. | -| firebase_get_security_rules | core | Use this to retrieve the security rules for a specified Firebase service. If there are multiple instances of that service in the product, the rules for the defualt instance are returned. | -| firebase_read_resources | core | Use this to read the contents of `firebase://` resources or list available resources | -| firestore_delete_document | firestore | Use this to delete a Firestore documents from a database in the current project by full document paths. Use this if you know the exact path of a document. | -| firestore_get_documents | firestore | Use this to retrieve one or more Firestore documents from a database in the current project by full document paths. Use this if you know the exact path of a document. | -| firestore_list_collections | firestore | Use this to retrieve a list of collections from a Firestore database in the current project. | -| firestore_query_collection | firestore | Use this to retrieve one or more Firestore documents from a collection is a database in the current project by a collection with a full document path. Use this if you know the exact path of a collection and the filtering clause you would like for the document. | -| auth_get_users | auth | Use this to retrieve one or more Firebase Auth users based on a list of UIDs or a list of emails. | -| auth_update_user | auth | Use this to disable, enable, or set a custom claim on a specific user's account. | -| auth_set_sms_region_policy | auth | Use this to set an SMS region policy for Firebase Authentication to restrict the regions which can receive text messages based on an ALLOW or DENY list of country codes. This policy will override any existing policies when set. | -| dataconnect_build | dataconnect | Use this to compile Firebase Data Connect schema, operations, and/or connectors and check for build errors. | -| dataconnect_generate_schema | dataconnect | Use this to generate a Firebase Data Connect Schema based on the users description of an app. | -| dataconnect_generate_operation | dataconnect | Use this to generate a single Firebase Data Connect query or mutation based on the currently deployed schema and the provided prompt. | -| dataconnect_list_services | dataconnect | Use this to list existing local and backend Firebase Data Connect services | -| dataconnect_execute | dataconnect | Use this to execute a GraphQL operation against a Data Connect service or its emulator. | -| storage_get_object_download_url | storage | Use this to retrieve the download URL for an object in a Cloud Storage for Firebase bucket. | -| messaging_send_message | messaging | Use this to send a message to a Firebase Cloud Messaging registration token or topic. ONLY ONE of `registration_token` or `topic` may be supplied in a specific call. | -| functions_get_logs | functions | Use this to retrieve a page of Cloud Functions log entries using Google Cloud Logging advanced filters. | -| remoteconfig_get_template | remoteconfig | Use this to retrieve the specified Firebase Remote Config template from the currently active Firebase Project. | -| remoteconfig_update_template | remoteconfig | Use this to publish a new remote config template or roll back to a specific version for the project | -| crashlytics_create_note | crashlytics | Add a note to an issue from crashlytics. | -| crashlytics_delete_note | crashlytics | Delete a note from a Crashlytics issue. | -| crashlytics_get_issue | crashlytics | Gets data for a Crashlytics issue, which can be used as a starting point for debugging. | -| crashlytics_list_events | crashlytics | Use this to list the most recent events matching the given filters.
Can be used to fetch sample crashes and exceptions for an issue,
which will include stack traces and other data useful for debugging. | -| crashlytics_batch_get_events | crashlytics | Gets specific events by resource name.
Can be used to fetch sample crashes and exceptions for an issue,
which will include stack traces and other data useful for debugging. | -| crashlytics_list_notes | crashlytics | Use this to list all notes for an issue in Crashlytics. | -| crashlytics_get_top_issues | crashlytics | Use this to count events and distinct impacted users, grouped by _issue_.
Groups are sorted by event count, in descending order.
Only counts events matching the given filters. | -| crashlytics_get_top_variants | crashlytics | Counts events and distinct impacted users, grouped by issue _variant_.
Groups are sorted by event count, in descending order.
Only counts events matching the given filters. | -| crashlytics_get_top_versions | crashlytics | Counts events and distinct impacted users, grouped by _version_.
Groups are sorted by event count, in descending order.
Only counts events matching the given filters. | -| crashlytics_get_top_apple_devices | crashlytics | Counts events and distinct impacted users, grouped by apple _device_.
Groups are sorted by event count, in descending order.
Only counts events matching the given filters.
Only relevant for iOS, iPadOS and MacOS applications. | -| crashlytics_get_top_android_devices | crashlytics | Counts events and distinct impacted users, grouped by android _device_.
Groups are sorted by event count, in descending order.
Only counts events matching the given filters.
Only relevant for Android applications. | -| crashlytics_get_top_operating_systems | crashlytics | Counts events and distinct impacted users, grouped by _operating system_.
Groups are sorted by event count, in descending order.
Only counts events matching the given filters. | -| crashlytics_update_issue | crashlytics | Use this to update the state of Crashlytics issue. | -| apphosting_fetch_logs | apphosting | Use this to fetch the most recent logs for a specified App Hosting backend. If `buildLogs` is specified, the logs from the build process for the latest build are returned. The most recent logs are listed first. | -| apphosting_list_backends | apphosting | Use this to retrieve a list of App Hosting backends in the current project. An empty list means that there are no backends. The `uri` is the public URL of the backend. A working backend will have a `managed_resources` array that will contain a `run_service` entry. That `run_service.service` is the resource name of the Cloud Run service serving the App Hosting backend. The last segment of that name is the service ID. `domains` is the list of domains that are associated with the backend. They either have type `CUSTOM` or `DEFAULT`. Every backend should have a `DEFAULT` domain. The actual domain that a user would use to conenct to the backend is the last parameter of the domain resource name. If a custom domain is correctly set up, it will have statuses ending in `ACTIVE`. | -| realtimedatabase_get_data | realtimedatabase | Use this to retrieve data from the specified location in a Firebase Realtime Database. | -| realtimedatabase_set_data | realtimedatabase | Use this to write data to the specified location in a Firebase Realtime Database. | +| Tool Name | Feature Group | Description | +| -------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| firebase_login | core | Use this to sign the user into the Firebase CLI and Firebase MCP server. This requires a Google Account, and sign in is required to create and work with Firebase Projects. | +| firebase_logout | core | Use this to sign the user out of the Firebase CLI and Firebase MCP server. | +| firebase_validate_security_rules | core | Use this to check Firebase Security Rules for Firestore, Storage, or Realtime Database for syntax and validation errors. | +| firebase_get_project | core | Use this to retrieve information about the currently active Firebase Project. | +| firebase_list_apps | core | Use this to retrieve a list of the Firebase Apps registered in the currently active Firebase project. Firebase Apps can be iOS, Android, or Web. | +| firebase_list_projects | core | Use this to retrieve a list of Firebase Projects that the signed-in user has access to. | +| firebase_get_sdk_config | core | Use this to retrieve the Firebase configuration information for a Firebase App. You must specify EITHER a platform OR the Firebase App ID for a Firebase App registered in the currently active Firebase Project. | +| firebase_create_project | core | Use this to create a new Firebase Project. | +| firebase_create_app | core | Use this to create a new Firebase App in the currently active Firebase Project. Firebase Apps can be iOS, Android, or Web. | +| firebase_create_android_sha | core | Use this to add the specified SHA certificate hash to the specified Firebase Android App. | +| firebase_get_environment | core | Use this to retrieve the current Firebase **environment** configuration for the Firebase CLI and Firebase MCP server, including current authenticated user, project directory, active Firebase Project, and more. | +| firebase_update_environment | core | Use this to update environment config for the Firebase CLI and Firebase MCP server, such as project directory, active project, active user account, accept terms of service, and more. Use `firebase_get_environment` to see the currently configured environment. | +| firebase_init | core | Use this to initialize selected Firebase services in the workspace (Cloud Firestore database, Firebase Data Connect, Firebase Realtime Database, Firebase AI Logic). All services are optional; specify only the products you want to set up. You can initialize new features into an existing project directory, but re-initializing an existing feature may overwrite configuration. To deploy the initialized features, run the `firebase deploy` command after `firebase_init` tool. | +| firebase_get_security_rules | core | Use this to retrieve the security rules for a specified Firebase service. If there are multiple instances of that service in the product, the rules for the defualt instance are returned. | +| firebase_read_resources | core | Use this to read the contents of `firebase://` resources or list available resources | +| firestore_delete_document | firestore | Use this to delete a Firestore documents from a database in the current project by full document paths. Use this if you know the exact path of a document. | +| firestore_get_documents | firestore | Use this to retrieve one or more Firestore documents from a database in the current project by full document paths. Use this if you know the exact path of a document. | +| firestore_list_collections | firestore | Use this to retrieve a list of collections from a Firestore database in the current project. | +| firestore_query_collection | firestore | Use this to retrieve one or more Firestore documents from a collection is a database in the current project by a collection with a full document path. Use this if you know the exact path of a collection and the filtering clause you would like for the document. | +| auth_get_users | auth | Use this to retrieve one or more Firebase Auth users based on a list of UIDs or a list of emails. | +| auth_update_user | auth | Use this to disable, enable, or set a custom claim on a specific user's account. | +| auth_set_sms_region_policy | auth | Use this to set an SMS region policy for Firebase Authentication to restrict the regions which can receive text messages based on an ALLOW or DENY list of country codes. This policy will override any existing policies when set. | +| dataconnect_build | dataconnect | Use this to compile Firebase Data Connect schema, operations, and/or connectors and check for build errors. | +| dataconnect_generate_schema | dataconnect | Use this to generate a Firebase Data Connect Schema based on the users description of an app. | +| dataconnect_generate_operation | dataconnect | Use this to generate a single Firebase Data Connect query or mutation based on the currently deployed schema and the provided prompt. | +| dataconnect_list_services | dataconnect | Use this to list existing local and backend Firebase Data Connect services | +| dataconnect_execute | dataconnect | Use this to execute a GraphQL operation against a Data Connect service or its emulator. | +| storage_get_object_download_url | storage | Use this to retrieve the download URL for an object in a Cloud Storage for Firebase bucket. | +| messaging_send_message | messaging | Use this to send a message to a Firebase Cloud Messaging registration token or topic. ONLY ONE of `registration_token` or `topic` may be supplied in a specific call. | +| functions_get_logs | functions | Use this to retrieve a page of Cloud Functions log entries using Google Cloud Logging advanced filters. | +| remoteconfig_get_template | remoteconfig | Use this to retrieve the specified Firebase Remote Config template from the currently active Firebase Project. | +| remoteconfig_update_template | remoteconfig | Use this to publish a new remote config template or roll back to a specific version for the project | +| crashlytics_create_note | crashlytics | Add a note to an issue from crashlytics. | +| crashlytics_delete_note | crashlytics | Delete a note from a Crashlytics issue. | +| crashlytics_get_issue | crashlytics | Gets data for a Crashlytics issue, which can be used as a starting point for debugging. | +| crashlytics_list_events | crashlytics | Use this to list the most recent events matching the given filters.
Can be used to fetch sample crashes and exceptions for an issue,
which will include stack traces and other data useful for debugging. | +| crashlytics_batch_get_events | crashlytics | Gets specific events by resource name.
Can be used to fetch sample crashes and exceptions for an issue,
which will include stack traces and other data useful for debugging. | +| crashlytics_list_notes | crashlytics | Use this to list all notes for an issue in Crashlytics. | +| crashlytics_get_report | crashlytics | Use this to request numerical reports from Crashlytics. The result aggregates the sum of events and impacted users, grouped by a dimension appropriate for that report. | +| crashlytics_update_issue | crashlytics | Use this to update the state of Crashlytics issue. | +| apphosting_fetch_logs | apphosting | Use this to fetch the most recent logs for a specified App Hosting backend. If `buildLogs` is specified, the logs from the build process for the latest build are returned. The most recent logs are listed first. | +| apphosting_list_backends | apphosting | Use this to retrieve a list of App Hosting backends in the current project. An empty list means that there are no backends. The `uri` is the public URL of the backend. A working backend will have a `managed_resources` array that will contain a `run_service` entry. That `run_service.service` is the resource name of the Cloud Run service serving the App Hosting backend. The last segment of that name is the service ID. `domains` is the list of domains that are associated with the backend. They either have type `CUSTOM` or `DEFAULT`. Every backend should have a `DEFAULT` domain. The actual domain that a user would use to conenct to the backend is the last parameter of the domain resource name. If a custom domain is correctly set up, it will have statuses ending in `ACTIVE`. | +| realtimedatabase_get_data | realtimedatabase | Use this to retrieve data from the specified location in a Firebase Realtime Database. | +| realtimedatabase_set_data | realtimedatabase | Use this to write data to the specified location in a Firebase Realtime Database. | | Prompt Name | Feature Group | Description | | ------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | firebase:deploy | core | Use this command to deploy resources to Firebase.

Arguments:
<prompt> (optional): any specific instructions you wish to provide about deploying | | firebase:init | core | Use this command to set up Firebase services, like backend and AI features. | | firebase:consult | core | Use this command to consult the Firebase Assistant with access to detailed up-to-date documentation for the Firebase platform.

Arguments:
<prompt>: a question to pass to the Gemini in Firebase model | -| crashlytics:connect | crashlytics | Access a Firebase application's Crashlytics data. | +| crashlytics:connect | crashlytics | Use this command to access a Firebase application's Crashlytics data. | | Resource Name | Description | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/src/mcp/prompts/crashlytics/connect.ts b/src/mcp/prompts/crashlytics/connect.ts index 1b357520480..01ad32b9efd 100644 --- a/src/mcp/prompts/crashlytics/connect.ts +++ b/src/mcp/prompts/crashlytics/connect.ts @@ -1,147 +1,42 @@ import { prompt } from "../../prompt"; +import { RESOURCE_CONTENT as connectResourceContent } from "../../resources/guides/crashlytics_connect"; export const connect = prompt( "crashlytics", { name: "connect", omitPrefix: false, - description: "Access a Firebase application's Crashlytics data.", + description: "Use this command to access a Firebase application's Crashlytics data.", annotations: { title: "Access Crashlytics data", }, }, async (unused, { accountEmail, firebaseCliCommand }) => { + const loggedInInstruction = ` +**The user is logged into Firebase as ${accountEmail || ""}. + `.trim(); + + const notLoggedInInstruction = ` +**Instruct the User to Log In** + The user is not logged in to Firebase. None of the Crashlytics tools will be able to authenticate until the user has logged in. Instruct the user to run \`${firebaseCliCommand} login\` before continuing, then use the \`firebase_get_environment\` tool to verify that the user is logged in. + `.trim(); + return [ { role: "user" as const, content: { type: "text", text: ` -You are going to help a developer prioritize and fix issues in their -mobile application by accessing their Firebase Crashlytics data. - -Active user: ${accountEmail || ""} - -General rules: -**ASK THE USER WHAT THEY WOULD LIKE TO DO BEFORE TAKING ACTION** -**ASK ONLY ONE QUESTION OF THE USER AT A TIME** -**MAKE SURE TO FOLLOW THE INSTRUCTIONS, ESPECIALLY WHERE THEY ASK YOU TO CHECK IN WITH THE USER** -**ADHERE TO SUGGESTED FORMATTING** - -## Required first steps! Absolutely required! Incredibly important! - - 1. **Make sure the user is logged in. No Crashlytics tools will work if the user is not logged in.** - a. Use the \`firebase_get_environment\` tool to verify that the user is logged in. - b. If the Firebase 'Active user' is set to , instruct the user to run \`${firebaseCliCommand} login\` - before continuing. Ignore other fields that are set to . We are just making sure the - user is logged in. - - 2. **Get the app ID for the Firebase application.** - a. **PRIORITIZE REMEMBERED APP ID ENTRIES** If an entry for this directory exists in the remembered app ids, use the remembered app id - for this directory without presenting any additional options. - i. If there are multiple remembered app ids for this directory, ask the user to choose one by providing - a numbered list of all the package names. Tell them that these values came from memories and how they can modify those values. - b. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Use the app IDs from the \`firebase_get_environment\` tool. - i. If you've already called this tool, use the previous response from context. - ii. If the 'Detected App IDs' is set to , ask the user for the value they want to use. - iii. If there are multiple 'Detected App IDs', ask the user to choose one by providing - a numbered list of all the package names and app ids. - c. **IF THERE IS A REMEMBERED VALUE BUT IT DOES NOT MATCH ANY DETECTED APP IDS** Ask if the user would like to replace the value with one of - the detected values. - i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version - number (typically "1"), a project number, a platform type ("android", "ios", or "web"), - and a sequence of hexadecimal characters. - ii. Replace the value for this directory with this valid app id, the android package name or ios bundle identifier, and the project directory. - c. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Ask if the user would like to remember the app id selection - i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version - number (typically "1"), a project number, a platform type ("android", "ios", or "web"), - and a sequence of hexadecimal characters. - ii. Store the valid app id value, the android package name or ios bundle identifier, and the project directory. - -## Next steps - -Once you have confirmed that the user is logged in to Firebase, confirmed the -id for the application that they want to access, and asked if they want to remember the app id for this directory, -ask the user what actions they would like to perform. - -Use the following format to ask the user what actions they would like to perform: - - 1. Prioritize the most impactful stability issues - 2. Diagnose and propose a fix for a crash - -Wait for their response before taking action. - -## Instructions for Using Crashlytics Data - -### How to prioritize issues - -Follow these steps to fetch issues and prioritize them. - - 1. Use the 'crashlytics_get_top_issues' tool to fetch up to 20 issues. - 1a. Analyze the user's query and apply the appropriate filters. - 1b. If the user asks for crashes, then set the issueErrorType filter to *FATAL*. - 1c. If the user asks about a particular time range, then set both the intervalStartTime and intervalEndTime. - 2. Use the 'crashlytics_get_top_versions' tool to fetch the top versions for this app. - 3. If the user instructions include statements about prioritization, use those instructions. - 4. If the user instructions do not include statements about prioritization, - then prioritize the returned issues using the following criteria: - 4a. The app versions for the issue include the most recent version of the app. - 4b. The number of users experiencing the issue across variants - 4c. The volume of crashes - 5. Return the top 5 issues, with a brief description each in a numerical list with the following format: - 1. Issue - * - * - * **Description:** - * **Rationale:** - 6. Ask the user if they would like to diagnose and fix any of the issues presented - -### How to diagnose and fix issues - -Follow these steps to diagnose and fix issues. +You will assist developers in investigating and resolving mobile application issues by leveraging Firebase Crashlytics data. - 1. Make sure you have a good understanding of the code structure and where different functionality exists - 2. Use the 'crashlytics_get_issue' tool to get more context on the issue. - 3. Use the 'crashlytics_batch_get_events' tool to get an example crash for this issue. Use the event names in the sampleEvent fields. - 3a. If you need to read more events, use the 'crashlytics_list_events' tool. - 3b. Apply the same filtering criteria that you used to find the issue, so that you find a appropriate events. - 4. Read the files that exist in the stack trace of the issue to understand the crash deeply. - 5. Determine possible root causes for the crash - no more than 5 potential root causes. - 6. Critique your own determination, analyzing how plausible each scenario is given the crash details. - 7. Choose the most likely root cause given your analysis. - 8. Write out a plan for the most likely root cause using the following criteria: - 8a. Write out a description of the issue and including - * A brief description of the cause of the issue - * A determination of your level of confidence in the cause of the issue using your analysis. - * A determination of which library is at fault, this codebase or a dependent library - * A determination for how complex the fix will be - 8b. The plan should include relevant files to change - 8c. The plan should include a test plan for how the user might verify the fix - 8d. Use the following format for the plan: +### Required First Steps - ## Cause - - - **Fault**: - - **Complexity**: - - ## Fix - - 1. - 2. + ${accountEmail ? loggedInInstruction : notLoggedInInstruction} - ## Test - - 1. - 2. +**Obtain the Firebase App ID.** + If an App ID is not readily available, consult this guide for selection: [Firebase App Id Guide](firebase://guides/app_id). - ## Other potential causes - 1. - 2. - - 9. Present the plan to the user and get approval before making the change. - 10. Only if they approve the plan, create a fix for the issue. - 10a. Be mindful of API contracts and do not add fields to resources without a clear way to populate those fields - 10b. If there is not enough information in the crash report to find a root cause, describe why you cannot fix the issue instead of making a guess. +${connectResourceContent} `.trim(), }, }, diff --git a/src/mcp/resources/guides/app_id.ts b/src/mcp/resources/guides/app_id.ts new file mode 100644 index 00000000000..b5b6a78372c --- /dev/null +++ b/src/mcp/resources/guides/app_id.ts @@ -0,0 +1,42 @@ +import { resource } from "../../resource"; + +export const RESOURCE_CONTENT = ` +### Firebase App ID + The Firebase App ID is used to identify a mobile or web client application to Firebase back end services such as Crashlytics or Remote Config. Use the information below to find the developer's App ID. + + 1. **PRIORITIZE REMEMBERED APP ID ENTRIES** If an entry for this directory exists in the remembered app ids, use the remembered app id + for this directory without presenting any additional options. + i. If there are multiple remembered app ids for this directory, ask the user to choose one by providing + a numbered list of all the package names. Tell them that these values came from memories and how they can modify those values. + 2. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Use the app IDs from the \`firebase_get_environment\` tool. + i. If you've already called this tool, use the previous response from context. + ii. If the 'Detected App IDs' is set to , ask the user for the value they want to use. + iii. If there are multiple 'Detected App IDs', ask the user to choose one by providing + a numbered list of all the package names and app ids. + 3. **IF THERE IS A REMEMBERED VALUE BUT IT DOES NOT MATCH ANY DETECTED APP IDS** Ask if the user would like to replace the value with one of + the detected values. + i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version + number (typically "1"), a project number, a platform type ("android", "ios", or "web"), + and a sequence of hexadecimal characters. + ii. Replace the value for this directory with this valid app id, the android package name or ios bundle identifier, and the project directory. + 4. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Ask if the user would like to remember the app id selection + i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version + number (typically "1"), a project number, a platform type ("android", "ios", or "web"), + and a sequence of hexadecimal characters. + ii. Store the valid app id value, the android package name or ios bundle identifier, and the project directory. +`.trim(); + +export const app_id = resource( + { + uri: "firebase://guides/app_id", + name: "app_id_guide", + title: "Firebase App Id Guide", + description: + "guides the coding agent through choosing a Firebase App ID in the current project", + }, + async (uri) => { + return { + contents: [{ uri, type: "text", text: RESOURCE_CONTENT }], + }; + }, +); diff --git a/src/mcp/resources/guides/crashlytics_connect.ts b/src/mcp/resources/guides/crashlytics_connect.ts new file mode 100644 index 00000000000..4ef1067b3f5 --- /dev/null +++ b/src/mcp/resources/guides/crashlytics_connect.ts @@ -0,0 +1,53 @@ +import { resource } from "../../resource"; + +export const RESOURCE_CONTENT = ` +### Instructions for Working with Firebase Crashlytics Tools + +When working interactively with a user, only ask the one question at a time. Do not proceed without user instructions. Upon receiving user instructions, refer to the relevant resources for guidance. + +Use the \`firebase_read_resources\` tool to access the following guides. + + 1. [Firebase App Id Guide](firebase://guides/app_id) + This guide provides crucial instructions for obtaining the application's App Id which is required for all API calls. + + 2. [Firebase Crashlytics Reports Guide](firebase://guides/crashlytics/reports) + This guide details how to request and use aggregated numerical data from Crashlytics. The agent should read this guide before requesting any report. + + 3. [Firebase Crashlytics Issues Guide](firebase://guides/crashlytics/issues) + This guide details how to work with issues within Crashlytics. The agent should read this guide before prioritizing issues or presenting issue data to the user. + + 4. [Investigating Crashlytics Issues Guide](firebase://guides/crashlytics/investigations) + This guide provides instructions on investigating the root causes of crashes and exceptions reported in Crashlytics issues. + +### How to Check That You Are Connected + +Verify that you can read the app's Crashlytics data by getting the topVersions report. This report will tell you which app versions have the most events. + a. Read the firebase://guides/app_id if you need to find the app_id. + b. Call the \`crashlytics_get_report\` tool to read the \`topVersions\` report. + c. Help the user resolve any issues that arise when trying to connect. + +After confirming that you can access Crashlytics, ask the user what they would like help with. Your capabilities include: + + - *Reading Crashlytics reports to prioritize or find important issues.* + Before fetching reports, read the critical instructions for the \`crashlytics_get_report\` tool in the [Firebase Crashlytics Reports Guide](firebase://guides/crashlytics/reports). + + - *Investigating bug reports using Crashlytics event data.* + Before attempting to investigate an individual issue, read the [Investigating Crashlytics Issues Guide](firebase://guides/crashlytics/investigations) to understand the best practices for debugging issues. + + - *Proposing code changes to resolve identified bugs.* + +`.trim(); + +export const crashlytics_connect = resource( + { + uri: "firebase://guides/crashlytics/connect", + name: "crashlytics_connect_guide", + title: "Firebase Crashlytics Connect Guide", + description: "Guides the coding agent to connect to Firebase Crashlytics.", + }, + async (uri) => { + return { + contents: [{ uri, type: "text", text: RESOURCE_CONTENT }], + }; + }, +); diff --git a/src/mcp/resources/guides/crashlytics_investigations.ts b/src/mcp/resources/guides/crashlytics_investigations.ts new file mode 100644 index 00000000000..98d19061421 --- /dev/null +++ b/src/mcp/resources/guides/crashlytics_investigations.ts @@ -0,0 +1,54 @@ +import { resource } from "../../resource"; + +const RESOURCE_CONTENT = ` +### How to Diagnose and Fix Crashlytics Issues + + Follow these steps to diagnose bugs and and propose fixes for issues. + + 1. Make sure you have a good understanding of the code structure and where different functionality exists. + 2. Use the 'crashlytics_get_issue' tool to get more context on the issue. + 3. Use the 'crashlytics_batch_get_events' tool to get an example crash for this issue. Use the event names in the sampleEvent fields. + 3a. If you need to read more events, use the 'crashlytics_list_events' tool. + 3b. Apply the same filtering criteria that you used to find the issue, so that you find a appropriate events. + 4. Read the files that exist in the stack trace of the issue to understand the crash deeply. + 5. Determine possible root causes for the crash - no more than 5 potential root causes. + 6. Critique your own determination, analyzing how plausible each scenario is given the crash details. + 7. Choose the most likely root cause given your analysis. + 8. Create a plan for the most likely root cause using the following format for the plan: + + ## Cause + + - **Fault**: + - **Complexity**: + + ## Fix + + 1. + 2. + + ## Test + + 1. + 2. + + ## Other potential causes + 1. + 2. + + 9. If there is not enough information in the crash report to find a root cause, describe why you cannot fix the issue instead of making a guess. +`.trim(); + +export const crashlytics_investigations = resource( + { + uri: "firebase://guides/crashlytics/investigations", + name: "crashlytics_investigations_guide", + title: "Firebase Crashlytics Investigations Guide", + description: + "Guides the coding agent when investigating bugs reported in Crashlytics issues, including procedures for diagnosing and fixing crashes.", + }, + async (uri) => { + return { + contents: [{ uri, type: "text", text: RESOURCE_CONTENT }], + }; + }, +); diff --git a/src/mcp/resources/guides/crashlytics_issues.ts b/src/mcp/resources/guides/crashlytics_issues.ts new file mode 100644 index 00000000000..6d5b444ffcd --- /dev/null +++ b/src/mcp/resources/guides/crashlytics_issues.ts @@ -0,0 +1,45 @@ +import { resource } from "../../resource"; + +const RESOURCE_CONTENT = ` +### How to Display Issues + +When displaying a list of issues, use the following format: + + 1. Issue + * + * + * **Description:** + +### How to Prioritize Crashlytics Issues + +Follow these steps to fetch issues and prioritize them. + + 1. Use the 'crashlytics_get_report' tool to fetch the 'topIssues' report. + 1a. Analyze the user's query and apply the appropriate filters. Use the information in the [Firebase Crashlytics Reports Guide](firebase://guides/crashlytics/reports) to further construct appropriate report requests. + 1b. If the user asks for crashes, then set the *issueErrorType* filter to *FATAL*. + 1c. If the user asks about a particular time range, then set both the *intervalStartTime* and *intervalEndTime*. + 2. Use the 'crashlytics_get_report' tool to fetch the 'topVersions' for this app. + 3. If the user instructions include statements about prioritization, use those instructions. + 4. If the user instructions do not include statements about prioritization, then prioritize the returned issues using the following criteria: + 4a. The app versions for the issue include the most recent version of the app. + 4b. The number of users experiencing the issue across variants + 4c. The volume of crashes + 5. Return the top 5 issues, with a brief description each in a numerical list with the recommended format. + 5a. Describe the rationale for the prioritization order. + 6. Ask the user if they would like to diagnose and fix any of the issues presented before taking any action. +`.trim(); + +export const crashlytics_issues = resource( + { + uri: "firebase://guides/crashlytics/issues", + name: "crashlytics_issues_guide", + title: "Firebase Crashlytics Issues Guide", + description: + "Guides the coding agent when working with Crashlytics issues, including prioritization rules and procedures for diagnosing and fixing crashes. ", + }, + async (uri) => { + return { + contents: [{ uri, type: "text", text: RESOURCE_CONTENT }], + }; + }, +); diff --git a/src/mcp/resources/guides/crashlytics_reports.ts b/src/mcp/resources/guides/crashlytics_reports.ts new file mode 100644 index 00000000000..c675b917477 --- /dev/null +++ b/src/mcp/resources/guides/crashlytics_reports.ts @@ -0,0 +1,115 @@ +import { resource } from "../../resource"; + +const RESOURCE_CONTENT = ` +### Crashlytics Reports + +Aggregate metrics for all of the events sent to Crashlytics are available as reports. +The following reports are available for all Crashlytics applications. + + - name: "topIssues" + display_name: "Top Issues" + usage: | + Counts events and distinct impacted users, grouped by issue. + Issue groups are sorted by event count, in descending order. + + - name: "topVariants" + display_name: "Top Variants" + usage: | + Counts events and distinct impacted users, grouped by issue variant. + Issue variant groups are sorted by event count, in descending order. + required: | + An issue filter including an issue id is required. + + - name: "topVersions" + display_name: "Top Versions" + usage: | + Counts events, grouped by app version. + Versions are sorted by event count, in descending order. + + - name: "topOperatingSystems" + display_name: "Top Operating Systems" + usage: | + Counts events, grouped by device operating systems and their versions. + Operating systems are sorted by event count, in descending order. + +Mobile apps have one of the following reports available, depending on the platform. + + - name: "topAndroidDevices" + display_name: "Top Android Devices" + usage: | + Counts events, grouped by android device. + Devices are sorted by event count, in descending order. + + - name: "topAppleDevices" + display_name: "Top Apple Devices" + usage: + Counts events, grouped by operating system and Apple device. + Devices are sorted by event count, in descending order. + +Report responses contain the following metrics: + + - eventsCount: the number of events matching + - impactedUsers: the number of distinct end users in all the matching events + +Report responses are always grouped by one of the following dimensions: + + - app version + - issue + - variant + - operating system + - mobile device type + +### Filters + +When setting report filters adhere to the following instructions. + + * Issue Filtering: + * Use the \`issueErrorTypes\` field to focus on events of different fatalities: + * FATAL: native crashes, which caused the app to exit. + * NON_FATAL: uncaught or manually reported exceptions, which did not crash the app. + * ANR: "app not responding" events, only relevant on Android platforms. + + * Time Interval: + * For a custom time range, you must specify both intervalStartTime and intervalEndTime. + * The specified time range must be within the last 90 days. + * If you don't provide a time range, it will default to the last 7 days. + + * Display Names (for app versions, operating systems, and devices): + * The values for versionDisplayNames,operatingSystemDisplayNames, and deviceDisplayNames must be obtained from the displayName field of a previous API response. + * These display names must match specific formats: + * Device: 'manufacturer (device)' + * Operating System: 'os (version)' + * App Version: 'version (build)' + +### Useful Reports + + * The "topIssues" report is comparable to the default view on the Crashlytics web dashboard. Use this report first to prioritize which issues are impacting the most users. Apply appropriate filters for time interval based on the user's query. + + * Report responses grouped by issue will include a sample event URI. Use the "crashlytics_batch_get_events" tool to fetch the complete record for any sample event. + + * When investigating an issue, use the appropriate top devices and top operating systems reports to understand what systems are impacted by the problem. Pass the "issueId" in the filter to narrow any report to a specific issue. + +### How to Display Issues + +When displaying a list of issues, use the following format: + + 1. Issue + * + * + * **Description:** +`.trim(); + +export const crashlytics_reports = resource( + { + uri: "firebase://guides/crashlytics/reports", + name: "crashlytics_reports_guide", + title: "Firebase Crashlytics Reports Guide", + description: + "Guides the coding agent through requesting Crashlytics reports, including setting appropriate filters and how to understand the metrics. The agent should read this guide before requesting any report.", + }, + async (uri) => { + return { + contents: [{ uri, type: "text", text: RESOURCE_CONTENT }], + }; + }, +); diff --git a/src/mcp/resources/index.ts b/src/mcp/resources/index.ts index 2be6b1507c7..5a007ceafd2 100644 --- a/src/mcp/resources/index.ts +++ b/src/mcp/resources/index.ts @@ -1,16 +1,24 @@ import { ReadResourceResult } from "@modelcontextprotocol/sdk/types"; import { McpContext } from "../types"; import { docs } from "./docs"; +import { app_id } from "./guides/app_id"; import { init_ai } from "./guides/init_ai"; import { init_auth } from "./guides/init_auth"; import { init_backend } from "./guides/init_backend"; import { init_firestore } from "./guides/init_firestore"; import { init_firestore_rules } from "./guides/init_firestore_rules"; import { init_hosting } from "./guides/init_hosting"; +import { crashlytics_investigations } from "./guides/crashlytics_investigations"; import { ServerResource, ServerResourceTemplate } from "../resource"; import { trackGA4 } from "../../track"; +import { crashlytics_issues } from "./guides/crashlytics_issues"; +import { crashlytics_reports } from "./guides/crashlytics_reports"; export const resources = [ + app_id, + crashlytics_investigations, + crashlytics_issues, + crashlytics_reports, init_backend, init_ai, init_firestore, diff --git a/src/mcp/tools/core/get_environment.ts b/src/mcp/tools/core/get_environment.ts index 7344fa32e89..229b990668e 100644 --- a/src/mcp/tools/core/get_environment.ts +++ b/src/mcp/tools/core/get_environment.ts @@ -100,7 +100,7 @@ export const get_environment = tool( { name: "get_environment", description: - "Use this to retrieve the current Firebase **environment** configuration for the Firebase CLI and Firebase MCP server, including current authenticated user, project directory, active Firebase Project, and more.", + "Use this to retrieve the current Firebase **environment** configuration for the Firebase CLI and Firebase MCP server, including current authenticated user, project directory, active Firebase Project, and more. All tools require the user to be authenticated, but not all information is required for all tools. Pay attention to the tool requirements for which pieces of information are required.", inputSchema: z.object({}), annotations: { title: "Get Firebase Environment Info", diff --git a/src/mcp/tools/crashlytics/events.ts b/src/mcp/tools/crashlytics/events.ts index 77cb39240a0..51523565b14 100644 --- a/src/mcp/tools/crashlytics/events.ts +++ b/src/mcp/tools/crashlytics/events.ts @@ -1,10 +1,12 @@ -import { z } from "zod"; -import { tool } from "../../tool"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types"; import { dump, DumpOptions } from "js-yaml"; +import { z } from "zod"; import { batchGetEvents, listEvents } from "../../../crashlytics/events"; +import { ApplicationIdSchema, EventFilterSchema } from "../../../crashlytics/filters"; import { BatchGetEventsResponse, Breadcrumb, + Error, ErrorType, Event, Exception, @@ -12,10 +14,9 @@ import { ListEventsResponse, Log, Thread, - Error, } from "../../../crashlytics/types"; -import { ApplicationIdSchema, EventFilterSchema } from "../../../crashlytics/filters"; -import { mcpError } from "../../util"; +import { RESOURCE_CONTENT as forceAppIdGuide } from "../../resources/guides/app_id"; +import { tool } from "../../tool"; const DUMP_OPTIONS: DumpOptions = { lineWidth: 200 }; @@ -46,6 +47,9 @@ function formatFrames(origFrames: Frame[], maxFrames = 20): string[] { // Formats an event into more legible, token-efficient text content sections function toText(event: Event): Record { + if (!event) { + return {}; + } const result: Record = {}; for (const [key, value] of Object.entries(event)) { if (key === "logs") { @@ -61,7 +65,7 @@ function toText(event: Event): Record { const breadcrumbs = (value as Breadcrumb[]) || []; const slicedBreadcrumbs = breadcrumbs.length > 10 ? breadcrumbs.slice(-10) : breadcrumbs; const breadcrumbLines = slicedBreadcrumbs.map((b) => { - const paramString = Object.entries(b.params) + const paramString = Object.entries(b?.params || {}) .map(([k, v]) => `${k}: ${v}`) .join(", "); const params = paramString ? ` { ${paramString} }` : ""; @@ -135,10 +139,24 @@ export const list_events = tool( }, }, async ({ appId, filter, pageSize }) => { - if (!appId) return mcpError(`Must specify 'appId' parameter.`); - if (!filter || (!filter.issueId && !filter.issueVariantId)) - return mcpError(`Must specify 'filter.issueId' or 'filter.issueVariantId' parameters.`); - + const result: CallToolResult = { content: [] }; + if (!appId) { + result.isError = true; + result.content.push({ type: "text", text: "Must specify 'appId' parameter" }); + result.content.push({ type: "text", text: forceAppIdGuide }); + } + if (!filter || (!filter.issueId && !filter.issueVariantId)) { + result.isError = true; + result.content.push({ + type: "text", + text: `Must specify 'filter.issueId' or 'filter.issueVariantId' parameters.`, + }); + } + if (result.content.length > 0) { + // There are errors or guides the agent must read + return result; + } + // Otherwise continue and list events const response: ListEventsResponse = await listEvents(appId, filter, pageSize); const eventsContent = response.events?.map((e) => toText(e)) || []; return { @@ -171,10 +189,24 @@ export const batch_get_events = tool( }, }, async ({ appId, names }) => { - if (!appId) return mcpError(`Must specify 'appId' parameter.`); - if (!names || names.length === 0) - return mcpError(`Must provide event resource names in name parameter.`); - + const result: CallToolResult = { content: [] }; + if (!appId) { + result.isError = true; + result.content.push({ type: "text", text: "Must specify 'appId' parameter." }); + result.content.push({ type: "text", text: forceAppIdGuide }); + } + if (!names || names.length === 0) { + result.isError = true; + result.content.push({ + type: "text", + text: "Must provide event resource names in name parameter.", + }); + } + if (result.content.length > 0) { + // There are errors or guides the agent must read + return result; + } + // Otherwise continue and get events const response: BatchGetEventsResponse = await batchGetEvents(appId, names); const eventsContent = response.events?.map((e) => toText(e)) || []; return { diff --git a/src/mcp/tools/crashlytics/index.ts b/src/mcp/tools/crashlytics/index.ts index 4d11ddf7169..c1242869525 100644 --- a/src/mcp/tools/crashlytics/index.ts +++ b/src/mcp/tools/crashlytics/index.ts @@ -2,14 +2,7 @@ import type { ServerTool } from "../../tool"; import { create_note, list_notes, delete_note } from "./notes"; import { get_issue, update_issue } from "./issues"; import { list_events, batch_get_events } from "./events"; -import { - get_top_issues, - get_top_variants, - get_top_versions, - get_top_apple_devices, - get_top_operating_systems, - get_top_android_devices, -} from "./reports"; +import { get_report } from "./reports"; export const crashlyticsTools: ServerTool[] = [ create_note, @@ -18,11 +11,6 @@ export const crashlyticsTools: ServerTool[] = [ list_events, batch_get_events, list_notes, - get_top_issues, - get_top_variants, - get_top_versions, - get_top_apple_devices, - get_top_android_devices, - get_top_operating_systems, + get_report, update_issue, ]; diff --git a/src/mcp/tools/crashlytics/issues.ts b/src/mcp/tools/crashlytics/issues.ts index 8f9c2af5a24..0656777b746 100644 --- a/src/mcp/tools/crashlytics/issues.ts +++ b/src/mcp/tools/crashlytics/issues.ts @@ -1,9 +1,12 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types"; import { z } from "zod"; -import { tool } from "../../tool"; +import { ApplicationIdSchema, IssueIdSchema } from "../../../crashlytics/filters"; import { getIssue, updateIssue } from "../../../crashlytics/issues"; import { State } from "../../../crashlytics/types"; -import { ApplicationIdSchema, IssueIdSchema } from "../../../crashlytics/filters"; -import { mcpError, toContent } from "../../util"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; + +import { RESOURCE_CONTENT as forceAppIdGuide } from "../../resources/guides/app_id"; export const get_issue = tool( "crashlytics", @@ -23,9 +26,21 @@ export const get_issue = tool( }, }, async ({ appId, issueId }) => { - if (!appId) return mcpError(`Must specify 'appId' parameter.`); - if (!issueId) return mcpError(`Must specify 'issueId' parameter.`); - + const result: CallToolResult = { content: [] }; + if (!appId) { + result.isError = true; + result.content.push({ type: "text", text: "Must specify 'appId' parameter" }); + result.content.push({ type: "text", text: forceAppIdGuide }); + } + if (!issueId) { + result.isError = true; + result.content.push({ type: "text", text: "Must specify 'issueId' parameter." }); + } + if (result.content.length > 0) { + // There are errors or guides the agent must read + return result; + } + // Continue and get the issue data return toContent(await getIssue(appId, issueId)); }, ); @@ -51,10 +66,25 @@ export const update_issue = tool( }, }, async ({ appId, issueId, state }) => { - if (!appId) return mcpError(`Must specify 'app_id' parameter.`); - if (!issueId) return mcpError(`Must specify 'issue_id' parameter.`); - if (!state) return mcpError(`Must specify 'state' parameter.`); - + const result: CallToolResult = { content: [] }; + if (!appId) { + result.isError = true; + result.content.push({ type: "text", text: "Must specify 'appId' parameter" }); + result.content.push({ type: "text", text: forceAppIdGuide }); + } + if (!issueId) { + result.isError = true; + result.content.push({ type: "text", text: "Must specify 'issueId' parameter." }); + } + if (!state) { + result.isError = true; + result.content.push({ type: "text", text: "Must specify 'state' parameter" }); + } + if (result.content.length > 0) { + // There are errors or guides the agent must read + return result; + } + // Continue and get the issue data return toContent(await updateIssue(appId, issueId, state)); }, ); diff --git a/src/mcp/tools/crashlytics/reports.ts b/src/mcp/tools/crashlytics/reports.ts index 3edd8e6e436..64966c56092 100644 --- a/src/mcp/tools/crashlytics/reports.ts +++ b/src/mcp/tools/crashlytics/reports.ts @@ -1,191 +1,92 @@ -import { tool } from "../../tool"; -import { mcpError, toContent } from "../../util"; -import { - CrashlyticsReport, - getReport, - ReportInputSchema, - ReportInput, - simplifyReport, -} from "../../../crashlytics/reports"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { validateEventFilters } from "../../../crashlytics/filters"; +import { dump, DumpOptions } from "js-yaml"; +import { EventFilter, validateEventFilters } from "../../../crashlytics/filters"; +import { getReport, ReportInputSchema, simplifyReport } from "../../../crashlytics/reports"; +import { Report } from "../../../crashlytics/types"; +import { tool } from "../../tool"; -// Generates the tool call fn for requesting a Crashlytics report +import { RESOURCE_CONTENT as forceAppIdGuide } from "../../resources/guides/app_id"; -function getReportContent( - report: CrashlyticsReport, - additionalPrompt?: string, -): (input: ReportInput) => Promise { - return async ({ appId, filter, pageSize }) => { - if (!appId) return mcpError(`Must specify 'appId' parameter.`); - filter ??= {}; - if (!!filter.intervalStartTime && !filter.intervalEndTime) { - // interval.end_time is required if interval.start_time is set but the agent likes to forget it - filter.intervalEndTime = new Date().toISOString(); - } - if (report === CrashlyticsReport.TopIssues && !!filter.issueId) { - delete filter.issueId; - } - validateEventFilters(filter); // throws here if invalid filters - const reportResponse = simplifyReport(await getReport(report, appId, filter, pageSize)); - if (!reportResponse.groups?.length) { - additionalPrompt = "This report response contains no results."; - } - if (additionalPrompt) { - reportResponse.usage = (reportResponse.usage || "").concat("\n", additionalPrompt); - } - return toContent(reportResponse); - }; -} +const DUMP_OPTIONS: DumpOptions = { lineWidth: 200 }; -// Currently, it appears to work best if the five different supported reports -// are expressed as five different tools. This allows the usage and format -// of each report to be more clearly described. In the future, it may be possible -// to consolidate all of these into a single `get_report` tool. +const REPORT_ERROR_CONTENT = ` +Must specify the desired report: + * TOP_ISSUES - metrics grouped by *issue*. + * TOP_VARIANTS - metrics grouped by issue *variant* + * TOP_VERSIONS - metrics grouped by *version* + * TOP_OPERATING_SYSTEMS - metrics grouped by *operating system* + * TOP_ANDROID_DEVICES - metrics grouped by *device* + * TOP_APPLE_DEVICES - metrics grouped by *device* +`.trim(); -export const get_top_issues = tool( - "crashlytics", - { - name: "get_top_issues", - description: `Use this to count events and distinct impacted users, grouped by *issue*. - Groups are sorted by event count, in descending order. - Only counts events matching the given filters.`, - inputSchema: ReportInputSchema, - annotations: { - title: "Get Crashlytics Top Issues Report", - readOnlyHint: true, - }, - _meta: { - requiresAuth: true, - }, - }, - getReportContent( - CrashlyticsReport.TopIssues, - `The crashlytics_batch_get_event tool can retrieve the sample events in this response. - Pass the sampleEvent in the names field. - The crashlytics_list_events tool can retrieve a list of events for an issue in this response. - Pass the issue.id in the filter.issueId field.`, - ), -); - -export const get_top_variants = tool( - "crashlytics", - { - name: "get_top_variants", - description: `Counts events and distinct impacted users, grouped by issue *variant*. - Groups are sorted by event count, in descending order. - Only counts events matching the given filters.`, - inputSchema: ReportInputSchema, - annotations: { - title: "Get Crashlytics Top Variants Report", - readOnlyHint: true, - }, - _meta: { - requiresAuth: true, - }, - }, - getReportContent( - CrashlyticsReport.TopVariants, - `The crashlytics_get_top_issues tool can report the top issues for the variants in this response. - Pass the variant.displayName in the filter.variantDisplayNames field. - The crashlytics_list_events tool can retrieve a list of events for a variant in this response.`, - ), -); +function toText(response: Report, filters: EventFilter): Record { + const result: Record = { + name: response.name || "", // So name is first in the output + filters: dump(filters, DUMP_OPTIONS), + }; + for (const [key, value] of Object.entries(response)) { + if (key === "name") { + continue; + } + result[key] = dump(value, DUMP_OPTIONS); + } + return result; +} -export const get_top_versions = tool( - "crashlytics", - { - name: "get_top_versions", - description: `Counts events and distinct impacted users, grouped by *version*. - Groups are sorted by event count, in descending order. - Only counts events matching the given filters.`, - inputSchema: ReportInputSchema, - annotations: { - title: "Get Crashlytics Top Versions Report", - readOnlyHint: true, - }, - _meta: { - requiresAuth: true, - }, - }, - getReportContent( - CrashlyticsReport.TopVersions, - `The crashlytics_get_top_issues tool can report the top issues for the versions in this response. - Pass the version.displayName in the filter.versionDisplayNames field. - The crashlytics_list_events tool can retrieve a list of events for a version in this response.`, - ), -); +// Generates the tool call fn for requesting a Crashlytics report -export const get_top_apple_devices = tool( +export const get_report = tool( "crashlytics", { - name: "get_top_apple_devices", - description: `Counts events and distinct impacted users, grouped by apple *device*. - Groups are sorted by event count, in descending order. - Only counts events matching the given filters. - Only relevant for iOS, iPadOS and MacOS applications.`, + name: "get_report", + description: + `Use this to request numerical reports from Crashlytics. The result aggregates the sum of events and impacted users, grouped by a dimension appropriate for that report. Agents must read the [Firebase Crashlytics Reports Guide](firebase://guides/crashlytics/reports) using the \`firebase_read_resources\` tool before calling to understand criticial prerequisites for requesting reports and how to interpret the results. + `.trim(), inputSchema: ReportInputSchema, annotations: { - title: "Get Crashlytics Top Apple Devices Report", + title: "Get Crashlytics Report", readOnlyHint: true, }, _meta: { requiresAuth: true, }, }, - getReportContent( - CrashlyticsReport.TopAppleDevices, - `The crashlytics_get_top_issues tool can report the top issues for the devices in this response. - Pass the device.displayName in the filter.deviceDisplayNames field. - The crashlytics_list_events tool can retrieve a list of events for a device in this response.`, - ), -); + async ({ appId, report, pageSize, filter }) => { + const result: CallToolResult = { content: [] }; -export const get_top_android_devices = tool( - "crashlytics", - { - name: "get_top_android_devices", - description: `Counts events and distinct impacted users, grouped by android *device*. - Groups are sorted by event count, in descending order. - Only counts events matching the given filters. - Only relevant for Android applications.`, - inputSchema: ReportInputSchema, - annotations: { - title: "Get Crashlytics Top Android Devices Report", - readOnlyHint: true, - }, - _meta: { - requiresAuth: true, - }, - }, - getReportContent( - CrashlyticsReport.TopAndroidDevices, - `The crashlytics_get_top_issues tool can report the top issues for the devices in this response. - Pass the device.displayName in the filter.deviceDisplayNames field. - The crashlytics_list_events tool can retrieve a list of events for a device in this response.`, - ), -); + if (!report) { + result.isError = true; + result.content.push({ type: "text", text: `Error: ${REPORT_ERROR_CONTENT}` }); + } + if (!appId) { + result.isError = true; + result.content.push({ type: "text", text: "Must specify 'appId' parameter" }); + result.content.push({ type: "text", text: forceAppIdGuide }); + } + try { + filter = validateEventFilters(filter || {}); + } catch (error: any) { + result.isError = true; + result.content.push({ type: "text", text: `Error: ${error.message}` }); + } + if (result.content.length > 0) { + // There are errors or guides the agent needs to read first. + return result; + } + // Everything is OK so fetch report + const reportResponse = simplifyReport(await getReport(report, appId, filter, pageSize)); + reportResponse.usage = + reportResponse.groups && reportResponse.groups.length + ? reportResponse.usage || "" + : "This report response contains no results."; // Helps to make empty state more obvious -export const get_top_operating_systems = tool( - "crashlytics", - { - name: "get_top_operating_systems", - description: `Counts events and distinct impacted users, grouped by *operating system*. - Groups are sorted by event count, in descending order. - Only counts events matching the given filters.`, - inputSchema: ReportInputSchema, - annotations: { - title: "Get Crashlytics Top Operating Systems Report", - readOnlyHint: true, - }, - _meta: { - requiresAuth: true, - }, + return { + content: [ + { + type: "text", + text: dump(toText(reportResponse, filter), DUMP_OPTIONS), + }, + ], + }; }, - getReportContent( - CrashlyticsReport.TopOperatingSystems, - `The crashlytics_get_top_issues tool can report the top issues for the operating systems in this response. - Pass the operatingSystem.displayName in the filter.operatingSystemDisplayNames field. - The crashlytics_list_events tool can retrieve a list of events for an operating system in this response.`, - ), );