Skip to content

Commit 18ba252

Browse files
authored
Merge pull request #52 from korotovsky/resources
Added MCP Resources
2 parents c5752ba + d306814 commit 18ba252

File tree

8 files changed

+243
-50
lines changed

8 files changed

+243
-50
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,34 @@ Get list of channels
8181
- `limit` (number, default: 100): The maximum number of items to return. Must be an integer between 1 and 1000 (maximum 999).
8282
- `cursor` (string, optional): Cursor for pagination. Use the value of the last row and column in the response as next_cursor field returned from the previous request.
8383

84+
## Resources
85+
86+
The Slack MCP Server exposes two special directory resources for easy access to workspace metadata:
87+
88+
### 1. `slack://<workspace>/channels` — Directory of Channels
89+
90+
Fetches a CSV directory of all channels in the workspace, including public channels, private channels, DMs, and group DMs.
91+
92+
- **URI:** `slack://<workspace>/channels`
93+
- **Format:** `text/csv`
94+
- **Fields:**
95+
- `id`: Channel ID (e.g., `C1234567890`)
96+
- `name`: Channel name (e.g., `#general`, `@username_dm`)
97+
- `topic`: Channel topic (if any)
98+
- `purpose`: Channel purpose/description
99+
- `memberCount`: Number of members in the channel
100+
101+
### 2. `slack://<workspace>/users` — Directory of Users
102+
103+
Fetches a CSV directory of all users in the workspace.
104+
105+
- **URI:** `slack://<workspace>/users`
106+
- **Format:** `text/csv`
107+
- **Fields:**
108+
- `userID`: User ID (e.g., `U1234567890`)
109+
- `userName`: Slack username (e.g., `john`)
110+
- `realName`: User’s real name (e.g., `John Doe`)
111+
84112
## Setup Guide
85113

86114
- [Authentication Setup](docs/01-authentication-setup.md)

cmd/slack-mcp-server/main.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@ func main() {
2727
log.Fatalf("error in SLACK_MCP_ADD_MESSAGE_TOOL: %v", err)
2828
}
2929

30-
p := provider.New()
31-
32-
s := server.NewMCPServer(p,
33-
transport,
34-
)
30+
p := provider.New(transport)
31+
s := server.NewMCPServer(p)
3532

3633
go func() {
3734
newUsersWatcher(p)()

pkg/handler/channels.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ package handler
33
import (
44
"context"
55
"encoding/base64"
6+
"fmt"
67
"sort"
78
"strings"
89

910
"github.com/gocarina/gocsv"
1011
"github.com/korotovsky/slack-mcp-server/pkg/provider"
12+
"github.com/korotovsky/slack-mcp-server/pkg/server/auth"
13+
"github.com/korotovsky/slack-mcp-server/pkg/text"
1114
"github.com/mark3labs/mcp-go/mcp"
1215
)
1316

@@ -37,6 +40,53 @@ func NewChannelsHandler(apiProvider *provider.ApiProvider) *ChannelsHandler {
3740
}
3841
}
3942

43+
func (ch *ChannelsHandler) ChannelsResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
44+
// mark3labs/mcp-go does not support middlewares for resources.
45+
if authenticated, err := auth.IsAuthenticated(ctx, ch.apiProvider.ServerTransport()); !authenticated {
46+
return nil, err
47+
}
48+
49+
var channelList []Channel
50+
51+
if ready, err := ch.apiProvider.IsReady(); !ready {
52+
return nil, err
53+
}
54+
55+
_, ar, err := ch.apiProvider.ProvideGeneric()
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
ws, err := text.Workspace(ar.URL)
61+
if err != nil {
62+
return nil, fmt.Errorf("failed to parse workspace from URL: %v", err)
63+
}
64+
65+
channels := ch.apiProvider.ProvideChannelsMaps().Channels
66+
for _, channel := range channels {
67+
channelList = append(channelList, Channel{
68+
ID: channel.ID,
69+
Name: channel.Name,
70+
Topic: channel.Topic,
71+
Purpose: channel.Purpose,
72+
MemberCount: channel.MemberCount,
73+
})
74+
}
75+
76+
csvBytes, err := gocsv.MarshalBytes(&channelList)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
return []mcp.ResourceContents{
82+
mcp.TextResourceContents{
83+
URI: "slack://" + ws + "/channels",
84+
MIMEType: "text/csv",
85+
Text: string(csvBytes),
86+
},
87+
}, nil
88+
}
89+
4090
func (ch *ChannelsHandler) ChannelsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
4191
if ready, err := ch.apiProvider.IsReady(); !ready {
4292
return nil, err

pkg/handler/conversations.go

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/gocarina/gocsv"
1717
"github.com/korotovsky/slack-mcp-server/pkg/provider"
18+
"github.com/korotovsky/slack-mcp-server/pkg/server/auth"
1819
"github.com/korotovsky/slack-mcp-server/pkg/text"
1920
"github.com/mark3labs/mcp-go/mcp"
2021
"github.com/slack-go/slack"
@@ -32,6 +33,12 @@ type Message struct {
3233
Cursor string `json:"cursor"`
3334
}
3435

36+
type User struct {
37+
UserID string `json:"userID"`
38+
UserName string `json:"userName"`
39+
RealName string `json:"realName"`
40+
}
41+
3542
type conversationParams struct {
3643
channel string
3744
limit int
@@ -75,13 +82,59 @@ func NewConversationsHandler(apiProvider *provider.ApiProvider) *ConversationsHa
7582
}
7683
}
7784

