Skip to content

Commit b693039

Browse files
committed
Add proxy tools mode for wmcp (search-tools/call-tool)
1 parent 7109561 commit b693039

9 files changed

Lines changed: 725 additions & 37 deletions

File tree

src/repl/conf.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func NewConfigOptions() *ConfigOptions {
160160
co.RegisterOption("mcp.allowtools", StringOption, "Comma-separated list of allowed tools", "")
161161
co.RegisterOption("mcp.denytools", StringOption, "Comma-separated list of forbidden tools", "")
162162
co.RegisterOption("mcp.yolotools", StringOption, "Comma-separated list of yolo tools (allowed but potentially risky)", "")
163+
co.RegisterOption("mcp.proxytools", BooleanOption, "Expose only 'search-tools' and 'call-tool' to the agent; real tools are proxied behind them", "false")
163164

164165
co.initialized = true
165166

src/repl/wmcp_embed.go

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,11 @@ func embedGetService(r *REPL) (*wmcplib.MCPService, error) {
5858

5959
yolo := false
6060
debug := false
61+
proxy := false
6162
if r != nil {
6263
yolo = r.configOptions.GetBool("mcp.yolo")
6364
debug = r.configOptions.GetBool("mcp.debug")
65+
proxy = r.configOptions.GetBool("mcp.proxytools")
6466
}
6567

6668
service := wmcplib.NewMCPService(wmcplib.Options{
@@ -71,6 +73,7 @@ func embedGetService(r *REPL) (*wmcplib.MCPService, error) {
7173
NonInteractive: cfg.MaiOptions.NonInteractive,
7274
SessionMode: cfg.MaiOptions.SessionMode,
7375
DebugMode: debug || cfg.MaiOptions.DebugMode,
76+
ProxyToolsMode: proxy || cfg.MaiOptions.ProxyToolsMode,
7477
Prompter: newReplPrompter(r),
7578
})
7679

@@ -147,6 +150,10 @@ func embedListToolsFormatted(r *REPL, f Format) (string, error) {
147150
svc.Mutex.RLock()
148151
defer svc.Mutex.RUnlock()
149152

153+
if svc.ProxyToolsMode {
154+
return embedFormatProxyTools(f), nil
155+
}
156+
150157
switch f {
151158
case JSON:
152159
res := make(map[string][]wmcplib.Tool)
@@ -184,6 +191,96 @@ func embedListToolsFormatted(r *REPL, f Format) (string, error) {
184191
return embedFormatMarkdown(svc), nil
185192
}
186193

194+
// embedFormatProxyTools renders the two virtual proxy tools in the format
195+
// requested by the REPL. Used when the embedded service has ProxyToolsMode
196+
// enabled — the real underlying tools must not appear in the LLM-facing
197+
// catalog.
198+
func embedFormatProxyTools(f Format) string {
199+
tools := wmcplib.ProxyTools()
200+
switch f {
201+
case JSON:
202+
b, _ := json.Marshal(map[string][]wmcplib.Tool{"proxy": tools})
203+
return string(b)
204+
case XML:
205+
var out strings.Builder
206+
out.WriteString("<tools>\n")
207+
for _, tool := range tools {
208+
out.WriteString(fmt.Sprintf(" <tool server=%q name=%q>\n", "proxy", tool.Name))
209+
out.WriteString(fmt.Sprintf(" <description>%s</description>\n", tool.Description))
210+
for _, p := range tool.Parameters {
211+
required := ""
212+
if p.Required {
213+
required = " required=\"true\""
214+
}
215+
out.WriteString(fmt.Sprintf(" <param name=%q type=%q%s>%s</param>\n",
216+
p.Name, p.Type, required, p.Description))
217+
}
218+
out.WriteString(" </tool>\n")
219+
}
220+
out.WriteString("</tools>\n")
221+
return out.String()
222+
case Simple:
223+
var out strings.Builder
224+
notFirst := false
225+
for _, tool := range tools {
226+
if notFirst {
227+
out.WriteString("--\n")
228+
}
229+
notFirst = true
230+
out.WriteString(fmt.Sprintf("TOOLNAME: %s\n", tool.Name))
231+
out.WriteString(fmt.Sprintf("DESCRIPTION: %s\n", tool.Description))
232+
if len(tool.Parameters) > 0 {
233+
var examples []string
234+
for _, p := range tool.Parameters {
235+
examples = append(examples, fmt.Sprintf("%s=<value>", p.Name))
236+
}
237+
out.WriteString(fmt.Sprintf("USAGE: proxy %s %s\n", tool.Name, strings.Join(examples, " ")))
238+
}
239+
}
240+
return out.String()
241+
case Quiet:
242+
var out strings.Builder
243+
for _, tool := range tools {
244+
out.WriteString(fmt.Sprintf("- ToolName: %s\n", tool.Name))
245+
if tool.Description != "" {
246+
out.WriteString(fmt.Sprintf(" Description: %s\n", tool.Description))
247+
}
248+
if len(tool.Parameters) > 0 {
249+
out.WriteString(" Parameters:\n")
250+
for _, p := range tool.Parameters {
251+
req := ""
252+
if p.Required {
253+
req = " [required]"
254+
}
255+
out.WriteString(fmt.Sprintf(" - %s=<%s> : %s%s\n", p.Name, p.Type, p.Description, req))
256+
}
257+
}
258+
}
259+
return strings.TrimRight(out.String(), "\n")
260+
}
261+
262+
var out strings.Builder
263+
out.WriteString("# Tools Catalog\n\n")
264+
out.WriteString("## Proxy Tools Mode\n\n")
265+
out.WriteString("Only two virtual tools are exposed; they gate access to the real underlying tools.\n\n")
266+
for _, tool := range tools {
267+
out.WriteString(fmt.Sprintf("### %s\n", tool.Name))
268+
out.WriteString(fmt.Sprintf("**Description:** %s\n\n", tool.Description))
269+
if len(tool.Parameters) > 0 {
270+
out.WriteString("**Parameters:**\n")
271+
for _, p := range tool.Parameters {
272+
req := ""
273+
if p.Required {
274+
req = " (required)"
275+
}
276+
out.WriteString(fmt.Sprintf("- %s (%s)%s: %s\n", p.Name, p.Type, req, p.Description))
277+
}
278+
out.WriteString("\n")
279+
}
280+
}
281+
return out.String()
282+
}
283+
187284
func embedFormatQuiet(svc *wmcplib.MCPService) string {
188285
categoryOrder := []string{"File", "Analysis", "Inspection", "Metadata", "Editing"}
189286
toolsByCategory := make(map[string][]wmcplib.QuietToolEntry)
@@ -353,13 +450,32 @@ func embedCallTool(r *REPL, toolName string, args map[string]interface{}, timeou
353450
return "", err
354451
}
355452

453+
_ = timeoutSeconds // the library imposes its own 30s stdio timeout for now
454+
455+
// In proxy mode the agent may only call the two virtual tools; route them
456+
// through ProcessMCPRequest which knows how to handle them.
457+
if svc.ProxyToolsMode {
458+
req := wmcplib.JSONRPCRequest{
459+
JSONRPC: "2.0",
460+
Method: "tools/call",
461+
Params: wmcplib.CallToolParams{Name: toolName, Arguments: args},
462+
ID: time.Now().UnixNano(),
463+
}
464+
resp, _ := svc.ProcessMCPRequest(req)
465+
if resp == nil {
466+
return "", nil
467+
}
468+
if resp.Error != nil {
469+
return "", fmt.Errorf("%v", resp.Error)
470+
}
471+
return renderCallToolResult(resp.Result)
472+
}
473+
356474
server, resolvedName, err := svc.ResolveTool(toolName)
357475
if err != nil {
358476
return "", err
359477
}
360478

361-
_ = timeoutSeconds // the library imposes its own 30s stdio timeout for now
362-
363479
req := wmcplib.JSONRPCRequest{
364480
JSONRPC: "2.0",
365481
Method: "tools/call",
@@ -375,7 +491,14 @@ func embedCallTool(r *REPL, toolName string, args map[string]interface{}, timeou
375491
return "", fmt.Errorf("%v", resp.Error)
376492
}
377493

378-
resultBytes, _ := json.Marshal(resp.Result)
494+
return renderCallToolResult(resp.Result)
495+
}
496+
497+
// renderCallToolResult converts an MCP tools/call Result payload into the
498+
// plain-text representation the REPL expects (concatenated content text, or
499+
// raw JSON when the payload doesn't fit the CallToolResult shape).
500+
func renderCallToolResult(result interface{}) (string, error) {
501+
resultBytes, _ := json.Marshal(result)
379502
var toolResult wmcplib.CallToolResult
380503
if err := json.Unmarshal(resultBytes, &toolResult); err != nil {
381504
return string(resultBytes), nil

0 commit comments

Comments
 (0)