Skip to content

Commit 7841e20

Browse files
committed
fix(plugin-mcp): registerPluginMCPTools accepts registry + tests + remove pluginLookup
1 parent 3309dbc commit 7841e20

4 files changed

Lines changed: 175 additions & 14 deletions

File tree

‎cmd/sin-code/internal/plugins/registry.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ type MCPToolDef struct {
133133
Name string
134134
Description string
135135
Plugin string
136+
PluginPath string
136137
Tool string
137138
Binary string
138139
Args []string
@@ -171,6 +172,7 @@ func (r *Registry) MCPTools() []MCPToolDef {
171172
Name: "sin_plugin_" + p.Name + "_" + t.Name,
172173
Description: desc,
173174
Plugin: p.Name,
175+
PluginPath: p.Path,
174176
Tool: t.Name,
175177
Binary: t.Binary,
176178
Args: t.Args,

‎cmd/sin-code/internal/serve.go‎

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,10 @@ func registerAllMCPTools(server *mcp.Server) {
602602
func registerPluginMCPTools(server *mcp.Server) {
603603
reg := plugins.NewRegistry()
604604
_ = reg.LoadFromDir("")
605+
registerPluginMCPToolsWithReg(server, reg)
606+
}
607+
608+
func registerPluginMCPToolsWithReg(server *mcp.Server, reg *plugins.Registry) {
605609
for _, pt := range reg.MCPTools() {
606610
pt := pt
607611
server.AddTool(&mcp.Tool{
@@ -631,13 +635,9 @@ func registerPluginMCPTools(server *mcp.Server) {
631635
// path is resolved relative to the plugin dir; stdout/stderr are merged
632636
// and returned as a string. Timeout defaults to 60s.
633637
func runPluginMCPTool(ctx context.Context, pt plugins.MCPToolDef, args map[string]any) (string, error) {
634-
plugin, ok := pluginLookup(pt.Plugin)
635-
if !ok {
636-
return "", fmt.Errorf("plugin %q not loaded", pt.Plugin)
637-
}
638638
fullPath := pt.Binary
639639
if !filepath.IsAbs(fullPath) {
640-
fullPath = filepath.Join(plugin.Path, fullPath)
640+
fullPath = filepath.Join(pt.PluginPath, fullPath)
641641
}
642642
cmdArgs := make([]string, 0, len(pt.Args)+len(args))
643643
for _, a := range pt.Args {
@@ -653,21 +653,15 @@ func runPluginMCPTool(ctx context.Context, pt plugins.MCPToolDef, args map[strin
653653
execCtx, cancel := context.WithTimeout(ctx, timeout)
654654
defer cancel()
655655
c := exec.CommandContext(execCtx, fullPath, cmdArgs...)
656-
c.Dir = plugin.Path
656+
c.Dir = pt.PluginPath
657657
c.Env = append(os.Environ(), "SIN_PLUGIN="+pt.Plugin, "SIN_PLUGIN_TOOL="+pt.Tool)
658658
out, err := c.CombinedOutput()
659659
if err != nil {
660-
return fmt.Sprintf("%s\nERROR: %v", string(out), err), nil
660+
return string(out), fmt.Errorf("plugin tool %q: %w", pt.Name, err)
661661
}
662662
return string(out), nil
663663
}
664664

665-
func pluginLookup(name string) (*plugins.Plugin, bool) {
666-
reg := plugins.NewRegistry()
667-
_ = reg.LoadFromDir("")
668-
return reg.Get(name)
669-
}
670-
671665
func handleDiscover(ctx context.Context, args map[string]any) (string, error) {
672666
// discover takes path as positional argument, not --path
673667
path := "."

‎cmd/sin-code/internal/serve_test.go‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
package internal
44

55
import (
6+
"context"
67
"testing"
8+
"time"
9+
10+
"github.com/OpenSIN-Code/SIN-Code-Bundle/cmd/sin-code/internal/plugins"
11+
"github.com/modelcontextprotocol/go-sdk/mcp"
712
)
813

914
func TestServeCmd_Flags(t *testing.T) {
@@ -34,3 +39,163 @@ func TestRegisterAllMCPTools(t *testing.T) {
3439
t.Errorf("expected 13 tools, test config has %d", len(expectedTools))
3540
}
3641
}
42+
43+
func newPluginTestServer(t *testing.T, reg *plugins.Registry) *mcp.Server {
44+
t.Helper()
45+
server := mcp.NewServer(&mcp.Implementation{
46+
Name: "sin-code-plugin-test",
47+
Version: "test",
48+
}, &mcp.ServerOptions{
49+
Capabilities: &mcp.ServerCapabilities{
50+
Tools: &mcp.ToolCapabilities{},
51+
},
52+
})
53+
registerAllMCPTools(server)
54+
registerPluginMCPToolsWithReg(server, reg)
55+
return server
56+
}
57+
58+
func connectWithPluginReg(t *testing.T, reg *plugins.Registry) (*mcp.ClientSession, context.CancelFunc) {
59+
t.Helper()
60+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
61+
server := newPluginTestServer(t, reg)
62+
cTransport, sTransport := mcp.NewInMemoryTransports()
63+
ss, err := server.Connect(ctx, sTransport, nil)
64+
if err != nil {
65+
cancel()
66+
t.Fatalf("server connect: %v", err)
67+
}
68+
t.Cleanup(func() { ss.Close() })
69+
client := mcp.NewClient(&mcp.Implementation{
70+
Name: "test-client",
71+
Version: "0.1.0",
72+
}, nil)
73+
cs, err := client.Connect(ctx, cTransport, nil)
74+
if err != nil {
75+
cancel()
76+
ss.Close()
77+
t.Fatalf("client connect: %v", err)
78+
}
79+
t.Cleanup(func() { cs.Close() })
80+
return cs, cancel
81+
}
82+
83+
func TestPluginToolRegistered(t *testing.T) {
84+
reg := plugins.NewRegistry()
85+
reg.Register(&plugins.Plugin{
86+
Name: "testplug",
87+
Enabled: true,
88+
Tools: []plugins.PluginTool{
89+
{
90+
Name: "reverse",
91+
Description: "Reverse a string",
92+
Binary: "/bin/echo",
93+
Args: []string{"input"},
94+
Timeout: 10,
95+
},
96+
},
97+
})
98+
cs, cancel := connectWithPluginReg(t, reg)
99+
defer cancel()
100+
101+
toolsResult, err := cs.ListTools(context.Background(), nil)
102+
if err != nil {
103+
t.Fatalf("ListTools failed: %v", err)
104+
}
105+
106+
var names []string
107+
for _, tool := range toolsResult.Tools {
108+
names = append(names, tool.Name)
109+
}
110+
111+
want := "sin_plugin_testplug_reverse"
112+
found := false
113+
for _, n := range names {
114+
if n == want {
115+
found = true
116+
break
117+
}
118+
}
119+
if !found {
120+
t.Errorf("expected plugin tool %q in list; got %v", want, names)
121+
}
122+
}
123+
124+
func TestPluginToolCallSuccess(t *testing.T) {
125+
reg := plugins.NewRegistry()
126+
reg.Register(&plugins.Plugin{
127+
Name: "echoplug",
128+
Enabled: true,
129+
Path: "/tmp",
130+
Tools: []plugins.PluginTool{
131+
{
132+
Name: "say",
133+
Description: "Echo back input",
134+
Binary: "/bin/echo",
135+
Args: []string{"input"},
136+
Timeout: 10,
137+
},
138+
},
139+
})
140+
cs, cancel := connectWithPluginReg(t, reg)
141+
defer cancel()
142+
143+
result, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
144+
Name: "sin_plugin_echoplug_say",
145+
Arguments: map[string]any{"input": "hello-world"},
146+
})
147+
if err != nil {
148+
t.Fatalf("CallTool failed: %v", err)
149+
}
150+
if len(result.Content) == 0 {
151+
t.Fatal("expected non-empty Content")
152+
}
153+
tc, ok := result.Content[0].(*mcp.TextContent)
154+
if !ok {
155+
t.Fatalf("expected TextContent, got %T", result.Content[0])
156+
}
157+
if tc.Text != "--input hello-world\n" {
158+
t.Errorf("expected '--input hello-world\\n', got %q", tc.Text)
159+
}
160+
}
161+
162+
func TestPluginToolCallMissingBinary(t *testing.T) {
163+
reg := plugins.NewRegistry()
164+
reg.Register(&plugins.Plugin{
165+
Name: "badplug",
166+
Enabled: true,
167+
Path: "/tmp",
168+
Tools: []plugins.PluginTool{
169+
{
170+
Name: "broken",
171+
Description: "Intentionally missing binary",
172+
Binary: "./nonexistent-binary-xyz",
173+
Args: []string{},
174+
Timeout: 5,
175+
},
176+
},
177+
})
178+
cs, cancel := connectWithPluginReg(t, reg)
179+
defer cancel()
180+
181+
result, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
182+
Name: "sin_plugin_badplug_broken",
183+
Arguments: map[string]any{},
184+
})
185+
if err != nil {
186+
t.Fatalf("CallTool failed (expected error in result, not call-level): %v", err)
187+
}
188+
if !result.IsError {
189+
t.Error("expected IsError=true for missing binary, got false")
190+
}
191+
if len(result.Content) == 0 {
192+
t.Fatal("expected non-empty Content")
193+
}
194+
tc, ok := result.Content[0].(*mcp.TextContent)
195+
if !ok {
196+
t.Fatalf("expected TextContent, got %T", result.Content[0])
197+
}
198+
if tc.Text == "" {
199+
t.Errorf("expected error message in content, got empty")
200+
}
201+
}

‎cmd/sin-code/testdata/scripts/plugin_mcp.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ sin-code execute --command 'printf "name = \"mcp-test-plugin\"\nversion = \"1.0.
1212
stdout 'Exit:'
1313

1414
# Plugin binary: reads --input arg and outputs reversed string
15-
sin-code execute --command 'printf "#!/bin/bash\nwhile [[ \$# -gt 0 ]]; do\n case \$1 in\n --input)\n echo -n \"\$2\" | rev\n shift 2\n ;;\n *)\n shift\n ;;\n done\ndone\n" > $WORK/source-plugin/bin/reverse && chmod +x $WORK/source-plugin/bin/reverse'
15+
sin-code execute --command 'printf "#!/bin/bash\nwhile [[ \$# -gt 0 ]]; do\n case \$1 in\n --input)\n echo -n \"\$2\" | rev\n shift 2\n ;;\n *)\n shift\n ;;\n esac\ndone\n" > $WORK/source-plugin/bin/reverse && chmod +x $WORK/source-plugin/bin/reverse'
1616
stdout 'Exit:'
1717

1818
# Install plugin

0 commit comments

Comments
 (0)