From c84806ecee7c72e6cda00ba75c39c99637ff0af1 Mon Sep 17 00:00:00 2001 From: zer0stars <74260741+zer0stars@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:15:39 -0600 Subject: [PATCH 1/4] feat(mcpserver): add MCP server package and codegen tool Adds pkg/mcpserver (MCP-over-HTTP wrapper around gqlgen executors) and cmd/mcpgen (code generator that reads @mcpTool directives from GraphQL schemas and emits Go tool definitions). Features: - Context + version propagation through New() - WithTools / WithCondensedSchema / WithStateless / WithMaxQuerySize / WithTokenVerifier / WithLogger options - Built-in {prefix}_get_schema + {prefix}_query tools per service - Pre-warmed schema introspection cache - Bearer token auth middleware (opt-in) - Prometheus metrics: mcp_tool_calls_total, mcp_tool_duration_seconds - Condensed SDL generation for LLM-friendly schema summaries - Full tool annotations (readOnly, destructive hints) - Dual logging: zerolog (infra) + slog (MCP SDK convention) Consumer integration pattern: h, err := mcpserver.New(ctx, mcpserver.NewGQLGenExecutor(es), "DIMO ", version, prefix, mcpserver.WithTools(graph.MCPTools), mcpserver.WithCondensedSchema(graph.CondensedSchema)) Downstream PRs wire this into identity-api, telemetry-api, fetch-api. --- .gitignore | 3 +- cmd/mcpgen/condensed.go | 378 +++++++++ cmd/mcpgen/condensed_filter.go | 148 ++++ cmd/mcpgen/condensed_sdl.go | 448 ++++++++++ cmd/mcpgen/condensed_signals.go | 361 ++++++++ cmd/mcpgen/condensed_test.go | 440 ++++++++++ cmd/mcpgen/generate.go | 279 ++++++ cmd/mcpgen/generate_test.go | 246 ++++++ cmd/mcpgen/main.go | 72 ++ cmd/mcpgen/testdata/all_types.graphqls | 25 + cmd/mcpgen/testdata/basic.graphqls | 16 + cmd/mcpgen/testdata/defaults.graphqls | 21 + cmd/mcpgen/testdata/deprecated.graphqls | 20 + cmd/mcpgen/testdata/docstrings.graphqls | 13 + cmd/mcpgen/testdata/edge_connection.graphqls | 47 ++ cmd/mcpgen/testdata/examples.graphqls | 18 + cmd/mcpgen/testdata/hidden.graphqls | 15 + .../testdata/invalid_selection.graphqls | 10 + cmd/mcpgen/testdata/list_types.graphqls | 11 + cmd/mcpgen/testdata/long_description.graphqls | 27 + cmd/mcpgen/testdata/oneof.graphqls | 18 + cmd/mcpgen/testdata/pagination_did.graphqls | 59 ++ .../testdata/scalar_descriptions.graphqls | 21 + cmd/mcpgen/testdata/self_evident.graphqls | 17 + cmd/mcpgen/testdata/signals.graphqls | 98 +++ go.mod | 59 +- go.sum | 394 +++++++-- pkg/mcpserver/condensed_schema_test.go | 98 +++ pkg/mcpserver/executor.go | 106 +++ pkg/mcpserver/gqlgen.go | 53 ++ pkg/mcpserver/gqlgen_test.go | 78 ++ pkg/mcpserver/mcpserver.go | 163 ++++ pkg/mcpserver/mcpserver_test.go | 796 ++++++++++++++++++ pkg/mcpserver/metrics.go | 24 + pkg/mcpserver/tools.go | 170 ++++ 35 files changed, 4674 insertions(+), 78 deletions(-) create mode 100644 cmd/mcpgen/condensed.go create mode 100644 cmd/mcpgen/condensed_filter.go create mode 100644 cmd/mcpgen/condensed_sdl.go create mode 100644 cmd/mcpgen/condensed_signals.go create mode 100644 cmd/mcpgen/condensed_test.go create mode 100644 cmd/mcpgen/generate.go create mode 100644 cmd/mcpgen/generate_test.go create mode 100644 cmd/mcpgen/main.go create mode 100644 cmd/mcpgen/testdata/all_types.graphqls create mode 100644 cmd/mcpgen/testdata/basic.graphqls create mode 100644 cmd/mcpgen/testdata/defaults.graphqls create mode 100644 cmd/mcpgen/testdata/deprecated.graphqls create mode 100644 cmd/mcpgen/testdata/docstrings.graphqls create mode 100644 cmd/mcpgen/testdata/edge_connection.graphqls create mode 100644 cmd/mcpgen/testdata/examples.graphqls create mode 100644 cmd/mcpgen/testdata/hidden.graphqls create mode 100644 cmd/mcpgen/testdata/invalid_selection.graphqls create mode 100644 cmd/mcpgen/testdata/list_types.graphqls create mode 100644 cmd/mcpgen/testdata/long_description.graphqls create mode 100644 cmd/mcpgen/testdata/oneof.graphqls create mode 100644 cmd/mcpgen/testdata/pagination_did.graphqls create mode 100644 cmd/mcpgen/testdata/scalar_descriptions.graphqls create mode 100644 cmd/mcpgen/testdata/self_evident.graphqls create mode 100644 cmd/mcpgen/testdata/signals.graphqls create mode 100644 pkg/mcpserver/condensed_schema_test.go create mode 100644 pkg/mcpserver/executor.go create mode 100644 pkg/mcpserver/gqlgen.go create mode 100644 pkg/mcpserver/gqlgen_test.go create mode 100644 pkg/mcpserver/mcpserver.go create mode 100644 pkg/mcpserver/mcpserver_test.go create mode 100644 pkg/mcpserver/metrics.go create mode 100644 pkg/mcpserver/tools.go diff --git a/.gitignore b/.gitignore index 3f43588..847a2d3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,6 @@ settings.yaml __debug_bin* *.code-workspace .history/ +/mcpgen - +CLAUDE.md diff --git a/cmd/mcpgen/condensed.go b/cmd/mcpgen/condensed.go new file mode 100644 index 0000000..36cd3be --- /dev/null +++ b/cmd/mcpgen/condensed.go @@ -0,0 +1,378 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/DIMO-Network/server-garage/pkg/mcpserver" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" +) + +// schemaAnalysis holds pre-computed metadata used to produce a more compact condensed SDL. +type schemaAnalysis struct { + edgeTypes map[string]bool // types matching the Relay Edge pattern + connectionTypes map[string]bool // types matching the Relay Connection pattern + signalTypes map[string]bool // types that contain @isSignal fields + hasDIDFields bool // true if any field description contains the DID format string + queryReachable map[string]bool // types reachable from query args / input types +} + +// analyzeSchema pre-scans the schema to detect patterns that can be collapsed in the condensed output. +func analyzeSchema(schema *ast.Schema) schemaAnalysis { + a := schemaAnalysis{ + edgeTypes: make(map[string]bool), + connectionTypes: make(map[string]bool), + signalTypes: make(map[string]bool), + queryReachable: computeQueryReachable(schema), + } + for name, def := range schema.Types { + if def.Kind != ast.Object { + continue + } + if isEdgeType(def) { + a.edgeTypes[name] = true + } + if isConnectionType(def) { + a.connectionTypes[name] = true + } + if hasSignalFields(def) { + a.signalTypes[name] = true + } + if !a.hasDIDFields { + for _, f := range def.Fields { + if containsDIDFormat(f.Description) { + a.hasDIDFields = true + break + } + } + } + } + + return a +} + +// computeQueryReachable finds all types that are reachable from query/mutation arguments +// or are input types, enums, scalars, interfaces, or unions. Response-only object types +// (leaf types that only appear in field return values) are excluded. +func computeQueryReachable(schema *ast.Schema) map[string]bool { + reachable := make(map[string]bool) + + // Seed: all input objects, enums, scalars, interfaces, and unions are always relevant. + for name, def := range schema.Types { + switch def.Kind { + case ast.InputObject, ast.Enum, ast.Scalar, ast.Interface, ast.Union: + reachable[name] = true + } + } + + // Seed: types referenced by query/mutation/subscription field arguments. + for _, opType := range []*ast.Definition{schema.Query, schema.Mutation, schema.Subscription} { + if opType == nil { + continue + } + for _, field := range opType.Fields { + for _, arg := range field.Arguments { + collectReferencedTypes(arg.Type, schema, reachable) + } + } + } + + // Walk: transitively include types referenced by fields of already-reachable input objects. + // (e.g., if an input type references another input type) + changed := true + for changed { + changed = false + for name := range reachable { + def := schema.Types[name] + if def == nil || def.Kind != ast.InputObject { + continue + } + for _, f := range def.Fields { + typeName := namedType(f.Type) + if !reachable[typeName] { + reachable[typeName] = true + changed = true + } + } + } + } + + return reachable +} + +// collectReferencedTypes adds the named type from a GraphQL type reference to the set. +func collectReferencedTypes(t *ast.Type, schema *ast.Schema, set map[string]bool) { + name := namedType(t) + set[name] = true + // If this is an input object, also include its field types. + if def, ok := schema.Types[name]; ok && def.Kind == ast.InputObject { + for _, f := range def.Fields { + childName := namedType(f.Type) + if !set[childName] { + collectReferencedTypes(f.Type, schema, set) + } + } + } +} + +// isEdgeType returns true if a type contains the Relay Edge fields: node: T! and cursor: String!. +func isEdgeType(def *ast.Definition) bool { + hasNode, hasCursor := false, false + for _, f := range def.Fields { + if strings.HasPrefix(f.Name, "__") { + continue + } + if f.Name == "node" && f.Type.NonNull { + hasNode = true + } + if f.Name == "cursor" && f.Type.NonNull && f.Type.NamedType == "String" { + hasCursor = true + } + } + return hasNode && hasCursor +} + +// isConnectionType returns true if a type contains the Relay Connection fields: +// totalCount, edges, nodes, pageInfo. +func isConnectionType(def *ast.Definition) bool { + required := map[string]bool{"totalCount": false, "edges": false, "nodes": false, "pageInfo": false} + for _, f := range def.Fields { + if _, ok := required[f.Name]; ok { + required[f.Name] = true + } + } + for _, found := range required { + if !found { + return false + } + } + return true +} + +// hasSignalFields returns true if any field on the type has the @isSignal directive. +func hasSignalFields(def *ast.Definition) bool { + for _, f := range def.Fields { + if f.Directives.ForName("isSignal") != nil { + return true + } + } + return false +} + +// loadGraphQLSchema loads .graphqls files and parses them into an ast.Schema. +func loadGraphQLSchema(paths []string) (*ast.Schema, error) { + var sources []*ast.Source + for _, p := range paths { + data, err := os.ReadFile(p) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", p, err) + } + sources = append(sources, &ast.Source{ + Name: filepath.Base(p), + Input: string(data), + }) + } + schema, gqlErr := gqlparser.LoadSchema(sources...) + if gqlErr != nil { + return nil, fmt.Errorf("parsing schema: %s", gqlErr.Error()) + } + return schema, nil +} + +// extractTools finds Query fields annotated with @mcpTool and returns tool definitions. +// This is the same logic as parseSchema but accepts a pre-parsed *ast.Schema. +func extractTools(schema *ast.Schema, prefix string) ([]mcpserver.ToolDefinition, error) { + queryType := schema.Query + if queryType == nil { + return nil, fmt.Errorf("no Query type found in schema") + } + + var tools []mcpserver.ToolDefinition + for _, field := range queryType.Fields { + dir := field.Directives.ForName("mcpTool") + if dir == nil { + continue + } + + nameArg := dir.Arguments.ForName("name") + descArg := dir.Arguments.ForName("description") + selArg := dir.Arguments.ForName("selection") + if nameArg == nil || descArg == nil || selArg == nil { + return nil, fmt.Errorf("field %s: @mcpTool directive missing required argument(s)", field.Name) + } + + toolName := nameArg.Value.Raw + if prefix != "" { + toolName = prefix + "_" + toolName + } + description := descArg.Value.Raw + selection := selArg.Value.Raw + + readOnly := true // default + if readOnlyArg := dir.Arguments.ForName("readOnly"); readOnlyArg != nil { + readOnly = readOnlyArg.Value.Raw == "true" + } + + if selection != "" { + returnTypeName := namedType(field.Type) + returnType := schema.Types[returnTypeName] + if returnType == nil { + return nil, fmt.Errorf("return type %s not found", returnTypeName) + } + if err := validateSelection(selection, returnType); err != nil { + return nil, fmt.Errorf("field %s: %w", field.Name, err) + } + } + + var args []mcpserver.ArgDefinition + for _, a := range field.Arguments { + argDesc := a.Description + if argDesc == "" { + if a.Type.NonNull { + argDesc = fmt.Sprintf("%s (%s, required)", a.Name, a.Type.String()) + } else { + argDesc = fmt.Sprintf("%s (%s, optional)", a.Name, a.Type.String()) + } + } + jsonType, itemsType := mapGraphQLType(a.Type, schema) + args = append(args, mcpserver.ArgDefinition{ + Name: a.Name, + Type: jsonType, + Description: argDesc, + Required: a.Type.NonNull, + ItemsType: itemsType, + EnumValues: enumValues(a.Type, schema), + }) + } + + query := buildQueryString(field, selection) + + var annotations *mcp.ToolAnnotations + if readOnly { + f := false + annotations = &mcp.ToolAnnotations{ + ReadOnlyHint: true, + DestructiveHint: &f, + OpenWorldHint: &f, + IdempotentHint: true, + } + } + + tools = append(tools, mcpserver.ToolDefinition{ + Name: toolName, + Description: description, + Args: args, + Query: query, + Annotations: annotations, + }) + } + + return tools, nil +} + +// generateCondensedSDL produces a compact GraphQL SDL representation of the schema. +// It strips directive definitions and introspection built-ins, omits deprecated fields, +// collapses Edge/Connection types, collapses @isSignal fields into compact tables, +// preserves @oneOf and default values, and inlines @mcpExample directives as comments. +func generateCondensedSDL(schema *ast.Schema) string { + analysis := analyzeSchema(schema) + var sb strings.Builder + + builtinTypes := map[string]bool{ + "String": true, "Int": true, "Float": true, "Boolean": true, "ID": true, + } + + operationTypes := map[string]bool{} + if schema.Query != nil { + operationTypes[schema.Query.Name] = true + } + if schema.Mutation != nil { + operationTypes[schema.Mutation.Name] = true + } + if schema.Subscription != nil { + operationTypes[schema.Subscription.Name] = true + } + + // Collect non-builtin type names, split into scalars and the rest. + var scalarNames []string + var typeNames []string + for name := range schema.Types { + if builtinTypes[name] || strings.HasPrefix(name, "__") || operationTypes[name] { + continue + } + if analysis.edgeTypes[name] || analysis.connectionTypes[name] { + continue + } + if schema.Types[name].Kind == ast.Scalar { + scalarNames = append(scalarNames, name) + } else { + typeNames = append(typeNames, name) + } + } + sort.Strings(scalarNames) + sort.Strings(typeNames) + + // Emit scalars and convention notes first so LLMs have type context before seeing fields. + for _, name := range scalarNames { + writeTypeDefinition(&sb, schema.Types[name], &analysis) + } + + // Convention notes for collapsed patterns. + var notes []string + if analysis.hasDIDFields { + notes = append(notes, "# All tokenDID fields use the format did:erc721:::") + } + if len(analysis.edgeTypes) > 0 { + notes = append(notes, "# All *Edge types: { node: T!, cursor: String! }") + } + if len(analysis.connectionTypes) > 0 { + notes = append(notes, "# All *Connection types: { totalCount: Int!, edges: [TEdge!]!, nodes: [T!]!, pageInfo: PageInfo! }") + } + if len(notes) > 0 { + if sb.Len() > 0 { + sb.WriteString("\n") + } + sb.WriteString(strings.Join(notes, "\n")) + sb.WriteString("\n") + } + + // Signal reference table next (before queries so LLMs see the signal catalog first). + if len(analysis.signalTypes) > 0 { + writeSignalReferenceTable(&sb, schema, &analysis) + } + + // Then operations. + if schema.Query != nil { + if sb.Len() > 0 { + sb.WriteString("\n") + } + writeOperationType(&sb, schema.Query) + } + if schema.Mutation != nil { + if sb.Len() > 0 { + sb.WriteString("\n") + } + writeOperationType(&sb, schema.Mutation) + } + if schema.Subscription != nil { + if sb.Len() > 0 { + sb.WriteString("\n") + } + writeOperationType(&sb, schema.Subscription) + } + + // Then all other types. + for _, name := range typeNames { + if sb.Len() > 0 { + sb.WriteString("\n") + } + writeTypeDefinition(&sb, schema.Types[name], &analysis) + } + + return sb.String() +} diff --git a/cmd/mcpgen/condensed_filter.go b/cmd/mcpgen/condensed_filter.go new file mode 100644 index 0000000..481d22f --- /dev/null +++ b/cmd/mcpgen/condensed_filter.go @@ -0,0 +1,148 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/vektah/gqlparser/v2/ast" +) + +// isDeprecated returns true if the directive list includes @deprecated. +func isDeprecated(dirs ast.DirectiveList) bool { + return dirs.ForName("deprecated") != nil +} + +// isHidden returns true if the directive list includes @mcpHide. +func isHidden(dirs ast.DirectiveList) bool { + return dirs.ForName("mcpHide") != nil +} + +// shouldOmitField returns true if the field/value should be excluded from condensed output. +func shouldOmitField(dirs ast.DirectiveList) bool { + return isDeprecated(dirs) || isHidden(dirs) +} + +// selfEvidentFields are field names whose semantics are universally understood +// (comparison operators, standard filter fields) and never need descriptions. +var selfEvidentFields = map[string]bool{ + "eq": true, "neq": true, "gt": true, "lt": true, "gte": true, "lte": true, + "in": true, "notIn": true, "or": true, "and": true, "not": true, + "containsAll": true, "containsAny": true, +} + +// stopWords are filler words ignored when comparing descriptions against field/type names. +var stopWords = map[string]bool{ + "the": true, "of": true, "for": true, "this": true, "a": true, "an": true, + "is": true, "in": true, "to": true, "and": true, "or": true, "by": true, + "on": true, "at": true, "from": true, "with": true, "its": true, "that": true, + "true": true, "false": true, +} + +// isSelfEvidentDescription returns true if a description trivially restates the field name. +// Uses word-overlap: if every non-stop-word in the description also appears in the +// field name or type name (split on camelCase boundaries), the description adds nothing. +func isSelfEvidentDescription(desc, fieldName, typeName string) bool { + if desc == "" { + return false + } + if selfEvidentFields[fieldName] { + return true + } + + // Build the set of "known" words from field name + type name. + // Include both the camelCase-split words and the raw lowercase forms, + // since descriptions may use either "dataVersion" or "data version". + knownWords := make(map[string]bool) + knownWords[strings.ToLower(fieldName)] = true + knownWords[strings.ToLower(typeName)] = true + for _, w := range strings.Fields(camelToSpaced(fieldName)) { + knownWords[w] = true + } + for _, w := range strings.Fields(camelToSpaced(typeName)) { + knownWords[w] = true + } + + // Tokenize description, strip punctuation, check if all meaningful words are known. + for _, w := range strings.Fields(strings.ToLower(desc)) { + w = strings.Trim(w, ".,;:!?'\"()-") + if w == "" || len(w) <= 1 || stopWords[w] { + continue + } + if !knownWords[w] { + return false + } + } + return true +} + +// camelToSpaced converts PascalCase/camelCase to lowercase space-separated words. +// e.g., "DeviceDefinition" → "device definition" +func camelToSpaced(s string) string { + var sb strings.Builder + for i, r := range s { + if i > 0 && r >= 'A' && r <= 'Z' { + sb.WriteByte(' ') + } + sb.WriteRune(r) + } + return strings.ToLower(sb.String()) +} + +// paginationArgs are standard Relay pagination argument names whose descriptions +// are well-known and add no value for LLMs. +var paginationArgs = map[string]bool{ + "first": true, "after": true, "last": true, "before": true, +} + +// isPaginationArg returns true if the argument is a standard Relay pagination argument. +func isPaginationArg(name string) bool { + return paginationArgs[name] +} + +// didFormatPattern matches the common DID format description that appears on many tokenDID fields. +var didFormatPattern = regexp.MustCompile(`(?i)did:erc721:::`) + +// containsDIDFormat returns true if a description contains the DID format string. +func containsDIDFormat(desc string) bool { + return didFormatPattern.MatchString(desc) +} + +// filterFieldDescription returns the description to use for a field, or "" if it should be omitted. +// It strips self-evident descriptions and DID format descriptions. +func filterFieldDescription(desc, fieldName, typeName string) string { + if desc == "" { + return "" + } + if isSelfEvidentDescription(desc, fieldName, typeName) { + return "" + } + if containsDIDFormat(desc) { + return "" + } + return desc +} + +// filterArgDescription returns the description to use for an argument, or "" if it should be omitted. +// Pagination arguments and self-evident descriptions are stripped. +func filterArgDescription(desc, argName string) string { + if desc == "" || isPaginationArg(argName) { + return "" + } + + knownWords := make(map[string]bool) + knownWords[strings.ToLower(argName)] = true + for _, w := range strings.Fields(camelToSpaced(argName)) { + knownWords[w] = true + } + + for _, w := range strings.Fields(strings.ToLower(desc)) { + w = strings.Trim(w, ".,;:!?'\"()-") + if w == "" || len(w) <= 1 || stopWords[w] { + continue + } + if !knownWords[w] { + return desc + } + } + return "" +} diff --git a/cmd/mcpgen/condensed_sdl.go b/cmd/mcpgen/condensed_sdl.go new file mode 100644 index 0000000..05974cc --- /dev/null +++ b/cmd/mcpgen/condensed_sdl.go @@ -0,0 +1,448 @@ +package main + +import ( + "strings" + + "github.com/vektah/gqlparser/v2/ast" +) + +// writeOperationType writes a Query, Mutation, or Subscription type in SDL format, +// including inline @mcpExample comments after annotated fields. +// Deprecated fields are omitted. +func writeOperationType(sb *strings.Builder, def *ast.Definition) { + sb.WriteString("type ") + sb.WriteString(def.Name) + sb.WriteString(" {\n") + + prevHadExtra := false + for _, field := range def.Fields { + if strings.HasPrefix(field.Name, "__") || shouldOmitField(field.Directives) { + continue + } + + desc := filterFieldDescription(field.Description, field.Name, def.Name) + hasExtra := desc != "" + + if prevHadExtra { + sb.WriteString("\n") + } + + if desc != "" { + writeDescription(sb, desc, " ") + } + + sb.WriteString(" ") + sb.WriteString(field.Name) + writeFieldArguments(sb, field.Arguments) + sb.WriteString(": ") + sb.WriteString(field.Type.String()) + sb.WriteString("\n") + + for _, dir := range field.Directives { + if dir.Name != "mcpExample" { + continue + } + descArg := dir.Arguments.ForName("description") + queryArg := dir.Arguments.ForName("query") + if descArg != nil && queryArg != nil { + sb.WriteString(" # Example - ") + sb.WriteString(descArg.Value.Raw) + sb.WriteString(":\n") + sb.WriteString(" # ") + sb.WriteString(queryArg.Value.Raw) + sb.WriteString("\n") + hasExtra = true + } + } + + prevHadExtra = hasExtra + } + + sb.WriteString("}\n") +} + +// writeTypeDefinition writes a non-operation type in SDL format. +// It skips deprecated fields, preserves @oneOf on input types, adds scalar descriptions, +// and collapses @isSignal fields into compact tables. +// Response-only types and types without useful descriptions use compact single-line format. +func writeTypeDefinition(sb *strings.Builder, def *ast.Definition, analysis *schemaAnalysis) { + switch def.Kind { + case ast.Object, ast.InputObject: + isResponseOnly := def.Kind == ast.Object && !analysis.queryReachable[def.Name] && !analysis.signalTypes[def.Name] + + // Signal types always get the categorized table treatment. + // Response-only types and types without useful descriptions use single-line format. + if !analysis.signalTypes[def.Name] && (isResponseOnly || !hasUsefulFieldDescriptions(def)) { + writeCompactInputOrType(sb, def) + return + } + + writeTypeHeader(sb, def) + sb.WriteString(" {\n") + + if analysis.signalTypes[def.Name] { + writeFieldsWithSignalCollapsing(sb, def) + } else { + for _, field := range def.Fields { + if shouldOmitField(field.Directives) { + continue + } + desc := filterFieldDescription(field.Description, field.Name, def.Name) + if desc != "" { + writeDescription(sb, desc, " ") + } + sb.WriteString(" ") + sb.WriteString(field.Name) + writeFieldArguments(sb, field.Arguments) + sb.WriteString(": ") + sb.WriteString(field.Type.String()) + writeFieldDefaultValue(sb, field) + sb.WriteString("\n") + } + } + sb.WriteString("}\n") + + case ast.Enum: + // Auto-compact enums with no useful value descriptions. + if !hasUsefulEnumDescriptions(def) { + writeCompactEnum(sb, def) + return + } + sb.WriteString("enum ") + sb.WriteString(def.Name) + sb.WriteString(" {\n") + for _, val := range def.EnumValues { + if shouldOmitField(val.Directives) { + continue + } + if val.Description != "" { + writeDescription(sb, val.Description, " ") + } + sb.WriteString(" ") + sb.WriteString(val.Name) + sb.WriteString("\n") + } + sb.WriteString("}\n") + + case ast.Interface: + // Use compact format for interfaces. + sb.WriteString("interface ") + sb.WriteString(def.Name) + sb.WriteString(" { ") + first := true + for _, field := range def.Fields { + if shouldOmitField(field.Directives) { + continue + } + if !first { + sb.WriteString(", ") + } + sb.WriteString(field.Name) + sb.WriteString(": ") + sb.WriteString(field.Type.String()) + first = false + } + sb.WriteString(" }\n") + + case ast.Union: + sb.WriteString("union ") + sb.WriteString(def.Name) + sb.WriteString(" = ") + for i, t := range def.Types { + if i > 0 { + sb.WriteString(" | ") + } + sb.WriteString(t) + } + sb.WriteString("\n") + + case ast.Scalar: + sb.WriteString("scalar ") + sb.WriteString(def.Name) + if def.Description != "" { + sb.WriteString(" # ") + // Join multi-line descriptions into a single inline comment. + desc := strings.Join(strings.Fields(def.Description), " ") + sb.WriteString(desc) + } + sb.WriteString("\n") + } +} + +// hasUsefulFieldDescriptions returns true if any non-deprecated field on the type has a +// description that survives filtering (not self-evident, not DID format). +func hasUsefulFieldDescriptions(def *ast.Definition) bool { + for _, field := range def.Fields { + if shouldOmitField(field.Directives) { + continue + } + if filterFieldDescription(field.Description, field.Name, def.Name) != "" { + return true + } + } + return false +} + +// hasUsefulEnumDescriptions returns true if any enum value has a non-empty description. +func hasUsefulEnumDescriptions(def *ast.Definition) bool { + for _, val := range def.EnumValues { + if shouldOmitField(val.Directives) { + continue + } + if val.Description != "" { + return true + } + } + return false +} + +// writeTypeHeader writes the shared prefix for input/type definitions: +// keyword, name, @oneOf (if applicable), and implements clause. +func writeTypeHeader(sb *strings.Builder, def *ast.Definition) { + if def.Kind == ast.InputObject { + sb.WriteString("input ") + } else { + sb.WriteString("type ") + } + sb.WriteString(def.Name) + if def.Kind == ast.InputObject && def.Directives.ForName("oneOf") != nil { + sb.WriteString(" @oneOf") + } + if len(def.Interfaces) > 0 { + sb.WriteString(" implements ") + for i, iface := range def.Interfaces { + if i > 0 { + sb.WriteString(" & ") + } + sb.WriteString(iface) + } + } +} + +// writeCompactInputOrType emits an input or type with no useful descriptions as a single line. +func writeCompactInputOrType(sb *strings.Builder, def *ast.Definition) { + writeTypeHeader(sb, def) + sb.WriteString(" { ") + first := true + for _, field := range def.Fields { + if shouldOmitField(field.Directives) { + continue + } + if !first { + sb.WriteString(", ") + } + sb.WriteString(field.Name) + writeInlineArgs(sb, field.Arguments) + sb.WriteString(": ") + sb.WriteString(field.Type.String()) + writeFieldDefaultValue(sb, field) + first = false + } + sb.WriteString(" }\n") +} + +// writeCompactEnum emits an enum on a single line: enum Foo { A, B, C } +func writeCompactEnum(sb *strings.Builder, def *ast.Definition) { + sb.WriteString("enum ") + sb.WriteString(def.Name) + sb.WriteString(" { ") + first := true + for _, val := range def.EnumValues { + if shouldOmitField(val.Directives) { + continue + } + if !first { + sb.WriteString(", ") + } + sb.WriteString(val.Name) + first = false + } + sb.WriteString(" }\n") +} + +// writeInlineArgs writes arguments in compact inline format: (name: Type, name2: Type2) +// with no descriptions. Used in compact mode and for compact type fields. +func writeInlineArgs(sb *strings.Builder, args ast.ArgumentDefinitionList) { + if len(args) == 0 { + return + } + sb.WriteString("(") + for i, arg := range args { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(arg.Name) + sb.WriteString(": ") + sb.WriteString(arg.Type.String()) + writeDefaultValue(sb, arg) + } + sb.WriteString(")") +} + +// writeFieldArguments writes field arguments in SDL format. +// Uses inline format when no arguments have non-trivial descriptions, multi-line otherwise. +// Pagination args and self-evident arg descriptions are stripped. +// Preserves default values when present. +func writeFieldArguments(sb *strings.Builder, args ast.ArgumentDefinitionList) { + if len(args) == 0 { + return + } + + // Check if any arg has a meaningful description after filtering. + hasDescriptions := false + for _, arg := range args { + if filterArgDescription(arg.Description, arg.Name) != "" { + hasDescriptions = true + break + } + } + + if hasDescriptions { + sb.WriteString("(\n") + for _, arg := range args { + desc := filterArgDescription(arg.Description, arg.Name) + if desc != "" { + writeDescription(sb, desc, " ") + } + sb.WriteString(" ") + sb.WriteString(arg.Name) + sb.WriteString(": ") + sb.WriteString(arg.Type.String()) + writeDefaultValue(sb, arg) + sb.WriteString("\n") + } + sb.WriteString(" )") + } else { + sb.WriteString("(") + for i, arg := range args { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(arg.Name) + sb.WriteString(": ") + sb.WriteString(arg.Type.String()) + writeDefaultValue(sb, arg) + } + sb.WriteString(")") + } +} + +// writeRawDefault appends " = " to the builder for a default value. +func writeRawDefault(sb *strings.Builder, val *ast.Value) { + if val == nil { + return + } + sb.WriteString(" = ") + if val.Kind == ast.StringValue { + sb.WriteString("\"") + sb.WriteString(val.Raw) + sb.WriteString("\"") + } else { + sb.WriteString(val.Raw) + } +} + +// writeDefaultValue appends " = " to the builder if the argument has a default value. +func writeDefaultValue(sb *strings.Builder, arg *ast.ArgumentDefinition) { + writeRawDefault(sb, arg.DefaultValue) +} + +// writeFieldDefaultValue appends " = " for input object fields with default values. +func writeFieldDefaultValue(sb *strings.Builder, field *ast.FieldDefinition) { + writeRawDefault(sb, field.DefaultValue) +} + +// writeDescription writes a GraphQL description string with the given indentation. +// Short descriptions use inline "..." syntax. Long descriptions (>200 chars) or those +// containing quotes use block string syntax with word-wrapping for readability. +// Structural elements (list items starting with "- ", blank-line paragraph breaks) +// are preserved so LLMs can parse constraints and options. +func writeDescription(sb *strings.Builder, desc, indent string) { + // Collapse multi-line descriptions to single line for compactness. + collapsed := strings.Join(strings.Fields(desc), " ") + + needsBlock := strings.Contains(collapsed, "\"") || len(collapsed) > 200 + if !needsBlock { + sb.WriteString(indent) + sb.WriteString("\"") + sb.WriteString(collapsed) + sb.WriteString("\"\n") + return + } + + // Block string with structure-aware wrapping. + paragraphs := splitDescriptionParagraphs(desc) + sb.WriteString(indent) + sb.WriteString("\"\"\"\n") + for i, para := range paragraphs { + if para == "" { + // Blank line between paragraphs. + sb.WriteString("\n") + continue + } + for _, line := range wordWrap(para, 80) { + sb.WriteString(indent) + sb.WriteString(line) + sb.WriteString("\n") + } + // Add blank line between paragraphs (but not after the last one). + if i < len(paragraphs)-1 && paragraphs[i+1] != "" { + // Only if next paragraph is not already a blank separator. + } + } + sb.WriteString(indent) + sb.WriteString("\"\"\"\n") +} + +// splitDescriptionParagraphs splits a description into logical paragraphs. +// It treats blank lines as paragraph separators and lines starting with "- " +// as individual list items (each becomes its own paragraph prefixed with "- "). +func splitDescriptionParagraphs(desc string) []string { + rawLines := strings.Split(desc, "\n") + var paragraphs []string + var current []string + + flush := func() { + if len(current) > 0 { + paragraphs = append(paragraphs, strings.Join(strings.Fields(strings.Join(current, " ")), " ")) + current = nil + } + } + + for _, line := range rawLines { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + flush() + continue + } + if strings.HasPrefix(trimmed, "- ") { + flush() + // Keep the list item as its own paragraph with the "- " prefix. + paragraphs = append(paragraphs, "- "+strings.Join(strings.Fields(trimmed[2:]), " ")) + continue + } + current = append(current, trimmed) + } + flush() + return paragraphs +} + +// wordWrap splits text into lines of at most maxWidth characters, breaking at word boundaries. +func wordWrap(text string, maxWidth int) []string { + words := strings.Fields(text) + if len(words) == 0 { + return nil + } + var lines []string + current := words[0] + for _, w := range words[1:] { + if len(current)+1+len(w) > maxWidth { + lines = append(lines, current) + current = w + } else { + current += " " + w + } + } + lines = append(lines, current) + return lines +} diff --git a/cmd/mcpgen/condensed_signals.go b/cmd/mcpgen/condensed_signals.go new file mode 100644 index 0000000..8b41bfb --- /dev/null +++ b/cmd/mcpgen/condensed_signals.go @@ -0,0 +1,361 @@ +package main + +import ( + "fmt" + "regexp" + "sort" + "strings" + + "github.com/vektah/gqlparser/v2/ast" +) + +var unitRegexp = regexp.MustCompile(`(?i)unit:\s*'([^']+)'`) + +var privilegeRegexp = regexp.MustCompile(`(?i)required privileges:\s*\[([^\]]+)\]`) + +// signalGroup groups signal fields by their return type and argument signature. +type signalGroup struct { + returnType string + argSig string // e.g. "(agg: FloatAggregation!, filter: SignalFloatFilter)" + fields []*ast.FieldDefinition +} + +// typeArgInfo associates a signal type name with its grouped signal fields. +type typeArgInfo struct { + typeName string + groups []signalGroup +} + +// signalCategoryInfo holds a group of signals under a category label. +type signalCategoryInfo struct { + label string + prefix string + fields []*ast.FieldDefinition +} + +// writeSignalReferenceTable emits a standalone signal reference section before the signal types. +// This is shared by all signal types (e.g., SignalAggregations and SignalCollection). +func writeSignalReferenceTable(sb *strings.Builder, schema *ast.Schema, analysis *schemaAnalysis) { + // Use the first signal type (alphabetically) as the source for signal fields. + var signalTypeNames []string + for name := range analysis.signalTypes { + signalTypeNames = append(signalTypeNames, name) + } + sort.Strings(signalTypeNames) + if len(signalTypeNames) == 0 { + return + } + + // Collect signals and arg signatures from ALL signal types. + var signals []*ast.FieldDefinition + var typeInfos []typeArgInfo + + for _, typeName := range signalTypeNames { + def := schema.Types[typeName] + var typeSignals []*ast.FieldDefinition + for _, f := range def.Fields { + if shouldOmitField(f.Directives) || f.Directives.ForName("isSignal") == nil { + continue + } + typeSignals = append(typeSignals, f) + } + if len(signals) == 0 { + signals = typeSignals // use first type's signals for the reference table + } + typeInfos = append(typeInfos, typeArgInfo{typeName: typeName, groups: groupSignalFields(typeSignals)}) + } + + if len(signals) == 0 { + return + } + + sb.WriteString("\n") + fmt.Fprintf(sb, "# ═══ SIGNAL FIELDS (%d total) ═══\n", len(signals)) + + // Emit per-type calling conventions so the LLM knows how to query each + // signal type. The args depend on the parent type, not the individual signal. + sb.WriteString("#\n") + sb.WriteString("# All signals below exist on every signal type. Calling convention per type:\n") + for _, ti := range typeInfos { + fmt.Fprintf(sb, "# %s:\n", ti.typeName) + for _, g := range ti.groups { + fmt.Fprintf(sb, "# fieldName%s: %s\n", g.argSig, g.returnType) + } + } + sb.WriteString("#\n") + + // Emit markdown table. + sb.WriteString("# | Signal | Type | Unit | Description |\n") + sb.WriteString("# |--------|------|------|-------------|\n") + + categories := categorizeSignals(signals) + for _, cat := range categories { + priv := dominantPrivilege(cat.fields) + if priv != "" { + fmt.Fprintf(sb, "# ── %s (privilege: %s) ──\n", cat.label, priv) + } else { + fmt.Fprintf(sb, "# ── %s ──\n", cat.label) + } + for _, f := range cat.fields { + writeSignalTableRow(sb, f, priv) + } + } +} + +// writeFieldsWithSignalCollapsing writes fields for types that contain @isSignal fields. +// Non-signal fields are emitted normally. Signal fields reference the standalone table. +func writeFieldsWithSignalCollapsing(sb *strings.Builder, def *ast.Definition) { + var nonSignal []*ast.FieldDefinition + signalCount := 0 + + for _, f := range def.Fields { + if shouldOmitField(f.Directives) { + continue + } + if f.Directives.ForName("isSignal") != nil { + signalCount++ + } else { + nonSignal = append(nonSignal, f) + } + } + + // Emit non-signal fields normally. + for _, field := range nonSignal { + desc := filterFieldDescription(field.Description, field.Name, def.Name) + if desc != "" { + writeDescription(sb, desc, " ") + } + sb.WriteString(" ") + sb.WriteString(field.Name) + writeFieldArguments(sb, field.Arguments) + sb.WriteString(": ") + sb.WriteString(field.Type.String()) + sb.WriteString("\n") + } + + if signalCount > 0 { + fmt.Fprintf(sb, " # + %d signal fields (see SIGNAL FIELDS table above)\n", signalCount) + } +} + +// writeSignalTableRow writes a signal as a markdown table row. +// categoryPrivilege is the dominant privilege for the category; if the field's +// privilege differs, it is shown inline in the description column. +func writeSignalTableRow(sb *strings.Builder, f *ast.FieldDefinition, categoryPrivilege string) { + rt := baseSignalType(f.Type) + unit := extractUnit(f.Description) + shortDesc := extractShortDescription(f.Description) + // Drop descriptions that just restate the signal name + unit. + if !isNonObviousSignalDesc(shortDesc, f.Name) { + shortDesc = "" + } + + // Show privilege inline if it differs from the category's dominant privilege. + fieldPriv := extractPrivilege(f.Description) + if fieldPriv != "" && fieldPriv != categoryPrivilege { + if shortDesc != "" { + shortDesc += " (privilege: " + fieldPriv + ")" + } else { + shortDesc = "privilege: " + fieldPriv + } + } + + fmt.Fprintf(sb, "# | %s | %s | %s | %s |\n", f.Name, rt, unit, shortDesc) +} + +// baseSignalType returns the base type name for a signal field's return type. +// For wrapper types like SignalFloat, SignalString, SignalLocation it strips the +// "Signal" prefix. For plain types (Float, String, Location) it returns as-is. +func baseSignalType(t *ast.Type) string { + name := t.NamedType + if t.Elem != nil { + name = t.Elem.NamedType + } + if after, ok := strings.CutPrefix(name, "Signal"); ok && after != "" { + return after + } + return name +} + +// isNonObviousSignalDesc returns true if a signal description adds value beyond the name+unit. +// Compares description words against the camelCase-split field name. If fewer than 2 words +// in the description are novel (not in the field name and not stop words), it's redundant. +func isNonObviousSignalDesc(desc, fieldName string) bool { + if desc == "" { + return false + } + nameWords := strings.Fields(strings.ToLower(camelToSpaced(fieldName))) + nameSet := make(map[string]bool, len(nameWords)) + for _, w := range nameWords { + nameSet[w] = true + } + descWords := strings.Fields(strings.ToLower(desc)) + novelWords := 0 + for _, w := range descWords { + w = strings.Trim(w, ".,;:!?'\"()-") + if w == "" || stopWords[w] { + continue + } + if !nameSet[w] { + novelWords++ + } + } + return novelWords >= 2 +} + +// categorizeSignals groups signal fields by their first camelCase word prefix. +// Prefixes with fewer than 2 signals are grouped into "other". +func categorizeSignals(signals []*ast.FieldDefinition) []signalCategoryInfo { + // Count signals per depth-1 prefix. + prefixCount := map[string]int{} + for _, f := range signals { + if p := firstCamelPrefix(f.Name); p != "" { + prefixCount[p]++ + } + } + + // Assign each signal to its prefix (if count >= 2) or "other". + type assignment struct { + field *ast.FieldDefinition + prefix string + } + var assigns []assignment + for _, f := range signals { + p := firstCamelPrefix(f.Name) + if p != "" && prefixCount[p] >= 2 { + assigns = append(assigns, assignment{field: f, prefix: p}) + } else { + assigns = append(assigns, assignment{field: f, prefix: "other"}) + } + } + + // Group by prefix, maintaining first-seen order. + var prefixOrder []string + groupMap := map[string]*signalCategoryInfo{} + for _, a := range assigns { + if g, ok := groupMap[a.prefix]; ok { + g.fields = append(g.fields, a.field) + } else { + label := prefixToLabel(a.prefix) + g := &signalCategoryInfo{label: label, prefix: a.prefix, fields: []*ast.FieldDefinition{a.field}} + groupMap[a.prefix] = g + prefixOrder = append(prefixOrder, a.prefix) + } + } + + categories := make([]signalCategoryInfo, 0, len(prefixOrder)) + for _, p := range prefixOrder { + categories = append(categories, *groupMap[p]) + } + return categories +} + +// firstCamelPrefix returns the first word of a camelCase name (before the first uppercase letter), +// or "" if the name has no camelCase boundary. +func firstCamelPrefix(name string) string { + for i, r := range name { + if i > 0 && r >= 'A' && r <= 'Z' { + return name[:i] + } + } + return "" +} + +// prefixToLabel converts a depth-1 camelCase prefix to an uppercase category label. +func prefixToLabel(prefix string) string { + return strings.ToUpper(prefix) +} + +// dominantPrivilege finds the most common privilege string among a group of fields. +// On ties, the alphabetically-first privilege wins for determinism. +func dominantPrivilege(fields []*ast.FieldDefinition) string { + counts := map[string]int{} + for _, f := range fields { + m := privilegeRegexp.FindStringSubmatch(f.Description) + if len(m) >= 2 { + counts[m[1]]++ + } + } + best := "" + bestCount := 0 + for p, c := range counts { + if c > bestCount || (c == bestCount && (best == "" || p < best)) { + best = p + bestCount = c + } + } + return best +} + +// extractPrivilege returns the privilege string from a single field's description. +func extractPrivilege(desc string) string { + m := privilegeRegexp.FindStringSubmatch(desc) + if len(m) >= 2 { + return m[1] + } + return "" +} + +// groupSignalFields groups fields by their return type and argument signature. +func groupSignalFields(fields []*ast.FieldDefinition) []signalGroup { + type groupKey struct { + returnType string + argSig string + } + keyOrder := []groupKey{} + groupMap := map[groupKey]*signalGroup{} + + for _, f := range fields { + rt := f.Type.String() + sig := buildArgSignature(f.Arguments) + key := groupKey{returnType: rt, argSig: sig} + if g, ok := groupMap[key]; ok { + g.fields = append(g.fields, f) + } else { + g := &signalGroup{returnType: rt, argSig: sig, fields: []*ast.FieldDefinition{f}} + groupMap[key] = g + keyOrder = append(keyOrder, key) + } + } + + groups := make([]signalGroup, 0, len(keyOrder)) + for _, k := range keyOrder { + groups = append(groups, *groupMap[k]) + } + return groups +} + +// buildArgSignature creates a compact string representation of a field's argument list. +func buildArgSignature(args ast.ArgumentDefinitionList) string { + if len(args) == 0 { + return "()" + } + var parts []string + for _, a := range args { + parts = append(parts, a.Name+": "+a.Type.String()) + } + return "(" + strings.Join(parts, ", ") + ")" +} + +// extractUnit pulls the unit string from a signal field description, e.g. "Unit: 'km/h'" → "km/h". +func extractUnit(desc string) string { + m := unitRegexp.FindStringSubmatch(desc) + if len(m) >= 2 { + return m[1] + } + return "" +} + +// extractShortDescription returns the first sentence of a description, stripping unit/privilege info. +func extractShortDescription(desc string) string { + // Take first line or sentence. + s := desc + if idx := strings.Index(s, "\n"); idx >= 0 { + s = s[:idx] + } + if idx := strings.Index(s, ". "); idx >= 0 { + s = s[:idx] + } + s = strings.TrimSuffix(s, ".") + return strings.TrimSpace(s) +} diff --git a/cmd/mcpgen/condensed_test.go b/cmd/mcpgen/condensed_test.go new file mode 100644 index 0000000..79d957d --- /dev/null +++ b/cmd/mcpgen/condensed_test.go @@ -0,0 +1,440 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" +) + +func TestCondensedSDLBasic(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/basic.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Should contain Query type with all fields (including non-annotated ones) + assert.Contains(t, sdl, "type Query {") + assert.Contains(t, sdl, "hello(name: String!): String!") + assert.Contains(t, sdl, "vehicle(tokenId: Int!): Vehicle") + assert.Contains(t, sdl, "noMcp: String!") + + // Response-only types should use compact single-line format. + assert.Contains(t, sdl, "type Vehicle { tokenId: Int!, owner: String! }") + + // Should NOT contain directive definitions or introspection types + assert.NotContains(t, sdl, "directive @mcpTool") + assert.NotContains(t, sdl, "__Schema") + assert.NotContains(t, sdl, "__Type") +} + +func TestCondensedSDLWithExamples(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/examples.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Should contain inline examples as comments + assert.Contains(t, sdl, "# Example - Get vehicle 123:") + assert.Contains(t, sdl, "# { vehicle(tokenId: 123) { tokenId owner } }") + assert.Contains(t, sdl, "# Example - Get just the owner:") + assert.Contains(t, sdl, "# { vehicle(tokenId: 456) { owner } }") + assert.Contains(t, sdl, "# Example - Find Teslas:") + + // Response-only types should use compact single-line format. + assert.Contains(t, sdl, "type Vehicle {") +} + +func TestCondensedSDLAllTypes(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/all_types.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Should contain enum + assert.Contains(t, sdl, "enum Status {") + assert.Contains(t, sdl, "ACTIVE") + assert.Contains(t, sdl, "INACTIVE") + + // Should contain custom scalar + assert.Contains(t, sdl, "scalar DateTime") + + // Should NOT contain built-in scalars + assert.NotContains(t, sdl, "scalar String") + assert.NotContains(t, sdl, "scalar Int") + assert.NotContains(t, sdl, "scalar Boolean") +} + +func TestCondensedSDLDocstrings(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/docstrings.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Should include explicit docstrings on arguments + assert.Contains(t, sdl, "The vehicle's NFT token ID") +} + +func TestGeneratedOutputWithCondensedSchema(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/basic.graphqls"}) + require.NoError(t, err) + + tools, err := extractTools(schema, "test") + require.NoError(t, err) + + condensedSDL := generateCondensedSDL(schema) + output, err := generateGoFile("graph", tools, condensedSDL) + require.NoError(t, err) + + assert.Contains(t, output, "var CondensedSchema = ") + assert.Contains(t, output, "type Query") +} + +func TestCondensedSDLDeprecated(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/deprecated.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Vehicle is response-only, compact format. Deprecated field 'image' excluded. + assert.Contains(t, sdl, "type Vehicle { tokenId: Int!, owner: String!, imageURI: String! }") + assert.NotContains(t, sdl, "image: String") + + // Deprecated enum value should be omitted. + assert.Contains(t, sdl, "ACTIVE") + assert.Contains(t, sdl, "INACTIVE") + assert.NotContains(t, sdl, "PENDING") +} + +func TestCondensedSDLOneOf(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/oneof.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Input type should have @oneOf annotation. + assert.Contains(t, sdl, "input AftermarketDeviceBy @oneOf {") +} + +func TestCondensedSDLScalarDescriptions(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/scalar_descriptions.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Scalars should have inline comments. + assert.Contains(t, sdl, "scalar Address # 0x-prefixed checksummed hex address") + assert.Contains(t, sdl, "scalar Time # RFC-3339 date-time string") + assert.Contains(t, sdl, "scalar Bytes # lowercase hex with 0x prefix") +} + +func TestCondensedSDLDefaultValues(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/defaults.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Default values should be preserved. + assert.Contains(t, sdl, "maxGapSeconds: Int = 300") + assert.Contains(t, sdl, "minDurationSeconds: Int = 60") + assert.Contains(t, sdl, "format: OutputFormat = JSON") +} + +func TestCondensedSDLEdgeConnection(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/edge_connection.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Edge types should be omitted. + assert.NotContains(t, sdl, "type VehicleEdge") + assert.NotContains(t, sdl, "type DCNEdge") + + // Connection types should be omitted. + assert.NotContains(t, sdl, "type VehicleConnection") + assert.NotContains(t, sdl, "type DCNConnection") + + // Summary comments should be present. + assert.Contains(t, sdl, "# All *Edge types: { node: T!, cursor: String! }") + assert.Contains(t, sdl, "# All *Connection types:") + + // Response-only types should use compact single-line format. + assert.Contains(t, sdl, "type PageInfo {") + assert.Contains(t, sdl, "type Vehicle {") + assert.Contains(t, sdl, "type DCN {") +} + +func TestCondensedSDLSignalCollapsing(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/signals.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Non-signal field should be emitted normally. + assert.Contains(t, sdl, "timestamp: String!") + + // Should have signal header with total count (9 signals now with approximate field). + assert.Contains(t, sdl, "SIGNAL FIELDS (9 total)") + + // Should have per-type calling conventions. + assert.Contains(t, sdl, "All signals below exist on every signal type") + assert.Contains(t, sdl, "SignalAggregations:") + assert.Contains(t, sdl, "fieldName(agg: FloatAggregation!, filter: SignalFloatFilter): Float") + + // Should have markdown table header with Type column. + assert.Contains(t, sdl, "| Signal | Type | Unit | Description |") + + // Individual signal fields should appear as table rows with base type. + // Self-evident descriptions (speed, engine speed) are dropped. + assert.Contains(t, sdl, "| speed | Float | km/h |") + assert.Contains(t, sdl, "| powertrainCombustionEngineSpeed | Float | rpm |") + // Non-obvious descriptions (PID codes) are kept. + assert.Contains(t, sdl, "| obdRunTime | Float | s | PID 1F - Engine run time |") + // String type shown. + assert.Contains(t, sdl, "| vin | String |") + + // Category headers should use separator-line format, not table rows. + assert.Contains(t, sdl, "── OTHER (privilege: VEHICLE_NON_LOCATION_DATA) ──") + // On tie, alphabetically-first privilege wins: VEHICLE_ALL_TIME_LOCATION. + assert.Contains(t, sdl, "── CURRENT (privilege: VEHICLE_ALL_TIME_LOCATION) ──") + assert.NotContains(t, sdl, "| **") + + // Per-field privilege override: the approximate field's privilege differs from + // the category's dominant (VEHICLE_ALL_TIME_LOCATION), so it's shown inline. + assert.Contains(t, sdl, "| currentLocationApproximateLatitude | Float | degrees | privilege: VEHICLE_APPROXIMATE_LOCATION |") + + // Signal fields should NOT appear as full field definitions. + assert.NotContains(t, sdl, "speed(agg: FloatAggregation!") +} + +func TestCondensedSDLLongDescription(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/long_description.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Long description should use block string syntax. + assert.Contains(t, sdl, `"""`) + // Lines should be word-wrapped (no single line >200 chars in the description). + for _, line := range strings.Split(sdl, "\n") { + trimmed := strings.TrimSpace(line) + // Skip non-description lines. + if trimmed == `"""` || trimmed == "" || strings.HasPrefix(trimmed, "#") { + continue + } + // Description content lines within block strings should be ≤ ~85 chars + // (80 wrap + indent). We check raw line length is reasonable. + if len(line) > 200 { + t.Errorf("line too long (%d chars): %s", len(line), line[:80]+"...") + } + } + // Key constraints should still be present in the wrapped output. + assert.Contains(t, sdl, "31 days") + assert.Contains(t, sdl, "mechanism") + + // List items should be preserved as separate lines, not collapsed into prose. + assert.Contains(t, sdl, "- ignitionDetection:") + assert.Contains(t, sdl, "- frequencyAnalysis:") + assert.Contains(t, sdl, "- changePointDetection:") + assert.Contains(t, sdl, "- refuel:") +} + +func TestWordWrap(t *testing.T) { + tests := []struct { + text string + width int + expected []string + }{ + {"short text", 80, []string{"short text"}}, + {"", 80, nil}, + { + "The quick brown fox jumps over the lazy dog and then runs away very quickly", + 30, + []string{ + "The quick brown fox jumps over", + "the lazy dog and then runs", + "away very quickly", + }, + }, + { + "superlongwordthatexceedswidth next", + 10, + []string{"superlongwordthatexceedswidth", "next"}, + }, + } + for _, tt := range tests { + result := wordWrap(tt.text, tt.width) + assert.Equal(t, tt.expected, result, "wordWrap(%q, %d)", tt.text, tt.width) + } +} + +func TestCondensedSDLSelfEvidentDescriptions(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/self_evident.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Attestation is response-only, compact single-line format (no descriptions). + assert.Contains(t, sdl, "type Attestation { id: String!, type: String!, dataVersion: String!, payload: String! }") + // Descriptions are not emitted in compact format, so self-evident check is moot here. + // Tested via unit tests below. +} + +func TestCondensedSDLPaginationArgsInlined(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/pagination_did.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Pagination args should be inlined (no multi-line descriptions). + // The vehicles query had descriptions only on pagination args + filterBy. + // filterBy has a real description, so it should still be multi-line. + assert.Contains(t, sdl, "filterBy: VehiclesFilter") + + // Pagination arg descriptions should NOT appear. + assert.NotContains(t, sdl, "Mutually exclusive with") + assert.NotContains(t, sdl, "A cursor for pagination") +} + +func TestCondensedSDLDIDDescriptionsStripped(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/pagination_did.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Vehicle is response-only, compact format — DID description stripped. + assert.Contains(t, sdl, "type Vehicle { tokenId: Int!, tokenDID: String!, owner: Address! }") + // No descriptions in compact format, so DID description is inherently excluded. + + // Summary comment for DID format should still appear. + assert.Contains(t, sdl, "# All tokenDID fields use the format did:erc721:") +} + +func TestCondensedSDLExpandedSelfEvident(t *testing.T) { + // Self-evident description patterns like "Model for this device definition." + // are tested via the isSelfEvidentDescription unit tests below. + // Response-only types are now omitted, so we only verify the unit test logic. +} + +func TestIsSelfEvidentDescription(t *testing.T) { + tests := []struct { + desc, field, typ string + expected bool + }{ + {"id", "id", "Attestation", true}, + {"type", "type", "Attestation", true}, + {"the id", "id", "Attestation", true}, + {"the id.", "id", "Attestation", true}, + {"The dataVersion of the Attestation.", "dataVersion", "Attestation", true}, + {"The dataVersion of the Attestation", "dataVersion", "Attestation", true}, + {"A meaningful description that should be kept.", "payload", "Attestation", false}, + {"Vehicle speed.", "speed", "SignalAggregations", false}, + {"", "id", "Foo", false}, + // " for this ." patterns + {"Model for this device definition.", "model", "DeviceDefinition", true}, + {"Name for this device definition.", "name", "DeviceDefinition", true}, + {"model for this DeviceDefinition", "model", "DeviceDefinition", true}, + // " of the ." patterns + {"model of the DeviceDefinition.", "model", "DeviceDefinition", true}, + } + for _, tt := range tests { + t.Run(tt.desc+"_"+tt.field, func(t *testing.T) { + assert.Equal(t, tt.expected, isSelfEvidentDescription(tt.desc, tt.field, tt.typ)) + }) + } +} + +func TestAnalyzeSchemaEdgeDetection(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/edge_connection.graphqls"}) + require.NoError(t, err) + + analysis := analyzeSchema(schema) + + assert.True(t, analysis.edgeTypes["VehicleEdge"]) + assert.True(t, analysis.edgeTypes["DCNEdge"]) + assert.False(t, analysis.edgeTypes["Vehicle"]) + + assert.True(t, analysis.connectionTypes["VehicleConnection"]) + assert.True(t, analysis.connectionTypes["DCNConnection"]) + assert.False(t, analysis.connectionTypes["PageInfo"]) +} + +func TestAnalyzeSchemaSignalDetection(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/signals.graphqls"}) + require.NoError(t, err) + + analysis := analyzeSchema(schema) + + assert.True(t, analysis.signalTypes["SignalAggregations"]) + assert.False(t, analysis.signalTypes["SignalFloatFilter"]) +} + +func TestCondensedSDLMcpHide(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/hidden.graphqls"}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + + // Hidden field should be excluded. + assert.NotContains(t, sdl, "internalScore") + // Non-hidden fields should remain. + assert.Contains(t, sdl, "tokenId") + assert.Contains(t, sdl, "owner") + assert.Contains(t, sdl, "name") +} + +func TestCondensedSDLStructuralValidity(t *testing.T) { + // Round-trip validation: for each testdata schema, generate condensed SDL, + // strip comment lines (signal tables, examples), and verify the remaining + // SDL is parseable as valid GraphQL. + entries, err := os.ReadDir("testdata") + require.NoError(t, err) + + for _, e := range entries { + if e.IsDir() || filepath.Ext(e.Name()) != ".graphqls" { + continue + } + t.Run(e.Name(), func(t *testing.T) { + schema, err := loadGraphQLSchema([]string{filepath.Join("testdata", e.Name())}) + require.NoError(t, err) + + sdl := generateCondensedSDL(schema) + require.NotEmpty(t, sdl) + + // Strip comment-only lines (signal tables, examples, convention notes). + var cleaned []string + for _, line := range strings.Split(sdl, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "#") { + continue + } + cleaned = append(cleaned, line) + } + cleanedSDL := strings.Join(cleaned, "\n") + if strings.TrimSpace(cleanedSDL) == "" { + return // all-comments output (unlikely but safe) + } + + // Parse the cleaned SDL to verify structural validity. + // Tolerate "Undefined type" errors for collapsed Edge/Connection types + // which are intentionally omitted and described via convention comments. + _, gqlErr := gqlparser.LoadSchema(&ast.Source{ + Name: "condensed", + Input: cleanedSDL, + }) + if gqlErr != nil { + for _, line := range strings.Split(gqlErr.Error(), "\n") { + if strings.Contains(line, "Undefined type") { + continue // expected for collapsed types + } + t.Errorf("condensed SDL has structural error:\n%s\n\nCleaned SDL:\n%s", line, cleanedSDL) + } + } + }) + } +} diff --git a/cmd/mcpgen/generate.go b/cmd/mcpgen/generate.go new file mode 100644 index 0000000..d339440 --- /dev/null +++ b/cmd/mcpgen/generate.go @@ -0,0 +1,279 @@ +package main + +import ( + "fmt" + "go/format" + "strings" + "text/template" + "unicode" + + "github.com/DIMO-Network/server-garage/pkg/mcpserver" + "github.com/vektah/gqlparser/v2/ast" +) + +// buildQueryString constructs a GraphQL query string from a field definition. +func buildQueryString(field *ast.FieldDefinition, selection string) string { + var sb strings.Builder + + // Build variable declarations + if len(field.Arguments) > 0 { + sb.WriteString("query(") + for i, a := range field.Arguments { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString("$") + sb.WriteString(a.Name) + sb.WriteString(": ") + sb.WriteString(a.Type.String()) + } + sb.WriteString(") { ") + } else { + sb.WriteString("{ ") + } + + // Build field call + sb.WriteString(field.Name) + if len(field.Arguments) > 0 { + sb.WriteString("(") + for i, a := range field.Arguments { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(a.Name) + sb.WriteString(": $") + sb.WriteString(a.Name) + } + sb.WriteString(")") + } + + // Add selection set + if selection != "" { + sb.WriteString(" { ") + sb.WriteString(selection) + sb.WriteString(" }") + } + + sb.WriteString(" }") + return sb.String() +} + +// validateSelection checks that top-level field names in the selection exist on the type. +func validateSelection(selection string, typeDef *ast.Definition) error { + fields := extractTopLevelFields(selection) + for _, f := range fields { + found := false + for _, tf := range typeDef.Fields { + if tf.Name == f { + found = true + break + } + } + if !found { + return fmt.Errorf("field %q not found on type %s", f, typeDef.Name) + } + } + return nil +} + +func isIdentChar(r rune) bool { + return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' +} + +// extractTopLevelFields extracts top-level field names from a selection string, +// skipping nested { } blocks, fragment spreads, aliases, and directives. +func extractTopLevelFields(selection string) []string { + var fields []string + depth := 0 + parenDepth := 0 + skipCount := 0 + tokens := strings.Fields(selection) + for _, tok := range tokens { + // Save previous depth state to determine if we were already nested. + prevDepth := depth + prevParenDepth := parenDepth + + // Track brace and paren depth from characters in this token. + for _, ch := range tok { + switch ch { + case '{': + depth++ + case '}': + depth-- + case '(': + parenDepth++ + case ')': + parenDepth-- + } + } + + // Decrement skipCount before the depth check so it is always + // consumed, even when the skipped token also changes depth. + if skipCount > 0 { + skipCount-- + continue + } + + // Skip if we were inside a nested block before this token, + // or if this token opened/is inside a nested block. + if prevDepth > 0 || prevParenDepth > 0 || depth > 0 || parenDepth > 0 { + continue + } + + // Fragment spreads: "..." (followed by "on Type"), "...on Type", or "...FragName". + if strings.HasPrefix(tok, "...") { + rest := tok[3:] + if rest == "" { + // Bare "..." — skip "on" and type name. + skipCount = 2 + } else if strings.HasPrefix(rest, "on") && (len(rest) == 2 || !isIdentChar(rune(rest[2]))) { + // "...on Type" or "...onSomeType" — skip the type name. + skipCount = 1 + } + // "...FragmentName" — just skip the token itself (no trailing tokens to skip). + continue + } + + // Skip directives (e.g., @skip, @include). + if strings.HasPrefix(tok, "@") { + continue + } + + // Skip aliases — tokens ending with ":". + if strings.HasSuffix(tok, ":") { + continue + } + + // Clean attached braces and parens from field name. + clean := strings.TrimRight(tok, "{(") + if clean != "" && clean != "}" { + fields = append(fields, clean) + } + } + return fields +} + +// namedType unwraps list types (Elem) until it reaches a named type. +func namedType(t *ast.Type) string { + for t.Elem != nil { + t = t.Elem + } + return t.NamedType +} + +// mapScalarName maps a GraphQL scalar/type name to a JSON Schema type string. +func mapScalarName(name string, schema *ast.Schema) string { + switch name { + case "Int": + return "integer" + case "Float": + return "number" + case "Boolean": + return "boolean" + case "String", "ID": + return "string" + default: + if def, ok := schema.Types[name]; ok { + if def.Kind == ast.Scalar || def.Kind == ast.Enum { + return "string" + } + } + return "object" + } +} + +// mapGraphQLType converts a GraphQL type to a JSON Schema type string and optional items type. +// For list types, it returns ("array", elemType). +func mapGraphQLType(t *ast.Type, schema *ast.Schema) (jsonType, itemsType string) { + if t.Elem != nil { + elemType, _ := mapGraphQLType(t.Elem, schema) + return "array", elemType + } + return mapScalarName(t.NamedType, schema), "" +} + +// enumValues returns the allowed values for an enum type, or nil if not an enum. +// Deprecated enum values are excluded. +func enumValues(t *ast.Type, schema *ast.Schema) []string { + name := namedType(t) + def, ok := schema.Types[name] + if !ok || def.Kind != ast.Enum { + return nil + } + var vals []string + for _, v := range def.EnumValues { + if v.Directives.ForName("deprecated") != nil { + continue + } + vals = append(vals, v.Name) + } + if len(vals) == 0 { + return nil + } + return vals +} + +var goFileTemplate = template.Must(template.New("mcptools").Funcs(template.FuncMap{ + "deref": func(b *bool) bool { return *b }, +}).Parse(`// Code generated by mcpgen. DO NOT EDIT. +package {{.Package}} + +import ( + "github.com/DIMO-Network/server-garage/pkg/mcpserver" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func boolPtr(b bool) *bool { return &b } + +var MCPTools = []mcpserver.ToolDefinition{ +{{- range .Tools}} + { + Name: {{printf "%q" .Name}}, + Description: {{printf "%q" .Description}}, + Args: []mcpserver.ArgDefinition{ + {{- range .Args}} + {Name: {{printf "%q" .Name}}, Type: {{printf "%q" .Type}}, Description: {{printf "%q" .Description}}, Required: {{.Required}}, ItemsType: {{printf "%q" .ItemsType}}{{if .EnumValues}}, EnumValues: []string{ {{- range $i, $v := .EnumValues}}{{if $i}}, {{end}}{{printf "%q" $v}}{{end -}} }{{end}}}, + {{- end}} + }, + Query: {{printf "%q" .Query}}, + {{- if .Annotations}} + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: {{.Annotations.ReadOnlyHint}}, + {{- if .Annotations.DestructiveHint}} + DestructiveHint: boolPtr({{deref .Annotations.DestructiveHint}}), + {{- end}} + {{- if .Annotations.OpenWorldHint}} + OpenWorldHint: boolPtr({{deref .Annotations.OpenWorldHint}}), + {{- end}} + IdempotentHint: {{.Annotations.IdempotentHint}}, + }, + {{- end}} + }, +{{- end}} +} + +var CondensedSchema = {{printf "%q" .CondensedSchema}} +`)) + +type templateData struct { + Package string + Tools []mcpserver.ToolDefinition + CondensedSchema string +} + +// generateGoFile renders a Go source file containing tool definitions and condensed schema. +func generateGoFile(pkg string, tools []mcpserver.ToolDefinition, condensedSchema string) (string, error) { + var sb strings.Builder + if err := goFileTemplate.Execute(&sb, templateData{ + Package: pkg, + Tools: tools, + CondensedSchema: condensedSchema, + }); err != nil { + return "", fmt.Errorf("executing template: %w", err) + } + formatted, err := format.Source([]byte(sb.String())) + if err != nil { + return "", fmt.Errorf("formatting generated code: %w", err) + } + return string(formatted), nil +} diff --git a/cmd/mcpgen/generate_test.go b/cmd/mcpgen/generate_test.go new file mode 100644 index 0000000..8e00656 --- /dev/null +++ b/cmd/mcpgen/generate_test.go @@ -0,0 +1,246 @@ +package main + +import ( + "testing" + + "github.com/DIMO-Network/server-garage/pkg/mcpserver" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vektah/gqlparser/v2/ast" +) + +// loadTools is a test helper that loads schema files and extracts tools. +func loadTools(t *testing.T, paths []string, prefix string) []mcpserver.ToolDefinition { + t.Helper() + schema, err := loadGraphQLSchema(paths) + require.NoError(t, err) + tools, err := extractTools(schema, prefix) + require.NoError(t, err) + return tools +} + +func TestParseDirective(t *testing.T) { + tools := loadTools(t, []string{"testdata/basic.graphqls"}, "") + require.Len(t, tools, 2) + + // hello tool — no selection, one arg + assert.Equal(t, "hello", tools[0].Name) + assert.Equal(t, "Say hello to someone", tools[0].Description) + require.Len(t, tools[0].Args, 1) + assert.Equal(t, "name", tools[0].Args[0].Name) + assert.Equal(t, "string", tools[0].Args[0].Type) + assert.True(t, tools[0].Args[0].Required) + assert.Equal(t, `query($name: String!) { hello(name: $name) }`, tools[0].Query) + + // vehicle_info tool — with selection set + assert.Equal(t, "vehicle_info", tools[1].Name) + assert.Equal(t, "Get vehicle info", tools[1].Description) + require.Len(t, tools[1].Args, 1) + assert.Equal(t, "tokenId", tools[1].Args[0].Name) + assert.Equal(t, "integer", tools[1].Args[0].Type) + assert.True(t, tools[1].Args[0].Required) + assert.Equal(t, `query($tokenId: Int!) { vehicle(tokenId: $tokenId) { tokenId owner } }`, tools[1].Query) +} + +func TestPrefixApplied(t *testing.T) { + tools := loadTools(t, []string{"testdata/basic.graphqls"}, "telemetry") + require.Len(t, tools, 2) + + assert.Equal(t, "telemetry_hello", tools[0].Name) + assert.Equal(t, "telemetry_vehicle_info", tools[1].Name) +} + +func TestArgDescriptionFromDocString(t *testing.T) { + tools := loadTools(t, []string{"testdata/docstrings.graphqls"}, "") + require.Len(t, tools, 1) + require.Len(t, tools[0].Args, 1) + + assert.Equal(t, "The vehicle's NFT token ID", tools[0].Args[0].Description) +} + +func TestArgDescriptionFallback(t *testing.T) { + tools := loadTools(t, []string{"testdata/basic.graphqls"}, "") + require.Len(t, tools, 2) + + assert.Equal(t, "name (String!, required)", tools[0].Args[0].Description) + assert.Equal(t, "tokenId (Int!, required)", tools[1].Args[0].Description) +} + +func TestInvalidSelection(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/invalid_selection.graphqls"}) + require.NoError(t, err) + _, err = extractTools(schema, "") + require.Error(t, err) + assert.Contains(t, err.Error(), "nonExistentField") +} + +func TestGeneratedOutput(t *testing.T) { + tools := loadTools(t, []string{"testdata/basic.graphqls"}, "test") + + output, err := generateGoFile("graph", tools, "") + require.NoError(t, err) + + assert.Contains(t, output, "package graph") + assert.Contains(t, output, `"github.com/DIMO-Network/server-garage/pkg/mcpserver"`) + assert.Contains(t, output, "DO NOT EDIT") + assert.Contains(t, output, "test_hello") + assert.Contains(t, output, "test_vehicle_info") +} + +func TestListReturnTypeWithSelection(t *testing.T) { + tools := loadTools(t, []string{"testdata/list_types.graphqls"}, "") + require.Len(t, tools, 1) + + tool := tools[0] + assert.Equal(t, "list_vehicles", tool.Name) + require.Len(t, tool.Args, 1) + assert.Equal(t, "array", tool.Args[0].Type) + assert.Equal(t, "integer", tool.Args[0].ItemsType) +} + +func TestExtractTopLevelFields(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "simple fields", + input: "id name", + expected: []string{"id", "name"}, + }, + { + name: "nested braces", + input: "id vehicles { make model }", + expected: []string{"id", "vehicles"}, + }, + { + name: "inline fragment with space", + input: "... on Vehicle { id } name", + expected: []string{"name"}, + }, + { + name: "inline fragment no space", + input: "...on Vehicle { id } name", + expected: []string{"name"}, + }, + { + name: "alias", + input: "alias: name id", + expected: []string{"name", "id"}, + }, + { + name: "directive", + input: "name @skip(if: true) id", + expected: []string{"name", "id"}, + }, + { + name: "named fragment spread", + input: "id ...VehicleFields name", + expected: []string{"id", "name"}, + }, + { + name: "fragment with attached brace", + input: "... on Vehicle{ id } name", + expected: []string{"name"}, + }, + { + name: "empty", + input: "", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractTopLevelFields(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestExtractTopLevelFieldsInlineFragment(t *testing.T) { + fields := extractTopLevelFields("...onVehicle { id } name") + assert.Equal(t, []string{"name"}, fields) +} + +func TestEnumValuesExtracted(t *testing.T) { + tools := loadTools(t, []string{"testdata/all_types.graphqls"}, "") + require.Len(t, tools, 1) + + statusArg := tools[0].Args[5] + assert.Equal(t, "status", statusArg.Name) + assert.Equal(t, "string", statusArg.Type) + assert.Equal(t, []string{"ACTIVE", "INACTIVE"}, statusArg.EnumValues) + + nameArg := tools[0].Args[0] + assert.Equal(t, "name", nameArg.Name) + assert.Nil(t, nameArg.EnumValues) +} + +func TestDeprecatedEnumValuesExcluded(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/deprecated.graphqls"}) + require.NoError(t, err) + + // The Status enum has ACTIVE, INACTIVE, and PENDING @deprecated. + def := schema.Types["Status"] + require.NotNil(t, def) + + vals := enumValues(&ast.Type{NamedType: "Status"}, schema) + assert.Equal(t, []string{"ACTIVE", "INACTIVE"}, vals) + assert.NotContains(t, vals, "PENDING") +} + +func TestGeneratedCodeIncludesAnnotations(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/basic.graphqls"}) + require.NoError(t, err) + + tools, err := extractTools(schema, "test") + require.NoError(t, err) + require.Len(t, tools, 2) + require.NotNil(t, tools[0].Annotations, "tool should have annotations") + assert.True(t, tools[0].Annotations.ReadOnlyHint) +} + +func TestGeneratedCodeWithAnnotationsCompiles(t *testing.T) { + schema, err := loadGraphQLSchema([]string{"testdata/basic.graphqls"}) + require.NoError(t, err) + + tools, err := extractTools(schema, "test") + require.NoError(t, err) + + condensed := generateCondensedSDL(schema) + output, err := generateGoFile("testpkg", tools, condensed) + require.NoError(t, err) + assert.Contains(t, output, "mcp.ToolAnnotations") + assert.Contains(t, output, "ReadOnlyHint") + assert.Contains(t, output, "boolPtr") +} + +func TestMapGraphQLTypeAllScalars(t *testing.T) { + tools := loadTools(t, []string{"testdata/all_types.graphqls"}, "") + require.Len(t, tools, 1) + + args := tools[0].Args + require.Len(t, args, 7) + + expected := []struct { + name string + typ string + itemsType string + }{ + {"name", "string", ""}, + {"limit", "integer", ""}, + {"score", "number", ""}, + {"active", "boolean", ""}, + {"since", "string", ""}, + {"status", "string", ""}, + {"ids", "array", "integer"}, + } + + for i, exp := range expected { + assert.Equal(t, exp.name, args[i].Name, "arg %d name", i) + assert.Equal(t, exp.typ, args[i].Type, "arg %d type", i) + assert.Equal(t, exp.itemsType, args[i].ItemsType, "arg %d itemsType", i) + } +} diff --git a/cmd/mcpgen/main.go b/cmd/mcpgen/main.go new file mode 100644 index 0000000..ebcede2 --- /dev/null +++ b/cmd/mcpgen/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" +) + +func main() { + schemaDir := flag.String("schema", "", "directory containing .graphqls files") + prefix := flag.String("prefix", "", "tool name prefix") + out := flag.String("out", "", "output Go file path") + pkg := flag.String("package", "", "Go package name for generated file") + flag.Parse() + + if *schemaDir == "" || *out == "" || *pkg == "" { + fmt.Fprintln(os.Stderr, "usage: mcpgen -schema -prefix -out -package ") + os.Exit(1) + } + + // Collect .graphqls files + entries, err := os.ReadDir(*schemaDir) + if err != nil { + fmt.Fprintf(os.Stderr, "reading schema directory: %v\n", err) + os.Exit(1) + } + + var paths []string + for _, e := range entries { + if !e.IsDir() && filepath.Ext(e.Name()) == ".graphqls" { + paths = append(paths, filepath.Join(*schemaDir, e.Name())) + } + } + + if len(paths) == 0 { + fmt.Fprintln(os.Stderr, "no .graphqls files found in", *schemaDir) + os.Exit(1) + } + + schema, err := loadGraphQLSchema(paths) + if err != nil { + fmt.Fprintf(os.Stderr, "loading schema: %v\n", err) + os.Exit(1) + } + + tools, err := extractTools(schema, *prefix) + if err != nil { + fmt.Fprintf(os.Stderr, "extracting tools: %v\n", err) + os.Exit(1) + } + + condensedSDL := generateCondensedSDL(schema) + + output, err := generateGoFile(*pkg, tools, condensedSDL) + if err != nil { + fmt.Fprintf(os.Stderr, "generating output: %v\n", err) + os.Exit(1) + } + + if err := os.MkdirAll(filepath.Dir(*out), 0o755); err != nil { + fmt.Fprintf(os.Stderr, "creating output directory: %v\n", err) + os.Exit(1) + } + + if err := os.WriteFile(*out, []byte(output), 0o644); err != nil { + fmt.Fprintf(os.Stderr, "writing output: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Generated %d tool(s) in %s\n", len(tools), *out) +} diff --git a/cmd/mcpgen/testdata/all_types.graphqls b/cmd/mcpgen/testdata/all_types.graphqls new file mode 100644 index 0000000..dc40e5f --- /dev/null +++ b/cmd/mcpgen/testdata/all_types.graphqls @@ -0,0 +1,25 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +scalar DateTime + +enum Status { + ACTIVE + INACTIVE +} + +type Query { + search( + name: String! + limit: Int! + score: Float! + active: Boolean! + since: DateTime! + status: Status! + ids: [Int!]! + ): [Result!]! + @mcpTool(name: "search", description: "Search with all arg types", selection: "id") +} + +type Result { + id: ID! +} diff --git a/cmd/mcpgen/testdata/basic.graphqls b/cmd/mcpgen/testdata/basic.graphqls new file mode 100644 index 0000000..85c539f --- /dev/null +++ b/cmd/mcpgen/testdata/basic.graphqls @@ -0,0 +1,16 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + hello(name: String!): String! + @mcpTool(name: "hello", description: "Say hello to someone", selection: "") + + vehicle(tokenId: Int!): Vehicle + @mcpTool(name: "vehicle_info", description: "Get vehicle info", selection: "tokenId owner") + + noMcp: String! +} + +type Vehicle { + tokenId: Int! + owner: String! +} diff --git a/cmd/mcpgen/testdata/defaults.graphqls b/cmd/mcpgen/testdata/defaults.graphqls new file mode 100644 index 0000000..f5e6257 --- /dev/null +++ b/cmd/mcpgen/testdata/defaults.graphqls @@ -0,0 +1,21 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + signals(config: SegmentConfig): SignalData + @mcpTool(name: "signals", description: "Get signals", selection: "value") +} + +input SegmentConfig { + maxGapSeconds: Int = 300 + minDurationSeconds: Int = 60 + format: OutputFormat = JSON +} + +enum OutputFormat { + JSON + CSV +} + +type SignalData { + value: Float +} diff --git a/cmd/mcpgen/testdata/deprecated.graphqls b/cmd/mcpgen/testdata/deprecated.graphqls new file mode 100644 index 0000000..edf96a7 --- /dev/null +++ b/cmd/mcpgen/testdata/deprecated.graphqls @@ -0,0 +1,20 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + vehicle(tokenId: Int!): Vehicle + @mcpTool(name: "vehicle_info", description: "Get vehicle info", selection: "tokenId owner imageURI") +} + +type Vehicle { + tokenId: Int! + owner: String! + "Use imageURI instead" + image: String @deprecated(reason: "Use imageURI instead") + imageURI: String! +} + +enum Status { + ACTIVE + INACTIVE + PENDING @deprecated(reason: "Use ACTIVE instead") +} diff --git a/cmd/mcpgen/testdata/docstrings.graphqls b/cmd/mcpgen/testdata/docstrings.graphqls new file mode 100644 index 0000000..b0ff21e --- /dev/null +++ b/cmd/mcpgen/testdata/docstrings.graphqls @@ -0,0 +1,13 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + vehicle( + """The vehicle's NFT token ID""" + tokenId: Int! + ): Vehicle + @mcpTool(name: "vehicle_info", description: "Get vehicle info", selection: "tokenId") +} + +type Vehicle { + tokenId: Int! +} diff --git a/cmd/mcpgen/testdata/edge_connection.graphqls b/cmd/mcpgen/testdata/edge_connection.graphqls new file mode 100644 index 0000000..230adb5 --- /dev/null +++ b/cmd/mcpgen/testdata/edge_connection.graphqls @@ -0,0 +1,47 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + vehicles(first: Int, after: String): VehicleConnection! + @mcpTool(name: "vehicles", description: "List vehicles", selection: "nodes { tokenId owner }") +} + +type Vehicle { + tokenId: Int! + owner: String! +} + +type VehicleEdge { + node: Vehicle! + cursor: String! +} + +type DCNEdge { + node: DCN! + cursor: String! +} + +type VehicleConnection { + totalCount: Int! + edges: [VehicleEdge!]! + nodes: [Vehicle!]! + pageInfo: PageInfo! +} + +type DCNConnection { + totalCount: Int! + edges: [DCNEdge!]! + nodes: [DCN!]! + pageInfo: PageInfo! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} + +type DCN { + name: String! + owner: String! +} diff --git a/cmd/mcpgen/testdata/examples.graphqls b/cmd/mcpgen/testdata/examples.graphqls new file mode 100644 index 0000000..e71fa8b --- /dev/null +++ b/cmd/mcpgen/testdata/examples.graphqls @@ -0,0 +1,18 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION +directive @mcpExample(description: String!, query: String!) repeatable on FIELD_DEFINITION + +type Query { + vehicle(tokenId: Int!): Vehicle + @mcpTool(name: "vehicle_info", description: "Get vehicle info", selection: "tokenId owner") + @mcpExample(description: "Get vehicle 123", query: "{ vehicle(tokenId: 123) { tokenId owner } }") + @mcpExample(description: "Get just the owner", query: "{ vehicle(tokenId: 456) { owner } }") + + vehicles(make: String!): [Vehicle!]! + @mcpExample(description: "Find Teslas", query: "{ vehicles(make: \"Tesla\") { tokenId make } }") +} + +type Vehicle { + tokenId: Int! + owner: String! + make: String! +} diff --git a/cmd/mcpgen/testdata/hidden.graphqls b/cmd/mcpgen/testdata/hidden.graphqls new file mode 100644 index 0000000..5743050 --- /dev/null +++ b/cmd/mcpgen/testdata/hidden.graphqls @@ -0,0 +1,15 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION +directive @mcpHide on FIELD_DEFINITION + +type Query { + vehicle(tokenId: Int!): Vehicle + @mcpTool(name: "vehicle_info", description: "Get vehicle info", selection: "tokenId owner") +} + +type Vehicle { + tokenId: Int! + owner: String! + "Internal debug field" + internalScore: Float @mcpHide + name: String! +} diff --git a/cmd/mcpgen/testdata/invalid_selection.graphqls b/cmd/mcpgen/testdata/invalid_selection.graphqls new file mode 100644 index 0000000..09c3479 --- /dev/null +++ b/cmd/mcpgen/testdata/invalid_selection.graphqls @@ -0,0 +1,10 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + vehicle(tokenId: Int!): Vehicle + @mcpTool(name: "vehicle_info", description: "Get vehicle info", selection: "nonExistentField") +} + +type Vehicle { + tokenId: Int! +} diff --git a/cmd/mcpgen/testdata/list_types.graphqls b/cmd/mcpgen/testdata/list_types.graphqls new file mode 100644 index 0000000..fc781ab --- /dev/null +++ b/cmd/mcpgen/testdata/list_types.graphqls @@ -0,0 +1,11 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + vehicles(ids: [Int!]!): [Vehicle!]! + @mcpTool(name: "list_vehicles", description: "List vehicles by IDs", selection: "tokenId owner") +} + +type Vehicle { + tokenId: Int! + owner: String! +} diff --git a/cmd/mcpgen/testdata/long_description.graphqls b/cmd/mcpgen/testdata/long_description.graphqls new file mode 100644 index 0000000..21e6c81 --- /dev/null +++ b/cmd/mcpgen/testdata/long_description.graphqls @@ -0,0 +1,27 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + """ + Returns vehicle usage segments detected using the specified mechanism. + Maximum date range: 31 days. Detection mechanisms: + - ignitionDetection: Uses 'isIgnitionOn' signal with configurable debouncing + - frequencyAnalysis: Analyzes signal update frequency to detect activity periods + - changePointDetection: CUSUM-based regime change detection + - refuel: Refueling segments (fuel level increased) + Segment IDs are stable and consistent across queries as long as the segment + start is captured in the underlying data source. A default set of signal + requests is always applied. When signalRequests is provided, those requests + are added on top of the default set; duplicates (same name and agg) are omitted. + """ + segments(tokenId: Int!, from: String!, to: String!): SegmentResult + @mcpTool(name: "segments", description: "Get signal segments", selection: "buckets { timestamp }") +} + +type SegmentResult { + buckets: [Bucket!]! +} + +type Bucket { + timestamp: String! + value: Float +} diff --git a/cmd/mcpgen/testdata/oneof.graphqls b/cmd/mcpgen/testdata/oneof.graphqls new file mode 100644 index 0000000..3ace21e --- /dev/null +++ b/cmd/mcpgen/testdata/oneof.graphqls @@ -0,0 +1,18 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION +directive @oneOf on INPUT_OBJECT + +type Query { + aftermarketDevice(by: AftermarketDeviceBy!): AftermarketDevice + @mcpTool(name: "aftermarket_device", description: "Get aftermarket device", selection: "tokenId serial") +} + +input AftermarketDeviceBy @oneOf { + tokenId: Int + serial: String + address: String +} + +type AftermarketDevice { + tokenId: Int! + serial: String! +} diff --git a/cmd/mcpgen/testdata/pagination_did.graphqls b/cmd/mcpgen/testdata/pagination_did.graphqls new file mode 100644 index 0000000..ef75eea --- /dev/null +++ b/cmd/mcpgen/testdata/pagination_did.graphqls @@ -0,0 +1,59 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + """ + List minted vehicles. + For now, these are always ordered by token ID in descending order. + """ + vehicles( + """ + The number of vehicles to retrieve. + Mutually exclusive with `last`. + """ + first: Int + "A cursor for pagination. Retrieve vehicles after this cursor." + after: String + """ + The number of vehicles to retrieve from the end of the list. + Mutually exclusive with `first`. + """ + last: Int + "A cursor for pagination. Retrieve vehicles before this cursor." + before: String + "Filter the vehicles based on specific criteria." + filterBy: VehiclesFilter + ): VehicleConnection! + @mcpTool(name: "vehicles", description: "List vehicles", selection: "nodes { tokenId owner }") +} + +type Vehicle { + tokenId: Int! + "The DID for this vehicle's token ID in the format did:erc721:::" + tokenDID: String! + "The Ethereum address of the owner of this vehicle." + owner: Address! +} + +"0x-prefixed checksummed hex address" +scalar Address + +input VehiclesFilter { + owner: Address +} + +type VehicleEdge { + node: Vehicle! + cursor: String! +} + +type VehicleConnection { + totalCount: Int! + edges: [VehicleEdge!]! + nodes: [Vehicle!]! + pageInfo: PageInfo! +} + +type PageInfo { + hasNextPage: Boolean! + endCursor: String +} diff --git a/cmd/mcpgen/testdata/scalar_descriptions.graphqls b/cmd/mcpgen/testdata/scalar_descriptions.graphqls new file mode 100644 index 0000000..03c312c --- /dev/null +++ b/cmd/mcpgen/testdata/scalar_descriptions.graphqls @@ -0,0 +1,21 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +"0x-prefixed checksummed hex address" +scalar Address + +"RFC-3339 date-time string" +scalar Time + +"lowercase hex with 0x prefix" +scalar Bytes + +type Query { + account(addr: Address!): Account + @mcpTool(name: "account", description: "Get account", selection: "addr balance") +} + +type Account { + addr: Address! + balance: Int! + createdAt: Time! +} diff --git a/cmd/mcpgen/testdata/self_evident.graphqls b/cmd/mcpgen/testdata/self_evident.graphqls new file mode 100644 index 0000000..18daa5d --- /dev/null +++ b/cmd/mcpgen/testdata/self_evident.graphqls @@ -0,0 +1,17 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION + +type Query { + attestation(id: String!): Attestation + @mcpTool(name: "attestation", description: "Get attestation", selection: "id type dataVersion") +} + +type Attestation { + "id" + id: String! + "type" + type: String! + "The dataVersion of the Attestation." + dataVersion: String! + "A meaningful description that should be kept." + payload: String! +} diff --git a/cmd/mcpgen/testdata/signals.graphqls b/cmd/mcpgen/testdata/signals.graphqls new file mode 100644 index 0000000..f20016c --- /dev/null +++ b/cmd/mcpgen/testdata/signals.graphqls @@ -0,0 +1,98 @@ +directive @mcpTool(name: String!, description: String!, selection: String!, readOnly: Boolean = true) on FIELD_DEFINITION +directive @isSignal on FIELD_DEFINITION +directive @hasAggregation on FIELD_DEFINITION + +type Query { + signalAggregations(tokenId: Int!): SignalAggregations + @mcpTool(name: "signal_aggs", description: "Get signal aggregations", selection: "timestamp speed powertrainCombustionEngineSpeed exteriorAirTemperature lowVoltageBatteryCurrentVoltage obdRunTime chassisAxleRow1WheelLeftTirePressure vin currentLocationLatitude") +} + +type SignalAggregations { + timestamp: String! + + """ + Vehicle speed. + Unit: 'km/h' + Required Privileges: [VEHICLE_NON_LOCATION_DATA] + """ + speed(agg: FloatAggregation!, filter: SignalFloatFilter): Float @isSignal @hasAggregation + + """ + Engine speed. + Unit: 'rpm' + Required Privileges: [VEHICLE_NON_LOCATION_DATA] + """ + powertrainCombustionEngineSpeed(agg: FloatAggregation!, filter: SignalFloatFilter): Float @isSignal @hasAggregation + + """ + Temperature outside the vehicle. + Unit: 'celsius' + Required Privileges: [VEHICLE_NON_LOCATION_DATA] + """ + exteriorAirTemperature(agg: FloatAggregation!, filter: SignalFloatFilter): Float @isSignal @hasAggregation + + """ + Current Voltage of the battery. + Unit: 'V' + Required Privileges: [VEHICLE_NON_LOCATION_DATA] + """ + lowVoltageBatteryCurrentVoltage(agg: FloatAggregation!, filter: SignalFloatFilter): Float @isSignal @hasAggregation + + """ + PID 1F - Engine run time. + Unit: 's' + Required Privileges: [VEHICLE_NON_LOCATION_DATA] + """ + obdRunTime(agg: FloatAggregation!, filter: SignalFloatFilter): Float @isSignal @hasAggregation + + """ + Tire pressure for left wheel on first axle. + Unit: 'kPa' + Required Privileges: [VEHICLE_NON_LOCATION_DATA] + """ + chassisAxleRow1WheelLeftTirePressure(agg: FloatAggregation!, filter: SignalFloatFilter): Float @isSignal @hasAggregation + + """ + Vehicle Identification Number. + Required Privileges: [VEHICLE_NON_LOCATION_DATA] + """ + vin(agg: StringAggregation!, filter: SignalStringFilter): String @isSignal @hasAggregation + + """ + Current latitude. + Unit: 'degrees' + Required Privileges: [VEHICLE_ALL_TIME_LOCATION] + """ + currentLocationLatitude(agg: FloatAggregation!, filter: SignalFloatFilter): Float @isSignal @hasAggregation + + """ + Approximate current latitude. + Unit: 'degrees' + Required Privileges: [VEHICLE_APPROXIMATE_LOCATION] + """ + currentLocationApproximateLatitude(agg: FloatAggregation!, filter: SignalFloatFilter): Float @isSignal @hasAggregation +} + +enum FloatAggregation { + FIRST + LAST + MIN + MAX + AVG +} + +enum StringAggregation { + FIRST + LAST + UNIQUE +} + +input SignalFloatFilter { + from: String! + to: String! +} + +input SignalStringFilter { + from: String! + to: String! +} diff --git a/go.mod b/go.mod index 265abb9..7d82fb4 100644 --- a/go.mod +++ b/go.mod @@ -1,54 +1,61 @@ module github.com/DIMO-Network/server-garage -go 1.24.0 +go 1.25.0 require ( - github.com/99designs/gqlgen v0.17.78 - github.com/DIMO-Network/cloudevent v0.1.4 - github.com/DIMO-Network/token-exchange-api v0.3.7 - github.com/caarlos0/env/v11 v11.3.1 - github.com/ethereum/go-ethereum v1.16.4 + github.com/99designs/gqlgen v0.17.89 + github.com/DIMO-Network/cloudevent v1.0.4 + github.com/DIMO-Network/token-exchange-api v0.4.0 + github.com/caarlos0/env/v11 v11.4.0 + github.com/ethereum/go-ethereum v1.17.1 + github.com/go-jose/go-jose/v3 v3.0.4 github.com/gofiber/contrib/jwt v1.1.2 - github.com/gofiber/fiber/v2 v2.52.9 + github.com/gofiber/fiber/v2 v2.52.12 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/joho/godotenv v1.5.1 - github.com/prometheus/client_golang v1.23.0 + github.com/modelcontextprotocol/go-sdk v1.4.1 + github.com/prometheus/client_golang v1.23.2 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 - github.com/vektah/gqlparser/v2 v2.5.30 - golang.org/x/sync v0.16.0 + github.com/vektah/gqlparser/v2 v2.5.32 + golang.org/x/sync v0.20.0 ) require ( - github.com/DIMO-Network/shared v1.0.7 // indirect + github.com/DIMO-Network/shared v1.1.5 // indirect github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/holiman/uint256 v1.3.2 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sosodev/duration v1.3.1 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/sosodev/duration v1.4.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.65.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/sys v0.37.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.42.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9595d83..ada17cb 100644 --- a/go.sum +++ b/go.sum @@ -1,63 +1,234 @@ -github.com/99designs/gqlgen v0.17.78 h1:bhIi7ynrc3js2O8wu1sMQj1YHPENDt3jQGyifoBvoVI= -github.com/99designs/gqlgen v0.17.78/go.mod h1:yI/o31IauG2kX0IsskM4R894OCCG1jXJORhtLQqB7Oc= -github.com/DIMO-Network/cloudevent v0.1.4 h1:c6Sq4CyHt05V8OtnEXekUCRGfVuR1pFkJevfiKt1sYM= -github.com/DIMO-Network/cloudevent v0.1.4/go.mod h1:Q2QpMEDYJ+VX0lz9SK2EUFxkuddV1XeF4aQ8LfegB68= -github.com/DIMO-Network/shared v1.0.7 h1:LfSgsqJ6R7EUyfo2GTfuhrCpoDcweJqe7eVOa4j7Xbo= -github.com/DIMO-Network/shared v1.0.7/go.mod h1:lDHUKwwT2LW6Zvd42Nb33dXklRNTmfyOlbUNx2dQfGY= -github.com/DIMO-Network/token-exchange-api v0.3.7 h1:i5Ygs9DuPSwE8BC90Q6gnKnfgqw19GseF2x0vZ9sCG8= -github.com/DIMO-Network/token-exchange-api v0.3.7/go.mod h1:gKoB1Zi3EXJqIyfLnTn1GV1NSzTMBDkRleChBU/EQv8= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8= +github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgxoBDMqMO/Nvy7bZ9a0nbU3I1DtFQK3YvB4= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= +github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc= +github.com/DIMO-Network/clickhouse-infra v0.0.7/go.mod h1:XS80lhSJNWBWGgZ+m4j7++zFj1wAXfmtV2gJfhGlabQ= +github.com/DIMO-Network/cloudevent v1.0.4 h1:bZ7tRPwCbiRgAlG1gfN7qpOOUI1OEXMMoMMyS9r4omk= +github.com/DIMO-Network/cloudevent v1.0.4/go.mod h1:I/9NcpMozV5Fw194WimhbkAsJtKVZf5UKYJ9hgc8Cdg= +github.com/DIMO-Network/shared v1.1.5 h1:cRI3BbeYOgolMkeBOSqbDVGxymnDfeP/q7xgIXJ5MkY= +github.com/DIMO-Network/shared v1.1.5/go.mod h1:lDHUKwwT2LW6Zvd42Nb33dXklRNTmfyOlbUNx2dQfGY= +github.com/DIMO-Network/token-exchange-api v0.4.0 h1:EayDrw9VdyAfc6rbpdnDxFhlN3lMhbonUJoouKZu35g= +github.com/DIMO-Network/token-exchange-api v0.4.0/go.mod h1:cldgAyDGLMNk3YIf2mr6vGohcO0ANlZ88fCbI+v/wEY= +github.com/DIMO-Network/yaml v0.1.0/go.mod h1:KkiehcbkVzH8Pf8f9dja8B2aW81gYYZSqfwzSj9yN68= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/avast/retry-go/v4 v4.5.1/go.mod h1:/sipNsvNB3RRuT5iNcb6h73nw3IBmXJ/H3XrCQYSOpc= +github.com/aws/aws-sdk-go-v2 v1.25.0/go.mod h1:G104G1Aho5WqF+SR3mDIobTABQzpYV0WxMsKxlMggOA= +github.com/aws/aws-sdk-go-v2/config v1.18.45/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0/go.mod h1:D+duLy2ylgatV+yTlQ8JTuLfDD0BnFvnQRc+o6tbZ4M= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0/go.mod h1:hL6BWM/d/qz113fVitZjbXR0E+RCTU1+x+1Idyn5NgE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= +github.com/aws/aws-sdk-go-v2/service/kms v1.28.1/go.mod h1:Y/mkxhbaWCswchbBBLRwet6uYKl/026DZXS87c0DmuU= +github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2/go.mod h1:TQZBt/WaQy+zTHoW++rnl8JBrmZ0VO6EUbVua1+foCA= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/smithy-go v1.20.0/go.mod h1:uo5RKksAl4PzhqaAbjd4rLgFoq5koTsQKYuGe7dklGc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= -github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc= +github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/cloudflare-go v0.114.0/go.mod h1:O7fYfFfA6wKqKFn2QIR9lhj7FDw6VQCGOY6hd2TBtd0= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ethereum/go-ethereum v1.16.4 h1:H6dU0r2p/amA7cYg6zyG9Nt2JrKKH6oX2utfcqrSpkQ= -github.com/ethereum/go-ethereum v1.16.4/go.mod h1:P7551slMFbjn2zOQaKrJShZVN/d8bGxp4/I6yZVlb5w= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw= +github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= +github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ericlagergren/decimal v0.0.0-20190420051523-6335edbaa640/go.mod h1:mdYyfAkzn9kyJ/kMk/7WE9ufl9lflh+2NvecQ5mAghs= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho= +github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fjl/gencodec v0.1.0/go.mod h1:Um1dFHPONZGTHog1qD1NaWjXJW/SPB38wPv0O8uZ2fI= +github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/contrib/jwt v1.1.2 h1:GmWnOqT4A15EkA8IPXwSpvNUXZR4u5SMj+geBmyLAjs= github.com/gofiber/contrib/jwt v1.1.2/go.mod h1:CpIwrkUQ3Q6IP8y9n3f0wP9bOnSKx39EDp2fBVgMFVk= -github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= -github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= +github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= +github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matryer/moq v0.6.0/go.mod h1:iEVhY/XBwFG/nbRyEf0oV+SqnTHZJ5wectzx7yT+y98= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -65,77 +236,180 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE= +github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= +github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs= +github.com/parquet-go/jsonlite v1.4.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0= +github.com/parquet-go/parquet-go v0.28.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg= +github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/protolambda/bls12-381-util v0.1.0/go.mod h1:cdkysJTRpeFeuUVx/TXGDQNMTiRAalk1vQw3TYTHcE4= +github.com/protolambda/zrnt v0.34.1/go.mod h1:A0fezkp9Tt3GBLATSPIbuY4ywYESyAuc/FFmPKg8Lqs= +github.com/protolambda/ztyp v0.2.2/go.mod h1:9bYgKGqg3wJqT9ac1gI2hnVb0STQq7p/1lapqrqY1dU= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= -github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= +github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/clickhouse v0.38.0/go.mod h1:4YCEhJkDA1L1GF8ndOf2RVXtdxY1Po30nmtwvDOb+8Q= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU= +github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= -github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= -github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= -github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= +github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/vertica/vertica-sql-go v1.3.5/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4= +github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA= +github.com/volatiletech/null/v8 v8.1.2/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g= +github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY= +github.com/volatiletech/sqlboiler/v4 v4.16.2/go.mod h1:B14BPBGTrJ2X6l7lwnvV/iXgYR48+ozGSlzHI3frl6U= +github.com/volatiletech/strmangle v0.0.6/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= +github.com/ydb-platform/ydb-go-sdk/v3 v3.127.0/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -147,29 +421,47 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/mcpserver/condensed_schema_test.go b/pkg/mcpserver/condensed_schema_test.go new file mode 100644 index 0000000..9642595 --- /dev/null +++ b/pkg/mcpserver/condensed_schema_test.go @@ -0,0 +1,98 @@ +package mcpserver + +import ( + "context" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWithCondensedSchema(t *testing.T) { + condensedSDL := "type Query {\n vehicle(tokenId: Int!): Vehicle\n}\n\ntype Vehicle {\n tokenId: Int!\n owner: String!\n}\n" + + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test Server", "0.1.0", "test", WithCondensedSchema(condensedSDL)) + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + transport := &mcp.StreamableClientTransport{ + Endpoint: ts.URL, + HTTPClient: ts.Client(), + } + + client := mcp.NewClient(&mcp.Implementation{ + Name: "test-client", + Version: "1.0", + }, nil) + + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + schemaResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_get_schema", + }) + require.NoError(t, err) + require.NotNil(t, schemaResult) + require.Len(t, schemaResult.Content, 1) + + contentJSON, err := json.Marshal(schemaResult.Content[0]) + require.NoError(t, err) + var textContent struct { + Text string `json:"text"` + } + require.NoError(t, json.Unmarshal(contentJSON, &textContent)) + + assert.Contains(t, textContent.Text, "type Query") + assert.Contains(t, textContent.Text, "type Vehicle") + assert.NotContains(t, textContent.Text, "__schema", "condensed schema should not contain introspection data") +} + +func TestWithoutCondensedSchemaFallback(t *testing.T) { + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test Server", "0.1.0", "test") + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + transport := &mcp.StreamableClientTransport{ + Endpoint: ts.URL, + HTTPClient: ts.Client(), + } + + client := mcp.NewClient(&mcp.Implementation{ + Name: "test-client", + Version: "1.0", + }, nil) + + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + schemaResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_get_schema", + }) + require.NoError(t, err) + require.NotNil(t, schemaResult) + require.Len(t, schemaResult.Content, 1) + + contentJSON, err := json.Marshal(schemaResult.Content[0]) + require.NoError(t, err) + var textContent struct { + Text string `json:"text"` + } + require.NoError(t, json.Unmarshal(contentJSON, &textContent)) + + assert.Contains(t, textContent.Text, "__schema", "without condensed schema, should return introspection JSON") +} diff --git a/pkg/mcpserver/executor.go b/pkg/mcpserver/executor.go new file mode 100644 index 0000000..99a2f7c --- /dev/null +++ b/pkg/mcpserver/executor.go @@ -0,0 +1,106 @@ +package mcpserver + +import ( + "context" + "fmt" +) + +// loadSchema runs the introspection query and returns the schema JSON. +func loadSchema(ctx context.Context, exec GraphQLExecutor) (string, error) { + result, err := exec.Execute(ctx, introspectionQuery, nil) + if err != nil { + return "", fmt.Errorf("introspection query: %w", err) + } + return string(result), nil +} + +// introspectionQuery is the full introspection query used to fetch the schema. +const introspectionQuery = ` +query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } +} +` diff --git a/pkg/mcpserver/gqlgen.go b/pkg/mcpserver/gqlgen.go new file mode 100644 index 0000000..c3e039c --- /dev/null +++ b/pkg/mcpserver/gqlgen.go @@ -0,0 +1,53 @@ +package mcpserver + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/99designs/gqlgen/graphql" + "github.com/99designs/gqlgen/graphql/executor" +) + +// gqlgenExecutor implements GraphQLExecutor using a gqlgen ExecutableSchema. +type gqlgenExecutor struct { + exec *executor.Executor +} + +// NewGQLGenExecutor returns a GraphQLExecutor backed by a gqlgen ExecutableSchema. +func NewGQLGenExecutor(es graphql.ExecutableSchema) GraphQLExecutor { + return &gqlgenExecutor{ + exec: executor.New(es), + } +} + +// Execute runs a GraphQL query against the gqlgen schema. +func (g *gqlgenExecutor) Execute(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + params := &graphql.RawParams{ + Query: query, + Variables: variables, + } + + ctx = graphql.StartOperationTrace(ctx) + opCtx, errs := g.exec.CreateOperationContext(ctx, params) + if errs != nil { + msgs := make([]string, len(errs)) + for i, e := range errs { + msgs[i] = e.Message + } + return nil, fmt.Errorf("graphql operation error: %s", strings.Join(msgs, "; ")) + } + + ctx = graphql.WithOperationContext(ctx, opCtx) + handler, ctx := g.exec.DispatchOperation(ctx, opCtx) + if handler == nil { + return nil, fmt.Errorf("graphql operation aborted by middleware") + } + resp := handler(ctx) + if resp == nil { + return nil, fmt.Errorf("graphql operation returned nil response") + } + + return json.Marshal(resp) +} diff --git a/pkg/mcpserver/gqlgen_test.go b/pkg/mcpserver/gqlgen_test.go new file mode 100644 index 0000000..dd8625f --- /dev/null +++ b/pkg/mcpserver/gqlgen_test.go @@ -0,0 +1,78 @@ +package mcpserver + +import ( + "context" + "testing" + + "github.com/99designs/gqlgen/graphql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" +) + +func testSchema() *ast.Schema { + return gqlparser.MustLoadSchema( + &ast.Source{ + Name: "test.graphqls", + Input: `type Query { hello: String! }`, + }, + ) +} + +func TestGQLGenExecutorSuccess(t *testing.T) { + es := &graphql.ExecutableSchemaMock{ + SchemaFunc: testSchema, + ComplexityFunc: func(ctx context.Context, typeName, fieldName string, childComplexity int, args map[string]any) (int, bool) { + return 0, false + }, + ExecFunc: func(ctx context.Context) graphql.ResponseHandler { + return func(ctx context.Context) *graphql.Response { + return &graphql.Response{ + Data: []byte(`{"hello":"world"}`), + } + } + }, + } + + exec := NewGQLGenExecutor(es) + result, err := exec.Execute(context.Background(), `{ hello }`, nil) + require.NoError(t, err) + assert.Contains(t, string(result), `"hello"`) +} + +func TestGQLGenExecutorInvalidQuery(t *testing.T) { + es := &graphql.ExecutableSchemaMock{ + SchemaFunc: testSchema, + ComplexityFunc: func(ctx context.Context, typeName, fieldName string, childComplexity int, args map[string]any) (int, bool) { + return 0, false + }, + ExecFunc: func(ctx context.Context) graphql.ResponseHandler { + return nil + }, + } + + exec := NewGQLGenExecutor(es) + _, err := exec.Execute(context.Background(), `{ nonExistent }`, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "graphql operation error") +} + +func TestGQLGenExecutorNilResponse(t *testing.T) { + es := &graphql.ExecutableSchemaMock{ + SchemaFunc: testSchema, + ComplexityFunc: func(ctx context.Context, typeName, fieldName string, childComplexity int, args map[string]any) (int, bool) { + return 0, false + }, + ExecFunc: func(ctx context.Context) graphql.ResponseHandler { + return func(ctx context.Context) *graphql.Response { + return nil + } + }, + } + + exec := NewGQLGenExecutor(es) + _, err := exec.Execute(context.Background(), `{ hello }`, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "nil response") +} diff --git a/pkg/mcpserver/mcpserver.go b/pkg/mcpserver/mcpserver.go new file mode 100644 index 0000000..b7e38d8 --- /dev/null +++ b/pkg/mcpserver/mcpserver.go @@ -0,0 +1,163 @@ +package mcpserver + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/http" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// GraphQLExecutor executes GraphQL operations and returns the JSON response. +type GraphQLExecutor interface { + Execute(ctx context.Context, query string, variables map[string]any) ([]byte, error) +} + +// ArgDefinition describes a single argument for a tool. +type ArgDefinition struct { + Name string + Type string // "string", "integer", "number", "boolean", "object", "array" + Description string + Required bool + ItemsType string // JSON Schema type for array elements + EnumValues []string // Allowed values for enum types +} + +// ToolDefinition describes an MCP tool backed by a GraphQL query. +type ToolDefinition struct { + Name string + Description string + Args []ArgDefinition + Query string + Annotations *mcp.ToolAnnotations +} + +// Option configures the MCP server. +type Option func(*config) + +// TokenVerifier validates a bearer token and returns an enriched context. +type TokenVerifier func(ctx context.Context, token string) (context.Context, error) + +// config holds internal configuration for the MCP server. +type config struct { + tools []ToolDefinition + condensedSchema string + stateless bool // default true + maxQuerySize int // default 65536 + tokenVerifier TokenVerifier + logger *slog.Logger +} + +// WithTools returns an Option that registers additional tool definitions. +func WithTools(tools []ToolDefinition) Option { + return func(c *config) { + c.tools = append(c.tools, tools...) + } +} + +// WithCondensedSchema returns an Option that provides a condensed SDL schema +// for the get_schema tool to return instead of the full introspection JSON. +func WithCondensedSchema(schema string) Option { + return func(c *config) { + c.condensedSchema = schema + } +} + +// WithStateless returns an Option that controls whether the MCP server runs in stateless mode. +func WithStateless(stateless bool) Option { + return func(c *config) { c.stateless = stateless } +} + +// WithMaxQuerySize returns an Option that sets the maximum allowed query size in bytes. +func WithMaxQuerySize(bytes int) Option { + return func(c *config) { c.maxQuerySize = bytes } +} + +// WithTokenVerifier returns an Option that adds bearer token authentication middleware. +// Use this for simple deployments where the MCP server manages its own auth. For services +// that already have HTTP middleware (e.g., JWT validation chains), wrap the handler +// externally instead — this avoids duplicating auth logic and lets you reuse existing +// middleware stacks. +func WithTokenVerifier(v TokenVerifier) Option { + return func(c *config) { c.tokenVerifier = v } +} + +// WithLogger returns an Option that sets an slog.Logger for MCP-level logging. +func WithLogger(logger *slog.Logger) Option { + return func(c *config) { c.logger = logger } +} + +// New creates an http.Handler that serves an MCP Streamable HTTP server. +// It wraps a GraphQL executor and exposes registered tools as MCP tools. +func New(ctx context.Context, exec GraphQLExecutor, serverName string, version string, toolPrefix string, opts ...Option) (http.Handler, error) { + if exec == nil { + return nil, errors.New("mcpserver: executor must not be nil") + } + if serverName == "" { + return nil, errors.New("mcpserver: serverName must be non-empty") + } + if toolPrefix == "" { + return nil, errors.New("mcpserver: toolPrefix must be non-empty") + } + + cfg := &config{ + stateless: true, + maxQuerySize: 65536, + } + for _, opt := range opts { + opt(cfg) + } + + server := mcp.NewServer(&mcp.Implementation{ + Name: serverName, + Version: version, + }, &mcp.ServerOptions{ + Logger: cfg.logger, + }) + + // Load the schema eagerly so initialization fails fast if the executor is broken. + schema, err := loadSchema(ctx, exec) + if err != nil { + return nil, fmt.Errorf("mcpserver: schema introspection failed: %w", err) + } + + schemaContent := schema + if cfg.condensedSchema != "" { + schemaContent = cfg.condensedSchema + } + registerBuiltinTools(server, exec, schemaContent, toolPrefix, cfg.maxQuerySize, cfg.logger) + registerShortcutTools(server, exec, cfg.tools, cfg.logger) + + handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { + return server + }, &mcp.StreamableHTTPOptions{ + Stateless: cfg.stateless, + }) + + var h http.Handler = handler + if cfg.tokenVerifier != nil { + h = tokenVerifierMiddleware(cfg.tokenVerifier, h) + } + + return h, nil +} + +func tokenVerifierMiddleware(verify TokenVerifier, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if !strings.HasPrefix(auth, "Bearer ") { + http.Error(w, "missing or invalid Authorization header", http.StatusUnauthorized) + return + } + token := strings.TrimPrefix(auth, "Bearer ") + ctx, err := verify(r.Context(), token) + if err != nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/pkg/mcpserver/mcpserver_test.go b/pkg/mcpserver/mcpserver_test.go new file mode 100644 index 0000000..572227d --- /dev/null +++ b/pkg/mcpserver/mcpserver_test.go @@ -0,0 +1,796 @@ +package mcpserver + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockExecutor implements GraphQLExecutor for testing. +type mockExecutor struct { + fn func(ctx context.Context, query string, variables map[string]any) ([]byte, error) +} + +func (m *mockExecutor) Execute(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + return m.fn(ctx, query, variables) +} + +func TestExecutorQuery(t *testing.T) { + expected := `{"data":{"vehicle":{"id":"123"}}}` + exec := &mockExecutor{ + fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + return []byte(expected), nil + }, + } + + result, err := exec.Execute(context.Background(), `{ vehicle { id } }`, nil) + require.NoError(t, err) + assert.JSONEq(t, expected, string(result)) +} + +type ctxKey string + +func TestExecutorContextPropagation(t *testing.T) { + key := ctxKey("user_id") + exec := &mockExecutor{ + fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + val := ctx.Value(key) + if val == nil { + return nil, fmt.Errorf("context value not propagated") + } + if val != "user-42" { + return nil, fmt.Errorf("unexpected context value: %v", val) + } + return []byte(`{"data":{}}`), nil + }, + } + + ctx := context.WithValue(context.Background(), key, "user-42") + _, err := exec.Execute(ctx, `{ me { id } }`, nil) + require.NoError(t, err) +} + +func TestExecutorError(t *testing.T) { + exec := &mockExecutor{ + fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + return nil, fmt.Errorf("execution failed") + }, + } + + _, err := exec.Execute(context.Background(), `{ fail }`, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "execution failed") +} + +func TestLoadSchema(t *testing.T) { + exec := &mockExecutor{ + fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + if strings.Contains(query, "__schema") { + return []byte(`{"data":{"__schema":{"types":[{"name":"Query"}]}}}`), nil + } + return nil, fmt.Errorf("unexpected query") + }, + } + + schema, err := loadSchema(context.Background(), exec) + require.NoError(t, err) + assert.Contains(t, schema, "__schema") +} + +func TestLoadSchemaError(t *testing.T) { + exec := &mockExecutor{ + fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + return nil, fmt.Errorf("connection refused") + }, + } + + _, err := loadSchema(context.Background(), exec) + require.Error(t, err) + assert.Contains(t, err.Error(), "introspection query") +} + +// mockGQLExecutor returns a GraphQLExecutor that handles introspection and echoes regular queries. +func mockGQLExecutor() GraphQLExecutor { + return &mockExecutor{ + fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + if strings.Contains(query, "__schema") { + return []byte(`{"data":{"__schema":{"queryType":{"name":"Query"},"types":[{"name":"Query"},{"name":"Vehicle"}]}}}`), nil + } + resp := map[string]any{ + "data": map[string]any{ + "echoQuery": query, + "echoVariables": variables, + }, + } + return json.Marshal(resp) + }, + } +} + +func TestShortcutTool(t *testing.T) { + toolDef := ToolDefinition{ + Name: "get_vehicle", + Description: "Fetches a vehicle by token ID.", + Args: []ArgDefinition{ + {Name: "tokenId", Type: "integer", Description: "The vehicle token ID", Required: true}, + }, + Query: `query GetVehicle($tokenId: Int!) { vehicle(tokenId: $tokenId) { id make model } }`, + } + + mcpServer := mcp.NewServer(&mcp.Implementation{ + Name: "test-server", + Version: "0.1.0", + }, nil) + + exec := mockGQLExecutor() + registerShortcutTools(mcpServer, exec, []ToolDefinition{toolDef}, nil) + + serverTransport, clientTransport := mcp.NewInMemoryTransports() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + _ = mcpServer.Run(ctx, serverTransport) + }() + + client := mcp.NewClient(&mcp.Implementation{ + Name: "test-client", + Version: "0.1.0", + }, nil) + + session, err := client.Connect(ctx, clientTransport, nil) + require.NoError(t, err) + + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "get_vehicle", + Arguments: map[string]any{ + "tokenId": 42, + }, + }) + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result.Content, 1) + + contentJSON, err := json.Marshal(result.Content[0]) + require.NoError(t, err) + var textContent struct { + Type string `json:"type"` + Text string `json:"text"` + } + require.NoError(t, json.Unmarshal(contentJSON, &textContent)) + assert.Equal(t, "text", textContent.Type) + + var resp map[string]any + require.NoError(t, json.Unmarshal([]byte(textContent.Text), &resp)) + data := resp["data"].(map[string]any) + assert.Equal(t, toolDef.Query, data["echoQuery"]) + + vars := data["echoVariables"].(map[string]any) + assert.Equal(t, float64(42), vars["tokenId"]) +} + +func TestMCPHandlerEndToEnd(t *testing.T) { + shortcutTool := ToolDefinition{ + Name: "get_vehicle", + Description: "Fetches a vehicle by token ID.", + Args: []ArgDefinition{ + {Name: "tokenId", Type: "integer", Description: "The vehicle token ID", Required: true}, + }, + Query: `query GetVehicle($tokenId: Int!) { vehicle(tokenId: $tokenId) { id } }`, + } + + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test Server", "0.1.0", "test", WithTools([]ToolDefinition{shortcutTool})) + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + transport := &mcp.StreamableClientTransport{ + Endpoint: ts.URL, + HTTPClient: ts.Client(), + } + + client := mcp.NewClient(&mcp.Implementation{ + Name: "test-client", + Version: "1.0", + }, nil) + + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + result, err := session.ListTools(ctx, &mcp.ListToolsParams{}) + require.NoError(t, err) + require.NotNil(t, result) + + toolNames := make(map[string]bool) + for _, tool := range result.Tools { + toolNames[tool.Name] = true + } + + assert.True(t, toolNames["test_get_schema"], "expected test_get_schema tool") + assert.True(t, toolNames["test_query"], "expected test_query tool") + assert.True(t, toolNames["get_vehicle"], "expected get_vehicle shortcut tool") + + schemaResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_get_schema", + }) + require.NoError(t, err) + require.NotNil(t, schemaResult) + require.Len(t, schemaResult.Content, 1) + + contentJSON, err := json.Marshal(schemaResult.Content[0]) + require.NoError(t, err) + var textContent struct { + Type string `json:"type"` + Text string `json:"text"` + } + require.NoError(t, json.Unmarshal(contentJSON, &textContent)) + assert.Equal(t, "text", textContent.Type) + assert.Contains(t, textContent.Text, "__schema") + + queryResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_query", + Arguments: map[string]any{ + "query": "{ vehicles { id } }", + }, + }) + require.NoError(t, err) + require.NotNil(t, queryResult) + require.Len(t, queryResult.Content, 1) +} + +func TestToolPrefixing(t *testing.T) { + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Prefixed Server", "0.1.0", "myprefix") + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + transport := &mcp.StreamableClientTransport{ + Endpoint: ts.URL, + HTTPClient: ts.Client(), + } + + client := mcp.NewClient(&mcp.Implementation{ + Name: "test-client", + Version: "1.0", + }, nil) + + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + result, err := session.ListTools(ctx, &mcp.ListToolsParams{}) + require.NoError(t, err) + require.NotNil(t, result) + + toolNames := make(map[string]bool) + for _, tool := range result.Tools { + toolNames[tool.Name] = true + } + + assert.True(t, toolNames["myprefix_get_schema"], "expected myprefix_get_schema tool") + assert.True(t, toolNames["myprefix_query"], "expected myprefix_query tool") + assert.False(t, toolNames["test_get_schema"], "should not have test prefix") + assert.False(t, toolNames["test_query"], "should not have test prefix") +} + +func TestEmptyPrefixRejected(t *testing.T) { + _, err := New(context.Background(), mockGQLExecutor(), "Test Server", "0.1.0", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "toolPrefix must be non-empty") +} + +func TestEmptyServerNameRejected(t *testing.T) { + _, err := New(context.Background(), mockGQLExecutor(), "", "0.1.0", "test") + require.Error(t, err) + assert.Contains(t, err.Error(), "serverName must be non-empty") +} + +func TestNilExecutorRejected(t *testing.T) { + _, err := New(context.Background(), nil, "Test Server", "0.1.0", "test") + require.Error(t, err) + assert.Contains(t, err.Error(), "executor must not be nil") +} + +func TestQueryToolErrorReturnsIsError(t *testing.T) { + exec := &mockExecutor{ + fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + if strings.Contains(query, "__schema") { + return []byte(`{"data":{"__schema":{"queryType":{"name":"Query"}}}}`), nil + } + return nil, fmt.Errorf("field not found: badField") + }, + } + + mcpServer := mcp.NewServer(&mcp.Implementation{ + Name: "test-server", + Version: "0.1.0", + }, nil) + + registerBuiltinTools(mcpServer, exec, `{"data":{}}`, "test", 65536, nil) + + serverTransport, clientTransport := mcp.NewInMemoryTransports() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + _ = mcpServer.Run(ctx, serverTransport) + }() + + client := mcp.NewClient(&mcp.Implementation{ + Name: "test-client", + Version: "0.1.0", + }, nil) + + session, err := client.Connect(ctx, clientTransport, nil) + require.NoError(t, err) + + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_query", + Arguments: map[string]any{ + "query": "{ badField }", + }, + }) + // Should NOT be a protocol error — the tool handled it gracefully. + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError, "expected IsError to be true for execution failure") + require.Len(t, result.Content, 1) + + contentJSON, err := json.Marshal(result.Content[0]) + require.NoError(t, err) + var textContent struct { + Text string `json:"text"` + } + require.NoError(t, json.Unmarshal(contentJSON, &textContent)) + assert.Contains(t, textContent.Text, "field not found") +} + +func ptr[T any](v T) *T { return &v } + +func TestBuiltinToolAnnotations(t *testing.T) { + exec := mockGQLExecutor() + mcpHandler, err := New(context.Background(), exec, "Test Server", "0.1.0", "test") + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx := context.Background() + transport := &mcp.StreamableClientTransport{ + Endpoint: ts.URL, + HTTPClient: ts.Client(), + } + + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0"}, nil) + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + toolsResult, err := session.ListTools(ctx, nil) + require.NoError(t, err) + + toolMap := make(map[string]*mcp.Tool) + for _, t := range toolsResult.Tools { + toolMap[t.Name] = t + } + + schema := toolMap["test_get_schema"] + require.NotNil(t, schema) + require.NotNil(t, schema.Annotations, "get_schema should have annotations") + assert.True(t, schema.Annotations.ReadOnlyHint) + assert.Equal(t, ptr(false), schema.Annotations.DestructiveHint) + assert.Equal(t, ptr(false), schema.Annotations.OpenWorldHint) + assert.True(t, schema.Annotations.IdempotentHint) + + query := toolMap["test_query"] + require.NotNil(t, query) + require.NotNil(t, query.Annotations, "query should have annotations") + assert.False(t, query.Annotations.ReadOnlyHint) + assert.Equal(t, ptr(false), query.Annotations.OpenWorldHint) +} + +func TestShortcutToolAnnotations(t *testing.T) { + tool := ToolDefinition{ + Name: "test_get_vehicle", + Description: "Get a vehicle", + Args: []ArgDefinition{{Name: "id", Type: "integer", Required: true}}, + Query: `query($id: Int!) { vehicle(id: $id) { id } }`, + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: true, + DestructiveHint: boolPtr(false), + OpenWorldHint: boolPtr(false), + IdempotentHint: true, + }, + } + + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", WithTools([]ToolDefinition{tool})) + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx := context.Background() + transport := &mcp.StreamableClientTransport{Endpoint: ts.URL, HTTPClient: ts.Client()} + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0"}, nil) + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + toolsResult, err := session.ListTools(ctx, nil) + require.NoError(t, err) + + var vehicleTool *mcp.Tool + for _, t := range toolsResult.Tools { + if t.Name == "test_get_vehicle" { + vehicleTool = t + break + } + } + require.NotNil(t, vehicleTool) + require.NotNil(t, vehicleTool.Annotations) + assert.True(t, vehicleTool.Annotations.ReadOnlyHint) + assert.Equal(t, boolPtr(false), vehicleTool.Annotations.DestructiveHint) +} + +func TestBuildInputSchemaArrayType(t *testing.T) { + args := []ArgDefinition{ + {Name: "ids", Type: "array", ItemsType: "integer", Description: "List of IDs", Required: true}, + {Name: "name", Type: "string", Description: "A name", Required: false}, + } + schema := buildInputSchema(args) + + assert.Equal(t, false, schema["additionalProperties"], "schema should disallow additional properties") + + properties := schema["properties"].(map[string]any) + + idsProp := properties["ids"].(map[string]any) + assert.Equal(t, "array", idsProp["type"]) + items := idsProp["items"].(map[string]any) + assert.Equal(t, "integer", items["type"]) + + nameProp := properties["name"].(map[string]any) + _, hasItems := nameProp["items"] + assert.False(t, hasItems, "non-array type should not have items") +} + +func TestQuerySizeLimitRejected(t *testing.T) { + exec := mockGQLExecutor() + mcpServer := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil) + registerBuiltinTools(mcpServer, exec, `{"data":{}}`, "test", 50, nil) + + serverTransport, clientTransport := mcp.NewInMemoryTransports() + ctx := context.Background() + go func() { _ = mcpServer.Run(ctx, serverTransport) }() + + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "0.1.0"}, nil) + session, err := client.Connect(ctx, clientTransport, nil) + require.NoError(t, err) + + bigQuery := strings.Repeat("x", 51) + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_query", + Arguments: map[string]any{"query": bigQuery}, + }) + require.NoError(t, err) + assert.True(t, result.IsError) + + contentJSON, _ := json.Marshal(result.Content[0]) + var tc struct{ Text string } + json.Unmarshal(contentJSON, &tc) + assert.Contains(t, tc.Text, "exceeds maximum size") +} + +func TestBuildInputSchemaEnumValues(t *testing.T) { + args := []ArgDefinition{ + { + Name: "status", + Type: "string", + Description: "Filter by status", + Required: true, + EnumValues: []string{"ACTIVE", "INACTIVE"}, + }, + { + Name: "name", + Type: "string", + Description: "A name", + Required: false, + }, + } + schema := buildInputSchema(args) + properties := schema["properties"].(map[string]any) + + statusProp := properties["status"].(map[string]any) + assert.Equal(t, "string", statusProp["type"]) + assert.Equal(t, []string{"ACTIVE", "INACTIVE"}, statusProp["enum"]) + + nameProp := properties["name"].(map[string]any) + _, hasEnum := nameProp["enum"] + assert.False(t, hasEnum, "non-enum type should not have enum field") +} + +type authCtxKey struct{} + +// headerRoundTripper wraps an http.RoundTripper and adds custom headers. +type headerRoundTripper struct { + base http.RoundTripper + headers http.Header +} + +func (h *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + for k, v := range h.headers { + req.Header[k] = v + } + return h.base.RoundTrip(req) +} + +func TestTokenVerifierAccepts(t *testing.T) { + verifier := func(ctx context.Context, token string) (context.Context, error) { + if token == "valid-token" { + return context.WithValue(ctx, authCtxKey{}, "authenticated"), nil + } + return ctx, fmt.Errorf("invalid token") + } + + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", + WithTokenVerifier(verifier), + ) + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx := context.Background() + httpClient := ts.Client() + httpClient.Transport = &headerRoundTripper{ + base: httpClient.Transport, + headers: http.Header{"Authorization": []string{"Bearer valid-token"}}, + } + transport := &mcp.StreamableClientTransport{ + Endpoint: ts.URL, + HTTPClient: httpClient, + } + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0"}, nil) + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + tools, err := session.ListTools(ctx, nil) + require.NoError(t, err) + assert.NotEmpty(t, tools.Tools) +} + +func TestTokenVerifierRejects(t *testing.T) { + verifier := func(ctx context.Context, token string) (context.Context, error) { + return ctx, fmt.Errorf("invalid") + } + + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", + WithTokenVerifier(verifier), + ) + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + resp, err := http.Post(ts.URL, "application/json", strings.NewReader(`{}`)) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) +} + +func TestTokenVerifierMissingHeader(t *testing.T) { + verifier := func(ctx context.Context, token string) (context.Context, error) { + return ctx, nil + } + + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", + WithTokenVerifier(verifier), + ) + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + resp, err := http.Post(ts.URL, "application/json", strings.NewReader(`{}`)) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) +} + +func TestShortcutToolUnexpectedArgs(t *testing.T) { + tool := ToolDefinition{ + Name: "test_get_vehicle", + Description: "Get a vehicle", + Args: []ArgDefinition{ + {Name: "tokenId", Type: "integer", Description: "The vehicle token ID", Required: true}, + }, + Query: `query($tokenId: Int!) { vehicle(tokenId: $tokenId) { id } }`, + } + + mcpServer := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil) + registerShortcutTools(mcpServer, mockGQLExecutor(), []ToolDefinition{tool}, nil) + + serverTransport, clientTransport := mcp.NewInMemoryTransports() + ctx := context.Background() + go func() { _ = mcpServer.Run(ctx, serverTransport) }() + + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "0.1.0"}, nil) + session, err := client.Connect(ctx, clientTransport, nil) + require.NoError(t, err) + + // The InputSchema sets additionalProperties: false, and the MCP SDK enforces + // JSON Schema validation server-side. An unexpected argument should be + // rejected at the protocol level before reaching the tool handler. + _, err = session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_get_vehicle", + Arguments: map[string]any{ + "tokenId": 123, + "notARealArg": "should not reach executor", + }, + }) + require.Error(t, err, "SDK should reject unexpected arguments via additionalProperties: false") + assert.Contains(t, err.Error(), "notARealArg") +} + +func TestMultipleShortcutToolsDispatch(t *testing.T) { + tools := []ToolDefinition{ + { + Name: "get_vehicle", + Description: "Get vehicle", + Args: []ArgDefinition{{Name: "id", Type: "integer", Required: true}}, + Query: `query($id: Int!) { vehicle(id: $id) { id } }`, + }, + { + Name: "get_user", + Description: "Get user", + Args: []ArgDefinition{{Name: "name", Type: "string", Required: true}}, + Query: `query($name: String!) { user(name: $name) { name } }`, + }, + { + Name: "get_status", + Description: "Get status", + Args: nil, + Query: `{ status }`, + }, + } + + mcpServer := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil) + exec := mockGQLExecutor() + registerShortcutTools(mcpServer, exec, tools, nil) + + serverTransport, clientTransport := mcp.NewInMemoryTransports() + ctx := context.Background() + go func() { _ = mcpServer.Run(ctx, serverTransport) }() + + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "0.1.0"}, nil) + session, err := client.Connect(ctx, clientTransport, nil) + require.NoError(t, err) + + for _, tool := range tools { + t.Run(tool.Name, func(t *testing.T) { + args := map[string]any{} + for _, a := range tool.Args { + switch a.Type { + case "integer": + args[a.Name] = 1 + case "string": + args[a.Name] = "test" + } + } + + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: tool.Name, + Arguments: args, + }) + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result.Content, 1) + + contentJSON, err := json.Marshal(result.Content[0]) + require.NoError(t, err) + var tc struct{ Text string } + require.NoError(t, json.Unmarshal(contentJSON, &tc)) + + var resp map[string]any + require.NoError(t, json.Unmarshal([]byte(tc.Text), &resp)) + data := resp["data"].(map[string]any) + assert.Equal(t, tool.Query, data["echoQuery"], "tool %s dispatched wrong query", tool.Name) + }) + } +} + +func TestTokenVerifierContextPropagation(t *testing.T) { + verifier := func(ctx context.Context, token string) (context.Context, error) { + return context.WithValue(ctx, authCtxKey{}, "user-from-token"), nil + } + + exec := &mockExecutor{ + fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { + if strings.Contains(query, "__schema") { + return []byte(`{"data":{"__schema":{"queryType":{"name":"Query"}}}}`), nil + } + val, ok := ctx.Value(authCtxKey{}).(string) + if !ok || val != "user-from-token" { + return nil, fmt.Errorf("expected context value 'user-from-token', got %v", ctx.Value(authCtxKey{})) + } + return []byte(`{"data":{"authenticated":true}}`), nil + }, + } + + mcpHandler, err := New(context.Background(), exec, "Test", "0.1.0", "test", + WithTokenVerifier(verifier), + ) + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx := context.Background() + httpClient := ts.Client() + httpClient.Transport = &headerRoundTripper{ + base: httpClient.Transport, + headers: http.Header{"Authorization": []string{"Bearer any-token"}}, + } + transport := &mcp.StreamableClientTransport{Endpoint: ts.URL, HTTPClient: httpClient} + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0"}, nil) + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_query", + Arguments: map[string]any{"query": `{ me { id } }`}, + }) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError, "tool should succeed with propagated context") +} + +func TestWithLoggerOption(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}) + logger := slog.New(handler) + + mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", + WithLogger(logger), + ) + require.NoError(t, err) + + ts := httptest.NewServer(mcpHandler) + defer ts.Close() + + ctx := context.Background() + transport := &mcp.StreamableClientTransport{Endpoint: ts.URL, HTTPClient: ts.Client()} + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0"}, nil) + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer func() { _ = session.Close() }() + + _, err = session.CallTool(ctx, &mcp.CallToolParams{ + Name: "test_query", + Arguments: map[string]any{"query": `{ __typename }`}, + }) + require.NoError(t, err) + assert.Contains(t, buf.String(), "tool call succeeded") +} diff --git a/pkg/mcpserver/metrics.go b/pkg/mcpserver/metrics.go new file mode 100644 index 0000000..b26f396 --- /dev/null +++ b/pkg/mcpserver/metrics.go @@ -0,0 +1,24 @@ +package mcpserver + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + toolCallsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "mcp_tool_calls_total", + Help: "Total number of MCP tool calls, categorized by tool and status.", + }, + []string{"tool", "status"}, + ) + + toolDurationSeconds = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "mcp_tool_duration_seconds", + Help: "Duration of MCP tool calls in seconds.", + }, + []string{"tool"}, + ) +) diff --git a/pkg/mcpserver/tools.go b/pkg/mcpserver/tools.go new file mode 100644 index 0000000..1100b6b --- /dev/null +++ b/pkg/mcpserver/tools.go @@ -0,0 +1,170 @@ +package mcpserver + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/rs/zerolog" +) + +func boolPtr(b bool) *bool { return &b } + +// queryInput is the input for the query tool. +type queryInput struct { + Query string `json:"query" jsonschema:"A GraphQL query or mutation string. Use $-prefixed variable placeholders for dynamic values."` + Variables map[string]any `json:"variables,omitempty" jsonschema:"A JSON object mapping variable names to values. Keys must match the $-prefixed placeholders declared in the query."` +} + +// executeTool runs a GraphQL query, instruments the call, and returns an MCP result. +// Execution errors are returned as tool-level errors (IsError: true), not protocol errors. +// +// Logging is dual-path: zerolog (via context) for infrastructure observability, and +// slog (via explicit logger) for the MCP SDK logging convention. zerolog is always +// attempted; slog fires only when WithLogger is configured. +func executeTool(ctx context.Context, toolName string, exec GraphQLExecutor, query string, variables map[string]any, logger *slog.Logger) (*mcp.CallToolResult, any, error) { + start := time.Now() + result, err := exec.Execute(ctx, query, variables) + duration := time.Since(start) + + zerologger := zerolog.Ctx(ctx) + status := "success" + if err != nil { + status = "error" + zerologger.Error().Err(err).Str("tool", toolName).Dur("duration", duration).Msg("tool call failed") + if logger != nil { + logger.ErrorContext(ctx, "tool call failed", "tool", toolName, "error", err, "duration", duration) + } + } else { + zerologger.Info().Str("tool", toolName).Dur("duration", duration).Msg("tool call succeeded") + if logger != nil { + logger.InfoContext(ctx, "tool call succeeded", "tool", toolName, "duration", duration) + } + } + toolCallsTotal.WithLabelValues(toolName, status).Inc() + toolDurationSeconds.WithLabelValues(toolName).Observe(duration.Seconds()) + + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: err.Error()}, + }, + IsError: true, + }, nil, nil + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(result)}, + }, + }, nil, nil +} + +// registerBuiltinTools registers the built-in tools (get_schema, query) on the server. +func registerBuiltinTools(server *mcp.Server, exec GraphQLExecutor, cachedSchema string, toolPrefix string, maxQuerySize int, logger *slog.Logger) { + schemaToolName := toolPrefix + "_get_schema" + mcp.AddTool(server, &mcp.Tool{ + Name: schemaToolName, + Description: "Returns the GraphQL schema describing all available types, fields, arguments, and their relationships. " + + "Call this first to understand the API before constructing any query.", + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: true, + DestructiveHint: boolPtr(false), + OpenWorldHint: boolPtr(false), + IdempotentHint: true, + }, + }, func(ctx context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) { + start := time.Now() + defer func() { + d := time.Since(start) + zerolog.Ctx(ctx).Info().Str("tool", schemaToolName).Dur("duration", d).Msg("tool call succeeded") + toolCallsTotal.WithLabelValues(schemaToolName, "success").Inc() + toolDurationSeconds.WithLabelValues(schemaToolName).Observe(d.Seconds()) + }() + if cachedSchema == "" { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "schema is unavailable"}, + }, + IsError: true, + }, nil, nil + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: cachedSchema}, + }, + }, nil, nil + }) + + queryToolName := toolPrefix + "_query" + mcp.AddTool(server, &mcp.Tool{ + Name: queryToolName, + Description: "Executes a GraphQL query or mutation and returns the result as JSON " + + "in the standard GraphQL response format ({\"data\": ...} on success, {\"errors\": [...]} on failure). " + + "Always call " + schemaToolName + " first to discover the schema. " + + "Prefer purpose-specific tools when available and only use this tool for operations they do not cover. " + + "Pass dynamic values using the 'variables' parameter with $-prefixed placeholders in the query " + + "instead of interpolating values directly into the query string.", + Annotations: &mcp.ToolAnnotations{ + OpenWorldHint: boolPtr(false), + }, + }, func(ctx context.Context, req *mcp.CallToolRequest, input queryInput) (*mcp.CallToolResult, any, error) { + if len(input.Query) > maxQuerySize { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: fmt.Sprintf("query exceeds maximum size of %d bytes", maxQuerySize)}, + }, + IsError: true, + }, nil, nil + } + return executeTool(ctx, queryToolName, exec, input.Query, input.Variables, logger) + }) +} + +// buildInputSchema constructs a ToolInputSchema from ArgDefinitions. +func buildInputSchema(args []ArgDefinition) map[string]any { + properties := make(map[string]any) + var required []string + for _, arg := range args { + prop := map[string]any{ + "type": arg.Type, + "description": arg.Description, + } + if arg.Type == "array" && arg.ItemsType != "" { + prop["items"] = map[string]any{"type": arg.ItemsType} + } + if len(arg.EnumValues) > 0 { + prop["enum"] = arg.EnumValues + } + properties[arg.Name] = prop + if arg.Required { + required = append(required, arg.Name) + } + } + schema := map[string]any{ + "type": "object", + "properties": properties, + "additionalProperties": false, + } + if len(required) > 0 { + schema["required"] = required + } + return schema +} + +// registerShortcutTools registers shortcut tools derived from ToolDefinitions. +func registerShortcutTools(server *mcp.Server, exec GraphQLExecutor, tools []ToolDefinition, logger *slog.Logger) { + for _, tool := range tools { + inputSchema := buildInputSchema(tool.Args) + + mcp.AddTool(server, &mcp.Tool{ + Name: tool.Name, + Description: tool.Description, + InputSchema: inputSchema, + Annotations: tool.Annotations, + }, func(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + return executeTool(ctx, tool.Name, exec, tool.Query, args, logger) + }) + } +} From 73eb358c8e9e6064ff8541bbd73516ebb9084aa0 Mon Sep 17 00:00:00 2001 From: zer0stars <74260741+zer0stars@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:34:46 -0400 Subject: [PATCH 2/4] chore(deps): pin cloudevent to v0.2.7 cloudevent v1.0.x predates v0.2.x chronologically (v0.2.7 released 2026-04-13 vs v1.0.4 on 2026-03-10). Go's MVS selects the higher semver, which pulls in the older API without RawEventID, breaking downstream consumers (fetch-api) that rely on the field. Pin here so MVS consistently picks v0.2.7. --- go.mod | 2 +- go.sum | 279 +-------------------------------------------------------- 2 files changed, 3 insertions(+), 278 deletions(-) diff --git a/go.mod b/go.mod index 7d82fb4..25adf50 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/99designs/gqlgen v0.17.89 - github.com/DIMO-Network/cloudevent v1.0.4 + github.com/DIMO-Network/cloudevent v0.2.7 github.com/DIMO-Network/token-exchange-api v0.4.0 github.com/caarlos0/env/v11 v11.4.0 github.com/ethereum/go-ethereum v1.17.1 diff --git a/go.sum b/go.sum index ada17cb..8e78b57 100644 --- a/go.sum +++ b/go.sum @@ -1,234 +1,71 @@ -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8= github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgxoBDMqMO/Nvy7bZ9a0nbU3I1DtFQK3YvB4= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= -github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc= -github.com/DIMO-Network/clickhouse-infra v0.0.7/go.mod h1:XS80lhSJNWBWGgZ+m4j7++zFj1wAXfmtV2gJfhGlabQ= -github.com/DIMO-Network/cloudevent v1.0.4 h1:bZ7tRPwCbiRgAlG1gfN7qpOOUI1OEXMMoMMyS9r4omk= -github.com/DIMO-Network/cloudevent v1.0.4/go.mod h1:I/9NcpMozV5Fw194WimhbkAsJtKVZf5UKYJ9hgc8Cdg= +github.com/DIMO-Network/cloudevent v0.2.7 h1:/cgFhUcWcliZYrmITkB8oIZb+zDhZvYNxWVGS2D3894= +github.com/DIMO-Network/cloudevent v0.2.7/go.mod h1:I/9NcpMozV5Fw194WimhbkAsJtKVZf5UKYJ9hgc8Cdg= github.com/DIMO-Network/shared v1.1.5 h1:cRI3BbeYOgolMkeBOSqbDVGxymnDfeP/q7xgIXJ5MkY= github.com/DIMO-Network/shared v1.1.5/go.mod h1:lDHUKwwT2LW6Zvd42Nb33dXklRNTmfyOlbUNx2dQfGY= github.com/DIMO-Network/token-exchange-api v0.4.0 h1:EayDrw9VdyAfc6rbpdnDxFhlN3lMhbonUJoouKZu35g= github.com/DIMO-Network/token-exchange-api v0.4.0/go.mod h1:cldgAyDGLMNk3YIf2mr6vGohcO0ANlZ88fCbI+v/wEY= -github.com/DIMO-Network/yaml v0.1.0/go.mod h1:KkiehcbkVzH8Pf8f9dja8B2aW81gYYZSqfwzSj9yN68= -github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= -github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= -github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= -github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= -github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/avast/retry-go/v4 v4.5.1/go.mod h1:/sipNsvNB3RRuT5iNcb6h73nw3IBmXJ/H3XrCQYSOpc= -github.com/aws/aws-sdk-go-v2 v1.25.0/go.mod h1:G104G1Aho5WqF+SR3mDIobTABQzpYV0WxMsKxlMggOA= -github.com/aws/aws-sdk-go-v2/config v1.18.45/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= -github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0/go.mod h1:D+duLy2ylgatV+yTlQ8JTuLfDD0BnFvnQRc+o6tbZ4M= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0/go.mod h1:hL6BWM/d/qz113fVitZjbXR0E+RCTU1+x+1Idyn5NgE= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= -github.com/aws/aws-sdk-go-v2/service/kms v1.28.1/go.mod h1:Y/mkxhbaWCswchbBBLRwet6uYKl/026DZXS87c0DmuU= -github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2/go.mod h1:TQZBt/WaQy+zTHoW++rnl8JBrmZ0VO6EUbVua1+foCA= -github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= -github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= -github.com/aws/smithy-go v1.20.0/go.mod h1:uo5RKksAl4PzhqaAbjd4rLgFoq5koTsQKYuGe7dklGc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc= github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudflare/cloudflare-go v0.114.0/go.mod h1:O7fYfFfA6wKqKFn2QIR9lhj7FDw6VQCGOY6hd2TBtd0= -github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= -github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= -github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= -github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= -github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw= -github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= -github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= -github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= -github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/ericlagergren/decimal v0.0.0-20190420051523-6335edbaa640/go.mod h1:mdYyfAkzn9kyJ/kMk/7WE9ufl9lflh+2NvecQ5mAghs= -github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= -github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho= github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I= -github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= -github.com/fjl/gencodec v0.1.0/go.mod h1:Um1dFHPONZGTHog1qD1NaWjXJW/SPB38wPv0O8uZ2fI= -github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= -github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= -github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= -github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= -github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= -github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/contrib/jwt v1.1.2 h1:GmWnOqT4A15EkA8IPXwSpvNUXZR4u5SMj+geBmyLAjs= github.com/gofiber/contrib/jwt v1.1.2/go.mod h1:CpIwrkUQ3Q6IP8y9n3f0wP9bOnSKx39EDp2fBVgMFVk= github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= -github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= -github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= -github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= -github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= -github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matryer/moq v0.6.0/go.mod h1:iEVhY/XBwFG/nbRyEf0oV+SqnTHZJ5wectzx7yT+y98= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -238,53 +75,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= -github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE= -github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= -github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs= -github.com/parquet-go/jsonlite v1.4.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0= -github.com/parquet-go/parquet-go v0.28.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg= -github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= -github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -293,44 +90,23 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/protolambda/bls12-381-util v0.1.0/go.mod h1:cdkysJTRpeFeuUVx/TXGDQNMTiRAalk1vQw3TYTHcE4= -github.com/protolambda/zrnt v0.34.1/go.mod h1:A0fezkp9Tt3GBLATSPIbuY4ywYESyAuc/FFmPKg8Lqs= -github.com/protolambda/ztyp v0.2.2/go.mod h1:9bYgKGqg3wJqT9ac1gI2hnVb0STQq7p/1lapqrqY1dU= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= -github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= -github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= -github.com/testcontainers/testcontainers-go/modules/clickhouse v0.38.0/go.mod h1:4YCEhJkDA1L1GF8ndOf2RVXtdxY1Po30nmtwvDOb+8Q= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= @@ -339,52 +115,19 @@ github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= -github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU= -github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= -github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= -github.com/vertica/vertica-sql-go v1.3.5/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4= -github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA= -github.com/volatiletech/null/v8 v8.1.2/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g= -github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY= -github.com/volatiletech/sqlboiler/v4 v4.16.2/go.mod h1:B14BPBGTrJ2X6l7lwnvV/iXgYR48+ozGSlzHI3frl6U= -github.com/volatiletech/strmangle v0.0.6/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0= -github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= -github.com/ydb-platform/ydb-go-sdk/v3 v3.127.0/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -392,10 +135,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -428,15 +169,12 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -444,24 +182,11 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= From f46cda3d496c3b45acfb2639aa1806f3e0d65c62 Mon Sep 17 00:00:00 2001 From: zer0stars <74260741+zer0stars@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:42:21 -0400 Subject: [PATCH 3/4] fix(lint): check errors and remove dead branch Fix CI lint failures: check json.Unmarshal return, wrap resp.Body.Close in deferred func to ignore error explicitly, and drop a staticcheck SA9003 empty-branch in condensed_sdl paragraph loop. --- cmd/mcpgen/condensed_sdl.go | 6 +----- pkg/mcpserver/mcpserver_test.go | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cmd/mcpgen/condensed_sdl.go b/cmd/mcpgen/condensed_sdl.go index 05974cc..615c2f6 100644 --- a/cmd/mcpgen/condensed_sdl.go +++ b/cmd/mcpgen/condensed_sdl.go @@ -374,7 +374,7 @@ func writeDescription(sb *strings.Builder, desc, indent string) { paragraphs := splitDescriptionParagraphs(desc) sb.WriteString(indent) sb.WriteString("\"\"\"\n") - for i, para := range paragraphs { + for _, para := range paragraphs { if para == "" { // Blank line between paragraphs. sb.WriteString("\n") @@ -385,10 +385,6 @@ func writeDescription(sb *strings.Builder, desc, indent string) { sb.WriteString(line) sb.WriteString("\n") } - // Add blank line between paragraphs (but not after the last one). - if i < len(paragraphs)-1 && paragraphs[i+1] != "" { - // Only if next paragraph is not already a blank separator. - } } sb.WriteString(indent) sb.WriteString("\"\"\"\n") diff --git a/pkg/mcpserver/mcpserver_test.go b/pkg/mcpserver/mcpserver_test.go index 572227d..e2a6169 100644 --- a/pkg/mcpserver/mcpserver_test.go +++ b/pkg/mcpserver/mcpserver_test.go @@ -497,7 +497,7 @@ func TestQuerySizeLimitRejected(t *testing.T) { contentJSON, _ := json.Marshal(result.Content[0]) var tc struct{ Text string } - json.Unmarshal(contentJSON, &tc) + require.NoError(t, json.Unmarshal(contentJSON, &tc)) assert.Contains(t, tc.Text, "exceeds maximum size") } @@ -595,7 +595,7 @@ func TestTokenVerifierRejects(t *testing.T) { resp, err := http.Post(ts.URL, "application/json", strings.NewReader(`{}`)) require.NoError(t, err) - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) } @@ -614,7 +614,7 @@ func TestTokenVerifierMissingHeader(t *testing.T) { resp, err := http.Post(ts.URL, "application/json", strings.NewReader(`{}`)) require.NoError(t, err) - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) } From d0fc8561283b98d26dbb31a3ac9ba389c8e8af11 Mon Sep 17 00:00:00 2001 From: zer0stars <74260741+zer0stars@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:58:50 -0400 Subject: [PATCH 4/4] feat(mcpserver): shrink condensed schema + require WithCondensedSchema mcpgen: stronger self-evident filtering (drop-entirely vs strip-only prefixes, morphological stems), global + per-category legend-fold on signal table, scalar description trim. mcpserver: remove dead introspection loadSchema path. WithCondensedSchema is now required; New() no longer takes a ctx arg. Shrinks downstream LLM context by ~18%. --- cmd/mcpgen/condensed_filter.go | 188 ++++++++++++++++++++++--- cmd/mcpgen/condensed_sdl.go | 18 +-- cmd/mcpgen/condensed_signals.go | 127 +++++++++++++++-- cmd/mcpgen/condensed_test.go | 57 ++++++-- pkg/mcpserver/condensed_schema_test.go | 45 +----- pkg/mcpserver/executor.go | 106 -------------- pkg/mcpserver/mcpserver.go | 20 +-- pkg/mcpserver/mcpserver_test.go | 67 +++------ pkg/mcpserver/tools.go | 8 -- 9 files changed, 367 insertions(+), 269 deletions(-) delete mode 100644 pkg/mcpserver/executor.go diff --git a/cmd/mcpgen/condensed_filter.go b/cmd/mcpgen/condensed_filter.go index 481d22f..68e7a6b 100644 --- a/cmd/mcpgen/condensed_filter.go +++ b/cmd/mcpgen/condensed_filter.go @@ -31,16 +31,101 @@ var selfEvidentFields = map[string]bool{ } // stopWords are filler words ignored when comparing descriptions against field/type names. +// Also includes type-like nouns ("address", "integer") whose presence is already conveyed by +// the field's GraphQL type and adds no signal in a docstring. var stopWords = map[string]bool{ "the": true, "of": true, "for": true, "this": true, "a": true, "an": true, "is": true, "in": true, "to": true, "and": true, "or": true, "by": true, "on": true, "at": true, "from": true, "with": true, "its": true, "that": true, - "true": true, "false": true, + "true": true, "false": true, "any": true, "which": true, + "particular": true, "specific": true, "specified": true, "given": true, + "based": true, "criteria": true, "matching": true, "these": true, + "returns": true, "return": true, "contains": true, + "address": true, "integer": true, "string": true, "boolean": true, "bytes": true, +} + +// wordStems maps morphological variants to a canonical stem so descriptions like +// "Filter for vehicles owned by this address" match field names like "owner". +// Both sides of each entry reduce to the key. Intentionally small — we only add +// pairs observed producing false-negative self-evident matches across DIMO schemas. +var wordStems = map[string]string{ + "owned": "owner", "owns": "owner", "owning": "owner", + "minted": "mint", "minting": "mint", "mints": "mint", + "paired": "pair", "pairing": "pair", "pairs": "pair", + "created": "create", "creating": "create", "creates": "create", "creator": "create", + "filtered": "filter", "filtering": "filter", "filters": "filter", + "connected": "connect", "connecting": "connect", "connects": "connect", "connection": "connect", + "vehicles": "vehicle", "devices": "device", + "addresses": "address", +} + +// stemOf returns the canonical stem for a word, or the word itself if no stem is known. +func stemOf(w string) string { + if s, ok := wordStems[w]; ok { + return s + } + return w +} + +// dropEntirelyPrefixes are verb+article openers where the remainder of the +// description always just names or explains the entity the field returns. +// Descriptions starting with any of these are treated as fully self-evident, +// regardless of remainder content (sort-order trivia, alias notes, etc.). +var dropEntirelyPrefixes = []string{ + "view a particular ", "retrieve a particular ", "get a particular ", + "fetch a particular ", "look up a particular ", "look up ", + "retrieves information about an ", "retrieves information about a ", + "retrieves information about ", + "list minted ", "lists minted ", "list ", "lists ", + "view ", "retrieve ", "retrieves ", "get ", "fetch ", "fetches ", +} + +// stripOnlyPrefixes are openers stripped off so the remainder is checked +// against field/type names. Used for qualifiers like "Filter for X ..." where +// the tail carries the load-bearing content. +var stripOnlyPrefixes = []string{ + "criteria to search for a ", "criteria to search for an ", "criteria to search for ", + "filters the ", "filter the ", "filters for ", "filter for ", + "filters by ", "filter by ", "filter on ", "filters on ", + "filter based on ", "filters based on ", +} + +// hasDropEntirelyPrefix reports whether desc opens with a verb+article prefix +// that renders the whole description self-evident. +func hasDropEntirelyPrefix(desc string) bool { + lower := strings.ToLower(strings.TrimSpace(desc)) + for _, p := range dropEntirelyPrefixes { + if strings.HasPrefix(lower, p) { + return true + } + } + return false +} + +// stripBoilerplatePrefix removes a known opener from desc. Returns the trimmed +// remainder (empty when the prefix consumed the entire description). Handles +// both drop-entirely and strip-only prefixes; callers pair this with +// hasDropEntirelyPrefix when they want to bail out early. +func stripBoilerplatePrefix(desc string) string { + lower := strings.ToLower(strings.TrimSpace(desc)) + for _, p := range dropEntirelyPrefixes { + if strings.HasPrefix(lower, p) { + return strings.TrimSpace(desc[len(p):]) + } + } + for _, p := range stripOnlyPrefixes { + if strings.HasPrefix(lower, p) { + return strings.TrimSpace(desc[len(p):]) + } + } + return desc } // isSelfEvidentDescription returns true if a description trivially restates the field name. // Uses word-overlap: if every non-stop-word in the description also appears in the // field name or type name (split on camelCase boundaries), the description adds nothing. +// Leading boilerplate openers (e.g. "View a particular", "Filter for") are stripped first +// so descriptions that would otherwise register as novel resolve to empty. func isSelfEvidentDescription(desc, fieldName, typeName string) bool { if desc == "" { return false @@ -49,17 +134,29 @@ func isSelfEvidentDescription(desc, fieldName, typeName string) bool { return true } + if hasDropEntirelyPrefix(desc) { + return true + } + desc = stripBoilerplatePrefix(desc) + if desc == "" { + return true + } + // Build the set of "known" words from field name + type name. // Include both the camelCase-split words and the raw lowercase forms, // since descriptions may use either "dataVersion" or "data version". knownWords := make(map[string]bool) - knownWords[strings.ToLower(fieldName)] = true - knownWords[strings.ToLower(typeName)] = true - for _, w := range strings.Fields(camelToSpaced(fieldName)) { + addKnown := func(w string) { knownWords[w] = true + knownWords[stemOf(w)] = true + } + addKnown(strings.ToLower(fieldName)) + addKnown(strings.ToLower(typeName)) + for _, w := range strings.Fields(camelToSpaced(fieldName)) { + addKnown(w) } for _, w := range strings.Fields(camelToSpaced(typeName)) { - knownWords[w] = true + addKnown(w) } // Tokenize description, strip punctuation, check if all meaningful words are known. @@ -68,20 +165,31 @@ func isSelfEvidentDescription(desc, fieldName, typeName string) bool { if w == "" || len(w) <= 1 || stopWords[w] { continue } - if !knownWords[w] { - return false + if knownWords[w] || knownWords[stemOf(w)] { + continue } + return false } return true } -// camelToSpaced converts PascalCase/camelCase to lowercase space-separated words. -// e.g., "DeviceDefinition" → "device definition" +// camelToSpaced converts PascalCase/camelCase to lowercase space-separated words, +// treating runs of uppercase letters as a single acronym. +// e.g., "DeviceDefinition" → "device definition", "DCNFilter" → "dcn filter", +// "tokenDID" → "token did". func camelToSpaced(s string) string { + runes := []rune(s) var sb strings.Builder - for i, r := range s { - if i > 0 && r >= 'A' && r <= 'Z' { - sb.WriteByte(' ') + isUpper := func(r rune) bool { return r >= 'A' && r <= 'Z' } + isLower := func(r rune) bool { return r >= 'a' && r <= 'z' } + for i, r := range runes { + if i > 0 && isUpper(r) { + prev := runes[i-1] + // camelCase boundary: lower → upper (fooBar → foo Bar). + // Acronym boundary: upper → upper where next is lower (DCNFilter → DCN Filter). + if isLower(prev) || (isUpper(prev) && i+1 < len(runes) && isLower(runes[i+1])) { + sb.WriteByte(' ') + } } sb.WriteRune(r) } @@ -107,6 +215,33 @@ func containsDIDFormat(desc string) bool { return didFormatPattern.MatchString(desc) } +// filterScalarDescription returns the inline description to emit after a scalar +// declaration, or "" if none is worth showing. Self-evident descriptions (that +// merely restate the scalar name) are dropped. Multi-sentence descriptions are +// truncated to the first sentence to strip analogies and spec footnotes. +func filterScalarDescription(desc, scalarName string) string { + if desc == "" { + return "" + } + collapsed := strings.Join(strings.Fields(desc), " ") + if first := firstSentence(collapsed); first != "" { + collapsed = first + } + if isSelfEvidentDescription(collapsed, "", scalarName) { + return "" + } + return collapsed +} + +// firstSentence returns the first sentence of s, including its terminating period, +// or the whole string if no sentence terminator is found. +func firstSentence(s string) string { + if idx := strings.Index(s, ". "); idx >= 0 { + return s[:idx+1] + } + return s +} + // filterFieldDescription returns the description to use for a field, or "" if it should be omitted. // It strips self-evident descriptions and DID format descriptions. func filterFieldDescription(desc, fieldName, typeName string) string { @@ -124,15 +259,33 @@ func filterFieldDescription(desc, fieldName, typeName string) string { // filterArgDescription returns the description to use for an argument, or "" if it should be omitted. // Pagination arguments and self-evident descriptions are stripped. -func filterArgDescription(desc, argName string) string { +// parentField is the name of the field the argument belongs to (e.g. "vehicle" for +// vehicle(tokenId:)); its tokens join knownWords so "The token ID of the vehicle" +// resolves as self-evident. +func filterArgDescription(desc, argName, parentField string) string { if desc == "" || isPaginationArg(argName) { return "" } + desc = stripBoilerplatePrefix(desc) + if desc == "" { + return "" + } + knownWords := make(map[string]bool) - knownWords[strings.ToLower(argName)] = true - for _, w := range strings.Fields(camelToSpaced(argName)) { + addKnown := func(w string) { knownWords[w] = true + knownWords[stemOf(w)] = true + } + addKnown(strings.ToLower(argName)) + for _, w := range strings.Fields(camelToSpaced(argName)) { + addKnown(w) + } + if parentField != "" { + addKnown(strings.ToLower(parentField)) + for _, w := range strings.Fields(camelToSpaced(parentField)) { + addKnown(w) + } } for _, w := range strings.Fields(strings.ToLower(desc)) { @@ -140,9 +293,10 @@ func filterArgDescription(desc, argName string) string { if w == "" || len(w) <= 1 || stopWords[w] { continue } - if !knownWords[w] { - return desc + if knownWords[w] || knownWords[stemOf(w)] { + continue } + return desc } return "" } diff --git a/cmd/mcpgen/condensed_sdl.go b/cmd/mcpgen/condensed_sdl.go index 615c2f6..5e878ec 100644 --- a/cmd/mcpgen/condensed_sdl.go +++ b/cmd/mcpgen/condensed_sdl.go @@ -33,7 +33,7 @@ func writeOperationType(sb *strings.Builder, def *ast.Definition) { sb.WriteString(" ") sb.WriteString(field.Name) - writeFieldArguments(sb, field.Arguments) + writeFieldArguments(sb, field.Arguments, field.Name) sb.WriteString(": ") sb.WriteString(field.Type.String()) sb.WriteString("\n") @@ -93,7 +93,7 @@ func writeTypeDefinition(sb *strings.Builder, def *ast.Definition, analysis *sch } sb.WriteString(" ") sb.WriteString(field.Name) - writeFieldArguments(sb, field.Arguments) + writeFieldArguments(sb, field.Arguments, field.Name) sb.WriteString(": ") sb.WriteString(field.Type.String()) writeFieldDefaultValue(sb, field) @@ -159,10 +159,8 @@ func writeTypeDefinition(sb *strings.Builder, def *ast.Definition, analysis *sch case ast.Scalar: sb.WriteString("scalar ") sb.WriteString(def.Name) - if def.Description != "" { + if desc := filterScalarDescription(def.Description, def.Name); desc != "" { sb.WriteString(" # ") - // Join multi-line descriptions into a single inline comment. - desc := strings.Join(strings.Fields(def.Description), " ") sb.WriteString(desc) } sb.WriteString("\n") @@ -282,8 +280,10 @@ func writeInlineArgs(sb *strings.Builder, args ast.ArgumentDefinitionList) { // writeFieldArguments writes field arguments in SDL format. // Uses inline format when no arguments have non-trivial descriptions, multi-line otherwise. // Pagination args and self-evident arg descriptions are stripped. -// Preserves default values when present. -func writeFieldArguments(sb *strings.Builder, args ast.ArgumentDefinitionList) { +// Preserves default values when present. parentField supplies extra context +// (e.g. the enclosing field name "vehicle" for vehicle(tokenId:)) so arg +// docstrings that restate it can be recognized as self-evident. +func writeFieldArguments(sb *strings.Builder, args ast.ArgumentDefinitionList, parentField string) { if len(args) == 0 { return } @@ -291,7 +291,7 @@ func writeFieldArguments(sb *strings.Builder, args ast.ArgumentDefinitionList) { // Check if any arg has a meaningful description after filtering. hasDescriptions := false for _, arg := range args { - if filterArgDescription(arg.Description, arg.Name) != "" { + if filterArgDescription(arg.Description, arg.Name, parentField) != "" { hasDescriptions = true break } @@ -300,7 +300,7 @@ func writeFieldArguments(sb *strings.Builder, args ast.ArgumentDefinitionList) { if hasDescriptions { sb.WriteString("(\n") for _, arg := range args { - desc := filterArgDescription(arg.Description, arg.Name) + desc := filterArgDescription(arg.Description, arg.Name, parentField) if desc != "" { writeDescription(sb, desc, " ") } diff --git a/cmd/mcpgen/condensed_signals.go b/cmd/mcpgen/condensed_signals.go index 8b41bfb..4cf5511 100644 --- a/cmd/mcpgen/condensed_signals.go +++ b/cmd/mcpgen/condensed_signals.go @@ -74,32 +74,124 @@ func writeSignalReferenceTable(sb *strings.Builder, schema *ast.Schema, analysis // Emit per-type calling conventions so the LLM knows how to query each // signal type. The args depend on the parent type, not the individual signal. - sb.WriteString("#\n") sb.WriteString("# All signals below exist on every signal type. Calling convention per type:\n") for _, ti := range typeInfos { - fmt.Fprintf(sb, "# %s:\n", ti.typeName) + if len(ti.groups) == 1 { + g := ti.groups[0] + fmt.Fprintf(sb, "# %s: fieldName%s: %s\n", ti.typeName, g.argSig, g.returnType) + continue + } + fmt.Fprintf(sb, "# %s:\n", ti.typeName) for _, g := range ti.groups { - fmt.Fprintf(sb, "# fieldName%s: %s\n", g.argSig, g.returnType) + fmt.Fprintf(sb, "# fieldName%s: %s\n", g.argSig, g.returnType) } } - sb.WriteString("#\n") - // Emit markdown table. - sb.WriteString("# | Signal | Type | Unit | Description |\n") - sb.WriteString("# |--------|------|------|-------------|\n") + // Emit type-exception list so the table can drop its Type column. + // Default is Float (dominant case in VSS); call out String / Location / other. + buckets := map[string][]string{} + bucketOrder := []string{} + for _, f := range signals { + rt := baseSignalType(f.Type) + if _, ok := buckets[rt]; !ok { + bucketOrder = append(bucketOrder, rt) + } + buckets[rt] = append(buckets[rt], f.Name) + } + defaultType := "Float" + if _, ok := buckets[defaultType]; !ok { + // Fall back to whichever bucket is largest. + maxN := 0 + for t, names := range buckets { + if len(names) > maxN { + defaultType = t + maxN = len(names) + } + } + } + fmt.Fprintf(sb, "# %s is the default type.", defaultType) + for _, t := range bucketOrder { + if t == defaultType { + continue + } + fmt.Fprintf(sb, " %s: %s.", t, strings.Join(buckets[t], ", ")) + } + sb.WriteString("\n") + + // Emit markdown table (Unit | Description; Type is omitted — see exception list above). + sb.WriteString("# | Signal | Unit | Description |\n") + sb.WriteString("# |--------|------|-------------|\n") + + // Global shared descriptions: descriptions repeated verbatim ≥3 times across + // the entire signal list (doors "Is item open or closed?", belts "Is the belt + // engaged", wheel "Rotational speed of a vehicle's wheel", etc.). Emit each + // once in a legend above the table; rows whose desc matches are blanked so + // the prose isn't repeated across every door/belt/wheel row. + globalShared := sharedDescriptions(signals, 3) + if len(globalShared) > 0 { + sb.WriteString("# Shared descriptions (blank rows below use these):\n") + for _, d := range globalShared { + fmt.Fprintf(sb, "# - %s\n", d) + } + } + globalSharedSet := make(map[string]bool, len(globalShared)) + for _, d := range globalShared { + globalSharedSet[d] = true + } categories := categorizeSignals(signals) for _, cat := range categories { priv := dominantPrivilege(cat.fields) + // Category-local shared descriptions (≥2 reps not already in the global + // set) — catches smaller clusters the global threshold misses. + localShared := sharedDescriptions(cat.fields, 2) + localSharedSet := make(map[string]bool, len(localShared)) + var localUnique []string + for _, d := range localShared { + if globalSharedSet[d] { + continue + } + localSharedSet[d] = true + localUnique = append(localUnique, d) + } + header := fmt.Sprintf("# ── %s ──", cat.label) if priv != "" { - fmt.Fprintf(sb, "# ── %s (privilege: %s) ──\n", cat.label, priv) - } else { - fmt.Fprintf(sb, "# ── %s ──\n", cat.label) + header = fmt.Sprintf("# ── %s (privilege: %s) ──", cat.label, priv) + } + sb.WriteString(header) + sb.WriteString("\n") + for _, d := range localUnique { + fmt.Fprintf(sb, "# shared: %s\n", d) } for _, f := range cat.fields { - writeSignalTableRow(sb, f, priv) + writeSignalTableRow(sb, f, priv, globalSharedSet, localSharedSet) + } + } +} + +// sharedDescriptions returns the set of non-obvious signal descriptions that +// repeat verbatim at least `min` times across fields, preserving first-observed +// order for stable output. +func sharedDescriptions(fields []*ast.FieldDefinition, min int) []string { + counts := map[string]int{} + order := []string{} + for _, f := range fields { + d := extractShortDescription(f.Description) + if d == "" || !isNonObviousSignalDesc(d, f.Name) { + continue + } + if _, seen := counts[d]; !seen { + order = append(order, d) } + counts[d]++ } + var out []string + for _, d := range order { + if counts[d] >= min { + out = append(out, d) + } + } + return out } // writeFieldsWithSignalCollapsing writes fields for types that contain @isSignal fields. @@ -127,7 +219,7 @@ func writeFieldsWithSignalCollapsing(sb *strings.Builder, def *ast.Definition) { } sb.WriteString(" ") sb.WriteString(field.Name) - writeFieldArguments(sb, field.Arguments) + writeFieldArguments(sb, field.Arguments, field.Name) sb.WriteString(": ") sb.WriteString(field.Type.String()) sb.WriteString("\n") @@ -141,14 +233,19 @@ func writeFieldsWithSignalCollapsing(sb *strings.Builder, def *ast.Definition) { // writeSignalTableRow writes a signal as a markdown table row. // categoryPrivilege is the dominant privilege for the category; if the field's // privilege differs, it is shown inline in the description column. -func writeSignalTableRow(sb *strings.Builder, f *ast.FieldDefinition, categoryPrivilege string) { - rt := baseSignalType(f.Type) +// globalShared and localShared are sets of description texts emitted as legends +// above the table or category; rows whose own description matches any of them +// are blanked so the table doesn't repeat identical prose. +func writeSignalTableRow(sb *strings.Builder, f *ast.FieldDefinition, categoryPrivilege string, globalShared, localShared map[string]bool) { unit := extractUnit(f.Description) shortDesc := extractShortDescription(f.Description) // Drop descriptions that just restate the signal name + unit. if !isNonObviousSignalDesc(shortDesc, f.Name) { shortDesc = "" } + if globalShared[shortDesc] || localShared[shortDesc] { + shortDesc = "" + } // Show privilege inline if it differs from the category's dominant privilege. fieldPriv := extractPrivilege(f.Description) @@ -160,7 +257,7 @@ func writeSignalTableRow(sb *strings.Builder, f *ast.FieldDefinition, categoryPr } } - fmt.Fprintf(sb, "# | %s | %s | %s | %s |\n", f.Name, rt, unit, shortDesc) + fmt.Fprintf(sb, "# | %s | %s | %s |\n", f.Name, unit, shortDesc) } // baseSignalType returns the base type name for a signal field's return type. diff --git a/cmd/mcpgen/condensed_test.go b/cmd/mcpgen/condensed_test.go index 79d957d..fc0bb65 100644 --- a/cmd/mcpgen/condensed_test.go +++ b/cmd/mcpgen/condensed_test.go @@ -181,22 +181,25 @@ func TestCondensedSDLSignalCollapsing(t *testing.T) { // Should have signal header with total count (9 signals now with approximate field). assert.Contains(t, sdl, "SIGNAL FIELDS (9 total)") - // Should have per-type calling conventions. + // Should have per-type calling conventions (compact format: one line per type + // when it has exactly one argument-signature group). assert.Contains(t, sdl, "All signals below exist on every signal type") assert.Contains(t, sdl, "SignalAggregations:") assert.Contains(t, sdl, "fieldName(agg: FloatAggregation!, filter: SignalFloatFilter): Float") - // Should have markdown table header with Type column. - assert.Contains(t, sdl, "| Signal | Type | Unit | Description |") + // Type column is dropped; exception list names non-default types. + assert.Contains(t, sdl, "Float is the default type.") + assert.Contains(t, sdl, "String: vin") + assert.Contains(t, sdl, "| Signal | Unit | Description |") + assert.NotContains(t, sdl, "| Signal | Type | Unit | Description |") - // Individual signal fields should appear as table rows with base type. - // Self-evident descriptions (speed, engine speed) are dropped. - assert.Contains(t, sdl, "| speed | Float | km/h |") - assert.Contains(t, sdl, "| powertrainCombustionEngineSpeed | Float | rpm |") + // Individual signal fields appear as table rows; self-evident descriptions dropped. + assert.Contains(t, sdl, "| speed | km/h |") + assert.Contains(t, sdl, "| powertrainCombustionEngineSpeed | rpm |") // Non-obvious descriptions (PID codes) are kept. - assert.Contains(t, sdl, "| obdRunTime | Float | s | PID 1F - Engine run time |") - // String type shown. - assert.Contains(t, sdl, "| vin | String |") + assert.Contains(t, sdl, "| obdRunTime | s | PID 1F - Engine run time |") + // String signals appear in the table without an explicit type column. + assert.Contains(t, sdl, "| vin | | Vehicle Identification Number |") // Category headers should use separator-line format, not table rows. assert.Contains(t, sdl, "── OTHER (privilege: VEHICLE_NON_LOCATION_DATA) ──") @@ -206,7 +209,7 @@ func TestCondensedSDLSignalCollapsing(t *testing.T) { // Per-field privilege override: the approximate field's privilege differs from // the category's dominant (VEHICLE_ALL_TIME_LOCATION), so it's shown inline. - assert.Contains(t, sdl, "| currentLocationApproximateLatitude | Float | degrees | privilege: VEHICLE_APPROXIMATE_LOCATION |") + assert.Contains(t, sdl, "| currentLocationApproximateLatitude | degrees | privilege: VEHICLE_APPROXIMATE_LOCATION |") // Signal fields should NOT appear as full field definitions. assert.NotContains(t, sdl, "speed(agg: FloatAggregation!") @@ -341,6 +344,21 @@ func TestIsSelfEvidentDescription(t *testing.T) { {"model for this DeviceDefinition", "model", "DeviceDefinition", true}, // " of the ." patterns {"model of the DeviceDefinition.", "model", "DeviceDefinition", true}, + // Boilerplate query-level openers strip to empty. + {"View a particular vehicle.", "vehicle", "Query", true}, + {"Retrieve a particular template.", "template", "Query", true}, + {"List minted vehicles.", "vehicles", "Query", true}, + {"criteria to search for a manufacturer", "by", "ManufacturerBy", true}, + // Filter-input descriptions that restate the field name via morphology. + {"Filter for vehicles owned by this address.", "owner", "VehiclesFilter", true}, + {"Filter for aftermarket devices owned by this address.", "owner", "AftermarketDevicesFilter", true}, + {"Filter for DCN owned by this address.", "owner", "DCNFilter", true}, + // Drop-entirely openers ("View a particular ...") are stripped regardless + // of remainder — the lookup mode is already enumerated in the `by:` input + // type's fields, so the tail ("by VIN") adds no load-bearing info. + {"View a particular vehicle by VIN.", "vehicle", "Query", true}, + // Strip-only openers ("Filter for ...") keep novel remainders. + {"Filter for vehicles produced by a manufacturer.", "manufacturerTokenId", "VehiclesFilter", false}, } for _, tt := range tests { t.Run(tt.desc+"_"+tt.field, func(t *testing.T) { @@ -349,6 +367,23 @@ func TestIsSelfEvidentDescription(t *testing.T) { } } +func TestStripBoilerplatePrefix(t *testing.T) { + tests := []struct{ in, want string }{ + {"View a particular vehicle.", "vehicle."}, + {"Retrieve a particular template.", "template."}, + {"Filter for vehicles owned by this address.", "vehicles owned by this address."}, + {"Filters the DCNs based on the specified criteria.", "DCNs based on the specified criteria."}, + {"List minted vehicles.", "vehicles."}, + {"No prefix here.", "No prefix here."}, + {"", ""}, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + assert.Equal(t, tt.want, stripBoilerplatePrefix(tt.in)) + }) + } +} + func TestAnalyzeSchemaEdgeDetection(t *testing.T) { schema, err := loadGraphQLSchema([]string{"testdata/edge_connection.graphqls"}) require.NoError(t, err) diff --git a/pkg/mcpserver/condensed_schema_test.go b/pkg/mcpserver/condensed_schema_test.go index 9642595..6e9c795 100644 --- a/pkg/mcpserver/condensed_schema_test.go +++ b/pkg/mcpserver/condensed_schema_test.go @@ -14,7 +14,7 @@ import ( func TestWithCondensedSchema(t *testing.T) { condensedSDL := "type Query {\n vehicle(tokenId: Int!): Vehicle\n}\n\ntype Vehicle {\n tokenId: Int!\n owner: String!\n}\n" - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test Server", "0.1.0", "test", WithCondensedSchema(condensedSDL)) + mcpHandler, err := New(mockGQLExecutor(), "Test Server", "0.1.0", "test", WithCondensedSchema(condensedSDL)) require.NoError(t, err) ts := httptest.NewServer(mcpHandler) @@ -56,43 +56,8 @@ func TestWithCondensedSchema(t *testing.T) { assert.NotContains(t, textContent.Text, "__schema", "condensed schema should not contain introspection data") } -func TestWithoutCondensedSchemaFallback(t *testing.T) { - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test Server", "0.1.0", "test") - require.NoError(t, err) - - ts := httptest.NewServer(mcpHandler) - defer ts.Close() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - transport := &mcp.StreamableClientTransport{ - Endpoint: ts.URL, - HTTPClient: ts.Client(), - } - - client := mcp.NewClient(&mcp.Implementation{ - Name: "test-client", - Version: "1.0", - }, nil) - - session, err := client.Connect(ctx, transport, nil) - require.NoError(t, err) - defer func() { _ = session.Close() }() - - schemaResult, err := session.CallTool(ctx, &mcp.CallToolParams{ - Name: "test_get_schema", - }) - require.NoError(t, err) - require.NotNil(t, schemaResult) - require.Len(t, schemaResult.Content, 1) - - contentJSON, err := json.Marshal(schemaResult.Content[0]) - require.NoError(t, err) - var textContent struct { - Text string `json:"text"` - } - require.NoError(t, json.Unmarshal(contentJSON, &textContent)) - - assert.Contains(t, textContent.Text, "__schema", "without condensed schema, should return introspection JSON") +func TestNewWithoutCondensedSchemaErrors(t *testing.T) { + _, err := New(mockGQLExecutor(), "Test Server", "0.1.0", "test") + require.Error(t, err) + assert.Contains(t, err.Error(), "WithCondensedSchema is required") } diff --git a/pkg/mcpserver/executor.go b/pkg/mcpserver/executor.go deleted file mode 100644 index 99a2f7c..0000000 --- a/pkg/mcpserver/executor.go +++ /dev/null @@ -1,106 +0,0 @@ -package mcpserver - -import ( - "context" - "fmt" -) - -// loadSchema runs the introspection query and returns the schema JSON. -func loadSchema(ctx context.Context, exec GraphQLExecutor) (string, error) { - result, err := exec.Execute(ctx, introspectionQuery, nil) - if err != nil { - return "", fmt.Errorf("introspection query: %w", err) - } - return string(result), nil -} - -// introspectionQuery is the full introspection query used to fetch the schema. -const introspectionQuery = ` -query IntrospectionQuery { - __schema { - queryType { name } - mutationType { name } - subscriptionType { name } - types { - ...FullType - } - directives { - name - description - locations - args { - ...InputValue - } - } - } -} - -fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } -} - -fragment InputValue on __InputValue { - name - description - type { ...TypeRef } - defaultValue -} - -fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } -} -` diff --git a/pkg/mcpserver/mcpserver.go b/pkg/mcpserver/mcpserver.go index b7e38d8..749a0e7 100644 --- a/pkg/mcpserver/mcpserver.go +++ b/pkg/mcpserver/mcpserver.go @@ -3,7 +3,6 @@ package mcpserver import ( "context" "errors" - "fmt" "log/slog" "net/http" "strings" @@ -92,7 +91,8 @@ func WithLogger(logger *slog.Logger) Option { // New creates an http.Handler that serves an MCP Streamable HTTP server. // It wraps a GraphQL executor and exposes registered tools as MCP tools. -func New(ctx context.Context, exec GraphQLExecutor, serverName string, version string, toolPrefix string, opts ...Option) (http.Handler, error) { +// A condensed SDL must be supplied via WithCondensedSchema. +func New(exec GraphQLExecutor, serverName string, version string, toolPrefix string, opts ...Option) (http.Handler, error) { if exec == nil { return nil, errors.New("mcpserver: executor must not be nil") } @@ -111,6 +111,10 @@ func New(ctx context.Context, exec GraphQLExecutor, serverName string, version s opt(cfg) } + if cfg.condensedSchema == "" { + return nil, errors.New("mcpserver: WithCondensedSchema is required") + } + server := mcp.NewServer(&mcp.Implementation{ Name: serverName, Version: version, @@ -118,17 +122,7 @@ func New(ctx context.Context, exec GraphQLExecutor, serverName string, version s Logger: cfg.logger, }) - // Load the schema eagerly so initialization fails fast if the executor is broken. - schema, err := loadSchema(ctx, exec) - if err != nil { - return nil, fmt.Errorf("mcpserver: schema introspection failed: %w", err) - } - - schemaContent := schema - if cfg.condensedSchema != "" { - schemaContent = cfg.condensedSchema - } - registerBuiltinTools(server, exec, schemaContent, toolPrefix, cfg.maxQuerySize, cfg.logger) + registerBuiltinTools(server, exec, cfg.condensedSchema, toolPrefix, cfg.maxQuerySize, cfg.logger) registerShortcutTools(server, exec, cfg.tools, cfg.logger) handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { diff --git a/pkg/mcpserver/mcpserver_test.go b/pkg/mcpserver/mcpserver_test.go index e2a6169..2819c2a 100644 --- a/pkg/mcpserver/mcpserver_test.go +++ b/pkg/mcpserver/mcpserver_test.go @@ -72,40 +72,10 @@ func TestExecutorError(t *testing.T) { assert.Contains(t, err.Error(), "execution failed") } -func TestLoadSchema(t *testing.T) { - exec := &mockExecutor{ - fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { - if strings.Contains(query, "__schema") { - return []byte(`{"data":{"__schema":{"types":[{"name":"Query"}]}}}`), nil - } - return nil, fmt.Errorf("unexpected query") - }, - } - - schema, err := loadSchema(context.Background(), exec) - require.NoError(t, err) - assert.Contains(t, schema, "__schema") -} - -func TestLoadSchemaError(t *testing.T) { - exec := &mockExecutor{ - fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { - return nil, fmt.Errorf("connection refused") - }, - } - - _, err := loadSchema(context.Background(), exec) - require.Error(t, err) - assert.Contains(t, err.Error(), "introspection query") -} - -// mockGQLExecutor returns a GraphQLExecutor that handles introspection and echoes regular queries. +// mockGQLExecutor returns a GraphQLExecutor that echoes regular queries back as JSON. func mockGQLExecutor() GraphQLExecutor { return &mockExecutor{ fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { - if strings.Contains(query, "__schema") { - return []byte(`{"data":{"__schema":{"queryType":{"name":"Query"},"types":[{"name":"Query"},{"name":"Vehicle"}]}}}`), nil - } resp := map[string]any{ "data": map[string]any{ "echoQuery": query, @@ -117,6 +87,9 @@ func mockGQLExecutor() GraphQLExecutor { } } +// testCondensedSchema is a minimal condensed SDL used by New(...) calls in tests. +const testCondensedSchema = "type Query {\n _empty: String\n}\n" + func TestShortcutTool(t *testing.T) { toolDef := ToolDefinition{ Name: "get_vehicle", @@ -190,7 +163,7 @@ func TestMCPHandlerEndToEnd(t *testing.T) { Query: `query GetVehicle($tokenId: Int!) { vehicle(tokenId: $tokenId) { id } }`, } - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test Server", "0.1.0", "test", WithTools([]ToolDefinition{shortcutTool})) + mcpHandler, err := New(mockGQLExecutor(), "Test Server", "0.1.0", "test", WithCondensedSchema(testCondensedSchema), WithTools([]ToolDefinition{shortcutTool})) require.NoError(t, err) ts := httptest.NewServer(mcpHandler) @@ -241,7 +214,7 @@ func TestMCPHandlerEndToEnd(t *testing.T) { } require.NoError(t, json.Unmarshal(contentJSON, &textContent)) assert.Equal(t, "text", textContent.Type) - assert.Contains(t, textContent.Text, "__schema") + assert.Contains(t, textContent.Text, "type Query") queryResult, err := session.CallTool(ctx, &mcp.CallToolParams{ Name: "test_query", @@ -255,7 +228,7 @@ func TestMCPHandlerEndToEnd(t *testing.T) { } func TestToolPrefixing(t *testing.T) { - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Prefixed Server", "0.1.0", "myprefix") + mcpHandler, err := New(mockGQLExecutor(), "Prefixed Server", "0.1.0", "myprefix", WithCondensedSchema(testCondensedSchema)) require.NoError(t, err) ts := httptest.NewServer(mcpHandler) @@ -294,19 +267,19 @@ func TestToolPrefixing(t *testing.T) { } func TestEmptyPrefixRejected(t *testing.T) { - _, err := New(context.Background(), mockGQLExecutor(), "Test Server", "0.1.0", "") + _, err := New(mockGQLExecutor(), "Test Server", "0.1.0", "", WithCondensedSchema(testCondensedSchema)) require.Error(t, err) assert.Contains(t, err.Error(), "toolPrefix must be non-empty") } func TestEmptyServerNameRejected(t *testing.T) { - _, err := New(context.Background(), mockGQLExecutor(), "", "0.1.0", "test") + _, err := New(mockGQLExecutor(), "", "0.1.0", "test", WithCondensedSchema(testCondensedSchema)) require.Error(t, err) assert.Contains(t, err.Error(), "serverName must be non-empty") } func TestNilExecutorRejected(t *testing.T) { - _, err := New(context.Background(), nil, "Test Server", "0.1.0", "test") + _, err := New(nil, "Test Server", "0.1.0", "test", WithCondensedSchema(testCondensedSchema)) require.Error(t, err) assert.Contains(t, err.Error(), "executor must not be nil") } @@ -314,9 +287,6 @@ func TestNilExecutorRejected(t *testing.T) { func TestQueryToolErrorReturnsIsError(t *testing.T) { exec := &mockExecutor{ fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { - if strings.Contains(query, "__schema") { - return []byte(`{"data":{"__schema":{"queryType":{"name":"Query"}}}}`), nil - } return nil, fmt.Errorf("field not found: badField") }, } @@ -370,7 +340,7 @@ func ptr[T any](v T) *T { return &v } func TestBuiltinToolAnnotations(t *testing.T) { exec := mockGQLExecutor() - mcpHandler, err := New(context.Background(), exec, "Test Server", "0.1.0", "test") + mcpHandler, err := New(exec, "Test Server", "0.1.0", "test", WithCondensedSchema(testCondensedSchema)) require.NoError(t, err) ts := httptest.NewServer(mcpHandler) @@ -424,7 +394,7 @@ func TestShortcutToolAnnotations(t *testing.T) { }, } - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", WithTools([]ToolDefinition{tool})) + mcpHandler, err := New(mockGQLExecutor(), "Test", "0.1.0", "test", WithCondensedSchema(testCondensedSchema), WithTools([]ToolDefinition{tool})) require.NoError(t, err) ts := httptest.NewServer(mcpHandler) @@ -552,7 +522,7 @@ func TestTokenVerifierAccepts(t *testing.T) { return ctx, fmt.Errorf("invalid token") } - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", + mcpHandler, err := New(mockGQLExecutor(), "Test", "0.1.0", "test", WithCondensedSchema(testCondensedSchema), WithTokenVerifier(verifier), ) require.NoError(t, err) @@ -585,7 +555,7 @@ func TestTokenVerifierRejects(t *testing.T) { return ctx, fmt.Errorf("invalid") } - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", + mcpHandler, err := New(mockGQLExecutor(), "Test", "0.1.0", "test", WithCondensedSchema(testCondensedSchema), WithTokenVerifier(verifier), ) require.NoError(t, err) @@ -604,7 +574,7 @@ func TestTokenVerifierMissingHeader(t *testing.T) { return ctx, nil } - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", + mcpHandler, err := New(mockGQLExecutor(), "Test", "0.1.0", "test", WithCondensedSchema(testCondensedSchema), WithTokenVerifier(verifier), ) require.NoError(t, err) @@ -727,9 +697,6 @@ func TestTokenVerifierContextPropagation(t *testing.T) { exec := &mockExecutor{ fn: func(ctx context.Context, query string, variables map[string]any) ([]byte, error) { - if strings.Contains(query, "__schema") { - return []byte(`{"data":{"__schema":{"queryType":{"name":"Query"}}}}`), nil - } val, ok := ctx.Value(authCtxKey{}).(string) if !ok || val != "user-from-token" { return nil, fmt.Errorf("expected context value 'user-from-token', got %v", ctx.Value(authCtxKey{})) @@ -738,7 +705,7 @@ func TestTokenVerifierContextPropagation(t *testing.T) { }, } - mcpHandler, err := New(context.Background(), exec, "Test", "0.1.0", "test", + mcpHandler, err := New(exec, "Test", "0.1.0", "test", WithCondensedSchema(testCondensedSchema), WithTokenVerifier(verifier), ) require.NoError(t, err) @@ -772,7 +739,7 @@ func TestWithLoggerOption(t *testing.T) { handler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}) logger := slog.New(handler) - mcpHandler, err := New(context.Background(), mockGQLExecutor(), "Test", "0.1.0", "test", + mcpHandler, err := New(mockGQLExecutor(), "Test", "0.1.0", "test", WithCondensedSchema(testCondensedSchema), WithLogger(logger), ) require.NoError(t, err) diff --git a/pkg/mcpserver/tools.go b/pkg/mcpserver/tools.go index 1100b6b..df859ed 100644 --- a/pkg/mcpserver/tools.go +++ b/pkg/mcpserver/tools.go @@ -82,14 +82,6 @@ func registerBuiltinTools(server *mcp.Server, exec GraphQLExecutor, cachedSchema toolCallsTotal.WithLabelValues(schemaToolName, "success").Inc() toolDurationSeconds.WithLabelValues(schemaToolName).Observe(d.Seconds()) }() - if cachedSchema == "" { - return &mcp.CallToolResult{ - Content: []mcp.Content{ - &mcp.TextContent{Text: "schema is unavailable"}, - }, - IsError: true, - }, nil, nil - } return &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.TextContent{Text: cachedSchema},