mcp-server-debug-thinking
Version:
Graph-based MCP server for systematic debugging using Problem-Solution Trees and Hypothesis-Experiment-Learning cycles
199 lines • 7.86 kB
JavaScript
import path from "path";
import fs from "fs/promises";
import { ensureDirectory, writeJsonFile, appendJsonLine, readJsonLines, fileExists, } from "../utils/storage.js";
import { logger } from "../utils/logger.js";
import { DATA_DIR_NAME } from "../constants.js";
/**
* グラフデータの永続化を担当するストレージクラス
* JSONL形式でノードとエッジを追記保存し、メタデータはJSONで保存
* データディレクトリ: ~/.debug-thinking-mcp/
*/
export class GraphStorage {
dataDir;
nodesFile;
edgesFile;
metadataFile;
/**
* ストレージパスを初期化
* 環境変数DEBUG_DATA_DIRが設定されていればそれを使用
* 指定がなければカレントディレクトリ(MCPを呼び出すプロジェクトのルート)に保存
*/
constructor() {
const baseDir = process.env.DEBUG_DATA_DIR || process.cwd();
this.dataDir = path.join(baseDir, DATA_DIR_NAME);
this.nodesFile = path.join(this.dataDir, "nodes.jsonl");
this.edgesFile = path.join(this.dataDir, "edges.jsonl");
this.metadataFile = path.join(this.dataDir, "graph-metadata.json");
}
/**
* ストレージディレクトリを作成して初期化
* ディレクトリが存在しない場合は再帰的に作成
*/
async initialize() {
await ensureDirectory(this.dataDir);
logger.dim(`📁 Graph storage initialized at: ${this.dataDir}`);
}
async saveNode(node) {
try {
// DateオブジェクトをISO文字列に変換してJSONシリアライズ可能に
const serializable = {
...node,
metadata: {
...node.metadata,
createdAt: node.metadata.createdAt.toISOString(),
updatedAt: node.metadata.updatedAt.toISOString(),
},
};
await appendJsonLine(this.nodesFile, serializable);
}
catch (error) {
logger.error("Failed to save node:", error);
throw error;
}
}
/**
* エッジをJSONLファイルに追記保存
* メタデータが存在する場合のみ日付を変換
*/
async saveEdge(edge) {
try {
// メタデータが存在する場合のみ日付をISO文字列に変換
const serializable = {
...edge,
metadata: edge.metadata
? {
...edge.metadata,
createdAt: edge.metadata.createdAt.toISOString(),
}
: undefined,
};
await appendJsonLine(this.edgesFile, serializable);
}
catch (error) {
logger.error("Failed to save edge:", error);
throw error;
}
}
/**
* グラフメタデータをJSONファイルに保存
* ルートノードリストとノード/エッジ数も記録
* 毎回上書き保存(追記ではない)
*/
async saveGraphMetadata(graph) {
try {
const metadata = {
...graph.metadata,
createdAt: graph.metadata.createdAt.toISOString(),
lastModified: graph.metadata.lastModified.toISOString(),
roots: graph.roots,
nodeCount: graph.nodes.size,
edgeCount: graph.edges.size,
};
await writeJsonFile(this.metadataFile, metadata);
}
catch (error) {
logger.error("Failed to save graph metadata:", error);
throw error;
}
}
/**
* 保存されたグラフデータを読み込み
* JSONLファイルからノードとエッジを復元し、Map構造を再構築
* 重複データは最新のものを保持
* @returns 復元されたグラフまたはnull(データがない場合)
*/
async loadGraph() {
try {
// 各ファイルの存在を確認(非同期で並列処理)
const hasNodes = await fileExists(this.nodesFile);
const hasEdges = await fileExists(this.edgesFile);
const hasMetadata = await fileExists(this.metadataFile);
if (!hasNodes && !hasEdges && !hasMetadata) {
return null;
}
// メタデータファイルから基本情報を読み込み
// ファイルがない/読み込み失敗時はデフォルト値を使用
let metadata = {
createdAt: new Date(),
lastModified: new Date(),
sessionCount: 0,
};
let roots = [];
if (hasMetadata) {
try {
const content = await fs.readFile(this.metadataFile, "utf-8");
const meta = JSON.parse(content);
metadata = {
...meta,
createdAt: new Date(meta.createdAt),
lastModified: new Date(meta.lastModified),
};
// ルート問題ノードのIDリストを復元
roots = meta.roots || [];
}
catch (error) {
logger.error("Failed to load metadata:", error);
}
}
// 空のグラフ構造を作成してデータをロード
const graph = {
nodes: new Map(),
edges: new Map(),
roots,
metadata,
};
// JSONLファイルからすべてのノードを読み込み
if (hasNodes) {
const nodes = await readJsonLines(this.nodesFile);
// 同一IDのノードが複数ある場合は最後のものを使用(アップンド形式のため)
const nodeMap = new Map();
for (const node of nodes) {
nodeMap.set(node.id, node);
}
for (const node of nodeMap.values()) {
graph.nodes.set(node.id, {
...node,
metadata: {
...node.metadata,
createdAt: new Date(node.metadata.createdAt),
updatedAt: new Date(node.metadata.updatedAt),
},
});
}
}
// JSONLファイルからすべてのエッジを読み込み
if (hasEdges) {
const edges = await readJsonLines(this.edgesFile);
// 同一IDのエッジが複数ある場合は最後のものを使用
const edgeMap = new Map();
for (const edge of edges) {
edgeMap.set(edge.id, edge);
}
for (const edge of edgeMap.values()) {
graph.edges.set(edge.id, {
...edge,
metadata: edge.metadata
? {
...edge.metadata,
createdAt: new Date(edge.metadata.createdAt),
}
: undefined,
});
}
}
return graph;
}
catch (error) {
logger.error("Failed to load graph:", error);
return null;
}
}
/**
* ストレージをクリア(テスト/リセット用)
* TODO: 実装予定 - ファイル削除またはディレクトリクリア
*/
async clearStorage() {
// TODO: 実装予定
}
}
//# sourceMappingURL=GraphStorage.js.map