@nocodelab/kadailog-mcp 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +138 -2
- package/dist/attachments.js +155 -0
- package/dist/index.js +492 -0
- package/dist/masters.js +99 -0
- package/package.json +37 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 deed LLC (ノーコードラボ)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,139 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nocodelab/kadailog-mcp
|
|
2
|
+
|
|
3
|
+
KadaiLog の公式 MCP サーバ。Claude Code / Codex などの AI エージェントから
|
|
4
|
+
KadaiLog の課題を読み書きするためのものです。
|
|
5
|
+
|
|
6
|
+
中身は KadaiLog の外部API(`/api/v1`)を叩くだけの薄いラッパーで、権限・入力検証・
|
|
7
|
+
変更履歴・通知はすべて KadaiLog 本体が担当します。
|
|
8
|
+
|
|
9
|
+
## 準備
|
|
10
|
+
|
|
11
|
+
KadaiLog にサインインし、**個人設定 > APIキー** でキーを発行します
|
|
12
|
+
(`tslg_…` で始まる文字列。表示されるのは発行直後の1回だけです)。
|
|
13
|
+
|
|
14
|
+
キーは**発行した本人の権限で動きます**。参加していないプロジェクトは、
|
|
15
|
+
このMCPサーバからも見えません。
|
|
16
|
+
メンバーのメールアドレスは**管理者の API キーでだけ返ります**。メンバー・ゲストのキーでは、
|
|
17
|
+
担当者・作成者・投稿者などは表示名で返り、`get_project_meta` のメンバーの `email` は `null` になります
|
|
18
|
+
(担当者の指定はメールアドレスでも表示名でもできます)。
|
|
19
|
+
|
|
20
|
+
発行時に、キーの使える範囲を絞れます(発行後は変えられないので、変えるときは新しいキーを発行します):
|
|
21
|
+
|
|
22
|
+
| 項目 | 選べる値 | 絞ったときの動き |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| 権限 | 読み書き(既定)/ 読み取り専用 | 読み取り専用キーで `create_issue` `update_issue` `add_comment` `update_comment` `mark_notifications_read` `create_wiki_page` `update_wiki_page` `link_issues` `unlink_issues` `watch_issue` `unwatch_issue`、マスタの `create_*` / `update_*` を呼ぶと、`insufficient_scope` のエラーになります |
|
|
25
|
+
| 対象プロジェクト | すべて(既定)/ 選んだプロジェクトだけ | 一覧・検索・通知・最近の更新は対象プロジェクトの分だけ返り、対象外のプロジェクトや課題・Wiki を直接指定すると `project_not_allowed` のエラーになります(使えるプロジェクトは `get_myself` の `apiKey.projects` で分かります) |
|
|
26
|
+
| 有効期限 | なし(既定)/ 30日 / 90日 / 1年 | 期限を過ぎると、どのツールも `api_key_expired` のエラーになります(いつ切れたかがメッセージに出ます) |
|
|
27
|
+
|
|
28
|
+
AI エージェントに渡すキーは、**読み取り専用・必要なプロジェクトだけ・期限つき**で発行しておくと、
|
|
29
|
+
エージェントが想定外の変更をしたり、キーが漏れたときの影響を小さくできます。
|
|
30
|
+
|
|
31
|
+
## 設定
|
|
32
|
+
|
|
33
|
+
Claude Code の場合、`.mcp.json` か `~/.claude.json` に追加します。
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"kadailog": {
|
|
39
|
+
"command": "npx",
|
|
40
|
+
"args": ["-y", "@nocodelab/kadailog-mcp"],
|
|
41
|
+
"env": {
|
|
42
|
+
"KADAILOG_API_KEY": "tslg_xxxxxxxxxxxx"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| 環境変数 | 必須 | 説明 |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `KADAILOG_API_KEY` | ○ | 個人設定で発行したAPIキー |
|
|
52
|
+
| `KADAILOG_API_URL` | | 接続先の上書き。既定は `https://api.kadailog.com/v1` |
|
|
53
|
+
|
|
54
|
+
改名前の `TASKLOG_API_KEY` / `TASKLOG_API_URL` も当面は読みます(新しい名前が優先)。
|
|
55
|
+
|
|
56
|
+
## ツール
|
|
57
|
+
|
|
58
|
+
| ツール | できること |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `get_myself` | 自分(APIキーの持ち主)の名前・権限とスペースの情報 |
|
|
61
|
+
| `list_my_issues` | 自分の担当課題(全プロジェクト・期限が近い順) |
|
|
62
|
+
| `list_notifications` | 自分宛ての通知(未読だけ・種類・フラグ付きで絞れる) |
|
|
63
|
+
| `mark_notifications_read` | 通知を既読にする(課題ごと/全部) |
|
|
64
|
+
| `list_projects` | 参加しているプロジェクトの一覧 |
|
|
65
|
+
| `get_project_meta` | そのプロジェクトのステータス・種別・カテゴリ・マイルストーン・メンバー一覧 |
|
|
66
|
+
| `list_issues` | 課題の絞り込み(担当者・ステータス・種別・優先度・マイルストーン・カテゴリ・更新日時・期限順など。各項目は複数指定でき、同じ項目の中は OR) |
|
|
67
|
+
| `search_issues` | 件名・本文のキーワード検索(スペース全体・日本語可) |
|
|
68
|
+
| `get_issue` | 課題1件の詳細(親子・関連課題・ウォッチ中かを含む) |
|
|
69
|
+
| `get_issue_history` | 課題の変更履歴(だれが・いつ・何を変えたか) |
|
|
70
|
+
| `link_issues` / `unlink_issues` | 関連課題をつなぐ・外す(別プロジェクトの課題とも。両方の課題に反映。読み取り専用キーでは不可) |
|
|
71
|
+
| `watch_issue` / `unwatch_issue` | 自分として課題をウォッチする・やめる(コメントや状態の変更が通知で届く。読み取り専用キーでは不可) |
|
|
72
|
+
| `list_watching` | 自分がウォッチ中の課題(全プロジェクト・更新の新しい順。`nextCursor` で続きを取れる) |
|
|
73
|
+
| `list_recent_activities` | 最近の更新(課題の作成・更新・コメント)。「昨日から何が変わった?」用 |
|
|
74
|
+
| `create_issue` | 課題の作成 |
|
|
75
|
+
| `update_issue` | 課題の更新(ステータス変更・担当変更・期限設定など) |
|
|
76
|
+
| `list_comments` | コメント一覧(各コメントの `id` は `update_comment` で使う) |
|
|
77
|
+
| `add_comment` | コメント追加 |
|
|
78
|
+
| `list_attachments` | 課題本体とコメントの添付ファイルの一覧 |
|
|
79
|
+
| `get_attachment` | 添付ファイル1件を読む。テキスト(md・txt・csv・json など)は本文を先頭1MBまで、画像(png・jpeg・gif・webp)は5MBまで画像として返す。それ以外(PDF など)はダウンロード用の URL(10分有効)を返す |
|
|
80
|
+
| `update_comment` | 自分のコメントの本文の編集(他の人のコメントは不可・画面には「(編集済み)」と出る) |
|
|
81
|
+
| `list_wiki_pages` | Wikiページの一覧・キーワード検索 |
|
|
82
|
+
| `get_wiki_page` | Wikiページの本文 |
|
|
83
|
+
| `create_wiki_page` | Wikiページの作成 |
|
|
84
|
+
| `update_wiki_page` | Wikiページのタイトル・本文の更新 |
|
|
85
|
+
| `create_milestone` / `update_milestone` | マイルストーン(発生バージョンと共通)の作成・名前と開始日・期限日の変更(管理者のみ) |
|
|
86
|
+
| `create_category` / `update_category` | カテゴリの作成・名前の変更(管理者のみ) |
|
|
87
|
+
| `create_type` / `update_type` | 種別の作成・名前と色の変更(管理者のみ) |
|
|
88
|
+
| `create_status` / `update_status` | ステータスの作成・名前と色の変更(管理者のみ。作れるのは完了扱いでないステータスだけ) |
|
|
89
|
+
|
|
90
|
+
マスタ(マイルストーン・カテゴリ・種別・ステータス)の作成・変更は、プロジェクト設定画面と同じく
|
|
91
|
+
**管理者のキーだけ**が使えます(メンバーのキーは `forbidden`、読み取り専用キーは `insufficient_scope`)。変えるときは今の名前で指定し、
|
|
92
|
+
同じ名前があると `conflict` になります。新しいステータスは完了扱いのステータスの直前に、新しい種別と
|
|
93
|
+
マイルストーンは末尾に入ります。削除・並べ替え・完了扱いの切り替え・使う項目の ON/OFF は画面から行ってください。
|
|
94
|
+
`create_issue` / `update_issue` は知らない名前のマスタを自動では作りません(先に `create_*` で作ります)。
|
|
95
|
+
|
|
96
|
+
課題は課題キー(`RENEW-12`)、プロジェクトはプロジェクトキー(`RENEW`)、Wikiページは
|
|
97
|
+
プロジェクトキー+タイトルで指定します。
|
|
98
|
+
ステータス・種別などはプロジェクトごとに違うため、名前で指定し、間違えると**候補付きの
|
|
99
|
+
エラー**が返ります(エージェントはそれを見て言い直せます)。
|
|
100
|
+
|
|
101
|
+
一覧系(`list_issues` / `list_my_issues` / `list_notifications` / `list_recent_activities` /
|
|
102
|
+
`list_wiki_pages` / `list_watching`)は、結果の `nextCursor` を次の呼び出しの `cursor` に渡すと続きを取れます
|
|
103
|
+
(`null` なら続きなし)。
|
|
104
|
+
|
|
105
|
+
## 注意
|
|
106
|
+
|
|
107
|
+
- 画面から操作したときと同じように、**変更履歴に残り、関係者に通知が飛びます**
|
|
108
|
+
(変更履歴には API 経由であることも記録されます)
|
|
109
|
+
- 添付ファイルは読むだけです(課題本体とコメントの添付。アップロード・削除と、プロジェクトの「ファイル」・Wiki の添付は未対応)。
|
|
110
|
+
`get_attachment` は受け取った URL から MCP サーバ自身がファイルを取ってきて返します。
|
|
111
|
+
Wiki の削除もできません(画面から行ってください)
|
|
112
|
+
- 作成系(課題・コメント・Wiki・マスタの作成など)は、呼び出しごとに `Idempotency-Key` を付けて送ります。
|
|
113
|
+
接続が切れたときは同じキーで2回まで送り直すので、実は1回目が成功していても二重にはなりません
|
|
114
|
+
- レート制限は 1キーあたり 60リクエスト/分
|
|
115
|
+
|
|
116
|
+
## エラーの読みかた
|
|
117
|
+
|
|
118
|
+
ツールの結果がエラーのときは、KadaiLog が返した `message` と `code` をそのまま出します
|
|
119
|
+
(例: `このAPIキーは読み取り専用です(必要: 読み書き)。… (code: insufficient_scope)`)。
|
|
120
|
+
キーに関わるものは、何をすれば直るかの案内も添えます。
|
|
121
|
+
|
|
122
|
+
| code | 意味 | 直しかた |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `unauthorized` | キーが無効(失効させた・設定の値が違う) | 個人設定でキーを確認し、`KADAILOG_API_KEY` を入れ替える |
|
|
125
|
+
| `api_key_expired` | キーの有効期限切れ | 新しいキーを発行して `KADAILOG_API_KEY` を入れ替える |
|
|
126
|
+
| `insufficient_scope` | 読み取り専用キーで書き込もうとした | 読み書きのキーを発行して入れ替える(または書き込みを人が行う) |
|
|
127
|
+
| `project_not_allowed` | キーの対象外のプロジェクト | 対象を広げたキーを発行して入れ替える |
|
|
128
|
+
| `not_found` | 課題・プロジェクト・ページ・添付ファイルがない(参加していないプロジェクトも含む。添付ファイルはキーの対象外のプロジェクトのものも) | キーや名前を確認する(候補がメッセージに出ます) |
|
|
129
|
+
| `forbidden` | 権限のない操作(ゲストのマスタ操作・読み取り専用スペースへの書き込みなど) | 画面の権限と同じ。管理者に相談する |
|
|
130
|
+
|
|
131
|
+
## 開発
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm install
|
|
135
|
+
npm run build
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`KADAILOG_API_URL` に開発環境の URL を渡せば、そちらに向けて動作確認できます。
|
|
2
139
|
|
|
3
|
-
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const TEXT_MAX_BYTES = 1024 * 1024;
|
|
3
|
+
const IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
4
|
+
const TEXT_EXTENSIONS = new Set(["md", "markdown", "txt", "csv", "tsv", "json", "log", "yaml", "yml"]);
|
|
5
|
+
const IMAGE_TYPES = {
|
|
6
|
+
png: "image/png",
|
|
7
|
+
jpg: "image/jpeg",
|
|
8
|
+
jpeg: "image/jpeg",
|
|
9
|
+
gif: "image/gif",
|
|
10
|
+
webp: "image/webp",
|
|
11
|
+
};
|
|
12
|
+
function extensionOf(fileName) {
|
|
13
|
+
const dot = fileName.lastIndexOf(".");
|
|
14
|
+
return dot < 0 ? "" : fileName.slice(dot + 1).toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function isText(att) {
|
|
17
|
+
const type = att.contentType.toLowerCase();
|
|
18
|
+
return (type.startsWith("text/") ||
|
|
19
|
+
type === "application/json" ||
|
|
20
|
+
TEXT_EXTENSIONS.has(extensionOf(att.fileName)));
|
|
21
|
+
}
|
|
22
|
+
function imageMimeType(att) {
|
|
23
|
+
const type = att.contentType.toLowerCase();
|
|
24
|
+
if (Object.values(IMAGE_TYPES).includes(type))
|
|
25
|
+
return type;
|
|
26
|
+
if (type === "image/jpg")
|
|
27
|
+
return "image/jpeg";
|
|
28
|
+
if (type === "" || type === "application/octet-stream") {
|
|
29
|
+
return IMAGE_TYPES[extensionOf(att.fileName)] ?? null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
async function readCapped(response, maxBytes) {
|
|
34
|
+
const reader = response.body?.getReader();
|
|
35
|
+
if (!reader)
|
|
36
|
+
return { bytes: new Uint8Array(), truncated: false };
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let total = 0;
|
|
39
|
+
let truncated = false;
|
|
40
|
+
for (;;) {
|
|
41
|
+
const { done, value } = await reader.read();
|
|
42
|
+
if (done)
|
|
43
|
+
break;
|
|
44
|
+
if (total + value.length > maxBytes) {
|
|
45
|
+
chunks.push(value.subarray(0, maxBytes - total));
|
|
46
|
+
total = maxBytes;
|
|
47
|
+
truncated = true;
|
|
48
|
+
await reader.cancel();
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
chunks.push(value);
|
|
52
|
+
total += value.length;
|
|
53
|
+
}
|
|
54
|
+
const bytes = new Uint8Array(total);
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const chunk of chunks) {
|
|
57
|
+
bytes.set(chunk, offset);
|
|
58
|
+
offset += chunk.length;
|
|
59
|
+
}
|
|
60
|
+
return { bytes, truncated };
|
|
61
|
+
}
|
|
62
|
+
function decodeText(bytes) {
|
|
63
|
+
try {
|
|
64
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: true });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
try {
|
|
68
|
+
return new TextDecoder("shift_jis", { fatal: true }).decode(bytes, { stream: true });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return new TextDecoder("utf-8").decode(bytes, { stream: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function json(data) {
|
|
76
|
+
return { type: "text", text: JSON.stringify(data, null, 2) };
|
|
77
|
+
}
|
|
78
|
+
function metadataOnly(att, note) {
|
|
79
|
+
return { content: [json({ ...att, note })] };
|
|
80
|
+
}
|
|
81
|
+
async function getAttachment(request, id) {
|
|
82
|
+
const result = await request("GET", `/attachments/${encodeURIComponent(id)}`);
|
|
83
|
+
if (result.isError)
|
|
84
|
+
return result;
|
|
85
|
+
const first = result.content[0];
|
|
86
|
+
const att = JSON.parse(first.type === "text" ? first.text : "{}");
|
|
87
|
+
const { downloadUrl, expiresAt, ...meta } = att;
|
|
88
|
+
const image = imageMimeType(att);
|
|
89
|
+
const text = !image && isText(att);
|
|
90
|
+
if (!image && !text) {
|
|
91
|
+
return metadataOnly(att, `この形式(${att.contentType})は中身を返せません。downloadUrl は ${expiresAt} まで有効です。`);
|
|
92
|
+
}
|
|
93
|
+
if (image && att.fileSize > IMAGE_MAX_BYTES) {
|
|
94
|
+
return metadataOnly(att, `画像が大きい(${IMAGE_MAX_BYTES / 1024 / 1024}MB超)ため中身は返していません。downloadUrl は ${expiresAt} まで有効です。`);
|
|
95
|
+
}
|
|
96
|
+
let response;
|
|
97
|
+
try {
|
|
98
|
+
response = await fetch(downloadUrl);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return metadataOnly(att, `ファイルを取得できませんでした(${err instanceof Error ? err.message : String(err)})。downloadUrl は ${expiresAt} まで有効です。`);
|
|
102
|
+
}
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
return metadataOnly(att, `ファイルを取得できませんでした(HTTP ${response.status})。`);
|
|
105
|
+
}
|
|
106
|
+
if (image) {
|
|
107
|
+
const { bytes, truncated } = await readCapped(response, IMAGE_MAX_BYTES);
|
|
108
|
+
if (truncated) {
|
|
109
|
+
return metadataOnly(att, `画像が大きいため中身は返していません。downloadUrl は ${expiresAt} まで有効です。`);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
json(meta),
|
|
114
|
+
{ type: "image", data: Buffer.from(bytes).toString("base64"), mimeType: image },
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const { bytes, truncated } = await readCapped(response, TEXT_MAX_BYTES);
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
json({
|
|
122
|
+
...meta,
|
|
123
|
+
...(truncated
|
|
124
|
+
? {
|
|
125
|
+
truncated: true,
|
|
126
|
+
note: `先頭 ${TEXT_MAX_BYTES / 1024 / 1024}MB だけを返しています。全体は downloadUrl(${expiresAt} まで有効)から取得できます。`,
|
|
127
|
+
downloadUrl,
|
|
128
|
+
expiresAt,
|
|
129
|
+
}
|
|
130
|
+
: {}),
|
|
131
|
+
}),
|
|
132
|
+
{ type: "text", text: decodeText(bytes) },
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function registerAttachmentTools(server, request, issueKey) {
|
|
137
|
+
server.registerTool("list_attachments", {
|
|
138
|
+
title: "添付ファイルの一覧",
|
|
139
|
+
description: "課題本体とコメントに添付されたファイルの一覧(古い順)を返す。各ファイルの id を get_attachment に渡すと中身を読める。" +
|
|
140
|
+
"target は issue=課題本体 / comment=コメント(commentId はそのコメントの id)。" +
|
|
141
|
+
"本文中の `attachment:<id>` という参照の id もこの id と同じ。",
|
|
142
|
+
inputSchema: { key: issueKey },
|
|
143
|
+
}, async ({ key }) => await request("GET", `/issues/${encodeURIComponent(key)}/attachments`));
|
|
144
|
+
server.registerTool("get_attachment", {
|
|
145
|
+
title: "添付ファイルを読む",
|
|
146
|
+
description: "添付ファイル1件を読む。テキスト(md・txt・csv・json など)は本文をテキストで(先頭1MBまで)、" +
|
|
147
|
+
"画像(png・jpeg・gif・webp、5MBまで)は画像として返す。それ以外の形式(PDF・Office など)は" +
|
|
148
|
+
"メタデータとダウンロード用の URL(downloadUrl。expiresAt まで有効・10分)だけを返す。",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
id: z
|
|
151
|
+
.string()
|
|
152
|
+
.describe("添付ファイルの id(list_attachments の id、または本文中の attachment:<id> の部分)"),
|
|
153
|
+
},
|
|
154
|
+
}, async ({ id }) => await getAttachment(request, id));
|
|
155
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { registerAttachmentTools } from "./attachments.js";
|
|
8
|
+
import { registerMasterTools } from "./masters.js";
|
|
9
|
+
const DEFAULT_API_URL = "https://api.kadailog.com/v1";
|
|
10
|
+
const API_URL = (process.env.KADAILOG_API_URL ||
|
|
11
|
+
process.env.TASKLOG_API_URL ||
|
|
12
|
+
DEFAULT_API_URL).replace(/\/+$/, "");
|
|
13
|
+
const API_KEY = process.env.KADAILOG_API_KEY || process.env.TASKLOG_API_KEY;
|
|
14
|
+
if (!API_KEY) {
|
|
15
|
+
console.error("KADAILOG_API_KEY が設定されていません。KadaiLog の「個人設定 > APIキー」で発行し、" +
|
|
16
|
+
"MCPサーバの env に設定してください。");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
function ok(data) {
|
|
20
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
21
|
+
}
|
|
22
|
+
function fail(message) {
|
|
23
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
24
|
+
}
|
|
25
|
+
const API_KEY_HINTS = {
|
|
26
|
+
api_key_expired: "APIキーの有効期限が切れています。KadaiLog の「個人設定 > APIキー」で新しいキーを発行し、" +
|
|
27
|
+
"MCP の設定(KADAILOG_API_KEY)を入れ替えてください。",
|
|
28
|
+
insufficient_scope: "このAPIキーは読み取り専用で、書き込みはできません。書き込みが必要なら、" +
|
|
29
|
+
"「個人設定 > APIキー」で読み書きのキーを発行して設定を入れ替えてください。",
|
|
30
|
+
project_not_allowed: "このAPIキーは一部のプロジェクトだけに使えます。使えるプロジェクトは get_myself の apiKey.projects で分かります。" +
|
|
31
|
+
"ほかのプロジェクトも扱うなら、対象を広げたキーを発行して設定を入れ替えてください。",
|
|
32
|
+
unauthorized: "APIキーが無効です(失効させた・設定の値が違う)。「個人設定 > APIキー」で確認してください。",
|
|
33
|
+
};
|
|
34
|
+
const MAX_ATTEMPTS = 3;
|
|
35
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
async function request(method, path, options = {}) {
|
|
37
|
+
const url = new URL(API_URL + path);
|
|
38
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
39
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
40
|
+
if (item !== undefined && item !== null && item !== "") {
|
|
41
|
+
url.searchParams.append(key, String(item));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const idempotencyKey = method === "POST" ? randomUUID() : undefined;
|
|
46
|
+
let response;
|
|
47
|
+
for (let attempt = 1;; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
response = await fetch(url, {
|
|
50
|
+
method,
|
|
51
|
+
headers: {
|
|
52
|
+
authorization: `Bearer ${API_KEY}`,
|
|
53
|
+
...(options.body === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { "content-type": "application/json" }),
|
|
56
|
+
...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
|
|
57
|
+
},
|
|
58
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (idempotencyKey && attempt < MAX_ATTEMPTS) {
|
|
63
|
+
await sleep(attempt * 1000);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
return fail(`KadaiLog に接続できませんでした (${API_URL}): ${err instanceof Error ? err.message : String(err)}`);
|
|
67
|
+
}
|
|
68
|
+
if (idempotencyKey && response.status === 409 && attempt < MAX_ATTEMPTS) {
|
|
69
|
+
const code = await response
|
|
70
|
+
.clone()
|
|
71
|
+
.json()
|
|
72
|
+
.then((b) => b.error?.code)
|
|
73
|
+
.catch(() => undefined);
|
|
74
|
+
if (code === "idempotency_in_progress") {
|
|
75
|
+
await sleep(attempt * 2000);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const text = await response.text();
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return fail(`KadaiLog から予期しない応答が返りました: ${text.slice(0, 300)}`);
|
|
88
|
+
}
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const error = parsed
|
|
91
|
+
.error;
|
|
92
|
+
if (response.status === 503 && error?.code === "maintenance") {
|
|
93
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
94
|
+
const wait = Number.isFinite(retryAfter) && retryAfter > 0
|
|
95
|
+
? ` 約${Math.max(1, Math.ceil(retryAfter / 60))}分後に再試行してください。`
|
|
96
|
+
: "";
|
|
97
|
+
return fail(`${error.message ?? "KadaiLog はメンテナンス中です"}${wait} (code: maintenance)`);
|
|
98
|
+
}
|
|
99
|
+
const code = error?.code ?? String(response.status);
|
|
100
|
+
const hint = API_KEY_HINTS[code];
|
|
101
|
+
return fail(`${error?.message ?? "エラーが発生しました"} (code: ${code})` +
|
|
102
|
+
(hint ? `\n${hint}` : ""));
|
|
103
|
+
}
|
|
104
|
+
return ok(parsed);
|
|
105
|
+
}
|
|
106
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
107
|
+
const server = new McpServer({ name: "kadailog", version });
|
|
108
|
+
const issueKey = z
|
|
109
|
+
.string()
|
|
110
|
+
.describe("課題キー。例: RENEW-12(list_issues や search_issues の key)");
|
|
111
|
+
const projectKey = z
|
|
112
|
+
.string()
|
|
113
|
+
.describe("プロジェクトキー。例: RENEW(list_projects の key)");
|
|
114
|
+
const statusArg = z
|
|
115
|
+
.string()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("ステータスの名前。プロジェクト固有なので get_project_meta で確認すること");
|
|
118
|
+
const typeArg = z
|
|
119
|
+
.string()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe("種別の名前。プロジェクト固有なので get_project_meta で確認すること");
|
|
122
|
+
const priorityArg = z
|
|
123
|
+
.enum(["high", "medium", "low"])
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("優先度。省略時は medium");
|
|
126
|
+
const assigneeArg = z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("担当者。メールアドレスか表示名、自分自身なら \"me\"。update_issue で空文字を渡すと担当を外す");
|
|
130
|
+
const dueDateArg = z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe("期限日 YYYY-MM-DD。update_issue で空文字を渡すと期限を外す");
|
|
134
|
+
const categoriesArg = z
|
|
135
|
+
.array(z.string())
|
|
136
|
+
.optional()
|
|
137
|
+
.describe("カテゴリ名の配列。指定した内容で置き換わる");
|
|
138
|
+
const milestonesArg = z
|
|
139
|
+
.array(z.string())
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("マイルストーン名の配列。指定した内容で置き換わる");
|
|
142
|
+
const versionsArg = z
|
|
143
|
+
.array(z.string())
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("発生バージョン名の配列。指定した内容で置き換わる。マスタはマイルストーンと共通" +
|
|
146
|
+
"(get_project_meta の milestones)。features.versions が ON のプロジェクトで使う");
|
|
147
|
+
const parentArg = z
|
|
148
|
+
.string()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe("親課題の課題キー(例 RENEW-12)。同じプロジェクトの課題のみ・階層は1段まで" +
|
|
151
|
+
"(親を持つ課題は親になれない)。update_issue で空文字を渡すと親子関係を外す");
|
|
152
|
+
const filterArg = (description) => z
|
|
153
|
+
.union([z.string(), z.array(z.string())])
|
|
154
|
+
.optional()
|
|
155
|
+
.describe(`${description}。複数指定するときは配列で渡すと、どれかに当てはまる課題を返す(OR)`);
|
|
156
|
+
const limitArg = (fallback) => z.number().optional().describe(`最大件数(既定${fallback}・上限100)`);
|
|
157
|
+
const cursorArg = z
|
|
158
|
+
.string()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("続きを取るときに、前回の結果の nextCursor をそのまま渡す。nextCursor が null なら続きはない" +
|
|
161
|
+
"(0件でも nextCursor があれば、まだ続きがある)");
|
|
162
|
+
const wikiTitleArg = z
|
|
163
|
+
.string()
|
|
164
|
+
.describe("Wikiページのタイトル。「/」区切りは階層(例: 10. 概要/01. 用語集)。list_wiki_pages の title をそのまま渡す");
|
|
165
|
+
server.registerTool("get_myself", {
|
|
166
|
+
title: "自分の情報",
|
|
167
|
+
description: "このAPIキーの持ち主(自分)の表示名・メールアドレス・権限と、スペースの名前・URLを返す。" +
|
|
168
|
+
"「自分の〜」を扱うときの起点。apiKey にこのキーのスコープ(scope: read=読み取り専用 / write=読み書き、" +
|
|
169
|
+
"projects: 使えるプロジェクトキーの配列・null なら全部、expiresAt: 有効期限)も入るので、" +
|
|
170
|
+
"書き込みや別プロジェクトの操作を頼まれて断られたときはここで確認する。",
|
|
171
|
+
inputSchema: {},
|
|
172
|
+
}, async () => await request("GET", "/me"));
|
|
173
|
+
server.registerTool("list_projects", {
|
|
174
|
+
title: "プロジェクト一覧",
|
|
175
|
+
description: "参加しているプロジェクトの一覧を返す。他のツールに渡すプロジェクトキーはここで調べる。",
|
|
176
|
+
inputSchema: {},
|
|
177
|
+
}, async () => await request("GET", "/projects"));
|
|
178
|
+
server.registerTool("get_project_meta", {
|
|
179
|
+
title: "プロジェクトの設定値",
|
|
180
|
+
description: "プロジェクトで使えるステータス・種別・カテゴリ・マイルストーン・メンバーの一覧を返す。" +
|
|
181
|
+
"メンバーの email は管理者の APIキーでだけ返り、それ以外は null(担当者は name でも指定できる)。" +
|
|
182
|
+
"ステータスや種別はプロジェクトごとに違うので、create_issue / update_issue の前にこれで確認すること。" +
|
|
183
|
+
"どのステータスが「完了」扱いかも isDone で分かる。" +
|
|
184
|
+
"足りないマスタは create_milestone / create_category / create_type / create_status で作り、" +
|
|
185
|
+
"名前や色は update_milestone / update_category / update_type / update_status で変えられる(管理者のキーのみ)。" +
|
|
186
|
+
"create_issue / update_issue は知らない名前を自動では作らない。",
|
|
187
|
+
inputSchema: { project: projectKey },
|
|
188
|
+
}, async ({ project }) => await request("GET", `/projects/${encodeURIComponent(project)}/meta`));
|
|
189
|
+
server.registerTool("list_issues", {
|
|
190
|
+
title: "課題の一覧・絞り込み",
|
|
191
|
+
description: "課題を条件で絞り込んで返す。project を指定するのが基本。" +
|
|
192
|
+
"project を省略できるのは assignee を指定し state が open か closed のときだけ" +
|
|
193
|
+
"(プロジェクト横断で「自分の未完了課題」を見る用途)。" +
|
|
194
|
+
"キーワードで探したいだけなら search_issues の方が適している。" +
|
|
195
|
+
"status / type / priority / assignee / category / milestone / version は配列で複数指定でき、" +
|
|
196
|
+
"同じ項目の中は OR・項目どうしは AND になる" +
|
|
197
|
+
"(例: status に [\"処理済み\", \"テスト中\"]、priority に [\"high\", \"medium\"])。",
|
|
198
|
+
inputSchema: {
|
|
199
|
+
project: projectKey.optional(),
|
|
200
|
+
assignee: filterArg("担当者で絞る。メールアドレス・表示名・\"me\"、未割り当ては \"none\""),
|
|
201
|
+
state: z
|
|
202
|
+
.enum(["open", "closed", "all"])
|
|
203
|
+
.optional()
|
|
204
|
+
.describe("未完了 open(既定) / 完了 closed / 全部 all"),
|
|
205
|
+
status: filterArg("ステータスの名前で絞る。プロジェクト固有なので get_project_meta で確認すること"),
|
|
206
|
+
type: filterArg("種別の名前で絞る。プロジェクト固有なので get_project_meta で確認すること"),
|
|
207
|
+
priority: filterArg("優先度で絞る。high / medium / low"),
|
|
208
|
+
milestone: filterArg("マイルストーン名で絞る"),
|
|
209
|
+
version: filterArg("発生バージョン名で絞る"),
|
|
210
|
+
category: filterArg("カテゴリ名で絞る"),
|
|
211
|
+
q: z
|
|
212
|
+
.string()
|
|
213
|
+
.optional()
|
|
214
|
+
.describe("件名・本文の部分一致で絞る(単純な部分一致。全文検索は search_issues)"),
|
|
215
|
+
sort: z
|
|
216
|
+
.enum(["updated", "due", "key"])
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("並び順。updated=更新新しい順(既定) / due=期限近い順 / key=キー順。" +
|
|
219
|
+
"cursor で全件をたどるときは、途中の更新で順番がずれない key を使う"),
|
|
220
|
+
updatedSince: z
|
|
221
|
+
.string()
|
|
222
|
+
.optional()
|
|
223
|
+
.describe("この日時以降に更新された課題だけ(ISO 8601。例 2026-10-11T09:00:00+09:00。時差を省くと日本時間)"),
|
|
224
|
+
limit: limitArg(50),
|
|
225
|
+
cursor: cursorArg,
|
|
226
|
+
},
|
|
227
|
+
}, async (args) => await request("GET", "/issues", { query: args }));
|
|
228
|
+
server.registerTool("list_my_issues", {
|
|
229
|
+
title: "自分の担当課題",
|
|
230
|
+
description: "自分が担当の課題を、参加している全プロジェクトから期限が近い順に返す(期限なしは後ろ)。" +
|
|
231
|
+
"「今日やること」「自分のタスク」を聞かれたらまずこれ。",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
state: z
|
|
234
|
+
.enum(["open", "closed"])
|
|
235
|
+
.optional()
|
|
236
|
+
.describe("未完了 open(既定) / 完了 closed"),
|
|
237
|
+
sort: z
|
|
238
|
+
.enum(["due", "updated", "key"])
|
|
239
|
+
.optional()
|
|
240
|
+
.describe("並び順。due=期限近い順(既定) / updated=更新新しい順 / key=キー順。" +
|
|
241
|
+
"cursor で全件をたどるときは、途中の更新で順番がずれない key を使う"),
|
|
242
|
+
limit: limitArg(50),
|
|
243
|
+
cursor: cursorArg,
|
|
244
|
+
},
|
|
245
|
+
}, async ({ state, sort, limit, cursor }) => await request("GET", "/issues", {
|
|
246
|
+
query: {
|
|
247
|
+
assignee: "me",
|
|
248
|
+
state: state ?? "open",
|
|
249
|
+
sort: sort ?? "due",
|
|
250
|
+
limit,
|
|
251
|
+
cursor,
|
|
252
|
+
},
|
|
253
|
+
}));
|
|
254
|
+
server.registerTool("search_issues", {
|
|
255
|
+
title: "課題の全文検索",
|
|
256
|
+
description: "スペース全体の課題を件名・本文のキーワードで検索する。日本語もそのまま渡してよい(2文字以上)。" +
|
|
257
|
+
"プロジェクトやステータスが分かっているなら list_issues の方が確実。",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
q: z.string().describe("検索キーワード(2文字以上)"),
|
|
260
|
+
limit: z.number().optional().describe("最大件数(既定20・上限100)"),
|
|
261
|
+
},
|
|
262
|
+
}, async ({ q, limit }) => await request("GET", "/search", { query: { q, limit } }));
|
|
263
|
+
server.registerTool("get_issue", {
|
|
264
|
+
title: "課題の詳細",
|
|
265
|
+
description: "課題キーを指定して1件の詳細を返す。親課題(parent)・子課題の一覧(children)・関連課題の一覧(related)・自分がウォッチ中か(watching)も含まれる。",
|
|
266
|
+
inputSchema: { key: issueKey },
|
|
267
|
+
}, async ({ key }) => await request("GET", `/issues/${encodeURIComponent(key)}`));
|
|
268
|
+
server.registerTool("create_issue", {
|
|
269
|
+
title: "課題を作成",
|
|
270
|
+
description: "課題を新規作成する。status / type / categories / milestones はプロジェクト固有の名前なので、" +
|
|
271
|
+
"自信がなければ先に get_project_meta を呼ぶこと。省略時は先頭のステータス・種別・優先度 medium になる。" +
|
|
272
|
+
"担当者を指定すると、その人に通知とメールが飛ぶ。",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
project: projectKey,
|
|
275
|
+
title: z.string().describe("件名"),
|
|
276
|
+
description: z.string().optional().describe("本文(Markdown)"),
|
|
277
|
+
status: statusArg,
|
|
278
|
+
type: typeArg,
|
|
279
|
+
priority: priorityArg,
|
|
280
|
+
assignee: assigneeArg,
|
|
281
|
+
dueDate: dueDateArg,
|
|
282
|
+
categories: categoriesArg,
|
|
283
|
+
milestones: milestonesArg,
|
|
284
|
+
versions: versionsArg,
|
|
285
|
+
parent: parentArg,
|
|
286
|
+
},
|
|
287
|
+
}, async (args) => await request("POST", "/issues", { body: args }));
|
|
288
|
+
server.registerTool("update_issue", {
|
|
289
|
+
title: "課題を更新",
|
|
290
|
+
description: "課題の項目を変更する。渡した項目だけが変わり、省略した項目はそのまま。" +
|
|
291
|
+
"課題を閉じるには status に「完了」扱いのステータス名を渡す(どれが完了扱いかは get_project_meta の isDone)。" +
|
|
292
|
+
"担当・期限・親課題を外したいときは空文字 \"\" を渡す。変更は課題の変更履歴に残り、関係者に通知される。",
|
|
293
|
+
inputSchema: {
|
|
294
|
+
key: issueKey,
|
|
295
|
+
title: z.string().optional().describe("件名"),
|
|
296
|
+
description: z.string().optional().describe("本文(Markdown)。全文が置き換わる"),
|
|
297
|
+
status: statusArg,
|
|
298
|
+
type: typeArg,
|
|
299
|
+
priority: priorityArg,
|
|
300
|
+
assignee: assigneeArg,
|
|
301
|
+
dueDate: dueDateArg,
|
|
302
|
+
categories: categoriesArg,
|
|
303
|
+
milestones: milestonesArg,
|
|
304
|
+
versions: versionsArg,
|
|
305
|
+
parent: parentArg,
|
|
306
|
+
},
|
|
307
|
+
}, async ({ key, ...body }) => await request("PATCH", `/issues/${encodeURIComponent(key)}`, { body }));
|
|
308
|
+
server.registerTool("get_issue_history", {
|
|
309
|
+
title: "課題の変更履歴",
|
|
310
|
+
description: "課題の変更履歴(だれが・いつ・何を変えたか)を古い順に返す。ステータス・担当者・期限日などの変更が分かる。" +
|
|
311
|
+
"via は app=画面から / api=外部API・MCP から。コメントは含まないので list_comments を使う。",
|
|
312
|
+
inputSchema: { key: issueKey },
|
|
313
|
+
}, async ({ key }) => await request("GET", `/issues/${encodeURIComponent(key)}/history`));
|
|
314
|
+
server.registerTool("link_issues", {
|
|
315
|
+
title: "関連課題をつなぐ",
|
|
316
|
+
description: "2つの課題を関連課題としてつなぐ(親子ではない対等なつながり・両方の課題に表示される)。" +
|
|
317
|
+
"別プロジェクトの課題ともつなげる。すでにつながっていれば added=false。" +
|
|
318
|
+
"親子関係にしたいときは update_issue の parent を使う。結果は get_issue の related で確認できる。",
|
|
319
|
+
inputSchema: {
|
|
320
|
+
key: issueKey,
|
|
321
|
+
other: z.string().describe("つなぐ相手の課題キー。例: RENEW-15"),
|
|
322
|
+
},
|
|
323
|
+
}, async ({ key, other }) => await request("POST", `/issues/${encodeURIComponent(key)}/relations`, {
|
|
324
|
+
body: { issue: other },
|
|
325
|
+
}));
|
|
326
|
+
server.registerTool("unlink_issues", {
|
|
327
|
+
title: "関連課題を外す",
|
|
328
|
+
description: "2つの課題の関連課題のつながりを外す(両方の課題から消える)。つながっていなければ removed=false。",
|
|
329
|
+
inputSchema: {
|
|
330
|
+
key: issueKey,
|
|
331
|
+
other: z.string().describe("外す相手の課題キー(get_issue の related の key)"),
|
|
332
|
+
},
|
|
333
|
+
}, async ({ key, other }) => await request("DELETE", `/issues/${encodeURIComponent(key)}/relations/${encodeURIComponent(other)}`));
|
|
334
|
+
server.registerTool("watch_issue", {
|
|
335
|
+
title: "課題をウォッチする",
|
|
336
|
+
description: "自分(APIキーの持ち主)として課題をウォッチする。担当でなくても、その課題のコメント・ステータス・" +
|
|
337
|
+
"担当者・期限日の変更が通知で届くようになる。アーカイブ済みプロジェクトの課題はウォッチできない。",
|
|
338
|
+
inputSchema: { key: issueKey },
|
|
339
|
+
}, async ({ key }) => await request("POST", `/issues/${encodeURIComponent(key)}/watch`));
|
|
340
|
+
server.registerTool("unwatch_issue", {
|
|
341
|
+
title: "課題のウォッチをやめる",
|
|
342
|
+
description: "自分(APIキーの持ち主)の課題のウォッチをやめる。その課題の通知が届かなくなる。",
|
|
343
|
+
inputSchema: { key: issueKey },
|
|
344
|
+
}, async ({ key }) => await request("DELETE", `/issues/${encodeURIComponent(key)}/watch`));
|
|
345
|
+
server.registerTool("list_watching", {
|
|
346
|
+
title: "ウォッチ中の課題",
|
|
347
|
+
description: "自分がウォッチしている課題を、参加している全プロジェクトから更新の新しい順に返す" +
|
|
348
|
+
"(アーカイブ済みプロジェクトの課題は除く)。返す形は list_issues と同じ。",
|
|
349
|
+
inputSchema: {
|
|
350
|
+
state: z
|
|
351
|
+
.enum(["open", "closed", "all"])
|
|
352
|
+
.optional()
|
|
353
|
+
.describe("未完了 open(既定) / 完了 closed / 全部 all"),
|
|
354
|
+
limit: limitArg(50),
|
|
355
|
+
cursor: cursorArg,
|
|
356
|
+
},
|
|
357
|
+
}, async (args) => await request("GET", "/watching", { query: args }));
|
|
358
|
+
server.registerTool("list_recent_activities", {
|
|
359
|
+
title: "最近の更新",
|
|
360
|
+
description: "スペース(または指定プロジェクト)の最近の更新を新しい順に返す。課題の作成・更新(変更内容つき)とコメント追加。" +
|
|
361
|
+
"「昨日から何が変わった?」は since に昨日の日時を渡す。コメントは先頭300文字まで(全文は list_comments)。" +
|
|
362
|
+
"Backlog から移行したコメントは含まない。" +
|
|
363
|
+
"同じ人が10分以内に続けて変えた内容は1件にまとまり、時刻は最初の変更のもの" +
|
|
364
|
+
"(短い間隔で since を進めながら見張ると取りこぼすことがあるので、見張る用途は少し前から重ねて取る)。",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
project: projectKey.optional().describe("プロジェクトキーで絞る(省略=参加している全プロジェクト)"),
|
|
367
|
+
since: z
|
|
368
|
+
.string()
|
|
369
|
+
.optional()
|
|
370
|
+
.describe("この日時より後の更新だけ(ISO 8601。例 2026-10-10T00:00:00+09:00。時差を省くと日本時間)"),
|
|
371
|
+
limit: limitArg(30),
|
|
372
|
+
cursor: cursorArg,
|
|
373
|
+
},
|
|
374
|
+
}, async (args) => await request("GET", "/activities", { query: args }));
|
|
375
|
+
server.registerTool("list_notifications", {
|
|
376
|
+
title: "自分宛ての通知",
|
|
377
|
+
description: "自分宛ての通知(メンション・担当に設定・ウォッチ中の課題のコメントなど)を新しい順に返す。" +
|
|
378
|
+
"「自分宛てのメンション」は type=mention で絞る。flagged は利用者が画面で付けた「後で見返す」目印。" +
|
|
379
|
+
"確認し終えたら mark_notifications_read で既読にできる。",
|
|
380
|
+
inputSchema: {
|
|
381
|
+
unread: z.boolean().optional().describe("true=未読だけ"),
|
|
382
|
+
type: z
|
|
383
|
+
.enum([
|
|
384
|
+
"mention",
|
|
385
|
+
"assigned",
|
|
386
|
+
"commented",
|
|
387
|
+
"status_changed",
|
|
388
|
+
"assignee_changed",
|
|
389
|
+
"due_changed",
|
|
390
|
+
"due_today",
|
|
391
|
+
"starred",
|
|
392
|
+
"support_inquiry",
|
|
393
|
+
])
|
|
394
|
+
.optional()
|
|
395
|
+
.describe("種類で絞る。mention=メンション / assigned=担当に設定 / commented=コメント / " +
|
|
396
|
+
"status_changed=ステータス変更 / assignee_changed=担当者変更 / due_changed=期限日変更 / " +
|
|
397
|
+
"due_today=今日が期限 / starred=スター / support_inquiry=お問い合わせ"),
|
|
398
|
+
flagged: z.boolean().optional().describe("true=フラグ付きだけ"),
|
|
399
|
+
limit: limitArg(20),
|
|
400
|
+
cursor: cursorArg,
|
|
401
|
+
},
|
|
402
|
+
}, async (args) => await request("GET", "/notifications", { query: args }));
|
|
403
|
+
server.registerTool("mark_notifications_read", {
|
|
404
|
+
title: "通知を既読にする",
|
|
405
|
+
description: "通知を既読にする。issue に課題キーを渡すとその課題の通知だけ、all=true で全部。" +
|
|
406
|
+
"画面の未読表示にも反映されるので、利用者が確認したと言ったとき・確認を頼まれたときに使う。",
|
|
407
|
+
inputSchema: {
|
|
408
|
+
issue: issueKey.optional(),
|
|
409
|
+
all: z.boolean().optional().describe("true=すべての未読通知を既読にする"),
|
|
410
|
+
},
|
|
411
|
+
}, async (body) => await request("POST", "/notifications/read", { body }));
|
|
412
|
+
server.registerTool("list_wiki_pages", {
|
|
413
|
+
title: "Wikiページの一覧",
|
|
414
|
+
description: "プロジェクトのWikiページの一覧(タイトル順・本文なし)を返す。タイトルの「/」区切りは階層。" +
|
|
415
|
+
"q でタイトル・本文の部分一致検索もできる。本文は get_wiki_page で読む。",
|
|
416
|
+
inputSchema: {
|
|
417
|
+
project: projectKey,
|
|
418
|
+
q: z.string().optional().describe("タイトル・本文の部分一致で絞る"),
|
|
419
|
+
limit: limitArg(50),
|
|
420
|
+
cursor: cursorArg,
|
|
421
|
+
},
|
|
422
|
+
}, async ({ project, ...query }) => await request("GET", `/projects/${encodeURIComponent(project)}/wiki`, { query }));
|
|
423
|
+
server.registerTool("get_wiki_page", {
|
|
424
|
+
title: "Wikiページを読む",
|
|
425
|
+
description: "Wikiページの本文(Markdown)を返す。ページはプロジェクトキーとタイトルで指定する。",
|
|
426
|
+
inputSchema: { project: projectKey, title: wikiTitleArg },
|
|
427
|
+
}, async ({ project, title }) => await request("GET", `/projects/${encodeURIComponent(project)}/wiki/page`, {
|
|
428
|
+
query: { title },
|
|
429
|
+
}));
|
|
430
|
+
server.registerTool("create_wiki_page", {
|
|
431
|
+
title: "Wikiページを作成",
|
|
432
|
+
description: "Wikiページを新しく作る。同じタイトルのページがあるとエラーになる(そのときは update_wiki_page)。" +
|
|
433
|
+
"階層の下に作るときはタイトルを「親/子」の形にする。",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
project: projectKey,
|
|
436
|
+
title: z.string().describe("タイトル。「/」区切りで階層になる"),
|
|
437
|
+
body: z.string().optional().describe("本文(Markdown)"),
|
|
438
|
+
},
|
|
439
|
+
}, async ({ project, title, body }) => await request("POST", `/projects/${encodeURIComponent(project)}/wiki`, {
|
|
440
|
+
body: { title, body },
|
|
441
|
+
}));
|
|
442
|
+
server.registerTool("update_wiki_page", {
|
|
443
|
+
title: "Wikiページを更新",
|
|
444
|
+
description: "Wikiページのタイトルや本文を変える。body を渡すと本文の全文が置き換わる" +
|
|
445
|
+
"(追記したいときは先に get_wiki_page で読み、全文を組み立てて渡す)。版の管理はないので、" +
|
|
446
|
+
"読んだときの updatedAt を expectedUpdatedAt に渡すこと(その後にだれかが更新していたら止まる)。",
|
|
447
|
+
inputSchema: {
|
|
448
|
+
project: projectKey,
|
|
449
|
+
title: wikiTitleArg,
|
|
450
|
+
newTitle: z.string().optional().describe("新しいタイトル(変えるときだけ)"),
|
|
451
|
+
body: z.string().optional().describe("新しい本文(Markdown)。全文が置き換わる"),
|
|
452
|
+
expectedUpdatedAt: z
|
|
453
|
+
.string()
|
|
454
|
+
.optional()
|
|
455
|
+
.describe("get_wiki_page で読んだときの updatedAt。食い違えば更新せずエラーになる"),
|
|
456
|
+
},
|
|
457
|
+
}, async ({ project, title, newTitle, body, expectedUpdatedAt }) => await request("PATCH", `/projects/${encodeURIComponent(project)}/wiki/page`, {
|
|
458
|
+
query: { title },
|
|
459
|
+
body: { title: newTitle, body, expectedUpdatedAt },
|
|
460
|
+
}));
|
|
461
|
+
server.registerTool("list_comments", {
|
|
462
|
+
title: "コメント一覧",
|
|
463
|
+
description: "課題に付いたコメントを古い順に返す。各コメントの id は update_comment での指定に使う" +
|
|
464
|
+
"(編集できるのは自分のコメントだけ)。編集されたコメントには editedAt が付く。",
|
|
465
|
+
inputSchema: { key: issueKey },
|
|
466
|
+
}, async ({ key }) => await request("GET", `/issues/${encodeURIComponent(key)}/comments`));
|
|
467
|
+
server.registerTool("add_comment", {
|
|
468
|
+
title: "コメントを追加",
|
|
469
|
+
description: "課題にコメントを追加する。本文で @表示名 と書くとその人にメンション通知が飛ぶ。",
|
|
470
|
+
inputSchema: {
|
|
471
|
+
key: issueKey,
|
|
472
|
+
body: z.string().describe("コメント本文(Markdown)"),
|
|
473
|
+
},
|
|
474
|
+
}, async ({ key, body }) => await request("POST", `/issues/${encodeURIComponent(key)}/comments`, {
|
|
475
|
+
body: { body },
|
|
476
|
+
}));
|
|
477
|
+
registerAttachmentTools(server, request, issueKey);
|
|
478
|
+
server.registerTool("update_comment", {
|
|
479
|
+
title: "コメントを編集",
|
|
480
|
+
description: "自分が書いたコメントの本文を直す(他の人のコメントは編集できない)。id は list_comments か " +
|
|
481
|
+
"add_comment の結果の id。本文は全文が置き換わり、画面には「(編集済み)」と出る。" +
|
|
482
|
+
"編集ではメンション通知は飛ばない。",
|
|
483
|
+
inputSchema: {
|
|
484
|
+
id: z.string().describe("コメントの id(list_comments / add_comment の結果)"),
|
|
485
|
+
body: z.string().describe("新しい本文(Markdown)。全文が置き換わる"),
|
|
486
|
+
},
|
|
487
|
+
}, async ({ id, body }) => await request("PATCH", `/comments/${encodeURIComponent(id)}`, {
|
|
488
|
+
body: { body },
|
|
489
|
+
}));
|
|
490
|
+
registerMasterTools(server, request, projectKey);
|
|
491
|
+
const transport = new StdioServerTransport();
|
|
492
|
+
await server.connect(transport);
|
package/dist/masters.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const colorArg = z
|
|
3
|
+
.enum(["red", "orange", "amber", "green", "teal", "blue", "violet", "pink", "gray"])
|
|
4
|
+
.optional();
|
|
5
|
+
const ADMIN_NOTE = "管理者のキーだけが使える(プロジェクト設定と同じ)。";
|
|
6
|
+
export function registerMasterTools(server, request, projectKey) {
|
|
7
|
+
const path = (project, collection, name) => `/projects/${encodeURIComponent(project)}/${collection}` +
|
|
8
|
+
(name === undefined ? "" : `/${encodeURIComponent(name)}`);
|
|
9
|
+
const currentName = (label) => z.string().describe(`変える${label}の今の名前(get_project_meta の名前をそのまま渡す)`);
|
|
10
|
+
server.registerTool("create_milestone", {
|
|
11
|
+
title: "マイルストーンを作成",
|
|
12
|
+
description: "マイルストーンを新しく作る(末尾に追加)。発生バージョンのマスタも共通なので、発生バージョンを足すときもこれを使う。" +
|
|
13
|
+
"作ったら create_issue / update_issue の milestones・versions に名前で指定できる。" +
|
|
14
|
+
"同じ名前があるとエラー(conflict)。features の milestones と versions がどちらも OFF のプロジェクトでは使えない。" +
|
|
15
|
+
ADMIN_NOTE,
|
|
16
|
+
inputSchema: {
|
|
17
|
+
project: projectKey,
|
|
18
|
+
name: z.string().describe("マイルストーン名(例: v1.2)"),
|
|
19
|
+
startDate: z.string().optional().describe("開始日 YYYY-MM-DD"),
|
|
20
|
+
dueDate: z.string().optional().describe("期限日 YYYY-MM-DD"),
|
|
21
|
+
},
|
|
22
|
+
}, async ({ project, ...body }) => await request("POST", path(project, "milestones"), { body }));
|
|
23
|
+
server.registerTool("update_milestone", {
|
|
24
|
+
title: "マイルストーンを変更",
|
|
25
|
+
description: "マイルストーン(発生バージョンと共通)の名前・開始日・期限日を変える。渡さなかった項目は今のまま。" +
|
|
26
|
+
"日付に空文字を渡すと外す。名前を変えると、課題に付いている分もそのまま新しい名前になる。" +
|
|
27
|
+
ADMIN_NOTE,
|
|
28
|
+
inputSchema: {
|
|
29
|
+
project: projectKey,
|
|
30
|
+
name: currentName("マイルストーン"),
|
|
31
|
+
newName: z.string().optional().describe("新しい名前(変えるときだけ)"),
|
|
32
|
+
startDate: z.string().optional().describe("開始日 YYYY-MM-DD。空文字で外す"),
|
|
33
|
+
dueDate: z.string().optional().describe("期限日 YYYY-MM-DD。空文字で外す"),
|
|
34
|
+
},
|
|
35
|
+
}, async ({ project, name, newName, startDate, dueDate }) => await request("PATCH", path(project, "milestones", name), {
|
|
36
|
+
body: { name: newName, startDate, dueDate },
|
|
37
|
+
}));
|
|
38
|
+
server.registerTool("create_category", {
|
|
39
|
+
title: "カテゴリを作成",
|
|
40
|
+
description: "カテゴリを新しく作る。同じ名前があるとエラー(conflict)。features.categories が OFF のプロジェクトでは使えない。" +
|
|
41
|
+
ADMIN_NOTE,
|
|
42
|
+
inputSchema: {
|
|
43
|
+
project: projectKey,
|
|
44
|
+
name: z.string().describe("カテゴリ名"),
|
|
45
|
+
},
|
|
46
|
+
}, async ({ project, name }) => await request("POST", path(project, "categories"), { body: { name } }));
|
|
47
|
+
server.registerTool("update_category", {
|
|
48
|
+
title: "カテゴリ名を変更",
|
|
49
|
+
description: "カテゴリの名前を変える。課題に付いている分もそのまま新しい名前になる。" + ADMIN_NOTE,
|
|
50
|
+
inputSchema: {
|
|
51
|
+
project: projectKey,
|
|
52
|
+
name: currentName("カテゴリ"),
|
|
53
|
+
newName: z.string().describe("新しい名前"),
|
|
54
|
+
},
|
|
55
|
+
}, async ({ project, name, newName }) => await request("PATCH", path(project, "categories", name), { body: { name: newName } }));
|
|
56
|
+
server.registerTool("create_type", {
|
|
57
|
+
title: "種別を作成",
|
|
58
|
+
description: "種別を新しく作る(末尾に追加。課題作成で種別を省いたときの既定=先頭の種別は変わらない)。" +
|
|
59
|
+
"同じ名前があるとエラー(conflict)。最大12個。" +
|
|
60
|
+
ADMIN_NOTE,
|
|
61
|
+
inputSchema: {
|
|
62
|
+
project: projectKey,
|
|
63
|
+
name: z.string().describe("種別名(例: 要調査)"),
|
|
64
|
+
color: colorArg.describe("色。省略時は blue"),
|
|
65
|
+
},
|
|
66
|
+
}, async ({ project, ...body }) => await request("POST", path(project, "types"), { body }));
|
|
67
|
+
server.registerTool("update_type", {
|
|
68
|
+
title: "種別を変更",
|
|
69
|
+
description: "種別の名前・色を変える。渡さなかった項目は今のまま。" + ADMIN_NOTE,
|
|
70
|
+
inputSchema: {
|
|
71
|
+
project: projectKey,
|
|
72
|
+
name: currentName("種別"),
|
|
73
|
+
newName: z.string().optional().describe("新しい名前(変えるときだけ)"),
|
|
74
|
+
color: colorArg.describe("新しい色(変えるときだけ)"),
|
|
75
|
+
},
|
|
76
|
+
}, async ({ project, name, newName, color }) => await request("PATCH", path(project, "types", name), { body: { name: newName, color } }));
|
|
77
|
+
server.registerTool("create_status", {
|
|
78
|
+
title: "ステータスを作成",
|
|
79
|
+
description: "ステータスを新しく作る。完了扱いでない(isDone: false)ステータスとして、完了扱いのステータスの直前に入る" +
|
|
80
|
+
"(完了扱いのステータスは作れない)。同じ名前があるとエラー(conflict)。最大12個。" +
|
|
81
|
+
ADMIN_NOTE,
|
|
82
|
+
inputSchema: {
|
|
83
|
+
project: projectKey,
|
|
84
|
+
name: z.string().describe("ステータス名(例: テスト中)"),
|
|
85
|
+
color: colorArg.describe("色。省略時は blue"),
|
|
86
|
+
},
|
|
87
|
+
}, async ({ project, ...body }) => await request("POST", path(project, "statuses"), { body }));
|
|
88
|
+
server.registerTool("update_status", {
|
|
89
|
+
title: "ステータスを変更",
|
|
90
|
+
description: "ステータスの名前・色を変える。渡さなかった項目は今のまま。完了扱いかどうか(isDone)は変えられない。" +
|
|
91
|
+
ADMIN_NOTE,
|
|
92
|
+
inputSchema: {
|
|
93
|
+
project: projectKey,
|
|
94
|
+
name: currentName("ステータス"),
|
|
95
|
+
newName: z.string().optional().describe("新しい名前(変えるときだけ)"),
|
|
96
|
+
color: colorArg.describe("新しい色(変えるときだけ)"),
|
|
97
|
+
},
|
|
98
|
+
}, async ({ project, name, newName, color }) => await request("PATCH", path(project, "statuses", name), { body: { name: newName, color } }));
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocodelab/kadailog-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "KadaiLog の公式MCPサーバ。Claude Code などのAIエージェントから課題を操作する",
|
|
5
|
+
"homepage": "https://docs.kadailog.com/advanced/ai-agents/",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"kadailog",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"claude",
|
|
11
|
+
"task-management"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"kadailog-mcp": "dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^26.2.0",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|