Skip to content

Commit c45b093

Browse files
google-labs-jules[bot]joehangemini-code-assist[bot]dsjadaun-google
authored
Feat: Combine remoteconfig publish and rollback tools (#9164)
* feat: Combine remoteconfig publish and rollback tools Combines the remoteconfig_publish_template and remoteconfig_rollback_template MCP tools into a single remoteconfig_update_template MCP tool. This new tool can either publish a new template by providing a template object, or rollback an existing template by providing a version number. * feat: Combine remoteconfig publish and rollback tools Combines the remoteconfig_publish_template and remoteconfig_rollback_template MCP tools into a single remoteconfig_update_template MCP tool. This new tool can either publish a new template by providing a template object, or rollback an existing template by providing a version number. * Update src/mcp/tools/remoteconfig/update_template.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Format * fix: Add validation for positive version number Adds validation to ensure that the version number provided for a rollback is a positive integer. * Cleaning up typing problem * Format * Fixing tests * Pr suggestion --------- 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: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Dushyant Singh Jadaun <[email protected]>
1 parent 103c6f7 commit c45b093

File tree

6 files changed

+158
-73
lines changed

6 files changed

+158
-73
lines changed

src/mcp/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ npx -y firebase-tools login
8383
| storage_get_object_download_url | storage | Retrieves the download URL for an object in Firebase Storage. |
8484
| messaging_send_message | messaging | Sends a message to a Firebase Cloud Messaging registration token or topic. ONLY ONE of `registration_token` or `topic` may be supplied in a specific call. |
8585
| remoteconfig_get_template | remoteconfig | Retrieves a remote config template for the project |
86-
| remoteconfig_publish_template | remoteconfig | Publishes a new remote config template for the project |
87-
| remoteconfig_rollback_template | remoteconfig | Rollback to a specific version of Remote Config template for a project |
86+
| remoteconfig_update_template | remoteconfig | Publishes a new remote config template or rolls back to a specific version for the project |
8887
| crashlytics_add_note | crashlytics | Add a note to an issue from crashlytics. |
8988
| crashlytics_delete_note | crashlytics | Delete a note from an issue in Crashlytics. |
9089
| crashlytics_get_issue_details | crashlytics | Gets the details about a specific crashlytics issue. |
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ServerTool } from "../../tool";
22
import { get_template } from "./get_template";
3-
import { rollback_template } from "./rollback_template";
4-
import { publish_template } from "./publish_template";
3+
import { update_template } from "./update_template";
54

6-
export const remoteConfigTools: ServerTool[] = [get_template, publish_template, rollback_template];
5+
export const remoteConfigTools: ServerTool[] = [get_template, update_template];

src/mcp/tools/remoteconfig/publish_template.ts

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

