mmm-mcp-server 0.1.2 → 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/dist/src/domain/appendForest.js +0 -50
- package/dist/src/domain/levels.js +3 -182
- 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 -91
- package/dist/src/tools/editElement.js +11 -44
- package/dist/src/tools/importMarkdownContext.js +9 -53
- package/dist/src/tools/listElements.js +2 -7
- package/dist/src/tools/moveElement.js +8 -50
- 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 -25
- package/dist/src/domain/moveElement.js +0 -93
|
@@ -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,88 +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
|
-
// 切替後はrootが無いマップを空のマップとして扱うので、この確認は切替前だけ。
|
|
27
|
-
const levelsOnly = await isLevelsOnly(client);
|
|
28
|
-
if (!levelsOnly) {
|
|
29
|
-
assertLevelsRootExists(await levelsDocExists(client, levelsPath, "root"), map.header.title);
|
|
30
|
-
}
|
|
31
|
-
if (levelsOnly) {
|
|
32
|
-
if (confirmed !== true) {
|
|
33
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
34
|
-
const allTargets = new Set();
|
|
35
|
-
const targets = [];
|
|
36
|
-
for (const elementId of elementIds) {
|
|
37
|
-
if (!locateInLevels(snapshot, elementId)) {
|
|
38
|
-
throw new Error(`elementId(${elementId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
39
|
-
}
|
|
40
|
-
const subtree = subtreeIdsInLevels(snapshot, elementId);
|
|
41
|
-
for (const id of subtree)
|
|
42
|
-
allTargets.add(id);
|
|
43
|
-
targets.push({
|
|
44
|
-
elementId,
|
|
45
|
-
path: formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId)),
|
|
46
|
-
subtreeCount: subtree.length,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
content: [
|
|
51
|
-
{
|
|
52
|
-
type: "text",
|
|
53
|
-
text: JSON.stringify({
|
|
54
|
-
confirmationRequired: true,
|
|
55
|
-
message: "削除は取り返しがつきません。以下の内容をユーザーに提示して明示的な同意を得てから、" +
|
|
56
|
-
"confirmed: trueで呼び直してください。",
|
|
57
|
-
targets,
|
|
58
|
-
totalElementCount: allTargets.size,
|
|
59
|
-
}, null, 2),
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
const levelsDeleted = [];
|
|
65
|
-
let levelsTotal = 0;
|
|
66
|
-
await client.runOptimistic(async () => {
|
|
67
|
-
levelsDeleted.length = 0;
|
|
68
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
69
|
-
for (const elementId of elementIds) {
|
|
70
|
-
if (!locateInLevels(snapshot, elementId))
|
|
71
|
-
continue; // 既に削除済み(リトライ時など)
|
|
72
|
-
levelsDeleted.push({
|
|
73
|
-
elementId,
|
|
74
|
-
path: formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId)),
|
|
75
|
-
subtreeCount: subtreeIdsInLevels(snapshot, elementId).length,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
const { writes, deletedIds } = buildLevelsDeleteWrites(levelsPath, snapshot, elementIds);
|
|
79
|
-
levelsTotal = deletedIds.size;
|
|
80
|
-
return writes;
|
|
81
|
-
});
|
|
82
|
-
return {
|
|
83
|
-
content: [
|
|
84
|
-
{
|
|
85
|
-
type: "text",
|
|
86
|
-
text: JSON.stringify({ deleted: levelsDeleted, totalElementCount: levelsTotal }, null, 2),
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
21
|
if (confirmed !== true) {
|
|
92
|
-
const
|
|
93
|
-
const existingBodies = existingDocs.map((d) => bodyFromFirestore(d.id, d.data, d.updateTime));
|
|
94
|
-
const byId = new Map(existingBodies.map((b) => [b.id, b]));
|
|
22
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
95
23
|
const allTargets = new Set();
|
|
96
24
|
const targets = [];
|
|
97
25
|
for (const elementId of elementIds) {
|
|
98
|
-
if (!
|
|
26
|
+
if (!locateInLevels(snapshot, elementId)) {
|
|
99
27
|
throw new Error(`elementId(${elementId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
100
28
|
}
|
|
101
|
-
const subtree =
|
|
29
|
+
const subtree = subtreeIdsInLevels(snapshot, elementId);
|
|
102
30
|
for (const id of subtree)
|
|
103
31
|
allTargets.add(id);
|
|
104
32
|
targets.push({
|
|
105
33
|
elementId,
|
|
106
|
-
path: formatPath(map.header.title,
|
|
34
|
+
path: formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId)),
|
|
107
35
|
subtreeCount: subtree.length,
|
|
108
36
|
});
|
|
109
37
|
}
|
|
@@ -122,31 +50,29 @@ export function registerDeleteElements(server) {
|
|
|
122
50
|
],
|
|
123
51
|
};
|
|
124
52
|
}
|
|
125
|
-
const
|
|
126
|
-
let
|
|
53
|
+
const levelsDeleted = [];
|
|
54
|
+
let levelsTotal = 0;
|
|
127
55
|
await client.runOptimistic(async () => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const currentDocs = await client.listDocuments(bodiesPath);
|
|
131
|
-
const currentBodies = currentDocs.map((d) => bodyFromFirestore(d.id, d.data, d.updateTime));
|
|
56
|
+
levelsDeleted.length = 0;
|
|
57
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
132
58
|
for (const elementId of elementIds) {
|
|
133
|
-
if (!
|
|
59
|
+
if (!locateInLevels(snapshot, elementId))
|
|
134
60
|
continue; // 既に削除済み(リトライ時など)
|
|
135
|
-
|
|
61
|
+
levelsDeleted.push({
|
|
136
62
|
elementId,
|
|
137
|
-
path: formatPath(map.header.title,
|
|
138
|
-
subtreeCount:
|
|
63
|
+
path: formatPath(map.header.title, ancestorDetailsInLevels(snapshot, elementId)),
|
|
64
|
+
subtreeCount: subtreeIdsInLevels(snapshot, elementId).length,
|
|
139
65
|
});
|
|
140
66
|
}
|
|
141
|
-
const { writes, deletedIds } =
|
|
142
|
-
|
|
67
|
+
const { writes, deletedIds } = buildLevelsDeleteWrites(levelsPath, snapshot, elementIds);
|
|
68
|
+
levelsTotal = deletedIds.size;
|
|
143
69
|
return writes;
|
|
144
70
|
});
|
|
145
71
|
return {
|
|
146
72
|
content: [
|
|
147
73
|
{
|
|
148
74
|
type: "text",
|
|
149
|
-
text: JSON.stringify({ deleted:
|
|
75
|
+
text: JSON.stringify({ deleted: levelsDeleted, totalElementCount: levelsTotal }, null, 2),
|
|
150
76
|
},
|
|
151
77
|
],
|
|
152
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);
|
|
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);
|
|
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,8 +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 {
|
|
5
|
-
import { assertLevelsInitialized, buildTreeFromLevels, readAllLevelDocs, } from "../domain/levels.js";
|
|
4
|
+
import { buildTreeFromLevels, readAllLevelDocs, } from "../domain/levels.js";
|
|
6
5
|
// ElementNodeの`done`を`checked`という名前で公開する(仕様書の用語に合わせる)。
|
|
7
6
|
function toChecked(nodes) {
|
|
8
7
|
return nodes.map((n) => ({
|
|
@@ -78,12 +77,8 @@ export function registerListElements(server) {
|
|
|
78
77
|
return { content: [{ type: "text", text: buildConfirmationRequestText(map.header.title) }] };
|
|
79
78
|
}
|
|
80
79
|
}
|
|
80
|
+
// levels/rootが無いマップは空のマップとして返す(アプリで開いてもrootは作られない)。
|
|
81
81
|
const levelsById = await readAllLevelDocs(client, levelsPathOf(map));
|
|
82
|
-
// 切替前はrootが無ければ未移行(bodiesに中身があり得る)なので止める。
|
|
83
|
-
// 切替後はアプリで開いてもrootが作られないので、空のマップとして返す。
|
|
84
|
-
if (!(await isLevelsOnly(client))) {
|
|
85
|
-
assertLevelsInitialized(levelsById, map.header.title);
|
|
86
|
-
}
|
|
87
82
|
const tree = buildTreeFromLevels(levelsById);
|
|
88
83
|
return {
|
|
89
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,62 +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
|
-
// 切替後はrootが無いマップを空のマップとして扱うので、この確認は切替前だけ。
|
|
29
|
-
const levelsOnly = await isLevelsOnly(client);
|
|
30
|
-
if (!levelsOnly) {
|
|
31
|
-
assertLevelsRootExists(await levelsDocExists(client, levelsPath, "root"), map.header.title);
|
|
32
|
-
}
|
|
33
23
|
if (elementId === newParentElementId) {
|
|
34
24
|
throw new Error("要素を自分自身の子にすることはできません。");
|
|
35
25
|
}
|
|
36
26
|
if (elementId === afterElementId) {
|
|
37
27
|
throw new Error("afterElementIdにelementId自身は指定できません。");
|
|
38
28
|
}
|
|
39
|
-
|
|
40
|
-
let levelsPathResult = null;
|
|
41
|
-
await client.runOptimistic(async () => {
|
|
42
|
-
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
43
|
-
if (afterElementId !== null && !locateInLevels(snapshot, afterElementId)) {
|
|
44
|
-
throw new Error(`afterElementId(${afterElementId})がこのマップに見つかりません。`);
|
|
45
|
-
}
|
|
46
|
-
const writes = buildLevelsMoveWrites(levelsPath, snapshot, elementId, newParentElementId, afterElementId);
|
|
47
|
-
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, newParentElementId));
|
|
48
|
-
return writes;
|
|
49
|
-
});
|
|
50
|
-
return {
|
|
51
|
-
content: [{ type: "text", text: JSON.stringify({ path: levelsPathResult }, null, 2) }],
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
let path = null;
|
|
29
|
+
let levelsPathResult = null;
|
|
55
30
|
await client.runOptimistic(async () => {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const byId = new Map(existingBodies.map((b) => [b.id, b]));
|
|
59
|
-
const body = byId.get(elementId);
|
|
60
|
-
if (!body) {
|
|
61
|
-
throw new Error(`elementId(${elementId})がこのマップに見つかりません。list_elementsで確認してください。`);
|
|
62
|
-
}
|
|
63
|
-
if (newParentElementId !== null && !byId.has(newParentElementId)) {
|
|
64
|
-
throw new Error(`newParentElementId(${newParentElementId})がこのマップに見つかりません。`);
|
|
65
|
-
}
|
|
66
|
-
if (afterElementId !== null && !byId.has(afterElementId)) {
|
|
31
|
+
const snapshot = await readLevelsSnapshot(client, levelsPath);
|
|
32
|
+
if (afterElementId !== null && !locateInLevels(snapshot, afterElementId)) {
|
|
67
33
|
throw new Error(`afterElementId(${afterElementId})がこのマップに見つかりません。`);
|
|
68
34
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (cur === elementId) {
|
|
72
|
-
throw new Error("要素を自分自身の配下(子孫)の中へ移動することはできません。");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const writes = await buildMoveElementWrites(client, bodiesPath, levelsPath, existingBodies, elementId, newParentElementId, afterElementId);
|
|
76
|
-
path = formatPath(map.header.title, newParentElementId === null
|
|
77
|
-
? []
|
|
78
|
-
: ancestorDetails(existingBodies, newParentElementId));
|
|
35
|
+
const writes = buildLevelsMoveWrites(levelsPath, snapshot, elementId, newParentElementId, afterElementId);
|
|
36
|
+
levelsPathResult = formatPath(map.header.title, ancestorDetailsInLevels(snapshot, newParentElementId));
|
|
79
37
|
return writes;
|
|
80
38
|
});
|
|
81
39
|
return {
|
|
82
|
-
content: [{ type: "text", text: JSON.stringify({ path }, null, 2) }],
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify({ path: levelsPathResult }, null, 2) }],
|
|
83
41
|
};
|
|
84
42
|
});
|
|
85
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);
|
|
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": {
|