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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js');
@@ -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
@@ -0,0 +1,2 @@
1
+ export declare const HELP_TOPICS: Record<string, string>;
2
+ export declare function getHelp(topic: string): string;
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
+ }
@@ -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
+ }