#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import express from "express";
import cors from "cors";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// MCPサーバーの設定
const server = new Server(
{
name: "network-diagram-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// データストア(後でMCPリクエストに応じて更新される)
const dataStore = {
nodes: [] as Array<{ id: string; label: string; x: number; y: number; color?: string; solution?: string; filePath?: string; lineNumber?: number; code?: string; group?: string }>,
edges: [] as Array<{ from: string; to: string; label?: string }>,
groups: [] as Array<{ id: string; label: string; color?: string }>,
lastUpdate: new Date().toISOString(),
};
// ツールの定義
const tools: Tool[] = [
{
name: "set_diagram",
description: "ネットワークダイアグラム全体を設定します。既存のダイアグラムは完全に上書きされます。",
inputSchema: {
type: "object",
properties: {
nodes: {
type: "array",
description: "ノードの配列",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "ノードのID",
},
label: {
type: "string",
description: "ノードのラベル",
},
color: {
type: "string",
description: "ノードの背景色(オプション、16進数カラーコード)。白文字なので濃い色を推奨(例: #2563eb, #dc2626, #059669, #7c3aed)",
},
solution: {
type: "string",
description: "ソリューション名(オプション)",
},
filePath: {
type: "string",
description: "プロジェクトルートからのファイルパス(オプション)",
},
lineNumber: {
type: "number",
description: "対象コードの行番号(オプション)",
},
code: {
type: "string",
description: "表示する生コード(オプション)",
},
group: {
type: "string",
description: "所属するグループのID(オプション)",
},
x: {
type: "number",
description: "X座標(オプション)",
},
y: {
type: "number",
description: "Y座標(オプション)",
},
},
required: ["id", "label"],
},
},
groups: {
type: "array",
description: "グループの配列(オプション)",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "グループのID",
},
label: {
type: "string",
description: "グループのラベル",
},
color: {
type: "string",
description: "グループの背景色(オプション、16進数カラーコード、例: #e0f2fe, #fef3c7, #e0e7ff)",
},
},
required: ["id", "label"],
},
},
edges: {
type: "array",
description: "エッジの配列",
items: {
type: "object",
properties: {
from: {
type: "string",
description: "開始ノードのID",
},
to: {
type: "string",
description: "終了ノードのID",
},
label: {
type: "string",
description: "エッジのラベル(オプション)",
},
},
required: ["from", "to"],
},
},
},
required: ["nodes", "edges"],
},
},
];
// ツール一覧を返すハンドラ
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
// ツール実行ハンドラ
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "set_diagram": {
const { nodes, edges, groups } = args as {
nodes: Array<{ id: string; label: string; x?: number; y?: number; color?: string; solution?: string; filePath?: string; lineNumber?: number; code?: string; group?: string }>;
edges: Array<{ from: string; to: string; label?: string }>;
groups?: Array<{ id: string; label: string; color?: string }>;
};
// ノードのデフォルト座標を設定
dataStore.nodes = nodes.map(node => ({
id: node.id,
label: node.label,
x: node.x ?? Math.random() * 800,
y: node.y ?? Math.random() * 600,
color: node.color,
solution: node.solution,
filePath: node.filePath,
lineNumber: node.lineNumber,
code: node.code,
group: node.group,
}));
// エッジをそのまま設定
dataStore.edges = edges;
// グループを設定
dataStore.groups = groups || [];
dataStore.lastUpdate = new Date().toISOString();
const url = `http://localhost:${actualPort}`;
return {
content: [
{
type: "text",
text: `ダイアグラムを設定しました: ${nodes.length}個のノード、${edges.length}個のエッジ\n\nWebインターフェース: ${url}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Expressサーバーの設定
const app = express();
const INITIAL_PORT = parseInt(process.env.PORT || "3000", 10);
let actualPort = INITIAL_PORT;
app.use(cors());
app.use(express.json());
// 静的ファイルの配信
app.use(express.static(path.join(__dirname, "public")));
// APIエンドポイント
app.get("/api/diagram", (req, res) => {
res.json(dataStore);
});
// ダイアグラム全体を設定(上書き)
app.put("/api/diagram", (req, res) => {
const { nodes, edges, groups } = req.body;
if (!Array.isArray(nodes) || !Array.isArray(edges)) {
return res.status(400).json({ error: "nodes and edges must be arrays" });
}
// ノードのデフォルト座標を設定
dataStore.nodes = nodes.map((node: any) => ({
id: node.id,
label: node.label,
x: node.x ?? Math.random() * 800,
y: node.y ?? Math.random() * 600,
color: node.color,
solution: node.solution,
filePath: node.filePath,
lineNumber: node.lineNumber,
code: node.code,
group: node.group,
}));
dataStore.edges = edges;
dataStore.groups = groups || [];
dataStore.lastUpdate = new Date().toISOString();
res.json({
success: true,
nodes: dataStore.nodes.length,
edges: dataStore.edges.length,
groups: dataStore.groups.length
});
});
app.get("/api/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// ポートを試行してサーバーを起動する関数
function startServer(port: number, maxRetries: number = 10): Promise<any> {
return new Promise((resolve, reject) => {
const server = app.listen(port, () => {
actualPort = port;
console.error(`HTTP server running on http://localhost:${port}`);
console.error(`Web interface: http://localhost:${port}`);
console.error(`API available at: http://localhost:${port}/api`);
resolve(server);
});
server.on("error", (err: any) => {
if (err.code === "EADDRINUSE") {
if (maxRetries > 0) {
console.error(`Port ${port} is in use, trying ${port + 1}...`);
server.close();
resolve(startServer(port + 1, maxRetries - 1));
} else {
reject(new Error(`Could not find available port after ${10 - maxRetries} attempts`));
}
} else {
reject(err);
}
});
});
}
// HTTPサーバーの起動
let httpServer: any;
startServer(INITIAL_PORT).then((server) => {
httpServer = server;
}).catch((error) => {
console.error("Failed to start HTTP server:", error);
process.exit(1);
});
// MCPサーバーの起動
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
// クリーンアップ
process.on("SIGINT", () => {
console.error("\nShutting down...");
httpServer.close();
process.exit(0);
});