@theihtisham/mcp-server-firebase 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +79 -0
- package/dist/services/firebase.d.ts +14 -0
- package/dist/services/firebase.js +163 -0
- package/dist/tools/auth.d.ts +3 -0
- package/dist/tools/auth.js +346 -0
- package/dist/tools/firestore.d.ts +3 -0
- package/dist/tools/firestore.js +802 -0
- package/dist/tools/functions.d.ts +3 -0
- package/dist/tools/functions.js +168 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/messaging.d.ts +3 -0
- package/dist/tools/messaging.js +296 -0
- package/dist/tools/realtime-db.d.ts +4 -0
- package/dist/tools/realtime-db.js +271 -0
- package/dist/tools/storage.d.ts +3 -0
- package/dist/tools/storage.js +279 -0
- package/dist/tools/types.d.ts +11 -0
- package/dist/tools/types.js +3 -0
- package/dist/utils/cache.d.ts +16 -0
- package/dist/utils/cache.js +75 -0
- package/dist/utils/errors.d.ts +15 -0
- package/dist/utils/errors.js +94 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +37 -0
- package/dist/utils/pagination.d.ts +28 -0
- package/dist/utils/pagination.js +75 -0
- package/dist/utils/validation.d.ts +22 -0
- package/dist/utils/validation.js +172 -0
- package/package.json +53 -0
- package/src/index.ts +94 -0
- package/src/services/firebase.ts +140 -0
- package/src/tools/auth.ts +375 -0
- package/src/tools/firestore.ts +931 -0
- package/src/tools/functions.ts +189 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/messaging.ts +324 -0
- package/src/tools/realtime-db.ts +307 -0
- package/src/tools/storage.ts +314 -0
- package/src/tools/types.ts +10 -0
- package/src/utils/cache.ts +82 -0
- package/src/utils/errors.ts +110 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/pagination.ts +105 -0
- package/src/utils/validation.ts +212 -0
- package/tests/cache.test.ts +139 -0
- package/tests/errors.test.ts +132 -0
- package/tests/firebase-service.test.ts +46 -0
- package/tests/pagination.test.ts +26 -0
- package/tests/tools.test.ts +226 -0
- package/tests/validation.test.ts +216 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as admin from 'firebase-admin';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
let app: admin.app.App | null = null;
|
|
5
|
+
|
|
6
|
+
export interface FirebaseConfig {
|
|
7
|
+
projectId?: string;
|
|
8
|
+
storageBucket?: string;
|
|
9
|
+
databaseURL?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getServiceAccount(): admin.ServiceAccount | undefined {
|
|
13
|
+
// 1. Try FIREBASE_SERVICE_ACCOUNT_KEY (JSON string)
|
|
14
|
+
const keyEnv = process.env['FIREBASE_SERVICE_ACCOUNT_KEY'];
|
|
15
|
+
if (keyEnv) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(keyEnv) as admin.ServiceAccount;
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'FIREBASE_SERVICE_ACCOUNT_KEY is set but contains invalid JSON. ' +
|
|
21
|
+
'Provide the full service account JSON as a single-line string.'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. Try FIREBASE_SERVICE_ACCOUNT_PATH (file path)
|
|
27
|
+
const pathEnv = process.env['FIREBASE_SERVICE_ACCOUNT_PATH'];
|
|
28
|
+
if (pathEnv) {
|
|
29
|
+
try {
|
|
30
|
+
const content = fs.readFileSync(pathEnv, 'utf-8');
|
|
31
|
+
return JSON.parse(content) as admin.ServiceAccount;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Failed to read service account file at "${pathEnv}": ${(err as Error).message}. ` +
|
|
35
|
+
'Ensure the path is correct and the file is valid JSON.'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Try individual env vars
|
|
41
|
+
const projectId = process.env['FIREBASE_PROJECT_ID'];
|
|
42
|
+
const clientEmail = process.env['FIREBASE_CLIENT_EMAIL'];
|
|
43
|
+
const privateKey = process.env['FIREBASE_PRIVATE_KEY']?.replace(/\\n/g, '\n');
|
|
44
|
+
|
|
45
|
+
if (projectId && clientEmail && privateKey) {
|
|
46
|
+
return { projectId, clientEmail, privateKey };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 4. Return undefined — rely on Application Default Credentials (ADC)
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function initializeFirebase(config?: FirebaseConfig): admin.app.App {
|
|
54
|
+
if (app) {
|
|
55
|
+
return app;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const serviceAccount = getServiceAccount();
|
|
59
|
+
|
|
60
|
+
const initOptions: admin.AppOptions = {};
|
|
61
|
+
|
|
62
|
+
if (serviceAccount) {
|
|
63
|
+
initOptions.credential = admin.credential.cert(serviceAccount);
|
|
64
|
+
}
|
|
65
|
+
// If no service account, ADC will be used (works on GCP, with gcloud CLI, etc.)
|
|
66
|
+
|
|
67
|
+
if (config?.projectId) {
|
|
68
|
+
initOptions.projectId = config.projectId;
|
|
69
|
+
} else if (serviceAccount?.projectId) {
|
|
70
|
+
initOptions.projectId = serviceAccount.projectId;
|
|
71
|
+
} else {
|
|
72
|
+
const envProject = process.env['FIREBASE_PROJECT_ID'];
|
|
73
|
+
if (envProject) {
|
|
74
|
+
initOptions.projectId = envProject;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (config?.storageBucket) {
|
|
79
|
+
initOptions.storageBucket = config.storageBucket;
|
|
80
|
+
} else {
|
|
81
|
+
const envBucket = process.env['FIREBASE_STORAGE_BUCKET'];
|
|
82
|
+
if (envBucket) {
|
|
83
|
+
initOptions.storageBucket = envBucket;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (config?.databaseURL) {
|
|
88
|
+
initOptions.databaseURL = config.databaseURL;
|
|
89
|
+
} else {
|
|
90
|
+
const envDbUrl = process.env['FIREBASE_DATABASE_URL'];
|
|
91
|
+
if (envDbUrl) {
|
|
92
|
+
initOptions.databaseURL = envDbUrl;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
app = admin.initializeApp(initOptions, 'mcp-firebase-server');
|
|
98
|
+
} catch (err: unknown) {
|
|
99
|
+
// If app already exists, get it
|
|
100
|
+
if (err instanceof Error && err.message.includes('already exists')) {
|
|
101
|
+
app = admin.app('mcp-firebase-server');
|
|
102
|
+
} else {
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return app;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getFirestore(): admin.firestore.Firestore {
|
|
111
|
+
const firebaseApp = initializeFirebase();
|
|
112
|
+
return firebaseApp.firestore();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getAuth(): admin.auth.Auth {
|
|
116
|
+
const firebaseApp = initializeFirebase();
|
|
117
|
+
return firebaseApp.auth();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getStorage(): admin.storage.Storage {
|
|
121
|
+
const firebaseApp = initializeFirebase();
|
|
122
|
+
return firebaseApp.storage();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getRealtimeDb(): admin.database.Database {
|
|
126
|
+
const firebaseApp = initializeFirebase();
|
|
127
|
+
return firebaseApp.database();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getMessaging(): admin.messaging.Messaging {
|
|
131
|
+
const firebaseApp = initializeFirebase();
|
|
132
|
+
return firebaseApp.messaging();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getApp(): admin.app.App {
|
|
136
|
+
if (!app) {
|
|
137
|
+
return initializeFirebase();
|
|
138
|
+
}
|
|
139
|
+
return app;
|
|
140
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { getAuth } from '../services/firebase.js';
|
|
2
|
+
import {
|
|
3
|
+
validateEmail,
|
|
4
|
+
validateUid,
|
|
5
|
+
validatePageSize,
|
|
6
|
+
handleFirebaseError,
|
|
7
|
+
formatSuccess,
|
|
8
|
+
formatListResult,
|
|
9
|
+
} from '../utils/index.js';
|
|
10
|
+
import type { ToolDefinition } from './types.js';
|
|
11
|
+
import * as admin from 'firebase-admin';
|
|
12
|
+
|
|
13
|
+
// ============================================================
|
|
14
|
+
// AUTH TOOLS
|
|
15
|
+
// ============================================================
|
|
16
|
+
|
|
17
|
+
export const authTools: ToolDefinition[] = [
|
|
18
|
+
// ── auth_create_user ──────────────────────────────────
|
|
19
|
+
{
|
|
20
|
+
name: 'auth_create_user',
|
|
21
|
+
description:
|
|
22
|
+
'Create a new Firebase Authentication user. At minimum provide email or phoneNumber.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object' as const,
|
|
25
|
+
properties: {
|
|
26
|
+
email: { type: 'string', description: 'User email address' },
|
|
27
|
+
emailVerified: { type: 'boolean', description: 'Whether email is verified (default: false)' },
|
|
28
|
+
phoneNumber: { type: 'string', description: 'Phone number in E.164 format (e.g., "+15555550100")' },
|
|
29
|
+
password: { type: 'string', description: 'Password (min 6 characters)' },
|
|
30
|
+
displayName: { type: 'string', description: 'Display name' },
|
|
31
|
+
photoURL: { type: 'string', description: 'Photo URL' },
|
|
32
|
+
disabled: { type: 'boolean', description: 'Whether account is disabled (default: false)' },
|
|
33
|
+
uid: { type: 'string', description: 'Custom UID (auto-generated if not provided)' },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
handler: async (args: Record<string, unknown>) => {
|
|
37
|
+
try {
|
|
38
|
+
const auth = getAuth();
|
|
39
|
+
const createOptions: admin.auth.CreateRequest = {};
|
|
40
|
+
|
|
41
|
+
if (args['email']) {
|
|
42
|
+
createOptions.email = validateEmail(args['email'] as string);
|
|
43
|
+
}
|
|
44
|
+
if (args['emailVerified'] !== undefined) {
|
|
45
|
+
createOptions.emailVerified = args['emailVerified'] as boolean;
|
|
46
|
+
}
|
|
47
|
+
if (args['phoneNumber']) {
|
|
48
|
+
createOptions.phoneNumber = (args['phoneNumber'] as string).trim();
|
|
49
|
+
}
|
|
50
|
+
if (args['password']) {
|
|
51
|
+
const password = args['password'] as string;
|
|
52
|
+
if (password.length < 6) {
|
|
53
|
+
throw new Error('Password must be at least 6 characters long.');
|
|
54
|
+
}
|
|
55
|
+
createOptions.password = password;
|
|
56
|
+
}
|
|
57
|
+
if (args['displayName']) {
|
|
58
|
+
createOptions.displayName = (args['displayName'] as string).trim();
|
|
59
|
+
}
|
|
60
|
+
if (args['photoURL']) {
|
|
61
|
+
createOptions.photoURL = (args['photoURL'] as string).trim();
|
|
62
|
+
}
|
|
63
|
+
if (args['disabled'] !== undefined) {
|
|
64
|
+
createOptions.disabled = args['disabled'] as boolean;
|
|
65
|
+
}
|
|
66
|
+
if (args['uid']) {
|
|
67
|
+
createOptions.uid = validateUid(args['uid'] as string);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const userRecord = await auth.createUser(createOptions);
|
|
71
|
+
|
|
72
|
+
return formatSuccess({
|
|
73
|
+
uid: userRecord.uid,
|
|
74
|
+
email: userRecord.email,
|
|
75
|
+
phoneNumber: userRecord.phoneNumber,
|
|
76
|
+
displayName: userRecord.displayName,
|
|
77
|
+
photoURL: userRecord.photoURL,
|
|
78
|
+
disabled: userRecord.disabled,
|
|
79
|
+
emailVerified: userRecord.emailVerified,
|
|
80
|
+
metadata: {
|
|
81
|
+
creationTime: userRecord.metadata.creationTime,
|
|
82
|
+
lastSignInTime: userRecord.metadata.lastSignInTime,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
handleFirebaseError(err, 'auth', 'create_user');
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// ── auth_get_user ─────────────────────────────────────
|
|
92
|
+
{
|
|
93
|
+
name: 'auth_get_user',
|
|
94
|
+
description: 'Get a Firebase user by UID, email, or phone number.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object' as const,
|
|
97
|
+
properties: {
|
|
98
|
+
uid: { type: 'string', description: 'User UID' },
|
|
99
|
+
email: { type: 'string', description: 'User email (alternative to uid)' },
|
|
100
|
+
phoneNumber: { type: 'string', description: 'User phone number (alternative to uid)' },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
handler: async (args: Record<string, unknown>) => {
|
|
104
|
+
try {
|
|
105
|
+
const auth = getAuth();
|
|
106
|
+
let userRecord: admin.auth.UserRecord;
|
|
107
|
+
|
|
108
|
+
if (args['uid']) {
|
|
109
|
+
userRecord = await auth.getUser(validateUid(args['uid'] as string));
|
|
110
|
+
} else if (args['email']) {
|
|
111
|
+
userRecord = await auth.getUserByEmail(validateEmail(args['email'] as string));
|
|
112
|
+
} else if (args['phoneNumber']) {
|
|
113
|
+
userRecord = await auth.getUserByPhoneNumber((args['phoneNumber'] as string).trim());
|
|
114
|
+
} else {
|
|
115
|
+
throw new Error('Provide at least one of: uid, email, or phoneNumber.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return formatSuccess({
|
|
119
|
+
uid: userRecord.uid,
|
|
120
|
+
email: userRecord.email,
|
|
121
|
+
phoneNumber: userRecord.phoneNumber,
|
|
122
|
+
displayName: userRecord.displayName,
|
|
123
|
+
photoURL: userRecord.photoURL,
|
|
124
|
+
disabled: userRecord.disabled,
|
|
125
|
+
emailVerified: userRecord.emailVerified,
|
|
126
|
+
customClaims: userRecord.customClaims,
|
|
127
|
+
tenantId: userRecord.tenantId,
|
|
128
|
+
providerData: userRecord.providerData.map((p) => ({
|
|
129
|
+
providerId: p.providerId,
|
|
130
|
+
uid: p.uid,
|
|
131
|
+
email: p.email,
|
|
132
|
+
displayName: p.displayName,
|
|
133
|
+
phoneNumber: p.phoneNumber,
|
|
134
|
+
photoURL: p.photoURL,
|
|
135
|
+
})),
|
|
136
|
+
metadata: {
|
|
137
|
+
creationTime: userRecord.metadata.creationTime,
|
|
138
|
+
lastSignInTime: userRecord.metadata.lastSignInTime,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
handleFirebaseError(err, 'auth', 'get_user');
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// ── auth_list_users ───────────────────────────────────
|
|
148
|
+
{
|
|
149
|
+
name: 'auth_list_users',
|
|
150
|
+
description:
|
|
151
|
+
'List Firebase Auth users with pagination. Returns user summaries.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object' as const,
|
|
154
|
+
properties: {
|
|
155
|
+
pageSize: { type: 'number', description: 'Results per page (1-1000, default 100)' },
|
|
156
|
+
pageToken: { type: 'string', description: 'Next page token from previous result' },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
handler: async (args: Record<string, unknown>) => {
|
|
160
|
+
try {
|
|
161
|
+
const auth = getAuth();
|
|
162
|
+
const pageSize = validatePageSize((args['pageSize'] as number) || 100);
|
|
163
|
+
const pageToken = args['pageToken'] as string | undefined;
|
|
164
|
+
|
|
165
|
+
const result = await auth.listUsers(pageSize, pageToken);
|
|
166
|
+
|
|
167
|
+
const users = result.users.map((u) => ({
|
|
168
|
+
uid: u.uid,
|
|
169
|
+
email: u.email,
|
|
170
|
+
displayName: u.displayName,
|
|
171
|
+
disabled: u.disabled,
|
|
172
|
+
emailVerified: u.emailVerified,
|
|
173
|
+
creationTime: u.metadata.creationTime,
|
|
174
|
+
lastSignInTime: u.metadata.lastSignInTime,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
return formatListResult(users, result.pageToken);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
handleFirebaseError(err, 'auth', 'list_users');
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// ── auth_update_user ──────────────────────────────────
|
|
185
|
+
{
|
|
186
|
+
name: 'auth_update_user',
|
|
187
|
+
description:
|
|
188
|
+
'Update an existing Firebase user. Only provided fields are updated.',
|
|
189
|
+
inputSchema: {
|
|
190
|
+
type: 'object' as const,
|
|
191
|
+
properties: {
|
|
192
|
+
uid: { type: 'string', description: 'User UID to update' },
|
|
193
|
+
email: { type: 'string', description: 'New email address' },
|
|
194
|
+
emailVerified: { type: 'boolean', description: 'Email verification status' },
|
|
195
|
+
phoneNumber: { type: 'string', description: 'New phone number (null to remove)' },
|
|
196
|
+
password: { type: 'string', description: 'New password (min 6 characters)' },
|
|
197
|
+
displayName: { type: 'string', description: 'New display name' },
|
|
198
|
+
photoURL: { type: 'string', description: 'New photo URL' },
|
|
199
|
+
disabled: { type: 'boolean', description: 'Whether to disable the account' },
|
|
200
|
+
},
|
|
201
|
+
required: ['uid'],
|
|
202
|
+
},
|
|
203
|
+
handler: async (args: Record<string, unknown>) => {
|
|
204
|
+
try {
|
|
205
|
+
const auth = getAuth();
|
|
206
|
+
const uid = validateUid(args['uid'] as string);
|
|
207
|
+
const updateOptions: admin.auth.UpdateRequest = {};
|
|
208
|
+
|
|
209
|
+
if (args['email'] !== undefined) {
|
|
210
|
+
updateOptions.email = validateEmail(args['email'] as string);
|
|
211
|
+
}
|
|
212
|
+
if (args['emailVerified'] !== undefined) {
|
|
213
|
+
updateOptions.emailVerified = args['emailVerified'] as boolean;
|
|
214
|
+
}
|
|
215
|
+
if (args['phoneNumber'] !== undefined) {
|
|
216
|
+
updateOptions.phoneNumber = args['phoneNumber'] as string | null;
|
|
217
|
+
}
|
|
218
|
+
if (args['password'] !== undefined) {
|
|
219
|
+
const password = args['password'] as string;
|
|
220
|
+
if (password.length < 6) {
|
|
221
|
+
throw new Error('Password must be at least 6 characters long.');
|
|
222
|
+
}
|
|
223
|
+
updateOptions.password = password;
|
|
224
|
+
}
|
|
225
|
+
if (args['displayName'] !== undefined) {
|
|
226
|
+
updateOptions.displayName = args['displayName'] as string;
|
|
227
|
+
}
|
|
228
|
+
if (args['photoURL'] !== undefined) {
|
|
229
|
+
updateOptions.photoURL = args['photoURL'] as string;
|
|
230
|
+
}
|
|
231
|
+
if (args['disabled'] !== undefined) {
|
|
232
|
+
updateOptions.disabled = args['disabled'] as boolean;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const userRecord = await auth.updateUser(uid, updateOptions);
|
|
236
|
+
|
|
237
|
+
return formatSuccess({
|
|
238
|
+
uid: userRecord.uid,
|
|
239
|
+
email: userRecord.email,
|
|
240
|
+
displayName: userRecord.displayName,
|
|
241
|
+
photoURL: userRecord.photoURL,
|
|
242
|
+
disabled: userRecord.disabled,
|
|
243
|
+
emailVerified: userRecord.emailVerified,
|
|
244
|
+
message: `User "${uid}" updated successfully.`,
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
handleFirebaseError(err, 'auth', 'update_user');
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// ── auth_delete_user ──────────────────────────────────
|
|
253
|
+
{
|
|
254
|
+
name: 'auth_delete_user',
|
|
255
|
+
description: 'Delete a Firebase user by UID.',
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: 'object' as const,
|
|
258
|
+
properties: {
|
|
259
|
+
uid: { type: 'string', description: 'User UID to delete' },
|
|
260
|
+
},
|
|
261
|
+
required: ['uid'],
|
|
262
|
+
},
|
|
263
|
+
handler: async (args: Record<string, unknown>) => {
|
|
264
|
+
try {
|
|
265
|
+
const auth = getAuth();
|
|
266
|
+
const uid = validateUid(args['uid'] as string);
|
|
267
|
+
await auth.deleteUser(uid);
|
|
268
|
+
|
|
269
|
+
return formatSuccess({
|
|
270
|
+
uid,
|
|
271
|
+
message: `User "${uid}" deleted successfully.`,
|
|
272
|
+
});
|
|
273
|
+
} catch (err) {
|
|
274
|
+
handleFirebaseError(err, 'auth', 'delete_user');
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// ── auth_verify_token ─────────────────────────────────
|
|
280
|
+
{
|
|
281
|
+
name: 'auth_verify_token',
|
|
282
|
+
description:
|
|
283
|
+
'Verify a Firebase ID token and return the decoded token with user info.',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object' as const,
|
|
286
|
+
properties: {
|
|
287
|
+
idToken: { type: 'string', description: 'Firebase ID token to verify' },
|
|
288
|
+
checkRevoked: { type: 'boolean', description: 'Check if token was revoked (default: true)' },
|
|
289
|
+
},
|
|
290
|
+
required: ['idToken'],
|
|
291
|
+
},
|
|
292
|
+
handler: async (args: Record<string, unknown>) => {
|
|
293
|
+
try {
|
|
294
|
+
const auth = getAuth();
|
|
295
|
+
const idToken = (args['idToken'] as string).trim();
|
|
296
|
+
const checkRevoked = (args['checkRevoked'] as boolean) ?? true;
|
|
297
|
+
|
|
298
|
+
if (idToken.length === 0) {
|
|
299
|
+
throw new Error('ID token cannot be empty.');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const decodedToken = await auth.verifyIdToken(idToken, checkRevoked);
|
|
303
|
+
|
|
304
|
+
return formatSuccess({
|
|
305
|
+
uid: decodedToken.uid,
|
|
306
|
+
email: decodedToken.email,
|
|
307
|
+
emailVerified: decodedToken.email_verified,
|
|
308
|
+
name: decodedToken.name,
|
|
309
|
+
picture: decodedToken.picture,
|
|
310
|
+
iss: decodedToken.iss,
|
|
311
|
+
aud: decodedToken.aud,
|
|
312
|
+
authTime: new Date(decodedToken.auth_time * 1000).toISOString(),
|
|
313
|
+
issuedAt: new Date(decodedToken.iat * 1000).toISOString(),
|
|
314
|
+
expiresAt: new Date(decodedToken.exp * 1000).toISOString(),
|
|
315
|
+
signInProvider: decodedToken.firebase?.sign_in_provider,
|
|
316
|
+
valid: true,
|
|
317
|
+
});
|
|
318
|
+
} catch (err) {
|
|
319
|
+
const error = err as Error;
|
|
320
|
+
return formatSuccess({
|
|
321
|
+
valid: false,
|
|
322
|
+
error: error.message,
|
|
323
|
+
suggestion: 'Ensure the token is a valid, non-expired Firebase ID token.',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
// ── auth_set_custom_claims ────────────────────────────
|
|
330
|
+
{
|
|
331
|
+
name: 'auth_set_custom_claims',
|
|
332
|
+
description:
|
|
333
|
+
'Set custom claims on a user for role-based access control.',
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: 'object' as const,
|
|
336
|
+
properties: {
|
|
337
|
+
uid: { type: 'string', description: 'User UID' },
|
|
338
|
+
customClaims: { type: 'object', description: 'Custom claims object (max 1000 bytes serialized)' },
|
|
339
|
+
},
|
|
340
|
+
required: ['uid', 'customClaims'],
|
|
341
|
+
},
|
|
342
|
+
handler: async (args: Record<string, unknown>) => {
|
|
343
|
+
try {
|
|
344
|
+
const auth = getAuth();
|
|
345
|
+
const uid = validateUid(args['uid'] as string);
|
|
346
|
+
const customClaims = args['customClaims'] as Record<string, unknown>;
|
|
347
|
+
|
|
348
|
+
const claimsSize = Buffer.byteLength(JSON.stringify(customClaims));
|
|
349
|
+
if (claimsSize > 1000) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Custom claims exceed 1000 bytes (got ${claimsSize}). ` +
|
|
352
|
+
'Reduce the number or size of claims.'
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const reservedKeys = ['iss', 'aud', 'auth_time', 'sub', 'iat', 'exp', 'email', 'email_verified', 'firebase'];
|
|
357
|
+
for (const key of Object.keys(customClaims)) {
|
|
358
|
+
if (reservedKeys.includes(key)) {
|
|
359
|
+
throw new Error(`Cannot set reserved claim key: "${key}".`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await auth.setCustomUserClaims(uid, customClaims);
|
|
364
|
+
|
|
365
|
+
return formatSuccess({
|
|
366
|
+
uid,
|
|
367
|
+
customClaims,
|
|
368
|
+
message: `Custom claims set for user "${uid}". Claims take effect on next token refresh.`,
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
handleFirebaseError(err, 'auth', 'set_custom_claims');
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
];
|