Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

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
}
}