src/mcp/tools/remoteconfig/rollback_template.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import * as nock from "nock";
4+
import * as api from "../../../api";
5+
import { RemoteConfigTemplate } from "../../../remoteconfig/interfaces";
6+
import { update_template } from "./update_template";
7+
import { toContent } from "../../util";
8+
import { McpContext } from "../../types";
9+
10+
const PROJECT_ID = "the-remote-config-project";
11+
const TEMPLATE: RemoteConfigTemplate = {
12+
conditions: [],
13+
parameters: {},
14+
parameterGroups: {},
15+
etag: "whatever",
16+
version: {
17+
versionNumber: "1",
18+
updateTime: "2020-01-01T12:00:00.000000Z",
19+
updateUser: {
20+
21+
},
22+
updateOrigin: "CONSOLE",
23+
updateType: "INCREMENTAL_UPDATE",
24+
},
25+
};
26+
27+
describe("update_template", () => {
28+
let sandbox: sinon.SinonSandbox;
29+
30+
beforeEach(() => {
31+
sandbox = sinon.createSandbox();
32+
});
33+
34+
afterEach(() => {
35+
sandbox.restore();
36+
nock.cleanAll();
37+
});
38+
39+
it("should publish the latest template", async () => {
40+
nock(api.remoteConfigApiOrigin())
41+
.put(`/v1/projects/${PROJECT_ID}/remoteConfig`)
42+
.reply(200, TEMPLATE);
43+
44+
const result = await update_template.fn({ template: TEMPLATE }, {
45+
projectId: PROJECT_ID,
46+
} as any as McpContext);
47+
expect(result).to.deep.equal(toContent(TEMPLATE));
48+
});
49+
50+
it("should publish the latest template with * etag", async () => {
51+
nock(api.remoteConfigApiOrigin())
52+
.put(`/v1/projects/${PROJECT_ID}/remoteConfig`, undefined, {
53+
reqheaders: {
54+
"If-Match": "*",
55+
},
56+
})
57+
.reply(200, TEMPLATE);
58+
59+
const result = await update_template.fn({ template: TEMPLATE, force: true }, {
60+
projectId: PROJECT_ID,
61+
} as any as McpContext);
62+
expect(result).to.deep.equal(toContent(TEMPLATE));
63+
});
64+
65+
it("should reject if the publish api call fails", async () => {
66+
nock(api.remoteConfigApiOrigin()).put(`/v1/projects/${PROJECT_ID}/remoteConfig`).reply(404, {});
67+
68+
await expect(
69+
update_template.fn({ template: TEMPLATE }, { projectId: PROJECT_ID } as any as McpContext),
70+
).to.be.rejected;
71+
});
72+
73+
it("should return a rollback to the version number specified", async () => {
74+
nock(api.remoteConfigApiOrigin())
75+
.post(`/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=1`)
76+
.reply(200, TEMPLATE);
77+
78+
const result = await update_template.fn({ version_number: 1 }, {
79+
projectId: PROJECT_ID,
80+
} as any as McpContext);
81+
expect(result).to.deep.equal(toContent(TEMPLATE));
82+
});
83+
84+
it("should reject if the rollback api call fails", async () => {
85+
nock(api.remoteConfigApiOrigin())
86+
.post(`/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=1`)
87+
.reply(404, {});
88+
89+
await expect(
90+
update_template.fn({ version_number: 1 }, { projectId: PROJECT_ID } as any as McpContext),
91+
).to.be.rejected;
92+
});
93+
});
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 { mcpError, toContent } from "../../util";
4+
import { publishTemplate } from "../../../remoteconfig/publish";
5+
import { rollbackTemplate } from "../../../remoteconfig/rollback";
6+
import { RemoteConfigTemplate } from "../../../remoteconfig/interfaces";
7+
8+
export const update_template = tool(
9+
{
10+
name: "update_template",
11+
description:
12+
"Publishes a new remote config template or rolls back to a specific version for the project",
13+
inputSchema: z
14+
.object({
15+
template: z.object({}).optional().describe("The Remote Config template object to publish."),
16+
version_number: z
17+
.number()
18+
.positive()
19+
.optional()
20+
.describe("The version number to roll back to."),
21+
force: z
22+
.boolean()
23+
.optional()
24+
.describe(
25+
"If true, the publish will bypass ETag validation and overwrite the current template. Defaults to false if not provided.",
26+
),
27+
})
28+
.refine(
29+
(data) =>
30+
(data.template && !data.version_number) || (!data.template && data.version_number),
31+
{
32+
message:
33+
"Either provide a template for publish, or a version number to rollback to, but not both.",
34+
},
35+
),
36+
annotations: {
37+
title: "Update Remote Config template",
38+
readOnlyHint: false,
39+
},
40+
_meta: {
41+
requiresAuth: true,
42+
requiresProject: true,
43+
},
44+
},
45+
async ({ template, version_number, force }, { projectId }) => {
46+
if (version_number) {
47+
return toContent(await rollbackTemplate(projectId, version_number!));
48+
}
49+
50+
if (template) {
51+
if (force === undefined) {
52+
return toContent(await publishTemplate(projectId, template as any as RemoteConfigTemplate));
53+
}
54+
return toContent(
55+
await publishTemplate(projectId, template as any as RemoteConfigTemplate, { force }),
56+
);
57+
}
58+
59+
// This part should not be reached due to the refine validation, but as a safeguard:
60+
return mcpError("Either a template or a version number must be specified.");
61+
},
62+
);

0 commit comments

Comments
 (0)