vibesql-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/README.md +62 -0
- package/bin/vibesql-mcp.js +2 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +69 -0
- package/dist/help.d.ts +2 -0
- package/dist/help.js +195 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +167 -0
- package/package.json +24 -0
- package/src/client.ts +96 -0
- package/src/help.ts +199 -0
- package/src/index.ts +215 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @vibesql/mcp
|
|
2
|
+
|
|
3
|
+
MCP server that connects your AI coding tool to a [VibeSQL](https://vibesql.online) PostgreSQL database. One command, 9 tools, zero config.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx vibesql-micro # start database on :5173
|
|
9
|
+
npx @vibesql/mcp # start MCP server
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Claude Code Setup
|
|
13
|
+
|
|
14
|
+
Add to `.claude/settings.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"vibesql": {
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["@vibesql/mcp"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Custom URL:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"vibesql": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["@vibesql/mcp", "--url", "http://10.0.0.5:5173"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Tools
|
|
41
|
+
|
|
42
|
+
| Tool | Description |
|
|
43
|
+
|------|-------------|
|
|
44
|
+
| `query` | Execute any SQL query (DDL + DML) |
|
|
45
|
+
| `list_tables` | List all tables in the database |
|
|
46
|
+
| `describe_table` | Get column schema for a table |
|
|
47
|
+
| `table_data` | Browse rows with pagination |
|
|
48
|
+
| `create_table` | Create a table (validated, no semicolons) |
|
|
49
|
+
| `insert_row` | Insert a row from JSON column-value pairs |
|
|
50
|
+
| `help` | Help on a VibeSQL topic |
|
|
51
|
+
| `help_products` | Product family overview |
|
|
52
|
+
| `help_architecture` | Architecture patterns |
|
|
53
|
+
|
|
54
|
+
The `query` tool is intentionally unrestricted -- it can execute any valid SQL including DROP, DELETE, and TRUNCATE. This is a local development tool.
|
|
55
|
+
|
|
56
|
+
## Environment
|
|
57
|
+
|
|
58
|
+
Set `VIBESQL_URL` to override the default `http://localhost:5173`. The `--url` CLI flag takes precedence over the env var.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
Apache-2.0
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface QueryResult {
|
|
2
|
+
columns: string[];
|
|
3
|
+
rows: unknown[][];
|
|
4
|
+
time: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class VibeClient {
|
|
7
|
+
private readonly baseUrl;
|
|
8
|
+
constructor(baseUrl: string);
|
|
9
|
+
health(): Promise<{
|
|
10
|
+
status: string;
|
|
11
|
+
}>;
|
|
12
|
+
query(sql: string, params?: unknown[]): Promise<QueryResult>;
|
|
13
|
+
listTables(): Promise<QueryResult>;
|
|
14
|
+
describeTable(table: string): Promise<QueryResult>;
|
|
15
|
+
tableData(table: string, limit: number, offset: number): Promise<QueryResult>;
|
|
16
|
+
insertRow(table: string, data: Record<string, unknown>): Promise<QueryResult>;
|
|
17
|
+
createTable(sql: string): Promise<QueryResult>;
|
|
18
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const TABLE_NAME_RE = /^[A-Za-z0-9_]+$/;
|
|
2
|
+
export class VibeClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
constructor(baseUrl) {
|
|
5
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
6
|
+
}
|
|
7
|
+
async health() {
|
|
8
|
+
const res = await fetch(`${this.baseUrl}/v1/health`);
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
throw new Error(`Health check failed: ${res.status} ${res.statusText}`);
|
|
11
|
+
}
|
|
12
|
+
return res.json();
|
|
13
|
+
}
|
|
14
|
+
async query(sql, params) {
|
|
15
|
+
const body = { sql };
|
|
16
|
+
if (params && params.length > 0) {
|
|
17
|
+
body.params = params;
|
|
18
|
+
}
|
|
19
|
+
const res = await fetch(`${this.baseUrl}/v1/query`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const text = await res.text().catch(() => '');
|
|
26
|
+
throw new Error(`Query failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ''}`);
|
|
27
|
+
}
|
|
28
|
+
return res.json();
|
|
29
|
+
}
|
|
30
|
+
async listTables() {
|
|
31
|
+
return this.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name`);
|
|
32
|
+
}
|
|
33
|
+
async describeTable(table) {
|
|
34
|
+
validateName(table, 'table');
|
|
35
|
+
return this.query(`SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position`, [table]);
|
|
36
|
+
}
|
|
37
|
+
async tableData(table, limit, offset) {
|
|
38
|
+
validateName(table, 'table');
|
|
39
|
+
return this.query(`SELECT * FROM "${table}" LIMIT $1 OFFSET $2`, [limit, offset]);
|
|
40
|
+
}
|
|
41
|
+
async insertRow(table, data) {
|
|
42
|
+
validateName(table, 'table');
|
|
43
|
+
const entries = Object.entries(data);
|
|
44
|
+
if (entries.length === 0) {
|
|
45
|
+
throw new Error('insert_row: data object must have at least one column');
|
|
46
|
+
}
|
|
47
|
+
for (const [col] of entries) {
|
|
48
|
+
validateName(col, 'column');
|
|
49
|
+
}
|
|
50
|
+
const columns = entries.map(([col]) => `"${col}"`).join(', ');
|
|
51
|
+
const placeholders = entries.map((_, i) => `$${i + 1}`).join(', ');
|
|
52
|
+
const values = entries.map(([, val]) => val);
|
|
53
|
+
return this.query(`INSERT INTO "${table}" (${columns}) VALUES (${placeholders}) RETURNING *`, values);
|
|
54
|
+
}
|
|
55
|
+
async createTable(sql) {
|
|
56
|
+
if (!/^CREATE\s+TABLE\b/i.test(sql)) {
|
|
57
|
+
throw new Error('create_table: SQL must start with CREATE TABLE');
|
|
58
|
+
}
|
|
59
|
+
if (sql.includes(';')) {
|
|
60
|
+
throw new Error('create_table: semicolons are not allowed (prevents multi-statement injection)');
|
|
61
|
+
}
|
|
62
|
+
return this.query(sql);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function validateName(name, kind) {
|
|
66
|
+
if (!TABLE_NAME_RE.test(name)) {
|
|
67
|
+
throw new Error(`Invalid ${kind} name "${name}" — only alphanumeric characters and underscores are allowed`);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/help.d.ts
ADDED
package/dist/help.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export const HELP_TOPICS = {
|
|
2
|
+
architecture: `# Architecture
|
|
3
|
+
|
|
4
|
+
This page describes the cross-cutting patterns shared across the VibeSQL product family.
|
|
5
|
+
|
|
6
|
+
## Envelope Encryption Pattern
|
|
7
|
+
|
|
8
|
+
vsql-vault, vsql-backup, and vsql-sync all use **envelope encryption**. The pattern separates the key that encrypts data (DEK) from the key that protects the DEK (KEK).
|
|
9
|
+
|
|
10
|
+
\`\`\`
|
|
11
|
+
plaintext
|
|
12
|
+
|
|
|
13
|
+
v AES-256-GCM (DEK)
|
|
14
|
+
ciphertext --> stored
|
|
15
|
+
|
|
16
|
+
DEK --> RSA wrap (KEK) --> wrapped DEK stored alongside ciphertext
|
|
17
|
+
\`\`\`
|
|
18
|
+
|
|
19
|
+
**Why envelope encryption?**
|
|
20
|
+
- The DEK can be rotated or revoked by replacing only the wrapped DEK — no re-encryption of data required.
|
|
21
|
+
- The KEK never touches the data storage layer; it lives in CryptAply.
|
|
22
|
+
- Compromising one DEK affects only the data it encrypted, not the KEK or other DEKs.
|
|
23
|
+
|
|
24
|
+
| Product | DEK scope | KEK source |
|
|
25
|
+
|---------|-----------|------------|
|
|
26
|
+
| vsql-vault | Per blob | CryptAply (RSA) |
|
|
27
|
+
| vsql-backup | Per backup set | CryptAply (RSA) |
|
|
28
|
+
| vsql-sync | Per session/batch | CryptAply (RSA) |
|
|
29
|
+
|
|
30
|
+
## Hash Chains
|
|
31
|
+
|
|
32
|
+
vsql-sync uses a **hash chain** to guarantee audit trail integrity. Each audit entry includes the hash of the previous entry. Any modification to a historical entry breaks the chain from that point forward, making tampering immediately detectable during verification.
|
|
33
|
+
|
|
34
|
+
## Merkle Trees (Backup Manifest Verification)
|
|
35
|
+
|
|
36
|
+
vsql-backup organizes its SHA-256 segment hashes into a **Merkle tree**. Each leaf is the hash of one backup segment. Parent nodes are hashes of their children. The root hash represents the entire backup set. To verify any single segment, you only need the segment's sibling hashes along the path to the root.
|
|
37
|
+
|
|
38
|
+
## Ed25519 Signing (Sync Audit Entries)
|
|
39
|
+
|
|
40
|
+
vsql-sync signs each audit trail entry with an **Ed25519** private key held by CryptAply. 64-byte signatures, fast verification, no key material on sync nodes.
|
|
41
|
+
|
|
42
|
+
## Dev vs Prod Mode
|
|
43
|
+
|
|
44
|
+
| Feature | Dev mode | Prod mode |
|
|
45
|
+
|---------|----------|-----------|
|
|
46
|
+
| TLS | Optional (HTTP allowed) | Required (HTTPS only) |
|
|
47
|
+
| KEK source | Local key file | CryptAply (remote) |
|
|
48
|
+
| Audit trail signing | Disabled or local key | Ed25519 via CryptAply |
|
|
49
|
+
| Access log retention | Short (debugging) | Configured per policy |
|
|
50
|
+
| PITR window | Hours | Days to weeks |
|
|
51
|
+
|
|
52
|
+
## How All Products Connect
|
|
53
|
+
|
|
54
|
+
\`\`\`
|
|
55
|
+
vibesql-micro (PostgreSQL HTTP server)
|
|
56
|
+
| query
|
|
57
|
+
v
|
|
58
|
+
application layer
|
|
59
|
+
| store blobs | replicate changes
|
|
60
|
+
v v
|
|
61
|
+
vsql-vault vsql-sync
|
|
62
|
+
| backup | audit signing
|
|
63
|
+
v |
|
|
64
|
+
vsql-backup |
|
|
65
|
+
| |
|
|
66
|
+
+---------------------->|
|
|
67
|
+
CryptAply
|
|
68
|
+
(KEK management, key lifecycle,
|
|
69
|
+
directive enforcement, compliance)
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
CryptAply is the trust anchor. All encrypted services depend on it for KEK operations.`,
|
|
73
|
+
products: `# VibeSQL Product Family
|
|
74
|
+
|
|
75
|
+
VibeSQL is a family of seven products delivering a compliant, governed, globally synchronized database ecosystem. Each product can be used independently or together as an integrated stack.
|
|
76
|
+
|
|
77
|
+
## Products at a Glance
|
|
78
|
+
|
|
79
|
+
| Product | Role | Port |
|
|
80
|
+
|---------|------|------|
|
|
81
|
+
| vibesql-micro | PostgreSQL-over-HTTP database server | 5173 |
|
|
82
|
+
| vsql-vault | Encrypted blob storage | 8443 |
|
|
83
|
+
| vsql-backup | Governed backup with pgBackRest engine | 8445 |
|
|
84
|
+
| vsql-sync | Governed data movement — PCI-scoped replication | 8444 |
|
|
85
|
+
| vsql-cryptaply | Key governance and compliance engine | — |
|
|
86
|
+
| vibesql-admin | Unified admin hub + MCP server | 5174 |
|
|
87
|
+
| vibesql (core) | Core database library | — |
|
|
88
|
+
|
|
89
|
+
## vibesql-micro
|
|
90
|
+
|
|
91
|
+
Lightweight PostgreSQL-over-HTTP database server with embedded PostgreSQL 16.1. Ships as a single binary with zero configuration required.
|
|
92
|
+
|
|
93
|
+
Key endpoints:
|
|
94
|
+
- POST /v1/query — Execute SQL statements
|
|
95
|
+
- GET /v1/health — Service health check
|
|
96
|
+
|
|
97
|
+
Designed for edge deployments, local-first applications, and embedded use cases.
|
|
98
|
+
|
|
99
|
+
## vsql-vault
|
|
100
|
+
|
|
101
|
+
Encrypted blob storage with envelope encryption. All data is encrypted at rest using AES-256-GCM. RSA key wrapping protects the data encryption keys. Features: access logging, configurable retention policies.
|
|
102
|
+
|
|
103
|
+
## vsql-backup
|
|
104
|
+
|
|
105
|
+
Rust binary wrapping pgBackRest (MIT, C) as the backup engine. Envelope encryption per backup set, manifest verification, PITR mechanics. CryptAply provides key governance authority.
|
|
106
|
+
|
|
107
|
+
## vsql-sync
|
|
108
|
+
|
|
109
|
+
Governed data movement layer. Encrypted replication payloads with envelope encryption. CRDT-based conflict resolution (LWW), Ed25519-signed audit trail with hash chaining, publication-based selective replication with column exclusion for PCI scope reduction.
|
|
110
|
+
|
|
111
|
+
## vsql-cryptaply (CryptAply)
|
|
112
|
+
|
|
113
|
+
Key governance and compliance engine. Directive-based policies over encryption keys used by vault, backup, and sync. Full key lifecycle management.
|
|
114
|
+
|
|
115
|
+
## vibesql-admin
|
|
116
|
+
|
|
117
|
+
Unified administration hub. One web UI for humans, one MCP server for AI agents, one help system for everyone.
|
|
118
|
+
|
|
119
|
+
## How the Products Work Together
|
|
120
|
+
|
|
121
|
+
\`\`\`
|
|
122
|
+
vibesql-micro --> vsql-vault --> vsql-backup (Rust + pgBackRest)
|
|
123
|
+
| |
|
|
124
|
+
vsql-sync |
|
|
125
|
+
(governed wire) |
|
|
126
|
+
| |
|
|
127
|
+
CryptAply <------------+
|
|
128
|
+
(KEK management, key lifecycle,
|
|
129
|
+
directive enforcement, compliance)
|
|
130
|
+
|
|
|
131
|
+
vibesql-admin
|
|
132
|
+
(unified hub + MCP server)
|
|
133
|
+
\`\`\``,
|
|
134
|
+
glossary: `# Glossary
|
|
135
|
+
|
|
136
|
+
Reference definitions for terms used across VibeSQL documentation.
|
|
137
|
+
|
|
138
|
+
## AES-256-GCM
|
|
139
|
+
Advanced Encryption Standard with a 256-bit key in Galois/Counter Mode. Provides both confidentiality and authenticated integrity. Used by vsql-vault, vsql-backup, and vsql-sync for data encryption.
|
|
140
|
+
|
|
141
|
+
## Blob
|
|
142
|
+
An opaque binary object stored in vsql-vault. A blob has an ID, metadata, and an encrypted payload.
|
|
143
|
+
|
|
144
|
+
## CDE (Cardholder Data Environment)
|
|
145
|
+
The systems and processes that store, process, or transmit cardholder data. PCI DSS controls apply to everything in scope of the CDE.
|
|
146
|
+
|
|
147
|
+
## CRDT (Conflict-free Replicated Data Type)
|
|
148
|
+
A data structure designed so that concurrent updates on different nodes can always be merged without coordination. vsql-sync uses CRDTs for conflict resolution.
|
|
149
|
+
|
|
150
|
+
## DEK (Data Encryption Key)
|
|
151
|
+
The symmetric key that directly encrypts data. An AES-256-GCM key. Generated per blob (vault), per backup set (backup), or per session (sync). Always stored wrapped by a KEK.
|
|
152
|
+
|
|
153
|
+
## Directive
|
|
154
|
+
A policy document in CryptAply that governs a key or key family. Specifies algorithm, rotation schedule, allowed consumers, and expiry behavior.
|
|
155
|
+
|
|
156
|
+
## Ed25519
|
|
157
|
+
An elliptic-curve digital signature algorithm using Curve25519. Produces 64-byte signatures with fast verification. Used by vsql-sync for audit trail signing.
|
|
158
|
+
|
|
159
|
+
## Envelope Encryption
|
|
160
|
+
A two-layer encryption pattern: a DEK encrypts the data; a KEK encrypts (wraps) the DEK. Only the wrapped DEK is stored alongside the ciphertext.
|
|
161
|
+
|
|
162
|
+
## Hash Chain
|
|
163
|
+
A sequence of records where each record contains the hash of the previous record. Modification of any historical record invalidates all subsequent hashes.
|
|
164
|
+
|
|
165
|
+
## KEK (Key Encryption Key)
|
|
166
|
+
The key that wraps (encrypts) a DEK. Managed by CryptAply and never exposed to the storage layer. In VibeSQL, KEKs are RSA keys.
|
|
167
|
+
|
|
168
|
+
## LWW (Last Writer Wins)
|
|
169
|
+
A CRDT conflict resolution strategy. The update with the higher logical timestamp is kept. Used by vsql-sync.
|
|
170
|
+
|
|
171
|
+
## Merkle Tree
|
|
172
|
+
A binary tree of hashes for efficient partial verification of backup segments.
|
|
173
|
+
|
|
174
|
+
## PCI DSS
|
|
175
|
+
Payment Card Industry Data Security Standard. Relevant VibeSQL features: envelope encryption (Req 3), access logging (Req 10), column exclusion (scope reduction).
|
|
176
|
+
|
|
177
|
+
## PITR (Point-in-Time Recovery)
|
|
178
|
+
The ability to restore a database to any recorded moment in time, not just the most recent backup.
|
|
179
|
+
|
|
180
|
+
## Publication
|
|
181
|
+
A named configuration in vsql-sync defining which tables and columns replicate to which subscriber nodes.
|
|
182
|
+
|
|
183
|
+
## RSA
|
|
184
|
+
Asymmetric encryption algorithm used as the KEK algorithm for wrapping DEKs.
|
|
185
|
+
|
|
186
|
+
## SHA-256
|
|
187
|
+
Secure Hash Algorithm 2 with 256-bit output. Used for segment integrity hashes and hash chain linking.`,
|
|
188
|
+
};
|
|
189
|
+
export function getHelp(topic) {
|
|
190
|
+
const key = topic.toLowerCase().trim();
|
|
191
|
+
const content = HELP_TOPICS[key];
|
|
192
|
+
if (content)
|
|
193
|
+
return content;
|
|
194
|
+
return `Unknown topic "${topic}". Available topics: ${Object.keys(HELP_TOPICS).join(', ')}`;
|
|
195
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { VibeClient } from './client.js';
|
|
5
|
+
import { getHelp, HELP_TOPICS } from './help.js';
|
|
6
|
+
// --- CLI args ---
|
|
7
|
+
function parseArgs() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
if (args[i] === '--url' && args[i + 1]) {
|
|
11
|
+
return args[++i];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return process.env['VIBESQL_URL'] || 'http://localhost:5173';
|
|
15
|
+
}
|
|
16
|
+
const baseUrl = parseArgs();
|
|
17
|
+
const client = new VibeClient(baseUrl);
|
|
18
|
+
// --- Health check ---
|
|
19
|
+
try {
|
|
20
|
+
await client.health();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
process.stderr.write(`\nCould not connect to VibeSQL at ${baseUrl}\n\n`);
|
|
24
|
+
process.stderr.write(`Start it with: npx vibesql-micro\n`);
|
|
25
|
+
process.stderr.write(`Or specify URL: npx @vibesql/mcp --url http://your-host:5173\n\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
// --- MCP Server ---
|
|
29
|
+
const server = new McpServer({
|
|
30
|
+
name: '@vibesql/mcp',
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
});
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Database tools
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
server.tool('query', 'Execute a SQL query against the VibeSQL database. This tool is unrestricted — it can execute any valid SQL including DDL (CREATE, ALTER, DROP) and DML (INSERT, UPDATE, DELETE). This is intentional for a local development tool.', {
|
|
37
|
+
sql: z.string().describe('SQL statement to execute'),
|
|
38
|
+
params: z.string().optional().describe('JSON array of query parameters'),
|
|
39
|
+
}, async ({ sql, params }) => {
|
|
40
|
+
try {
|
|
41
|
+
const parsedParams = params ? JSON.parse(params) : undefined;
|
|
42
|
+
const result = await client.query(sql, parsedParams);
|
|
43
|
+
const header = result.columns.join('\t');
|
|
44
|
+
const rows = result.rows.map((row) => row.join('\t')).join('\n');
|
|
45
|
+
const text = [header, rows].filter(Boolean).join('\n');
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: 'text', text: text || '(no rows returned)' }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
server.tool('list_tables', 'List all tables in the database', async () => {
|
|
58
|
+
try {
|
|
59
|
+
const result = await client.listTables();
|
|
60
|
+
const tables = result.rows.map((row) => row[0]);
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
server.tool('describe_table', 'Get column schema for a table (column name, data type, nullable, default)', {
|
|
73
|
+
table: z.string().describe('Table name to describe'),
|
|
74
|
+
}, async ({ table }) => {
|
|
75
|
+
try {
|
|
76
|
+
const result = await client.describeTable(table);
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
84
|
+
isError: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
server.tool('table_data', 'Browse rows from a table with pagination', {
|
|
89
|
+
table: z.string().describe('Table name to query'),
|
|
90
|
+
limit: z.number().int().positive().optional().describe('Maximum rows to return (default 50)'),
|
|
91
|
+
offset: z.number().int().nonnegative().optional().describe('Rows to skip (default 0)'),
|
|
92
|
+
}, async ({ table, limit, offset }) => {
|
|
93
|
+
try {
|
|
94
|
+
const result = await client.tableData(table, limit ?? 50, offset ?? 0);
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
server.tool('create_table', 'Create a new table. SQL must start with CREATE TABLE and must not contain semicolons.', {
|
|
107
|
+
sql: z.string().describe('CREATE TABLE DDL statement'),
|
|
108
|
+
}, async ({ sql }) => {
|
|
109
|
+
try {
|
|
110
|
+
await client.createTable(sql);
|
|
111
|
+
const nameMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i);
|
|
112
|
+
const tableName = nameMatch ? nameMatch[1] : 'unknown';
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: 'text', text: `Table "${tableName}" created successfully.` }],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
120
|
+
isError: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
server.tool('insert_row', 'Insert a row into a table. Data is a JSON object of column-value pairs. Supported value types: string, number, boolean, null.', {
|
|
125
|
+
table: z.string().describe('Table name'),
|
|
126
|
+
data: z.string().describe('JSON object of column→value pairs, e.g. {"name":"Alice","age":30}'),
|
|
127
|
+
}, async ({ table, data }) => {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(data);
|
|
130
|
+
const result = await client.insertRow(table, parsed);
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Help tools
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
server.tool('help', 'Get help on a VibeSQL topic. Available topics: architecture, products, glossary', {
|
|
146
|
+
topic: z.string().describe('Topic name (architecture, products, or glossary)'),
|
|
147
|
+
}, async ({ topic }) => {
|
|
148
|
+
const text = getHelp(topic);
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: 'text', text }],
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
server.tool('help_products', 'VibeSQL product family overview — all 7 products and how they connect', async () => {
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: 'text', text: HELP_TOPICS['products'] }],
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
server.tool('help_architecture', 'VibeSQL architecture patterns — envelope encryption, hash chains, Merkle trees', async () => {
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: 'text', text: HELP_TOPICS['architecture'] }],
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Start server
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
const transport = new StdioServerTransport();
|
|
167
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibesql-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for VibeSQL — connect your AI coding tool to a PostgreSQL database",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibesql-mcp": "./bin/vibesql-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["vibesql", "mcp", "postgresql", "database", "ai", "claude"],
|
|
14
|
+
"author": "PayEz <opensource@payez.net>",
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
18
|
+
"zod": "^3.23"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.7",
|
|
22
|
+
"@types/node": "^22.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export interface QueryResult {
|
|
2
|
+
columns: string[];
|
|
3
|
+
rows: unknown[][];
|
|
4
|
+
time: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const TABLE_NAME_RE = /^[A-Za-z0-9_]+$/;
|
|
8
|
+
|
|
9
|
+
export class VibeClient {
|
|
10
|
+
private readonly baseUrl: string;
|
|
11
|
+
|
|
12
|
+
constructor(baseUrl: string) {
|
|
13
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async health(): Promise<{ status: string }> {
|
|
17
|
+
const res = await fetch(`${this.baseUrl}/v1/health`);
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
throw new Error(`Health check failed: ${res.status} ${res.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
return res.json() as Promise<{ status: string }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async query(sql: string, params?: unknown[]): Promise<QueryResult> {
|
|
25
|
+
const body: Record<string, unknown> = { sql };
|
|
26
|
+
if (params && params.length > 0) {
|
|
27
|
+
body.params = params;
|
|
28
|
+
}
|
|
29
|
+
const res = await fetch(`${this.baseUrl}/v1/query`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const text = await res.text().catch(() => '');
|
|
36
|
+
throw new Error(`Query failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ''}`);
|
|
37
|
+
}
|
|
38
|
+
return res.json() as Promise<QueryResult>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async listTables(): Promise<QueryResult> {
|
|
42
|
+
return this.query(
|
|
43
|
+
`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async describeTable(table: string): Promise<QueryResult> {
|
|
48
|
+
validateName(table, 'table');
|
|
49
|
+
return this.query(
|
|
50
|
+
`SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position`,
|
|
51
|
+
[table]
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async tableData(table: string, limit: number, offset: number): Promise<QueryResult> {
|
|
56
|
+
validateName(table, 'table');
|
|
57
|
+
return this.query(
|
|
58
|
+
`SELECT * FROM "${table}" LIMIT $1 OFFSET $2`,
|
|
59
|
+
[limit, offset]
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async insertRow(table: string, data: Record<string, unknown>): Promise<QueryResult> {
|
|
64
|
+
validateName(table, 'table');
|
|
65
|
+
const entries = Object.entries(data);
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
throw new Error('insert_row: data object must have at least one column');
|
|
68
|
+
}
|
|
69
|
+
for (const [col] of entries) {
|
|
70
|
+
validateName(col, 'column');
|
|
71
|
+
}
|
|
72
|
+
const columns = entries.map(([col]) => `"${col}"`).join(', ');
|
|
73
|
+
const placeholders = entries.map((_, i) => `$${i + 1}`).join(', ');
|
|
74
|
+
const values = entries.map(([, val]) => val);
|
|
75
|
+
return this.query(
|
|
76
|
+
`INSERT INTO "${table}" (${columns}) VALUES (${placeholders}) RETURNING *`,
|
|
77
|
+
values
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async createTable(sql: string): Promise<QueryResult> {
|
|
82
|
+
if (!/^CREATE\s+TABLE\b/i.test(sql)) {
|
|
83
|
+
throw new Error('create_table: SQL must start with CREATE TABLE');
|
|
84
|
+
}
|
|
85
|
+
if (sql.includes(';')) {
|
|
86
|
+
throw new Error('create_table: semicolons are not allowed (prevents multi-statement injection)');
|
|
87
|
+
}
|
|
88
|
+
return this.query(sql);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateName(name: string, kind: string): void {
|
|
93
|
+
if (!TABLE_NAME_RE.test(name)) {
|
|
94
|
+
throw new Error(`Invalid ${kind} name "${name}" — only alphanumeric characters and underscores are allowed`);
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/help.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export const HELP_TOPICS: Record<string, string> = {
|
|
2
|
+
|
|
3
|
+
architecture: `# Architecture
|
|
4
|
+
|
|
5
|
+
This page describes the cross-cutting patterns shared across the VibeSQL product family.
|
|
6
|
+
|
|
7
|
+
## Envelope Encryption Pattern
|
|
8
|
+
|
|
9
|
+
vsql-vault, vsql-backup, and vsql-sync all use **envelope encryption**. The pattern separates the key that encrypts data (DEK) from the key that protects the DEK (KEK).
|
|
10
|
+
|
|
11
|
+
\`\`\`
|
|
12
|
+
plaintext
|
|
13
|
+
|
|
|
14
|
+
v AES-256-GCM (DEK)
|
|
15
|
+
ciphertext --> stored
|
|
16
|
+
|
|
17
|
+
DEK --> RSA wrap (KEK) --> wrapped DEK stored alongside ciphertext
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
**Why envelope encryption?**
|
|
21
|
+
- The DEK can be rotated or revoked by replacing only the wrapped DEK — no re-encryption of data required.
|
|
22
|
+
- The KEK never touches the data storage layer; it lives in CryptAply.
|
|
23
|
+
- Compromising one DEK affects only the data it encrypted, not the KEK or other DEKs.
|
|
24
|
+
|
|
25
|
+
| Product | DEK scope | KEK source |
|
|
26
|
+
|---------|-----------|------------|
|
|
27
|
+
| vsql-vault | Per blob | CryptAply (RSA) |
|
|
28
|
+
| vsql-backup | Per backup set | CryptAply (RSA) |
|
|
29
|
+
| vsql-sync | Per session/batch | CryptAply (RSA) |
|
|
30
|
+
|
|
31
|
+
## Hash Chains
|
|
32
|
+
|
|
33
|
+
vsql-sync uses a **hash chain** to guarantee audit trail integrity. Each audit entry includes the hash of the previous entry. Any modification to a historical entry breaks the chain from that point forward, making tampering immediately detectable during verification.
|
|
34
|
+
|
|
35
|
+
## Merkle Trees (Backup Manifest Verification)
|
|
36
|
+
|
|
37
|
+
vsql-backup organizes its SHA-256 segment hashes into a **Merkle tree**. Each leaf is the hash of one backup segment. Parent nodes are hashes of their children. The root hash represents the entire backup set. To verify any single segment, you only need the segment's sibling hashes along the path to the root.
|
|
38
|
+
|
|
39
|
+
## Ed25519 Signing (Sync Audit Entries)
|
|
40
|
+
|
|
41
|
+
vsql-sync signs each audit trail entry with an **Ed25519** private key held by CryptAply. 64-byte signatures, fast verification, no key material on sync nodes.
|
|
42
|
+
|
|
43
|
+
## Dev vs Prod Mode
|
|
44
|
+
|
|
45
|
+
| Feature | Dev mode | Prod mode |
|
|
46
|
+
|---------|----------|-----------|
|
|
47
|
+
| TLS | Optional (HTTP allowed) | Required (HTTPS only) |
|
|
48
|
+
| KEK source | Local key file | CryptAply (remote) |
|
|
49
|
+
| Audit trail signing | Disabled or local key | Ed25519 via CryptAply |
|
|
50
|
+
| Access log retention | Short (debugging) | Configured per policy |
|
|
51
|
+
| PITR window | Hours | Days to weeks |
|
|
52
|
+
|
|
53
|
+
## How All Products Connect
|
|
54
|
+
|
|
55
|
+
\`\`\`
|
|
56
|
+
vibesql-micro (PostgreSQL HTTP server)
|
|
57
|
+
| query
|
|
58
|
+
v
|
|
59
|
+
application layer
|
|
60
|
+
| store blobs | replicate changes
|
|
61
|
+
v v
|
|
62
|
+
vsql-vault vsql-sync
|
|
63
|
+
| backup | audit signing
|
|
64
|
+
v |
|
|
65
|
+
vsql-backup |
|
|
66
|
+
| |
|
|
67
|
+
+---------------------->|
|
|
68
|
+
CryptAply
|
|
69
|
+
(KEK management, key lifecycle,
|
|
70
|
+
directive enforcement, compliance)
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
CryptAply is the trust anchor. All encrypted services depend on it for KEK operations.`,
|
|
74
|
+
|
|
75
|
+
products: `# VibeSQL Product Family
|
|
76
|
+
|
|
77
|
+
VibeSQL is a family of seven products delivering a compliant, governed, globally synchronized database ecosystem. Each product can be used independently or together as an integrated stack.
|
|
78
|
+
|
|
79
|
+
## Products at a Glance
|
|
80
|
+
|
|
81
|
+
| Product | Role | Port |
|
|
82
|
+
|---------|------|------|
|
|
83
|
+
| vibesql-micro | PostgreSQL-over-HTTP database server | 5173 |
|
|
84
|
+
| vsql-vault | Encrypted blob storage | 8443 |
|
|
85
|
+
| vsql-backup | Governed backup with pgBackRest engine | 8445 |
|
|
86
|
+
| vsql-sync | Governed data movement — PCI-scoped replication | 8444 |
|
|
87
|
+
| vsql-cryptaply | Key governance and compliance engine | — |
|
|
88
|
+
| vibesql-admin | Unified admin hub + MCP server | 5174 |
|
|
89
|
+
| vibesql (core) | Core database library | — |
|
|
90
|
+
|
|
91
|
+
## vibesql-micro
|
|
92
|
+
|
|
93
|
+
Lightweight PostgreSQL-over-HTTP database server with embedded PostgreSQL 16.1. Ships as a single binary with zero configuration required.
|
|
94
|
+
|
|
95
|
+
Key endpoints:
|
|
96
|
+
- POST /v1/query — Execute SQL statements
|
|
97
|
+
- GET /v1/health — Service health check
|
|
98
|
+
|
|
99
|
+
Designed for edge deployments, local-first applications, and embedded use cases.
|
|
100
|
+
|
|
101
|
+
## vsql-vault
|
|
102
|
+
|
|
103
|
+
Encrypted blob storage with envelope encryption. All data is encrypted at rest using AES-256-GCM. RSA key wrapping protects the data encryption keys. Features: access logging, configurable retention policies.
|
|
104
|
+
|
|
105
|
+
## vsql-backup
|
|
106
|
+
|
|
107
|
+
Rust binary wrapping pgBackRest (MIT, C) as the backup engine. Envelope encryption per backup set, manifest verification, PITR mechanics. CryptAply provides key governance authority.
|
|
108
|
+
|
|
109
|
+
## vsql-sync
|
|
110
|
+
|
|
111
|
+
Governed data movement layer. Encrypted replication payloads with envelope encryption. CRDT-based conflict resolution (LWW), Ed25519-signed audit trail with hash chaining, publication-based selective replication with column exclusion for PCI scope reduction.
|
|
112
|
+
|
|
113
|
+
## vsql-cryptaply (CryptAply)
|
|
114
|
+
|
|
115
|
+
Key governance and compliance engine. Directive-based policies over encryption keys used by vault, backup, and sync. Full key lifecycle management.
|
|
116
|
+
|
|
117
|
+
## vibesql-admin
|
|
118
|
+
|
|
119
|
+
Unified administration hub. One web UI for humans, one MCP server for AI agents, one help system for everyone.
|
|
120
|
+
|
|
121
|
+
## How the Products Work Together
|
|
122
|
+
|
|
123
|
+
\`\`\`
|
|
124
|
+
vibesql-micro --> vsql-vault --> vsql-backup (Rust + pgBackRest)
|
|
125
|
+
| |
|
|
126
|
+
vsql-sync |
|
|
127
|
+
(governed wire) |
|
|
128
|
+
| |
|
|
129
|
+
CryptAply <------------+
|
|
130
|
+
(KEK management, key lifecycle,
|
|
131
|
+
directive enforcement, compliance)
|
|
132
|
+
|
|
|
133
|
+
vibesql-admin
|
|
134
|
+
(unified hub + MCP server)
|
|
135
|
+
\`\`\``,
|
|
136
|
+
|
|
137
|
+
glossary: `# Glossary
|
|
138
|
+
|
|
139
|
+
Reference definitions for terms used across VibeSQL documentation.
|
|
140
|
+
|
|
141
|
+
## AES-256-GCM
|
|
142
|
+
Advanced Encryption Standard with a 256-bit key in Galois/Counter Mode. Provides both confidentiality and authenticated integrity. Used by vsql-vault, vsql-backup, and vsql-sync for data encryption.
|
|
143
|
+
|
|
144
|
+
## Blob
|
|
145
|
+
An opaque binary object stored in vsql-vault. A blob has an ID, metadata, and an encrypted payload.
|
|
146
|
+
|
|
147
|
+
## CDE (Cardholder Data Environment)
|
|
148
|
+
The systems and processes that store, process, or transmit cardholder data. PCI DSS controls apply to everything in scope of the CDE.
|
|
149
|
+
|
|
150
|
+
## CRDT (Conflict-free Replicated Data Type)
|
|
151
|
+
A data structure designed so that concurrent updates on different nodes can always be merged without coordination. vsql-sync uses CRDTs for conflict resolution.
|
|
152
|
+
|
|
153
|
+
## DEK (Data Encryption Key)
|
|
154
|
+
The symmetric key that directly encrypts data. An AES-256-GCM key. Generated per blob (vault), per backup set (backup), or per session (sync). Always stored wrapped by a KEK.
|
|
155
|
+
|
|
156
|
+
## Directive
|
|
157
|
+
A policy document in CryptAply that governs a key or key family. Specifies algorithm, rotation schedule, allowed consumers, and expiry behavior.
|
|
158
|
+
|
|
159
|
+
## Ed25519
|
|
160
|
+
An elliptic-curve digital signature algorithm using Curve25519. Produces 64-byte signatures with fast verification. Used by vsql-sync for audit trail signing.
|
|
161
|
+
|
|
162
|
+
## Envelope Encryption
|
|
163
|
+
A two-layer encryption pattern: a DEK encrypts the data; a KEK encrypts (wraps) the DEK. Only the wrapped DEK is stored alongside the ciphertext.
|
|
164
|
+
|
|
165
|
+
## Hash Chain
|
|
166
|
+
A sequence of records where each record contains the hash of the previous record. Modification of any historical record invalidates all subsequent hashes.
|
|
167
|
+
|
|
168
|
+
## KEK (Key Encryption Key)
|
|
169
|
+
The key that wraps (encrypts) a DEK. Managed by CryptAply and never exposed to the storage layer. In VibeSQL, KEKs are RSA keys.
|
|
170
|
+
|
|
171
|
+
## LWW (Last Writer Wins)
|
|
172
|
+
A CRDT conflict resolution strategy. The update with the higher logical timestamp is kept. Used by vsql-sync.
|
|
173
|
+
|
|
174
|
+
## Merkle Tree
|
|
175
|
+
A binary tree of hashes for efficient partial verification of backup segments.
|
|
176
|
+
|
|
177
|
+
## PCI DSS
|
|
178
|
+
Payment Card Industry Data Security Standard. Relevant VibeSQL features: envelope encryption (Req 3), access logging (Req 10), column exclusion (scope reduction).
|
|
179
|
+
|
|
180
|
+
## PITR (Point-in-Time Recovery)
|
|
181
|
+
The ability to restore a database to any recorded moment in time, not just the most recent backup.
|
|
182
|
+
|
|
183
|
+
## Publication
|
|
184
|
+
A named configuration in vsql-sync defining which tables and columns replicate to which subscriber nodes.
|
|
185
|
+
|
|
186
|
+
## RSA
|
|
187
|
+
Asymmetric encryption algorithm used as the KEK algorithm for wrapping DEKs.
|
|
188
|
+
|
|
189
|
+
## SHA-256
|
|
190
|
+
Secure Hash Algorithm 2 with 256-bit output. Used for segment integrity hashes and hash chain linking.`,
|
|
191
|
+
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export function getHelp(topic: string): string {
|
|
195
|
+
const key = topic.toLowerCase().trim();
|
|
196
|
+
const content = HELP_TOPICS[key];
|
|
197
|
+
if (content) return content;
|
|
198
|
+
return `Unknown topic "${topic}". Available topics: ${Object.keys(HELP_TOPICS).join(', ')}`;
|
|
199
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { VibeClient } from './client.js';
|
|
5
|
+
import { getHelp, HELP_TOPICS } from './help.js';
|
|
6
|
+
|
|
7
|
+
// --- CLI args ---
|
|
8
|
+
|
|
9
|
+
function parseArgs(): string {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
if (args[i] === '--url' && args[i + 1]) {
|
|
13
|
+
return args[++i]!;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return process.env['VIBESQL_URL'] || 'http://localhost:5173';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const baseUrl = parseArgs();
|
|
20
|
+
const client = new VibeClient(baseUrl);
|
|
21
|
+
|
|
22
|
+
// No startup health check — MCP starts regardless.
|
|
23
|
+
// If vibesql-micro isn't running yet, individual tool calls return errors.
|
|
24
|
+
// This allows Claude to start vibesql-micro via bash, then use MCP tools.
|
|
25
|
+
|
|
26
|
+
// --- MCP Server ---
|
|
27
|
+
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: '@vibesql/mcp',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Database tools
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
server.tool(
|
|
38
|
+
'query',
|
|
39
|
+
'Execute a SQL query against the VibeSQL database. This tool is unrestricted — it can execute any valid SQL including DDL (CREATE, ALTER, DROP) and DML (INSERT, UPDATE, DELETE). This is intentional for a local development tool.',
|
|
40
|
+
{
|
|
41
|
+
sql: z.string().describe('SQL statement to execute'),
|
|
42
|
+
params: z.string().optional().describe('JSON array of query parameters'),
|
|
43
|
+
},
|
|
44
|
+
async ({ sql, params }) => {
|
|
45
|
+
try {
|
|
46
|
+
const parsedParams = params ? (JSON.parse(params) as unknown[]) : undefined;
|
|
47
|
+
const result = await client.query(sql, parsedParams);
|
|
48
|
+
const header = result.columns.join('\t');
|
|
49
|
+
const rows = result.rows.map((row) => (row as unknown[]).join('\t')).join('\n');
|
|
50
|
+
const text = [header, rows].filter(Boolean).join('\n');
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text' as const, text: text || '(no rows returned)' }],
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
server.tool(
|
|
64
|
+
'list_tables',
|
|
65
|
+
'List all tables in the database',
|
|
66
|
+
async () => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await client.listTables();
|
|
69
|
+
const tables = result.rows.map((row) => (row as unknown[])[0]);
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: 'text' as const, text: JSON.stringify(tables, null, 2) }],
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
|
76
|
+
isError: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
server.tool(
|
|
83
|
+
'describe_table',
|
|
84
|
+
'Get column schema for a table (column name, data type, nullable, default)',
|
|
85
|
+
{
|
|
86
|
+
table: z.string().describe('Table name to describe'),
|
|
87
|
+
},
|
|
88
|
+
async ({ table }) => {
|
|
89
|
+
try {
|
|
90
|
+
const result = await client.describeTable(table);
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
|
97
|
+
isError: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
server.tool(
|
|
104
|
+
'table_data',
|
|
105
|
+
'Browse rows from a table with pagination',
|
|
106
|
+
{
|
|
107
|
+
table: z.string().describe('Table name to query'),
|
|
108
|
+
limit: z.number().int().positive().optional().describe('Maximum rows to return (default 50)'),
|
|
109
|
+
offset: z.number().int().nonnegative().optional().describe('Rows to skip (default 0)'),
|
|
110
|
+
},
|
|
111
|
+
async ({ table, limit, offset }) => {
|
|
112
|
+
try {
|
|
113
|
+
const result = await client.tableData(table, limit ?? 50, offset ?? 0);
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
116
|
+
};
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
|
120
|
+
isError: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
'create_table',
|
|
128
|
+
'Create a new table. SQL must start with CREATE TABLE and must not contain semicolons.',
|
|
129
|
+
{
|
|
130
|
+
sql: z.string().describe('CREATE TABLE DDL statement'),
|
|
131
|
+
},
|
|
132
|
+
async ({ sql }) => {
|
|
133
|
+
try {
|
|
134
|
+
await client.createTable(sql);
|
|
135
|
+
const nameMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i);
|
|
136
|
+
const tableName = nameMatch ? nameMatch[1] : 'unknown';
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: 'text' as const, text: `Table "${tableName}" created successfully.` }],
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
|
143
|
+
isError: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
server.tool(
|
|
150
|
+
'insert_row',
|
|
151
|
+
'Insert a row into a table. Data is a JSON object of column-value pairs. Supported value types: string, number, boolean, null.',
|
|
152
|
+
{
|
|
153
|
+
table: z.string().describe('Table name'),
|
|
154
|
+
data: z.string().describe('JSON object of column→value pairs, e.g. {"name":"Alice","age":30}'),
|
|
155
|
+
},
|
|
156
|
+
async ({ table, data }) => {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(data) as Record<string, unknown>;
|
|
159
|
+
const result = await client.insertRow(table, parsed);
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Help tools
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
server.tool(
|
|
177
|
+
'help',
|
|
178
|
+
'Get help on a VibeSQL topic. Available topics: architecture, products, glossary',
|
|
179
|
+
{
|
|
180
|
+
topic: z.string().describe('Topic name (architecture, products, or glossary)'),
|
|
181
|
+
},
|
|
182
|
+
async ({ topic }) => {
|
|
183
|
+
const text = getHelp(topic);
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: 'text' as const, text }],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
server.tool(
|
|
191
|
+
'help_products',
|
|
192
|
+
'VibeSQL product family overview — all 7 products and how they connect',
|
|
193
|
+
async () => {
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: 'text' as const, text: HELP_TOPICS['products']! }],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
server.tool(
|
|
201
|
+
'help_architecture',
|
|
202
|
+
'VibeSQL architecture patterns — envelope encryption, hash chains, Merkle trees',
|
|
203
|
+
async () => {
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: 'text' as const, text: HELP_TOPICS['architecture']! }],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Start server
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
const transport = new StdioServerTransport();
|
|
215
|
+
await server.connect(transport);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"sourceMap": false
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|