Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/glean/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "glean-vnext",
"version": "0.2.40",
"version": "0.2.41",
"description": "Glean plugin for discovering skills and running tools.",
"author": {
"name": "Glean"
Expand Down
2 changes: 1 addition & 1 deletion plugins/glean/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "glean-vnext",
"version": "0.2.40",
"version": "0.2.41",
"description": "Glean Codex plugin for discovering skills and running tools.",
"author": {
"name": "Glean"
Expand Down
2 changes: 1 addition & 1 deletion plugins/glean/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "glean-vnext",
"displayName": "Glean vNext",
"version": "0.2.40",
"version": "0.2.41",
"description": "Search and act across your company's apps — Jira, Slack, Salesforce, Google Workspace, and more — without leaving Cursor.",
"author": {
"name": "Glean"
Expand Down
7 changes: 4 additions & 3 deletions plugins/glean/dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
closeCallbackServer,
} from "./auth-callback-server.js";
import { handleFindSkills } from "./tools/find-skills.js";
import { handleRunTool, runToolAnnotations } from "./tools/run-tool.js";
import {
handleRunTool,
isCursorClient,
runToolAnnotations,
} from "./tools/run-tool.js";
import { evictStaleSkills } from "./skill-writer.js";
import {
loadServerUrl,
Expand Down Expand Up @@ -281,6 +285,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
annotations: runToolAnnotations(
process.env.ENABLE_HITL === "true",
!!server.getClientCapabilities()?.elicitation,
isCursorClient(server),
),
};
const staticTools: Tool[] = [FIND_SKILLS_TOOL, runTool, SETUP_TOOL];
Expand Down
16 changes: 14 additions & 2 deletions src/tools/run-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ async function findToolJson(
// A stdio server's only client signal is clientInfo.name. Cursor reports
// "cursor-vscode" and already renders the tool name + arguments in its own
// expandable UI, so its approval prompt only needs a one-line review ask.
function isCursorClient(mcpServer: Server): boolean {
export function isCursorClient(mcpServer: Server): boolean {
return (mcpServer.getClientVersion()?.name ?? "")
.toLowerCase()
.startsWith("cursor");
Expand Down Expand Up @@ -423,12 +423,24 @@ export function buildRemoteArgs(
* the tool `readOnlyHint` to suppress the client's native run-tool confirmation
* and avoid a double prompt. Without HITL there is no gate of our own, so we
* leave annotations unset and let the client decide.
*
* TEMP (Cursor): Cursor 3.12.x only renders a server-initiated elicitation on
* its attended tool-call lane; a `run_tool` marked `readOnlyHint` lands on the
* auto-run lane where the HITL elicitation is silently dropped (5-minute hang,
* then fail-closed timeout) and no approval banner ever shows. Cursor also
* ignores the annotation for its own native prompt (it strips tool
* `annotations` on ingest), so the hint buys us nothing there anyway. Until the
* Cursor-side rendering is fixed, do NOT advertise `readOnlyHint` to Cursor so
* the call stays on the interactive lane and the banner renders. Claude Code is
* unaffected: it renders elicitation regardless of lane and the hint still
* correctly suppresses its native prompt.
*/
export function runToolAnnotations(
enableHitl: boolean,
clientSupportsElicitation: boolean,
isCursor: boolean,
): Tool["annotations"] {
return enableHitl && clientSupportsElicitation
return enableHitl && clientSupportsElicitation && !isCursor
? { readOnlyHint: true }
: undefined;
}
14 changes: 10 additions & 4 deletions tests/run-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,15 +710,21 @@ describe("formatArgumentsForFile", () => {
});

describe("runToolAnnotations", () => {
it("marks run_tool read-only when HITL gates an elicitation-capable client", () => {
expect(runToolAnnotations(true, true)).toEqual({ readOnlyHint: true });
it("marks run_tool read-only when HITL gates an elicitation-capable non-Cursor client", () => {
expect(runToolAnnotations(true, true, false)).toEqual({
readOnlyHint: true,
});
});

it("leaves annotations unset when HITL is disabled", () => {
expect(runToolAnnotations(false, true)).toBeUndefined();
expect(runToolAnnotations(false, true, false)).toBeUndefined();
});

it("leaves annotations unset when the client cannot elicit", () => {
expect(runToolAnnotations(true, false)).toBeUndefined();
expect(runToolAnnotations(true, false, false)).toBeUndefined();
});

it("does NOT advertise readOnlyHint to Cursor (its elicitation only renders on the attended lane)", () => {
expect(runToolAnnotations(true, true, true)).toBeUndefined();
});
});
Loading