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
187 lines
5.2 KiB
Go
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))
|
|
}
|
|
}
|
|
}
|