diff --git a/conversation.go b/conversation.go index 4cfab0c..ea581d3 100644 --- a/conversation.go +++ b/conversation.go @@ -329,7 +329,7 @@ func buildRun(conv chatConversation, tools []chatTool, model modelResolution, id mid := randomID() user := protobuf(nil).text(1, conv.Last).text(2, mid).uint(4, 1) turn := protobuf(nil).bytes(1, protobuf(nil).bytes(1, put(user)).text(10, mid)) - state = state.bytes(8, put(turn)).uint(10, 1).text(22, "cli") + state = state.bytes(8, put(turn)).uint(10, 1).text(22, agentClientType) rc := protobuf(nil).bytes(4, cursorEnvironment()) var mcp protobuf for _, t := range tools { diff --git a/host_http.go b/host_http.go index dfb707c..a0fbcc3 100644 --- a/host_http.go +++ b/host_http.go @@ -208,6 +208,9 @@ func (b *hostBody) Close() error { return nil } func (s *requestScope) json(ctx context.Context, method, path, token string, body any) (map[string]any, error) { + return s.jsonClient(ctx, method, path, token, body, agentClientType) +} +func (s *requestScope) jsonClient(ctx context.Context, method, path, token string, body any, clientType string) (map[string]any, error) { url := path if len(path) > 0 && path[0] == '/' { url = cursorAPI + path @@ -216,7 +219,7 @@ func (s *requestScope) json(ctx context.Context, method, path, token string, bod if body != nil { payload = encodeJSON(body) } - h := headers(token) + h := headersWithType(token, clientType) if strings.HasPrefix(path, "/auth/") || strings.HasPrefix(path, "/oauth/") { h.Del("Connect-Protocol-Version") } diff --git a/identity_test.go b/identity_test.go new file mode 100644 index 0000000..35400fe --- /dev/null +++ b/identity_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "bytes" + "testing" +) + +func TestClientIdentitySplit(t *testing.T) { + if got := headers("").Get("X-Cursor-Client-Type"); got != "cli" { + t.Fatalf("agent header client type = %q", got) + } + if agentClientType != "cli" { + t.Fatalf("agent client type = %q", agentClientType) + } + if got := headersWithType("token", catalogClientType).Get("X-Cursor-Client-Type"); got != "desktop" { + t.Fatalf("catalog header client type = %q", got) + } + if headersWithType("token", catalogClientType).Get("Authorization") != "Bearer token" { + t.Fatal("catalog header dropped the bearer token") + } + run, _ := buildRun(chatConversation{Last: "ping"}, nil, modelResolution{ID: "grok"}, "conv") + if bytes.Contains(run, []byte("desktop")) { + t.Fatal("agent run payload advertises desktop") + } + if !bytes.Contains(run, []byte(agentClientType)) { + t.Fatal("agent run payload is missing the cli identity") + } +} diff --git a/models.go b/models.go index 6e8343d..0af5cd0 100644 --- a/models.go +++ b/models.go @@ -277,10 +277,10 @@ func (p *pluginRuntime) catalog(s *requestScope, token string) ([]modelEntry, er var usableErr error ready := make(chan struct{}) go func() { - usable, usableErr = s.json(s.ctx, http.MethodPost, "/agent.v1.AgentService/GetUsableModels", token, map[string]any{}) + usable, usableErr = s.jsonClient(s.ctx, http.MethodPost, "/agent.v1.AgentService/GetUsableModels", token, map[string]any{}, catalogClientType) close(ready) }() - picker, err := s.json(s.ctx, http.MethodPost, "/aiserver.v1.AiService/AvailableModels", token, map[string]any{"useModelParameters": true, "doNotUseMarkdown": true}) + picker, err := s.jsonClient(s.ctx, http.MethodPost, "/aiserver.v1.AiService/AvailableModels", token, map[string]any{"useModelParameters": true, "doNotUseMarkdown": true}, catalogClientType) <-ready if err != nil { return nil, err diff --git a/runtime.go b/runtime.go index 394826c..be94754 100644 --- a/runtime.go +++ b/runtime.go @@ -22,6 +22,12 @@ const cursorAPI = "https://api2.cursor.sh" const clientVersion = "cli-2026.09.23-86fc751" const maxWireBytes = 64 << 20 +// Some Cursor team accounts only list models such as Claude Opus when the +// catalog client type is desktop. Agent runs stay cli so Cursor keeps the +// MCP tool protocol this plugin translates. +const agentClientType = "cli" +const catalogClientType = "desktop" + type hostCaller func(string, []byte) ([]byte, error) type rpcError struct { Code string `json:"code"` @@ -276,7 +282,10 @@ func jwtExpiry(token string) int64 { return int64(num(m["exp"]) * 1000) } func headers(token string) http.Header { - h := http.Header{"Content-Type": {"application/json"}, "Connect-Protocol-Version": {"1"}, "X-Cursor-Client-Version": {clientVersion}, "X-Cursor-Client-Type": {"cli"}, "X-Ghost-Mode": {"true"}} + return headersWithType(token, agentClientType) +} +func headersWithType(token, clientType string) http.Header { + h := http.Header{"Content-Type": {"application/json"}, "Connect-Protocol-Version": {"1"}, "X-Cursor-Client-Version": {clientVersion}, "X-Cursor-Client-Type": {clientType}, "X-Ghost-Mode": {"true"}} if token != "" { h.Set("Authorization", "Bearer "+token) }