Files
wehub-resource-sync 4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:02:24 +08:00

227 lines
5.2 KiB
Go

package mcp
import (
"encoding/json"
"math"
"strings"
googlejsonschema "github.com/google/jsonschema-go/jsonschema"
"github.com/pkg/errors"
)
func validateToolArguments(schema jsonSchema, arguments map[string]any) error {
if schema == nil {
return nil
}
if err := validateSchemaValue(schema, "argument", "argument", arguments, schema); err != nil {
return err
}
return validateArgumentsWithJSONSchema(schema, arguments)
}
func validateArgumentsWithJSONSchema(schema jsonSchema, arguments map[string]any) error {
data, err := json.Marshal(schema)
if err != nil {
return errors.Wrap(err, "failed to marshal MCP tool input schema")
}
jsonSchema := &googlejsonschema.Schema{}
if err := json.Unmarshal(data, jsonSchema); err != nil {
return errors.Wrap(err, "failed to parse MCP tool input schema")
}
resolved, err := jsonSchema.Resolve(nil)
if err != nil {
return errors.Wrap(err, "failed to resolve MCP tool input schema")
}
if err := resolved.Validate(arguments); err != nil {
return errors.Wrap(err, "MCP tool arguments do not match input schema")
}
return nil
}
func validateSchemaValue(schemaValue any, path string, label string, value any, root jsonSchema) error {
schema, ok := asSchemaMap(schemaValue)
if !ok {
return nil
}
if ref, ok := schema["$ref"].(string); ok && ref != "" {
resolved, ok := localSchemaDef(root, ref)
if !ok {
return nil
}
return validateSchemaValue(resolved, path, label, value, root)
}
if value == nil {
return nil
}
types := schemaTypes(schema["type"])
if len(types) == 0 && schema["properties"] != nil {
types = []string{"object"}
}
for _, schemaType := range types {
if schemaTypeMatchesValue(schemaType, value) {
if schemaType == "object" {
return validateObjectSchema(schema, path, value, root)
}
return nil
}
}
if len(types) == 0 {
return nil
}
return errors.Errorf(`%s "%s" must be %s`, label, path, types[0])
}
func validateObjectSchema(schema map[string]any, path string, value any, root jsonSchema) error {
object, ok := value.(map[string]any)
if !ok {
return errors.Errorf(`argument "%s" must be object`, path)
}
properties := schemaProperties(schema["properties"])
for _, required := range requiredNames(schema["required"]) {
if child, ok := object[required]; !ok || child == nil {
return errors.Errorf(`missing required argument "%s"`, joinSchemaPath(path, required))
}
}
if additionalProperties, ok := schema["additionalProperties"].(bool); ok && !additionalProperties {
for name := range object {
if _, ok := properties[name]; !ok {
return errors.Errorf(`unknown argument "%s"`, joinSchemaPath(path, name))
}
}
}
for name, childSchema := range properties {
childValue, ok := object[name]
if !ok {
continue
}
if err := validateSchemaValue(childSchema, joinSchemaPath(path, name), "argument", childValue, root); err != nil {
return err
}
}
return nil
}
func asSchemaMap(value any) (map[string]any, bool) {
switch typed := value.(type) {
case jsonSchema:
return map[string]any(typed), true
case map[string]any:
return typed, true
default:
return nil, false
}
}
func schemaProperties(value any) map[string]any {
switch typed := value.(type) {
case map[string]any:
return typed
case jsonSchema:
return map[string]any(typed)
default:
return map[string]any{}
}
}
func schemaTypes(value any) []string {
switch typed := value.(type) {
case string:
return []string{typed}
case []any:
types := make([]string, 0, len(typed))
for _, item := range typed {
if typeName, ok := item.(string); ok {
types = append(types, typeName)
}
}
return types
default:
return nil
}
}
func requiredNames(value any) []string {
switch typed := value.(type) {
case []string:
return typed
case []any:
names := make([]string, 0, len(typed))
for _, item := range typed {
if name, ok := item.(string); ok {
names = append(names, name)
}
}
return names
default:
return nil
}
}
func schemaTypeMatchesValue(schemaType string, value any) bool {
switch schemaType {
case "array":
_, ok := value.([]any)
return ok
case "boolean":
_, ok := value.(bool)
return ok
case "integer":
return isInteger(value)
case "number":
return isNumber(value)
case "object":
_, ok := value.(map[string]any)
return ok
case "string":
_, ok := value.(string)
return ok
default:
return true
}
}
func isInteger(value any) bool {
switch typed := value.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return true
case float32:
return math.Trunc(float64(typed)) == float64(typed)
case float64:
return math.Trunc(typed) == typed
default:
return false
}
}
func isNumber(value any) bool {
switch value.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return true
default:
return false
}
}
func localSchemaDef(root jsonSchema, ref string) (any, bool) {
const prefix = "#/$defs/"
if !strings.HasPrefix(ref, prefix) {
return nil, false
}
defs, ok := root["$defs"].(map[string]any)
if !ok {
return nil, false
}
return defs[strings.TrimPrefix(ref, prefix)], true
}
func joinSchemaPath(parent string, child string) string {
if parent == "" || parent == "argument" {
return child
}
return parent + "." + child
}