Skip to content

Accept project name or ID in MCP tools - #157

Merged
rgarcia merged 3 commits into
mainfrom
hypeship/project-name-or-id
Aug 13, 2026
Merged

Accept project name or ID in MCP tools#157
rgarcia merged 3 commits into
mainfrom
hypeship/project-name-or-id

Conversation

@rgarcia

@rgarcia rgarcia commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Bump @onkernel/sdk to 0.90.0 and send X-Kernel-Project so a tool call can select a project by name or ID.
  • Keep project_id as a deprecated alias on project-scoped tools and manage_projects.
  • Record $mcp_used_project / $mcp_used_project_id booleans on $mcp_tool_call events so we can measure deprecated-param usage without sending selector values.

project wins when both params are set. Existing project_id callers keep working.

Tests

  • bun test (230 pass)
  • bunx tsc --noEmit

Note

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 project parameter (name or ID), with project_id kept as a deprecated alias. project wins when both are set; existing project_id callers are unchanged.

The Kernel client is bumped to @onkernel/sdk 0.90.0 and passes project (replacing projectID) so API requests can resolve projects by name. Shared helpers requestedProject / projectForOperation centralize selection and connection-scope rules; manage_projects uses the same schema for get/update/delete/limit actions.

Managed-auth wait / launcher next_action payloads now emit project instead of project_id. PostHog $mcp_tool_call events add $mcp_used_project and $mcp_used_project_id booleans (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.

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.
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mcp Ready Ready Preview Aug 13, 2026 7:46pm

@socket-security

socket-security Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​onkernel/​sdk@​0.87.0 ⏵ 0.90.082 +1100100 +199 +1100

View full report

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Empty project blocks project_id
    • Updated manage_projects schema to require non-empty project and project_id values with z.string().min(1).optional(), so empty strings are rejected before handler fallback logic.
  • ✅ 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.

Create PR

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.

Comment thread src/lib/mcp/tools/projects.ts
Comment thread src/lib/mcp/tools/projects.test.ts Outdated
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 masnwilliams left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/mcp/project-selection.ts Outdated
Comment thread src/lib/mcp/tools/projects.ts Outdated
Comment thread src/lib/mcp/tools/projects.test.ts
Comment thread src/lib/mcp/tools/managed-auth-state.ts Outdated
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 masnwilliams left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rgarcia
rgarcia merged commit 2d98491 into main Aug 13, 2026
10 checks passed
@rgarcia
rgarcia deleted the hypeship/project-name-or-id branch August 13, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants