@bytebase/dbhub 0.1.0 → 0.1.2
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 +34 -33
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1489 -13
- package/dist/resources/employee-sqlite/employee.sql +117 -0
- package/dist/resources/employee-sqlite/load_department.sql +10 -0
- package/dist/resources/employee-sqlite/load_dept_emp.sql +1103 -0
- package/dist/resources/employee-sqlite/load_dept_manager.sql +17 -0
- package/dist/resources/employee-sqlite/load_employee.sql +1000 -0
- package/dist/resources/employee-sqlite/load_salary1.sql +9488 -0
- package/dist/resources/employee-sqlite/load_title.sql +1470 -0
- package/dist/resources/employee-sqlite/object.sql +74 -0
- package/dist/resources/employee-sqlite/show_elapsed.sql +4 -0
- package/dist/resources/employee-sqlite/test_employee_md5.sql +119 -0
- package/package.json +5 -4
- package/dist/config/demo-loader.js +0 -45
- package/dist/config/env.js +0 -146
- package/dist/connectors/interface.js +0 -55
- package/dist/connectors/manager.js +0 -91
- package/dist/connectors/mysql/index.js +0 -169
- package/dist/connectors/postgres/index.js +0 -172
- package/dist/connectors/sqlite/index.js +0 -208
- package/dist/connectors/sqlserver/index.js +0 -186
- package/dist/prompts/db-explainer.js +0 -201
- package/dist/prompts/index.js +0 -11
- package/dist/prompts/sql-generator.js +0 -114
- package/dist/resources/index.js +0 -12
- package/dist/resources/schema.js +0 -36
- package/dist/resources/tables.js +0 -17
- package/dist/server.js +0 -140
- package/dist/tools/index.js +0 -11
- package/dist/tools/list-connectors.js +0 -43
- package/dist/tools/run-query.js +0 -32
- package/dist/utils/response-formatter.js +0 -109
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import sql from 'mssql';
|
|
2
|
-
import { ConnectorRegistry } from '../interface.js';
|
|
3
|
-
/**
|
|
4
|
-
* SQL Server DSN parser
|
|
5
|
-
* Expected format: mssql://username:password@host:port/database
|
|
6
|
-
*/
|
|
7
|
-
export class SQLServerDSNParser {
|
|
8
|
-
parse(dsn) {
|
|
9
|
-
// Remove the protocol prefix
|
|
10
|
-
if (!this.isValidDSN(dsn)) {
|
|
11
|
-
throw new Error('Invalid SQL Server DSN format. Expected: mssql://username:password@host:port/database');
|
|
12
|
-
}
|
|
13
|
-
// Parse the DSN
|
|
14
|
-
const url = new URL(dsn);
|
|
15
|
-
const host = url.hostname;
|
|
16
|
-
const port = url.port ? parseInt(url.port, 10) : 1433; // Default SQL Server port
|
|
17
|
-
const database = url.pathname.substring(1); // Remove leading slash
|
|
18
|
-
const user = url.username;
|
|
19
|
-
const password = url.password;
|
|
20
|
-
// Parse additional options from query parameters
|
|
21
|
-
const options = {};
|
|
22
|
-
for (const [key, value] of url.searchParams.entries()) {
|
|
23
|
-
if (key === 'encrypt') {
|
|
24
|
-
options.encrypt = value;
|
|
25
|
-
}
|
|
26
|
-
else if (key === 'trustServerCertificate') {
|
|
27
|
-
options.trustServerCertificate = value === 'true';
|
|
28
|
-
}
|
|
29
|
-
else if (key === 'connectTimeout') {
|
|
30
|
-
options.connectTimeout = parseInt(value, 10);
|
|
31
|
-
}
|
|
32
|
-
else if (key === 'requestTimeout') {
|
|
33
|
-
options.requestTimeout = parseInt(value, 10);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// Construct and return the config
|
|
37
|
-
return {
|
|
38
|
-
user,
|
|
39
|
-
password,
|
|
40
|
-
server: host,
|
|
41
|
-
port,
|
|
42
|
-
database,
|
|
43
|
-
options: {
|
|
44
|
-
encrypt: options.encrypt ?? true, // Default to encrypted connection
|
|
45
|
-
trustServerCertificate: options.trustServerCertificate === true, // Need explicit conversion to boolean
|
|
46
|
-
connectTimeout: options.connectTimeout ?? 15000,
|
|
47
|
-
requestTimeout: options.requestTimeout ?? 15000,
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
getSampleDSN() {
|
|
52
|
-
return 'mssql://username:password@localhost:1433/database?encrypt=true';
|
|
53
|
-
}
|
|
54
|
-
isValidDSN(dsn) {
|
|
55
|
-
try {
|
|
56
|
-
const url = new URL(dsn);
|
|
57
|
-
return url.protocol === 'mssql:';
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* SQL Server connector
|
|
66
|
-
*/
|
|
67
|
-
export class SQLServerConnector {
|
|
68
|
-
constructor() {
|
|
69
|
-
this.id = 'sqlserver';
|
|
70
|
-
this.name = 'SQL Server';
|
|
71
|
-
this.dsnParser = new SQLServerDSNParser();
|
|
72
|
-
}
|
|
73
|
-
async connect(dsn) {
|
|
74
|
-
try {
|
|
75
|
-
this.config = this.dsnParser.parse(dsn);
|
|
76
|
-
if (!this.config.options) {
|
|
77
|
-
this.config.options = {};
|
|
78
|
-
}
|
|
79
|
-
this.connection = await new sql.ConnectionPool(this.config).connect();
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
throw error;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
async disconnect() {
|
|
86
|
-
if (this.connection) {
|
|
87
|
-
await this.connection.close();
|
|
88
|
-
this.connection = undefined;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async getTables() {
|
|
92
|
-
if (!this.connection) {
|
|
93
|
-
throw new Error('Not connected to SQL Server database');
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
const result = await this.connection.request().query(`
|
|
97
|
-
SELECT TABLE_NAME
|
|
98
|
-
FROM INFORMATION_SCHEMA.TABLES
|
|
99
|
-
ORDER BY TABLE_NAME
|
|
100
|
-
`);
|
|
101
|
-
return result.recordset.map((row) => row.TABLE_NAME);
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
throw new Error(`Failed to get tables: ${error.message}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
async tableExists(tableName) {
|
|
108
|
-
if (!this.connection) {
|
|
109
|
-
throw new Error('Not connected to SQL Server database');
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
const result = await this.connection.request()
|
|
113
|
-
.input('tableName', sql.VarChar, tableName)
|
|
114
|
-
.query(`
|
|
115
|
-
SELECT COUNT(*) as count
|
|
116
|
-
FROM INFORMATION_SCHEMA.TABLES
|
|
117
|
-
WHERE TABLE_NAME = @tableName
|
|
118
|
-
`);
|
|
119
|
-
return result.recordset[0].count > 0;
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
throw new Error(`Failed to check if table exists: ${error.message}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
async getTableSchema(tableName) {
|
|
126
|
-
if (!this.connection) {
|
|
127
|
-
throw new Error('Not connected to SQL Server database');
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
const result = await this.connection.request()
|
|
131
|
-
.input('tableName', sql.VarChar, tableName)
|
|
132
|
-
.query(`
|
|
133
|
-
SELECT
|
|
134
|
-
COLUMN_NAME as column_name,
|
|
135
|
-
DATA_TYPE as data_type,
|
|
136
|
-
IS_NULLABLE as is_nullable,
|
|
137
|
-
COLUMN_DEFAULT as column_default
|
|
138
|
-
FROM INFORMATION_SCHEMA.COLUMNS
|
|
139
|
-
WHERE TABLE_NAME = @tableName
|
|
140
|
-
ORDER BY ORDINAL_POSITION
|
|
141
|
-
`);
|
|
142
|
-
return result.recordset;
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async executeQuery(query) {
|
|
149
|
-
if (!this.connection) {
|
|
150
|
-
throw new Error('Not connected to SQL Server database');
|
|
151
|
-
}
|
|
152
|
-
const safetyCheck = this.validateQuery(query);
|
|
153
|
-
if (!safetyCheck.isValid) {
|
|
154
|
-
throw new Error(safetyCheck.message || "Query validation failed");
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const result = await this.connection.request().query(query);
|
|
158
|
-
return {
|
|
159
|
-
rows: result.recordset || [],
|
|
160
|
-
fields: result.recordset && result.recordset.length > 0
|
|
161
|
-
? Object.keys(result.recordset[0]).map(key => ({
|
|
162
|
-
name: key,
|
|
163
|
-
}))
|
|
164
|
-
: [],
|
|
165
|
-
rowCount: result.rowsAffected[0] || 0,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
throw new Error(`Failed to execute query: ${error.message}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
validateQuery(query) {
|
|
173
|
-
// Basic check to prevent non-SELECT queries
|
|
174
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
175
|
-
if (!normalizedQuery.startsWith('select')) {
|
|
176
|
-
return {
|
|
177
|
-
isValid: false,
|
|
178
|
-
message: "Only SELECT queries are allowed for security reasons."
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
return { isValid: true };
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// Create and register the connector
|
|
185
|
-
const sqlServerConnector = new SQLServerConnector();
|
|
186
|
-
ConnectorRegistry.register(sqlServerConnector);
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { ConnectorManager } from '../connectors/manager.js';
|
|
3
|
-
import { formatPromptSuccessResponse, formatPromptErrorResponse } from '../utils/response-formatter.js';
|
|
4
|
-
// Schema for database explainer prompt
|
|
5
|
-
export const dbExplainerSchema = {
|
|
6
|
-
target: z.string().describe("Name of the table, column, or database to explain")
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Database Explainer Prompt Handler
|
|
10
|
-
* Provides explanations about database elements
|
|
11
|
-
*/
|
|
12
|
-
export async function dbExplainerPromptHandler({ target }, _extra) {
|
|
13
|
-
try {
|
|
14
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
15
|
-
// First check if this is a table name
|
|
16
|
-
const tables = await connector.getTables();
|
|
17
|
-
const normalizedTarget = target.toLowerCase();
|
|
18
|
-
// Check if target matches a table
|
|
19
|
-
const matchingTable = tables.find(t => t.toLowerCase() === normalizedTarget);
|
|
20
|
-
if (matchingTable) {
|
|
21
|
-
// Explain the table
|
|
22
|
-
const columns = await connector.getTableSchema(matchingTable);
|
|
23
|
-
// Create a table structure description
|
|
24
|
-
const tableDescription = `Table: ${matchingTable}
|
|
25
|
-
|
|
26
|
-
Structure:
|
|
27
|
-
${columns.map(col => `- ${col.column_name} (${col.data_type})${col.is_nullable === 'YES' ? ', nullable' : ''}${col.column_default ? `, default: ${col.column_default}` : ''}`).join('\n')}
|
|
28
|
-
|
|
29
|
-
Purpose:
|
|
30
|
-
This table appears to store ${determineTablePurpose(matchingTable, columns)}
|
|
31
|
-
|
|
32
|
-
Relationships:
|
|
33
|
-
${determineRelationships(matchingTable, columns)}`;
|
|
34
|
-
return formatPromptSuccessResponse(tableDescription);
|
|
35
|
-
}
|
|
36
|
-
// Check if target is a table.column format
|
|
37
|
-
if (target.includes('.')) {
|
|
38
|
-
const [tableName, columnName] = target.split('.');
|
|
39
|
-
if (tables.find(t => t.toLowerCase() === tableName.toLowerCase())) {
|
|
40
|
-
// Get column info
|
|
41
|
-
const columns = await connector.getTableSchema(tableName);
|
|
42
|
-
const column = columns.find(c => c.column_name.toLowerCase() === columnName.toLowerCase());
|
|
43
|
-
if (column) {
|
|
44
|
-
const columnDescription = `Column: ${tableName}.${column.column_name}
|
|
45
|
-
|
|
46
|
-
Type: ${column.data_type}
|
|
47
|
-
Nullable: ${column.is_nullable === 'YES' ? 'Yes' : 'No'}
|
|
48
|
-
Default: ${column.column_default || 'None'}
|
|
49
|
-
|
|
50
|
-
Purpose:
|
|
51
|
-
${determineColumnPurpose(column.column_name, column.data_type)}`;
|
|
52
|
-
return formatPromptSuccessResponse(columnDescription);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// If target is not a specific table or column, provide database overview
|
|
57
|
-
// Determine if 'database' or similar term is in the target
|
|
58
|
-
if (['database', 'db', 'schema', 'overview', 'all'].includes(normalizedTarget)) {
|
|
59
|
-
let dbOverview = `Database Overview
|
|
60
|
-
|
|
61
|
-
Tables: ${tables.length}
|
|
62
|
-
${tables.map(t => `- ${t}`).join('\n')}
|
|
63
|
-
|
|
64
|
-
This database ${describeDatabasePurpose(tables)}`;
|
|
65
|
-
return formatPromptSuccessResponse(dbOverview);
|
|
66
|
-
}
|
|
67
|
-
// If no match is found but the target could be a partial match
|
|
68
|
-
const possibleTableMatches = tables.filter(t => t.toLowerCase().includes(normalizedTarget) ||
|
|
69
|
-
normalizedTarget.includes(t.toLowerCase()));
|
|
70
|
-
if (possibleTableMatches.length > 0) {
|
|
71
|
-
return formatPromptSuccessResponse(`Could not find exact match for "${target}". Did you mean one of these tables?\n\n${possibleTableMatches.join('\n')}`);
|
|
72
|
-
}
|
|
73
|
-
// No match found
|
|
74
|
-
return formatPromptErrorResponse(`Could not find a table, column, or database feature matching "${target}"`, "NOT_FOUND");
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
return formatPromptErrorResponse(`Error explaining database: ${error.message}`, "EXPLANATION_ERROR");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Helper function to make an educated guess about the purpose of a table
|
|
82
|
-
* based on its name and columns
|
|
83
|
-
*/
|
|
84
|
-
function determineTablePurpose(tableName, columns) {
|
|
85
|
-
const lowerTableName = tableName.toLowerCase();
|
|
86
|
-
const columnNames = columns.map(c => c.column_name.toLowerCase());
|
|
87
|
-
// Check for common patterns
|
|
88
|
-
if (lowerTableName.includes('user') || columnNames.includes('username') || columnNames.includes('email')) {
|
|
89
|
-
return 'user information and profiles';
|
|
90
|
-
}
|
|
91
|
-
if (lowerTableName.includes('order') || lowerTableName.includes('purchase')) {
|
|
92
|
-
return 'order or purchase transactions';
|
|
93
|
-
}
|
|
94
|
-
if (lowerTableName.includes('product') || lowerTableName.includes('item')) {
|
|
95
|
-
return 'product or item information';
|
|
96
|
-
}
|
|
97
|
-
if (lowerTableName.includes('log') || columnNames.includes('timestamp')) {
|
|
98
|
-
return 'event or activity logs';
|
|
99
|
-
}
|
|
100
|
-
if (columnNames.includes('created_at') && columnNames.includes('updated_at')) {
|
|
101
|
-
return 'tracking timestamped data records';
|
|
102
|
-
}
|
|
103
|
-
// Default
|
|
104
|
-
return 'data related to ' + tableName;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Helper function to determine potential relationships based on column names
|
|
108
|
-
*/
|
|
109
|
-
function determineRelationships(tableName, columns) {
|
|
110
|
-
const potentialRelationships = [];
|
|
111
|
-
// Look for _id columns which often indicate foreign keys
|
|
112
|
-
const idColumns = columns.filter(c => c.column_name.toLowerCase().endsWith('_id') &&
|
|
113
|
-
!c.column_name.toLowerCase().startsWith(tableName.toLowerCase()));
|
|
114
|
-
if (idColumns.length > 0) {
|
|
115
|
-
idColumns.forEach(col => {
|
|
116
|
-
const referencedTable = col.column_name.toLowerCase().replace('_id', '');
|
|
117
|
-
potentialRelationships.push(`May have a relationship with the "${referencedTable}" table (via ${col.column_name})`);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
// Check if the table itself might be referenced by others
|
|
121
|
-
if (columns.some(c => c.column_name.toLowerCase() === 'id')) {
|
|
122
|
-
potentialRelationships.push(`May be referenced by other tables as "${tableName.toLowerCase()}_id"`);
|
|
123
|
-
}
|
|
124
|
-
return potentialRelationships.length > 0
|
|
125
|
-
? potentialRelationships.join('\n')
|
|
126
|
-
: 'No obvious relationships identified based on column names';
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Helper function to determine the purpose of a column based on naming patterns
|
|
130
|
-
*/
|
|
131
|
-
function determineColumnPurpose(columnName, dataType) {
|
|
132
|
-
const lowerColumnName = columnName.toLowerCase();
|
|
133
|
-
if (lowerColumnName === 'id') {
|
|
134
|
-
return 'Primary identifier for records in this table';
|
|
135
|
-
}
|
|
136
|
-
if (lowerColumnName.endsWith('_id')) {
|
|
137
|
-
const referencedTable = lowerColumnName.replace('_id', '');
|
|
138
|
-
return `Foreign key reference to the "${referencedTable}" table`;
|
|
139
|
-
}
|
|
140
|
-
if (lowerColumnName.includes('name')) {
|
|
141
|
-
return 'Stores name information';
|
|
142
|
-
}
|
|
143
|
-
if (lowerColumnName.includes('email')) {
|
|
144
|
-
return 'Stores email address information';
|
|
145
|
-
}
|
|
146
|
-
if (lowerColumnName.includes('password') || lowerColumnName.includes('hash')) {
|
|
147
|
-
return 'Stores security credential information (likely hashed)';
|
|
148
|
-
}
|
|
149
|
-
if (lowerColumnName === 'created_at' || lowerColumnName === 'created_on') {
|
|
150
|
-
return 'Timestamp for when the record was created';
|
|
151
|
-
}
|
|
152
|
-
if (lowerColumnName === 'updated_at' || lowerColumnName === 'modified_at') {
|
|
153
|
-
return 'Timestamp for when the record was last updated';
|
|
154
|
-
}
|
|
155
|
-
if (lowerColumnName.includes('date') || lowerColumnName.includes('time')) {
|
|
156
|
-
return 'Stores date or time information';
|
|
157
|
-
}
|
|
158
|
-
if (lowerColumnName.includes('price') || lowerColumnName.includes('cost') || lowerColumnName.includes('amount')) {
|
|
159
|
-
return 'Stores monetary value information';
|
|
160
|
-
}
|
|
161
|
-
// Data type specific purposes
|
|
162
|
-
if (dataType.includes('boolean')) {
|
|
163
|
-
return 'Stores a true/false flag';
|
|
164
|
-
}
|
|
165
|
-
if (dataType.includes('json')) {
|
|
166
|
-
return 'Stores structured JSON data';
|
|
167
|
-
}
|
|
168
|
-
if (dataType.includes('text') || dataType.includes('varchar') || dataType.includes('char')) {
|
|
169
|
-
return 'Stores text information';
|
|
170
|
-
}
|
|
171
|
-
// Default
|
|
172
|
-
return `Stores ${dataType} data`;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Helper function to describe the overall database purpose based on tables
|
|
176
|
-
*/
|
|
177
|
-
function describeDatabasePurpose(tables) {
|
|
178
|
-
const tableNames = tables.map(t => t.toLowerCase());
|
|
179
|
-
if (tableNames.some(t => t.includes('user')) &&
|
|
180
|
-
tableNames.some(t => t.includes('order'))) {
|
|
181
|
-
return 'appears to be an e-commerce or customer order management system';
|
|
182
|
-
}
|
|
183
|
-
if (tableNames.some(t => t.includes('patient')) ||
|
|
184
|
-
tableNames.some(t => t.includes('medical'))) {
|
|
185
|
-
return 'appears to be related to healthcare or medical record management';
|
|
186
|
-
}
|
|
187
|
-
if (tableNames.some(t => t.includes('student')) ||
|
|
188
|
-
tableNames.some(t => t.includes('course'))) {
|
|
189
|
-
return 'appears to be related to education or student management';
|
|
190
|
-
}
|
|
191
|
-
if (tableNames.some(t => t.includes('employee')) ||
|
|
192
|
-
tableNames.some(t => t.includes('payroll'))) {
|
|
193
|
-
return 'appears to be related to HR or employee management';
|
|
194
|
-
}
|
|
195
|
-
if (tableNames.some(t => t.includes('inventory')) ||
|
|
196
|
-
tableNames.some(t => t.includes('stock'))) {
|
|
197
|
-
return 'appears to be related to inventory or stock management';
|
|
198
|
-
}
|
|
199
|
-
// Default
|
|
200
|
-
return 'contains multiple tables that store related information';
|
|
201
|
-
}
|
package/dist/prompts/index.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { sqlGeneratorPromptHandler, sqlGeneratorSchema } from './sql-generator.js';
|
|
2
|
-
import { dbExplainerPromptHandler, dbExplainerSchema } from './db-explainer.js';
|
|
3
|
-
/**
|
|
4
|
-
* Register all prompt handlers with the MCP server
|
|
5
|
-
*/
|
|
6
|
-
export function registerPrompts(server) {
|
|
7
|
-
// Register SQL Generator prompt
|
|
8
|
-
server.prompt("generate_sql", "Generate SQL queries from natural language descriptions", sqlGeneratorSchema, sqlGeneratorPromptHandler);
|
|
9
|
-
// Register Database Explainer prompt
|
|
10
|
-
server.prompt("explain_db", "Get explanations about database tables, columns, and structures", dbExplainerSchema, dbExplainerPromptHandler);
|
|
11
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { ConnectorManager } from '../connectors/manager.js';
|
|
3
|
-
import { formatPromptSuccessResponse, formatPromptErrorResponse } from '../utils/response-formatter.js';
|
|
4
|
-
// Schema for SQL generator prompt
|
|
5
|
-
export const sqlGeneratorSchema = {
|
|
6
|
-
description: z.string().describe("Natural language description of the SQL query to generate"),
|
|
7
|
-
dialect: z.enum(["postgres", "sqlite"]).optional().describe("SQL dialect to use (optional)")
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* SQL Generator Prompt Handler
|
|
11
|
-
* Generates SQL queries from natural language descriptions
|
|
12
|
-
*/
|
|
13
|
-
export async function sqlGeneratorPromptHandler({ description, dialect }, _extra) {
|
|
14
|
-
try {
|
|
15
|
-
// Get current connector to determine dialect if not specified
|
|
16
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
17
|
-
// Determine SQL dialect from connector if not explicitly provided
|
|
18
|
-
const sqlDialect = dialect ||
|
|
19
|
-
(connector.id === 'postgres' ? 'postgres' :
|
|
20
|
-
(connector.id === 'sqlite' ? 'sqlite' : 'postgres'));
|
|
21
|
-
// Get schema information to help with table/column references
|
|
22
|
-
const tables = await connector.getTables();
|
|
23
|
-
const tableSchemas = await Promise.all(tables.map(async (table) => {
|
|
24
|
-
try {
|
|
25
|
-
const columns = await connector.getTableSchema(table);
|
|
26
|
-
return {
|
|
27
|
-
table,
|
|
28
|
-
columns: columns.map(col => ({
|
|
29
|
-
name: col.column_name,
|
|
30
|
-
type: col.data_type
|
|
31
|
-
}))
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
// Skip tables we can't access
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}));
|
|
39
|
-
// Filter out null entries (tables we couldn't access)
|
|
40
|
-
const accessibleSchemas = tableSchemas.filter(schema => schema !== null);
|
|
41
|
-
// Generate a schema description for context
|
|
42
|
-
const schemaContext = accessibleSchemas.length > 0
|
|
43
|
-
? `Available tables and their columns:\n${accessibleSchemas.map(schema => `- ${schema.table}: ${schema.columns.map(col => `${col.name} (${col.type})`).join(', ')}`).join('\n')}`
|
|
44
|
-
: 'No schema information available.';
|
|
45
|
-
// Example queries for the given dialect to use as reference
|
|
46
|
-
const dialectExamples = {
|
|
47
|
-
postgres: [
|
|
48
|
-
"SELECT * FROM users WHERE created_at > NOW() - INTERVAL '1 day'",
|
|
49
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
50
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
51
|
-
],
|
|
52
|
-
sqlite: [
|
|
53
|
-
"SELECT * FROM users WHERE created_at > datetime('now', '-1 day')",
|
|
54
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
55
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
56
|
-
]
|
|
57
|
-
};
|
|
58
|
-
// Build a prompt that would help generate the SQL
|
|
59
|
-
// In a real implementation, this would call an AI model
|
|
60
|
-
const prompt = `
|
|
61
|
-
Generate a ${sqlDialect} SQL query based on this description: "${description}"
|
|
62
|
-
|
|
63
|
-
${schemaContext}
|
|
64
|
-
|
|
65
|
-
The query should:
|
|
66
|
-
1. Be written for ${sqlDialect} dialect
|
|
67
|
-
2. Use only the available tables and columns
|
|
68
|
-
3. Prioritize readability
|
|
69
|
-
4. Include appropriate comments
|
|
70
|
-
5. Be compatible with ${sqlDialect} syntax
|
|
71
|
-
`;
|
|
72
|
-
// In a real implementation, this would be the result from an AI model call
|
|
73
|
-
// For this demo, we'll generate a simple SQL query based on the description
|
|
74
|
-
let generatedSQL;
|
|
75
|
-
// Very simple pattern matching for demo purposes
|
|
76
|
-
// In a real implementation, this would use a language model
|
|
77
|
-
if (description.toLowerCase().includes('count')) {
|
|
78
|
-
generatedSQL = `-- Count query generated from: "${description}"
|
|
79
|
-
SELECT COUNT(*) AS count
|
|
80
|
-
FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0].table : 'table_name'};`;
|
|
81
|
-
}
|
|
82
|
-
else if (description.toLowerCase().includes('average') || description.toLowerCase().includes('avg')) {
|
|
83
|
-
const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : 'table_name';
|
|
84
|
-
const numericColumn = accessibleSchemas.length > 0
|
|
85
|
-
? accessibleSchemas[0].columns.find(col => ['int', 'numeric', 'decimal', 'float', 'real', 'double'].some(t => col.type.includes(t)))?.name || 'numeric_column'
|
|
86
|
-
: 'numeric_column';
|
|
87
|
-
generatedSQL = `-- Average query generated from: "${description}"
|
|
88
|
-
SELECT AVG(${numericColumn}) AS average
|
|
89
|
-
FROM ${table};`;
|
|
90
|
-
}
|
|
91
|
-
else if (description.toLowerCase().includes('join')) {
|
|
92
|
-
generatedSQL = `-- Join query generated from: "${description}"
|
|
93
|
-
SELECT t1.*, t2.*
|
|
94
|
-
FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : 'table1'} t1
|
|
95
|
-
JOIN ${accessibleSchemas.length > 1 ? accessibleSchemas[1]?.table : 'table2'} t2
|
|
96
|
-
ON t1.id = t2.${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : 'table1'}_id;`;
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
// Default to a simple SELECT
|
|
100
|
-
const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : 'table_name';
|
|
101
|
-
generatedSQL = `-- Query generated from: "${description}"
|
|
102
|
-
SELECT *
|
|
103
|
-
FROM ${table}
|
|
104
|
-
LIMIT 10;`;
|
|
105
|
-
}
|
|
106
|
-
// Return the generated SQL with explanations
|
|
107
|
-
return formatPromptSuccessResponse(generatedSQL,
|
|
108
|
-
// Add references to example queries that could help
|
|
109
|
-
dialectExamples[sqlDialect]);
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
return formatPromptErrorResponse(`Failed to generate SQL: ${error.message}`, "SQL_GENERATION_ERROR");
|
|
113
|
-
}
|
|
114
|
-
}
|
package/dist/resources/index.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { tablesResourceHandler } from './tables.js';
|
|
3
|
-
import { schemaResourceHandler } from './schema.js';
|
|
4
|
-
/**
|
|
5
|
-
* Register all resource handlers with the MCP server
|
|
6
|
-
*/
|
|
7
|
-
export function registerResources(server) {
|
|
8
|
-
// Resource for listing all tables
|
|
9
|
-
server.resource("tables", "db://tables", tablesResourceHandler);
|
|
10
|
-
// Resource for getting table schema
|
|
11
|
-
server.resource("schema", new ResourceTemplate("db://schema/{tableName}", { list: undefined }), schemaResourceHandler);
|
|
12
|
-
}
|
package/dist/resources/schema.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { ConnectorManager } from '../connectors/manager.js';
|
|
2
|
-
import { createResourceSuccessResponse, createResourceErrorResponse } from '../utils/response-formatter.js';
|
|
3
|
-
/**
|
|
4
|
-
* Schema resource handler
|
|
5
|
-
* Returns schema information for a specific table
|
|
6
|
-
*/
|
|
7
|
-
export async function schemaResourceHandler(uri, variables, _extra) {
|
|
8
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
9
|
-
// Handle tableName which could be a string or string array from URL template
|
|
10
|
-
const tableName = Array.isArray(variables.tableName)
|
|
11
|
-
? variables.tableName[0]
|
|
12
|
-
: variables.tableName;
|
|
13
|
-
try {
|
|
14
|
-
// If table doesn't exist, getTableSchema will throw an error
|
|
15
|
-
const columns = await connector.getTableSchema(tableName);
|
|
16
|
-
// Create a more structured response
|
|
17
|
-
const formattedColumns = columns.map(col => ({
|
|
18
|
-
name: col.column_name,
|
|
19
|
-
type: col.data_type,
|
|
20
|
-
nullable: col.is_nullable === 'YES',
|
|
21
|
-
default: col.column_default
|
|
22
|
-
}));
|
|
23
|
-
// Prepare response data
|
|
24
|
-
const responseData = {
|
|
25
|
-
table: tableName,
|
|
26
|
-
columns: formattedColumns,
|
|
27
|
-
count: formattedColumns.length
|
|
28
|
-
};
|
|
29
|
-
// Use the utility to create a standardized response
|
|
30
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
// Use the utility to create a standardized error response
|
|
34
|
-
return createResourceErrorResponse(uri.href, `Table '${tableName}' does not exist or cannot be accessed`, "TABLE_NOT_FOUND");
|
|
35
|
-
}
|
|
36
|
-
}
|
package/dist/resources/tables.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { ConnectorManager } from '../connectors/manager.js';
|
|
2
|
-
import { createResourceSuccessResponse } from '../utils/response-formatter.js';
|
|
3
|
-
/**
|
|
4
|
-
* Tables resource handler
|
|
5
|
-
* Returns a list of all tables in the database
|
|
6
|
-
*/
|
|
7
|
-
export async function tablesResourceHandler(uri, _extra) {
|
|
8
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
9
|
-
const tableNames = await connector.getTables();
|
|
10
|
-
// Prepare response data
|
|
11
|
-
const responseData = {
|
|
12
|
-
tables: tableNames,
|
|
13
|
-
count: tableNames.length
|
|
14
|
-
};
|
|
15
|
-
// Use the utility to create a standardized response
|
|
16
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
17
|
-
}
|