@williamp29/project-mcp-server 1.1.0 → 2.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/README.md +74 -107
- package/dist/api-explorer/api-executor.js +0 -2
- package/dist/api-explorer/tool-generator.d.ts +7 -7
- package/dist/api-explorer/tool-generator.js +13 -13
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +23 -6
- package/dist/db-explorer/db-executor.d.ts +21 -0
- package/dist/db-explorer/db-executor.js +77 -0
- package/dist/db-explorer/db-tool-generator.d.ts +67 -0
- package/dist/db-explorer/db-tool-generator.js +95 -0
- package/dist/db-explorer/drivers/index.d.ts +1 -0
- package/dist/db-explorer/drivers/index.js +1 -0
- package/dist/db-explorer/drivers/mysql-driver.d.ts +20 -0
- package/dist/db-explorer/drivers/mysql-driver.js +170 -0
- package/dist/db-explorer/index.d.ts +5 -0
- package/dist/db-explorer/index.js +4 -0
- package/dist/db-explorer/sql-validator.d.ts +10 -0
- package/dist/db-explorer/sql-validator.js +44 -0
- package/dist/db-explorer/types.d.ts +82 -0
- package/dist/db-explorer/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp-server.d.ts +20 -4
- package/dist/mcp-server.js +41 -7
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,153 +1,120 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Project MCP Server
|
|
2
2
|
|
|
3
|
-
A powerful Model Context Protocol (MCP) server that dynamically serves context about your project. It
|
|
3
|
+
A powerful **Model Context Protocol (MCP)** server that dynamically serves context about your project. It acts as a bridge, allowing LLM agents to explore and interact with your project's APIs (via OpenAPI) and databases.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start (Zero Config)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The easiest way to use this server is to run it directly with `npx`. This requires no code changes to your project.
|
|
8
8
|
|
|
9
|
-
###
|
|
10
|
-
1. **Discovery**: Start by calling `get_tags` to understand the broad areas of the API.
|
|
11
|
-
2. **Listing**: Use `get_tag_endpoints` or `get_all_endpoints` to see available actions for a specific topic.
|
|
12
|
-
3. **Details**: Call `get_endpoint` for a specific path and method to see exactly what parameters and request body are required.
|
|
13
|
-
4. **Execution**: Use `call_endpoint` to perform the actual API request.
|
|
9
|
+
### Run with npx
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
**For Cursor / Claude Desktop:**
|
|
12
|
+
Add this to your MCP settings configuration:
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- `PROJECT_MCP_AUTH_TYPE`: `bearer`, `identity`, or `none`.
|
|
32
|
-
- `PROJECT_MCP_AUTH_IDENTIFIABLE`: (Optional) e.g., `UID:`.
|
|
33
|
-
- `PROJECT_MCP_AUTH_IDENTIFIER`: Default ID value.
|
|
34
|
-
|
|
35
|
-
### 3. Usage in Cursor / Claude Desktop
|
|
36
|
-
Add a new MCP server with the following command:
|
|
37
|
-
```bash
|
|
38
|
-
node /path/to/dist/cli.js
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"my-project": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@williamp29/project-mcp-server"],
|
|
20
|
+
"env": {
|
|
21
|
+
"PROJECT_MCP_API_BASE_URL": "...",
|
|
22
|
+
"PROJECT_MCP_DB_HOST": "..."
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
39
27
|
```
|
|
28
|
+
*(Note: You can pass environment variables directly in the JSON config or load them from a file if your MCP client supports it.)*
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npx @williamp29/project-mcp-server
|
|
44
|
-
```
|
|
30
|
+
---
|
|
45
31
|
|
|
46
|
-
## Integration
|
|
32
|
+
## Library Integration (Advanced)
|
|
47
33
|
|
|
48
|
-
|
|
34
|
+
For deep integration, install the package as a dependency in your Node.js project. This allows you to customize authentication, add custom tools, or use it programmatically.
|
|
49
35
|
|
|
50
|
-
###
|
|
36
|
+
### 1. Install
|
|
51
37
|
```bash
|
|
52
38
|
npm install @williamp29/project-mcp-server
|
|
53
39
|
```
|
|
54
40
|
|
|
55
|
-
###
|
|
56
|
-
Create a file
|
|
41
|
+
### 2. Create an Entry Point
|
|
42
|
+
Create a file (e.g., `mcp-server.ts`) to configure and start your server:
|
|
57
43
|
|
|
58
|
-
```
|
|
59
|
-
// mcp-serve.js
|
|
44
|
+
```typescript
|
|
60
45
|
import { MCPServer } from "@williamp29/project-mcp-server";
|
|
61
46
|
import { AuthStrategy, GlobalAuthContext } from "@williamp29/project-mcp-server/api-explorer";
|
|
62
47
|
|
|
63
|
-
//
|
|
64
|
-
class
|
|
65
|
-
name = "
|
|
48
|
+
// (Optional) Define a custom authentication strategy
|
|
49
|
+
class MyCustomAuth implements AuthStrategy {
|
|
50
|
+
name = "MyCustomAuth";
|
|
66
51
|
async getHeaders() {
|
|
67
|
-
//
|
|
52
|
+
// Fetch token from vault, env, or database
|
|
68
53
|
return { "Authorization": `Bearer ${process.env.MY_API_TOKEN}` };
|
|
69
54
|
}
|
|
70
55
|
}
|
|
71
56
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
const server = new MCPServer({
|
|
58
|
+
specPath: "./openapi-spec.json", // Path to your OpenAPI spec
|
|
59
|
+
authContext: new GlobalAuthContext(new MyCustomAuth())
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
server.start().catch(console.error);
|
|
75
63
|
```
|
|
76
64
|
|
|
77
|
-
###
|
|
78
|
-
In your project's `package.json`:
|
|
65
|
+
### 3. Add Script to package.json
|
|
79
66
|
```json
|
|
80
67
|
{
|
|
81
68
|
"scripts": {
|
|
82
|
-
"mcp
|
|
69
|
+
"mcp": "ts-node mcp-server.ts"
|
|
83
70
|
}
|
|
84
71
|
}
|
|
85
72
|
```
|
|
86
73
|
|
|
87
|
-
###
|
|
88
|
-
In your Cursor MCP settings (`mcp_settings.json`):
|
|
74
|
+
### 4. Use in Cursor
|
|
89
75
|
```json
|
|
90
76
|
{
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"my-integrated-project": {
|
|
79
|
+
"command": "npm",
|
|
80
|
+
"args": ["run", "mcp"],
|
|
81
|
+
"cwd": "/absolute/path/to/your/project"
|
|
82
|
+
}
|
|
95
83
|
}
|
|
96
84
|
}
|
|
97
85
|
```
|
|
98
86
|
|
|
99
|
-
Now Cursor will run your custom script whenever it needs to use the MCP server.
|
|
100
|
-
|
|
101
87
|
---
|
|
102
88
|
|
|
103
|
-
##
|
|
89
|
+
## Configuration Reference
|
|
104
90
|
|
|
105
|
-
|
|
91
|
+
### Environment Variables
|
|
92
|
+
These variables control the server behavior. They are automatically loaded if you use the `npx` method or if you use `dotenv` in your custom script.
|
|
106
93
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
| Variable | Description | Default |
|
|
95
|
+
| :--- | :--- | :--- |
|
|
96
|
+
| `PROJECT_MCP_API_BASE_URL` | Base URL of the target API | - |
|
|
97
|
+
| `PROJECT_MCP_AUTH_TYPE` | Auth method: `bearer`, `identity`, `none` | `none` |
|
|
98
|
+
| `PROJECT_MCP_AUTH_IDENTIFIER` | Default ID for `identity` auth | - |
|
|
99
|
+
| `PROJECT_MCP_DB_HOST` | Database hostname | - |
|
|
100
|
+
| `PROJECT_MCP_DB_PORT` | Database port | `3306` |
|
|
101
|
+
| `PROJECT_MCP_DB_USER` | Database username | - |
|
|
102
|
+
| `PROJECT_MCP_DB_PASSWORD` | Database password | - |
|
|
103
|
+
| `PROJECT_MCP_DB_DATABASE` | Database name | - |
|
|
110
104
|
|
|
111
|
-
|
|
112
|
-
server.start().catch(console.error);
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Custom Authentication Strategy
|
|
116
|
-
You can implement your own logic for fetching or rotating tokens:
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
import { MCPServer } from "@williamp29/project-mcp-server";
|
|
120
|
-
import { AuthStrategy, GlobalAuthContext } from "@williamp29/project-mcp-server/api-explorer";
|
|
121
|
-
|
|
122
|
-
class MyCustomAuth implements AuthStrategy {
|
|
123
|
-
name = "MyCustomAuth";
|
|
124
|
-
async getHeaders() {
|
|
125
|
-
const token = await fetchTokenFromVault();
|
|
126
|
-
return { "X-Custom-Auth": token };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const authContext = new GlobalAuthContext(new MyCustomAuth());
|
|
131
|
-
const server = new MCPServer("./spec.json", authContext);
|
|
132
|
-
server.start();
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Request Hooks
|
|
136
|
-
Add custom logic to every request (logging, tracing, extra headers):
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
import { addRequestHook } from "@williamp29/project-mcp-server/api-explorer";
|
|
140
|
-
|
|
141
|
-
addRequestHook((config) => {
|
|
142
|
-
console.error(`[API] ${config.method?.toUpperCase()} ${config.url}`);
|
|
143
|
-
config.headers["X-Request-ID"] = "mcp-123";
|
|
144
|
-
return config;
|
|
145
|
-
});
|
|
146
|
-
```
|
|
105
|
+
---
|
|
147
106
|
|
|
148
107
|
## Features
|
|
149
|
-
|
|
150
|
-
- **
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
108
|
+
|
|
109
|
+
- **API Explorer**:
|
|
110
|
+
- Automatically parses OpenAPI 3.0 specifications.
|
|
111
|
+
- Exposes tools like `api_get_tags`, `api_get_endpoints`, and `api_call_endpoint`.
|
|
112
|
+
- Supports dynamic path parameters and JSON bodies.
|
|
113
|
+
- `api_set_identity`: Switch the active user context for API calls dynamically during a session.
|
|
114
|
+
|
|
115
|
+
- **Database Explorer**:
|
|
116
|
+
- Inspect schemas with `db_list_tables`, `db_describe_tables`, and `db_get_schemas`.
|
|
117
|
+
- Discover database structure with `db_get_relationships` (supports table filtering).
|
|
118
|
+
- Analyze data with `db_get_table_stats` and `db_sample_rows`.
|
|
119
|
+
- Run validated SQL with `db_run_query`, `db_run_update_statement`, and `db_run_delete_statement`.
|
|
120
|
+
- Supports MySQL (driver included).
|
|
@@ -24,14 +24,12 @@ export class ApiExecutor {
|
|
|
24
24
|
});
|
|
25
25
|
// Add interceptor for dynamic auth and hooks
|
|
26
26
|
this.client.interceptors.request.use(async (config) => {
|
|
27
|
-
// 1. Inject Dynamic Auth Headers
|
|
28
27
|
const authHeaders = await this.authContext.getHeaders();
|
|
29
28
|
// Log for debugging (stderr)
|
|
30
29
|
if (Object.keys(authHeaders).length > 0) {
|
|
31
30
|
console.error(`[ApiExecutor] Injecting auth headers from strategy: ${this.authContext.strategy.name}`);
|
|
32
31
|
}
|
|
33
32
|
Object.assign(config.headers, authHeaders);
|
|
34
|
-
// 2. Run Request Hooks
|
|
35
33
|
return await runRequestHooks(config);
|
|
36
34
|
});
|
|
37
35
|
}
|
|
@@ -20,32 +20,32 @@ export declare class ToolGenerator {
|
|
|
20
20
|
* These definitions are used by MCPServer to register tools with the SDK.
|
|
21
21
|
*/
|
|
22
22
|
getToolDefinitions(): {
|
|
23
|
-
|
|
23
|
+
api_get_tags: {
|
|
24
24
|
description: string;
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
api_get_tag_endpoints: {
|
|
27
27
|
description: string;
|
|
28
28
|
inputSchema: z.ZodObject<{
|
|
29
29
|
tag: z.ZodString;
|
|
30
30
|
}, z.core.$strip>;
|
|
31
31
|
};
|
|
32
|
-
|
|
32
|
+
api_get_tags_endpoints: {
|
|
33
33
|
description: string;
|
|
34
34
|
inputSchema: z.ZodObject<{
|
|
35
35
|
tags: z.ZodArray<z.ZodString>;
|
|
36
36
|
}, z.core.$strip>;
|
|
37
37
|
};
|
|
38
|
-
|
|
38
|
+
api_get_all_endpoints: {
|
|
39
39
|
description: string;
|
|
40
40
|
};
|
|
41
|
-
|
|
41
|
+
api_get_endpoint: {
|
|
42
42
|
description: string;
|
|
43
43
|
inputSchema: z.ZodObject<{
|
|
44
44
|
method: z.ZodString;
|
|
45
45
|
path: z.ZodString;
|
|
46
46
|
}, z.core.$strip>;
|
|
47
47
|
};
|
|
48
|
-
|
|
48
|
+
api_get_endpoints: {
|
|
49
49
|
description: string;
|
|
50
50
|
inputSchema: z.ZodObject<{
|
|
51
51
|
requests: z.ZodArray<z.ZodObject<{
|
|
@@ -54,7 +54,7 @@ export declare class ToolGenerator {
|
|
|
54
54
|
}, z.core.$strip>>;
|
|
55
55
|
}, z.core.$strip>;
|
|
56
56
|
};
|
|
57
|
-
|
|
57
|
+
api_call_endpoint: {
|
|
58
58
|
description: string;
|
|
59
59
|
inputSchema: z.ZodObject<{
|
|
60
60
|
method: z.ZodString;
|
|
@@ -17,32 +17,32 @@ export class ToolGenerator {
|
|
|
17
17
|
*/
|
|
18
18
|
getToolDefinitions() {
|
|
19
19
|
return {
|
|
20
|
-
|
|
20
|
+
api_get_tags: {
|
|
21
21
|
description: "Get all unique tags defined in the API spec. This helps to group and discover endpoints.",
|
|
22
22
|
},
|
|
23
|
-
|
|
23
|
+
api_get_tag_endpoints: {
|
|
24
24
|
description: "Get all endpoints associated with a specific tag. Returns a summary of each endpoint.",
|
|
25
25
|
inputSchema: z.object({
|
|
26
26
|
tag: z.string().describe("The tag to filter endpoints by."),
|
|
27
27
|
}),
|
|
28
28
|
},
|
|
29
|
-
|
|
29
|
+
api_get_tags_endpoints: {
|
|
30
30
|
description: "Get all endpoints associated with multiple tags. Returns a summary of each endpoint.",
|
|
31
31
|
inputSchema: z.object({
|
|
32
32
|
tags: z.array(z.string()).describe("The tags to filter endpoints by."),
|
|
33
33
|
}),
|
|
34
34
|
},
|
|
35
|
-
|
|
35
|
+
api_get_all_endpoints: {
|
|
36
36
|
description: "Get a summarized list of all endpoints available in the API.",
|
|
37
37
|
},
|
|
38
|
-
|
|
38
|
+
api_get_endpoint: {
|
|
39
39
|
description: "Get detailed information about a specific endpoint, including parameters and request body schema.",
|
|
40
40
|
inputSchema: z.object({
|
|
41
41
|
method: z.string().describe("The HTTP method (GET, POST, etc.)."),
|
|
42
42
|
path: z.string().describe("The endpoint path."),
|
|
43
43
|
}),
|
|
44
44
|
},
|
|
45
|
-
|
|
45
|
+
api_get_endpoints: {
|
|
46
46
|
description: "Get detailed information for multiple specific endpoints.",
|
|
47
47
|
inputSchema: z.object({
|
|
48
48
|
requests: z.array(z.object({
|
|
@@ -51,7 +51,7 @@ export class ToolGenerator {
|
|
|
51
51
|
})).describe("List of endpoint requests."),
|
|
52
52
|
}),
|
|
53
53
|
},
|
|
54
|
-
|
|
54
|
+
api_call_endpoint: {
|
|
55
55
|
description: "Execute a request to a project's endpoint using the specified parameters and body.",
|
|
56
56
|
inputSchema: z.object({
|
|
57
57
|
method: z.string().describe("The HTTP method."),
|
|
@@ -64,17 +64,17 @@ export class ToolGenerator {
|
|
|
64
64
|
}
|
|
65
65
|
handleToolCall(name, args) {
|
|
66
66
|
switch (name) {
|
|
67
|
-
case "
|
|
67
|
+
case "api_get_tags":
|
|
68
68
|
return this.parser.getTags();
|
|
69
|
-
case "
|
|
69
|
+
case "api_get_tag_endpoints":
|
|
70
70
|
return this.parser.getEndpointsByTag(args.tag);
|
|
71
|
-
case "
|
|
71
|
+
case "api_get_tags_endpoints":
|
|
72
72
|
return this.parser.getEndpointsByTags(args.tags);
|
|
73
|
-
case "
|
|
73
|
+
case "api_get_all_endpoints":
|
|
74
74
|
return this.parser.getEndpoints();
|
|
75
|
-
case "
|
|
75
|
+
case "api_get_endpoint":
|
|
76
76
|
return this.parser.getEndpoint(args.method, args.path);
|
|
77
|
-
case "
|
|
77
|
+
case "api_get_endpoints":
|
|
78
78
|
return args.requests.map((r) => this.parser.getEndpoint(r.method, r.path));
|
|
79
79
|
default:
|
|
80
80
|
throw new Error(`Unknown tool: ${name}`);
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import "dotenv/config";
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
2
3
|
import { MCPServer } from "./index.js";
|
|
3
4
|
import path from "path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const specPath = process.env.PROJECT_MCP_OPENAPI_SPEC || path.resolve(process.cwd(), "openapi-spec.json");
|
|
6
|
+
let dbConfig = undefined;
|
|
7
|
+
if (process.env.PROJECT_MCP_DB_HOST) {
|
|
8
|
+
dbConfig = {
|
|
9
|
+
driver: process.env.PROJECT_MCP_DB_DRIVER || "mysql",
|
|
10
|
+
host: process.env.PROJECT_MCP_DB_HOST || "localhost",
|
|
11
|
+
port: parseInt(process.env.PROJECT_MCP_DB_PORT || "3306", 10),
|
|
12
|
+
user: process.env.PROJECT_MCP_DB_USER || "",
|
|
13
|
+
password: process.env.PROJECT_MCP_DB_PASSWORD || "",
|
|
14
|
+
database: process.env.PROJECT_MCP_DB_DATABASE || "",
|
|
15
|
+
poolSize: parseInt(process.env.PROJECT_MCP_DB_POOL_SIZE || "10", 10),
|
|
16
|
+
enableRunQuery: process.env.PROJECT_MCP_DB_ENABLE_QUERY !== "false",
|
|
17
|
+
enableRunUpdateStatement: process.env.PROJECT_MCP_DB_ENABLE_UPDATE !== "false",
|
|
18
|
+
enableRunDeleteStatement: process.env.PROJECT_MCP_DB_ENABLE_DELETE === "true",
|
|
19
|
+
enableRunStatement: process.env.PROJECT_MCP_DB_ENABLE_STATEMENT === "true",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const server = new MCPServer({
|
|
23
|
+
specPath,
|
|
24
|
+
dbConfig
|
|
25
|
+
});
|
|
9
26
|
server.start().catch((error) => {
|
|
10
|
-
console.error("
|
|
27
|
+
console.error("Failed to start MCP server:", error);
|
|
11
28
|
process.exit(1);
|
|
12
29
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DbDriver, DbConfig } from "./types.js";
|
|
2
|
+
export declare class DbExecutor {
|
|
3
|
+
private driver;
|
|
4
|
+
private config;
|
|
5
|
+
constructor(driver: DbDriver, config: DbConfig);
|
|
6
|
+
getDriver(): DbDriver;
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
disconnect(): Promise<void>;
|
|
9
|
+
listTables(): Promise<import("./types.js").TableInfo[]>;
|
|
10
|
+
describeTable(table: string): Promise<import("./types.js").ColumnInfo[]>;
|
|
11
|
+
describeTables(tables: string[]): Promise<Record<string, any>>;
|
|
12
|
+
getTableSchema(table: string): Promise<string>;
|
|
13
|
+
getTableSchemas(tables: string[]): Promise<Record<string, string>>;
|
|
14
|
+
getRelationships(tables?: string[]): Promise<import("./types.js").Relationship[]>;
|
|
15
|
+
getTableStats(table: string): Promise<import("./types.js").TableStats>;
|
|
16
|
+
sampleRows(table: string, limit?: number): Promise<any[]>;
|
|
17
|
+
runQuery(query: string): Promise<import("./types.js").QueryResult>;
|
|
18
|
+
runUpdateStatement(query: string): Promise<import("./types.js").UpdateResult>;
|
|
19
|
+
runDeleteStatement(query: string): Promise<import("./types.js").DeleteResult>;
|
|
20
|
+
runStatement(query: string): Promise<import("./types.js").StatementResult>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { SqlValidator } from "./sql-validator.js";
|
|
2
|
+
export class DbExecutor {
|
|
3
|
+
driver;
|
|
4
|
+
config;
|
|
5
|
+
constructor(driver, config) {
|
|
6
|
+
this.driver = driver;
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
getDriver() {
|
|
10
|
+
return this.driver;
|
|
11
|
+
}
|
|
12
|
+
async connect() {
|
|
13
|
+
await this.driver.connect();
|
|
14
|
+
}
|
|
15
|
+
async disconnect() {
|
|
16
|
+
await this.driver.disconnect();
|
|
17
|
+
}
|
|
18
|
+
async listTables() {
|
|
19
|
+
return await this.driver.listTables();
|
|
20
|
+
}
|
|
21
|
+
async describeTable(table) {
|
|
22
|
+
return await this.driver.describeTable(table);
|
|
23
|
+
}
|
|
24
|
+
async describeTables(tables) {
|
|
25
|
+
const results = {};
|
|
26
|
+
for (const table of tables) {
|
|
27
|
+
results[table] = await this.driver.describeTable(table);
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
async getTableSchema(table) {
|
|
32
|
+
return await this.driver.getTableSchema(table);
|
|
33
|
+
}
|
|
34
|
+
async getTableSchemas(tables) {
|
|
35
|
+
const results = {};
|
|
36
|
+
for (const table of tables) {
|
|
37
|
+
results[table] = await this.driver.getTableSchema(table);
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
async getRelationships(tables) {
|
|
42
|
+
return await this.driver.getRelationships(tables);
|
|
43
|
+
}
|
|
44
|
+
async getTableStats(table) {
|
|
45
|
+
return await this.driver.getTableStats(table);
|
|
46
|
+
}
|
|
47
|
+
async sampleRows(table, limit) {
|
|
48
|
+
return await this.driver.sampleRows(table, limit);
|
|
49
|
+
}
|
|
50
|
+
async runQuery(query) {
|
|
51
|
+
if (this.config.enableRunQuery === false) {
|
|
52
|
+
throw new Error("Tool 'run_query' is disabled.");
|
|
53
|
+
}
|
|
54
|
+
SqlValidator.isSelectOnly(query);
|
|
55
|
+
return await this.driver.executeQuery(query);
|
|
56
|
+
}
|
|
57
|
+
async runUpdateStatement(query) {
|
|
58
|
+
if (this.config.enableRunUpdateStatement === false) {
|
|
59
|
+
throw new Error("Tool 'run_update_statement' is disabled.");
|
|
60
|
+
}
|
|
61
|
+
SqlValidator.isUpdateOnly(query);
|
|
62
|
+
return await this.driver.executeUpdate(query);
|
|
63
|
+
}
|
|
64
|
+
async runDeleteStatement(query) {
|
|
65
|
+
if (this.config.enableRunDeleteStatement !== true) {
|
|
66
|
+
throw new Error("Tool 'run_delete_statement' is disabled.");
|
|
67
|
+
}
|
|
68
|
+
SqlValidator.isDeleteOnly(query);
|
|
69
|
+
return await this.driver.executeDelete(query);
|
|
70
|
+
}
|
|
71
|
+
async runStatement(query) {
|
|
72
|
+
if (this.config.enableRunStatement !== true) {
|
|
73
|
+
throw new Error("Tool 'run_statement' is disabled.");
|
|
74
|
+
}
|
|
75
|
+
return await this.driver.executeStatement(query);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DbExecutor } from "./db-executor.js";
|
|
3
|
+
export declare class DbToolGenerator {
|
|
4
|
+
private executor;
|
|
5
|
+
constructor(executor: DbExecutor);
|
|
6
|
+
getToolDefinitions(): {
|
|
7
|
+
db_list_tables: {
|
|
8
|
+
description: string;
|
|
9
|
+
};
|
|
10
|
+
db_describe_tables: {
|
|
11
|
+
description: string;
|
|
12
|
+
inputSchema: z.ZodObject<{
|
|
13
|
+
tables: z.ZodArray<z.ZodString>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
};
|
|
16
|
+
db_get_schemas: {
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: z.ZodObject<{
|
|
19
|
+
tables: z.ZodArray<z.ZodString>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
};
|
|
22
|
+
db_get_relationships: {
|
|
23
|
+
description: string;
|
|
24
|
+
inputSchema: z.ZodObject<{
|
|
25
|
+
tables: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
};
|
|
28
|
+
db_get_table_stats: {
|
|
29
|
+
description: string;
|
|
30
|
+
inputSchema: z.ZodObject<{
|
|
31
|
+
table: z.ZodString;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
};
|
|
34
|
+
db_sample_rows: {
|
|
35
|
+
description: string;
|
|
36
|
+
inputSchema: z.ZodObject<{
|
|
37
|
+
table: z.ZodString;
|
|
38
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
};
|
|
41
|
+
db_run_query: {
|
|
42
|
+
description: string;
|
|
43
|
+
inputSchema: z.ZodObject<{
|
|
44
|
+
query: z.ZodString;
|
|
45
|
+
}, z.core.$strip>;
|
|
46
|
+
};
|
|
47
|
+
db_run_update_statement: {
|
|
48
|
+
description: string;
|
|
49
|
+
inputSchema: z.ZodObject<{
|
|
50
|
+
query: z.ZodString;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
};
|
|
53
|
+
db_run_delete_statement: {
|
|
54
|
+
description: string;
|
|
55
|
+
inputSchema: z.ZodObject<{
|
|
56
|
+
query: z.ZodString;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
};
|
|
59
|
+
db_run_statement: {
|
|
60
|
+
description: string;
|
|
61
|
+
inputSchema: z.ZodObject<{
|
|
62
|
+
query: z.ZodString;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
handleToolCall(name: string, args: any): Promise<any[] | Record<string, any> | import("./types.js").TableStats | import("./types.js").QueryResult | import("./types.js").DeleteResult | import("./types.js").StatementResult>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export class DbToolGenerator {
|
|
3
|
+
executor;
|
|
4
|
+
constructor(executor) {
|
|
5
|
+
this.executor = executor;
|
|
6
|
+
}
|
|
7
|
+
getToolDefinitions() {
|
|
8
|
+
return {
|
|
9
|
+
db_list_tables: {
|
|
10
|
+
description: "List all tables in the current database.",
|
|
11
|
+
},
|
|
12
|
+
db_describe_tables: {
|
|
13
|
+
description: "Get detailed information about columns for multiple tables.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
tables: z.array(z.string()).describe("List of table names to describe."),
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
db_get_schemas: {
|
|
19
|
+
description: "Get the DDL statements for multiple tables.",
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
tables: z.array(z.string()).describe("List of table names."),
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
db_get_relationships: {
|
|
25
|
+
description: "Get foreign key relationships in the database, optionally filtered by tables.",
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
tables: z.array(z.string()).optional().describe("Optional list of table names to filter relationships by."),
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
db_get_table_stats: {
|
|
31
|
+
description: "Get row count and other statistics for a table.",
|
|
32
|
+
inputSchema: z.object({
|
|
33
|
+
table: z.string().describe("The name of the table."),
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
db_sample_rows: {
|
|
37
|
+
description: "Fetch sample rows from a table.",
|
|
38
|
+
inputSchema: z.object({
|
|
39
|
+
table: z.string().describe("The name of the table."),
|
|
40
|
+
limit: z.number().optional().default(10).describe("Maximum number of rows to fetch."),
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
db_run_query: {
|
|
44
|
+
description: "Execute a read-only SELECT query. Blocks modification keywords.",
|
|
45
|
+
inputSchema: z.object({
|
|
46
|
+
query: z.string().describe("The SQL SELECT query to execute."),
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
db_run_update_statement: {
|
|
50
|
+
description: "Execute an INSERT or UPDATE statement. Blocks DELETE and other dangerous keywords.",
|
|
51
|
+
inputSchema: z.object({
|
|
52
|
+
query: z.string().describe("The SQL INSERT/UPDATE statement."),
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
db_run_delete_statement: {
|
|
56
|
+
description: "Execute a DELETE or TRUNCATE statement. Blocks DROP/ALTER.",
|
|
57
|
+
inputSchema: z.object({
|
|
58
|
+
query: z.string().describe("The SQL DELETE/TRUNCATE statement."),
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
db_run_statement: {
|
|
62
|
+
description: "Execute any SQL statement. Full access, use with extreme caution. Disabled by default.",
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
query: z.string().describe("The SQL statement to execute."),
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async handleToolCall(name, args) {
|
|
70
|
+
switch (name) {
|
|
71
|
+
case "db_list_tables":
|
|
72
|
+
return await this.executor.listTables();
|
|
73
|
+
case "db_describe_tables":
|
|
74
|
+
return await this.executor.describeTables(args.tables);
|
|
75
|
+
case "db_get_schemas":
|
|
76
|
+
return await this.executor.getTableSchemas(args.tables);
|
|
77
|
+
case "db_get_relationships":
|
|
78
|
+
return await this.executor.getRelationships(args.tables);
|
|
79
|
+
case "db_get_table_stats":
|
|
80
|
+
return await this.executor.getTableStats(args.table);
|
|
81
|
+
case "db_sample_rows":
|
|
82
|
+
return await this.executor.sampleRows(args.table, args.limit);
|
|
83
|
+
case "db_run_query":
|
|
84
|
+
return await this.executor.runQuery(args.query);
|
|
85
|
+
case "db_run_update_statement":
|
|
86
|
+
return await this.executor.runUpdateStatement(args.query);
|
|
87
|
+
case "db_run_delete_statement":
|
|
88
|
+
return await this.executor.runDeleteStatement(args.query);
|
|
89
|
+
case "db_run_statement":
|
|
90
|
+
return await this.executor.runStatement(args.query);
|
|
91
|
+
default:
|
|
92
|
+
throw new Error(`Unknown database tool: ${name}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MySQLDriver } from "./mysql-driver.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MySQLDriver } from "./mysql-driver.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DbDriver, DbConfig, TableInfo, ColumnInfo, Relationship, TableStats, QueryResult, UpdateResult, DeleteResult, StatementResult } from "../types.js";
|
|
2
|
+
export declare class MySQLDriver implements DbDriver {
|
|
3
|
+
name: string;
|
|
4
|
+
private pool;
|
|
5
|
+
private config;
|
|
6
|
+
constructor(config: DbConfig);
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
disconnect(): Promise<void>;
|
|
9
|
+
private getPool;
|
|
10
|
+
listTables(): Promise<TableInfo[]>;
|
|
11
|
+
describeTable(table: string): Promise<ColumnInfo[]>;
|
|
12
|
+
getTableSchema(table: string): Promise<string>;
|
|
13
|
+
getRelationships(tables?: string[]): Promise<Relationship[]>;
|
|
14
|
+
getTableStats(table: string): Promise<TableStats>;
|
|
15
|
+
sampleRows(table: string, limit?: number): Promise<any[]>;
|
|
16
|
+
executeQuery(query: string): Promise<QueryResult>;
|
|
17
|
+
executeUpdate(query: string): Promise<UpdateResult>;
|
|
18
|
+
executeDelete(query: string): Promise<DeleteResult>;
|
|
19
|
+
executeStatement(query: string): Promise<StatementResult>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import mysql from "mysql2/promise";
|
|
2
|
+
export class MySQLDriver {
|
|
3
|
+
name = "mysql";
|
|
4
|
+
pool = null;
|
|
5
|
+
config;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async connect() {
|
|
10
|
+
if (this.pool)
|
|
11
|
+
return;
|
|
12
|
+
this.pool = mysql.createPool({
|
|
13
|
+
host: this.config.host,
|
|
14
|
+
port: this.config.port,
|
|
15
|
+
user: this.config.user,
|
|
16
|
+
password: this.config.password,
|
|
17
|
+
database: this.config.database,
|
|
18
|
+
connectionLimit: this.config.poolSize || 10,
|
|
19
|
+
waitForConnections: true,
|
|
20
|
+
queueLimit: 0
|
|
21
|
+
});
|
|
22
|
+
const connection = await this.pool.getConnection();
|
|
23
|
+
connection.release();
|
|
24
|
+
console.error(`[MySQLDriver] Connected to ${this.config.host}:${this.config.port}/${this.config.database}`);
|
|
25
|
+
}
|
|
26
|
+
async disconnect() {
|
|
27
|
+
if (this.pool) {
|
|
28
|
+
await this.pool.end();
|
|
29
|
+
this.pool = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
getPool() {
|
|
33
|
+
if (!this.pool)
|
|
34
|
+
throw new Error("Driver not connected. Call connect() first.");
|
|
35
|
+
return this.pool;
|
|
36
|
+
}
|
|
37
|
+
async listTables() {
|
|
38
|
+
const [rows] = await this.getPool().execute(`SELECT TABLE_NAME as name, TABLE_SCHEMA as \`schema\`, TABLE_TYPE as type, TABLE_COMMENT as comment
|
|
39
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
40
|
+
WHERE TABLE_SCHEMA = ?`, [this.config.database]);
|
|
41
|
+
return rows.map((r) => ({
|
|
42
|
+
name: r.name,
|
|
43
|
+
schema: r.schema,
|
|
44
|
+
type: r.type,
|
|
45
|
+
comment: r.comment || undefined
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
async describeTable(table) {
|
|
49
|
+
const [rows] = await this.getPool().execute(`SELECT
|
|
50
|
+
COLUMN_NAME as name,
|
|
51
|
+
DATA_TYPE as type,
|
|
52
|
+
COLUMN_TYPE as fullType,
|
|
53
|
+
IS_NULLABLE as nullable,
|
|
54
|
+
COLUMN_DEFAULT as defaultValue,
|
|
55
|
+
COLUMN_KEY as columnKey,
|
|
56
|
+
EXTRA as extra,
|
|
57
|
+
COLUMN_COMMENT as comment,
|
|
58
|
+
CHARACTER_MAXIMUM_LENGTH as characterMaxLength,
|
|
59
|
+
NUMERIC_PRECISION as numericPrecision,
|
|
60
|
+
NUMERIC_SCALE as numericScale
|
|
61
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
62
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
63
|
+
ORDER BY ORDINAL_POSITION`, [this.config.database, table]);
|
|
64
|
+
return rows.map((r) => {
|
|
65
|
+
const isEnum = r.fullType.startsWith("enum(") || r.fullType.startsWith("set(");
|
|
66
|
+
let enumValues;
|
|
67
|
+
if (isEnum) {
|
|
68
|
+
const matches = r.fullType.match(/'([^']*)'/g);
|
|
69
|
+
if (matches) {
|
|
70
|
+
enumValues = matches.map((m) => m.replace(/'/g, ""));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
name: r.name,
|
|
75
|
+
type: r.type,
|
|
76
|
+
fullType: r.fullType,
|
|
77
|
+
nullable: r.nullable === "YES",
|
|
78
|
+
defaultValue: r.defaultValue,
|
|
79
|
+
isPrimaryKey: r.columnKey === "PRI",
|
|
80
|
+
isForeignKey: r.columnKey === "MUL",
|
|
81
|
+
isAutoIncrement: r.extra?.includes("auto_increment") || false,
|
|
82
|
+
comment: r.comment || undefined,
|
|
83
|
+
extra: r.extra || undefined,
|
|
84
|
+
enumValues,
|
|
85
|
+
characterMaxLength: r.characterMaxLength ? Number(r.characterMaxLength) : undefined,
|
|
86
|
+
numericPrecision: r.numericPrecision ? Number(r.numericPrecision) : undefined,
|
|
87
|
+
numericScale: r.numericScale ? Number(r.numericScale) : undefined
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async getTableSchema(table) {
|
|
92
|
+
const [rows] = await this.getPool().execute(`SHOW CREATE TABLE \`${table}\``);
|
|
93
|
+
return rows[0]?.["Create Table"] || "";
|
|
94
|
+
}
|
|
95
|
+
async getRelationships(tables) {
|
|
96
|
+
let query = `SELECT
|
|
97
|
+
CONSTRAINT_NAME as constraintName,
|
|
98
|
+
TABLE_NAME as fromTable,
|
|
99
|
+
COLUMN_NAME as fromColumn,
|
|
100
|
+
REFERENCED_TABLE_NAME as toTable,
|
|
101
|
+
REFERENCED_COLUMN_NAME as toColumn
|
|
102
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
103
|
+
WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL`;
|
|
104
|
+
const params = [this.config.database];
|
|
105
|
+
if (tables && tables.length > 0) {
|
|
106
|
+
const placeholders = tables.map(() => "?").join(",");
|
|
107
|
+
query += ` AND (TABLE_NAME IN (${placeholders}) OR REFERENCED_TABLE_NAME IN (${placeholders}))`;
|
|
108
|
+
params.push(...tables, ...tables);
|
|
109
|
+
}
|
|
110
|
+
const [rows] = await this.getPool().execute(query, params);
|
|
111
|
+
return rows.map((r) => ({
|
|
112
|
+
constraintName: r.constraintName,
|
|
113
|
+
fromTable: r.fromTable,
|
|
114
|
+
fromColumn: r.fromColumn,
|
|
115
|
+
toTable: r.toTable,
|
|
116
|
+
toColumn: r.toColumn
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
async getTableStats(table) {
|
|
120
|
+
const [rows] = await this.getPool().execute(`SELECT
|
|
121
|
+
TABLE_ROWS as rowCount,
|
|
122
|
+
DATA_LENGTH as dataLength,
|
|
123
|
+
INDEX_LENGTH as indexLength,
|
|
124
|
+
CREATE_TIME as createTime,
|
|
125
|
+
UPDATE_TIME as updateTime
|
|
126
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
127
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`, [this.config.database, table]);
|
|
128
|
+
const r = rows[0];
|
|
129
|
+
if (!r)
|
|
130
|
+
throw new Error(`Table ${table} not found.`);
|
|
131
|
+
return {
|
|
132
|
+
rowCount: Number(r.rowCount),
|
|
133
|
+
dataLength: Number(r.dataLength),
|
|
134
|
+
indexLength: Number(r.indexLength),
|
|
135
|
+
createTime: r.createTime ? new Date(r.createTime) : undefined,
|
|
136
|
+
updateTime: r.updateTime ? new Date(r.updateTime) : undefined
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async sampleRows(table, limit = 10) {
|
|
140
|
+
const safeLimit = Math.max(1, Math.floor(Number(limit) || 10));
|
|
141
|
+
const [rows] = await this.getPool().execute(`SELECT * FROM \`${table}\` LIMIT ${safeLimit}`);
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
144
|
+
async executeQuery(query) {
|
|
145
|
+
const [rows] = await this.getPool().execute(query);
|
|
146
|
+
return { rows };
|
|
147
|
+
}
|
|
148
|
+
async executeUpdate(query) {
|
|
149
|
+
const [result] = await this.getPool().execute(query);
|
|
150
|
+
return {
|
|
151
|
+
affectedRows: result.affectedRows,
|
|
152
|
+
insertId: result.insertId,
|
|
153
|
+
message: result.info
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async executeDelete(query) {
|
|
157
|
+
const [result] = await this.getPool().execute(query);
|
|
158
|
+
return {
|
|
159
|
+
affectedRows: result.affectedRows,
|
|
160
|
+
message: result.info
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async executeStatement(query) {
|
|
164
|
+
const [result] = await this.getPool().execute(query);
|
|
165
|
+
return {
|
|
166
|
+
success: true,
|
|
167
|
+
data: result
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class SqlValidator {
|
|
2
|
+
/**
|
|
3
|
+
* Blocks keywords that are not allowed for a given operation.
|
|
4
|
+
* Checks case-insensitively and avoids being fooled by keywords inside strings/comments.
|
|
5
|
+
*/
|
|
6
|
+
static validate(query: string, blockedKeywords: string[]): void;
|
|
7
|
+
static isSelectOnly(query: string): void;
|
|
8
|
+
static isUpdateOnly(query: string): void;
|
|
9
|
+
static isDeleteOnly(query: string): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class SqlValidator {
|
|
2
|
+
/**
|
|
3
|
+
* Blocks keywords that are not allowed for a given operation.
|
|
4
|
+
* Checks case-insensitively and avoids being fooled by keywords inside strings/comments.
|
|
5
|
+
*/
|
|
6
|
+
static validate(query, blockedKeywords) {
|
|
7
|
+
if (blockedKeywords.length === 0)
|
|
8
|
+
return;
|
|
9
|
+
const queryWithoutComments = query
|
|
10
|
+
.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*|--.*/g, "$1")
|
|
11
|
+
.trim();
|
|
12
|
+
const queryWithoutLiterals = queryWithoutComments
|
|
13
|
+
.replace(/'(?:''|[^'])*'/g, "''")
|
|
14
|
+
.replace(/"(?:""|[^"])*"/g, '""')
|
|
15
|
+
.replace(/`(?:``|[^`])*`/g, "``");
|
|
16
|
+
const tokens = queryWithoutLiterals.toUpperCase().split(/\s+/);
|
|
17
|
+
for (const keyword of blockedKeywords) {
|
|
18
|
+
if (tokens.includes(keyword.toUpperCase())) {
|
|
19
|
+
throw new Error(`Forbidden keyword detected: ${keyword}. Statement blocked for security.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
static isSelectOnly(query) {
|
|
24
|
+
const q = query.trim().toUpperCase();
|
|
25
|
+
if (!q.startsWith("SELECT") && !q.startsWith("SHOW") && !q.startsWith("DESCRIBE") && !q.startsWith("EXPLAIN")) {
|
|
26
|
+
throw new Error("Only read-only queries (SELECT, SHOW, DESCRIBE, EXPLAIN) are allowed.");
|
|
27
|
+
}
|
|
28
|
+
this.validate(query, ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE", "REPLACE"]);
|
|
29
|
+
}
|
|
30
|
+
static isUpdateOnly(query) {
|
|
31
|
+
const q = query.trim().toUpperCase();
|
|
32
|
+
if (!q.startsWith("INSERT") && !q.startsWith("UPDATE") && !q.startsWith("REPLACE")) {
|
|
33
|
+
throw new Error("Only modification queries (INSERT, UPDATE, REPLACE) are allowed.");
|
|
34
|
+
}
|
|
35
|
+
this.validate(query, ["DELETE", "DROP", "ALTER", "TRUNCATE"]);
|
|
36
|
+
}
|
|
37
|
+
static isDeleteOnly(query) {
|
|
38
|
+
const q = query.trim().toUpperCase();
|
|
39
|
+
if (!q.startsWith("DELETE") && !q.startsWith("TRUNCATE")) {
|
|
40
|
+
throw new Error("Only removal queries (DELETE, TRUNCATE) are allowed.");
|
|
41
|
+
}
|
|
42
|
+
this.validate(query, ["DROP", "ALTER"]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export interface DbDriver {
|
|
2
|
+
name: string;
|
|
3
|
+
connect(): Promise<void>;
|
|
4
|
+
disconnect(): Promise<void>;
|
|
5
|
+
listTables(): Promise<TableInfo[]>;
|
|
6
|
+
describeTable(table: string): Promise<ColumnInfo[]>;
|
|
7
|
+
getTableSchema(table: string): Promise<string>;
|
|
8
|
+
getRelationships(tables?: string[]): Promise<Relationship[]>;
|
|
9
|
+
getTableStats(table: string): Promise<TableStats>;
|
|
10
|
+
sampleRows(table: string, limit?: number): Promise<any[]>;
|
|
11
|
+
executeQuery(query: string): Promise<QueryResult>;
|
|
12
|
+
executeUpdate(query: string): Promise<UpdateResult>;
|
|
13
|
+
executeDelete(query: string): Promise<DeleteResult>;
|
|
14
|
+
executeStatement(query: string): Promise<StatementResult>;
|
|
15
|
+
}
|
|
16
|
+
export interface DbConfig {
|
|
17
|
+
driver: 'mysql';
|
|
18
|
+
host: string;
|
|
19
|
+
port: number;
|
|
20
|
+
user: string;
|
|
21
|
+
password: string;
|
|
22
|
+
database: string;
|
|
23
|
+
poolSize?: number;
|
|
24
|
+
enableRunQuery?: boolean;
|
|
25
|
+
enableRunUpdateStatement?: boolean;
|
|
26
|
+
enableRunDeleteStatement?: boolean;
|
|
27
|
+
enableRunStatement?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface TableInfo {
|
|
30
|
+
name: string;
|
|
31
|
+
schema: string;
|
|
32
|
+
type: 'BASE TABLE' | 'VIEW';
|
|
33
|
+
comment?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ColumnInfo {
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
fullType: string;
|
|
39
|
+
nullable: boolean;
|
|
40
|
+
defaultValue?: string | null;
|
|
41
|
+
isPrimaryKey: boolean;
|
|
42
|
+
isForeignKey: boolean;
|
|
43
|
+
isAutoIncrement: boolean;
|
|
44
|
+
comment?: string;
|
|
45
|
+
extra?: string;
|
|
46
|
+
enumValues?: string[];
|
|
47
|
+
characterMaxLength?: number;
|
|
48
|
+
numericPrecision?: number;
|
|
49
|
+
numericScale?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface Relationship {
|
|
52
|
+
fromTable: string;
|
|
53
|
+
fromColumn: string;
|
|
54
|
+
toTable: string;
|
|
55
|
+
toColumn: string;
|
|
56
|
+
constraintName: string;
|
|
57
|
+
}
|
|
58
|
+
export interface TableStats {
|
|
59
|
+
rowCount: number;
|
|
60
|
+
dataLength?: number;
|
|
61
|
+
indexLength?: number;
|
|
62
|
+
createTime?: Date;
|
|
63
|
+
updateTime?: Date;
|
|
64
|
+
}
|
|
65
|
+
export interface QueryResult {
|
|
66
|
+
rows: any[];
|
|
67
|
+
fields?: any[];
|
|
68
|
+
}
|
|
69
|
+
export interface UpdateResult {
|
|
70
|
+
affectedRows: number;
|
|
71
|
+
insertId?: number | string;
|
|
72
|
+
message?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface DeleteResult {
|
|
75
|
+
affectedRows: number;
|
|
76
|
+
message?: string;
|
|
77
|
+
}
|
|
78
|
+
export interface StatementResult {
|
|
79
|
+
success: boolean;
|
|
80
|
+
message?: string;
|
|
81
|
+
data?: any;
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/mcp-server.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { AuthContext } from "./api-explorer/auth/index.js";
|
|
2
|
+
import type { DbConfig } from "./db-explorer/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* The main MCP Server implementation that coordinates the OpenAPI parser,
|
|
5
|
+
* tool generation, and API execution.
|
|
6
|
+
*
|
|
7
|
+
* It registers meta-tools that allow LLMs to:
|
|
8
|
+
* 1. Discover API structure (tags, endpoints, schemas).
|
|
9
|
+
* 2. Execute calls to any endpoint with dynamic authentication.
|
|
10
|
+
* 3. Impersonate users via the set_identity tool.
|
|
11
|
+
*/
|
|
12
|
+
export interface MCPServerOptions {
|
|
13
|
+
specPath: string;
|
|
14
|
+
authContext?: AuthContext;
|
|
15
|
+
dbConfig?: DbConfig;
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
18
|
* The main MCP Server implementation that coordinates the OpenAPI parser,
|
|
4
19
|
* tool generation, and API execution.
|
|
@@ -9,17 +24,18 @@ import { AuthContext } from "./api-explorer/auth/index.js";
|
|
|
9
24
|
* 3. Impersonate users via the set_identity tool.
|
|
10
25
|
*/
|
|
11
26
|
export declare class MCPServer {
|
|
12
|
-
private specPath;
|
|
13
27
|
private server;
|
|
14
28
|
private parser;
|
|
15
29
|
private toolGenerator;
|
|
16
30
|
private apiExecutor;
|
|
17
31
|
private authContext;
|
|
32
|
+
private dbExecutor?;
|
|
33
|
+
private dbToolGenerator?;
|
|
34
|
+
private specPath;
|
|
18
35
|
/**
|
|
19
|
-
* @param
|
|
20
|
-
* @param authContext - Optional custom AuthContext. If not provided, creates one from environment variables.
|
|
36
|
+
* @param options - Configuration options for the MCP Server.
|
|
21
37
|
*/
|
|
22
|
-
constructor(
|
|
38
|
+
constructor(options: MCPServerOptions);
|
|
23
39
|
private initParser;
|
|
24
40
|
private registerTools;
|
|
25
41
|
/**
|
package/dist/mcp-server.js
CHANGED
|
@@ -5,6 +5,8 @@ import { OpenAPIParser } from "./api-explorer/openapi-parser.js";
|
|
|
5
5
|
import { ToolGenerator } from "./api-explorer/tool-generator.js";
|
|
6
6
|
import { ApiExecutor } from "./api-explorer/api-executor.js";
|
|
7
7
|
import { createAuthContextFromEnv } from "./api-explorer/auth/index.js";
|
|
8
|
+
import { DbExecutor, DbToolGenerator } from "./db-explorer/index.js";
|
|
9
|
+
import { MySQLDriver } from "./db-explorer/drivers/mysql-driver.js";
|
|
8
10
|
/**
|
|
9
11
|
* The main MCP Server implementation that coordinates the OpenAPI parser,
|
|
10
12
|
* tool generation, and API execution.
|
|
@@ -15,22 +17,28 @@ import { createAuthContextFromEnv } from "./api-explorer/auth/index.js";
|
|
|
15
17
|
* 3. Impersonate users via the set_identity tool.
|
|
16
18
|
*/
|
|
17
19
|
export class MCPServer {
|
|
18
|
-
specPath;
|
|
19
20
|
server;
|
|
20
21
|
parser;
|
|
21
22
|
toolGenerator;
|
|
22
23
|
apiExecutor;
|
|
23
24
|
authContext;
|
|
25
|
+
dbExecutor;
|
|
26
|
+
dbToolGenerator;
|
|
27
|
+
specPath;
|
|
24
28
|
/**
|
|
25
|
-
* @param
|
|
26
|
-
* @param authContext - Optional custom AuthContext. If not provided, creates one from environment variables.
|
|
29
|
+
* @param options - Configuration options for the MCP Server.
|
|
27
30
|
*/
|
|
28
|
-
constructor(
|
|
29
|
-
this.specPath = specPath;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.specPath = options.specPath;
|
|
30
33
|
this.parser = new OpenAPIParser();
|
|
31
34
|
this.toolGenerator = new ToolGenerator(this.parser);
|
|
32
|
-
this.authContext = authContext || createAuthContextFromEnv();
|
|
35
|
+
this.authContext = options.authContext || createAuthContextFromEnv();
|
|
33
36
|
this.apiExecutor = new ApiExecutor(undefined, this.authContext);
|
|
37
|
+
if (options.dbConfig) {
|
|
38
|
+
const driver = new MySQLDriver(options.dbConfig);
|
|
39
|
+
this.dbExecutor = new DbExecutor(driver, options.dbConfig);
|
|
40
|
+
this.dbToolGenerator = new DbToolGenerator(this.dbExecutor);
|
|
41
|
+
}
|
|
34
42
|
this.server = new McpServer({
|
|
35
43
|
name: "project-mcp-server",
|
|
36
44
|
version: "1.0.0",
|
|
@@ -40,6 +48,9 @@ export class MCPServer {
|
|
|
40
48
|
try {
|
|
41
49
|
await this.parser.loadSpec(this.specPath);
|
|
42
50
|
console.error(`Loaded OpenAPI spec from ${this.specPath}`);
|
|
51
|
+
if (this.dbExecutor) {
|
|
52
|
+
await this.dbExecutor.connect();
|
|
53
|
+
}
|
|
43
54
|
this.registerTools();
|
|
44
55
|
}
|
|
45
56
|
catch (error) {
|
|
@@ -56,7 +67,7 @@ export class MCPServer {
|
|
|
56
67
|
}, async (args) => {
|
|
57
68
|
try {
|
|
58
69
|
let result;
|
|
59
|
-
if (name === "
|
|
70
|
+
if (name === "api_call_endpoint") {
|
|
60
71
|
result = await this.apiExecutor.callEndpoint(args);
|
|
61
72
|
}
|
|
62
73
|
else {
|
|
@@ -74,6 +85,29 @@ export class MCPServer {
|
|
|
74
85
|
}
|
|
75
86
|
});
|
|
76
87
|
});
|
|
88
|
+
if (this.dbToolGenerator) {
|
|
89
|
+
const dbDefinitions = this.dbToolGenerator.getToolDefinitions();
|
|
90
|
+
Object.entries(dbDefinitions).forEach(([name, def]) => {
|
|
91
|
+
const toolDef = def;
|
|
92
|
+
this.server.registerTool(name, {
|
|
93
|
+
description: toolDef.description,
|
|
94
|
+
inputSchema: toolDef.inputSchema,
|
|
95
|
+
}, async (args) => {
|
|
96
|
+
try {
|
|
97
|
+
const result = await this.dbToolGenerator.handleToolCall(name, args);
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
77
111
|
this.server.registerTool("set_identity", {
|
|
78
112
|
description: "Set the identity (identifier) for future API requests. Requires an 'identity' auth strategy to be active.",
|
|
79
113
|
inputSchema: z.object({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamp29/project-mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A ModelContextProtocol server to let agents discover your project, such as APIs (using OpenAPI) or other resources.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -29,19 +29,27 @@
|
|
|
29
29
|
"types": "./dist/api-explorer/index.d.ts",
|
|
30
30
|
"import": "./dist/api-explorer/index.js",
|
|
31
31
|
"default": "./dist/api-explorer/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./db-explorer": {
|
|
34
|
+
"types": "./dist/db-explorer/index.d.ts",
|
|
35
|
+
"import": "./dist/db-explorer/index.js",
|
|
36
|
+
"default": "./dist/db-explorer/index.js"
|
|
32
37
|
}
|
|
33
38
|
},
|
|
34
39
|
"scripts": {
|
|
35
40
|
"build": "tsc",
|
|
36
41
|
"start": "node dist/cli.js",
|
|
37
42
|
"dev": "ts-node src/cli.ts",
|
|
38
|
-
"
|
|
43
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/cli.js",
|
|
44
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
45
|
+
"publish": "npm publish --access public"
|
|
39
46
|
},
|
|
40
47
|
"dependencies": {
|
|
41
48
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
42
49
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
43
50
|
"axios": "^1.13.2",
|
|
44
|
-
"dotenv": "^
|
|
51
|
+
"dotenv": "^16.4.5",
|
|
52
|
+
"mysql2": "^3.16.1",
|
|
45
53
|
"openapi-types": "^12.1.3",
|
|
46
54
|
"zod": "^4.3.5"
|
|
47
55
|
},
|