mmm-mcp-server 0.1.1 → 0.1.3
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.en.md +228 -0
- package/README.md +2 -0
- package/dist/src/domain/appendForest.js +0 -50
- package/dist/src/domain/levels.js +3 -180
- package/dist/src/domain/levelsOps.js +10 -2
- package/dist/src/domain/mapResolver.js +1 -4
- package/dist/src/domain/markdownParser.js +20 -21
- package/dist/src/firestore/restClient.js +0 -39
- package/dist/src/server.js +1 -1
- package/dist/src/tools/autoStructureThought.js +9 -48
- package/dist/src/tools/createMap.js +3 -7
- package/dist/src/tools/deleteElements.js +17 -87
- package/dist/src/tools/editElement.js +11 -44
- package/dist/src/tools/importMarkdownContext.js +9 -53
- package/dist/src/tools/listElements.js +2 -2
- package/dist/src/tools/moveElement.js +8 -46
- package/dist/src/tools/updateTaskStatus.js +15 -59
- package/package.json +3 -3
- package/dist/src/domain/body.js +0 -127
- package/dist/src/domain/deleteElements.js +0 -85
- package/dist/src/domain/levelsMode.js +0 -24
- package/dist/src/domain/moveElement.js +0 -93
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { mergeWritesByPath } from "../firestore/restClient.js";
|
|
3
2
|
import { getContext } from "../mcpContext.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { bodyFromFirestore, ancestorDetails } from "../domain/body.js";
|
|
8
|
-
import { buildForestFromTree, buildAppendWrites } from "../domain/appendForest.js";
|
|
9
|
-
import { assertLevelsRootExists, buildAppendLevelWrites, levelsDocExists } from "../domain/levels.js";
|
|
3
|
+
import { ancestorDetailsInLevels, buildLevelsAppendWrites, locateInLevels, readLevelsSnapshot } from "../domain/levelsOps.js";
|
|
4
|
+
import { resolveMap, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
5
|
+
import { buildForestFromTree } from "../domain/appendForest.js";
|
|
10
6
|
const treeNodeSchema = z.lazy(() => z.object({
|
|
11
7
|
detail: z.string().min(1).describe("要素の本文"),
|
|
12
8
|
done: z.boolean().optional().describe("TODOマップのときのチェック状態(省略時false)"),
|
|
@@ -29,57 +25,22 @@ export function registerAutoStructureThought(server) {
|
|
|
29
25
|
const map = await resolveMap(client, session, mapId);
|
|
30
26
|
assertCanEditMap(map, session);
|
|
31
27
|
const levelsPath = levelsPathOf(map);
|
|
32
|
-
const bodiesPath = bodiesPathOf(map);
|
|
33
28
|
const targetParentId = parentElementId ?? null;
|
|
34
29
|
const forest = buildForestFromTree(nodes);
|
|
35
|
-
|
|
36
|
-
let levelsPathResult = "";
|
|
37
|
-
await client.runOptimistic(async () => {
|
|
38
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath, map.header.title);
|
|
39
|
-
if (targetParentId !== null && !locateInLevels(snapshot, targetParentId)) {
|
|
40
|
-
throw new Error(`parentElementId(${targetParentId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
41
|
-
}
|
|
42
|
-
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, targetParentId));
|
|
43
|
-
return buildLevelsAppendWrites(levelsPath, snapshot, forest, targetParentId, map.header.isTodo);
|
|
44
|
-
});
|
|
45
|
-
return {
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: "text",
|
|
49
|
-
text: JSON.stringify({ path: levelsPathResult, addedElementCount: forest.length }, null, 2),
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
// 読み取り(bodies一覧・levels存在確認)から書き込みまでを楽観的
|
|
55
|
-
// ロックでまとめる。読み取り後に対象ドキュメントが他から変更される
|
|
56
|
-
// とコミットがFAILED_PRECONDITIONになり、読み取りからやり直す。
|
|
57
|
-
let createdIds = [];
|
|
58
|
-
let existingBodiesForPath = [];
|
|
30
|
+
let levelsPathResult = "";
|
|
59
31
|
await client.runOptimistic(async () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const existingBodies = existingDocs.map((d) => bodyFromFirestore(d.id, d.data, d.updateTime));
|
|
63
|
-
existingBodiesForPath = existingBodies;
|
|
64
|
-
if (targetParentId !== null && !existingBodies.some((b) => b.id === targetParentId)) {
|
|
32
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
33
|
+
if (targetParentId !== null && !locateInLevels(snapshot, targetParentId)) {
|
|
65
34
|
throw new Error(`parentElementId(${targetParentId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
66
35
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// 挿入先のlevelsドキュメントが実際に存在するかを直接確認する
|
|
70
|
-
// (bodies側の状態から推測すると、levelsとbodiesが食い違っている
|
|
71
|
-
// 場合にarrayUnion書き込みが「ドキュメントが無い」で失敗しうるため)。
|
|
72
|
-
const targetLevelId = targetParentId ?? "root";
|
|
73
|
-
const targetHasLevelsDoc = targetLevelId === "root" || (await levelsDocExists(client, levelsPath, targetLevelId));
|
|
74
|
-
const levelWrites = buildAppendLevelWrites(levelsPath, forest, targetParentId, targetHasLevelsDoc, map.header.isTodo);
|
|
75
|
-
return mergeWritesByPath([...writes, ...levelWrites]);
|
|
36
|
+
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, targetParentId));
|
|
37
|
+
return buildLevelsAppendWrites(levelsPath, snapshot, forest, targetParentId, map.header.isTodo);
|
|
76
38
|
});
|
|
77
|
-
const path = formatPath(map.header.title, ancestorDetails(existingBodiesForPath, targetParentId));
|
|
78
39
|
return {
|
|
79
40
|
content: [
|
|
80
41
|
{
|
|
81
42
|
type: "text",
|
|
82
|
-
text: JSON.stringify({ path, addedElementCount:
|
|
43
|
+
text: JSON.stringify({ path: levelsPathResult, addedElementCount: forest.length }, null, 2),
|
|
83
44
|
},
|
|
84
45
|
],
|
|
85
46
|
};
|
|
@@ -12,12 +12,8 @@ export function registerCreateMap(server) {
|
|
|
12
12
|
const { client, session } = getContext();
|
|
13
13
|
const existing = await client.listDocuments(`users/${session.uid}/headers`);
|
|
14
14
|
const maxOrder = existing.reduce((max, doc) => Math.max(max, Number(doc.data.order ?? 0)), 0);
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// 直後に書き込み系ツールを呼んでも「アプリで一度開いてください」で
|
|
18
|
-
// 弾かれない(移行ではなく単なる初期化なので問題ない)。
|
|
19
|
-
// migratedはアプリの新規作成(header_add_model.dart)と揃える。無いと、
|
|
20
|
-
// 切替前のアプリが開いたときに未移行とみなしてbodiesから作り直してしまう。
|
|
15
|
+
// 自前でIDを採番し、header本体と空のlevels/rootを1回のcommitWritesに
|
|
16
|
+
// まとめて作る(アプリの新規作成 header_add_model.dart と揃える)。
|
|
21
17
|
const headerId = newDocId();
|
|
22
18
|
const headerPath = `users/${session.uid}/headers/${headerId}`;
|
|
23
19
|
await client.commitWrites([
|
|
@@ -25,7 +21,7 @@ export function registerCreateMap(server) {
|
|
|
25
21
|
path: headerPath,
|
|
26
22
|
fields: { title, order: maxOrder + 1, isTodo, editors: [], viewers: [] },
|
|
27
23
|
},
|
|
28
|
-
{ path: `${headerPath}/levels/root`, fields: { children: []
|
|
24
|
+
{ path: `${headerPath}/levels/root`, fields: { children: [] } },
|
|
29
25
|
]);
|
|
30
26
|
return {
|
|
31
27
|
content: [
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getContext } from "../mcpContext.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { resolveMap, bodiesPathOf, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
6
|
-
import { bodyFromFirestore, ancestorDetails, collectSubtreeIds } from "../domain/body.js";
|
|
7
|
-
import { assertLevelsRootExists, levelsDocExists } from "../domain/levels.js";
|
|
8
|
-
import { buildDeleteElementWrites } from "../domain/deleteElements.js";
|
|
3
|
+
import { ancestorDetailsInLevels, buildLevelsDeleteWrites, locateInLevels, subtreeIdsInLevels, readLevelsSnapshot } from "../domain/levelsOps.js";
|
|
4
|
+
import { resolveMap, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
9
5
|
export function registerDeleteElements(server) {
|
|
10
6
|
server.tool("delete_elements", "既存マップから要素を削除する。指定した要素とその配下(子孫)を丸ごと削除する。" +
|
|
11
7
|
"削除は取り返しがつかないため、まずconfirmedを省略(またはfalse)で呼び、" +
|
|
@@ -22,84 +18,20 @@ export function registerDeleteElements(server) {
|
|
|
22
18
|
const map = await resolveMap(client, session, mapId);
|
|
23
19
|
assertCanEditMap(map, session);
|
|
24
20
|
const levelsPath = levelsPathOf(map);
|
|
25
|
-
const bodiesPath = bodiesPathOf(map);
|
|
26
|
-
assertLevelsRootExists(await levelsDocExists(client, levelsPath, "root"), map.header.title);
|
|
27
|
-
if (await isLevelsOnly(client)) {
|
|
28
|
-
if (confirmed !== true) {
|
|
29
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath, map.header.title);
|
|
30
|
-
const allTargets = new Set();
|
|
31
|
-
const targets = [];
|
|
32
|
-
for (const elementId of elementIds) {
|
|
33
|
-
if (!locateInLevels(snapshot, elementId)) {
|
|
34
|
-
throw new Error(`elementId(${elementId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
35
|
-
}
|
|
36
|
-
const subtree = subtreeIdsInLevels(snapshot, elementId);
|
|
37
|
-
for (const id of subtree)
|
|
38
|
-
allTargets.add(id);
|
|
39
|
-
targets.push({
|
|
40
|
-
elementId,
|
|
41
|
-
path: formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId)),
|
|
42
|
-
subtreeCount: subtree.length,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: "text",
|
|
49
|
-
text: JSON.stringify({
|
|
50
|
-
confirmationRequired: true,
|
|
51
|
-
message: "削除は取り返しがつきません。以下の内容をユーザーに提示して明示的な同意を得てから、" +
|
|
52
|
-
"confirmed: trueで呼び直してください。",
|
|
53
|
-
targets,
|
|
54
|
-
totalElementCount: allTargets.size,
|
|
55
|
-
}, null, 2),
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
const levelsDeleted = [];
|
|
61
|
-
let levelsTotal = 0;
|
|
62
|
-
await client.runOptimistic(async () => {
|
|
63
|
-
levelsDeleted.length = 0;
|
|
64
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath, map.header.title);
|
|
65
|
-
for (const elementId of elementIds) {
|
|
66
|
-
if (!locateInLevels(snapshot, elementId))
|
|
67
|
-
continue; // 既に削除済み(リトライ時など)
|
|
68
|
-
levelsDeleted.push({
|
|
69
|
-
elementId,
|
|
70
|
-
path: formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId)),
|
|
71
|
-
subtreeCount: subtreeIdsInLevels(snapshot, elementId).length,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
const { writes, deletedIds } = buildLevelsDeleteWrites(levelsPath, snapshot, elementIds);
|
|
75
|
-
levelsTotal = deletedIds.size;
|
|
76
|
-
return writes;
|
|
77
|
-
});
|
|
78
|
-
return {
|
|
79
|
-
content: [
|
|
80
|
-
{
|
|
81
|
-
type: "text",
|
|
82
|
-
text: JSON.stringify({ deleted: levelsDeleted, totalElementCount: levelsTotal }, null, 2),
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
21
|
if (confirmed !== true) {
|
|
88
|
-
const
|
|
89
|
-
const existingBodies = existingDocs.map((d) => bodyFromFirestore(d.id, d.data, d.updateTime));
|
|
90
|
-
const byId = new Map(existingBodies.map((b) => [b.id, b]));
|
|
22
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
91
23
|
const allTargets = new Set();
|
|
92
24
|
const targets = [];
|
|
93
25
|
for (const elementId of elementIds) {
|
|
94
|
-
if (!
|
|
26
|
+
if (!locateInLevels(snapshot, elementId)) {
|
|
95
27
|
throw new Error(`elementId(${elementId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
96
28
|
}
|
|
97
|
-
const subtree =
|
|
29
|
+
const subtree = subtreeIdsInLevels(snapshot, elementId);
|
|
98
30
|
for (const id of subtree)
|
|
99
31
|
allTargets.add(id);
|
|
100
32
|
targets.push({
|
|
101
33
|
elementId,
|
|
102
|
-
path: formatPath(map.header.title,
|
|
34
|
+
path: formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId)),
|
|
103
35
|
subtreeCount: subtree.length,
|
|
104
36
|
});
|
|
105
37
|
}
|
|
@@ -118,31 +50,29 @@ export function registerDeleteElements(server) {
|
|
|
118
50
|
],
|
|
119
51
|
};
|
|
120
52
|
}
|
|
121
|
-
const
|
|
122
|
-
let
|
|
53
|
+
const levelsDeleted = [];
|
|
54
|
+
let levelsTotal = 0;
|
|
123
55
|
await client.runOptimistic(async () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const currentDocs = await client.listDocuments(bodiesPath);
|
|
127
|
-
const currentBodies = currentDocs.map((d) => bodyFromFirestore(d.id, d.data, d.updateTime));
|
|
56
|
+
levelsDeleted.length = 0;
|
|
57
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
128
58
|
for (const elementId of elementIds) {
|
|
129
|
-
if (!
|
|
59
|
+
if (!locateInLevels(snapshot, elementId))
|
|
130
60
|
continue; // 既に削除済み(リトライ時など)
|
|
131
|
-
|
|
61
|
+
levelsDeleted.push({
|
|
132
62
|
elementId,
|
|
133
|
-
path: formatPath(map.header.title,
|
|
134
|
-
subtreeCount:
|
|
63
|
+
path: formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId)),
|
|
64
|
+
subtreeCount: subtreeIdsInLevels(snapshot, elementId).length,
|
|
135
65
|
});
|
|
136
66
|
}
|
|
137
|
-
const { writes, deletedIds } =
|
|
138
|
-
|
|
67
|
+
const { writes, deletedIds } = buildLevelsDeleteWrites(levelsPath, snapshot, elementIds);
|
|
68
|
+
levelsTotal = deletedIds.size;
|
|
139
69
|
return writes;
|
|
140
70
|
});
|
|
141
71
|
return {
|
|
142
72
|
content: [
|
|
143
73
|
{
|
|
144
74
|
type: "text",
|
|
145
|
-
text: JSON.stringify({ deleted:
|
|
75
|
+
text: JSON.stringify({ deleted: levelsDeleted, totalElementCount: levelsTotal }, null, 2),
|
|
146
76
|
},
|
|
147
77
|
],
|
|
148
78
|
};
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getContext } from "../mcpContext.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { resolveMap, bodiesPathOf, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
6
|
-
import { bodyFromFirestore, ancestorDetails } from "../domain/body.js";
|
|
7
|
-
import { mergeWritesByPath } from "../firestore/restClient.js";
|
|
8
|
-
import { assertLevelsRootExists, buildUpdateEntryWrites, levelsDocExists } from "../domain/levels.js";
|
|
3
|
+
import { ancestorDetailsInLevels, buildLevelsPatchWrites, readLevelsSnapshot } from "../domain/levelsOps.js";
|
|
4
|
+
import { resolveMap, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
9
5
|
export function registerEditElement(server) {
|
|
10
6
|
server.tool("edit_element", "既存マップの要素1件の本文(detail)を書き換える。要素の追加はauto_structure_thought・" +
|
|
11
7
|
"import_markdown_contextを、チェック状態の変更はupdate_task_statusを使うこと。", {
|
|
@@ -17,49 +13,20 @@ export function registerEditElement(server) {
|
|
|
17
13
|
const map = await resolveMap(client, session, mapId);
|
|
18
14
|
assertCanEditMap(map, session);
|
|
19
15
|
const levelsPath = levelsPathOf(map);
|
|
20
|
-
|
|
21
|
-
if (await isLevelsOnly(client)) {
|
|
22
|
-
let levelsPathResult = null;
|
|
23
|
-
await client.runOptimistic(async () => {
|
|
24
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath, map.header.title);
|
|
25
|
-
const { writes, found } = buildLevelsPatchWrites(levelsPath, snapshot, [
|
|
26
|
-
{ elementId, patch: { detail } },
|
|
27
|
-
]);
|
|
28
|
-
if (!found.has(elementId)) {
|
|
29
|
-
throw new Error(`elementId(${elementId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
30
|
-
}
|
|
31
|
-
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId));
|
|
32
|
-
return writes;
|
|
33
|
-
});
|
|
34
|
-
return {
|
|
35
|
-
content: [{ type: "text", text: JSON.stringify({ path: levelsPathResult }, null, 2) }],
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
assertLevelsRootExists(await levelsDocExists(client, levelsPath, "root"), map.header.title);
|
|
39
|
-
let path = null;
|
|
16
|
+
let levelsPathResult = null;
|
|
40
17
|
await client.runOptimistic(async () => {
|
|
41
|
-
const
|
|
42
|
-
|
|
18
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
19
|
+
const { writes, found } = buildLevelsPatchWrites(levelsPath, snapshot, [
|
|
20
|
+
{ elementId, patch: { detail } },
|
|
21
|
+
]);
|
|
22
|
+
if (!found.has(elementId)) {
|
|
43
23
|
throw new Error(`elementId(${elementId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
44
24
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const existingBodies = existingDocs.map((d) => bodyFromFirestore(d.id, d.data, d.updateTime));
|
|
48
|
-
path = formatPath(map.header.title, ancestorDetails(existingBodies, elementId));
|
|
49
|
-
const writes = [
|
|
50
|
-
{
|
|
51
|
-
path: `${bodiesPath}/${elementId}`,
|
|
52
|
-
fields: { detail },
|
|
53
|
-
requireUpdateTime: body.updateTime,
|
|
54
|
-
},
|
|
55
|
-
];
|
|
56
|
-
const levelWrites = await buildUpdateEntryWrites(client, levelsPath, [
|
|
57
|
-
{ parentId: body.parent, elementId, patch: { detail } },
|
|
58
|
-
]);
|
|
59
|
-
return mergeWritesByPath([...writes, ...levelWrites]);
|
|
25
|
+
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId));
|
|
26
|
+
return writes;
|
|
60
27
|
});
|
|
61
28
|
return {
|
|
62
|
-
content: [{ type: "text", text: JSON.stringify({ path }, null, 2) }],
|
|
29
|
+
content: [{ type: "text", text: JSON.stringify({ path: levelsPathResult }, null, 2) }],
|
|
63
30
|
};
|
|
64
31
|
});
|
|
65
32
|
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { mergeWritesByPath } from "../firestore/restClient.js";
|
|
3
2
|
import { getContext } from "../mcpContext.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { resolveMap, bodiesPathOf, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
7
|
-
import { bodyFromFirestore, ancestorDetails } from "../domain/body.js";
|
|
8
|
-
import { buildAppendWrites } from "../domain/appendForest.js";
|
|
3
|
+
import { ancestorDetailsInLevels, buildLevelsAppendWrites, locateInLevels, readLevelsSnapshot } from "../domain/levelsOps.js";
|
|
4
|
+
import { resolveMap, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
9
5
|
import { parseMarkdownForImport } from "../domain/markdownParser.js";
|
|
10
|
-
import { assertLevelsRootExists, buildAppendLevelWrites, levelsDocExists } from "../domain/levels.js";
|
|
11
6
|
export function registerImportMarkdownContext(server) {
|
|
12
7
|
server.tool("import_markdown_context", "任意の深さのMarkdown(インデントで階層を表現したリスト、見出しは`#`)を組み立てて、" +
|
|
13
8
|
"既存のマップに流し込む。見出しも1つの要素として取り込む。" +
|
|
@@ -26,66 +21,27 @@ export function registerImportMarkdownContext(server) {
|
|
|
26
21
|
const map = await resolveMap(client, session, mapId);
|
|
27
22
|
assertCanEditMap(map, session);
|
|
28
23
|
const levelsPath = levelsPathOf(map);
|
|
29
|
-
const bodiesPath = bodiesPathOf(map);
|
|
30
24
|
const targetParentId = parentElementId ?? null;
|
|
31
25
|
const parsed = parseMarkdownForImport(markdown, { headingsAsNodes: true });
|
|
32
26
|
if (parsed.nodes.length === 0) {
|
|
33
27
|
throw new Error("Markdownからリスト項目・見出しが見つかりませんでした。");
|
|
34
28
|
}
|
|
35
|
-
|
|
36
|
-
let levelsPathResult = "";
|
|
37
|
-
await client.runOptimistic(async () => {
|
|
38
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath, map.header.title);
|
|
39
|
-
if (targetParentId !== null && !locateInLevels(snapshot, targetParentId)) {
|
|
40
|
-
throw new Error(`parentElementId(${targetParentId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
41
|
-
}
|
|
42
|
-
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, targetParentId));
|
|
43
|
-
return buildLevelsAppendWrites(levelsPath, snapshot, parsed.nodes, targetParentId, map.header.isTodo);
|
|
44
|
-
});
|
|
45
|
-
return {
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: "text",
|
|
49
|
-
text: JSON.stringify({
|
|
50
|
-
path: levelsPathResult,
|
|
51
|
-
addedElementCount: parsed.nodes.length,
|
|
52
|
-
checkboxIgnored: !map.header.isTodo && parsed.hasCheckbox,
|
|
53
|
-
}, null, 2),
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
// 読み取り(bodies一覧・levels存在確認)から書き込みまでを楽観的
|
|
59
|
-
// ロックでまとめる。読み取り後に対象ドキュメントが他から変更される
|
|
60
|
-
// とコミットがFAILED_PRECONDITIONになり、読み取りからやり直す。
|
|
61
|
-
let createdIds = [];
|
|
62
|
-
let existingBodiesForPath = [];
|
|
29
|
+
let levelsPathResult = "";
|
|
63
30
|
await client.runOptimistic(async () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const existingBodies = existingDocs.map((d) => bodyFromFirestore(d.id, d.data, d.updateTime));
|
|
67
|
-
existingBodiesForPath = existingBodies;
|
|
68
|
-
if (targetParentId !== null && !existingBodies.some((b) => b.id === targetParentId)) {
|
|
31
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
32
|
+
if (targetParentId !== null && !locateInLevels(snapshot, targetParentId)) {
|
|
69
33
|
throw new Error(`parentElementId(${targetParentId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
70
34
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// 挿入先のlevelsドキュメントが実際に存在するかを直接確認する
|
|
74
|
-
// (bodies側の状態から推測すると、levelsとbodiesが食い違っている
|
|
75
|
-
// 場合にarrayUnion書き込みが「ドキュメントが無い」で失敗しうるため)。
|
|
76
|
-
const targetLevelId = targetParentId ?? "root";
|
|
77
|
-
const targetHasLevelsDoc = targetLevelId === "root" || (await levelsDocExists(client, levelsPath, targetLevelId));
|
|
78
|
-
const levelWrites = buildAppendLevelWrites(levelsPath, parsed.nodes, targetParentId, targetHasLevelsDoc, map.header.isTodo);
|
|
79
|
-
return mergeWritesByPath([...writes, ...levelWrites]);
|
|
35
|
+
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, targetParentId));
|
|
36
|
+
return buildLevelsAppendWrites(levelsPath, snapshot, parsed.nodes, targetParentId, map.header.isTodo);
|
|
80
37
|
});
|
|
81
|
-
const path = formatPath(map.header.title, ancestorDetails(existingBodiesForPath, targetParentId));
|
|
82
38
|
return {
|
|
83
39
|
content: [
|
|
84
40
|
{
|
|
85
41
|
type: "text",
|
|
86
42
|
text: JSON.stringify({
|
|
87
|
-
path,
|
|
88
|
-
addedElementCount:
|
|
43
|
+
path: levelsPathResult,
|
|
44
|
+
addedElementCount: parsed.nodes.length,
|
|
89
45
|
checkboxIgnored: !map.header.isTodo && parsed.hasCheckbox,
|
|
90
46
|
}, null, 2),
|
|
91
47
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getContext } from "../mcpContext.js";
|
|
3
3
|
import { resolveMap, levelsPathOf } from "../domain/mapResolver.js";
|
|
4
|
-
import {
|
|
4
|
+
import { buildTreeFromLevels, readAllLevelDocs, } from "../domain/levels.js";
|
|
5
5
|
// ElementNodeの`done`を`checked`という名前で公開する(仕様書の用語に合わせる)。
|
|
6
6
|
function toChecked(nodes) {
|
|
7
7
|
return nodes.map((n) => ({
|
|
@@ -77,8 +77,8 @@ export function registerListElements(server) {
|
|
|
77
77
|
return { content: [{ type: "text", text: buildConfirmationRequestText(map.header.title) }] };
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
// levels/rootが無いマップは空のマップとして返す(アプリで開いてもrootは作られない)。
|
|
80
81
|
const levelsById = await readAllLevelDocs(client, levelsPathOf(map));
|
|
81
|
-
assertLevelsInitialized(levelsById, map.header.title);
|
|
82
82
|
const tree = buildTreeFromLevels(levelsById);
|
|
83
83
|
return {
|
|
84
84
|
content: [
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getContext } from "../mcpContext.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { resolveMap, bodiesPathOf, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
6
|
-
import { bodyFromFirestore, ancestorDetails } from "../domain/body.js";
|
|
7
|
-
import { assertLevelsRootExists, levelsDocExists } from "../domain/levels.js";
|
|
8
|
-
import { buildMoveElementWrites } from "../domain/moveElement.js";
|
|
3
|
+
import { ancestorDetailsInLevels, buildLevelsMoveWrites, locateInLevels, readLevelsSnapshot } from "../domain/levelsOps.js";
|
|
4
|
+
import { resolveMap, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
9
5
|
export function registerMoveElement(server) {
|
|
10
6
|
server.tool("move_element", "既存マップの要素1件(配下ごと)を、同じマップ内の別の場所へ移動する。" +
|
|
11
7
|
"階層をまたいだ移動(昇格・降格)も、同じ階層内の並べ替えも、これ1つで行える。", {
|
|
@@ -24,58 +20,24 @@ export function registerMoveElement(server) {
|
|
|
24
20
|
const map = await resolveMap(client, session, mapId);
|
|
25
21
|
assertCanEditMap(map, session);
|
|
26
22
|
const levelsPath = levelsPathOf(map);
|
|
27
|
-
const bodiesPath = bodiesPathOf(map);
|
|
28
|
-
assertLevelsRootExists(await levelsDocExists(client, levelsPath, "root"), map.header.title);
|
|
29
23
|
if (elementId === newParentElementId) {
|
|
30
24
|
throw new Error("要素を自分自身の子にすることはできません。");
|
|
31
25
|
}
|
|
32
26
|
if (elementId === afterElementId) {
|
|
33
27
|
throw new Error("afterElementIdにelementId自身は指定できません。");
|
|
34
28
|
}
|
|
35
|
-
|
|
36
|
-
let levelsPathResult = null;
|
|
37
|
-
await client.runOptimistic(async () => {
|
|
38
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath, map.header.title);
|
|
39
|
-
if (afterElementId !== null && !locateInLevels(snapshot, afterElementId)) {
|
|
40
|
-
throw new Error(`afterElementId(${afterElementId})がこのマップに見つかりません。`);
|
|
41
|
-
}
|
|
42
|
-
const writes = buildLevelsMoveWrites(levelsPath, snapshot, elementId, newParentElementId, afterElementId);
|
|
43
|
-
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, newParentElementId));
|
|
44
|
-
return writes;
|
|
45
|
-
});
|
|
46
|
-
return {
|
|
47
|
-
content: [{ type: "text", text: JSON.stringify({ path: levelsPathResult }, null, 2) }],
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
let path = null;
|
|
29
|
+
let levelsPathResult = null;
|
|
51
30
|
await client.runOptimistic(async () => {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const byId = new Map(existingBodies.map((b) => [b.id, b]));
|
|
55
|
-
const body = byId.get(elementId);
|
|
56
|
-
if (!body) {
|
|
57
|
-
throw new Error(`elementId(${elementId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
58
|
-
}
|
|
59
|
-
if (newParentElementId !== null && !byId.has(newParentElementId)) {
|
|
60
|
-
throw new Error(`newParentElementId(${newParentElementId})がこのマップに見つかりません。`);
|
|
61
|
-
}
|
|
62
|
-
if (afterElementId !== null && !byId.has(afterElementId)) {
|
|
31
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
32
|
+
if (afterElementId !== null && !locateInLevels(snapshot, afterElementId)) {
|
|
63
33
|
throw new Error(`afterElementId(${afterElementId})がこのマップに見つかりません。`);
|
|
64
34
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (cur === elementId) {
|
|
68
|
-
throw new Error("要素を自分自身の配下(子孫)の中へ移動することはできません。");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const writes = await buildMoveElementWrites(client, bodiesPath, levelsPath, existingBodies, elementId, newParentElementId, afterElementId);
|
|
72
|
-
path = formatPath(map.header.title, newParentElementId === null
|
|
73
|
-
? []
|
|
74
|
-
: ancestorDetails(existingBodies, newParentElementId));
|
|
35
|
+
const writes = buildLevelsMoveWrites(levelsPath, snapshot, elementId, newParentElementId, afterElementId);
|
|
36
|
+
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, newParentElementId));
|
|
75
37
|
return writes;
|
|
76
38
|
});
|
|
77
39
|
return {
|
|
78
|
-
content: [{ type: "text", text: JSON.stringify({ path }, null, 2) }],
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify({ path: levelsPathResult }, null, 2) }],
|
|
79
41
|
};
|
|
80
42
|
});
|
|
81
43
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getContext } from "../mcpContext.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { resolveMap, bodiesPathOf, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
6
|
-
import { bodyFromFirestore, ancestorDetails } from "../domain/body.js";
|
|
7
|
-
import { mergeWritesByPath } from "../firestore/restClient.js";
|
|
8
|
-
import { assertLevelsRootExists, buildUpdateEntryWrites, levelsDocExists } from "../domain/levels.js";
|
|
3
|
+
import { ancestorDetailsInLevels, buildLevelsPatchWrites, readLevelsSnapshot } from "../domain/levelsOps.js";
|
|
4
|
+
import { resolveMap, levelsPathOf, formatPath, assertCanEditMap, } from "../domain/mapResolver.js";
|
|
9
5
|
export function registerUpdateTaskStatus(server) {
|
|
10
6
|
server.tool("update_task_status", "TODOマップの要素のチェック状態を書き換える。1件でも複数件でもよい。" +
|
|
11
7
|
"「親をチェックすると配下も完了扱いになる」というアプリ内の表示上の挙動は" +
|
|
@@ -26,62 +22,22 @@ export function registerUpdateTaskStatus(server) {
|
|
|
26
22
|
throw new Error(`「${map.header.title}」はTODOマップではないため、チェック状態を持ちません。`);
|
|
27
23
|
}
|
|
28
24
|
const levelsPath = levelsPathOf(map);
|
|
29
|
-
|
|
30
|
-
if (await isLevelsOnly(client)) {
|
|
31
|
-
let levelsResults = [];
|
|
32
|
-
await client.runOptimistic(async () => {
|
|
33
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath, map.header.title);
|
|
34
|
-
const { writes, found } = buildLevelsPatchWrites(levelsPath, snapshot, updates.map((u) => ({ elementId: u.elementId, patch: { done: u.checked } })));
|
|
35
|
-
levelsResults = updates.map((u) => ({
|
|
36
|
-
elementId: u.elementId,
|
|
37
|
-
path: found.has(u.elementId)
|
|
38
|
-
? formatPath(map.header.title, ancestorDetailsInLevels(snapshot, u.elementId))
|
|
39
|
-
: null,
|
|
40
|
-
checked: u.checked,
|
|
41
|
-
found: found.has(u.elementId),
|
|
42
|
-
}));
|
|
43
|
-
return writes;
|
|
44
|
-
});
|
|
45
|
-
return {
|
|
46
|
-
content: [{ type: "text", text: JSON.stringify({ results: levelsResults }, null, 2) }],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
// 読み取り(bodies一覧・levelsの該当ドキュメント)から書き込みまでを
|
|
50
|
-
// 楽観的ロックでまとめる。読み取り後に他の操作が同じlevelsドキュメントを
|
|
51
|
-
// 書き換えていた場合はコミットがFAILED_PRECONDITIONになり、
|
|
52
|
-
// 読み取りからやり直す(runOptimisticが自動でリトライする)。
|
|
53
|
-
let results = [];
|
|
25
|
+
let levelsResults = [];
|
|
54
26
|
await client.runOptimistic(async () => {
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
writes.push({
|
|
69
|
-
path: `${bodiesPath}/${update.elementId}`,
|
|
70
|
-
fields: { done: update.checked },
|
|
71
|
-
});
|
|
72
|
-
levelUpdates.push({
|
|
73
|
-
parentId: body.parent,
|
|
74
|
-
elementId: update.elementId,
|
|
75
|
-
patch: { done: update.checked },
|
|
76
|
-
});
|
|
77
|
-
const path = formatPath(map.header.title, ancestorDetails(existingBodies, update.elementId));
|
|
78
|
-
results.push({ elementId: update.elementId, path, checked: update.checked, found: true });
|
|
79
|
-
}
|
|
80
|
-
const levelWrites = await buildUpdateEntryWrites(client, levelsPath, levelUpdates);
|
|
81
|
-
return mergeWritesByPath([...writes, ...levelWrites]);
|
|
27
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
28
|
+
const { writes, found } = buildLevelsPatchWrites(levelsPath, snapshot, updates.map((u) => ({ elementId: u.elementId, patch: { done: u.checked } })));
|
|
29
|
+
levelsResults = updates.map((u) => ({
|
|
30
|
+
elementId: u.elementId,
|
|
31
|
+
path: found.has(u.elementId)
|
|
32
|
+
? formatPath(map.header.title, ancestorDetailsInLevels(snapshot, u.elementId))
|
|
33
|
+
: null,
|
|
34
|
+
checked: u.checked,
|
|
35
|
+
found: found.has(u.elementId),
|
|
36
|
+
}));
|
|
37
|
+
return writes;
|
|
82
38
|
});
|
|
83
39
|
return {
|
|
84
|
-
content: [{ type: "text", text: JSON.stringify({ results }, null, 2) }],
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify({ results: levelsResults }, null, 2) }],
|
|
85
41
|
};
|
|
86
42
|
});
|
|
87
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mmm-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"mcpName": "io.github.nhorie-canal/mmm-mcp-server",
|
|
5
5
|
"description": "Matryoshka Mind Map の自分のデータをClaudeから直接読み書きするMCPサーバー",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"!dist/**/*.test.js"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
32
32
|
"start": "node dist/src/server.js",
|
|
33
33
|
"login": "node dist/bin/login.js",
|
|
34
|
-
"test": "
|
|
34
|
+
"test": "npm run build && node --test \"dist/src/**/*.test.js\"",
|
|
35
35
|
"prepublishOnly": "npm test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|