Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Added `appcheck:providers:list`, `appcheck:providers:get` and `appcheck:providers:set` to configure App Check attestation providers for an app.
- Added `appcheck:apps:list` to show every app with its configured App Check providers.
- Added web app support for Crashlytics MCP tools and prompts.>>>>>>> main
- Added web app support for Crashlytics MCP tools and prompts.
- Added support for forwarding custom HTTP headers (`Mcp-Param-*`) to remote MCP tools when defined in tool parameter input schemas (`x-mcp-header`), per [SEP-2243](https://modelcontextprotocol.io/seps/2243-http-standardization).
40 changes: 39 additions & 1 deletion src/mcp/onemcp/onemcp_server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
feature: "auth",
});
expect(clientRequestStub).to.have.been.calledOnce;
expect(clientRequestStub.firstCall.args[0].headers).to.deep.include({

Check warning on line 56 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .headers on an `any` value
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION,
"Mcp-Method": "tools/list",
});
Expand Down Expand Up @@ -104,7 +104,7 @@
});

describe("callTool", () => {
const mockContext: any = {

Check warning on line 107 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type
projectId: "test-project",
};

Expand All @@ -122,11 +122,11 @@
body: { result: mockCallResult },
});

const result = await tool.fn({ arg: "val" }, mockContext);

Check warning on line 125 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `McpContext`

expect(result).to.deep.equal(mockCallResult);
expect(ensureStub).to.have.been.calledOnceWith(
mockContext.projectId,

Check warning on line 129 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .projectId on an `any` value
serverUrl,
feature,
true,
Expand All @@ -143,7 +143,7 @@
id: 1,
},
});
expect(clientRequestStub.secondCall.args[0].headers).to.deep.include({

Check warning on line 146 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .headers on an `any` value
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION,
"Mcp-Method": "tools/call",
"Mcp-Name": "test_tool",
Expand All @@ -151,6 +151,44 @@
});
});

it("should include Mcp-Param-X HTTP headers when tool inputSchema defines x-mcp-header", async () => {
const mockMcpTool = {
name: "execute_sql",
inputSchema: {
type: "object",
properties: {
region: {
type: "string",
"x-mcp-header": "Region",
},
query: {
type: "string",
},
},
},
};
clientRequestStub.onFirstCall().resolves({
body: { result: { tools: [mockMcpTool] } },
});

const tools = await server.listTools();
const tool = tools[0];

clientRequestStub.onSecondCall().resolves({
body: { result: { content: [] } },
});

await tool.fn({ region: "us-west1", query: "SELECT 1" }, mockContext);

Check warning on line 181 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `McpContext`

expect(clientRequestStub.secondCall.args[0].headers).to.deep.include({

Check warning on line 183 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .headers on an `any` value
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION,
"Mcp-Method": "tools/call",
"Mcp-Name": "execute_sql",
"x-goog-user-project": "test-project",
"Mcp-Param-Region": "us-west1",
});
});

it("should proxy tool call without x-goog-user-project header if projectId is missing", async () => {
const mockMcpTool = { name: "test_tool", inputSchema: { type: "object", properties: {} } };
clientRequestStub.onFirstCall().resolves({
Expand All @@ -164,10 +202,10 @@
body: { result: { content: [] } },
});

await tool.fn({ arg: "val" }, { ...mockContext, projectId: undefined });

Check warning on line 205 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `McpContext`

expect(clientRequestStub.secondCall.args[0].headers?.["x-goog-user-project"]).to.be.undefined;

Check warning on line 207 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .headers on an `any` value
expect(clientRequestStub.secondCall.args[0].headers).to.deep.equal({

Check warning on line 208 in src/mcp/onemcp/onemcp_server.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .headers on an `any` value
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION,
"Mcp-Method": "tools/call",
"Mcp-Name": "test_tool",
Expand Down Expand Up @@ -240,7 +278,7 @@

const fn = (serverWithFilter as any).callTool.bind(serverWithFilter);

await expect(fn("disallowed_tool", {}, mockContext)).to.be.rejectedWith(
await expect(fn("disallowed_tool", undefined, {}, mockContext)).to.be.rejectedWith(
FirebaseError,
/is not allowed on remote server/,
);
Expand Down
19 changes: 18 additions & 1 deletion src/mcp/onemcp/onemcp_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ListToolsRequest,
CallToolRequest,
LATEST_PROTOCOL_VERSION,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { Client } from "../../apiv2";
import { ServerTool, ServerToolMeta } from "../tool";
Expand Down Expand Up @@ -96,7 +97,7 @@ export class OneMcpServer {
[x: string]: unknown;
},
ctx: McpContext,
) => this.callTool(mcpTool.name, args, ctx),
) => this.callTool(mcpTool.name, mcpTool.inputSchema, args, ctx),
isAvailable: () => Promise.resolve(true),
}));
} catch (error) {
Expand All @@ -111,6 +112,7 @@ export class OneMcpServer {
*/
private async callTool(
toolName: string,
inputSchema: Tool["inputSchema"] | undefined,
args: {
[x: string]: unknown;
},
Expand All @@ -126,6 +128,20 @@ export class OneMcpServer {
);
}

const paramHeaders: Record<string, string> = {};
const props = inputSchema?.properties;
if (props && typeof props === "object" && args) {
for (const [paramName, propSchema] of Object.entries(props)) {
if (propSchema && typeof propSchema === "object" && "x-mcp-header" in propSchema) {
const headerName = (propSchema as Record<string, unknown>)["x-mcp-header"];
const argValue = args[paramName];
if (argValue !== undefined && argValue !== null) {
paramHeaders[`Mcp-Param-${headerName}`] = String(argValue);
}
}
}
}
Comment thread
joehan marked this conversation as resolved.

// TODO: Optimize this to not call ensure on every tool call.
await ensure(ctx.projectId, this.serverUrl, this.feature, /* silent=*/ true);
try {
Expand All @@ -149,6 +165,7 @@ export class OneMcpServer {
"Mcp-Method": "tools/call",
"Mcp-Name": toolName,
...(ctx.projectId ? { "x-goog-user-project": ctx.projectId } : {}),
...paramHeaders,
},
},
);
Expand Down
Loading