Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ MYSQL_USER=root
MYSQL_PASS=your_password
MYSQL_DB=

# Alternative: MySQL connection string (mysql CLI format)
# If provided, this takes precedence over individual connection settings above
# Useful for rotating credentials or temporary connections
# Example: mysql --default-auth=mysql_native_password -A -hHOST -PPORT -uUSER -pPASS database_name
# MYSQL_CONNECTION_STRING=

# Leave MYSQL_DB empty for multi-DB mode
# Set MYSQL_DB to a specific database name for single-DB mode

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
dist
node_modules/
.env
.env
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,19 @@ For more control over the MCP server's behavior, you can use these advanced conf
- `MYSQL_PASS`: MySQL password
- `MYSQL_DB`: Target database name (leave empty for multi-DB mode)

#### Alternative: Connection String

For scenarios requiring frequent credential rotation or temporary connections, you can use a MySQL connection string instead of individual environment variables:

- `MYSQL_CONNECTION_STRING`: MySQL CLI-format connection string (e.g., `mysql --default-auth=mysql_native_password -A -hHOST -PPORT -uUSER -pPASS database_name`)

When `MYSQL_CONNECTION_STRING` is provided, it takes precedence over individual connection settings. This is particularly useful for:
- Rotating credentials that expire frequently
- Temporary database connections
- Quick testing with different database configurations

**Note:** For security, this should only be set via environment variables, not stored in version-controlled configuration files. Consider using the `prompt` input type in Claude Code's MCP configuration for credentials that expire.

### Performance Configuration

- `MYSQL_POOL_SIZE`: Connection pool size (default: "10")
Expand Down
39 changes: 28 additions & 11 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import * as dotenv from "dotenv";
import { SchemaPermissions } from "../types/index.js";
import { parseSchemaPermissions } from "../utils/index.js";
import { parseSchemaPermissions, parseMySQLConnectionString } from "../utils/index.js";

export const MCP_VERSION = "2.0.2";

// @INFO: Load environment variables from .env file
dotenv.config();

// @INFO: Parse connection string if provided
// Connection string takes precedence over individual environment variables
const connectionStringConfig = process.env.MYSQL_CONNECTION_STRING
? parseMySQLConnectionString(process.env.MYSQL_CONNECTION_STRING)
: {};

