Skip to content

Commit eb6c195

Browse files
committed
Preserve tools when navigation does not commit
1 parent 7d63a19 commit eb6c195

2 files changed

Lines changed: 64 additions & 31 deletions

File tree

server/lib/webmcpclient/client.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ func (c *connection) eventLoop(events <-chan browsersurface.Event) {
117117
c.stateMu.Lock()
118118
c.markDocumentChangedLocked(documentKey{sessionID: event.SessionID, frameID: event.FrameID})
119119
c.abandonFrameInvocationsLocked(event.SessionID, event.FrameID)
120-
c.removeFrameToolsLocked(event.SessionID, event.FrameID)
121120
c.stateMu.Unlock()
122121
c.signalStateChanged()
123122
case browsersurface.EventDocumentChanged:
@@ -397,7 +396,6 @@ func (c *connection) invoke(ctx context.Context, toolRef string, input map[strin
397396
}, nil
398397
}
399398
if _, abandoned := c.abandonedInvocations[key]; abandoned {
400-
delete(c.abandonedInvocations, key)
401399
c.stateMu.Unlock()
402400
return InvocationResult{InvocationID: started.InvocationID}, ErrOutcomeUnknown
403401
}

server/lib/webmcpclient/client_test.go

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,20 @@ type fakeCDP struct {
5050
server *httptest.Server
5151
url string
5252

53-
mu sync.Mutex
54-
connections int
55-
relatedAttaches int
56-
enabledSessions map[string]int
57-
omitResponse bool
58-
closeOnInvoke bool
59-
detachAfterResponse bool
60-
navigateBeforeResult bool
61-
invokeResponseDelay time.Duration
62-
toolCount int
63-
popupOpen bool
64-
write func(any)
53+
mu sync.Mutex
54+
connections int
55+
relatedAttaches int
56+
invocationCount int
57+
enabledSessions map[string]int
58+
omitResponse bool
59+
closeOnInvoke bool
60+
detachAfterResponse bool
61+
navigateBeforeResult bool
62+
navigationDoesNotCommit bool
63+
invokeResponseDelay time.Duration
64+
toolCount int
65+
popupOpen bool
66+
write func(any)
6567
}
6668

6769
func newFakeCDP(t *testing.T, omitResponse bool) *fakeCDP {
@@ -210,44 +212,56 @@ func (f *fakeCDP) serve(w http.ResponseWriter, r *http.Request) {
210212
"params": map[string]any{"tools": tools},
211213
})
212214
case "WebMCP.invokeTool":
213-
if f.closeOnInvoke {
215+
f.mu.Lock()
216+
f.invocationCount++
217+
invocationID := fmt.Sprintf("invocation-%d", f.invocationCount)
218+
closeOnInvoke := f.closeOnInvoke
219+
responseDelay := f.invokeResponseDelay
220+
omitResponse := f.omitResponse
221+
navigateBeforeResult := f.navigateBeforeResult
222+
navigationDoesNotCommit := f.navigationDoesNotCommit
223+
detachAfterResponse := f.detachAfterResponse
224+
f.mu.Unlock()
225+
if closeOnInvoke {
214226
conn.CloseNow()
215227
return
216228
}
217-
if f.invokeResponseDelay > 0 {
218-
time.Sleep(f.invokeResponseDelay)
229+
if responseDelay > 0 {
230+
time.Sleep(responseDelay)
219231
}
220-
respond(map[string]any{"invocationId": "invocation-1"})
221-
if !f.omitResponse {
222-
if f.navigateBeforeResult {
232+
respond(map[string]any{"invocationId": invocationID})
233+
if !omitResponse {
234+
if navigateBeforeResult {
223235
write(map[string]any{
224236
"method": "Page.frameStartedLoading", "sessionId": request.SessionID,
225237
"params": map[string]any{"frameId": "iframe-frame"},
226238
})
227239
}
228240
output := any(map[string]any{"content": []map[string]any{{"type": "text", "text": request.SessionID}}})
229-
if f.navigateBeforeResult {
241+
if navigateBeforeResult {
230242
output = []any{}
231243
}
232244
write(map[string]any{
233245
"method": "WebMCP.toolResponded", "sessionId": request.SessionID,
234246
"params": map[string]any{
235-
"invocationId": "invocation-1", "status": "Completed", "output": output,
247+
"invocationId": invocationID, "status": "Completed", "output": output,
236248
},
237249
})
238-
if !f.navigateBeforeResult {
250+
if !navigateBeforeResult {
239251
write(map[string]any{
240252
"method": "Page.frameStartedLoading", "sessionId": request.SessionID,
241253
"params": map[string]any{"frameId": "iframe-frame"},
242254
})
243255
}
244-
write(map[string]any{
245-
"method": "Page.frameNavigated", "sessionId": request.SessionID,
246-
"params": map[string]any{"frame": map[string]any{
247-
"id": "iframe-frame", "loaderId": "next-loader", "url": "https://payments.example/success",
248-
}},
249-
})
250-
if f.detachAfterResponse {
256+
if !navigationDoesNotCommit {
257+
write(map[string]any{
258+
"method": "Page.frameNavigated", "sessionId": request.SessionID,
259+
"params": map[string]any{"frame": map[string]any{
260+
"id": "iframe-frame", "loaderId": "next-loader", "url": "https://payments.example/success",
261+
}},
262+
})
263+
}
264+
if detachAfterResponse {
251265
write(map[string]any{
252266
"method": "Target.detachedFromTarget",
253267
"params": map[string]any{"sessionId": request.SessionID},
@@ -420,12 +434,33 @@ func TestInvocationPreservesResponseObservedBeforeFrameNavigation(t *testing.T)
420434
func TestInvocationNavigationBeforeResponseHasUnknownOutcome(t *testing.T) {
421435
fake := newFakeCDP(t, false)
422436
fake.navigateBeforeResult = true
437+
fake.navigationDoesNotCommit = true
423438
manager := NewManager(staticUpstream{url: fake.url})
424439
t.Cleanup(func() { _ = manager.Close() })
440+
toolRef := paymentToolRef(t, manager)
425441

426-
result, err := manager.Invoke(context.Background(), paymentToolRef(t, manager), map[string]any{})
442+
result, err := manager.Invoke(context.Background(), toolRef, map[string]any{})
427443
require.ErrorIs(t, err, ErrOutcomeUnknown)
428444
require.Equal(t, "invocation-1", result.InvocationID)
445+
446+
tools, err := manager.Tools(context.Background())
447+
require.NoError(t, err)
448+
toolStillRegistered := false
449+
for _, tool := range tools {
450+
if tool.Ref == toolRef {
451+
toolStillRegistered = true
452+
break
453+
}
454+
}
455+
require.True(t, toolStillRegistered)
456+
457+
fake.mu.Lock()
458+
fake.navigateBeforeResult = false
459+
fake.navigationDoesNotCommit = false
460+
fake.mu.Unlock()
461+
result, err = manager.Invoke(context.Background(), toolRef, map[string]any{})
462+
require.NoError(t, err)
463+
require.Equal(t, "Completed", result.Status)
429464
}
430465

431466
func TestInvocationReturnsCompletedResponseBeforeTargetDetach(t *testing.T) {

0 commit comments

Comments
 (0)