xampp-mcp 1.0.0 → 1.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/README.md +16 -0
- package/dist/tools/diagramEr.js +273 -0
- package/dist/tools/diagramRender.js +211 -0
- package/dist/tools/registry.js +4 -2
- package/docs/tools.md +58 -32
- package/package.json +1 -1
- package/dist/tools/tableCreate.js +0 -61
package/README.md
CHANGED
|
@@ -62,9 +62,25 @@ Variables opcionales:
|
|
|
62
62
|
- Para nombres de base/tabla usa `snake_case` (`_`) y evita `-`.
|
|
63
63
|
- El MCP usa UTF-8 (`utf8mb4`) para preservar tildes y caracteres especiales.
|
|
64
64
|
|
|
65
|
+
## Diagramas ER (flujo recomendado)
|
|
66
|
+
|
|
67
|
+
Cuando pidas un diagrama de base de datos en VS Code chat:
|
|
68
|
+
|
|
69
|
+
1. Ejecutar `mcp_xamppmcp_diagram_er` para obtener Mermaid desde el esquema real.
|
|
70
|
+
2. Previsualizar en chat con `renderMermaidDiagram` usando `structuredContent.previewRequest.args.markup`.
|
|
71
|
+
3. Si el usuario confirma SVG, ejecutar `mcp_xamppmcp_diagram_render` con `structuredContent.renderRequest.args`.
|
|
72
|
+
|
|
73
|
+
### Salida SVG por defecto
|
|
74
|
+
|
|
75
|
+
- Si no se envía `outputPath`, el SVG se guarda en `diagrams/<database>.svg` en la raíz del proyecto.
|
|
76
|
+
- Para compatibilidad con clientes estrictos, `diagram_er` entrega un `renderRequest` mínimo (`code`) y `diagram_render` infiere la base desde el hint Mermaid `%% database: <db>`.
|
|
77
|
+
|
|
65
78
|
Lista completa de tools:
|
|
66
79
|
- [docs/tools.md](docs/tools.md)
|
|
67
80
|
|
|
81
|
+
Historial de cambios:
|
|
82
|
+
- [CHANGELOG.md](CHANGELOG.md)
|
|
83
|
+
|
|
68
84
|
## Licencia
|
|
69
85
|
|
|
70
86
|
Este proyecto está bajo licencia MIT. Consulta [LICENSE](LICENSE).
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { executeMysqlSql } from "../runtime/mysqlClient.js";
|
|
2
|
+
import { ToolExecutionError, toToolTextResult } from "../runtime/errors.js";
|
|
3
|
+
import { getBoolean, getOptionalString, getString } from "../runtime/validators.js";
|
|
4
|
+
import { assertDatabaseIdentifier, assertIdentifier, escapeSqlLiteral, } from "../security/policy.js";
|
|
5
|
+
import { mysqlConnectionFromArgs } from "./shared.js";
|
|
6
|
+
function valueOrEmpty(row, key) {
|
|
7
|
+
return (row[key] ?? "").trim();
|
|
8
|
+
}
|
|
9
|
+
function parseTabularOutput(stdout) {
|
|
10
|
+
const lines = stdout
|
|
11
|
+
.split(/\r?\n/)
|
|
12
|
+
.map((line) => line.trimEnd())
|
|
13
|
+
.filter((line) => line.length > 0);
|
|
14
|
+
if (lines.length < 2) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const header = lines[0];
|
|
18
|
+
if (!header) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const headers = header.split("\t").map((item) => item.trim());
|
|
22
|
+
if (headers.length === 0) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const rows = [];
|
|
26
|
+
for (const line of lines.slice(1)) {
|
|
27
|
+
const cells = line.split("\t");
|
|
28
|
+
const row = {};
|
|
29
|
+
for (let index = 0; index < headers.length; index += 1) {
|
|
30
|
+
const key = headers[index];
|
|
31
|
+
if (!key) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
row[key] = (cells[index] ?? "").trim();
|
|
35
|
+
}
|
|
36
|
+
rows.push(row);
|
|
37
|
+
}
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
function parseTableRows(stdout) {
|
|
41
|
+
const rows = parseTabularOutput(stdout);
|
|
42
|
+
return rows
|
|
43
|
+
.map((row) => ({
|
|
44
|
+
TABLE_NAME: valueOrEmpty(row, "TABLE_NAME"),
|
|
45
|
+
}))
|
|
46
|
+
.filter((row) => row.TABLE_NAME.length > 0);
|
|
47
|
+
}
|
|
48
|
+
function parseColumnRows(stdout) {
|
|
49
|
+
const rows = parseTabularOutput(stdout);
|
|
50
|
+
return rows
|
|
51
|
+
.map((row) => ({
|
|
52
|
+
TABLE_NAME: valueOrEmpty(row, "TABLE_NAME"),
|
|
53
|
+
COLUMN_NAME: valueOrEmpty(row, "COLUMN_NAME"),
|
|
54
|
+
COLUMN_TYPE: valueOrEmpty(row, "COLUMN_TYPE"),
|
|
55
|
+
IS_NULLABLE: valueOrEmpty(row, "IS_NULLABLE"),
|
|
56
|
+
COLUMN_KEY: valueOrEmpty(row, "COLUMN_KEY"),
|
|
57
|
+
}))
|
|
58
|
+
.filter((row) => row.TABLE_NAME.length > 0 && row.COLUMN_NAME.length > 0);
|
|
59
|
+
}
|
|
60
|
+
function parseForeignKeyRows(stdout) {
|
|
61
|
+
const rows = parseTabularOutput(stdout);
|
|
62
|
+
return rows
|
|
63
|
+
.map((row) => ({
|
|
64
|
+
TABLE_NAME: valueOrEmpty(row, "TABLE_NAME"),
|
|
65
|
+
COLUMN_NAME: valueOrEmpty(row, "COLUMN_NAME"),
|
|
66
|
+
REFERENCED_TABLE_NAME: valueOrEmpty(row, "REFERENCED_TABLE_NAME"),
|
|
67
|
+
REFERENCED_COLUMN_NAME: valueOrEmpty(row, "REFERENCED_COLUMN_NAME"),
|
|
68
|
+
}))
|
|
69
|
+
.filter((row) => row.TABLE_NAME.length > 0 &&
|
|
70
|
+
row.COLUMN_NAME.length > 0 &&
|
|
71
|
+
row.REFERENCED_TABLE_NAME.length > 0 &&
|
|
72
|
+
row.REFERENCED_COLUMN_NAME.length > 0);
|
|
73
|
+
}
|
|
74
|
+
function parseTableFilter(tablesCsv) {
|
|
75
|
+
if (!tablesCsv) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const parsed = tablesCsv
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((item) => item.trim())
|
|
81
|
+
.filter((item) => item.length > 0);
|
|
82
|
+
for (const table of parsed) {
|
|
83
|
+
assertIdentifier(table, "tables");
|
|
84
|
+
}
|
|
85
|
+
return Array.from(new Set(parsed));
|
|
86
|
+
}
|
|
87
|
+
function buildInClause(column, values) {
|
|
88
|
+
if (values.length === 0) {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
const literals = values.map((value) => escapeSqlLiteral(value)).join(", ");
|
|
92
|
+
return ` AND ${column} IN (${literals})`;
|
|
93
|
+
}
|
|
94
|
+
function normalizeType(columnType) {
|
|
95
|
+
const normalized = columnType.trim();
|
|
96
|
+
if (normalized.length === 0) {
|
|
97
|
+
return "string";
|
|
98
|
+
}
|
|
99
|
+
const firstToken = normalized.split(/\s+/)[0] ?? "";
|
|
100
|
+
const safeToken = firstToken.replace(/[^A-Za-z0-9_]/g, "").toLowerCase();
|
|
101
|
+
return safeToken.length > 0 ? safeToken : "string";
|
|
102
|
+
}
|
|
103
|
+
function relationCardinality(isNullable) {
|
|
104
|
+
return isNullable.toUpperCase() === "YES" ? "|o--o{" : "||--o{";
|
|
105
|
+
}
|
|
106
|
+
export function createDiagramErTool(environment) {
|
|
107
|
+
return {
|
|
108
|
+
name: "diagram_er",
|
|
109
|
+
description: "Generates a Mermaid ER diagram from the real MySQL schema",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
database: { type: "string" },
|
|
114
|
+
tables: { type: "string", description: "Optional comma-separated table filter" },
|
|
115
|
+
show_columns: { type: "boolean" },
|
|
116
|
+
show_types: { type: "boolean" },
|
|
117
|
+
host: { type: "string" },
|
|
118
|
+
port: { type: "number" },
|
|
119
|
+
user: { type: "string" },
|
|
120
|
+
password: { type: "string" },
|
|
121
|
+
},
|
|
122
|
+
required: ["database"],
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
},
|
|
125
|
+
annotations: {
|
|
126
|
+
title: "Generate ER Diagram",
|
|
127
|
+
readOnlyHint: true,
|
|
128
|
+
openWorldHint: true,
|
|
129
|
+
},
|
|
130
|
+
handler: async (args) => {
|
|
131
|
+
const database = getString(args.database, "database");
|
|
132
|
+
const tablesCsv = getOptionalString(args.tables, "tables");
|
|
133
|
+
const showColumns = getBoolean(args.show_columns, "show_columns", true);
|
|
134
|
+
const showTypes = getBoolean(args.show_types, "show_types", true);
|
|
135
|
+
assertDatabaseIdentifier(database, "database");
|
|
136
|
+
const selectedTables = parseTableFilter(tablesCsv);
|
|
137
|
+
const schemaLiteral = escapeSqlLiteral(database);
|
|
138
|
+
const tableFilterForTables = buildInClause("TABLE_NAME", selectedTables);
|
|
139
|
+
const tablesResult = await executeMysqlSql(environment, {
|
|
140
|
+
...mysqlConnectionFromArgs(args),
|
|
141
|
+
sql: `
|
|
142
|
+
SELECT TABLE_NAME
|
|
143
|
+
FROM information_schema.TABLES
|
|
144
|
+
WHERE TABLE_SCHEMA = ${schemaLiteral}
|
|
145
|
+
AND TABLE_TYPE = 'BASE TABLE'${tableFilterForTables}
|
|
146
|
+
ORDER BY TABLE_NAME
|
|
147
|
+
`,
|
|
148
|
+
});
|
|
149
|
+
const tableRows = parseTableRows(tablesResult.stdout);
|
|
150
|
+
const tableNames = tableRows
|
|
151
|
+
.map((row) => row.TABLE_NAME)
|
|
152
|
+
.filter((value) => typeof value === "string" && value.length > 0);
|
|
153
|
+
if (tableNames.length === 0) {
|
|
154
|
+
throw new ToolExecutionError(`No tables found in database ${database}`, "NOT_FOUND");
|
|
155
|
+
}
|
|
156
|
+
const tableFilterForColumns = buildInClause("TABLE_NAME", tableNames);
|
|
157
|
+
const columnsResult = await executeMysqlSql(environment, {
|
|
158
|
+
...mysqlConnectionFromArgs(args),
|
|
159
|
+
sql: `
|
|
160
|
+
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY
|
|
161
|
+
FROM information_schema.COLUMNS
|
|
162
|
+
WHERE TABLE_SCHEMA = ${schemaLiteral}${tableFilterForColumns}
|
|
163
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
164
|
+
`,
|
|
165
|
+
});
|
|
166
|
+
const fkFilter = buildInClause("kcu.TABLE_NAME", tableNames);
|
|
167
|
+
const foreignKeysResult = await executeMysqlSql(environment, {
|
|
168
|
+
...mysqlConnectionFromArgs(args),
|
|
169
|
+
sql: `
|
|
170
|
+
SELECT
|
|
171
|
+
kcu.TABLE_NAME,
|
|
172
|
+
kcu.COLUMN_NAME,
|
|
173
|
+
kcu.REFERENCED_TABLE_NAME,
|
|
174
|
+
kcu.REFERENCED_COLUMN_NAME
|
|
175
|
+
FROM information_schema.KEY_COLUMN_USAGE kcu
|
|
176
|
+
WHERE kcu.TABLE_SCHEMA = ${schemaLiteral}
|
|
177
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL${fkFilter}
|
|
178
|
+
ORDER BY kcu.TABLE_NAME, kcu.COLUMN_NAME
|
|
179
|
+
`,
|
|
180
|
+
});
|
|
181
|
+
const columnRows = parseColumnRows(columnsResult.stdout);
|
|
182
|
+
const foreignKeyRows = parseForeignKeyRows(foreignKeysResult.stdout);
|
|
183
|
+
const columnsByTable = new Map();
|
|
184
|
+
const fkColumns = new Set();
|
|
185
|
+
const nullableByColumn = new Map();
|
|
186
|
+
for (const column of columnRows) {
|
|
187
|
+
const tableName = column.TABLE_NAME;
|
|
188
|
+
const columnName = column.COLUMN_NAME;
|
|
189
|
+
if (!tableName || !columnName) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const current = columnsByTable.get(tableName) ?? [];
|
|
193
|
+
current.push(column);
|
|
194
|
+
columnsByTable.set(tableName, current);
|
|
195
|
+
nullableByColumn.set(`${tableName}.${columnName}`, column.IS_NULLABLE || "NO");
|
|
196
|
+
}
|
|
197
|
+
for (const fk of foreignKeyRows) {
|
|
198
|
+
if (!fk.TABLE_NAME || !fk.COLUMN_NAME) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
fkColumns.add(`${fk.TABLE_NAME}.${fk.COLUMN_NAME}`);
|
|
202
|
+
}
|
|
203
|
+
const diagramLines = ["erDiagram"];
|
|
204
|
+
if (showColumns) {
|
|
205
|
+
for (const tableName of tableNames) {
|
|
206
|
+
diagramLines.push(` ${tableName} {`);
|
|
207
|
+
const columns = columnsByTable.get(tableName) ?? [];
|
|
208
|
+
for (const column of columns) {
|
|
209
|
+
const columnName = column.COLUMN_NAME;
|
|
210
|
+
if (!columnName) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const flags = [];
|
|
214
|
+
if ((column.COLUMN_KEY ?? "").toUpperCase() === "PRI") {
|
|
215
|
+
flags.push("PK");
|
|
216
|
+
}
|
|
217
|
+
if (fkColumns.has(`${tableName}.${columnName}`)) {
|
|
218
|
+
flags.push("FK");
|
|
219
|
+
}
|
|
220
|
+
const typeToken = showTypes ? normalizeType(column.COLUMN_TYPE ?? "") : "string";
|
|
221
|
+
const suffix = flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
|
222
|
+
diagramLines.push(` ${typeToken} ${columnName}${suffix}`);
|
|
223
|
+
}
|
|
224
|
+
diagramLines.push(" }");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const fk of foreignKeyRows) {
|
|
228
|
+
const childTable = fk.TABLE_NAME;
|
|
229
|
+
const childColumn = fk.COLUMN_NAME;
|
|
230
|
+
const parentTable = fk.REFERENCED_TABLE_NAME;
|
|
231
|
+
if (!childTable || !childColumn || !parentTable) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const nullable = nullableByColumn.get(`${childTable}.${childColumn}`) ?? "NO";
|
|
235
|
+
const cardinality = relationCardinality(nullable);
|
|
236
|
+
diagramLines.push(` ${parentTable} ${cardinality} ${childTable} : \"${childColumn}\"`);
|
|
237
|
+
}
|
|
238
|
+
const mermaid = [`%% database: ${database}`, ...diagramLines].join("\n");
|
|
239
|
+
const text = [
|
|
240
|
+
`ER diagram generated for database: ${database}`,
|
|
241
|
+
"Suggested flow: first preview with renderMermaidDiagram using structuredContent.previewRequest.args.markup.",
|
|
242
|
+
"After preview, ask the user in the current conversation language whether SVG output is needed.",
|
|
243
|
+
"",
|
|
244
|
+
"```mermaid",
|
|
245
|
+
mermaid,
|
|
246
|
+
"```",
|
|
247
|
+
].join("\n");
|
|
248
|
+
const structured = {
|
|
249
|
+
database,
|
|
250
|
+
tables: tableNames,
|
|
251
|
+
tableCount: tableNames.length,
|
|
252
|
+
relationshipCount: foreignKeyRows.length,
|
|
253
|
+
mermaid,
|
|
254
|
+
previewRequest: {
|
|
255
|
+
tool: "renderMermaidDiagram",
|
|
256
|
+
args: {
|
|
257
|
+
title: `ER Preview - ${database}`,
|
|
258
|
+
markup: mermaid,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
renderRequest: {
|
|
262
|
+
tool: "diagram_render",
|
|
263
|
+
args: {
|
|
264
|
+
code: mermaid,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
return toToolTextResult(text, {
|
|
269
|
+
...structured,
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { request } from "node:https";
|
|
4
|
+
import { ToolExecutionError, toToolTextResult } from "../runtime/errors.js";
|
|
5
|
+
import { getBoolean, getOptionalString, getString } from "../runtime/validators.js";
|
|
6
|
+
const SUPPORTED_DIAGRAM_TYPES = [
|
|
7
|
+
"erDiagram",
|
|
8
|
+
"flowchart",
|
|
9
|
+
"graph",
|
|
10
|
+
"sequenceDiagram",
|
|
11
|
+
"classDiagram",
|
|
12
|
+
"stateDiagram-v2",
|
|
13
|
+
"stateDiagram",
|
|
14
|
+
"journey",
|
|
15
|
+
"gantt",
|
|
16
|
+
"pie",
|
|
17
|
+
"gitGraph",
|
|
18
|
+
"gitgraph",
|
|
19
|
+
"mindmap",
|
|
20
|
+
"timeline",
|
|
21
|
+
"quadrantChart",
|
|
22
|
+
"sankey-beta",
|
|
23
|
+
"sankey",
|
|
24
|
+
"xychart-beta",
|
|
25
|
+
"xychart",
|
|
26
|
+
"block-beta",
|
|
27
|
+
"block",
|
|
28
|
+
];
|
|
29
|
+
function sanitizeFileName(value) {
|
|
30
|
+
return value
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9_-]+/g, "_")
|
|
33
|
+
.replace(/^_+|_+$/g, "")
|
|
34
|
+
.slice(0, 120);
|
|
35
|
+
}
|
|
36
|
+
function resolveOutputPath(args) {
|
|
37
|
+
if (args.outputPath && args.outputPath.trim().length > 0) {
|
|
38
|
+
const normalized = args.outputPath.trim();
|
|
39
|
+
return normalized.toLowerCase().endsWith(".svg") ? normalized : `${normalized}.svg`;
|
|
40
|
+
}
|
|
41
|
+
const baseFromDatabase = args.database ? sanitizeFileName(args.database) : "";
|
|
42
|
+
const baseFromTitle = args.title ? sanitizeFileName(args.title) : "";
|
|
43
|
+
const fileBase = baseFromDatabase || baseFromTitle || "diagram";
|
|
44
|
+
return path.join(process.cwd(), "diagrams", `${fileBase}.svg`);
|
|
45
|
+
}
|
|
46
|
+
function extractDatabaseHint(code) {
|
|
47
|
+
const lines = code.split(/\r?\n/);
|
|
48
|
+
for (const rawLine of lines) {
|
|
49
|
+
const line = rawLine.trim();
|
|
50
|
+
const match = /^%%\s*database\s*:\s*([A-Za-z0-9_\-]+)\s*$/i.exec(line);
|
|
51
|
+
if (!match) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const database = match[1]?.trim();
|
|
55
|
+
if (database && database.length > 0) {
|
|
56
|
+
return database;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function detectDiagramType(code) {
|
|
62
|
+
const firstNonEmptyLine = code
|
|
63
|
+
.split(/\r?\n/)
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.find((line) => line.length > 0 && !line.startsWith("%%"));
|
|
66
|
+
if (!firstNonEmptyLine) {
|
|
67
|
+
throw new ToolExecutionError("code cannot be empty", "INVALID_INPUT");
|
|
68
|
+
}
|
|
69
|
+
const matched = SUPPORTED_DIAGRAM_TYPES.find((diagramType) => firstNonEmptyLine.toLowerCase().startsWith(diagramType.toLowerCase()));
|
|
70
|
+
if (!matched) {
|
|
71
|
+
throw new ToolExecutionError(`Unsupported Mermaid diagram type. Start your code with one of: ${SUPPORTED_DIAGRAM_TYPES.join(", ")}`, "INVALID_INPUT");
|
|
72
|
+
}
|
|
73
|
+
return matched;
|
|
74
|
+
}
|
|
75
|
+
export async function renderMermaidToSvg(code) {
|
|
76
|
+
return await new Promise((resolve, reject) => {
|
|
77
|
+
const req = request("https://kroki.io/mermaid/svg", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
"content-type": "text/plain; charset=utf-8",
|
|
81
|
+
accept: "image/svg+xml",
|
|
82
|
+
"content-length": Buffer.byteLength(code, "utf8"),
|
|
83
|
+
},
|
|
84
|
+
}, (response) => {
|
|
85
|
+
let payload = "";
|
|
86
|
+
response.setEncoding("utf8");
|
|
87
|
+
response.on("data", (chunk) => {
|
|
88
|
+
payload += chunk;
|
|
89
|
+
});
|
|
90
|
+
response.on("end", () => {
|
|
91
|
+
const statusCode = response.statusCode ?? 0;
|
|
92
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
93
|
+
reject(new ToolExecutionError(`Remote Mermaid render failed (${statusCode})`, "RENDER_FAILED"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!payload.toLowerCase().includes("<svg")) {
|
|
97
|
+
reject(new ToolExecutionError("Renderer did not return valid SVG content", "RENDER_FAILED"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
resolve(payload);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
req.on("error", (error) => {
|
|
104
|
+
reject(new ToolExecutionError(`Remote Mermaid render error: ${error.message}`, "RENDER_FAILED"));
|
|
105
|
+
});
|
|
106
|
+
req.write(code);
|
|
107
|
+
req.end();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function createDiagramRenderTool(_environment) {
|
|
111
|
+
return {
|
|
112
|
+
name: "diagram_render",
|
|
113
|
+
description: "Validates Mermaid code and returns a rendered SVG image when possible",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
code: { type: "string", description: "Mermaid diagram source" },
|
|
118
|
+
title: { type: "string" },
|
|
119
|
+
database: { type: "string", description: "Database name for default output file naming" },
|
|
120
|
+
render_image: { type: "boolean", description: "Render remote SVG image (default true)" },
|
|
121
|
+
save_file: { type: "boolean", description: "Save SVG to disk (default true)" },
|
|
122
|
+
outputPath: { type: "string", description: "Optional absolute or relative SVG output path" },
|
|
123
|
+
},
|
|
124
|
+
required: ["code"],
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
},
|
|
127
|
+
annotations: {
|
|
128
|
+
title: "Render Mermaid Diagram",
|
|
129
|
+
readOnlyHint: false,
|
|
130
|
+
openWorldHint: true,
|
|
131
|
+
},
|
|
132
|
+
handler: async (args) => {
|
|
133
|
+
const code = getString(args.code, "code");
|
|
134
|
+
const title = getOptionalString(args.title, "title");
|
|
135
|
+
const database = getOptionalString(args.database, "database") ?? extractDatabaseHint(code);
|
|
136
|
+
const renderImage = getBoolean(args.render_image, "render_image", true);
|
|
137
|
+
const saveFile = getBoolean(args.save_file, "save_file", true);
|
|
138
|
+
const outputPathInput = getOptionalString(args.outputPath, "outputPath");
|
|
139
|
+
const diagramType = detectDiagramType(code);
|
|
140
|
+
const titleLine = title ? `Title: ${title}\n` : "";
|
|
141
|
+
const text = [
|
|
142
|
+
"Mermaid diagram ready.",
|
|
143
|
+
titleLine.length > 0 ? titleLine.trimEnd() : "",
|
|
144
|
+
`Type: ${diagramType}`,
|
|
145
|
+
"",
|
|
146
|
+
"```mermaid",
|
|
147
|
+
code,
|
|
148
|
+
"```",
|
|
149
|
+
]
|
|
150
|
+
.filter((line) => line.length > 0)
|
|
151
|
+
.join("\n");
|
|
152
|
+
if (!renderImage) {
|
|
153
|
+
return toToolTextResult(text, {
|
|
154
|
+
title,
|
|
155
|
+
diagramType,
|
|
156
|
+
mermaid: code,
|
|
157
|
+
rendered: false,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const svg = await renderMermaidToSvg(code);
|
|
162
|
+
const outputPath = resolveOutputPath({
|
|
163
|
+
outputPath: outputPathInput,
|
|
164
|
+
database,
|
|
165
|
+
title,
|
|
166
|
+
});
|
|
167
|
+
if (saveFile) {
|
|
168
|
+
const outputDir = path.dirname(outputPath);
|
|
169
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
170
|
+
await fs.writeFile(outputPath, svg, "utf8");
|
|
171
|
+
}
|
|
172
|
+
const imageBase64 = Buffer.from(svg, "utf8").toString("base64");
|
|
173
|
+
const savedLine = saveFile ? `\nSaved SVG: ${outputPath}` : "\nSVG file was not saved (save_file=false).";
|
|
174
|
+
const result = {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: `${text}${savedLine}`,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: "image",
|
|
182
|
+
data: imageBase64,
|
|
183
|
+
mimeType: "image/svg+xml",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
structuredContent: {
|
|
187
|
+
title,
|
|
188
|
+
diagramType,
|
|
189
|
+
mermaid: code,
|
|
190
|
+
rendered: true,
|
|
191
|
+
mimeType: "image/svg+xml",
|
|
192
|
+
saved: saveFile,
|
|
193
|
+
outputPath: saveFile ? outputPath : undefined,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const reason = error instanceof Error ? error.message : "Unknown render error";
|
|
200
|
+
const fallbackText = `${text}\n\nSVG render fallback: ${reason}`;
|
|
201
|
+
return toToolTextResult(fallbackText, {
|
|
202
|
+
title,
|
|
203
|
+
diagramType,
|
|
204
|
+
mermaid: code,
|
|
205
|
+
rendered: false,
|
|
206
|
+
renderError: reason,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
package/dist/tools/registry.js
CHANGED
|
@@ -2,13 +2,14 @@ import { createDbCreateTool } from "./dbCreate.js";
|
|
|
2
2
|
import { createDbExportTool } from "./dbExport.js";
|
|
3
3
|
import { createDbImportTool } from "./dbImport.js";
|
|
4
4
|
import { createDbInspectTool } from "./dbInspect.js";
|
|
5
|
+
import { createDiagramErTool } from "./diagramEr.js";
|
|
6
|
+
import { createDiagramRenderTool } from "./diagramRender.js";
|
|
5
7
|
import { createGrantManageTool } from "./grantManage.js";
|
|
6
8
|
import { createPhpCliRunTool } from "./phpCliRun.js";
|
|
7
9
|
import { createQueryExecuteTool } from "./queryExecute.js";
|
|
8
10
|
import { createPreflightCheckTool } from "./preflightCheck.js";
|
|
9
11
|
import { createQueryReadonlyTool } from "./queryReadonly.js";
|
|
10
12
|
import { createStackStatusTool } from "./stackStatus.js";
|
|
11
|
-
import { createTableCreateTool } from "./tableCreate.js";
|
|
12
13
|
import { createUserCreateTool } from "./userCreate.js";
|
|
13
14
|
export function createToolRegistry(environment) {
|
|
14
15
|
const tools = [
|
|
@@ -18,12 +19,13 @@ export function createToolRegistry(environment) {
|
|
|
18
19
|
createQueryExecuteTool(environment),
|
|
19
20
|
createDbInspectTool(environment),
|
|
20
21
|
createDbCreateTool(environment),
|
|
21
|
-
createTableCreateTool(environment),
|
|
22
22
|
createUserCreateTool(environment),
|
|
23
23
|
createGrantManageTool(environment),
|
|
24
24
|
createDbExportTool(environment),
|
|
25
25
|
createDbImportTool(environment),
|
|
26
26
|
createPhpCliRunTool(environment),
|
|
27
|
+
createDiagramErTool(environment),
|
|
28
|
+
createDiagramRenderTool(environment),
|
|
27
29
|
];
|
|
28
30
|
const toolMap = new Map();
|
|
29
31
|
for (const tool of tools) {
|
package/docs/tools.md
CHANGED
|
@@ -46,10 +46,6 @@ Campos de **base de datos** (no aceptan `-`):
|
|
|
46
46
|
- `db_export.database`
|
|
47
47
|
- `db_import.database`
|
|
48
48
|
- `grant_manage.database`
|
|
49
|
-
- `table_create.database`
|
|
50
|
-
|
|
51
|
-
Campos de **tabla** (no aceptan `-`):
|
|
52
|
-
- `table_create.table`
|
|
53
49
|
|
|
54
50
|
### Ejemplo válido para `db_create`
|
|
55
51
|
|
|
@@ -85,17 +81,6 @@ Ejemplo recomendado de texto:
|
|
|
85
81
|
- Español: `José Núñez - información pública`
|
|
86
82
|
- Inglés: `John Smith - public information`
|
|
87
83
|
|
|
88
|
-
### Ejemplo que falla en `table_create.table`
|
|
89
|
-
|
|
90
|
-
```json
|
|
91
|
-
{
|
|
92
|
-
"database": "mcp_test_2026",
|
|
93
|
-
"table": "items-test",
|
|
94
|
-
"createStatement": "CREATE TABLE `items-test` (id INT)",
|
|
95
|
-
"confirmed": true
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
84
|
## query_readonly
|
|
100
85
|
Ejecuta solo consultas `SELECT`, `SHOW`, `DESCRIBE`, `EXPLAIN`.
|
|
101
86
|
|
|
@@ -141,23 +126,6 @@ Input:
|
|
|
141
126
|
- `user?: string`
|
|
142
127
|
- `password?: string`
|
|
143
128
|
|
|
144
|
-
## table_create
|
|
145
|
-
Crea una tabla con sentencia `CREATE TABLE`.
|
|
146
|
-
|
|
147
|
-
Validaciones adicionales:
|
|
148
|
-
- `createStatement` debe empezar con `CREATE TABLE`.
|
|
149
|
-
- `createStatement` debe apuntar a la misma tabla indicada en `table`.
|
|
150
|
-
|
|
151
|
-
Input:
|
|
152
|
-
- `database: string`
|
|
153
|
-
- `table: string`
|
|
154
|
-
- `createStatement: string`
|
|
155
|
-
- `confirmed: boolean`
|
|
156
|
-
- `host?: string`
|
|
157
|
-
- `port?: number`
|
|
158
|
-
- `user?: string`
|
|
159
|
-
- `password?: string`
|
|
160
|
-
|
|
161
129
|
## user_create
|
|
162
130
|
Crea usuario MySQL.
|
|
163
131
|
|
|
@@ -219,3 +187,61 @@ Ejecuta script PHP con `php.exe` de XAMPP.
|
|
|
219
187
|
Input:
|
|
220
188
|
- `scriptPath: string`
|
|
221
189
|
- `args?: string`
|
|
190
|
+
|
|
191
|
+
## diagram_er
|
|
192
|
+
Genera un diagrama ER en Mermaid a partir del esquema real de MySQL (`information_schema`).
|
|
193
|
+
|
|
194
|
+
Input:
|
|
195
|
+
- `database: string`
|
|
196
|
+
- `tables?: string` (lista opcional separada por comas, p. ej. `users,orders`)
|
|
197
|
+
- `show_columns?: boolean` (default `true`)
|
|
198
|
+
- `show_types?: boolean` (default `true`)
|
|
199
|
+
- `host?: string`
|
|
200
|
+
- `port?: number`
|
|
201
|
+
- `user?: string`
|
|
202
|
+
- `password?: string`
|
|
203
|
+
|
|
204
|
+
Salida:
|
|
205
|
+
- Bloque de texto Mermaid (` ```mermaid ... ``` `)
|
|
206
|
+
- `structuredContent.mermaid` con el código del diagrama
|
|
207
|
+
- `structuredContent.previewRequest` para previsualizar primero con `renderMermaidDiagram`
|
|
208
|
+
- `structuredContent.renderRequest` con payload sugerido para llamar `diagram_render`
|
|
209
|
+
|
|
210
|
+
Notas:
|
|
211
|
+
- `diagram_er` no genera SVG automáticamente; prioriza la previsualización rápida en chat.
|
|
212
|
+
- Para SVG, usar `diagram_render` solo cuando el usuario lo solicite.
|
|
213
|
+
|
|
214
|
+
Flujo recomendado en VS Code chat:
|
|
215
|
+
1. Ejecutar `mcp_xamppmcp_diagram_er`.
|
|
216
|
+
2. Llamar `renderMermaidDiagram` usando `structuredContent.previewRequest.args.markup` para previsualización rápida.
|
|
217
|
+
3. Preguntar al usuario en el idioma actual de la conversación si también quiere el SVG.
|
|
218
|
+
4. Si responde que sí, llamar `mcp_xamppmcp_diagram_render` con `structuredContent.renderRequest.args`.
|
|
219
|
+
|
|
220
|
+
## diagram_render
|
|
221
|
+
Valida Mermaid, renderiza SVG y opcionalmente guarda archivo en disco.
|
|
222
|
+
|
|
223
|
+
Input:
|
|
224
|
+
- `code: string` (código Mermaid)
|
|
225
|
+
- `title?: string`
|
|
226
|
+
- `database?: string` (para nombrar archivo por defecto)
|
|
227
|
+
- `render_image?: boolean` (default `true`; si es `false`, solo valida y devuelve texto)
|
|
228
|
+
- `save_file?: boolean` (default `true`)
|
|
229
|
+
- `outputPath?: string` (ruta opcional de salida para el SVG)
|
|
230
|
+
|
|
231
|
+
Tipos Mermaid soportados al inicio del código:
|
|
232
|
+
- `erDiagram`, `flowchart`, `graph`, `sequenceDiagram`, `classDiagram`
|
|
233
|
+
- `stateDiagram-v2`, `stateDiagram`, `journey`, `gantt`, `pie`
|
|
234
|
+
- `gitGraph`, `gitgraph`, `mindmap`, `timeline`, `quadrantChart`
|
|
235
|
+
- `sankey-beta`, `sankey`, `xychart-beta`, `xychart`, `block-beta`, `block`
|
|
236
|
+
|
|
237
|
+
Salida:
|
|
238
|
+
- Bloque de texto Mermaid validado
|
|
239
|
+
- Bloque de imagen `image/svg+xml` cuando el render remoto está disponible
|
|
240
|
+
- `structuredContent` con `diagramType`, `title`, `mermaid`, `rendered`, `saved`, `outputPath`
|
|
241
|
+
|
|
242
|
+
Notas:
|
|
243
|
+
- `diagram_render` usa render remoto (Kroki) para producir SVG, por eso tiene `openWorldHint: true`.
|
|
244
|
+
- Si no hay conectividad o falla el render remoto, la tool devuelve fallback textual con el Mermaid.
|
|
245
|
+
- Si `save_file=true` y no se envía `outputPath`, el archivo se guarda por defecto en `diagrams/<database>.svg` (o `diagrams/<title>.svg`) en la raíz del proyecto.
|
|
246
|
+
- Si una llamada solo envía `code`, la tool también intenta inferir la base de datos desde un comentario Mermaid `%% database: <db>` para evitar guardar como `diagram.svg`.
|
|
247
|
+
- `diagram_er` publica `renderRequest.args` con payload mínimo (`code`) para máxima compatibilidad con clientes que validan esquemas de tools de forma estricta (y evitan errores por propiedades adicionales).
|
package/package.json
CHANGED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { executeMysqlSql } from "../runtime/mysqlClient.js";
|
|
2
|
-
import { toToolTextResult } from "../runtime/errors.js";
|
|
3
|
-
import { getBoolean, getString } from "../runtime/validators.js";
|
|
4
|
-
import { assertDatabaseIdentifier, assertIdentifier, requireConfirmation } from "../security/policy.js";
|
|
5
|
-
import { mysqlConnectionFromArgs } from "./shared.js";
|
|
6
|
-
function normalizeCreateStatement(statement, tableName) {
|
|
7
|
-
const compact = statement.trim();
|
|
8
|
-
const upper = compact.toUpperCase();
|
|
9
|
-
if (!upper.startsWith("CREATE TABLE")) {
|
|
10
|
-
throw new Error("createStatement must start with CREATE TABLE");
|
|
11
|
-
}
|
|
12
|
-
if (!upper.includes(tableName.toUpperCase())) {
|
|
13
|
-
throw new Error("createStatement must target the provided table");
|
|
14
|
-
}
|
|
15
|
-
return compact;
|
|
16
|
-
}
|
|
17
|
-
export function createTableCreateTool(environment) {
|
|
18
|
-
return {
|
|
19
|
-
name: "table_create",
|
|
20
|
-
description: "Creates a table using a CREATE TABLE statement",
|
|
21
|
-
inputSchema: {
|
|
22
|
-
type: "object",
|
|
23
|
-
properties: {
|
|
24
|
-
database: { type: "string" },
|
|
25
|
-
table: { type: "string" },
|
|
26
|
-
createStatement: { type: "string" },
|
|
27
|
-
confirmed: { type: "boolean" },
|
|
28
|
-
host: { type: "string" },
|
|
29
|
-
port: { type: "number" },
|
|
30
|
-
user: { type: "string" },
|
|
31
|
-
password: { type: "string" },
|
|
32
|
-
},
|
|
33
|
-
required: ["database", "table", "createStatement", "confirmed"],
|
|
34
|
-
additionalProperties: false,
|
|
35
|
-
},
|
|
36
|
-
annotations: {
|
|
37
|
-
title: "Create Table",
|
|
38
|
-
destructiveHint: true,
|
|
39
|
-
openWorldHint: false,
|
|
40
|
-
},
|
|
41
|
-
handler: async (args) => {
|
|
42
|
-
const database = getString(args.database, "database");
|
|
43
|
-
const table = getString(args.table, "table");
|
|
44
|
-
const createStatement = getString(args.createStatement, "createStatement");
|
|
45
|
-
const confirmed = getBoolean(args.confirmed, "confirmed");
|
|
46
|
-
requireConfirmation(confirmed, "table_create");
|
|
47
|
-
assertDatabaseIdentifier(database, "database");
|
|
48
|
-
assertIdentifier(table, "table");
|
|
49
|
-
const sql = normalizeCreateStatement(createStatement, table);
|
|
50
|
-
await executeMysqlSql(environment, {
|
|
51
|
-
...mysqlConnectionFromArgs(args),
|
|
52
|
-
database,
|
|
53
|
-
sql,
|
|
54
|
-
});
|
|
55
|
-
return toToolTextResult(`Table ${database}.${table} created`, {
|
|
56
|
-
database,
|
|
57
|
-
table,
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|