@rokrokss/claude-slack-channel 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -1,6 +1,48 @@
1
1
  # Claude Slack Channel
2
2
 
3
- Claude Code 세션과 Slack을 양방향으로 연결합니다. Slack DM이나 채널 멘션으로 Claude Code와 대화할 수 있습니다.
3
+ Claude Code 세션과 Slack을 양방향으로 연결하는 MCP 채널 서버입니다. Slack DM이나 채널 멘션으로 Claude Code와 대화할 수 있습니다.
4
+
5
+ ## 설치
6
+
7
+ 프로젝트 `.mcp.json` 또는 `~/.claude.json`에 추가합니다:
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "slack-channel": {
13
+ "command": "bunx",
14
+ "args": ["@rokrokss/claude-slack-channel"],
15
+ "env": {
16
+ "SLACK_BOT_TOKEN": "xoxb-...",
17
+ "SLACK_APP_TOKEN": "xapp-...",
18
+ "SLACK_ALLOW_FROM": "U123,U456",
19
+ "SLACK_WORKSPACE": "your-workspace",
20
+ "SLACK_ACK_REACTION": "eyes"
21
+ }
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ | 환경변수 | 필수 | 설명 |
28
+ |---|---|---|
29
+ | `SLACK_BOT_TOKEN` | O | Bot User OAuth Token (`xoxb-...`) |
30
+ | `SLACK_APP_TOKEN` | O | App-Level Token (`xapp-...`) |
31
+ | `SLACK_ALLOW_FROM` | O | 허용할 Slack 사용자 ID (쉼표 구분) |
32
+ | `SLACK_WORKSPACE` | O | Slack 워크스페이스 서브도메인 (예: `msuniverse`). permalink 생성에 사용 |
33
+ | `SLACK_BOT_OWNER` | - | 봇 소유자 사용자 ID. allowlist 없이도 항상 허용 |
34
+ | `SLACK_ACK_REACTION` | - | 수신 확인 이모지 (예: `eyes`). 답장 후 자동 제거 |
35
+ | `SLACK_DEFAULT_COLOR` | - | 메시지 사이드바 색상 hex (기본: `#e5da9a`) |
36
+ | `SLACK_SHOW_FOOTER` | - | 메시지 하단 footer 표시 여부 (기본: `true`). `false`로 설정 시 숨김 |
37
+ | `SLACK_FORCE_SOCKET_MODE` | - | `1`로 설정 시 플래그 감지를 건너뛰고 Socket Mode 강제 연결 |
38
+
39
+ ## 실행
40
+
41
+ ```bash
42
+ claude --dangerously-load-development-channels server:slack-channel
43
+ ```
44
+
45
+ 허용된 사용자는 DM이든 채널이든 어디서든 봇과 대화할 수 있습니다.
4
46
 
5
47
  ## 아키텍처
6
48
 
@@ -8,22 +50,22 @@ Claude Code 세션과 Slack을 양방향으로 연결합니다. Slack DM이나
8
50
  ┌─────────────────────────────────────────────────────────────────┐
9
51
  │ Claude Code (Host) │
10
52
  │ │
11
- │ Claude LLM ◄──── <channel> notification ◄──── stdout │
12
- │ │
53
+ │ Claude LLM <──── <channel> notification <──── stdout │
54
+ │ │ ^
13
55
  │ │ tool call (reply/react/...) │ │
14
- │ │
15
- │ stdin ────► server.ts (MCP Server) ────► mcp.notification() │
16
- │ │
56
+ v │ │
57
+ │ stdin ────> server.ts (MCP Server) ────> mcp.notification() │
58
+ │ │ ^
17
59
  │ │ │ │
18
- │ │
60
+ v │ │
19
61
  │ Slack Web API Socket Mode (WebSocket) │
20
62
  │ │ │ │
21
63
  └────────────────────┼───────────┼────────────────────────────────┘
22
64
  │ │
23
-
65
+ v
24
66
  ┌──────────────────┴──────┐
25
- │ Slack Platform
26
- (채널, DM, 스레드)
67
+ │ Slack Platform
68
+ (채널, DM, 스레드)
27
69
  └─────────────────────────┘
28
70
  ```
29
71
 
@@ -42,8 +84,10 @@ main()
42
84
  │ --dangerously-load-development-channels 또는 --channels 확인
43
85
 
44
86
  ├── [플래그 있음] startSocketMode()
87
+ │ ├── killPreviousInstance() → 기존 인스턴스 종료 (PID 파일)
88
+ │ ├── writePidFile() → 자신의 PID 기록
45
89
  │ ├── web.auth.test() → botUserId 확인
46
- │ │ └── 실패 ──► process.exit(1)
90
+ │ │ └── 실패 process.exit(1)
47
91
  │ └── socket.start() → Slack 이벤트 수신 시작
48
92
 
49
93
  ├── [플래그 없음] Socket Mode 스킵 (Tools-only mode)
@@ -53,39 +97,123 @@ main()
53
97
 
54
98
  Socket Mode는 부모 프로세스(Claude Code)에 channels 플래그가 있을 때만 연결됩니다. 플래그 없이 실행된 세션은 도구만 제공하며 Socket Mode를 건드리지 않으므로, 기존 channels 세션의 연결이 보호됩니다.
55
99
 
100
+ ### Channels 플래그 감지 (`hasChannelsFlag`)
101
+
102
+ Claude Code는 `--dangerously-load-development-channels` 또는 `--channels` 플래그와 함께 실행될 때만 채널 기능을 사용합니다. 이 MCP 서버는 부모 프로세스의 커맨드라인을 검사하여 플래그 존재 여부를 판단합니다.
103
+
104
+ ```
105
+ hasChannelsFlag()
106
+
107
+ ├── SLACK_FORCE_SOCKET_MODE=1 ? ──── true (환경변수 오버라이드)
108
+
109
+ └── findAncestorClaudeCommand()
110
+
111
+ │ process.ppid부터 시작, 최대 5단계 탐색
112
+
113
+ v
114
+ ┌──────────────────────────────────────────────────────┐
115
+ │ PID 1234 (bun server.ts) │
116
+ │ └── ppid -> PID 1230 │
117
+ │ │
118
+ │ PID 1230 (node ...) │
119
+ │ └── ppid -> PID 1225 │
120
+ │ │
121
+ │ PID 1225 (claude --channels server:slack) <── 발견! │
122
+ │ └── 커맨드라인에 "channels" 포함 -> true │
123
+ └──────────────────────────────────────────────────────┘
124
+ ```
125
+
126
+ **프로세스 정보 조회 방법:**
127
+
128
+ | 플랫폼 | 명령 |
129
+ |---|---|
130
+ | macOS / Linux | `ps -o ppid=,command= -p <pid>` |
131
+ | Windows | `wmic process where ProcessId=<pid> get ParentProcessId,CommandLine /format:csv` |
132
+
133
+ 각 단계에서 부모 PID와 커맨드라인을 함께 가져와, `claude`라는 이름의 프로세스를 찾으면 그 커맨드라인에서 플래그를 확인합니다. 5단계 안에 찾지 못하면 `false`를 반환하며, Socket Mode를 시작하지 않습니다.
134
+
135
+ **왜 이것이 중요한가:**
136
+
137
+ Claude Code는 하나의 앱에 대해 여러 세션을 열 수 있습니다. 플래그 없이 실행된 세션이 Socket Mode에 연결하면, 이미 연결된 channels 세션의 WebSocket을 끊어버립니다. 플래그 검사를 통해 일반 세션은 tools-only 모드로 동작하여 기존 channels 세션을 보호합니다.
138
+
139
+ ```
140
+ 세션 A: claude --channels server:slack <- Socket Mode 연결 O
141
+ 세션 B: claude <- Socket Mode 스킵 (tools-only)
142
+ 세션 C: claude <- Socket Mode 스킵 (tools-only)
143
+ 세션 A의 연결은 안전하게 유지됨
144
+ ```
145
+
146
+ ### Socket Preemption (PID 파일 기반 단일 인스턴스 보장)
147
+
148
+ Slack Socket Mode는 **앱당 하나의 WebSocket 연결만** 허용합니다. 두 번째 연결이 생기면 첫 번째가 끊기며 이벤트가 유실될 수 있습니다. PID 파일(`~/.claude/channels/slack/socket.pid`)을 사용하여 항상 최신 인스턴스만 Socket Mode를 유지합니다.
149
+
150
+ ```
151
+ 인스턴스 A 시작 인스턴스 B 시작
152
+ │ │
153
+ v │
154
+ PID 파일 없음 │
155
+ │ │
156
+ v │
157
+ socket.pid <- A.pid 기록 │
158
+ │ │
159
+ v │
160
+ Socket Mode 연결 O │
161
+ 이벤트 수신 중... │
162
+ │ v
163
+ │ socket.pid 읽기 -> A.pid
164
+ │ │
165
+ │ v
166
+ │ kill(A.pid, 0) -> 생존 확인
167
+ │ │
168
+ │<──── SIGTERM ─────────────── kill(A.pid, SIGTERM)
169
+ │ │
170
+ v v
171
+ cleanupPidFile() socket.pid <- B.pid 기록
172
+ PID 파일 정리 (자기 PID일 때만) │
173
+ │ v
174
+ v Socket Mode 연결 O
175
+ 프로세스 종료 이벤트 수신 중...
176
+ ```
177
+
178
+ **PID 파일 안전 장치:**
179
+
180
+ - **생존 확인**: `process.kill(oldPid, 0)` — signal 0은 프로세스를 죽이지 않고 존재 여부만 확인합니다. 이미 종료된 프로세스에 SIGTERM을 보내는 것을 방지합니다.
181
+ - **소유권 확인**: `cleanupPidFile()`은 PID 파일의 내용이 자신의 PID일 때만 삭제합니다. 새 인스턴스가 이미 PID를 덮어썼다면 건드리지 않습니다.
182
+ - **종료 핸들링**: `exit`, `SIGTERM`, `SIGINT` 세 가지 시그널 모두에서 PID 파일을 정리합니다.
183
+
56
184
  ## 인바운드 메시지 파이프라인
57
185
 
58
- Slack에서 메시지가 들어오면 6단계 파이프라인을 거칩니다. 각 단계에서 조건을 만족하지 않으면 메시지를 drop합니다.
186
+ Slack에서 메시지가 들어오면 5단계 파이프라인을 거칩니다. 각 단계에서 조건을 만족하지 않으면 메시지를 drop합니다.
59
187
 
60
188
  ```
61
189
  Slack Event (message / app_mention)
62
190
 
63
-
191
+ v
64
192
  ┌─────────────┐
65
193
  │ 1. Dedup │──── 이미 처리한 이벤트? ──── drop
66
194
  └──────┬──────┘
67
195
  │ 새 이벤트
68
-
196
+ v
69
197
  ┌─────────────┐
70
198
  │ 2. Stale │──── 10분 이상 된 이벤트? ──── drop
71
199
  └──────┬──────┘
72
200
  │ 최근 이벤트
73
-
201
+ v
74
202
  ┌─────────────┐
75
203
  │ 3. Empty │──── 텍스트/파일/블록 없음? ──── drop
76
204
  └──────┬──────┘
77
205
  │ 내용 있음
78
-
206
+ v
79
207
  ┌─────────────┐
80
208
  │ 4. Gate │──── 봇 자신? subtype 불허? ──── drop
81
- │ (접근제어) │──── allowlist에 없음? ──────── drop
209
+ │ (접근제어) │──── allowlist에 없음? ──────── drop
82
210
  └──────┬──────┘
83
211
  │ 허용된 사용자
84
-
212
+ v
85
213
  ┌─────────────┐
86
214
  │ 5. Deliver │──── permalink 생성
87
215
  │ │──── ack reaction 추가
88
- │ │──── mcp.notification() Claude
216
+ │ │──── mcp.notification() -> Claude
89
217
  └─────────────┘
90
218
  ```
91
219
 
@@ -93,107 +221,78 @@ Slack Event (message / app_mention)
93
221
 
94
222
  Claude가 사용할 수 있는 MCP 도구:
95
223
 
96
- ```
97
- Claude (tool call)
98
-
99
- ├── reply ──────────── 메시지 전송
100
- │ chat.postMessage mrkdwn 포맷 변환
101
- │ ack reaction 자동 제거
102
-
103
- ├── react ─────────── 이모지 리액션 추가
104
- │ reactions.add
105
-
106
- ├── delete_bot_message ── 봇 메시지 삭제
107
- │ chat.delete (자기 메시지만)
108
-
109
- └── fetch_dm_thread ── DM 스레드 읽기
110
- conversations.replies
111
- (is_dm=true일 때만 사용)
112
- ```
224
+ | 도구 | 기능 | Slack API |
225
+ |---|---|---|
226
+ | `reply` | 메시지 전송 (mrkdwn 포맷 변환, ack reaction 자동 제거) | `chat.postMessage` |
227
+ | `react` | 이모지 리액션 추가 | `reactions.add` |
228
+ | `delete_bot_message` | 자신의 메시지 삭제 | `chat.delete` |
229
+ | `fetch_dm_thread` | DM 스레드 내용 읽기 (`is_dm=true`일 때만 사용) | `conversations.replies` |
113
230
 
114
231
  모든 아웃바운드 호출은 **audit log**에 기록되며, **outbound gate**를 통과해야 합니다 (인바운드를 받은 채널에만 응답 가능).
115
232
 
116
- ## 보안 레이어
233
+ ## 보안
117
234
 
118
235
  ```
119
- ┌─────────────────────────────────────────────────────┐
120
- │ Inbound Security
121
-
122
- │ Allowlist Gate ── SLACK_ALLOW_FROM에 등록된
123
- │ 사용자만 Claude에 메시지 전달
124
-
125
- │ Bot Owner ────── SLACK_BOT_OWNER는 항상 허용
126
-
127
- │ Self-loop ────── 봇 자신의 메시지 자동 drop
128
-
129
- │ Dedup ────────── 중복 이벤트 필터링 (TTL 10분)
130
-
131
- ├─────────────────────────────────────────────────────┤
132
- Outbound Security
133
-
134
- │ Outbound Gate ── 인바운드 수신 이력이 있는
135
- │ 채널에만 응답 허용
136
-
137
- │ Prompt Hardening ── 시스템 프롬프트에서
138
- │ 설정 변경 요청 거부
139
- └─────────────────────────────────────────────────────┘
140
- ```
141
-
142
- ## 프로젝트 구조
143
-
144
- ```
145
- server.ts MCP 서버, Slack 클라이언트, 이벤트 핸들링
146
- tools.ts MCP 도구 등록 (reply, react, delete_bot_message, fetch_dm_thread)
147
- lib/
148
- gate.ts 접근 제어 (Access, GateOptions, gate)
149
- process.ts 부모 프로세스 channels 플래그 감지 (hasChannelsFlag)
150
- security.ts 아웃바운드 게이트 (assertOutboundAllowed)
151
- formatting.ts Slack mrkdwn 변환, 메시지 텍스트 추출
152
- audit.ts 감사 로그 (AuditEntry, auditLog)
153
- event.ts DM 판별, 스레드 해석, stale/empty 필터링
154
- resilience.ts 이벤트 중복 제거 (EventDeduplicator)
155
- permalink.ts Slack 퍼마링크 빌더
156
- index.ts barrel re-export
157
- server.test.ts lib/ 테스트 (bun:test, 85개)
236
+ ┌───────────────────────────────────────────────────────┐
237
+ │ Inbound Security
238
+
239
+ │ Allowlist Gate ── SLACK_ALLOW_FROM에 등록된
240
+ │ 사용자만 Claude에 메시지 전달
241
+
242
+ │ Bot Owner ────── SLACK_BOT_OWNER는 항상 허용
243
+
244
+ │ Self-loop ────── 봇 자신의 메시지 자동 drop
245
+
246
+ │ Dedup ────────── 중복 이벤트 필터링 (TTL 10분)
247
+
248
+ ├───────────────────────────────────────────────────────┤
249
+ Outbound Security
250
+
251
+ │ Outbound Gate ── 인바운드 수신 이력이 있는
252
+ │ 채널에만 응답 허용
253
+
254
+ │ Prompt Hardening ── 시스템 프롬프트에서
255
+ │ 설정 변경 요청 거부
256
+ └───────────────────────────────────────────────────────┘
158
257
  ```
159
258
 
160
259
  ## 데이터 흐름 상세
161
260
 
162
- ### Permission Request 흐름
261
+ ### Permission Request
163
262
 
164
263
  도구 실행에 권한 확인이 필요할 때 Slack으로 알림을 보냅니다:
165
264
 
166
265
  ```
167
- Claude Code ──► permission_request notification
266
+ Claude Code ──> permission_request notification
168
267
 
169
-
268
+ v
170
269
  server.ts 수신
171
270
 
172
-
271
+ v
173
272
  마지막 inbound 채널/스레드로
174
273
  Slack 알림 전송 (경고색 #f0ad4e)
175
274
 
176
-
275
+ v
177
276
  사용자가 Slack에서 확인
178
277
  (승인/거부는 터미널에서)
179
278
  ```
180
279
 
181
- ### Ack Reaction 흐름
280
+ ### Ack Reaction
182
281
 
183
282
  메시지 수신 확인과 답장 완료를 이모지로 표시합니다:
184
283
 
185
284
  ```
186
- 메시지 수신 ──► ack reaction 추가 (예: 👀)
285
+ 메시지 수신 ──> ack reaction 추가 (예: eyes)
187
286
 
188
-
287
+ v
189
288
  Claude가 처리 중...
190
289
 
191
-
290
+ v
192
291
  reply 도구 호출 시
193
292
  ack reaction 자동 제거
194
293
  ```
195
294
 
196
- ## 기존 Slack App 추가 설정
295
+ ## Slack App 설정
197
296
 
198
297
  이미 Slack App이 있다면 (예: slack MCP 서버용), 같은 앱에 아래 설정을 추가합니다.
199
298
 
@@ -225,50 +324,28 @@ Claude Code ──► permission_request notification
225
324
 
226
325
  **OAuth & Permissions** → **Reinstall to Workspace** → Bot Token (`xoxb-...`) 복사
227
326
 
228
- ## 설치 및 설정
229
-
230
- 프로젝트 `.mcp.json` 또는 `~/.claude.json`에 추가합니다:
327
+ ## 프로젝트 구조
231
328
 
232
- ```json
233
- {
234
- "mcpServers": {
235
- "slack-channel": {
236
- "command": "bunx",
237
- "args": ["@rokrokss/claude-slack-channel"],
238
- "env": {
239
- "SLACK_BOT_TOKEN": "xoxb-...",
240
- "SLACK_APP_TOKEN": "xapp-...",
241
- "SLACK_ALLOW_FROM": "U123,U456",
242
- "SLACK_WORKSPACE": "your-workspace",
243
- "SLACK_ACK_REACTION": "eyes"
244
- }
245
- }
246
- }
247
- }
248
329
  ```
249
-
250
- | 환경변수 | 필수 | 설명 |
251
- |---|---|---|
252
- | `SLACK_BOT_TOKEN` | O | Bot User OAuth Token (`xoxb-...`) |
253
- | `SLACK_APP_TOKEN` | O | App-Level Token (`xapp-...`) |
254
- | `SLACK_ALLOW_FROM` | O | 허용할 Slack 사용자 ID (쉼표 구분) |
255
- | `SLACK_WORKSPACE` | O | Slack 워크스페이스 서브도메인 (예: `msuniverse`). permalink 생성에 사용 |
256
- | `SLACK_BOT_OWNER` | X | 봇 소유자 사용자 ID. allowlist 없이도 항상 허용 |
257
- | `SLACK_ACK_REACTION` | X | 수신 확인 이모지 (예: `eyes`). 답장 자동 제거 |
258
- | `SLACK_DEFAULT_COLOR` | X | 메시지 사이드바 색상 hex (기본: `#e5da9a`) |
259
-
260
- 허용된 사용자는 DM이든 채널이든 어디서든 봇과 대화할 수 있습니다.
261
-
262
- ## 실행
263
-
264
- ```bash
265
- claude --dangerously-load-development-channels server:slack-channel
330
+ server.ts MCP 서버, Slack 클라이언트, 이벤트 핸들링
331
+ tools.ts MCP 도구 등록 (reply, react, delete_bot_message, fetch_dm_thread)
332
+ lib/
333
+ gate.ts 접근 제어 (Access, GateOptions, gate)
334
+ process.ts 부모 프로세스 channels 플래그 감지 (hasChannelsFlag)
335
+ security.ts 아웃바운드 게이트 (assertOutboundAllowed)
336
+ formatting.ts Slack mrkdwn 변환, 메시지 텍스트 추출
337
+ audit.ts 감사 로그 (AuditEntry, auditLog)
338
+ event.ts DM 판별, 스레드 해석, stale/empty 필터링
339
+ resilience.ts 이벤트 중복 제거 (EventDeduplicator)
340
+ permalink.ts Slack 퍼마링크 빌더
341
+ index.ts barrel re-export
342
+ server.test.ts lib/ 테스트 (bun:test)
266
343
  ```
267
344
 
268
345
  ## 개발
269
346
 
270
347
  ```bash
271
- bun test # 테스트 실행 (85개)
348
+ bun test # 테스트 실행
272
349
  bun run typecheck # 타입 체크
273
350
  ```
274
351
 
package/lib/formatting.ts CHANGED
@@ -2,6 +2,21 @@ export function fixSlackMrkdwn(text: string): string {
2
2
  return text.replace(/\*([^*]+)\*/g, '\u200B*$1*\u200B')
3
3
  }
4
4
 
5
+ function richTextElementToString(e: any): string {
6
+ switch (e.type) {
7
+ case 'link': return e.text || e.url || ''
8
+ case 'user': return `<@${e.user_id}>`
9
+ case 'channel': return `<#${e.channel_id}>`
10
+ case 'usergroup': return `<!subteam^${e.usergroup_id}>`
11
+ case 'emoji': return e.unicode ? String.fromCodePoint(...e.unicode.split('-').map((h: string) => parseInt(h, 16))) : `:${e.name}:`
12
+ case 'broadcast': return `<!${e.range}>`
13
+ case 'color': return e.value ?? ''
14
+ case 'date': return e.fallback ?? `<!date^${e.timestamp}^${e.format}>`
15
+ case 'team': return `<!team^${e.team_id}>`
16
+ default: return e.text ?? ''
17
+ }
18
+ }
19
+
5
20
  export function extractMessageText(msg: Record<string, any>): string {
6
21
  const parts: string[] = []
7
22
 
@@ -10,7 +25,7 @@ export function extractMessageText(msg: Record<string, any>): string {
10
25
  if (block.type === 'rich_text' && block.elements) {
11
26
  for (const elem of block.elements) {
12
27
  if (elem.elements) {
13
- parts.push(elem.elements.map((e: any) => e.text ?? '').join(''))
28
+ parts.push(elem.elements.map(richTextElementToString).join(''))
14
29
  }
15
30
  }
16
31
  } else if (block.type === 'section') {
@@ -21,7 +36,10 @@ export function extractMessageText(msg: Record<string, any>): string {
21
36
  } else if (block.type === 'header') {
22
37
  if (block.text?.text) parts.push(`*${block.text.text}*`)
23
38
  } else if (block.type === 'context' && block.elements) {
24
- const texts = block.elements.map((e: any) => e.text ?? '').filter(Boolean)
39
+ const texts = block.elements.map((e: any) => {
40
+ if (e.type === 'image') return e.alt_text || '[image]'
41
+ return e.text ?? ''
42
+ }).filter(Boolean)
25
43
  if (texts.length) parts.push(texts.join(' '))
26
44
  } else if (block.type === 'divider') {
27
45
  parts.push('---')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokrokss/claude-slack-channel",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Two-way Slack channel for Claude Code — Slack DM과 채널 멘션으로 Claude Code 세션과 대화",
5
5
  "type": "module",
6
6
  "bin": "./server.ts",
package/server.ts CHANGED
@@ -78,7 +78,9 @@ const allowFromList = (process.env['SLACK_ALLOW_FROM'] || '')
78
78
  const ackReaction = (process.env['SLACK_ACK_REACTION'] || '').trim().replace(/^:|:$/g, '') || undefined
79
79
  console.error(`[slack] ackReaction: ${ackReaction ?? '(disabled)'}`)
80
80
  const botOwner = (process.env['SLACK_BOT_OWNER'] || '').trim() || undefined
81
+ const showFooter = (process.env['SLACK_SHOW_FOOTER'] ?? 'true').trim().toLowerCase() !== 'false'
81
82
  console.error(`[slack] botOwner: ${botOwner ?? '(not set)'}`)
83
+ console.error(`[slack] showFooter: ${showFooter}`)
82
84
  const workspace = process.env['SLACK_WORKSPACE'] || ''
83
85
  if (!workspace) {
84
86
  console.error('[slack] SLACK_WORKSPACE is required for permalink generation. Set it in .mcp.json env field.')
@@ -158,7 +160,7 @@ async function resolveUserName(userId: string): Promise<string> {
158
160
  // ---------------------------------------------------------------------------
159
161
 
160
162
  const mcp = new McpServer(
161
- { name: 'slack-channel', version: '0.2.0' },
163
+ { name: 'slack-channel', version: '0.3.1' },
162
164
  {
163
165
  capabilities: {
164
166
  experimental: {
@@ -195,10 +197,16 @@ registerTools({
195
197
  web,
196
198
  stateDir: STATE_DIR,
197
199
  defaultColor: DEFAULT_COLOR,
200
+ botOwner,
201
+ showFooter,
198
202
  assertOutboundAllowed,
199
203
  lastInboundMessageId,
200
204
  pendingAckReactions,
201
205
  resolveUserName,
206
+ clearInboundContext: () => {
207
+ lastInboundContext = null
208
+ console.error('[slack] inbound context cleared (reply sent)')
209
+ },
202
210
  })
203
211
 
204
212
  // ---------------------------------------------------------------------------
@@ -206,6 +214,7 @@ registerTools({
206
214
  // ---------------------------------------------------------------------------
207
215
 
208
216
  // 마지막 inbound 메시지의 채널+스레드를 추적하여 permission 알림 전송 대상으로 사용
217
+ // reply 도구 호출 시 초기화 — Slack 대화 처리 완료로 간주
209
218
  let lastInboundContext: { channelId: string; threadTs: string } | null = null
210
219
 
211
220
  mcp.server.setNotificationHandler(
@@ -229,13 +238,13 @@ mcp.server.setNotificationHandler(
229
238
 
230
239
  const { channelId, threadTs } = lastInboundContext
231
240
  try {
232
- const ownerTag = botOwner ? ` <@${botOwner}>` : ''
233
241
  await web.chat.postMessage({
234
242
  channel: channelId,
235
243
  thread_ts: threadTs,
236
244
  attachments: [{
237
245
  color: '#f0ad4e',
238
- text: `터미널에서 도구 실행 권한 확인이 필요합니다. ${ownerTag}\n\`${tool_name}\`: ${description}`,
246
+ text: `터미널에서 도구 실행 권한 확인이 필요합니다.\n\`${tool_name}\`: ${description}`,
247
+ ...(showFooter && botOwner ? { footer: `다음 사용자가 만듬 <@${botOwner}>` } : {}),
239
248
  mrkdwn_in: ['text'],
240
249
  }],
241
250
  unfurl_links: false,
package/tools.ts CHANGED
@@ -8,14 +8,17 @@ export interface ToolDependencies {
8
8
  web: WebClient
9
9
  stateDir: string
10
10
  defaultColor: string
11
+ botOwner?: string
12
+ showFooter: boolean
11
13
  assertOutboundAllowed: (chatId: string) => void
12
14
  lastInboundMessageId: Map<string, string>
13
15
  pendingAckReactions: Map<string, { channel: string; ts: string; emoji: string }>
14
16
  resolveUserName: (userId: string) => Promise<string>
17
+ clearInboundContext: () => void
15
18
  }
16
19
 
17
20
  export function registerTools(deps: ToolDependencies): void {
18
- const { mcp, web, stateDir, defaultColor, assertOutboundAllowed, lastInboundMessageId, pendingAckReactions, resolveUserName } = deps
21
+ const { mcp, web, stateDir, defaultColor, botOwner, showFooter, assertOutboundAllowed, lastInboundMessageId, pendingAckReactions, resolveUserName, clearInboundContext } = deps
19
22
 
20
23
  mcp.registerTool('reply', {
21
24
  description: 'Send a message to a Slack channel or DM.',
@@ -48,6 +51,7 @@ export function registerTools(deps: ToolDependencies): void {
48
51
  color,
49
52
  text: fixSlackMrkdwn(args.text),
50
53
  mrkdwn_in: ['text'],
54
+ ...(showFooter && botOwner ? { footer: `다음 사용자가 만듬 <@${botOwner}>` } : {}),
51
55
  }],
52
56
  unfurl_links: false,
53
57
  unfurl_media: false,
@@ -70,6 +74,9 @@ export function registerTools(deps: ToolDependencies): void {
70
74
  } catch (err) { console.error('[slack] ack reaction auto-remove failed:', err) }
71
75
  }
72
76
 
77
+ // Slack 대화 처리 완료로 간주 — permission_request 알림 대상 초기화
78
+ clearInboundContext()
79
+
73
80
  return {
74
81
  content: [{
75
82
  type: 'text' as const,