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
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ jobs:
Review this PR. Don't nitpick style. Either:
- Approve if there are no significant issues
- Leave comments for minor suggestions
- Request changes for bugs, security issues, or significant design problems
- Request changes for bugs, security issues, or significant design problems
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Expose your MCP server over HTTP for remote access:

## Commands

The `ophis.Command(nil)` adds these subcommands to your CLI:
The `ophis.Command(nil)` adds these subcommands to your CLI (the default command name is `mcp`, configurable via `Config.CommandName`):

```
mcp
Expand Down Expand Up @@ -104,6 +104,20 @@ config := &ophis.Config{
rootCmd.AddCommand(ophis.Command(config))
```

### Custom Command Name

By default the ophis command is named `mcp`. If your CLI already uses `mcp` for something else, set `CommandName` to avoid the collision:

```go
config := &ophis.Config{
CommandName: "agent",
}

rootCmd.AddCommand(ophis.Command(config))
```

The command tree, editor config (`enable`/`disable`), and internal filters all use the configured name automatically.

See [docs/config.md](docs/config.md) for detailed configuration options.

## How It Works
Expand Down
26 changes: 21 additions & 5 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ import (

// Config customizes MCP server behavior and command-to-tool conversion.
type Config struct {
// CommandName is the Use name for the top-level command returned by Command().
// It is also used by GetCmdPath to locate the ophis command in the Cobra tree
// and by cmdFilter to exclude ophis subcommands from tool exposure.
// Default: "mcp".
CommandName string

// Selectors defines rules for converting commands to MCP tools.
// Each selector specifies which commands to match and which flags to include.
//
// Basic safety filters are always applied first:
// - Hidden/deprecated commands and flags are excluded
// - Non-runnable commands are excluded
// - Built-in commands (mcp, help, completion) are excluded
// - Built-in commands (the ophis command, help, completion) are excluded
//
// Then selectors are evaluated in order for each command:
// 1. The first selector whose CmdSelector returns true is used
Expand Down Expand Up @@ -54,6 +60,14 @@ type Config struct {
toolNamePrefix string // resolved prefix (either ToolNamePrefix or root command name)
}

// commandName returns the configured CommandName, defaulting to "mcp".
func (c *Config) commandName() string {
if c != nil && c.CommandName != "" {
return c.CommandName
}
return "mcp"
}

func (c *Config) serveStdio(cmd *cobra.Command) error {
if c.Transport == nil {
c.Transport = &mcp.StdioTransport{}
Expand Down Expand Up @@ -136,7 +150,7 @@ func (c *Config) registerToolsRecursive(cmd *cobra.Command) {
}

// apply basic filters
if cmdFilter(cmd) {
if c.cmdFilter(cmd) {
return
}

Expand All @@ -161,8 +175,10 @@ func (c *Config) registerToolsRecursive(cmd *cobra.Command) {
}
}

// cmdFilter returns true if cmd should be filtered out
func cmdFilter(cmd *cobra.Command) bool {
// cmdFilter returns true if cmd should be filtered out.
// It uses the configured CommandName (defaulting to "mcp") to exclude
// the ophis command group from being exposed as MCP tools.
func (c *Config) cmdFilter(cmd *cobra.Command) bool {
if cmd.Hidden || cmd.Deprecated != "" {
return true
}
Expand All @@ -171,5 +187,5 @@ func cmdFilter(cmd *cobra.Command) bool {
return true
}

return AllowCmdsContaining("mcp", "help", "completion")(cmd)
return AllowCmdsContaining(c.commandName(), "help", "completion")(cmd)
}
43 changes: 42 additions & 1 deletion config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,49 @@ func TestCmdFilter(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := cmdFilter(tt.cmd)
// Default config uses "mcp" as command name.
c := &Config{}
result := c.cmdFilter(tt.cmd)
assert.Equal(t, tt.expected, result)
})
}
}

func TestCmdFilterCustomCommandName(t *testing.T) {
c := &Config{CommandName: "agent"}

// "agent" should be filtered out (it is the ophis command name).
agentCmd := &cobra.Command{
Use: "agent",
Run: func(_ *cobra.Command, _ []string) {},
}
assert.True(t, c.cmdFilter(agentCmd))

// "mcp" should NOT be filtered out (it is no longer the ophis command name).
mcpCmd := &cobra.Command{
Use: "mcp",
Run: func(_ *cobra.Command, _ []string) {},
}
assert.False(t, c.cmdFilter(mcpCmd))

// A normal command should pass through.
normalCmd := &cobra.Command{
Use: "status",
Run: func(_ *cobra.Command, _ []string) {},
}
assert.False(t, c.cmdFilter(normalCmd))
}

func TestCommandNameDefault(t *testing.T) {
// Empty CommandName defaults to "mcp".
c := &Config{}
assert.Equal(t, "mcp", c.commandName())

// Explicit CommandName is used as-is.
c = &Config{CommandName: "agent"}
assert.Equal(t, "agent", c.commandName())

// Nil receiver defaults to "mcp" (Command(nil) is a valid call).
var nilConfig *Config
assert.Equal(t, "mcp", nilConfig.commandName())
}
2 changes: 1 addition & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Always filtered regardless of configuration:

- Hidden and deprecated commands/flags
- Non-runnable commands (no Run/RunE)
- Built-in commands (mcp, help, completion)
- Built-in commands (the ophis command, help, completion). The ophis command name defaults to "mcp" but can be changed via Config.CommandName

## Examples

Expand Down
2 changes: 1 addition & 1 deletion examples/make/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.24.6
replace github.com/njayp/ophis => ../../

require (
github.com/njayp/ophis v1.1.1
github.com/njayp/ophis v1.1.2
github.com/spf13/cobra v1.10.2
)

Expand Down
15 changes: 8 additions & 7 deletions internal/cfgmgr/cmd/claude/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import (
)

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

// enableCommand creates a Cobra command for adding an MCP server to Claude Desktop.
func enableCommand() *cobra.Command {
f := &enableFlags{}
func enableCommand(commandName string) *cobra.Command {
f := &enableFlags{commandName: commandName}
cmd := &cobra.Command{
Use: "enable",
Short: "Add server to Claude config",
Expand All @@ -45,7 +46,7 @@ func (f *enableFlags) run(cmd *cobra.Command) error {
}

// Build server configuration
mcpPath, err := manager.GetCmdPath(cmd)
mcpPath, err := manager.GetCmdPath(cmd, f.commandName)
if err != nil {
return fmt.Errorf("failed to determine MCP command path: %w", err)
}
Expand Down
6 changes: 4 additions & 2 deletions internal/cfgmgr/cmd/claude/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import (
)

// Command creates a new Cobra command for managing Claude Desktop MCP servers.
func Command() *cobra.Command {
// commandName is the name of the ophis root command (e.g. "mcp" or "agent"),
// used by enable to build the correct command path for editor config files.
func Command(commandName string) *cobra.Command {
cmd := &cobra.Command{
Use: "claude",
Short: "Manage Claude Desktop MCP servers",
Long: "Manage MCP server configuration for Claude Desktop",
}

// Add subcommands
cmd.AddCommand(enableCommand(), disableCommand(), listCommand())
cmd.AddCommand(enableCommand(commandName), disableCommand(), listCommand())
return cmd
}
18 changes: 10 additions & 8 deletions internal/cfgmgr/cmd/cursor/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ import (
)

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

// enableCommand creates a Cobra command for adding an MCP server to Cursor.
func enableCommand() *cobra.Command {
f := &enableFlags{}
// commandName is the name of the ophis root command in the Cobra tree.
func enableCommand(commandName string) *cobra.Command {
f := &enableFlags{commandName: commandName}
cmd := &cobra.Command{
Use: "enable",
Short: "Add server to Cursor config",
Expand Down Expand Up @@ -48,7 +50,7 @@ func (f *enableFlags) run(cmd *cobra.Command) error {
}

// Build server configuration
mcpPath, err := manager.GetCmdPath(cmd)
mcpPath, err := manager.GetCmdPath(cmd, f.commandName)
if err != nil {
return fmt.Errorf("failed to determine MCP command path: %w", err)
}
Expand Down
6 changes: 4 additions & 2 deletions internal/cfgmgr/cmd/cursor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import (
)

// Command creates a new Cobra command for managing Cursor MCP servers.
func Command() *cobra.Command {
// commandName is the Use name of the ophis root command (e.g. "mcp" or "agent"),
// threaded through to enableCommand so that GetCmdPath can locate it.
func Command(commandName string) *cobra.Command {
cmd := &cobra.Command{
Use: "cursor",
Short: "Manage Cursor MCP servers",
Long: "Manage MCP server configuration for Cursor",
}

// Add subcommands
cmd.AddCommand(enableCommand(), disableCommand(), listCommand())
cmd.AddCommand(enableCommand(commandName), disableCommand(), listCommand())
return cmd
}
18 changes: 10 additions & 8 deletions internal/cfgmgr/cmd/vscode/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ import (
)

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

// enableCommand creates a Cobra command for adding an MCP server to VSCode.
func enableCommand() *cobra.Command {
f := &enableFlags{}
// commandName is the Use name of the ophis root command (e.g. "mcp" or "agent").
func enableCommand(commandName string) *cobra.Command {
f := &enableFlags{commandName: commandName}
cmd := &cobra.Command{
Use: "enable",
Short: "Add server to VSCode config",
Expand Down Expand Up @@ -48,7 +50,7 @@ func (f *enableFlags) run(cmd *cobra.Command) error {
}

// Build server configuration
mcpPath, err := manager.GetCmdPath(cmd)
mcpPath, err := manager.GetCmdPath(cmd, f.commandName)
if err != nil {
return fmt.Errorf("failed to determine MCP command path: %w", err)
}
Expand Down
6 changes: 4 additions & 2 deletions internal/cfgmgr/cmd/vscode/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import (
)

// Command creates a new Cobra command for managing VSCode MCP servers.
func Command() *cobra.Command {
// commandName is the name of the ophis command in the Cobra tree (e.g. "mcp" or "agent"),
// used by enable to build the correct command path for editor config files.
func Command(commandName string) *cobra.Command {
cmd := &cobra.Command{
Use: "vscode",
Short: "Manage VSCode MCP servers",
Long: "Manage MCP server configuration for Visual Studio Code",
}

// Add subcommands
cmd.AddCommand(enableCommand(), disableCommand(), listCommand())
cmd.AddCommand(enableCommand(commandName), disableCommand(), listCommand())
return cmd
}
21 changes: 11 additions & 10 deletions internal/cfgmgr/manager/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,25 @@ func DeriveServerName(executablePath string) string {
return serverName
}

// GetCmdPath builds the command path to the MCP command.
// It returns the slice of command names from after the root command up to and including "mcp".
// GetCmdPath builds the command path to the ophis command.
// It returns the slice of command names from after the root command up to and
// including the command identified by commandName.
//
// Example: for command path "myapp alpha mcp start", returns ["alpha", "mcp"].
// Example: for command path "myapp agent claude enable" with commandName "agent",
// returns ["agent"].
//
// The returned slice can be used as arguments when invoking the executable.
// Returns an error if "mcp" is not found in the command path.
func GetCmdPath(cmd *cobra.Command) ([]string, error) {
// Returns an error if commandName is not found in the command path.
func GetCmdPath(cmd *cobra.Command, commandName string) ([]string, error) {
path := cmd.CommandPath()
args := strings.Fields(path)

// Find the index of "mcp" in the command path
name := "mcp"
index := slices.Index(args, name)
// Find the index of the ophis command in the command path
index := slices.Index(args, commandName)
if index == -1 {
return nil, fmt.Errorf("command %q not found in path %q", name, path)
return nil, fmt.Errorf("command %q not found in path %q", commandName, path)
}

// Return the slice from after the root command to the MCP command
// Return the slice from after the root command to the ophis command
return args[1 : index+1], nil
}
Loading