truss-db-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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/ai-query.d.ts +7 -0
- package/dist/lib/ai-query.d.ts.map +1 -0
- package/dist/lib/ai-query.js +224 -0
- package/dist/lib/ai-query.js.map +1 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +30 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/connection.d.ts +14 -0
- package/dist/lib/connection.d.ts.map +1 -0
- package/dist/lib/connection.js +175 -0
- package/dist/lib/connection.js.map +1 -0
- package/dist/lib/license.d.ts +4 -0
- package/dist/lib/license.d.ts.map +1 -0
- package/dist/lib/license.js +94 -0
- package/dist/lib/license.js.map +1 -0
- package/dist/lib/query-optimizer.d.ts +3 -0
- package/dist/lib/query-optimizer.d.ts.map +1 -0
- package/dist/lib/query-optimizer.js +155 -0
- package/dist/lib/query-optimizer.js.map +1 -0
- package/dist/tools/connect.d.ts +3 -0
- package/dist/tools/connect.d.ts.map +1 -0
- package/dist/tools/connect.js +37 -0
- package/dist/tools/connect.js.map +1 -0
- package/dist/tools/describe-table.d.ts +3 -0
- package/dist/tools/describe-table.d.ts.map +1 -0
- package/dist/tools/describe-table.js +60 -0
- package/dist/tools/describe-table.js.map +1 -0
- package/dist/tools/export-data.d.ts +3 -0
- package/dist/tools/export-data.d.ts.map +1 -0
- package/dist/tools/export-data.js +68 -0
- package/dist/tools/export-data.js.map +1 -0
- package/dist/tools/generate-migration.d.ts +3 -0
- package/dist/tools/generate-migration.d.ts.map +1 -0
- package/dist/tools/generate-migration.js +36 -0
- package/dist/tools/generate-migration.js.map +1 -0
- package/dist/tools/list-tables.d.ts +3 -0
- package/dist/tools/list-tables.d.ts.map +1 -0
- package/dist/tools/list-tables.js +45 -0
- package/dist/tools/list-tables.js.map +1 -0
- package/dist/tools/natural-language-query.d.ts +3 -0
- package/dist/tools/natural-language-query.d.ts.map +1 -0
- package/dist/tools/natural-language-query.js +53 -0
- package/dist/tools/natural-language-query.js.map +1 -0
- package/dist/tools/optimize-query.d.ts +3 -0
- package/dist/tools/optimize-query.d.ts.map +1 -0
- package/dist/tools/optimize-query.js +51 -0
- package/dist/tools/optimize-query.js.map +1 -0
- package/dist/tools/query.d.ts +3 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +79 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/evals/eval-query.ts +356 -0
- package/evals/run-evals.ts +36 -0
- package/evals/test.db +0 -0
- package/glama.json +4 -0
- package/package.json +47 -0
- package/smithery.yaml +18 -0
- package/src/index.ts +99 -0
- package/src/lib/ai-query.ts +274 -0
- package/src/lib/config.ts +33 -0
- package/src/lib/connection.ts +263 -0
- package/src/lib/license.ts +118 -0
- package/src/lib/query-optimizer.ts +191 -0
- package/src/tools/connect.ts +43 -0
- package/src/tools/describe-table.ts +67 -0
- package/src/tools/export-data.ts +81 -0
- package/src/tools/generate-migration.ts +43 -0
- package/src/tools/list-tables.ts +52 -0
- package/src/tools/natural-language-query.ts +61 -0
- package/src/tools/optimize-query.ts +59 -0
- package/src/tools/query.ts +89 -0
- package/src/types.ts +115 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
interface EvalResult {
|
|
8
|
+
name: string;
|
|
9
|
+
passed: boolean;
|
|
10
|
+
details: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface EvalSuite {
|
|
14
|
+
passed: number;
|
|
15
|
+
failed: number;
|
|
16
|
+
results: EvalResult[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Test Database Setup ─────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export function createTestDb(): string {
|
|
22
|
+
const dbPath = resolve(__dirname, 'test.db');
|
|
23
|
+
const db = new Database(dbPath);
|
|
24
|
+
|
|
25
|
+
// Drop existing tables
|
|
26
|
+
db.exec('DROP TABLE IF EXISTS order_items');
|
|
27
|
+
db.exec('DROP TABLE IF EXISTS orders');
|
|
28
|
+
db.exec('DROP TABLE IF EXISTS products');
|
|
29
|
+
db.exec('DROP TABLE IF EXISTS customers');
|
|
30
|
+
|
|
31
|
+
// Create schema
|
|
32
|
+
db.exec(`
|
|
33
|
+
CREATE TABLE customers (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
name TEXT NOT NULL,
|
|
36
|
+
email TEXT UNIQUE NOT NULL,
|
|
37
|
+
city TEXT,
|
|
38
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE products (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
name TEXT NOT NULL,
|
|
44
|
+
price REAL NOT NULL,
|
|
45
|
+
category TEXT NOT NULL,
|
|
46
|
+
stock INTEGER DEFAULT 0
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE orders (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
customer_id INTEGER NOT NULL,
|
|
52
|
+
total REAL NOT NULL,
|
|
53
|
+
status TEXT DEFAULT 'pending',
|
|
54
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
55
|
+
FOREIGN KEY (customer_id) REFERENCES customers(id)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE order_items (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
order_id INTEGER NOT NULL,
|
|
61
|
+
product_id INTEGER NOT NULL,
|
|
62
|
+
quantity INTEGER NOT NULL,
|
|
63
|
+
price REAL NOT NULL,
|
|
64
|
+
FOREIGN KEY (order_id) REFERENCES orders(id),
|
|
65
|
+
FOREIGN KEY (product_id) REFERENCES products(id)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
|
|
69
|
+
CREATE INDEX idx_orders_status ON orders(status);
|
|
70
|
+
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
|
|
71
|
+
CREATE INDEX idx_products_category ON products(category);
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
// Insert sample data
|
|
75
|
+
const insertCustomer = db.prepare(
|
|
76
|
+
'INSERT INTO customers (name, email, city) VALUES (?, ?, ?)'
|
|
77
|
+
);
|
|
78
|
+
const insertProduct = db.prepare(
|
|
79
|
+
'INSERT INTO products (name, price, category, stock) VALUES (?, ?, ?, ?)'
|
|
80
|
+
);
|
|
81
|
+
const insertOrder = db.prepare(
|
|
82
|
+
'INSERT INTO orders (customer_id, total, status, created_at) VALUES (?, ?, ?, ?)'
|
|
83
|
+
);
|
|
84
|
+
const insertItem = db.prepare(
|
|
85
|
+
'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Customers
|
|
89
|
+
const customers = [
|
|
90
|
+
['Alice Johnson', 'alice@example.com', 'New York'],
|
|
91
|
+
['Bob Smith', 'bob@example.com', 'San Francisco'],
|
|
92
|
+
['Carol White', 'carol@example.com', 'Chicago'],
|
|
93
|
+
['David Brown', 'david@example.com', 'New York'],
|
|
94
|
+
['Eve Davis', 'eve@example.com', 'San Francisco'],
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const c of customers) {
|
|
98
|
+
insertCustomer.run(...c);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Products
|
|
102
|
+
const products = [
|
|
103
|
+
['Laptop', 999.99, 'Electronics', 50],
|
|
104
|
+
['Phone', 699.99, 'Electronics', 100],
|
|
105
|
+
['Headphones', 149.99, 'Electronics', 200],
|
|
106
|
+
['Desk', 299.99, 'Furniture', 30],
|
|
107
|
+
['Chair', 199.99, 'Furniture', 45],
|
|
108
|
+
['Book: SQL Mastery', 39.99, 'Books', 500],
|
|
109
|
+
['Book: Data Science', 49.99, 'Books', 300],
|
|
110
|
+
['Monitor', 449.99, 'Electronics', 75],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const p of products) {
|
|
114
|
+
insertProduct.run(...p);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Orders
|
|
118
|
+
const orders = [
|
|
119
|
+
[1, 1149.98, 'completed', '2026-01-15 10:00:00'],
|
|
120
|
+
[2, 699.99, 'completed', '2026-01-20 14:30:00'],
|
|
121
|
+
[1, 299.99, 'shipped', '2026-02-01 09:15:00'],
|
|
122
|
+
[3, 189.98, 'completed', '2026-02-10 16:45:00'],
|
|
123
|
+
[4, 999.99, 'pending', '2026-03-01 11:00:00'],
|
|
124
|
+
[5, 449.99, 'shipped', '2026-03-10 08:30:00'],
|
|
125
|
+
[2, 39.99, 'completed', '2026-03-15 13:00:00'],
|
|
126
|
+
[3, 1149.98, 'pending', '2026-03-20 17:00:00'],
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const o of orders) {
|
|
130
|
+
insertOrder.run(...o);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Order items
|
|
134
|
+
const items = [
|
|
135
|
+
[1, 1, 1, 999.99], // Alice: 1 Laptop
|
|
136
|
+
[1, 3, 1, 149.99], // Alice: 1 Headphones
|
|
137
|
+
[2, 2, 1, 699.99], // Bob: 1 Phone
|
|
138
|
+
[3, 4, 1, 299.99], // Alice: 1 Desk
|
|
139
|
+
[4, 6, 2, 39.99], // Carol: 2 SQL books
|
|
140
|
+
[4, 3, 1, 149.99], // Carol: 1 Headphones (wait, total is 189.98 = 2*39.99 + ... let me fix)
|
|
141
|
+
[5, 1, 1, 999.99], // David: 1 Laptop
|
|
142
|
+
[6, 8, 1, 449.99], // Eve: 1 Monitor
|
|
143
|
+
[7, 6, 1, 39.99], // Bob: 1 SQL book
|
|
144
|
+
[8, 1, 1, 999.99], // Carol: 1 Laptop
|
|
145
|
+
[8, 3, 1, 149.99], // Carol: 1 Headphones
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
for (const i of items) {
|
|
149
|
+
insertItem.run(...i);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
db.close();
|
|
153
|
+
return dbPath;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Query Evals ─────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
export function runQueryEvals(): EvalSuite {
|
|
159
|
+
const dbPath = createTestDb();
|
|
160
|
+
const db = new Database(dbPath, { readonly: true });
|
|
161
|
+
const results: EvalResult[] = [];
|
|
162
|
+
|
|
163
|
+
// Eval 1: Basic SELECT
|
|
164
|
+
try {
|
|
165
|
+
const rows = db.prepare('SELECT COUNT(*) as cnt FROM customers').all() as Array<{ cnt: number }>;
|
|
166
|
+
const count = rows[0].cnt;
|
|
167
|
+
results.push({
|
|
168
|
+
name: 'Basic SELECT COUNT(*)',
|
|
169
|
+
passed: count === 5,
|
|
170
|
+
details: `Expected 5 customers, got ${count}`,
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
results.push({ name: 'Basic SELECT COUNT(*)', passed: false, details: String(err) });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Eval 2: JOIN query
|
|
177
|
+
try {
|
|
178
|
+
const rows = db.prepare(`
|
|
179
|
+
SELECT c.name, COUNT(o.id) as order_count
|
|
180
|
+
FROM customers c
|
|
181
|
+
LEFT JOIN orders o ON c.id = o.customer_id
|
|
182
|
+
GROUP BY c.id
|
|
183
|
+
ORDER BY order_count DESC
|
|
184
|
+
`).all() as Array<{ name: string; order_count: number }>;
|
|
185
|
+
|
|
186
|
+
const alice = rows.find(r => r.name === 'Alice Johnson');
|
|
187
|
+
results.push({
|
|
188
|
+
name: 'JOIN with GROUP BY',
|
|
189
|
+
passed: alice?.order_count === 2 && rows.length === 5,
|
|
190
|
+
details: `Alice has ${alice?.order_count} orders (expected 2), total customers: ${rows.length}`,
|
|
191
|
+
});
|
|
192
|
+
} catch (err) {
|
|
193
|
+
results.push({ name: 'JOIN with GROUP BY', passed: false, details: String(err) });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Eval 3: Aggregate query
|
|
197
|
+
try {
|
|
198
|
+
const rows = db.prepare(`
|
|
199
|
+
SELECT category, SUM(stock) as total_stock, AVG(price) as avg_price
|
|
200
|
+
FROM products
|
|
201
|
+
GROUP BY category
|
|
202
|
+
ORDER BY total_stock DESC
|
|
203
|
+
`).all() as Array<{ category: string; total_stock: number; avg_price: number }>;
|
|
204
|
+
|
|
205
|
+
const books = rows.find(r => r.category === 'Books');
|
|
206
|
+
results.push({
|
|
207
|
+
name: 'Aggregate with GROUP BY',
|
|
208
|
+
passed: books?.total_stock === 800 && rows.length === 3,
|
|
209
|
+
details: `Books stock: ${books?.total_stock} (expected 800), categories: ${rows.length}`,
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
212
|
+
results.push({ name: 'Aggregate with GROUP BY', passed: false, details: String(err) });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Eval 4: Subquery
|
|
216
|
+
try {
|
|
217
|
+
const rows = db.prepare(`
|
|
218
|
+
SELECT name FROM customers
|
|
219
|
+
WHERE id IN (SELECT customer_id FROM orders WHERE status = 'pending')
|
|
220
|
+
ORDER BY name
|
|
221
|
+
`).all() as Array<{ name: string }>;
|
|
222
|
+
|
|
223
|
+
const names = rows.map(r => r.name);
|
|
224
|
+
results.push({
|
|
225
|
+
name: 'Subquery with IN',
|
|
226
|
+
passed: names.includes('David Brown') && names.includes('Carol White') && names.length === 2,
|
|
227
|
+
details: `Pending customers: ${names.join(', ')} (expected David Brown, Carol White)`,
|
|
228
|
+
});
|
|
229
|
+
} catch (err) {
|
|
230
|
+
results.push({ name: 'Subquery with IN', passed: false, details: String(err) });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Eval 5: EXPLAIN QUERY PLAN
|
|
234
|
+
try {
|
|
235
|
+
const rows = db.prepare(
|
|
236
|
+
'EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 1'
|
|
237
|
+
).all() as Array<{ detail: string }>;
|
|
238
|
+
|
|
239
|
+
const usesIndex = rows.some(r => r.detail.includes('idx_orders_customer_id'));
|
|
240
|
+
results.push({
|
|
241
|
+
name: 'EXPLAIN shows index usage',
|
|
242
|
+
passed: usesIndex,
|
|
243
|
+
details: `Index used: ${usesIndex}. Plan: ${rows.map(r => r.detail).join('; ')}`,
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
results.push({ name: 'EXPLAIN shows index usage', passed: false, details: String(err) });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Eval 6: Foreign key info
|
|
250
|
+
try {
|
|
251
|
+
const fks = db.pragma('foreign_key_list("orders")') as Array<{
|
|
252
|
+
table: string;
|
|
253
|
+
from: string;
|
|
254
|
+
to: string;
|
|
255
|
+
}>;
|
|
256
|
+
|
|
257
|
+
results.push({
|
|
258
|
+
name: 'Foreign key detection',
|
|
259
|
+
passed: fks.length === 1 && fks[0].table === 'customers',
|
|
260
|
+
details: `FKs: ${fks.map(f => `${f.from} -> ${f.table}(${f.to})`).join(', ')}`,
|
|
261
|
+
});
|
|
262
|
+
} catch (err) {
|
|
263
|
+
results.push({ name: 'Foreign key detection', passed: false, details: String(err) });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Eval 7: Write rejection (simulate free tier)
|
|
267
|
+
try {
|
|
268
|
+
const writePatterns = [
|
|
269
|
+
'INSERT INTO customers (name, email) VALUES ("test", "test@test.com")',
|
|
270
|
+
'UPDATE customers SET name = "test" WHERE id = 1',
|
|
271
|
+
'DELETE FROM customers WHERE id = 1',
|
|
272
|
+
'DROP TABLE customers',
|
|
273
|
+
'ALTER TABLE customers ADD COLUMN phone TEXT',
|
|
274
|
+
'CREATE TABLE test (id INTEGER)',
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
const writePattern = /^\s*(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|REPLACE|MERGE)\b/i;
|
|
278
|
+
const allBlocked = writePatterns.every(sql => writePattern.test(sql));
|
|
279
|
+
|
|
280
|
+
results.push({
|
|
281
|
+
name: 'Write pattern detection',
|
|
282
|
+
passed: allBlocked,
|
|
283
|
+
details: `All ${writePatterns.length} write patterns correctly detected: ${allBlocked}`,
|
|
284
|
+
});
|
|
285
|
+
} catch (err) {
|
|
286
|
+
results.push({ name: 'Write pattern detection', passed: false, details: String(err) });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Eval 8: Table listing
|
|
290
|
+
try {
|
|
291
|
+
const tables = db.prepare(
|
|
292
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
293
|
+
).all() as Array<{ name: string }>;
|
|
294
|
+
|
|
295
|
+
const names = tables.map(t => t.name);
|
|
296
|
+
const expected = ['customers', 'order_items', 'orders', 'products'];
|
|
297
|
+
const allPresent = expected.every(n => names.includes(n));
|
|
298
|
+
|
|
299
|
+
results.push({
|
|
300
|
+
name: 'Table listing',
|
|
301
|
+
passed: allPresent && names.length === 4,
|
|
302
|
+
details: `Tables: ${names.join(', ')} (expected: ${expected.join(', ')})`,
|
|
303
|
+
});
|
|
304
|
+
} catch (err) {
|
|
305
|
+
results.push({ name: 'Table listing', passed: false, details: String(err) });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Eval 9: Column info via pragma
|
|
309
|
+
try {
|
|
310
|
+
const cols = db.pragma('table_info("customers")') as Array<{
|
|
311
|
+
name: string;
|
|
312
|
+
type: string;
|
|
313
|
+
notnull: number;
|
|
314
|
+
pk: number;
|
|
315
|
+
}>;
|
|
316
|
+
|
|
317
|
+
const idCol = cols.find(c => c.name === 'id');
|
|
318
|
+
const emailCol = cols.find(c => c.name === 'email');
|
|
319
|
+
|
|
320
|
+
results.push({
|
|
321
|
+
name: 'Column info extraction',
|
|
322
|
+
passed: idCol?.pk === 1 && emailCol?.notnull === 1 && cols.length === 5,
|
|
323
|
+
details: `id is PK: ${idCol?.pk === 1}, email NOT NULL: ${emailCol?.notnull === 1}, column count: ${cols.length}`,
|
|
324
|
+
});
|
|
325
|
+
} catch (err) {
|
|
326
|
+
results.push({ name: 'Column info extraction', passed: false, details: String(err) });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Eval 10: CSV export format
|
|
330
|
+
try {
|
|
331
|
+
const rows = db.prepare('SELECT name, email FROM customers ORDER BY name LIMIT 2').all() as Array<{
|
|
332
|
+
name: string;
|
|
333
|
+
email: string;
|
|
334
|
+
}>;
|
|
335
|
+
|
|
336
|
+
const header = 'name,email';
|
|
337
|
+
const dataRows = rows.map(r => `${r.name},${r.email}`);
|
|
338
|
+
const csv = [header, ...dataRows].join('\n');
|
|
339
|
+
|
|
340
|
+
const expectedCsv = 'name,email\nAlice Johnson,alice@example.com\nBob Smith,bob@example.com';
|
|
341
|
+
results.push({
|
|
342
|
+
name: 'CSV export format',
|
|
343
|
+
passed: csv === expectedCsv,
|
|
344
|
+
details: csv === expectedCsv ? 'CSV matches expected format' : `Got: ${csv}`,
|
|
345
|
+
});
|
|
346
|
+
} catch (err) {
|
|
347
|
+
results.push({ name: 'CSV export format', passed: false, details: String(err) });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
db.close();
|
|
351
|
+
|
|
352
|
+
const passed = results.filter(r => r.passed).length;
|
|
353
|
+
const failed = results.filter(r => !r.passed).length;
|
|
354
|
+
|
|
355
|
+
return { passed, failed, results };
|
|
356
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { runQueryEvals } from './eval-query.js';
|
|
2
|
+
|
|
3
|
+
console.log('========================================');
|
|
4
|
+
console.log(' Database Query MCP -- Evaluation Suite');
|
|
5
|
+
console.log('========================================\n');
|
|
6
|
+
|
|
7
|
+
// ── Query Evals ─────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
console.log('--- Query & Schema Evals ---\n');
|
|
10
|
+
|
|
11
|
+
const queryResults = runQueryEvals();
|
|
12
|
+
|
|
13
|
+
for (const r of queryResults.results) {
|
|
14
|
+
const icon = r.passed ? 'PASS' : 'FAIL';
|
|
15
|
+
console.log(` [${icon}] ${r.name}`);
|
|
16
|
+
console.log(` ${r.details}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`\n Query: ${queryResults.passed}/${queryResults.passed + queryResults.failed} passed\n`);
|
|
20
|
+
|
|
21
|
+
// ── Summary ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const totalPassed = queryResults.passed;
|
|
24
|
+
const totalFailed = queryResults.failed;
|
|
25
|
+
const total = totalPassed + totalFailed;
|
|
26
|
+
|
|
27
|
+
console.log('========================================');
|
|
28
|
+
console.log(` TOTAL: ${totalPassed}/${total} passed`);
|
|
29
|
+
|
|
30
|
+
if (totalFailed > 0) {
|
|
31
|
+
console.log(` ${totalFailed} failure(s)`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log('========================================');
|
|
35
|
+
|
|
36
|
+
process.exit(totalFailed > 0 ? 1 : 0);
|
package/evals/test.db
ADDED
|
Binary file
|
package/glama.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "truss-db-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Database query MCP server for Claude Code — SQLite, PostgreSQL, and MySQL with AI-powered query tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"truss-db-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"eval": "tsx evals/run-evals.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"database",
|
|
18
|
+
"sqlite",
|
|
19
|
+
"postgresql",
|
|
20
|
+
"mysql",
|
|
21
|
+
"sql",
|
|
22
|
+
"query",
|
|
23
|
+
"claude",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"ai"
|
|
26
|
+
],
|
|
27
|
+
"author": "TRUSS",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
31
|
+
"better-sqlite3": "^11.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"pg": "^8.12.0",
|
|
35
|
+
"mysql2": "^3.11.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"pg": { "optional": true },
|
|
39
|
+
"mysql2": { "optional": true }
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "^5.5.0",
|
|
43
|
+
"tsx": "^4.19.0",
|
|
44
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
45
|
+
"@types/node": "^22.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
startCommand:
|
|
2
|
+
type: stdio
|
|
3
|
+
configSchema:
|
|
4
|
+
type: object
|
|
5
|
+
properties:
|
|
6
|
+
TRUSS_LICENSE_KEY:
|
|
7
|
+
type: string
|
|
8
|
+
description: "License key for Pro features (optional)"
|
|
9
|
+
DATABASE_URL:
|
|
10
|
+
type: string
|
|
11
|
+
description: "Database connection URL (postgres:// or mysql://)"
|
|
12
|
+
ANTHROPIC_API_KEY:
|
|
13
|
+
type: string
|
|
14
|
+
description: "API key for AI-powered query features (Pro tier)"
|
|
15
|
+
OPENAI_API_KEY:
|
|
16
|
+
type: string
|
|
17
|
+
description: "Alternative AI API key (Pro tier)"
|
|
18
|
+
required: []
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
|
|
6
|
+
// Tool registrations
|
|
7
|
+
import { registerConnect } from './tools/connect.js';
|
|
8
|
+
import { registerListTables } from './tools/list-tables.js';
|
|
9
|
+
import { registerDescribeTable } from './tools/describe-table.js';
|
|
10
|
+
import { registerQuery } from './tools/query.js';
|
|
11
|
+
import { registerNaturalLanguageQuery } from './tools/natural-language-query.js';
|
|
12
|
+
import { registerOptimizeQuery } from './tools/optimize-query.js';
|
|
13
|
+
import { registerGenerateMigration } from './tools/generate-migration.js';
|
|
14
|
+
import { registerExportData } from './tools/export-data.js';
|
|
15
|
+
|
|
16
|
+
// Lib
|
|
17
|
+
import { closeConnection } from './lib/connection.js';
|
|
18
|
+
import { getLicenseStatus } from './lib/license.js';
|
|
19
|
+
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: 'truss-db-mcp',
|
|
22
|
+
version: '1.0.0',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ── Status Tool ─────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
server.tool(
|
|
28
|
+
'db_status',
|
|
29
|
+
'Check database connection status and license tier.',
|
|
30
|
+
{},
|
|
31
|
+
async () => {
|
|
32
|
+
const license = await getLicenseStatus();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
content: [{
|
|
36
|
+
type: 'text' as const,
|
|
37
|
+
text: JSON.stringify({
|
|
38
|
+
license: {
|
|
39
|
+
tier: license.tier,
|
|
40
|
+
valid: license.valid,
|
|
41
|
+
expires_at: license.expiresAt,
|
|
42
|
+
},
|
|
43
|
+
...(license.tier === 'free' ? {
|
|
44
|
+
upgrade_info: {
|
|
45
|
+
url: 'https://truss.dev/pricing',
|
|
46
|
+
price: '$25/mo',
|
|
47
|
+
pro_features: [
|
|
48
|
+
'Natural language to SQL (AI-powered)',
|
|
49
|
+
'Query optimization & EXPLAIN analysis',
|
|
50
|
+
'Migration generation',
|
|
51
|
+
'CSV/JSON data export',
|
|
52
|
+
'Write operations (INSERT/UPDATE/DELETE)',
|
|
53
|
+
'PostgreSQL & MySQL support',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
} : {}),
|
|
57
|
+
}, null, 2),
|
|
58
|
+
}],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// ── Free Tier Tools ─────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
registerConnect(server);
|
|
66
|
+
registerListTables(server);
|
|
67
|
+
registerDescribeTable(server);
|
|
68
|
+
registerQuery(server);
|
|
69
|
+
|
|
70
|
+
// ── Pro Tier Tools ──────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
registerNaturalLanguageQuery(server);
|
|
73
|
+
registerOptimizeQuery(server);
|
|
74
|
+
registerGenerateMigration(server);
|
|
75
|
+
registerExportData(server);
|
|
76
|
+
|
|
77
|
+
// ── Start Server ────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
async function main(): Promise<void> {
|
|
80
|
+
const transport = new StdioServerTransport();
|
|
81
|
+
|
|
82
|
+
// Clean shutdown
|
|
83
|
+
process.on('SIGINT', () => {
|
|
84
|
+
closeConnection();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
});
|
|
87
|
+
process.on('SIGTERM', () => {
|
|
88
|
+
closeConnection();
|
|
89
|
+
process.exit(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await server.connect(transport);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
main().catch((err) => {
|
|
96
|
+
console.error('Fatal error starting MCP server:', err);
|
|
97
|
+
closeConnection();
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|