From 6565d0dde98d92fb52237403bac123aea41ea41d Mon Sep 17 00:00:00 2001 From: Amit <3540115+amitdeshmukh@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:55:00 +0530 Subject: [PATCH 1/2] feat(mcp): teach FK auto-traversal in find_path + metric_by_dimension GraphJin auto-traverses any single FK chain between two tables, so the collapsed shape `{ { { sum(...) } } }` works even when the underlying join is N hops. Surface this fact in the contract instead of making every consumer (ax-crew, custom MCP clients, the CLI) repeat it in their actor descriptions. - get_query_syntax.patterns.metric_by_dimension gains an `auto_traversal_note` that explains the collapsed form and tells agents to call find_path first to confirm a single FK path exists. - find_path now emits `collapsed_example_query`, `collapsed_example_query_compiles`, an optional repair warning, and a `collapsed_note` whenever the path has intermediates. Both example queries run through validateExampleQuery so the response proves the auto-traversal compiles on this schema rather than promising it. CLI (`graphjin cli schema path`) inherits the new fields automatically since it's a thin runToolCmd wrapper; MCP/CLI parity tests stay green. Co-Authored-By: Claude Opus 4.7 (1M context) --- serv/mcp_contract_test.go | 92 ++++++++++++++++++++++++++++++++++++++ serv/mcp_query_patterns.go | 22 ++++----- serv/mcp_schema.go | 63 +++++++++++++++++++++----- 3 files changed, 155 insertions(+), 22 deletions(-) diff --git a/serv/mcp_contract_test.go b/serv/mcp_contract_test.go index 8c0b41a7..0b6b3db9 100644 --- a/serv/mcp_contract_test.go +++ b/serv/mcp_contract_test.go @@ -575,6 +575,85 @@ func TestValidateExampleQuery_AmbiguousFK(t *testing.T) { } } +// TestHandleFindPath_CollapsedExample verifies that find_path emits a +// collapsed example query (`{ { } }`) alongside the full +// nested example when the path has intermediates. The collapsed shape +// is what an analyst actually wants for per-dimension aggregations — +// GraphJin auto-traverses the FK chain. Teaching this through the +// contract lets every consumer (MCP, CLI) get the lesson for free. +func TestHandleFindPath_CollapsedExample(t *testing.T) { + ms := newSQLiteMCPServerWithSchema(t, []string{ + `CREATE TABLE category (catid INTEGER PRIMARY KEY, label TEXT)`, + `CREATE TABLE product (pid INTEGER PRIMARY KEY, catid INTEGER REFERENCES category(catid), title TEXT)`, + `CREATE TABLE orderitem (oiid INTEGER PRIMARY KEY, pid INTEGER REFERENCES product(pid), amt NUMERIC)`, + }) + + req := newToolRequest(map[string]any{ + "from_table": "category", + "to_table": "orderitem", + }) + result, err := ms.handleFindPath(context.Background(), req) + if err != nil { + t.Fatalf("handleFindPath: %v", err) + } + out := assertToolStructuredMap(t, result) + + pathRaw, _ := out["path"].([]any) + if len(pathRaw) < 2 { + t.Fatalf("expected a 2+ hop path between category and orderitem, got %d hops: %+v", len(pathRaw), out) + } + + collapsed, _ := out["collapsed_example_query"].(string) + if collapsed == "" { + t.Fatalf("expected collapsed_example_query when path has intermediates; got: %+v", out) + } + if !strings.Contains(collapsed, "category") || !strings.Contains(collapsed, "orderitem") { + t.Errorf("collapsed query must nest category and orderitem directly; got: %s", collapsed) + } + if strings.Contains(collapsed, "product") { + t.Errorf("collapsed query must NOT name the intermediate `product`; got: %s", collapsed) + } + + compiles, _ := out["collapsed_example_query_compiles"].(bool) + if !compiles { + t.Errorf("collapsed example must compile via auto-traversal; got warning: %v", out["collapsed_example_query_warning"]) + } + + note, _ := out["collapsed_note"].(string) + if note == "" { + t.Errorf("collapsed_note must be populated when collapsed query is emitted") + } else if !strings.Contains(stringToLower(note), "auto-travers") { + t.Errorf("collapsed_note should mention auto-traversal; got: %q", note) + } +} + +// TestHandleFindPath_DirectRelationship verifies that find_path does NOT +// emit a collapsed query when the path is a single hop — the full and +// collapsed forms would be identical. +func TestHandleFindPath_DirectRelationship(t *testing.T) { + ms := newSQLiteMCPServerWithSchema(t, []string{ + `CREATE TABLE users (uid INTEGER PRIMARY KEY, label TEXT)`, + `CREATE TABLE orders (oid INTEGER PRIMARY KEY, uid INTEGER REFERENCES users(uid), amt NUMERIC)`, + }) + + req := newToolRequest(map[string]any{ + "from_table": "users", + "to_table": "orders", + }) + result, err := ms.handleFindPath(context.Background(), req) + if err != nil { + t.Fatalf("handleFindPath: %v", err) + } + out := assertToolStructuredMap(t, result) + + if collapsed, ok := out["collapsed_example_query"]; ok && collapsed != "" { + t.Errorf("collapsed_example_query should be omitted for single-hop paths; got: %v", collapsed) + } + if note, ok := out["collapsed_note"]; ok && note != "" { + t.Errorf("collapsed_note should be omitted for single-hop paths; got: %v", note) + } +} + func newSQLiteReadyMCPServer(t *testing.T, queries map[string]string, queryVars map[string]string, fragments ...map[string]string) *mcpServer { t.Helper() @@ -901,6 +980,19 @@ func TestCanonicalQueryPatterns(t *testing.T) { if mbd.WrongExample == "" || mbd.WrongReason == "" { t.Errorf("metric_by_dimension must include WrongExample and WrongReason (load-bearing per P3)") } + // AutoTraversalNote teaches the collapsed `{ { } }` + // shape — this is what makes per-dimension aggregations clean even + // when dim and fact are not directly FK-linked. Must mention both + // auto-traversal and find_path so agents know to verify the path. + if mbd.AutoTraversalNote == "" { + t.Errorf("metric_by_dimension must include AutoTraversalNote so agents learn the collapsed shape") + } else { + for _, marker := range []string{"auto-travers", "find_path"} { + if !strings.Contains(stringToLower(mbd.AutoTraversalNote), stringToLower(marker)) { + t.Errorf("metric_by_dimension.AutoTraversalNote should mention %q; got: %q", marker, mbd.AutoTraversalNote) + } + } + } // Patterns must use placeholder column names like , // , . Literal 'id' / 'name' tokens diff --git a/serv/mcp_query_patterns.go b/serv/mcp_query_patterns.go index 66c7e302..e3041e36 100644 --- a/serv/mcp_query_patterns.go +++ b/serv/mcp_query_patterns.go @@ -3,14 +3,15 @@ package serv // Three canonical query shapes; Wrong/Right contrast is load-bearing for small-model pattern matching. type QueryPattern struct { - Name string `json:"name"` - Title string `json:"title"` - Question string `json:"question"` - Rule string `json:"rule"` - Why string `json:"why"` - WrongExample string `json:"wrong_example,omitempty"` - WrongReason string `json:"wrong_reason,omitempty"` - RightExample string `json:"right_example"` + Name string `json:"name"` + Title string `json:"title"` + Question string `json:"question"` + Rule string `json:"rule"` + Why string `json:"why"` + WrongExample string `json:"wrong_example,omitempty"` + WrongReason string `json:"wrong_reason,omitempty"` + RightExample string `json:"right_example"` + AutoTraversalNote string `json:"auto_traversal_note,omitempty"` } func canonicalQueryPatterns() []QueryPattern { @@ -26,7 +27,7 @@ func canonicalQueryPatterns() []QueryPattern { sum_ } }`, - WrongReason: "distinct on a fact-table FK dedupes rows but does not produce per-dimension aggregates. The compiler will reject this when the nested join references a column outside the distinct list.", + WrongReason: "distinct on a fact-table FK dedupes rows but does not produce per-dimension aggregates. The compiler will reject this when the nested join references a column outside the distinct list.", RightExample: `query { { @@ -36,6 +37,7 @@ func canonicalQueryPatterns() []QueryPattern { } } }`, + AutoTraversalNote: "When the dimension and fact tables are not directly related, you can still nest them directly — GraphJin auto-traverses any single FK path between them. For example, `{ productcategory { salesorderdetail { sum_amt: sum() } } }` works even if the underlying chain is productcategory → productsubcategory → product → specialofferproduct → salesorderdetail. Use this collapsed form for clean per-dimension aggregates; call find_path first to confirm a single FK path exists.", }, { Name: "time_series", @@ -65,7 +67,7 @@ func canonicalQueryPatterns() []QueryPattern { } # Nesting would fail because its FK is NOT in # distinct. Only nest joins whose FK is the same column listed in distinct.`, - WrongReason: "Nesting through a non-distinct FK column is rejected (a column dropped by GROUP BY cannot be a join key). Only nest joins whose FK column is itself in distinct.", + WrongReason: "Nesting through a non-distinct FK column is rejected (a column dropped by GROUP BY cannot be a join key). Only nest joins whose FK column is itself in distinct.", RightExample: `query { (distinct: [], order_by: { : desc }, limit: 10) { diff --git a/serv/mcp_schema.go b/serv/mcp_schema.go index d14eb8a5..4b4d18bb 100644 --- a/serv/mcp_schema.go +++ b/serv/mcp_schema.go @@ -107,10 +107,14 @@ func (ms *mcpServer) registerSchemaTools() { mcp.Description("Optional database name. Omit to search all databases."), ), mcp.WithOutputSchema[struct { - Path []core.PathStep `json:"path"` - ExampleQuery string `json:"example_query"` - ExampleQueryCompiles bool `json:"example_query_compiles"` - ExampleQueryWarning *FixQueryErrorResult `json:"example_query_warning,omitempty"` + Path []core.PathStep `json:"path"` + ExampleQuery string `json:"example_query"` + ExampleQueryCompiles bool `json:"example_query_compiles"` + ExampleQueryWarning *FixQueryErrorResult `json:"example_query_warning,omitempty"` + CollapsedExampleQuery string `json:"collapsed_example_query,omitempty"` + CollapsedExampleQueryCompiles bool `json:"collapsed_example_query_compiles,omitempty"` + CollapsedExampleQueryWarning *FixQueryErrorResult `json:"collapsed_example_query_warning,omitempty"` + CollapsedNote string `json:"collapsed_note,omitempty"` }](), ), ms.handleFindPath) @@ -489,16 +493,51 @@ func (ms *mcpServer) handleFindPath(ctx context.Context, req mcp.CallToolRequest exampleQuery := generatePathExampleQuery(fromTable, path, ms.resolvePKColumn) compiles, warning := ms.validateExampleQuery(exampleQuery) + // When the path has intermediates, also emit a collapsed form that + // nests `from` directly into the final `to`. GraphJin auto-traverses + // any single FK path between two tables, so this shape compiles and + // is what an analyst actually wants for per-dimension aggregations. + // Validating both proves the auto-traversal works on this schema. + var ( + collapsedQuery string + collapsedCompiles bool + collapsedWarning *FixQueryErrorResult + collapsedNote string + ) + if len(path) >= 2 { + toTable := path[len(path)-1].To + collapsedQuery = generatePathExampleQuery(fromTable, + []core.PathStep{{To: toTable}}, ms.resolvePKColumn) + collapsedCompiles, collapsedWarning = ms.validateExampleQuery(collapsedQuery) + if collapsedCompiles { + collapsedNote = "GraphJin auto-traverses the multi-hop FK path; you can nest `" + + fromTable + "` and `" + toTable + "` directly. Use this collapsed form for " + + "per-dimension aggregations (see get_query_syntax.patterns.metric_by_dimension)." + } else { + collapsedNote = "Auto-traversal between `" + fromTable + "` and `" + path[len(path)-1].To + + "` did not compile on this schema (see collapsed_example_query_warning); " + + "use the full nested example_query instead." + } + } + result := struct { - Path []core.PathStep `json:"path"` - ExampleQuery string `json:"example_query"` - ExampleQueryCompiles bool `json:"example_query_compiles"` - ExampleQueryWarning *FixQueryErrorResult `json:"example_query_warning,omitempty"` + Path []core.PathStep `json:"path"` + ExampleQuery string `json:"example_query"` + ExampleQueryCompiles bool `json:"example_query_compiles"` + ExampleQueryWarning *FixQueryErrorResult `json:"example_query_warning,omitempty"` + CollapsedExampleQuery string `json:"collapsed_example_query,omitempty"` + CollapsedExampleQueryCompiles bool `json:"collapsed_example_query_compiles,omitempty"` + CollapsedExampleQueryWarning *FixQueryErrorResult `json:"collapsed_example_query_warning,omitempty"` + CollapsedNote string `json:"collapsed_note,omitempty"` }{ - Path: path, - ExampleQuery: exampleQuery, - ExampleQueryCompiles: compiles, - ExampleQueryWarning: warning, + Path: path, + ExampleQuery: exampleQuery, + ExampleQueryCompiles: compiles, + ExampleQueryWarning: warning, + CollapsedExampleQuery: collapsedQuery, + CollapsedExampleQueryCompiles: collapsedCompiles, + CollapsedExampleQueryWarning: collapsedWarning, + CollapsedNote: collapsedNote, } return ms.toolResultJSON("find_path", args, result) } From 6ef6f7e6bf64e2e5a8d5465d93aac88177089dfa Mon Sep 17 00:00:00 2001 From: Amit <3540115+amitdeshmukh@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:08:19 +0530 Subject: [PATCH 2/2] feat(mcp): wrong_dialect arm + JS-runtime MCP wrapper note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups from the same multi-model agent test, reduced to the items that survive the framework-generic / "teach DSL via contract" filter: - fix_query_error gains a `wrong_dialect` arm that catches the two Hasura aggregate-leak shapes: the `aggregation:` argument (`unknown argument 'aggregation'`) and the `_aggregate` field suffix (the source query carries the suffix and the error reports the table missing). Repair shows GraphJin's leaf-field aggregate shape — sum_/ sum(expr: { mul: [...] }) — with placeholder column names so it stays generic across schemas. - get_js_runtime_api adds one Notes entry distinguishing in-runtime results (gj.tools.* returns decoded objects) from direct MCP-caller results (use result.structuredContent or JSON.parse(result.content[0].text)). One sentence at the wrapper layer beats per-tool noise on every tool description. Skipped from the same feedback round: per-table aggregate suggestions and pre-substituted columns in aggregations.usage. Both cross from teach-DSL into guess-business-logic / schema-shape inference, which collides with the framework-generic rule. Co-Authored-By: Claude Opus 4.7 (1M context) --- serv/mcp_contract_test.go | 35 +++++++++++++---------- serv/mcp_fix_query_error.go | 55 ++++++++++++++++++++++++++++++++++--- serv/mcp_js_runtime.go | 1 + serv/mcp_schema.go | 6 +--- 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/serv/mcp_contract_test.go b/serv/mcp_contract_test.go index 0b6b3db9..f9005006 100644 --- a/serv/mcp_contract_test.go +++ b/serv/mcp_contract_test.go @@ -575,12 +575,6 @@ func TestValidateExampleQuery_AmbiguousFK(t *testing.T) { } } -// TestHandleFindPath_CollapsedExample verifies that find_path emits a -// collapsed example query (`{ { } }`) alongside the full -// nested example when the path has intermediates. The collapsed shape -// is what an analyst actually wants for per-dimension aggregations — -// GraphJin auto-traverses the FK chain. Teaching this through the -// contract lets every consumer (MCP, CLI) get the lesson for free. func TestHandleFindPath_CollapsedExample(t *testing.T) { ms := newSQLiteMCPServerWithSchema(t, []string{ `CREATE TABLE category (catid INTEGER PRIMARY KEY, label TEXT)`, @@ -627,9 +621,6 @@ func TestHandleFindPath_CollapsedExample(t *testing.T) { } } -// TestHandleFindPath_DirectRelationship verifies that find_path does NOT -// emit a collapsed query when the path is a single hop — the full and -// collapsed forms would be identical. func TestHandleFindPath_DirectRelationship(t *testing.T) { ms := newSQLiteMCPServerWithSchema(t, []string{ `CREATE TABLE users (uid INTEGER PRIMARY KEY, label TEXT)`, @@ -748,6 +739,7 @@ func TestBuildFixQueryErrorRepair_Arms(t *testing.T) { cases := []struct { name string errorMsg string + query string // optional; defaults to "query { foo { bar } }" wantKind string wantInRepair []string wantTools []string @@ -798,6 +790,21 @@ func TestBuildFixQueryErrorRepair_Arms(t *testing.T) { wantInRepair: []string{"", ""}, wantTools: []string{"describe_table", "get_query_syntax"}, }, + { + name: "wrong_dialect_argument", + errorMsg: `unknown argument 'aggregation' on field 'orders'`, + wantKind: fixKindWrongDialect, + wantInRepair: []string{"sum(expr:", "sum_", "count_"}, + wantTools: []string{"get_query_syntax", "describe_table"}, + }, + { + name: "wrong_dialect_aggregate_suffix", + errorMsg: `table not found: orders_aggregate`, + query: `query { orders_aggregate { aggregate { count } } }`, + wantKind: fixKindWrongDialect, + wantInRepair: []string{"orders", "sum(expr:", "_aggregate"}, + wantTools: []string{"get_query_syntax"}, + }, { name: "generic_fallback", errorMsg: `something completely unexpected happened`, @@ -807,7 +814,11 @@ func TestBuildFixQueryErrorRepair_Arms(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - res := buildFixQueryErrorRepair("query { foo { bar } }", tc.errorMsg, false) + query := tc.query + if query == "" { + query = "query { foo { bar } }" + } + res := buildFixQueryErrorRepair(query, tc.errorMsg, false) if res.Kind != tc.wantKind { t.Fatalf("kind: got %q want %q", res.Kind, tc.wantKind) } @@ -980,10 +991,6 @@ func TestCanonicalQueryPatterns(t *testing.T) { if mbd.WrongExample == "" || mbd.WrongReason == "" { t.Errorf("metric_by_dimension must include WrongExample and WrongReason (load-bearing per P3)") } - // AutoTraversalNote teaches the collapsed `{ { } }` - // shape — this is what makes per-dimension aggregations clean even - // when dim and fact are not directly FK-linked. Must mention both - // auto-traversal and find_path so agents know to verify the path. if mbd.AutoTraversalNote == "" { t.Errorf("metric_by_dimension must include AutoTraversalNote so agents learn the collapsed shape") } else { diff --git a/serv/mcp_fix_query_error.go b/serv/mcp_fix_query_error.go index cccc4157..e9792684 100644 --- a/serv/mcp_fix_query_error.go +++ b/serv/mcp_fix_query_error.go @@ -24,6 +24,7 @@ const ( fixKindTableNotFound = "table_not_found" fixKindColumnNotFound = "column_not_found" fixKindFieldNotOnTable = "field_not_on_table" + fixKindWrongDialect = "wrong_dialect" fixKindOperatorInvalid = "operator_or_syntax_invalid" fixKindSyntaxParse = "syntax_or_parse_error" fixKindPermission = "permission_denied" @@ -33,10 +34,12 @@ const ( ) var ( - reAmbiguousRel = regexp.MustCompile(`ambiguous relationship\s+(\S+)\s*->\s*(\S+):\s*multiple foreign keys\s*\(([^)]+)\)`) - reNestedShape = regexp.MustCompile(`nested selection '([^']+)' joins through parent column '([^']+)\.([^']+)', which is not in distinct: \[([^\]]+)\]`) - rePartitionReq = regexp.MustCompile(`table\s+"([^"]+)"\s+requires a filter on (?:partition|temporal) column\s+"([^"]+)"`) - reFieldNotOnTable = regexp.MustCompile(`field '([^']+)' is not a column or a function`) + reAmbiguousRel = regexp.MustCompile(`ambiguous relationship\s+(\S+)\s*->\s*(\S+):\s*multiple foreign keys\s*\(([^)]+)\)`) + reNestedShape = regexp.MustCompile(`nested selection '([^']+)' joins through parent column '([^']+)\.([^']+)', which is not in distinct: \[([^\]]+)\]`) + rePartitionReq = regexp.MustCompile(`table\s+"([^"]+)"\s+requires a filter on (?:partition|temporal) column\s+"([^"]+)"`) + reFieldNotOnTable = regexp.MustCompile(`field '([^']+)' is not a column or a function`) + reWrongDialectArg = regexp.MustCompile(`unknown argument\s+['"` + "`" + `]?(aggregation|aggregate)['"` + "`" + `]?`) + reWrongDialectField = regexp.MustCompile(`(?i)([a-z0-9_]+)_aggregate\b`) ) // buildFixQueryErrorRepair classifies a failing query+error and returns structured repair guidance. @@ -53,6 +56,8 @@ func buildFixQueryErrorRepair(query, errorMsg string, analyticsMode bool) FixQue fillPartitionFilterArm(&res, errorMsg) case reFieldNotOnTable.MatchString(errorMsg): fillFieldNotOnTableArm(&res, errorMsg) + case isWrongDialectError(errorMsg, query): + fillWrongDialectArm(&res, errorMsg, query) case strings.Contains(errLower, "relationship not found"): fillUnknownRelArm(&res, errorMsg) case strings.Contains(errLower, "table") && (strings.Contains(errLower, "not found") || strings.Contains(errLower, "unknown")): @@ -187,6 +192,48 @@ query { }`, field) } +// isWrongDialectError flags Hasura-style aggregate leakage: either the `aggregation:` argument or a `
_aggregate` field suffix in the source query. +func isWrongDialectError(errorMsg, query string) bool { + if reWrongDialectArg.MatchString(errorMsg) { + return true + } + errLower := strings.ToLower(errorMsg) + if !(strings.Contains(errLower, "table") && (strings.Contains(errLower, "not found") || strings.Contains(errLower, "unknown") || strings.Contains(errLower, "does not exist"))) { + return false + } + return reWrongDialectField.MatchString(query) +} + +func fillWrongDialectArm(res *FixQueryErrorResult, errorMsg, query string) { + res.Kind = fixKindWrongDialect + res.FollowUpTools = []string{"get_query_syntax", "describe_table", "get_table_sample"} + + tableHint := "
" + if m := reWrongDialectField.FindStringSubmatch(query); m != nil { + tableHint = m[1] + } + + if reWrongDialectArg.MatchString(errorMsg) { + res.Diagnosis = "Query used the Hasura/PostgREST `aggregation`/`aggregate` argument. GraphJin has no such argument — aggregates are leaf-level fields: `sum_`, `avg_`, `count_`, or `: sum(expr: { mul: [, ] })` for arithmetic. Call get_query_syntax for the full grammar." + } else { + res.Diagnosis = fmt.Sprintf( + "Query referenced `%s_aggregate` — the Hasura aggregate-table shape. GraphJin has no `_aggregate` suffix; aggregates live as leaf fields on the original table: `sum_`, `count_`, or `: sum(expr: { ... })` for arithmetic. Call get_query_syntax for the full grammar.", + tableHint) + } + + res.RepairedQuery = fmt.Sprintf( + `# Aggregates are leaf fields on %s — no _aggregate selection, no aggregation: argument. +query { + %s { + count_ + sum_ + avg_ + revenue: sum(expr: { mul: [, ] }) + } +}`, + tableHint, tableHint) +} + func fillUnknownRelArm(res *FixQueryErrorResult, errorMsg string) { res.Kind = fixKindUnknownRelationship res.Diagnosis = "GraphJin has no relationship between the named tables. Confirm the join path before retrying." diff --git a/serv/mcp_js_runtime.go b/serv/mcp_js_runtime.go index 878380e1..d9fc685b 100644 --- a/serv/mcp_js_runtime.go +++ b/serv/mcp_js_runtime.go @@ -125,6 +125,7 @@ func (ms *mcpServer) buildJSRuntimeAPI() JSRuntimeAPI { }, Notes: []string{ "IMPORTANT: All gj.tools.* functions return DECODED native JavaScript objects — ready to use directly.", + "Direct MCP callers (NOT inside gj.tools.*): tool results are MCP-wrapped — prefer `result.structuredContent` when present (populated for tools with output schemas); otherwise `JSON.parse(result.content[0].text)`. gj.tools.* unwraps this for you.", "Example: var result = gj.tools.executeGraphql({query: 'query GetOrders { orders { id total } }'}); var orders = result.data.orders;", "Example: var tables = gj.tools.listTables().tables;", "Example: var schema = gj.tools.describeTable({table: 'orders'});", diff --git a/serv/mcp_schema.go b/serv/mcp_schema.go index 4b4d18bb..bb47d253 100644 --- a/serv/mcp_schema.go +++ b/serv/mcp_schema.go @@ -493,11 +493,7 @@ func (ms *mcpServer) handleFindPath(ctx context.Context, req mcp.CallToolRequest exampleQuery := generatePathExampleQuery(fromTable, path, ms.resolvePKColumn) compiles, warning := ms.validateExampleQuery(exampleQuery) - // When the path has intermediates, also emit a collapsed form that - // nests `from` directly into the final `to`. GraphJin auto-traverses - // any single FK path between two tables, so this shape compiles and - // is what an analyst actually wants for per-dimension aggregations. - // Validating both proves the auto-traversal works on this schema. + // When the path has intermediates, emit a collapsed `{ { } }` shape that GraphJin auto-traverses. var ( collapsedQuery string collapsedCompiles bool