xampp-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dony-aep
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,42 +2,16 @@
2
2
 
3
3
  MCP server para administrar XAMPP y MySQL en Windows desde clientes MCP por `stdio` (VS Code, Copilot agents, etc.).
4
4
 
5
- ## Alcance v1
6
-
7
- - Windows + XAMPP local.
8
- - Transporte MCP: `stdio`.
9
- - Estado de Apache/MySQL y recomendación de arranque manual desde XAMPP.
10
- - Administración de base de datos (crear DB, tablas, usuarios, grants, import/export).
11
- - Consultas SQL de solo lectura y ejecución general con confirmación explícita.
12
- - Inspección de estado de base de datos (resumen y detalle de tablas).
13
-
14
5
  ## Requisitos
15
6
 
16
- - Node.js 20+.
17
- - XAMPP instalado (por defecto en `C:\xampp`).
18
- - Permisos de sistema suficientes para las acciones de servicio si usas modo `service`.
7
+ - Windows
8
+ - Node.js 20+
9
+ - XAMPP instalado (por defecto en `C:\xampp`)
19
10
 
20
- ## Configuración
11
+ ## Instalación
21
12
 
22
- Variables opcionales de entorno:
23
-
24
- - `XAMPP_DIR` (default `C:\xampp`)
25
- - `XAMPP_DEFAULT_MODE` (`console` | `service`, default `console`)
26
- - `XAMPP_APACHE_SERVICE` (default `Apache2.4`)
27
- - `XAMPP_MYSQL_SERVICE` (default `mysql`)
28
- - `MYSQL_HOST` (default `127.0.0.1`)
29
- - `MYSQL_PORT` (default `3306`)
30
- - `MYSQL_USER` (default `root`)
31
- - `MYSQL_PASSWORD` (sin default)
32
-
33
- ## Scripts
34
-
35
- - `npm run dev`: ejecución directa TypeScript.
36
- - `npm run build`: compila a `dist`.
37
- - `npm run start`: ejecuta servidor compilado.
38
- - `npm run lint`: typecheck.
39
-
40
- ## Uso como paquete npm
13
+ Paquete npm:
14
+ - https://www.npmjs.com/package/xampp-mcp
41
15
 
42
16
  Instalación global:
43
17
 
@@ -45,86 +19,68 @@ Instalación global:
45
19
  npm install -g xampp-mcp
46
20
  ```
47
21
 
48
- Ejecución directa:
49
-
50
- ```powershell
51
- xampp-mcp
52
- ```
53
-
54
22
  Sin instalación global:
55
23
 
56
24
  ```powershell
57
25
  npx -y xampp-mcp
58
26
  ```
59
27
 
60
- Ejemplo de configuración MCP en VS Code:
28
+ ## Configuración en VS Code
29
+
30
+ Ejemplo recomendado en `.vscode/mcp.json`:
61
31
 
62
32
  ```jsonc
63
33
  {
64
- "servers": {
65
- "xamppMcp": {
66
- "type": "stdio",
67
- "command": "xampp-mcp",
68
- "env": {
69
- "XAMPP_DIR": "C:\\xampp",
70
- "XAMPP_DEFAULT_MODE": "console"
71
- }
72
- }
73
- }
34
+ "servers": {
35
+ "xamppMcp": {
36
+ "type": "stdio",
37
+ "command": "npx",
38
+ "args": ["-y", "xampp-mcp"],
39
+ "env": {
40
+ "XAMPP_DIR": "C:\\xampp",
41
+ "XAMPP_DEFAULT_MODE": "console"
42
+ }
43
+ }
44
+ }
74
45
  }
