Documentation
¶
Overview ¶
Package webops is a mobile-first web executor for Model Context Protocol servers. It connects to an MCP server (typically cli-mcp projecting a urfave/cli command tree, but anything that speaks MCP), enumerates the server's tools, and presents annotated favorites as one-push mobile buttons. Tool input forms are generated from the MCP tool's JSON Schema; output streams back over Server-Sent Events.
Threat model: this is a CLI-to-RCE bridge by construction. Default posture refuses to bind any non-Tailscale interface; `DangerouslySkipTailscale=true` opts out for localhost-only development. See README threat-model section.
Architecture (intentional):
browser ── HTTP ── webops.Server ── MCP (stdio) ── MCP server ── tools
webops never invokes a *cli.Command directly. Every action goes through the MCP server, which is the trust boundary.
Index ¶
Examples ¶
Constants ¶
const ( MDKeyFavorite = "webops.favorite" MDKeyLabel = "webops.label" MDKeyGroup = "webops.group" )
MDKey* are the well-known tool.Meta keys that cli-mcp stamps onto MCP Tool entries for webops to consume. The contract lives in cli-mcp's README under "Extension-namespace annotations".
Variables ¶
This section is empty.
Functions ¶
func ListenAndServe ¶
ListenAndServe is the package-level convenience: NewServer + Run.
func TestOnlyConnectAndList ¶
TestOnlyConnectAndList is a test helper that runs the MCP-connection + ListTools half of Server.Run without binding a listener. Useful for mounting the Handler under httptest.NewServer.
Do not call from production code.
Types ¶
type Auth ¶
type Auth interface {
// Middleware wraps the protected handler. Implementations may also
// mount their own auth endpoints onto the inner mux (registration,
// login) by routing through pattern matching on r.URL.Path before
// delegating to inner.
Middleware(inner http.Handler) http.Handler
}
Auth is the authentication backend interface. Implementations gate every HTTP request before it reaches the executor. The Middleware shape lets drivers expose their own challenge/response endpoints (e.g. WebAuthn registration and login) alongside the protected routes.
Built-in drivers:
- DangerouslyAllowAllAuth: STUB. Allows every request. Never deploy. The prefix follows the OpenClaw convention: any name that disables a load-bearing safety property carries the `Dangerously` prefix so call sites cannot pretend they did not know.
Planned (see #2):
- WebAuthn + TOTP: asymmetric-key challenge with a TOTP second factor. Phone holds the private key, server holds the registered public key and a per-user TOTP seed.
type DangerouslyAllowAllAuth ¶
type DangerouslyAllowAllAuth struct{}
DangerouslyAllowAllAuth lets every request through with no authentication. Useful for the DangerouslySkipTailscale=true example and unit tests. NEVER deploy with this.
func (DangerouslyAllowAllAuth) Middleware ¶
func (DangerouslyAllowAllAuth) Middleware(inner http.Handler) http.Handler
Middleware implements Auth.
type Field ¶
type Field struct {
Name string
Type string // "text" "number" "checkbox"
Required bool
Description string
Default string
Enum []string
}
Field is one rendered input on a tool's form.
type MCPTarget ¶
type MCPTarget struct {
// Command launches an MCP server as a subprocess and speaks stdio.
// Path-style: ["myapp", "--mcp-mode"] (first element is the binary).
Command []string
// Env is appended to the subprocess environment if Command is set.
Env []string
// HTTP is reserved for the planned HTTP/SSE transport. Setting it
// currently returns an error.
HTTP string
}
MCPTarget describes how the server connects to its upstream MCP server. Exactly one of Command or HTTP must be set.
type Options ¶
type Options struct {
// Addr is the TCP address to bind. Required.
Addr string
// Auth is the authentication backend. Required. Stub:
// DangerouslyAllowAllAuth{} (never deploy with that).
Auth Auth
// DangerouslySkipTailscale bypasses the Tailscale-interface bind gate.
// The listen address must still resolve to a loopback (127.0.0.1, ::1)
// when this is set; webops refuses to bind anywhere else even with
// the gate off. For development on a machine that has Tailscale
// installed but is not currently logged in, or in CI.
//
// Naming: the cli-web-ops convention follows OpenClaw - any flag that
// turns a load-bearing safety property off carries the `Dangerously`
// prefix so call sites read as the alarm they are.
DangerouslySkipTailscale bool
// DangerouslyBindAnywhere additionally bypasses the loopback check
// that DangerouslySkipTailscale leaves in place. Setting this lets
// webops bind 0.0.0.0 or any routable interface. There is no
// legitimate reason to set this in production; it exists so an
// integration test can bind a non-loopback inside an isolated
// network namespace.
DangerouslyBindAnywhere bool
// ShowAllTools exposes every tool, not just webops.favorite ones, on
// the home screen.
ShowAllTools bool
// CommandTimeout caps how long a single tool call may take before the
// SSE stream is closed and the upstream call is cancelled.
CommandTimeout time.Duration
// TailscaleInterface overrides the interface name(s) considered
// Tailscale-managed.
TailscaleInterface []string
// MCPClientName / MCPClientVersion identify webops to the MCP server.
// Sensible defaults are used when empty.
MCPClientName string
MCPClientVersion string
}
Options configure the web server.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the webops HTTP server.
func NewServer ¶
NewServer prepares a Server. Does not open the MCP connection (that happens in Run).
Example ¶
Build a webops.Server that connects to an MCP server subprocess over stdio. The Tailscale-binding gate is enforced unless DangerouslySkipTailscale is set. The MCP server is the trust boundary - webops never invokes a CLI directly.
package main
import (
"fmt"
"github.com/coilysiren/cli-web-ops/webops"
)
func main() {
srv, err := webops.NewServer(
webops.MCPTarget{
Command: []string{"cli-mcp-binary"},
},
webops.Options{
Addr: "127.0.0.1:8443",
Auth: webops.DangerouslyAllowAllAuth{}, // STUB - do not deploy
DangerouslySkipTailscale: true, // dev only
},
)
fmt.Println("ok:", srv != nil && err == nil)
}
Output: ok: true