xampp-mcp 0.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 ADDED
@@ -0,0 +1,130 @@
1
+ # xampp-mcp
2
+
3
+ MCP server para administrar XAMPP y MySQL en Windows desde clientes MCP por `stdio` (VS Code, Copilot agents, etc.).
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
+ ## Requisitos
15
+
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`.
19
+
20
+ ## Configuración
21
+
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
41
+
42
+ Instalación global:
43
+
44
+ ```powershell
45
+ npm install -g xampp-mcp
46
+ ```
47
+
48
+ Ejecución directa:
49
+
50
+ ```powershell
51
+ xampp-mcp
52
+ ```
53
+
54
+ Sin instalación global:
55
+
56
+ ```powershell
57
+ npx -y xampp-mcp
58
+ ```
59
+
60
+ Ejemplo de configuración MCP en VS Code:
61
+
62
+ ```jsonc
63
+ {
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
+ }
74
+ }
75
+ ```
76
+
77
+ Si usas `npx` en vez de instalación global:
78
+
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
+ ```
94
+
95
+ ## Publicación npm
96
+
97
+ ```powershell
98
+ npm login
99
+ npm version patch
100
+ npm publish
101
+ ```
102
+
103
+ ## Seguridad
104
+
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.
111
+
112
+ ## Reglas de nombres
113
+
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`.
118
+
119
+ ## Codificación de texto
120
+
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.
125
+
126
+ ## Próximos pasos sugeridos
127
+
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.
@@ -0,0 +1,89 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function env(name) {
5
+ const value = process.env[name];
6
+ if (!value) {
7
+ return undefined;
8
+ }
9
+ const trimmed = value.trim();
10
+ if (trimmed.length === 0) {
11
+ return undefined;
12
+ }
13
+ const startsWithDouble = trimmed.startsWith("\"") && trimmed.endsWith("\"");
14
+ const startsWithSingle = trimmed.startsWith("'") && trimmed.endsWith("'");
15
+ if ((startsWithDouble || startsWithSingle) && trimmed.length >= 2) {
16
+ const unwrapped = trimmed.slice(1, -1).trim();
17
+ return unwrapped.length > 0 ? unwrapped : undefined;
18
+ }
19
+ return trimmed;
20
+ }
21
+ function normalizePathValue(value) {
22
+ if (!value.toLowerCase().startsWith("file://")) {
23
+ return value;
24
+ }
25
+ try {
26
+ return fileURLToPath(value);
27
+ }
28
+ catch {
29
+ return value;
30
+ }
31
+ }
32
+ function resolveXamppPath(xamppDir, ...segments) {
33
+ return path.join(xamppDir, ...segments);
34
+ }
35
+ function parseMode(value) {
36
+ if (value === "service") {
37
+ return "service";
38
+ }
39
+ return "console";
40
+ }
41
+ function parsePort(value, fallback) {
42
+ if (!value) {
43
+ return fallback;
44
+ }
45
+ const parsed = Number.parseInt(value, 10);
46
+ if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
47
+ return parsed;
48
+ }
49
+ return fallback;
50
+ }
51
+ export function loadEnvironment() {
52
+ const xamppDir = normalizePathValue(env("XAMPP_DIR") ?? "C:\\xampp");
53
+ return {
54
+ xamppDir,
55
+ defaultMode: parseMode(env("XAMPP_DEFAULT_MODE")),
56
+ apacheServiceName: env("XAMPP_APACHE_SERVICE") ?? "Apache2.4",
57
+ mysqlServiceName: env("XAMPP_MYSQL_SERVICE") ?? "mysql",
58
+ mysqlDefaultHost: env("MYSQL_HOST") ?? "127.0.0.1",
59
+ mysqlDefaultPort: parsePort(env("MYSQL_PORT"), 3306),
60
+ mysqlDefaultUser: env("MYSQL_USER") ?? "root",
61
+ mysqlDefaultPassword: env("MYSQL_PASSWORD"),
62
+ paths: {
63
+ apacheStart: resolveXamppPath(xamppDir, "apache_start.bat"),
64
+ apacheStop: resolveXamppPath(xamppDir, "apache_stop.bat"),
65
+ apacheExe: resolveXamppPath(xamppDir, "apache", "bin", "httpd.exe"),
66
+ mysqlStart: resolveXamppPath(xamppDir, "mysql_start.bat"),
67
+ mysqlStop: resolveXamppPath(xamppDir, "mysql_stop.bat"),
68
+ mysqldExe: resolveXamppPath(xamppDir, "mysql", "bin", "mysqld.exe"),
69
+ mysqlIni: resolveXamppPath(xamppDir, "mysql", "bin", "my.ini"),
70
+ mysqlExe: resolveXamppPath(xamppDir, "mysql", "bin", "mysql.exe"),
71
+ mysqldumpExe: resolveXamppPath(xamppDir, "mysql", "bin", "mysqldump.exe"),
72
+ phpExe: resolveXamppPath(xamppDir, "php", "php.exe"),
73
+ },
74
+ };
75
+ }
76
+ export function getPathAvailability(environment) {
77
+ return {
78
+ apacheStart: { path: environment.paths.apacheStart, exists: existsSync(environment.paths.apacheStart) },
79
+ apacheStop: { path: environment.paths.apacheStop, exists: existsSync(environment.paths.apacheStop) },
80
+ apacheExe: { path: environment.paths.apacheExe, exists: existsSync(environment.paths.apacheExe) },
81
+ mysqlStart: { path: environment.paths.mysqlStart, exists: existsSync(environment.paths.mysqlStart) },
82
+ mysqlStop: { path: environment.paths.mysqlStop, exists: existsSync(environment.paths.mysqlStop) },
83
+ mysqldExe: { path: environment.paths.mysqldExe, exists: existsSync(environment.paths.mysqldExe) },
84
+ mysqlIni: { path: environment.paths.mysqlIni, exists: existsSync(environment.paths.mysqlIni) },
85
+ mysqlExe: { path: environment.paths.mysqlExe, exists: existsSync(environment.paths.mysqlExe) },
86
+ mysqldumpExe: { path: environment.paths.mysqldumpExe, exists: existsSync(environment.paths.mysqldumpExe) },
87
+ phpExe: { path: environment.paths.phpExe, exists: existsSync(environment.paths.phpExe) },
88
+ };
89
+ }
@@ -0,0 +1,117 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function unwrapQuoted(value) {
5
+ const trimmed = value.trim();
6
+ if (trimmed.length < 2) {
7
+ return trimmed;
8
+ }
9
+ const startsWithDouble = trimmed.startsWith("\"") && trimmed.endsWith("\"");
10
+ const startsWithSingle = trimmed.startsWith("'") && trimmed.endsWith("'");
11
+ if (startsWithDouble || startsWithSingle) {
12
+ return trimmed.slice(1, -1).trim();
13
+ }
14
+ return trimmed;
15
+ }
16
+ function normalizeFileUri(value) {
17
+ if (!value.toLowerCase().startsWith("file://")) {
18
+ return value;
19
+ }
20
+ try {
21
+ return fileURLToPath(value);
22
+ }
23
+ catch {
24
+ return value;
25
+ }
26
+ }
27
+ function normalizeCommandValue(value) {
28
+ return normalizeFileUri(unwrapQuoted(value));
29
+ }
30
+ function quoteForCmd(value) {
31
+ if (value.length === 0) {
32
+ return "\"\"";
33
+ }
34
+ const escaped = value.replace(/([\^&|<>()%!\"])|\r|\n/g, "^$1");
35
+ return `"${escaped}"`;
36
+ }
37
+ function isBatchScript(filePath) {
38
+ const extension = path.extname(normalizeCommandValue(filePath)).toLowerCase();
39
+ return extension === ".bat" || extension === ".cmd";
40
+ }
41
+ function toSpawnSpec(command, args) {
42
+ const normalizedCommand = normalizeCommandValue(command);
43
+ if (!isBatchScript(normalizedCommand)) {
44
+ return { file: normalizedCommand, args };
45
+ }
46
+ return {
47
+ file: "cmd.exe",
48
+ args: ["/d", "/c", "call", normalizedCommand, ...args],
49
+ };
50
+ }
51
+ export async function runCommand(options) {
52
+ const { command, args = [], cwd, env, timeoutMs = 30_000, stdin, } = options;
53
+ const start = Date.now();
54
+ const normalizedCommand = normalizeCommandValue(command);
55
+ const spawnSpec = toSpawnSpec(normalizedCommand, args);
56
+ return await new Promise((resolve, reject) => {
57
+ const child = spawn(spawnSpec.file, spawnSpec.args, {
58
+ cwd,
59
+ env,
60
+ windowsHide: true,
61
+ stdio: "pipe",
62
+ });
63
+ let stdout = "";
64
+ let stderr = "";
65
+ let timedOut = false;
66
+ const timer = setTimeout(() => {
67
+ timedOut = true;
68
+ child.kill();
69
+ }, timeoutMs);
70
+ child.stdout.setEncoding("utf8");
71
+ child.stderr.setEncoding("utf8");
72
+ child.stdout.on("data", (chunk) => {
73
+ stdout += chunk;
74
+ });
75
+ child.stderr.on("data", (chunk) => {
76
+ stderr += chunk;
77
+ });
78
+ child.on("error", (error) => {
79
+ clearTimeout(timer);
80
+ reject(error);
81
+ });
82
+ child.on("close", (exitCode) => {
83
+ clearTimeout(timer);
84
+ resolve({
85
+ command: normalizedCommand,
86
+ args,
87
+ exitCode,
88
+ stdout: stdout.trim(),
89
+ stderr: stderr.trim(),
90
+ timedOut,
91
+ durationMs: Date.now() - start,
92
+ });
93
+ });
94
+ if (stdin !== undefined) {
95
+ child.stdin.write(stdin);
96
+ }
97
+ child.stdin.end();
98
+ });
99
+ }
100
+ export function runDetachedCommand(options) {
101
+ const { command, args = [], cwd, env, } = options;
102
+ const normalizedCommand = normalizeCommandValue(command);
103
+ const spawnSpec = toSpawnSpec(normalizedCommand, args);
104
+ const child = spawn(spawnSpec.file, spawnSpec.args, {
105
+ cwd,
106
+ env,
107
+ windowsHide: true,
108
+ detached: true,
109
+ stdio: "ignore",
110
+ });
111
+ child.unref();
112
+ return {
113
+ command: normalizedCommand,
114
+ args,
115
+ pid: child.pid,
116
+ };
117
+ }
@@ -0,0 +1,39 @@
1
+ export class ToolExecutionError extends Error {
2
+ code;
3
+ constructor(message, code = "TOOL_EXECUTION_ERROR") {
4
+ super(message);
5
+ this.name = "ToolExecutionError";
6
+ this.code = code;
7
+ }
8
+ }
9
+ function normalizeError(error) {
10
+ if (error instanceof Error) {
11
+ return error.message;
12
+ }
13
+ if (typeof error === "string") {
14
+ return error;
15
+ }
16
+ return "Unexpected error";
17
+ }
18
+ export function toToolErrorResult(error) {
19
+ return {
20
+ isError: true,
21
+ content: [
22
+ {
23
+ type: "text",
24
+ text: normalizeError(error),
25
+ },
26
+ ],
27
+ };
28
+ }
29
+ export function toToolTextResult(text, structuredContent) {
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text,
35
+ },
36
+ ],
37
+ structuredContent,
38
+ };
39
+ }
@@ -0,0 +1,111 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { runCommand } from "./commandRunner.js";
3
+ import { ToolExecutionError } from "./errors.js";
4
+ const MYSQL_CHARACTER_SET = "utf8mb4";
5
+ const MYSQL_COLLATION = "utf8mb4_unicode_ci";
6
+ function isMysqlUnavailable(detail) {
7
+ const normalized = detail.toLowerCase();
8
+ return (normalized.includes("can't connect to mysql server") ||
9
+ normalized.includes("cannot connect to mysql server") ||
10
+ normalized.includes("connection refused") ||
11
+ normalized.includes("actively refused") ||
12
+ normalized.includes("error 2002") ||
13
+ normalized.includes("errno 2002"));
14
+ }
15
+ function resolveConnection(environment, options) {
16
+ return {
17
+ host: options.host ?? environment.mysqlDefaultHost,
18
+ port: options.port ?? environment.mysqlDefaultPort,
19
+ user: options.user ?? environment.mysqlDefaultUser,
20
+ password: options.password ?? environment.mysqlDefaultPassword ?? "",
21
+ };
22
+ }
23
+ function baseMysqlArgs(connection) {
24
+ const args = [
25
+ "--protocol=tcp",
26
+ `--default-character-set=${MYSQL_CHARACTER_SET}`,
27
+ "--host",
28
+ connection.host,
29
+ "--port",
30
+ String(connection.port),
31
+ "--user",
32
+ connection.user,
33
+ ];
34
+ if (connection.password.length > 0) {
35
+ args.push(`--password=${connection.password}`);
36
+ }
37
+ return args;
38
+ }
39
+ function assertSuccess(result, context, connection) {
40
+ if (result.timedOut) {
41
+ throw new ToolExecutionError(`${context} timed out after ${result.durationMs}ms`, "COMMAND_TIMEOUT");
42
+ }
43
+ if (result.exitCode !== 0) {
44
+ const detail = result.stderr.length > 0 ? result.stderr : result.stdout;
45
+ if (connection && isMysqlUnavailable(detail)) {
46
+ throw new ToolExecutionError(`MySQL is not reachable at ${connection.host}:${connection.port}. Ask the user to open XAMPP Control Panel, start MySQL, and confirm when done before retrying this operation. Do not run mysql_start.bat automatically.`, "MYSQL_UNREACHABLE");
47
+ }
48
+ throw new ToolExecutionError(`${context} failed (${result.exitCode ?? "unknown"}): ${detail}`, "COMMAND_FAILED");
49
+ }
50
+ }
51
+ export async function executeMysqlSql(environment, options) {
52
+ const connection = resolveConnection(environment, options);
53
+ const args = baseMysqlArgs(connection);
54
+ if (options.database) {
55
+ args.push(options.database);
56
+ }
57
+ const sessionHeader = `SET NAMES ${MYSQL_CHARACTER_SET} COLLATE ${MYSQL_COLLATION};`;
58
+ const sqlStatement = options.sql.endsWith(";") ? options.sql : `${options.sql};`;
59
+ const result = await runCommand({
60
+ command: environment.paths.mysqlExe,
61
+ args,
62
+ stdin: `${sessionHeader}\n${sqlStatement}\n`,
63
+ timeoutMs: options.timeoutMs ?? 30_000,
64
+ });
65
+ assertSuccess(result, "MySQL query", connection);
66
+ return result;
67
+ }
68
+ export async function exportDatabase(environment, options) {
69
+ const connection = resolveConnection(environment, options);
70
+ const args = [
71
+ "--protocol=tcp",
72
+ `--default-character-set=${MYSQL_CHARACTER_SET}`,
73
+ "--host",
74
+ connection.host,
75
+ "--port",
76
+ String(connection.port),
77
+ "--user",
78
+ connection.user,
79
+ ];
80
+ if (connection.password.length > 0) {
81
+ args.push(`--password=${connection.password}`);
82
+ }
83
+ if (options.includeCreateDatabase) {
84
+ args.push("--databases");
85
+ }
86
+ if (options.addDropTable) {
87
+ args.push("--add-drop-table");
88
+ }
89
+ args.push(options.database);
90
+ const result = await runCommand({
91
+ command: environment.paths.mysqldumpExe,
92
+ args,
93
+ timeoutMs: options.timeoutMs ?? 120_000,
94
+ });
95
+ assertSuccess(result, "MySQL export", connection);
96
+ await fs.writeFile(options.outputPath, `${result.stdout}\n`, "utf8");
97
+ return result;
98
+ }
99
+ export async function importDatabase(environment, options) {
100
+ const sql = await fs.readFile(options.inputPath, "utf8");
101
+ const result = await executeMysqlSql(environment, {
102
+ host: options.host,
103
+ port: options.port,
104
+ user: options.user,
105
+ password: options.password,
106
+ database: options.database,
107
+ sql,
108
+ timeoutMs: options.timeoutMs ?? 180_000,
109
+ });
110
+ return result;
111
+ }
@@ -0,0 +1,60 @@
1
+ import { ToolExecutionError } from "./errors.js";
2
+ export function getString(value, fieldName, options) {
3
+ if (value === undefined || value === null) {
4
+ if (options?.optional) {
5
+ return "";
6
+ }
7
+ throw new ToolExecutionError(`${fieldName} is required`, "INVALID_INPUT");
8
+ }
9
+ if (typeof value !== "string") {
10
+ throw new ToolExecutionError(`${fieldName} must be a string`, "INVALID_INPUT");
11
+ }
12
+ const trimmed = value.trim();
13
+ if (!options?.optional && trimmed.length === 0) {
14
+ throw new ToolExecutionError(`${fieldName} cannot be empty`, "INVALID_INPUT");
15
+ }
16
+ return trimmed;
17
+ }
18
+ export function getOptionalString(value, fieldName) {
19
+ if (value === undefined || value === null) {
20
+ return undefined;
21
+ }
22
+ if (typeof value !== "string") {
23
+ throw new ToolExecutionError(`${fieldName} must be a string`, "INVALID_INPUT");
24
+ }
25
+ const trimmed = value.trim();
26
+ return trimmed.length > 0 ? trimmed : undefined;
27
+ }
28
+ export function getBoolean(value, fieldName, fallback = false) {
29
+ if (value === undefined || value === null) {
30
+ return fallback;
31
+ }
32
+ if (typeof value !== "boolean") {
33
+ throw new ToolExecutionError(`${fieldName} must be a boolean`, "INVALID_INPUT");
34
+ }
35
+ return value;
36
+ }
37
+ export function getOptionalNumber(value, fieldName) {
38
+ if (value === undefined || value === null) {
39
+ return undefined;
40
+ }
41
+ if (typeof value !== "number" || !Number.isFinite(value)) {
42
+ throw new ToolExecutionError(`${fieldName} must be a number`, "INVALID_INPUT");
43
+ }
44
+ return value;
45
+ }
46
+ export function getEnum(value, fieldName, accepted, fallback) {
47
+ if (value === undefined || value === null) {
48
+ if (fallback !== undefined) {
49
+ return fallback;
50
+ }
51
+ throw new ToolExecutionError(`${fieldName} is required`, "INVALID_INPUT");
52
+ }
53
+ if (typeof value !== "string") {
54
+ throw new ToolExecutionError(`${fieldName} must be a string`, "INVALID_INPUT");
55
+ }
56
+ if (accepted.includes(value)) {
57
+ return value;
58
+ }
59
+ throw new ToolExecutionError(`${fieldName} must be one of: ${accepted.join(", ")}`, "INVALID_INPUT");
60
+ }
@@ -0,0 +1,44 @@
1
+ import { ToolExecutionError } from "../runtime/errors.js";
2
+ const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
3
+ const USERNAME_PATTERN = /^[A-Za-z0-9._$-]+$/;
4
+ const HOST_PATTERN = /^[A-Za-z0-9.%_-]+$/;
5
+ function buildIdentifierErrorMessage(value, fieldName) {
6
+ const base = `Invalid ${fieldName}. Use snake_case with letters, numbers and underscore only; it must start with a letter or underscore.`;
7
+ if (!value.includes("-")) {
8
+ return base;
9
+ }
10
+ const suggested = value.replace(/-/g, "_");
11
+ return `${base} Hyphen (-) is not allowed by policy. Suggested ${fieldName}: "${suggested}".`;
12
+ }
13
+ export function requireConfirmation(confirmed, action) {
14
+ if (confirmed) {
15
+ return;
16
+ }
17
+ throw new ToolExecutionError(`${action} requires explicit confirmation. Set "confirmed": true to proceed.`, "CONFIRMATION_REQUIRED");
18
+ }
19
+ export function assertIdentifier(value, fieldName) {
20
+ if (!IDENTIFIER_PATTERN.test(value)) {
21
+ throw new ToolExecutionError(buildIdentifierErrorMessage(value, fieldName), "INVALID_IDENTIFIER");
22
+ }
23
+ }
24
+ export function assertDatabaseIdentifier(value, fieldName) {
25
+ if (!IDENTIFIER_PATTERN.test(value)) {
26
+ throw new ToolExecutionError(buildIdentifierErrorMessage(value, fieldName), "INVALID_IDENTIFIER");
27
+ }
28
+ }
29
+ export function assertUsername(value) {
30
+ if (!USERNAME_PATTERN.test(value)) {
31
+ throw new ToolExecutionError("Invalid username. Allowed chars: letters, numbers, . _ $ -", "INVALID_USERNAME");
32
+ }
33
+ }
34
+ export function assertHost(value) {
35
+ if (!HOST_PATTERN.test(value)) {
36
+ throw new ToolExecutionError("Invalid host. Allowed chars: letters, numbers, %, ., _, -", "INVALID_HOST");
37
+ }
38
+ }
39
+ export function escapeSqlIdentifier(identifier) {
40
+ return `\`${identifier.replace(/`/g, "``")}\``;
41
+ }
42
+ export function escapeSqlLiteral(value) {
43
+ return `'${value.replace(/'/g, "''")}'`;
44
+ }
package/dist/server.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { loadEnvironment } from "./config/env.js";
6
+ import { createToolRegistry } from "./tools/registry.js";
7
+ import { toToolErrorResult } from "./runtime/errors.js";
8
+ async function main() {
9
+ const environment = loadEnvironment();
10
+ const toolRegistry = createToolRegistry(environment);
11
+ const server = new Server({
12
+ name: "xampp-mcp",
13
+ version: "0.1.0",
14
+ }, {
15
+ capabilities: {
16
+ tools: {},
17
+ },
18
+ });
19
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
20
+ const tools = toolRegistry.list().map((tool) => ({
21
+ name: tool.name,
22
+ description: tool.description,
23
+ inputSchema: tool.inputSchema,
24
+ annotations: tool.annotations,
25
+ }));
26
+ return { tools };
27
+ });
28
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29
+ const tool = toolRegistry.get(request.params.name);
30
+ if (!tool) {
31
+ return toToolErrorResult(`Unknown tool: ${request.params.name}`);
32
+ }
33
+ const rawArgs = request.params.arguments ?? {};
34
+ try {
35
+ return await tool.handler(rawArgs);
36
+ }
37
+ catch (error) {
38
+ return toToolErrorResult(error);
39
+ }
40
+ });
41
+ const transport = new StdioServerTransport();
42
+ await server.connect(transport);
43
+ }
44
+ main().catch((error) => {
45
+ process.stderr.write(`Fatal error: ${String(error)}\n`);
46
+ process.exit(1);
47
+ });
@@ -0,0 +1,50 @@
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, escapeSqlIdentifier, requireConfirmation } from "../security/policy.js";
5
+ import { mysqlConnectionFromArgs } from "./shared.js";
6
+ const DEFAULT_DB_CHARACTER_SET = "utf8mb4";
7
+ const DEFAULT_DB_COLLATION = "utf8mb4_unicode_ci";
8
+ export function createDbCreateTool(environment) {
9
+ return {
10
+ name: "db_create",
11
+ description: "Creates a MySQL database",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ database: { type: "string" },
16
+ ifNotExists: { type: "boolean" },
17
+ confirmed: { type: "boolean" },
18
+ host: { type: "string" },
19
+ port: { type: "number" },
20
+ user: { type: "string" },
21
+ password: { type: "string" },
22
+ },
23
+ required: ["database", "confirmed"],
24
+ additionalProperties: false,
25
+ },
26
+ annotations: {
27
+ title: "Create Database",
28
+ destructiveHint: true,
29
+ openWorldHint: false,
30
+ },
31
+ handler: async (args) => {
32
+ const database = getString(args.database, "database");
33
+ const confirmed = getBoolean(args.confirmed, "confirmed");
34
+ const ifNotExists = getBoolean(args.ifNotExists, "ifNotExists", true);
35
+ requireConfirmation(confirmed, "db_create");
36
+ assertDatabaseIdentifier(database, "database");
37
+ const sql = `CREATE DATABASE ${ifNotExists ? "IF NOT EXISTS " : ""}${escapeSqlIdentifier(database)} CHARACTER SET ${DEFAULT_DB_CHARACTER_SET} COLLATE ${DEFAULT_DB_COLLATION}`;
38
+ await executeMysqlSql(environment, {
39
+ ...mysqlConnectionFromArgs(args),
40
+ sql,
41
+ });
42
+ return toToolTextResult(`Database ${database} created`, {
43
+ database,
44
+ ifNotExists,
45
+ characterSet: DEFAULT_DB_CHARACTER_SET,
46
+ collation: DEFAULT_DB_COLLATION,
47
+ });
48
+ },
49
+ };
50
+ }