detectharness

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 18 Imported by: 0

README

detect-harness

release CI Go Reference license platform

Add a polished MCP installer to any Go server. Define one stdio server once, detect the AI harnesses on the machine, and let detect-harness generate and safely update every client-specific configuration.

Install the Go library:

go get github.com/sairaph/detect-harness@latest

Then detect and configure selected harnesses:

installer, err := detectharness.New(detectharness.StdioServer{
    Name:    "my-mcp",
    Command: "/absolute/path/to/my-mcp",
    Args:    []string{"mcp"},
    Env:     map[string]string{"MY_MCP_MODE": "write"},
})
if err != nil {
    log.Fatal(err)
}

ctx := context.Background()
detected := installer.Detect(ctx)

plan := installer.Plan(ctx, []detectharness.ID{
    detectharness.Cursor,
    detectharness.Codex,
}, detectharness.Present, detectharness.PlanOptions{})

// Show plan.Changes() in your CLI, ask for confirmation, then apply it.
results := installer.Apply(ctx, plan)

The library does not provide a TUI or impose an installer flow. Your CLI owns selection and presentation; detect-harness owns detection, valid config generation, conflict handling, and safe persistence.

Need the language-neutral companion for a Node, Python, Rust, or other server?

go install github.com/sairaph/detect-harness/cmd/detect-harness@latest

Set DETECT_HARNESS_BIN to that executable, or pass its path directly to a wrapper. Tagged releases also include native binaries and packaged wrappers for all supported platforms and languages.

Why detect-harness

MCP clients agree on the protocol but not on where or how they store servers. An installer otherwise needs to independently solve client detection, platform paths, JSONC comments, TOML tables, YAML lists, collisions, concurrent edits, permissions, and uninstall behavior.

detect-harness provides one reusable implementation:

  • One canonical definition - a single StdioServer becomes valid config for every supported client.
  • Evidence-based detection - present, absent, and unavailable are distinct; detection failures are never silently treated as absence.
  • Plan before apply - inspect changes, show a confirmation UI, then apply the exact snapshots that were planned.
  • Safe by default - same-name foreign entries are conflicts unless the caller explicitly opts into replacement.
  • Format aware - JSON, JSONC, TOML, and YAML receive their required native shapes while unrelated settings and comments are retained.
  • Cross-language - use the Go package directly or invoke the versioned JSON companion through typed Node, Python, and Rust wrappers.

Supported harnesses

ID Harness Config shape
claude-desktop Claude Desktop JSON mcpServers
claude-code Claude Code JSON mcpServers
cursor Cursor JSON mcpServers
codex Codex CLI TOML mcp_servers
gemini-cli Gemini CLI JSON mcpServers
windsurf Windsurf JSON mcpServers
zed Zed JSONC context_servers
cline Cline JSON mcpServers
zoo-code Zoo Code ^ JSON mcpServers
amazon-q Amazon Q Developer CLI JSON mcpServers
continue Continue YAML mcpServers list
opencode OpenCode JSONC mcp local command
vscode VS Code / Copilot JSONC servers with stdio type

^ The deprecated roo-code id is accepted as an input alias and resolves to zoo-code.

Paths are resolved per platform using the user home, XDG configuration root, and authoritative Windows application-data directories.

Core API

Detect
detections := installer.Detect(ctx)

Each result includes a stable harness ID, state, evidence, resolved config path, and reload hint. Detection can also run without a server definition:

detections, err := detectharness.DetectHarnesses(ctx, detectharness.DetectOptions{})
Plan and apply
plan := installer.Plan(ctx, selected, detectharness.Present, detectharness.PlanOptions{})
changes := plan.Changes()
results := installer.Apply(ctx, plan)

Use detectharness.Absent to remove the registration. Installer.Ensure is a plan-and-apply convenience method for unattended flows.

Render without writing
config, err := detectharness.RenderConfig(detectharness.VSCode, server)

This is useful for previews, documentation, fixtures, and installers that own their persistence layer.

Project scope

Version 0.2 adds directory-local (per-project) configuration support for the 10 harnesses that provide it:

Harness Project file Shareable Reload hint
Claude Code .mcp.json Yes (commit) restart session (approval gate)
Cursor .cursor/mcp.json Yes restart Cursor
Codex CLI .codex/config.toml Yes (trust gate) restart Codex
Gemini CLI .gemini/settings.json Yes (trust gate) /mcp reload
Zed .zed/settings.json Yes live (watched)
Zoo Code .roo/mcp.json Yes hot-reload (watched)
Amazon Q .amazonq/mcp.json Yes restart session
Continue .continue/mcpServers/detect-harness.yaml Yes hot-reload on save
OpenCode opencode.json Yes restart OpenCode
VS Code .vscode/mcp.json Yes (trust gate) MCP: List Servers

