Skip to content

Commit 444dbfe

Browse files
Improve MCP tool action params ergonomics and validation
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 1ee57e0 commit 444dbfe

2 files changed

Lines changed: 65 additions & 11 deletions

File tree

src/agent/mcp/client.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { normalizeMCPToolParams } from "@/agent/mcp/client";
2+
3+
describe("normalizeMCPToolParams", () => {
4+
it("returns object inputs unchanged", () => {
5+
const input = { query: "laptops", limit: 5 };
6+
expect(normalizeMCPToolParams(input)).toEqual(input);
7+
});
8+
9+
it("parses valid JSON object strings", () => {
10+
const json = "{\"query\":\"weather\",\"units\":\"metric\"}";
11+
expect(normalizeMCPToolParams(json)).toEqual({
12+
query: "weather",
13+
units: "metric",
14+
});
15+
});
16+
17+
it("throws for invalid JSON strings", () => {
18+
expect(() => normalizeMCPToolParams("{invalid")).toThrow(
19+
"Invalid MCP tool params JSON string"
20+
);
21+
});
22+
23+
it("throws when parsed JSON is not an object", () => {
24+
expect(() => normalizeMCPToolParams("[1,2,3]")).toThrow(
25+
"must parse to a JSON object"
26+
);
27+
});
28+
});

src/agent/mcp/client.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,38 @@ interface ServerConnection {
1616
actions: AgentActionDefinition[];
1717
}
1818

19+
const MCPToolActionParams = z.object({
20+
params: z
21+
.union([z.string(), z.record(z.string(), z.unknown())])
22+
.describe(
23+
"Parameters for the MCP tool. Provide either a JSON object directly or a JSON string."
24+
),
25+
});
26+
27+
type MCPToolActionInput = z.infer<typeof MCPToolActionParams>;
28+
29+
export function normalizeMCPToolParams(
30+
input: MCPToolActionInput["params"]
31+
): Record<string, unknown> {
32+
if (typeof input === "string") {
33+
let parsed: unknown;
34+
try {
35+
parsed = JSON.parse(input);
36+
} catch (error) {
37+
const message = error instanceof Error ? error.message : String(error);
38+
throw new Error(`Invalid MCP tool params JSON string: ${message}`);
39+
}
40+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
41+
throw new Error(
42+
"MCP tool params must parse to a JSON object, not an array or primitive"
43+
);
44+
}
45+
return parsed as Record<string, unknown>;
46+
}
47+
48+
return input;
49+
}
50+
1951
class MCPClient {
2052
private servers: Map<string, ServerConnection> = new Map();
2153
private debug: boolean;
@@ -115,26 +147,20 @@ class MCPClient {
115147
// Create action definition
116148
return {
117149
type: tool.name,
118-
actionParams: z
119-
.object({
120-
params: z
121-
.string()
122-
.describe(
123-
`The stringified parameters to the ${tool.name} MCP tool. Here is the schema: ${JSON.stringify(tool.inputSchema)}`
124-
),
125-
})
126-
.describe(tool.description ?? ""),
150+
actionParams: MCPToolActionParams.describe(
151+
`${tool.description ?? ""} Tool input schema: ${JSON.stringify(tool.inputSchema)}`
152+
),
127153
run: async (
128154
ctx: ActionContext,
129-
action: any
155+
action: MCPToolActionInput
130156
): Promise<ActionOutput> => {
131157
if (!ctx.mcpClient) {
132158
throw new Error(
133159
"MCP client not available. Please ensure an MCP server is connected."
134160
);
135161
}
136162

137-
const params = JSON.parse(action.params);
163+
const params = normalizeMCPToolParams(action.params);
138164
const targetServerId = serverId;
139165

140166
const result = await ctx.mcpClient.executeTool(

0 commit comments

Comments
 (0)