Skip to content

Commit 431e674

Browse files
google-labs-jules[bot]joehannohe427
authored
Combine auth_get_user and auth_list_users MCP tools (#9165)
* feat(mcp): Combine auth_get_user and auth_list_users tools Combines the `auth_get_user` and `auth_list_users` MCP tools into a single `auth_get_users` tool. This new tool has two optional arguments, `uids` and `emails`. - When no arguments are provided, it behaves like `auth_list_users`. - When either `emails` or `uids` is provided, it looks up users by the provided identifiers. Removes unused variables and ensures that `passwordHash` and `salt` fields are consistently removed from all user objects returned by the tool. * Formats * Parallelize calls * Small fixes * npm format --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Joe Hanley <[email protected]> Co-authored-by: Alexander Nohe <[email protected]>
1 parent 8f5ae01 commit 431e674

File tree

7 files changed

+149
-213
lines changed

7 files changed

+149
-213
lines changed

src/mcp/tools/auth/get_user.spec.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.

src/mcp/tools/auth/get_user.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import { get_users } from "./get_users";
4+
import * as auth from "../../../gcp/auth";
5+
import { toContent } from "../../util";
6+
import { McpContext } from "../../types";
7+
8+
describe("get_users tool", () => {
9+
const projectId = "test-project";
10+
const users = [
11+
{ uid: "uid1", email: "[email protected]", passwordHash: "hash", salt: "salt" },
12+
{ uid: "uid2", email: "[email protected]", passwordHash: "hash", salt: "salt" },
13+
];
14+
const prunedUsers = [
15+
{ uid: "uid1", email: "[email protected]" },
16+
{ uid: "uid2", email: "[email protected]" },
17+
];
18+
19+
let findUserStub: sinon.SinonStub;
20+
let listUsersStub: sinon.SinonStub;
21+
22+
beforeEach(() => {
23+
findUserStub = sinon.stub(auth, "findUser");
24+
listUsersStub = sinon.stub(auth, "listUsers");
25+
});
26+
27+
afterEach(() => {
28+
sinon.restore();
29+
});
30+
31+
context("when no identifiers are provided", () => {
32+
it("should list all users", async () => {
33+
listUsersStub.resolves(users);
34+
const result = await get_users.fn({}, { projectId } as McpContext);
35+
expect(listUsersStub).to.be.calledWith(projectId, 100);
36+
expect(result).to.deep.equal(toContent(prunedUsers));
37+
});
38+
});
39+
40+
context("when uids are provided", () => {
41+
it("should get users by uid", async () => {
42+
findUserStub.onFirstCall().resolves(users[0]);
43+
findUserStub.onSecondCall().resolves(users[1]);
44+
const result = await get_users.fn({ uids: ["uid1", "uid2"] }, { projectId } as McpContext);
45+
expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, "uid1");
46+
expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, "uid2");
47+
expect(result).to.deep.equal(toContent(prunedUsers));
48+
});
49+
50+
it("should handle not found users", async () => {
51+
findUserStub.onFirstCall().resolves(users[0]);
52+
findUserStub.onSecondCall().rejects(new Error("User not found"));
53+
const result = await get_users.fn({ uids: ["uid1", "uid2"] }, { projectId } as McpContext);
54+
expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, "uid1");
55+
expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, "uid2");
56+
expect(result).to.deep.equal(toContent([prunedUsers[0]]));
57+
});
58+
});
59+
60+
context("when emails are provided", () => {
61+
it("should get users by email", async () => {
62+
findUserStub.onFirstCall().resolves(users[0]);
63+
findUserStub.onSecondCall().resolves(users[1]);
64+
const result = await get_users.fn({ emails: ["[email protected]", "[email protected]"] }, {
65+
projectId,
66+
} as McpContext);
67+
expect(findUserStub).to.be.calledWith(projectId, "[email protected]", undefined, undefined);
68+
expect(findUserStub).to.be.calledWith(projectId, "[email protected]", undefined, undefined);
69+
expect(result).to.deep.equal(toContent(prunedUsers));
70+
});
71+
});
72+
73+
context("when phone_numbers are provided", () => {
74+
it("should get users by phone number", async () => {
75+
findUserStub.onFirstCall().resolves(users[0]);
76+
findUserStub.onSecondCall().resolves(users[1]);
77+
const result = await get_users.fn({ phone_numbers: ["+11111111111", "+22222222222"] }, {
78+
projectId,
79+
} as McpContext);
80+
expect(findUserStub).to.be.calledWith(projectId, undefined, "+11111111111", undefined);
81+
expect(findUserStub).to.be.calledWith(projectId, undefined, "+22222222222", undefined);
82+
expect(result).to.deep.equal(toContent(prunedUsers));
83+
});
84+
});
85+
});

