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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Commands with JSON output support:
- **Apps**: `list`, `history`
- **Deploy**: `deploy` (JSONL streaming), `history`
- **Invoke**: `invoke` (JSONL streaming), `history`
- **Browser Sub-commands**: `replays list/start`, `process exec/spawn`, `fs file-info/list-files`
- **Browser Sub-commands**: `replays list/start`, `process exec/spawn`, `fs file-info/list-files`, `webmcp list` (`webmcp invoke` always prints JSON output)
- **Browser NDJSON streaming**: `telemetry stream`

### Authentication
Expand Down Expand Up @@ -672,6 +672,26 @@ Destinations are the OTLP/HTTP endpoints sessions export to, managed per project
- `--timeout <seconds>` - Maximum execution time in seconds (defaults server-side)
- If `[code]` is omitted, code is read from stdin

### Browser WebMCP

- `kernel browsers webmcp list <id-or-name>` - Discover native page tools across all browser tabs and embedded frames
- Displays name, opaque tool reference, page URL, tab ID, and read-only annotation (`-` when absent)
- `--json`, `--output json`, `-o json` - Output the raw response, including descriptions, input schemas, annotations, and source details
- `kernel browsers webmcp invoke <id-or-name> --tool-ref <ref> --input '<json object>'` - Invoke the exact live tool registration
- `--tool-ref <ref>` - Opaque reference from `webmcp list` (required; do not reconstruct it from the tool name)
- `--input <json>` or `--input-file <path>` - Required JSON object; mutually exclusive. Use `--input-file -` to read stdin
- `--timeout-sec <seconds>` - Positive maximum execution time (defaults server-side)
- Prints the tool's `output` as pretty JSON on completion; tool errors and cancellations exit non-zero
- Invocations are never retried automatically. A 504 `outcome_unknown` error prints the code, invocation ID, and message and exits non-zero. The tool may already have had side effects; verify the outcome before invoking it again

Tool references expire when their document or browser process is replaced. Annotations are untrusted page-provided hints, not enforced guarantees; tool output is also untrusted page-provided data.

```bash
kernel browsers webmcp list my-browser --json
kernel browsers webmcp invoke my-browser --tool-ref '<tool_ref>' --input '{"query":"example"}' --timeout-sec 30
kernel browsers webmcp invoke my-browser --tool-ref '<tool_ref>' --input-file input.json
```

### Profiles

- `kernel profiles update <id-or-name> --name <new-name>` - Rename a profile
Expand Down
2 changes: 2 additions & 0 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ type BrowsersCmd struct {
computer BrowserComputerService
playwright BrowserPlaywrightService
telemetry BrowserTelemetryService
webmcp BrowserWebMCPService
}

