a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/zzet/gortex/internal/daemon"
|
|
gortexmcp "github.com/zzet/gortex/internal/mcp"
|
|
)
|
|
|
|
// newLocalToolExecutor builds the daemon.LocalExecutor closure used by
|
|
// the multi-server router. It wraps the in-process MCP server's tool
|
|
// dispatch with the same body-shape the HTTP handler
|
|
// accepts (`{"arguments": {...}}` or flat-args), so a router-routed
|
|
// local call returns bytes shaped exactly like a remote-proxied
|
|
// response — the caller can't tell the difference and the response
|
|
// content type stays JSON.
|
|
//
|
|
// We re-use the local mcp.Server's MCPServer() rather than calling
|
|
// back into the HTTP handler so this path doesn't recursively flow
|
|
// through the router again. The router's localExec is the single
|
|
// canonical "run this tool here" entrypoint; everything else goes
|
|
// through it.
|
|
func newLocalToolExecutor(srv *gortexmcp.Server, logger *zap.Logger) daemon.LocalExecutor {
|
|
if srv == nil || srv.MCPServer() == nil {
|
|
return func(_ context.Context, _ string, _ []byte) ([]byte, int, error) {
|
|
return nil, 500, fmt.Errorf("local executor: no MCP server attached")
|
|
}
|
|
}
|
|
return func(ctx context.Context, toolName string, body []byte) ([]byte, int, error) {
|
|
tool := srv.MCPServer().GetTool(toolName)
|
|
if tool == nil {
|
|
payload := map[string]any{
|
|
"error": "tool_not_found",
|
|
"message": fmt.Sprintf("tool '%s' not found", toolName),
|
|
}
|
|
out, _ := json.Marshal(payload)
|
|
return out, 404, nil
|
|
}
|
|
|
|
var args map[string]any
|
|
if len(body) > 0 {
|
|
var nested struct {
|
|
Arguments map[string]any `json:"arguments"`
|
|
}
|
|
if err := json.Unmarshal(body, &nested); err == nil && nested.Arguments != nil {
|
|
args = nested.Arguments
|
|
} else {
|
|
_ = json.Unmarshal(body, &args)
|
|
}
|
|
}
|
|
|
|
mcpReq := mcp.CallToolRequest{
|
|
Params: mcp.CallToolParams{
|
|
Name: toolName,
|
|
Arguments: args,
|
|
},
|
|
}
|
|
result, err := tool.Handler(ctx, mcpReq)
|
|
if err != nil {
|
|
logger.Warn("local executor: tool call error",
|
|
zap.String("tool", toolName), zap.Error(err))
|
|
payload := map[string]any{
|
|
"error": "tool_error",
|
|
"message": err.Error(),
|
|
}
|
|
out, _ := json.Marshal(payload)
|
|
return out, 500, nil
|
|
}
|
|
|
|
// Mirror the same response shape the HTTP handler emits so
|
|
// the proxy and local paths are indistinguishable downstream.
|
|
resp := struct {
|
|
IsError bool `json:"is_error,omitempty"`
|
|
Content []map[string]any `json:"content,omitempty"`
|
|
}{IsError: result.IsError}
|
|
for _, c := range result.Content {
|
|
if tc, ok := c.(mcp.TextContent); ok {
|
|
resp.Content = append(resp.Content, map[string]any{
|
|
"type": "text",
|
|
"text": tc.Text,
|
|
})
|
|
}
|
|
}
|
|
out, err := json.Marshal(resp)
|
|
if err != nil {
|
|
return nil, 500, err
|
|
}
|
|
return out, 200, nil
|
|
}
|
|
}
|