Accept project name or ID in MCP tools - #157
Conversation
Bump @onkernel/sdk to 0.90.0 and send X-Kernel-Project so callers can select a project by name or ID. Keep project_id as a deprecated alias and record which selector was used on $mcp_tool_call events.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Empty project blocks project_id
- Updated
manage_projectsschema to require non-emptyprojectandproject_idvalues withz.string().min(1).optional(), so empty strings are rejected before handler fallback logic.
- Updated
- ✅ Fixed: Projects test stubs McpServer
- Replaced the fake-server handler invocation test with a real MCP contract test using
connectTestMcp(McpServer+Client+InMemoryTransport), including tool discovery, invalid-input rejection, auth propagation, and serialized valid-call assertions.
- Replaced the fake-server handler invocation test with a real MCP contract test using
Or push these changes by commenting:
@cursor push a18d57eee8
Preview (a18d57eee8)
diff --git a/src/lib/mcp/tools/projects.test.ts b/src/lib/mcp/tools/projects.test.ts
--- a/src/lib/mcp/tools/projects.test.ts
+++ b/src/lib/mcp/tools/projects.test.ts
@@ -1,40 +1,106 @@
/// <reference types="bun-types" />
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { describe, expect, test } from "bun:test";
-import { organizationWideAuthInfo } from "@/lib/mcp/auth-context.test-fixtures";
-import { registerProjectCapabilities } from "@/lib/mcp/tools/projects";
+import { afterEach, describe, expect, test } from "bun:test";
+import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures";
+import {
+ kernelClientMock,
+ resetKernelClientFactory,
+} from "@/lib/mcp/kernel-client.test-fixtures";
-type ToolResult = {
- content: Array<{ type: string; text: string }>;
-};
+const { registerProjectCapabilities } = await import(
+ "@/lib/mcp/tools/projects"
+);
-type ToolHandler = (
- params: Record<string, unknown>,
- extra: { authInfo?: unknown },
-) => Promise<ToolResult>;
+describe("manage_projects contract", () => {
+ afterEach(resetKernelClientFactory);
-function captureHandler() {
- let handler: ToolHandler | undefined;
- const server = {
- resource() {},
- tool(_name: string, ...args: unknown[]) {
- handler = args.at(-1) as ToolHandler;
- },
- } as unknown as McpServer;
- registerProjectCapabilities(server);
- if (!handler) throw new Error("manage_projects was not registered");
- return handler;
-}
+ test("discovers the tool and rejects empty project selectors", async () => {
+ let retrieveCalls = 0;
+ kernelClientMock.factory = () => ({
+ projects: {
+ retrieve: async (idOrName: string) => {
+ retrieveCalls += 1;
+ return { id: idOrName };
+ },
+ },
+ });
+ const { client, close } = await connectTestMcp(
+ registerProjectCapabilities,
+ {},
+ );
-describe("manage_projects", () => {
- test("requires project or project_id for get", async () => {
- const result = await captureHandler()(
- { action: "get" },
- { authInfo: organizationWideAuthInfo() },
+ try {
+ const tools = await client.listTools();
+ const tool = tools.tools.find(
+ (entry) => entry.name === "manage_projects",
+ );
+ const schema = tool?.inputSchema as {
+ properties: Record<string, { minLength?: number }>;
+ };
+
+ expect(tool).toBeDefined();
+ expect(schema.properties.project.minLength).toBe(1);
+ expect(schema.properties.project_id.minLength).toBe(1);
+
+ const emptyProject = await client.callTool({
+ name: "manage_projects",
+ arguments: { action: "get", project: "" },
+ });
+ const emptyProjectID = await client.callTool({
+ name: "manage_projects",
+ arguments: { action: "get", project_id: "" },
+ });
+ const emptyProjectWithDeprecatedFallback = await client.callTool({
+ name: "manage_projects",
+ arguments: { action: "get", project: "", project_id: "proj_123" },
+ });
+
+ expect(emptyProject.isError).toBe(true);
+ expect(emptyProjectID.isError).toBe(true);
+ expect(emptyProjectWithDeprecatedFallback.isError).toBe(true);
+ expect(retrieveCalls).toBe(0);
+ } finally {
+ await close();
+ }
+ });
+
+ test("executes valid get requests for both project selectors", async () => {
+ const retrieveArgs: string[] = [];
+ const tokens: string[] = [];
+ kernelClientMock.factory = (token) => {
+ tokens.push(token);
+ return {
+ projects: {
+ retrieve: async (idOrName: string) => {
+ retrieveArgs.push(idOrName);
+ return { id: idOrName };
+ },
+ },
+ };
+ };
+ const { client, close } = await connectTestMcp(
+ registerProjectCapabilities,
+ {},
);
- expect(result.content[0].text).toContain(
- "project or project_id is required",
- );
+
+ try {
+ const byName = await client.callTool({
+ name: "manage_projects",
+ arguments: { action: "get", project: "billing" },
+ });
+ const byID = await client.callTool({
+ name: "manage_projects",
+ arguments: { action: "get", project_id: "proj_123" },
+ });
+
+ expect(byName.isError).toBeUndefined();
+ expect(byID.isError).toBeUndefined();
+ expect(tokens).toEqual(["test-token", "test-token"]);
+ expect(retrieveArgs).toEqual(["billing", "proj_123"]);
+ expect(toolResultJSON(byName)).toEqual({ id: "billing" });
+ expect(toolResultJSON(byID)).toEqual({ id: "proj_123" });
+ } finally {
+ await close();
+ }
});
});
diff --git a/src/lib/mcp/tools/projects.ts b/src/lib/mcp/tools/projects.ts
--- a/src/lib/mcp/tools/projects.ts
+++ b/src/lib/mcp/tools/projects.ts
@@ -30,12 +30,14 @@
.describe("Operation to perform."),
project: z
.string()
+ .min(1)
.describe(
"Project name or ID. Required for get, update, delete, get_limits, and update_limits.",
)
.optional(),
project_id: z
.string()
+ .min(1)
.describe(
"Deprecated: use `project` instead. Project ID. Required for get, update, delete, get_limits, and update_limits.",
)You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 45e21c9. Configure here.
Empty project no longer shadows project_id. manage_projects now requires min(1) on both params and the contract test uses a real MCP server.
masnwilliams
left a comment
There was a problem hiding this comment.
requesting changes for three structural blockers: keep fixed-project selection policy consistent across both parameter names, keep one canonical selector contract, and exercise successful calls through the real MCP boundary with injected dependencies. the optional project behavior itself is right; the issue is preserving the scope and testing architecture established by the recent MCP work.
bun test (231 passing) and TypeScript pass locally.
Use the same fixed-project rule for project and project_id, share the selector schema with manage_projects, inject McpDependencies for successful MCP contract tests, and keep AuthLoginInput free of transport aliases.
masnwilliams
left a comment
There was a problem hiding this comment.
latest changes address all review findings. fixed-scope selection is consistent, the selector contract is centralized, successful MCP calls are covered through injected dependencies, and managed-auth state is clean. local tests, TypeScript, formatting, CI, and BugBot are green.


Summary
@onkernel/sdkto 0.90.0 and sendX-Kernel-Projectso a tool call can select a project by name or ID.project_idas a deprecated alias on project-scoped tools andmanage_projects.$mcp_used_project/$mcp_used_project_idbooleans on$mcp_tool_callevents so we can measure deprecated-param usage without sending selector values.projectwins when both params are set. Existingproject_idcallers keep working.Tests
bun test(230 pass)bunx tsc --noEmitNote
Medium Risk
Wide change to how every project-scoped tool routes API calls; backward compatibility relies on alias behavior and SDK 0.90 project resolution, so mis-scoped requests are the main failure mode.
Overview
Project-scoped MCP tools now take an optional
projectparameter (name or ID), withproject_idkept as a deprecated alias.projectwins when both are set; existingproject_idcallers are unchanged.The Kernel client is bumped to @onkernel/sdk 0.90.0 and passes
project(replacingprojectID) so API requests can resolve projects by name. Shared helpersrequestedProject/projectForOperationcentralize selection and connection-scope rules;manage_projectsuses the same schema for get/update/delete/limit actions.Managed-auth
wait/ launchernext_actionpayloads now emitprojectinstead ofproject_id. PostHog$mcp_tool_callevents add$mcp_used_projectand$mcp_used_project_idbooleans (no selector values) to track adoption of the new param.Reviewed by Cursor Bugbot for commit 27abe09. Bugbot is set up for automated code reviews on this repo. Configure here.