src/mcp/tools/auth/get_users.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { z } from "zod";
2+
import { tool } from "../../tool";
3+
import { toContent } from "../../util";
4+
import { findUser, listUsers, UserInfo } from "../../../gcp/auth";
5+
6+
export const get_users = tool(
7+
{
8+
name: "auth_get_users",
9+
description: "Retrieves users based on a list of UIDs or a list of emails.",
10+
inputSchema: z.object({
11+
uids: z.array(z.string()).optional().describe("A list of user UIDs to retrieve."),
12+
emails: z.array(z.string()).optional().describe("A list of user emails to retrieve."),
13+
phone_numbers: z
14+
.array(z.string())
15+
.optional()
16+
.describe("A list of user phone numbers to retrieve."),
17+
limit: z
18+
.number()
19+
.optional()
20+
.default(100)
21+
.describe("The numbers of users to return. 500 is the upper limit. Defaults to 100."),
22+
}),
23+
annotations: {
24+
title: "Get Firebase Auth Users",
25+
readOnlyHint: true,
26+
},
27+
_meta: {
28+
requiresAuth: true,
29+
requiresProject: true,
30+
},
31+
},
32+
async ({ uids, emails, phone_numbers, limit }, { projectId }) => {
33+
const prune = (user: UserInfo) => {
34+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
35+
const { passwordHash, salt, ...prunedUser } = user;
36+
return prunedUser;
37+
};
38+
let users: UserInfo[] = [];
39+
if (uids?.length) {
40+
const promises = uids.map((uid) =>
41+
findUser(projectId, undefined, undefined, uid).catch(() => null),
42+
);
43+
users.push(...(await Promise.all(promises)).filter((u): u is UserInfo => !!u));
44+
}
45+
if (emails?.length) {
46+
const promises = emails.map((email) =>
47+
findUser(projectId, email, undefined, undefined).catch(() => null),
48+
);
49+
users.push(...(await Promise.all(promises)).filter((u): u is UserInfo => !!u));
50+
}
51+
if (phone_numbers?.length) {
52+
const promises = phone_numbers.map((phone) =>
53+
findUser(projectId, undefined, phone, undefined).catch(() => null),
54+
);
55+
users.push(...(await Promise.all(promises)).filter((u): u is UserInfo => !!u));
56+
}
57+
if (!uids?.length && !emails?.length && !phone_numbers?.length) {
58+
users = await listUsers(projectId, limit || 100);
59+
}
60+
return toContent(users.map(prune));
61+
},
62+
);

src/mcp/tools/auth/index.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import { ServerTool } from "../../tool";
2-
import { get_user } from "./get_user";
2+
import { get_users } from "./get_users";
33
import { disable_user } from "./disable_user";
44
import { set_claim } from "./set_claims";
55
import { set_sms_region_policy } from "./set_sms_region_policy";
6-
import { list_users } from "./list_users";
76

8-
export const authTools: ServerTool[] = [
9-
get_user,
10-
disable_user,
11-
list_users,
12-
set_claim,
13-
set_sms_region_policy,
14-
];
7+
export const authTools: ServerTool[] = [get_users, disable_user, set_claim, set_sms_region_policy];

src/mcp/tools/auth/list_users.spec.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/mcp/tools/auth/list_users.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)