Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion host_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}
Expand Down
28 changes: 28 additions & 0 deletions identity_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
4 changes: 2 additions & 2 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
}
Expand Down