@ignission/slack-task-mcp 0.1.0 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ignission/slack-task-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP Server for Slack task management with Claude Code",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/auth.js CHANGED
@@ -7,14 +7,16 @@
7
7
  */
8
8
 
9
9
  import crypto from "node:crypto";
10
- import fs from "node:fs/promises";
11
- import os from "node:os";
12
- import path from "node:path";
13
10
  import open from "open";
11
+ import {
12
+ listWorkspaces,
13
+ saveCredentials,
14
+ deleteCredentialsByDomain,
15
+ deleteAllCredentials,
16
+ getCredentialsDir,
17
+ } from "./credentials.js";
14
18
 
15
19
  // 定数
16
- const DATA_DIR = path.join(os.homedir(), ".slack-task-mcp");
17
- const CREDENTIALS_FILE = path.join(DATA_DIR, "credentials.json");
18
20
  const AUTH_TIMEOUT = 5 * 60 * 1000; // 5分
19
21
  const POLL_INTERVAL = 2000; // 2秒
20
22
 
@@ -22,49 +24,6 @@ const POLL_INTERVAL = 2000; // 2秒
22
24
  const OAUTH_WORKER_URL =
23
25
  process.env.OAUTH_WORKER_URL || "https://slack-task-mcp-oauth.ignission.workers.dev";
24
26
 
25
- /**
26
- * データディレクトリを初期化
27
- */
28
- async function initDataDir() {
29
- try {
30
- await fs.mkdir(DATA_DIR, { recursive: true });
31
- } catch (_err) {
32
- // 既に存在する場合は無視
33
- }
34
- }
35
-
36
- /**
37
- * credentials.json を読み込み
38
- */
39
- export async function loadCredentials() {
40
- try {
41
- const data = await fs.readFile(CREDENTIALS_FILE, "utf-8");
42
- return JSON.parse(data);
43
- } catch (_err) {
44
- return null;
45
- }
46
- }
47
-
48
- /**
49
- * credentials.json を保存
50
- */
51
- async function saveCredentials(credentials) {
52
- await initDataDir();
53
- await fs.writeFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
54
- }
55
-
56
- /**
57
- * credentials.json を削除
58
- */
59
- async function deleteCredentials() {
60
- try {
61
- await fs.unlink(CREDENTIALS_FILE);
62
- return true;
63
- } catch (_err) {
64
- return false;
65
- }
66
- }
67
-
68
27
  /**
69
28
  * セッション ID を生成
70
29
  */
