import { Effect, Encoding, Option, Schema } from "effect"; import { HttpClient, HttpClientRequest } from "effect/unstable/http"; const SLACK_MCP_HOST = "mcp.slack.com"; const SLACK_MCP_PATH = "/mcp"; const SLACK_FILE_INFO_URL = "https://slack.com/api/files.info"; const SLACK_FILE_HOST = "files.slack.com"; const MAX_SLACK_FILE_BYTES = 10 * 1024 * 1024; const SlackReadFileArgs = Schema.Struct({ file_id: Schema.String }); const decodeSlackReadFileArgs = Schema.decodeUnknownOption(SlackReadFileArgs); const SlackFileInfo = Schema.Struct({ id: Schema.String, name: Schema.optional(Schema.String), title: Schema.optional(Schema.String), mimetype: Schema.String, size: Schema.Number, url_private: Schema.optional(Schema.String), url_private_download: Schema.optional(Schema.String), }); const SlackFileInfoSuccess = Schema.Struct({ ok: Schema.Literal(true), file: SlackFileInfo, }); const decodeSlackFileInfoSuccess = Schema.decodeUnknownOption(SlackFileInfoSuccess); const parseUrl = Option.liftThrowable((value: string) => new URL(value)); const isSlackMcpEndpoint = (value: string): boolean => Option.match(parseUrl(value), { onNone: () => false, onSome: (url) => url.protocol === "https:" && url.hostname === SLACK_MCP_HOST && url.pathname.replace(/\/$/, "") === SLACK_MCP_PATH, }); const trustedSlackFileUrl = (value: string): URL | null => Option.match(parseUrl(value), { onNone: () => null, onSome: (url) => (url.protocol === "https:" && url.hostname === SLACK_FILE_HOST ? url : null), }); const bearerHeaders = (accessToken: string): Readonly> => ({ Authorization: `Bearer ${accessToken}`, }); const safeTitle = (value: string): string => value.replace(/[\r\n]/g, " "); type SlackImageToolResult = { readonly content: readonly [ { readonly type: "text"; readonly text: string }, { readonly type: "image"; readonly data: string; readonly mimeType: string }, ]; }; interface RecoverSlackConnectFileInput { readonly endpoint: string; readonly toolName: string; readonly args: unknown; readonly accessToken: string | null; readonly upstreamErrorMessage: string; } /** * Recovers Slack Connect images that Slack's hosted MCP server reports as * `file_not_found` by resolving the file through Slack's Web API with the same * user OAuth grant. Returns `None` for non-Slack calls and for any fallback * failure so the caller can preserve the original upstream error. */ export const recoverSlackConnectFile = ( input: RecoverSlackConnectFileInput, ): Effect.Effect, never, HttpClient.HttpClient> => { if ( input.toolName !== "slack_read_file" || !input.upstreamErrorMessage.includes("file_not_found") || !isSlackMcpEndpoint(input.endpoint) || input.accessToken === null || input.accessToken.length === 0 ) { return Effect.succeed(Option.none()); } const accessToken = input.accessToken; const args = Option.getOrUndefined(decodeSlackReadFileArgs(input.args)); const fileId = args?.file_id.trim(); if (fileId === undefined || !/^F[A-Z0-9]+$/.test(fileId)) { return Effect.succeed(Option.none()); } return Effect.gen(function* () { const client = yield* HttpClient.HttpClient; const infoUrl = new URL(SLACK_FILE_INFO_URL); infoUrl.searchParams.set("file", fileId); const infoResponse = yield* client.execute( HttpClientRequest.get(infoUrl, { headers: bearerHeaders(accessToken) }), ); if (infoResponse.status < 200 || infoResponse.status >= 300) return Option.none(); const info = Option.getOrUndefined(decodeSlackFileInfoSuccess(yield* infoResponse.json)); if ( info === undefined || !info.file.mimetype.startsWith("image/") || !Number.isSafeInteger(info.file.size) || info.file.size < 0 || info.file.size > MAX_SLACK_FILE_BYTES ) { return Option.none(); } const downloadUrl = trustedSlackFileUrl( info.file.url_private_download ?? info.file.url_private ?? "", ); if (downloadUrl === null) return Option.none(); const downloadResponse = yield* client.execute( HttpClientRequest.get(downloadUrl, { headers: bearerHeaders(accessToken) }), ); if (downloadResponse.status < 200 || downloadResponse.status >= 300) return Option.none(); const responseMimeType = downloadResponse.headers["content-type"] ?.split(";", 1)[0] ?.trim() .toLowerCase(); if (responseMimeType !== undefined && !responseMimeType.startsWith("image/")) { return Option.none(); } const bytes = new Uint8Array(yield* downloadResponse.arrayBuffer); if (bytes.byteLength > MAX_SLACK_FILE_BYTES) return Option.none(); const title = safeTitle(info.file.title ?? info.file.name ?? info.file.id); const recovered: SlackImageToolResult = { content: [ { type: "text", text: `File ID: ${info.file.id}\nTitle: ${title}\nMIME Type: ${info.file.mimetype}\nSize: ${info.file.size} bytes\n`, }, { type: "image", data: Encoding.encodeBase64(bytes), mimeType: info.file.mimetype, }, ], }; return Option.some(recovered); }).pipe( Effect.catch(() => Effect.succeed(Option.none())), Effect.withSpan("plugin.mcp.slack_connect_file_fallback"), ); };