Skip to content

Commit cf2cfdc

Browse files
committed
feat: improve SSE client reliability and performance
- Add enhanced SSE client wrapper with immediate EOF detection - Implement adaptive health checks and connection monitoring - Add debounced config writes with content hash detection - Reduce log verbosity for Fx events
1 parent 97d3ac3 commit cf2cfdc

File tree

7 files changed

+942
-180
lines changed

7 files changed

+942
-180
lines changed

internal/cursor/manager.go

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os/user"
99
"path/filepath"
1010
"sync"
11+
"time"
1112

1213
"github.com/co-browser/agent-browser/internal/events"
1314
"github.com/co-browser/agent-browser/internal/log"
@@ -18,6 +19,7 @@ const (
1819
agentBrowserURL = "http://localhost:8087/sse" // Default local agent MCP URL
1920
cursorConfigDir = ".cursor"
2021
cursorMCPFile = "mcp.json"
22+
debounceDelay = 2 * time.Second // Debounce delay for config rewrites
2123
)
2224

2325
// ServerEntry defines the structure for server entries within mcp.json
@@ -32,9 +34,12 @@ type MCPServersConfig struct {
3234

3335
// CursorConfigManager handles reading and writing the Cursor mcp.json file.
3436
type CursorConfigManager struct {
35-
logger log.Logger
36-
filePath string
37-
mu sync.Mutex // Mutex to protect file writes
37+
logger log.Logger
38+
filePath string
39+
mu sync.Mutex // Mutex to protect file writes
40+
debounceTimer *time.Timer
41+
pendingWrite bool
42+
configHash string // Used to detect actual changes
3843
}
3944

4045
// NewCursorConfigManager creates a new manager instance.
@@ -52,11 +57,22 @@ func NewCursorConfigManager(logger log.Logger) (*CursorConfigManager, error) {
5257
logger.Info().Str("path", filePath).Msg("Cursor MCP config path determined")
5358

5459
return &CursorConfigManager{
55-
logger: logger,
56-
filePath: filePath,
60+
logger: logger,
61+
filePath: filePath,
62+
pendingWrite: false,
5763
}, nil
5864
}
5965

66+
// computeConfigHash generates a simple hash of the config to detect changes
67+
func (m *CursorConfigManager) computeConfigHash(config *MCPServersConfig) string {
68+
data, err := json.Marshal(config)
69+
if err != nil {
70+
// If we can't hash it, return a random value to force write
71+
return fmt.Sprintf("error-%d", time.Now().UnixNano())
72+
}
73+
return fmt.Sprintf("%x", data)
74+
}
75+
6076
// readConfig reads and parses the mcp.json file.
6177
// Returns an empty config if the file doesn't exist.
6278
func (m *CursorConfigManager) readConfig() (*MCPServersConfig, error) {
@@ -93,9 +109,83 @@ func (m *CursorConfigManager) readConfig() (*MCPServersConfig, error) {
93109
return config, nil
94110
}
95111

112+
// scheduleConfigWrite schedules a config write with debouncing
113+
func (m *CursorConfigManager) scheduleConfigWrite(config *MCPServersConfig, forceWrite bool) {
114+
m.mu.Lock()
115+
defer m.mu.Unlock()
116+
117+
// Compute hash to check for changes
118+
newHash := m.computeConfigHash(config)
119+
120+
// Skip if content is unchanged and not forcing write
121+
if !forceWrite && newHash == m.configHash && m.configHash != "" {
122+
m.logger.Debug().Msg("Config unchanged and no force flag set, skipping write")
123+
return
124+
}
125+
126+
// Store updated hash
127+
m.configHash = newHash
128+
129+
// If a timer is already running, stop it
130+
if m.debounceTimer != nil {
131+
m.debounceTimer.Stop()
132+
}
133+
134+
// Store config for later write
135+
finalConfig := *config // Make a copy
136+
137+
// Set pending flag
138+
if !m.pendingWrite {
139+
m.pendingWrite = true
140+
if forceWrite {
141+
m.logger.Debug().Dur("delay", debounceDelay).Msg("Scheduling forced config write (debounced)")
142+
} else {
143+
m.logger.Debug().Dur("delay", debounceDelay).Msg("Scheduling config write (debounced)")
144+
}
145+
}
146+
147+
// Create new timer
148+
m.debounceTimer = time.AfterFunc(debounceDelay, func() {
149+
m.doConfigWrite(&finalConfig)
150+
})
151+
}
152+
153+
// doConfigWrite performs the actual file write operation
154+
func (m *CursorConfigManager) doConfigWrite(config *MCPServersConfig) {
155+
m.mu.Lock()
156+
m.pendingWrite = false
157+
m.mu.Unlock()
158+
159+
m.logger.Info().Msg("Writing Cursor mcp.json after debounce delay")
160+
161+
// Ensure directory exists
162+
dir := filepath.Dir(m.filePath)
163+
if err := os.MkdirAll(dir, 0750); err != nil {
164+
m.logger.Error().Err(err).Str("dir", dir).Msg("Failed to create directory for mcp.json")
165+
return
166+
}
167+
168+
// Marshal with indentation for readability
169+
data, err := json.MarshalIndent(config, "", " ")
170+
if err != nil {
171+
m.logger.Error().Err(err).Msg("Failed to marshal mcp.json data")
172+
return
173+
}
174+
175+
// Write file
176+
err = os.WriteFile(m.filePath, data, 0640)
177+
if err != nil {
178+
m.logger.Error().Err(err).Str("path", m.filePath).Msg("Failed to write mcp.json file")
179+
return
180+
}
181+
182+
m.logger.Info().Msg("Successfully wrote Cursor mcp.json file")
183+
}
184+
96185
// writeConfig writes the configuration back to mcp.json.
97186
// It ensures the directory exists.
98187
func (m *CursorConfigManager) writeConfig(config *MCPServersConfig) error {
188+
// Only used for immediate writes during startup
99189
m.mu.Lock()
100190
defer m.mu.Unlock()
101191

@@ -117,6 +207,9 @@ func (m *CursorConfigManager) writeConfig(config *MCPServersConfig) error {
117207
return fmt.Errorf("failed to write %s: %w", m.filePath, err)
118208
}
119209

210+
// Store hash of written config
211+
m.configHash = m.computeConfigHash(config)
212+
120213
return nil
121214
}
122215

@@ -143,43 +236,51 @@ func (m *CursorConfigManager) EnsureAgentEntry() error {
143236
needsWrite = true
144237
} else {
145238
m.logger.Debug().Str("name", agentBrowserEntryName).Msg("Agent Browser entry already exists and is correct.")
146-
// We still rewrite the file to potentially trigger Cursor's refresh mechanism
239+
// IMPORTANT: Force rewrite on startup even if content is unchanged
240+
// This ensures Cursor activates the connection
147241
needsWrite = true
148242
}
149243

150244
if needsWrite {
151-
m.logger.Info().Msg("Rewriting Cursor mcp.json file...")
245+
// For initial setup, we use immediate write to ensure it's ready ASAP
246+
m.logger.Info().Msg("Writing Cursor mcp.json file for startup activation...")
152247
err = m.writeConfig(config)
153248
if err != nil {
154249
return fmt.Errorf("failed to write cursor config after ensuring entry: %w", err)
155250
}
156-
m.logger.Info().Msg("Successfully rewrote Cursor mcp.json file.")
157-
} else {
158-
m.logger.Info().Msg("No changes needed, but still rewriting Cursor mcp.json file to trigger refresh.")
159-
err = m.writeConfig(config)
160-
if err != nil {
161-
return fmt.Errorf("failed to rewrite cursor config: %w", err)
162-
}
163-
m.logger.Info().Msg("Successfully rewrote Cursor mcp.json file.")
251+
m.logger.Info().Msg("Successfully wrote Cursor mcp.json file.")
164252
}
165253
return nil
166254
}
167255

168256
// HandleLocalToolsRefreshed handles the event indicating local tools may have changed.
169-
// It triggers a rewrite of the mcp.json file.
257+
// It triggers a debounced rewrite of the mcp.json file.
170258
func (m *CursorConfigManager) HandleLocalToolsRefreshed(event events.Event) {
171259
_, ok := event.(*events.LocalToolsRefreshedEvent)
172260
if !ok {
173261
m.logger.Error().Str("eventType", string(event.Type())).Msg("Received event of unexpected type in HandleLocalToolsRefreshed")
174262
return
175263
}
176264

177-
m.logger.Info().Msg("Local tools refreshed event received, triggering Cursor mcp.json rewrite.")
265+
m.logger.Info().Msg("Local tools refreshed event received, scheduling Cursor mcp.json rewrite.")
178266

179-
// Reuse EnsureAgentEntry which handles read/check/write logic
180-
if err := m.EnsureAgentEntry(); err != nil {
181-
m.logger.Error().Err(err).Msg("Error rewriting mcp.json on LocalToolsRefreshed event")
267+
// Read current config
268+
config, err := m.readConfig()
269+
if err != nil {
270+
m.logger.Error().Err(err).Msg("Failed to read mcp.json during tools refresh")
271+
return
272+
}
273+
274+
// Ensure Agent Browser entry is present
275+
entry, exists := config.MCPServers[agentBrowserEntryName]
276+
if !exists || entry.URL != agentBrowserURL {
277+
m.logger.Info().Msg("Ensuring Agent Browser entry during tools refresh")
278+
config.MCPServers[agentBrowserEntryName] = ServerEntry{URL: agentBrowserURL}
182279
}
280+
281+
// Schedule debounced write with force flag because tools have changed
282+
// This ensures Cursor picks up tool changes even if mcp.json content hasn't changed
283+
m.scheduleConfigWrite(config, true)
183284
}
184285

185286
// RegisterEventHandlers subscribes the manager to relevant events.

0 commit comments

Comments
 (0)