Use detectharness.ProjectScopeDir(dir) to select a project scope and pass it through PlanOptions or DetectOptions:

scope := detectharness.ProjectScopeDir("/path/to/project")

// Project-scoped detection: reports which project files exist.
detections, err := detectharness.DetectHarnesses(ctx, detectharness.DetectOptions{
    Scope: scope,
})

// Project-scoped planning: writes to the project-local config file.
plan := installer.Plan(ctx, selected, detectharness.Present, detectharness.PlanOptions{
    Scope: scope,
})

Harnesses without project support (claude-desktop, windsurf, cline) return unavailable / skipped — never an error that aborts a multi-harness operation. The zero-value Scope (global) preserves existing behavior exactly.

Resolve conflicts

The default ConflictError policy only updates or removes entries that exactly match the canonical server definition:

options := detectharness.PlanOptions{
    ConflictPolicy: detectharness.ConflictReplace,
}

Use ConflictReplace only when your installer owns the stable server name and intentionally wants to migrate its executable path or environment.

Companion protocol

The detect-harness binary reads exactly one versioned JSON request from stdin and writes exactly one JSON response to stdout. Secrets never appear in process arguments.

printf '%s' '{"version":1,"operation":"detect"}' | detect-harness

Operations:

Operation Purpose
detect Return all harness detections and evidence
render Generate one standalone harness configuration
update Plan selected additions/removals and optionally apply them

The machine-readable contract lives in protocol/, including Draft 2020-12 request and response schemas.

Language wrappers

Language Package source API
Node / TypeScript wrappers/node DetectHarnessClient
Python wrappers/python Client
Rust wrappers/rust Client

All wrappers expose typed detect, render, plan, and update calls. They resolve an explicit binary path first, then DETECT_HARNESS_BIN, then detect-harness on PATH. They invoke without a shell, bound process output, support timeouts, and validate protocol responses.

From a source checkout:

npm ci --prefix wrappers/node
npm run build --prefix wrappers/node
# Then install /path/to/detect-harness/wrappers/node in your application.
python -m pip install ./wrappers/python
cargo add --path wrappers/rust

Every tagged GitHub release contains the npm tarball, Python wheel and source distribution, Rust crate, protocol schemas, checksums, and native companion binaries for macOS, Linux, and Windows on AMD64 and ARM64.

Safety model

  • Invalid roots, wrong container types, duplicate JSON keys, oversized configs, and multi-document YAML are rejected without writes.
  • JSONC comments, TOML source outside managed tables, YAML nodes, and unrelated settings are retained.
  • Plans retain snapshots and recheck immediately before atomic publication.
  • New same-directory temporary files use restrictive permissions; existing config permissions are retained, and staged content is flushed before replacement.
  • Config targets that are symbolic links are rejected.
  • Library operations are serialized by short-lived lock files; locks older than five minutes are treated as stale and recovered.
  • Multi-harness changes report partial results instead of hiding failures.
  • Serialized protocol plans do not include complete user config contents.

Build and test

Requirements: Go 1.22+, Node 18+, Python 3.10+, and Rust 1.71+.

go test ./...
go vet ./...

npm test --prefix wrappers/node
PYTHONPATH=wrappers/python/src python -m unittest discover -s wrappers/python/tests -v
cargo test --manifest-path wrappers/rust/Cargo.toml

Build the companion for the current platform:

go build -o detect-harness ./cmd/detect-harness

Releases

The root package.json is the version authority. Update every wrapper manifest together with:

npm run version:set -- 0.2.0
npm run version:check

When that version change reaches main, the release workflow verifies all manifests, creates v<version>, runs the full test suite, and publishes one GitHub Release containing every binary and wrapper format. A manually pushed version tag must match the root version and point to a commit on main.

Transport support

Version 1 manages local stdio MCP servers. Remote HTTP and SSE transports need client-specific capability handling and are intentionally not implied by the current API.

License

MIT. Copyright © 2026 Łael Al-Halawani.

Documentation

Overview

Package detectharness detects AI harnesses and safely manages stdio MCP server registrations across their native configuration formats.

Configuration can be managed in two scopes: the zero-value global scope (system/user configuration) and a project scope that targets a directory-local configuration for harnesses that support per-project MCP overrides. Use Scope and ProjectScopeDir to select a scope.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsSupported

func IsSupported(id ID) bool

IsSupported reports whether id identifies a built-in harness. Deprecated aliases (for example "roo-code") are accepted and resolve to their canonical harness.

func RenderConfig

func RenderConfig(id ID, server StdioServer) (string, error)

