From 5637b6f72f14459d896e0dc80cd69f1c7b10c82b Mon Sep 17 00:00:00 2001 From: Dmitrii Korotovskii Date: Thu, 8 Jan 2026 22:14:37 +0100 Subject: [PATCH 01/23] Improved integration tests on channels list --- pkg/handler/channels_test.go | 286 ++++++++++++++--------------------- 1 file changed, 114 insertions(+), 172 deletions(-) diff --git a/pkg/handler/channels_test.go b/pkg/handler/channels_test.go index 65a6e079..2e8286c5 100644 --- a/pkg/handler/channels_test.go +++ b/pkg/handler/channels_test.go @@ -3,60 +3,36 @@ package handler import ( "context" "encoding/csv" - "encoding/json" "fmt" - "os" "regexp" "strconv" "strings" "testing" + "time" "github.com/google/uuid" "github.com/korotovsky/slack-mcp-server/pkg/test/util" - "github.com/openai/openai-go/packages/param" + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/openai/openai-go" - "github.com/openai/openai-go/option" - "github.com/openai/openai-go/responses" ) -type channelsListToolArgs struct { - ChannelTypes ChannelTypes `json:"channel_types"` - Cursor string `json:"cursor"` - Limit int `json:"limit"` - Sort string `json:"sort,omitempty"` +type testEnv struct { + mcpClient *client.Client + ctx context.Context } -type ChannelTypes []string - -func (c *ChannelTypes) UnmarshalJSON(data []byte) error { - var raw string - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - parts := strings.Split(raw, ",") - allowed := map[string]bool{ - "public_channel": true, - "private_channel": true, - "im": true, - "mpim": true, - } - for _, ch := range parts { - if !allowed[ch] { - return fmt.Errorf("invalid channel type %q", ch) - } - } - *c = parts - return nil +type matchingRule struct { + csvFieldName string + csvFieldValueRE string } -func TestIntegrationChannelsList(t *testing.T) { +func setupTestEnv(t *testing.T) (*testEnv, func()) { + t.Helper() + sseKey := uuid.New().String() require.NotEmpty(t, sseKey, "sseKey must be generated for integration tests") - apiKey := os.Getenv("SLACK_MCP_OPENAI_API") - require.NotEmpty(t, apiKey, "SLACK_MCP_OPENAI_API must be set for integration tests") cfg := util.MCPConfig{ SSEKey: sseKey, @@ -64,158 +40,124 @@ func TestIntegrationChannelsList(t *testing.T) { MessageToolMark: true, } - mcp, err := util.SetupMCP(cfg) - if err != nil { - t.Fatalf("Failed to set up MCP server: %v", err) + mcpServer, err := util.SetupMCP(cfg) + require.NoError(t, err, "Failed to set up MCP server") + + fwd, err := util.SetupForwarding(context.Background(), "http://"+mcpServer.Host+":"+strconv.Itoa(mcpServer.Port)) + require.NoError(t, err, "Failed to set up ngrok forwarding") + + sseURL := fmt.Sprintf("%s://%s/sse", fwd.URL.Scheme, fwd.URL.Host) + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + + mcpClient, err := client.NewSSEMCPClient(sseURL, + client.WithHeaders(map[string]string{ + "Authorization": "Bearer " + sseKey, + }), + ) + require.NoError(t, err, "Failed to create MCP client") + + err = mcpClient.Start(ctx) + require.NoError(t, err, "Failed to start MCP client") + + initReq := mcp.InitializeRequest{} + initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initReq.Params.ClientInfo = mcp.Implementation{ + Name: "channels-test-client", + Version: "1.0.0", } - fwd, err := util.SetupForwarding(context.Background(), "http://"+mcp.Host+":"+strconv.Itoa(mcp.Port)) - if err != nil { - t.Fatalf("Failed to set up ngrok forwarding: %v", err) + initReq.Params.Capabilities = mcp.ClientCapabilities{} + + _, err = mcpClient.Initialize(ctx, initReq) + require.NoError(t, err, "Failed to initialize MCP client") + + cleanup := func() { + cancel() + mcpClient.Close() + fwd.Shutdown() + mcpServer.Shutdown() } - defer fwd.Shutdown() - defer mcp.Shutdown() - client := openai.NewClient(option.WithAPIKey(apiKey)) - ctx := context.Background() + return &testEnv{ + mcpClient: mcpClient, + ctx: ctx, + }, cleanup +} + +func runChannelTest(t *testing.T, env *testEnv, channelType string, expectedChannels []matchingRule) { + t.Helper() - type matchingRule struct { - csvFieldName string - csvFieldValueRE string - RowPosition *int - TotalRows *int + callReq := mcp.CallToolRequest{} + callReq.Params.Name = "channels_list" + callReq.Params.Arguments = map[string]any{ + "channel_types": channelType, } - type tc struct { - name string - input string - expectedToolName string - expectedToolOutputMatchingRules []matchingRule - expectedLLMOutputMatchingRules []string + result, err := env.mcpClient.CallTool(env.ctx, callReq) + require.NoError(t, err, "Tool call failed") + require.NotNil(t, result, "Tool result is nil") + require.False(t, result.IsError, "Tool returned error") + + var toolOutput strings.Builder + for _, content := range result.Content { + if textContent, ok := content.(mcp.TextContent); ok { + toolOutput.WriteString(textContent.Text) + } } - cases := []tc{ - { - name: "Get list of channels", - input: "Provide a list of slack channels.", - expectedToolName: "channels_list", - expectedToolOutputMatchingRules: []matchingRule{ - { - csvFieldName: "Name", - csvFieldValueRE: `^#general$`, - }, - { - csvFieldName: "Name", - csvFieldValueRE: `^#testcase-1$`, - }, - { - csvFieldName: "Name", - csvFieldValueRE: `^#testcase-2$`, - }, - { - csvFieldName: "Name", - csvFieldValueRE: `^#testcase-3$`, - }, - }, - expectedLLMOutputMatchingRules: []string{ - "channels", "#general", "#testcase-1", "#testcase-2", "#testcase-3", - }, - }, + require.NotEmpty(t, toolOutput.String(), "No tool output captured") + + reader := csv.NewReader(strings.NewReader(toolOutput.String())) + rows, err := reader.ReadAll() + require.NoError(t, err, "Failed to parse CSV") + require.GreaterOrEqual(t, len(rows), 1, "CSV must have at least a header row") + + header := rows[0] + dataRows := rows[1:] + colIndex := map[string]int{} + for i, col := range header { + colIndex[col] = i } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - params := responses.ResponseNewParams{ - Model: "gpt-4.1-mini", - Tools: []responses.ToolUnionParam{ - { - OfMcp: &responses.ToolMcpParam{ - ServerLabel: "slack-mcp-server", - ServerURL: fmt.Sprintf("%s://%s/sse", fwd.URL.Scheme, fwd.URL.Host), - RequireApproval: responses.ToolMcpRequireApprovalUnionParam{ - OfMcpToolApprovalSetting: param.NewOpt("never"), - }, - Headers: map[string]string{ - "Authorization": "Bearer " + sseKey, - }, - }, - }, - }, - Input: responses.ResponseNewParamsInputUnion{ - OfString: openai.String(tc.input), - }, - } + for _, rule := range expectedChannels { + idx, ok := colIndex[rule.csvFieldName] + require.Truef(t, ok, "CSV did not contain column %q, toolOutput: %q", rule.csvFieldName, toolOutput.String()) + + re, err := regexp.Compile(rule.csvFieldValueRE) + require.NoErrorf(t, err, "Invalid regex %q", rule.csvFieldValueRE) - resp, err := client.Responses.New(ctx, params) - require.NoError(t, err, "API call failed") - - assert.NotNil(t, resp.Status, "completed") - - var llmOutput strings.Builder - var toolOutput strings.Builder - for _, out := range resp.Output { - if out.Type == "message" && out.Role == "assistant" { - for _, c := range out.Content { - if c.Type == "output_text" { - llmOutput.WriteString(c.Text) - } - } - } - if out.Type == "mcp_call" && out.Name == tc.expectedToolName { - toolOutput.WriteString(out.Output) - } + found := false + for _, row := range dataRows { + if idx < len(row) && re.MatchString(row[idx]) { + found = true + break } + } + assert.Truef(t, found, "No row in column %q matched %q; full CSV:\n%s", + rule.csvFieldName, rule.csvFieldValueRE, toolOutput.String()) + } +} - require.NotEmpty(t, toolOutput, "no tool output captured") +func TestIntegrationPublicChannelsList(t *testing.T) { + env, cleanup := setupTestEnv(t) + defer cleanup() - // Parse CSV - reader := csv.NewReader(strings.NewReader(toolOutput.String())) - rows, err := reader.ReadAll() - require.NoError(t, err, "failed to parse CSV") + expectedChannels := []matchingRule{ + {csvFieldName: "Name", csvFieldValueRE: `^#general$`}, + {csvFieldName: "Name", csvFieldValueRE: `^#testcase-1$`}, + {csvFieldName: "Name", csvFieldValueRE: `^#testcase-2$`}, + {csvFieldName: "Name", csvFieldValueRE: `^#testcase-3$`}, + } - header := rows[0] - dataRows := rows[1:] - colIndex := map[string]int{} - for i, col := range header { - colIndex[col] = i - } + runChannelTest(t, env, "public_channel", expectedChannels) +} - for _, rule := range tc.expectedToolOutputMatchingRules { - if rule.TotalRows != nil && *rule.TotalRows > 0 { - assert.Equalf(t, *rule.TotalRows, len(dataRows), - "expected %d data rows, got %d", rule.TotalRows, len(dataRows)) - } - - idx, ok := colIndex[rule.csvFieldName] - require.Truef(t, ok, "CSV did not contain column %q, toolOutput: %q", rule.csvFieldName, toolOutput.String()) - - re, err := regexp.Compile(rule.csvFieldValueRE) - require.NoErrorf(t, err, "invalid regex %q", rule.csvFieldValueRE) - - if rule.RowPosition != nil && *rule.RowPosition >= 0 { - require.Lessf(t, rule.RowPosition, len(dataRows), "RowPosition %d out of range (only %d data rows)", rule.RowPosition, len(dataRows)) - value := dataRows[*rule.RowPosition][idx] - assert.Regexpf(t, re, value, "row %d, column %q: expected to match %q, got %q", - rule.RowPosition, rule.csvFieldName, rule.csvFieldValueRE, value) - continue - } - - found := false - for _, row := range dataRows { - if idx < len(row) && re.MatchString(row[idx]) { - found = true - break - } - } - assert.Truef(t, found, "no row in column %q matched %q; full CSV:\n%s", - rule.csvFieldName, rule.csvFieldValueRE, toolOutput.String()) - } +func TestIntegrationPrivateChannelsList(t *testing.T) { + env, cleanup := setupTestEnv(t) + defer cleanup() - for _, pattern := range tc.expectedLLMOutputMatchingRules { - re, err := regexp.Compile(pattern) - require.NoErrorf(t, err, "invalid LLM regex %q", pattern) - assert.Regexpf(t, re, llmOutput.String(), "LLM output did not match regex %q; output:\n%s", - pattern, llmOutput.String()) - } - }) + expectedChannels := []matchingRule{ + {csvFieldName: "Name", csvFieldValueRE: `^#testcase-4$`}, } + + runChannelTest(t, env, "private_channel", expectedChannels) } From cd36f5edff126800b74296b33eb9fdb892777191 Mon Sep 17 00:00:00 2001 From: Bhaskoro Muthohar Date: Tue, 20 Jan 2026 15:18:33 +0700 Subject: [PATCH 02/23] fix: wait for cache to be ready before serving stdio requests Fixes #135 --- cmd/slack-mcp-server/main.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/slack-mcp-server/main.go b/cmd/slack-mcp-server/main.go index c555bc12..1b3b360f 100644 --- a/cmd/slack-mcp-server/main.go +++ b/cmd/slack-mcp-server/main.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/korotovsky/slack-mcp-server/pkg/provider" "github.com/korotovsky/slack-mcp-server/pkg/server" @@ -51,6 +52,12 @@ func main() { switch transport { case "stdio": + for { + if ready, _ := p.IsReady(); ready { + break + } + time.Sleep(100 * time.Millisecond) + } if err := s.ServeStdio(); err != nil { logger.Fatal("Server error", zap.String("context", "console"), From 95fa6dfaff8e2effce0ec31d0b1420e522cae9c6 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Tue, 20 Jan 2026 17:47:40 +0200 Subject: [PATCH 03/23] feat: Set mcp-server as entrypoint in Dockerfile --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9d65f601..becf5a85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,8 @@ WORKDIR /app/mcp-server EXPOSE 3001 -CMD ["mcp-server", "--transport", "sse"] +ENTRYPOINT ["mcp-server"] +CMD ["--transport", "sse"] FROM alpine:3.22 AS production @@ -39,4 +40,5 @@ WORKDIR /app EXPOSE 3001 -CMD ["mcp-server", "--transport", "sse"] +ENTRYPOINT ["mcp-server"] +CMD ["--transport", "sse"] From aa79f8f730e13edc4f037ffcb083551d65ab22fb Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Wed, 21 Jan 2026 12:55:34 +0200 Subject: [PATCH 04/23] docs: update docker examples for ENTRYPOINT --- docs/03-configuration-and-usage.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/03-configuration-and-usage.md b/docs/03-configuration-and-usage.md index 530ec470..1550fdbf 100644 --- a/docs/03-configuration-and-usage.md +++ b/docs/03-configuration-and-usage.md @@ -128,7 +128,6 @@ Open your `claude_desktop_config.json` and add the mcp server to the list of `mc "-e", "SLACK_MCP_XOXP_TOKEN", "ghcr.io/korotovsky/slack-mcp-server", - "mcp-server", "--transport", "stdio" ], @@ -155,7 +154,6 @@ Open your `claude_desktop_config.json` and add the mcp server to the list of `mc "-e", "SLACK_MCP_XOXD_TOKEN", "ghcr.io/korotovsky/slack-mcp-server", - "mcp-server", "--transport", "stdio" ], @@ -245,7 +243,7 @@ docker pull ghcr.io/korotovsky/slack-mcp-server:latest docker run -i --rm \ -e SLACK_MCP_XOXC_TOKEN \ -e SLACK_MCP_XOXD_TOKEN \ - slack-mcp-server mcp-server --transport stdio + ghcr.io/korotovsky/slack-mcp-server:latest --transport stdio ``` Or, the docker-compose way: From ed0ced28f18d001ac6532da12d11dcf2d217a765 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Thu, 22 Jan 2026 08:55:30 +0200 Subject: [PATCH 05/23] ci: add Trivy scan for Go dependencies --- .github/workflows/security-trivy.yaml | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/security-trivy.yaml diff --git a/.github/workflows/security-trivy.yaml b/.github/workflows/security-trivy.yaml new file mode 100644 index 00000000..d4002c08 --- /dev/null +++ b/.github/workflows/security-trivy.yaml @@ -0,0 +1,31 @@ +name: Security (Trivy) + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + trivy-fs: + name: Trivy filesystem scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.1 + + - name: Run Trivy (Go dependencies) + uses: aquasecurity/trivy-action@0.33.1 + with: + scan-type: fs + scan-ref: . + scanners: vuln + vuln-type: library + severity: CRITICAL,HIGH + format: table + ignore-unfixed: true + exit-code: 1 From 4edf1b0c01ff953b6209e112915fbdeb084011a2 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Thu, 22 Jan 2026 09:03:24 +0200 Subject: [PATCH 06/23] ci: add Dependabot for Go and GitHub Actions --- .github/dependabot.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..7487d58c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +version: 2 + +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 3 + groups: + go-dependencies: + patterns: + - "*" + commit-message: + prefix: "deps(go)" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 2 + groups: + github-actions: + patterns: + - "*" + commit-message: + prefix: "deps(gha)" From 2fcebee117a669e0b0400000997ae9848536de40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 07:32:22 +0000 Subject: [PATCH 07/23] deps(gha): bump the github-actions group with 2 updates Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-go](https://github.com/actions/setup-go). Updates `actions/checkout` from 4 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) Updates `actions/setup-go` from 5 to 6 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-go dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/integration-tests.yaml | 4 ++-- .github/workflows/release-image.yaml | 2 +- .github/workflows/release.yaml | 4 ++-- .github/workflows/unit-tests.yaml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 895fc6d0..ab899b03 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: "go.mod" diff --git a/.github/workflows/release-image.yaml b/.github/workflows/release-image.yaml index a9d5b7b0..3d8acd48 100644 --- a/.github/workflows/release-image.yaml +++ b/.github/workflows/release-image.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ed558934..d9771c21 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -23,9 +23,9 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index f2a7f815..0433d1b2 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: "go.mod" From 9fbdefa98b05633d3435fb7363db2614f4c95b7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 07:32:55 +0000 Subject: [PATCH 08/23] deps(go): bump the go-dependencies group with 11 updates Bumps the go-dependencies group with 11 updates: | Package | From | To | | --- | --- | --- | | [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) | `0.40.0` | `0.43.2` | | [github.com/openai/openai-go](https://github.com/openai/openai-go) | `1.11.0` | `1.12.0` | | [github.com/refraction-networking/utls](https://github.com/refraction-networking/utls) | `1.8.0` | `1.8.2` | | [github.com/rusq/slackauth](https://github.com/rusq/slackauth) | `0.6.1` | `0.7.1` | | [github.com/rusq/slackdump/v3](https://github.com/rusq/slackdump) | `3.1.6` | `3.1.11` | | [github.com/stretchr/testify](https://github.com/stretchr/testify) | `1.10.0` | `1.11.1` | | [go.uber.org/zap](https://github.com/uber-go/zap) | `1.27.0` | `1.27.1` | | [golang.ngrok.com/ngrok/v2](https://github.com/ngrok/ngrok-go) | `2.0.0` | `2.1.1` | | [golang.org/x/net](https://github.com/golang/net) | `0.40.0` | `0.47.0` | | [golang.org/x/sync](https://github.com/golang/sync) | `0.14.0` | `0.18.0` | | [golang.org/x/time](https://github.com/golang/time) | `0.12.0` | `0.14.0` | Updates `github.com/mark3labs/mcp-go` from 0.40.0 to 0.43.2 - [Release notes](https://github.com/mark3labs/mcp-go/releases) - [Commits](https://github.com/mark3labs/mcp-go/compare/v0.40.0...v0.43.2) Updates `github.com/openai/openai-go` from 1.11.0 to 1.12.0 - [Release notes](https://github.com/openai/openai-go/releases) - [Changelog](https://github.com/openai/openai-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/openai-go/compare/v1.11.0...v1.12.0) Updates `github.com/refraction-networking/utls` from 1.8.0 to 1.8.2 - [Release notes](https://github.com/refraction-networking/utls/releases) - [Commits](https://github.com/refraction-networking/utls/compare/v1.8.0...v1.8.2) Updates `github.com/rusq/slackauth` from 0.6.1 to 0.7.1 - [Commits](https://github.com/rusq/slackauth/compare/v0.6.1...v0.7.1) Updates `github.com/rusq/slackdump/v3` from 3.1.6 to 3.1.11 - [Release notes](https://github.com/rusq/slackdump/releases) - [Commits](https://github.com/rusq/slackdump/compare/v3.1.6...v3.1.11) Updates `github.com/stretchr/testify` from 1.10.0 to 1.11.1 - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.10.0...v1.11.1) Updates `go.uber.org/zap` from 1.27.0 to 1.27.1 - [Release notes](https://github.com/uber-go/zap/releases) - [Changelog](https://github.com/uber-go/zap/blob/master/CHANGELOG.md) - [Commits](https://github.com/uber-go/zap/compare/v1.27.0...v1.27.1) Updates `golang.ngrok.com/ngrok/v2` from 2.0.0 to 2.1.1 - [Release notes](https://github.com/ngrok/ngrok-go/releases) - [Changelog](https://github.com/ngrok/ngrok-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/ngrok/ngrok-go/compare/v2.0.0...v2.1.1) Updates `golang.org/x/net` from 0.40.0 to 0.47.0 - [Commits](https://github.com/golang/net/compare/v0.40.0...v0.47.0) Updates `golang.org/x/sync` from 0.14.0 to 0.18.0 - [Commits](https://github.com/golang/sync/compare/v0.14.0...v0.18.0) Updates `golang.org/x/time` from 0.12.0 to 0.14.0 - [Commits](https://github.com/golang/time/compare/v0.12.0...v0.14.0) --- updated-dependencies: - dependency-name: github.com/mark3labs/mcp-go dependency-version: 0.43.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/openai/openai-go dependency-version: 1.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/refraction-networking/utls dependency-version: 1.8.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/rusq/slackauth dependency-version: 0.7.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/rusq/slackdump/v3 dependency-version: 3.1.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/stretchr/testify dependency-version: 1.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: go.uber.org/zap dependency-version: 1.27.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: golang.ngrok.com/ngrok/v2 dependency-version: 2.1.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/net dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/sync dependency-version: 0.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/time dependency-version: 0.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 69 +++++++++++++------------- go.sum | 149 ++++++++++++++++++++++++++++++--------------------------- 2 files changed, 114 insertions(+), 104 deletions(-) diff --git a/go.mod b/go.mod index 0ac5c40f..822319c4 100644 --- a/go.mod +++ b/go.mod @@ -5,42 +5,46 @@ go 1.24.4 require ( github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/google/uuid v1.6.0 - github.com/mark3labs/mcp-go v0.40.0 + github.com/mark3labs/mcp-go v0.43.2 github.com/mattn/go-isatty v0.0.20 - github.com/openai/openai-go v1.11.0 - github.com/refraction-networking/utls v1.8.0 + github.com/openai/openai-go v1.12.0 + github.com/refraction-networking/utls v1.8.2 github.com/rusq/slack v0.9.6-0.20250408103104-dd80d1b6337f - github.com/rusq/slackauth v0.6.1 - github.com/rusq/slackdump/v3 v3.1.6 + github.com/rusq/slackauth v0.7.1 + github.com/rusq/slackdump/v3 v3.1.11 github.com/rusq/tagops v0.1.1 github.com/slack-go/slack v0.17.3 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/takara2314/slack-go-util v0.3.0 - go.uber.org/zap v1.27.0 - golang.ngrok.com/ngrok/v2 v2.0.0 - golang.org/x/net v0.40.0 - golang.org/x/sync v0.14.0 - golang.org/x/time v0.12.0 + go.uber.org/zap v1.27.1 + golang.ngrok.com/ngrok/v2 v2.1.1 + golang.org/x/net v0.47.0 + golang.org/x/sync v0.18.0 + golang.org/x/time v0.14.0 ) require ( github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403 // indirect - github.com/andybalholm/brotli v1.0.6 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/caiguanhao/readqr v1.0.0 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/bubbles v0.21.0 // indirect - github.com/charmbracelet/bubbletea v1.3.5 // indirect - github.com/charmbracelet/colorprofile v0.3.1 // indirect - github.com/charmbracelet/huh v0.7.0 // indirect - github.com/charmbracelet/huh/spinner v0.0.0-20250519092748-d6f1597485e0 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.3.3 // indirect + github.com/charmbracelet/huh v0.8.0 // indirect + github.com/charmbracelet/huh/spinner v0.0.0-20250714122654-40d2b68703eb // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.9.2 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20250520193441-8304e91a28cb // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/ansi v0.11.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.14 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20251118172736-77d017256798 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.6.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -51,17 +55,15 @@ require ( github.com/go-stack/stack v1.8.1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible // indirect - github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/jpillora/backoff v1.0.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect @@ -82,16 +84,17 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/ysmood/fetchup v0.3.0 // indirect github.com/ysmood/goob v0.4.0 // indirect - github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/got v0.41.0 // indirect github.com/ysmood/gson v0.7.3 // indirect github.com/ysmood/leakless v0.9.0 // indirect - github.com/yuin/goldmark v1.7.12 // indirect + github.com/yuin/goldmark v1.7.13 // indirect go.uber.org/multierr v1.11.0 // indirect golang.ngrok.com/muxado/v2 v2.0.1 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2cd59b41..8aec04f1 100644 --- a/go.sum +++ b/go.sum @@ -2,50 +2,58 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403 h1:EtZwYyLbkEcIt+B//6sujwRCnHuTEK3qiSypAX5aJeM= github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403/go.mod h1:mM6WvakkX2m+NgMiPCfFFjwfH4KzENC07zeGEqq9U7s= -github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= -github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/caiguanhao/readqr v1.0.0 h1:axynewywpUyqZxFjKPtEbr97PzSOMrJsfn9bKkp+22w= +github.com/caiguanhao/readqr v1.0.0/go.mod h1:oaAqEl5Zt0XzeIJf7nCEzJFz4is8rfE+Vgiw8b07vMM= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= -github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= -github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= -github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= -github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= -github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= -github.com/charmbracelet/huh/spinner v0.0.0-20250519092748-d6f1597485e0 h1:CiQY7CVtEigidVu1vzLxqdW3Tg2DB66R/2OaM3E2rbI= -github.com/charmbracelet/huh/spinner v0.0.0-20250519092748-d6f1597485e0/go.mod h1:D/ml7UtSMq/cwoJiHJ78KFzGrx4m01ALekBSHImKiu4= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= +github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= +github.com/charmbracelet/huh/spinner v0.0.0-20250714122654-40d2b68703eb h1:foK5EYUrChM3+7lK6qCEH43p/3oljGMtWtRq+tv3As4= +github.com/charmbracelet/huh/spinner v0.0.0-20250714122654-40d2b68703eb/go.mod h1:imftm8y+Db+rZ4Jcb6A7qJ0eOX78s9m84n8cdipC+R0= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY= -github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/ansi v0.11.1 h1:iXAC8SyMQDJgtcz9Jnw+HU8WMEctHzoTAETIeA3JXMk= +github.com/charmbracelet/x/ansi v0.11.1/go.mod h1:M49wjzpIujwPceJ+t5w3qh2i87+HRtHohgb5iTyepL0= +github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= +github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/strings v0.0.0-20250520193441-8304e91a28cb h1:JFEeU2KTS+W0dkZVbLeEgXI+PLBRZGomQKeWDpQD+V0= -github.com/charmbracelet/x/exp/strings v0.0.0-20250520193441-8304e91a28cb/go.mod h1:Rgw3/F+xlcUc5XygUtimVSxAqCOsqyvJjqF5UHRvc5k= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/exp/strings v0.0.0-20251118172736-77d017256798 h1:g0RVaqkUdTikWLqrBdk2ZvJ9oTQOS0HZlYjYE8Tu7yg= +github.com/charmbracelet/x/exp/strings v0.0.0-20251118172736-77d017256798/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= +github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -82,10 +90,6 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible h1:VryeOTiaZfAzwx8xBcID1KlJCeoWSIpsNbSk+/D2LNk= -github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= -github.com/inconshreveable/log15/v3 v3.0.0-testing.5 h1:h4e0f3kjgg+RJBlKOabrohjHe47D3bbAB9BgMrc3DYA= -github.com/inconshreveable/log15/v3 v3.0.0-testing.5/go.mod h1:3GQg1SVrLoWGfRv/kAZMsdyU5cp8eFc1P3cw+Wwku94= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -93,26 +97,26 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mark3labs/mcp-go v0.40.0 h1:M0oqK412OHBKut9JwXSsj4KanSmEKpzoW8TcxoPOkAU= -github.com/mark3labs/mcp-go v0.40.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= +github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= +github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= @@ -123,15 +127,14 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/openai/openai-go v1.11.0 h1:ztH+W0ug5Kh9+/EErHa8KAmhwixkzjK57rXyE+ZnSCk= -github.com/openai/openai-go v1.11.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= +github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/playwright-community/playwright-go v0.5200.0 h1:z/5LGuX2tBrg3ug1HupMXLjIG93f1d2MWdDsNhkMQ9c= github.com/playwright-community/playwright-go v0.5200.0/go.mod h1:UnnyQZaqUOO5ywAZu60+N4EiWReUqX1MQBBA3Oofvf8= 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/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= -github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= +github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -142,10 +145,10 @@ github.com/rusq/fsadapter v1.1.0 h1:/tuzrPNGr4Tx2f8fPK+WudSRBLDvjjDaqVvto1yrVdk= github.com/rusq/fsadapter v1.1.0/go.mod h1:aSH7MYrWvAGiFkz1qGPE8OknkplFfQSj66leC0eSqYg= github.com/rusq/slack v0.9.6-0.20250408103104-dd80d1b6337f h1:w4klfw1A3iZv5qWg1YHcRF2bJuRDV7aOpsF6sLLSs0A= github.com/rusq/slack v0.9.6-0.20250408103104-dd80d1b6337f/go.mod h1:gULX17QqyNX4BF001nHKlSe0uKYI+MAKiDQ7oi80BYI= -github.com/rusq/slackauth v0.6.1 h1:s09G3WHSA1yz6H9dHT+Yo6DCZF34ClY31tQz849B++Q= -github.com/rusq/slackauth v0.6.1/go.mod h1:wAtNCbeKH0pnaZnqJjG5RKY3e5BF9F2L/YTzhOjBIb0= -github.com/rusq/slackdump/v3 v3.1.6 h1:t6hi49jSDWpiXqyna8OlEd2I2zkLBgi9XZGr+xDl5ik= -github.com/rusq/slackdump/v3 v3.1.6/go.mod h1:c9AiEEkmLWIbQJuxDIK+K9H5g6kdfc06Eqk6DmLWWps= +github.com/rusq/slackauth v0.7.1 h1:D4peflZtHSyQFh5pLeBI8n0f12enuA9D25mA0KaHo8o= +github.com/rusq/slackauth v0.7.1/go.mod h1:UOqfnUaJeygO9rYShAhsLxAZjbbEBNaLZpsdw03W3R0= +github.com/rusq/slackdump/v3 v3.1.11 h1:gFMi7asrlBP67lyXHN95uZ9InpU+DTjfAY3Pebyd90c= +github.com/rusq/slackdump/v3 v3.1.11/go.mod h1:Kt2VO0In8WBAQP7y6fhxScPgAGOM8UQkl8qt37C0pEw= github.com/rusq/tagops v0.1.1 h1:R5MHPR822lSg3LFr0RS3DFS0CapRiqtuHVD5NlOMOvY= github.com/rusq/tagops v0.1.1/go.mod h1:mUJ5WoHxrSv9wreCrHQkAeMevt5aXFadlOdLM6UsoHc= github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= @@ -155,8 +158,8 @@ github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cA github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/takara2314/slack-go-util v0.3.0 h1:vA4WV5liJkZ9JMa2dVN+Rj6u8EW2jRiupOGkn52SMrg= github.com/takara2314/slack-go-util v0.3.0/go.mod h1:zAMjTWVT2/cDkJtbFm+AtNg5dp+l0FpknJZs8q95NWs= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -173,6 +176,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8= @@ -181,8 +186,8 @@ github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= -github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= -github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/got v0.41.0 h1:XiFH311ltTSGyxjeKcNvy7dzbJjjTzn6DBgK313JHBs= +github.com/ysmood/got v0.41.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= @@ -190,27 +195,27 @@ github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3R github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= -github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.ngrok.com/muxado/v2 v2.0.1 h1:jM9i6Pom6GGmnPrHKNR6OJRrUoHFkSZlJ3/S0zqdVpY= golang.ngrok.com/muxado/v2 v2.0.1/go.mod h1:wzxJYX4xiAtmwumzL+QsukVwFRXmPNv86vB8RPpOxyM= -golang.ngrok.com/ngrok/v2 v2.0.0 h1:eUEF7ULph6hUdOVR9r7oue2UhT2vvDoLAo0q//N6vJo= -golang.ngrok.com/ngrok/v2 v2.0.0/go.mod h1:nppMCtZ44/KeGrDHOV0c4bRyMGdHCEBo2Rvjdv/1Uio= +golang.ngrok.com/ngrok/v2 v2.1.1 h1:HhBEBiTx8Rsf1txH3909ky0XS5xCBYWQWABiX1iuSBc= +golang.ngrok.com/ngrok/v2 v2.1.1/go.mod h1:0tZJGx2wKb8HO1IR3hzToPwwI7ggE4nl88/AFACgy2A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -218,13 +223,13 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -235,32 +240,34 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.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/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -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/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 4454d67ff6a6d3be75a22b8ce5ac29d49723943e Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 00:03:28 -0300 Subject: [PATCH 09/23] feat: govslack compatibility --- pkg/provider/api.go | 8 +++++--- pkg/provider/edge/edge.go | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pkg/provider/api.go b/pkg/provider/api.go index f67f1b9e..b85c824e 100644 --- a/pkg/provider/api.go +++ b/pkg/provider/api.go @@ -123,9 +123,11 @@ type ApiProvider struct { func NewMCPSlackClient(authProvider auth.Provider, logger *zap.Logger) (*MCPSlackClient, error) { httpClient := transport.ProvideHTTPClient(authProvider.Cookies(), logger) - slackClient := slack.New(authProvider.SlackToken(), - slack.OptionHTTPClient(httpClient), - ) + slackOpts := []slack.Option{slack.OptionHTTPClient(httpClient)} + if os.Getenv("SLACK_MCP_GOVSLACK") == "true" { + slackOpts = append(slackOpts, slack.OptionAPIURL("https://slack-gov.com/api/")) + } + slackClient := slack.New(authProvider.SlackToken(), slackOpts...) authResp, err := slackClient.AuthTest() if err != nil { diff --git a/pkg/provider/edge/edge.go b/pkg/provider/edge/edge.go index cf1e3bac..ff938dcd 100644 --- a/pkg/provider/edge/edge.go +++ b/pkg/provider/edge/edge.go @@ -64,6 +64,15 @@ var ( ErrNoToken = errors.New("token is empty") ) +// getSlackBaseDomain returns the base domain for Slack API endpoints. +// Returns "slack-gov.com" if SLACK_MCP_GOVSLACK=true, otherwise "slack.com". +func getSlackBaseDomain() string { + if os.Getenv("SLACK_MCP_GOVSLACK") == "true" { + return "slack-gov.com" + } + return "slack.com" +} + func NewWithClient(workspaceName string, teamID string, token string, cl *http.Client, opt ...Option) (*Client, error) { if teamID == "" { return nil, ErrNoTeamID @@ -79,8 +88,8 @@ func NewWithClient(workspaceName string, teamID string, token string, cl *http.C cl: cl, token: token, teamID: teamID, - webclientAPI: fmt.Sprintf("https://%s.slack.com/api/", workspaceName), - edgeAPI: fmt.Sprintf("https://edgeapi.slack.com/cache/%s/", teamID), + webclientAPI: fmt.Sprintf("https://%s.%s/api/", workspaceName, getSlackBaseDomain()), + edgeAPI: fmt.Sprintf("https://edgeapi.%s/cache/%s/", getSlackBaseDomain(), teamID), tape: tape, }, nil } @@ -118,7 +127,7 @@ func NewWithInfo(info *slack.AuthTestResponse, prov auth.Provider, opt ...Option token: prov.SlackToken(), teamID: info.TeamID, webclientAPI: info.URL + "api/", - edgeAPI: fmt.Sprintf("https://edgeapi.slack.com/cache/%s/", info.TeamID), + edgeAPI: fmt.Sprintf("https://edgeapi.%s/cache/%s/", getSlackBaseDomain(), info.TeamID), tape: nopTape{}, } From 10463f3d50f9843d650ee53281ea4517f4cfcb81 Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 00:06:55 -0300 Subject: [PATCH 10/23] docs: updated with option --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6debb1f3..d9bfdd52 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Fetches a CSV directory of all users in the workspace. | `SLACK_MCP_USERS_CACHE` | No | `~/Library/Caches/slack-mcp-server/users_cache.json` (macOS)
`~/.cache/slack-mcp-server/users_cache.json` (Linux)
`%LocalAppData%/slack-mcp-server/users_cache.json` (Windows) | Path to the users cache file. Used to cache Slack user information to avoid repeated API calls on startup. | | `SLACK_MCP_CHANNELS_CACHE` | No | `~/Library/Caches/slack-mcp-server/channels_cache_v2.json` (macOS)
`~/.cache/slack-mcp-server/channels_cache_v2.json` (Linux)
`%LocalAppData%/slack-mcp-server/channels_cache_v2.json` (Windows) | Path to the channels cache file. Used to cache Slack channel information to avoid repeated API calls on startup. | | `SLACK_MCP_LOG_LEVEL` | No | `info` | Log-level for stdout or stderr. Valid values are: `debug`, `info`, `warn`, `error`, `panic` and `fatal` | +| `SLACK_MCP_GOVSLACK` | No | `nil` | Set to `true` to enable [GovSlack](https://slack.com/solutions/govslack) mode. Routes API calls to `slack-gov.com` endpoints instead of `slack.com` for FedRAMP-compliant government workspaces. | *You need one of: `xoxp` (user), `xoxb` (bot), or both `xoxc`/`xoxd` tokens for authentication. From 893f15816bf5af459f21b088fbda9ccf22f03432 Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 00:49:46 -0300 Subject: [PATCH 11/23] ci: triggering build From 58218638ff7b2297d855d81e6ec72518c099a63a Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 00:51:07 -0300 Subject: [PATCH 12/23] fix: using built package of my fork --- .github/workflows/release-image.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-image.yaml b/.github/workflows/release-image.yaml index 3d8acd48..4c805151 100644 --- a/.github/workflows/release-image.yaml +++ b/.github/workflows/release-image.yaml @@ -6,8 +6,8 @@ on: - '*' env: - DOCKERHUB_IMAGE_NAME: korotovsky/slack-mcp-server - DOCKERHUB_USERNAME: korotovsky + DOCKERHUB_IMAGE_NAME: aron-muon/slack-mcp-server + DOCKERHUB_USERNAME: aron-muon TAG: ${{ github.ref_name == 'main' && 'latest' || github.ref_type == 'tag' && github.ref_name && startsWith(github.ref_name, 'v') && github.ref_name || 'unknown' }} jobs: From cdc1ad41a1e8245533fd62a62ead0ced1051d369 Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 28 Jan 2026 01:37:18 -0300 Subject: [PATCH 13/23] Update Docker Hub image name and username --- .github/workflows/release-image.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-image.yaml b/.github/workflows/release-image.yaml index 4c805151..3d8acd48 100644 --- a/.github/workflows/release-image.yaml +++ b/.github/workflows/release-image.yaml @@ -6,8 +6,8 @@ on: - '*' env: - DOCKERHUB_IMAGE_NAME: aron-muon/slack-mcp-server - DOCKERHUB_USERNAME: aron-muon + DOCKERHUB_IMAGE_NAME: korotovsky/slack-mcp-server + DOCKERHUB_USERNAME: korotovsky TAG: ${{ github.ref_name == 'main' && 'latest' || github.ref_type == 'tag' && github.ref_name && startsWith(github.ref_name, 'v') && github.ref_name || 'unknown' }} jobs: From c38c460f487f36190b78123de35c87efca3bde14 Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 02:05:52 -0300 Subject: [PATCH 14/23] ci: aron-moun --- .github/workflows/release-image.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-image.yaml b/.github/workflows/release-image.yaml index 3d8acd48..4c805151 100644 --- a/.github/workflows/release-image.yaml +++ b/.github/workflows/release-image.yaml @@ -6,8 +6,8 @@ on: - '*' env: - DOCKERHUB_IMAGE_NAME: korotovsky/slack-mcp-server - DOCKERHUB_USERNAME: korotovsky + DOCKERHUB_IMAGE_NAME: aron-muon/slack-mcp-server + DOCKERHUB_USERNAME: aron-muon TAG: ${{ github.ref_name == 'main' && 'latest' || github.ref_type == 'tag' && github.ref_name && startsWith(github.ref_name, 'v') && github.ref_name || 'unknown' }} jobs: From 73aea44145f53fc2ea36b28d69a30d6ee04113f7 Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 04:09:42 -0300 Subject: [PATCH 15/23] fix: using govslack to verify token --- pkg/oauth/manager.go | 12 +++++++----- pkg/provider/edge/edge.go | 10 +++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/oauth/manager.go b/pkg/oauth/manager.go index baab0464..f5833865 100644 --- a/pkg/oauth/manager.go +++ b/pkg/oauth/manager.go @@ -7,6 +7,8 @@ import ( "net/url" "strings" "time" + + "github.com/korotovsky/slack-mcp-server/pkg/provider/edge" ) type Manager struct { @@ -57,7 +59,7 @@ func (m *Manager) GetAuthURL(state string) string { "state": {state}, } - return "https://slack.com/oauth/v2/authorize?" + params.Encode() + return "https://" + edge.GetSlackBaseDomain() + "/oauth/v2/authorize?" + params.Encode() } // HandleCallback exchanges OAuth code for access token @@ -69,7 +71,7 @@ func (m *Manager) HandleCallback(code, state string) (*TokenResponse, error) { "redirect_uri": {m.redirectURI}, } - resp, err := m.httpClient.PostForm("https://slack.com/api/oauth.v2.access", data) + resp, err := m.httpClient.PostForm("https://"+edge.GetSlackBaseDomain()+"/api/oauth.v2.access", data) if err != nil { return nil, fmt.Errorf("failed to exchange code: %w", err) } @@ -99,8 +101,8 @@ func (m *Manager) HandleCallback(code, state string) (*TokenResponse, error) { } token := &TokenResponse{ - AccessToken: result.AuthedUser.AccessToken, // User token (xoxp-...) - BotToken: result.AccessToken, // Bot token (xoxb-...) if available + AccessToken: result.AuthedUser.AccessToken, // User token (xoxp-...) + BotToken: result.AccessToken, // Bot token (xoxb-...) if available UserID: result.AuthedUser.ID, TeamID: result.Team.ID, BotUserID: result.BotUserID, @@ -124,7 +126,7 @@ func (m *Manager) HandleCallback(code, state string) (*TokenResponse, error) { // ValidateToken validates an access token with Slack func (m *Manager) ValidateToken(accessToken string) (*TokenInfo, error) { - req, err := http.NewRequest("POST", "https://slack.com/api/auth.test", nil) + req, err := http.NewRequest("POST", "https://"+edge.GetSlackBaseDomain()+"/api/auth.test", nil) if err != nil { return nil, err } diff --git a/pkg/provider/edge/edge.go b/pkg/provider/edge/edge.go index ff938dcd..1ef99eef 100644 --- a/pkg/provider/edge/edge.go +++ b/pkg/provider/edge/edge.go @@ -64,9 +64,9 @@ var ( ErrNoToken = errors.New("token is empty") ) -// getSlackBaseDomain returns the base domain for Slack API endpoints. +// GetSlackBaseDomain returns the base domain for Slack API endpoints. // Returns "slack-gov.com" if SLACK_MCP_GOVSLACK=true, otherwise "slack.com". -func getSlackBaseDomain() string { +func GetSlackBaseDomain() string { if os.Getenv("SLACK_MCP_GOVSLACK") == "true" { return "slack-gov.com" } @@ -88,8 +88,8 @@ func NewWithClient(workspaceName string, teamID string, token string, cl *http.C cl: cl, token: token, teamID: teamID, - webclientAPI: fmt.Sprintf("https://%s.%s/api/", workspaceName, getSlackBaseDomain()), - edgeAPI: fmt.Sprintf("https://edgeapi.%s/cache/%s/", getSlackBaseDomain(), teamID), + webclientAPI: fmt.Sprintf("https://%s.%s/api/", workspaceName, GetSlackBaseDomain()), + edgeAPI: fmt.Sprintf("https://edgeapi.%s/cache/%s/", GetSlackBaseDomain(), teamID), tape: tape, }, nil } @@ -127,7 +127,7 @@ func NewWithInfo(info *slack.AuthTestResponse, prov auth.Provider, opt ...Option token: prov.SlackToken(), teamID: info.TeamID, webclientAPI: info.URL + "api/", - edgeAPI: fmt.Sprintf("https://edgeapi.%s/cache/%s/", getSlackBaseDomain(), info.TeamID), + edgeAPI: fmt.Sprintf("https://edgeapi.%s/cache/%s/", GetSlackBaseDomain(), info.TeamID), tape: nopTape{}, } From 7027f20f3af74373c9f2feded3cbcab7a007a7a1 Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 04:32:40 -0300 Subject: [PATCH 16/23] fix: use the right url context --- pkg/handler/channels.go | 7 ++++++- pkg/handler/conversations.go | 14 ++++++++++++-- pkg/oauth/manager.go | 2 ++ pkg/oauth/types.go | 1 + pkg/server/auth/context.go | 1 + pkg/server/auth/oauth_middleware.go | 2 +- 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pkg/handler/channels.go b/pkg/handler/channels.go index 312ac70c..293ebb07 100644 --- a/pkg/handler/channels.go +++ b/pkg/handler/channels.go @@ -76,7 +76,12 @@ func (ch *ChannelsHandler) getSlackClient(ctx context.Context) (*slack.Client, e } // Use token directly from context (already validated by middleware) - return slack.New(userCtx.AccessToken), nil + // Set API URL from auth.test response to support external tokens and GovSlack + opts := []slack.Option{} + if userCtx.URL != "" { + opts = append(opts, slack.OptionAPIURL(userCtx.URL+"api/")) + } + return slack.New(userCtx.AccessToken, opts...), nil } func (ch *ChannelsHandler) ChannelsResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { diff --git a/pkg/handler/conversations.go b/pkg/handler/conversations.go index f8a5486a..95d37698 100644 --- a/pkg/handler/conversations.go +++ b/pkg/handler/conversations.go @@ -118,7 +118,12 @@ func (h *ConversationsHandler) getSlackClient(ctx context.Context) (*slack.Clien } // Use user token by default - return slack.New(userCtx.AccessToken), nil + // Set API URL from auth.test response to support external tokens and GovSlack + opts := []slack.Option{} + if userCtx.URL != "" { + opts = append(opts, slack.OptionAPIURL(userCtx.URL+"api/")) + } + return slack.New(userCtx.AccessToken, opts...), nil } // getBotSlackClient creates a Slack client using bot token (OAuth mode) @@ -138,7 +143,12 @@ func (h *ConversationsHandler) getBotSlackClient(ctx context.Context) (*slack.Cl } // Use bot token - return slack.New(userCtx.BotToken), nil + // Set API URL from auth.test response to support external tokens and GovSlack + opts := []slack.Option{} + if userCtx.URL != "" { + opts = append(opts, slack.OptionAPIURL(userCtx.URL+"api/")) + } + return slack.New(userCtx.BotToken, opts...), nil } // getProvider returns the provider (legacy mode) or error (OAuth mode) diff --git a/pkg/oauth/manager.go b/pkg/oauth/manager.go index f5833865..f9ddc8e9 100644 --- a/pkg/oauth/manager.go +++ b/pkg/oauth/manager.go @@ -145,6 +145,7 @@ func (m *Manager) ValidateToken(accessToken string) (*TokenInfo, error) { Error string `json:"error"` UserID string `json:"user_id"` TeamID string `json:"team_id"` + URL string `json:"url"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { @@ -158,6 +159,7 @@ func (m *Manager) ValidateToken(accessToken string) (*TokenInfo, error) { return &TokenInfo{ UserID: result.UserID, TeamID: result.TeamID, + URL: result.URL, }, nil } diff --git a/pkg/oauth/types.go b/pkg/oauth/types.go index 3770901d..b475fc6a 100644 --- a/pkg/oauth/types.go +++ b/pkg/oauth/types.go @@ -16,6 +16,7 @@ type TokenResponse struct { type TokenInfo struct { UserID string TeamID string + URL string // Workspace URL from auth.test (e.g., https://workspace.slack.com/) } // OAuthManager handles OAuth 2.0 flow with Slack diff --git a/pkg/server/auth/context.go b/pkg/server/auth/context.go index cc51723f..4f9291f7 100644 --- a/pkg/server/auth/context.go +++ b/pkg/server/auth/context.go @@ -12,6 +12,7 @@ type UserContext struct { AccessToken string // User token (xoxp-...) for per-request client creation BotToken string // Bot token (xoxb-...) if available - for posting as bot BotUserID string // Bot user ID if available + URL string // Workspace URL from auth.test (e.g., https://workspace.slack.com/) } // WithUserContext adds user context to the context diff --git a/pkg/server/auth/oauth_middleware.go b/pkg/server/auth/oauth_middleware.go index 7a283078..0352530c 100644 --- a/pkg/server/auth/oauth_middleware.go +++ b/pkg/server/auth/oauth_middleware.go @@ -50,6 +50,7 @@ func OAuthMiddleware(oauthMgr oauth.OAuthManager, logger *zap.Logger) server.Too AccessToken: token, // User token for per-request client BotToken: storedToken.BotToken, // Bot token if available BotUserID: storedToken.BotUserID, // Bot user ID if available + URL: tokenInfo.URL, // Workspace URL for API calls } // Inject user context @@ -64,4 +65,3 @@ func OAuthMiddleware(oauthMgr oauth.OAuthManager, logger *zap.Logger) server.Too } } } - From 63ae9378d4070d5685c09db8bfb5fbe2596c3900 Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 05:06:03 -0300 Subject: [PATCH 17/23] fix: retrieving user info --- pkg/handler/conversations.go | 80 +++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/pkg/handler/conversations.go b/pkg/handler/conversations.go index 95d37698..6eb1953b 100644 --- a/pkg/handler/conversations.go +++ b/pkg/handler/conversations.go @@ -321,7 +321,7 @@ func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Conte } ch.logger.Debug("Fetched conversation history", zap.Int("message_count", len(history.Messages))) - messages := ch.convertMessagesFromHistory(history.Messages, historyParams.ChannelID, false) + messages := ch.convertMessagesFromHistory(ctx, slackClient, history.Messages, historyParams.ChannelID, false) return marshalMessagesToCSV(messages) } @@ -374,7 +374,7 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context, ch.logger.Debug("Fetched conversation history", zap.Int("message_count", len(history.Messages))) - messages := ch.convertMessagesFromHistory(history.Messages, params.channel, params.activity) + messages := ch.convertMessagesFromHistory(ctx, slackClient, history.Messages, params.channel, params.activity) if len(messages) > 0 && history.HasMore { messages[len(messages)-1].Cursor = history.ResponseMetaData.NextCursor @@ -431,7 +431,7 @@ func (ch *ConversationsHandler) ConversationsRepliesHandler(ctx context.Context, } ch.logger.Debug("Fetched conversation replies", zap.Int("count", len(replies))) - messages := ch.convertMessagesFromHistory(replies, params.channel, params.activity) + messages := ch.convertMessagesFromHistory(ctx, slackClient, replies, params.channel, params.activity) if len(messages) > 0 && hasMore { messages[len(messages)-1].Cursor = nextCursor } @@ -478,7 +478,7 @@ func (ch *ConversationsHandler) ConversationsSearchHandler(ctx context.Context, } ch.logger.Debug("Search completed", zap.Int("matches", len(messagesRes.Matches))) - messages := ch.convertMessagesFromSearch(messagesRes.Matches) + messages := ch.convertMessagesFromSearch(ctx, slackClient, messagesRes.Matches) if len(messages) > 0 && messagesRes.Pagination.Page < messagesRes.Pagination.PageCount { nextCursor := fmt.Sprintf("page:%d", messagesRes.Pagination.Page+1) messages[len(messages)-1].Cursor = base64.StdEncoding.EncodeToString([]byte(nextCursor)) @@ -508,17 +508,21 @@ func isChannelAllowed(channel string) bool { return !isNegated } -func (ch *ConversationsHandler) convertMessagesFromHistory(slackMessages []slack.Message, channel string, includeActivity bool) []Message { +func (ch *ConversationsHandler) convertMessagesFromHistory(ctx context.Context, slackClient *slack.Client, slackMessages []slack.Message, channel string, includeActivity bool) []Message { // Get users map (if available) - var usersMap *provider.UsersCache + var usersMap map[string]slack.User if !ch.oauthEnabled { - usersMap = ch.apiProvider.ProvideUsersMap() + cache := ch.apiProvider.ProvideUsersMap() + usersMap = cache.Users } else { - // OAuth mode: no cache, use empty map - usersMap = &provider.UsersCache{ - Users: make(map[string]slack.User), - UsersInv: make(map[string]string), + // OAuth mode: fetch user info from Slack API + var userIDs []string + for _, msg := range slackMessages { + if msg.User != "" { + userIDs = append(userIDs, msg.User) + } } + usersMap = ch.fetchUsersForMessages(ctx, slackClient, userIDs) } var messages []Message warn := false @@ -528,7 +532,7 @@ func (ch *ConversationsHandler) convertMessagesFromHistory(slackMessages []slack continue } - userName, realName, ok := getUserInfo(msg.User, usersMap.Users) + userName, realName, ok := getUserInfo(msg.User, usersMap) if !ok && msg.SubType == "bot_message" { userName, realName, ok = getBotInfo(msg.Username) @@ -578,23 +582,27 @@ func (ch *ConversationsHandler) convertMessagesFromHistory(slackMessages []slack return messages } -func (ch *ConversationsHandler) convertMessagesFromSearch(slackMessages []slack.SearchMessage) []Message { +func (ch *ConversationsHandler) convertMessagesFromSearch(ctx context.Context, slackClient *slack.Client, slackMessages []slack.SearchMessage) []Message { // Get users map (if available) - var usersMap *provider.UsersCache + var usersMap map[string]slack.User if !ch.oauthEnabled { - usersMap = ch.apiProvider.ProvideUsersMap() + cache := ch.apiProvider.ProvideUsersMap() + usersMap = cache.Users } else { - // OAuth mode: no cache, use empty map - usersMap = &provider.UsersCache{ - Users: make(map[string]slack.User), - UsersInv: make(map[string]string), + // OAuth mode: fetch user info from Slack API + var userIDs []string + for _, msg := range slackMessages { + if msg.User != "" { + userIDs = append(userIDs, msg.User) + } } + usersMap = ch.fetchUsersForMessages(ctx, slackClient, userIDs) } var messages []Message warn := false for _, msg := range slackMessages { - userName, realName, ok := getUserInfo(msg.User, usersMap.Users) + userName, realName, ok := getUserInfo(msg.User, usersMap) if !ok && msg.User == "" && msg.Username != "" { userName, realName, ok = getBotInfo(msg.Username) @@ -934,6 +942,38 @@ func getUserInfo(userID string, usersMap map[string]slack.User) (userName, realN return userID, userID, false } +// fetchUsersForMessages fetches user info from Slack API for the given user IDs +// and returns a map of userID -> slack.User. This is used in OAuth mode where +// we don't have a pre-populated users cache. +func (ch *ConversationsHandler) fetchUsersForMessages(ctx context.Context, client *slack.Client, userIDs []string) map[string]slack.User { + usersMap := make(map[string]slack.User) + if client == nil { + return usersMap + } + + // Deduplicate user IDs + seen := make(map[string]bool) + var uniqueIDs []string + for _, id := range userIDs { + if id != "" && !seen[id] { + seen[id] = true + uniqueIDs = append(uniqueIDs, id) + } + } + + // Fetch each user's info + for _, userID := range uniqueIDs { + user, err := client.GetUserInfoContext(ctx, userID) + if err != nil { + ch.logger.Debug("Failed to fetch user info", zap.String("userID", userID), zap.Error(err)) + continue + } + usersMap[userID] = *user + } + + return usersMap +} + func getBotInfo(botID string) (userName, realName string, ok bool) { return botID, botID, true } From 0670667a8d095695ef7f43311996fd145214840f Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 05:26:08 -0300 Subject: [PATCH 18/23] fic: additional issues --- pkg/handler/channels.go | 31 +++++++++++++++++++++++++++++-- pkg/handler/conversations.go | 23 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/pkg/handler/channels.go b/pkg/handler/channels.go index 293ebb07..0ecaaf4d 100644 --- a/pkg/handler/channels.go +++ b/pkg/handler/channels.go @@ -404,12 +404,39 @@ func (ch *ChannelsHandler) channelsHandlerOAuth(ctx context.Context, request mcp } for _, c := range channels { + name := "#" + c.Name + memberCount := c.NumMembers + + // Handle DM channels - they have empty names but have a User field + if c.IsIM && c.User != "" { + // Fetch user info to get their display name + user, err := client.GetUserInfo(c.User) + if err != nil { + ch.logger.Debug("Failed to get user info for DM", zap.String("userID", c.User), zap.Error(err)) + name = "@" + c.User // Fallback to user ID + } else if user != nil { + if user.Profile.DisplayName != "" { + name = "@" + user.Profile.DisplayName + } else { + name = "@" + user.Name + } + } + memberCount = 2 // DMs always have 2 members + } else if c.IsMpIM { + // Group DMs - use the purpose or a placeholder + if c.Purpose.Value != "" { + name = c.Purpose.Value + } else { + name = "Group DM" + } + } + allChannels = append(allChannels, Channel{ ID: c.ID, - Name: "#" + c.Name, + Name: name, Topic: c.Topic.Value, Purpose: c.Purpose.Value, - MemberCount: c.NumMembers, + MemberCount: memberCount, }) } } diff --git a/pkg/handler/conversations.go b/pkg/handler/conversations.go index 6eb1953b..f28476c7 100644 --- a/pkg/handler/conversations.go +++ b/pkg/handler/conversations.go @@ -311,6 +311,9 @@ func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Conte var history *slack.GetConversationHistoryResponse if ch.oauthEnabled { + if slackClient == nil { + return nil, fmt.Errorf("slack client is nil in OAuth mode") + } history, err = slackClient.GetConversationHistoryContext(ctx, &historyParams) } else { history, err = ch.apiProvider.Slack().GetConversationHistoryContext(ctx, &historyParams) @@ -319,6 +322,10 @@ func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Conte ch.logger.Error("GetConversationHistoryContext failed", zap.Error(err)) return nil, err } + if history == nil { + ch.logger.Error("GetConversationHistoryContext returned nil response") + return nil, fmt.Errorf("failed to get conversation history: nil response") + } ch.logger.Debug("Fetched conversation history", zap.Int("message_count", len(history.Messages))) messages := ch.convertMessagesFromHistory(ctx, slackClient, history.Messages, historyParams.ChannelID, false) @@ -363,6 +370,9 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context, var history *slack.GetConversationHistoryResponse if ch.oauthEnabled { + if slackClient == nil { + return nil, fmt.Errorf("slack client is nil in OAuth mode") + } history, err = slackClient.GetConversationHistoryContext(ctx, &historyParams) } else { history, err = ch.apiProvider.Slack().GetConversationHistoryContext(ctx, &historyParams) @@ -371,6 +381,10 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context, ch.logger.Error("GetConversationHistoryContext failed", zap.Error(err)) return nil, err } + if history == nil { + ch.logger.Error("GetConversationHistoryContext returned nil response") + return nil, fmt.Errorf("failed to get conversation history: nil response") + } ch.logger.Debug("Fetched conversation history", zap.Int("message_count", len(history.Messages))) @@ -678,6 +692,11 @@ func (ch *ConversationsHandler) parseParamsToolConversations(request mcp.CallToo } if strings.HasPrefix(channel, "#") || strings.HasPrefix(channel, "@") { + // OAuth mode doesn't have a channel cache - require channel IDs + if ch.oauthEnabled { + ch.logger.Error("Channel name not supported in OAuth mode", zap.String("channel", channel)) + return nil, fmt.Errorf("channel name %q is not supported in OAuth mode. Please use the channel ID (e.g., 'C1234567890') instead. You can get channel IDs from the channels_list tool", channel) + } if ready, err := ch.apiProvider.IsReady(); !ready { if errors.Is(err, provider.ErrUsersNotReady) { ch.logger.Warn( @@ -968,6 +987,10 @@ func (ch *ConversationsHandler) fetchUsersForMessages(ctx context.Context, clien ch.logger.Debug("Failed to fetch user info", zap.String("userID", userID), zap.Error(err)) continue } + if user == nil { + ch.logger.Debug("User info returned nil", zap.String("userID", userID)) + continue + } usersMap[userID] = *user } From bc6be95e5dc186f97090c76b327b4f664028788d Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 05:31:47 -0300 Subject: [PATCH 19/23] fix: member count and channel id lookup --- pkg/handler/channels.go | 12 +++- pkg/handler/conversations.go | 125 +++++++++++++++++++++++++++-------- 2 files changed, 110 insertions(+), 27 deletions(-) diff --git a/pkg/handler/channels.go b/pkg/handler/channels.go index 0ecaaf4d..422d3c1d 100644 --- a/pkg/handler/channels.go +++ b/pkg/handler/channels.go @@ -421,7 +421,7 @@ func (ch *ChannelsHandler) channelsHandlerOAuth(ctx context.Context, request mcp name = "@" + user.Name } } - memberCount = 2 // DMs always have 2 members + memberCount = 2 // 1:1 DMs always have exactly 2 members } else if c.IsMpIM { // Group DMs - use the purpose or a placeholder if c.Purpose.Value != "" { @@ -429,6 +429,16 @@ func (ch *ChannelsHandler) channelsHandlerOAuth(ctx context.Context, request mcp } else { name = "Group DM" } + // Get actual member count for group DMs + members, _, err := client.GetUsersInConversation(&slack.GetUsersInConversationParameters{ + ChannelID: c.ID, + Limit: 1000, + }) + if err != nil { + ch.logger.Debug("Failed to get members for MPIM", zap.String("channelID", c.ID), zap.Error(err)) + } else { + memberCount = len(members) + } } allChannels = append(allChannels, Channel{ diff --git a/pkg/handler/conversations.go b/pkg/handler/conversations.go index f28476c7..ccaccd9d 100644 --- a/pkg/handler/conversations.go +++ b/pkg/handler/conversations.go @@ -159,6 +159,72 @@ func (h *ConversationsHandler) getProvider() (*provider.ApiProvider, error) { return h.apiProvider, nil } +// resolveChannelName resolves a channel name (e.g., "#general" or "@username") to a channel ID +// using the Slack API. This is used in OAuth mode where there's no channel cache. +func (h *ConversationsHandler) resolveChannelName(ctx context.Context, client *slack.Client, channelName string) (string, error) { + if client == nil { + return "", fmt.Errorf("slack client is nil") + } + + // Handle @username for DMs + if strings.HasPrefix(channelName, "@") { + username := strings.TrimPrefix(channelName, "@") + // Look up user by name + users, err := client.GetUsersContext(ctx) + if err != nil { + return "", fmt.Errorf("failed to get users: %w", err) + } + for _, user := range users { + if user.Name == username || user.Profile.DisplayName == username { + // Open a DM with this user + channel, _, _, err := client.OpenConversationContext(ctx, &slack.OpenConversationParameters{ + Users: []string{user.ID}, + }) + if err != nil { + return "", fmt.Errorf("failed to open DM with user %s: %w", username, err) + } + if channel == nil { + return "", fmt.Errorf("failed to open DM with user %s: nil channel returned", username) + } + return channel.ID, nil + } + } + return "", fmt.Errorf("user %q not found", username) + } + + // Handle #channel-name + name := strings.TrimPrefix(channelName, "#") + + // Search through public and private channels + channelTypes := []string{"public_channel", "private_channel"} + for _, chanType := range channelTypes { + cursor := "" + for { + params := &slack.GetConversationsParameters{ + Types: []string{chanType}, + Limit: 200, + Cursor: cursor, + } + channels, nextCursor, err := client.GetConversationsContext(ctx, params) + if err != nil { + h.logger.Debug("Failed to get conversations", zap.String("type", chanType), zap.Error(err)) + break + } + for _, c := range channels { + if c.Name == name { + return c.ID, nil + } + } + if nextCursor == "" { + break + } + cursor = nextCursor + } + } + + return "", fmt.Errorf("channel %q not found", channelName) +} + // UsersResource streams a CSV of all users func (ch *ConversationsHandler) UsersResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { ch.logger.Debug("UsersResource called", zap.Any("params", request.Params)) @@ -346,7 +412,7 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context, slackClient = client } - params, err := ch.parseParamsToolConversations(request) + params, err := ch.parseParamsToolConversations(ctx, slackClient, request) if err != nil { ch.logger.Error("Failed to parse history params", zap.Error(err)) return nil, err @@ -410,7 +476,7 @@ func (ch *ConversationsHandler) ConversationsRepliesHandler(ctx context.Context, slackClient = client } - params, err := ch.parseParamsToolConversations(request) + params, err := ch.parseParamsToolConversations(ctx, slackClient, request) if err != nil { ch.logger.Error("Failed to parse replies params", zap.Error(err)) return nil, err @@ -660,7 +726,7 @@ func (ch *ConversationsHandler) convertMessagesFromSearch(ctx context.Context, s return messages } -func (ch *ConversationsHandler) parseParamsToolConversations(request mcp.CallToolRequest) (*conversationParams, error) { +func (ch *ConversationsHandler) parseParamsToolConversations(ctx context.Context, slackClient *slack.Client, request mcp.CallToolRequest) (*conversationParams, error) { channel := request.GetString("channel_id", "") if channel == "" { ch.logger.Error("channel_id missing in conversations params") @@ -692,33 +758,40 @@ func (ch *ConversationsHandler) parseParamsToolConversations(request mcp.CallToo } if strings.HasPrefix(channel, "#") || strings.HasPrefix(channel, "@") { - // OAuth mode doesn't have a channel cache - require channel IDs + // OAuth mode: resolve channel name to ID using Slack API if ch.oauthEnabled { - ch.logger.Error("Channel name not supported in OAuth mode", zap.String("channel", channel)) - return nil, fmt.Errorf("channel name %q is not supported in OAuth mode. Please use the channel ID (e.g., 'C1234567890') instead. You can get channel IDs from the channels_list tool", channel) - } - if ready, err := ch.apiProvider.IsReady(); !ready { - if errors.Is(err, provider.ErrUsersNotReady) { - ch.logger.Warn( - "WARNING: Slack users sync is not ready yet, you may experience some limited functionality and see UIDs instead of resolved names as well as unable to query users by their @handles. Users sync is part of channels sync and operations on channels depend on users collection (IM, MPIM). Please wait until users are synced and try again", - zap.Error(err), - ) + ch.logger.Debug("Resolving channel name in OAuth mode", zap.String("channel", channel)) + resolvedID, err := ch.resolveChannelName(ctx, slackClient, channel) + if err != nil { + ch.logger.Error("Failed to resolve channel name", zap.String("channel", channel), zap.Error(err)) + return nil, fmt.Errorf("failed to resolve channel name %q: %w", channel, err) } - if errors.Is(err, provider.ErrChannelsNotReady) { - ch.logger.Warn( - "WARNING: Slack channels sync is not ready yet, you may experience some limited functionality and be able to request conversation only by Channel ID, not by its name. Please wait until channels are synced and try again.", - zap.Error(err), - ) + channel = resolvedID + } else { + // Legacy mode: use channel cache + if ready, err := ch.apiProvider.IsReady(); !ready { + if errors.Is(err, provider.ErrUsersNotReady) { + ch.logger.Warn( + "WARNING: Slack users sync is not ready yet, you may experience some limited functionality and see UIDs instead of resolved names as well as unable to query users by their @handles. Users sync is part of channels sync and operations on channels depend on users collection (IM, MPIM). Please wait until users are synced and try again", + zap.Error(err), + ) + } + if errors.Is(err, provider.ErrChannelsNotReady) { + ch.logger.Warn( + "WARNING: Slack channels sync is not ready yet, you may experience some limited functionality and be able to request conversation only by Channel ID, not by its name. Please wait until channels are synced and try again.", + zap.Error(err), + ) + } + return nil, fmt.Errorf("channel %q not found in empty cache", channel) } - return nil, fmt.Errorf("channel %q not found in empty cache", channel) - } - channelsMaps := ch.apiProvider.ProvideChannelsMaps() - chn, ok := channelsMaps.ChannelsInv[channel] - if !ok { - ch.logger.Error("Channel not found in synced cache", zap.String("channel", channel)) - return nil, fmt.Errorf("channel %q not found in synced cache. Try to remove old cache file and restart MCP Server", channel) + channelsMaps := ch.apiProvider.ProvideChannelsMaps() + chn, ok := channelsMaps.ChannelsInv[channel] + if !ok { + ch.logger.Error("Channel not found in synced cache", zap.String("channel", channel)) + return nil, fmt.Errorf("channel %q not found in synced cache. Try to remove old cache file and restart MCP Server", channel) + } + channel = channelsMaps.Channels[chn].ID } - channel = channelsMaps.Channels[chn].ID } return &conversationParams{ From bab9d43720a46bc9f33037136ec3b0c8889163e8 Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 05:46:38 -0300 Subject: [PATCH 20/23] fix: latest oauth errors --- pkg/handler/conversations.go | 158 +++++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 42 deletions(-) diff --git a/pkg/handler/conversations.go b/pkg/handler/conversations.go index ccaccd9d..35c67202 100644 --- a/pkg/handler/conversations.go +++ b/pkg/handler/conversations.go @@ -531,7 +531,7 @@ func (ch *ConversationsHandler) ConversationsSearchHandler(ctx context.Context, slackClient = client } - params, err := ch.parseParamsToolSearch(request) + params, err := ch.parseParamsToolSearch(ctx, slackClient, request) if err != nil { ch.logger.Error("Failed to parse search params", zap.Error(err)) return nil, err @@ -675,6 +675,10 @@ func (ch *ConversationsHandler) convertMessagesFromSearch(ctx context.Context, s if msg.User != "" { userIDs = append(userIDs, msg.User) } + // Also collect user IDs from DM channel names (they appear as user IDs like U1234) + if strings.HasPrefix(msg.Channel.Name, "U") { + userIDs = append(userIDs, msg.Channel.Name) + } } usersMap = ch.fetchUsersForMessages(ctx, slackClient, userIDs) } @@ -700,13 +704,34 @@ func (ch *ConversationsHandler) convertMessagesFromSearch(ctx context.Context, s msgText := msg.Text + text.AttachmentsTo2CSV(msg.Text, msg.Attachments) + // Format channel name properly + channelDisplay := fmt.Sprintf("#%s", msg.Channel.Name) + // Check if this is a DM (channel ID starts with D) or if the name looks like a user ID + if strings.HasPrefix(msg.Channel.ID, "D") || strings.HasPrefix(msg.Channel.Name, "U") { + // This is a DM - try to get the user's name + if strings.HasPrefix(msg.Channel.Name, "U") { + // The "name" is actually a user ID - look it up + if user, exists := usersMap[msg.Channel.Name]; exists { + if user.Profile.DisplayName != "" { + channelDisplay = "@" + user.Profile.DisplayName + } else { + channelDisplay = "@" + user.Name + } + } else { + channelDisplay = "@" + msg.Channel.Name + } + } else { + channelDisplay = "@" + msg.Channel.Name + } + } + messages = append(messages, Message{ MsgID: msg.Timestamp, UserID: msg.User, UserName: userName, RealName: realName, Text: text.ProcessText(msgText), - Channel: fmt.Sprintf("#%s", msg.Channel.Name), + Channel: channelDisplay, ThreadTs: threadTs, Time: timestamp, Reactions: "", @@ -866,7 +891,7 @@ func (ch *ConversationsHandler) parseParamsToolAddMessage(request mcp.CallToolRe }, nil } -func (ch *ConversationsHandler) parseParamsToolSearch(req mcp.CallToolRequest) (*searchParams, error) { +func (ch *ConversationsHandler) parseParamsToolSearch(ctx context.Context, slackClient *slack.Client, req mcp.CallToolRequest) (*searchParams, error) { rawQuery := strings.TrimSpace(req.GetString("search_query", "")) freeText, filters := splitQuery(rawQuery) @@ -874,14 +899,14 @@ func (ch *ConversationsHandler) parseParamsToolSearch(req mcp.CallToolRequest) ( addFilter(filters, "is", "thread") } if chName := req.GetString("filter_in_channel", ""); chName != "" { - f, err := ch.paramFormatChannel(chName) + f, err := ch.paramFormatChannel(ctx, slackClient, chName) if err != nil { ch.logger.Error("Invalid channel filter", zap.String("filter", chName), zap.Error(err)) return nil, err } addFilter(filters, "in", f) } else if im := req.GetString("filter_in_im_or_mpim", ""); im != "" { - f, err := ch.paramFormatUser(im) + f, err := ch.paramFormatUser(ctx, slackClient, im) if err != nil { ch.logger.Error("Invalid IM/MPIM filter", zap.String("filter", im), zap.Error(err)) return nil, err @@ -889,7 +914,7 @@ func (ch *ConversationsHandler) parseParamsToolSearch(req mcp.CallToolRequest) ( addFilter(filters, "in", f) } if with := req.GetString("filter_users_with", ""); with != "" { - f, err := ch.paramFormatUser(with) + f, err := ch.paramFormatUser(ctx, slackClient, with) if err != nil { ch.logger.Error("Invalid with-user filter", zap.String("filter", with), zap.Error(err)) return nil, err @@ -897,7 +922,7 @@ func (ch *ConversationsHandler) parseParamsToolSearch(req mcp.CallToolRequest) ( addFilter(filters, "with", f) } if from := req.GetString("filter_users_from", ""); from != "" { - f, err := ch.paramFormatUser(from) + f, err := ch.paramFormatUser(ctx, slackClient, from) if err != nil { ch.logger.Error("Invalid from-user filter", zap.String("filter", from), zap.Error(err)) return nil, err @@ -959,31 +984,45 @@ func (ch *ConversationsHandler) parseParamsToolSearch(req mcp.CallToolRequest) ( }, nil } -func (ch *ConversationsHandler) paramFormatUser(raw string) (string, error) { - if ch.oauthEnabled { - // OAuth mode: require user IDs, not names - raw = strings.TrimSpace(raw) - if strings.HasPrefix(raw, "U") { - return fmt.Sprintf("<@%s>", raw), nil - } - return "", fmt.Errorf("in OAuth mode, please use user ID (U...) instead of name: %s", raw) - } - - users := ch.apiProvider.ProvideUsersMap() +func (ch *ConversationsHandler) paramFormatUser(ctx context.Context, slackClient *slack.Client, raw string) (string, error) { raw = strings.TrimSpace(raw) + + // Handle user ID directly if strings.HasPrefix(raw, "U") { - u, ok := users.Users[raw] - if !ok { - return "", fmt.Errorf("user %q not found", raw) - } - return fmt.Sprintf("<@%s>", u.ID), nil + return fmt.Sprintf("<@%s>", raw), nil } + + // Strip @ prefix if present if strings.HasPrefix(raw, "<@") { raw = raw[2:] + if idx := strings.Index(raw, ">"); idx >= 0 { + raw = raw[:idx] + } + return fmt.Sprintf("<@%s>", raw), nil } if strings.HasPrefix(raw, "@") { raw = raw[1:] } + + if ch.oauthEnabled { + // OAuth mode: resolve username to user ID via Slack API + if slackClient == nil { + return "", fmt.Errorf("slack client is nil") + } + users, err := slackClient.GetUsersContext(ctx) + if err != nil { + return "", fmt.Errorf("failed to get users: %w", err) + } + for _, user := range users { + if user.Name == raw || user.Profile.DisplayName == raw { + return fmt.Sprintf("<@%s>", user.ID), nil + } + } + return "", fmt.Errorf("user %q not found", raw) + } + + // Legacy mode: use cached users + users := ch.apiProvider.ProvideUsersMap() uid, ok := users.UsersInv[raw] if !ok { return "", fmt.Errorf("user %q not found", raw) @@ -991,32 +1030,67 @@ func (ch *ConversationsHandler) paramFormatUser(raw string) (string, error) { return fmt.Sprintf("<@%s>", uid), nil } -func (ch *ConversationsHandler) paramFormatChannel(raw string) (string, error) { +func (ch *ConversationsHandler) paramFormatChannel(ctx context.Context, slackClient *slack.Client, raw string) (string, error) { raw = strings.TrimSpace(raw) - - if ch.oauthEnabled { - // OAuth mode: use channel ID directly - if strings.HasPrefix(raw, "C") || strings.HasPrefix(raw, "G") { + + // Handle channel ID directly - for search, we need the channel name + if strings.HasPrefix(raw, "C") || strings.HasPrefix(raw, "G") { + if ch.oauthEnabled { + // In OAuth mode with a channel ID, we need to get the channel name for search + if slackClient != nil { + info, err := slackClient.GetConversationInfoContext(ctx, &slack.GetConversationInfoInput{ + ChannelID: raw, + }) + if err == nil && info != nil { + return info.Name, nil + } + } + // Fallback: use the ID (search might still work) return raw, nil } - return "", fmt.Errorf("in OAuth mode, please use channel ID (C... or G...) instead of name: %s", raw) - } - - cms := ch.apiProvider.ProvideChannelsMaps() - if strings.HasPrefix(raw, "#") { - if id, ok := cms.ChannelsInv[raw]; ok { - return cms.Channels[id].Name, nil - } - return "", fmt.Errorf("channel %q not found", raw) - } - // Handle both C (standard channels) and G (private groups/channels) prefixes - if strings.HasPrefix(raw, "C") || strings.HasPrefix(raw, "G") { + // Legacy mode: look up name from cache + cms := ch.apiProvider.ProvideChannelsMaps() if chn, ok := cms.Channels[raw]; ok { return chn.Name, nil } - return "", fmt.Errorf("channel %q not found", raw) + return raw, nil // Fallback to ID + } + + // Handle channel name + name := strings.TrimPrefix(raw, "#") + + if ch.oauthEnabled { + // OAuth mode: resolve channel name via Slack API (just validate it exists) + if slackClient == nil { + return "", fmt.Errorf("slack client is nil") + } + // Search for the channel to validate it exists + channelTypes := []string{"public_channel", "private_channel"} + for _, chanType := range channelTypes { + params := &slack.GetConversationsParameters{ + Types: []string{chanType}, + Limit: 200, + } + channels, _, err := slackClient.GetConversationsContext(ctx, params) + if err != nil { + continue + } + for _, c := range channels { + if c.Name == name { + return c.Name, nil + } + } + } + // Channel not found but try using the name anyway (might be in later pages) + return name, nil + } + + // Legacy mode: look up from cache + cms := ch.apiProvider.ProvideChannelsMaps() + if id, ok := cms.ChannelsInv[raw]; ok { + return cms.Channels[id].Name, nil } - return "", fmt.Errorf("invalid channel format: %q", raw) + return "", fmt.Errorf("channel %q not found", raw) } func marshalMessagesToCSV(messages []Message) (*mcp.CallToolResult, error) { From 9f64b8272f340e3e5257e3efedab4bbb126c4cde Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 28 Jan 2026 05:55:53 -0300 Subject: [PATCH 21/23] Update release-image.yaml --- .github/workflows/release-image.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-image.yaml b/.github/workflows/release-image.yaml index 4c805151..3d8acd48 100644 --- a/.github/workflows/release-image.yaml +++ b/.github/workflows/release-image.yaml @@ -6,8 +6,8 @@ on: - '*' env: - DOCKERHUB_IMAGE_NAME: aron-muon/slack-mcp-server - DOCKERHUB_USERNAME: aron-muon + DOCKERHUB_IMAGE_NAME: korotovsky/slack-mcp-server + DOCKERHUB_USERNAME: korotovsky TAG: ${{ github.ref_name == 'main' && 'latest' || github.ref_type == 'tag' && github.ref_name && startsWith(github.ref_name, 'v') && github.ref_name || 'unknown' }} jobs: From 13144b8072bded3e9d3f9d5c77a78139f11d5fcc Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 06:04:15 -0300 Subject: [PATCH 22/23] fix: additional formatting issue --- pkg/text/text_processor.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/text/text_processor.go b/pkg/text/text_processor.go index 9731d61d..87cbdc94 100644 --- a/pkg/text/text_processor.go +++ b/pkg/text/text_processor.go @@ -193,6 +193,21 @@ func HumanizeCertificates(certs []*x509.Certificate) string { } func filterSpecialChars(text string) string { + // Handle Slack user mentions: <@U1234567> or <@U1234567|displayname> + // Replace with @displayname or @userid + userMentionWithName := regexp.MustCompile(`<@(U[A-Z0-9]+)\|([^>]+)>`) + text = userMentionWithName.ReplaceAllString(text, "@$2") + + userMentionNoName := regexp.MustCompile(`<@(U[A-Z0-9]+)>`) + text = userMentionNoName.ReplaceAllString(text, "@$1") + + // Handle Slack channel mentions: <#C1234567> or <#C1234567|channel-name> + channelMentionWithName := regexp.MustCompile(`<#(C[A-Z0-9]+)\|([^>]+)>`) + text = channelMentionWithName.ReplaceAllString(text, "#$2") + + channelMentionNoName := regexp.MustCompile(`<#(C[A-Z0-9]+)>`) + text = channelMentionNoName.ReplaceAllString(text, "#$1") + replaceWithCommaCheck := func(match []string, isLast bool) string { var url, linkText string From 7e5b7c184f93dab08ab6b1500249933a5a347067 Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Wed, 28 Jan 2026 06:27:13 -0300 Subject: [PATCH 23/23] fix: additional formatting issue --- pkg/handler/channels.go | 11 +++-- pkg/handler/conversations.go | 82 ++++++++++++++++++++++++++++++++---- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/pkg/handler/channels.go b/pkg/handler/channels.go index 422d3c1d..2a3e0d6c 100644 --- a/pkg/handler/channels.go +++ b/pkg/handler/channels.go @@ -27,8 +27,8 @@ type Channel struct { } type ChannelsHandler struct { - apiProvider *provider.ApiProvider // Legacy mode - tokenStorage oauth.TokenStorage // OAuth mode + apiProvider *provider.ApiProvider // Legacy mode + tokenStorage oauth.TokenStorage // OAuth mode oauthEnabled bool validTypes map[string]bool logger *zap.Logger @@ -415,7 +415,11 @@ func (ch *ChannelsHandler) channelsHandlerOAuth(ctx context.Context, request mcp ch.logger.Debug("Failed to get user info for DM", zap.String("userID", c.User), zap.Error(err)) name = "@" + c.User // Fallback to user ID } else if user != nil { - if user.Profile.DisplayName != "" { + // Priority: RealName (full name) → DisplayName → Name (username) + // This provides more consistent formatting across users + if user.RealName != "" { + name = "@" + user.RealName + } else if user.Profile.DisplayName != "" { name = "@" + user.Profile.DisplayName } else { name = "@" + user.Name @@ -469,4 +473,3 @@ func (ch *ChannelsHandler) channelsHandlerOAuth(ctx context.Context, request mcp ch.logger.Debug("Returning channels", zap.Int("count", len(allChannels))) return mcp.NewToolResultText(string(csvBytes)), nil } - diff --git a/pkg/handler/conversations.go b/pkg/handler/conversations.go index 35c67202..cd85dc72 100644 --- a/pkg/handler/conversations.go +++ b/pkg/handler/conversations.go @@ -81,8 +81,8 @@ type addMessageParams struct { } type ConversationsHandler struct { - apiProvider *provider.ApiProvider // Legacy mode - tokenStorage oauth.TokenStorage // OAuth mode + apiProvider *provider.ApiProvider // Legacy mode + tokenStorage oauth.TokenStorage // OAuth mode oauthEnabled bool logger *zap.Logger } @@ -341,7 +341,7 @@ func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Conte zap.String("thread_ts", params.threadTs), zap.String("content_type", params.contentType), ) - + var respChannel, respTimestamp string if ch.oauthEnabled { respChannel, respTimestamp, err = slackClient.PostMessageContext(ctx, params.channel, options...) @@ -374,7 +374,7 @@ func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Conte Latest: respTimestamp, Inclusive: true, } - + var history *slack.GetConversationHistoryResponse if ch.oauthEnabled { if slackClient == nil { @@ -433,7 +433,7 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context, Cursor: params.cursor, Inclusive: false, } - + var history *slack.GetConversationHistoryResponse if ch.oauthEnabled { if slackClient == nil { @@ -496,7 +496,7 @@ func (ch *ConversationsHandler) ConversationsRepliesHandler(ctx context.Context, Cursor: params.cursor, Inclusive: false, } - + var replies []slack.Message var hasMore bool var nextCursor string @@ -545,7 +545,7 @@ func (ch *ConversationsHandler) ConversationsSearchHandler(ctx context.Context, Count: params.limit, Page: params.page, } - + var messagesRes *slack.SearchMessages if ch.oauthEnabled { messagesRes, _, err = slackClient.SearchContext(ctx, params.query, searchParams) @@ -596,11 +596,20 @@ func (ch *ConversationsHandler) convertMessagesFromHistory(ctx context.Context, usersMap = cache.Users } else { // OAuth mode: fetch user info from Slack API + // Collect all user IDs from messages AND from mentions in message text var userIDs []string + userMentionRe := regexp.MustCompile(`<@(U[A-Z0-9]+)(?:\|[^>]*)?>`) for _, msg := range slackMessages { if msg.User != "" { userIDs = append(userIDs, msg.User) } + // Extract user IDs from mentions in the text + matches := userMentionRe.FindAllStringSubmatch(msg.Text, -1) + for _, match := range matches { + if len(match) > 1 { + userIDs = append(userIDs, match[1]) + } + } } usersMap = ch.fetchUsersForMessages(ctx, slackClient, userIDs) } @@ -629,6 +638,8 @@ func (ch *ConversationsHandler) convertMessagesFromHistory(ctx context.Context, } msgText := msg.Text + text.AttachmentsTo2CSV(msg.Text, msg.Attachments) + // Expand user mentions to display names + msgText = expandUserMentions(msgText, usersMap) var reactionParts []string for _, r := range msg.Reactions { @@ -670,7 +681,9 @@ func (ch *ConversationsHandler) convertMessagesFromSearch(ctx context.Context, s usersMap = cache.Users } else { // OAuth mode: fetch user info from Slack API + // Collect all user IDs from messages AND from mentions in message text var userIDs []string + userMentionRe := regexp.MustCompile(`<@(U[A-Z0-9]+)(?:\|[^>]*)?>`) for _, msg := range slackMessages { if msg.User != "" { userIDs = append(userIDs, msg.User) @@ -679,6 +692,13 @@ func (ch *ConversationsHandler) convertMessagesFromSearch(ctx context.Context, s if strings.HasPrefix(msg.Channel.Name, "U") { userIDs = append(userIDs, msg.Channel.Name) } + // Extract user IDs from mentions in the text + matches := userMentionRe.FindAllStringSubmatch(msg.Text, -1) + for _, match := range matches { + if len(match) > 1 { + userIDs = append(userIDs, match[1]) + } + } } usersMap = ch.fetchUsersForMessages(ctx, slackClient, userIDs) } @@ -703,6 +723,8 @@ func (ch *ConversationsHandler) convertMessagesFromSearch(ctx context.Context, s } msgText := msg.Text + text.AttachmentsTo2CSV(msg.Text, msg.Attachments) + // Expand user mentions to display names (search API may already do this, but be safe) + msgText = expandUserMentions(msgText, usersMap) // Format channel name properly channelDisplay := fmt.Sprintf("#%s", msg.Channel.Name) @@ -712,7 +734,10 @@ func (ch *ConversationsHandler) convertMessagesFromSearch(ctx context.Context, s if strings.HasPrefix(msg.Channel.Name, "U") { // The "name" is actually a user ID - look it up if user, exists := usersMap[msg.Channel.Name]; exists { - if user.Profile.DisplayName != "" { + // Priority: RealName → DisplayName → Name for consistent formatting + if user.RealName != "" { + channelDisplay = "@" + user.RealName + } else if user.Profile.DisplayName != "" { channelDisplay = "@" + user.Profile.DisplayName } else { channelDisplay = "@" + user.Name @@ -1148,6 +1173,47 @@ func getBotInfo(botID string) (userName, realName string, ok bool) { return botID, botID, true } +// expandUserMentions replaces Slack user mentions (<@U1234567>) with display names (@Name) +// using the provided users map. If a display name is already in the mention (<@U1234567|Name>), +// it's handled by the text processor. This function handles the case where the mention +// only contains the user ID. +func expandUserMentions(text string, usersMap map[string]slack.User) string { + if usersMap == nil { + return text + } + + // Match user mentions without display name: <@U1234567> + // Don't match mentions that already have display name: <@U1234567|Name> + userMentionRe := regexp.MustCompile(`<@(U[A-Z0-9]+)>`) + + return userMentionRe.ReplaceAllStringFunc(text, func(match string) string { + // Extract user ID from <@U1234567> + submatch := userMentionRe.FindStringSubmatch(match) + if len(submatch) < 2 { + return match + } + userID := submatch[1] + + // Look up user in map + if user, exists := usersMap[userID]; exists { + // Priority: RealName → DisplayName → Name for consistent formatting + name := user.RealName + if name == "" { + name = user.Profile.DisplayName + } + if name == "" { + name = user.Name + } + if name != "" { + return "@" + name + } + } + + // Fallback to just the user ID + return "@" + userID + }) +} + func limitByNumeric(limit string, defaultLimit int) (int, error) { if limit == "" { return defaultLimit, nil