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

98 lines
3.3 KiB
Go

package mcp
import (
"fmt"
"regexp"
"github.com/mark3labs/mcp-go/mcp"
)
// toolNamePattern is the tool-name convention enforced by the MCP
// ecosystem (and the Anthropic tool API): a lowercase identifier of
// letters, digits and underscores, 1..64 characters.
var toolNamePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`)
// SchemaViolation is one problem found in a tool's MCP schema by
// LintToolSchema. Tool names the offending tool, Rule the convention
// broken, Detail a human-readable explanation.
type SchemaViolation struct {
Tool string `json:"tool"`
Rule string `json:"rule"`
Detail string `json:"detail"`
}
func (v SchemaViolation) String() string {
return fmt.Sprintf("%s [%s]: %s", v.Tool, v.Rule, v.Detail)
}
// LintToolSchema checks one tool's MCP schema against the spec
// conventions every Gortex tool must satisfy:
//
// - name is a lowercase [a-z0-9_] identifier, 1..64 chars
// - description is non-empty and carries no control characters
// - the input schema, when present, has type "object"
// - every property declares a "type" (or a "$ref")
// - every entry in `required` names a declared property
//
// It returns every violation found (nil when the tool is clean) so a
// release-gating test can lint the whole surface in one pass and
// report all problems at once.
func LintToolSchema(tool mcp.Tool) []SchemaViolation {
var out []SchemaViolation
add := func(rule, detail string) {
out = append(out, SchemaViolation{Tool: tool.Name, Rule: rule, Detail: detail})
}
switch {
case tool.Name == "":
add("name", "tool name is empty")
case !toolNamePattern.MatchString(tool.Name):
add("name", fmt.Sprintf("name %q is not a lowercase [a-z0-9_] identifier of 1..64 chars", tool.Name))
}
if tool.Description == "" {
add("description", "description is empty")
} else if scrubControlChars(tool.Description) != tool.Description {
add("description", "description carries control characters or ANSI escapes")
}
schema := tool.InputSchema
hasSchema := schema.Type != "" || len(schema.Properties) > 0 || len(schema.Required) > 0
if hasSchema {
if schema.Type != "" && schema.Type != "object" {
add("input_schema", fmt.Sprintf("input schema type is %q, want \"object\"", schema.Type))
}
for propName, raw := range schema.Properties {
m, ok := raw.(map[string]any)
if !ok {
add("property", fmt.Sprintf("property %q is not a JSON-Schema object", propName))
continue
}
_, hasType := m["type"]
_, hasRef := m["$ref"]
if !hasType && !hasRef {
add("property", fmt.Sprintf("property %q declares no \"type\"", propName))
}
}
for _, req := range schema.Required {
if _, ok := schema.Properties[req]; !ok {
add("required", fmt.Sprintf("required property %q is not declared in properties", req))
}
}
}
return out
}
// LintAllTools lints every tool currently registered live with the
// server. The lazy split is off by default, so the full surface is
// already live; if a test or runtime sets GORTEX_LAZY_TOOLS=1 to
// exercise the deferred path, clear it before linting. Returns every
// violation across every tool.
func LintAllTools(s *Server) []SchemaViolation {
var out []SchemaViolation
for _, st := range s.mcpServer.ListTools() {
out = append(out, LintToolSchema(st.Tool)...)
}
return out
}