RenderConfig generates a complete global configuration containing only server for one harness. It does not inspect or mutate the filesystem.

func RenderConfigScoped added in v0.2.0

func RenderConfigScoped(id ID, server StdioServer, scope Scope) (string, error)

RenderConfigScoped generates a complete configuration containing only server for one harness in the requested scope. For project scope it targets the harness's directory-local file; the directory itself is not inspected or created. An unsupported project scope returns an error.

Types

type ApplyState

type ApplyState string

ApplyState is the outcome of applying one planned change.

const (
	Applied       ApplyState = "applied"
	ApplyNoop     ApplyState = "noop"
	ApplySkipped  ApplyState = "skipped"
	ApplyConflict ApplyState = "conflict"
	ApplyFailed   ApplyState = "failed"
)

type Change

type Change struct {
	HarnessID ID           `json:"harnessId"`
	Name      string       `json:"name"`
	Path      string       `json:"path,omitempty"`
	Desired   DesiredState `json:"desired"`
	State     ChangeState  `json:"state"`
	Action    string       `json:"action,omitempty"`
	Reason    string       `json:"reason,omitempty"`
	Scope     ScopeMode    `json:"scope,omitempty"`
	ScopeDir  string       `json:"scopeDir,omitempty"`
	Before    string       `json:"-"`
	After     string       `json:"-"`
}

Change is a serializable description of one planned harness change.

type ChangeState

type ChangeState string

ChangeState is the outcome of planning one harness.

const (
	ChangeReady       ChangeState = "ready"
	ChangeNoop        ChangeState = "noop"
	ChangeConflict    ChangeState = "conflict"
	ChangeUnavailable ChangeState = "unavailable"
)

type ConflictPolicy

type ConflictPolicy string

ConflictPolicy controls same-name entries that do not exactly match Server.

const (
	ConflictError   ConflictPolicy = "error"
	ConflictReplace ConflictPolicy = "replace"
)

type DesiredState

type DesiredState string

DesiredState describes the registration state a plan should establish.

const (
	Present DesiredState = "present"
	Absent  DesiredState = "absent"
)

type DetectOptions

type DetectOptions struct {
	Platform string
	HomeDir  string
	Env      map[string]string
	// Scope selects global (zero value) or project-scoped detection.
	Scope Scope
}

DetectOptions overrides the host environment. It is primarily useful for tests and sandboxes.

type Detection

type Detection struct {
	Harness
	State       DetectionState `json:"state"`
	Evidence    []string       `json:"evidence,omitempty"`
	Reason      string         `json:"reason,omitempty"`
	ConfigPath  string         `json:"configPath,omitempty"`
	ConfigError string         `json:"configError,omitempty"`
	Scope       ScopeMode      `json:"scope,omitempty"`
	ScopeDir    string         `json:"scopeDir,omitempty"`
}

Detection is the result of probing one harness.

func DetectHarnesses

func DetectHarnesses(ctx context.Context, options DetectOptions) ([]Detection, error)

DetectHarnesses probes all built-in harnesses without requiring an MCP server definition. Zero-valued options use the current host environment.

type DetectionState

type DetectionState string

DetectionState distinguishes absence from an environment that could not be inspected.

const (
	Detected    DetectionState = "present"
	NotDetected DetectionState = "absent"
	Unavailable DetectionState = "unavailable"
)

type Harness

type Harness struct {
	ID         ID     `json:"id"`
	Name       string `json:"name"`
	ReloadHint string `json:"reloadHint"`
	// Project describes project-scoped (directory-local) configuration support.
	// It is nil for harnesses that only support a global configuration.
	Project *ProjectScope `json:"project,omitempty"`
}

Harness describes a supported AI harness without probing the local machine.

func Supported

func Supported() []Harness

Supported returns the ordered built-in harness catalog.

type ID

type ID string

ID is the stable identifier of a supported AI harness.

const (
	ClaudeDesktop ID = "claude-desktop"
	ClaudeCode    ID = "claude-code"
	Cursor        ID = "cursor"
	Codex         ID = "codex"
	GeminiCLI     ID = "gemini-cli"
	Windsurf      ID = "windsurf"
	Zed           ID = "zed"
	Cline         ID = "cline"
	RooCode       ID = "roo-code" // Deprecated: canonical id is ZooCode; kept as an alias for backward compatibility.
	ZooCode       ID = "zoo-code"
	AmazonQ       ID = "amazon-q"
	Continue      ID = "continue"
	OpenCode      ID = "opencode"
	VSCode        ID = "vscode"
)

func CanonicalID added in v0.2.0

func CanonicalID(id ID) ID