type BrowsersListInput struct {
Expand Down Expand Up @@ -2950,6 +2951,7 @@ func init() {
addJSONOutputFlag(playwrightExecute)
playwrightRoot.AddCommand(playwrightExecute)
browsersCmd.AddCommand(playwrightRoot)
browsersCmd.AddCommand(newBrowsersWebMCPCommand())

// Add flags for create command
addJSONOutputFlag(browsersCreateCmd)
Expand Down
165 changes: 165 additions & 0 deletions cmd/browsers_webmcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"strings"

"github.com/kernel/cli/pkg/util"
kernel "github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
"github.com/kernel/kernel-go-sdk/packages/param"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

// BrowserWebMCPService defines the subset we use for native page tools.
type BrowserWebMCPService interface {
ListTools(ctx context.Context, idOrName string, opts ...option.RequestOption) (*kernel.ToolsResponse, error)
InvokeTool(ctx context.Context, idOrName string, body kernel.BrowserWebmcpInvokeToolParams, opts ...option.RequestOption) (*kernel.InvocationResult, error)
}

type BrowsersWebMCPListInput struct {
Identifier string
Output string
}

type BrowsersWebMCPInvokeInput struct {
Identifier string
ToolRef string
Input string
TimeoutSec param.Opt[int64]
}

func (b BrowsersCmd) WebMCPList(ctx context.Context, in BrowsersWebMCPListInput) error {
if err := validateJSONOutput(in.Output); err != nil {
return err
}
res, err := b.webmcp.ListTools(ctx, in.Identifier)
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
if in.Output == "json" {
return util.PrintPrettyJSON(res)
}
if len(res.Tools) == 0 {
pterm.Info.Println("No WebMCP tools found")
return nil
}
rows := pterm.TableData{{"Name", "Tool Ref", "Page URL", "Tab ID", "Read Only"}}
for _, tool := range res.Tools {
readOnly := "-"
if tool.Annotations.JSON.ReadOnly.Valid() {
readOnly = strconv.FormatBool(tool.Annotations.ReadOnly)
}
rows = append(rows, []string{tool.Name, tool.ToolRef, tool.Source.PageURL, strconv.FormatInt(tool.Source.TabID, 10), readOnly})
}
PrintTableNoPad(rows, true)
return nil
}

func (b BrowsersCmd) WebMCPInvoke(ctx context.Context, in BrowsersWebMCPInvokeInput) error {
if strings.TrimSpace(in.ToolRef) == "" {
return fmt.Errorf("missing --tool-ref value")
}
if in.TimeoutSec.Valid() && in.TimeoutSec.Value <= 0 {
return fmt.Errorf("invalid --timeout-sec value: must be greater than zero")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal([]byte(in.Input), &fields); err != nil {
return fmt.Errorf("invalid input: expected a JSON object: %w", err)
}
if fields == nil {
return fmt.Errorf("invalid input: expected a JSON object")
}
input := make(map[string]any, len(fields))
for key, value := range fields {
input[key] = value
}
params := kernel.BrowserWebmcpInvokeToolParams{InvokeRequest: kernel.InvokeRequestParam{
ToolRef: in.ToolRef, Input: input, TimeoutSec: in.TimeoutSec,
}}
// A lost response can hide completed side effects, so never retry an invocation.
res, err := b.webmcp.InvokeTool(ctx, in.Identifier, params, option.WithMaxRetries(0))
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
if res.Status != kernel.InvocationResultStatusCompleted {
return fmt.Errorf("WebMCP invocation %s: %s: %s", res.InvocationID, res.Status, res.ErrorText)
}
// Preserve page-provided JSON numbers rather than re-encoding SDK float64 values.
var result struct {
Output json.RawMessage `json:"output"`
}
if err := json.Unmarshal([]byte(res.RawJSON()), &result); err != nil {
return err
}
data, err := json.MarshalIndent(result.Output, "", " ")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}

func newBrowsersWebMCPCommand() *cobra.Command {
root := &cobra.Command{Use: "webmcp", Short: "Discover and invoke native page tools"}
list := &cobra.Command{Use: "list <id-or-name>", Short: "List WebMCP tools across browser tabs and frames", Args: cobra.ExactArgs(1), RunE: runBrowsersWebMCPList}
addJSONOutputFlag(list)
list.Flags().Bool("json", false, "Output the raw API response as JSON (alias for --output json)")
invoke := &cobra.Command{Use: "invoke <id-or-name>", Short: "Invoke a WebMCP tool without automatic retries", Args: cobra.ExactArgs(1), RunE: runBrowsersWebMCPInvoke}
invoke.Flags().String("tool-ref", "", "Opaque tool reference from webmcp list")
_ = invoke.MarkFlagRequired("tool-ref")
invoke.Flags().String("input", "", "Tool input as a JSON object")
invoke.Flags().String("input-file", "", "Path to a JSON object file (use '-' for stdin)")
invoke.MarkFlagsOneRequired("input", "input-file")
invoke.MarkFlagsMutuallyExclusive("input", "input-file")
invoke.Flags().Int64("timeout-sec", 0, "Maximum execution time in seconds (default per server)")
root.AddCommand(list, invoke)
return root
}

func runBrowsersWebMCPList(cmd *cobra.Command, args []string) error {
output, _ := cmd.Flags().GetString("output")
if err := validateJSONOutput(output); err != nil {
return err
}
asJSON, _ := cmd.Flags().GetBool("json")
if asJSON {
output = "json"
}
client := getKernelClient(cmd)
b := BrowsersCmd{webmcp: &client.Browsers.Webmcp}
return b.WebMCPList(cmd.Context(), BrowsersWebMCPListInput{Identifier: args[0], Output: output})
}

func runBrowsersWebMCPInvoke(cmd *cobra.Command, args []string) error {
toolRef, _ := cmd.Flags().GetString("tool-ref")
input, _ := cmd.Flags().GetString("input")
if cmd.Flags().Changed("input-file") {
path, _ := cmd.Flags().GetString("input-file")
var data []byte
var err error
if path == "-" {
data, err = io.ReadAll(cmd.InOrStdin())
} else {
data, err = os.ReadFile(path)
}
if err != nil {
return fmt.Errorf("failed to read input file: %w", err)
}
input = string(data)
}
var timeout param.Opt[int64]
if cmd.Flags().Changed("timeout-sec") {
value, _ := cmd.Flags().GetInt64("timeout-sec")
timeout = kernel.Opt(value)
}
client := getKernelClient(cmd)
b := BrowsersCmd{webmcp: &client.Browsers.Webmcp}
return b.WebMCPInvoke(cmd.Context(), BrowsersWebMCPInvokeInput{Identifier: args[0], ToolRef: toolRef, Input: input, TimeoutSec: timeout})
}
Loading
Loading