Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

# CI staging
go.sum.bkp

# Windows
*.exe

Expand Down
Binary file modified docs/diagrams/plantuml/hld-components.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 18 additions & 4 deletions docs/diagrams/plantuml/hld-components.puml
Original file line number Diff line number Diff line change
@@ -1,30 +1,44 @@
@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
[Plan Builder] ..> [Route Pass]
[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


Expand Down
169 changes: 6 additions & 163 deletions pkg/mcp_server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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
},
)
}
44 changes: 2 additions & 42 deletions test/robot/functional/mcp.robot
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading