feat: add blocks parameter and bump slack-go-util to v0.4.0

Add optional `blocks` parameter to conversations_add_message for raw
Slack Block Kit JSON support (rich_text lists, code blocks, etc.).

When blocks is provided it takes precedence over text/content_type for
message rendering. The text parameter serves as notification fallback.

The blocks argument accepts both a JSON string and a raw JSON array to
accommodate different MCP client serialization behaviors.

Also bumps takara2314/slack-go-util from v0.3.0 to v0.4.0 which adds
nested list support to the existing text/markdown conversion path.
This commit is contained in:
Arnold Mendez
2026-05-07 10:10:53 -04:00
parent b24b1b08af
commit 97b24505c9
4 changed files with 75 additions and 21 deletions
+4 -4
View File
@@ -1,6 +1,6 @@
module github.com/korotovsky/slack-mcp-server
go 1.24.4
go 1.25
require (
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
@@ -13,9 +13,9 @@ require (
github.com/rusq/slackauth v0.7.1
github.com/rusq/slackdump/v3 v3.1.13
github.com/rusq/tagops v0.1.1
github.com/slack-go/slack v0.17.3
github.com/slack-go/slack v0.19.0
github.com/stretchr/testify v1.11.1
github.com/takara2314/slack-go-util v0.3.0
github.com/takara2314/slack-go-util v0.4.0
go.uber.org/zap v1.27.1
golang.ngrok.com/ngrok/v2 v2.1.1
golang.org/x/net v0.50.0
@@ -87,7 +87,7 @@ require (
github.com/ysmood/got v0.42.3 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.ngrok.com/muxado/v2 v2.0.1 // indirect
golang.org/x/crypto v0.48.0 // indirect
+6
View File
@@ -153,6 +153,8 @@ 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=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/slack-go/slack v0.19.0 h1:J8lL/nGTsIUX53HU8YxZeI3PDkA+sxZsFrI2Dew7h44=
github.com/slack-go/slack v0.19.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -162,6 +164,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
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/takara2314/slack-go-util v0.4.0 h1:sv/Cd95HNvIPDejpTY1bytT68Lb4dXgtcUFQhJZJNis=
github.com/takara2314/slack-go-util v0.4.0/go.mod h1:6m63Q+4p5F1qfa5NKfqspSWkgKCM168ZjabHBTXqQj0=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -197,6 +201,8 @@ github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
+61 -16
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
@@ -96,6 +97,7 @@ type addMessageParams struct {
threadTs string
text string
contentType string
blocks []slack.Block
}
type addReactionParams struct {
@@ -221,21 +223,30 @@ func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Conte
options = append(options, slack.MsgOptionTS(params.threadTs))
}
switch params.contentType {
case "text/plain":
options = append(options, slack.MsgOptionDisableMarkdown())
options = append(options, slack.MsgOptionText(params.text, false))
case "text/markdown":
blocks, err := slackGoUtil.ConvertMarkdownTextToBlocks(params.text)
if err != nil {
ch.logger.Warn("Markdown parsing error", zap.Error(err))
if params.blocks != nil {
// Raw blocks provided: use them directly. If text is also provided, it
// serves as the notification/fallback text.
options = append(options, slack.MsgOptionBlocks(params.blocks...))
if params.text != "" {
options = append(options, slack.MsgOptionText(params.text, false))
}
} else {
switch params.contentType {
case "text/plain":
options = append(options, slack.MsgOptionDisableMarkdown())
options = append(options, slack.MsgOptionText(params.text, false))
} else {
options = append(options, slack.MsgOptionBlocks(blocks...))
case "text/markdown":
blocks, err := slackGoUtil.ConvertMarkdownTextToBlocks(params.text)
if err != nil {
ch.logger.Warn("Markdown parsing error", zap.Error(err))
options = append(options, slack.MsgOptionDisableMarkdown())
options = append(options, slack.MsgOptionText(params.text, false))
} else {
options = append(options, slack.MsgOptionBlocks(blocks...))
}
default:
return nil, errors.New("content_type must be either 'text/plain' or 'text/markdown'")
}
default:
return nil, errors.New("content_type must be either 'text/plain' or 'text/markdown'")
}
unfurlOpt := os.Getenv("SLACK_MCP_ADD_MESSAGE_UNFURLING")
@@ -1698,10 +1709,6 @@ func (ch *ConversationsHandler) parseParamsToolAddMessage(ctx context.Context, r
// Backward compatibility with "payload" parameter
msgText = request.GetString("payload", "")
}
if msgText == "" {
ch.logger.Error("Message text missing")
return nil, errors.New("text must be a string")
}
contentType := request.GetString("content_type", "text/markdown")
if contentType != "text/plain" && contentType != "text/markdown" {
@@ -1709,11 +1716,49 @@ func (ch *ConversationsHandler) parseParamsToolAddMessage(ctx context.Context, r
return nil, errors.New("content_type must be either 'text/plain' or 'text/markdown'")
}
// Parse optional raw blocks JSON. Accepts blocks as either:
// - A JSON string containing a blocks array: "blocks": "[{...}]"
// - A raw JSON array (parsed by MCP SDK): "blocks": [{...}]
var blocks []slack.Block
args := request.GetArguments()
if rawBlocks, ok := args["blocks"]; ok && rawBlocks != nil {
var blocksJSON []byte
switch v := rawBlocks.(type) {
case string:
if v != "" {
blocksJSON = []byte(v)
}
default:
// Raw JSON array/object passed directly - re-marshal to bytes
var err error
blocksJSON, err = json.Marshal(v)
if err != nil {
ch.logger.Error("Failed to marshal blocks argument", zap.Error(err))
return nil, fmt.Errorf("blocks must be valid Slack Block Kit JSON: %w", err)
}
}
if blocksJSON != nil {
var slackBlocks slack.Blocks
if err := json.Unmarshal(blocksJSON, &slackBlocks); err != nil {
ch.logger.Error("Failed to parse blocks JSON", zap.Error(err))
return nil, fmt.Errorf("blocks must be valid Slack Block Kit JSON: %w", err)
}
blocks = slackBlocks.BlockSet
}
}
// Require either text or blocks
if msgText == "" && blocks == nil {
ch.logger.Error("Message text and blocks both missing")
return nil, errors.New("either text or blocks must be provided")
}
return &addMessageParams{
channel: channel,
threadTs: threadTs,
text: msgText,
contentType: contentType,
blocks: blocks,
}, nil
}
+4 -1
View File
@@ -181,7 +181,10 @@ func NewMCPServer(provider *provider.ApiProvider, logger *zap.Logger, enabledToo
),
mcp.WithString("content_type",
mcp.DefaultString("text/markdown"),
mcp.Description("Content type of the message. Default is 'text/markdown'. Allowed values: 'text/markdown', 'text/plain'."),
mcp.Description("Content type of the message. Default is 'text/markdown'. Allowed values: 'text/markdown', 'text/plain'. Ignored when blocks is provided."),
),
mcp.WithString("blocks",
mcp.Description("Raw Slack Block Kit JSON array for rich message formatting (rich_text lists, code blocks, etc.). When provided, this takes precedence over text/content_type for rendering. The text parameter becomes the notification fallback text."),
),
), conversationsHandler.ConversationsAddMessageHandler)
}