75
46
  ```
76
47
 
77
- Si usas `npx` en vez de instalación global:
48
+ Variables opcionales:
78
49
 
79
- ```jsonc
80
- {
81
- "servers": {
82
- "xamppMcp": {
83
- "type": "stdio",
84
- "command": "npx",
85
- "args": ["-y", "xampp-mcp"],
86
- "env": {
87
- "XAMPP_DIR": "C:\\xampp",
88
- "XAMPP_DEFAULT_MODE": "console"
89
- }
90
- }
91
- }
92
- }
93
- ```
50
+ - `XAMPP_DIR` (default `C:\xampp`)
51
+ - `XAMPP_DEFAULT_MODE` (`console` | `service`, default `console`)
52
+ - `XAMPP_APACHE_SERVICE` (default `Apache2.4`)
53
+ - `XAMPP_MYSQL_SERVICE` (default `mysql`)
54
+ - `MYSQL_HOST` (default `127.0.0.1`)
55
+ - `MYSQL_PORT` (default `3306`)
56
+ - `MYSQL_USER` (default `root`)
57
+ - `MYSQL_PASSWORD` (sin default)
94
58
 
95
- ## Publicación npm
59
+ ## Notas de uso importantes
96
60
 
97
- ```powershell
98
- npm login
99
- npm version patch
100
- npm publish
101
- ```
61
+ - Si Apache/MySQL están apagados, primero debes iniciarlos manualmente desde XAMPP Control Panel.
62
+ - Para nombres de base/tabla usa `snake_case` (`_`) y evita `-`.
63
+ - El MCP usa UTF-8 (`utf8mb4`) para preservar tildes y caracteres especiales.
64
+
65
+ ## Diagramas ER (flujo recomendado)
102
66
 
103
- ## Seguridad
67
+ Cuando pidas un diagrama de base de datos en VS Code chat:
104
68
 
105
- - No hay tool de shell genérico.
106
- - Operaciones sensibles requieren `confirmed: true`.
107
- - Validación estricta de identificadores para evitar inyección SQL en DDL común.
108
- - En v1 el transporte es local `stdio`; no se incluye HTTP/OAuth.
109
- - El control de arranque/parada de Apache/MySQL se mantiene fuera del catálogo activo; el usuario debe operarlo manualmente desde XAMPP.
110
- - Si MySQL/Apache están apagados, las tools deben pedir activación manual en XAMPP Control Panel y esperar confirmación del usuario antes de reintentar.
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`.
111
72
 
112
- ## Reglas de nombres
73
+ ### Salida SVG por defecto
113
74
 
114
- - Usar `snake_case` para nombres de base, tablas y demás identificadores.
115
- - Regla: empezar por letra o `_`, y luego usar solo letras, números y `_`.
116
- - No usar `-` (guion medio); el MCP devuelve sugerencia automática reemplazando `-` por `_`.
117
- - Ejemplo recomendado: `database-example-2026` → `database_example_2026`.
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>`.
118
77
 
119
- ## Codificación de texto
78
+ Lista completa de tools:
79
+ - [docs/tools.md](docs/tools.md)
120
80
 
121
- - Política única: usar UTF-8 (`utf8mb4`) para español e inglés.
122
- - No hace falta cambiar encoding según idioma o contexto; se conserva el texto original (con o sin tildes).
123
- - `db_create` crea bases con `utf8mb4` / `utf8mb4_unicode_ci` para evitar problemas con acentos y caracteres especiales.
124
- - En ejecución SQL, el MCP fuerza `SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci` por sesión para evitar conversiones `cp850` en Windows.
81
+ Historial de cambios:
82
+ - [CHANGELOG.md](CHANGELOG.md)
125
83
 
126
- ## Próximos pasos sugeridos
84
+ ## Licencia
127
85
 
128
- - Agregar tareas asíncronas para imports/exports largos.
129
- - Añadir control de logs (`apache/error.log`, `mysql/error.log`) como tools read-only.
130
- - Extender a transporte HTTP con OAuth 2.1 para escenarios remotos.
86
+ Este proyecto está bajo licencia MIT. Consulta [LICENSE](LICENSE).
package/dist/server.js CHANGED
@@ -10,7 +10,7 @@ async function main() {
10
10
  const toolRegistry = createToolRegistry(environment);
11
11
  const server = new Server({
12
12
  name: "xampp-mcp",
13
- version: "0.1.0",
13
+ version: "1.0.0",
14
14
  }, {
15
15
  capabilities: {
16
16
  tools: {},
@@ -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
+ }
@@ -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,7 +1,15 @@
1
1
  {
2
2
  "name": "xampp-mcp",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for XAMPP and MySQL administration on Windows",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/dony-aep/xampp-mcp.git"
8
+ },
9
+ "homepage": "https://github.com/dony-aep/xampp-mcp#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/dony-aep/xampp-mcp/issues"
12
+ },
5
13
  "type": "module",
6
14
  "main": "dist/server.js",
7
15
  "bin": {
@@ -10,6 +18,7 @@
10
18
  "files": [
11
19
  "dist",
12
20
  "README.md",
21
+ "LICENSE",
13
22
  "docs",
14
23
  "package.json"
15
24
  ],
@@ -31,7 +40,7 @@
31
40
  "mysql",
32
41
  "vscode"
33
42
  ],
34
- "license": "UNLICENSED",
43
+ "license": "MIT",
35
44
  "dependencies": {
36
45
  "@modelcontextprotocol/sdk": "^1.22.0"
37
46
  },
@@ -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
- }