From cc064c0ac492d90cd3e874bc08bc4ad9cd574a58 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 15 May 2025 15:51:21 -0700 Subject: [PATCH 01/13] initial apphosting mcp tool --- src/gcp/apphosting.ts | 13 +++++++ src/mcp/tools/apphosting/index.ts | 4 ++ src/mcp/tools/apphosting/list_backends.ts | 46 +++++++++++++++++++++++ src/mcp/tools/index.ts | 2 + src/mcp/types.ts | 1 + src/mcp/util.ts | 2 + 6 files changed, 68 insertions(+) create mode 100644 src/mcp/tools/apphosting/index.ts create mode 100644 src/mcp/tools/apphosting/list_backends.ts diff --git a/src/gcp/apphosting.ts b/src/gcp/apphosting.ts index 8d734426fc6..832dced595e 100644 --- a/src/gcp/apphosting.ts +++ b/src/gcp/apphosting.ts @@ -333,6 +333,19 @@ export async function getBackend( return res.body; } +/** + * Gets traffic details. + */ +export async function getTraffic( + projectId: string, + location: string, + backendId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`; + const res = await client.get(name); + return res.body; +} + /** * List all backends present in a project and location. */ diff --git a/src/mcp/tools/apphosting/index.ts b/src/mcp/tools/apphosting/index.ts new file mode 100644 index 00000000000..d036475d488 --- /dev/null +++ b/src/mcp/tools/apphosting/index.ts @@ -0,0 +1,4 @@ +import { ServerTool } from "../../tool"; +import { list_backends } from "./list_backends"; + +export const appHostingTools: ServerTool[] = [list_backends]; diff --git a/src/mcp/tools/apphosting/list_backends.ts b/src/mcp/tools/apphosting/list_backends.ts new file mode 100644 index 00000000000..2530172ece5 --- /dev/null +++ b/src/mcp/tools/apphosting/list_backends.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; +import { tool } from "../../tool.js"; +import { toContent } from "../../util.js"; +import { NO_PROJECT_ERROR } from "../../errors.js"; +import { + Backend, + getTraffic, + listBackends, + parseBackendName, + Traffic, +} from "../../../gcp/apphosting.js"; + +export const list_backends = tool( + { + name: "list_backends", + description: + "Retrieves a list of App Hosting backends in the current project. The `uri` is the public URL of the backend.", + inputSchema: z.object({ + location: z + .string() + .optional() + .default("-") + .describe("Limit the listed backends to this region."), + }), + annotations: { + title: "List App Hosting backends.", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ location } = {}, { projectId }) => { + if (!projectId) return NO_PROJECT_ERROR; + if (!location) location = "-"; + const data: (Backend & { traffic: Traffic })[] = []; + const backends = await listBackends(projectId, location); + for (const backend of backends.backends) { + const { location, id } = parseBackendName(backend.name); + const traffic = await getTraffic(projectId, location, id); + data.push({ ...backend, traffic: traffic }); + } + return toContent(data); + }, +); diff --git a/src/mcp/tools/index.ts b/src/mcp/tools/index.ts index a803dc406a9..bec130ddde7 100644 --- a/src/mcp/tools/index.ts +++ b/src/mcp/tools/index.ts @@ -7,6 +7,7 @@ import { coreTools } from "./core/index.js"; import { storageTools } from "./storage/index.js"; import { messagingTools } from "./messaging/index.js"; import { remoteConfigTools } from "./remoteconfig/index.js"; +import { appHostingTools } from "./apphosting/index.js"; /** availableTools returns the list of MCP tools available given the server flags */ export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] { @@ -28,6 +29,7 @@ const tools: Record = { storage: addFeaturePrefix("storage", storageTools), messaging: addFeaturePrefix("messaging", messagingTools), remoteconfig: addFeaturePrefix("remoteconfig", remoteConfigTools), + apphosting: addFeaturePrefix("apphosting", appHostingTools), }; function addFeaturePrefix(feature: string, tools: ServerTool[]): ServerTool[] { diff --git a/src/mcp/types.ts b/src/mcp/types.ts index a0c23f8cde6..6306f700b6b 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -5,5 +5,6 @@ export const SERVER_FEATURES = [ "auth", "messaging", "remoteconfig", + "apphosting", ] as const; export type ServerFeature = (typeof SERVER_FEATURES)[number]; diff --git a/src/mcp/util.ts b/src/mcp/util.ts index 770000342f8..3e5eccd0028 100644 --- a/src/mcp/util.ts +++ b/src/mcp/util.ts @@ -4,6 +4,7 @@ import { dump } from "js-yaml"; import { platform } from "os"; import { ServerFeature } from "./types"; import { + apphostingOrigin, authManagementOrigin, dataconnectOrigin, firestoreOrigin, @@ -85,6 +86,7 @@ const SERVER_FEATURE_APIS: Record = { auth: authManagementOrigin(), messaging: messagingApiOrigin(), remoteconfig: remoteConfigApiOrigin(), + apphosting: apphostingOrigin(), }; /** From a4088ba74d66977ff1b3a46e15bf354fe5c776f8 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Fri, 16 May 2025 21:21:13 -0700 Subject: [PATCH 02/13] add fetchServiceLogs function --- src/gcp/run.ts | 71 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/gcp/run.ts b/src/gcp/run.ts index 286f12e82f9..c9e3921aed0 100644 --- a/src/gcp/run.ts +++ b/src/gcp/run.ts @@ -1,6 +1,6 @@ import { Client } from "../apiv2"; import { FirebaseError } from "../error"; -import { runOrigin } from "../api"; +import { cloudloggingOrigin, runOrigin } from "../api"; import * as proto from "./proto"; import * as iam from "./iam"; import { backoff } from "../throttler/throttler"; @@ -344,3 +344,72 @@ export async function setInvokerUpdate( }; await setIamPolicy(serviceName, policy, httpClient); } + +interface EntriesListRequest { + resourceNames: string[]; + filter?: string; + orderBy?: string; + pageSize?: number; + pageToken?: string; +} + +interface EntriesListResponse { + entries?: LogEntry[]; + nextPageToken?: string; +} + +interface LogEntry { + logName: string; + resource: unknown; + timestamp: string; + receiveTimestamp: string; + httpRequest?: unknown; + + protoPayload?: unknown; + textPayload?: string; + jsonPayload?: unknown; + + severity: + | "DEFAULT" + | "DEBUG" + | "INFO" + | "NOTICE" + | "WARNING" + | "ERROR" + | "CRITICAL" + | "ALERT" + | "EMERGENCY"; +} + +/** + * Fetches recent logs for a given Cloud Run service using the Cloud Logging API. + * @param projectId The Google Cloud project ID. + * @param serviceId The resource name of the Cloud Run service. + * @return A promise that resolves with the log entries. + */ +export async function fetchServiceLogs(projectId: string, serviceId: string): Promise { + const loggingClient = new Client({ + urlPrefix: cloudloggingOrigin(), + apiVersion: "v2", + }); + + const requestBody: EntriesListRequest = { + resourceNames: [`projects/${projectId}`], + filter: `resource.type="cloud_run_revision" AND resource.labels.service_name="${serviceId}"`, + orderBy: "timestamp desc", + pageSize: 100, + }; + + try { + const response = await loggingClient.post( + "/entries:list", + requestBody, + ); + return response.body.entries || []; + } catch (err: any) { + throw new FirebaseError(`Failed to fetch logs for Cloud Run service ${serviceId}`, { + original: err, + status: err?.context?.response?.statusCode, + }); + } +} From 98d42f31f9b2e3c6c0ce358a6b11e1c70526b971 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Fri, 16 May 2025 21:21:43 -0700 Subject: [PATCH 03/13] add run tool to fetch logs --- src/mcp/tools/apphosting/list_backends.ts | 5 +++- src/mcp/tools/index.ts | 2 ++ src/mcp/tools/run/fetch_logs.ts | 30 +++++++++++++++++++++++ src/mcp/tools/run/index.ts | 4 +++ src/mcp/types.ts | 1 + src/mcp/util.ts | 2 ++ 6 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/mcp/tools/run/fetch_logs.ts create mode 100644 src/mcp/tools/run/index.ts diff --git a/src/mcp/tools/apphosting/list_backends.ts b/src/mcp/tools/apphosting/list_backends.ts index 2530172ece5..42144707287 100644 --- a/src/mcp/tools/apphosting/list_backends.ts +++ b/src/mcp/tools/apphosting/list_backends.ts @@ -14,7 +14,10 @@ export const list_backends = tool( { name: "list_backends", description: - "Retrieves a list of App Hosting backends in the current project. The `uri` is the public URL of the backend.", + "Retrieves 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.", inputSchema: z.object({ location: z .string() diff --git a/src/mcp/tools/index.ts b/src/mcp/tools/index.ts index 332a08838a6..7b83dcb9eb5 100644 --- a/src/mcp/tools/index.ts +++ b/src/mcp/tools/index.ts @@ -9,6 +9,7 @@ import { messagingTools } from "./messaging/index.js"; import { remoteConfigTools } from "./remoteconfig/index.js"; import { crashlyticsTools } from "./crashlytics/index.js"; import { appHostingTools } from "./apphosting/index.js"; +import { runTools } from "./run/index.js"; /** availableTools returns the list of MCP tools available given the server flags */ export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] { @@ -32,6 +33,7 @@ const tools: Record = { remoteconfig: addFeaturePrefix("remoteconfig", remoteConfigTools), crashlytics: addFeaturePrefix("crashlytics", crashlyticsTools), apphosting: addFeaturePrefix("apphosting", appHostingTools), + run: addFeaturePrefix("run", runTools), }; function addFeaturePrefix(feature: string, tools: ServerTool[]): ServerTool[] { diff --git a/src/mcp/tools/run/fetch_logs.ts b/src/mcp/tools/run/fetch_logs.ts new file mode 100644 index 00000000000..3890e7bc39e --- /dev/null +++ b/src/mcp/tools/run/fetch_logs.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; +import { tool } from "../../tool.js"; +import { toContent } from "../../util.js"; +import { NO_PROJECT_ERROR } from "../../errors.js"; +import { fetchServiceLogs } from "../../../gcp/run.js"; + +export const fetch_logs = tool( + { + name: "fetch_logs", + description: + "Fetches recent logs for a Cloud Run service. Includes details such as the message, severity, and timestamp.", + inputSchema: z.object({ + serviceId: z.string().describe("The Cloud Run service ID."), + }), + annotations: { + title: "Fetch recent Cloud Run service logs.", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ serviceId } = {}, { projectId }) => { + if (!projectId) return NO_PROJECT_ERROR; + if (!serviceId) return toContent("A Cloud Run service ID must be provided."); + const data = await fetchServiceLogs(projectId, serviceId); + return toContent(data); + }, +); diff --git a/src/mcp/tools/run/index.ts b/src/mcp/tools/run/index.ts new file mode 100644 index 00000000000..0dac7c3f910 --- /dev/null +++ b/src/mcp/tools/run/index.ts @@ -0,0 +1,4 @@ +import { ServerTool } from "../../tool"; +import { fetch_logs } from "./fetch_logs"; + +export const runTools: ServerTool[] = [fetch_logs]; diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 784137b4ab4..7db4fcedd4f 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -7,6 +7,7 @@ export const SERVER_FEATURES = [ "remoteconfig", "crashlytics", "apphosting", + "run", ] as const; export type ServerFeature = (typeof SERVER_FEATURES)[number]; diff --git a/src/mcp/util.ts b/src/mcp/util.ts index abd0fc5ed1f..501df37471a 100644 --- a/src/mcp/util.ts +++ b/src/mcp/util.ts @@ -12,6 +12,7 @@ import { remoteConfigApiOrigin, storageOrigin, crashlyticsApiOrigin, + cloudRunApiOrigin, } from "../api"; import { check } from "../ensureApiEnabled"; @@ -89,6 +90,7 @@ const SERVER_FEATURE_APIS: Record = { remoteconfig: remoteConfigApiOrigin(), crashlytics: crashlyticsApiOrigin(), apphosting: apphostingOrigin(), + run: cloudRunApiOrigin(), }; /** From 9cb48c154bf1e78dd51f3cefdefc200487b49aa5 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Fri, 16 May 2025 21:24:20 -0700 Subject: [PATCH 04/13] add a little more description for location --- src/mcp/tools/apphosting/list_backends.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mcp/tools/apphosting/list_backends.ts b/src/mcp/tools/apphosting/list_backends.ts index 42144707287..e64588bc30b 100644 --- a/src/mcp/tools/apphosting/list_backends.ts +++ b/src/mcp/tools/apphosting/list_backends.ts @@ -23,7 +23,9 @@ export const list_backends = tool( .string() .optional() .default("-") - .describe("Limit the listed backends to this region."), + .describe( + "Limit the listed backends to this region. By default, it will list all backends across all regions.", + ), }), annotations: { title: "List App Hosting backends.", From b6916b9f471f7c19074f0850c2f0b4ce526624fa Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Tue, 20 May 2025 11:21:29 -0700 Subject: [PATCH 05/13] cleaning up logic a bit --- src/mcp/tools/apphosting/list_backends.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mcp/tools/apphosting/list_backends.ts b/src/mcp/tools/apphosting/list_backends.ts index e64588bc30b..e1aa1aaf7fa 100644 --- a/src/mcp/tools/apphosting/list_backends.ts +++ b/src/mcp/tools/apphosting/list_backends.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { tool } from "../../tool.js"; import { toContent } from "../../util.js"; -import { NO_PROJECT_ERROR } from "../../errors.js"; import { Backend, getTraffic, @@ -37,7 +36,7 @@ export const list_backends = tool( }, }, async ({ location } = {}, { projectId }) => { - if (!projectId) return NO_PROJECT_ERROR; + projectId = projectId || ""; if (!location) location = "-"; const data: (Backend & { traffic: Traffic })[] = []; const backends = await listBackends(projectId, location); @@ -46,6 +45,11 @@ export const list_backends = tool( const traffic = await getTraffic(projectId, location, id); data.push({ ...backend, traffic: traffic }); } + if (!data.length) { + return toContent( + `No backends exist for project ${projectId}${location !== "-" ? ` in ${location}` : ""}.`, + ); + } return toContent(data); }, ); From d757d2d4719cde0fdd57b413d14be727a9c64526 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 22 May 2025 11:21:59 -0700 Subject: [PATCH 06/13] add new tools to smoke test --- scripts/mcp-tests/gemini-smoke-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mcp-tests/gemini-smoke-test.ts b/scripts/mcp-tests/gemini-smoke-test.ts index d0c894167ba..f2a6ff6b233 100644 --- a/scripts/mcp-tests/gemini-smoke-test.ts +++ b/scripts/mcp-tests/gemini-smoke-test.ts @@ -17,7 +17,7 @@ await client.connect( args: [ "experimental:mcp", "--only", - "firestore,dataconnect,messaging,remoteconfig,crashlytics,auth,storage", + "firestore,dataconnect,messaging,remoteconfig,crashlytics,auth,storage,apphosting,run", ], }), ); From dccc983ba35dd341aa8b73a7528d7d411ab52c52 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 22 May 2025 11:34:16 -0700 Subject: [PATCH 07/13] don't reinvent the cloud run logs wheel --- src/gcp/run.ts | 62 +++++++------------------------------------------- 1 file changed, 8 insertions(+), 54 deletions(-) diff --git a/src/gcp/run.ts b/src/gcp/run.ts index c9e3921aed0..3c075f3b220 100644 --- a/src/gcp/run.ts +++ b/src/gcp/run.ts @@ -1,10 +1,11 @@ import { Client } from "../apiv2"; import { FirebaseError } from "../error"; -import { cloudloggingOrigin, runOrigin } from "../api"; +import { runOrigin } from "../api"; import * as proto from "./proto"; import * as iam from "./iam"; import { backoff } from "../throttler/throttler"; import { logger } from "../logger"; +import { listEntries, LogEntry } from "./cloudlogging"; const API_VERSION = "v1"; @@ -345,42 +346,6 @@ export async function setInvokerUpdate( await setIamPolicy(serviceName, policy, httpClient); } -interface EntriesListRequest { - resourceNames: string[]; - filter?: string; - orderBy?: string; - pageSize?: number; - pageToken?: string; -} - -interface EntriesListResponse { - entries?: LogEntry[]; - nextPageToken?: string; -} - -interface LogEntry { - logName: string; - resource: unknown; - timestamp: string; - receiveTimestamp: string; - httpRequest?: unknown; - - protoPayload?: unknown; - textPayload?: string; - jsonPayload?: unknown; - - severity: - | "DEFAULT" - | "DEBUG" - | "INFO" - | "NOTICE" - | "WARNING" - | "ERROR" - | "CRITICAL" - | "ALERT" - | "EMERGENCY"; -} - /** * Fetches recent logs for a given Cloud Run service using the Cloud Logging API. * @param projectId The Google Cloud project ID. @@ -388,28 +353,17 @@ interface LogEntry { * @return A promise that resolves with the log entries. */ export async function fetchServiceLogs(projectId: string, serviceId: string): Promise { - const loggingClient = new Client({ - urlPrefix: cloudloggingOrigin(), - apiVersion: "v2", - }); - - const requestBody: EntriesListRequest = { - resourceNames: [`projects/${projectId}`], - filter: `resource.type="cloud_run_revision" AND resource.labels.service_name="${serviceId}"`, - orderBy: "timestamp desc", - pageSize: 100, - }; + const filter = `resource.type="cloud_run_revision" AND resource.labels.service_name="${serviceId}"`; + const pageSize = 100; + const order = "desc"; try { - const response = await loggingClient.post( - "/entries:list", - requestBody, - ); - return response.body.entries || []; + const entries = await listEntries(projectId, filter, pageSize, order); + return entries || []; } catch (err: any) { throw new FirebaseError(`Failed to fetch logs for Cloud Run service ${serviceId}`, { original: err, - status: err?.context?.response?.statusCode, + status: (err as any)?.context?.response?.statusCode, }); } } From 4312b4935e09ba5b555d60d738b5a1c054829b30 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 22 May 2025 12:23:13 -0700 Subject: [PATCH 08/13] back out cloud run tool --- scripts/mcp-tests/gemini-smoke-test.ts | 2 +- src/mcp/tools/index.ts | 2 -- src/mcp/tools/run/fetch_logs.ts | 30 -------------------------- src/mcp/tools/run/index.ts | 4 ---- src/mcp/types.ts | 1 - src/mcp/util.ts | 2 -- 6 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 src/mcp/tools/run/fetch_logs.ts delete mode 100644 src/mcp/tools/run/index.ts diff --git a/scripts/mcp-tests/gemini-smoke-test.ts b/scripts/mcp-tests/gemini-smoke-test.ts index f2a6ff6b233..fcb268b59e2 100644 --- a/scripts/mcp-tests/gemini-smoke-test.ts +++ b/scripts/mcp-tests/gemini-smoke-test.ts @@ -17,7 +17,7 @@ await client.connect( args: [ "experimental:mcp", "--only", - "firestore,dataconnect,messaging,remoteconfig,crashlytics,auth,storage,apphosting,run", + "firestore,dataconnect,messaging,remoteconfig,crashlytics,auth,storage,apphosting", ], }), ); diff --git a/src/mcp/tools/index.ts b/src/mcp/tools/index.ts index 7b83dcb9eb5..332a08838a6 100644 --- a/src/mcp/tools/index.ts +++ b/src/mcp/tools/index.ts @@ -9,7 +9,6 @@ import { messagingTools } from "./messaging/index.js"; import { remoteConfigTools } from "./remoteconfig/index.js"; import { crashlyticsTools } from "./crashlytics/index.js"; import { appHostingTools } from "./apphosting/index.js"; -import { runTools } from "./run/index.js"; /** availableTools returns the list of MCP tools available given the server flags */ export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] { @@ -33,7 +32,6 @@ const tools: Record = { remoteconfig: addFeaturePrefix("remoteconfig", remoteConfigTools), crashlytics: addFeaturePrefix("crashlytics", crashlyticsTools), apphosting: addFeaturePrefix("apphosting", appHostingTools), - run: addFeaturePrefix("run", runTools), }; function addFeaturePrefix(feature: string, tools: ServerTool[]): ServerTool[] { diff --git a/src/mcp/tools/run/fetch_logs.ts b/src/mcp/tools/run/fetch_logs.ts deleted file mode 100644 index 3890e7bc39e..00000000000 --- a/src/mcp/tools/run/fetch_logs.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { z } from "zod"; -import { tool } from "../../tool.js"; -import { toContent } from "../../util.js"; -import { NO_PROJECT_ERROR } from "../../errors.js"; -import { fetchServiceLogs } from "../../../gcp/run.js"; - -export const fetch_logs = tool( - { - name: "fetch_logs", - description: - "Fetches recent logs for a Cloud Run service. Includes details such as the message, severity, and timestamp.", - inputSchema: z.object({ - serviceId: z.string().describe("The Cloud Run service ID."), - }), - annotations: { - title: "Fetch recent Cloud Run service logs.", - readOnlyHint: true, - }, - _meta: { - requiresAuth: true, - requiresProject: true, - }, - }, - async ({ serviceId } = {}, { projectId }) => { - if (!projectId) return NO_PROJECT_ERROR; - if (!serviceId) return toContent("A Cloud Run service ID must be provided."); - const data = await fetchServiceLogs(projectId, serviceId); - return toContent(data); - }, -); diff --git a/src/mcp/tools/run/index.ts b/src/mcp/tools/run/index.ts deleted file mode 100644 index 0dac7c3f910..00000000000 --- a/src/mcp/tools/run/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ServerTool } from "../../tool"; -import { fetch_logs } from "./fetch_logs"; - -export const runTools: ServerTool[] = [fetch_logs]; diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 7db4fcedd4f..784137b4ab4 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -7,7 +7,6 @@ export const SERVER_FEATURES = [ "remoteconfig", "crashlytics", "apphosting", - "run", ] as const; export type ServerFeature = (typeof SERVER_FEATURES)[number]; diff --git a/src/mcp/util.ts b/src/mcp/util.ts index 427080955e7..256e139a91d 100644 --- a/src/mcp/util.ts +++ b/src/mcp/util.ts @@ -12,7 +12,6 @@ import { remoteConfigApiOrigin, storageOrigin, crashlyticsApiOrigin, - cloudRunApiOrigin, } from "../api"; import { check } from "../ensureApiEnabled"; @@ -90,7 +89,6 @@ const SERVER_FEATURE_APIS: Record = { remoteconfig: remoteConfigApiOrigin(), crashlytics: crashlyticsApiOrigin(), apphosting: apphostingOrigin(), - run: cloudRunApiOrigin(), }; /** From 6a092a603683baad435250980f15b55317859a82 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 22 May 2025 13:29:22 -0700 Subject: [PATCH 09/13] creates a new logs helper for app hosting for both build and runtime logs --- src/gcp/apphosting.ts | 5 ++ src/gcp/run.ts | 15 +++-- src/mcp/tools/apphosting/fetch_logs.ts | 86 ++++++++++++++++++++++++++ src/mcp/tools/apphosting/index.ts | 3 +- 4 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/mcp/tools/apphosting/fetch_logs.ts diff --git a/src/gcp/apphosting.ts b/src/gcp/apphosting.ts index 832dced595e..0f711abc2db 100644 --- a/src/gcp/apphosting.ts +++ b/src/gcp/apphosting.ts @@ -42,6 +42,11 @@ export interface Backend { uri: string; serviceAccount?: string; appId?: string; + managedResources?: ManagedResource[]; +} + +export interface ManagedResource { + runService: { service: string }; } export type BackendOutputOnlyFields = "name" | "createTime" | "updateTime" | "uri"; diff --git a/src/gcp/run.ts b/src/gcp/run.ts index 3c075f3b220..571f158b55c 100644 --- a/src/gcp/run.ts +++ b/src/gcp/run.ts @@ -353,17 +353,24 @@ export async function setInvokerUpdate( * @return A promise that resolves with the log entries. */ export async function fetchServiceLogs(projectId: string, serviceId: string): Promise { - const filter = `resource.type="cloud_run_revision" AND resource.labels.service_name="${serviceId}"`; + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + const timestampFilter = `timestamp >= "${thirtyDaysAgo.toISOString()}"`; + const filter = `resource.type="cloud_run_revision" AND resource.labels.service_name="${serviceId}" AND ${timestampFilter}`; const pageSize = 100; const order = "desc"; try { const entries = await listEntries(projectId, filter, pageSize, order); return entries || []; - } catch (err: any) { + } catch (err: unknown) { + let status: number | undefined; + if (typeof err === 'object' && err !== null && 'context' in err && typeof err.context === 'object' && err.context !== null && 'response' in err.context && typeof err.context.response === 'object' && err.context.response !== null && 'statusCode' in err.context.response && typeof err.context.response.statusCode === 'number') { + status = err.context.response.statusCode; + } throw new FirebaseError(`Failed to fetch logs for Cloud Run service ${serviceId}`, { - original: err, - status: (err as any)?.context?.response?.statusCode, + original: err instanceof Error ? err : undefined, + status: status, }); } } diff --git a/src/mcp/tools/apphosting/fetch_logs.ts b/src/mcp/tools/apphosting/fetch_logs.ts new file mode 100644 index 00000000000..3cc1db2cbf1 --- /dev/null +++ b/src/mcp/tools/apphosting/fetch_logs.ts @@ -0,0 +1,86 @@ +import { z } from "zod"; +import { tool } from "../../tool.js"; +import { toContent } from "../../util.js"; +import { + Backend, + getBackend, + getTraffic, + listBuilds, + parseBackendName, + Traffic, +} from "../../../gcp/apphosting.js"; +import { last } from "../../../utils.js"; +import { FirebaseError } from "../../../error.js"; +import { fetchServiceLogs } from "../../../gcp/run.js"; +import { listEntries } from "../../../gcp/cloudlogging.js"; + +export const fetch_logs = tool( + { + name: "fetch_logs", + description: + "Fetches 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.", + inputSchema: z.object({ + buildLogs: z + .boolean() + .default(false) + .describe( + "If specified, the logs for the most recent build will be returned instead of the logs for the service. The build logs are returned 'in order', to be read from top to bottom.", + ), + backendId: z.string().describe("The ID of the backend for which to fetch logs."), + location: z + .string() + .describe( + "The specific region for the backend. By default, if a backend is uniquely named across all locations, that one will be used.", + ), + }), + annotations: { + title: "Fetch logs for App Hosting backends and builds.", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ buildLogs, backendId, location } = {}, { projectId }) => { + projectId ||= ""; + location ||= ""; + if (!backendId) { + return toContent(`backendId must be specified.`); + } + const backend = await getBackend(projectId, location, backendId); + // if (location === "-") location = parseBackendName(backend.name).location; + const traffic = await getTraffic(projectId, location, backendId); + const data: Backend & { traffic: Traffic } = { ...backend, traffic }; + + if (buildLogs) { + const builds = await listBuilds(projectId, location, backendId); + // builds.builds.sort((a, b) => a.c) + const build = last(builds.builds); + const r = new RegExp(`region=${location}/([0-9a-f-]+)?`); + const match = r.exec(build.buildLogsUri ?? ""); + if (!match) { + throw new FirebaseError("Unable to determine the build ID."); + } + const buildId = match[1]; + // Thirty days ago makes sure we get any saved data within the default retention period. + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + const timestampFilter = `timestamp >= "${thirtyDaysAgo.toISOString()}"`; + const filter = `resource.type="build" resource.labels.build_id="${buildId}" ${timestampFilter}`; + const entries = await listEntries(projectId, filter, 100, "asc"); + if (!Array.isArray(entries) || !entries.length) { + return toContent("No logs found."); + } + return toContent(entries); + } + + const serviceName = last(data.managedResources)?.runService.service; + if (!serviceName) { + throw new FirebaseError("Unable to get service name from managedResources."); + } + const serviceId = last(serviceName.split("/")); + const logs = await fetchServiceLogs(projectId, serviceId); + return toContent(logs); + }, +); diff --git a/src/mcp/tools/apphosting/index.ts b/src/mcp/tools/apphosting/index.ts index d036475d488..0c9c3919894 100644 --- a/src/mcp/tools/apphosting/index.ts +++ b/src/mcp/tools/apphosting/index.ts @@ -1,4 +1,5 @@ import { ServerTool } from "../../tool"; +import { fetch_logs } from "./fetch_logs"; import { list_backends } from "./list_backends"; -export const appHostingTools: ServerTool[] = [list_backends]; +export const appHostingTools: ServerTool[] = [fetch_logs, list_backends]; From 049df716e115939f901741e49d664a56ff64e197 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 22 May 2025 13:52:58 -0700 Subject: [PATCH 10/13] lint issues --- src/mcp/tools/apphosting/fetch_logs.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/mcp/tools/apphosting/fetch_logs.ts b/src/mcp/tools/apphosting/fetch_logs.ts index 3cc1db2cbf1..99554b84a60 100644 --- a/src/mcp/tools/apphosting/fetch_logs.ts +++ b/src/mcp/tools/apphosting/fetch_logs.ts @@ -1,14 +1,7 @@ import { z } from "zod"; import { tool } from "../../tool.js"; import { toContent } from "../../util.js"; -import { - Backend, - getBackend, - getTraffic, - listBuilds, - parseBackendName, - Traffic, -} from "../../../gcp/apphosting.js"; +import { Backend, getBackend, getTraffic, listBuilds, Traffic } from "../../../gcp/apphosting.js"; import { last } from "../../../utils.js"; import { FirebaseError } from "../../../error.js"; import { fetchServiceLogs } from "../../../gcp/run.js"; @@ -49,13 +42,14 @@ export const fetch_logs = tool( return toContent(`backendId must be specified.`); } const backend = await getBackend(projectId, location, backendId); - // if (location === "-") location = parseBackendName(backend.name).location; const traffic = await getTraffic(projectId, location, backendId); const data: Backend & { traffic: Traffic } = { ...backend, traffic }; if (buildLogs) { const builds = await listBuilds(projectId, location, backendId); - // builds.builds.sort((a, b) => a.c) + builds.builds.sort( + (a, b) => new Date(a.createTime).getTime() - new Date(b.createTime).getTime(), + ); const build = last(builds.builds); const r = new RegExp(`region=${location}/([0-9a-f-]+)?`); const match = r.exec(build.buildLogsUri ?? ""); From c02087789fbaea505d0a4f2ee402846c4c80fc41 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 22 May 2025 13:57:34 -0700 Subject: [PATCH 11/13] some funny business got into my code. undoing it --- src/gcp/run.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/gcp/run.ts b/src/gcp/run.ts index 571f158b55c..3c075f3b220 100644 --- a/src/gcp/run.ts +++ b/src/gcp/run.ts @@ -353,24 +353,17 @@ export async function setInvokerUpdate( * @return A promise that resolves with the log entries. */ export async function fetchServiceLogs(projectId: string, serviceId: string): Promise { - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - const timestampFilter = `timestamp >= "${thirtyDaysAgo.toISOString()}"`; - const filter = `resource.type="cloud_run_revision" AND resource.labels.service_name="${serviceId}" AND ${timestampFilter}`; + const filter = `resource.type="cloud_run_revision" AND resource.labels.service_name="${serviceId}"`; const pageSize = 100; const order = "desc"; try { const entries = await listEntries(projectId, filter, pageSize, order); return entries || []; - } catch (err: unknown) { - let status: number | undefined; - if (typeof err === 'object' && err !== null && 'context' in err && typeof err.context === 'object' && err.context !== null && 'response' in err.context && typeof err.context.response === 'object' && err.context.response !== null && 'statusCode' in err.context.response && typeof err.context.response.statusCode === 'number') { - status = err.context.response.statusCode; - } + } catch (err: any) { throw new FirebaseError(`Failed to fetch logs for Cloud Run service ${serviceId}`, { - original: err instanceof Error ? err : undefined, - status: status, + original: err, + status: (err as any)?.context?.response?.statusCode, }); } } From 4dad065015fa1de2d25551bcb2f28ec5c2c438e2 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 22 May 2025 14:00:37 -0700 Subject: [PATCH 12/13] add changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a74a8c500f..e9c55c46b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Adds MCP tools for App Hosting (#8605) - Fixed crash when starting the App Hosting emulator in certain applications (#8624) - Fixed issue where, with `webframeworks` enabled, `firebase init hosting` re-prompts users for source. (#8587) -- Update typescript version in functions template to avoid build issue with @google-cloud/storage depedency (#8194) +- Update typescript version in functions template to avoid build issue with @google-cloud/storage depedency (#8194) \ No newline at end of file From 67e3d1427b4e603f153c0855465ff0d0bf49a8f8 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 22 May 2025 14:05:08 -0700 Subject: [PATCH 13/13] formatting. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c55c46b38..c0d14388d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ - Adds MCP tools for App Hosting (#8605) - Fixed crash when starting the App Hosting emulator in certain applications (#8624) - Fixed issue where, with `webframeworks` enabled, `firebase init hosting` re-prompts users for source. (#8587) -- Update typescript version in functions template to avoid build issue with @google-cloud/storage depedency (#8194) \ No newline at end of file +- Update typescript version in functions template to avoid build issue with @google-cloud/storage depedency (#8194)