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
9 changes: 8 additions & 1 deletion server/cmd/api/api/webmcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,14 @@ func (s *ApiService) InvokeWebMCPTool(ctx context.Context, request oapi.InvokeWe
Message: "the invocation started, but its final outcome could not be observed; do not retry automatically",
}
failure.InvocationId = nonEmptyString(result.InvocationID)
return oapi.InvokeWebMCPTool504JSONResponse(failure), nil
return oapi.InvokeWebMCPTool504JSONResponse{Body: failure, Headers: oapi.InvokeWebMCPTool504ResponseHeaders{XShouldRetry: "false"}}, nil
case errors.Is(err, webmcpclient.ErrToolPageUnloaded):
failure := oapi.WebMCPInvocationFailure{
Code: oapi.ToolPageUnloaded,
Message: "the tool's page was unloaded before the tool responded; it may have had side effects, so do not retry automatically",
}
failure.InvocationId = nonEmptyString(result.InvocationID)
return oapi.InvokeWebMCPTool410JSONResponse{Body: failure, Headers: oapi.InvokeWebMCPTool410ResponseHeaders{XShouldRetry: "false"}}, nil
default:
logger.FromContext(ctx).Error("failed to invoke WebMCP tool", "err", err)
return oapi.InvokeWebMCPTool500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to invoke WebMCP tool"}}, nil
Expand Down
30 changes: 29 additions & 1 deletion server/cmd/api/api/webmcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -254,11 +256,37 @@ func TestInvokeWebMCPToolReportsUnknownOutcome(t *testing.T) {
Body: &oapi.WebMCPInvokeRequest{ToolRef: "wmcp_test", Input: map[string]any{}},
})
require.NoError(t, err)
body := response.(oapi.InvokeWebMCPTool504JSONResponse)
recorder := httptest.NewRecorder()
require.NoError(t, response.VisitInvokeWebMCPToolResponse(recorder))
require.Equal(t, http.StatusGatewayTimeout, recorder.Code)
require.Equal(t, "false", recorder.Header().Get("X-Should-Retry"))
var body oapi.WebMCPInvocationFailure
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &body))
require.Equal(t, oapi.OutcomeUnknown, body.Code)
require.Equal(t, "invocation-1", *body.InvocationId)
}

func TestInvokeWebMCPToolReportsUnloadedToolPage(t *testing.T) {
client := &fakeWebMCPClient{
result: webmcpclient.InvocationResult{InvocationID: "invocation-1"},
invokeErr: webmcpclient.ErrToolPageUnloaded,
}
service := &ApiService{webmcp: client}

response, err := service.InvokeWebMCPTool(context.Background(), oapi.InvokeWebMCPToolRequestObject{
Body: &oapi.WebMCPInvokeRequest{ToolRef: "wmcp_test", Input: map[string]any{}},
})
require.NoError(t, err)
recorder := httptest.NewRecorder()
require.NoError(t, response.VisitInvokeWebMCPToolResponse(recorder))
require.Equal(t, http.StatusGone, recorder.Code)
require.Equal(t, "false", recorder.Header().Get("X-Should-Retry"))
var body oapi.WebMCPInvocationFailure
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &body))
require.Equal(t, oapi.ToolPageUnloaded, body.Code)
require.Equal(t, "invocation-1", *body.InvocationId)
}

func TestGetWebMCPToolsReturnsNotFoundWithoutPage(t *testing.T) {
service := &ApiService{webmcp: &fakeWebMCPClient{toolsErr: webmcpclient.ErrNoPageTarget}}
response, err := service.GetWebMCPTools(context.Background(), oapi.GetWebMCPToolsRequestObject{})
Expand Down
2 changes: 1 addition & 1 deletion server/docs/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ The reference below is generated from `runtime/browser-repl-help.ts`; edit that
### WebMCP methods

- **`webmcp.listTools(options?)`** — Return tools registered across every open tab and embedded frame. Each result contains `tool_ref`, MCP-compatible `tool` metadata, and source window/tab/frame metadata. Set `options.excludeCustom` to omit custom tools.
- **`webmcp.invokeTool(toolRef, input?, options?)`** — Invoke one exact WebMCP registration without changing the attached target. `options.timeoutSec` bounds the request. Results have `invocation_id`, status, and optional output or error text. Do not automatically retry an `outcome_unknown` failure.
- **`webmcp.invokeTool(toolRef, input?, options?)`** — Invoke one exact WebMCP registration without changing the attached target. `options.timeoutSec` bounds the request. Results have `invocation_id`, status, and optional output or error text. Do not automatically retry an `outcome_unknown` or `tool_page_unloaded` failure.
- **`webmcp.addCustomTools({ namespace, tools, forceOverwriteNamespace? })`** — Atomically add a non-empty batch of custom tools. Every definition requires `kind`, `match.url_patterns`, tool metadata, and an `execute` function; `outputSchema` is optional. Set `forceOverwriteNamespace` to replace every existing tool in that namespace. Returns the added tools with generated IDs.
- **`webmcp.listCustomTools()`** — Return serializable summaries of every custom tool, including its generated ID and namespace.
- **`webmcp.removeCustomTool(id)`** — Remove one custom tool by generated ID and return whether it existed. Active invocations continue.
Expand Down
Loading
Loading