Skip to content

Commit 8b44e46

Browse files
authored
[MCP] Introduce Crashlytics MCP tool list_top_issues. (#8570)
1 parent 1c51856 commit 8b44e46

File tree

7 files changed

+102
-0
lines changed

7 files changed

+102
-0
lines changed

src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export const remoteConfigApiOrigin = () =>
127127
utils.envOverride("FIREBASE_REMOTE_CONFIG_URL", "https://firebaseremoteconfig.googleapis.com");
128128
export const messagingApiOrigin = () =>
129129
utils.envOverride("FIREBASE_MESSAGING_CONFIG_URL", "https://fcm.googleapis.com");
130+
export const crashlyticsApiOrigin = () =>
131+
utils.envOverride("FIREBASE_CRASHLYTICS_URL", "https://firebasecrashlytics.googleapis.com");
130132
export const resourceManagerOrigin = () =>
131133
utils.envOverride("FIREBASE_RESOURCEMANAGER_URL", "https://cloudresourcemanager.googleapis.com");
132134
export const rulesOrigin = () =>

src/crashlytics/listTopIssues.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Client } from "../apiv2";
2+
import { logger } from "../logger";
3+
import { FirebaseError } from "../error";
4+
import { crashlyticsApiOrigin } from "../api";
5+
6+
const TIMEOUT = 10000;
7+
8+
const apiClient = new Client({
9+
urlPrefix: crashlyticsApiOrigin(),
10+
apiVersion: "v1alpha",
11+
});
12+
13+
export async function listTopIssues(
14+
projectId: string,
15+
appId: string,
16+
issueCount: number,
17+
): Promise<string> {
18+
try {
19+
const queryParams = new URLSearchParams();
20+
queryParams.set("page_size", `${issueCount}`);
21+
22+
const requestProjectId = parseProjectId(appId);
23+
if (requestProjectId === undefined) {
24+
throw new FirebaseError("Unable to get the projectId from the AppId.");
25+
}
26+
27+
const response = await apiClient.request<void, string>({
28+
method: "GET",
29+
headers: {
30+
"Content-Type": "application/json",
31+
},
32+
path: `/projects/${requestProjectId}/apps/${appId}/reports/topIssues`,
33+
queryParams: queryParams,
34+
timeout: TIMEOUT,
35+
});
36+
37+
return response.body;
38+
} catch (err: any) {
39+
logger.debug(err.message);
40+
throw new FirebaseError(
41+
`Failed to fetch the top issues for the Firebase Project ${projectId}, AppId ${appId}. Error: ${err}.`,
42+
{ original: err },
43+
);
44+
}
45+
}
46+
47+
function parseProjectId(appId: string): string | undefined {
48+
const appIdParts = appId.split(":");
49+
if (appIdParts.length > 1) {
50+
return appIdParts[1];
51+
}
52+
return undefined;
53+
}

src/mcp/tools/crashlytics/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { ServerTool } from "../../tool.js";
2+
import { list_top_issues } from "./list_top_issues.js";
3+
4+
export const crashlyticsTools: ServerTool[] = [list_top_issues];
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { z } from "zod";
2+
import { tool } from "../../tool.js";
3+
import { mcpError, toContent } from "../../util.js";
4+
import { listTopIssues } from "../../../crashlytics/listTopIssues.js";
5+
6+
export const list_top_issues = tool(
7+
{
8+
name: "list_top_issues",
9+
description: "List the top crashes from crashlytics happening in the application.",
10+
inputSchema: z.object({
11+
app_id: z
12+
.string()
13+
.optional()
14+
.describe(
15+
"AppId for which the issues list is fetched. Defaults to the first appId provided by firebase_list_apps.",
16+
),
17+
issue_count: z
18+
.number()
19+
.optional()
20+
.describe("Number of issues that needs to be fetched. Defaults to 10 if unspecified."),
21+
}),
22+
annotations: {
23+
title: "List Top Crashlytics Issues.",
24+
readOnlyHint: true,
25+
},
26+
_meta: {
27+
requiresAuth: true,
28+
requiresProject: true,
29+
},
30+
},
31+
async ({ app_id, issue_count }, { projectId }) => {
32+
if (!app_id) return mcpError(`Must specify 'app_id' parameter.`);
33+
34+
issue_count ??= 10;
35+
36+
return toContent(await listTopIssues(projectId!, app_id, issue_count));
37+
},
38+
);

src/mcp/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { coreTools } from "./core/index.js";
77
import { storageTools } from "./storage/index.js";
88
import { messagingTools } from "./messaging/index.js";
99
import { remoteConfigTools } from "./remoteconfig/index.js";
10+
import { crashlyticsTools } from "./crashlytics/index.js";
1011

1112
/** availableTools returns the list of MCP tools available given the server flags */
1213
export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] {
@@ -28,6 +29,7 @@ const tools: Record<ServerFeature, ServerTool[]> = {
2829
storage: addFeaturePrefix("storage", storageTools),
2930
messaging: addFeaturePrefix("messaging", messagingTools),
3031
remoteconfig: addFeaturePrefix("remoteconfig", remoteConfigTools),
32+
crashlytics: addFeaturePrefix("crashlytics", crashlyticsTools),
3133
};
3234

3335
function addFeaturePrefix(feature: string, tools: ServerTool[]): ServerTool[] {

src/mcp/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const SERVER_FEATURES = [
55
"auth",
66
"messaging",
77
"remoteconfig",
8+
"crashlytics",
89
] as const;
910
export type ServerFeature = (typeof SERVER_FEATURES)[number];
1011

src/mcp/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
messagingApiOrigin,
1111
remoteConfigApiOrigin,
1212
storageOrigin,
13+
crashlyticsApiOrigin,
1314
} from "../api";
1415
import { check } from "../ensureApiEnabled";
1516

@@ -85,6 +86,7 @@ const SERVER_FEATURE_APIS: Record<ServerFeature, string> = {
8586
auth: authManagementOrigin(),
8687
messaging: messagingApiOrigin(),
8788
remoteconfig: remoteConfigApiOrigin(),
89+
crashlytics: crashlyticsApiOrigin(),
8890
};
8991

9092
/**

0 commit comments

Comments
 (0)