Skip to content

Commit 6a1f838

Browse files
authored
Merge pull request #108 from kernel/codex/projects-mcp-tool
[codex] Add MCP project management tool
2 parents 060a782 + 608f5aa commit 6a1f838

2 files changed

Lines changed: 161 additions & 0 deletions

File tree

src/lib/mcp/register.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { registerDocsTools } from "@/lib/mcp/tools/docs";
88
import { registerExtensionTools } from "@/lib/mcp/tools/extensions";
99
import { registerPlaywrightTool } from "@/lib/mcp/tools/playwright";
1010
import { registerProfileCapabilities } from "@/lib/mcp/tools/profiles";
11+
import { registerProjectCapabilities } from "@/lib/mcp/tools/projects";
1112
import { registerProxyTools } from "@/lib/mcp/tools/proxies";
1213
import { registerShellTool } from "@/lib/mcp/tools/shell";
1314

@@ -16,6 +17,7 @@ export function registerMcpCapabilities(server: McpServer) {
1617
registerKernelPrompts(server);
1718
registerDocsTools(server);
1819
registerBrowserCapabilities(server);
20+
registerProjectCapabilities(server);
1921
registerBrowserPoolCapabilities(server);
2022
registerProxyTools(server);
2123
registerExtensionTools(server);

src/lib/mcp/tools/projects.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { z } from "zod";
3+
import { createKernelClient } from "@/lib/mcp/kernel-client";
4+
5+
export function registerProjectCapabilities(server: McpServer) {
6+
// manage_projects -- Create, list, get, update, and delete organization projects
7+
server.tool(
8+
"manage_projects",
9+
'Manage Kernel projects for resource isolation within an organization. Use "create" to create a project, "list" to discover projects, "get" to retrieve one, "update" to rename or archive one, or "delete" to remove an empty project.',
10+
{
11+
action: z
12+
.enum(["create", "list", "get", "update", "delete"])
13+
.describe("Operation to perform."),
14+
project_id: z
15+
.string()
16+
.describe("Project ID. Required for get, update, and delete.")
17+
.optional(),
18+
name: z.string().describe("(create, update) Project name.").optional(),
19+
status: z
20+
.enum(["active", "archived"])
21+
.describe('(update) Project status. Use "archived" to archive.')
22+
.optional(),
23+
query: z
24+
.string()
25+
.describe(
26+
"(list) Case-insensitive substring match against project name.",
27+
)
28+
.optional(),
29+
limit: z.number().describe("(list) Max results per page.").optional(),
30+
offset: z.number().describe("(list) Pagination offset.").optional(),
31+
},
32+
async (params, extra) => {
33+
if (!extra.authInfo) throw new Error("Authentication required");
34+
const client = createKernelClient(extra.authInfo.token);
35+
36+
try {
37+
switch (params.action) {
38+
case "create": {
39+
if (!params.name) {
40+
return {
41+
content: [
42+
{ type: "text", text: "Error: name is required for create." },
43+
],
44+
};
45+
}
46+
const project = await client.projects.create({ name: params.name });
47+
return {
48+
content: [
49+
{ type: "text", text: JSON.stringify(project, null, 2) },
50+
],
51+
};
52+
}
53+
case "list": {
54+
const page = await client.projects.list({
55+
...(params.query && { query: params.query }),
56+
...(params.limit !== undefined && { limit: params.limit }),
57+
...(params.offset !== undefined && { offset: params.offset }),
58+
});
59+
const items = page.getPaginatedItems();
60+
return {
61+
content: [
62+
{
63+
type: "text",
64+
text: JSON.stringify(
65+
{
66+
items,
67+
has_more: page.has_more,
68+
next_offset: page.next_offset,
69+
},
70+
null,
71+
2,
72+
),
73+
},
74+
],
75+
};
76+
}
77+
case "get": {
78+
if (!params.project_id) {
79+
return {
80+
content: [
81+
{
82+
type: "text",
83+
text: "Error: project_id is required for get.",
84+
},
85+
],
86+
};
87+
}
88+
const project = await client.projects.retrieve(params.project_id);
89+
return {
90+
content: [
91+
{ type: "text", text: JSON.stringify(project, null, 2) },
92+
],
93+
};
94+
}
95+
case "update": {
96+
if (!params.project_id) {
97+
return {
98+
content: [
99+
{
100+
type: "text",
101+
text: "Error: project_id is required for update.",
102+
},
103+
],
104+
};
105+
}
106+
if (!params.name && !params.status) {
107+
return {
108+
content: [
109+
{
110+
type: "text",
111+
text: "Error: name or status is required for update.",
112+
},
113+
],
114+
};
115+
}
116+
const updateParams: Parameters<typeof client.projects.update>[1] =
117+
{};
118+
if (params.name) updateParams.name = params.name;
119+
if (params.status) updateParams.status = params.status;
120+
const project = await client.projects.update(
121+
params.project_id,
122+
updateParams,
123+
);
124+
return {
125+
content: [
126+
{ type: "text", text: JSON.stringify(project, null, 2) },
127+
],
128+
};
129+
}
130+
case "delete": {
131+
if (!params.project_id) {
132+
return {
133+
content: [
134+
{
135+
type: "text",
136+
text: "Error: project_id is required for delete.",
137+
},
138+
],
139+
};
140+
}
141+
await client.projects.delete(params.project_id);
142+
return {
143+
content: [{ type: "text", text: "Project deleted successfully" }],
144+
};
145+
}
146+
}
147+
} catch (error) {
148+
return {
149+
content: [
150+
{
151+
type: "text",
152+
text: `Error in manage_projects (${params.action}): ${error instanceof Error ? error.message : String(error)}`,
153+
},
154+
],
155+
};
156+
}
157+
},
158+
);
159+
}

0 commit comments

Comments
 (0)