From 6ecd8e298d9f459cd289bf9616f80cd173a6fa89 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 15:28:25 +0200 Subject: [PATCH 01/13] fix(android): support Windows in emulator process spawning syscall.SysProcAttr{Setpgid} doesn't compile on Windows. Extract setProcAttr into OS-specific files (Setpgid on Unix, CREATE_NEW_PROCESS_GROUP on Windows) and use runtime.GOOS to pick a real Android SDK home default on Windows instead of only darwin/linux paths. --- docs/adding-platform.md | 2 +- platforms/android/android.go | 21 +++++++++++++++------ platforms/android/procattr_unix.go | 13 +++++++++++++ platforms/android/procattr_windows.go | 13 +++++++++++++ 4 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 platforms/android/procattr_unix.go create mode 100644 platforms/android/procattr_windows.go diff --git a/docs/adding-platform.md b/docs/adding-platform.md index cb3bc7c..939cdee 100644 --- a/docs/adding-platform.md +++ b/docs/adding-platform.md @@ -44,7 +44,7 @@ In `internal/config/config.go`, add `MyPlatformConfig` and wire it up. - Do NOT add verification tools (tap, screenshot, getTree) — those belong in Controllers - Implement `AwaitReady` for a good developer experience -- Use `SysProcAttr{Setpgid:true}` when spawning long-lived processes +- For long-lived spawned processes, set process attrs via a package-private `setProcAttr(cmd *exec.Cmd)` helper split across `procattr_unix.go` (`//go:build !windows`, `Setpgid:true`) and `procattr_windows.go` (`//go:build windows`, `CreationFlags: CREATE_NEW_PROCESS_GROUP`) — see `platforms/android/procattr_*.go`. `Setpgid` is Unix-only; any platform adapter targeting Windows needs this split. - Add integration tests gated on `MCPSIM_INTEGRATION=1` ## Testing diff --git a/platforms/android/android.go b/platforms/android/android.go index a76ae86..8787e1c 100644 --- a/platforms/android/android.go +++ b/platforms/android/android.go @@ -7,9 +7,10 @@ import ( "fmt" "os" "os/exec" + "path/filepath" + "runtime" "strconv" "strings" - "syscall" "time" "github.com/espetro/mcp-sim/internal/config" @@ -46,9 +47,17 @@ func New(cfg config.AndroidConfig) (*Platform, error) { androidHome = os.Getenv("ANDROID_HOME") if androidHome == "" { if home, _ := os.UserHomeDir(); home != "" { - androidHome = home + "/Library/Android/sdk" - if _, err := os.Stat(androidHome); os.IsNotExist(err) { - androidHome = home + "/Android/Sdk" + switch runtime.GOOS { + case "darwin": + androidHome = filepath.Join(home, "Library", "Android", "sdk") + case "windows": + if localAppData := os.Getenv("LOCALAPPDATA"); localAppData != "" { + androidHome = filepath.Join(localAppData, "Android", "Sdk") + } else { + androidHome = filepath.Join(home, "AppData", "Local", "Android", "Sdk") + } + default: + androidHome = filepath.Join(home, "Android", "Sdk") } } } @@ -176,7 +185,7 @@ func (p *Platform) Start(ctx context.Context, target string, opts contract.Start } cmd := p.emulatorCmd(ctx, args...) - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + setProcAttr(cmd) if err := cmd.Start(); err != nil { return contract.Device{}, fmt.Errorf("emulator start: %w", err) } @@ -289,7 +298,7 @@ func (p *Platform) Wipe(ctx context.Context, target string) error { _ = p.Stop(ctx, target) // Restart with -wipe-data. cmd := p.emulatorCmd(ctx, "-avd", target, "-wipe-data") - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + setProcAttr(cmd) _ = cmd.Start() return nil } diff --git a/platforms/android/procattr_unix.go b/platforms/android/procattr_unix.go new file mode 100644 index 0000000..5e209c1 --- /dev/null +++ b/platforms/android/procattr_unix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package android + +import ( + "os/exec" + "syscall" +) + +// setProcAttr configures cmd to survive parent death by starting it in its own process group. +func setProcAttr(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} +} diff --git a/platforms/android/procattr_windows.go b/platforms/android/procattr_windows.go new file mode 100644 index 0000000..15380d4 --- /dev/null +++ b/platforms/android/procattr_windows.go @@ -0,0 +1,13 @@ +//go:build windows + +package android + +import ( + "os/exec" + "syscall" +) + +// setProcAttr configures cmd to survive parent death by placing it in a new process group. +func setProcAttr(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP} +} From e0a362a22c270bec821af3ca4937c0a41a675de7 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 15:28:29 +0200 Subject: [PATCH 02/13] fix(agentdevice): replace pkill with tracked process kill pkill -f is Unix-only and fragile (matches by command line, not the actual child PID). Store the *exec.Cmd from Start() and kill it directly via cmd.Process.Kill(), which is cross-platform stdlib. --- controllers/agentdevice/agentdevice.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/controllers/agentdevice/agentdevice.go b/controllers/agentdevice/agentdevice.go index 9add1c6..271eb5a 100644 --- a/controllers/agentdevice/agentdevice.go +++ b/controllers/agentdevice/agentdevice.go @@ -19,6 +19,7 @@ type Controller struct { running bool mu sync.Mutex stopCh chan struct{} + cmd *exec.Cmd } // New creates a new agent-device controller adapter. @@ -57,6 +58,7 @@ func (c *Controller) Start(ctx context.Context, cfg contract.StartConfig) (contr c.running = true c.stopCh = make(chan struct{}) + c.cmd = cmd return contract.ProxyInfo{ Name: c.Name(), @@ -74,9 +76,9 @@ func (c *Controller) Stop(ctx context.Context) error { return nil } - // Find and kill the process. - cmd := exec.CommandContext(ctx, "pkill", "-f", "agent-device proxy") - _ = cmd.Run() + if c.cmd != nil && c.cmd.Process != nil { + _ = c.cmd.Process.Kill() + } c.running = false close(c.stopCh) From e06701abfe4ee11839c9bcd8899867ac6305e5fb Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 15:28:33 +0200 Subject: [PATCH 03/13] fix(config): resolve default config path via os.UserHomeDir os.Getenv("HOME") is unset on Windows (USERPROFILE is used instead), so default config-file discovery silently no-op'd there. os.UserHomeDir() resolves the right env var per OS. --- internal/config/config.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index cec7f8b..3d73c1c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "path/filepath" "strconv" "gopkg.in/yaml.v3" @@ -88,8 +89,8 @@ func Load() (Config, error) { if err := loadFile(path, &cfg); err != nil { return Config{}, fmt.Errorf("loading config from %s: %w", path, err) } - } else if home := os.Getenv("HOME"); home != "" { - defaultPath := home + "/.config/mcp-sim/config.yaml" + } else if home, err := os.UserHomeDir(); err == nil && home != "" { + defaultPath := filepath.Join(home, ".config", "mcp-sim", "config.yaml") _ = loadFile(defaultPath, &cfg) // ignore error if missing } From f8d5f3a47b7234812fd5ce9df28ae6cd91ba7890 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 15:28:37 +0200 Subject: [PATCH 04/13] ci: add cross-platform build matrix New job builds/vets/tests on ubuntu/windows/macos to catch OS-specific compile regressions (e.g. the Setpgid/Windows breakage this branch fixes) going forward. task validate stays ubuntu-only since its golangci-lint and Taskfile vars aren't guaranteed on native Windows runners. --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69b5ab1..2aa3360 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,24 @@ jobs: - name: Run validate run: task validate + + cross-platform: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build + run: go build ./... + + - name: Vet + run: go vet ./... + + - name: Test + run: go test ./... From 9c020894a2ca3abe7605e58ad0f70a080588102a Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 15:28:56 +0200 Subject: [PATCH 05/13] docs: stop implying the whole server is macOS-only Only the iOS adapter needs macOS (Xcode/xcrun). Android and the agent-device controller already work cross-platform. --- AGENTS.md | 7 ++++--- README.md | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0e489df..dddf85c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,10 @@ AI agent context for the mcp-sim Go project (mobile emulator lifecycle MCP serve ## Project Overview -mcp-sim is an MCP server for managing iOS Simulator and Android Emulator lifecycle -on a remote macOS host. Built-in adapters for iOS (xcrun simctl), Android (emulator + adb), -and agent-device controller. +mcp-sim is an MCP server for managing iOS Simulator and Android Emulator lifecycle, +runnable on macOS, Linux, or Windows. Built-in adapters for iOS (xcrun simctl, macOS-only — +requires Xcode), Android (emulator + adb, cross-platform), and agent-device controller +(cross-platform). ## Backlog diff --git a/README.md b/README.md index 04c5916..b1a982d 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ mcp-sim is resilient: each platform adapter is auto-detected at startup, and a missing tool just means that platform's tools are skipped. You can run the server with **only iOS**, **only Android**, or **both**. -For full functionality on macOS, install: +The server itself runs on macOS, Linux, and Windows. iOS support requires macOS + Xcode (Simulator is Apple-only tooling); Android and the agent-device controller work on all three OSes. -| Tool | Required for | Install | -|------|---|---| -| Xcode + iOS Simulators | iOS tools | `xcode-select --install` (or App Store → Xcode) | -| Android SDK + `emulator` + `adb` | Android tools | Install [Android Studio](https://developer.android.com/studio) or `brew install --cask android-commandlinetools` | -| `agent-device` | verification controller | `brew install agent-device` (or see [agent-device docs](https://github.com/espetro/agent-device)) | +| Tool | Required for | OS | Install | +|------|---|---|---| +| Xcode + iOS Simulators | iOS tools | macOS only | `xcode-select --install` (or App Store → Xcode) | +| Android SDK + `emulator` + `adb` | Android tools | macOS, Linux, Windows | Install [Android Studio](https://developer.android.com/studio) or `brew install --cask android-commandlinetools` | +| `agent-device` | verification controller | macOS, Linux, Windows | `brew install agent-device` (or see [agent-device docs](https://github.com/espetro/agent-device)) | Each tool is checked via PATH probing at server startup. If `xcode-select -p` succeeds the iOS adapter registers; if `emulator` or `adb` is on PATH, the Android adapter registers; if `agent-device` resolves, the controller registers. Otherwise the relevant MCP tools are simply absent — the server still starts. From c4b06ff849c3a4de3a3326578ca27c0475e44115 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 15:29:04 +0200 Subject: [PATCH 06/13] feat(service): install mcp-sim as a native OS service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `mcp-sim service install|uninstall|start|stop|restart|status|run` via github.com/kardianos/service, so a single command registers mcp-sim as a launchd service (macOS), systemd service (Linux), or Windows Service — covering all three OSes the server now runs on. A `--user` flag installs a per-user service (LaunchAgent / systemd --user) that doesn't need root. Extracted the registry/HTTP-server wiring shared by "serve" and the new service program into internal/bootstrap, and rename docs/launchd.md to docs/service.md now that it covers all three platforms. --- AGENTS.md | 2 +- README.md | 2 +- cmd/mcp-sim/main.go | 161 ++++++++++++++++++++++++-------- docs/launchd.md | 50 ---------- docs/service.md | 86 +++++++++++++++++ go.mod | 1 + go.sum | 2 + internal/bootstrap/bootstrap.go | 63 +++++++++++++ internal/service/service.go | 92 ++++++++++++++++++ 9 files changed, 368 insertions(+), 91 deletions(-) delete mode 100644 docs/launchd.md create mode 100644 docs/service.md create mode 100644 internal/bootstrap/bootstrap.go create mode 100644 internal/service/service.go diff --git a/AGENTS.md b/AGENTS.md index dddf85c..8a64adc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ build time via `-ldflags "-X .../internal/version.Version=..."`. |----------|---------| | `docs/architecture.md` | Adapter model + separation of concerns | | `docs/tailscale.md` | Network deployment over Tailscale | -| `docs/launchd.md` | macOS service management | +| `docs/service.md` | Running mcp-sim as a native OS service (launchd/systemd/Windows Service) | | `docs/adding-platform.md` | Implementing new Platform adapters | | `CONTRIBUTING.md` | PR process + load-bearing separation rule | | `pkg/contract/platform.go` | Platform interface | diff --git a/README.md b/README.md index b1a982d..59eb63b 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ For Tailscale-based remote access, see [docs/tailscale.md](docs/tailscale.md). - [Architecture](docs/architecture.md) — adapter model and separation of concerns - [Tailscale setup](docs/tailscale.md) — running over Tailscale -- [launchd](docs/launchd.md) — macOS service management +- [Running as a service](docs/service.md) — install as a native OS service (launchd/systemd/Windows Service) - [Adding a platform](docs/adding-platform.md) — implementing the Platform interface ## License diff --git a/cmd/mcp-sim/main.go b/cmd/mcp-sim/main.go index 21bf0a3..f0822d9 100644 --- a/cmd/mcp-sim/main.go +++ b/cmd/mcp-sim/main.go @@ -5,22 +5,23 @@ import ( "flag" "fmt" "io" - "net/http" "os" "os/signal" "sort" "syscall" "github.com/espetro/mcp-sim/controllers/agentdevice" + "github.com/espetro/mcp-sim/internal/bootstrap" "github.com/espetro/mcp-sim/internal/config" "github.com/espetro/mcp-sim/internal/core" - srv "github.com/espetro/mcp-sim/internal/http" applog "github.com/espetro/mcp-sim/internal/log" + svc "github.com/espetro/mcp-sim/internal/service" "github.com/espetro/mcp-sim/internal/version" "github.com/espetro/mcp-sim/pkg/mcp" "github.com/espetro/mcp-sim/platforms/android" "github.com/espetro/mcp-sim/platforms/ios" + kservice "github.com/kardianos/service" sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -29,6 +30,7 @@ const ( cmdHelp = "help" cmdServe = "serve" cmdMCP = "mcp" + cmdService = "service" cmdVersion = "version" ) @@ -40,6 +42,7 @@ Usage: Commands: serve Start the HTTP/SSE server (long-lived, default for service mode) mcp Run over stdio (spawnable per agent session) + service Install/manage mcp-sim as a native OS service version Print version information help Print this message @@ -73,6 +76,32 @@ Flags: Reads JSON-RPC from stdin, writes to stdout. ` +const usageService = `mcp-sim service — install/manage mcp-sim as a native OS service + +Installs mcp-sim as a launchd service (macOS), systemd service (Linux), or +Windows Service (Windows), managed by the OS the same way as any other +background service. + +Usage: + mcp-sim service [flags] + +Actions: + install Register mcp-sim with the OS service manager + uninstall Remove mcp-sim from the OS service manager + start Start the installed service + stop Stop the installed service + restart Restart the installed service + status Print the installed service's status + run Hidden entry point invoked by the OS service manager itself + +Flags (only meaningful with "install"): + -listen, --listen Address to bind (overrides config; carried to "run") + -config, --config Path to YAML config file (carried to "run") + -user, --user Install as a per-user service, no root required + (unsupported on Windows) + -h, --help Show this help +` + func main() { if err := run(os.Args[0], os.Args[1:], os.Stdout); err != nil { fmt.Fprintln(os.Stderr, "error:", err) @@ -95,6 +124,8 @@ func run(prog string, args []string, stdout io.Writer) error { return runServe(prog, args[1:], stdout) case cmdMCP: return runMCP(prog, args[1:], stdout) + case cmdService: + return runService(prog, args[1:], stdout) case "-v", "--version": return printVersion(stdout) default: @@ -166,60 +197,112 @@ func runMCP(prog string, args []string, stdout io.Writer) error { return mcpImpl(prog, *configPath) } -// serveImpl and mcpImpl are the actual implementation of each subcommand. -// Extracted so run() stays small and command flags parse first. -func serveImpl(prog, listenAddr, configPath string) error { - _ = configPath // TODO: pass through to config.Load; env override preserved +// runService parses the service action and flags, then dispatches to +// kardianos/service's control functions or runs as the installed service. +func runService(prog string, args []string, stdout io.Writer) error { + if len(args) == 0 || isHelpFlag(args[0]) || args[0] == cmdHelp { + fmt.Fprint(stdout, usageService) + return nil + } + action := args[0] + + fs := flag.NewFlagSet(cmdService, flag.ContinueOnError) + fs.SetOutput(stdout) + listenAddr := fs.String("listen", "", "Address to bind (overrides config)") + configPath := fs.String("config", "", "Path to YAML config file") + userService := fs.Bool("user", false, "Install as a per-user service (no root required; unsupported on Windows)") + fs.Usage = func() { fmt.Fprint(stdout, usageService) } + if err := fs.Parse(args[1:]); err != nil { + if err == flag.ErrHelp { + return nil + } + fmt.Fprint(stdout, usageService) + return err + } + + var svcArgs []string + if *listenAddr != "" { + svcArgs = append(svcArgs, "--listen", *listenAddr) + } + if *configPath != "" { + svcArgs = append(svcArgs, "--config", *configPath) + } + + svcConfig, err := svc.BuildConfig(svcArgs, *userService) + if err != nil { + return fmt.Errorf("building service config: %w", err) + } + cfg, err := config.Load() if err != nil { return fmt.Errorf("loading config: %w", err) } - if listenAddr != "" { - cfg.Server.Listen = listenAddr + if *listenAddr != "" { + cfg.Server.Listen = *listenAddr } logger := applog.New(cfg.Server.LogLevel, cfg.Server.LogFormat) - ctx := applog.WithContext(context.Background(), logger) + program := svc.NewProgram(cfg, logger) - registry := core.NewRegistry(logger) - lifecycle := core.NewLifecycle(registry) - - if cfg.Platforms.IOS.Enabled { - iosPlatform, err := ios.New(ctx, cfg.Platforms.IOS) - if err != nil { - return fmt.Errorf("ios platform: %w", err) - } - if iosPlatform == nil { - logger.Warn("ios platform disabled — Xcode/xcrun not detected, skipping iOS tools") - } else { - registry.RegisterPlatform(iosPlatform) - } + s, err := kservice.New(program, svcConfig) + if err != nil { + return fmt.Errorf("creating service: %w", err) } - if cfg.Platforms.Android.Enabled { - androidPlatform, err := android.New(cfg.Platforms.Android) + + switch action { + case "run": + return s.Run() + case "status": + status, err := s.Status() if err != nil { - return fmt.Errorf("android platform: %w", err) + return err } - if androidPlatform == nil { - logger.Warn("android platform disabled — emulator/adb not detected, skipping Android tools") - } else { - registry.RegisterPlatform(androidPlatform) + fmt.Fprintln(stdout, serviceStatusString(status)) + return nil + case "install", "uninstall", "start", "stop", "restart": + if err := kservice.Control(s, action); err != nil { + return err } + fmt.Fprintf(stdout, "mcp-sim service: %s ok\n", action) + return nil + default: + fmt.Fprintf(os.Stderr, "%s: unknown service action %q\n\n", prog, action) + fmt.Fprint(stdout, usageService) + os.Exit(2) + return nil // unreachable } - if cfg.Controllers.AgentDevice.Enabled { - registry.RegisterController(agentdevice.New(cfg.Controllers.AgentDevice)) +} + +func serviceStatusString(status kservice.Status) string { + switch status { + case kservice.StatusRunning: + return "running" + case kservice.StatusStopped: + return "stopped" + default: + return "unknown" } +} - mcpServer := mcp.NewServer(registry, lifecycle, logger) +// serveImpl and mcpImpl are the actual implementation of each subcommand. +// Extracted so run() stays small and command flags parse first. +func serveImpl(prog, listenAddr, configPath string) error { + _ = configPath // TODO: pass through to config.Load; env override preserved + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + if listenAddr != "" { + cfg.Server.Listen = listenAddr + } - mux := http.NewServeMux() - mux.Handle("/mcp", mcpServer.StreamableHTTPHandler()) - mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprintln(w, "ok") - }) + logger := applog.New(cfg.Server.LogLevel, cfg.Server.LogFormat) + ctx := applog.WithContext(context.Background(), logger) - httpServer := srv.New(cfg.Server.Listen, http.HandlerFunc(mux.ServeHTTP)) + registry, httpServer, err := bootstrap.BuildHTTPServer(ctx, cfg, logger) + if err != nil { + return err + } logger.Info("mcp-sim starting", "version", version.Version, diff --git a/docs/launchd.md b/docs/launchd.md deleted file mode 100644 index cc2a027..0000000 --- a/docs/launchd.md +++ /dev/null @@ -1,50 +0,0 @@ -# macOS launchd service - -## Using launchd - -Create `~/Library/LaunchAgents/com.espetro.mcp-sim.plist`: - -```xml - - - - - Label - com.espetro.mcp-sim - ProgramArguments - - /usr/local/bin/mcp-sim - serve - --listen - :9090 - - RunAtLoad - - KeepAlive - - StandardOutPath - /usr/local/var/log/mcp-sim.log - StandardErrorPath - /usr/local/var/log/mcp-sim.log - - -``` - -Load the service: - -```bash -mkdir -p /usr/local/var/log -launchctl load ~/Library/LaunchAgents/com.espetro.mcp-sim.plist -``` - -## Using Homebrew services - -If installed via Homebrew: - -```bash -brew services start mcp-sim -brew services stop mcp-sim -brew services restart mcp-sim -``` - -Logs: `brew services log mcp-sim` diff --git a/docs/service.md b/docs/service.md new file mode 100644 index 0000000..f5e5226 --- /dev/null +++ b/docs/service.md @@ -0,0 +1,86 @@ +# Running mcp-sim as a service + +`mcp-sim service` installs mcp-sim as a native background service, managed by +whichever OS it runs on: + +- macOS → `launchd` +- Linux → `systemd` (or SysV/Upstart as fallback) +- Windows → Windows Service + +## Install and manage + +```bash +mcp-sim service install --listen :9090 --config ~/.config/mcp-sim/config.yaml +mcp-sim service start +mcp-sim service status +mcp-sim service stop +mcp-sim service restart +mcp-sim service uninstall +``` + +`--listen` and `--config` are only read at `install` time — they're recorded +as the arguments the OS service manager passes to the hidden +`mcp-sim service run` entry point it invokes on every start. + +Installing system-wide requires elevated privileges (`sudo` on macOS/Linux, an +elevated shell on Windows). To skip that on macOS/Linux, install a per-user +service instead (LaunchAgent / `systemd --user`, no root needed): + +```bash +mcp-sim service install --user --listen :9090 +mcp-sim service start +``` + +Windows Service install always needs elevation — there's no per-user +equivalent on that OS. + +## Manual launchd customization (macOS) + +If you'd rather manage the launchd plist yourself instead of using +`mcp-sim service install`, create +`~/Library/LaunchAgents/com.espetro.mcp-sim.plist`: + +```xml + + + + + Label + com.espetro.mcp-sim + ProgramArguments + + /usr/local/bin/mcp-sim + serve + --listen + :9090 + + RunAtLoad + + KeepAlive + + StandardOutPath + /usr/local/var/log/mcp-sim.log + StandardErrorPath + /usr/local/var/log/mcp-sim.log + + +``` + +Load the service: + +```bash +mkdir -p /usr/local/var/log +launchctl load ~/Library/LaunchAgents/com.espetro.mcp-sim.plist +``` + +### Using Homebrew services + +If installed via Homebrew: + +```bash +brew services start mcp-sim +brew services stop mcp-sim +brew services restart mcp-sim +``` + +Logs: `brew services log mcp-sim` diff --git a/go.mod b/go.mod index 0353f49..5e23a0b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/espetro/mcp-sim go 1.25.0 require ( + github.com/kardianos/service v1.2.4 github.com/modelcontextprotocol/go-sdk v1.6.1 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index e90e507..bcfbb19 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk= +github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc= github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go new file mode 100644 index 0000000..2f27ad8 --- /dev/null +++ b/internal/bootstrap/bootstrap.go @@ -0,0 +1,63 @@ +// Package bootstrap wires the registry, MCP server, and HTTP handler shared +// by the "serve" subcommand and the OS-native service program. +package bootstrap + +import ( + "context" + "fmt" + "log/slog" + "net/http" + + "github.com/espetro/mcp-sim/controllers/agentdevice" + "github.com/espetro/mcp-sim/internal/config" + "github.com/espetro/mcp-sim/internal/core" + srv "github.com/espetro/mcp-sim/internal/http" + "github.com/espetro/mcp-sim/pkg/mcp" + "github.com/espetro/mcp-sim/platforms/android" + "github.com/espetro/mcp-sim/platforms/ios" +) + +// BuildHTTPServer constructs the registry and HTTP server for serve/service modes. +// It registers whichever platform/controller adapters are enabled and detected. +func BuildHTTPServer(ctx context.Context, cfg config.Config, logger *slog.Logger) (*core.Registry, *srv.Server, error) { + registry := core.NewRegistry(logger) + lifecycle := core.NewLifecycle(registry) + + if cfg.Platforms.IOS.Enabled { + iosPlatform, err := ios.New(ctx, cfg.Platforms.IOS) + if err != nil { + return nil, nil, fmt.Errorf("ios platform: %w", err) + } + if iosPlatform == nil { + logger.Warn("ios platform disabled — Xcode/xcrun not detected, skipping iOS tools") + } else { + registry.RegisterPlatform(iosPlatform) + } + } + if cfg.Platforms.Android.Enabled { + androidPlatform, err := android.New(cfg.Platforms.Android) + if err != nil { + return nil, nil, fmt.Errorf("android platform: %w", err) + } + if androidPlatform == nil { + logger.Warn("android platform disabled — emulator/adb not detected, skipping Android tools") + } else { + registry.RegisterPlatform(androidPlatform) + } + } + if cfg.Controllers.AgentDevice.Enabled { + registry.RegisterController(agentdevice.New(cfg.Controllers.AgentDevice)) + } + + mcpServer := mcp.NewServer(registry, lifecycle, logger) + + mux := http.NewServeMux() + mux.Handle("/mcp", mcpServer.StreamableHTTPHandler()) + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "ok") + }) + + httpServer := srv.New(cfg.Server.Listen, http.HandlerFunc(mux.ServeHTTP)) + return registry, httpServer, nil +} diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..240af99 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,92 @@ +// Package service wires mcp-sim into a native OS-managed background service +// (launchd on macOS, systemd on Linux, Windows Service on Windows) via +// github.com/kardianos/service. +package service + +import ( + "context" + "log/slog" + "os" + + "github.com/espetro/mcp-sim/internal/bootstrap" + "github.com/espetro/mcp-sim/internal/config" + "github.com/espetro/mcp-sim/internal/core" + + kservice "github.com/kardianos/service" +) + +// Program implements kservice.Interface, wrapping the same HTTP server +// construction used by the "serve" subcommand. +type Program struct { + cfg config.Config + logger *slog.Logger + + registry *core.Registry + cancel context.CancelFunc + done chan struct{} +} + +// NewProgram creates a service program for the given config. +func NewProgram(cfg config.Config, logger *slog.Logger) *Program { + return &Program{cfg: cfg, logger: logger} +} + +// Start is invoked by the OS service manager. It must return quickly, so the +// HTTP server runs in a background goroutine. +func (p *Program) Start(s kservice.Service) error { + ctx, cancel := context.WithCancel(context.Background()) + p.cancel = cancel + + registry, httpServer, err := bootstrap.BuildHTTPServer(ctx, p.cfg, p.logger) + if err != nil { + cancel() + return err + } + p.registry = registry + p.done = make(chan struct{}) + + go func() { + defer close(p.done) + if err := httpServer.ListenAndServe(ctx); err != nil { + p.logger.Error("http server stopped with error", "error", err) + } + }() + return nil +} + +// Stop is invoked by the OS service manager for graceful shutdown. +func (p *Program) Stop(s kservice.Service) error { + if p.cancel != nil { + p.cancel() + } + if p.done != nil { + <-p.done + } + if p.registry != nil { + p.registry.ShutdownAll() + } + return nil +} + +// BuildConfig builds the kardianos/service Config used to install mcp-sim as +// a native OS service. args are extra CLI flags carried through to the +// hidden "service run" entry point the installed service actually invokes. +// When user is true, installs a per-user service (LaunchAgent / systemd +// --user) that doesn't require root — unsupported on Windows, where a +// Windows Service always needs an elevated install regardless. +func BuildConfig(args []string, user bool) (*kservice.Config, error) { + exe, err := os.Executable() + if err != nil { + return nil, err + } + return &kservice.Config{ + Name: "mcp-sim", + DisplayName: "mcp-sim", + Description: "MCP server for mobile emulator lifecycle", + Executable: exe, + Arguments: append([]string{"service", "run"}, args...), + Option: kservice.KeyValue{ + "UserService": user, + }, + }, nil +} From f113df16b3761c38cbd6f207ec77b4de3b77c9c6 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 15:42:29 +0200 Subject: [PATCH 07/13] docs(readme): collapse go install into a details section Homebrew/GitHub Releases are the primary install paths; go install is a source-build fallback, not the recommended default. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 59eb63b..1366620 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,15 @@ MCPSIM_AGENT_DEVICE_ENABLED=false mcp-sim serve brew install espetro/mcp-sim/mcp-sim ``` -### go install +
+go install (build from source) ```bash go install github.com/espetro/mcp-sim/cmd/mcp-sim@latest ``` +
+ ### GitHub Releases Download pre-built binaries from [github.com/espetro/mcp-sim/releases](https://github.com/espetro/mcp-sim/releases). From 12ab74cca63c6f9da8ca3782821cb0db3885f519 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 16:07:02 +0200 Subject: [PATCH 08/13] docs: drop docs/releases, duplicated by CHANGELOG.md Refs #29 --- docs/releases/v0.1.0.md | 50 ----------------------------------------- docs/releases/v0.1.1.md | 48 --------------------------------------- 2 files changed, 98 deletions(-) delete mode 100644 docs/releases/v0.1.0.md delete mode 100644 docs/releases/v0.1.1.md diff --git a/docs/releases/v0.1.0.md b/docs/releases/v0.1.0.md deleted file mode 100644 index 3bd3cb7..0000000 --- a/docs/releases/v0.1.0.md +++ /dev/null @@ -1,50 +0,0 @@ -# v0.1.0 — initial MVP release - -**Released:** 2026-07-05 -**Tag:** `v0.1.0` on `main` (commit `85e6f1e`) - -## Highlights - -- MCP server for managing iOS Simulator and Android Emulator lifecycle on a remote macOS host. -- 10 MCP tools: `list_devices`, `boot_device`, `stop_device`, `get_state`, `await_ready`, `wipe_device`, `open_url`, `start_controller`, `stop_controller`, `controller_status`. -- Streamable HTTP transport at `/mcp` plus stdio transport via the `mcp` subcommand. -- Lazy platform registration: iOS, Android, and agent-device auto-detect their dependencies at startup and skip gracefully when missing. Same binary serves Swift-only, Android-only, or full-stack users. -- Cross-compiled binaries for darwin/{amd64,arm64}, linux/{amd64,arm64}, windows/amd64 via GoReleaser. -- Homebrew tap at `espetro/homebrew-mcp-sim` (planned, not yet published in v0.1.0). - -## Install - -```bash -brew install espetro/mcp-sim/mcp-sim -# or -go install github.com/espetro/mcp-sim/cmd/mcp-sim@latest -# or download from https://github.com/espetro/mcp-sim/releases/tag/v0.1.0 -``` - -## Prerequisites - -mcp-sim auto-detects prerequisites. If any are missing, that platform's tools are simply absent from the MCP server. - -- **iOS Simulator**: Xcode + Command Line Tools (`xcode-select --install`) -- **Android Emulator**: Android SDK with `emulator` and `adb` on PATH -- **agent-device**: installed separately (`brew install agent-device`); invoked by the agent client, not by mcp-sim - -To force-disable a platform: - -```bash -MCPSIM_IOS_ENABLED=false mcp-sim serve -MCPSIM_ANDROID_ENABLED=false mcp-sim serve -MCPSIM_AGENT_DEVICE_ENABLED=false mcp-sim serve -``` - -## Known issues (fixed in v0.1.1) - -- `await_ready` default timeout too short for iOS Simulator boot on slower Macs. -- `open_url https://...` fails on Android AVDs without a browser app — surfaces as a clear error but is unexpected. -- `stop_device` did not always terminate Android emulators; fixed via PID tracking + SIGTERM to process group. - -See [v0.1.1 release notes](v0.1.1.md) for fixes. - -## Full changelog - -See [CHANGELOG.md](../../CHANGELOG.md). \ No newline at end of file diff --git a/docs/releases/v0.1.1.md b/docs/releases/v0.1.1.md deleted file mode 100644 index f90197e..0000000 --- a/docs/releases/v0.1.1.md +++ /dev/null @@ -1,48 +0,0 @@ -# v0.1.1 — integration-test hardening - -**Released:** 2026-07-05 (planned) -**Tag:** `v0.1.1` on `main` (planned) - -Patch release. Fixes the bugs surfaced by the v0.1.0 integration test. - -## Fixed - -- **Android emulator killed by request ctx** — `boot_device` returned success but the emulator died ~10s later when adb showed `offline`. Cause: `exec.CommandContext(reqCtx)` propagated SIGKILL when the SDK cancelled the request. Emulator now spawned with `context.Background()` so it survives the request lifecycle. -- **Stdio inherited from server** — emulator/agent-device stdio redirected to `io.Discard` to avoid SIGPIPE on backgrounded servers. -- **`open_url` errors had no context** — both iOS and Android now use `CombinedOutput()` and include stderr so callers see the actual reason an intent failed (e.g. "Activity not started, unable to resolve Intent" on AVDs without a browser). -- **`stop_device` didn't reliably terminate Android emulator** — now tracks spawned PIDs and SIGTERMs the process group directly via `syscall.Kill(-pid, SIGTERM)`. `adb emu kill` becomes a fallback. -- **`await_ready` default timeout too short** — bumped from 60s to 180s. iOS Simulator boot on slower Macs takes ~90s. -- **`boot_device` default timeout too short** — bumped from 60s to 120s in `lifecycle.BootDevice`. - -## Changed - -- **`controllers/agentdevice`** — properly invokes `agent-device proxy` (the proxy subcommand shipped in agent-device v0.18+). The initial v0.1.0 implementation was based on testing with an older v0.14 binary that didn't have `proxy`; v0.1.1 restores the proxy-spawning behavior with correct flags (`--port`, `--host`, `--daemon-auth-token`). The controller now: - - `Start()`: spawns `agent-device proxy --port N` as a detached child - - `Stop()`: SIGTERMs the process group (PID tracked) - - `Status()`: probes `/health` on the proxy -- **AGENTS.md** — removed the `## Current release` section (went stale at every release). Release docs now live in `docs/releases/` and AGENTS.md references them via progressive disclosure. -- **README prerequisites** — agent-device entry now specifies "v0.18+" since the controller adapter requires the `proxy` subcommand. - -## Added - -- `docs/releases/v0.1.0.md` — release notes for the v0.1.0 tag. -- `docs/releases/v0.1.1.md` — this file. -- `.agents/plans/2026-07-05-v0.1.0-integration-test.md` — integration test report (gitignored). -- `.agents/plans/2026-07-05-v0.1.1-release.md` — release plan (gitignored). -- **Homebrew tap live** — `espetro/homebrew-mcp-sim` created on GitHub. GoReleaser's `brews` block now ships a `service` stanza (`run`, `keep_alive`, `log_path`), so `brew services start/stop/restart mcp-sim` (documented in `docs/launchd.md`) actually works once the formula publishes. -- **`.github/workflows/release.yml`** — new GitHub Actions workflow triggers GoReleaser on `v*` tag push, replacing the manual-only `task release` flow. Requires `TAP_GITHUB_TOKEN` repo secret (scoped to `homebrew-mcp-sim`) alongside the default `GITHUB_TOKEN`. - -## How to upgrade - -```bash -brew upgrade mcp-sim -# or -go install github.com/espetro/mcp-sim/cmd/mcp-sim@v0.1.1 -``` - -No config migration required. - -## Known caveats (still) - -- **Android AVDs without a browser app** still reject `open_url https://...`. We surface this as a clear "Activity not started" error instead of a generic exit code, but the user must install Chrome or another browser on the AVD. Tracked for v0.1.2. -- **agent-device v0.14 and earlier** register the controller adapter but `start_controller` fails at spawn time (no `proxy` subcommand). Upgrade to v0.18+ for full controller support. \ No newline at end of file From bead612c405e831b0266a0a9bcce432a42245927 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 16:07:05 +0200 Subject: [PATCH 09/13] chore(taskfile): add docmd tasks for doc site preview and build Refs #29 --- .gitignore | 1 + Taskfile.yml | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.gitignore b/.gitignore index bafcd24..0eb9add 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Binaries bin/ dist/ +site/ *.exe *.exe~ *.dll diff --git a/Taskfile.yml b/Taskfile.yml index 8050a7c..3608dfe 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -39,6 +39,16 @@ tasks: cmds: - go mod tidy + docs: + desc: Run docmd dev server for docs/ (live preview) + cmds: + - npx @docmd/core dev + + docs-build: + desc: Build static docs site from docs/ (outputs to site/) + cmds: + - npx @docmd/core build + validate: desc: Typecheck + lint + test (AI-agent parseable) run: when_changed From 927e2eeb844fc30e8af2fa7ace8b2f05cfd20d98 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 16:07:09 +0200 Subject: [PATCH 10/13] docs(readme): link hosted doc site and local preview task Refs #29 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 1366620..750c549 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,19 @@ For Tailscale-based remote access, see [docs/tailscale.md](docs/tailscale.md). ## Docs +Browse the hosted docs site: **https://espetro.github.io/mcp-sim/** + - [Architecture](docs/architecture.md) — adapter model and separation of concerns - [Tailscale setup](docs/tailscale.md) — running over Tailscale - [Running as a service](docs/service.md) — install as a native OS service (launchd/systemd/Windows Service) - [Adding a platform](docs/adding-platform.md) — implementing the Platform interface +Run the docs site locally: + +```bash +task docs +``` + ## License Apache 2.0 From 17c8e5fc7ca61fc8893c7c9c0f9bc1279ccce876 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 16:07:10 +0200 Subject: [PATCH 11/13] ci: deploy doc site to GitHub Pages on push to main Builds docs/ via docmd (task docs-build) and publishes through the actions/upload-pages-artifact + actions/deploy-pages flow, avoiding a branch-commit deploy hack. Refs #29 --- .github/workflows/docs.yml | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..5e910c5 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,49 @@ +name: Deploy docs + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'Taskfile.yml' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + docs: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install task + run: go install github.com/go-task/task/v3/cmd/task@latest + + - name: Build docs + run: task docs-build + + - uses: actions/upload-pages-artifact@v3 + with: + path: site/ + + - uses: actions/deploy-pages@v4 + id: deploy From da1a0151606ead21e8930d6ccaf516d392de7245 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 16:12:20 +0200 Subject: [PATCH 12/13] ci(goreleaser): ship windows binaries procattr split (procattr_unix.go/procattr_windows.go) and cross-platform adapters already support Windows; GoReleaser was still darwin/linux-only. --- .goreleaser.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index 4410ed2..88d901d 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -16,6 +16,7 @@ builds: goos: - darwin - linux + - windows goarch: - amd64 - arm64 From 9e333da97bce6c13e09ee337dcc54e3d5d7b28aa Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 5 Jul 2026 16:13:50 +0200 Subject: [PATCH 13/13] chore(release): bump version references to v0.2.0 Refs #29, #27 --- AGENTS.md | 2 +- CHANGELOG.md | 23 ++++++++++++++++++++++- README.md | 2 +- internal/version/version.go | 6 +++--- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8a64adc..9ab62fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ Issues #1–#21 cover M1 milestone tasks. M2 hardening tracked separately. ## Current release -v0.1.1 — patch release with bug fixes. Branch `main` holds released commits. Use `develop` for +v0.2.0 — cross-platform support (Windows/Linux), native service install, hosted doc site. Branch `main` holds released commits. Use `develop` for new work. Versions live in `internal/version/version.go` and are injected at build time via `-ldflags "-X .../internal/version.Version=..."`. diff --git a/CHANGELOG.md b/CHANGELOG.md index f3213e1..5f5b48a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,26 @@ Nothing yet. ### Security --> +## [0.2.0] - 2026-07-05 + +### Added + +- `mcp-sim service` subcommand (install/uninstall/start/stop/restart/status/run) via `github.com/kardianos/service` — installs mcp-sim as a native background service (launchd/systemd/Windows Service), with a `--user` mode for non-root installs on macOS/Linux. +- Windows binaries in GoReleaser cross-compilation matrix (darwin/linux/windows × amd64/arm64). +- Hosted doc site at https://espetro.github.io/mcp-sim/, built from `docs/` via docmd and deployed to GitHub Pages on push to `main`. `task docs` / `task docs-build` for local preview/build. + +### Changed + +- Android adapter and agent-device controller now run correctly on Windows and Linux (previously darwin/linux-only): `setProcAttr` split into `procattr_unix.go`/`procattr_windows.go` (Setpgid vs `CREATE_NEW_PROCESS_GROUP`); Android SDK home-dir probing covers macOS/Linux/Windows defaults. +- `agent-device` controller stop now uses `cmd.Process.Kill()` instead of Unix-only `pkill -f`. +- Default config path now resolved via `os.UserHomeDir()` instead of raw `$HOME` lookup, fixing Windows. +- CI (`ci.yml`) now runs a cross-platform matrix (ubuntu/windows/macos) for build+vet+test. +- `docs/launchd.md` renamed to `docs/service.md`; README/AGENTS.md wording corrected — only iOS Simulator tools require macOS, the rest of the server runs on all three OSes. + +### Removed + +- `docs/releases/` — release notes duplicated root `CHANGELOG.md`; dropped in favor of the single changelog. + ## [0.1.1] - 2026-07-05 ### Fixed @@ -59,6 +79,7 @@ Nothing yet. - Cross-compiled releases via GoReleaser (darwin/linux/windows × amd64/arm64) - Homebrew tap `espetro/homebrew-mcp-sim` (when published) -[Unreleased]: https://github.com/espetro/mcp-sim/compare/v0.1.1...HEAD +[Unreleased]: https://github.com/espetro/mcp-sim/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/espetro/mcp-sim/compare/v0.1.1...v0.2.0 [0.1.1]: https://github.com/espetro/mcp-sim/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/espetro/mcp-sim/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 750c549..f4c1beb 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Show version: ```bash mcp-sim version -# mcp-sim 0.1.1 (commit, date) +# mcp-sim 0.2.0 (commit, date) ``` Configure your MCP client (Claude Code, Cursor, etc.): diff --git a/internal/version/version.go b/internal/version/version.go index 6592208..8008dbd 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -2,13 +2,13 @@ // // All variables are ldflags-overridable: // -// go build -ldflags "-X github.com/espetro/mcp-sim/internal/version.Version=v0.1.1 \ +// go build -ldflags "-X github.com/espetro/mcp-sim/internal/version.Version=v0.2.0 \ // -X github.com/espetro/mcp-sim/internal/version.Commit=$(git rev-parse HEAD) \ // -X github.com/espetro/mcp-sim/internal/version.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" package version -// Version is the semantic version of mcp-sim (e.g. "0.1.1"). -var Version = "0.1.1" +// Version is the semantic version of mcp-sim (e.g. "0.2.0"). +var Version = "0.2.0" // Commit is the git commit SHA the binary was built from. var Commit = "none"