Skip to content
Open
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
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Transform any Cobra CLI into an MCP server**

Ophis automatically converts your Cobra commands into MCP tools, and provides CLI commands for integration with Claude Desktop, VSCode, and Cursor.
Ophis automatically converts your Cobra commands into MCP tools, and provides CLI commands for integration with Claude Desktop, VSCode, Cursor, and Zed.

## Quick Start

Expand Down Expand Up @@ -32,7 +32,7 @@ func main() {
}
```

### Enable in Claude Desktop, VSCode, or Cursor
### Enable in Claude Desktop, VSCode, Cursor, or Zed

```bash
# Claude Desktop
Expand All @@ -44,6 +44,9 @@ func main() {

# Cursor
./my-cli mcp cursor enable

# Zed
./my-cli mcp zed enable
```

Your CLI commands are now available as MCP tools!
Expand Down Expand Up @@ -73,10 +76,14 @@ mcp
│ ├── enable # Add server to VSCode config
│ ├── disable # Remove server from VSCode config
│ └── list # List VSCode MCP servers
└── cursor
├── enable # Add server to Cursor config
├── disable # Remove server from Cursor config
└── list # List Cursor MCP servers
├── cursor
│ ├── enable # Add server to Cursor config
│ ├── disable # Remove server from Cursor config
│ └── list # List Cursor MCP servers
└── zed
├── enable # Add server to Zed config
├── disable # Remove server from Zed config
└── list # List Zed MCP servers
```

## Configuration
Expand Down
2 changes: 1 addition & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Always filtered regardless of configuration:

## DefaultEnv

Editors like Claude Desktop, VSCode, and Cursor launch MCP server subprocesses with a minimal environment. On macOS this typically means a PATH of just `/usr/bin:/bin:/usr/sbin:/sbin`, which cannot find executables managed by mise, asdf, homebrew, nix, or installed to non-standard paths.
Editors like Claude Desktop, VSCode, Cursor, and Zed launch MCP server subprocesses with a minimal environment. On macOS this typically means a PATH of just `/usr/bin:/bin:/usr/sbin:/sbin`, which cannot find executables managed by mise, asdf, homebrew, nix, or installed to non-standard paths.

`DefaultEnv` specifies environment variables that are automatically included when `enable` writes a server config for any editor. These are merged with user-provided `--env` values; user values take precedence on conflict.

Expand Down
8 changes: 6 additions & 2 deletions examples/make/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ go 1.24.6
replace github.com/njayp/ophis => ../../

require (
github.com/njayp/ophis v1.1.3
github.com/njayp/ophis v1.1.4
github.com/spf13/cobra v1.10.2
)

require (
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/modelcontextprotocol/go-sdk v1.3.0 // indirect
github.com/modelcontextprotocol/go-sdk v1.3.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.3 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)
12 changes: 10 additions & 2 deletions examples/make/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,31 @@ github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbc
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/modelcontextprotocol/go-sdk v1.3.0 h1:gMfZkv3DzQF5q/DcQePo5rahEY+sguyPfXDfNBcT0Zs=
github.com/modelcontextprotocol/go-sdk v1.3.0/go.mod h1:AnQ//Qc6+4nIyyrB4cxBU7UW9VibK4iOZBeyP/rF1IE=
github.com/modelcontextprotocol/go-sdk v1.3.1 h1:TfqtNKOIWN4Z1oqmPAiWDC2Jq7K9OdJaooe0teoXASI=
github.com/modelcontextprotocol/go-sdk v1.3.1/go.mod h1:DgVX498dMD8UJlseK1S5i1T4tFz2fkBk4xogC3D15nw=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a h1:a6TNDN9CgG+cYjaeN8l2mc4kSz2iMiCDQxPEyltUV/I=
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ go 1.24.6

require (
github.com/google/jsonschema-go v0.4.2
github.com/modelcontextprotocol/go-sdk v1.3.0
github.com/modelcontextprotocol/go-sdk v1.3.1
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a
)

require (
Expand All @@ -16,8 +17,11 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.3 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/tools v0.37.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
12 changes: 10 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,35 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/modelcontextprotocol/go-sdk v1.3.0 h1:gMfZkv3DzQF5q/DcQePo5rahEY+sguyPfXDfNBcT0Zs=
github.com/modelcontextprotocol/go-sdk v1.3.0/go.mod h1:AnQ//Qc6+4nIyyrB4cxBU7UW9VibK4iOZBeyP/rF1IE=
github.com/modelcontextprotocol/go-sdk v1.3.1 h1:TfqtNKOIWN4Z1oqmPAiWDC2Jq7K9OdJaooe0teoXASI=
github.com/modelcontextprotocol/go-sdk v1.3.1/go.mod h1:DgVX498dMD8UJlseK1S5i1T4tFz2fkBk4xogC3D15nw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a h1:a6TNDN9CgG+cYjaeN8l2mc4kSz2iMiCDQxPEyltUV/I=
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
54 changes: 54 additions & 0 deletions internal/cfgmgr/cmd/zed/disable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package zed

import (
"fmt"
"os"

"github.com/njayp/ophis/internal/cfgmgr/manager"
"github.com/spf13/cobra"
)

type disableFlags struct {
configPath string
serverName string
workspace bool
}

// disableCommand creates a Cobra command for removing an MCP server from Zed.
func disableCommand() *cobra.Command {
f := &disableFlags{}
cmd := &cobra.Command{
Use: "disable",
Short: "Remove server from Zed config",
Long: "Remove this application from Zed MCP context servers",
RunE: func(_ *cobra.Command, _ []string) error {
return f.run()
},
}

// Add flags
flags := cmd.Flags()
flags.StringVar(&f.configPath, "config-path", "", "Path to Zed settings file")
flags.StringVar(&f.serverName, "server-name", "", "Name of the MCP server to remove (default: derived from executable name)")
flags.BoolVar(&f.workspace, "workspace", false, "Remove from workspace settings (.zed/settings.json) instead of user settings")

return cmd
}

func (f *disableFlags) run() error {
if f.serverName == "" {
executablePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to determine executable path: %w", err)
}

f.serverName = manager.DeriveServerName(executablePath)
}

m, err := manager.NewZedManager(f.configPath, f.workspace)
if err != nil {
return err
}

return m.DisableServer(f.serverName)
}
14 changes: 14 additions & 0 deletions internal/cfgmgr/cmd/zed/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Package zed provides CLI commands for managing Zed MCP context servers.
//
// This package implements the 'mcp zed' subcommands:
// - enable: Add MCP server to Zed configuration
// - disable: Remove MCP server from Zed configuration
// - list: Show all configured MCP servers
//
// Supports both workspace (.zed/settings.json) and user-level configurations
// (~/.config/zed/settings.json). When modifying the settings file, all existing
// Zed settings (theme, fonts, keybindings, etc.) are preserved unchanged.
//
// This is an internal package and should not be imported directly by users of the ophis library.
// These commands are automatically available when using ophis.Command() in your application.
package zed
93 changes: 93 additions & 0 deletions internal/cfgmgr/cmd/zed/enable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package zed

import (
"fmt"
"os"

"github.com/njayp/ophis/internal/cfgmgr/manager"
"github.com/njayp/ophis/internal/cfgmgr/manager/zed"
"github.com/spf13/cobra"
)

type enableFlags struct {
commandName string
defaultEnv map[string]string
configPath string
logLevel string
serverName string
workspace bool
env map[string]string
}

// enableCommand creates a Cobra command for adding an MCP server to Zed.
// commandName is the Use name of the ophis root command (e.g. "mcp" or "agent").
// defaultEnv is merged into the server env; user-provided --env values take precedence.
func enableCommand(commandName string, defaultEnv map[string]string) *cobra.Command {
f := &enableFlags{commandName: commandName, defaultEnv: defaultEnv}
cmd := &cobra.Command{
Use: "enable",
Short: "Add server to Zed config",
Long: "Add this application as an MCP context server in Zed",
RunE: func(cmd *cobra.Command, _ []string) error {
return f.run(cmd)
},
}

// Add flags
flags := cmd.Flags()
flags.StringVar(&f.logLevel, "log-level", "", "Log level (debug, info, warn, error)")
flags.StringVar(&f.configPath, "config-path", "", "Path to Zed settings file")
flags.StringVar(&f.serverName, "server-name", "", "Name for the MCP server (default: derived from executable name)")
flags.BoolVar(&f.workspace, "workspace", false, "Add to workspace settings (.zed/settings.json) instead of user settings")
flags.StringToStringVarP(&f.env, "env", "e", nil, "Environment variables (e.g., --env KEY1=value1 --env KEY2=value2)")

return cmd
}

func (f *enableFlags) run(cmd *cobra.Command) error {
// Get the current executable path
executablePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to determine executable path: %w", err)
}

// Build server configuration
mcpPath, err := manager.GetCmdPath(cmd, f.commandName)
if err != nil {
return fmt.Errorf("failed to determine MCP command path: %w", err)
}

server := zed.Server{
Command: executablePath,
Args: append(mcpPath, "start"),
}

// Add log level to args if specified
if f.logLevel != "" {
server.Args = append(server.Args, "--log-level", f.logLevel)
}

// Merge default env with user-provided env.
// User values take precedence on conflict.
env := make(map[string]string)
for k, v := range f.defaultEnv {
env[k] = v
}
for k, v := range f.env {
env[k] = v
}
if len(env) > 0 {
server.Env = env
}

if f.serverName == "" {
f.serverName = manager.DeriveServerName(executablePath)
}

m, err := manager.NewZedManager(f.configPath, f.workspace)
if err != nil {
return err
}

return m.EnableServer(f.serverName, server)
}
44 changes: 44 additions & 0 deletions internal/cfgmgr/cmd/zed/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package zed

import (
"fmt"

"github.com/njayp/ophis/internal/cfgmgr/manager"
"github.com/spf13/cobra"
)

type listFlags struct {
configPath string
workspace bool
}

// listCommand creates a Cobra command for listing configured MCP servers in Zed.
func listCommand() *cobra.Command {
f := &listFlags{}
cmd := &cobra.Command{
Use: "list",
Short: "Show Zed MCP context servers",
Long: "Show all MCP context servers configured in Zed",
RunE: func(_ *cobra.Command, _ []string) error {
return f.run()
},
}

// Add flags
flags := cmd.Flags()
flags.StringVar(&f.configPath, "config-path", "", "Path to Zed settings file")
flags.BoolVar(&f.workspace, "workspace", false, "List from workspace settings (.zed/settings.json) instead of user settings")

return cmd
}

func (f *listFlags) run() error {
m, err := manager.NewZedManager(f.configPath, f.workspace)
if err != nil {
return err
}

fmt.Printf("Zed MCP context servers:\n\n")
m.Print()
return nil
}
Loading
Loading