@@ -129,7 +88,7 @@ export async function authenticate(options = {}) {
129
88
  const result = await pollForToken(sessionId);
130
89
 
131
90
  if (result.status === "success") {
132
- // credentials を保存
91
+ // credentials を保存(team_domain を追加)
133
92
  const credentials = {
134
93
  access_token: result.access_token,
135
94
  token_type: result.token_type,
@@ -137,6 +96,7 @@ export async function authenticate(options = {}) {
137
96
  user_id: result.user_id,
138
97
  team_id: result.team_id,
139
98
  team_name: result.team_name,
99
+ team_domain: result.team_domain || extractDomainFromTeamName(result.team_name),
140
100
  created_at: result.created_at,
141
101
  };
142
102
 
@@ -145,7 +105,8 @@ export async function authenticate(options = {}) {
145
105
  console.log("");
146
106
  console.log("✅ 認証が完了しました!");
147
107
  console.log(` ワークスペース: ${credentials.team_name}`);
148
- console.log(` トークンは ${CREDENTIALS_FILE} に保存されました`);
108
+ console.log(` ドメイン: ${credentials.team_domain}.slack.com`);
109
+ console.log(` 保存先: ${getCredentialsDir()}/${credentials.team_id}.json`);
149
110
 
150
111
  return true;
151
112
  }
@@ -169,48 +130,80 @@ export async function authenticate(options = {}) {
169
130
  return false;
170
131
  }
171
132
 
133
+ /**
134
+ * team_nameからドメインを推測(フォールバック用)
135
+ */
136
+ function extractDomainFromTeamName(teamName) {
137
+ return teamName
138
+ .toLowerCase()
139
+ .replace(/[^a-z0-9-]/g, "-")
140
+ .replace(/-+/g, "-")
141
+ .replace(/^-|-$/g, "");
142
+ }
143
+
172
144
  /**
173
145
  * 認証状態を表示
174
146
  */
175
147
  export async function showStatus() {
176
- const credentials = await loadCredentials();
148
+ const workspaces = await listWorkspaces();
177
149
 
178
150
  console.log("📋 認証状態");
179
151
  console.log("");
180
152
 
181
- if (!credentials) {
153
+ if (workspaces.length === 0) {
182
154
  console.log("状態: ❌ 未認証");
183
155
  console.log("");
184
- console.log("`npx slack-task-mcp auth` を実行して認証してください");
156
+ console.log("`npx @ignission/slack-task-mcp auth login` を実行して認証してください");
185
157
  return;
186
158
  }
187
159
 
188
- console.log("状態:認証済み");
189
- console.log(`ユーザー ID: ${credentials.user_id}`);
190
- console.log(`ワークスペース: ${credentials.team_name} (${credentials.team_id})`);
191
- console.log(`認証日時: ${credentials.created_at}`);
192
- console.log(`スコープ: ${credentials.scope}`);
160
+ console.log(`状態:${workspaces.length} ワークスペース認証済み`);
161
+ console.log("");
162
+
163
+ for (const ws of workspaces) {
164
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
165
+ console.log(`📌 ${ws.team_name}`);
166
+ console.log(` ドメイン: ${ws.team_domain}.slack.com`);
167
+ console.log(` チームID: ${ws.team_id}`);
168
+ console.log(` ユーザーID: ${ws.user_id}`);
169
+ console.log(` 認証日時: ${ws.created_at}`);
170
+ }
171
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
193
172
  }
194
173
 
195
174
  /**
196
175
  * ログアウト
176
+ * @param {object} options
177
+ * @param {string} [options.workspace] - ログアウトするワークスペース名またはドメイン
197
178
  */
198
- export async function logout() {
199
- const credentials = await loadCredentials();
179
+ export async function logout(options = {}) {
180
+ const { workspace } = options;
200
181
 
201
- if (!credentials) {
202
- console.log("ℹ️ 認証情報はありません");
203
- return true;
182
+ if (workspace) {
183
+ // 指定ワークスペースのみログアウト
184
+ const deleted = await deleteCredentialsByDomain(workspace);
185
+
186
+ if (deleted) {
187
+ console.log(`✅ ワークスペース「${workspace}」からログアウトしました`);
188
+ return true;
189
+ }
190
+
191
+ console.error(`❌ ワークスペース「${workspace}」が見つかりません`);
192
+ console.log("");
193
+ console.log("`npx @ignission/slack-task-mcp auth status` で認証済みワークスペースを確認してください");
194
+ return false;
204
195
  }
205
196
 
206
- const deleted = await deleteCredentials();
197
+ // 全ワークスペースをログアウト
198
+ const workspaces = await listWorkspaces();
207
199
 
208
- if (deleted) {
209
- console.log(" ログアウトしました");
210
- console.log(` ${CREDENTIALS_FILE} を削除しました`);
200
+ if (workspaces.length === 0) {
201
+ console.log("ℹ️ 認証情報はありません");
211
202
  return true;
212
- } else {
213
- console.error("❌ ログアウトに失敗しました");
214
- return false;
215
203
  }
204
+
205
+ const count = await deleteAllCredentials();
206
+
207
+ console.log(`✅ ${count} ワークスペースからログアウトしました`);
208
+ return true;
216
209
  }
package/src/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * CLI エントリーポイント
5
5
  *
6
- * npx slack-task-mcp [command] [options]
6
+ * npx @ignission/slack-task-mcp [command] [options]
7
7
  */
8
8
 
9
9
  import { authenticate, logout, showStatus } from "./auth.js";
@@ -20,6 +20,10 @@ function parseOptions(args) {
20
20
  if (args[i] === "--no-browser") {
21
21
  options.noBrowser = true;
22
22
  }
23
+ if (args[i] === "--workspace" || args[i] === "-w") {
24
+ options.workspace = args[i + 1];
25
+ i++;
26
+ }
23
27
  }
24
28
  return options;
25
29
  }
@@ -30,23 +34,26 @@ function showHelp() {
30
34
  Slack Task MCP Server
31
35
 
32
36
  Usage:
33
- npx slack-task-mcp [command] [options]
37
+ npx @ignission/slack-task-mcp [command] [options]
34
38
 
35
39
  Commands:
36
- auth Slack OAuth 認証を開始
37
- auth status 認証状態を表示
38
- auth logout ログアウト
39
- (なし) MCP サーバーとして起動
40
+ auth login 新しいワークスペースを認証
41
+ auth status 認証状態を表示
42
+ auth logout 全ワークスペースからログアウト
43
+ auth logout -w <name> 指定ワークスペースのみログアウト
44
+ (なし) MCP サーバーとして起動
40
45
 
41
46
  Options:
42
- --no-browser ブラウザを自動で開かない
43
- --help, -h ヘルプを表示
47
+ --no-browser ブラウザを自動で開かない
48
+ -w, --workspace <name> ワークスペースを指定(ドメイン名)
49
+ --help, -h ヘルプを表示
44
50
 
45
51
  Examples:
46
- npx slack-task-mcp auth
47
- npx slack-task-mcp auth status
48
- npx slack-task-mcp auth logout
49
- npx slack-task-mcp auth --no-browser
52
+ npx @ignission/slack-task-mcp auth login
53
+ npx @ignission/slack-task-mcp auth login --no-browser
54
+ npx @ignission/slack-task-mcp auth status
55
+ npx @ignission/slack-task-mcp auth logout
56
+ npx @ignission/slack-task-mcp auth logout --workspace mycompany
50
57
  `);
51
58
  }
52
59
 
@@ -60,17 +67,29 @@ async function main() {
60
67
 
61
68
  // auth コマンド
62
69
  if (command === "auth") {
63
- if (subCommand === "status") {
70
+ const options = parseOptions(args);
71
+
72
+ if (subCommand === "login") {
73
+ // 認証フロー
74
+ const success = await authenticate(options);
75
+ process.exit(success ? 0 : 1);
76
+ } else if (subCommand === "status") {
64
77
  await showStatus();
65
78
  process.exit(0);
66
79
  } else if (subCommand === "logout") {
67
- const success = await logout();
80
+ const success = await logout(options);
68
81
  process.exit(success ? 0 : 1);
69
- } else {
70
- // 認証フロー
71
- const options = parseOptions(args);
82
+ } else if (!subCommand) {
83
+ // auth 単体は非推奨メッセージを表示してloginへ
84
+ console.log("⚠️ `auth` コマンドは `auth login` に変更されました");
85
+ console.log("");
72
86
  const success = await authenticate(options);
73
87
  process.exit(success ? 0 : 1);
88
+ } else {
89
+ console.error(`❌ 不明なサブコマンド: auth ${subCommand}`);
90
+ console.error("");
91
+ console.error("使用可能なサブコマンド: login, status, logout");
92
+ process.exit(1);
74
93
  }
75
94
  }
76
95
 
@@ -81,7 +100,7 @@ async function main() {
81
100
  } else {
82
101
  console.error(`❌ 不明なコマンド: ${command}`);
83
102
  console.error("");
84
- console.error("ヘルプを表示するには: npx slack-task-mcp --help");
103
+ console.error("ヘルプを表示するには: npx @ignission/slack-task-mcp --help");
85
104
  process.exit(1);
86
105
  }
87
106
  }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * 複数ワークスペースの認証情報管理モジュール
3
+ *
4
+ * 認証情報は credentials/{team_id}.json に保存
5
+ */
6
+
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { getCredentialsDir as _getCredentialsDir, getDataDir } from "./paths.js";
10
+
11
+ // Re-export for use in other modules
12
+ export { _getCredentialsDir as getCredentialsDir };
13
+
14
+ /**
15
+ * 認証情報ディレクトリを初期化
16
+ */
17
+ async function ensureCredentialsDir() {
18
+ const dir = __getCredentialsDir();
19
+ await fs.mkdir(dir, { recursive: true });
20
+ return dir;
21
+ }
22
+
23
+ /**
24
+ * データディレクトリを初期化
25
+ */
26
+ export async function ensureDataDir() {
27
+ const dir = getDataDir();
28
+ await fs.mkdir(dir, { recursive: true });
29
+ return dir;
30
+ }
31
+
32
+ /**
33
+ * 全ワークスペース一覧を取得
34
+ * @returns {Promise<Array<{team_id: string, team_name: string, team_domain: string, user_id: string, created_at: string}>>}
35
+ */
36
+ export async function listWorkspaces() {
37
+ const dir = _getCredentialsDir();
38
+
39
+ try {
40
+ const files = await fs.readdir(dir);
41
+ const workspaces = [];
42
+
43
+ for (const file of files) {
44
+ if (!file.endsWith(".json")) continue;
45
+
46
+ try {
47
+ const filePath = path.join(dir, file);
48
+ const data = await fs.readFile(filePath, "utf-8");
49
+ const creds = JSON.parse(data);
50
+
51
+ workspaces.push({
52
+ team_id: creds.team_id,
53
+ team_name: creds.team_name,
54
+ team_domain: creds.team_domain,
55
+ user_id: creds.user_id,
56
+ created_at: creds.created_at,
57
+ });
58
+ } catch {
59
+ // 破損したファイルはスキップ
60
+ }
61
+ }
62
+
63
+ return workspaces;
64
+ } catch {
65
+ // ディレクトリが存在しない場合
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * team_domainで認証情報を検索
72
+ * @param {string} teamDomain - ワークスペースのドメイン(例: "myworkspace")
73
+ * @returns {Promise<object|null>} 認証情報またはnull
74
+ */
75
+ export async function getCredentialsByDomain(teamDomain) {
76
+ const dir = _getCredentialsDir();
77
+
78
+ try {
79
+ const files = await fs.readdir(dir);
80
+
81
+ for (const file of files) {
82
+ if (!file.endsWith(".json")) continue;
83
+
84
+ try {
85
+ const filePath = path.join(dir, file);
86
+ const data = await fs.readFile(filePath, "utf-8");
87
+ const creds = JSON.parse(data);
88
+
89
+ if (creds.team_domain === teamDomain) {
90
+ return creds;
91
+ }
92
+ } catch {
93
+ // 破損したファイルはスキップ
94
+ }
95
+ }
96
+
97
+ return null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * team_idで認証情報を取得
105
+ * @param {string} teamId - ワークスペースID(例: "T01234567")
106
+ * @returns {Promise<object|null>} 認証情報またはnull
107
+ */
108
+ export async function getCredentialsById(teamId) {
109
+ const filePath = path.join(_getCredentialsDir(), `${teamId}.json`);
110
+
111
+ try {
112
+ const data = await fs.readFile(filePath, "utf-8");
113
+ return JSON.parse(data);
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 最初に見つかった認証情報を取得(単一ワークスペース用の互換性)
121
+ * @returns {Promise<object|null>} 認証情報またはnull
122
+ */
123
+ export async function getFirstCredentials() {
124
+ const workspaces = await listWorkspaces();
125
+ if (workspaces.length === 0) return null;
126
+
127
+ return getCredentialsById(workspaces[0].team_id);
128
+ }
129
+
130
+ /**
131
+ * 認証情報を保存
132
+ * @param {object} credentials - 認証情報
133
+ * @param {string} credentials.access_token
134
+ * @param {string} credentials.token_type
135
+ * @param {string} credentials.scope
136
+ * @param {string} credentials.user_id
137
+ * @param {string} credentials.team_id
138
+ * @param {string} credentials.team_name
139
+ * @param {string} credentials.team_domain
140
+ * @param {string} credentials.created_at
141
+ */
142
+ export async function saveCredentials(credentials) {
143
+ await ensureCredentialsDir();
144
+
145
+ const filePath = path.join(_getCredentialsDir(), `${credentials.team_id}.json`);
146
+ await fs.writeFile(filePath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
147
+ }
148
+
149
+ /**
150
+ * 指定ワークスペースの認証情報を削除
151
+ * @param {string} teamId - ワークスペースID
152
+ * @returns {Promise<boolean>} 削除成功/失敗
153
+ */
154
+ export async function deleteCredentials(teamId) {
155
+ const filePath = path.join(_getCredentialsDir(), `${teamId}.json`);
156
+
157
+ try {
158
+ await fs.unlink(filePath);
159
+ return true;
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * team_domainで認証情報を削除
167
+ * @param {string} teamDomain - ワークスペースのドメイン
168
+ * @returns {Promise<boolean>} 削除成功/失敗
169
+ */
170
+ export async function deleteCredentialsByDomain(teamDomain) {
171
+ const creds = await getCredentialsByDomain(teamDomain);
172
+ if (!creds) return false;
173
+
174
+ return deleteCredentials(creds.team_id);
175
+ }
176
+
177
+ /**
178
+ * 全ワークスペースの認証情報を削除
179
+ * @returns {Promise<number>} 削除した件数
180
+ */
181
+ export async function deleteAllCredentials() {
182
+ const dir = _getCredentialsDir();
183
+ let count = 0;
184
+
185
+ try {
186
+ const files = await fs.readdir(dir);
187
+
188
+ for (const file of files) {
189
+ if (!file.endsWith(".json")) continue;
190
+
191
+ try {
192
+ await fs.unlink(path.join(dir, file));
193
+ count++;
194
+ } catch {
195
+ // 削除失敗は無視
196
+ }
197
+ }
198
+ } catch {
199
+ // ディレクトリが存在しない場合
200
+ }
201
+
202
+ return count;
203
+ }
package/src/index.js CHANGED
@@ -9,20 +9,20 @@
9
9
  */
10
10
 
11
11
  import fs from "node:fs/promises";
12
- import os from "node:os";
13
- import path from "node:path";
14
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
14
  import { WebClient } from "@slack/web-api";
17
15
  import { z } from "zod";
18
- import { loadCredentials } from "./auth.js";
16
+ import { getTasksPath } from "./paths.js";
17
+ import {
18
+ getCredentialsByDomain,
19
+ getFirstCredentials,
20
+ listWorkspaces,
21
+ ensureDataDir,
22
+ } from "./credentials.js";
19
23
  import { analyzeRequest } from "./agents/analyze.js";
20
24
  import { draftReply } from "./agents/draft-reply.js";
21
25
 
22
- // データ保存先
23
- const DATA_DIR = path.join(os.homedir(), ".slack-task-mcp");
24
- const TASKS_FILE = path.join(DATA_DIR, "tasks.json");
25
-
26
26
  // ============================================
27
27
  // ツールパラメータ用 Zodスキーマ
28
28
  // ※ 分析/添削の結果スキーマはagents/配下に移動
@@ -41,18 +41,60 @@ const _SearchParamsSchema = z.object({
41
41
  channel: z.string().optional().describe("チャンネル名で絞り込み(#なし)"),
42
42
  });
43
43
 
44
- // Slack クライアント(User Token使用)
45
- let slackClient = null;
44
+ // Slack クライアントのキャッシュ(team_domain -> WebClient)
45
+ const slackClients = new Map();
46
46
 
47
47
  /**
48
- * データディレクトリを初期化
48
+ * URLからteam_domainを抽出
49
+ * @param {string} slackUrl - Slack URL(例: https://myworkspace.slack.com/archives/C123/p456)
50
+ * @returns {string|null} team_domain または null
49
51
  */
50
- async function initDataDir() {
51
- try {
52
- await fs.mkdir(DATA_DIR, { recursive: true });
53
- } catch (_err) {
54
- // 既に存在する場合は無視
52
+ function extractTeamDomain(slackUrl) {
53
+ const match = slackUrl.match(/https:\/\/([^.]+)\.slack\.com/);
54
+ return match ? match[1] : null;
55
+ }
56
+
57
+ /**
58
+ * team_domain用のSlackクライアントを取得
59
+ * @param {string} teamDomain - ワークスペースのドメイン
60
+ * @returns {Promise<WebClient|null>}
61
+ */
62
+ async function getSlackClient(teamDomain) {
63
+ // キャッシュにあればそれを返す
64
+ if (slackClients.has(teamDomain)) {
65
+ return slackClients.get(teamDomain);
66
+ }
67
+
68
+ // 認証情報を取得
69
+ const credentials = await getCredentialsByDomain(teamDomain);
70
+ if (!credentials?.access_token) {
71
+ return null;
72
+ }
73
+
74
+ // クライアントを作成してキャッシュ
75
+ const client = new WebClient(credentials.access_token);
76
+ slackClients.set(teamDomain, client);
77
+ return client;
78
+ }
79
+
80
+ /**
81
+ * 最初のワークスペースのSlackクライアントを取得(フォールバック用)
82
+ * @returns {Promise<WebClient|null>}
83
+ */
84
+ async function getDefaultSlackClient() {
85
+ const credentials = await getFirstCredentials();
86
+ if (!credentials?.access_token) {
87
+ return null;
55
88
  }
89
+
90
+ // キャッシュにあればそれを返す
91
+ if (slackClients.has(credentials.team_domain)) {
92
+ return slackClients.get(credentials.team_domain);
93
+ }
94
+
95
+ const client = new WebClient(credentials.access_token);
96
+ slackClients.set(credentials.team_domain, client);
97
+ return client;
56
98
  }
57
99
 
58
100
  /**
@@ -60,7 +102,7 @@ async function initDataDir() {
60
102
  */
61
103
  async function loadTasks() {
62
104
  try {
63
- const data = await fs.readFile(TASKS_FILE, "utf-8");
105
+ const data = await fs.readFile(getTasksPath(), "utf-8");
64
106
  return JSON.parse(data);
65
107
  } catch (_err) {
66
108
  return { tasks: [] };
@@ -71,7 +113,8 @@ async function loadTasks() {
71
113
  * タスクデータを保存
72
114
  */
73
115
  async function saveTasks(data) {
74
- await fs.writeFile(TASKS_FILE, JSON.stringify(data, null, 2));
116
+ await ensureDataDir();
117
+ await fs.writeFile(getTasksPath(), JSON.stringify(data, null, 2));
75
118
  }
76
119
 
77
120
  /**
@@ -100,10 +143,10 @@ function parseSlackUrl(url) {
100
143
  /**
101
144
  * スレッドのメッセージを取得(ページネーション対応)
102
145
  */
103
- async function getThreadMessages(channel, threadTs) {
146
+ async function getThreadMessages(slackClient, channel, threadTs) {
104
147
  if (!slackClient) {
105
148
  throw new Error(
106
- "Slack認証されていません。`npx slack-task-mcp auth` を実行して認証してください。",
149
+ "Slack認証されていません。`npx @ignission/slack-task-mcp auth login` を実行して認証してください。",
107
150
  );
108
151
  }
109
152
 
@@ -136,7 +179,7 @@ async function getThreadMessages(channel, threadTs) {
136
179
  /**
137
180
  * ユーザー情報を取得
138
181
  */
139
- async function getUserInfo(userId) {
182
+ async function getUserInfo(slackClient, userId) {
140
183
  if (!slackClient) return { name: userId, real_name: userId };
141
184
 
142
185
  try {
@@ -150,7 +193,7 @@ async function getUserInfo(userId) {
150
193
  /**
151
194
  * Slackメッセージを検索
152
195
  */
153
- async function searchSlackMessages(query, count = 10) {
196
+ async function searchSlackMessages(slackClient, query, count = 10) {
154
197
  if (!slackClient) {
155
198
  throw new Error("Slack client not initialized");
156
199
  }
@@ -175,7 +218,7 @@ async function searchSlackMessages(query, count = 10) {
175
218
  /**
176
219
  * 検索結果をMarkdown形式にフォーマット
177
220
  */
178
- async function formatSearchResults(messages, total, _requestedCount) {
221
+ async function formatSearchResults(slackClient, messages, total, _requestedCount) {
179
222
  if (messages.length === 0) {
180
223
  return "🔍 該当するメッセージはありません";
181
224
  }
@@ -198,7 +241,7 @@ async function formatSearchResults(messages, total, _requestedCount) {
198
241
  // ユーザー名を取得(キャッシュ)
199
242
  let userName = msg.user || msg.username || "不明";
200
243
  if (msg.user && !userCache[msg.user]) {
201
- const userInfo = await getUserInfo(msg.user);
244
+ const userInfo = await getUserInfo(slackClient, msg.user);
202
245
  userCache[msg.user] = userInfo.real_name || userInfo.name || msg.user;
203
246
  }
204
247
  if (msg.user) {
@@ -234,7 +277,7 @@ async function formatSearchResults(messages, total, _requestedCount) {
234
277
  /**
235
278
  * メッセージをフォーマット
236
279
  */
237
- async function formatMessages(messages) {
280
+ async function formatMessages(slackClient, messages) {
238
281
  const formatted = [];
239
282
  const userCache = {};
240
283
 
@@ -242,7 +285,7 @@ async function formatMessages(messages) {
242
285
  // ユーザー名を取得(キャッシュ)
243
286
  let userName = msg.user;
244
287
  if (msg.user && !userCache[msg.user]) {
245
- const userInfo = await getUserInfo(msg.user);
288
+ const userInfo = await getUserInfo(slackClient, msg.user);
246
289
  userCache[msg.user] = userInfo.real_name || userInfo.name || msg.user;
247
290
  }
248
291
  userName = userCache[msg.user] || msg.user;
@@ -288,9 +331,36 @@ server.tool(
288
331
  };
289
332
  }
290
333
 
334
+ // URLからワークスペースを特定
335
+ const teamDomain = extractTeamDomain(url);
336
+ if (!teamDomain) {
337
+ return {
338
+ content: [
339
+ { type: "text", text: "URLからワークスペースを特定できませんでした。" },
340
+ ],
341
+ };
342
+ }
343
+
344
+ // 該当ワークスペースのクライアントを取得
345
+ const slackClient = await getSlackClient(teamDomain);
346
+ if (!slackClient) {
347
+ const workspaces = await listWorkspaces();
348
+ const workspaceList = workspaces.length > 0
349
+ ? `\n認証済み: ${workspaces.map(w => w.team_domain).join(", ")}`
350
+ : "";
351
+ return {
352
+ content: [
353
+ {
354
+ type: "text",
355
+ text: `❌ ワークスペース「${teamDomain}」は認証されていません。\n\n\`npx @ignission/slack-task-mcp auth login\` で認証してください。${workspaceList}`,
356
+ },
357
+ ],
358
+ };
359
+ }
360
+
291
361
  const { channel, threadTs } = parsed;
292
- const messages = await getThreadMessages(channel, threadTs);
293
- const formatted = await formatMessages(messages);
362
+ const messages = await getThreadMessages(slackClient, channel, threadTs);
363
+ const formatted = await formatMessages(slackClient, messages);
294
364
 
295
365
  // 読みやすい形式でテキスト化
296
366
  const text = formatted.map((m) => `[${m.timestamp}] ${m.user}:\n${m.text}`).join("\n\n---\n\n");
@@ -324,7 +394,7 @@ server.tool(
324
394
  source_url: z.string().optional().describe("元のSlack URL"),
325
395
  },
326
396
  async ({ title, purpose, steps, source_url }) => {
327
- await initDataDir();
397
+ await ensureDataDir();
328
398
  const data = await loadTasks();
329
399
 
330
400
  const task = {
@@ -358,7 +428,7 @@ server.tool(
358
428
 
359
429
  // ツール: タスク一覧を取得
360
430
  server.tool("list_tasks", "保存されているタスクの一覧を取得します", {}, async () => {
361
- await initDataDir();
431
+ await ensureDataDir();
362
432
  const data = await loadTasks();
363
433
 
364
434
  if (data.tasks.length === 0) {
@@ -418,7 +488,7 @@ server.tool(
418
488
  days: z.number().optional().describe("過去N日以内に作成/完了したタスク"),
419
489
  },
420
490
  async ({ keyword, status = "all", days }) => {
421
- await initDataDir();
491
+ await ensureDataDir();
422
492
  const data = await loadTasks();
423
493
 
424
494
  if (data.tasks.length === 0) {
@@ -499,7 +569,7 @@ server.tool(
499
569
  step_number: z.number().describe("完了するステップ番号"),
500
570
  },
501
571
  async ({ task_id, step_number }) => {
502
- await initDataDir();
572
+ await ensureDataDir();
503
573
  const data = await loadTasks();
504
574
 
505
575
  // タスクを検索
@@ -808,13 +878,15 @@ server.tool(
808
878
  channel: z.string().optional().describe("チャンネル名で絞り込み(#なし)"),
809
879
  },
810
880
  async ({ query, count = 10, channel }) => {
811
- // 未認証チェック
881
+ // デフォルトのSlackクライアントを取得
882
+ const slackClient = await getDefaultSlackClient();
883
+
812
884
  if (!slackClient) {
813
885
  return {
814
886
  content: [
815
887
  {
816
888
  type: "text",
817
- text: "❌ Slack認証されていません。\n\n`npx slack-task-mcp auth` を実行して認証してください。",
889
+ text: "❌ Slack認証されていません。\n\n`npx @ignission/slack-task-mcp auth login` を実行して認証してください。",
818
890
  },
819
891
  ],
820
892
  };
@@ -825,10 +897,10 @@ server.tool(
825
897
  const fullQuery = channel ? `${query} in:#${channel}` : query;
826
898
 
827
899
  // 検索実行
828
- const { messages, total } = await searchSlackMessages(fullQuery, count);
900
+ const { messages, total } = await searchSlackMessages(slackClient, fullQuery, count);
829
901
 
830
902
  // 結果をフォーマット
831
- const formatted = await formatSearchResults(messages, total, count);
903
+ const formatted = await formatSearchResults(slackClient, messages, total, count);
832
904
 
833
905
  return {
834
906
  content: [
@@ -845,7 +917,7 @@ server.tool(
845
917
  content: [
846
918
  {
847
919
  type: "text",
848
- text: "❌ 検索権限がありません。\n\n`search:read` スコープが必要です。\n`npx slack-task-mcp auth` で再認証してください。",
920
+ text: "❌ 検索権限がありません。\n\n`search:read` スコープが必要です。\n`npx @ignission/slack-task-mcp auth login` で再認証してください。",
849
921
  },
850
922
  ],
851
923
  };
@@ -878,17 +950,18 @@ server.tool(
878
950
 
879
951
  // サーバー起動
880
952
  async function main() {
881
- // OAuth認証からトークンを取得
882
- const credentials = await loadCredentials();
883
- if (credentials?.access_token) {
884
- slackClient = new WebClient(credentials.access_token);
885
- } else {
953
+ // データディレクトリを初期化
954
+ await ensureDataDir();
955
+
956
+ // 認証状態を確認(起動時のログ)
957
+ const workspaces = await listWorkspaces();
958
+ if (workspaces.length === 0) {
886
959
  console.error("❌ Slack認証されていません。");
887
- console.error(" `npx slack-task-mcp auth` を実行して認証してください。");
960
+ console.error(" `npx @ignission/slack-task-mcp auth login` を実行して認証してください。");
961
+ } else {
962
+ console.error(`✅ ${workspaces.length} ワークスペース認証済み: ${workspaces.map(w => w.team_domain).join(", ")}`);
888
963
  }
889
964
 
890
- await initDataDir();
891
-
892
965
  const transport = new StdioServerTransport();
893
966
  await server.connect(transport);
894
967
  }
package/src/paths.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * XDG Base Directory準拠のパス解決モジュール
3
+ *
4
+ * macOS/Linux: XDG環境変数またはデフォルト値を使用
5
+ * - $XDG_CONFIG_HOME (デフォルト: ~/.config)
6
+ * - $XDG_DATA_HOME (デフォルト: ~/.local/share)
7
+ */
8
+
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
12
+ const APP_NAME = "slack-task-mcp";
13
+
14
+ /**
15
+ * XDG_CONFIG_HOMEのパスを取得
16
+ * @returns {string} 設定ディレクトリのパス
17
+ */
18
+ function getXdgConfigHome() {
19
+ return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
20
+ }
21
+
22
+ /**
23
+ * XDG_DATA_HOMEのパスを取得
24
+ * @returns {string} データディレクトリのパス
25
+ */
26
+ function getXdgDataHome() {
27
+ return process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
28
+ }
29
+
30
+ /**
31
+ * アプリケーションの設定ディレクトリを取得
32
+ * @returns {string} $XDG_CONFIG_HOME/slack-task-mcp
33
+ */
34
+ export function getConfigDir() {
35
+ return path.join(getXdgConfigHome(), APP_NAME);
36
+ }
37
+
38
+ /**
39
+ * アプリケーションのデータディレクトリを取得
40
+ * @returns {string} $XDG_DATA_HOME/slack-task-mcp
41
+ */
42
+ export function getDataDir() {
43
+ return path.join(getXdgDataHome(), APP_NAME);
44
+ }
45
+
46
+ /**
47
+ * 認証情報ディレクトリを取得
48
+ * @returns {string} $XDG_DATA_HOME/slack-task-mcp/credentials
49
+ */
50
+ export function getCredentialsDir() {
51
+ return path.join(getDataDir(), "credentials");
52
+ }
53
+
54
+ /**
55
+ * タスクファイルのパスを取得
56
+ * @returns {string} $XDG_DATA_HOME/slack-task-mcp/tasks.json
57
+ */
58
+ export function getTasksPath() {
59
+ return path.join(getDataDir(), "tasks.json");
60
+ }