CanonicalID resolves deprecated harness ids (for example "roo-code") to their canonical replacement. It is exported so protocol-level callers can deduplicate ids consistently with the registry.

type Installer

type Installer struct {
	// contains filtered or unexported fields
}

Installer detects harnesses and manages one MCP server registration.

func New

func New(server StdioServer, options ...Option) (*Installer, error)

New validates server once and returns a reusable installer.

func (*Installer) Apply

func (i *Installer) Apply(ctx context.Context, plan *Plan) []Result

Apply writes ready changes from a plan and returns one result per change.

func (*Installer) Detect

func (i *Installer) Detect(ctx context.Context) []Detection

Detect probes every supported harness using global configuration.

func (*Installer) Ensure

func (i *Installer) Ensure(ctx context.Context, ids []ID, desired DesiredState, options PlanOptions) []Result

Ensure plans and applies a selection in one call. Use Plan and Apply separately when showing a preview or requiring user confirmation.

func (*Installer) Plan

func (i *Installer) Plan(ctx context.Context, ids []ID, desired DesiredState, options PlanOptions) *Plan

Plan inspects selected harnesses and computes changes without writing files.

type Option

type Option func(*installerOptions) error

Option customizes an Installer.

func WithEnvironment

func WithEnvironment(options DetectOptions) Option

WithEnvironment overrides platform, home, and environment resolution.

type Plan

type Plan struct {
	// contains filtered or unexported fields
}

Plan is an immutable set of configuration changes. Its internal snapshots are retained so Apply can reject files changed after planning.

func (*Plan) Changes

func (p *Plan) Changes() []Change

Changes returns a copy suitable for display or serialization.

type PlanOptions

type PlanOptions struct {
	ConflictPolicy ConflictPolicy
	// Scope selects global (zero value) or project-scoped planning.
	Scope Scope
}

PlanOptions controls planning without performing writes.

type ProjectScope added in v0.2.0

type ProjectScope struct {
	// Path is the canonical project file location relative to the project
	// directory (for example ".mcp.json").
	Path string `json:"path"`
	// ReloadHint describes how a user picks up project-scope changes.
	ReloadHint string `json:"reloadHint"`
	// Lifecycle summarizes creation, merge, and preemptive-create behavior.
	Lifecycle string `json:"lifecycle"`
	// Shareable indicates the file is intended to be committed to version control.
	Shareable bool `json:"shareable"`
	// TrustGate indicates the harness gates project servers behind a trust or
	// approval dialog before they are loaded.
	TrustGate bool `json:"trustGate"`
}

ProjectScope describes how a harness supports directory-local MCP configuration. It is informational metadata for library consumers building install UX; detect-harness never creates files unless asked to plan/apply.

type Result

type Result struct {
	HarnessID ID           `json:"harnessId"`
	Name      string       `json:"name"`
	Path      string       `json:"path,omitempty"`
	Desired   DesiredState `json:"desired"`
	State     ApplyState   `json:"state"`
	Action    string       `json:"action,omitempty"`
	Reason    string       `json:"reason,omitempty"`
	Scope     ScopeMode    `json:"scope,omitempty"`
	ScopeDir  string       `json:"scopeDir,omitempty"`
}

Result reports the outcome for one harness without hiding partial success.

type Scope added in v0.2.0

type Scope struct {
	Mode ScopeMode `json:"mode,omitempty"`
	// Dir is the project directory. It is required when Mode is ScopeProject and
	// ignored otherwise. Relative paths are resolved against the process work
	// directory. Absolute paths are recommended.
	Dir string `json:"dir,omitempty"`
}

Scope selects where configuration is detected and applied. The zero value is global scope and preserves existing behavior.

func ProjectScopeDir added in v0.2.0

func ProjectScopeDir(dir string) Scope

ProjectScope returns a scope that targets directory-local configuration in dir.

type ScopeMode added in v0.2.0

type ScopeMode string

ScopeMode selects the configuration scope.

const (
	// ScopeGlobal is the zero-value scope: system/user configuration used today.
	// It matches existing global behavior exactly.
	ScopeGlobal ScopeMode = ""
	// ScopeProject targets a directory-local (per-project) configuration.
	ScopeProject ScopeMode = "project"
)

type StdioServer

type StdioServer struct {
	Name    string            `json:"name"`
	Command string            `json:"command"`
	Args    []string          `json:"args,omitempty"`
	Env     map[string]string `json:"env,omitempty"`
}

StdioServer is the single, client-independent definition of a local MCP server.

Directories

Path Synopsis
cmd
detect-harness command
Package protocol defines the stable JSON contract used by the companion binary and language wrappers.
Package protocol defines the stable JSON contract used by the companion binary and language wrappers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL