Files
wehub-resource-sync a06f331eb8
install-script / powershell-syntax (push) Waiting to run
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
install-script / install (macos-14) (push) Waiting to run
install-script / install (ubuntu-latest) (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

187 lines
5.2 KiB
Go

package mcp
import (
"strings"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// aliasCanonicals maps a hallucinated / mistyped MCP parameter name to the
// canonical Gortex parameter name(s) an agent most likely intended. A
// rewrite only fires when exactly one candidate is a real, not-yet-set
// parameter of the tool being called (see resolveParamAlias), so a
// multi-target entry resolves itself against the tool's actual schema.
var aliasCanonicals = map[string][]string{
// identifier
"symbol": {"id"},
"symbol_id": {"id"},
"symbolid": {"id"},
"node": {"id"},
"node_id": {"id"},
"target": {"id"},
// file path
"file": {"path"},
"filepath": {"path"},
"file_path": {"path"},
"filename": {"path"},
// query / search text
"q": {"query"},
"search": {"query"},
"text": {"query"},
"term": {"query"},
"keyword": {"query"},
// `pattern` only resolves to `query` on tools that have no real `pattern`
// parameter (search_symbols / search_text). On search_ast / grep_results /
// ctx_grep — where `pattern` is a real parameter — it is never treated as
// unknown, so the alias is inert there.
"pattern": {"query"},
// edit payloads
"new_body": {"new_string", "new_source", "content"},
"new_content": {"new_string", "new_source", "content"},
"body": {"content", "new_string", "new_source"},
"old_body": {"old_string", "old_source"},
"replacement": {"new_string", "new_source"},
// task / instruction
"prompt": {"task"},
"goal": {"task"},
"instruction": {"task"},
}
// paramRewrite records one applied alias rewrite, for debug logging.
type paramRewrite struct{ from, to string }
// reconcileArgKeys rewrites, in place, argument keys that are not real
// parameters of the tool to their canonical names. A key is rewritten
// only when it confidently resolves to exactly one real, not-yet-supplied
// parameter. Returns the rewrites applied.
func reconcileArgKeys(args map[string]any, real map[string]bool) []paramRewrite {
if len(args) == 0 || len(real) == 0 {
return nil
}
var unknown []string
for k := range args {
if !real[k] {
unknown = append(unknown, k)
}
}
var rewrites []paramRewrite
for _, k := range unknown {
target := resolveParamAlias(k, real, args)
if target == "" {
continue
}
args[target] = args[k]
delete(args, k)
rewrites = append(rewrites, paramRewrite{from: k, to: target})
}
return rewrites
}
// resolveParamAlias returns the canonical parameter name key was most
// likely meant to be, or "" when there is no confident single match. A
// candidate qualifies only if it is a real parameter of the tool and is
// not already present in args (so an explicit value is never displaced).
func resolveParamAlias(key string, real map[string]bool, args map[string]any) string {
keyLower := strings.ToLower(strings.TrimSpace(key))
candidates := map[string]struct{}{}
for _, c := range aliasCanonicals[keyLower] {
candidates[c] = struct{}{}
}
// Edit-distance typo match against the tool's real parameters.
for r := range real {
if len(r) < 4 {
continue // too short to typo-match safely
}
if d := levenshtein(keyLower, strings.ToLower(r)); d > 0 && d <= 2 {
candidates[r] = struct{}{}
}
}
var viable []string
for c := range candidates {
if !real[c] {
continue
}
if _, present := args[c]; present {
continue
}
viable = append(viable, c)
}
if len(viable) == 1 {
return viable[0]
}
return ""
}
// levenshtein computes the edit distance between two short strings.
func levenshtein(a, b string) int {
if a == b {
return 0
}
ra, rb := []rune(a), []rune(b)
prev := make([]int, len(rb)+1)
for j := range prev {
prev[j] = j
}
for i := 1; i <= len(ra); i++ {
cur := make([]int, len(rb)+1)
cur[0] = i
for j := 1; j <= len(rb); j++ {
cost := 1
if ra[i-1] == rb[j-1] {
cost = 0
}
cur[j] = min(cur[j-1]+1, prev[j]+1, prev[j-1]+cost)
}
prev = cur
}
return prev[len(rb)]
}
// toolParamNames returns the set of real parameter names declared by the
// named tool's input schema, or nil when the tool or its schema is unknown.
func (s *Server) toolParamNames(toolName string) map[string]bool {
if s == nil || s.mcpServer == nil {
return nil
}
st := s.mcpServer.GetTool(toolName)
if st == nil {
return nil
}
props := st.Tool.InputSchema.Properties
if len(props) == 0 {
return nil
}
out := make(map[string]bool, len(props))
for k := range props {
out[k] = true
}
return out
}
// reconcileToolParams tolerates hallucinated / mistyped parameter names on
// an incoming tool call: any argument key that is not a real parameter of
// the tool but confidently resolves to one is rewritten in place before
// the handler reads arguments. A no-op when every key is already valid.
func (s *Server) reconcileToolParams(req *mcp.CallToolRequest) {
if req == nil {
return
}
args, ok := req.Params.Arguments.(map[string]any)
if !ok || len(args) == 0 {
return
}
real := s.toolParamNames(req.Params.Name)
if len(real) == 0 {
return
}
for _, rw := range reconcileArgKeys(args, real) {
if s.logger != nil {
s.logger.Debug("tool_param_alias: accepted aliased parameter",
zap.String("tool", req.Params.Name),
zap.String("from", rw.from),
zap.String("to", rw.to))
}
}
}