Skip to content

Commit 0432e2b

Browse files
authored
Merge pull request #1 from thepwagner/accept-prefix
allow passing a prefix for exposing non-root commands
2 parents a2c4962 + 04f2d08 commit 0432e2b

2 files changed

Lines changed: 51 additions & 12 deletions

File tree

command.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import (
1616
"github.com/urfave/cli/v3"
1717
)
1818

19-
const toolDelimiter = "_"
19+
const ToolDelimiter = "_"
2020

21-
func MCPCommand(root *cli.Command) *cli.Command {
21+
func MCPCommand(root *cli.Command, prefix ...string) *cli.Command {
2222
// Calling root.Run will modify root.Action, so store if it is non-nil first
2323
// We'll use this to decide to hide the root app if it's just a help command.
2424
hasRootAction := root.Action != nil
@@ -30,7 +30,7 @@ func MCPCommand(root *cli.Command) *cli.Command {
3030
slog.Debug("building MCP server", slog.Any("app", root.Name))
3131

3232
slog.Debug("serving MCP server")
33-
srv, err := MPCServer(root, hasRootAction)
33+
srv, err := MPCServer(root, hasRootAction, prefix...)
3434
if err != nil {
3535
return err
3636
}
@@ -40,13 +40,18 @@ func MCPCommand(root *cli.Command) *cli.Command {
4040
}
4141
}
4242

43-
func MPCServer(root *cli.Command, hasRootAction bool) (*server.MCPServer, error) {
43+
func MPCServer(root *cli.Command, hasRootAction bool, prefix ...string) (*server.MCPServer, error) {
4444
srv := server.NewMCPServer(root.Name, root.Version, server.WithToolCapabilities(true))
4545

4646
toolHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
47-
args := strings.Split(request.Params.Name, toolDelimiter)
47+
args := strings.Split(request.Params.Name, ToolDelimiter)
48+
49+
// Assume we were called with a root command - it should not be forwarded when we fork.
4850
args = args[1:]
4951

52+
// If we were not called from the root command, add any prefix that was specified:
53+
args = append(prefix, args...)
54+
5055
// We're about to execute some user input, how bad of an idea is that?
5156
// Because we hardcode `os.Args[0]` and don't use a shell, we're safe from command injection.
5257
// I _assume_ mcp-go has verified the arguments are actually what our tool said it can use.
@@ -68,8 +73,8 @@ func MPCServer(root *cli.Command, hasRootAction bool) (*server.MCPServer, error)
6873
}
6974
}
7075
var logFields []any
71-
for _, arg := range args {
72-
logFields = append(logFields, slog.Any("arg", arg))
76+
for i, arg := range args {
77+
logFields = append(logFields, slog.Any(fmt.Sprintf("%d", i), arg))
7378
}
7479
slog.Info("forking", slog.Any("cmd", os.Args[0]), slog.Group("args", logFields...))
7580

command_test.go

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package urfaveclimcp_test
33
import (
44
"context"
55
"os"
6+
"strings"
67
"testing"
78

89
"github.com/mark3labs/mcp-go/client"
@@ -14,6 +15,12 @@ import (
1415
"github.com/urfave/cli/v3"
1516
)
1617

18+
func init() {
19+
// The server is going to fork os.Args[0], because it thinks that is the CLI app.
20+
// Right now it's actually some test harness value, let's spoof it to a known ~safe value
21+
os.Args = []string{"/bin/echo"}
22+
}
23+
1724
func TestMCPCommand(t *testing.T) {
1825
t.Parallel()
1926

@@ -89,10 +96,6 @@ func TestMCPCommandServer(t *testing.T) {
8996
func TestMCPCommandServer_CallTool(t *testing.T) {
9097
t.Parallel()
9198

92-
// The server is going to fork os.Args[0], because it thinks that is the CLI app.
93-
// Right now it's actually some test harness value, let's spoof it to a known ~safe value
94-
os.Args = []string{"/bin/echo"}
95-
9699
root := &cli.Command{
97100
Name: "test",
98101
Flags: []cli.Flag{
@@ -108,7 +111,6 @@ func TestMCPCommandServer_CallTool(t *testing.T) {
108111

109112
transport := transport.NewInProcessTransport(srv)
110113
c := client.NewClient(transport)
111-
112114
_, err = c.Initialize(t.Context(), mcp.InitializeRequest{})
113115
require.NoError(t, err)
114116

@@ -124,3 +126,35 @@ func TestMCPCommandServer_CallTool(t *testing.T) {
124126
// The output of `echo --target 689` is returned, because that's how we would call the CLI app.
125127
assert.Equal(t, "--target 689\n", content.Text)
126128
}
129+
130+
func TestMCPCommandServer_CallTool_Subcommand(t *testing.T) {
131+
t.Parallel()
132+
133+
root := &cli.Command{
134+
Name: "test",
135+
Commands: []*cli.Command{
136+
{
137+
Name: "sub",
138+
Action: func(context.Context, *cli.Command) error { return nil },
139+
},
140+
},
141+
}
142+
srv, err := urfaveclimcp.MPCServer(root, false, "foo", "bar")
143+
assert.NoError(t, err)
144+
145+
transport := transport.NewInProcessTransport(srv)
146+
c := client.NewClient(transport)
147+
_, err = c.Initialize(t.Context(), mcp.InitializeRequest{})
148+
require.NoError(t, err)
149+
150+
req := mcp.CallToolRequest{}
151+
req.Params.Name = strings.Join([]string{"test", "sub"}, urfaveclimcp.ToolDelimiter)
152+
153+
callResult, err := c.CallTool(t.Context(), req)
154+
require.NoError(t, err)
155+
assert.Len(t, callResult.Content, 1)
156+
content, ok := callResult.Content[0].(mcp.TextContent)
157+
assert.True(t, ok)
158+
159+
assert.Equal(t, "foo bar sub\n", content.Text)
160+
}

0 commit comments

Comments
 (0)