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.
3436type 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.
6278func (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.
98187func (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.
170258func (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