diff --git a/AGENTS.md b/AGENTS.md index a213bd0..c8861d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,13 +4,13 @@ This document is a comprehensive guide for an AI agent tasked with developing an ## 1. Project Overview -**Sysdig MCP Server** is a Go-based Model Context Protocol (MCP) server that exposes Sysdig Monitor platform capabilities to LLMs. It provides tools for querying Kubernetes metrics and executing SysQL queries through multiple transport protocols (stdio, streamable-http, SSE). Sysdig Secure-specific tools live in the separate [@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server) package. +**Sysdig MCP Server** is a Go-based Model Context Protocol (MCP) server that exposes Sysdig Monitor platform capabilities to LLMs. It provides tools for querying Kubernetes metrics through multiple transport protocols (stdio, streamable-http, SSE). Sysdig Secure-specific tools live in the separate [@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server) package. ### 1.1. Quick Facts | Topic | Details | | --- | --- | -| **Purpose** | Expose vetted Sysdig Monitor workflows (plus shared SysQL tooling) to LLMs through MCP tools. | +| **Purpose** | Expose vetted Sysdig Monitor workflows to LLMs through MCP tools. | | **Tech Stack** | Go 1.26+, `mcp-go`, Cobra CLI, Ginkgo/Gomega, `golangci-lint`, Nix. | | **Entry Point** | `cmd/server/main.go` (Cobra CLI that wires config, Sysdig client, etc.). | | **Dev Shell** | `nix develop` provides a consistent development environment. | diff --git a/README.md b/README.md index 218d246..c090ca7 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ > **Breaking change — this MCP server now focuses on Sysdig Monitor.** > > Starting with the next major release, the dedicated Sysdig Secure tools (`list_runtime_events`, `get_event_info`, `get_event_process_tree`) have been removed from this server. For Sysdig Secure use cases, install the new **[@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server)** package, which provides comprehensive coverage of Sysdig Secure capabilities. -> -> The SysQL tools (`generate_sysql`, `run_sysql`) remain available here because they can be used against both Monitor and Secure datasets. --- @@ -35,7 +33,7 @@ ## Description -This is an implementation of an [MCP (Model Context Protocol) Server](https://modelcontextprotocol.io/quickstart/server) that exposes Sysdig Monitor capabilities to LLMs, plus the cross-cutting SysQL tooling. New tools and functionalities will be added over time following semantic versioning. The goal is to provide a simple and easy-to-use interface for querying information from the Sysdig platform using LLMs. +This is an implementation of an [MCP (Model Context Protocol) Server](https://modelcontextprotocol.io/quickstart/server) that exposes Sysdig Monitor capabilities to LLMs. New tools and functionalities will be added over time following semantic versioning. The goal is to provide a simple and easy-to-use interface for querying information from the Sysdig platform using LLMs. For Sysdig Secure-specific workflows, use the dedicated [@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server). @@ -44,7 +42,7 @@ For Sysdig Secure-specific workflows, use the dedicated [@sysdig/secure-mcp-serv Get up and running with the Sysdig MCP Server quickly using our pre-built Docker image. 1. **Get your API Token**: - Go to your Sysdig instance and navigate to **Settings > Sysdig Monitor API** (or **Sysdig Secure API** — either works, since SysQL tools accept both). This token is required to authenticate requests to the Sysdig Platform (See the [Configuration](#configuration) section for more details). + Go to your Sysdig instance and navigate to **Settings > Sysdig Monitor API**. This token is required to authenticate requests to the Sysdig Platform (See the [Configuration](#configuration) section for more details). 2. **Configure your MCP client**: @@ -142,19 +140,6 @@ The server dynamically filters the available tools based on the permissions asso > **Note:** When a time window is provided, the underlying PromQL is wrapped in the aggregation appropriate for each tool (`avg_over_time`, `max_over_time`, `min_over_time`, `increase`, etc.) and evaluated at `end`. See [`internal/infra/mcp/tools/README.md`](./internal/infra/mcp/tools/README.md) for the per-tool aggregation table. -### Sysdig Monitor & Sysdig Secure - -- **`generate_sysql`** - - **Description**: Generates a SysQL query from a natural language question. - - **Required Permission**: `sage.exec` - - **Sample Prompt**: "List top 10 pods by memory usage in the last hour" - - **Note**: The `generate_sysql` tool currently does not work with Service Account tokens and will return a 500 error. For this tool, use an API token assigned to a regular user account. - -- **`run_sysql`** - - **Description**: Execute a pre-written SysQL query directly (use only when user provides explicit query). - - **Required Permission**: `sage.exec`, `risks.read` - - **Sample Prompt**: "Run this query: MATCH CloudResource WHERE type = 'aws_s3_bucket' LIMIT 10" - ## Requirements - [Go](https://go.dev/doc/install) 1.26 or higher (if running without Docker). @@ -216,8 +201,6 @@ To use the MCP server tools, your API token needs specific permissions on the Sy | Permission | Sysdig UI Permission Name | |----------------------|---------------------------------------------| | `metrics-data.read` | Data Access Settings: "Metrics Data" (Read) | -| `risks.read` | Risks: "Access to risk feature" (Read) | -| `sage.exec` | SysQL: "AI Query Generation" (Exec) | **Additional Permissions:** @@ -234,9 +217,6 @@ To use the MCP server tools, your API token needs specific permissions on the Sy For detailed instructions, see the official [Sysdig Roles Administration documentation](https://docs.sysdig.com/en/administration/roles-administration/). ->[!IMPORTANT] -> **Service Account Limitation:** The `generate_sysql` tool currently does not work with Service Account tokens and will return a 500 error. For this tool, use an API token assigned to a regular user account. - ## Server Setup diff --git a/cmd/server/main.go b/cmd/server/main.go index 84b82a6..b20f236 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -113,9 +113,6 @@ func setupHandler(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *mcp systemClock := clock.NewSystemClock() handler := mcp.NewHandler(Version, sysdigClient) handler.RegisterTools( - tools.NewToolRunSysql(sysdigClient), - tools.NewToolGenerateSysql(sysdigClient), - tools.NewK8sListClusters(sysdigClient, systemClock), tools.NewK8sListNodes(sysdigClient, systemClock), tools.NewK8sListCronjobs(sysdigClient, systemClock), diff --git a/docker-base-aarch64.nix b/docker-base-aarch64.nix index 4dcb7a6..97c1b0d 100644 --- a/docker-base-aarch64.nix +++ b/docker-base-aarch64.nix @@ -1,7 +1,7 @@ { imageName = "quay.io/sysdig/sysdig-mini-ubi9"; - imageDigest = "sha256:4c41436ce108c1576399e4c624f72238c3a9577b570a97115d941c907bf40909"; - hash = "sha256-eLn7KUR4QqmHr5eVIdRy9uR0J1ooCPfUHxTLDMOlV0w="; + imageDigest = "sha256:51a8e50674f95e4e3089e1b44ad3ee61f7b1979e5f9edac8de726c2acd997349"; + hash = "sha256-jTmkEwnYSNBMIOr8aBLCGzC+bh+Xjm3to51mqkl3Hyc="; finalImageName = "quay.io/sysdig/sysdig-mini-ubi9"; finalImageTag = "1"; } diff --git a/docker-base-amd64.nix b/docker-base-amd64.nix index 0177210..33e73d2 100644 --- a/docker-base-amd64.nix +++ b/docker-base-amd64.nix @@ -1,7 +1,7 @@ { imageName = "quay.io/sysdig/sysdig-mini-ubi9"; - imageDigest = "sha256:4c41436ce108c1576399e4c624f72238c3a9577b570a97115d941c907bf40909"; - hash = "sha256-XLdqxTuzuRc6ariE/Q3ME/pC/PvT/sQtXmXWhWoLT44="; + imageDigest = "sha256:51a8e50674f95e4e3089e1b44ad3ee61f7b1979e5f9edac8de726c2acd997349"; + hash = "sha256-6X3VUknvtn8AiNbZHLAmFLoidqqSgnwrPl684za+U0U="; finalImageName = "quay.io/sysdig/sysdig-mini-ubi9"; finalImageTag = "1"; } diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 05f97a8..81141e8 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -12,8 +12,5 @@ **Problem**: Tests failing with "command not found" - **Solution**: Enter Nix shell with `nix develop` or `direnv allow`. All dev tools are provided by the flake. -**Problem**: `generate_sysql` returning 500 error -- **Solution**: This tool requires a regular user API token, not a Service Account token. Switch to a user-based token. - **Problem**: Pre-commit hooks not running - **Solution**: Run `pre-commit install` to install git hooks, then `pre-commit run -a` to test all files. diff --git a/flake.lock b/flake.lock index f81c2dc..d3ce5d6 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1780930886, - "narHash": "sha256-rppURzHviaQN131F+nLiLdGfcb0uCd9gGP0E5+iw9MI=", + "lastModified": 1781607440, + "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8c3cede7ddc26bd659d2d383b5610efbd2c7a16e", + "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158", "type": "github" }, "original": { @@ -36,11 +36,11 @@ }, "nixpkgs-25-11": { "locked": { - "lastModified": 1780511130, - "narHash": "sha256-2v9lT4ya59Lh1FqPeLnz1MoX9y/wz2huqfe9RtQZITk=", + "lastModified": 1781509190, + "narHash": "sha256-uJZs9Di8I6ciTp6jiojj0HzlNpBkud8ax5aT/O5aJkw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "535f3e6942cb1cead3929c604320d3db54b542b9", + "rev": "d6df3513510aa548c83868fd22bfddd0a8c0a0d4", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index b496b72..5ad8c1c 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/sysdiglabs/sysdig-mcp-server go 1.26 require ( - github.com/mark3labs/mcp-go v0.54.1 + github.com/mark3labs/mcp-go v0.55.0 github.com/oapi-codegen/runtime v1.4.1 - github.com/onsi/ginkgo/v2 v2.29.0 - github.com/onsi/gomega v1.41.0 + github.com/onsi/ginkgo/v2 v2.31.0 + github.com/onsi/gomega v1.42.0 github.com/spf13/cobra v1.10.2 go.uber.org/mock v0.6.0 gopkg.in/yaml.v2 v2.4.0 @@ -28,9 +28,9 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/text v0.38.0 // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/tools v0.46.0 // indirect ) diff --git a/go.sum b/go.sum index 3be70fd..56e3c00 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mark3labs/mcp-go v0.54.1 h1:Ap/ptEB9FtWzFKM8NDsTA7QDxerQOC06eZigrTldVj0= -github.com/mark3labs/mcp-go v0.54.1/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas= +github.com/mark3labs/mcp-go v0.55.0 h1:lJfz2aoctiwK+sI991+uIYwmKNIBciI+O7zsyDsa4U8= +github.com/mark3labs/mcp-go v0.55.0/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= @@ -51,10 +51,10 @@ github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/ github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/runtime v1.4.1 h1:9nwLoI+KrWxzbBcp0jO/R8uXqbik/HUyCvPeU68Y/qo= github.com/oapi-codegen/runtime v1.4.1/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= -github.com/onsi/ginkgo/v2 v2.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag= -github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= -github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA= -github.com/onsi/gomega v1.41.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/onsi/ginkgo/v2 v2.31.0 h1:GtuJos5DFUV9EerYJo8RhYxosYNGvOdDE5haKq6Grfs= +github.com/onsi/ginkgo/v2 v2.31.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.42.0 h1:CJby8u36xb7v34W78F8WKvqTQP7PCMIPB78IVDB73l4= +github.com/onsi/gomega v1.42.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -90,16 +90,16 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/infra/mcp/tools/README.md b/internal/infra/mcp/tools/README.md index aafea75..07919b3 100644 --- a/internal/infra/mcp/tools/README.md +++ b/internal/infra/mcp/tools/README.md @@ -23,13 +23,6 @@ The handler filters tools dynamically based on the Sysdig user's permissions. Ea | `k8s_list_underutilized_pods_cpu_quota` | `tool_k8s_list_underutilized_pods_cpu_quota.go` | List Kubernetes pods with CPU usage below 25% of the quota limit. | `metrics-data.read` | "Show the top 10 underutilized pods by CPU quota in cluster 'production'" | | `k8s_list_underutilized_pods_memory_quota` | `tool_k8s_list_underutilized_pods_memory_quota.go` | List Kubernetes pods with memory usage below 25% of the limit. | `metrics-data.read` | "Show the top 10 underutilized pods by memory quota in cluster 'production'" | -### Sysdig Monitor & Sysdig Secure - -| Tool | File | Capability | Required Permissions | Useful Prompts | -|---|---|---|---|---| -| `generate_sysql` | `tool_generate_sysql.go` | Convert natural language to SysQL via Sysdig Sage. | `sage.exec` (does not work with Service Accounts) | "Create a SysQL to list S3 buckets." | -| `run_sysql` | `tool_run_sysql.go` | Execute caller-supplied Sysdig SysQL queries safely. | `sage.exec`, `risks.read` | "Run the following SysQL…". | - > Dedicated Sysdig Secure tools (runtime events, event details, process trees) live in the separate [@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server) package. ## Historical range (start / end) diff --git a/internal/infra/mcp/tools/tool_generate_sysql.go b/internal/infra/mcp/tools/tool_generate_sysql.go deleted file mode 100644 index b21cc7a..0000000 --- a/internal/infra/mcp/tools/tool_generate_sysql.go +++ /dev/null @@ -1,63 +0,0 @@ -package tools - -import ( - "context" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" -) - -type ToolGenerateSysql struct { - sysdigClient sysdig.ExtendedClientWithResponsesInterface -} - -func NewToolGenerateSysql(client sysdig.ExtendedClientWithResponsesInterface) *ToolGenerateSysql { - return &ToolGenerateSysql{ - sysdigClient: client, - } -} - -func (h *ToolGenerateSysql) handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - question := request.GetString("question", "") - if question == "" { - return mcp.NewToolResultError("question is required"), nil - } - - response, err := h.sysdigClient.GenerateSysqlWithResponse(ctx, question) - if err != nil { - return mcp.NewToolResultError("error triggering request: " + err.Error()), nil - } - if response.HTTPResponse.StatusCode >= 400 { - return mcp.NewToolResultErrorf("error generating SysQL query, status code: %d, response: %s", response.HTTPResponse.StatusCode, response.Body), nil - } - - res, err := mcp.NewToolResultJSON(response.JSON200) - if err != nil { - return mcp.NewToolResultError("error parsing response: " + err.Error()), nil - } - - return res, nil -} - -func (h *ToolGenerateSysql) RegisterInServer(s *server.MCPServer) { - tool := mcp.NewTool( - "generate_sysql", - mcp.WithDescription(`Generates a SysQL query from a natural language question.`), - mcp.WithString( - "question", - mcp.Description("A natural language question to be translated into a SysQL query."), - mcp.Required(), - Examples( - `List all my containers with packages affected by vulnerabilities`, - `Tell me the resources affected by any vulnerability that affects packages`, - `Give me the vulnerabilities affecting images`, - ), - ), - mcp.WithOutputSchema[map[string]any](), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - WithRequiredPermissions("sage.exec"), - ) - s.AddTool(tool, h.handle) -} diff --git a/internal/infra/mcp/tools/tool_generate_sysql_test.go b/internal/infra/mcp/tools/tool_generate_sysql_test.go deleted file mode 100644 index 3afecac..0000000 --- a/internal/infra/mcp/tools/tool_generate_sysql_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package tools_test - -import ( - "context" - "fmt" - "net/http" - - "github.com/mark3labs/mcp-go/client" - "github.com/mark3labs/mcp-go/mcp" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "go.uber.org/mock/gomock" - - inframcp "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp/tools" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig/mocks" -) - -var _ = Describe("ToolGenerateSysql", func() { - var ( - mockClient *mocks.MockExtendedClientWithResponsesInterface - tool *tools.ToolGenerateSysql - ctrl *gomock.Controller - handler *inframcp.Handler - mcpClient *client.Client - ) - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - mockClient = mocks.NewMockExtendedClientWithResponsesInterface(ctrl) - mockClient.EXPECT().GetMyPermissionsWithResponse(gomock.Any(), gomock.Any()).Return(&sysdig.GetMyPermissionsResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.UserPermissions{ - Permissions: []string{"sage.exec"}, - }, - }, nil).AnyTimes() - tool = tools.NewToolGenerateSysql(mockClient) - handler = inframcp.NewHandler("dev", mockClient) - handler.RegisterTools(tool) - - var err error - mcpClient, err = handler.ServeInProcessClient() - Expect(err).NotTo(HaveOccurred()) - - _, err = mcpClient.Initialize(context.Background(), mcp.InitializeRequest{}) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - ctrl.Finish() - }) - - It("should handle a successful request", func(ctx SpecContext) { - question := "all vulnerabilities across my workloads" - mockClient.EXPECT().GenerateSysqlWithResponse(gomock.Any(), question).Return(&sysdig.GenerateSysqlResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.SysqlQuery{ - Text: "MATCH KubeWorkload AFFECTED_BY Vulnerability RETURN KubeWorkload, Vulnerability;\n", - }, - }, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "generate_sysql", - Arguments: map[string]any{ - "question": question, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.StructuredContent).To(Equal(map[string]any{"text": "MATCH KubeWorkload AFFECTED_BY Vulnerability RETURN KubeWorkload, Vulnerability;\n"})) - Expect(result.IsError).To(BeFalse()) - }) - - It("should return an error if question is missing", func(ctx SpecContext) { - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "generate_sysql", - Arguments: map[string]any{}, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.IsError).To(BeTrue()) - Expect(result.Content[0]).To(Equal(mcp.TextContent{Type: "text", Text: "question is required"})) - }) - - It("should handle a client error", func(ctx SpecContext) { - question := "what is bash" - mockClient.EXPECT().GenerateSysqlWithResponse(gomock.Any(), question).Return(nil, fmt.Errorf("client error")) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "generate_sysql", - Arguments: map[string]any{ - "question": question, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.IsError).To(BeTrue()) - Expect(result.Content[0]).To(Equal(mcp.TextContent{Type: "text", Text: "error triggering request: client error"})) - }) - - It("should handle a non-200 status code", func(ctx SpecContext) { - question := "what is bash" - mockClient.EXPECT().GenerateSysqlWithResponse(gomock.Any(), question).Return(&sysdig.GenerateSysqlResponse{ - HTTPResponse: &http.Response{ - StatusCode: 404, - }, - Body: []byte("Not Found"), - }, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "generate_sysql", - Arguments: map[string]any{ - "question": question, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.IsError).To(BeTrue()) - Expect(result.Content[0]).To(Equal(mcp.TextContent{Type: "text", Text: "error generating SysQL query, status code: 404, response: Not Found"})) - }) -}) diff --git a/internal/infra/mcp/tools/tool_run_sysql.go b/internal/infra/mcp/tools/tool_run_sysql.go deleted file mode 100644 index aa9fb24..0000000 --- a/internal/infra/mcp/tools/tool_run_sysql.go +++ /dev/null @@ -1,70 +0,0 @@ -package tools - -import ( - "context" - "strings" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" -) - -type ToolRunSysql struct { - sysdigClient sysdig.ExtendedClientWithResponsesInterface -} - -func NewToolRunSysql(client sysdig.ExtendedClientWithResponsesInterface) *ToolRunSysql { - return &ToolRunSysql{ - sysdigClient: client, - } -} - -func (h *ToolRunSysql) handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - sysqlQuery := request.GetString("sysql_query", "") - if sysqlQuery == "" { - return mcp.NewToolResultErrorf("sysql_query is required"), nil - } - - // Ensure the query ends with a semicolon - if !strings.HasSuffix(strings.TrimSpace(sysqlQuery), ";") { - sysqlQuery = sysqlQuery + ";" - } - - // Create the request body - body := sysdig.QuerySysqlPostJSONRequestBody{ - Q: sysqlQuery, - } - - response, err := h.sysdigClient.QuerySysqlPostWithResponse(ctx, body) - if err != nil { - return mcp.NewToolResultErrorFromErr("error triggering request", err), nil - } - if response.StatusCode() >= 400 { - return mcp.NewToolResultErrorf("error retrieving SysQL results, status code: %d, response: %s", response.StatusCode(), response.Body), nil - } - - return mcp.NewToolResultJSON(response.JSON200) -} - -func (h *ToolRunSysql) RegisterInServer(s *server.MCPServer) { - tool := mcp.NewTool( - "run_sysql", - mcp.WithDescription(`Execute a SysQL query directly against the Sysdig API. You should try generating a SysQL query first to ensure that it's valid.`), - mcp.WithString( - "sysql_query", - mcp.Description("A valid SysQL query string to execute directly."), - mcp.Required(), - Examples( - `MATCH Vulnerability WHERE Vulnerability.severity = 'Critical' RETURN Vulnerability LIMIT 10`, - `MATCH KubeWorkload AFFECTED_BY Vulnerability WHERE KubeWorkload.namespaceName = 'production' RETURN KubeWorkload, Vulnerability`, - `MATCH CloudResource WHERE CloudResource.type =~ '(?i).*S3 Bucket.*' RETURN DISTINCT CloudResource`, - `MATCH Vulnerability WHERE Vulnerability.name =~ '(?i)CVE-2024-1234' RETURN Vulnerability`, - ), - ), - mcp.WithOutputSchema[map[string]any](), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - WithRequiredPermissions("sage.exec", "risks.read"), - ) - s.AddTool(tool, h.handle) -} diff --git a/internal/infra/mcp/tools/tool_run_sysql_test.go b/internal/infra/mcp/tools/tool_run_sysql_test.go deleted file mode 100644 index e5ef1e0..0000000 --- a/internal/infra/mcp/tools/tool_run_sysql_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package tools_test - -import ( - "context" - "fmt" - "net/http" - - "github.com/mark3labs/mcp-go/client" - "github.com/mark3labs/mcp-go/mcp" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "go.uber.org/mock/gomock" - - inframcp "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp/tools" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig/mocks" -) - -var _ = Describe("ToolRunSysql", func() { - var ( - mockClient *mocks.MockExtendedClientWithResponsesInterface - tool *tools.ToolRunSysql - ctrl *gomock.Controller - handler *inframcp.Handler - mcpClient *client.Client - ) - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - mockClient = mocks.NewMockExtendedClientWithResponsesInterface(ctrl) - mockClient.EXPECT().GetMyPermissionsWithResponse(gomock.Any(), gomock.Any()).Return(&sysdig.GetMyPermissionsResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.UserPermissions{ - Permissions: []string{"sage.exec", "risks.read"}, - }, - }, nil).AnyTimes() - tool = tools.NewToolRunSysql(mockClient) - handler = inframcp.NewHandler("dev", mockClient) - handler.RegisterTools(tool) - - var err error - mcpClient, err = handler.ServeInProcessClient() - Expect(err).NotTo(HaveOccurred()) - - _, err = mcpClient.Initialize(context.Background(), mcp.InitializeRequest{}) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - ctrl.Finish() - }) - - It("should handle a successful request with a query ending in semicolon", func(ctx SpecContext) { - sysqlQuery := "MATCH Vulnerability WHERE severity = 'Critical' LIMIT 10;" - - expectedBody := sysdig.QuerySysqlPostJSONRequestBody{ - Q: sysqlQuery, - } - - mockClient.EXPECT().QuerySysqlPostWithResponse(gomock.Any(), expectedBody).Return(&sysdig.QuerySysqlPostResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.QueryResponse{ - Items: []map[string]any{ - {"id": "vuln-1", "severity": "Critical"}, - }, - }, - }, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "run_sysql", - Arguments: map[string]any{ - "sysql_query": sysqlQuery, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeFalse()) - }) - - It("should add a semicolon if the query does not end with one", func(ctx SpecContext) { - sysqlQueryWithoutSemicolon := "MATCH Vulnerability WHERE severity = 'Critical' LIMIT 10" - sysqlQueryWithSemicolon := sysqlQueryWithoutSemicolon + ";" - - // Use DoAndReturn to explicitly verify that the semicolon was added to body.Q - mockClient.EXPECT().QuerySysqlPostWithResponse( - gomock.Any(), - gomock.AssignableToTypeOf(sysdig.QuerySysqlPostJSONRequestBody{}), - ).DoAndReturn(func(ctx context.Context, body sysdig.QuerySysqlPostJSONRequestBody, reqEditors ...sysdig.RequestEditorFn) (*sysdig.QuerySysqlPostResponse, error) { - // Explicitly verify that the semicolon was added and that it is exactly as expected - Expect(body.Q).To(Equal(sysqlQueryWithSemicolon), "Expected the query to have a semicolon appended") - Expect(body.Q).To(HaveSuffix(";"), "Expected the query to end with a semicolon") - - return &sysdig.QuerySysqlPostResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.QueryResponse{ - Items: []map[string]any{ - {"id": "vuln-1", "severity": "Critical"}, - }, - }, - }, nil - }) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "run_sysql", - Arguments: map[string]any{ - "sysql_query": sysqlQueryWithoutSemicolon, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeFalse()) - }) - - It("should handle queries with trailing whitespace before semicolon", func(ctx SpecContext) { - sysqlQueryWithWhitespace := "MATCH Vulnerability WHERE severity = 'Critical' LIMIT 10 ;" - - expectedBody := sysdig.QuerySysqlPostJSONRequestBody{ - Q: sysqlQueryWithWhitespace, - } - - mockClient.EXPECT().QuerySysqlPostWithResponse(gomock.Any(), expectedBody).Return(&sysdig.QuerySysqlPostResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.QueryResponse{ - Items: []map[string]any{ - {"id": "vuln-1", "severity": "Critical"}, - }, - }, - }, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "run_sysql", - Arguments: map[string]any{ - "sysql_query": sysqlQueryWithWhitespace, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeFalse()) - }) - - It("should return an error if sysql_query is missing", func(ctx SpecContext) { - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "run_sysql", - Arguments: map[string]any{}, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) - - It("should return an error if sysql_query is empty", func(ctx SpecContext) { - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "run_sysql", - Arguments: map[string]any{ - "sysql_query": "", - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) - - It("should handle a client error", func(ctx SpecContext) { - sysqlQuery := "MATCH Vulnerability WHERE severity = 'Critical' LIMIT 10;" - - expectedBody := sysdig.QuerySysqlPostJSONRequestBody{ - Q: sysqlQuery, - } - - mockClient.EXPECT().QuerySysqlPostWithResponse(gomock.Any(), expectedBody).Return(nil, fmt.Errorf("client error")) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "run_sysql", - Arguments: map[string]any{ - "sysql_query": sysqlQuery, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) - - It("should handle a non-200 status code", func(ctx SpecContext) { - sysqlQuery := "MATCH Vulnerability WHERE severity = 'Critical' LIMIT 10;" - - expectedBody := sysdig.QuerySysqlPostJSONRequestBody{ - Q: sysqlQuery, - } - - mockClient.EXPECT().QuerySysqlPostWithResponse(gomock.Any(), expectedBody).Return(&sysdig.QuerySysqlPostResponse{ - HTTPResponse: &http.Response{ - StatusCode: 400, - }, - Body: []byte("Bad Request: Invalid query syntax"), - }, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "run_sysql", - Arguments: map[string]any{ - "sysql_query": sysqlQuery, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) -}) diff --git a/internal/infra/sysdig/client_extension.go b/internal/infra/sysdig/client_extension.go index 2f8f8ad..787416e 100644 --- a/internal/infra/sysdig/client_extension.go +++ b/internal/infra/sysdig/client_extension.go @@ -8,5 +8,4 @@ type ExtendedClientWithResponsesInterface interface { ClientInterface ClientWithResponsesInterface GetMyPermissionsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetMyPermissionsResponse, error) - GenerateSysqlWithResponse(ctx context.Context, question string, reqEditors ...RequestEditorFn) (*GenerateSysqlResponse, error) } diff --git a/internal/infra/sysdig/client_generate_sysql.go b/internal/infra/sysdig/client_generate_sysql.go deleted file mode 100644 index 7871bc5..0000000 --- a/internal/infra/sysdig/client_generate_sysql.go +++ /dev/null @@ -1,119 +0,0 @@ -package sysdig - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" -) - -type GenerateSysqlResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SysqlQuery -} - -func (r *GenerateSysqlResponse) String() string { - if r.Body != nil { - return string(r.Body) - } - return "" -} - -type SysqlQuery struct { - ID string `json:"id,omitempty"` - Text string `json:"text"` - Sender string `json:"sender,omitempty"` - Type string `json:"type,omitempty"` - Time string `json:"time,omitempty"` - Status string `json:"status,omitempty"` - Metadata *struct { - SubscriptionInfo struct { - MonthlyLimit int `json:"monthly_limit,omitempty"` - MonthlyCount int `json:"monthly_count,omitempty"` - WarningPercent int `json:"warning_percent,omitempty"` - } `json:"subscription_info"` - TranslateErrorType *string `json:"translate_error_type,omitempty"` - AlternativeQuestions []string `json:"alternative_questions,omitempty"` - } `json:"metadata,omitempty"` -} - -func NewGenerateSysqlRequest(server string, question string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := "/api/sage/sysql/generate" - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - queryValues := queryURL.Query() - queryValues.Set("question", question) - queryURL.RawQuery = queryValues.Encode() - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -func (c *Client) GenerateSysql(ctx context.Context, question string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGenerateSysqlRequest(c.Server, question) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func ParseGenerateSysqlResponse(rsp *http.Response) (*GenerateSysqlResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GenerateSysqlResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - temp := &SysqlQuery{} - if err := json.Unmarshal(bodyBytes, &temp); err != nil { - return nil, fmt.Errorf("failed to parse generate sysql response: %w. Body: %s", err, string(bodyBytes)) - } - - switch rsp.StatusCode { - case http.StatusOK: - var dest SysqlQuery - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - } - return response, nil -} - -func (c *ClientWithResponses) GenerateSysqlWithResponse(ctx context.Context, question string, reqEditors ...RequestEditorFn) (*GenerateSysqlResponse, error) { - rsp, err := c.ClientInterface.(*Client).GenerateSysql(ctx, question, reqEditors...) - if err != nil { - return nil, err - } - return ParseGenerateSysqlResponse(rsp) -} diff --git a/internal/infra/sysdig/client_generate_sysql_integration_test.go b/internal/infra/sysdig/client_generate_sysql_integration_test.go deleted file mode 100644 index 4222c72..0000000 --- a/internal/infra/sysdig/client_generate_sysql_integration_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package sysdig_test - -import ( - "context" - "net/http" - "os" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" -) - -var _ = Describe("Sysdig Generate Sysql Client", func() { - var client sysdig.ExtendedClientWithResponsesInterface - - BeforeEach(func() { - sysdigURL := os.Getenv("SYSDIG_MCP_API_HOST") - sysdigToken := os.Getenv("SYSDIG_MCP_API_TOKEN") - - var err error - client, err = sysdig.NewSysdigClient(sysdig.WithFixedHostAndToken(sysdigURL, sysdigToken)) - Expect(err).ToNot(HaveOccurred()) - }) - - Context("when generating a sysql query", func() { - It("should return the sysql query successfully", func(ctx context.Context) { - resp, err := client.GenerateSysqlWithResponse(ctx, "what are the latest events?") - Expect(err).ToNot(HaveOccurred()) - Expect(resp.HTTPResponse.StatusCode).To(Equal(http.StatusOK)) - Expect(resp.JSON200).ToNot(BeNil()) - Expect(resp.JSON200.Text).ToNot(BeEmpty()) - }) - }) -}) diff --git a/internal/infra/sysdig/mocks/client_extension.go b/internal/infra/sysdig/mocks/client_extension.go index 0dc8961..c5cc445 100644 --- a/internal/infra/sysdig/mocks/client_extension.go +++ b/internal/infra/sysdig/mocks/client_extension.go @@ -2723,26 +2723,6 @@ func (mr *MockExtendedClientWithResponsesInterfaceMockRecorder) EditZoneV1WithRe return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditZoneV1WithResponse", reflect.TypeOf((*MockExtendedClientWithResponsesInterface)(nil).EditZoneV1WithResponse), varargs...) } -// GenerateSysqlWithResponse mocks base method. -func (m *MockExtendedClientWithResponsesInterface) GenerateSysqlWithResponse(ctx context.Context, question string, reqEditors ...sysdig.RequestEditorFn) (*sysdig.GenerateSysqlResponse, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, question} - for _, a := range reqEditors { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GenerateSysqlWithResponse", varargs...) - ret0, _ := ret[0].(*sysdig.GenerateSysqlResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GenerateSysqlWithResponse indicates an expected call of GenerateSysqlWithResponse. -func (mr *MockExtendedClientWithResponsesInterfaceMockRecorder) GenerateSysqlWithResponse(ctx, question any, reqEditors ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, question}, reqEditors...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateSysqlWithResponse", reflect.TypeOf((*MockExtendedClientWithResponsesInterface)(nil).GenerateSysqlWithResponse), varargs...) -} - // GetAcceptedRiskV1 mocks base method. func (m *MockExtendedClientWithResponsesInterface) GetAcceptedRiskV1(ctx context.Context, acceptedRiskID sysdig.AcceptedRiskID, reqEditors ...sysdig.RequestEditorFn) (*http.Response, error) { m.ctrl.T.Helper() diff --git a/package.nix b/package.nix index 90122a9..4a4c610 100644 --- a/package.nix +++ b/package.nix @@ -1,10 +1,10 @@ { buildGo126Module, versionCheckHook }: buildGo126Module (finalAttrs: { pname = "sysdig-mcp-server"; - version = "2.0.1"; + version = "3.0.0"; src = ./.; # This hash is automatically re-calculated with `just rehash-package-nix`. This is automatically called as well by `just update`. - vendorHash = "sha256-diD5m9+O7Qn7ln7F/R22Iaz2wNf7yth7ef4JEafrR9o="; + vendorHash = "sha256-/0V1S76eMZUH/TZqV+cKJTihJo8y+Qau9riGGXRa4lk="; subPackages = [ "cmd/server"