85+
func (ch *ConversationsHandler) UsersResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
86+
// mark3labs/mcp-go does not support middlewares for resources.
87+
if authenticated, err := auth.IsAuthenticated(ctx, ch.apiProvider.ServerTransport()); !authenticated {
88+
return nil, err
89+
}
90+
91+
if ready, err := ch.apiProvider.IsReady(); !ready {
92+
return nil, err
93+
}
94+
95+
_, ar, err := ch.apiProvider.ProvideGeneric()
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
ws, err := text.Workspace(ar.URL)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to parse workspace from URL: %v", err)
103+
}
104+
105+
usersMaps := ch.apiProvider.ProvideUsersMap()
106+
users := usersMaps.Users
107+
108+
usersList := make([]User, 0, len(users))
109+
for _, user := range users {
110+
usersList = append(usersList, User{
111+
UserID: user.ID,
112+
UserName: user.Name,
113+
RealName: user.RealName,
114+
})
115+
}
116+
117+
csvBytes, err := gocsv.MarshalBytes(&usersList)
118+
if err != nil {
119+
return nil, err
120+
}
121+
122+
return []mcp.ResourceContents{
123+
mcp.TextResourceContents{
124+
URI: "slack://" + ws + "/users",
125+
MIMEType: "text/csv",
126+
Text: string(csvBytes),
127+
},
128+
}, nil
129+
}
130+
78131
func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
79132
params, err := ch.parseParamsToolAddMessage(request)
80133
if err != nil {
81134
return nil, err
82135
}
83136

84-
api, err := ch.apiProvider.ProvideGeneric()
137+
api, _, err := ch.apiProvider.ProvideGeneric()
85138
if err != nil {
86139
return nil, err
87140
}
@@ -140,7 +193,7 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context,
140193
return nil, err
141194
}
142195

143-
api, err := ch.apiProvider.ProvideGeneric()
196+
api, _, err := ch.apiProvider.ProvideGeneric()
144197
if err != nil {
145198
return nil, err
146199
}
@@ -179,7 +232,7 @@ func (ch *ConversationsHandler) ConversationsRepliesHandler(ctx context.Context,
179232
return nil, errors.New("thread_ts must be a string")
180233
}
181234

182-
api, err := ch.apiProvider.ProvideGeneric()
235+
api, _, err := ch.apiProvider.ProvideGeneric()
183236
if err != nil {
184237
return nil, err
185238
}
@@ -214,7 +267,7 @@ func (ch *ConversationsHandler) ConversationsSearchHandler(ctx context.Context,
214267
return nil, err
215268
}
216269

217-
api, err := ch.apiProvider.ProvideGeneric()
270+
api, _, err := ch.apiProvider.ProvideGeneric()
218271
if err != nil {
219272
return nil, err
220273
}

pkg/provider/api.go

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ type ChannelsCache struct {
4343
}
4444

