@arvoretech/mysql-mcp 1.0.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/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { MySQLMCPServer } from "./server.js";
4
+
5
+ async function main(): Promise<void> {
6
+ try {
7
+ const server = MySQLMCPServer.fromEnvironment();
8
+ server.setupGracefulShutdown();
9
+ await server.start();
10
+ } catch (error) {
11
+ console.error("Failed to start MySQL MCP Server:", error);
12
+ process.exit(1);
13
+ }
14
+ }
15
+
16
+ if (import.meta.url === `file://${process.argv[1]}`) {
17
+ main().catch((error) => {
18
+ console.error("Fatal error:", error);
19
+ process.exit(1);
20
+ });
21
+ }
22
+
23
+ export { MySQLMCPServer } from "./server.js";
24
+ export { MySQLConnection } from "./database.js";
25
+ export { MySQLMCPTools } from "./tools.js";
26
+ export * from "./types.js";
package/src/server.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { MySQLConnection } from "./database.js";
4
+ import { MySQLMCPTools } from "./tools.js";
5
+ import {
6
+ MySQLConfig,
7
+ MySQLConfigSchema,
8
+ ReadQueryParams,
9
+ DescribeTableParams,
10
+ ReadQueryParamsSchema,
11
+ DescribeTableParamsSchema,
12
+ MySQLMCPError,
13
+ } from "./types.js";
14
+
15
+ export class MySQLMCPServer {
16
+ private server: McpServer;
17
+ private db: MySQLConnection;
18
+ private tools: MySQLMCPTools;
19
+
20
+ constructor(config: MySQLConfig) {
21
+ this.server = new McpServer({
22
+ name: "mysql-mcp-server",
23
+ version: "1.0.0",
24
+ });
25
+
26
+ this.db = new MySQLConnection(config);
27
+ this.tools = new MySQLMCPTools(this.db);
28
+
29
+ this.setupTools();
30
+ }
31
+
32
+ static fromEnvironment(): MySQLMCPServer {
33
+ const config = MySQLConfigSchema.parse({
34
+ host: process.env.MYSQL_HOST || "localhost",
35
+ port: parseInt(process.env.MYSQL_PORT || "3306", 10),
36
+ user: process.env.MYSQL_USER || "root",
37
+ password: process.env.MYSQL_PASSWORD || "",
38
+ database: process.env.MYSQL_DATABASE || "test",
39
+ ssl: process.env.MYSQL_SSL === "true",
40
+ connectionTimeout: parseInt(
41
+ process.env.MYSQL_CONNECTION_TIMEOUT || "30000",
42
+ 10
43
+ ),
44
+ });
45
+
46
+ return new MySQLMCPServer(config);
47
+ }
48
+
49
+ private setupTools(): void {
50
+ this.server.registerTool(
51
+ "read_query",
52
+ {
53
+ title: "Execute Read Query",
54
+ description: "Execute a SELECT query on the MySQL database",
55
+ inputSchema: { query: ReadQueryParamsSchema.shape.query },
56
+ },
57
+ async (params) => {
58
+ return this.tools.readQuery(params as ReadQueryParams);
59
+ }
60
+ );
61
+
62
+ this.server.registerTool(
63
+ "list_tables",
64
+ {
65
+ title: "List Tables",
66
+ description: "List all tables in the current database",
67
+ inputSchema: {},
68
+ },
69
+ async () => {
70
+ return this.tools.listTables();
71
+ }
72
+ );
73
+
74
+ this.server.registerTool(
75
+ "describe_table",
76
+ {
77
+ title: "Describe Table",
78
+ description:
79
+ "Get the structure and schema information of a specific table",
80
+ inputSchema: { tableName: DescribeTableParamsSchema.shape.tableName },
81
+ },
82
+ async (params) => {
83
+ return this.tools.describeTable(params as DescribeTableParams);
84
+ }
85
+ );
86
+
87
+ this.server.registerTool(
88
+ "show_databases",
89
+ {
90
+ title: "Show Databases",
91
+ description: "List all available databases on the MySQL server",
92
+ inputSchema: {},
93
+ },
94
+ async () => {
95
+ return this.tools.showDatabases();
96
+ }
97
+ );
98
+ }
99
+
100
+ async start(): Promise<void> {
101
+ try {
102
+ await this.db.connect();
103
+
104
+ const isConnected = await this.db.testConnection();
105
+ if (!isConnected) {
106
+ throw new MySQLMCPError(
107
+ "Database connection test failed",
108
+ "CONNECTION_TEST_FAILED"
109
+ );
110
+ }
111
+
112
+ const transport = new StdioServerTransport();
113
+ await this.server.connect(transport);
114
+
115
+ console.error("MySQL MCP Server started successfully");
116
+ } catch (error) {
117
+ console.error(
118
+ "Failed to start MySQL MCP Server:",
119
+ error instanceof Error ? error.message : error
120
+ );
121
+ await this.cleanup();
122
+ process.exit(1);
123
+ }
124
+ }
125
+
126
+ async cleanup(): Promise<void> {
127
+ try {
128
+ await this.db.disconnect();
129
+ } catch (error) {
130
+ console.error(
131
+ "Error during cleanup:",
132
+ error instanceof Error ? error.message : error
133
+ );
134
+ }
135
+ }
136
+
137
+ setupGracefulShutdown(): void {
138
+ const shutdown = async (signal: string): Promise<void> => {
139
+ console.error(`Received ${signal}, shutting down gracefully...`);
140
+ await this.cleanup();
141
+ process.exit(0);
142
+ };
143
+
144
+ process.on("SIGINT", () => shutdown("SIGINT"));
145
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
146
+ process.on("uncaughtException", async (error) => {
147
+ console.error("Uncaught exception:", error);
148
+ await this.cleanup();
149
+ process.exit(1);
150
+ });
151
+ process.on("unhandledRejection", async (reason) => {
152
+ console.error("Unhandled rejection:", reason);
153
+ await this.cleanup();
154
+ process.exit(1);
155
+ });
156
+ }
157
+ }
@@ -0,0 +1,228 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { MySQLMCPTools } from "./tools.js";
3
+ import { MySQLConnection } from "./database.js";
4
+ import { MySQLMCPError } from "./types.js";
5
+
6
+ vi.mock("./database.js", () => ({
7
+ MySQLConnection: vi.fn(),
8
+ }));
9
+
10
+ describe("MySQLMCPTools", () => {
11
+ let tools: MySQLMCPTools;
12
+ let mockDb: {
13
+ executeQuery: ReturnType<typeof vi.fn>;
14
+ listTables: ReturnType<typeof vi.fn>;
15
+ describeTable: ReturnType<typeof vi.fn>;
16
+ showDatabases: ReturnType<typeof vi.fn>;
17
+ };
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ mockDb = {
22
+ executeQuery: vi.fn(),
23
+ listTables: vi.fn(),
24
+ describeTable: vi.fn(),
25
+ showDatabases: vi.fn(),
26
+ };
27
+ tools = new MySQLMCPTools(mockDb as unknown as MySQLConnection);
28
+ });
29
+
30
+ describe("readQuery", () => {
31
+ it("should execute query and return formatted result", async () => {
32
+ mockDb.executeQuery.mockResolvedValue({
33
+ data: [
34
+ { id: 1, name: "John" },
35
+ { id: 2, name: "Jane" },
36
+ ],
37
+ rowCount: 2,
38
+ executionTime: 15,
39
+ });
40
+
41
+ const result = await tools.readQuery({ query: "SELECT * FROM users" });
42
+
43
+ expect(result.content).toHaveLength(1);
44
+ expect(result.content[0].type).toBe("text");
45
+
46
+ const parsed = JSON.parse(result.content[0].text);
47
+ expect(parsed.query).toBe("SELECT * FROM users");
48
+ expect(parsed.rowCount).toBe(2);
49
+ expect(parsed.executionTime).toBe("15ms");
50
+ expect(parsed.data).toHaveLength(2);
51
+ });
52
+
53
+ it("should return error result when query fails", async () => {
54
+ mockDb.executeQuery.mockRejectedValue(
55
+ new MySQLMCPError("Syntax error", "ER_PARSE_ERROR", "42000")
56
+ );
57
+
58
+ const result = await tools.readQuery({ query: "SELECT * FROM invalid" });
59
+
60
+ const parsed = JSON.parse(result.content[0].text);
61
+ expect(parsed.error).toContain("MySQL Error");
62
+ expect(parsed.query).toBe("SELECT * FROM invalid");
63
+ });
64
+
65
+ it("should handle unexpected errors", async () => {
66
+ mockDb.executeQuery.mockRejectedValue(new Error("Unexpected error"));
67
+
68
+ const result = await tools.readQuery({ query: "SELECT * FROM users" });
69
+
70
+ const parsed = JSON.parse(result.content[0].text);
71
+ expect(parsed.error).toContain("Unexpected error");
72
+ });
73
+
74
+ it("should handle empty result sets", async () => {
75
+ mockDb.executeQuery.mockResolvedValue({
76
+ data: [],
77
+ rowCount: 0,
78
+ executionTime: 5,
79
+ });
80
+
81
+ const result = await tools.readQuery({
82
+ query: "SELECT * FROM empty_table",
83
+ });
84
+
85
+ const parsed = JSON.parse(result.content[0].text);
86
+ expect(parsed.rowCount).toBe(0);
87
+ expect(parsed.data).toEqual([]);
88
+ });
89
+ });
90
+
91
+ describe("listTables", () => {
92
+ it("should return list of tables", async () => {
93
+ mockDb.listTables.mockResolvedValue([
94
+ {
95
+ TABLE_NAME: "users",
96
+ TABLE_TYPE: "BASE TABLE",
97
+ TABLE_SCHEMA: "testdb",
98
+ },
99
+ {
100
+ TABLE_NAME: "posts",
101
+ TABLE_TYPE: "BASE TABLE",
102
+ TABLE_SCHEMA: "testdb",
103
+ },
104
+ { TABLE_NAME: "user_view", TABLE_TYPE: "VIEW", TABLE_SCHEMA: "testdb" },
105
+ ]);
106
+
107
+ const result = await tools.listTables();
108
+
109
+ const parsed = JSON.parse(result.content[0].text);
110
+ expect(parsed.tableCount).toBe(3);
111
+ expect(parsed.tables).toHaveLength(3);
112
+ expect(parsed.tables[0].name).toBe("users");
113
+ expect(parsed.tables[0].type).toBe("BASE TABLE");
114
+ expect(parsed.tables[0].schema).toBe("testdb");
115
+ });
116
+
117
+ it("should handle empty table list", async () => {
118
+ mockDb.listTables.mockResolvedValue([]);
119
+
120
+ const result = await tools.listTables();
121
+
122
+ const parsed = JSON.parse(result.content[0].text);
123
+ expect(parsed.tableCount).toBe(0);
124
+ expect(parsed.tables).toEqual([]);
125
+ });
126
+
127
+ it("should return error result when listing fails", async () => {
128
+ mockDb.listTables.mockRejectedValue(
129
+ new MySQLMCPError("Access denied", "ER_ACCESS_DENIED")
130
+ );
131
+
132
+ const result = await tools.listTables();
133
+
134
+ const parsed = JSON.parse(result.content[0].text);
135
+ expect(parsed.error).toContain("MySQL Error");
136
+ });
137
+ });
138
+
139
+ describe("describeTable", () => {
140
+ it("should return table structure", async () => {
141
+ mockDb.describeTable.mockResolvedValue([
142
+ {
143
+ COLUMN_NAME: "id",
144
+ DATA_TYPE: "int",
145
+ IS_NULLABLE: "NO",
146
+ COLUMN_DEFAULT: null,
147
+ COLUMN_KEY: "PRI",
148
+ EXTRA: "auto_increment",
149
+ COLUMN_COMMENT: "Primary key",
150
+ },
151
+ {
152
+ COLUMN_NAME: "email",
153
+ DATA_TYPE: "varchar",
154
+ IS_NULLABLE: "NO",
155
+ COLUMN_DEFAULT: null,
156
+ COLUMN_KEY: "UNI",
157
+ EXTRA: "",
158
+ COLUMN_COMMENT: "User email",
159
+ },
160
+ {
161
+ COLUMN_NAME: "created_at",
162
+ DATA_TYPE: "timestamp",
163
+ IS_NULLABLE: "YES",
164
+ COLUMN_DEFAULT: "CURRENT_TIMESTAMP",
165
+ COLUMN_KEY: "",
166
+ EXTRA: "DEFAULT_GENERATED",
167
+ COLUMN_COMMENT: "",
168
+ },
169
+ ]);
170
+
171
+ const result = await tools.describeTable({ tableName: "users" });
172
+
173
+ const parsed = JSON.parse(result.content[0].text);
174
+ expect(parsed.tableName).toBe("users");
175
+ expect(parsed.columnCount).toBe(3);
176
+ expect(parsed.columns).toHaveLength(3);
177
+ expect(parsed.columns[0].name).toBe("id");
178
+ expect(parsed.columns[0].type).toBe("int");
179
+ expect(parsed.columns[0].nullable).toBe(false);
180
+ expect(parsed.columns[0].key).toBe("PRI");
181
+ expect(parsed.columns[1].nullable).toBe(false);
182
+ expect(parsed.columns[2].nullable).toBe(true);
183
+ });
184
+
185
+ it("should return error result when table not found", async () => {
186
+ mockDb.describeTable.mockRejectedValue(
187
+ new MySQLMCPError("Table doesn't exist", "ER_NO_SUCH_TABLE")
188
+ );
189
+
190
+ const result = await tools.describeTable({ tableName: "non_existent" });
191
+
192
+ const parsed = JSON.parse(result.content[0].text);
193
+ expect(parsed.error).toContain("MySQL Error");
194
+ expect(parsed.tableName).toBe("non_existent");
195
+ });
196
+ });
197
+
198
+ describe("showDatabases", () => {
199
+ it("should return list of databases", async () => {
200
+ mockDb.showDatabases.mockResolvedValue([
201
+ { Database: "information_schema" },
202
+ { Database: "testdb" },
203
+ { Database: "production" },
204
+ ]);
205
+
206
+ const result = await tools.showDatabases();
207
+
208
+ const parsed = JSON.parse(result.content[0].text);
209
+ expect(parsed.databaseCount).toBe(3);
210
+ expect(parsed.databases).toEqual([
211
+ "information_schema",
212
+ "testdb",
213
+ "production",
214
+ ]);
215
+ });
216
+
217
+ it("should return error result when listing databases fails", async () => {
218
+ mockDb.showDatabases.mockRejectedValue(
219
+ new MySQLMCPError("Access denied", "ER_ACCESS_DENIED")
220
+ );
221
+
222
+ const result = await tools.showDatabases();
223
+
224
+ const parsed = JSON.parse(result.content[0].text);
225
+ expect(parsed.error).toContain("MySQL Error");
226
+ });
227
+ });
228
+ });
package/src/tools.ts ADDED
@@ -0,0 +1,212 @@
1
+ import { MySQLConnection } from "./database.js";
2
+ import {
3
+ ReadQueryParams,
4
+ DescribeTableParams,
5
+ McpToolResult,
6
+ MySQLMCPError,
7
+ } from "./types.js";
8
+
9
+ export class MySQLMCPTools {
10
+ constructor(private db: MySQLConnection) {}
11
+
12
+ async readQuery(params: ReadQueryParams): Promise<McpToolResult> {
13
+ try {
14
+ const result = await this.db.executeQuery(params.query);
15
+
16
+ const resultText = JSON.stringify(
17
+ {
18
+ query: params.query,
19
+ rowCount: result.rowCount,
20
+ executionTime: `${result.executionTime}ms`,
21
+ data: result.data,
22
+ },
23
+ null,
24
+ 2
25
+ );
26
+
27
+ return {
28
+ content: [
29
+ {
30
+ type: "text",
31
+ text: resultText,
32
+ },
33
+ ],
34
+ };
35
+ } catch (error) {
36
+ const errorMessage =
37
+ error instanceof MySQLMCPError
38
+ ? `MySQL Error: ${error.message}`
39
+ : `Unexpected error: ${
40
+ error instanceof Error ? error.message : "Unknown error"
41
+ }`;
42
+
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: JSON.stringify(
48
+ {
49
+ error: errorMessage,
50
+ query: params.query,
51
+ },
52
+ null,
53
+ 2
54
+ ),
55
+ },
56
+ ],
57
+ };
58
+ }
59
+ }
60
+
61
+ async listTables(): Promise<McpToolResult> {
62
+ try {
63
+ const tables = await this.db.listTables();
64
+
65
+ const resultText = JSON.stringify(
66
+ {
67
+ tableCount: tables.length,
68
+ tables: tables.map((table) => ({
69
+ name: table.TABLE_NAME,
70
+ type: table.TABLE_TYPE,
71
+ schema: table.TABLE_SCHEMA,
72
+ })),
73
+ },
74
+ null,
75
+ 2
76
+ );
77
+
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: resultText,
83
+ },
84
+ ],
85
+ };
86
+ } catch (error) {
87
+ const errorMessage =
88
+ error instanceof MySQLMCPError
89
+ ? `MySQL Error: ${error.message}`
90
+ : `Unexpected error: ${
91
+ error instanceof Error ? error.message : "Unknown error"
92
+ }`;
93
+
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: JSON.stringify(
99
+ {
100
+ error: errorMessage,
101
+ },
102
+ null,
103
+ 2
104
+ ),
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ }
110
+
111
+ async describeTable(params: DescribeTableParams): Promise<McpToolResult> {
112
+ try {
113
+ const columns = await this.db.describeTable(params.tableName);
114
+
115
+ const resultText = JSON.stringify(
116
+ {
117
+ tableName: params.tableName,
118
+ columnCount: columns.length,
119
+ columns: columns.map((column) => ({
120
+ name: column.COLUMN_NAME,
121
+ type: column.DATA_TYPE,
122
+ nullable: column.IS_NULLABLE === "YES",
123
+ default: column.COLUMN_DEFAULT,
124
+ key: column.COLUMN_KEY,
125
+ extra: column.EXTRA,
126
+ comment: column.COLUMN_COMMENT,
127
+ })),
128
+ },
129
+ null,
130
+ 2
131
+ );
132
+
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: resultText,
138
+ },
139
+ ],
140
+ };
141
+ } catch (error) {
142
+ const errorMessage =
143
+ error instanceof MySQLMCPError
144
+ ? `MySQL Error: ${error.message}`
145
+ : `Unexpected error: ${
146
+ error instanceof Error ? error.message : "Unknown error"
147
+ }`;
148
+
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: JSON.stringify(
154
+ {
155
+ error: errorMessage,
156
+ tableName: params.tableName,
157
+ },
158
+ null,
159
+ 2
160
+ ),
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ }
166
+
167
+ async showDatabases(): Promise<McpToolResult> {
168
+ try {
169
+ const databases = await this.db.showDatabases();
170
+
171
+ const resultText = JSON.stringify(
172
+ {
173
+ databaseCount: databases.length,
174
+ databases: databases.map((db) => db.Database),
175
+ },
176
+ null,
177
+ 2
178
+ );
179
+
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: resultText,
185
+ },
186
+ ],
187
+ };
188
+ } catch (error) {
189
+ const errorMessage =
190
+ error instanceof MySQLMCPError
191
+ ? `MySQL Error: ${error.message}`
192
+ : `Unexpected error: ${
193
+ error instanceof Error ? error.message : "Unknown error"
194
+ }`;
195
+
196
+ return {
197
+ content: [
198
+ {
199
+ type: "text",
200
+ text: JSON.stringify(
201
+ {
202
+ error: errorMessage,
203
+ },
204
+ null,
205
+ 2
206
+ ),
207
+ },
208
+ ],
209
+ };
210
+ }
211
+ }
212
+ }
package/src/types.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+
3
+ export const MySQLConfigSchema = z.object({
4
+ host: z.string(),
5
+ port: z.number().min(1).max(65535).default(3306),
6
+ user: z.string(),
7
+ password: z.string(),
8
+ database: z.string(),
9
+ ssl: z.boolean().optional().default(false),
10
+ connectionTimeout: z.number().optional().default(30000),
11
+ });
12
+
13
+ export type MySQLConfig = z.output<typeof MySQLConfigSchema>;
14
+ export type MySQLConfigInput = z.input<typeof MySQLConfigSchema>;
15
+
16
+ export const ReadQueryParamsSchema = z.object({
17
+ query: z.string().min(1),
18
+ });
19
+
20
+ export type ReadQueryParams = z.infer<typeof ReadQueryParamsSchema>;
21
+
22
+ export const DescribeTableParamsSchema = z.object({
23
+ tableName: z.string().min(1),
24
+ });
25
+
26
+ export type DescribeTableParams = z.infer<typeof DescribeTableParamsSchema>;
27
+
28
+ export interface TableInfo {
29
+ TABLE_NAME: string;
30
+ TABLE_TYPE: string;
31
+ TABLE_SCHEMA: string;
32
+ }
33
+
34
+ export interface ColumnInfo {
35
+ COLUMN_NAME: string;
36
+ DATA_TYPE: string;
37
+ IS_NULLABLE: string;
38
+ COLUMN_DEFAULT: string | null;
39
+ COLUMN_KEY: string;
40
+ EXTRA: string;
41
+ COLUMN_COMMENT: string;
42
+ }
43
+
44
+ export interface DatabaseInfo {
45
+ Database: string;
46
+ }
47
+
48
+ export interface QueryResult {
49
+ data: Record<string, unknown>[];
50
+ rowCount: number;
51
+ executionTime: number;
52
+ }
53
+
54
+ export interface McpToolResult {
55
+ [x: string]: unknown;
56
+ content: Array<{
57
+ type: "text";
58
+ text: string;
59
+ }>;
60
+ }
61
+
62
+ export class MySQLMCPError extends Error {
63
+ constructor(
64
+ message: string,
65
+ public readonly code?: string,
66
+ public readonly sqlState?: string
67
+ ) {
68
+ super(message);
69
+ this.name = "MySQLMCPError";
70
+ }
71
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true,
17
+ "allowSyntheticDefaultImports": true
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
21
+ }