diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54a75fabb..a84767dcc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,7 +91,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v5.0.0 with: - go-version: '~1.22' + go-version: '~1.23' check-latest: true cache: true id: go @@ -193,6 +193,7 @@ jobs: echo "BUILDPATCHVERSION=$env:BUILDPATCHVERSION" >> $GITHUB_ENV python cicd/python/build.py --verbose --build + python cicd/python/build.py --verbose --build-mcp-client build\stackql.exe --help @@ -260,7 +261,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v5.0.0 with: - go-version: '~1.22' + go-version: '~1.24' check-latest: true cache: true id: go @@ -345,6 +346,16 @@ jobs: echo "BUILDPATCHVERSION=${BUILDPATCHVERSION}" } >> "${GITHUB_ENV}" python cicd/python/build.py --verbose --build + + - name: Build MCP client + env: + BUILDCOMMITSHA: ${{github.sha}} + BUILDBRANCH: ${{github.ref}} + BUILDPLATFORM: ${{runner.os}} + BUILDPATCHVERSION: ${{github.run_number}} + CGO_ENABLED: 1 + run: | + python cicd/python/build.py --verbose --build-mcp-client - name: Test if: success() @@ -410,6 +421,13 @@ jobs: with: name: stackql_linux_amd64 path: build/stackql + + - name: Upload Artifact + uses: actions/upload-artifact@v4.3.1 + if: success() + with: + name: stackql_mcp_client_linux_amd64 + path: build/stackql_mcp_client - name: prepare deb boilerplate run: | @@ -459,6 +477,11 @@ jobs: name: stackql_linux_amd64 path: build + - name: Download Artifact + uses: actions/download-artifact@v4.1.2 + with: + name: stackql_mcp_client_linux_amd64 + path: build - name: Download deb Artifact uses: actions/download-artifact@v4.1.2 @@ -474,13 +497,15 @@ jobs: - name: Stackql permissions run: | sudo chmod a+rwx build/stackql + sudo chmod a+rwx build/stackql_mcp_client ls -al build/stackql + ls -al build/stackql_mcp_client ls -al . - name: Set up Go 1.x uses: actions/setup-go@v5.0.0 with: - go-version: '~1.22' + go-version: '~1.24' check-latest: true cache: true id: go @@ -649,6 +674,7 @@ jobs: if: matrix.registry != 'test/registry' env: PYTHONPATH: '${{ env.PYTHONPATH }}:${{ github.workspace }}/test/python' + IS_SKIP_MCP_TEST: 'true' run: | mkdir -p deb_test cp stackql_${{env.BUILDMAJORVERSION}}.${{env.BUILDMINORVERSION}}.${{env.BUILDPATCHVERSION}}_amd64.deb deb_test/ @@ -682,7 +708,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v5.0.0 with: - go-version: '~1.22' + go-version: '~1.23' check-latest: true cache: true id: go @@ -693,7 +719,7 @@ jobs: cache: pip python-version: '3.12' - - name: Git Ref Parse + - name: Git Ref Parse id: git_ref_parse run: | { @@ -763,6 +789,16 @@ jobs: echo "BUILDPATCHVERSION=${BUILDPATCHVERSION}" } >> "${GITHUB_ENV}" python cicd/python/build.py --verbose --build + + - name: Build MCP client + env: + BUILDCOMMITSHA: ${{github.sha}} + BUILDBRANCH: ${{github.ref}} + BUILDPLATFORM: ${{runner.os}} + BUILDPATCHVERSION: ${{github.run_number}} + CGO_ENABLED: 1 + run: | + python cicd/python/build.py --verbose --build-mcp-client - name: Test if: success() @@ -931,6 +967,7 @@ jobs: pkgVersion: ${{env.BUILDMAJORVERSION}}.${{env.BUILDMINORVERSION}}.${{env.BUILDPATCHVERSION}} pkgArchitecture: 'arm64' PYTHONPATH: '${{ env.PYTHONPATH }}:${{ github.workspace }}/test/python' + IS_SKIP_MCP_TEST: 'true' run: | mkdir -p deb_test DEB_FILE="${pkgName}_${pkgVersion}_${pkgArchitecture}.deb" @@ -979,6 +1016,13 @@ jobs: with: name: stackql_linux_amd64 path: build + + - name: Download MCP Client Artifact + # uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.2 + with: + name: stackql_mcp_client_linux_amd64 + path: build - name: Setup WSL with dependencies # uses: Vampire/setup-wsl@v1 @@ -1090,7 +1134,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v5.0.0 with: - go-version: '~1.22' + go-version: '~1.23' check-latest: true cache: true id: go @@ -1164,6 +1208,7 @@ jobs: } >> "${GITHUB_ENV}" python cicd/python/build.py --verbose --build + python cicd/python/build.py --verbose --build-mcp-client - name: Test if: success() @@ -1236,7 +1281,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v5.0.0 with: - go-version: '~1.22' + go-version: '~1.23' check-latest: true cache: true id: go @@ -1288,6 +1333,7 @@ jobs: export GOOS="darwin" export GOARCH="arm64" python cicd/python/build.py --verbose --build + python cicd/python/build.py --verbose --build-mcp-client - name: Upload Artifact uses: actions/upload-artifact@v4.3.1 @@ -1421,7 +1467,7 @@ jobs: - name: Pull Docker base images for cache purposes if: env.BUILD_IMAGE_REQUIRED == 'true' run: | - docker pull --platform ${{ matrix.platform }} golang:1.18.4-bullseye || echo 'could not pull image for cache purposes' + docker pull --platform ${{ matrix.platform }} golang:1.23-bullseye || echo 'could not pull image for cache purposes' docker pull --platform ${{ matrix.platform }} ubuntu:22.04 || echo 'could not pull image for cache purposes' - name: Pull Docker image for cache purposes @@ -1659,6 +1705,7 @@ jobs: - name: Run robot mocked functional tests env: PYTHONPATH: '${{ env.PYTHONPATH }}:${{ github.workspace }}/test/python' + IS_SKIP_MCP_TEST: 'true' if: success() && env.CI_IS_EXPRESS != 'true' && matrix.platform == 'linux/amd64' && env.BUILD_IMAGE_REQUIRED == 'true' && matrix.db_backend == 'sqlite' timeout-minutes: ${{ vars.DEFAULT_STEP_TIMEOUT_MIN == '' && 20 || vars.DEFAULT_STEP_TIMEOUT_MIN }} run: | @@ -1669,6 +1716,7 @@ jobs: - name: Run POSTGRES BACKEND robot mocked functional tests env: PYTHONPATH: '${{ env.PYTHONPATH }}:${{ github.workspace }}/test/python' + IS_SKIP_MCP_TEST: 'true' if: success() && env.CI_IS_EXPRESS != 'true' && matrix.platform == 'linux/amd64' && env.BUILD_IMAGE_REQUIRED == 'true' && matrix.db_backend == 'postgres_tcp' timeout-minutes: ${{ vars.DEFAULT_LONG_STEP_TIMEOUT_MIN == '' && 40 || vars.DEFAULT_LONG_STEP_TIMEOUT_MIN }} run: | diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 000000000..71b68ac4f --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,36 @@ +name: Claude PR Assistant + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude-code-action: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude PR Action + uses: anthropics/claude-code-action@beta + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + timeout_minutes: "60" \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c9f910dbe..5fb0eee7d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ on: env: - GOLANGCI_LINT_VERSION: ${{ vars.GOLANGCI_LINT_VERSION == '' && 'v1.59.1' || vars.GOLANGCI_LINT_VERSION }} + GOLANGCI_LINT_VERSION: ${{ vars.GOLANGCI_LINT_VERSION == '' && 'v2.5.0' || vars.GOLANGCI_LINT_VERSION }} DEFAULT_STEP_TIMEOUT: ${{ vars.DEFAULT_STEP_TIMEOUT_MIN == '' && '20' || vars.DEFAULT_STEP_TIMEOUT_MIN }} jobs: @@ -25,7 +25,7 @@ jobs: - name: Setup Go environment uses: actions/setup-go@v5.0.0 with: - go-version: '1.22.0' + go-version: '1.23.0' cache: false - name: Check workflow files @@ -35,7 +35,7 @@ jobs: - name: Run golangci-lint - uses: golangci/golangci-lint-action@v4.0.0 + uses: golangci/golangci-lint-action@v8.0.0 with: version: ${{ env.GOLANGCI_LINT_VERSION }} args: --timeout ${{ env.DEFAULT_STEP_TIMEOUT }}m \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 96f856c0f..c33248d1e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,325 +1,228 @@ -# This code is licensed under the terms of the MIT license. - -## StackQL Acknowledgment: This file is sourced from https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322. -## StackQL Acknowledgment: We profusely thank the creator. - -## Golden config for golangci-lint v1.51.2 -# -# This is the best config for golangci-lint based on my experience and opinion. -# It is very strict, but not extremely strict. -# Feel free to adopt and change it for your needs. - -run: - # Timeout for analysis, e.g. 30s, 5m. - # Default: 1m - timeout: 10m - - -# This file contains only configs which differ from defaults. -# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml -linters-settings: - cyclop: - # The maximal code complexity to report. - # Default: 10 - max-complexity: 30 - # The maximal average package complexity. - # If it's higher than 0.0 (float) the check is enabled - # Default: 0.0 - package-average: 10.0 - - errcheck: - # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. - # Such cases aren't reported by default. - # Default: false - check-type-assertions: true - - exhaustive: - # Program elements to check for exhaustiveness. - # Default: [ switch ] - check: - - switch - - map - - exhaustruct: - # List of regular expressions to exclude struct packages and names from check. - # Default: [] - exclude: - # std libs - - "^net/http.Client$" - - "^net/http.Cookie$" - - "^net/http.Request$" - - "^net/http.Response$" - - "^net/http.Server$" - - "^net/http.Transport$" - - "^net/url.URL$" - - "^os/exec.Cmd$" - - "^reflect.StructField$" - # public libs - - "^github.com/Shopify/sarama.Config$" - - "^github.com/Shopify/sarama.ProducerMessage$" - - "^github.com/mitchellh/mapstructure.DecoderConfig$" - - "^github.com/prometheus/client_golang/.+Opts$" - - "^github.com/spf13/cobra.Command$" - - "^github.com/spf13/cobra.CompletionOptions$" - - "^github.com/stretchr/testify/mock.Mock$" - - "^github.com/testcontainers/testcontainers-go.+Request$" - - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" - - "^golang.org/x/tools/go/analysis.Analyzer$" - - "^google.golang.org/protobuf/.+Options$" - - "^gopkg.in/yaml.v3.Node$" - - funlen: - # Checks the number of lines in a function. - # If lower than 0, disable the check. - # Default: 60 - lines: 100 - # Checks the number of statements in a function. - # If lower than 0, disable the check. - # Default: 40 - statements: 50 - - gocognit: - # Minimal code complexity to report. - # Default: 30 (but we recommend 10-20) - min-complexity: 20 - - gocritic: - # Settings passed to gocritic. - # The settings key is the name of a supported gocritic checker. - # The list of supported checkers can be find in https://go-critic.github.io/overview. - settings: - captLocal: - # Whether to restrict checker to params only. - # Default: true - paramsOnly: false - underef: - # Whether to skip (*x).method() calls where x is a pointer receiver. - # Default: true - skipRecvDeref: false - - mnd: - # List of function patterns to exclude from analysis. - # Values always ignored: `time.Date`, - # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, - # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. - # Default: [] - ignored-functions: - - os.Chmod - - os.Mkdir - - os.MkdirAll - - os.OpenFile - - os.WriteFile - - prometheus.ExponentialBuckets - - prometheus.ExponentialBucketsRange - - prometheus.LinearBuckets - - gomodguard: - blocked: - # List of blocked modules. - # Default: [] - modules: - - github.com/golang/protobuf: - recommendations: - - google.golang.org/protobuf - reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" - - github.com/satori/go.uuid: - recommendations: - - github.com/google/uuid - reason: "satori's package is not maintained" - - github.com/gofrs/uuid: - recommendations: - - github.com/google/uuid - reason: "gofrs' package is not go module" - - govet: - # Enable all analyzers. - # Default: false - enable-all: true - # Disable analyzers by name. - # Run `go tool vet help` to see all analyzers. - # Default: [] - disable: - - fieldalignment # too strict - # Settings per analyzer. - settings: - shadow: - # Whether to be strict about shadowing; can be noisy. - # Default: false - strict: true - - nakedret: - # Make an issue if func has more lines of code than this setting, and it has naked returns. - # Default: 30 - max-func-lines: 0 - - nolintlint: - # Exclude following linters from requiring an explanation. - # Default: [] - allow-no-explanation: [ funlen, gocognit, lll ] - # Enable to require an explanation of nonzero length after each nolint directive. - # Default: false - require-explanation: true - # Enable to require nolint directives to mention the specific linter being suppressed. - # Default: false - require-specific: true - - rowserrcheck: - # database/sql is always checked - # Default: [] - packages: - - github.com/jmoiron/sqlx - - tenv: - # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. - # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. - # Default: false - all: true - - +version: "2" +output: + formats: + text: + path: stdout linters: - disable-all: true + default: none enable: - ## enabled by default - - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases - - gosimple # specializes in simplifying a code - - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - - ineffassign # detects when assignments to existing variables are not used - - staticcheck # is a go vet on steroids, applying a ton of static analysis checks - - typecheck # like the front-end of a Go compiler, parses and type-checks Go code - - unused # checks for unused constants, variables, functions and types - ## disabled by default - - asasalint # checks for pass []any as any in variadic func(...any) - - asciicheck # checks that your code does not contain non-ASCII identifiers - - bidichk # checks for dangerous unicode character sequences - - bodyclose # checks whether HTTP response body is closed successfully - - cyclop # checks function and package cyclomatic complexity - - dupl # tool for code clone detection - - durationcheck # checks for two durations multiplied together - - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error - - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 - - exhaustive # checks exhaustiveness of enum switch statements - - exportloopref # checks for pointers to enclosing loop variables - - forbidigo # forbids identifiers - - funlen # tool for detection of long functions - - gocheckcompilerdirectives # validates go compiler directive comments (//go:) - - gochecknoglobals # checks that no global variables exist - - gochecknoinits # checks that no init functions are present in Go code - - gocognit # computes and checks the cognitive complexity of functions - - goconst # finds repeated strings that could be replaced by a constant - - gocritic # provides diagnostics that check for bugs, performance and style issues - - gocyclo # computes and checks the cyclomatic complexity of functions - - godot # checks if comments end in a period - - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt - # - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod //TODO: re-enable - - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations - - goprintffuncname # checks that printf-like functions are named with f at the end - - gosec # inspects source code for security problems - - lll # reports long lines - - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) - - makezero # finds slice declarations with non-zero initial length - - mnd # detects magic numbers - - musttag # enforces field tags in (un)marshaled structs - - nakedret # finds naked returns in functions greater than a specified function length - - nestif # reports deeply nested if statements - - nilerr # finds the code that returns nil even if it checks that the error is not nil - - nilnil # checks that there is no simultaneous return of nil error and an invalid value - - noctx # finds sending http request without context.Context - - nolintlint # reports ill-formed or insufficient nolint directives - - nonamedreturns # reports all named returns - - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL - - predeclared # finds code that shadows one of Go's predeclared identifiers - - promlinter # checks Prometheus metrics naming via promlint - - reassign # checks that package variables are not reassigned - - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint - - rowserrcheck # checks whether Err of rows is checked successfully - - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - - stylecheck # is a replacement for golint - - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 - - testableexamples # checks if examples are testable (have an expected output) - - testpackage # makes you use a separate _test package - - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - - unconvert # removes unnecessary type conversions - - unparam # reports unused function parameters - - usestdlibvars # detects the possibility to use variables/constants from the Go standard library - - wastedassign # finds wasted assignment statements - - whitespace # detects leading and trailing whitespace - - ## you may want to enable - #- decorder # checks declaration order and count of types, constants, variables and functions - #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized - #- gci # controls golang package import order and makes it always deterministic - #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega - #- godox # detects FIXME, TODO and other comment keywords - #- goheader # checks is file header matches to pattern - #- interfacebloat # checks the number of methods inside an interface - #- ireturn # accept interfaces, return concrete types - #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated - #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope - #- wrapcheck # checks that errors returned from external packages are wrapped - - ## disabled - #- containedctx # detects struct contained context.Context field - #- contextcheck # [too many false positives] checks the function whether use a non-inherited context - #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages - #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - #- dupword # [useless without config] checks for duplicate words in the source code - #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted - #- forcetypeassert # [replaced by errcheck] finds forced type assertions - #- goerr113 # [too strict] checks the errors handling expressions - #- gofmt # [replaced by goimports] checks whether code was gofmt-ed - #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed - #- grouper # analyzes expression groups - #- importas # enforces consistent import aliases - #- maintidx # measures the maintainability index of each function - #- misspell # [useless] finds commonly misspelled English words in comments - #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity - #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test - #- tagliatelle # checks the struct tags - #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers - #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines - - ## deprecated - #- deadcode # [deprecated, replaced by unused] finds unused code - #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized - #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes - #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible - #- interfacer # [deprecated] suggests narrower interface types - #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted - #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name - #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs - #- structcheck # [deprecated, replaced by unused] finds unused struct fields - #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants - - + - asasalint + - asciicheck + - bidichk + - bodyclose + - cyclop + - dupl + - durationcheck + - errcheck + - errname + - errorlint + - exhaustive + - forbidigo + - funlen + - gocheckcompilerdirectives + - gochecknoglobals + - gochecknoinits + - gocognit + # - goconst + - gocritic + - gocyclo + - godot + - gomodguard + - goprintffuncname + - gosec + - govet + - ineffassign + - lll + - loggercheck + - makezero + - mnd + - musttag + - nakedret + - nestif + - nilerr + - nilnil + # - noctx + # - nolintlint + - nonamedreturns + - nosprintfhostport + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - sqlclosecheck + # - staticcheck + - testableexamples + - testpackage + - tparallel + - unconvert + - unparam + - unused + - usestdlibvars + - wastedassign + - whitespace + settings: + cyclop: + max-complexity: 30 + package-average: 10 + errcheck: + check-type-assertions: true + exhaustive: + check: + - switch + - map + exhaustruct: + exclude: + - ^net/http.Client$ + - ^net/http.Cookie$ + - ^net/http.Request$ + - ^net/http.Response$ + - ^net/http.Server$ + - ^net/http.Transport$ + - ^net/url.URL$ + - ^os/exec.Cmd$ + - ^reflect.StructField$ + - ^github.com/Shopify/sarama.Config$ + - ^github.com/Shopify/sarama.ProducerMessage$ + - ^github.com/mitchellh/mapstructure.DecoderConfig$ + - ^github.com/prometheus/client_golang/.+Opts$ + - ^github.com/spf13/cobra.Command$ + - ^github.com/spf13/cobra.CompletionOptions$ + - ^github.com/stretchr/testify/mock.Mock$ + - ^github.com/testcontainers/testcontainers-go.+Request$ + - ^github.com/testcontainers/testcontainers-go.FromDockerfile$ + - ^golang.org/x/tools/go/analysis.Analyzer$ + - ^google.golang.org/protobuf/.+Options$ + - ^gopkg.in/yaml.v3.Node$ + funlen: + lines: 100 + statements: 50 + gocognit: + min-complexity: 20 + gocritic: + settings: + captLocal: + paramsOnly: false + underef: + skipRecvDeref: false + gomodguard: + blocked: + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: satori's package is not maintained + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: gofrs' package is not go module + govet: + disable: + - fieldalignment + enable-all: true + settings: + shadow: + strict: true + mnd: + ignored-functions: + - os.Chmod + - os.Mkdir + - os.MkdirAll + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets + - prometheus.ExponentialBucketsRange + - prometheus.LinearBuckets + nakedret: + max-func-lines: 0 + nolintlint: + require-explanation: true + require-specific: true + allow-no-explanation: + - funlen + - gocognit + - lll + rowserrcheck: + packages: + - github.com/jmoiron/sqlx + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - lll + source: ^//\s*go:generate\s + - linters: + - godot + source: (noinspection|TODO) + - linters: + - gocritic + source: //noinspection + - linters: + - errorlint + source: ^\s+if _, ok := err\.\([^.]+\.InternalError\); ok { + - linters: + - goconst + path: internal\/test\/.*\.go + - linters: + - mnd + - revive + - nolintlint + - lll + - gochecknoglobals + - unused + path: pkg\/mcp_server\/.*\.go + - linters: + - mnd + - revive + - nolintlint + - lll + - gochecknoglobals + - unused + - errorlint + - godot + - unparam + path: internal\/stackql\/mcpbackend\/.*\.go + - linters: + - gochecknoglobals + - lll + - revive + path: mcp_client\/cmd\/.*\.go + - linters: + - staticcheck + path: ast_format_postgres\.go + - linters: + - bodyclose + - dupl + - errcheck + - funlen + - goconst + - gosec + - noctx + - revive + - wrapcheck + - govet + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ issues: - # Maximum count of issues with the same text. - # Set to 0 to disable. - # Default: 3 max-same-issues: 50 - - exclude-rules: - - source: "^//\\s*go:generate\\s" - linters: [ lll ] - - source: "(noinspection|TODO)" - linters: [ godot ] - - source: "//noinspection" - linters: [ gocritic ] - - source: "^\\s+if _, ok := err\\.\\([^.]+\\.InternalError\\); ok {" - linters: [ errorlint ] - - path: "internal\\/test\\/.*\\.go" - linters: - - goconst - - path: "_test\\.go" - linters: - - bodyclose - - dupl - - funlen - - goconst - - gosec - - noctx - - revive - - typecheck - - wrapcheck -output: - formats: - - format: colored-line-number +formatters: + enable: + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.vscode/.gitignore b/.vscode/.gitignore index 9a4c18091..93bff4803 100644 --- a/.vscode/.gitignore +++ b/.vscode/.gitignore @@ -3,3 +3,4 @@ !launch.json !settings.json !example.env +!mcp.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 433fa1571..285001b56 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -214,9 +214,9 @@ "description": "Auth Input arg String", "default": "{}", "options": [ - "{ \"local_openssl\": { \"type\": \"null_auth\"}, \"azure\": { \"type\": \"azure_default\" }, \"digitalocean\": { \"type\": \"bearer\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/digitalocean-key.txt\" }, \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/stackql-security-reviewer.json\" }, \"googleadmin\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/ryuk-it-query.json\" }, \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/okta-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"type\": \"basic\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/github-key.txt\" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/aws-secret-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/sumologic-token.txt\", \"type\": \"basic\" } }", - "{ \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/google/functional-test-dummy-sa-key.json\" }, \"googleadmin\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/google/functional-test-dummy-sa-key.json\" },s \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/okta/api-key.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"type\": \"basic\", \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/okta/api-key.txt\" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/aws/functional-test-dummy-aws-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/netlify/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/k8s/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/sumologic/sumologic-token.txt\", \"type\": \"basic\" } }", - "{ \"pgi\": { \"type\": \"sql_data_source::postgres\", \"sqlDataSource\": { \"dsn\": \"postgres://stackql:stackql@127.0.0.1:8432\" } }, \"azure\": { \"type\": \"azure_default\" }, \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/stackql-security-reviewer.json\" }, \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/okta-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"type\": \"basic\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/github-key.txt\" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/aws-secret-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/sumologic-token.txt\", \"type\": \"basic\" } }", + "{ \"local_openssl\": { \"type\": \"null_auth\"}, \"azure\": { \"type\": \"azure_default\" }, \"digitalocean\": { \"type\": \"bearer\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/digitalocean-key.txt\" }, \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/stackql-security-reviewer.json\" }, \"googleadmin\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/ryuk-it-query.json\" }, \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/okta-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"credentialsenvvar\": \"STACKQL_GITHUB_TOKEN\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/aws-secret-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/sumologic-token.txt\", \"type\": \"basic\" } }", + "{ \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/google/functional-test-dummy-sa-key.json\" }, \"googleadmin\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/google/functional-test-dummy-sa-key.json\" }, \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/okta/api-key.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"credentialsenvvar\": \"STACKQL_GITHUB_TOKEN\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/aws/functional-test-dummy-aws-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/netlify/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/k8s/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/sumologic/sumologic-token.txt\", \"type\": \"basic\" } }", + "{ \"pgi\": { \"type\": \"sql_data_source::postgres\", \"sqlDataSource\": { \"dsn\": \"postgres://stackql:stackql@127.0.0.1:8432\" } }, \"azure\": { \"type\": \"azure_default\" }, \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/stackql-security-reviewer.json\" }, \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/okta-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"credentialsenvvar\": \"STACKQL_GITHUB_TOKEN\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/aws-secret-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/sumologic-token.txt\", \"type\": \"basic\" } }", "{ \"digitalocean\": { \"username_var\": \"DUMMY_DIGITALOCEAN_USERNAME\", \"password_var\": \"DUMMY_DIGITALOCEAN_PASSWORD\", \"type\": \"bearer\" }, \"azure\": {\"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsenvvar\": \"AZ_ACCESS_TOKEN\"} }", "{}" ] @@ -380,6 +380,16 @@ "true", "false" ] + }, + { + "type": "pickString", + "id": "mcpServerType", + "description": "MCP server type", + "default": "stdio", + "options": [ + "http", + "stdio" + ] } ], "configurations": [ @@ -445,17 +455,54 @@ "${input:queryString}" ], }, + { + "name": "run MCP standalone server", + "type": "go", + "request": "launch", + "envFile": "${workspaceFolder}/.vscode/.env", + "mode": "debug", + "program": "${workspaceFolder}/stackql", + "args": [ + "mcp", + "--pgsrv.port=6555", + "--tls.allowInsecure", + "--auth=${input:authString}", + "--session=${input:sessionString}", + "--gc=${input:gcString}", + "--registry=${input:registryString}", + "--namespaces=${input:namespaceString}", + "--sqlBackend=${input:sqlBackendString}", + "--dbInternal=${input:dbInternalString}", + "--export.alias=${input:exportAliasString}", + "--pgsrv.debug.enable=${input:serverDebugPublish}", + "--mcp.server.type=${input:mcpServerType}", + ], + }, + { + "name": "run MCP client", + "type": "go", + "request": "launch", + "envFile": "${workspaceFolder}/.vscode/.env", + "mode": "debug", + "program": "${workspaceFolder}/mcp_client/cmd", + "args": [ + "exec", + "--client-type=http", + ], + }, { "name": "run server", "type": "go", "request": "launch", + "envFile": "${workspaceFolder}/.vscode/.env", "mode": "debug", "program": "${workspaceFolder}/stackql", "args": [ "srv", - "--pgsrv.port=5888", + "--pgsrv.port=6555", "--tls.allowInsecure", "--auth=${input:authString}", + "--session=${input:sessionString}", "--gc=${input:gcString}", "--registry=${input:registryString}", "--namespaces=${input:namespaceString}", diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 000000000..117fe9896 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,69 @@ +{ + "inputs": [ + { + "type": "pickString", + "id": "pg_url", + "description": "PostgreSQL URL (e.g. postgresql://user:pass@host.docker.internal:5432/mydb)", + "options": [ + "postgresql://stackql:stackql@host.docker.internal:7432/stackql" + ], + "default": "postgresql://stackql:stackql@host.docker.internal:7432/stackql" + }, + { + "type": "pickString", + "id": "registryString", + "description": "Registry Configuration", + "options": [ + "{ \"url\": \"file://${workspaceFolder}/test/registry-sandbox\", \"localDocRoot\": \"${workspaceFolder}/test/registry-sandbox\", \"verifyConfig\": { \"nopVerify\": true } }", + "{ \"url\": \"file://${workspaceFolder}/test/registry\", \"localDocRoot\": \"${workspaceFolder}/test/registry\", \"verifyConfig\": { \"nopVerify\": true } }", + "{ \"url\": \"file://${workspaceFolder}/test/registry-mocked\", \"localDocRoot\": \"${workspaceFolder}/test/registry-mocked\", \"verifyConfig\": { \"nopVerify\": true } }", + "{ \"url\": \"file://${workspaceFolder}/test/registry-mocked-native\", \"localDocRoot\": \"${workspaceFolder}/test/registry-mocked-native\", \"verifyConfig\": { \"nopVerify\": true } }", + "{ \"url\": \"file://${workspaceFolder}/test/registry-advanced\", \"localDocRoot\": \"${workspaceFolder}/test/registry-advanced\", \"verifyConfig\": { \"nopVerify\": true } }", + "{ \"url\": \"file://${workspaceFolder}/build/.stackql\", \"localDocRoot\": \"${workspaceFolder}/build/.stackql\", \"verifyConfig\": { \"nopVerify\": true } }", + "{ \"url\": \"file://${workspaceFolder}/docs/examples/empty-registry\", \"localDocRoot\": \"${workspaceFolder}/docs/examples/empty-registry\" }", + "{ \"url\": \"https://cdn.statically.io/gh/stackql/stackql-provider-registry/main/providers\", \"localDocRoot\": \"${workspaceFolder}/test/registry\" }", + "{ \"url\": \"https://cdn.statically.io/gh/stackql/stackql-provider-registry/dev/providers\" }", + "{ \"url\": \"https://registry-dev.stackql.app/providers\" }", + "{ \"url\": \"https://registry.stackql.app/providers\" }", + "{\"url\": \"http://localhost:1094/gh/stackql/stackql-provider-registry/main/providers\", \"verifyConfig\": {\"nopVerify\": true}}", + ], + "default": "{ \"url\": \"file://${workspaceFolder}/test/registry\", \"localDocRoot\": \"${workspaceFolder}/test/registry\", \"verifyConfig\": { \"nopVerify\": true } }" + }, + { + "type": "pickString", + "id": "authString", + "description": "Auth Input arg String", + "default": "{}", + "options": [ + "{ \"local_openssl\": { \"type\": \"null_auth\"}, \"azure\": { \"type\": \"azure_default\" }, \"digitalocean\": { \"type\": \"bearer\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/digitalocean-key.txt\" }, \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/stackql-security-reviewer.json\" }, \"googleadmin\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/ryuk-it-query.json\" }, \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/okta-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"credentialsenvvar\": \"STACKQL_GITHUB_TOKEN\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/aws-secret-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/sumologic-token.txt\", \"type\": \"basic\" } }", + "{ \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/google/functional-test-dummy-sa-key.json\" }, \"googleadmin\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/google/functional-test-dummy-sa-key.json\" }, \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/okta/api-key.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"credentialsenvvar\": \"STACKQL_GITHUB_TOKEN\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/aws/functional-test-dummy-aws-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/netlify/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/k8s/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/test/assets/credentials/dummy/sumologic/sumologic-token.txt\", \"type\": \"basic\" } }", + "{ \"pgi\": { \"type\": \"sql_data_source::postgres\", \"sqlDataSource\": { \"dsn\": \"postgres://stackql:stackql@127.0.0.1:8432\" } }, \"azure\": { \"type\": \"azure_default\" }, \"google\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/stackql-security-reviewer.json\" }, \"okta\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/okta-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"SSWS \" }, \"github\": { \"credentialsenvvar\": \"STACKQL_GITHUB_TOKEN\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"aws\": { \"type\": \"aws_signing_v4\", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/aws-secret-key.txt\", \"keyID\": \"AKIA376P4FQSS2ONB2NS\" }, \"netlify\": { \"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/netlify-token.txt\" }, \"k8s\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/k8s-token.txt\", \"type\": \"api_key\", \"valuePrefix\": \"Bearer \" }, \"sumologic\": { \"credentialsfilepath\": \"${workspaceFolder}/cicd/keys/integration/sumologic-token.txt\", \"type\": \"basic\" } }", + "{ \"digitalocean\": { \"username_var\": \"DUMMY_DIGITALOCEAN_USERNAME\", \"password_var\": \"DUMMY_DIGITALOCEAN_PASSWORD\", \"type\": \"bearer\" }, \"azure\": {\"type\": \"api_key\", \"valuePrefix\": \"Bearer \", \"credentialsenvvar\": \"AZ_ACCESS_TOKEN\"} }", + "{}" + ] + }, + ], + "servers": { + "postgres": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "mcp/postgres", + "postgresql://stackql:stackql@host.docker.internal:8432/stackql" + ] + }, + "stackqlLocal": { + "type": "stdio", + "command": "${workspaceFolder}/build/stackql", + "args": [ + "mcp", + "--tls.allowInsecure", + "--mcp.server.type=stdio", + "--auth=${input:authString}", + "--registry=${input:registryString}" + ] + } + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..cb53991a0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,81 @@ +# Repository Guidelines + +These guidelines help contributors work effectively on this repository. We gratefully acknowledge [mcp-postgres](https://github.com/gldc/mcp-postgres) as the chief inspiration for the MCP server function and this document. + +We also encourage reading [`docs/developer_guide.md`](/docs/developer_guide.md) for further useful information. + + +## Project Structure & Module Organization + +- Entrypoint: [`stackql/main.go`](/stackql/main.go). +- Ideally, foregin system semantics are dealt with in the `any-sdk` repository. +- Loose adherence to popular idioms: + - App internals in [`internal`](/internal). + - Re-usable modules in [`pkg`](/pkg). +— The MCP server function is built upon the golang MCP SDK. +- CICD: please see [the github actions workflows](/.github/workflows). +- Docs: `README.md`, this `AGENTS.md`. + +## Build, Test, and Development Commands + +- Create env: `python -m venv .venv && source .venv/bin/activate` +- Install deps: `pip install -r requirements.txt` +- Run server (no DB): `python postgres_server.py` +- Run with DB: `POSTGRES_CONNECTION_STRING="postgresql://user:pass@host:5432/db" python postgres_server.py` +- Docker build/run: `docker build -t mcp-postgres .` then `docker run -e POSTGRES_CONNECTION_STRING=... -p 8000:8000 mcp-postgres` + +## Coding Style & Naming Conventions + +- Publish and program to abstractions. + +## Testing Guidelines + +- Black box regression tests are effectively mandatory. The canaonical ones reside in [`test/robot/functional`](/test/robot/functional). + +## Tools & Resources + +- Please inspect using the API. + + +## Commit & Pull Request Guidelines + +- Fork and pull model for general public; we **strongly** welcome public contributions, comment and issues. + +## Security & Configuration Tips + +- WIP. + +--- + +## StackQL Resource Key Encoding Quirk + +### Hierarchical Keys and URL Encoding + +StackQL resource methods (especially for GitHub refs/tags) require exact matches for hierarchical keys, such as `refs/tags/x1.3`. However, due to dependency library behavior (e.g., gorilla/mux in Go), forward slashes in parameters may be interpreted as path delimiters, causing query failures or incorrect resource matching. + +**Workaround:** Always URL encode forward slashes in resource keys when constructing queries. For example: + +- **Incorrect:** + ```sql + SELECT ref FROM github.git.refs WHERE owner = 'stackql' AND repo = 'mcp-stackql' AND ref = 'refs/tags/x2.0'; + ``` +- **Correct:** + ```sql + SELECT ref FROM github.git.refs WHERE owner = 'stackql' AND repo = 'mcp-stackql' AND ref = 'tags%2Fx2.0'; + ``` + +This ensures the backend treats the parameter as a literal string, not a path. + +### Guidance for LLMs and Contributors + +- Always encode slashes in hierarchical resource keys for StackQL queries. +- Do not use wildcards or inequalities for `WHERE` clauses that map to parameters (eg: HTTP path parameters); in such cases, only exact matches are supported. +- Document this quirk in scripts, tests, and code comments to avoid confusion. +- Example for LLM prompt: + > "When querying StackQL resources with keys containing slashes, always URL encode the slash." + +### Why This Is Necessary + +Many RESTful routing libraries (like gorilla/mux) treat slashes as path separators. Encoding slashes prevents misinterpretation and ensures correct resource access. + +Refer to this section whenever you encounter issues with resource keys containing slashes or hierarchical identifiers. diff --git a/Dockerfile b/Dockerfile index 3543edbc7..c790e9dce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22-bullseye AS sourceprep +FROM golang:1.23-bullseye AS sourceprep ENV SRC_DIR=/work/stackql/src @@ -12,6 +12,8 @@ ADD pkg ${SRC_DIR}/pkg ADD stackql ${SRC_DIR}/stackql +ADD mcp_client ${SRC_DIR}/mcp_client + ADD test ${SRC_DIR}/test COPY go.mod go.sum ${SRC_DIR}/ @@ -56,6 +58,10 @@ RUN cd ${SRC_DIR} \ --tags "sqlite_stackql" \ -o ${BUILD_DIR}/stackql ./stackql +RUN cd ${SRC_DIR} \ + && go build \ + -o ${BUILD_DIR}/stackql_mcp_client ./mcp_client/cmd + FROM python:3.11-bullseye AS utility ARG TEST_ROOT_DIR=/opt/test/stackql @@ -135,6 +141,8 @@ COPY --from=registrymock /opt/test/stackql ${TEST_ROOT_DIR}/ COPY --from=builder /work/stackql/build/stackql ${TEST_ROOT_DIR}/build/ +COPY --from=builder /work/stackql/build/stackql_mcp_client ${TEST_ROOT_DIR}/build/ + RUN if [ "${RUN_INTEGRATION_TESTS}" = "1" ]; then env PYTHONPATH="$PYTHONPATH:${TEST_ROOT_DIR}/test/python" robot ${TEST_ROOT_DIR}/test/robot/functional; fi FROM ubuntu:22.04 AS app @@ -161,6 +169,8 @@ ENV PATH="${APP_DIR}:${PATH}" COPY --from=integration ${TEST_ROOT_DIR}/build/stackql ${APP_DIR}/ +COPY --from=integration ${TEST_ROOT_DIR}/build/stackql_mcp_client ${APP_DIR}/ + RUN apt-get update \ && apt-get install -y ca-certificates openssl netcat-traditional jq dnsutils \ && update-ca-certificates diff --git a/README.md b/README.md index cceeaa714..08afcc7bd 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,9 @@ Forks of the following support our work: * [gorilla/mux](https://github.com/gorilla/mux) * [readline](https://github.com/chzyer/readline) * [psql-wire](https://github.com/jeroenrinzema/psql-wire) +* [mcp-postgres](https://github.com/gldc/mcp-postgres) +* [the `golang` MCP SDK](https://github.com/modelcontextprotocol/go-sdk) +* ...and more. Please excuse us for any omissions. We gratefully acknowledge these pieces of work. diff --git a/cicd/python/build.py b/cicd/python/build.py index 71fb3ea0e..ed6944cb7 100644 --- a/cicd/python/build.py +++ b/cicd/python/build.py @@ -27,6 +27,18 @@ def build_stackql(verbose :bool) -> int: shell=True ) +def build_stackql_mcp_client(verbose :bool) -> int: + os.environ['BUILDMAJORVERSION'] = os.environ.get('BUILDMAJORVERSION', '1') + os.environ['BUILDMINORVERSION'] = os.environ.get('BUILDMINORVERSION', '1') + os.environ['BUILDPATCHVERSION'] = os.environ.get('BUILDPATCHVERSION', '1') + os.environ['CGO_ENABLED'] = os.environ.get('CGO_ENABLED', '1') + return subprocess.call( + 'go build ' + f'{"-x -v" if verbose else ""} ' + '-o build/stackql_mcp_client ./mcp_client/cmd', + shell=True + ) + def unit_test_stackql(verbose :bool) -> int: return subprocess.call( @@ -34,7 +46,7 @@ def unit_test_stackql(verbose :bool) -> int: shell=True ) -def sanitise_val(val :any) -> str: +def sanitise_val(val) -> str: if isinstance(val, bool): return str(val).lower() return str(val) @@ -65,6 +77,7 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--verbose', action='store_true') parser.add_argument('--build', action='store_true') + parser.add_argument('--build-mcp-client', action='store_true') parser.add_argument('--test', action='store_true') parser.add_argument('--robot-test', action='store_true') parser.add_argument('--robot-test-integration', action='store_true') @@ -75,6 +88,10 @@ def main(): ret_code = build_stackql(args.verbose) if ret_code != 0: exit(ret_code) + if args.build_mcp_client: + ret_code = build_stackql_mcp_client(args.verbose) + if ret_code != 0: + exit(ret_code) if args.test: ret_code = unit_test_stackql(args.verbose) if ret_code != 0: diff --git a/docker-compose-credentials.yml b/docker-compose-credentials.yml index c40c8526d..7ef08a21a 100644 --- a/docker-compose-credentials.yml +++ b/docker-compose-credentials.yml @@ -1,4 +1,4 @@ -version: "3.9" + services: credentialsgen: diff --git a/docs/developer_guide.md b/docs/developer_guide.md index 90f3b5ed5..c651cfd63 100644 --- a/docs/developer_guide.md +++ b/docs/developer_guide.md @@ -77,7 +77,7 @@ go test -timeout 1200s --tags "sqlite_stackql" ./... **Note**: this requires the local build (above) to have been completed successfully, which builds a binary in `./build/`. ```bash -env PYTHONPATH="$PYTHONPATH:$(pwd)/test/python" robot -d test/robot/functional test/robot/functional +env PYTHONPATH="$PYTHONPATH:$(pwd)/test/python" robot -d test/robot/reports test/robot/functional ``` Or better yet, if you have docker desktop and the `postgres` image cited in the docker compose files: @@ -188,8 +188,8 @@ Local testing of the application: 2. Build the executable [as per the root README](/README.md#build) 3. Perform registry rewrites as needed for mocking `python3 test/python/stackql_test_tooling/registry_rewrite.py --srcdir "$(pwd)/test/registry/src" --destdir "$(pwd)/test/registry-mocked/src"`. 3. Run robot tests: - - Functional tests, mocked as needed `robot -d test/robot/functional test/robot/functional`. - - Integration tests `robot -d test/robot/integration test/robot/integration`. For these, you will need to set various envirnonment variables as per the github actions. + - Functional tests, mocked as needed `robot -d test/robot/reports test/robot/functional`. + - Integration tests `robot -d test/robot/reports test/robot/integration`. For these, you will need to set various envirnonment variables as per the github actions. 4. Run the deprecated manual python tests: - Prepare with `cp test/db/db.sqlite test/db/tmp/python-tests-tmp-db.sqlite`. - Run with `python3 test/deprecated/python/main.py`. diff --git a/docs/licenses/gldc_mcp_postgres_license b/docs/licenses/gldc_mcp_postgres_license new file mode 100644 index 000000000..f68d09573 --- /dev/null +++ b/docs/licenses/gldc_mcp_postgres_license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 gldc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/licenses/mcp_sdk_license b/docs/licenses/mcp_sdk_license new file mode 100644 index 000000000..02f6ba3ed --- /dev/null +++ b/docs/licenses/mcp_sdk_license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Go MCP SDK Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/go.mod b/go.mod index 9bfba4eb6..049d8ab46 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/stackql/stackql -go 1.22.0 +go 1.23.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.1 @@ -10,6 +10,7 @@ require ( github.com/jackc/pgx/v5 v5.0.4 github.com/lib/pq v1.10.4 github.com/magiconair/properties v1.8.6 + github.com/modelcontextprotocol/go-sdk v1.0.0 github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4 github.com/sirupsen/logrus v1.9.3 github.com/snowflakedb/gosnowflake v1.15.0 @@ -18,10 +19,10 @@ require ( github.com/spf13/viper v1.10.1 github.com/stackql/any-sdk v0.2.2-beta07 github.com/stackql/go-suffix-map v0.0.1-alpha01 - github.com/stackql/psql-wire v0.1.1-beta23 + github.com/stackql/psql-wire v0.1.1-beta25 github.com/stackql/stackql-parser v0.0.15-alpha06 github.com/stretchr/testify v1.10.0 - golang.org/x/sync v0.10.0 + golang.org/x/sync v0.15.0 gonum.org/v1/gonum v0.15.1 gopkg.in/yaml.v2 v2.4.0 gotest.tools v2.2.0+incompatible @@ -81,6 +82,7 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/flatbuffers v24.12.23+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/jsonschema-go v0.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect @@ -112,21 +114,22 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/xo/dburl v0.23.2 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.21.0 // indirect - golang.org/x/crypto v0.32.0 // indirect + golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.34.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.29.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.34.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect google.golang.org/grpc v1.67.1 // indirect diff --git a/go.sum b/go.sum index e50435c93..304a6eaac 100644 --- a/go.sum +++ b/go.sum @@ -261,6 +261,8 @@ 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-jsonnet v0.17.0 h1:/9NIEfhK1NQRKl3sP2536b2+x5HnZMdql7x3yK/l8JY= github.com/google/go-jsonnet v0.17.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -407,6 +409,8 @@ github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8D github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74= +github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -465,8 +469,8 @@ github.com/stackql/any-sdk v0.2.2-beta07 h1:c/MaT8p4lB30xslJo9LQm3JDWMMfzwheGXqf github.com/stackql/any-sdk v0.2.2-beta07/go.mod h1:m1o5TCfyKkdt2bREB3itwPv1MhM+lk4eu24KpPohFoY= github.com/stackql/go-suffix-map v0.0.1-alpha01 h1:TDUDS8bySu41Oo9p0eniUeCm43mnRM6zFEd6j6VUaz8= github.com/stackql/go-suffix-map v0.0.1-alpha01/go.mod h1:QAi+SKukOyf4dBtWy8UMy+hsXXV+yyEE4vmBkji2V7g= -github.com/stackql/psql-wire v0.1.1-beta23 h1:1ayYMjZArfDcIMyEOKnm+Bp1zRCISw8pguvTFuUhhVQ= -github.com/stackql/psql-wire v0.1.1-beta23/go.mod h1:a44Wd8kDC3irFLpGutarKDBqhJ/aqXlj1aMzO5bVJYg= +github.com/stackql/psql-wire v0.1.1-beta25 h1:DFBLjtz9N1S9gIYhqsjVZtVZMVSg7c0vvirPT29+S3s= +github.com/stackql/psql-wire v0.1.1-beta25/go.mod h1:a44Wd8kDC3irFLpGutarKDBqhJ/aqXlj1aMzO5bVJYg= github.com/stackql/readline v0.0.2-alpha05 h1:ID4QzGdplFBsrSnTuz8pvKzWw96JbrJg8fsLry2UriU= github.com/stackql/readline v0.0.2-alpha05/go.mod h1:OFAYOdXk/X4+5GYiDXFfaGrk+bCN6Qv0SYY5HNzD2E0= github.com/stackql/stackql-go-sqlite3 v1.0.4-stackql h1:fp70Vdw+PCVEoPrAhkyqPuAlrIiHT79mght/0rlR4oY= @@ -492,6 +496,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= +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.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -553,8 +559,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -592,8 +598,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -631,8 +637,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -662,8 +668,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -721,12 +727,12 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -735,8 +741,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -796,8 +802,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/stackql/acid/tsm_physio/access_methods.go b/internal/stackql/acid/tsm_physio/access_methods.go index dcdd70843..fd56c0f21 100644 --- a/internal/stackql/acid/tsm_physio/access_methods.go +++ b/internal/stackql/acid/tsm_physio/access_methods.go @@ -1,4 +1,4 @@ -package tsm_physio //nolint:revive,stylecheck // prefer this nomenclature +package tsm_physio //nolint:stylecheck,revive // prefer this nomenclature var ( _ AccessMethods = (*accessMethods)(nil) diff --git a/internal/stackql/acid/tsm_physio/best_effort_coordinator.go b/internal/stackql/acid/tsm_physio/best_effort_coordinator.go index 69a26e578..87ed2183a 100644 --- a/internal/stackql/acid/tsm_physio/best_effort_coordinator.go +++ b/internal/stackql/acid/tsm_physio/best_effort_coordinator.go @@ -1,4 +1,4 @@ -package tsm_physio //nolint:revive,stylecheck // prefer this nomenclature +package tsm_physio //nolint:stylecheck // prefer this nomenclature import ( "fmt" diff --git a/internal/stackql/acid/tsm_physio/buffer_pool.go b/internal/stackql/acid/tsm_physio/buffer_pool.go index f0c31b811..495775ea4 100644 --- a/internal/stackql/acid/tsm_physio/buffer_pool.go +++ b/internal/stackql/acid/tsm_physio/buffer_pool.go @@ -1,4 +1,4 @@ -package tsm_physio //nolint:revive,stylecheck // prefer this nomenclature +package tsm_physio //nolint:stylecheck,revive // prefer this nomenclature var ( _ BufferPool = (*bufferPool)(nil) diff --git a/internal/stackql/acid/tsm_physio/lazy_coordinator.go b/internal/stackql/acid/tsm_physio/lazy_coordinator.go index d91f875c8..38e08b4e2 100644 --- a/internal/stackql/acid/tsm_physio/lazy_coordinator.go +++ b/internal/stackql/acid/tsm_physio/lazy_coordinator.go @@ -1,4 +1,4 @@ -package tsm_physio //nolint:revive,stylecheck // prefer this nomenclature +package tsm_physio //nolint:stylecheck,revive // prefer this nomenclature import ( "fmt" diff --git a/internal/stackql/acid/tsm_physio/lock_manager.go b/internal/stackql/acid/tsm_physio/lock_manager.go index c6364500d..7540e5837 100644 --- a/internal/stackql/acid/tsm_physio/lock_manager.go +++ b/internal/stackql/acid/tsm_physio/lock_manager.go @@ -1,4 +1,4 @@ -package tsm_physio //nolint:revive,stylecheck // prefer this nomenclature +package tsm_physio //nolint:stylecheck,revive // prefer this nomenclature var ( _ LockManager = (*lockManager)(nil) diff --git a/internal/stackql/acid/tsm_physio/statement.go b/internal/stackql/acid/tsm_physio/statement.go index bc165cc77..20e08b25e 100644 --- a/internal/stackql/acid/tsm_physio/statement.go +++ b/internal/stackql/acid/tsm_physio/statement.go @@ -1,4 +1,4 @@ -package tsm_physio //nolint:revive,stylecheck // prefer this nomenclature +package tsm_physio //nolint:stylecheck // prefer this nomenclature import ( "github.com/stackql/stackql-parser/go/vt/sqlparser" diff --git a/internal/stackql/acid/tsm_physio/tsm_implementation.go b/internal/stackql/acid/tsm_physio/tsm_implementation.go index 8c9742250..98f06b943 100644 --- a/internal/stackql/acid/tsm_physio/tsm_implementation.go +++ b/internal/stackql/acid/tsm_physio/tsm_implementation.go @@ -1,4 +1,4 @@ -package tsm_physio //nolint:revive,stylecheck // prefer this nomenclature +package tsm_physio //nolint:stylecheck,revive // prefer this nomenclature import ( "github.com/stackql/stackql/internal/stackql/acid/tsm" diff --git a/internal/stackql/acid/tsm_physio/txn_orchestrator.go b/internal/stackql/acid/tsm_physio/txn_orchestrator.go index 3d8c7776d..3b2133cee 100644 --- a/internal/stackql/acid/tsm_physio/txn_orchestrator.go +++ b/internal/stackql/acid/tsm_physio/txn_orchestrator.go @@ -1,4 +1,4 @@ -package tsm_physio //nolint:revive,stylecheck // prefer this nomenclature +package tsm_physio //nolint:stylecheck // prefer this nomenclature import ( "fmt" diff --git a/internal/stackql/acid/tsm_physio/txn_provider.go b/internal/stackql/acid/tsm_physio/txn_provider.go index 0134c29e4..aa1f6c765 100644 --- a/internal/stackql/acid/tsm_physio/txn_provider.go +++ b/internal/stackql/acid/tsm_physio/txn_provider.go @@ -24,7 +24,7 @@ const ( // that orchestrates transaction managers. type Provider interface { // Create a new transaction manager. - GetOrchestrator(handler.HandlerContext) (Orchestrator, error) + getOrchestrator(handler.HandlerContext) (Orchestrator, error) GetTSM(handlerCtx handler.HandlerContext) (tsm.TSM, error) } @@ -32,7 +32,7 @@ type standardProvider struct { ctx txn_context.ITransactionCoordinatorContext } -func (sp *standardProvider) GetOrchestrator(handlerCtx handler.HandlerContext) (Orchestrator, error) { +func (sp *standardProvider) getOrchestrator(handlerCtx handler.HandlerContext) (Orchestrator, error) { tsmInstance, walError := GetTSM(handlerCtx) if walError != nil { return nil, walError @@ -67,3 +67,12 @@ func GetProviderInstance(ctx txn_context.ITransactionCoordinatorContext) (Provid }) return providerSingleton, err } + +func NewOrchestrator(handlerCtx handler.HandlerContext) (Orchestrator, error) { + txnProvider, txnProviderErr := GetProviderInstance( + handlerCtx.GetTxnCoordinatorCtx()) + if txnProviderErr != nil { + return nil, txnProviderErr + } + return txnProvider.getOrchestrator(handlerCtx) +} diff --git a/internal/stackql/acid/txn_context/txn_context.go b/internal/stackql/acid/txn_context/txn_context.go index f00947013..fc0297884 100644 --- a/internal/stackql/acid/txn_context/txn_context.go +++ b/internal/stackql/acid/txn_context/txn_context.go @@ -1,4 +1,4 @@ -package txn_context //nolint:revive,stylecheck // meaning of package name is clear +package txn_context //nolint:stylecheck,revive // meaning of package name is clear var ( _ ITransactionContext = &transactionContext{} diff --git a/internal/stackql/astanalysis/earlyanalysis/first_passes.go b/internal/stackql/astanalysis/earlyanalysis/first_passes.go index c43cb0e82..c527c3f26 100644 --- a/internal/stackql/astanalysis/earlyanalysis/first_passes.go +++ b/internal/stackql/astanalysis/earlyanalysis/first_passes.go @@ -28,7 +28,6 @@ const ( ) var ( - //nolint:revive // prefer declarative errPgOnly error = fmt.Errorf("cannot accomodate PG-only statement when backend is not matched to PG") ) diff --git a/internal/stackql/astvisit/assign_leftover_references.go b/internal/stackql/astvisit/assign_leftover_references.go index cd19ba0d1..93e3724b3 100644 --- a/internal/stackql/astvisit/assign_leftover_references.go +++ b/internal/stackql/astvisit/assign_leftover_references.go @@ -81,7 +81,7 @@ func (v *standardLeftoverReferencesAstVisitor) findTableLeftover( return nil, fmt.Errorf("could not locate table corresponding to expression '%s'", colName.GetRawVal()) } -//nolint:dupl,funlen,gocognit,gocyclo,cyclop,errcheck,staticcheck,goconst,gocritic,lll,nestif,govet,gomnd,exhaustive,revive // defer uplifts on analysers +//nolint:dupl,funlen,gocognit,gocyclo,cyclop,errcheck,staticcheck,gocritic,lll,nestif,govet,gomnd,exhaustive,revive // defer uplifts on analysers func (v *standardLeftoverReferencesAstVisitor) Visit(node sqlparser.SQLNode) error { var err error diff --git a/internal/stackql/astvisit/query_rewriting.go b/internal/stackql/astvisit/query_rewriting.go index d49eb21fc..5cd6c4ceb 100644 --- a/internal/stackql/astvisit/query_rewriting.go +++ b/internal/stackql/astvisit/query_rewriting.go @@ -25,9 +25,8 @@ import ( ) var ( - _ QueryRewriteAstVisitor = &standardQueryRewriteAstVisitor{} - //nolint:revive // acceptable - isJSONEachCompatibleRegexp *regexp.Regexp = regexp.MustCompile( + _ QueryRewriteAstVisitor = &standardQueryRewriteAstVisitor{} + isJSONEachCompatibleRegexp *regexp.Regexp = regexp.MustCompile( `^(.*\.value|value)$`) ) diff --git a/internal/stackql/astvisit/table_extract.go b/internal/stackql/astvisit/table_extract.go index a96a4500d..0c391ea13 100644 --- a/internal/stackql/astvisit/table_extract.go +++ b/internal/stackql/astvisit/table_extract.go @@ -47,7 +47,7 @@ func (v *standardParserTableExtractAstVisitor) MergeTableExprs() sqlparser.Table return v.tables } -//nolint:dupl,funlen,gocognit,gocyclo,cyclop,errcheck,staticcheck,gocritic,lll,exhaustive,nestif,gomnd,revive // defer uplifts on analysers +//nolint:dupl,funlen,gocognit,gocyclo,cyclop,errcheck,staticcheck,goconst,gocritic,lll,exhaustive,nestif,gomnd,revive // defer uplifts on analysers func (v *standardParserTableExtractAstVisitor) Visit(node sqlparser.SQLNode) error { var err error diff --git a/internal/stackql/cmd/exec.go b/internal/stackql/cmd/exec.go index 8004de50d..b2d4cb24c 100644 --- a/internal/stackql/cmd/exec.go +++ b/internal/stackql/cmd/exec.go @@ -1,5 +1,5 @@ /* -Copyright © 2019 stackql info@stackql.io +Copyright © 2025 stackql info@stackql.io Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/stackql/cmd/mcp.go b/internal/stackql/cmd/mcp.go new file mode 100644 index 000000000..8bc8f61eb --- /dev/null +++ b/internal/stackql/cmd/mcp.go @@ -0,0 +1,86 @@ +/* +Copyright © 2025 stackql info@stackql.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "context" + "encoding/json" + + "github.com/spf13/cobra" + + "github.com/stackql/any-sdk/pkg/logging" + "github.com/stackql/stackql/internal/stackql/acid/tsm_physio" + "github.com/stackql/stackql/internal/stackql/entryutil" + "github.com/stackql/stackql/internal/stackql/iqlerror" + "github.com/stackql/stackql/internal/stackql/mcpbackend" + "github.com/stackql/stackql/pkg/mcp_server" +) + +//nolint:gochecknoglobals // cobra pattern +var ( + mcpServerType string // overwritten by flag + mcpConfig string // overwritten by flag +) + +//nolint:gochecknoglobals // cobra pattern +var mcpSrvCmd = &cobra.Command{ + Use: "mcp", + Short: "run mcp server", + Long: ` + Run a MCP protocol server. + Supports MCP client connections from all manner or libs. + `, + //nolint:revive // acceptable for now + Run: func(cmd *cobra.Command, args []string) { + flagErr := dependentFlagHandler(&runtimeCtx) + iqlerror.PrintErrorAndExitOneIfError(flagErr) + inputBundle, err := entryutil.BuildInputBundle(runtimeCtx) + iqlerror.PrintErrorAndExitOneIfError(err) + handlerCtx, err := entryutil.BuildHandlerContext(runtimeCtx, nil, queryCache, inputBundle, false) + iqlerror.PrintErrorAndExitOneIfError(err) + iqlerror.PrintErrorAndExitOneIfNil(handlerCtx, "handler context is unexpectedly nil") + var config mcp_server.Config + json.Unmarshal([]byte(mcpConfig), &config) //nolint:errcheck // TODO: investigate + config.Server.Transport = mcpServerType + var isReadOnly bool + if config.Server.IsReadOnly != nil { + isReadOnly = *config.Server.IsReadOnly + } + orchestrator, orchestratorErr := tsm_physio.NewOrchestrator(handlerCtx) + iqlerror.PrintErrorAndExitOneIfError(orchestratorErr) + iqlerror.PrintErrorAndExitOneIfNil(orchestrator, "orchestrator is unexpectedly nil") + // handlerCtx.SetTSMOrchestrator(orchestrator) + backend, backendErr := mcpbackend.NewStackqlMCPBackendService( + isReadOnly, + orchestrator, + handlerCtx, + logging.GetLogger(), + ) + iqlerror.PrintErrorAndExitOneIfError(backendErr) + iqlerror.PrintErrorAndExitOneIfNil(backend, "mcp backend is unexpectedly nil") + + server, serverErr := mcp_server.NewAgnosticBackendServer( + backend, + &config, + logging.GetLogger(), + ) + // server, serverErr := mcp_server.NewExampleHTTPBackendServer( + // logging.GetLogger(), + // ) + iqlerror.PrintErrorAndExitOneIfError(serverErr) + server.Start(context.Background()) //nolint:errcheck // TODO: investigate + }, +} diff --git a/internal/stackql/cmd/registry.go b/internal/stackql/cmd/registry.go index 53e42dfac..092c820f5 100644 --- a/internal/stackql/cmd/registry.go +++ b/internal/stackql/cmd/registry.go @@ -1,5 +1,5 @@ /* -Copyright © 2019 stackql info@stackql.io +Copyright © 2025 stackql info@stackql.io Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/stackql/cmd/root.go b/internal/stackql/cmd/root.go index 20038de2f..7440de8c9 100644 --- a/internal/stackql/cmd/root.go +++ b/internal/stackql/cmd/root.go @@ -1,5 +1,5 @@ /* -Copyright © 2019 stackql info@stackql.io +Copyright © 2025 stackql info@stackql.io Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -197,6 +197,10 @@ func init() { rootCmd.AddCommand(shellCmd) rootCmd.AddCommand(registryCmd) rootCmd.AddCommand(srvCmd) + rootCmd.AddCommand(mcpSrvCmd) + + mcpSrvCmd.PersistentFlags().StringVar(&mcpConfig, "mcp.config", "{}", "MCP server config file path (YAML or JSON)") + mcpSrvCmd.PersistentFlags().StringVar(&mcpServerType, "mcp.server.type", "http", "MCP server type (http or stdio for now)") } func mergeConfigFromFile(runtimeCtx *dto.RuntimeCtx, flagSet pflag.FlagSet) { diff --git a/internal/stackql/cmd/shell.go b/internal/stackql/cmd/shell.go index 00af56a56..f64010ecb 100644 --- a/internal/stackql/cmd/shell.go +++ b/internal/stackql/cmd/shell.go @@ -1,5 +1,5 @@ /* -Copyright © 2019 stackql info@stackql.io +Copyright © 2025 stackql info@stackql.io Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/stackql/cmd/srv.go b/internal/stackql/cmd/srv.go index b8cd9a5ff..b157a4fd9 100644 --- a/internal/stackql/cmd/srv.go +++ b/internal/stackql/cmd/srv.go @@ -1,5 +1,5 @@ /* -Copyright © 2019 stackql info@stackql.io +Copyright © 2025 stackql info@stackql.io Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/stackql/data_staging/output_data_staging/packet_preparator.go b/internal/stackql/data_staging/output_data_staging/packet_preparator.go index fdb4801c3..956220dd8 100644 --- a/internal/stackql/data_staging/output_data_staging/packet_preparator.go +++ b/internal/stackql/data_staging/output_data_staging/packet_preparator.go @@ -1,4 +1,4 @@ -package output_data_staging //nolint:revive,stylecheck // package name is helpful +package output_data_staging //nolint:stylecheck,revive // package name is helpful import ( "fmt" diff --git a/internal/stackql/datasource/sql_datasource/sql_datasource.go b/internal/stackql/datasource/sql_datasource/sql_datasource.go index b11b1cc56..dcc0a3b66 100644 --- a/internal/stackql/datasource/sql_datasource/sql_datasource.go +++ b/internal/stackql/datasource/sql_datasource/sql_datasource.go @@ -1,4 +1,4 @@ -package sql_datasource //nolint:revive,stylecheck // package name is helpful +package sql_datasource //nolint:stylecheck,revive // package name is helpful import ( "database/sql" diff --git a/internal/stackql/dbmsinternal/dbmsinternal.go b/internal/stackql/dbmsinternal/dbmsinternal.go index 0f617538d..3c27dfe93 100644 --- a/internal/stackql/dbmsinternal/dbmsinternal.go +++ b/internal/stackql/dbmsinternal/dbmsinternal.go @@ -11,7 +11,7 @@ import ( "github.com/stackql/stackql/internal/stackql/sql_system" ) -//nolint:lll,revive // complex regex +//nolint:lll // complex regex var ( _ Router = &standardDBMSInternalRouter{} internalTableRegexp *regexp.Regexp = regexp.MustCompile(`(?i)^(?:public\.)?(?:pg_type|pg_namespace|pg_catalog.*|current_schema|pg_.*|information_schema.*)`) diff --git a/internal/stackql/driver/aggregation_compute_disks_integration_test.go b/internal/stackql/driver/aggregation_compute_disks_integration_test.go index 546ad62b0..4871d27b1 100644 --- a/internal/stackql/driver/aggregation_compute_disks_integration_test.go +++ b/internal/stackql/driver/aggregation_compute_disks_integration_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksOrderByCrtTmstpAsc(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSelectComputeDisksOrderByCrtTmstpAsc") if err != nil { @@ -99,7 +99,7 @@ func TestSelectComputeDisksAggOrderBySizeAsc(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSelectComputeDisksAggSizeOrderSizeAsc}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksAggOrderBySizeDesc(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSelectComputeDisksAggOrderBySizeDesc") if err != nil { @@ -137,7 +137,7 @@ func TestSelectComputeDisksAggOrderBySizeDesc(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSelectComputeDisksAggSizeOrderSizeDesc}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksAggTotalSize(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSelectComputeDisksAggTotalSize") if err != nil { @@ -175,7 +175,7 @@ func TestSelectComputeDisksAggTotalSize(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSelectComputeDisksAggSizeTotal}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksAggTotalString(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSelectComputeDisksAggTotalString") if err != nil { diff --git a/internal/stackql/driver/aggregation_container_subnetworks_integration_test.go b/internal/stackql/driver/aggregation_container_subnetworks_integration_test.go index ae09767ed..5999d4f97 100644 --- a/internal/stackql/driver/aggregation_container_subnetworks_integration_test.go +++ b/internal/stackql/driver/aggregation_container_subnetworks_integration_test.go @@ -49,7 +49,7 @@ func TestSimpleAggGoogleContainerSubnetworksGroupedAllowedDriverOutputAsc(t *tes stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSimpleAggCountGroupedGoogleCotainerSubnetworkTableFileAsc}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSimpleAggGoogleContainerSubnetworksGroupedAllowedDriverOutputDesc(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "table", "TestSimpleAggGoogleContainerSubnetworksGroupedAllowedDriverOutputDesc") if err != nil { diff --git a/internal/stackql/driver/aggregation_paginated_compute_disks_integration_test.go b/internal/stackql/driver/aggregation_paginated_compute_disks_integration_test.go index 42d7c4ceb..641764421 100644 --- a/internal/stackql/driver/aggregation_paginated_compute_disks_integration_test.go +++ b/internal/stackql/driver/aggregation_paginated_compute_disks_integration_test.go @@ -15,7 +15,7 @@ import ( lrucache "github.com/stackql/stackql-parser/go/cache" ) -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksOrderByCrtTmstpAscPaginated(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSelectComputeDisksOrderByCrtTmstpAscPaginated") if err != nil { @@ -93,7 +93,7 @@ func TestSelectComputeDisksAggOrderBySizeAscPaginated(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSelectComputeDisksAggPaginatedSizeOrderSizeAsc}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksAggOrderBySizeDescPaginated(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSelectComputeDisksAggOrderBySizeDescPaginated") if err != nil { @@ -132,7 +132,7 @@ func TestSelectComputeDisksAggOrderBySizeDescPaginated(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSelectComputeDisksAggPaginatedSizeOrderSizeDesc}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksAggTotalSizePaginated(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSelectComputeDisksAggTotalSizePaginated") if err != nil { @@ -171,7 +171,7 @@ func TestSelectComputeDisksAggTotalSizePaginated(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSelectComputeDisksAggPaginatedSizeTotal}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksAggTotalStringPaginated(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSelectComputeDisksAggTotalStringPaginated") if err != nil { diff --git a/internal/stackql/driver/driver.go b/internal/stackql/driver/driver.go index 73d188570..b807cb6bb 100644 --- a/internal/stackql/driver/driver.go +++ b/internal/stackql/driver/driver.go @@ -46,11 +46,7 @@ func (sdf *basicStackQLDriverFactory) newSQLDriver() (StackQLDriver, error) { if err != nil { return nil, err } - txnProvider, txnProviderErr := tsm_physio.GetProviderInstance(sdf.handlerCtx.GetTxnCoordinatorCtx()) - if txnProviderErr != nil { - return nil, txnProviderErr - } - txnOrchestrator, orcErr := txnProvider.GetOrchestrator(sdf.handlerCtx) + txnOrchestrator, orcErr := tsm_physio.NewOrchestrator(sdf.handlerCtx) if orcErr != nil { return nil, orcErr } @@ -186,12 +182,7 @@ func (dr *basicStackQLDriver) SplitCompoundQuery(s string) ([]string, error) { } func NewStackQLDriver(handlerCtx handler.HandlerContext) (StackQLDriver, error) { - txnProvider, txnProviderErr := tsm_physio.GetProviderInstance( - handlerCtx.GetTxnCoordinatorCtx()) - if txnProviderErr != nil { - return nil, txnProviderErr - } - txnOrchestrator, orcErr := txnProvider.GetOrchestrator(handlerCtx) + txnOrchestrator, orcErr := tsm_physio.NewOrchestrator(handlerCtx) if orcErr != nil { return nil, orcErr } diff --git a/internal/stackql/driver/driver_integration_test.go b/internal/stackql/driver/driver_integration_test.go index 355e3ffd5..136ef859b 100644 --- a/internal/stackql/driver/driver_integration_test.go +++ b/internal/stackql/driver/driver_integration_test.go @@ -63,7 +63,7 @@ func TestSimpleSelectGoogleComputeInstanceDriver(t *testing.T) { t.Logf("simple select driver integration test passed") } -//nolint:lll,errcheck,govet // legacy test +//nolint:lll,govet // legacy test func TestSimpleSelectGoogleComputeInstanceDriverOutput(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSimpleSelectGoogleComputeInstanceDriverOutput") if err != nil { @@ -98,7 +98,7 @@ func TestSimpleSelectGoogleComputeInstanceDriverOutput(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSimpleSelectGoogleComputeInstanceTextFile01, testobjects.ExpectedSimpleSelectGoogleComputeInstanceTextFile02}) } -//nolint:lll,errcheck,govet // legacy test +//nolint:lll,govet // legacy test func TestSimpleSelectGoogleComputeInstanceDriverOutputRepeated(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSimpleSelectGoogleComputeInstanceDriverOutputRepeated") if err != nil { @@ -133,7 +133,7 @@ func TestSimpleSelectGoogleComputeInstanceDriverOutputRepeated(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSimpleSelectGoogleComputeInstanceTextFile01, testobjects.ExpectedSimpleSelectGoogleComputeInstanceTextFile02}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSimpleSelectGoogleContainerSubnetworksAllowedDriverOutput(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSimpleSelectGoogleContainerSubnetworksAllowedDriverOutput") if err != nil { @@ -168,7 +168,7 @@ func TestSimpleSelectGoogleContainerSubnetworksAllowedDriverOutput(t *testing.T) stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSimpleSelectGoogleCotainerSubnetworkTextFile01, testobjects.ExpectedSimpleSelectGoogleCotainerSubnetworkTextFile02}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSimpleInsertGoogleComputeNetworkAsync(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSimpleInsertGoogleComputeNetworkAsync") if err != nil { diff --git a/internal/stackql/driver/functions_integration_test.go b/internal/stackql/driver/functions_integration_test.go index 0a905340e..cf2ca720e 100644 --- a/internal/stackql/driver/functions_integration_test.go +++ b/internal/stackql/driver/functions_integration_test.go @@ -18,7 +18,7 @@ import ( lrucache "github.com/stackql/stackql-parser/go/cache" ) -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksOrderByCrtTmstpAscPlusJsonExtract(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "csv", "TestSelectComputeDisksOrderByCrtTmstpAscPlusJsonExtract") if err != nil { @@ -56,7 +56,7 @@ func TestSelectComputeDisksOrderByCrtTmstpAscPlusJsonExtract(t *testing.T) { stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSelectComputeDisksOrderCrtTmstpAscPlusJsonExtract}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksOrderByCrtTmstpAscPlusCoalesceJsonExtract(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "csv", "TestSelectComputeDisksOrderByCrtTmstpAscPlusCoalesceJsonExtract") if err != nil { @@ -94,7 +94,7 @@ func TestSelectComputeDisksOrderByCrtTmstpAscPlusCoalesceJsonExtract(t *testing. stackqltestutil.RunCaptureTestAgainstFiles(t, testSubject, []string{testobjects.ExpectedSelectComputeDisksOrderCrtTmstpAscPlusJsonExtractCoalesce}) } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSelectComputeDisksOrderByCrtTmstpAscPlusCoalesceJsonInstr(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "csv", "TestSelectComputeDisksOrderByCrtTmstpAscPlusCoalesceJsonInstr") if err != nil { diff --git a/internal/stackql/driver/okta_integration_test.go b/internal/stackql/driver/okta_integration_test.go index 674a8d659..776d4fc99 100644 --- a/internal/stackql/driver/okta_integration_test.go +++ b/internal/stackql/driver/okta_integration_test.go @@ -72,7 +72,7 @@ func TestSelectOktaApplicationAppsDriver(t *testing.T) { t.Logf("simple select driver integration test passed") } -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestSimpleSelectOktaApplicationAppsDriverOutput(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "text", "TestSimpleSelectOktaApplicationAppsDriverOutput") if err != nil { diff --git a/internal/stackql/driver/routine_complex_integration_test.go b/internal/stackql/driver/routine_complex_integration_test.go index cec20a18f..582d655ac 100644 --- a/internal/stackql/driver/routine_complex_integration_test.go +++ b/internal/stackql/driver/routine_complex_integration_test.go @@ -18,7 +18,7 @@ import ( lrucache "github.com/stackql/stackql-parser/go/cache" ) -//nolint:govet,lll,errcheck // legacy test +//nolint:govet,lll // legacy test func TestUnionAllSelectComputeDisksOrderByCrtTmstpAscPlusCoalesceJsonExtract(t *testing.T) { runtimeCtx, err := stackqltestutil.GetRuntimeCtx(testobjects.GetGoogleProviderString(), "csv", "TestUnionAllSelectComputeDisksOrderByCrtTmstpAscPlusCoalesceJsonExtract") if err != nil { diff --git a/internal/stackql/execution/mono_valent_execution.go b/internal/stackql/execution/mono_valent_execution.go index 58bdd243f..5482e61dc 100644 --- a/internal/stackql/execution/mono_valent_execution.go +++ b/internal/stackql/execution/mono_valent_execution.go @@ -1472,7 +1472,7 @@ func shimProcessHTTP( return httpResponse, nil } -//nolint:funlen,gocognit // acceptable for now +//nolint:funlen,gocognit,errcheck // acceptable for now func GetMonitorExecutor( handlerCtx handler.HandlerContext, provider anysdk.Provider, diff --git a/internal/stackql/internal_data_transfer/internaldto/heirarchy_identifiers.go b/internal/stackql/internal_data_transfer/internaldto/heirarchy_identifiers.go index c74f51ac4..22eb1b7b0 100644 --- a/internal/stackql/internal_data_transfer/internaldto/heirarchy_identifiers.go +++ b/internal/stackql/internal_data_transfer/internaldto/heirarchy_identifiers.go @@ -10,7 +10,7 @@ import ( var ( _ HeirarchyIdentifiers = &standardHeirarchyIdentifiers{} - pgInternalObjectRegex *regexp.Regexp = regexp.MustCompile(`^pg_.*`) //nolint:revive // prefer declarative + pgInternalObjectRegex *regexp.Regexp = regexp.MustCompile(`^pg_.*`) ) type HeirarchyIdentifiers interface { diff --git a/internal/stackql/mcpbackend/mcp_service_stackql.go b/internal/stackql/mcpbackend/mcp_service_stackql.go new file mode 100644 index 000000000..f85629a81 --- /dev/null +++ b/internal/stackql/mcpbackend/mcp_service_stackql.go @@ -0,0 +1,479 @@ +package mcpbackend + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/sirupsen/logrus" + "github.com/stackql/stackql/internal/stackql/acid/tsm_physio" + "github.com/stackql/stackql/internal/stackql/handler" + "github.com/stackql/stackql/internal/stackql/internal_data_transfer/internaldto" + "github.com/stackql/stackql/pkg/mcp_server" + "github.com/stackql/stackql/pkg/presentation" +) + +var ( + _ mcp_server.Backend = mcp_server.Backend(nil) +) + +const ( + resultsFormatMarkdown = "markdown" + resultsFormatJSON = "json" + unlimitedRowLimit int = -1 +) + +type StackqlInterrogator interface { + // This struct is responsible for interrogating the StackQL engine. + // Each method provides the requisite query string. + + GetShowProviders(mcp_server.HierarchyInput, string) (string, error) + GetShowServices(mcp_server.HierarchyInput, string) (string, error) + GetShowResources(mcp_server.HierarchyInput, string) (string, error) + GetShowMethods(mcp_server.HierarchyInput) (string, error) + // GetShowTables(mcp_server.HierarchyInput) (string, error) + GetDescribeTable(mcp_server.HierarchyInput) (string, error) + GetForeignKeys(mcp_server.HierarchyInput) (string, error) + FindRelationships(mcp_server.HierarchyInput) (string, error) + GetQuery(mcp_server.QueryInput) (string, error) + GetQueryJSON(mcp_server.QueryJSONInput) (string, error) + // GetListTableResources(mcp_server.HierarchyInput) (string, error) + // GetReadTableResource(mcp_server.HierarchyInput) (string, error) + GetPromptWriteSafeSelectTool() (string, error) + // GetPromptExplainPlanTipsTool() (string, error) + // GetListTablesJSON(mcp_server.ListTablesInput) (string, error) + // GetListTablesJSONPage(mcp_server.ListTablesPageInput) (string, error) +} + +type simpleStackqlInterrogator struct{} + +func NewSimpleStackqlInterrogator() StackqlInterrogator { + return &simpleStackqlInterrogator{} +} + +func (s *simpleStackqlInterrogator) GetShowProviders(_ mcp_server.HierarchyInput, likeStr string) (string, error) { + sb := strings.Builder{} + sb.WriteString("SHOW PROVIDERS") + if likeStr != "" { + sb.WriteString(" LIKE '") + sb.WriteString(likeStr) + sb.WriteString("'") + } + return sb.String(), nil +} + +func (s *simpleStackqlInterrogator) GetShowServices(hI mcp_server.HierarchyInput, likeStr string) (string, error) { + sb := strings.Builder{} + sb.WriteString("SHOW SERVICES") + if hI.Provider == "" { + return "", fmt.Errorf("provider not specified") + } + sb.WriteString(" IN ") + sb.WriteString(hI.Provider) + if likeStr != "" { + sb.WriteString(" LIKE '") + sb.WriteString(likeStr) + sb.WriteString("'") + } + return sb.String(), nil +} + +func (s *simpleStackqlInterrogator) GetShowResources(hI mcp_server.HierarchyInput, likeString string) (string, error) { + sb := strings.Builder{} + sb.WriteString("SHOW RESOURCES") + if hI.Provider == "" || hI.Service == "" { + return "", fmt.Errorf("provider and / or service not specified") + } + sb.WriteString(" IN ") + sb.WriteString(hI.Provider) + if hI.Service != "" { + sb.WriteString(".") + sb.WriteString(hI.Service) + } + if likeString != "" { + sb.WriteString(" LIKE '") + sb.WriteString(likeString) + sb.WriteString("'") + } + return sb.String(), nil +} + +func (s *simpleStackqlInterrogator) GetShowMethods(hI mcp_server.HierarchyInput) (string, error) { + sb := strings.Builder{} + sb.WriteString("SHOW METHODS") + if hI.Provider == "" || hI.Service == "" || hI.Resource == "" { + return "", fmt.Errorf("provider, service and / or resource not specified") + } + sb.WriteString(" IN ") + sb.WriteString(hI.Provider) + if hI.Service != "" { + sb.WriteString(".") + sb.WriteString(hI.Service) + } + if hI.Resource != "" { + sb.WriteString(".") + sb.WriteString(hI.Resource) + } + return sb.String(), nil +} + +func (s *simpleStackqlInterrogator) GetDescribeTable(hI mcp_server.HierarchyInput) (string, error) { + sb := strings.Builder{} + sb.WriteString("DESCRIBE TABLE") + if hI.Provider == "" || hI.Service == "" || hI.Resource == "" { + return "", fmt.Errorf("provider, service and / or resource not specified") + } + sb.WriteString(" ") + sb.WriteString(hI.Provider) + if hI.Service != "" { + sb.WriteString(".") + sb.WriteString(hI.Service) + } + if hI.Resource != "" { + sb.WriteString(".") + sb.WriteString(hI.Resource) + } + return sb.String(), nil +} + +func (s *simpleStackqlInterrogator) GetForeignKeys(hI mcp_server.HierarchyInput) (string, error) { + return mcp_server.ExplainerForeignKeyStackql, nil +} + +func (s *simpleStackqlInterrogator) FindRelationships(hI mcp_server.HierarchyInput) (string, error) { + return mcp_server.ExplainerFindRelationships, nil +} + +func (s *simpleStackqlInterrogator) GetQuery(qI mcp_server.QueryInput) (string, error) { + if qI.SQL == "" { + return "", fmt.Errorf("no SQL provided") + } + return qI.SQL, nil +} + +func (s *simpleStackqlInterrogator) GetQueryJSON(qI mcp_server.QueryJSONInput) (string, error) { + if qI.SQL == "" { + return "", fmt.Errorf("no SQL provided") + } + return qI.SQL, nil +} + +func (s *simpleStackqlInterrogator) GetPromptWriteSafeSelectTool() (string, error) { + return mcp_server.ExplainerPromptWriteSafeSelectTool, nil +} + +// func (s *simpleStackqlInterrogator) composeWhereClause(params map[string]any) (string, error) { +// sb := strings.Builder{} +// sb.WriteString(" WHERE ") +// for key, value := range params { +// sb.WriteString(fmt.Sprintf("%s = '%v' AND ", key, value)) +// } +// // Remove the trailing " AND " +// whereClause := strings.TrimSuffix(sb.String(), " AND ") +// return whereClause, nil +// } + +// func (s *simpleStackqlInterrogator) GetReadTableResource(hI mcp_server.HierarchyInput) (string, error) { +// sb := strings.Builder{} +// sb.WriteString("SELECT * FROM") +// if hI.Provider == "" || hI.Service == "" || hI.Resource == "" { +// return "", fmt.Errorf("provider, service and / or resource not specified") +// } +// sb.WriteString(" ") +// sb.WriteString(hI.Provider) +// if hI.Service != "" { +// sb.WriteString(".") +// sb.WriteString(hI.Service) +// } +// if hI.Resource != "" { +// sb.WriteString(".") +// sb.WriteString(hI.Resource) +// } +// if len(hI.Parameters) > 0 { +// whereClause, err := s.composeWhereClause(hI.Parameters) +// if err != nil { +// return "", err +// } +// sb.WriteString(" " + whereClause) +// } +// return sb.String(), nil +// } + +type stackqlMCPService struct { + isReadOnly bool + txnOrchestrator tsm_physio.Orchestrator + interrogator StackqlInterrogator + handlerCtx handler.HandlerContext + logger *logrus.Logger +} + +func NewStackqlMCPBackendService( + isReadOnly bool, + txnOrchestrator tsm_physio.Orchestrator, + handlerCtx handler.HandlerContext, + logger *logrus.Logger, +) (mcp_server.Backend, error) { + if logger == nil { + logger = logrus.New() + logger.SetLevel(logrus.InfoLevel) + } + if handlerCtx == nil { + return nil, fmt.Errorf("handler context is nil") + } + if txnOrchestrator == nil { + return nil, fmt.Errorf("transaction orchestrator is nil") + } + return &stackqlMCPService{ + isReadOnly: isReadOnly, + txnOrchestrator: txnOrchestrator, + interrogator: NewSimpleStackqlInterrogator(), + logger: logger, + handlerCtx: handlerCtx, + }, nil +} + +func (b *stackqlMCPService) getDefaultFormat() string { + return resultsFormatMarkdown +} + +func (b *stackqlMCPService) Ping(ctx context.Context) error { + return nil +} + +func (b *stackqlMCPService) Close() error { + return nil +} + +// Server and environment info +func (b *stackqlMCPService) ServerInfo(ctx context.Context, args any) (mcp_server.ServerInfoOutput, error) { + return mcp_server.ServerInfoOutput{ + Name: "Stackql MCP Service", + Info: "This is the Stackql MCP Service.", + IsReadOnly: b.isReadOnly, + }, nil +} + +// Current DB identity details +func (b *stackqlMCPService) DBIdentity(ctx context.Context, args any) (map[string]any, error) { + return map[string]any{ + "identity": "stackql_mcp_service", + }, nil +} + +func (b *stackqlMCPService) Greet(ctx context.Context, args mcp_server.GreetInput) (string, error) { + return "Hi " + args.Name, nil +} + +func (b *stackqlMCPService) RunQuery(ctx context.Context, args mcp_server.QueryInput) (string, error) { + q, qErr := b.interrogator.GetQuery(args) + if qErr != nil { + return "", qErr + } + rv := b.renderQueryResults(q, args.Format, args.RowLimit) + return rv, nil +} + +func (b *stackqlMCPService) RunQueryJSON(ctx context.Context, input mcp_server.QueryJSONInput) ([]map[string]interface{}, error) { + q := input.SQL + if q == "" { + return nil, fmt.Errorf("no SQL provided") + } + results, ok := b.extractQueryResults(q, input.RowLimit) + if !ok { + return nil, fmt.Errorf("failed to extract query results") + } + return results, nil +} + +// func (b *stackqlMCPService) ListTableResources(ctx context.Context, hI mcp_server.HierarchyInput) ([]string, error) { +// return []string{}, nil +// } + +// func (b *stackqlMCPService) ReadTableResource(ctx context.Context, hI mcp_server.HierarchyInput) ([]map[string]interface{}, error) { +// return []map[string]interface{}{}, nil +// } + +func (b *stackqlMCPService) PromptWriteSafeSelectTool(ctx context.Context, args mcp_server.HierarchyInput) (string, error) { + return b.interrogator.GetPromptWriteSafeSelectTool() +} + +// func (b *stackqlMCPService) PromptExplainPlanTipsTool(ctx context.Context) (string, error) { +// return "stub", nil +// } + +func (b *stackqlMCPService) ListTablesJSON(ctx context.Context, input mcp_server.ListTablesInput) ([]map[string]interface{}, error) { + hI := mcp_server.HierarchyInput{} + likeStr := "" + if input.Hierarchy != nil { + hI = *input.Hierarchy + } + if input.NameLike != nil { + likeStr = *input.NameLike + } + q, qErr := b.interrogator.GetShowResources(hI, likeStr) + if qErr != nil { + return nil, qErr + } + results, ok := b.extractQueryResults(q, input.RowLimit) + if !ok { + return nil, fmt.Errorf("failed to extract query results") + } + return results, nil +} + +func (b *stackqlMCPService) ListTablesJSONPage(ctx context.Context, input mcp_server.ListTablesPageInput) (map[string]interface{}, error) { + return map[string]interface{}{}, nil +} + +func (b *stackqlMCPService) ListTables(ctx context.Context, hI mcp_server.HierarchyInput) (string, error) { + return b.ListResources(ctx, hI) +} + +func (b *stackqlMCPService) ListMethods(ctx context.Context, hI mcp_server.HierarchyInput) (string, error) { + q, qErr := b.interrogator.GetShowMethods(hI) + if qErr != nil { + return "", qErr + } + rv := b.renderQueryResults(q, hI.Format, hI.RowLimit) + return rv, nil +} + +func (b *stackqlMCPService) getUpdatedHandlerCtx(query string) (handler.HandlerContext, error) { + clonedCtx := b.handlerCtx.Clone() + clonedCtx.SetRawQuery(query) + return clonedCtx, nil +} + +func (b *stackqlMCPService) applyQuery(query string) ([]internaldto.ExecutorOutput, bool) { + updatedCtx, ctxErr := b.getUpdatedHandlerCtx(query) + if ctxErr != nil { + return nil, false + } + r, ok := b.txnOrchestrator.ProcessQueryOrQueries(updatedCtx) + return r, ok +} + +func (b *stackqlMCPService) extractQueryResults(query string, rowLimit int) ([]map[string]interface{}, bool) { + r, ok := b.applyQuery(query) + var rv []map[string]interface{} + rowCount := 0 + for _, resp := range r { + sqlRowStream := resp.GetSQLResult() + if sqlRowStream == nil { + ok = false + break + } + for { + row, err := sqlRowStream.Read() + if err == io.EOF { + rowArr := row.ToArr() + rv = append(rv, rowArr...) + break + } + if err != nil || row == nil { + ok = false + break + } + rowArr := row.ToArr() + rv = append(rv, rowArr...) + rowCount += len(rowArr) + if rowLimit > 0 && rowCount >= rowLimit { + break + } + } + } + return rv, (ok && len(rv) > 0) +} + +func (b *stackqlMCPService) renderQueryResultsAsMarkdown(results []map[string]interface{}) string { + if len(results) == 0 { + return "**no results**" + } + var sb strings.Builder + headerRow := presentation.NewMarkdownRowFromMap(results[0]) + sb.WriteString(headerRow.HeaderString() + "\n") + sb.WriteString(headerRow.SeparatorString() + "\n") + for _, row := range results[1:] { + markdownRow := presentation.NewMarkdownRowFromMap(row) + sb.WriteString(markdownRow.RowString() + "\n") + } + return sb.String() +} + +func (b *stackqlMCPService) renderQueryResultsAsJSON(results []map[string]interface{}) string { + if len(results) == 0 { + return `{"error": "**no results**"}` + } + jsonData, err := json.Marshal(results) + if err != nil { + return fmt.Sprintf(`{"error": "%v"}`, err) + } + return string(jsonData) +} + +func (b *stackqlMCPService) renderQueryResults(query string, format string, rowLimit int) string { + results, ok := b.extractQueryResults(query, rowLimit) + if format == "" { + format = b.getDefaultFormat() + } + switch format { + case resultsFormatMarkdown: + if !ok || len(results) == 0 { + return "**no results**" + } + return b.renderQueryResultsAsMarkdown(results) + case resultsFormatJSON: + if !ok || len(results) == 0 { + return `{"error": "**no results**"}` + } + return b.renderQueryResultsAsJSON(results) + default: + return fmt.Sprintf("unsupported format: %s", format) + } +} + +func (b *stackqlMCPService) DescribeTable(ctx context.Context, hI mcp_server.HierarchyInput) (string, error) { + q, qErr := b.interrogator.GetDescribeTable(hI) + if qErr != nil { + return "", qErr + } + rv := b.renderQueryResults(q, hI.Format, hI.RowLimit) + return rv, nil +} + +func (b *stackqlMCPService) GetForeignKeys(ctx context.Context, hI mcp_server.HierarchyInput) (string, error) { + return b.interrogator.GetForeignKeys(hI) +} + +func (b *stackqlMCPService) FindRelationships(ctx context.Context, hI mcp_server.HierarchyInput) (string, error) { + return b.interrogator.FindRelationships(hI) +} + +func (b *stackqlMCPService) ListProviders(ctx context.Context) (string, error) { + q, qErr := b.interrogator.GetShowProviders(mcp_server.HierarchyInput{}, "") + if qErr != nil { + return "", qErr + } + rv := b.renderQueryResults(q, "", unlimitedRowLimit) + return rv, nil +} + +func (b *stackqlMCPService) ListServices(ctx context.Context, hI mcp_server.HierarchyInput) (string, error) { + q, qErr := b.interrogator.GetShowServices(hI, "") + if qErr != nil { + return "", qErr + } + rv := b.renderQueryResults(q, hI.Format, hI.RowLimit) + return rv, nil +} + +func (b *stackqlMCPService) ListResources(ctx context.Context, hI mcp_server.HierarchyInput) (string, error) { + q, qErr := b.interrogator.GetShowResources(hI, "") + if qErr != nil { + return "", qErr + } + rv := b.renderQueryResults(q, hI.Format, hI.RowLimit) + return rv, nil +} diff --git a/internal/stackql/output/output.go b/internal/stackql/output/output.go index 4a309bc1e..be408eddf 100644 --- a/internal/stackql/output/output.go +++ b/internal/stackql/output/output.go @@ -58,13 +58,8 @@ func GetOutputWriter( ci := pgtype.NewConnInfo() switch outputCtx.RuntimeContext.OutputFormat { case constants.JSONStr: - jsonWriter := JSONWriter{ - ci: ci, - writer: writer, - errWriter: errWriter, - outputCtx: outputCtx, - } - return &jsonWriter, nil + jsonWriter := NewJSONWriter(writer, errWriter) + return jsonWriter, nil case constants.TableStr: tablewriter := TableWriter{ AbstractTabularWriter{ @@ -112,10 +107,15 @@ func GetOutputWriter( } type JSONWriter struct { - ci *pgtype.ConnInfo writer io.Writer errWriter io.Writer - outputCtx internaldto.OutputContext +} + +func NewJSONWriter(writer io.Writer, errWriter io.Writer) IOutputWriter { + return &JSONWriter{ + writer: writer, + errWriter: errWriter, + } } type AbstractTabularWriter struct { @@ -147,44 +147,19 @@ type PrettyWriter struct { errWriter io.Writer } -func resToArr(res sqldata.ISQLResult) []map[string]interface{} { - var keys []string - for _, col := range res.GetColumns() { - keys = append(keys, col.GetName()) - } - var retVal []map[string]interface{} - for _, r := range res.GetRows() { - rowArr := r.GetRowDataNaive() - if len(rowArr) == 0 { - continue - } - rm := make(map[string]interface{}) - for i, c := range keys { - switch tp := rowArr[i].(type) { - case []byte: - rm[c] = string(tp) - default: - rm[c] = tp - } - } - retVal = append(retVal, rm) - } - return retVal -} - func (jw *JSONWriter) writeRowsFromResult(res sqldata.ISQLResultStream) error { for { r, err := res.Read() logging.GetLogger().Debugln(fmt.Sprintf("result from stream: %v", r)) if err != nil { if errors.Is(err, io.EOF) { - rowsArr := resToArr(r) + rowsArr := r.ToArr() jw.writeRows(rowsArr) //nolint:errcheck // output stream is not critical return nil } return err } - rowsArr := resToArr(r) + rowsArr := r.ToArr() jw.writeRows(rowsArr) //nolint:errcheck // output stream is not critical } } diff --git a/internal/stackql/planbuilder/entrypoint.go b/internal/stackql/planbuilder/entrypoint.go index 081e79d69..0d07c535f 100644 --- a/internal/stackql/planbuilder/entrypoint.go +++ b/internal/stackql/planbuilder/entrypoint.go @@ -38,7 +38,7 @@ func (pb *standardPlanBuilder) BuildUndoPlanFromContext(_ handler.HandlerContext return nil, nil } -//nolint:funlen,gocognit // no big deal +//nolint:funlen,gocognit,errcheck // no big deal func (pb *standardPlanBuilder) BuildPlanFromContext(handlerCtx handler.HandlerContext) (plan.Plan, error) { defer handlerCtx.GetGarbageCollector().Close() tcc, err := internaldto.NewTxnControlCounters(handlerCtx.GetTxnCounterMgr()) diff --git a/internal/stackql/planbuilder/plan_builder.go b/internal/stackql/planbuilder/plan_builder.go index 97d0184e3..1db2c1f96 100644 --- a/internal/stackql/planbuilder/plan_builder.go +++ b/internal/stackql/planbuilder/plan_builder.go @@ -310,8 +310,8 @@ func (pgb *standardPlanGraphBuilder) handleDescribe(pbi planbuilderinput.PlanBui if err != nil { return err } - var extended bool = strings.TrimSpace(strings.ToUpper(node.Extended)) == "EXTENDED" //nolint:revive // acceptable - var full bool = strings.TrimSpace(strings.ToUpper(node.Full)) == "FULL" //nolint:revive // acceptable + var extended bool = strings.TrimSpace(strings.ToUpper(node.Extended)) == "EXTENDED" + var full bool = strings.TrimSpace(strings.ToUpper(node.Full)) == "FULL" _, isView := md.GetHeirarchyObjects().GetHeirarchyIDs().GetView() if isView { stmtCtx, sOk := primitiveGenerator.GetPrimitiveComposer().GetIndirectDescribeSelectCtx() diff --git a/internal/stackql/planbuilderinput/deprecated.go b/internal/stackql/planbuilderinput/deprecated.go index f2054b5e3..9a3970e4b 100644 --- a/internal/stackql/planbuilderinput/deprecated.go +++ b/internal/stackql/planbuilderinput/deprecated.go @@ -8,7 +8,6 @@ import ( "github.com/stackql/stackql/internal/stackql/sqlstream" ) -//nolint:revive // prefer declarative var ( multipleWhitespaceRegexp *regexp.Regexp = regexp.MustCompile(`\s+`) getOidsRegexp *regexp.Regexp = regexp.MustCompile(`(?i)select\s+t\.oid,\s+(?:NULL|typarray)\s+from.*pg_type`) //nolint:lll // long string diff --git a/internal/stackql/primitivebuilder/shortcuts.go b/internal/stackql/primitivebuilder/shortcuts.go index 3b6e7d4a8..c68e4cf52 100644 --- a/internal/stackql/primitivebuilder/shortcuts.go +++ b/internal/stackql/primitivebuilder/shortcuts.go @@ -329,6 +329,7 @@ func convertProviderServicesToMap( return retVal } +//nolint:errcheck // future proofing func filterServices( services map[string]anysdk.ProviderService, tableFilter func(anysdk.ITable) (anysdk.ITable, error), diff --git a/internal/stackql/primitivegenerator/statement_analyzer.go b/internal/stackql/primitivegenerator/statement_analyzer.go index d1ead5af0..0bd3d1077 100644 --- a/internal/stackql/primitivegenerator/statement_analyzer.go +++ b/internal/stackql/primitivegenerator/statement_analyzer.go @@ -37,7 +37,6 @@ import ( "github.com/stackql/stackql-parser/go/vt/sqlparser" ) -//nolint:revive // prefer this way var ( synonymJSONRegexp *regexp.Regexp = regexp.MustCompile(`^application/[\S]*json[\S]*$`) synonymXMLRegexp *regexp.Regexp = regexp.MustCompile(`^(?:application|text)/[\S]*xml[\S]*$`) diff --git a/internal/stackql/provider/generic.go b/internal/stackql/provider/generic.go index 680079ad0..dfc68989b 100644 --- a/internal/stackql/provider/generic.go +++ b/internal/stackql/provider/generic.go @@ -27,7 +27,7 @@ import ( ) var ( - //nolint:revive,unused // prefer declarative + //nolint:unused // prefer declarative gitHubLinksNextRegex *regexp.Regexp = regexp.MustCompile(`.*<(?P[^>]*)>;\ rel="next".*`) ) diff --git a/internal/stackql/sql_system/postgres.go b/internal/stackql/sql_system/postgres.go index 2856b729c..079f3e63b 100644 --- a/internal/stackql/sql_system/postgres.go +++ b/internal/stackql/sql_system/postgres.go @@ -219,6 +219,7 @@ func (eng *postgresSystem) ObtainRelationalColumnFromExternalSQLtable( return eng.obtainRelationalColumnFromExternalSQLtable(hierarchyIDs, colName) } +//nolint:gosec // who cares func (eng *postgresSystem) obtainRelationalColumnsFromExternalSQLtable( hierarchyIDs internaldto.HeirarchyIdentifiers, ) ([]typing.RelationalColumn, error) { @@ -300,6 +301,7 @@ func (eng *postgresSystem) getSQLExternalSchema(providerName string) string { return rv } +//nolint:gosec // who cares func (eng *postgresSystem) obtainRelationalColumnFromExternalSQLtable( hierarchyIDs internaldto.HeirarchyIdentifiers, colName string, @@ -766,7 +768,7 @@ func (eng *postgresSystem) GetMaterializedViewByName(viewName string) (internald return rv, ok } -//nolint:errcheck // TODO: establish pattern +//nolint:errcheck,gosec // TODO: establish pattern func (eng *postgresSystem) getMaterializedViewByName( naiveViewName string, txn *sql.Tx) (internaldto.RelationDTO, bool) { fullyQualifiedRelationName := eng.getFullyQualifiedRelationName(naiveViewName) @@ -843,7 +845,7 @@ func (eng *postgresSystem) GetPhysicalTableByName( // TODO: implement temp tables // -//nolint:errcheck // TODO: establish pattern +//nolint:errcheck,gosec // TODO: establish pattern func (eng *postgresSystem) getTableByName( naiveTableName string, txn *sql.Tx) (internaldto.RelationDTO, bool) { diff --git a/internal/stackql/sql_system/setup_scripts.go b/internal/stackql/sql_system/setup_scripts.go index d24a40b77..28c650e14 100644 --- a/internal/stackql/sql_system/setup_scripts.go +++ b/internal/stackql/sql_system/setup_scripts.go @@ -1,4 +1,4 @@ -package sql_system //nolint:revive,stylecheck // package name is meaningful and readable +package sql_system //nolint:stylecheck,revive // package name is meaningful and readable import ( "github.com/stackql/any-sdk/public/sqlengine" diff --git a/internal/stackql/sql_system/sql_system.go b/internal/stackql/sql_system/sql_system.go index e226cc946..74ccf6ac1 100644 --- a/internal/stackql/sql_system/sql_system.go +++ b/internal/stackql/sql_system/sql_system.go @@ -1,4 +1,4 @@ -package sql_system //nolint:revive,stylecheck // package name is meaningful and readable +package sql_system //nolint:stylecheck,revive // package name is meaningful and readable import ( "database/sql" diff --git a/internal/stackql/sql_system/sqlite.go b/internal/stackql/sql_system/sqlite.go index fd3313dbf..de6b2f7f6 100644 --- a/internal/stackql/sql_system/sqlite.go +++ b/internal/stackql/sql_system/sqlite.go @@ -297,6 +297,7 @@ func (eng *sqLiteSystem) getSQLExternalSchema(providerName string) string { return rv } +//nolint:gosec // who cares func (eng *sqLiteSystem) obtainRelationalColumnsFromExternalSQLtable( hierarchyIDs internaldto.HeirarchyIdentifiers, ) ([]typing.RelationalColumn, error) { @@ -362,6 +363,7 @@ func (eng *sqLiteSystem) obtainRelationalColumnsFromExternalSQLtable( return rv, nil } +//nolint:gosec // TODO: establish pattern func (eng *sqLiteSystem) obtainRelationalColumnFromExternalSQLtable( hierarchyIDs internaldto.HeirarchyIdentifiers, colName string, @@ -837,7 +839,7 @@ func (eng *sqLiteSystem) IsRelationExported(relationName string) bool { return matches } -//nolint:errcheck // TODO: establish pattern +//nolint:errcheck,gosec // TODO: establish pattern func (eng *sqLiteSystem) getMaterializedViewByName(naiveViewName string, txn *sql.Tx) (internaldto.RelationDTO, bool) { fullyQualifiedRelationName := eng.getFullyQualifiedRelationName(naiveViewName) q := `SELECT view_ddl FROM "__iql__.materialized_views" WHERE view_name = ? and deleted_dttm IS NULL` @@ -917,7 +919,7 @@ func (eng *sqLiteSystem) GetPhysicalTableByName( // TODO: implement temp tables // -//nolint:errcheck // TODO: establish pattern +//nolint:errcheck,gosec // TODO: establish pattern func (eng *sqLiteSystem) getTableByName( naiveTableName string, txn *sql.Tx, diff --git a/internal/stackql/typing/generic_typing_config.go b/internal/stackql/typing/generic_typing_config.go index 0bcb119fd..0d5802b84 100644 --- a/internal/stackql/typing/generic_typing_config.go +++ b/internal/stackql/typing/generic_typing_config.go @@ -50,7 +50,7 @@ func getTypeMappings(sqlDialect string) (map[string]ORMCoupling, error) { } } -//nolint:goconst,unparam // let it ride +//nolint:unparam // let it ride func getDefaultRelationalType(sqlDialect string) string { switch sqlDialect { case constants.SQLDialectPostgres: @@ -191,7 +191,6 @@ func newTypingConfig(sqlDialect string) (Config, error) { }, nil } -//nolint:goconst // defer cleanup func getOidForSQLDatabaseTypeName(typeName string) oid.Oid { typeNameLowered := strings.ToLower(typeName) switch strings.ToLower(typeNameLowered) { @@ -248,7 +247,6 @@ func (tc *genericTypingConfig) getScannableObjectForNativeResult(colSchema *sql. return new(sql.NullInt64) case "int64", "bigint": return new(sql.NullInt64) - //nolint:goconst // let it ride case "numeric", "decimal", "float", "float32", "float64": return new(sql.NullFloat64) case "bool": diff --git a/internal/stackql/typing/relayed_column_metadata.go b/internal/stackql/typing/relayed_column_metadata.go index 07649d98e..4a542bd8d 100644 --- a/internal/stackql/typing/relayed_column_metadata.go +++ b/internal/stackql/typing/relayed_column_metadata.go @@ -47,6 +47,7 @@ func (cd *relayedColumnMetadata) GetRelationalType() string { return cd.coupling.GetRelationalType() } +//nolint:goconst // ok func (cd *relayedColumnMetadata) getOidForRelationalType(relType string) oid.Oid { relType = strings.ToLower(relType) switch relType { diff --git a/internal/stackql/util/annotated_tabulation.go b/internal/stackql/util/annotated_tabulation.go index 141fc2c6c..5a71a47d7 100644 --- a/internal/stackql/util/annotated_tabulation.go +++ b/internal/stackql/util/annotated_tabulation.go @@ -1,4 +1,4 @@ -package util +package util //nolint:revive // fine for now import ( "github.com/stackql/any-sdk/anysdk" diff --git a/internal/stackql/util/plan_utility.go b/internal/stackql/util/plan_utility.go index 7a8a4827a..4485eb70f 100644 --- a/internal/stackql/util/plan_utility.go +++ b/internal/stackql/util/plan_utility.go @@ -1,4 +1,4 @@ -package util +package util //nolint:revive // fine for now import ( "fmt" diff --git a/internal/stackql/util/utility.go b/internal/stackql/util/utility.go index cfdb38d46..9861d6492 100644 --- a/internal/stackql/util/utility.go +++ b/internal/stackql/util/utility.go @@ -1,4 +1,4 @@ -package util +package util //nolint:revive // fine for now import ( "path" diff --git a/internal/test/testobjects/request_payload.go b/internal/test/testobjects/request_payload.go index 00ad496e5..4deb97099 100644 --- a/internal/test/testobjects/request_payload.go +++ b/internal/test/testobjects/request_payload.go @@ -64,7 +64,7 @@ const ( ` ) -//nolint:revive,gochecknoglobals // This is a test file +//nolint:gochecknoglobals // This is a test file var ( CreateGoogleBQDatasetRequestPayload01 string = fmt.Sprintf(` { diff --git a/mcp_client/.gitignore b/mcp_client/.gitignore new file mode 100644 index 000000000..2c6b18d6b --- /dev/null +++ b/mcp_client/.gitignore @@ -0,0 +1,2 @@ +.stackql +__debug_bin \ No newline at end of file diff --git a/mcp_client/cmd/exec.go b/mcp_client/cmd/exec.go new file mode 100644 index 000000000..79bb6474c --- /dev/null +++ b/mcp_client/cmd/exec.go @@ -0,0 +1,80 @@ +/* +Copyright © 2025 stackql info@stackql.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackql/stackql/pkg/mcp_server" +) + +var ( + actionName string // overwritten by flag + actionArgs string // overwritten by flag +) + +const ( + listToolsAction = "list_tools" + listProvidersAction = "list_providers" +) + +// execCmd represents the exec command. +// +//nolint:gochecknoglobals // cobra pattern +var execCmd = &cobra.Command{ + Use: "exec", + Short: "Run mcp client queries", + Long: `simple mcp client example +`, + Run: func(cmd *cobra.Command, args []string) { + client, setupErr := mcp_server.NewMCPClient( + clientType, + url, + nil, + ) + if setupErr != nil { + panic(fmt.Sprintf("error setting up mcp client: %v", setupErr)) + } + var outputString string + switch actionName { + case listToolsAction: + rv, rvErr := client.InspectTools() + if rvErr != nil { + panic(fmt.Sprintf("error inspecting tools: %v", rvErr)) + } + output, outPutErr := json.MarshalIndent(rv, "", " ") + if outPutErr != nil { + panic(fmt.Sprintf("error marshaling output: %v", outPutErr)) + } + outputString = string(output) + default: + var args map[string]any + jsonErr := json.Unmarshal([]byte(actionArgs), &args) + if jsonErr != nil { + panic(fmt.Sprintf("error unmarshaling action args: %v", jsonErr)) + } + rv, rvErr := client.CallToolText(actionName, args) + if rvErr != nil { + panic(fmt.Sprintf("error calling tool %s: %v", actionName, rvErr)) + } + outputString = rv + } + //nolint:forbidigo // legacy + fmt.Println(outputString) + }, +} diff --git a/mcp_client/cmd/main.go b/mcp_client/cmd/main.go new file mode 100644 index 000000000..732719422 --- /dev/null +++ b/mcp_client/cmd/main.go @@ -0,0 +1,32 @@ +/* +Copyright © 2025 stackql info@stackql.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "fmt" + "os" +) + +func main() { + if err := execute(); err != nil { + fmt.Println(err) //nolint:forbidigo // this is the main entry point + os.Exit(1) + } +} + +func execute() error { + return Execute() +} diff --git a/mcp_client/cmd/root.go b/mcp_client/cmd/root.go new file mode 100644 index 000000000..8c515270f --- /dev/null +++ b/mcp_client/cmd/root.go @@ -0,0 +1,103 @@ +/* +Copyright © 2025 stackql info@stackql.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stackql/stackql/pkg/mcp_server" +) + +var ( + clientType = "http" + url = "127.0.0.1:9191" +) + +//nolint:revive,gochecknoglobals // explicit preferred +var ( + BuildMajorVersion string = "" + BuildMinorVersion string = "" + BuildPatchVersion string = "" + BuildCommitSHA string = "" + BuildShortCommitSHA string = "" + BuildDate string = "" + BuildPlatform string = "" +) + +// rootCmd represents the base command when called without any subcommands. +// +//nolint:gochecknoglobals // global vars are a pattern for this lib +var rootCmd = &cobra.Command{ + Use: "stackql_mcp_client", + Version: "0.1.0", + Short: "Cloud asset management and automation using SQL", + Long: `stackql mcp client`, + //nolint:revive // acceptable for now + Run: func(cmd *cobra.Command, args []string) { + // in the root command is executed with no arguments, print the help message + usagemsg := cmd.Long + "\n\n" + cmd.UsageString() + fmt.Println(usagemsg) //nolint:forbidigo // legacy + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() error { + return rootCmd.Execute() +} + +//nolint:lll,funlen,gochecknoinits,mnd // init is a pattern for this lib +func init() { + cobra.OnInitialize(initConfig) + rootCmd.SetVersionTemplate("stackql v{{.Version}} " + BuildPlatform + " (" + BuildShortCommitSHA + ")\nBuildDate: " + BuildDate + "\nhttps://stackql.io\n") + + rootCmd.PersistentFlags().StringVar(&clientType, "client-type", mcp_server.MCPClientTypeSTDIO, "MCP client type (http or stdio for now)") + rootCmd.PersistentFlags().StringVar(&url, "url", "http://127.0.0.1:9876", "MCP server URL. Relevant for http and sse client types.") + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // var dummyString string + + // rootCmd.PersistentFlags().StringVar(&runtimeCtx.DBInternalCfgRaw, dto.DBInternalCfgRawKey, "{}", "JSON / YAML string to configure DBMS housekeeping query handling") + + rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.AddCommand(execCmd) + execCmd.PersistentFlags().StringVar(&actionName, "exec.action", "list_tools", "MCP server action name") + execCmd.PersistentFlags().StringVar(&actionArgs, "exec.args", "{}", "MCP server action arguments as JSON string") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + // mergeConfigFromFile(&runtimeCtx, *rootCmd.PersistentFlags()) + + // logging.SetLogger(runtimeCtx.LogLevelStr) + // config.CreateDirIfNotExists(runtimeCtx.ApplicationFilesRootPath, os.FileMode(runtimeCtx.ApplicationFilesRootPathMode)) //nolint:errcheck,lll // TODO: investigate + // config.CreateDirIfNotExists(path.Join(runtimeCtx.ApplicationFilesRootPath, runtimeCtx.ProviderStr), os.FileMode(runtimeCtx.ApplicationFilesRootPathMode)) //nolint:errcheck,lll // TODO: investigate + // config.CreateDirIfNotExists(config.GetReadlineDirPath(runtimeCtx), os.FileMode(runtimeCtx.ApplicationFilesRootPathMode)) //nolint:errcheck,lll // TODO: investigate + // viper.SetConfigFile(path.Join(runtimeCtx.ApplicationFilesRootPath, runtimeCtx.ViperCfgFileName)) + // viper.AddConfigPath(runtimeCtx.ApplicationFilesRootPath) + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) //nolint:forbidigo // legacy + } +} diff --git a/pkg/mcp_server/README.md b/pkg/mcp_server/README.md new file mode 100644 index 000000000..005c9b86c --- /dev/null +++ b/pkg/mcp_server/README.md @@ -0,0 +1,270 @@ +# StackQL MCP Server Package + +This package implements a Model Context Protocol (MCP) server for StackQL, enabling LLMs to consume StackQL as a first-class information source. + +## Overview + +The `mcp_server` package provides: + +1. **Backend Interface Abstraction**: A clean interface for executing queries that can be implemented for in-memory, TCP, or other communication methods +2. **Configuration Management**: Comprehensive configuration structures with JSON and YAML support +3. **MCP Server Implementation**: A complete MCP server supporting multiple transports (stdio, TCP, WebSocket) + +## Architecture + +The package is designed with zero dependencies on StackQL internals, making it modular and reusable. The key components are: + +- `Backend`: Interface for query execution and schema retrieval +- `Config`: Configuration structures with validation +- `MCPServer`: Main server implementation supporting MCP protocol +- `ExampleBackend`: Sample implementation for testing and demonstration + +## Usage + +### Basic Usage + +```go +package main + +import ( + "context" + "log" + + "github.com/stackql/stackql/pkg/mcp_server" +) + +func main() { + // Create server with default configuration and example backend + server, err := mcp_server.NewMCPServerWithExampleBackend(nil) + if err != nil { + log.Fatal(err) + } + + // Start the server + ctx := context.Background() + if err := server.Start(ctx); err != nil { + log.Fatal(err) + } + + // Server will run until context is cancelled + <-ctx.Done() + + // Graceful shutdown + server.Stop(context.Background()) +} +``` + +### Custom Configuration + +```go +config := &mcp_server.Config{ + Server: mcp_server.ServerConfig{ + Name: "My StackQL MCP Server", + Version: "1.0.0", + Description: "Custom MCP server for StackQL", + MaxConcurrentRequests: 50, + RequestTimeout: mcp_server.Duration(30 * time.Second), + }, + Backend: mcp_server.BackendConfig{ + Type: "stackql", + ConnectionString: "stackql://localhost:5432", + MaxConnections: 20, + ConnectionTimeout: mcp_server.Duration(10 * time.Second), + QueryTimeout: mcp_server.Duration(60 * time.Second), + }, + Transport: mcp_server.TransportConfig{ + EnabledTransports: []string{"stdio", "tcp"}, + TCP: mcp_server.TCPTransportConfig{ + Address: "0.0.0.0", + Port: 8080, + }, + }, + Logging: mcp_server.LoggingConfig{ + Level: "info", + Format: "json", + Output: "/var/log/mcp-server.log", + }, +} + +server, err := mcp_server.NewMCPServer(config, backend, logger) +``` + +### Implementing a Custom Backend + +```go +type MyBackend struct { + // Your backend implementation +} + +func (b *MyBackend) Execute(ctx context.Context, query string, params map[string]interface{}) (*mcp_server.QueryResult, error) { + // Execute the query using your preferred method + // Return structured results +} + +func (b *MyBackend) GetSchema(ctx context.Context) (*mcp_server.Schema, error) { + // Return schema information about available providers and resources +} + +func (b *MyBackend) Ping(ctx context.Context) error { + // Verify backend connectivity +} + +func (b *MyBackend) Close() error { + // Clean up resources +} +``` + +## Configuration + +### JSON Configuration Example + +```json +{ + "server": { + "name": "StackQL MCP Server", + "version": "1.0.0", + "description": "Model Context Protocol server for StackQL", + "max_concurrent_requests": 100, + "request_timeout": "30s" + }, + "backend": { + "type": "stackql", + "connection_string": "stackql://localhost", + "max_connections": 10, + "connection_timeout": "10s", + "query_timeout": "30s", + "retry": { + "enabled": true, + "max_attempts": 3, + "initial_delay": "100ms", + "max_delay": "5s", + "multiplier": 2.0 + } + }, + "transport": { + "enabled_transports": ["stdio", "tcp"], + "tcp": { + "address": "localhost", + "port": 8080, + "max_connections": 100, + "read_timeout": "30s", + "write_timeout": "30s" + } + }, + "logging": { + "level": "info", + "format": "text", + "output": "stdout", + "enable_request_logging": false + } +} +``` + +### YAML Configuration Example + +```yaml +server: + name: "StackQL MCP Server" + version: "1.0.0" + description: "Model Context Protocol server for StackQL" + max_concurrent_requests: 100 + request_timeout: "30s" + +backend: + type: "stackql" + connection_string: "stackql://localhost" + max_connections: 10 + connection_timeout: "10s" + query_timeout: "30s" + retry: + enabled: true + max_attempts: 3 + initial_delay: "100ms" + max_delay: "5s" + multiplier: 2.0 + +transport: + enabled_transports: ["stdio", "tcp"] + tcp: + address: "localhost" + port: 8080 + max_connections: 100 + read_timeout: "30s" + write_timeout: "30s" + +logging: + level: "info" + format: "text" + output: "stdout" + enable_request_logging: false +``` + +## MCP Protocol Support + +The server implements the Model Context Protocol specification and supports: + +- **Initialization**: Capability negotiation with MCP clients +- **Resources**: Listing and reading StackQL resources (providers, services, resources) +- **Tools**: Query execution tool for running StackQL queries +- **Multiple Transports**: stdio, TCP, and WebSocket (WebSocket implementation is placeholder) + +### Supported MCP Methods + +- `initialize`: Server initialization and capability negotiation +- `resources/list`: List available StackQL resources +- `resources/read`: Read specific resource data +- `tools/list`: List available tools (StackQL query execution) +- `tools/call`: Execute StackQL queries + +## Transport Support + +### Stdio Transport +- Primary transport for command-line integration +- JSON-RPC over stdin/stdout +- Ideal for shell integrations and CLI tools + +### TCP Transport +- HTTP-based JSON-RPC +- Suitable for network-based integrations +- Configurable address, port, and connection limits + +### WebSocket Transport (Placeholder) +- Real-time bidirectional communication +- Suitable for web applications +- Currently implemented as placeholder + +## Development + +### Testing + +The package includes an example backend for testing: + +```bash +go test ./pkg/mcp_server/... +``` + +### Integration with StackQL + +To integrate with actual StackQL: + +1. Implement the `Backend` interface using StackQL's query execution engine +2. Map StackQL's schema information to the `Schema` structure +3. Handle StackQL-specific error types and convert them to `BackendError` + +## Dependencies + +The package uses minimal external dependencies: +- `github.com/gorilla/mux`: HTTP routing (already available in StackQL) +- `golang.org/x/sync`: Concurrency utilities (already available in StackQL) +- `gopkg.in/yaml.v2`: YAML configuration support (already available in StackQL) + +No MCP SDK dependency is required as the package implements the MCP protocol directly. + +## Future Enhancements + +1. **Full WebSocket Implementation**: Complete WebSocket transport support +2. **Stdio Transport**: Complete stdio JSON-RPC implementation +3. **Authentication**: Add authentication and authorization support +4. **Streaming**: Support for streaming large query results +5. **Caching**: Query result caching for improved performance +6. **Metrics**: Prometheus metrics for monitoring and observability \ No newline at end of file diff --git a/pkg/mcp_server/backend.go b/pkg/mcp_server/backend.go new file mode 100644 index 000000000..955af530d --- /dev/null +++ b/pkg/mcp_server/backend.go @@ -0,0 +1,303 @@ +package mcp_server //nolint:revive // fine for now + +import ( + "context" + "database/sql/driver" +) + +type Backend interface { + + // Ping verifies the backend connection is active. + Ping(ctx context.Context) error + + // Close gracefully shuts down the backend connection. + Close() error + // Server and environment info + ServerInfo(ctx context.Context, args any) (ServerInfoOutput, error) + + // Current DB identity details + DBIdentity(ctx context.Context, args any) (map[string]any, error) + + Greet(ctx context.Context, args GreetInput) (string, error) + + // Execute a SQL query with typed input (preferred) + RunQuery(ctx context.Context, args QueryInput) (string, error) + + // Execute a SQL query and return JSON rows with typed input (preferred) + RunQueryJSON(ctx context.Context, input QueryJSONInput) ([]map[string]interface{}, error) + + // List resource URIs for tables in a schema + // ListTableResources(ctx context.Context, hI HierarchyInput) ([]string, error) + + // Read rows from a table resource + // ReadTableResource(ctx context.Context, hI HierarchyInput) ([]map[string]interface{}, error) + + // Prompt: guidelines for writing safe SELECT queries + PromptWriteSafeSelectTool(ctx context.Context, args HierarchyInput) (string, error) + + // Prompt: tips for reading EXPLAIN ANALYZE output + // PromptExplainPlanTipsTool(ctx context.Context) (string, error) + + // List tables in a schema with optional filters and return JSON rows + ListTablesJSON(ctx context.Context, input ListTablesInput) ([]map[string]interface{}, error) + + // List tables with pagination and filters + ListTablesJSONPage(ctx context.Context, input ListTablesPageInput) (map[string]interface{}, error) + + // List all schemas in the database + ListProviders(ctx context.Context) (string, error) + + ListServices(ctx context.Context, hI HierarchyInput) (string, error) + + ListResources(ctx context.Context, hI HierarchyInput) (string, error) + + ListMethods(ctx context.Context, hI HierarchyInput) (string, error) + + // List all tables in a specific schema + // ListTables(ctx context.Context, hI HierarchyInput) (string, error) + + // Get detailed information about a table + DescribeTable(ctx context.Context, hI HierarchyInput) (string, error) + + // Get foreign key information for a table + GetForeignKeys(ctx context.Context, hI HierarchyInput) (string, error) + + // Find both explicit and implied relationships for a table + FindRelationships(ctx context.Context, hI HierarchyInput) (string, error) +} + +// QueryResult represents the result of a query execution. +type QueryResult interface { + // GetColumns returns metadata about each column in the result set. + GetColumns() []ColumnInfo + + // GetRows returns the actual data returned by the query. + GetRows() [][]interface{} + + // GetRowsAffected returns the number of rows affected by DML operations. + GetRowsAffected() int64 + + // GetExecutionTime returns the time taken to execute the query in milliseconds. + GetExecutionTime() int64 +} + +// ColumnInfo provides metadata about a result column. +type ColumnInfo interface { + // GetName returns the column name as returned by the query. + GetName() string + + // GetType returns the data type of the column (e.g., "string", "int64", "float64"). + GetType() string + + // IsNullable indicates whether the column can contain null values. + IsNullable() bool +} + +// SchemaProvider represents the metadata structure of available resources. +type SchemaProvider interface { + // GetProviders returns all available providers (e.g., aws, google, azure). + GetProviders() []Provider +} + +// Provider represents a StackQL provider with its services and resources. +type Provider interface { + // GetName returns the provider identifier (e.g., "aws", "google"). + GetName() string + + // GetVersion returns the provider version. + GetVersion() string + + // GetServices returns all services available in this provider. + GetServices() []Service +} + +// Service represents a service within a provider. +type Service interface { + // GetName returns the service identifier (e.g., "ec2", "compute"). + GetName() string + + // GetResources returns all resources available in this service. + GetResources() []Resource +} + +// Resource represents a queryable resource. +type Resource interface { + // GetName returns the resource identifier (e.g., "instances", "buckets"). + GetName() string + + // GetMethods returns the available operations for this resource. + GetMethods() []string + + // GetFields returns the available fields in this resource. + GetFields() []Field +} + +// Field represents a field within a resource. +type Field interface { + // GetName returns the field identifier. + GetName() string + + // GetType returns the field data type. + GetType() string + + // IsRequired indicates if this field is mandatory for certain operations. + IsRequired() bool + + // GetDescription returns human-readable documentation for the field. + GetDescription() string +} + +// BackendError represents an error that occurred in the backend. +type BackendError struct { + // Code is a machine-readable error code. + Code string `json:"code"` + + // Message is a human-readable error message. + Message string `json:"message"` + + // Details contains additional context about the error. + Details map[string]interface{} `json:"details,omitempty"` +} + +func (e *BackendError) Error() string { + return e.Message +} + +// Ensure BackendError implements the driver.Valuer interface for database compatibility. +func (e *BackendError) Value() (driver.Value, error) { + return e.Message, nil +} + +// Private implementations of interfaces + +type queryResult struct { + Columns []ColumnInfo `json:"columns"` + Rows [][]interface{} `json:"rows"` + RowsAffected int64 `json:"rows_affected"` + ExecutionTime int64 `json:"execution_time_ms"` +} + +func (qr *queryResult) GetColumns() []ColumnInfo { return qr.Columns } +func (qr *queryResult) GetRows() [][]interface{} { return qr.Rows } +func (qr *queryResult) GetRowsAffected() int64 { return qr.RowsAffected } +func (qr *queryResult) GetExecutionTime() int64 { return qr.ExecutionTime } + +type columnInfo struct { + Name string `json:"name"` + Type string `json:"type"` + Nullable bool `json:"nullable"` +} + +func (ci *columnInfo) GetName() string { return ci.Name } +func (ci *columnInfo) GetType() string { return ci.Type } +func (ci *columnInfo) IsNullable() bool { return ci.Nullable } + +type schemaProvider struct { + Providers []Provider `json:"providers"` +} + +func (sp *schemaProvider) GetProviders() []Provider { return sp.Providers } + +type provider struct { + Name string `json:"name"` + Version string `json:"version"` + Services []Service `json:"services"` +} + +func (p *provider) GetName() string { return p.Name } +func (p *provider) GetVersion() string { return p.Version } +func (p *provider) GetServices() []Service { return p.Services } + +type service struct { + Name string `json:"name"` + Resources []Resource `json:"resources"` +} + +func (s *service) GetName() string { return s.Name } +func (s *service) GetResources() []Resource { return s.Resources } + +type resource struct { + Name string `json:"name"` + Methods []string `json:"methods"` + Fields []Field `json:"fields"` +} + +func (r *resource) GetName() string { return r.Name } +func (r *resource) GetMethods() []string { return r.Methods } +func (r *resource) GetFields() []Field { return r.Fields } + +type field struct { + Name string `json:"name"` + Type string `json:"type"` + Required bool `json:"required"` + Description string `json:"description,omitempty"` +} + +func (f *field) GetName() string { return f.Name } +func (f *field) GetType() string { return f.Type } +func (f *field) IsRequired() bool { return f.Required } +func (f *field) GetDescription() string { return f.Description } + +// Factory functions + +// NewQueryResult creates a new QueryResult instance. +func NewQueryResult(columns []ColumnInfo, rows [][]interface{}, rowsAffected, executionTime int64) QueryResult { + return &queryResult{ + Columns: columns, + Rows: rows, + RowsAffected: rowsAffected, + ExecutionTime: executionTime, + } +} + +// NewColumnInfo creates a new ColumnInfo instance. +func NewColumnInfo(name, colType string, nullable bool) ColumnInfo { + return &columnInfo{ + Name: name, + Type: colType, + Nullable: nullable, + } +} + +// NewSchemaProvider creates a new SchemaProvider instance. +func NewSchemaProvider(providers []Provider) SchemaProvider { + return &schemaProvider{ + Providers: providers, + } +} + +// NewProvider creates a new Provider instance. +func NewProvider(name, version string, services []Service) Provider { + return &provider{ + Name: name, + Version: version, + Services: services, + } +} + +// NewService creates a new Service instance. +func NewService(name string, resources []Resource) Service { + return &service{ + Name: name, + Resources: resources, + } +} + +// NewResource creates a new Resource instance. +func NewResource(name string, methods []string, fields []Field) Resource { + return &resource{ + Name: name, + Methods: methods, + Fields: fields, + } +} + +// NewField creates a new Field instance. +func NewField(name, fieldType string, required bool, description string) Field { + return &field{ + Name: name, + Type: fieldType, + Required: required, + Description: description, + } +} diff --git a/pkg/mcp_server/client.go b/pkg/mcp_server/client.go new file mode 100644 index 000000000..6df6b7b61 --- /dev/null +++ b/pkg/mcp_server/client.go @@ -0,0 +1,160 @@ +package mcp_server //nolint:revive // fine for now + +// create an http client that can talk to the mcp server + +import ( + "context" + "fmt" + "net/http" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/sirupsen/logrus" +) + +const ( + MCPClientTypeHTTP = "http" + MCPClientTypeSTDIO = "stdio" +) + +type MCPClient interface { + InspectTools() ([]map[string]any, error) + CallToolText(toolName string, args map[string]any) (string, error) +} + +func NewMCPClient(clientType string, baseURL string, logger *logrus.Logger) (MCPClient, error) { + switch clientType { + case MCPClientTypeHTTP: + return newHTTPMCPClient(baseURL, logger) + case MCPClientTypeSTDIO: + return newStdioMCPClient(logger) + default: + return nil, fmt.Errorf("unknown client type: %s", clientType) + } +} + +func newHTTPMCPClient(baseURL string, logger *logrus.Logger) (MCPClient, error) { + if logger == nil { + logger = logrus.New() + logger.SetLevel(logrus.InfoLevel) + } + return &httpMCPClient{ + baseURL: baseURL, + httpClient: http.DefaultClient, + logger: logger, + }, nil +} + +type httpMCPClient struct { + baseURL string + httpClient *http.Client + logger *logrus.Logger +} + +func (c *httpMCPClient) connect() (*mcp.ClientSession, error) { + url := c.baseURL + ctx := context.Background() + + // Create the URL for the server. + c.logger.Infof("Connecting to MCP server at %s", url) + + // Create an MCP client. + client := mcp.NewClient(&mcp.Implementation{ + Name: "stackql-client", + Version: "1.0.0", + }, nil) + + // Connect to the server. + return client.Connect(ctx, &mcp.StreamableClientTransport{Endpoint: url}, nil) +} + +func (c *httpMCPClient) connectOrDie() *mcp.ClientSession { + session, err := c.connect() + if err != nil { + c.logger.Fatalf("Failed to connect: %v", err) + } + return session +} + +func (c *httpMCPClient) InspectTools() ([]map[string]any, error) { + session := c.connectOrDie() + defer session.Close() + + c.logger.Infof("Connected to server (session ID: %s)", session.ID()) + + // First, list available tools. + c.logger.Infof("Listing available tools...") + toolsResult, err := session.ListTools(context.Background(), nil) + if err != nil { + c.logger.Fatalf("Failed to list tools: %v", err) + } + var rv []map[string]any + for _, tool := range toolsResult.Tools { + c.logger.Infof(" - %s: %s\n", tool.Name, tool.Description) + toolInfo := map[string]any{ + "name": tool.Name, + "description": tool.Description, + } + rv = append(rv, toolInfo) + } + + c.logger.Infof("Client completed successfully") + return rv, nil +} + +func (c *httpMCPClient) callTool(toolName string, args map[string]any) (*mcp.CallToolResult, error) { + session := c.connectOrDie() + defer session.Close() + + c.logger.Infof("Connected to server (session ID: %s)", session.ID()) + + c.logger.Infof("Calling tool %s...", toolName) + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: toolName, + Arguments: args, + }) + if err != nil { + c.logger.Errorf("Failed to call tool %s: %v\n", toolName, err) + return result, err + } + + c.logger.Infof("Client completed successfully") + return result, nil +} + +func (c *httpMCPClient) CallToolText(toolName string, args map[string]any) (string, error) { + toolCall, toolCallErr := c.callTool(toolName, args) + if toolCallErr != nil { + return "", toolCallErr + } + var result string + for _, content := range toolCall.Content { + if textContent, ok := content.(*mcp.TextContent); ok { + result += textContent.Text + "\n" + } + } + return result, nil +} + +type stdioMCPClient struct { + logger *logrus.Logger +} + +func newStdioMCPClient(logger *logrus.Logger) (MCPClient, error) { + if logger == nil { + logger = logrus.New() + logger.SetLevel(logrus.InfoLevel) + } + return &stdioMCPClient{ + logger: logger, + }, nil +} + +func (c *stdioMCPClient) InspectTools() ([]map[string]any, error) { + c.logger.Infof("stdio MCP client not implemented yet") + return nil, nil +} + +func (c *stdioMCPClient) CallToolText(toolName string, args map[string]any) (string, error) { + c.logger.Infof("stdio MCP client not implemented yet") + return "", nil +} diff --git a/pkg/mcp_server/config.go b/pkg/mcp_server/config.go new file mode 100644 index 000000000..0f3d4416e --- /dev/null +++ b/pkg/mcp_server/config.go @@ -0,0 +1,208 @@ +package mcp_server //nolint:revive,stylecheck,mnd // fine for now + +import ( + "encoding/json" + "fmt" + "time" + + "gopkg.in/yaml.v2" +) + +// Config represents the complete configuration for the MCP server. +type Config struct { + // Server contains server-specific configuration. + Server ServerConfig `json:"server" yaml:"server"` + + // Backend contains backend-specific configuration. + Backend BackendConfig `json:"backend" yaml:"backend"` +} + +func (c *Config) GetServerTransport() string { + if c.Server.Transport == "" { + return DefaultConfig().Server.Transport + } + return c.Server.Transport +} + +func (c *Config) GetServerAddress() string { + if c.Server.Address == "" { + return DefaultConfig().Server.Address + } + return c.Server.Address +} + +// ServerConfig contains configuration for the MCP server itself. +type ServerConfig struct { + // Name is the server name advertised to clients. + Name string `json:"name" yaml:"name"` + + // Transport specifies the transport configuration for the server. + Transport string `json:"transport" yaml:"transport"` + + // Address is the server Address advertised to clients. + Address string `json:"address" yaml:"address"` + + // Scheme is the protocol scheme used by the server. + Scheme string `json:"scheme" yaml:"scheme"` + + // Version is the server version advertised to clients. + Version string `json:"version" yaml:"version"` + + // Description is a human-readable description of the server. + Description string `json:"description" yaml:"description"` + + // MaxConcurrentRequests limits the number of concurrent client requests. + MaxConcurrentRequests int `json:"max_concurrent_requests" yaml:"max_concurrent_requests"` + + // RequestTimeout specifies the timeout for individual requests. + RequestTimeout Duration `json:"request_timeout" yaml:"request_timeout"` + + IsReadOnly *bool `json:"read_only,omitempty" yaml:"read_only,omitempty"` +} + +// BackendConfig contains configuration for the backend connection. +type BackendConfig struct { + // Type specifies the backend type ("stackql", "tcp", "memory"). + Type string `json:"type" yaml:"type"` + + // ConnectionString contains the connection details for the backend. + // Format depends on the backend type. + ConnectionString string `json:"connection_string" yaml:"connection_string"` + + // MaxConnections limits the number of backend connections. + MaxConnections int `json:"max_connections" yaml:"max_connections"` + + // ConnectionTimeout specifies the timeout for backend connections. + ConnectionTimeout Duration `json:"connection_timeout" yaml:"connection_timeout"` + + // QueryTimeout specifies the timeout for individual queries. + QueryTimeout Duration `json:"query_timeout" yaml:"query_timeout"` +} + +// Duration is a wrapper around time.Duration that can be marshaled to/from JSON and YAML. +type Duration time.Duration + +// MarshalJSON implements json.Marshaler. +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(d).String()) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (d *Duration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + duration, err := time.ParseDuration(s) + if err != nil { + return err + } + *d = Duration(duration) + return nil +} + +// MarshalYAML implements yaml.Marshaler. +func (d Duration) MarshalYAML() (interface{}, error) { + return time.Duration(d).String(), nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + duration, err := time.ParseDuration(s) + if err != nil { + return err + } + *d = Duration(duration) + return nil +} + +// DefaultConfig returns a configuration with sensible defaults. +func defaultConfig() *Config { + return &Config{ + Server: ServerConfig{ + Name: "StackQL MCP Server", + Version: "0.1.0", + Description: "Model Context Protocol server for StackQL", + MaxConcurrentRequests: 100, + Transport: serverTransportStdIO, + Address: DefaultHTTPServerAddress, + RequestTimeout: Duration(30 * time.Second), + }, + Backend: BackendConfig{ + Type: "stackql", + ConnectionString: "stackql://localhost", + MaxConnections: 10, + ConnectionTimeout: Duration(10 * time.Second), + QueryTimeout: Duration(30 * time.Second), + }, + } +} + +// DefaultConfig returns a configuration with sensible defaults. +func DefaultConfig() *Config { + rv := defaultConfig() + return rv +} + +func DefaultHTTPConfig() *Config { + rv := defaultConfig() + rv.Server.Transport = serverTransportHTTP + return rv +} + +func DefaultSSEConfig() *Config { + rv := defaultConfig() + rv.Server.Transport = serverTransportSSE + return rv +} + +// Validate validates the configuration and returns an error if invalid. +// +//nolint:gocognit // simple validation logic +func (c *Config) Validate() error { + // if c.Server.Name == "" { + // return fmt.Errorf("server.name is required") + // } + // if c.Server.Version == "" { + // return fmt.Errorf("server.version is required") + // } + // if c.Server.MaxConcurrentRequests <= 0 { + // return fmt.Errorf("server.max_concurrent_requests must be greater than 0") + // } + // if c.Backend.Type == "" { + // return fmt.Errorf("backend.type is required") + // } + // if c.Backend.MaxConnections <= 0 { + // return fmt.Errorf("backend.max_connections must be greater than 0") + // } + + return nil +} + +// LoadFromJSON loads configuration from JSON data. +func LoadFromJSON(data []byte) (*Config, error) { + config := &Config{} + if err := json.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("failed to parse JSON config: %w", err) + } + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + return config, nil +} + +// LoadFromYAML loads configuration from YAML data. +func LoadFromYAML(data []byte) (*Config, error) { + config := &Config{} + if err := yaml.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("failed to parse YAML config: %w", err) + } + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + return config, nil +} diff --git a/pkg/mcp_server/dto.go b/pkg/mcp_server/dto.go new file mode 100644 index 000000000..ec42cf00f --- /dev/null +++ b/pkg/mcp_server/dto.go @@ -0,0 +1,150 @@ +package mcp_server //nolint:revive,stylecheck // fine for now + +type GreetingInput struct { + Name string `json:"name" jsonschema:"the name of the person to greet"` +} + +type GreetingOutput struct { + Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"` +} + +/* + +Comment AA + +Please turn the below python classes into golang structures of the same name with json and yaml attributes exposed + +```python +class QueryInput(BaseModel): + sql: str = Field(description="SQL statement to execute") + parameters: Optional[List[Any]] = Field(default=None, description="Positional parameters for the SQL") + row_limit: int = Field(default=500, ge=1, le=10000, description="Max rows to return for SELECT queries") + format: Literal["markdown", "json"] = Field(default="markdown", description="Output format for results") + + +class QueryJSONInput(BaseModel): + sql: str + parameters: Optional[List[Any]] = None + row_limit: int = 500 + +class ListSchemasInput(BaseModel): + include_system: bool = Field(default=False, description="Include pg_* and information_schema") + include_temp: bool = Field(default=False, description="Include temporary schemas (pg_temp_*)") + require_usage: bool = Field(default=True, description="Only list schemas with USAGE privilege") + row_limit: int = Field(default=10000, ge=1, le=100000, description="Maximum number of schemas to return") + name_like: Optional[str] = Field(default=None, description="Filter schema names by LIKE pattern (use % and _). '*' and '?' will be translated.") + case_sensitive: bool = Field(default=False, description="When true, use LIKE instead of ILIKE for name_like") + +class ListSchemasPageInput(BaseModel): + include_system: bool = False + include_temp: bool = False + require_usage: bool = True + page_size: int = Field(default=500, ge=1, le=10000) + cursor: Optional[str] = None + name_like: Optional[str] = None + case_sensitive: bool = False + + +class ListTablesInput(BaseModel): + db_schema: Optional[str] = Field(default=None, description="Schema to list tables from; defaults to current_schema()") + name_like: Optional[str] = Field(default=None, description="Filter table_name by pattern; '*' and '?' translate to SQL wildcards") + case_sensitive: bool = Field(default=False, description="Use LIKE (true) or ILIKE (false) for name_like") + table_types: Optional[List[str]] = Field( + default=None, + description="Limit to specific information_schema table_type values (e.g., 'BASE TABLE','VIEW')", + ) + row_limit: int = Field(default=10000, ge=1, le=100000) + + +class ListTablesPageInput(BaseModel): + db_schema: Optional[str] = None + name_like: Optional[str] = None + case_sensitive: bool = False + table_types: Optional[List[str]] = None + page_size: int = Field(default=500, ge=1, le=10000) + cursor: Optional[str] = None + + def to_list_tables_input(self) -> ListTablesInput: + return ListTablesInput( + db_schema=self.db_schema, + name_like=self.name_like, + case_sensitive=self.case_sensitive, + table_types=self.table_types, + row_limit=self.page_size + ) +``` + +*/ + +type GreetInput struct { + Name string `json:"name" jsonschema:"the person to greet"` +} + +type HierarchyInput struct { + Provider string `json:"provider,omitempty" yaml:"provider,omitempty"` + Service string `json:"service,omitempty" yaml:"service,omitempty"` + Resource string `json:"resource,omitempty" yaml:"resource,omitempty"` + Method string `json:"method,omitempty" yaml:"method,omitempty"` + RowLimit int `json:"row_limit,omitempty" yaml:"row_limit,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + // Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"` +} + +type ServerInfoOutput struct { + Name string `json:"name" jsonschema:"server name"` + Info string `json:"info" jsonschema:"server info"` + IsReadOnly bool `json:"read_only" jsonschema:"is the database read-only"` +} + +type QueryInput struct { + SQL string `json:"sql" yaml:"sql"` + RowLimit int `json:"row_limit,omitempty" yaml:"row_limit,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + // Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"` +} + +type QueryJSONInput struct { + SQL string `json:"sql" yaml:"sql"` + RowLimit int `json:"row_limit,omitempty" yaml:"row_limit,omitempty"` + // Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"` +} + +type ListSchemasInput struct { + IncludeSystem bool `json:"include_system,omitempty" yaml:"include_system,omitempty"` + IncludeTemp bool `json:"include_temp,omitempty" yaml:"include_temp,omitempty"` + RequireUsage bool `json:"require_usage,omitempty" yaml:"require_usage,omitempty"` + RowLimit int `json:"row_limit,omitempty" yaml:"row_limit,omitempty"` + NameLike *string `json:"name_like,omitempty" yaml:"name_like,omitempty"` + CaseSensitive bool `json:"case_sensitive,omitempty" yaml:"case_sensitive,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` +} + +type ListSchemasPageInput struct { + IncludeSystem bool `json:"include_system" yaml:"include_system"` + IncludeTemp bool `json:"include_temp" yaml:"include_temp"` + RequireUsage bool `json:"require_usage" yaml:"require_usage"` + PageSize int `json:"page_size" yaml:"page_size"` + Cursor *string `json:"cursor,omitempty" yaml:"cursor,omitempty"` + NameLike *string `json:"name_like,omitempty" yaml:"name_like,omitempty"` + CaseSensitive bool `json:"case_sensitive" yaml:"case_sensitive"` + Format string `json:"format" yaml:"format"` +} + +type ListTablesInput struct { + Hierarchy *HierarchyInput `json:"hierarchy,omitempty" yaml:"hierarchy,omitempty"` + NameLike *string `json:"name_like,omitempty" yaml:"name_like,omitempty"` + CaseSensitive bool `json:"case_sensitive,omitempty" yaml:"case_sensitive,omitempty"` + TableTypes []string `json:"table_types,omitempty" yaml:"table_types,omitempty"` + RowLimit int `json:"row_limit,omitempty" yaml:"row_limit,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` +} + +type ListTablesPageInput struct { + Hierarchy *HierarchyInput `json:"hierarchy,omitempty" yaml:"hierarchy,omitempty"` + NameLike *string `json:"name_like,omitempty" yaml:"name_like,omitempty"` + CaseSensitive bool `json:"case_sensitive,omitempty" yaml:"case_sensitive,omitempty"` + TableTypes []string `json:"table_types,omitempty" yaml:"table_types,omitempty"` + PageSize int `json:"page_size,omitempty" yaml:"page_size,omitempty"` + Cursor *string `json:"cursor,omitempty" yaml:"cursor,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` +} diff --git a/pkg/mcp_server/example_backend.go b/pkg/mcp_server/example_backend.go new file mode 100644 index 000000000..8d39b388f --- /dev/null +++ b/pkg/mcp_server/example_backend.go @@ -0,0 +1,149 @@ +package mcp_server //nolint:revive // fine for now + +import ( + "context" + "time" +) + +const ( + ExplainerForeignKeyStackql = "At present, foreign keys are not meaningfully supported in stackql." + ExplainerFindRelationships = "At present, relationship finding is not meaningfully supported in stackql." + ExplainerPromptWriteSafeSelectTool = `In order to ascertain the best safe select query, the correct query form is: + > SHOW methods IN ..; + From the output, one can infer the best access method for the SQL "select" verb and the **required** WHERE clause attributes.` +) + +// ExampleBackend is a simple implementation of the Backend interface for demonstration purposes. +// This shows how to implement the Backend interface without depending on StackQL internals. +type ExampleBackend struct { + connectionString string + connected bool +} + +// Stub all Backend interface methods below + +func (b *ExampleBackend) Greet(ctx context.Context, args GreetInput) (string, error) { + return "Hi " + args.Name, nil +} + +func (b *ExampleBackend) ServerInfo(ctx context.Context, _ any) (ServerInfoOutput, error) { + return ServerInfoOutput{ + Name: "Stackql explorer", + Info: "This is an example server.", + IsReadOnly: false, + }, nil +} + +// Please adjust all below to sensible signatures in keeping with what is above. +// Do it now! +func (b *ExampleBackend) DBIdentity(ctx context.Context, _ any) (map[string]any, error) { + return map[string]any{ + "identity": "stub", + }, nil +} + +func (b *ExampleBackend) RunQuery(ctx context.Context, args QueryInput) (string, error) { + return "stub", nil +} + +func (b *ExampleBackend) RunQueryJSON(ctx context.Context, input QueryJSONInput) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + +// func (b *ExampleBackend) ListTableResources(ctx context.Context, hI HierarchyInput) ([]string, error) { +// return []string{}, nil +// } + +func (b *ExampleBackend) ReadTableResource(ctx context.Context, hI HierarchyInput) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + +func (b *ExampleBackend) PromptWriteSafeSelectTool(ctx context.Context, args HierarchyInput) (string, error) { + return ExplainerPromptWriteSafeSelectTool, nil +} + +// func (b *ExampleBackend) PromptExplainPlanTipsTool(ctx context.Context) (string, error) { +// return "stub", nil +// } + +func (b *ExampleBackend) ListTablesJSON(ctx context.Context, input ListTablesInput) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + +func (b *ExampleBackend) ListTablesJSONPage(ctx context.Context, input ListTablesPageInput) (map[string]interface{}, error) { + return map[string]interface{}{}, nil +} + +func (b *ExampleBackend) ListTables(ctx context.Context, hI HierarchyInput) (string, error) { + return "stub", nil +} + +func (b *ExampleBackend) ListMethods(ctx context.Context, hI HierarchyInput) (string, error) { + return "stub", nil +} + +func (b *ExampleBackend) DescribeTable(ctx context.Context, hI HierarchyInput) (string, error) { + return "stub", nil +} + +func (b *ExampleBackend) GetForeignKeys(ctx context.Context, hI HierarchyInput) (string, error) { + return ExplainerForeignKeyStackql, nil +} + +func (b *ExampleBackend) FindRelationships(ctx context.Context, hI HierarchyInput) (string, error) { + return ExplainerFindRelationships, nil +} + +func (b *ExampleBackend) ListProviders(ctx context.Context) (string, error) { + return "stub", nil +} + +func (b *ExampleBackend) ListServices(ctx context.Context, hI HierarchyInput) (string, error) { + return "stub", nil +} + +func (b *ExampleBackend) ListResources(ctx context.Context, hI HierarchyInput) (string, error) { + return "stub", nil +} + +// NewExampleBackend creates a new example backend instance. +func NewExampleBackend(connectionString string) Backend { + return &ExampleBackend{ + connectionString: connectionString, + connected: false, + } +} + +// Ping implements the Backend interface. +func (b *ExampleBackend) Ping(ctx context.Context) error { + if !b.connected { + // Simulate connection establishment + b.connected = true + } + + // Simulate a ping operation + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(10 * time.Millisecond): + return nil + } +} + +// Close implements the Backend interface. +func (b *ExampleBackend) Close() error { + b.connected = false + return nil +} + +// NewMCPServerWithExampleBackend creates a new MCP server with an example backend. +// This is a convenience function for testing and demonstration purposes. +func NewMCPServerWithExampleBackend(config *Config) (MCPServer, error) { + if config == nil { + config = DefaultConfig() + } + + backend := NewExampleBackend(config.Backend.ConnectionString) + + return newMCPServer(config, backend, nil) +} diff --git a/pkg/mcp_server/logging_middleware.go b/pkg/mcp_server/logging_middleware.go new file mode 100644 index 000000000..93b65cb58 --- /dev/null +++ b/pkg/mcp_server/logging_middleware.go @@ -0,0 +1,51 @@ +package mcp_server //nolint:revive // fine for now + +// Courtesy https://github.com/modelcontextprotocol/go-sdk/blob/1dcbf62661fc9c54ae364e0af80433db347e2fc4/examples/http/logging_middleware.go#L24 +// With profuse thanks. + +import ( + "net/http" + "time" + + "github.com/sirupsen/logrus" +) + +// responseWriter wraps http.ResponseWriter to capture the status code. +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +func loggingHandler(handler http.Handler, logger *logrus.Logger) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Create a response writer wrapper to capture status code. + wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} + + // Log request details. + logger.Infof("[REQUEST] %s | %s | %s %s", + start.Format(time.RFC3339), + r.RemoteAddr, + r.Method, + r.URL.Path) + + // Call the actual handler. + handler.ServeHTTP(wrapped, r) + + // Log response details. + duration := time.Since(start) + logger.Infof("[RESPONSE] %s | %s | %s %s | Status: %d | Duration: %v", + time.Now().Format(time.RFC3339), + r.RemoteAddr, + r.Method, + r.URL.Path, + wrapped.statusCode, + duration) + }) +} diff --git a/pkg/mcp_server/server.go b/pkg/mcp_server/server.go new file mode 100644 index 000000000..7d424a17b --- /dev/null +++ b/pkg/mcp_server/server.go @@ -0,0 +1,523 @@ +package mcp_server //nolint:revive // fine for now + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + + "github.com/sirupsen/logrus" + "golang.org/x/sync/semaphore" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +const ( + serverTransportStdIO = "stdio" + serverTransportHTTP = "http" + serverTransportSSE = "sse" + DefaultHTTPServerAddress = "127.0.0.1:9876" +) + +type MCPServer interface { + Start(context.Context) error + Stop() error +} + +// simpleMCPServer implements the Model Context Protocol server for StackQL. +type simpleMCPServer struct { + config *Config + backend Backend + logger *logrus.Logger + + server *mcp.Server + + // Concurrency control + requestSemaphore *semaphore.Weighted + + // Server state + mu sync.RWMutex + running bool + servers []io.Closer // Track all running servers for cleanup +} + +func (s *simpleMCPServer) runHTTPServer(server *mcp.Server, address string) error { + // Create the streamable HTTP handler. + handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server { + return server + }, nil) + + handlerWithLogging := loggingHandler(handler, s.logger) + + s.logger.Debugf("MCP server listening on %s", address) + s.logger.Debugf("Available tool: cityTime (cities: nyc, sf, boston)") + + // Start the HTTP server with logging handler. + //nolint:gosec // TODO: find viable alternative to http.ListenAndServe + if err := http.ListenAndServe(address, handlerWithLogging); err != nil { + s.logger.Errorf("Server failed: %v", err) + return err + } + return nil +} + +func NewExampleBackendServer(config *Config, logger *logrus.Logger) (MCPServer, error) { + backend := NewExampleBackend("example-connection-string") + return newMCPServer(config, backend, logger) +} + +func NewAgnosticBackendServer(backend Backend, config *Config, logger *logrus.Logger) (MCPServer, error) { + return newMCPServer(config, backend, logger) +} + +// func NewExampleHTTPBackendServer(config *Config, logger *logrus.Logger) (MCPServer, error) { +// backend := NewExampleBackend("example-connection-string") +// if config == nil { +// config = DefaultHTTPConfig() +// } +// return NewMCPServer(config, backend, logger) +// } + +// NewMCPServer creates a new MCP server with the provided configuration and backend. +// +//nolint:gocognit,funlen // ok +func newMCPServer(config *Config, backend Backend, logger *logrus.Logger) (MCPServer, error) { + if config == nil { + config = DefaultConfig() + } + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + if backend == nil { + return nil, fmt.Errorf("backend is required") + } + if logger == nil { + logger = logrus.New() + logger.SetLevel(logrus.InfoLevel) + // logger.SetOutput(io.Discard) + } + + server := mcp.NewServer( + &mcp.Implementation{Name: "stackql", Version: "v0.1.0"}, + nil, + ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "greet", + Description: "Say hi. A simple liveness check.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args GreetInput) (*mcp.CallToolResult, any, error) { + greeting, greetingErr := backend.Greet(ctx, args) + if greetingErr != nil { + return nil, nil, greetingErr + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: greeting}, + }, + }, nil, nil + }, + ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "server_info", + Description: "Get server information", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args any) (*mcp.CallToolResult, ServerInfoOutput, error) { + rv, rvErr := backend.ServerInfo(ctx, args) + if rvErr != nil { + return nil, ServerInfoOutput{}, rvErr + } + return nil, rv, nil + }, + ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "db_identity", + Description: "get current database identity", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args any) (*mcp.CallToolResult, map[string]any, error) { + rv, rvErr := backend.DBIdentity(ctx, args) + if rvErr != nil { + return nil, nil, rvErr + } + return nil, rv, nil + }, + ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "query_v2", + Description: "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 QueryInput) (*mcp.CallToolResult, any, error) { + logger.Warnf("Received query: %s", arg.SQL) + rv, rvErr := backend.RunQuery(ctx, arg) + if rvErr != nil { + return nil, nil, rvErr + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: rv}, + }, + }, nil, nil + }, + ) + mcp.AddTool( + server, + &mcp.Tool{ + Name: "query_json_v2", + Description: "Execute a SQL query and return a JSON array of rows, as text.", + // Input and output schemas can be defined here if needed. + }, + func(ctx context.Context, req *mcp.CallToolRequest, args QueryJSONInput) (*mcp.CallToolResult, any, error) { + arr, err := backend.RunQueryJSON(ctx, args) + if err != nil { + return nil, nil, err + } + bytesArr, marshalErr := json.Marshal(arr) + if marshalErr != nil { + return nil, nil, fmt.Errorf("failed to marshal query result to JSON: %w", marshalErr) + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(bytesArr)}, + }, + }, nil, nil + }, + ) + + // mcp.AddTool( + // server, + // &mcp.Tool{ + // Name: "list_table_resources", + // Description: "List resource URIs for tables in a schema.", + // }, + // func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + // result, err := backend.ListTableResources(ctx, args) + // if err != nil { + // return nil, nil, err + // } + // return &mcp.CallToolResult{ + // Content: []mcp.Content{ + // &mcp.TextContent{Text: fmt.Sprintf("%v", result)}, + // }, + // }, result, nil + // }, + // ) + + // mcp.AddTool( + // server, + // &mcp.Tool{ + // Name: "read_table_resource", + // Description: "Read rows from a table resource.", + // }, + // func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + // result, err := backend.ReadTableResource(ctx, args) + // if err != nil { + // return nil, nil, err + // } + // return &mcp.CallToolResult{ + // Content: []mcp.Content{ + // &mcp.TextContent{Text: fmt.Sprintf("%v", result)}, + // }, + // }, result, nil + // }, + // ) + + mcp.AddTool( + server, + &mcp.Tool{ + Name: "prompt_write_safe_select_tool", + Description: "Prompt: guidelines for writing safe SELECT queries.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + result, err := backend.PromptWriteSafeSelectTool(ctx, args) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: result}, + }, + }, result, nil + }, + ) + + // 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{ + Name: "list_tables_json", + Description: "List tables in a schema and return JSON rows.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args ListTablesInput) (*mcp.CallToolResult, any, error) { + result, err := backend.ListTablesJSON(ctx, args) + if err != nil { + return nil, nil, err + } + bytesArr, marshalErr := json.Marshal(result) + if marshalErr != nil { + return nil, nil, fmt.Errorf("failed to marshal result to JSON: %w", marshalErr) + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(bytesArr)}, + }, + }, result, nil + }, + ) + + mcp.AddTool( + server, + &mcp.Tool{ + Name: "list_tables_json_page", + Description: "List tables with pagination and filters, returns JSON.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args ListTablesPageInput) (*mcp.CallToolResult, any, error) { + result, err := backend.ListTablesJSONPage(ctx, args) + if err != nil { + return nil, nil, err + } + bytesArr, marshalErr := json.Marshal(result) + if marshalErr != nil { + return nil, nil, fmt.Errorf("failed to marshal result to JSON: %w", marshalErr) + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(bytesArr)}, + }, + }, result, nil + }, + ) + + mcp.AddTool( + server, + &mcp.Tool{ + Name: "list_providers", + Description: "List all schemas/providers in the database.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + result, err := backend.ListProviders(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{ + Name: "list_services", + Description: "List services for a provider.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + result, err := backend.ListServices(ctx, args) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: result}, + }, + }, result, nil + }, + ) + + mcp.AddTool( + server, + &mcp.Tool{ + Name: "list_resources", + Description: "List resources for a service.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + result, err := backend.ListResources(ctx, args) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: result}, + }, + }, result, nil + }, + ) + + mcp.AddTool( + server, + &mcp.Tool{ + Name: "list_methods", + Description: "List methods for a resource.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + result, err := backend.ListMethods(ctx, args) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: result}, + }, + }, result, nil + }, + ) + + mcp.AddTool( + server, + &mcp.Tool{ + Name: "describe_table", + Description: "Get detailed information about a table.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + result, err := backend.DescribeTable(ctx, args) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: result}, + }, + }, result, nil + }, + ) + + mcp.AddTool( + server, + &mcp.Tool{ + Name: "get_foreign_keys", + Description: "Get foreign key information for a table.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + result, err := backend.GetForeignKeys(ctx, args) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: result}, + }, + }, result, nil + }, + ) + + mcp.AddTool( + server, + &mcp.Tool{ + Name: "find_relationships", + Description: "Find explicit and implied relationships for a table.", + }, + func(ctx context.Context, req *mcp.CallToolRequest, args HierarchyInput) (*mcp.CallToolResult, any, error) { + result, err := backend.FindRelationships(ctx, args) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: result}, + }, + }, result, nil + }, + ) + + return &simpleMCPServer{ + config: config, + backend: backend, + logger: logger, + server: server, + requestSemaphore: semaphore.NewWeighted(int64(config.Server.MaxConcurrentRequests)), + servers: make([]io.Closer, 0), + }, nil +} + +// Start starts the MCP server with all configured transports. +// +//nolint:errcheck // ok for now +func (s *simpleMCPServer) Start(ctx context.Context) error { + s.mu.Lock() + defer func() { + s.mu.Unlock() + s.running = false + }() + if s.running { + return fmt.Errorf("server is already running") + } + s.running = true + return s.run(ctx) +} + +// Synchronous server run. +func (s *simpleMCPServer) run(ctx context.Context) error { + switch s.config.GetServerTransport() { + case serverTransportHTTP: + return s.runHTTPServer(s.server, s.config.GetServerAddress()) + case serverTransportSSE: + return fmt.Errorf("SSE transport not yet implemented") + case serverTransportStdIO: + // Default to stdio transport + return s.server.Run(ctx, &mcp.StdioTransport{}) + default: + return fmt.Errorf("unsupported transport: %s", s.config.Server.Transport) + } +} + +// Stop gracefully stops the MCP server and all transports. +func (s *simpleMCPServer) Stop() error { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.running { + return nil + } + + // Close all servers + var errs []error + for _, server := range s.servers { + if err := server.Close(); err != nil { + errs = append(errs, err) + } + } + + // Close backend + if err := s.backend.Close(); err != nil { + errs = append(errs, err) + } + + s.running = false + s.servers = s.servers[:0] + + if len(errs) > 0 { + return fmt.Errorf("errors during shutdown: %v", errs) + } + + s.logger.Printf("MCP server stopped") + return nil +} diff --git a/pkg/mcp_server/server_test.go b/pkg/mcp_server/server_test.go new file mode 100644 index 000000000..3fb97b6a2 --- /dev/null +++ b/pkg/mcp_server/server_test.go @@ -0,0 +1,164 @@ +package mcp_server //nolint:testpackage,revive // fine for now + +import ( + "context" + "encoding/json" + "testing" + "time" +) + +func TestDefaultConfig(t *testing.T) { + config := DefaultConfig() + + if config == nil { + t.Fatal("DefaultConfig() returned nil") + } + + if err := config.Validate(); err != nil { + t.Fatalf("Default config validation failed: %v", err) + } + + if config.Server.Name == "" { + t.Error("Server name should not be empty") + } + + if config.Server.Version == "" { + t.Error("Server version should not be empty") + } +} + +// func TestConfigValidation(t *testing.T) { +// tests := []struct { +// name string +// config *Config +// wantError bool +// }{ +// { +// name: "valid default config", +// config: DefaultConfig(), +// wantError: false, +// }, +// { +// name: "empty server name", +// config: &Config{ +// Server: ServerConfig{ +// Name: "", +// Version: "1.0.0", +// MaxConcurrentRequests: 100, +// }, +// Backend: BackendConfig{ +// Type: "stackql", +// MaxConnections: 10, +// }, +// }, +// wantError: true, +// }, +// { +// name: "invalid transport", +// config: &Config{ +// Server: ServerConfig{ +// Name: "Test Server", +// Version: "1.0.0", +// MaxConcurrentRequests: 100, +// }, +// Backend: BackendConfig{ +// Type: "stackql", +// MaxConnections: 10, +// }, +// }, +// wantError: true, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// err := tt.config.Validate() +// if (err != nil) != tt.wantError { +// t.Errorf("Config.Validate() error = %v, wantError %v", err, tt.wantError) +// } +// }) +// } +// } + +func TestExampleBackend(t *testing.T) { + backend := NewExampleBackend("test://localhost") + ctx := context.Background() + + // Test Ping + if err := backend.Ping(ctx); err != nil { + t.Fatalf("Ping failed: %v", err) + } + + // Test Close + if err := backend.Close(); err != nil { + t.Fatalf("Close failed: %v", err) + } +} + +func TestMCPServerCreation(t *testing.T) { + config := DefaultConfig() + backend := NewExampleBackend("test://localhost") + + server, err := newMCPServer(config, backend, nil) + if err != nil { + t.Fatalf("NewMCPServer failed: %v", err) + } + + if server == nil { + t.Fatal("Server should not be nil") + } + + // Test that server implements MCPServer interface + var _ MCPServer = server +} + +func TestDurationMarshaling(t *testing.T) { + d := Duration(30 * time.Second) + + // Test JSON marshaling + jsonData, err := json.Marshal(d) + if err != nil { + t.Fatalf("JSON marshal failed: %v", err) + } + + var d2 Duration + if err := json.Unmarshal(jsonData, &d2); err != nil { + t.Fatalf("JSON unmarshal failed: %v", err) + } + + if time.Duration(d) != time.Duration(d2) { + t.Errorf("Duration mismatch after JSON round-trip: %v != %v", d, d2) + } +} + +func TestBackendError(t *testing.T) { + err := &BackendError{ + Code: "TEST_ERROR", + Message: "Test error message", + Details: map[string]interface{}{"field": "value"}, + } + + if err.Error() != "Test error message" { + t.Errorf("Expected error message 'Test error message', got '%s'", err.Error()) + } + + // Test Value() method for database compatibility + val, dbErr := err.Value() + if dbErr != nil { + t.Fatalf("Value() failed: %v", dbErr) + } + + if val != "Test error message" { + t.Errorf("Expected value 'Test error message', got '%v'", val) + } +} + +func TestNewMCPServerWithExampleBackend(t *testing.T) { + server, err := NewMCPServerWithExampleBackend(nil) + if err != nil { + t.Fatalf("NewMCPServerWithExampleBackend failed: %v", err) + } + + if server == nil { + t.Fatal("Server should not be nil") + } +} diff --git a/pkg/presentation/markdown_row.go b/pkg/presentation/markdown_row.go new file mode 100644 index 000000000..171312ede --- /dev/null +++ b/pkg/presentation/markdown_row.go @@ -0,0 +1,72 @@ +package presentation + +import ( + "fmt" + "sort" + "strings" +) + +type MarkdownRow interface { + Headers() []string + Values() []any + RowString() string + HeaderString() string + SeparatorString() string +} + +func NewMarkdownRowFromMap(row map[string]interface{}) MarkdownRow { + var columns []string + var values []any + for k := range row { + columns = append(columns, k) + } + sort.Strings(columns) + for _, k := range columns { + v := row[k] + values = append(values, v) + } + return &simpleMardownRow{ + columns: columns, + values: values, + } +} + +type simpleMardownRow struct { + columns []string + values []any +} + +func (s *simpleMardownRow) Headers() []string { + return s.columns +} + +func (s *simpleMardownRow) Values() []any { + return s.values +} + +func (s *simpleMardownRow) RowString() string { + var sb strings.Builder + for i := 0; i < len(s.columns); i++ { + sb.WriteString(fmt.Sprintf("| %v ", s.values[i])) + } + sb.WriteString("|") + return sb.String() +} + +func (s *simpleMardownRow) SeparatorString() string { + var sb strings.Builder + for range s.columns { + sb.WriteString("|---") + } + sb.WriteString("|") + return sb.String() +} + +func (s *simpleMardownRow) HeaderString() string { + var sb strings.Builder + for i := 0; i < len(s.columns); i++ { + sb.WriteString(fmt.Sprintf("| %s ", s.columns[i])) + } + sb.WriteString("|") + return sb.String() +} diff --git a/pkg/textutil/textutil.go b/pkg/textutil/textutil.go index 18777b80f..ce403ae73 100644 --- a/pkg/textutil/textutil.go +++ b/pkg/textutil/textutil.go @@ -4,7 +4,6 @@ import ( "regexp" ) -//nolint:revive // Explicit type declaration removes any ambiguity var ( namespaceLikeStringRegex *regexp.Regexp = regexp.MustCompile(`{{.*}}`) ) diff --git a/stackql/main.go b/stackql/main.go index abc1264b4..48ff23070 100644 --- a/stackql/main.go +++ b/stackql/main.go @@ -1,5 +1,5 @@ /* -Copyright © 2019 stackql info@stackql.io +Copyright © 2025 stackql info@stackql.io Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/python/stackql_test_tooling/stackql_context.py b/test/python/stackql_test_tooling/stackql_context.py index 8d90f82b7..2c881608f 100644 --- a/test/python/stackql_test_tooling/stackql_context.py +++ b/test/python/stackql_test_tooling/stackql_context.py @@ -12,6 +12,7 @@ from registry_cfg import RegistryCfg _exe_name = 'stackql' +_mcp_client_exe_name = 'stackql_mcp_client' IS_WINDOWS = '0' if os.name == 'nt': @@ -143,10 +144,16 @@ def get_json_from_local_file(fp :str) -> typing.Any: REPOSITORY_ROOT_UNIX = get_unix_path(repository_root) def get_stackql_exe(execution_env :str, is_preinstalled :bool): - _default_stackqk_exe = ' '.join(get_unix_path(os.path.join(repository_root, 'build', _exe_name)).splitlines()) + _default_stackql_exe = ' '.join(get_unix_path(os.path.join(repository_root, 'build', _exe_name)).splitlines()) if is_preinstalled: return 'stackql' - return _default_stackqk_exe + return _default_stackql_exe + + def get_stackql_mcp_client_exe(execution_env :str, is_preinstalled :bool): + _default_stackql_mcp_client_exe = ' '.join(get_unix_path(os.path.join(repository_root, 'build', _mcp_client_exe_name)).splitlines()) + if is_preinstalled: + return _mcp_client_exe_name + return _default_stackql_mcp_client_exe def get_registry_mocked(execution_env :str) -> RegistryCfg: return RegistryCfg( @@ -934,6 +941,7 @@ def get_registry_mock_url(execution_env :str) -> str: 'REGISTRY_DEPRECATED_CFG_STR': _REGISTRY_DEPRECATED, 'REGISTRY_MOCKED_CFG_STR': get_registry_mocked(execution_env), 'REGISTRY_NO_VERIFY_CFG_STR': _get_registry_no_verify(_sundry_config.get('registry_path')), + 'REGISTRY_NO_VERIFY_CFG_JSON_STR': _get_registry_no_verify(_sundry_config.get('registry_path')).get_config_str(execution_environment=execution_env), 'REGISTRY_NULL': _REGISTRY_NULL, 'REPOSITORY_ROOT': repository_root, 'SQL_BACKEND_CFG_STR_ANALYTICS': get_analytics_sql_backend(execution_env, sql_backend_str), @@ -941,6 +949,7 @@ def get_registry_mock_url(execution_env :str) -> str: 'SQL_CLIENT_EXPORT_BACKEND': get_export_sql_backend(execution_env, sql_backend_str), 'SQL_CLIENT_EXPORT_CONNECTION_ARG': get_export_sql_connection_arg(execution_env, sql_backend_str), 'STACKQL_EXE': get_stackql_exe(execution_env, must_use_stackql_preinstalled), + 'STACKQL_MCP_CLIENT_EXE': get_stackql_mcp_client_exe(execution_env, must_use_stackql_preinstalled), 'SUMOLOGIC_SECRET_STR': SUMOLOGIC_SECRET_STR, ## queries and expectations 'AWS_CC_VIEW_SELECT_PROJECTION_BUCKET_COMPLEX_EXPECTED': AWS_CC_VIEW_SELECT_PROJECTION_BUCKET_COMPLEX_EXPECTED, diff --git a/test/robot/functional/mcp.robot b/test/robot/functional/mcp.robot new file mode 100644 index 000000000..f7232e0c4 --- /dev/null +++ b/test/robot/functional/mcp.robot @@ -0,0 +1,122 @@ +*** Settings *** +Resource ${CURDIR}${/}stackql.resource + + +*** Keywords *** +Start MCP HTTP Server + Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available + Start Process ${STACKQL_EXE} + ... mcp + ... \-\-mcp.server.type\=http + ... \-\-mcp.config + ... {"server": {"transport": "http", "address": "127.0.0.1:9912"} } + ... \-\-registry + ... ${REGISTRY_NO_VERIFY_CFG_JSON_STR} + ... \-\-auth + ... ${AUTH_CFG_STR} + ... \-\-tls.allowInsecure + Sleep 5s + +*** Settings *** +Suite Setup Start MCP HTTP Server + + +*** Test Cases *** +MCP HTTP Server Run List Tools + Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available + Sleep 5s + ${result}= Run Process ${STACKQL_MCP_CLIENT_EXE} + ... exec + ... \-\-client\-type\=http + ... \-\-url\=http://127.0.0.1:9912 + ... stdout=${CURDIR}${/}tmp${/}MCP-HTTP-Server-Run-List-Tools.txt + ... stderr=${CURDIR}${/}tmp${/}MCP-HTTP-Server-Run-List-Tools-stderr.txt + Should Contain ${result.stdout} Get server information + Should Be Equal As Integers ${result.rc} 0 + + +MCP HTTP Server Verify Greeting Tool + Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available + Sleep 5s + ${result}= Run Process ${STACKQL_MCP_CLIENT_EXE} + ... exec + ... \-\-client\-type\=http + ... \-\-url\=http://127.0.0.1:9912 + ... \-\-exec.action greet + ... \-\-exec.args {"name": "JOE BLOW"} + ... stdout=${CURDIR}${/}tmp${/}MCP-HTTP-Server-Verify-Greeting-Tool.txt + ... stderr=${CURDIR}${/}tmp${/}MCP-HTTP-Server-Verify-Greeting-Tool-stderr.txt + Should Contain ${result.stdout} JOE BLOW + Should Be Equal As Integers ${result.rc} 0 + + +MCP HTTP Server List Providers Tool + Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available + Sleep 5s + ${result}= Run Process ${STACKQL_MCP_CLIENT_EXE} + ... exec + ... \-\-client\-type\=http + ... \-\-url\=http://127.0.0.1:9912 + ... \-\-exec.action list_providers + ... stdout=${CURDIR}${/}tmp${/}MCP-HTTP-Server-List-Providers.txt + ... stderr=${CURDIR}${/}tmp${/}MCP-HTTP-Server-List-Providers-stderr.txt + Should Contain ${result.stdout} local_openssl + Should Be Equal As Integers ${result.rc} 0 + + +MCP HTTP Server List Services Tool + Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available + Sleep 5s + ${result}= Run Process ${STACKQL_MCP_CLIENT_EXE} + ... exec + ... \-\-client\-type\=http + ... \-\-url\=http://127.0.0.1:9912 + ... \-\-exec.action list_services + ... \-\-exec.args {"provider": "google"} + ... stdout=${CURDIR}${/}tmp${/}MCP-HTTP-Server-List-Services.txt + ... stderr=${CURDIR}${/}tmp${/}MCP-HTTP-Server-List-Services-stderr.txt + Should Contain ${result.stdout} YouTube Analytics API + Should Be Equal As Integers ${result.rc} 0 + +MCP HTTP Server List Resources Tool + Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available + Sleep 5s + ${result}= Run Process ${STACKQL_MCP_CLIENT_EXE} + ... exec + ... \-\-client\-type\=http + ... \-\-url\=http://127.0.0.1:9912 + ... \-\-exec.action list_resources + ... \-\-exec.args {"provider": "google", "service": "cloudresourcemanager"} + ... stdout=${CURDIR}${/}tmp${/}MCP-HTTP-Server-List-Resources.txt + ... stderr=${CURDIR}${/}tmp${/}MCP-HTTP-Server-List-Resources-stderr.txt + Should Contain ${result.stdout} projects + Should Be Equal As Integers ${result.rc} 0 + +MCP HTTP Server List Methods Tool + Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available + Sleep 5s + ${result}= Run Process ${STACKQL_MCP_CLIENT_EXE} + ... exec + ... \-\-client\-type\=http + ... \-\-url\=http://127.0.0.1:9912 + ... \-\-exec.action list_methods + ... \-\-exec.args {"provider": "google", "service": "compute", "resource": "instances"} + ... stdout=${CURDIR}${/}tmp${/}MCP-HTTP-Server-List-Methods.txt + ... stderr=${CURDIR}${/}tmp${/}MCP-HTTP-Server-List-Methods-stderr.txt + Should Contain ${result.stdout} getScreenshot + Should Be Equal As Integers ${result.rc} 0 + +MCP HTTP Server Query Tool + Pass Execution If "%{IS_SKIP_MCP_TEST=false}" == "true" Some platforms do not have the MCP client available + Sleep 5s + ${result}= Run Process ${STACKQL_MCP_CLIENT_EXE} + ... exec + ... \-\-client\-type\=http + ... \-\-url\=http://127.0.0.1:9912 + ... \-\-exec.action query_v2 + ... \-\-exec.args {"sql": "SELECT assetType, count(*) as asset_count FROM google.cloudasset.assets WHERE parentType \= 'projects' and parent \= 'testing-project' GROUP BY assetType order by count(*) desc, assetType desc;"} + ... stdout=${CURDIR}${/}tmp${/}MCP-HTTP-Server-Query-Tool.txt + ... stderr=${CURDIR}${/}tmp${/}MCP-HTTP-Server-Query-Tool-stderr.txt + Should Contain ${result.stdout} cloudkms.googleapis.com + Should Be Equal As Integers ${result.rc} 0 + diff --git a/test/robot/functional/stackql.resource b/test/robot/functional/stackql.resource index 7d06f6464..97d8dbe31 100644 --- a/test/robot/functional/stackql.resource +++ b/test/robot/functional/stackql.resource @@ -58,7 +58,6 @@ Prepare StackQL Environment Set Environment Variable DD_API_KEY %{DD_API_KEY=myusername} Set Environment Variable DD_APPLICATION_KEY %{DD_APPLICATION_KEY=mypassword} Start All Mock Servers - Generate Container Credentials for StackQL PG Server mTLS Start StackQL PG Server mTLS ${PG_SRV_PORT_MTLS} ${PG_SRV_MTLS_CFG_STR} {} {} ${SQL_BACKEND_CFG_STR_CANONICAL} ${PG_SRV_PORT_DOCKER_MTLS} Start StackQL PG Server mTLS ${PG_SRV_PORT_MTLS_WITH_NAMESPACES} ${PG_SRV_MTLS_CFG_STR} ${NAMESPACES_TTL_SPECIALCASE_TRANSPARENT} {} ${SQL_BACKEND_CFG_STR_CANONICAL} ${PG_SRV_PORT_DOCKER_MTLS_WITH_NAMESPACES} Start StackQL PG Server mTLS ${PG_SRV_PORT_MTLS_WITH_EAGER_GC} ${PG_SRV_MTLS_CFG_STR} {} ${GC_CFG_EAGER} ${SQL_BACKEND_CFG_STR_CANONICAL} ${PG_SRV_PORT_DOCKER_MTLS_WITH_EAGER_GC} @@ -67,14 +66,6 @@ Prepare StackQL Environment Start Postgres External Source If Viable Sleep 50s -Generate Container Credentials for StackQL PG Server mTLS - IF "${EXECUTION_PLATFORM}" == "docker" - ${res} = Run Process docker compose \-f docker\-compose\-credentials.yml - ... run \-\-rm credentialsgen - Log Credentials gen completed - Should Be Equal As Integers ${res.rc} 0 - END - Start StackQL PG Server mTLS [Arguments] ${_SRV_PORT_MTLS} ${_MTLS_CFG_STR} ${_NAMESPACES_CFG} ${_GC_CFG} ${_SQL_BACKEND_CFG} ${_DOCKER_PORT} IF "${EXECUTION_PLATFORM}" == "native"