Skip to content

Commit b44827d

Browse files
committed
Tighten API key MCP validation
Mark API-key tool validation and caught SDK failures as MCP error results so callers do not see failed operations as successful text responses. Require integer pagination inputs at the MCP schema boundary so agents get immediate validation feedback before invoking the SDK.
1 parent 23a9202 commit b44827d

1 file changed

Lines changed: 45 additions & 78 deletions

File tree

src/lib/mcp/tools/api-keys.ts

Lines changed: 45 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { z } from "zod";
33
import { createKernelClient } from "@/lib/mcp/kernel-client";
44

5+
function textResponse(text: string) {
6+
return { content: [{ type: "text" as const, text }] };
7+
}
8+
9+
function jsonResponse(value: unknown) {
10+
return textResponse(JSON.stringify(value, null, 2) ?? String(value));
11+
}
12+
13+
function errorResponse(text: string) {
14+
return { ...textResponse(text), isError: true as const };
15+
}
16+
17+
function errorMessage(error: unknown) {
18+
return error instanceof Error ? error.message : String(error);
19+
}
20+
521
export function registerAPIKeyCapabilities(server: McpServer) {
622
// manage_api_keys -- Create, list, get, update, and delete Kernel API keys
723
server.tool(
@@ -33,8 +49,18 @@ export function registerAPIKeyCapabilities(server: McpServer) {
3349
"(create) Days until expiry, up to 3650. Use null for no expiry.",
3450
)
3551
.optional(),
36-
limit: z.number().describe("(list) Max results per page.").optional(),
37-
offset: z.number().describe("(list) Pagination offset.").optional(),
52+
limit: z
53+
.number()
54+
.int()
55+
.min(1)
56+
.describe("(list) Max results per page.")
57+
.optional(),
58+
offset: z
59+
.number()
60+
.int()
61+
.min(0)
62+
.describe("(list) Pagination offset.")
63+
.optional(),
3864
},
3965
async (params, extra) => {
4066
if (!extra.authInfo) throw new Error("Authentication required");
@@ -44,11 +70,7 @@ export function registerAPIKeyCapabilities(server: McpServer) {
4470
switch (params.action) {
4571
case "create": {
4672
if (!params.name) {
47-
return {
48-
content: [
49-
{ type: "text", text: "Error: name is required for create." },
50-
],
51-
};
73+
return errorResponse("Error: name is required for create.");
5274
}
5375
const createParams: Parameters<typeof client.apiKeys.create>[0] = {
5476
name: params.name,
@@ -60,106 +82,51 @@ export function registerAPIKeyCapabilities(server: McpServer) {
6082
createParams.days_to_expire = params.days_to_expire;
6183
}
6284
const apiKey = await client.apiKeys.create(createParams);
63-
return {
64-
content: [
65-
{ type: "text", text: JSON.stringify(apiKey, null, 2) },
66-
],
67-
};
85+
return jsonResponse(apiKey);
6886
}
6987
case "list": {
7088
const page = await client.apiKeys.list({
7189
...(params.limit !== undefined && { limit: params.limit }),
7290
...(params.offset !== undefined && { offset: params.offset }),
7391
});
7492
const items = page.getPaginatedItems();
75-
return {
76-
content: [
77-
{
78-
type: "text",
79-
text: JSON.stringify(
80-
{
81-
items,
82-
has_more: page.has_more,
83-
next_offset: page.next_offset,
84-
},
85-
null,
86-
2,
87-
),
88-
},
89-
],
90-
};
93+
return jsonResponse({
94+
items,
95+
has_more: page.has_more,
96+
next_offset: page.next_offset,
97+
});
9198
}
9299
case "get": {
93100
if (!params.api_key_id) {
94-
return {
95-
content: [
96-
{
97-
type: "text",
98-
text: "Error: api_key_id is required for get.",
99-
},
100-
],
101-
};
101+
return errorResponse("Error: api_key_id is required for get.");
102102
}
103103
const apiKey = await client.apiKeys.retrieve(params.api_key_id);
104-
return {
105-
content: [
106-
{ type: "text", text: JSON.stringify(apiKey, null, 2) },
107-
],
108-
};
104+
return jsonResponse(apiKey);
109105
}
110106
case "update": {
111107
if (!params.api_key_id) {
112-
return {
113-
content: [
114-
{
115-
type: "text",
116-
text: "Error: api_key_id is required for update.",
117-
},
118-
],
119-
};
108+
return errorResponse("Error: api_key_id is required for update.");
120109
}
121110
if (!params.name) {
122-
return {
123-
content: [
124-
{ type: "text", text: "Error: name is required for update." },
125-
],
126-
};
111+
return errorResponse("Error: name is required for update.");
127112
}
128113
const apiKey = await client.apiKeys.update(params.api_key_id, {
129114
name: params.name,
130115
});
131-
return {
132-
content: [
133-
{ type: "text", text: JSON.stringify(apiKey, null, 2) },
134-
],
135-
};
116+
return jsonResponse(apiKey);
136117
}
137118
case "delete": {
138119
if (!params.api_key_id) {
139-
return {
140-
content: [
141-
{
142-
type: "text",
143-
text: "Error: api_key_id is required for delete.",
144-
},
145-
],
146-
};
120+
return errorResponse("Error: api_key_id is required for delete.");
147121
}
148122
await client.apiKeys.delete(params.api_key_id);
149-
return {
150-
content: [{ type: "text", text: "API key deleted successfully" }],
151-
};
123+
return textResponse("API key deleted successfully");
152124
}
153125
}
154126
} catch (error) {
155-
return {
156-
content: [
157-
{
158-
type: "text",
159-
text: `Error in manage_api_keys (${params.action}): ${error instanceof Error ? error.message : String(error)}`,
160-
},
161-
],
162-
};
127+
return errorResponse(
128+
`Error in manage_api_keys (${params.action}): ${errorMessage(error)}`,
129+
);
163130
}
164131
},
165132
);

0 commit comments

Comments
 (0)