diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 181939a5..a4afd97a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -335,6 +335,18 @@ jobs: check-latest: true cache: true id: go + + - name: Check go sum coherence + run: | + cp go.sum go.sum.bkp + sumDiff="$(diff go.sum go.sum.bkp)" || true; + if [ "$sumDiff" = "" ]; then + echo "go.sum check passed" + else + echo "go.sum is inconsistent, might be time to run 'go mod tidy' and commit"; + exit 1; + fi + - name: Setup Python uses: actions/setup-python@v5.0.0 diff --git a/.gitignore b/.gitignore index 5a47f250..2400fdb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +# CI staging +go.sum.bkp + # Windows *.exe diff --git a/docs/diagrams/plantuml/hld-components.png b/docs/diagrams/plantuml/hld-components.png index 9f771254..c26a1683 100644 Binary files a/docs/diagrams/plantuml/hld-components.png and b/docs/diagrams/plantuml/hld-components.png differ diff --git a/docs/diagrams/plantuml/hld-components.puml b/docs/diagrams/plantuml/hld-components.puml index f561cc37..84965736 100644 --- a/docs/diagrams/plantuml/hld-components.puml +++ b/docs/diagrams/plantuml/hld-components.puml @@ -1,20 +1,29 @@ @startuml -node "Openapi-StackQL" as OpenapiStackQL -node "StackQL Parser" as StackQLParser +node "any-sdk" as AnySdk +node "stackql-parser" as StackQLParser node "psql-wire" as PsqlWire +node "stackql-provider-registry" as ProviderRegistry +node "stackql-go-sqlite3" as StackqlSqlite +node "postgres" as Postgres +node "postgres clients\neg: libpq" as PostgresClient +node "AI agents\neg: Claude Code" as AIAgent [Shell] ..> [Command Runner] [Exec] ..> [Command Runner] [Command Runner] ..> [Driver] +PostgresClient -> [Server] [Server] ..> [Driver] [Server] ..> [Wire Server] [Wire Server] ..> PsqlWire +[MCP Server] ..> [Driver] +AIAgent -> [MCP Server] [Driver] ..> [Query Submitter] [Query Submitter] ..> [Plan Builder] [Plan Builder] ..> [Initial Passes Screener Analyzer] : Mature the AST [Initial Passes Screener Analyzer] ..> [Initial Passes Screener Analyzer] : Nested Indirection [Initial Passes Screener Analyzer] ..> [Indirect] : Indirect Expansion +[Initial Passes Screener Analyzer] ..> ProviderRegistry [Plan Builder] ..> [Parser] [Indirect] ..> [Parser] [Parser] ..> StackQLParser @@ -22,9 +31,14 @@ node "psql-wire" as PsqlWire [Plan Builder] ..> [Primitive Builder] [Primitive Builder] ..> [Primitive Graph] [Plan Builder] ..> [Primitive Graph] -[Primitive Builder] ..> OpenapiStackQL +[Primitive Graph] ..> [Executor] +[Executor] ..> [Relational Algebra] +[Executor] ..> ProviderRegistry +[Relational Algebra] ..> StackqlSqlite +[Relational Algebra] -> Postgres +[Primitive Builder] ..> AnySdk [Route Pass] ..> [Parameter Router] -[Parameter Router] ..> OpenapiStackQL +[Parameter Router] ..> AnySdk [Route Pass] ..> [Route Pass] : Nesting / Composition diff --git a/pkg/mcp_server/server.go b/pkg/mcp_server/server.go index f6cff8e3..7c2b48a6 100644 --- a/pkg/mcp_server/server.go +++ b/pkg/mcp_server/server.go @@ -166,7 +166,7 @@ func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPSe server, &mcp.Tool{ Name: "query_v2", - Description: "Execute a SQL query. Please adhere to the expected parameters. Returns a textual response", + Description: "Deprecated: Please switch to query_v3. Execute a SQL query. Please adhere to the expected parameters. Returns a textual response", // Input and output schemas can be defined here if needed. }, func(ctx context.Context, req *mcp.CallToolRequest, arg dto.QueryInput) (*mcp.CallToolResult, any, error) { @@ -279,7 +279,7 @@ func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPSe server, &mcp.Tool{ Name: "prompt_write_safe_select_tool", - Description: "Prompt: guidelines for writing safe SELECT queries.", + Description: "PLACEHOLDER Future proofing: prompt guidelines for writing safe SELECT queries.", }, func(ctx context.Context, req *mcp.CallToolRequest, args dto.HierarchyInput) (*mcp.CallToolResult, any, error) { result, err := backend.PromptWriteSafeSelectTool(ctx, args) @@ -292,25 +292,6 @@ func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPSe }, ) - // mcp.AddTool( - // server, - // &mcp.Tool{ - // Name: "prompt_explain_plan_tips_tool", - // Description: "Prompt: tips for reading EXPLAIN ANALYZE output.", - // }, - // func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { - // result, err := backend.PromptExplainPlanTipsTool(ctx) - // if err != nil { - // return nil, nil, err - // } - // return &mcp.CallToolResult{ - // Content: []mcp.Content{ - // &mcp.TextContent{Text: result}, - // }, - // }, result, nil - // }, - // ) - mcp.AddTool( server, &mcp.Tool{ @@ -338,7 +319,7 @@ func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPSe server, &mcp.Tool{ Name: "list_tables_json_page", - Description: "List tables with pagination and filters, returns JSON.", + Description: "Future proofing: List tables with pagination and filters, returns JSON.", }, func(ctx context.Context, req *mcp.CallToolRequest, args dto.ListTablesPageInput) (*mcp.CallToolResult, any, error) { result, err := backend.ListTablesJSONPage(ctx, args) @@ -429,7 +410,7 @@ func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPSe server, &mcp.Tool{ Name: "describe_table", - Description: "Get detailed information about a table.", + Description: "PLACEHOLDER Future proofing: Get detailed information about a table.", }, func(ctx context.Context, req *mcp.CallToolRequest, args dto.HierarchyInput) (*mcp.CallToolResult, any, error) { result, err := backend.DescribeTable(ctx, args) @@ -446,7 +427,7 @@ func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPSe server, &mcp.Tool{ Name: "get_foreign_keys", - Description: "Get foreign key information for a table.", + Description: "PLACEHOLDER Future proofing: Get foreign key information for a table.", }, func(ctx context.Context, req *mcp.CallToolRequest, args dto.HierarchyInput) (*mcp.CallToolResult, any, error) { result, err := backend.GetForeignKeys(ctx, args) @@ -463,7 +444,7 @@ func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPSe server, &mcp.Tool{ Name: "find_relationships", - Description: "Find explicit and implied relationships for a table.", + Description: "PLACEHOLDER Future proofing: Find explicit and implied relationships for a table.", }, func(ctx context.Context, req *mcp.CallToolRequest, args dto.HierarchyInput) (*mcp.CallToolResult, any, error) { result, err := backend.FindRelationships(ctx, args) @@ -476,10 +457,6 @@ func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPSe }, ) - // --- new: register namespaced meta.* and query.* tools --- - registerNamespacedTools(server, backend, logger) - // --------------------------------------------------------- - return &simpleMCPServer{ config: config, backend: backend, @@ -553,137 +530,3 @@ func (s *simpleMCPServer) Stop() error { s.logger.Printf("MCP server stopped") return nil } - -// registerNamespacedTools adds meta.* and query.* tools (namespaced variants). -// -//nolint:gocognit,funlen // ok for now -func registerNamespacedTools(server *mcp.Server, backend Backend, logger *logrus.Logger) { - // meta.server_info - mcp.AddTool( - server, - &mcp.Tool{ - Name: "meta.server_info", - Description: "Namespaced: Get server information.", - }, - func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, dto.ServerInfoDTO, error) { - info, err := backend.ServerInfo(ctx, nil) - if err != nil { - return nil, dto.ServerInfoDTO{}, err - } - out := dto.ServerInfoDTO{Name: info.Name, Info: info.Info, IsReadOnly: info.IsReadOnly} - bytesOut, _ := json.Marshal(out) - return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(bytesOut)}}}, out, nil - }, - ) - - // meta.db_identity - mcp.AddTool( - server, - &mcp.Tool{ - Name: "meta.db_identity", - Description: "Namespaced: Get current database identity.", - }, - func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, dto.DBIdentityDTO, error) { - id, err := backend.DBIdentity(ctx, nil) - if err != nil { - return nil, dto.DBIdentityDTO{}, err - } - out := dto.DBIdentityDTO{Identity: fmt.Sprintf("%v", id["identity"])} - bytesOut, _ := json.Marshal(out) - return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(bytesOut)}}}, out, nil - }, - ) - - mcp.AddTool( - server, - &mcp.Tool{ - Name: "query.exec_text", - Description: "Namespaced: Execute SQL returning textual result.", - }, - func(ctx context.Context, req *mcp.CallToolRequest, arg dto.QueryInput) (*mcp.CallToolResult, any, error) { - logger.Infof("query.exec_text SQL: %s", arg.SQL) - rawText, err := backend.RunQuery(ctx, arg) - if err != nil { - return nil, nil, err - } - out := dto.QueryResultDTO{Raw: rawText, Format: "text"} - bytesOut, _ := json.Marshal(out) - return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(bytesOut)}}}, out, nil - }, - ) - - mcp.AddTool( - server, - &mcp.Tool{ - Name: "query.exec_json", - Description: "Namespaced: Execute SQL returning JSON array as text.", - }, - func(ctx context.Context, req *mcp.CallToolRequest, arg dto.QueryJSONInput) (*mcp.CallToolResult, any, error) { - rows, err := backend.RunQueryJSON(ctx, arg) - if err != nil { - return nil, nil, err - } - dtObj := dto.QueryResultDTO{ - Rows: rows, - RowCount: len(rows), - Format: "json", - } - bytesOut, _ := json.Marshal(dtObj) - return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(bytesOut)}}}, dtObj, nil - }, - ) - - // meta_describe_table - mcp.AddTool( - server, - &mcp.Tool{ - Name: "meta_describe_table", - Description: "Describe a stackql relation. This publishes the bullk of the columns returned from a SELECT.", - }, - func(ctx context.Context, req *mcp.CallToolRequest, args dto.HierarchyInput) (*mcp.CallToolResult, any, error) { - result, err := backend.DescribeTable(ctx, args) - if err != nil { - return nil, nil, err - } - out := dto.QueryResultDTO{Rows: result, RowCount: len(result), Format: "json"} - bytesOut, _ := json.Marshal(out) - return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(bytesOut)}}}, out, nil - }, - ) - - // meta.get_foreign_keys - mcp.AddTool( - server, - &mcp.Tool{ - Name: "meta.get_foreign_keys", - Description: "Namespaced: Get foreign keys for a table.", - }, - func(ctx context.Context, req *mcp.CallToolRequest, args dto.HierarchyInput) (*mcp.CallToolResult, any, error) { - result, err := backend.GetForeignKeys(ctx, args) - if err != nil { - return nil, nil, err - } - out := dto.QueryResultDTO{Rows: result, RowCount: len(result), Format: "json"} - bytesOut, _ := json.Marshal(out) - return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(bytesOut)}}}, out, nil - }, - ) - - // meta.find_relationships - mcp.AddTool( - server, - &mcp.Tool{ - Name: "meta.find_relationships", - Description: "Namespaced: Find relationships for a table.", - }, - func(ctx context.Context, req *mcp.CallToolRequest, args dto.HierarchyInput) (*mcp.CallToolResult, any, error) { - result, err := backend.FindRelationships(ctx, args) - if err != nil { - return nil, nil, err - } - out := dto.SimpleTextDTO{Text: result} - bytesOut, _ := json.Marshal(out) - return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(bytesOut)}}}, out, nil - }, - ) -} diff --git a/test/robot/functional/mcp.robot b/test/robot/functional/mcp.robot index eeeeefbf..da2fbfd8 100644 --- a/test/robot/functional/mcp.robot +++ b/test/robot/functional/mcp.robot @@ -325,46 +325,6 @@ MCP HTTPS Server JSON DTO Query V3 JSON ${row_count}= Get From Dictionary ${query_obj} row_count Should Be True ${row_count} > 0 -MCP HTTPS Server Query Exec Text - Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available - # Future proofing: raw text format reserved; may gain structured hints later. - ${ns_query_text}= Run Process - ... ${STACKQL_MCP_CLIENT_EXE} - ... exec - ... \-\-client\-type\=http - ... \-\-url\=https://127.0.0.1:9004 - ... \-\-client\-cfg - ... { "apply_tls_globally": true, "insecure_skip_verify": true, "ca_file": "test/server/mtls/credentials/pg_server_cert.pem", "promote_leaf_to_ca": true } - ... \-\-exec.action - ... query.exec_text - ... \-\-exec.args - ... {"sql":"SELECT 1 as foo"} - ... stdout=${CURDIR}${/}tmp${/}MCP-HTTPS-query-exec-text.txt - ... stderr=${CURDIR}${/}tmp${/}MCP-HTTPS-query-exec-text-stderr.txt - Should Be Equal As Integers ${ns_query_text.rc} 0 - Should Contain ${ns_query_text.stdout} foo - -MCP HTTPS Server JSON DTO Query Exec JSON - Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available - ${ns_query_json}= Run Process - ... ${STACKQL_MCP_CLIENT_EXE} - ... exec - ... \-\-client\-type\=http - ... \-\-url\=https://127.0.0.1:9004 - ... \-\-client\-cfg - ... { "apply_tls_globally": true, "insecure_skip_verify": true, "ca_file": "test/server/mtls/credentials/pg_server_cert.pem", "promote_leaf_to_ca": true } - ... \-\-exec.action - ... query.exec_json - ... \-\-exec.args - ... {"sql":"SELECT 1 as foo","row_limit":5} - ... stdout=${CURDIR}${/}tmp${/}MCP-HTTPS-query-exec-json.txt - ... stderr=${CURDIR}${/}tmp${/}MCP-HTTPS-query-exec-json-stderr.txt - Should Be Equal As Integers ${ns_query_json.rc} 0 - ${ns_query_json_obj}= Parse MCP JSON Output ${ns_query_json.stdout} - Should Be Equal ${ns_query_json_obj["format"]} json - ${ns_row_count}= Get From Dictionary ${ns_query_json_obj} row_count - Should Be True ${ns_row_count} >= 0 - MCP HTTPS Server JSON DTO Meta Get Foreign Keys [Documentation] Future proofing: foreign key discovery not yet implemented; placeholder. Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available @@ -376,7 +336,7 @@ MCP HTTPS Server JSON DTO Meta Get Foreign Keys ... \-\-client\-cfg ... { "apply_tls_globally": true, "insecure_skip_verify": true, "ca_file": "test/server/mtls/credentials/pg_server_cert.pem", "promote_leaf_to_ca": true } ... \-\-exec.action - ... meta.get_foreign_keys + ... get_foreign_keys ... \-\-exec.args ... {"provider":"google","service":"cloudresourcemanager","resource":"projects"} ... stdout=${CURDIR}${/}tmp${/}MCP-HTTPS-meta-get-foreign-keys.txt @@ -395,7 +355,7 @@ MCP HTTPS Server JSON DTO Meta Find Relationships ... \-\-client\-cfg ... { "apply_tls_globally": true, "insecure_skip_verify": true, "ca_file": "test/server/mtls/credentials/pg_server_cert.pem", "promote_leaf_to_ca": true } ... \-\-exec.action - ... meta.find_relationships + ... find_relationships ... \-\-exec.args ... {"provider":"google","service":"cloudresourcemanager","resource":"projects"} ... stdout=${CURDIR}${/}tmp${/}MCP-HTTPS-meta-find-relationships.txt