// @INFO: Update the environment setup to ensure database is correctly set
if (process.env.NODE_ENV === "test" && !process.env.MYSQL_DB) {
process.env.MYSQL_DB = "mcp_test_db"; // @INFO: Ensure we have a database name for tests
Expand Down Expand Up @@ -42,8 +48,9 @@ export const REMOTE_SECRET_KEY = process.env.REMOTE_SECRET_KEY || "";
export const PORT = process.env.PORT || 3000;

// Check if we're in multi-DB mode (no specific DB set)
const dbFromEnvOrConnString = connectionStringConfig.database || process.env.MYSQL_DB;
export const isMultiDbMode =
!process.env.MYSQL_DB || process.env.MYSQL_DB.trim() === "";
!dbFromEnvOrConnString || dbFromEnvOrConnString.trim() === "";

export const mcpConfig = {
server: {
Expand All @@ -52,23 +59,33 @@ export const mcpConfig = {
connectionTypes: ["stdio", "streamableHttp"],
},
mysql: {
// Use Unix socket if provided, otherwise use host/port
...(process.env.MYSQL_SOCKET_PATH
// Use Unix socket if provided (connection string takes precedence), otherwise use host/port
...(connectionStringConfig.socketPath || process.env.MYSQL_SOCKET_PATH
? {
socketPath: process.env.MYSQL_SOCKET_PATH,
socketPath: connectionStringConfig.socketPath || process.env.MYSQL_SOCKET_PATH,
}
: {
host: process.env.MYSQL_HOST || "127.0.0.1",
port: Number(process.env.MYSQL_PORT || "3306"),
host: connectionStringConfig.host || process.env.MYSQL_HOST || "127.0.0.1",
port: connectionStringConfig.port || Number(process.env.MYSQL_PORT || "3306"),
}),
user: process.env.MYSQL_USER || "root",
user: connectionStringConfig.user || process.env.MYSQL_USER || "root",
password:
process.env.MYSQL_PASS === undefined ? "" : process.env.MYSQL_PASS,
database: process.env.MYSQL_DB || undefined, // Allow undefined database for multi-DB mode
connectionStringConfig.password !== undefined
? connectionStringConfig.password
: process.env.MYSQL_PASS === undefined
? ""
: process.env.MYSQL_PASS,
database: connectionStringConfig.database || process.env.MYSQL_DB || undefined, // Allow undefined database for multi-DB mode
connectionLimit: 10,
authPlugins: {
mysql_clear_password: () => () =>
Buffer.from(process.env.MYSQL_PASS || "root"),
Buffer.from(
connectionStringConfig.password !== undefined
? connectionStringConfig.password
: process.env.MYSQL_PASS !== undefined
? process.env.MYSQL_PASS
: ""
),
},
...(process.env.MYSQL_SSL === "true"
? {
Expand Down
129 changes: 129 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,132 @@ export function parseSchemaPermissions(

return permissions;
}

// MySQL connection configuration type
export interface MySQLConnectionConfig {
host?: string;
port?: number;
user?: string;
password?: string;
database?: string;
socketPath?: string;
}

// Function to parse MySQL connection string (mysql CLI format)
// Example: mysql --default-auth=mysql_native_password -A -hrdsproxy.staging.luno.com -P3306 -uUSER -pPASS database_name
export function parseMySQLConnectionString(
connectionString: string,
): MySQLConnectionConfig {
const config: MySQLConnectionConfig = {};

// Remove 'mysql' command at the start if present
let cleanedString = connectionString.trim().replace(/^mysql\s+/, '');

// Parse flags and options
const tokens = [];
let currentToken = '';
let inQuotes = false;
let quoteChar: string | null = null;

for (let i = 0; i < cleanedString.length; i++) {
const char = cleanedString[i];

if ((char === '"' || char === "'") && (!inQuotes || char === quoteChar)) {
// Toggle quote state without adding the quote character
inQuotes = !inQuotes;
quoteChar = inQuotes ? char : null;
} else if (char === ' ' && !inQuotes) {
if (currentToken) {
tokens.push(currentToken);
currentToken = '';
}
} else {
currentToken += char;
}
}

if (currentToken) {
tokens.push(currentToken);
}

// Process tokens
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];

// Check for combined short options (e.g., -uUSER, -pPASS, -hHOST, -PPORT)
if (token.startsWith('-') && !token.startsWith('--')) {
const flag = token[1];
let value = token.substring(2);

// If no value attached, check next token
if (!value && i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
value = tokens[i + 1];
i++;
}

switch (flag) {
case 'h':
config.host = value;
break;
case 'P': {
const port = parseInt(value, 10);
if (Number.isNaN(port) || !Number.isFinite(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port: ${value}`);
}
config.port = port;
break;
}
case 'u':
config.user = value;
break;
case 'p':
config.password = value;
break;
case 'S':
config.socketPath = value;
break;
}
}
// Check for long options (e.g., --host=HOST, --port=PORT)
else if (token.startsWith('--')) {
const [flag, ...valueParts] = token.substring(2).split('=');
let value = valueParts.join('=');

// If no value with =, check next token
if (!value && i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
value = tokens[i + 1];
i++;
}

switch (flag) {
case 'host':
config.host = value;
break;
case 'port': {
const port = parseInt(value, 10);
if (Number.isNaN(port) || !Number.isFinite(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port: ${value}`);
}
config.port = port;
break;
}
case 'user':
config.user = value;
break;
case 'password':
config.password = value;
break;
case 'socket':
config.socketPath = value;
break;
}
}
// Last positional argument (not starting with -) is the database name
else if (!token.startsWith('-')) {
// Only consider it a database if it's one of the last arguments and not part of a flag
config.database = token;
}
}

return config;
}
Loading