4545
type ApiProvider struct {
46-
boot func(ap *ApiProvider) *slack.Client
46+
transport string
47+
boot func(ap *ApiProvider) *slack.Client
4748

4849
authProvider *auth.ValueAuth
4950
authResponse *slack2.AuthTestResponse
@@ -73,7 +74,7 @@ type Channel struct {
7374
IsPrivate bool `json:"private"`
7475
}
7576

76-
func New() *ApiProvider {
77+
func New(transport string) *ApiProvider {
7778
var (
7879
authProvider auth.ValueAuth
7980
err error
@@ -87,7 +88,7 @@ func New() *ApiProvider {
8788
panic(err)
8889
}
8990

90-
return newWithXOXP(authProvider)
91+
return newWithXOXP(transport, authProvider)
9192
}
9293

9394
// Fall back to XOXC/XOXD tokens (session-based)
@@ -103,10 +104,10 @@ func New() *ApiProvider {
103104
panic(err)
104105
}
105106

106-
return newWithXOXC(authProvider)
107+
return newWithXOXC(transport, authProvider)
107108
}
108109

109-
func newWithXOXP(authProvider auth.ValueAuth) *ApiProvider {
110+
func newWithXOXP(transport string, authProvider auth.ValueAuth) *ApiProvider {
110111
usersCache := os.Getenv("SLACK_MCP_USERS_CACHE")
111112
if usersCache == "" {
112113
usersCache = ".users_cache.json"
@@ -118,6 +119,7 @@ func newWithXOXP(authProvider auth.ValueAuth) *ApiProvider {
118119
}
119120

120121
return &ApiProvider{
122+
transport: transport,
121123
boot: func(ap *ApiProvider) *slack.Client {
122124
api := slack.New(authProvider.SlackToken())
123125
res, err := api.AuthTest()
@@ -150,7 +152,7 @@ func newWithXOXP(authProvider auth.ValueAuth) *ApiProvider {
150152
}
151153
}
152154

153-
func newWithXOXC(authProvider auth.ValueAuth) *ApiProvider {
155+
func newWithXOXC(transport string, authProvider auth.ValueAuth) *ApiProvider {
154156
usersCache := os.Getenv("SLACK_MCP_USERS_CACHE")
155157
if usersCache == "" {
156158
usersCache = ".users_cache.json"
@@ -162,6 +164,7 @@ func newWithXOXC(authProvider auth.ValueAuth) *ApiProvider {
162164
}
163165

164166
return &ApiProvider{
167+
transport: transport,
165168
boot: func(ap *ApiProvider) *slack.Client {
166169
api := slack.New(authProvider.SlackToken(),
167170
withHTTPClientOption(authProvider.Cookies()),
@@ -201,22 +204,30 @@ func newWithXOXC(authProvider auth.ValueAuth) *ApiProvider {
201204
}
202205
}
203206

204-
func (ap *ApiProvider) ProvideGeneric() (*slack.Client, error) {
207+
func (ap *ApiProvider) ProvideGeneric() (*slack.Client, *slack2.AuthTestResponse, error) {
205208
if ap.clientGeneric == nil {
206209
ap.clientGeneric = ap.boot(ap)
207210
}
208211

209-
return ap.clientGeneric, nil
212+
return ap.clientGeneric, ap.authResponse, nil
210213
}
211214

212-
func (ap *ApiProvider) ProvideEnterprise() (*edge.Client, error) {
215+
func (ap *ApiProvider) ProvideEnterprise() (*edge.Client, *slack2.AuthTestResponse, error) {
213216
if ap.clientEnterprise == nil {
214217
ap.clientEnterprise, _ = edge.NewWithInfo(ap.authResponse, ap.authProvider,
215218
withHTTPClientEdgeOption(ap.authProvider.Cookies()),
216219
)
217220
}
218221

219-
return ap.clientEnterprise, nil
222+
return ap.clientEnterprise, ap.authResponse, nil
223+
}
224+
225+
func (ap *ApiProvider) AuthResponse() (*slack2.AuthTestResponse, error) {
226+
if ap.authResponse == nil {
227+
return nil, errors.New("not authenticated")
228+
}
229+
230+
return ap.authResponse, nil
220231
}
221232

222233
func (ap *ApiProvider) RefreshUsers(ctx context.Context) error {
@@ -237,7 +248,7 @@ func (ap *ApiProvider) RefreshUsers(ctx context.Context) error {
237248

238249
optionLimit := slack.GetUsersOptionLimit(1000)
239250

240-
client, err := ap.ProvideGeneric()
251+
client, _, err := ap.ProvideGeneric()
241252
if err != nil {
242253
return err
243254
}
@@ -324,12 +335,12 @@ func (ap *ApiProvider) GetChannels(ctx context.Context, channelTypes []string) [
324335
nextcur string
325336
)
326337

327-
clientGeneric, err := ap.ProvideGeneric()
338+
clientGeneric, _, err := ap.ProvideGeneric()
328339
if err != nil {
329340
return nil
330341
}
331342

332-
clientE, err := ap.ProvideEnterprise()
343+
clientE, _, err := ap.ProvideEnterprise()
333344
if err != nil {
334345
return nil
335346
}
@@ -452,6 +463,10 @@ func (ap *ApiProvider) IsReady() (bool, error) {
452463
return true, nil
453464
}
454465

466+
func (ap *ApiProvider) ServerTransport() string {
467+
return ap.transport
468+
}
469+
455470
func withHTTPClientOption(cookies []*http.Cookie) func(c *slack.Client) {
456471
return func(c *slack.Client) {
457472
slack.OptionHTTPClient(provideHTTPClient(cookies))(c)

0 commit comments

Comments
 (0)