@goforgeit/mcp-slack 0.4.11 → 0.4.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,7 +18,9 @@ var ForgeOAuthTokensSchema = z.object({
18
18
  connectedAt: z.string(),
19
19
  scope: z.string().optional(),
20
20
  email: z.string().optional(),
21
- accountId: z.string().optional()
21
+ accountId: z.string().optional(),
22
+ /** User-level access token (Slack xoxp-* token for search operations) */
23
+ userAccessToken: z.string().optional()
22
24
  });
23
25
  var DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
24
26
  function createForgeTokenManager(options) {
@@ -100,6 +102,12 @@ function createForgeTokenManager(options) {
100
102
  }
101
103
  return cachedTokens.accessToken;
102
104
  },
105
+ async getUserAccessToken() {
106
+ if (!cachedTokens) {
107
+ await this.getAccessToken();
108
+ }
109
+ return cachedTokens?.userAccessToken ?? null;
110
+ },
103
111
  getTokenInfo() {
104
112
  return cachedTokens;
105
113
  },
@@ -131,6 +139,13 @@ function createSlackMCPServer(options) {
131
139
  const token = await tokenManager.getAccessToken();
132
140
  return new WebClient(token);
133
141
  }
142
+ async function getUserClient() {
143
+ const userToken = await tokenManager.getUserAccessToken();
144
+ if (userToken) {
145
+ return new WebClient(userToken);
146
+ }
147
+ return getClient();
148
+ }
134
149
  registerTool(
135
150
  "list_channels",
136
151
  "List Slack channels the bot has access to",
@@ -192,7 +207,7 @@ function createSlackMCPServer(options) {
192
207
  count: z2.number().optional().default(20).describe("Number of results")
193
208
  },
194
209
  async (args) => {
195
- const client = await getClient();
210
+ const client = await getUserClient();
196
211
  const result = await client.search.messages({
197
212
  query: args.query,
198
213
  sort: args.sort || "score",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../shared/src/mcp-auth/forge-token-manager.ts"],"sourcesContent":["/**\n * @goforgeit/mcp-slack — MCP server for Slack\n *\n * Provides channel, message, and user tools via stdio transport.\n * Reads Forge's OAuth tokens and uses Slack Web API.\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport { WebClient } from '@slack/web-api';\nimport { createForgeTokenManager } from '@forge/shared/mcp-auth';\n\nconst SLACK_TOKEN_URL = 'https://slack.com/api/oauth.v2.access';\n\nexport interface SlackMCPOptions {\n tokenPath: string;\n clientId: string;\n clientSecret: string;\n}\n\ninterface ToolHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;\n}\n\ninterface SlackMCPServer {\n server: McpServer;\n getRegisteredTools(): Record<string, ToolHandler>;\n start(): Promise<void>;\n}\n\nexport function createSlackMCPServer(options: SlackMCPOptions): SlackMCPServer {\n const tokenManager = createForgeTokenManager({\n tokenPath: options.tokenPath,\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n tokenUrl: SLACK_TOKEN_URL,\n });\n\n const server = new McpServer({\n name: '@goforgeit/mcp-slack',\n version: '0.1.0',\n });\n\n const registeredTools: Record<string, ToolHandler> = {};\n\n function registerTool(\n name: string,\n description: string,\n schema: Record<string, z.ZodType>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>,\n ) {\n registeredTools[name] = { handler };\n server.tool(name, description, schema, handler);\n }\n\n async function getClient(): Promise<WebClient> {\n const token = await tokenManager.getAccessToken();\n return new WebClient(token);\n }\n\n // --- Channel Tools ---\n\n registerTool(\n 'list_channels',\n 'List Slack channels the bot has access to',\n {\n types: z\n .string()\n .optional()\n .default('public_channel')\n .describe('Channel types (comma-separated: public_channel, private_channel, mpim, im)'),\n limit: z.number().optional().default(100).describe('Maximum channels to return'),\n },\n async (args: { types?: string; limit?: number }) => {\n const client = await getClient();\n const result = await client.conversations.list({\n types: args.types || 'public_channel',\n limit: args.limit || 100,\n });\n\n const channels = (result.channels || []).map((ch) => ({\n id: ch.id,\n name: ch.name,\n topic: ch.topic?.value,\n num_members: ch.num_members,\n }));\n\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ channels }) }],\n };\n },\n );\n\n registerTool(\n 'get_channel_history',\n 'Get message history from a Slack channel',\n {\n channelId: z.string().describe('Slack channel ID'),\n limit: z.number().optional().default(20).describe('Number of messages to fetch'),\n oldest: z.string().optional().describe('Start of time range (Unix timestamp)'),\n latest: z.string().optional().describe('End of time range (Unix timestamp)'),\n },\n async (args: { channelId: string; limit?: number; oldest?: string; latest?: string }) => {\n const client = await getClient();\n const result = await client.conversations.history({\n channel: args.channelId,\n limit: args.limit || 20,\n oldest: args.oldest,\n latest: args.latest,\n });\n\n const messages = (result.messages || []).map((msg) => ({\n ts: msg.ts,\n text: msg.text,\n user: msg.user,\n type: msg.type,\n }));\n\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ messages }) }],\n };\n },\n );\n\n // --- Message Tools ---\n\n registerTool(\n 'search_messages',\n 'Search Slack messages across all channels',\n {\n query: z.string().describe('Search query'),\n sort: z.enum(['score', 'timestamp']).optional().default('score').describe('Sort order'),\n count: z.number().optional().default(20).describe('Number of results'),\n },\n async (args: { query: string; sort?: 'score' | 'timestamp'; count?: number }) => {\n const client = await getClient();\n const result = await client.search.messages({\n query: args.query,\n sort: args.sort || 'score',\n count: args.count || 20,\n });\n\n const matches = result.messages?.matches || [];\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n matches: matches.map((m) => ({\n ts: m.ts,\n text: m.text,\n channel: m.channel,\n user: m.user,\n })),\n total: result.messages?.total || 0,\n }),\n },\n ],\n };\n },\n );\n\n // --- User Tools ---\n\n registerTool(\n 'list_users',\n 'List Slack workspace members',\n {\n limit: z.number().optional().default(100).describe('Maximum users to return'),\n },\n async (args: { limit?: number }) => {\n const client = await getClient();\n const result = await client.users.list({\n limit: args.limit || 100,\n });\n\n const members = (result.members || []).map((m) => ({\n id: m.id,\n name: m.name,\n real_name: m.real_name,\n is_bot: m.is_bot,\n }));\n\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ members }) }],\n };\n },\n );\n\n registerTool(\n 'get_user_info',\n 'Get detailed information about a Slack user',\n {\n userId: z.string().describe('Slack user ID'),\n },\n async (args: { userId: string }) => {\n const client = await getClient();\n const result = await client.users.info({\n user: args.userId,\n });\n\n const user = result.user;\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n id: user?.id,\n name: user?.name,\n real_name: user?.real_name,\n profile: user?.profile,\n is_bot: user?.is_bot,\n tz: user?.tz,\n }),\n },\n ],\n };\n },\n );\n\n return {\n server,\n getRegisteredTools() {\n return registeredTools;\n },\n async start() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n },\n };\n}\n\n// CLI entry point\nconst isMainModule =\n process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\\\/g, '/'));\nif (isMainModule || process.env.FORGE_MCP_START === 'true') {\n const tokenPath = process.env.FORGE_TOKEN_PATH;\n const clientId = process.env.FORGE_CLIENT_ID;\n const clientSecret = process.env.FORGE_CLIENT_SECRET;\n\n if (!tokenPath || !clientId || !clientSecret) {\n console.error(\n 'Required environment variables: FORGE_TOKEN_PATH, FORGE_CLIENT_ID, FORGE_CLIENT_SECRET',\n );\n process.exit(1);\n }\n\n const mcpServer = createSlackMCPServer({ tokenPath, clientId, clientSecret });\n mcpServer.start().catch((err) => {\n console.error('Failed to start MCP server:', err);\n process.exit(1);\n });\n}\n","/**\n * Forge Token Manager — reads Forge's OAuthTokens format and handles refresh.\n *\n * Used by @forge/mcp-* packages to get valid access tokens for API calls.\n * Reads from FORGE_TOKEN_PATH, validates with Zod, and refreshes when\n * the token is expired or within 5 minutes of expiry.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\n/** Zod schema for Forge's OAuth token file format */\nexport const ForgeOAuthTokensSchema = z.object({\n accessToken: z.string(),\n refreshToken: z.string().optional(),\n expiresAt: z.number(),\n providerId: z.string(),\n connectedAt: z.string(),\n scope: z.string().optional(),\n email: z.string().optional(),\n accountId: z.string().optional(),\n});\n\nexport type ForgeOAuthTokens = z.infer<typeof ForgeOAuthTokensSchema>;\n\nexport interface ForgeTokenManagerOptions {\n /** Path to the Forge OAuth token JSON file */\n tokenPath: string;\n /** OAuth client ID for token refresh */\n clientId: string;\n /** OAuth client secret for token refresh */\n clientSecret: string;\n /** OAuth token endpoint URL for refresh requests */\n tokenUrl: string;\n /** Buffer in ms before expiry to trigger refresh (default: 5 minutes) */\n refreshBufferMs?: number;\n}\n\nexport interface ForgeTokenManager {\n /** Returns a valid access token, refreshing if needed */\n getAccessToken(): Promise<string>;\n /** Returns the current token data, or null if not yet loaded */\n getTokenInfo(): ForgeOAuthTokens | null;\n /** Register a callback for when tokens are refreshed */\n onRefresh(callback: (tokens: ForgeOAuthTokens) => void): void;\n}\n\nconst DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes\n\nexport function createForgeTokenManager(options: ForgeTokenManagerOptions): ForgeTokenManager {\n const {\n tokenPath,\n clientId,\n clientSecret,\n tokenUrl,\n refreshBufferMs = DEFAULT_REFRESH_BUFFER_MS,\n } = options;\n\n let cachedTokens: ForgeOAuthTokens | null = null;\n const refreshCallbacks: Array<(tokens: ForgeOAuthTokens) => void> = [];\n\n function isExpiredOrNearExpiry(tokens: ForgeOAuthTokens): boolean {\n return Date.now() >= tokens.expiresAt - refreshBufferMs;\n }\n\n async function readTokenFile(): Promise<ForgeOAuthTokens> {\n let raw: string;\n try {\n raw = await fs.readFile(tokenPath, 'utf-8');\n } catch (err: unknown) {\n if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {\n throw new Error(`Token file not found: ${tokenPath}`);\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(`Invalid JSON in token file: ${tokenPath}`);\n }\n\n const result = ForgeOAuthTokensSchema.safeParse(parsed);\n if (!result.success) {\n throw new Error(`Invalid token file format: ${result.error.message}`);\n }\n\n return result.data;\n }\n\n async function refreshToken(tokens: ForgeOAuthTokens): Promise<ForgeOAuthTokens> {\n if (!tokens.refreshToken) {\n throw new Error('No refresh token available — cannot refresh expired token');\n }\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: tokens.refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n });\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = await response.json();\n\n const refreshedTokens: ForgeOAuthTokens = {\n ...tokens,\n accessToken: data.access_token,\n refreshToken: data.refresh_token || tokens.refreshToken,\n expiresAt: Date.now() + data.expires_in * 1000,\n };\n\n // Write refreshed tokens back to file\n await fs.mkdir(path.dirname(tokenPath), { recursive: true });\n await fs.writeFile(tokenPath, JSON.stringify(refreshedTokens), { mode: 0o600 });\n\n // Notify listeners\n for (const cb of refreshCallbacks) {\n cb(refreshedTokens);\n }\n\n return refreshedTokens;\n }\n\n return {\n async getAccessToken(): Promise<string> {\n if (cachedTokens && !isExpiredOrNearExpiry(cachedTokens)) {\n return cachedTokens.accessToken;\n }\n\n const tokens = cachedTokens || (await readTokenFile());\n\n if (isExpiredOrNearExpiry(tokens)) {\n if (tokens.refreshToken) {\n cachedTokens = await refreshToken(tokens);\n } else {\n // No refresh token — return the access token as-is.\n // Some providers (e.g. Slack bot tokens) issue non-expiring tokens\n // without refresh tokens, so the expiresAt may be unreliable.\n cachedTokens = tokens;\n }\n } else {\n cachedTokens = tokens;\n }\n\n return cachedTokens.accessToken;\n },\n\n getTokenInfo(): ForgeOAuthTokens | null {\n return cachedTokens;\n },\n\n onRefresh(callback: (tokens: ForgeOAuthTokens) => void): void {\n refreshCallbacks.push(callback);\n },\n };\n}\n\n/**\n * Create a ForgeTokenManager from standard environment variables.\n *\n * Expected env vars:\n * - FORGE_TOKEN_PATH: path to the OAuth token JSON file\n * - FORGE_CLIENT_ID: OAuth client ID\n * - FORGE_CLIENT_SECRET: OAuth client secret\n *\n * @param tokenUrl The provider's token endpoint URL\n */\nexport function createForgeTokenManagerFromEnv(tokenUrl: string): ForgeTokenManager {\n const tokenPath = process.env.FORGE_TOKEN_PATH;\n const clientId = process.env.FORGE_CLIENT_ID;\n const clientSecret = process.env.FORGE_CLIENT_SECRET;\n\n if (!tokenPath) throw new Error('FORGE_TOKEN_PATH environment variable is required');\n if (!clientId) throw new Error('FORGE_CLIENT_ID environment variable is required');\n if (!clientSecret) throw new Error('FORGE_CLIENT_SECRET environment variable is required');\n\n return createForgeTokenManager({ tokenPath, clientId, clientSecret, tokenUrl });\n}\n"],"mappings":";;;AAOA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AACrC,SAAS,KAAAA,UAAS;AAClB,SAAS,iBAAiB;;;ACF1B,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,SAAS;AAGX,IAAM,yBAAyB,EAAE,OAAO;EAC7C,aAAa,EAAE,OAAM;EACrB,cAAc,EAAE,OAAM,EAAG,SAAQ;EACjC,WAAW,EAAE,OAAM;EACnB,YAAY,EAAE,OAAM;EACpB,aAAa,EAAE,OAAM;EACrB,OAAO,EAAE,OAAM,EAAG,SAAQ;EAC1B,OAAO,EAAE,OAAM,EAAG,SAAQ;EAC1B,WAAW,EAAE,OAAM,EAAG,SAAQ;CAC/B;AA0BD,IAAM,4BAA4B,IAAI,KAAK;AAErC,SAAU,wBAAwB,SAAiC;AACvE,QAAM,EACJ,WACA,UACA,cACA,UACA,kBAAkB,0BAAyB,IACzC;AAEJ,MAAI,eAAwC;AAC5C,QAAM,mBAA8D,CAAA;AAEpE,WAAS,sBAAsB,QAAwB;AACrD,WAAO,KAAK,IAAG,KAAM,OAAO,YAAY;EAC1C;AAEA,iBAAe,gBAAa;AAC1B,QAAI;AACJ,QAAI;AACF,YAAM,MAAS,YAAS,WAAW,OAAO;IAC5C,SAAS,KAAc;AACrB,UAAI,OAAO,OAAO,QAAQ,YAAY,UAAU,OAAO,IAAI,SAAS,UAAU;AAC5E,cAAM,IAAI,MAAM,yBAAyB,SAAS,EAAE;MACtD;AACA,YAAM;IACR;AAEA,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,GAAG;IACzB,QAAQ;AACN,YAAM,IAAI,MAAM,+BAA+B,SAAS,EAAE;IAC5D;AAEA,UAAM,SAAS,uBAAuB,UAAU,MAAM;AACtD,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,IAAI,MAAM,8BAA8B,OAAO,MAAM,OAAO,EAAE;IACtE;AAEA,WAAO,OAAO;EAChB;AAEA,iBAAe,aAAa,QAAwB;AAClD,QAAI,CAAC,OAAO,cAAc;AACxB,YAAM,IAAI,MAAM,gEAA2D;IAC7E;AAEA,UAAM,OAAO,IAAI,gBAAgB;MAC/B,YAAY;MACZ,eAAe,OAAO;MACtB,WAAW;MACX,eAAe;KAChB;AAED,UAAM,WAAW,MAAM,MAAM,UAAU;MACrC,QAAQ;MACR,SAAS,EAAE,gBAAgB,oCAAmC;MAC9D,MAAM,KAAK,SAAQ;KACpB;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAI;AACrC,YAAM,IAAI,MAAM,yBAAyB,SAAS,MAAM,MAAM,SAAS,EAAE;IAC3E;AAEA,UAAM,OAAO,MAAM,SAAS,KAAI;AAEhC,UAAM,kBAAoC;MACxC,GAAG;MACH,aAAa,KAAK;MAClB,cAAc,KAAK,iBAAiB,OAAO;MAC3C,WAAW,KAAK,IAAG,IAAK,KAAK,aAAa;;AAI5C,UAAS,SAAW,aAAQ,SAAS,GAAG,EAAE,WAAW,KAAI,CAAE;AAC3D,UAAS,aAAU,WAAW,KAAK,UAAU,eAAe,GAAG,EAAE,MAAM,IAAK,CAAE;AAG9E,eAAW,MAAM,kBAAkB;AACjC,SAAG,eAAe;IACpB;AAEA,WAAO;EACT;AAEA,SAAO;IACL,MAAM,iBAAc;AAClB,UAAI,gBAAgB,CAAC,sBAAsB,YAAY,GAAG;AACxD,eAAO,aAAa;MACtB;AAEA,YAAM,SAAS,gBAAiB,MAAM,cAAa;AAEnD,UAAI,sBAAsB,MAAM,GAAG;AACjC,YAAI,OAAO,cAAc;AACvB,yBAAe,MAAM,aAAa,MAAM;QAC1C,OAAO;AAIL,yBAAe;QACjB;MACF,OAAO;AACL,uBAAe;MACjB;AAEA,aAAO,aAAa;IACtB;IAEA,eAAY;AACV,aAAO;IACT;IAEA,UAAU,UAA4C;AACpD,uBAAiB,KAAK,QAAQ;IAChC;;AAEJ;;;AD3JA,IAAM,kBAAkB;AAmBjB,SAAS,qBAAqB,SAA0C;AAC7E,QAAM,eAAe,wBAAwB;AAAA,IAC3C,WAAW,QAAQ;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,cAAc,QAAQ;AAAA,IACtB,UAAU;AAAA,EACZ,CAAC;AAED,QAAM,SAAS,IAAI,UAAU;AAAA,IAC3B,MAAM;AAAA,IACN,SAAS;AAAA,EACX,CAAC;AAED,QAAM,kBAA+C,CAAC;AAEtD,WAAS,aACP,MACA,aACA,QAEA,SACA;AACA,oBAAgB,IAAI,IAAI,EAAE,QAAQ;AAClC,WAAO,KAAK,MAAM,aAAa,QAAQ,OAAO;AAAA,EAChD;AAEA,iBAAe,YAAgC;AAC7C,UAAM,QAAQ,MAAM,aAAa,eAAe;AAChD,WAAO,IAAI,UAAU,KAAK;AAAA,EAC5B;AAIA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,MACE,OAAOC,GACJ,OAAO,EACP,SAAS,EACT,QAAQ,gBAAgB,EACxB,SAAS,4EAA4E;AAAA,MACxF,OAAOA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,GAAG,EAAE,SAAS,4BAA4B;AAAA,IACjF;AAAA,IACA,OAAO,SAA6C;AAClD,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO,cAAc,KAAK;AAAA,QAC7C,OAAO,KAAK,SAAS;AAAA,QACrB,OAAO,KAAK,SAAS;AAAA,MACvB,CAAC;AAED,YAAM,YAAY,OAAO,YAAY,CAAC,GAAG,IAAI,CAAC,QAAQ;AAAA,QACpD,IAAI,GAAG;AAAA,QACP,MAAM,GAAG;AAAA,QACT,OAAO,GAAG,OAAO;AAAA,QACjB,aAAa,GAAG;AAAA,MAClB,EAAE;AAEF,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AAEA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,MACE,WAAWA,GAAE,OAAO,EAAE,SAAS,kBAAkB;AAAA,MACjD,OAAOA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,SAAS,6BAA6B;AAAA,MAC/E,QAAQA,GAAE,OAAO,EAAE,SAAS,EAAE,SAAS,sCAAsC;AAAA,MAC7E,QAAQA,GAAE,OAAO,EAAE,SAAS,EAAE,SAAS,oCAAoC;AAAA,IAC7E;AAAA,IACA,OAAO,SAAkF;AACvF,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO,cAAc,QAAQ;AAAA,QAChD,SAAS,KAAK;AAAA,QACd,OAAO,KAAK,SAAS;AAAA,QACrB,QAAQ,KAAK;AAAA,QACb,QAAQ,KAAK;AAAA,MACf,CAAC;AAED,YAAM,YAAY,OAAO,YAAY,CAAC,GAAG,IAAI,CAAC,SAAS;AAAA,QACrD,IAAI,IAAI;AAAA,QACR,MAAM,IAAI;AAAA,QACV,MAAM,IAAI;AAAA,QACV,MAAM,IAAI;AAAA,MACZ,EAAE;AAEF,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC,EAAE,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AAIA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,MACE,OAAOA,GAAE,OAAO,EAAE,SAAS,cAAc;AAAA,MACzC,MAAMA,GAAE,KAAK,CAAC,SAAS,WAAW,CAAC,EAAE,SAAS,EAAE,QAAQ,OAAO,EAAE,SAAS,YAAY;AAAA,MACtF,OAAOA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,SAAS,mBAAmB;AAAA,IACvE;AAAA,IACA,OAAO,SAA0E;AAC/E,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO,OAAO,SAAS;AAAA,QAC1C,OAAO,KAAK;AAAA,QACZ,MAAM,KAAK,QAAQ;AAAA,QACnB,OAAO,KAAK,SAAS;AAAA,MACvB,CAAC;AAED,YAAM,UAAU,OAAO,UAAU,WAAW,CAAC;AAC7C,aAAO;AAAA,QACL,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU;AAAA,cACnB,SAAS,QAAQ,IAAI,CAAC,OAAO;AAAA,gBAC3B,IAAI,EAAE;AAAA,gBACN,MAAM,EAAE;AAAA,gBACR,SAAS,EAAE;AAAA,gBACX,MAAM,EAAE;AAAA,cACV,EAAE;AAAA,cACF,OAAO,OAAO,UAAU,SAAS;AAAA,YACnC,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAIA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,MACE,OAAOA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,GAAG,EAAE,SAAS,yBAAyB;AAAA,IAC9E;AAAA,IACA,OAAO,SAA6B;AAClC,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO,MAAM,KAAK;AAAA,QACrC,OAAO,KAAK,SAAS;AAAA,MACvB,CAAC;AAED,YAAM,WAAW,OAAO,WAAW,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,QACjD,IAAI,EAAE;AAAA,QACN,MAAM,EAAE;AAAA,QACR,WAAW,EAAE;AAAA,QACb,QAAQ,EAAE;AAAA,MACZ,EAAE;AAEF,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,KAAK,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,MACxE;AAAA,IACF;AAAA,EACF;AAEA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,MACE,QAAQA,GAAE,OAAO,EAAE,SAAS,eAAe;AAAA,IAC7C;AAAA,IACA,OAAO,SAA6B;AAClC,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO,MAAM,KAAK;AAAA,QACrC,MAAM,KAAK;AAAA,MACb,CAAC;AAED,YAAM,OAAO,OAAO;AACpB,aAAO;AAAA,QACL,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU;AAAA,cACnB,IAAI,MAAM;AAAA,cACV,MAAM,MAAM;AAAA,cACZ,WAAW,MAAM;AAAA,cACjB,SAAS,MAAM;AAAA,cACf,QAAQ,MAAM;AAAA,cACd,IAAI,MAAM;AAAA,YACZ,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,qBAAqB;AACnB,aAAO;AAAA,IACT;AAAA,IACA,MAAM,QAAQ;AACZ,YAAM,YAAY,IAAI,qBAAqB;AAC3C,YAAM,OAAO,QAAQ,SAAS;AAAA,IAChC;AAAA,EACF;AACF;AAGA,IAAM,eACJ,QAAQ,KAAK,CAAC,KAAK,YAAY,IAAI,SAAS,QAAQ,KAAK,CAAC,EAAE,QAAQ,OAAO,GAAG,CAAC;AACjF,IAAI,gBAAgB,QAAQ,IAAI,oBAAoB,QAAQ;AAC1D,QAAM,YAAY,QAAQ,IAAI;AAC9B,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,eAAe,QAAQ,IAAI;AAEjC,MAAI,CAAC,aAAa,CAAC,YAAY,CAAC,cAAc;AAC5C,YAAQ;AAAA,MACN;AAAA,IACF;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,YAAY,qBAAqB,EAAE,WAAW,UAAU,aAAa,CAAC;AAC5E,YAAU,MAAM,EAAE,MAAM,CAAC,QAAQ;AAC/B,YAAQ,MAAM,+BAA+B,GAAG;AAChD,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":["z","z"]}
1
+ {"version":3,"sources":["../src/index.ts","../../shared/src/mcp-auth/forge-token-manager.ts"],"sourcesContent":["/**\n * @goforgeit/mcp-slack — MCP server for Slack\n *\n * Provides channel, message, and user tools via stdio transport.\n * Reads Forge's OAuth tokens and uses Slack Web API.\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport { WebClient } from '@slack/web-api';\nimport { createForgeTokenManager } from '@forge/shared/mcp-auth';\n\nconst SLACK_TOKEN_URL = 'https://slack.com/api/oauth.v2.access';\n\nexport interface SlackMCPOptions {\n tokenPath: string;\n clientId: string;\n clientSecret: string;\n}\n\ninterface ToolHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;\n}\n\ninterface SlackMCPServer {\n server: McpServer;\n getRegisteredTools(): Record<string, ToolHandler>;\n start(): Promise<void>;\n}\n\nexport function createSlackMCPServer(options: SlackMCPOptions): SlackMCPServer {\n const tokenManager = createForgeTokenManager({\n tokenPath: options.tokenPath,\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n tokenUrl: SLACK_TOKEN_URL,\n });\n\n const server = new McpServer({\n name: '@goforgeit/mcp-slack',\n version: '0.1.0',\n });\n\n const registeredTools: Record<string, ToolHandler> = {};\n\n function registerTool(\n name: string,\n description: string,\n schema: Record<string, z.ZodType>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>,\n ) {\n registeredTools[name] = { handler };\n server.tool(name, description, schema, handler);\n }\n\n async function getClient(): Promise<WebClient> {\n const token = await tokenManager.getAccessToken();\n return new WebClient(token);\n }\n\n /** Get a client using the user token (needed for search operations) */\n async function getUserClient(): Promise<WebClient> {\n const userToken = await tokenManager.getUserAccessToken();\n if (userToken) {\n return new WebClient(userToken);\n }\n // Fall back to bot token (search will fail but other operations work)\n return getClient();\n }\n\n // --- Channel Tools ---\n\n registerTool(\n 'list_channels',\n 'List Slack channels the bot has access to',\n {\n types: z\n .string()\n .optional()\n .default('public_channel')\n .describe('Channel types (comma-separated: public_channel, private_channel, mpim, im)'),\n limit: z.number().optional().default(100).describe('Maximum channels to return'),\n },\n async (args: { types?: string; limit?: number }) => {\n const client = await getClient();\n const result = await client.conversations.list({\n types: args.types || 'public_channel',\n limit: args.limit || 100,\n });\n\n const channels = (result.channels || []).map((ch) => ({\n id: ch.id,\n name: ch.name,\n topic: ch.topic?.value,\n num_members: ch.num_members,\n }));\n\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ channels }) }],\n };\n },\n );\n\n registerTool(\n 'get_channel_history',\n 'Get message history from a Slack channel',\n {\n channelId: z.string().describe('Slack channel ID'),\n limit: z.number().optional().default(20).describe('Number of messages to fetch'),\n oldest: z.string().optional().describe('Start of time range (Unix timestamp)'),\n latest: z.string().optional().describe('End of time range (Unix timestamp)'),\n },\n async (args: { channelId: string; limit?: number; oldest?: string; latest?: string }) => {\n const client = await getClient();\n const result = await client.conversations.history({\n channel: args.channelId,\n limit: args.limit || 20,\n oldest: args.oldest,\n latest: args.latest,\n });\n\n const messages = (result.messages || []).map((msg) => ({\n ts: msg.ts,\n text: msg.text,\n user: msg.user,\n type: msg.type,\n }));\n\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ messages }) }],\n };\n },\n );\n\n // --- Message Tools ---\n\n registerTool(\n 'search_messages',\n 'Search Slack messages across all channels',\n {\n query: z.string().describe('Search query'),\n sort: z.enum(['score', 'timestamp']).optional().default('score').describe('Sort order'),\n count: z.number().optional().default(20).describe('Number of results'),\n },\n async (args: { query: string; sort?: 'score' | 'timestamp'; count?: number }) => {\n const client = await getUserClient();\n const result = await client.search.messages({\n query: args.query,\n sort: args.sort || 'score',\n count: args.count || 20,\n });\n\n const matches = result.messages?.matches || [];\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n matches: matches.map((m) => ({\n ts: m.ts,\n text: m.text,\n channel: m.channel,\n user: m.user,\n })),\n total: result.messages?.total || 0,\n }),\n },\n ],\n };\n },\n );\n\n // --- User Tools ---\n\n registerTool(\n 'list_users',\n 'List Slack workspace members',\n {\n limit: z.number().optional().default(100).describe('Maximum users to return'),\n },\n async (args: { limit?: number }) => {\n const client = await getClient();\n const result = await client.users.list({\n limit: args.limit || 100,\n });\n\n const members = (result.members || []).map((m) => ({\n id: m.id,\n name: m.name,\n real_name: m.real_name,\n is_bot: m.is_bot,\n }));\n\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ members }) }],\n };\n },\n );\n\n registerTool(\n 'get_user_info',\n 'Get detailed information about a Slack user',\n {\n userId: z.string().describe('Slack user ID'),\n },\n async (args: { userId: string }) => {\n const client = await getClient();\n const result = await client.users.info({\n user: args.userId,\n });\n\n const user = result.user;\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n id: user?.id,\n name: user?.name,\n real_name: user?.real_name,\n profile: user?.profile,\n is_bot: user?.is_bot,\n tz: user?.tz,\n }),\n },\n ],\n };\n },\n );\n\n return {\n server,\n getRegisteredTools() {\n return registeredTools;\n },\n async start() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n },\n };\n}\n\n// CLI entry point\nconst isMainModule =\n process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\\\/g, '/'));\nif (isMainModule || process.env.FORGE_MCP_START === 'true') {\n const tokenPath = process.env.FORGE_TOKEN_PATH;\n const clientId = process.env.FORGE_CLIENT_ID;\n const clientSecret = process.env.FORGE_CLIENT_SECRET;\n\n if (!tokenPath || !clientId || !clientSecret) {\n console.error(\n 'Required environment variables: FORGE_TOKEN_PATH, FORGE_CLIENT_ID, FORGE_CLIENT_SECRET',\n );\n process.exit(1);\n }\n\n const mcpServer = createSlackMCPServer({ tokenPath, clientId, clientSecret });\n mcpServer.start().catch((err) => {\n console.error('Failed to start MCP server:', err);\n process.exit(1);\n });\n}\n","/**\n * Forge Token Manager — reads Forge's OAuthTokens format and handles refresh.\n *\n * Used by @goforgeit/mcp-* packages to get valid access tokens for API calls.\n * Reads from FORGE_TOKEN_PATH, validates with Zod, and refreshes when\n * the token is expired or within 5 minutes of expiry.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\n/** Zod schema for Forge's OAuth token file format */\nexport const ForgeOAuthTokensSchema = z.object({\n accessToken: z.string(),\n refreshToken: z.string().optional(),\n expiresAt: z.number(),\n providerId: z.string(),\n connectedAt: z.string(),\n scope: z.string().optional(),\n email: z.string().optional(),\n accountId: z.string().optional(),\n /** User-level access token (Slack xoxp-* token for search operations) */\n userAccessToken: z.string().optional(),\n});\n\nexport type ForgeOAuthTokens = z.infer<typeof ForgeOAuthTokensSchema>;\n\nexport interface ForgeTokenManagerOptions {\n /** Path to the Forge OAuth token JSON file */\n tokenPath: string;\n /** OAuth client ID for token refresh */\n clientId: string;\n /** OAuth client secret for token refresh */\n clientSecret: string;\n /** OAuth token endpoint URL for refresh requests */\n tokenUrl: string;\n /** Buffer in ms before expiry to trigger refresh (default: 5 minutes) */\n refreshBufferMs?: number;\n}\n\nexport interface ForgeTokenManager {\n /** Returns a valid access token, refreshing if needed */\n getAccessToken(): Promise<string>;\n /** Returns the user-level access token if available (e.g., Slack xoxp-* for search) */\n getUserAccessToken(): Promise<string | null>;\n /** Returns the current token data, or null if not yet loaded */\n getTokenInfo(): ForgeOAuthTokens | null;\n /** Register a callback for when tokens are refreshed */\n onRefresh(callback: (tokens: ForgeOAuthTokens) => void): void;\n}\n\nconst DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes\n\nexport function createForgeTokenManager(options: ForgeTokenManagerOptions): ForgeTokenManager {\n const {\n tokenPath,\n clientId,\n clientSecret,\n tokenUrl,\n refreshBufferMs = DEFAULT_REFRESH_BUFFER_MS,\n } = options;\n\n let cachedTokens: ForgeOAuthTokens | null = null;\n const refreshCallbacks: Array<(tokens: ForgeOAuthTokens) => void> = [];\n\n function isExpiredOrNearExpiry(tokens: ForgeOAuthTokens): boolean {\n return Date.now() >= tokens.expiresAt - refreshBufferMs;\n }\n\n async function readTokenFile(): Promise<ForgeOAuthTokens> {\n let raw: string;\n try {\n raw = await fs.readFile(tokenPath, 'utf-8');\n } catch (err: unknown) {\n if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {\n throw new Error(`Token file not found: ${tokenPath}`);\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(`Invalid JSON in token file: ${tokenPath}`);\n }\n\n const result = ForgeOAuthTokensSchema.safeParse(parsed);\n if (!result.success) {\n throw new Error(`Invalid token file format: ${result.error.message}`);\n }\n\n return result.data;\n }\n\n async function refreshToken(tokens: ForgeOAuthTokens): Promise<ForgeOAuthTokens> {\n if (!tokens.refreshToken) {\n throw new Error('No refresh token available — cannot refresh expired token');\n }\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: tokens.refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n });\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = await response.json();\n\n const refreshedTokens: ForgeOAuthTokens = {\n ...tokens,\n accessToken: data.access_token,\n refreshToken: data.refresh_token || tokens.refreshToken,\n expiresAt: Date.now() + data.expires_in * 1000,\n };\n\n // Write refreshed tokens back to file\n await fs.mkdir(path.dirname(tokenPath), { recursive: true });\n await fs.writeFile(tokenPath, JSON.stringify(refreshedTokens), { mode: 0o600 });\n\n // Notify listeners\n for (const cb of refreshCallbacks) {\n cb(refreshedTokens);\n }\n\n return refreshedTokens;\n }\n\n return {\n async getAccessToken(): Promise<string> {\n if (cachedTokens && !isExpiredOrNearExpiry(cachedTokens)) {\n return cachedTokens.accessToken;\n }\n\n const tokens = cachedTokens || (await readTokenFile());\n\n if (isExpiredOrNearExpiry(tokens)) {\n if (tokens.refreshToken) {\n cachedTokens = await refreshToken(tokens);\n } else {\n // No refresh token — return the access token as-is.\n // Some providers (e.g. Slack bot tokens) issue non-expiring tokens\n // without refresh tokens, so the expiresAt may be unreliable.\n cachedTokens = tokens;\n }\n } else {\n cachedTokens = tokens;\n }\n\n return cachedTokens.accessToken;\n },\n\n async getUserAccessToken(): Promise<string | null> {\n // Ensure tokens are loaded\n if (!cachedTokens) {\n await this.getAccessToken();\n }\n return cachedTokens?.userAccessToken ?? null;\n },\n\n getTokenInfo(): ForgeOAuthTokens | null {\n return cachedTokens;\n },\n\n onRefresh(callback: (tokens: ForgeOAuthTokens) => void): void {\n refreshCallbacks.push(callback);\n },\n };\n}\n\n/**\n * Create a ForgeTokenManager from standard environment variables.\n *\n * Expected env vars:\n * - FORGE_TOKEN_PATH: path to the OAuth token JSON file\n * - FORGE_CLIENT_ID: OAuth client ID\n * - FORGE_CLIENT_SECRET: OAuth client secret\n *\n * @param tokenUrl The provider's token endpoint URL\n */\nexport function createForgeTokenManagerFromEnv(tokenUrl: string): ForgeTokenManager {\n const tokenPath = process.env.FORGE_TOKEN_PATH;\n const clientId = process.env.FORGE_CLIENT_ID;\n const clientSecret = process.env.FORGE_CLIENT_SECRET;\n\n if (!tokenPath) throw new Error('FORGE_TOKEN_PATH environment variable is required');\n if (!clientId) throw new Error('FORGE_CLIENT_ID environment variable is required');\n if (!clientSecret) throw new Error('FORGE_CLIENT_SECRET environment variable is required');\n\n return createForgeTokenManager({ tokenPath, clientId, clientSecret, tokenUrl });\n}\n"],"mappings":"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","names":["z","z"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goforgeit/mcp-slack",
3
- "version": "0.4.11",
3
+ "version": "0.4.19",
4
4
  "type": "module",
5
5
  "description": "Forge MCP server for Slack (channels, messages, users)",
6
6
  "bin": {
@@ -8,25 +8,18 @@
8
8
  },
9
9
  "main": "dist/index.js",
10
10
  "types": "dist/index.d.ts",
11
- "publishConfig": {
12
- "access": "public"
13
- },
14
- "files": [
15
- "dist",
16
- "README.md"
17
- ],
18
11
  "scripts": {
19
12
  "build": "tsup",
20
13
  "typecheck": "tsc --noEmit",
21
14
  "test": "vitest run"
22
15
  },
23
16
  "dependencies": {
17
+ "@forge/shared": "workspace:*",
24
18
  "@modelcontextprotocol/sdk": "^1.12.1",
25
19
  "@slack/web-api": "^7.0.0",
26
20
  "zod": "^4.2.1"
27
21
  },
28
22
  "devDependencies": {
29
- "@forge/shared": "workspace:*",
30
23
  "@types/node": "^22.10.2",
31
24
  "tsup": "^8.0.0",
32
25
  "vitest": "^3.2.4"
@@ -0,0 +1,185 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createSlackMCPServer } from './index';
3
+
4
+ vi.mock('@forge/shared/mcp-auth', () => ({
5
+ createForgeTokenManager: () => ({
6
+ getAccessToken: vi.fn().mockResolvedValue('xoxb-mock-token'),
7
+ getUserAccessToken: vi.fn().mockResolvedValue('xoxp-mock-user-token'),
8
+ getTokenInfo: () => null,
9
+ onRefresh: vi.fn(),
10
+ }),
11
+ }));
12
+
13
+ const mockConversationsList = vi.fn();
14
+ const mockConversationsHistory = vi.fn();
15
+ const mockSearchMessages = vi.fn();
16
+ const mockUsersList = vi.fn();
17
+ const mockUsersInfo = vi.fn();
18
+ const mockChatPostMessage = vi.fn();
19
+
20
+ vi.mock('@slack/web-api', () => ({
21
+ WebClient: vi.fn().mockImplementation(() => ({
22
+ conversations: {
23
+ list: mockConversationsList,
24
+ history: mockConversationsHistory,
25
+ },
26
+ search: {
27
+ messages: mockSearchMessages,
28
+ },
29
+ users: {
30
+ list: mockUsersList,
31
+ info: mockUsersInfo,
32
+ },
33
+ chat: {
34
+ postMessage: mockChatPostMessage,
35
+ },
36
+ })),
37
+ }));
38
+
39
+ describe('createSlackMCPServer', () => {
40
+ beforeEach(() => vi.clearAllMocks());
41
+
42
+ it('creates a server with registered tools', () => {
43
+ const server = createSlackMCPServer({
44
+ tokenPath: '/tmp/slack.json',
45
+ clientId: 'id',
46
+ clientSecret: 'secret',
47
+ });
48
+ expect(server).toBeDefined();
49
+ const tools = server.getRegisteredTools();
50
+ expect(tools).toHaveProperty('list_channels');
51
+ expect(tools).toHaveProperty('get_channel_history');
52
+ expect(tools).toHaveProperty('search_messages');
53
+ expect(tools).toHaveProperty('list_users');
54
+ expect(tools).toHaveProperty('get_user_info');
55
+ });
56
+ });
57
+
58
+ describe('Slack Channel Tools', () => {
59
+ beforeEach(() => vi.clearAllMocks());
60
+
61
+ it('list_channels returns channels', async () => {
62
+ mockConversationsList.mockResolvedValue({
63
+ ok: true,
64
+ channels: [
65
+ { id: 'C001', name: 'general', topic: { value: 'General chat' }, num_members: 50 },
66
+ { id: 'C002', name: 'random', topic: { value: '' }, num_members: 45 },
67
+ ],
68
+ });
69
+
70
+ const server = createSlackMCPServer({
71
+ tokenPath: '/tmp/slack.json',
72
+ clientId: 'id',
73
+ clientSecret: 'secret',
74
+ });
75
+ const tools = server.getRegisteredTools();
76
+ const result = await tools.list_channels.handler({ limit: 100 });
77
+ const parsed = JSON.parse(result.content[0].text);
78
+ expect(parsed.channels).toHaveLength(2);
79
+ expect(parsed.channels[0].name).toBe('general');
80
+ });
81
+
82
+ it('get_channel_history returns messages', async () => {
83
+ mockConversationsHistory.mockResolvedValue({
84
+ ok: true,
85
+ messages: [
86
+ { ts: '1234567890.123', text: 'Hello world', user: 'U001' },
87
+ { ts: '1234567891.456', text: 'Hi there', user: 'U002' },
88
+ ],
89
+ });
90
+
91
+ const server = createSlackMCPServer({
92
+ tokenPath: '/tmp/slack.json',
93
+ clientId: 'id',
94
+ clientSecret: 'secret',
95
+ });
96
+ const tools = server.getRegisteredTools();
97
+ const result = await tools.get_channel_history.handler({
98
+ channelId: 'C001',
99
+ limit: 10,
100
+ });
101
+ const parsed = JSON.parse(result.content[0].text);
102
+ expect(parsed.messages).toHaveLength(2);
103
+ });
104
+ });
105
+
106
+ describe('Slack Message Tools', () => {
107
+ beforeEach(() => vi.clearAllMocks());
108
+
109
+ it('search_messages returns search results', async () => {
110
+ mockSearchMessages.mockResolvedValue({
111
+ ok: true,
112
+ messages: {
113
+ matches: [
114
+ {
115
+ ts: '123',
116
+ text: 'Found message',
117
+ channel: { id: 'C001', name: 'general' },
118
+ user: 'U001',
119
+ },
120
+ ],
121
+ total: 1,
122
+ },
123
+ });
124
+
125
+ const server = createSlackMCPServer({
126
+ tokenPath: '/tmp/slack.json',
127
+ clientId: 'id',
128
+ clientSecret: 'secret',
129
+ });
130
+ const tools = server.getRegisteredTools();
131
+ const result = await tools.search_messages.handler({ query: 'test', count: 10 });
132
+ const parsed = JSON.parse(result.content[0].text);
133
+ expect(parsed.matches).toHaveLength(1);
134
+ expect(parsed.total).toBe(1);
135
+ });
136
+ });
137
+
138
+ describe('Slack User Tools', () => {
139
+ beforeEach(() => vi.clearAllMocks());
140
+
141
+ it('list_users returns user list', async () => {
142
+ mockUsersList.mockResolvedValue({
143
+ ok: true,
144
+ members: [
145
+ { id: 'U001', name: 'alice', real_name: 'Alice Smith', is_bot: false },
146
+ { id: 'U002', name: 'bob', real_name: 'Bob Jones', is_bot: false },
147
+ ],
148
+ });
149
+
150
+ const server = createSlackMCPServer({
151
+ tokenPath: '/tmp/slack.json',
152
+ clientId: 'id',
153
+ clientSecret: 'secret',
154
+ });
155
+ const tools = server.getRegisteredTools();
156
+ const result = await tools.list_users.handler({ limit: 100 });
157
+ const parsed = JSON.parse(result.content[0].text);
158
+ expect(parsed.members).toHaveLength(2);
159
+ });
160
+
161
+ it('get_user_info returns user details', async () => {
162
+ mockUsersInfo.mockResolvedValue({
163
+ ok: true,
164
+ user: {
165
+ id: 'U001',
166
+ name: 'alice',
167
+ real_name: 'Alice Smith',
168
+ profile: { email: 'alice@example.com', title: 'Engineer' },
169
+ is_bot: false,
170
+ tz: 'America/Los_Angeles',
171
+ },
172
+ });
173
+
174
+ const server = createSlackMCPServer({
175
+ tokenPath: '/tmp/slack.json',
176
+ clientId: 'id',
177
+ clientSecret: 'secret',
178
+ });
179
+ const tools = server.getRegisteredTools();
180
+ const result = await tools.get_user_info.handler({ userId: 'U001' });
181
+ const parsed = JSON.parse(result.content[0].text);
182
+ expect(parsed.id).toBe('U001');
183
+ expect(parsed.real_name).toBe('Alice Smith');
184
+ });
185
+ });
package/src/index.ts ADDED
@@ -0,0 +1,266 @@
1
+ /**
2
+ * @goforgeit/mcp-slack — MCP server for Slack
3
+ *
4
+ * Provides channel, message, and user tools via stdio transport.
5
+ * Reads Forge's OAuth tokens and uses Slack Web API.
6
+ */
7
+
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { z } from 'zod';
11
+ import { WebClient } from '@slack/web-api';
12
+ import { createForgeTokenManager } from '@forge/shared/mcp-auth';
13
+
14
+ const SLACK_TOKEN_URL = 'https://slack.com/api/oauth.v2.access';
15
+
16
+ export interface SlackMCPOptions {
17
+ tokenPath: string;
18
+ clientId: string;
19
+ clientSecret: string;
20
+ }
21
+
22
+ interface ToolHandler {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;
25
+ }
26
+
27
+ interface SlackMCPServer {
28
+ server: McpServer;
29
+ getRegisteredTools(): Record<string, ToolHandler>;
30
+ start(): Promise<void>;
31
+ }
32
+
33
+ export function createSlackMCPServer(options: SlackMCPOptions): SlackMCPServer {
34
+ const tokenManager = createForgeTokenManager({
35
+ tokenPath: options.tokenPath,
36
+ clientId: options.clientId,
37
+ clientSecret: options.clientSecret,
38
+ tokenUrl: SLACK_TOKEN_URL,
39
+ });
40
+
41
+ const server = new McpServer({
42
+ name: '@goforgeit/mcp-slack',
43
+ version: '0.1.0',
44
+ });
45
+
46
+ const registeredTools: Record<string, ToolHandler> = {};
47
+
48
+ function registerTool(
49
+ name: string,
50
+ description: string,
51
+ schema: Record<string, z.ZodType>,
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>,
54
+ ) {
55
+ registeredTools[name] = { handler };
56
+ server.tool(name, description, schema, handler);
57
+ }
58
+
59
+ async function getClient(): Promise<WebClient> {
60
+ const token = await tokenManager.getAccessToken();
61
+ return new WebClient(token);
62
+ }
63
+
64
+ /** Get a client using the user token (needed for search operations) */
65
+ async function getUserClient(): Promise<WebClient> {
66
+ const userToken = await tokenManager.getUserAccessToken();
67
+ if (userToken) {
68
+ return new WebClient(userToken);
69
+ }
70
+ // Fall back to bot token (search will fail but other operations work)
71
+ return getClient();
72
+ }
73
+
74
+ // --- Channel Tools ---
75
+
76
+ registerTool(
77
+ 'list_channels',
78
+ 'List Slack channels the bot has access to',
79
+ {
80
+ types: z
81
+ .string()
82
+ .optional()
83
+ .default('public_channel')
84
+ .describe('Channel types (comma-separated: public_channel, private_channel, mpim, im)'),
85
+ limit: z.number().optional().default(100).describe('Maximum channels to return'),
86
+ },
87
+ async (args: { types?: string; limit?: number }) => {
88
+ const client = await getClient();
89
+ const result = await client.conversations.list({
90
+ types: args.types || 'public_channel',
91
+ limit: args.limit || 100,
92
+ });
93
+
94
+ const channels = (result.channels || []).map((ch) => ({
95
+ id: ch.id,
96
+ name: ch.name,
97
+ topic: ch.topic?.value,
98
+ num_members: ch.num_members,
99
+ }));
100
+
101
+ return {
102
+ content: [{ type: 'text' as const, text: JSON.stringify({ channels }) }],
103
+ };
104
+ },
105
+ );
106
+
107
+ registerTool(
108
+ 'get_channel_history',
109
+ 'Get message history from a Slack channel',
110
+ {
111
+ channelId: z.string().describe('Slack channel ID'),
112
+ limit: z.number().optional().default(20).describe('Number of messages to fetch'),
113
+ oldest: z.string().optional().describe('Start of time range (Unix timestamp)'),
114
+ latest: z.string().optional().describe('End of time range (Unix timestamp)'),
115
+ },
116
+ async (args: { channelId: string; limit?: number; oldest?: string; latest?: string }) => {
117
+ const client = await getClient();
118
+ const result = await client.conversations.history({
119
+ channel: args.channelId,
120
+ limit: args.limit || 20,
121
+ oldest: args.oldest,
122
+ latest: args.latest,
123
+ });
124
+
125
+ const messages = (result.messages || []).map((msg) => ({
126
+ ts: msg.ts,
127
+ text: msg.text,
128
+ user: msg.user,
129
+ type: msg.type,
130
+ }));
131
+
132
+ return {
133
+ content: [{ type: 'text' as const, text: JSON.stringify({ messages }) }],
134
+ };
135
+ },
136
+ );
137
+
138
+ // --- Message Tools ---
139
+
140
+ registerTool(
141
+ 'search_messages',
142
+ 'Search Slack messages across all channels',
143
+ {
144
+ query: z.string().describe('Search query'),
145
+ sort: z.enum(['score', 'timestamp']).optional().default('score').describe('Sort order'),
146
+ count: z.number().optional().default(20).describe('Number of results'),
147
+ },
148
+ async (args: { query: string; sort?: 'score' | 'timestamp'; count?: number }) => {
149
+ const client = await getUserClient();
150
+ const result = await client.search.messages({
151
+ query: args.query,
152
+ sort: args.sort || 'score',
153
+ count: args.count || 20,
154
+ });
155
+
156
+ const matches = result.messages?.matches || [];
157
+ return {
158
+ content: [
159
+ {
160
+ type: 'text' as const,
161
+ text: JSON.stringify({
162
+ matches: matches.map((m) => ({
163
+ ts: m.ts,
164
+ text: m.text,
165
+ channel: m.channel,
166
+ user: m.user,
167
+ })),
168
+ total: result.messages?.total || 0,
169
+ }),
170
+ },
171
+ ],
172
+ };
173
+ },
174
+ );
175
+
176
+ // --- User Tools ---
177
+
178
+ registerTool(
179
+ 'list_users',
180
+ 'List Slack workspace members',
181
+ {
182
+ limit: z.number().optional().default(100).describe('Maximum users to return'),
183
+ },
184
+ async (args: { limit?: number }) => {
185
+ const client = await getClient();
186
+ const result = await client.users.list({
187
+ limit: args.limit || 100,
188
+ });
189
+
190
+ const members = (result.members || []).map((m) => ({
191
+ id: m.id,
192
+ name: m.name,
193
+ real_name: m.real_name,
194
+ is_bot: m.is_bot,
195
+ }));
196
+
197
+ return {
198
+ content: [{ type: 'text' as const, text: JSON.stringify({ members }) }],
199
+ };
200
+ },
201
+ );
202
+
203
+ registerTool(
204
+ 'get_user_info',
205
+ 'Get detailed information about a Slack user',
206
+ {
207
+ userId: z.string().describe('Slack user ID'),
208
+ },
209
+ async (args: { userId: string }) => {
210
+ const client = await getClient();
211
+ const result = await client.users.info({
212
+ user: args.userId,
213
+ });
214
+
215
+ const user = result.user;
216
+ return {
217
+ content: [
218
+ {
219
+ type: 'text' as const,
220
+ text: JSON.stringify({
221
+ id: user?.id,
222
+ name: user?.name,
223
+ real_name: user?.real_name,
224
+ profile: user?.profile,
225
+ is_bot: user?.is_bot,
226
+ tz: user?.tz,
227
+ }),
228
+ },
229
+ ],
230
+ };
231
+ },
232
+ );
233
+
234
+ return {
235
+ server,
236
+ getRegisteredTools() {
237
+ return registeredTools;
238
+ },
239
+ async start() {
240
+ const transport = new StdioServerTransport();
241
+ await server.connect(transport);
242
+ },
243
+ };
244
+ }
245
+
246
+ // CLI entry point
247
+ const isMainModule =
248
+ process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
249
+ if (isMainModule || process.env.FORGE_MCP_START === 'true') {
250
+ const tokenPath = process.env.FORGE_TOKEN_PATH;
251
+ const clientId = process.env.FORGE_CLIENT_ID;
252
+ const clientSecret = process.env.FORGE_CLIENT_SECRET;
253
+
254
+ if (!tokenPath || !clientId || !clientSecret) {
255
+ console.error(
256
+ 'Required environment variables: FORGE_TOKEN_PATH, FORGE_CLIENT_ID, FORGE_CLIENT_SECRET',
257
+ );
258
+ process.exit(1);
259
+ }
260
+
261
+ const mcpServer = createSlackMCPServer({ tokenPath, clientId, clientSecret });
262
+ mcpServer.start().catch((err) => {
263
+ console.error('Failed to start MCP server:', err);
264
+ process.exit(1);
265
+ });
266
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ target: 'node20',
7
+ outDir: 'dist',
8
+ clean: true,
9
+ dts: true,
10
+ sourcemap: true,
11
+ noExternal: ['@forge/shared'],
12
+ banner: {
13
+ js: '#!/usr/bin/env node',
14
+ },
15
+ });