@ignission/slack-task-mcp 0.1.0 → 0.2.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.ja.md +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/auth.js +63 -70
- package/src/cli.js +37 -18
- package/src/credentials.js +203 -0
- package/src/index.js +117 -44
- package/src/paths.js +60 -0
package/README.ja.md
CHANGED
|
@@ -4,7 +4,7 @@ Slack タスク管理 MCP サーバー for Claude Code / Claude Desktop
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/slack-task-mcp)
|
|
6
6
|
|
|
7
|
-
[English](
|
|
7
|
+
[English](https://github.com/ignission/slack-task-mcp/blob/main/packages/core/README.md)
|
|
8
8
|
|
|
9
9
|
## こんな人向け
|
|
10
10
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Slack Task Management MCP Server for Claude Code / Claude Desktop
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/slack-task-mcp)
|
|
6
6
|
|
|
7
|
-
[日本語](
|
|
7
|
+
[日本語](https://github.com/ignission/slack-task-mcp/blob/main/packages/core/README.ja.md)
|
|
8
8
|
|
|
9
9
|
## Who is this for?
|
|
10
10
|
|
package/package.json
CHANGED
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(`
|
|
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
|
|
148
|
+
const workspaces = await listWorkspaces();
|
|
177
149
|
|
|
178
150
|
console.log("📋 認証状態");
|
|
179
151
|
console.log("");
|
|
180
152
|
|
|
181
|
-
if (
|
|
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(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
179
|
+
export async function logout(options = {}) {
|
|
180
|
+
const { workspace } = options;
|
|
200
181
|
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
197
|
+
// 全ワークスペースをログアウト
|
|
198
|
+
const workspaces = await listWorkspaces();
|
|
207
199
|
|
|
208
|
-
if (
|
|
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
|
|
37
|
-
auth status
|
|
38
|
-
auth logout
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
48
|
-
npx slack-task-mcp auth
|
|
49
|
-
npx slack-task-mcp auth
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
+
}
|