/** * Cursor Protobuf Encoder/Decoder * Implements ConnectRPC protobuf wire format for Cursor API */ import { v4 as uuidv4 } from "uuid"; import zlib from "zlib"; const DEBUG = process.env.CURSOR_PROTOBUF_DEBUG === "1"; const log = (tag, ...args) => DEBUG && console.log(`[PROTOBUF:${tag}]`, ...args); const textDecoder = new TextDecoder(); const PROTOBUF_SCHEMA_VERSION = "1.1.3"; // ==================== SCHEMAS ==================== const WIRE_TYPE = { VARINT: 0, FIXED64: 1, LEN: 2, FIXED32: 5 }; const ROLE = { USER: 1, ASSISTANT: 2 }; const UNIFIED_MODE = { CHAT: 1, AGENT: 2 }; const THINKING_LEVEL = { UNSPECIFIED: 0, MEDIUM: 1, HIGH: 2 }; const CLIENT_SIDE_TOOL_V2 = { MCP: 19 }; const CLIENT_SIDE_TOOL_V2_MCP = 19; const FIELD = { // StreamUnifiedChatRequestWithTools (top level) REQUEST: 1, // StreamUnifiedChatRequest MESSAGES: 1, UNKNOWN_2: 2, INSTRUCTION: 3, UNKNOWN_4: 4, MODEL: 5, WEB_TOOL: 8, UNKNOWN_13: 13, CURSOR_SETTING: 15, UNKNOWN_19: 19, CONVERSATION_ID: 23, METADATA: 26, IS_AGENTIC: 27, SUPPORTED_TOOLS: 29, MESSAGE_IDS: 30, MCP_TOOLS: 34, LARGE_CONTEXT: 35, UNKNOWN_38: 38, UNIFIED_MODE: 46, UNKNOWN_47: 47, SHOULD_DISABLE_TOOLS: 48, THINKING_LEVEL: 49, UNKNOWN_51: 51, UNKNOWN_53: 53, UNIFIED_MODE_NAME: 54, // ConversationMessage MSG_CONTENT: 1, MSG_ROLE: 2, MSG_ID: 13, MSG_TOOL_RESULTS: 18, MSG_IS_AGENTIC: 29, MSG_UNIFIED_MODE: 47, MSG_SUPPORTED_TOOLS: 51, // ConversationMessage.ToolResult TOOL_RESULT_CALL_ID: 1, TOOL_RESULT_NAME: 2, TOOL_RESULT_INDEX: 3, TOOL_RESULT_RAW_ARGS: 5, TOOL_RESULT_RESULT: 8, TOOL_RESULT_TOOL_CALL: 11, TOOL_RESULT_MODEL_CALL_ID: 12, // ClientSideToolV2Result (nested inside ToolResult.result) CLIENT_RESULT_TOOL: 1, CLIENT_RESULT_MCP_RESULT: 28, CLIENT_RESULT_TOOL_CALL_ID: 35, CLIENT_RESULT_MODEL_CALL_ID: 48, CLIENT_RESULT_TOOL_INDEX: 49, // MCPResult (nested inside ClientSideToolV2Result.mcp_result) MCP_RESULT_SELECTED_TOOL: 1, MCP_RESULT_RESULT: 2, // ClientSideToolV2Call (nested inside ToolResult.tool_call) CLIENT_CALL_TOOL: 1, CLIENT_CALL_MCP_PARAMS: 27, CLIENT_CALL_TOOL_CALL_ID: 3, CLIENT_CALL_NAME: 9, CLIENT_CALL_RAW_ARGS: 10, CLIENT_CALL_TOOL_INDEX: 48, CLIENT_CALL_MODEL_CALL_ID: 49, // Model MODEL_NAME: 1, MODEL_EMPTY: 4, // Instruction INSTRUCTION_TEXT: 1, // CursorSetting SETTING_PATH: 1, SETTING_UNKNOWN_3: 3, SETTING_UNKNOWN_6: 6, SETTING_UNKNOWN_8: 8, SETTING_UNKNOWN_9: 9, // CursorSetting.Unknown6 SETTING6_FIELD_1: 1, SETTING6_FIELD_2: 2, // Metadata META_PLATFORM: 1, META_ARCH: 2, META_VERSION: 3, META_CWD: 4, META_TIMESTAMP: 5, // MessageId MSGID_ID: 1, MSGID_SUMMARY: 2, MSGID_ROLE: 3, // MCPTool MCP_TOOL_NAME: 1, MCP_TOOL_DESC: 2, MCP_TOOL_PARAMS: 3, MCP_TOOL_SERVER: 4, // StreamUnifiedChatResponseWithTools (response) TOOL_CALL: 1, RESPONSE: 2, // ClientSideToolV2Call TOOL_ID: 3, TOOL_NAME: 9, TOOL_RAW_ARGS: 10, TOOL_IS_LAST: 11, TOOL_IS_LAST_ALT: 15, TOOL_MCP_PARAMS: 27, // MCPParams MCP_TOOLS_LIST: 1, // MCPParams.Tool (nested) MCP_NESTED_NAME: 1, MCP_NESTED_PARAMS: 3, // StreamUnifiedChatResponse RESPONSE_TEXT: 1, THINKING: 25, // Thinking THINKING_TEXT: 1 }; // Known response field numbers — used to detect unknown fields from protocol updates const KNOWN_RESPONSE_FIELDS = new Set([ FIELD.TOOL_CALL, FIELD.RESPONSE, FIELD.TOOL_ID, FIELD.TOOL_NAME, FIELD.TOOL_RAW_ARGS, FIELD.TOOL_IS_LAST, FIELD.TOOL_MCP_PARAMS, FIELD.RESPONSE_TEXT, FIELD.THINKING ]); // ==================== PRIMITIVE ENCODING ==================== export function encodeVarint(value) { const bytes = []; while (value >= 0x80) { bytes.push((value & 0x7F) | 0x80); value >>>= 7; } bytes.push(value & 0x7F); return new Uint8Array(bytes); } export function encodeField(fieldNum, wireType, value) { const tag = (fieldNum << 3) | wireType; const tagBytes = encodeVarint(tag); if (wireType === WIRE_TYPE.VARINT) { const valueBytes = encodeVarint(value); return concatArrays(tagBytes, valueBytes); } if (wireType === WIRE_TYPE.LEN) { const dataBytes = typeof value === "string" ? new TextEncoder().encode(value) : value instanceof Uint8Array ? value : Buffer.isBuffer(value) ? new Uint8Array(value) : new Uint8Array(0); const lengthBytes = encodeVarint(dataBytes.length); return concatArrays(tagBytes, lengthBytes, dataBytes); } return new Uint8Array(0); } function concatArrays(...arrays) { const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; } // ==================== MESSAGE ENCODING ==================== /** * Format tool name: "toolName" → "mcp_custom_toolName" * Also handles: "mcp__server__tool" → "mcp_server_tool" */ function formatToolName(name) { const base = typeof name === "string" && name.length > 0 ? name : "tool"; if (base.startsWith("mcp__")) { const rest = base.slice("mcp__".length); const splitIdx = rest.indexOf("__"); if (splitIdx >= 0) { const server = rest.slice(0, splitIdx) || "custom"; const toolName = rest.slice(splitIdx + 2) || "tool"; return `mcp_${server}_${toolName}`; } return `mcp_custom_${rest || "tool"}`; } if (base.startsWith("mcp_")) return base; return `mcp_custom_${base}`; } /** * Parse formatted tool name: "mcp_server_tool" → { serverName, selectedTool } */ function parseToolName(formattedName) { if (typeof formattedName !== "string" || !formattedName.startsWith("mcp_")) { return { serverName: "custom", selectedTool: formattedName || "tool" }; } const tail = formattedName.slice("mcp_".length); const splitIdx = tail.indexOf("_"); if (splitIdx < 0) { return { serverName: "custom", selectedTool: tail || "tool" }; } return { serverName: tail.slice(0, splitIdx) || "custom", selectedTool: tail.slice(splitIdx + 1) || "tool" }; } /** * Parse tool_call_id into { toolCallId, modelCallId } * Cursor uses "\nmc_" delimiter for model_call_id */ function parseToolId(id) { const delimiter = "\nmc_"; const idx = id.indexOf(delimiter); if (idx >= 0) { return { toolCallId: id.slice(0, idx), modelCallId: id.slice(idx + delimiter.length) }; } return { toolCallId: id, modelCallId: null }; } /** * Encode MCPResult proto: { selected_tool, result } */ function encodeMcpResult(selectedTool, resultContent) { return concatArrays( encodeField(FIELD.MCPR_SELECTED_TOOL, WIRE_TYPE.LEN, selectedTool), encodeField(FIELD.MCPR_RESULT, WIRE_TYPE.LEN, resultContent) ); } /** * Encode ClientSideToolV2Result proto: { tool, mcp_result, call_id, model_call_id, tool_index } * Represents the result of executing a tool */ function encodeClientSideToolV2Result(toolCallId, modelCallId, selectedTool, resultContent, toolIndex = 1) { return concatArrays( encodeField(FIELD.CV2R_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2_MCP), encodeField(FIELD.CV2R_MCP_RESULT, WIRE_TYPE.LEN, encodeMcpResult(selectedTool, resultContent)), encodeField(FIELD.CV2R_CALL_ID, WIRE_TYPE.LEN, toolCallId), ...(modelCallId ? [encodeField(FIELD.CV2R_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)] : []), encodeField(FIELD.CV2R_TOOL_INDEX, WIRE_TYPE.VARINT, toolIndex > 0 ? toolIndex : 1) ); } /** * Encode MCPParams.Tool nested inside ClientSideToolV2Call */ function encodeMcpParamsForCall(toolName, rawArgs, serverName) { const tool = concatArrays( encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName), encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, rawArgs), encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, serverName) ); return encodeField(FIELD.MCP_TOOLS_LIST, WIRE_TYPE.LEN, tool); } /** * Encode ClientSideToolV2Call proto: { tool, mcp_params, call_id, name, raw_args, tool_index, model_call_id } * Represents a tool call definition */ function encodeClientSideToolV2Call(toolCallId, toolName, selectedTool, serverName, rawArgs, modelCallId, toolIndex = 1) { return concatArrays( encodeField(FIELD.CV2C_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2_MCP), encodeField(FIELD.CV2C_MCP_PARAMS, WIRE_TYPE.LEN, encodeMcpParamsForCall(selectedTool, rawArgs, serverName)), encodeField(FIELD.CV2C_CALL_ID, WIRE_TYPE.LEN, toolCallId), encodeField(FIELD.CV2C_NAME, WIRE_TYPE.LEN, toolName), encodeField(FIELD.CV2C_RAW_ARGS, WIRE_TYPE.LEN, rawArgs), encodeField(FIELD.CV2C_TOOL_INDEX, WIRE_TYPE.VARINT, toolIndex > 0 ? toolIndex : 1), ...(modelCallId ? [encodeField(FIELD.CV2C_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)] : []) ); } /** * Encode ConversationMessage.ToolResult with full structure * Matches Cursor proto: tool_call_id, tool_name, tool_index, raw_args, result, tool_call */ export function encodeToolResult(toolResult) { const originalName = toolResult.tool_name || toolResult.name || ""; const toolName = formatToolName(originalName); const rawArgs = toolResult.raw_args || "{}"; const resultContent = toolResult.result_content || toolResult.result || ""; const { toolCallId, modelCallId } = parseToolId(toolResult.tool_call_id || ""); const toolIndex = toolResult.tool_index || toolResult.index || 1; // Parse tool name to extract server and selected tool const { serverName, selectedTool } = parseToolName(toolName); return concatArrays( encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId), encodeField(FIELD.TOOL_RESULT_NAME, WIRE_TYPE.LEN, toolName), encodeField(FIELD.TOOL_RESULT_INDEX, WIRE_TYPE.VARINT, toolIndex > 0 ? toolIndex : 1), ...(modelCallId ? [encodeField(FIELD.TOOL_RESULT_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)] : []), encodeField(FIELD.TOOL_RESULT_RAW_ARGS, WIRE_TYPE.LEN, rawArgs), encodeField(FIELD.TOOL_RESULT_RESULT, WIRE_TYPE.LEN, encodeClientSideToolV2Result(toolCallId, modelCallId, selectedTool, resultContent, toolIndex) ), encodeField(FIELD.TOOL_RESULT_TOOL_CALL, WIRE_TYPE.LEN, encodeClientSideToolV2Call(toolCallId, toolName, selectedTool, serverName, rawArgs, modelCallId, toolIndex) ) ); } export function encodeMessage(content, role, messageId, chatModeEnum = null, isLast = false, hasTools = false, toolResults = [], serverBubbleId = null) { const hasToolResults = toolResults.length > 0; return concatArrays( encodeField(FIELD.MSG_CONTENT, WIRE_TYPE.LEN, content), encodeField(FIELD.MSG_ROLE, WIRE_TYPE.VARINT, role), encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId), // Only include server_bubble_id if explicitly provided (last assistant message only) ...(serverBubbleId ? [encodeField(FIELD.MSG_SERVER_BUBBLE_ID, WIRE_TYPE.LEN, serverBubbleId)] : []), ...(hasToolResults ? toolResults.map(tr => encodeField(FIELD.MSG_TOOL_RESULTS, WIRE_TYPE.LEN, encodeToolResult(tr)) ) : []), encodeField(FIELD.MSG_IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0), encodeField(FIELD.MSG_UNIFIED_MODE, WIRE_TYPE.VARINT, hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT), ...(isLast && hasTools ? [encodeField(FIELD.MSG_SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))] : []) ); } export function encodeInstruction(text) { return text ? encodeField(FIELD.INSTRUCTION_TEXT, WIRE_TYPE.LEN, text) : new Uint8Array(0); } export function encodeModel(modelName) { return concatArrays( encodeField(FIELD.MODEL_NAME, WIRE_TYPE.LEN, modelName), encodeField(FIELD.MODEL_EMPTY, WIRE_TYPE.LEN, new Uint8Array(0)) ); } export function encodeCursorSetting() { const unknown6 = concatArrays( encodeField(FIELD.SETTING6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)), encodeField(FIELD.SETTING6_FIELD_2, WIRE_TYPE.LEN, new Uint8Array(0)) ); return concatArrays( encodeField(FIELD.SETTING_PATH, WIRE_TYPE.LEN, "cursor\\aisettings"), encodeField(FIELD.SETTING_UNKNOWN_3, WIRE_TYPE.LEN, new Uint8Array(0)), encodeField(FIELD.SETTING_UNKNOWN_6, WIRE_TYPE.LEN, unknown6), encodeField(FIELD.SETTING_UNKNOWN_8, WIRE_TYPE.VARINT, 1), encodeField(FIELD.SETTING_UNKNOWN_9, WIRE_TYPE.VARINT, 1) ); } export function encodeMetadata() { return concatArrays( encodeField(FIELD.META_PLATFORM, WIRE_TYPE.LEN, process.platform || "linux"), encodeField(FIELD.META_ARCH, WIRE_TYPE.LEN, process.arch || "x64"), encodeField(FIELD.META_VERSION, WIRE_TYPE.LEN, process.version || "v20.0.0"), encodeField(FIELD.META_CWD, WIRE_TYPE.LEN, process.cwd?.() || "/"), encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString()) ); } export function encodeMessageId(messageId, role, summaryId = null) { return concatArrays( encodeField(FIELD.MSGID_ID, WIRE_TYPE.LEN, messageId), ...(summaryId ? [encodeField(FIELD.MSGID_SUMMARY, WIRE_TYPE.LEN, summaryId)] : []), encodeField(FIELD.MSGID_ROLE, WIRE_TYPE.VARINT, role) ); } export function encodeMcpTool(tool) { const toolName = tool.function?.name || tool.name || ""; const toolDesc = tool.function?.description || tool.description || ""; const inputSchema = tool.function?.parameters || tool.input_schema || {}; return concatArrays( ...(toolName ? [encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName)] : []), ...(toolDesc ? [encodeField(FIELD.MCP_TOOL_DESC, WIRE_TYPE.LEN, toolDesc)] : []), ...(Object.keys(inputSchema).length > 0 ? [encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, JSON.stringify(inputSchema))] : []), encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, "custom") ); } // ==================== REQUEST BUILDING ==================== export function encodeRequest(messages, modelName, tools = [], reasoningEffort = null) { const hasTools = tools?.length > 0; const isAgentic = hasTools; const formattedMessages = []; const messageIds = []; const normalizedMessages = []; // Guardrail: split mixed assistant payload into separate assistant messages // This prevents protobuf encoding errors when tool calls and results are in same message for (let i = 0; i < messages.length; i++) { const msg = messages[i]; const hasToolCalls = Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0; const hasToolResults = Array.isArray(msg?.tool_results) && msg.tool_results.length > 0; if (msg?.role === "assistant" && hasToolCalls && hasToolResults) { log( "ENCODE", `normalizing mixed assistant tool payload at msg[${i}] (calls=${msg.tool_calls.length}, results=${msg.tool_results.length})` ); // Keep assistant tool call message without embedded results normalizedMessages.push({ ...msg, tool_results: [] }); // Avoid inserting duplicate assistant tool-result message if next one already matches const nextMsg = messages[i + 1]; const nextHasToolResults = nextMsg?.role === "assistant" && Array.isArray(nextMsg?.tool_results) && nextMsg.tool_results.length > 0; const currentIds = new Set( msg.tool_results.map(tr => tr?.tool_call_id).filter(id => typeof id === "string") ); const nextIds = new Set( (nextMsg?.tool_results || []) .map(tr => tr?.tool_call_id) .filter(id => typeof id === "string") ); let sameIds = currentIds.size > 0 && currentIds.size === nextIds.size; if (sameIds) { for (const id of currentIds) { if (!nextIds.has(id)) { sameIds = false; break; } } } if (!(nextHasToolResults && sameIds)) { normalizedMessages.push({ role: "assistant", content: "", tool_results: msg.tool_results }); } continue; } normalizedMessages.push(msg); } // Prepare messages for (let i = 0; i < normalizedMessages.length; i++) { const msg = normalizedMessages[i]; const role = msg.role === "user" ? ROLE.USER : ROLE.ASSISTANT; const msgId = uuidv4(); const isLast = i === normalizedMessages.length - 1; formattedMessages.push({ content: msg.content, role, messageId: msgId, isLast, hasTools, toolResults: msg.tool_results || [] }); messageIds.push({ messageId: msgId, role }); } // Map reasoning effort to thinking level let thinkingLevel = THINKING_LEVEL.UNSPECIFIED; if (reasoningEffort === "medium") thinkingLevel = THINKING_LEVEL.MEDIUM; else if (reasoningEffort === "high") thinkingLevel = THINKING_LEVEL.HIGH; // Build request return concatArrays( // Messages ...formattedMessages.map(fm => encodeField(FIELD.MESSAGES, WIRE_TYPE.LEN, encodeMessage(fm.content, fm.role, fm.messageId, null, fm.isLast, fm.hasTools, fm.toolResults) ) ), // Static fields encodeField(FIELD.UNKNOWN_2, WIRE_TYPE.VARINT, 1), encodeField(FIELD.INSTRUCTION, WIRE_TYPE.LEN, encodeInstruction("")), encodeField(FIELD.UNKNOWN_4, WIRE_TYPE.VARINT, 1), encodeField(FIELD.MODEL, WIRE_TYPE.LEN, encodeModel(modelName)), encodeField(FIELD.WEB_TOOL, WIRE_TYPE.LEN, ""), encodeField(FIELD.UNKNOWN_13, WIRE_TYPE.VARINT, 1), encodeField(FIELD.CURSOR_SETTING, WIRE_TYPE.LEN, encodeCursorSetting()), encodeField(FIELD.UNKNOWN_19, WIRE_TYPE.VARINT, 1), encodeField(FIELD.CONVERSATION_ID, WIRE_TYPE.LEN, uuidv4()), encodeField(FIELD.METADATA, WIRE_TYPE.LEN, encodeMetadata()), // Tool-related fields encodeField(FIELD.IS_AGENTIC, WIRE_TYPE.VARINT, isAgentic ? 1 : 0), ...(isAgentic ? [encodeField(FIELD.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))] : []), // Message IDs ...messageIds.map(mid => encodeField(FIELD.MESSAGE_IDS, WIRE_TYPE.LEN, encodeMessageId(mid.messageId, mid.role)) ), // MCP Tools ...(tools?.length > 0 ? tools.map(tool => encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool)) ) : []), // Mode fields encodeField(FIELD.LARGE_CONTEXT, WIRE_TYPE.VARINT, 0), encodeField(FIELD.UNKNOWN_38, WIRE_TYPE.VARINT, 0), encodeField(FIELD.UNIFIED_MODE, WIRE_TYPE.VARINT, isAgentic ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT), encodeField(FIELD.UNKNOWN_47, WIRE_TYPE.LEN, ""), encodeField(FIELD.SHOULD_DISABLE_TOOLS, WIRE_TYPE.VARINT, isAgentic ? 0 : 1), encodeField(FIELD.THINKING_LEVEL, WIRE_TYPE.VARINT, thinkingLevel), encodeField(FIELD.UNKNOWN_51, WIRE_TYPE.VARINT, 0), encodeField(FIELD.UNKNOWN_53, WIRE_TYPE.VARINT, 1), encodeField(FIELD.UNIFIED_MODE_NAME, WIRE_TYPE.LEN, isAgentic ? "Agent" : "Ask") ); } export function buildChatRequest(messages, modelName, tools = [], reasoningEffort = null) { return encodeField(FIELD.REQUEST, WIRE_TYPE.LEN, encodeRequest(messages, modelName, tools, reasoningEffort)); } /** * Encode a tool result as ClientSideToolV2Result (field 2 of StreamUnifiedChatRequestWithTools) * This is sent as a SEPARATE request frame, not inside conversation messages. * Proto: StreamUnifiedChatRequestWithTools.client_side_tool_v2_result = 2 */ export function buildToolResultRequest(toolResult) { const { toolCallId, modelCallId } = parseToolId(toolResult.tool_call_id || ""); const rawName = toolResult.tool_name || ""; const resultContent = toolResult.result_content || ""; // selected_tool = raw tool name (e.g. "Write", "Read") per cursor-api Rust source: // McpResult { selected_tool: tool_name, result } where tool_name is the mcpParams.tools[0].name // which is the name AFTER server prefix stripping (e.g. "custom_Write" -> name = "Write") // Actually cursor-api uses: name = tool_name.slice_unchecked(d+1..) → raw name without "custom_" // So selected_tool = raw tool name without any prefix const selectedTool = rawName.startsWith("mcp_custom_") ? rawName.slice("mcp_custom_".length) : rawName.startsWith("mcp_") ? rawName.slice(4) : rawName; // ClientSideToolV2Result per proto: // field 1 (tool): varint = 19 (MCP) // field 28 (mcp_result): LEN { field 1: selected_tool, field 2: result } // field 35 (tool_call_id): string // field 48 (model_call_id): string (optional) // NO tool_index (None in Rust source: encode_tool_result sets tool_index: None) const cv2Result = concatArrays( encodeField(FIELD.CV2R_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2_MCP), encodeField(FIELD.CV2R_MCP_RESULT, WIRE_TYPE.LEN, encodeMcpResult(selectedTool, resultContent)), encodeField(FIELD.CV2R_CALL_ID, WIRE_TYPE.LEN, toolCallId), ...(modelCallId ? [encodeField(FIELD.CV2R_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)] : []) // tool_index intentionally omitted (None per Rust source) ); // StreamUnifiedChatRequestWithTools: field 2 = client_side_tool_v2_result return encodeField(2, WIRE_TYPE.LEN, cv2Result); } export function wrapConnectRPCFrame(payload, compress = false) { let finalPayload = payload; let flags = 0x00; if (compress) { finalPayload = new Uint8Array(zlib.gzipSync(Buffer.from(payload))); flags = 0x01; } const frame = new Uint8Array(5 + finalPayload.length); frame[0] = flags; frame[1] = (finalPayload.length >> 24) & 0xFF; frame[2] = (finalPayload.length >> 16) & 0xFF; frame[3] = (finalPayload.length >> 8) & 0xFF; frame[4] = finalPayload.length & 0xFF; frame.set(finalPayload, 5); return frame; } export function generateCursorBody(messages, modelName, tools = [], reasoningEffort = null) { log("BODY", `Generating: ${messages.length} msgs, model=${modelName}, tools=${tools.length}, reasoning=${reasoningEffort || "none"}`); const protobuf = buildChatRequest(messages, modelName, tools, reasoningEffort); const framed = wrapConnectRPCFrame(protobuf, false); // Cursor doesn't support compressed requests log("BODY", `Protobuf=${protobuf.length}B, Framed=${framed.length}B`); return framed; } /** * Generate a framed tool result body to send as a separate request frame. * Uses field 2 (client_side_tool_v2_result) of StreamUnifiedChatRequestWithTools. */ export function generateToolResultBody(toolResult) { const protobuf = buildToolResultRequest(toolResult); return wrapConnectRPCFrame(protobuf, false); } // ==================== PRIMITIVE DECODING ==================== export function decodeVarint(buffer, offset) { let result = 0; let shift = 0; let pos = offset; while (pos < buffer.length) { const b = buffer[pos]; result |= (b & 0x7F) << shift; pos++; if (!(b & 0x80)) break; shift += 7; } return [result, pos]; } export function decodeField(buffer, offset) { if (offset >= buffer.length) return [null, null, null, offset]; const [tag, pos1] = decodeVarint(buffer, offset); const fieldNum = tag >> 3; const wireType = tag & 0x07; let value; let pos = pos1; if (wireType === WIRE_TYPE.VARINT) { [value, pos] = decodeVarint(buffer, pos); } else if (wireType === WIRE_TYPE.LEN) { const [length, pos2] = decodeVarint(buffer, pos); value = buffer.slice(pos2, pos2 + length); pos = pos2 + length; } else if (wireType === WIRE_TYPE.FIXED64) { value = buffer.slice(pos, pos + 8); pos += 8; } else if (wireType === WIRE_TYPE.FIXED32) { value = buffer.slice(pos, pos + 4); pos += 4; } else { value = null; } return [fieldNum, wireType, value, pos]; } export function decodeMessage(data) { const fields = new Map(); let pos = 0; while (pos < data.length) { const [fieldNum, wireType, value, newPos] = decodeField(data, pos); if (fieldNum === null) break; if (!fields.has(fieldNum)) fields.set(fieldNum, []); fields.get(fieldNum).push({ wireType, value }); pos = newPos; } return fields; } // ==================== RESPONSE PARSING ==================== export function parseConnectRPCFrame(buffer) { if (buffer.length < 5) return null; const flags = buffer[0]; const length = (buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4]; if (buffer.length < 5 + length) return null; let payload = buffer.slice(5, 5 + length); // Decompress if gzip if (flags === 0x01) { try { payload = new Uint8Array(zlib.gunzipSync(Buffer.from(payload))); } catch (err) { log("PARSE", `Decompression failed: ${err.message}`); } } return { flags, length, payload, consumed: 5 + length }; } function extractToolCall(toolCallData) { const toolCall = decodeMessage(toolCallData); let toolCallId = ""; let toolName = ""; let rawArgs = ""; let isLast = false; // Extract tool call ID if (toolCall.has(FIELD.TOOL_ID)) { const fullId = new TextDecoder().decode(toolCall.get(FIELD.TOOL_ID)[0].value); toolCallId = fullId.split("\n")[0]; // Cursor returns multi-line ID, take first line } // Extract tool name if (toolCall.has(FIELD.TOOL_NAME)) { toolName = new TextDecoder().decode(toolCall.get(FIELD.TOOL_NAME)[0].value); } // Extract is_last flag if (toolCall.has(FIELD.TOOL_IS_LAST)) { isLast = toolCall.get(FIELD.TOOL_IS_LAST)[0].value !== 0; } // Extract MCP params - nested real tool info if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) { try { const mcpParams = decodeMessage(toolCall.get(FIELD.TOOL_MCP_PARAMS)[0].value); if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) { const tool = decodeMessage(mcpParams.get(FIELD.MCP_TOOLS_LIST)[0].value); if (tool.has(FIELD.MCP_NESTED_NAME)) { toolName = new TextDecoder().decode(tool.get(FIELD.MCP_NESTED_NAME)[0].value); } if (tool.has(FIELD.MCP_NESTED_PARAMS)) { rawArgs = new TextDecoder().decode(tool.get(FIELD.MCP_NESTED_PARAMS)[0].value); } } } catch (err) { log("EXTRACT", `MCP parse error: ${err.message}`); } } // Fallback to raw_args if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) { rawArgs = new TextDecoder().decode(toolCall.get(FIELD.TOOL_RAW_ARGS)[0].value); } if (toolCallId && toolName) { return { id: toolCallId, type: "function", function: { name: toolName, arguments: rawArgs || "{}" }, isLast }; } return null; } function extractTextAndThinking(responseData) { const nested = decodeMessage(responseData); let text = null; let thinking = null; // Extract text if (nested.has(FIELD.RESPONSE_TEXT)) { text = new TextDecoder().decode(nested.get(FIELD.RESPONSE_TEXT)[0].value); } // Extract thinking if (nested.has(FIELD.THINKING)) { try { const thinkingMsg = decodeMessage(nested.get(FIELD.THINKING)[0].value); if (thinkingMsg.has(FIELD.THINKING_TEXT)) { thinking = new TextDecoder().decode(thinkingMsg.get(FIELD.THINKING_TEXT)[0].value); } } catch (err) { log("EXTRACT", `Thinking parse error: ${err.message}`); } } return { text, thinking }; } export function extractTextFromResponse(payload) { try { const fields = decodeMessage(payload); // Warn about unknown field numbers — may indicate a Cursor protocol update for (const fieldNum of fields.keys()) { if (!KNOWN_RESPONSE_FIELDS.has(fieldNum)) { log( "SCHEMA", `Unknown response field #${fieldNum} detected. Schema v${PROTOBUF_SCHEMA_VERSION} may be outdated.` ); } } // Field 1: ClientSideToolV2Call if (fields.has(FIELD.TOOL_CALL)) { const toolCall = extractToolCall(fields.get(FIELD.TOOL_CALL)[0].value); if (toolCall) { log("EXTRACT", `Tool call: ${toolCall.function.name}`); return { text: null, error: null, toolCall, thinking: null }; } } // Field 2: StreamUnifiedChatResponse if (fields.has(FIELD.RESPONSE)) { const { text, thinking } = extractTextAndThinking(fields.get(FIELD.RESPONSE)[0].value); if (text || thinking) { return { text, error: null, toolCall: null, thinking }; } } return { text: null, error: null, toolCall: null, thinking: null }; } catch (err) { log("EXTRACT", `Decode failed (schema v${PROTOBUF_SCHEMA_VERSION}): ${err.message}`); return { text: null, error: null, toolCall: null, thinking: null, raw: Buffer.from(payload).toString("base64"), decodeError: err.message }; } } // ==================== EXPORTS ==================== export default { encodeVarint, encodeField, encodeMessage, buildChatRequest, wrapConnectRPCFrame, generateCursorBody, decodeVarint, decodeField, decodeMessage, parseConnectRPCFrame, extractTextFromResponse };