#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { initializeApp, applicationDefault, App } from "firebase-admin/app";
import { getFirestore, Firestore } from "firebase-admin/firestore";
import { Logging } from "@google-cloud/logging";
import { readFileSync, existsSync } from "fs";
import { join } from "path";
// Detect project from .firebaserc or env
function detectProjectId(): string {
// Env var takes precedence
if (process.env.FIREBASE_PROJECT_ID) {
return process.env.FIREBASE_PROJECT_ID;
}
// Try .firebaserc in current directory
const firebaseRcPath = join(process.cwd(), ".firebaserc");
if (existsSync(firebaseRcPath)) {
try {
const rc = JSON.parse(readFileSync(firebaseRcPath, "utf-8"));
// Get active alias or default
const activeAlias = rc.activeAlias || "default";
const projectId = rc.projects?.[activeAlias];
if (projectId) {
console.error(`Detected project from .firebaserc: ${projectId}`);
return projectId;
}
} catch {
// Ignore parse errors
}
}
throw new Error(
"Could not detect Firebase project. Set FIREBASE_PROJECT_ID env var or run from a directory with .firebaserc"
);
}
let app: App;
let db: Firestore;
let logging: Logging;
let projectId: string;
function initFirebase() {
projectId = detectProjectId();
app = initializeApp({
credential: applicationDefault(),
projectId,
});
db = getFirestore(app);
logging = new Logging({ projectId });
console.error(`Firebase initialized for project: ${projectId}`);
}
function docToObject(doc: FirebaseFirestore.DocumentSnapshot) {
if (!doc.exists) return null;
return {
id: doc.id,
path: doc.ref.path,
data: doc.data(),
};
}
const tools = [
{
name: "list_collections",
description: "List top-level collections in Firestore",
inputSchema: { type: "object" as const, properties: {} },
},
{
name: "list_subcollections",
description: "List subcollections of a document",
inputSchema: {
type: "object" as const,
properties: {
documentPath: { type: "string", description: "Full document path (e.g., 'users/user123')" },
},
required: ["documentPath"],
},
},
{
name: "list_documents",
description: "List documents in a collection with optional limit",
inputSchema: {
type: "object" as const,
properties: {
collectionPath: { type: "string", description: "Collection path (e.g., 'users' or 'users/user123/orders')" },
limit: { type: "number", description: "Max documents to return (default: 20)" },
},
required: ["collectionPath"],
},
},
{
name: "get_document",
description: "Get a single document by path",
inputSchema: {
type: "object" as const,
properties: {
documentPath: { type: "string", description: "Full document path (e.g., 'users/user123')" },
},
required: ["documentPath"],
},
},
{
name: "query_collection",
description: "Query a collection with filters",
inputSchema: {
type: "object" as const,
properties: {
collectionPath: { type: "string", description: "Collection path" },
filters: {
type: "array",
description: "Array of filter objects: {field, operator, value}",
items: {
type: "object",
properties: {
field: { type: "string" },
operator: { type: "string", enum: ["==", "!=", "<", "<=", ">", ">=", "array-contains", "in", "array-contains-any"] },
value: {},
},
required: ["field", "operator", "value"],
},
},
orderBy: { type: "string", description: "Field to order by" },
orderDirection: { type: "string", enum: ["asc", "desc"] },
limit: { type: "number", description: "Max documents to return (default: 20)" },
},
required: ["collectionPath"],
},
},
{
name: "get_function_logs",
description: "Get Firebase Cloud Function logs with optional grep-style filtering",
inputSchema: {
type: "object" as const,
properties: {
pattern: { type: "string", description: "Regex pattern to filter log messages" },
severity: { type: "string", enum: ["DEBUG", "INFO", "NOTICE", "WARNING", "ERROR", "CRITICAL"], description: "Minimum severity level" },
functionName: { type: "string", description: "Filter by function name" },
limit: { type: "number", description: "Max log entries to return (default: 50)" },
hoursAgo: { type: "number", description: "Fetch logs from last N hours (default: 1)" },
},
},
},
];
async function handleListCollections() {
const collections = await db.listCollections();
return collections.map((col) => col.id);
}
async function handleListSubcollections(documentPath: string) {
const docRef = db.doc(documentPath);
const collections = await docRef.listCollections();
return collections.map((col) => col.id);
}
async function handleListDocuments(collectionPath: string, limit = 20) {
const snapshot = await db.collection(collectionPath).limit(limit).get();
return snapshot.docs.map(docToObject);
}
async function handleGetDocument(documentPath: string) {
const doc = await db.doc(documentPath).get();
return docToObject(doc);
}
interface QueryFilter {
field: string;
operator: FirebaseFirestore.WhereFilterOp;
value: any;
}
async function handleQueryCollection(
collectionPath: string,
filters?: QueryFilter[],
orderBy?: string,
orderDirection?: "asc" | "desc",
limit = 20
) {
let query: FirebaseFirestore.Query = db.collection(collectionPath);
if (filters) {
for (const filter of filters) {
query = query.where(filter.field, filter.operator, filter.value);
}
}
if (orderBy) {
query = query.orderBy(orderBy, orderDirection || "asc");
}
query = query.limit(limit);
const snapshot = await query.get();
return snapshot.docs.map(docToObject);
}
async function handleGetFunctionLogs(
pattern?: string,
severity?: string,
functionName?: string,
limit = 50,
hoursAgo = 1
) {
// Build Cloud Logging filter
const filterParts = [
`resource.type="cloud_function"`,
`resource.labels.project_id="${projectId}"`,
];
if (functionName) {
filterParts.push(`resource.labels.function_name="${functionName}"`);
}
if (severity) {
filterParts.push(`severity>="${severity}"`);
}
const startTime = new Date(Date.now() - hoursAgo * 60 * 60 * 1000).toISOString();
filterParts.push(`timestamp>="${startTime}"`);
const filter = filterParts.join(" AND ");
const [entries] = await logging.getEntries({
filter,
pageSize: limit * 2, // Fetch extra in case we need to filter by pattern
orderBy: "timestamp desc",
});
let results = entries.map((entry) => {
const data = entry.data as any;
const message = typeof data === "string"
? data
: data?.message || data?.textPayload || JSON.stringify(data);
return {
timestamp: entry.metadata.timestamp?.toString() || "",
severity: entry.metadata.severity?.toString() || "DEFAULT",
function: entry.metadata.resource?.labels?.function_name || "",
message,
};
});
// Apply grep pattern filter
if (pattern) {
const regex = new RegExp(pattern, "i");
results = results.filter((log) => regex.test(log.message));
}
return results.slice(0, limit);
}
async function main() {
initFirebase();
const server = new Server(
{ name: "firebase-live-mcp-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result: any;
switch (name) {
case "list_collections":
result = await handleListCollections();
break;
case "list_subcollections":
result = await handleListSubcollections(args?.documentPath as string);
break;
case "list_documents":
result = await handleListDocuments(args?.collectionPath as string, args?.limit as number);
break;
case "get_document":
result = await handleGetDocument(args?.documentPath as string);
break;
case "query_collection":
result = await handleQueryCollection(
args?.collectionPath as string,
args?.filters as QueryFilter[],
args?.orderBy as string,
args?.orderDirection as "asc" | "desc",
args?.limit as number
);
break;
case "get_function_logs":
result = await handleGetFunctionLogs(
args?.pattern as string,
args?.severity as string,
args?.functionName as string,
args?.limit as number,
args?.hoursAgo as number
);
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Firebase Live MCP Server running on stdio");
}
main().catch(console.error);