mmm-mcp-server 0.1.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 +346 -0
- package/dist/bin/login.js +119 -0
- package/dist/src/auth/appleClientSecretRemote.js +29 -0
- package/dist/src/auth/appleLoopback.js +75 -0
- package/dist/src/auth/appleTokenExchange.js +29 -0
- package/dist/src/auth/firebaseAuthRest.js +98 -0
- package/dist/src/auth/googleLoopback.js +93 -0
- package/dist/src/auth/googleOAuthConfig.js +26 -0
- package/dist/src/auth/session.js +58 -0
- package/dist/src/auth/tokenStore.js +32 -0
- package/dist/src/domain/appendForest.js +82 -0
- package/dist/src/domain/body.js +127 -0
- package/dist/src/domain/deleteElements.js +85 -0
- package/dist/src/domain/header.js +27 -0
- package/dist/src/domain/idGen.js +11 -0
- package/dist/src/domain/levels.js +242 -0
- package/dist/src/domain/levelsMode.js +24 -0
- package/dist/src/domain/levelsOps.js +199 -0
- package/dist/src/domain/mapResolver.js +52 -0
- package/dist/src/domain/markdownParser.js +122 -0
- package/dist/src/domain/moveElement.js +93 -0
- package/dist/src/firebaseConfig.js +36 -0
- package/dist/src/firestore/restClient.js +267 -0
- package/dist/src/firestore/valueCodec.js +65 -0
- package/dist/src/mcpContext.js +11 -0
- package/dist/src/server.js +33 -0
- package/dist/src/tools/autoStructureThought.js +87 -0
- package/dist/src/tools/createMap.js +39 -0
- package/dist/src/tools/deleteElements.js +150 -0
- package/dist/src/tools/editElement.js +65 -0
- package/dist/src/tools/importMarkdownContext.js +95 -0
- package/dist/src/tools/listElements.js +97 -0
- package/dist/src/tools/listMaps.js +47 -0
- package/dist/src/tools/moveElement.js +81 -0
- package/dist/src/tools/updateTaskStatus.js +87 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noriaki Horie
|
|
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
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# mmm-mcp-server
|
|
2
|
+
|
|
3
|
+
Matryoshka Mind Map(マトリョシカ、マインドマップ/TODOアプリ)の自分のマップを、
|
|
4
|
+
AIエージェントとの会話から直接読み書きできるようにするツールです。MCP
|
|
5
|
+
(Model Context Protocol)という共通規格に対応したツールなので、Claude
|
|
6
|
+
Desktop・Claude Codeに限らず、MCPに対応したAIエージェントであれば基本的に
|
|
7
|
+
同じ手順で使えます(**動作確認はClaude Desktop / Claude Codeで実施**しています。
|
|
8
|
+
他のクライアントでの登録方法はそれぞれのドキュメントを参照してください)。
|
|
9
|
+
|
|
10
|
+
- 「今のマップ一覧を見せて」と話しかけるとマップ一覧を教えてくれる
|
|
11
|
+
- 会話の中で考えた内容を、そのままマインドマップの階層構造として追加できる
|
|
12
|
+
- 「〇〇のタスク終わったよ」と伝えるだけでTODOにチェックが付く
|
|
13
|
+
- 長いメモやMarkdownをそのままマップに流し込める
|
|
14
|
+
|
|
15
|
+
パソコンの中だけで動くツールで、常時起動しているサーバーはありません。使うときだけ
|
|
16
|
+
起動し、あなた自身のログイン情報であなた自身のマップにアクセスします(開発者側は
|
|
17
|
+
あなたのデータを見ることができません)。
|
|
18
|
+
|
|
19
|
+
> **2026年9月30日より前にダウンロードした方へ**: アプリ側のデータ形式の切り替えに
|
|
20
|
+
> 伴い、古い版ではマップへの書き込みが失敗するようになります。`mmm_mcp` フォルダで
|
|
21
|
+
> `git pull && npm run build` を実行し、AIエージェントを再起動してください。
|
|
22
|
+
|
|
23
|
+
> このページは、ある程度パソコン操作に慣れていない方でも迷わず進められるよう、
|
|
24
|
+
> ターミナル(コマンドを打つ画面)の開き方から順に説明しています。
|
|
25
|
+
>
|
|
26
|
+
> **対応OS**: macOS・Windowsの両方で動作するはずです。Node.jsとgitだけで
|
|
27
|
+
> 動くツールで、OS固有の機能には依存していません。ただし**実機での動作
|
|
28
|
+
> 確認はmacOSでのみ行っています。** Windowsで試して気づいた点があれば、
|
|
29
|
+
> このリポジトリのIssueで教えてください。手順のうち、操作方法がOSで
|
|
30
|
+
> 異なる箇所(ターミナルの開き方・Node.jsのインストール)はmacOS/Windows
|
|
31
|
+
> それぞれ記載しています。それ以外のコマンドはどちらのOSでも同じです。
|
|
32
|
+
|
|
33
|
+
## 1. 事前に必要なもの
|
|
34
|
+
|
|
35
|
+
- Matryoshka Mind Mapのアカウント(**匿名利用のみのアカウントは使えません。**
|
|
36
|
+
Google・Apple・メールアドレスのいずれかでログインできる状態にしてください。
|
|
37
|
+
アプリ内の「メールアドレスを連携」機能で後から連携することもできます)
|
|
38
|
+
- MCPに対応したAIエージェント(Claude Desktop / Claude Codeで動作確認済み。
|
|
39
|
+
他のMCP対応クライアントでも基本的に使えるはずです)
|
|
40
|
+
- Node.js(プログラムを動かすための土台。次の手順でインストールします)
|
|
41
|
+
|
|
42
|
+
## 2. ターミナルを開く
|
|
43
|
+
|
|
44
|
+
### macOSの場合
|
|
45
|
+
|
|
46
|
+
1. キーボードで `command`キーを押しながら`スペース`キーを押す(Spotlight検索が開く)
|
|
47
|
+
2. 「ターミナル」と入力する
|
|
48
|
+
3. 一覧に出てきた「ターミナル」を左クリック、またはEnterキーを押す
|
|
49
|
+
|
|
50
|
+
### Windowsの場合
|
|
51
|
+
|
|
52
|
+
1. キーボードで `Windows`キーを押す(スタートメニューの検索が開く)
|
|
53
|
+
2. 「PowerShell」と入力する
|
|
54
|
+
3. 一覧に出てきた「Windows PowerShell」を左クリック、またはEnterキーを押す
|
|
55
|
+
|
|
56
|
+
黒っぽい(または白い)ウィンドウが開けば準備完了です。以降の手順は、この
|
|
57
|
+
ウィンドウに1行ずつコマンドを貼り付けてEnterキーを押していきます(コマンド
|
|
58
|
+
自体はmacOS・Windowsで共通です)。
|
|
59
|
+
|
|
60
|
+
## 3. Node.jsをインストールする
|
|
61
|
+
|
|
62
|
+
すでにインストール済みか、次のコマンドで確認できます。
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
node --version
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`v18` 以上のバージョン番号(例: `v20.11.0`)が表示されればインストール済みです。
|
|
69
|
+
「command not found」のようなエラーが出た場合は、下記からインストーラーを
|
|
70
|
+
ダウンロードしてください。
|
|
71
|
+
|
|
72
|
+
https://nodejs.org/
|
|
73
|
+
|
|
74
|
+
サイトを開くと大きなボタンが2つ出ます。**「LTS」と書かれている方**を左クリックして
|
|
75
|
+
ダウンロードしてください(使っているOSに合わせて、macOSなら`.pkg`、Windowsなら
|
|
76
|
+
`.msi`のインストーラーが自動でダウンロードされます)。ダウンロードしたファイルを
|
|
77
|
+
ダブルクリックし、画面の指示に従って進めてください
|
|
78
|
+
(macOSは「続ける」→「同意する」→「インストール」、Windowsは
|
|
79
|
+
「Next」→「I accept...」→「Install」のように、案内される通りに次へ進めば
|
|
80
|
+
インストールできます)。
|
|
81
|
+
|
|
82
|
+
インストールが終わったら、ターミナルに戻ってもう一度確認します。
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
node --version
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 4. mmm-mcp-serverをダウンロードする
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git clone https://github.com/nhorie-canal/mmm_mcp.git
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
コマンドの実行が終わったら、作られたフォルダに移動します。
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
cd mmm_mcp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 5. セットアップする
|
|
101
|
+
|
|
102
|
+
必要な部品を取り込みます(少し時間がかかります)。
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm install
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
続けて、使える状態にビルドします。
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm run build
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 6. ログインする
|
|
115
|
+
|
|
116
|
+
Matryoshka Mind Mapアプリで普段どのログイン方法を使っているかに合わせて、
|
|
117
|
+
次の3つから選んでください。**いずれの方法も、あなた自身が事前に何かを準備する
|
|
118
|
+
必要はありません。**
|
|
119
|
+
|
|
120
|
+
### Googleでログインしている場合
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
node dist/bin/login.js login --method google
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
ブラウザが自動で開くので、いつも使っているGoogleアカウントでログインします。
|
|
127
|
+
|
|
128
|
+
### Appleでログインしている場合
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
node dist/bin/login.js login --method apple
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
こちらもブラウザが自動で開くので、Apple IDでログインします。
|
|
135
|
+
|
|
136
|
+
### メールアドレスとパスワードでログインしている場合
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
node dist/bin/login.js login --method email
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
メールアドレスとパスワードを聞かれるので入力してください(パスワードは
|
|
143
|
+
画面に表示されません)。
|
|
144
|
+
|
|
145
|
+
> **「パスワードが違います」というエラーが出た場合**、実際にパスワードを
|
|
146
|
+
> 間違えているとは限りません。そのアカウントがアプリ内でGoogle・Appleの
|
|
147
|
+
> どちらかでログインしていて、そもそもメールアドレス+パスワードでの
|
|
148
|
+
> ログイン情報を持っていない場合も同じエラーになります。心当たりが
|
|
149
|
+
> あれば、上の「Googleでログインしている場合」または「Appleでログイン
|
|
150
|
+
> している場合」の手順を試してください。
|
|
151
|
+
|
|
152
|
+
ログインに成功すると、以後は毎回ログインし直す必要はありません(ログイン情報は
|
|
153
|
+
あなたのパソコンの中だけに、あなた以外読めない形で保存されます)。
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
node dist/bin/login.js status
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
でログイン状態を確認できます。ログアウトしたい場合は次のコマンドです。
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
node dist/bin/login.js logout
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 7. AIエージェントにMatryoshkaを教える
|
|
166
|
+
|
|
167
|
+
ここでは動作確認済みのClaude Desktop / Claude Codeへの登録方法を示します。
|
|
168
|
+
他のMCP対応クライアント(例: Codexなど)を使っている場合も、多くは同じように
|
|
169
|
+
「サーバーを起動するコマンド」(`node <mmm_mcpのパス>/dist/src/server.js`)を
|
|
170
|
+
そのクライアントのMCPサーバー設定に登録するだけで使えるはずです。具体的な
|
|
171
|
+
登録方法はお使いのクライアントのドキュメントを確認してください。
|
|
172
|
+
|
|
173
|
+
このステップで、あなたが今どこのフォルダにいるかによって次のコマンドの一部が
|
|
174
|
+
変わります。次のコマンドで今いる場所(絶対パス)を確認してください。
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pwd
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
表示された場所(例: `/Users/あなたの名前/mmm_mcp`)を、以下の
|
|
181
|
+
`<mmm_mcpのパス>`の部分に置き換えて使います。
|
|
182
|
+
|
|
183
|
+
### Claude Codeを使っている場合
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
claude mcp add mmm -- node <mmm_mcpのパス>/dist/src/server.js
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Claude Desktopを使っている場合
|
|
190
|
+
|
|
191
|
+
Claude Desktopの設定ファイル(`claude_desktop_config.json`)を開き、
|
|
192
|
+
`mcpServers`の中に次を追記します。
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"mcpServers": {
|
|
197
|
+
"mmm": {
|
|
198
|
+
"command": "node",
|
|
199
|
+
"args": ["<mmm_mcpのパス>/dist/src/server.js"]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
保存したらClaude Desktopを再起動してください。
|
|
206
|
+
|
|
207
|
+
## 8. 使ってみる
|
|
208
|
+
|
|
209
|
+
AIエージェントとの会話で、次のように話しかけてみてください。
|
|
210
|
+
|
|
211
|
+
- 「Matryoshkaのマップ一覧を見せて」
|
|
212
|
+
- 「〇〇マップに、××というタスクを追加して」
|
|
213
|
+
- 「〇〇マップの、△△っていうタスク終わったよ」
|
|
214
|
+
- 「このメモをMarkdownで貼るので〇〇マップに取り込んで」
|
|
215
|
+
|
|
216
|
+
**マップの中身(要素の文字列)を初めて読むときだけ**、実際の中身を返す前に
|
|
217
|
+
必ず本人の許可を求める仕組みになっています。中身に個人的な内容が含まれる
|
|
218
|
+
可能性に配慮したものです。
|
|
219
|
+
|
|
220
|
+
ツールは中身を返す代わりに、「ユーザー本人に直接確認してから`confirmed: true`
|
|
221
|
+
を付けて呼び直せ」という指示だけを返します。実際に確認したかどうかをこちら側で
|
|
222
|
+
検証する手段は無いベストエフォートの経路です。使っているAIエージェントが指示
|
|
223
|
+
通りきちんと確認してくれるかは、そのエージェント次第になります。同じマップは、
|
|
224
|
+
そのサーバーが起動している間(概ね1回の会話)は再確認されません。
|
|
225
|
+
|
|
226
|
+
(以前はMCP標準の確認ダイアログ機能「elicitation」で強制する経路も
|
|
227
|
+
併用していましたが、対応を広告していながら実際にはダイアログを出さない
|
|
228
|
+
クライアントがあり、本人が一度も確認画面を見ないまま読み取れなくなる
|
|
229
|
+
不具合があったため、2026-09-03に廃止し、上記の方式に一本化しました)
|
|
230
|
+
|
|
231
|
+
## 書き方を安定させる
|
|
232
|
+
|
|
233
|
+
AIエージェントに任せると楽な反面、そのままだと読みにくいマップになりがちです。
|
|
234
|
+
次の文言を、AIエージェントが毎回読み込む指示ファイル(Claude Codeなら
|
|
235
|
+
`~/.claude/CLAUDE.md`)に貼っておくと、書き方が安定します。
|
|
236
|
+
|
|
237
|
+
```markdown
|
|
238
|
+
## mmmへの書き方
|
|
239
|
+
|
|
240
|
+
- 1つの要素は1行で読み切れる長さにする。説明を詰め込まない。
|
|
241
|
+
理由や補足を残したいときは、子要素として1段下に置く
|
|
242
|
+
- URLは単独の要素にする。「資料はこちら https://...」のように
|
|
243
|
+
説明文と混ぜてはいけない。「資料」という要素を作り、その子に
|
|
244
|
+
URLだけを置く
|
|
245
|
+
- やり忘れを防ぐための項目は時系列で並べる。「リリース直後」
|
|
246
|
+
「2週間後」「〇〇になったら」のように、いつやるかを項目名にする
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
それぞれの理由は次のとおりです。
|
|
250
|
+
|
|
251
|
+
- **1行にする** … Matryoshkaは要素を入れ子で表示するので、1つが長いと
|
|
252
|
+
一覧性が落ちて、階層をたどる良さが消えてしまいます
|
|
253
|
+
- **URLを混ぜない** … 説明文と同じ要素に入れるとアプリでリンクとして
|
|
254
|
+
扱われず、タップしても開けません
|
|
255
|
+
- **時系列で並べる** … 作業の名前で並べるより、順に消していく形になるぶん、
|
|
256
|
+
抜けに気づきやすくなります
|
|
257
|
+
|
|
258
|
+
## よくあるトラブル
|
|
259
|
+
|
|
260
|
+
| 症状 | 原因・対処 |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `node --version`が「command not found」になる | 手順3のNode.jsインストールがまだ。インストール後、ターミナルを一度閉じて開き直す |
|
|
263
|
+
| ログイン時に「パスワードが違います」と出る | 手順6の補足を参照。Google/Appleでログインし直してみる |
|
|
264
|
+
| 話しかけてもマップ操作をしてくれない | 手順7の登録が正しいか確認する。Claude Desktopは登録後に再起動が必要 |
|
|
265
|
+
| マップの中身を教えてくれない・確認だけで止まる | 「読んでいいですか」の確認に「はい」と答えているか確認する |
|
|
266
|
+
| 大きなマップをAIが読み込めない | 要素が数百を超えると`list_elements`の応答が大きくなり、一度に受け取れないことがある。AIエージェントによっては、いったんファイルに保存して必要な部分だけ読む形で対応する。頻繁に起きるならマップを分ける(最上位の要素を左スワイプすると、配下ごと独立したマップになる) |
|
|
267
|
+
|
|
268
|
+
## 活用例(応用編): フォルダ単位でタスクを自動記録する
|
|
269
|
+
|
|
270
|
+
ここから先は**一つの使い方の例**です。全員にすすめる標準的な使い方という
|
|
271
|
+
わけではなく、「こういう設定をすればこんなことができる」という可能性を
|
|
272
|
+
示すものなので、自分に合わないと思えば無視してかまいません。
|
|
273
|
+
|
|
274
|
+
**やりたいこと**: Claude Codeで作業しているフォルダ(プロジェクト)ごとに
|
|
275
|
+
同名のマップを1つ対応させ、作業中に出てきたタスクをAIエージェントが確認なしで
|
|
276
|
+
そのマップへリアルタイムに記録・更新していく。
|
|
277
|
+
|
|
278
|
+
> ここで示す(2)の設定はClaude Codeの「グローバルCLAUDE.md」という、常に
|
|
279
|
+
> 読み込ませる指示ファイルの仕組みを使った例です。他のAIエージェントでも、
|
|
280
|
+
> 同じように「毎回読み込ませる指示」を設定できる機能があれば、同じ考え方を
|
|
281
|
+
> 応用できるはずです。
|
|
282
|
+
|
|
283
|
+
### 必要な設定
|
|
284
|
+
|
|
285
|
+
**(1) mmmサーバーを全フォルダ共通で使えるように登録し直す**
|
|
286
|
+
|
|
287
|
+
手順7でClaude Codeに登録するとき、そのままだと今いるフォルダでしか
|
|
288
|
+
`mmm`ツールが使えません。どのフォルダで`claude`を起動しても使えるように
|
|
289
|
+
するには、`--scope user`を付けて登録します。
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
claude mcp add mmm --scope user -- node <mmm_mcpのパス>/dist/src/server.js
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**(2) グローバルのCLAUDE.md(`~/.claude/CLAUDE.md`)に運用ルールを書く**
|
|
296
|
+
|
|
297
|
+
例えば次のような文言を追記します。
|
|
298
|
+
|
|
299
|
+
```markdown
|
|
300
|
+
## mmmでのタスク管理
|
|
301
|
+
|
|
302
|
+
作業フォルダで発生したタスクは、mmm(Matryoshka Mind Map連携)に
|
|
303
|
+
記録してリアルタイムで管理する。
|
|
304
|
+
|
|
305
|
+
- 作業フォルダ名をそのままマップのタイトルとして扱う
|
|
306
|
+
- list_mapsで同名のマップが既にあるか確認する。無ければ、
|
|
307
|
+
タイトルの確認を挟まずそのままcreate_map(title=フォルダ名,
|
|
308
|
+
isTodo=true)で作成してよい(この運用に限り事前確認は不要とする)
|
|
309
|
+
- 既存タスクを把握するためlist_elementsを呼ぶと、そのマップを
|
|
310
|
+
そのプロセスで初めて読むときだけ確認が入る。それ以降は
|
|
311
|
+
auto_structure_thought/update_task_statusでリアルタイムに
|
|
312
|
+
追記・更新する
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### 動きの理由
|
|
316
|
+
|
|
317
|
+
- `create_map`はタイトルを直接引数で受け取るツールで、「呼ぶ前に会話で
|
|
318
|
+
確認を取る」というのはツールの説明文に書かれたAIエージェント向けの
|
|
319
|
+
推奨動作にすぎません。より具体的なCLAUDE.mdの指示を優先させることで、
|
|
320
|
+
確認なしの自動作成にできます
|
|
321
|
+
- マップの中身を読む`list_elements`だけは、秘匿情報への配慮として
|
|
322
|
+
サーバー側で確認を強制しています(上の「使ってみる」節を参照)。
|
|
323
|
+
これはフォルダ単位の運用にしても変わらず、同じマップをその
|
|
324
|
+
プロセスで初めて読むときに1回だけ確認が入ります
|
|
325
|
+
|
|
326
|
+
### 注意点
|
|
327
|
+
|
|
328
|
+
- 全フォルダで自動的にマップが増えていくため、普段使っている厳選された
|
|
329
|
+
マインドマップの中に、作業ログ的なマップが混ざります。人によっては
|
|
330
|
+
雑多に感じるかもしれません。専用のアカウントを分ける、対象フォルダを
|
|
331
|
+
限定するなど、自分に合う形に調整してください
|
|
332
|
+
- ここで示した`create_map`の確認省略は、あくまでこの運用を自分のCLAUDE.mdで
|
|
333
|
+
明示した場合の話です。何も設定しなければ、これまで通り会話内で確認を
|
|
334
|
+
取ってから作成されます
|
|
335
|
+
|
|
336
|
+
## このツールについて
|
|
337
|
+
|
|
338
|
+
- あなたのログイン情報(更新トークン)はあなたのパソコンの中
|
|
339
|
+
(macOSは`~/.config/mmm-mcp/credentials.json`、Windowsは
|
|
340
|
+
`C:\Users\<ユーザー名>\.config\mmm-mcp\credentials.json`)にのみ保存され、
|
|
341
|
+
他の場所には送信されません
|
|
342
|
+
- マップの内容はあなたのFirebaseアカウントに直接読み書きされ、開発者を含む
|
|
343
|
+
第三者のサーバーを経由しません(Appleでログインする場合のみ、ログインの
|
|
344
|
+
最後の一手続きで署名専用の中継サーバーを経由しますが、あなたのマップの
|
|
345
|
+
中身は一切通りません)
|
|
346
|
+
- 常時起動しているプロセスはなく、AIエージェントとの会話中だけ動作します
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as readline from "node:readline/promises";
|
|
3
|
+
import { stdin, stdout } from "node:process";
|
|
4
|
+
import { signInWithPassword, signInWithIdp, lookupAccountEmail, } from "../src/auth/firebaseAuthRest.js";
|
|
5
|
+
import { saveCredentials, clearCredentials, loadCredentials, credentialsPath, } from "../src/auth/tokenStore.js";
|
|
6
|
+
import { signInWithGoogleLoopback } from "../src/auth/googleLoopback.js";
|
|
7
|
+
import { signInWithAppleLoopback } from "../src/auth/appleLoopback.js";
|
|
8
|
+
import { exchangeAppleAuthorizationCode } from "../src/auth/appleTokenExchange.js";
|
|
9
|
+
import { fetchAppleClientSecret } from "../src/auth/appleClientSecretRemote.js";
|
|
10
|
+
// readlineに非表示入力(パスワード用)の機能が無いため、入力中だけ
|
|
11
|
+
// stdoutへの書き込みを差し替えてエコーバックを抑える。
|
|
12
|
+
function promptHidden(rl, promptText) {
|
|
13
|
+
const write = stdout.write.bind(stdout);
|
|
14
|
+
let hide = false;
|
|
15
|
+
stdout.write = ((chunk, ...args) => {
|
|
16
|
+
if (hide && typeof chunk === "string" && chunk !== promptText && !chunk.includes("\n")) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
// @ts-expect-error - Node標準のwriteシグネチャをそのまま委譲する
|
|
20
|
+
return write(chunk, ...args);
|
|
21
|
+
});
|
|
22
|
+
hide = true;
|
|
23
|
+
return rl.question(promptText).finally(() => {
|
|
24
|
+
hide = false;
|
|
25
|
+
stdout.write = write;
|
|
26
|
+
stdout.write("\n");
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function promptEmailPassword() {
|
|
30
|
+
const rl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
|
|
31
|
+
const email = await rl.question("メールアドレス: ");
|
|
32
|
+
const password = await promptHidden(rl, "パスワード(入力は表示されません): ");
|
|
33
|
+
rl.close();
|
|
34
|
+
return { email: email.trim(), password };
|
|
35
|
+
}
|
|
36
|
+
function printUsage() {
|
|
37
|
+
console.log("使い方:");
|
|
38
|
+
console.log(" mmm-login login --method email (メールアドレス+パスワードでサインイン)");
|
|
39
|
+
console.log(" mmm-login login --method google (Googleアカウントでサインイン)");
|
|
40
|
+
console.log(" mmm-login login --method apple (Appleアカウントでサインイン)");
|
|
41
|
+
console.log(" mmm-login logout (サインアウトしてローカルの認証情報を消す)");
|
|
42
|
+
console.log(" mmm-login status (サインイン状態を確認する)");
|
|
43
|
+
}
|
|
44
|
+
async function main() {
|
|
45
|
+
const [, , command, ...rest] = process.argv;
|
|
46
|
+
if (command === "logout") {
|
|
47
|
+
clearCredentials();
|
|
48
|
+
console.log("サインアウトしました。");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (command === "status") {
|
|
52
|
+
const creds = loadCredentials();
|
|
53
|
+
if (!creds) {
|
|
54
|
+
console.log("サインインしていません。");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log(`サインイン中: ${creds.email ?? "(メールアドレス不明)"} (${creds.provider})`);
|
|
58
|
+
console.log(`保存先: ${credentialsPath()}`);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (command !== "login") {
|
|
63
|
+
printUsage();
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const methodIndex = rest.indexOf("--method");
|
|
68
|
+
const method = methodIndex >= 0 ? rest[methodIndex + 1] : undefined;
|
|
69
|
+
if (method === "email") {
|
|
70
|
+
const { email, password } = await promptEmailPassword();
|
|
71
|
+
const result = await signInWithPassword(email, password);
|
|
72
|
+
saveCredentials({
|
|
73
|
+
uid: result.localId,
|
|
74
|
+
email: result.email,
|
|
75
|
+
refreshToken: result.refreshToken,
|
|
76
|
+
provider: "password",
|
|
77
|
+
});
|
|
78
|
+
console.log(`サインインしました: ${result.email ?? result.localId}`);
|
|
79
|
+
}
|
|
80
|
+
else if (method === "google") {
|
|
81
|
+
const idToken = await signInWithGoogleLoopback();
|
|
82
|
+
const result = await signInWithIdp("google.com", idToken);
|
|
83
|
+
const email = result.email ?? (await lookupAccountEmail(result.idToken));
|
|
84
|
+
saveCredentials({
|
|
85
|
+
uid: result.localId,
|
|
86
|
+
email,
|
|
87
|
+
refreshToken: result.refreshToken,
|
|
88
|
+
provider: "google.com",
|
|
89
|
+
});
|
|
90
|
+
console.log(`サインインしました: ${email ?? result.localId}`);
|
|
91
|
+
}
|
|
92
|
+
else if (method === "apple") {
|
|
93
|
+
// Cloud Functionの疎通確認を先に済ませ、失敗するならブラウザでの
|
|
94
|
+
// サインイン操作をさせる前に気付けるようにする。client_secretは
|
|
95
|
+
// 短命(5分)なので、ブラウザでの操作に時間がかかった場合に備えて
|
|
96
|
+
// 実際のトークン交換の直前にもう一度取り直す。
|
|
97
|
+
await fetchAppleClientSecret();
|
|
98
|
+
const code = await signInWithAppleLoopback();
|
|
99
|
+
const clientSecret = await fetchAppleClientSecret();
|
|
100
|
+
const appleIdToken = await exchangeAppleAuthorizationCode(code, clientSecret);
|
|
101
|
+
const result = await signInWithIdp("apple.com", appleIdToken);
|
|
102
|
+
const email = result.email ?? (await lookupAccountEmail(result.idToken));
|
|
103
|
+
saveCredentials({
|
|
104
|
+
uid: result.localId,
|
|
105
|
+
email,
|
|
106
|
+
refreshToken: result.refreshToken,
|
|
107
|
+
provider: "apple.com",
|
|
108
|
+
});
|
|
109
|
+
console.log(`サインインしました: ${email ?? result.localId}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
printUsage();
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
main().catch((err) => {
|
|
117
|
+
console.error(`エラー: ${err instanceof Error ? err.message : String(err)}`);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { APPLE_CLIENT_SECRET_FUNCTION_URL } from "../firebaseConfig.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sign in with Appleのclient_secret(JWT)を、Cloud Function経由で取得する。
|
|
4
|
+
* 秘密鍵はこのFunction側だけが持ち、全ユーザーのCLIが共有で呼び出す
|
|
5
|
+
* (client_secretはアプリ自身を認証するものであり、ユーザーごとに
|
|
6
|
+
* 異なるものではないため)。
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchAppleClientSecret() {
|
|
9
|
+
let res;
|
|
10
|
+
try {
|
|
11
|
+
res = await fetch(APPLE_CLIENT_SECRET_FUNCTION_URL);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
throw new Error(`client_secretの発行元(${APPLE_CLIENT_SECRET_FUNCTION_URL})に接続できませんでした: ${err instanceof Error ? err.message : String(err)}`);
|
|
15
|
+
}
|
|
16
|
+
const text = await res.text();
|
|
17
|
+
let data;
|
|
18
|
+
try {
|
|
19
|
+
data = text ? JSON.parse(text) : {};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
data = { error: text };
|
|
23
|
+
}
|
|
24
|
+
if (!res.ok || typeof data.client_secret !== "string") {
|
|
25
|
+
const message = data.error ?? `HTTP ${res.status}`;
|
|
26
|
+
throw new Error(`client_secretの取得に失敗しました: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
return data.client_secret;
|
|
29
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import open from "open";
|
|
4
|
+
import { APPLE_SERVICES_ID, APPLE_RELAY_URL, APPLE_LOOPBACK_PORT } from "../firebaseConfig.js";
|
|
5
|
+
const AUTHORIZE_ENDPOINT = "https://appleid.apple.com/auth/authorize";
|
|
6
|
+
function base64url(buffer) {
|
|
7
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Appleは(Googleと違って)localhostへのredirect_uriを許可しないため、
|
|
11
|
+
* Firebase Hosting上の中継ページ(web/mcp-apple-relay.html)を経由して
|
|
12
|
+
* このプロセスに認可コードを引き渡してもらう。中継ページのURLは
|
|
13
|
+
* 固定でなければならず、そこにポート番号を含められないため、
|
|
14
|
+
* ここでは毎回固定ポート(APPLE_LOOPBACK_PORT)でローカルサーバーを立てる。
|
|
15
|
+
*/
|
|
16
|
+
export async function signInWithAppleLoopback() {
|
|
17
|
+
const state = base64url(randomBytes(16));
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const server = createServer((req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${APPLE_LOOPBACK_PORT}`);
|
|
22
|
+
if (url.pathname !== "/callback") {
|
|
23
|
+
res.writeHead(404).end();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const returnedState = url.searchParams.get("state");
|
|
27
|
+
const code = url.searchParams.get("code");
|
|
28
|
+
const error = url.searchParams.get("error");
|
|
29
|
+
if (error) {
|
|
30
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
31
|
+
res.end("<p>サインインがキャンセルされました。このタブは閉じて構いません。</p>");
|
|
32
|
+
server.close();
|
|
33
|
+
reject(new Error(`Apple認証が拒否されました: ${error}`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (returnedState !== state || !code) {
|
|
37
|
+
// stateが一致しない・codeが無いリクエストは、favicon取得等の無関係な
|
|
38
|
+
// アクセスの可能性もあるため、応答だけしてサーバーは閉じずに待ち続ける。
|
|
39
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
40
|
+
res.end("<p>不正なリクエストです。このタブは閉じて構いません。</p>");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
44
|
+
res.end("<p>Appleサインインが完了しました。このタブは閉じてターミナルに戻ってください。</p>");
|
|
45
|
+
server.close();
|
|
46
|
+
resolve(code);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
res.writeHead(500).end("internal error");
|
|
50
|
+
server.close();
|
|
51
|
+
reject(err);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
server.listen(APPLE_LOOPBACK_PORT, "127.0.0.1", () => {
|
|
55
|
+
const authUrl = new URL(AUTHORIZE_ENDPOINT);
|
|
56
|
+
authUrl.searchParams.set("client_id", APPLE_SERVICES_ID);
|
|
57
|
+
authUrl.searchParams.set("redirect_uri", APPLE_RELAY_URL);
|
|
58
|
+
authUrl.searchParams.set("response_type", "code");
|
|
59
|
+
authUrl.searchParams.set("response_mode", "query");
|
|
60
|
+
authUrl.searchParams.set("state", state);
|
|
61
|
+
console.log("ブラウザでAppleのサインイン画面を開きます…");
|
|
62
|
+
open(authUrl.toString()).catch(() => {
|
|
63
|
+
console.log(`自動で開けなかった場合は次のURLを開いてください:\n${authUrl.toString()}`);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
server.on("error", (err) => {
|
|
67
|
+
if (err.code === "EADDRINUSE") {
|
|
68
|
+
reject(new Error(`ポート${APPLE_LOOPBACK_PORT}が使用中です。他のプロセスを終了してから再試行してください。`));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
reject(err);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { APPLE_SERVICES_ID, APPLE_RELAY_URL } from "../firebaseConfig.js";
|
|
2
|
+
const APPLE_TOKEN_ENDPOINT = "https://appleid.apple.com/auth/token";
|
|
3
|
+
/** Appleの認可コードをid_tokenに交換する(トークンエンドポイントへのサーバー間通信)。 */
|
|
4
|
+
export async function exchangeAppleAuthorizationCode(code, clientSecret) {
|
|
5
|
+
const res = await fetch(APPLE_TOKEN_ENDPOINT, {
|
|
6
|
+
method: "POST",
|
|
7
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
8
|
+
body: new URLSearchParams({
|
|
9
|
+
grant_type: "authorization_code",
|
|
10
|
+
code,
|
|
11
|
+
client_id: APPLE_SERVICES_ID,
|
|
12
|
+
client_secret: clientSecret,
|
|
13
|
+
redirect_uri: APPLE_RELAY_URL,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
const text = await res.text();
|
|
17
|
+
let data;
|
|
18
|
+
try {
|
|
19
|
+
data = text ? JSON.parse(text) : {};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
data = { error: text };
|
|
23
|
+
}
|
|
24
|
+
if (!res.ok || typeof data.id_token !== "string") {
|
|
25
|
+
const message = data.error_description ?? data.error ?? `HTTP ${res.status}`;
|
|
26
|
+
throw new Error(`Appleトークン交換に失敗しました: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
return data.id_token;
|
|
29
|
+
}
|