db4app-mcp-server 0.1.5 → 0.1.7
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 +63 -42
- package/dist/index.js +118 -182
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# db4app-mcp-server
|
|
2
2
|
|
|
3
|
-
MCP server for db4.app - enables LLMs to interact with browser-based Postgres databases via Model Context Protocol.
|
|
3
|
+
MCP server for db4.app - enables LLMs to interact with browser-based Postgres databases via Model Context Protocol using the Postgres TCP protocol.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -16,6 +16,8 @@ npx db4app-mcp-server
|
|
|
16
16
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
|
+
**Important**: This MCP server uses the **Postgres TCP protocol** to connect directly to your browser database, just like `psql` or other Postgres clients. It uses TLS encryption and password authentication.
|
|
20
|
+
|
|
19
21
|
### With LM Studio
|
|
20
22
|
|
|
21
23
|
1. Locate your `mcp.json` file:
|
|
@@ -23,7 +25,12 @@ npx db4app-mcp-server
|
|
|
23
25
|
- **Windows:** `%APPDATA%\LM Studio\mcp.json`
|
|
24
26
|
- **Linux:** `~/.config/LM Studio/mcp.json`
|
|
25
27
|
|
|
26
|
-
2.
|
|
28
|
+
2. **Get your Connection ID and Auth Token**:
|
|
29
|
+
- Open the Database page in your browser
|
|
30
|
+
- Your **Connection ID** is displayed in the Connection Info section
|
|
31
|
+
- Your **Auth Token** is also shown in the Connection Info section (this is your password)
|
|
32
|
+
|
|
33
|
+
3. Add this to the `mcpServers` object in your `mcp.json`:
|
|
27
34
|
|
|
28
35
|
```json
|
|
29
36
|
{
|
|
@@ -32,35 +39,39 @@ npx db4app-mcp-server
|
|
|
32
39
|
"command": "npx",
|
|
33
40
|
"args": [
|
|
34
41
|
"-y",
|
|
35
|
-
"db4app-mcp-server"
|
|
42
|
+
"db4app-mcp-server@0.1.6"
|
|
36
43
|
],
|
|
37
44
|
"env": {
|
|
38
|
-
"
|
|
45
|
+
"MCP_POSTGRES_URL": "postgres://postgres:YOUR_AUTH_TOKEN@YOUR_CONNECTION_ID.pg.db4.app",
|
|
39
46
|
"LM_STUDIO_EMBEDDING_URL": "http://localhost:1234/v1/embeddings",
|
|
40
47
|
"LM_STUDIO_EMBEDDING_MODEL": "text-embedding-qwen3-embedding-4b",
|
|
41
|
-
"
|
|
42
|
-
"MCP_PUBLIC_KEY": "YOUR_PUBLIC_KEY_HERE",
|
|
43
|
-
"MCP_USE_ENCRYPTION": "true"
|
|
48
|
+
"MCP_SCHEMA": "rag_mcp"
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
**Note**:
|
|
55
|
+
**Note**:
|
|
56
|
+
- Replace `YOUR_AUTH_TOKEN` with your actual auth token from step 2
|
|
57
|
+
- Replace `YOUR_CONNECTION_ID` with your Connection ID from step 2
|
|
58
|
+
- The connection string format is: `postgres://postgres:AUTH_TOKEN@CONNECTION_ID.pg.db4.app`
|
|
59
|
+
- **Postgres TCP Protocol**: This server uses the standard Postgres wire protocol with TLS encryption, just like `psql` or DBeaver
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
4. **Install an Embedding Model** (Required for RAG features):
|
|
53
62
|
- Open LM Studio → Search tab
|
|
54
63
|
- Search for embedding models (e.g., "bge", "e5", "text-embedding")
|
|
55
64
|
- Download and load an embedding-capable model
|
|
56
65
|
- Update `LM_STUDIO_EMBEDDING_MODEL` in your `mcp.json` with the exact model name
|
|
57
66
|
- Enable headless server mode in LM Studio
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
5. Restart LM Studio to load the configuration.
|
|
60
69
|
|
|
61
70
|
### With Claude Desktop
|
|
62
71
|
|
|
63
|
-
|
|
72
|
+
1. **Get your Connection ID and Auth Token** (same as LM Studio setup above)
|
|
73
|
+
|
|
74
|
+
2. Add to your `claude_desktop_config.json`:
|
|
64
75
|
|
|
65
76
|
```json
|
|
66
77
|
{
|
|
@@ -69,66 +80,76 @@ Add to your `claude_desktop_config.json`:
|
|
|
69
80
|
"command": "npx",
|
|
70
81
|
"args": [
|
|
71
82
|
"-y",
|
|
72
|
-
"db4app-mcp-server"
|
|
83
|
+
"db4app-mcp-server@0.1.6"
|
|
73
84
|
],
|
|
74
85
|
"env": {
|
|
75
|
-
"
|
|
86
|
+
"MCP_POSTGRES_URL": "postgres://postgres:YOUR_AUTH_TOKEN@YOUR_CONNECTION_ID.pg.db4.app",
|
|
76
87
|
"LM_STUDIO_EMBEDDING_URL": "http://localhost:1234/v1/embeddings",
|
|
77
88
|
"LM_STUDIO_EMBEDDING_MODEL": "text-embedding-qwen3-embedding-4b",
|
|
78
|
-
"
|
|
79
|
-
"MCP_PUBLIC_KEY": "YOUR_PUBLIC_KEY_HERE",
|
|
80
|
-
"MCP_USE_ENCRYPTION": "true"
|
|
89
|
+
"MCP_SCHEMA": "rag_mcp"
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
```
|
|
86
95
|
|
|
87
|
-
**Note**:
|
|
96
|
+
**Note**:
|
|
97
|
+
- Replace `YOUR_AUTH_TOKEN` with your actual auth token
|
|
98
|
+
- Replace `YOUR_CONNECTION_ID` with your Connection ID
|
|
99
|
+
- The connection string format is: `postgres://postgres:AUTH_TOKEN@CONNECTION_ID.pg.db4.app`
|
|
88
100
|
|
|
89
101
|
## Configuration
|
|
90
102
|
|
|
91
103
|
All configuration is done via environment variables:
|
|
92
104
|
|
|
93
|
-
- `
|
|
105
|
+
- `MCP_POSTGRES_URL` - **Required**: Postgres connection string. Format: `postgres://postgres:AUTH_TOKEN@CONNECTION_ID.pg.db4.app`. Get your Connection ID and Auth Token from the Database page → Connection Info section.
|
|
106
|
+
- `MCP_CONNECTION_ID` - **Optional**: Browser connection ID. Used to construct connection URL if `MCP_POSTGRES_URL` is not provided (requires `MCP_AUTH_TOKEN`).
|
|
107
|
+
- `MCP_AUTH_TOKEN` - **Optional**: Auth token for authentication. Used to construct connection URL if `MCP_POSTGRES_URL` is not provided (requires `MCP_CONNECTION_ID`).
|
|
94
108
|
- `LM_STUDIO_EMBEDDING_URL` - LM Studio embedding API endpoint (default: `http://localhost:1234/v1/embeddings`)
|
|
95
109
|
- `LM_STUDIO_EMBEDDING_MODEL` - Optional model name for embeddings
|
|
96
|
-
- `
|
|
97
|
-
- `MCP_PUBLIC_KEY` - **Optional**: Base64-encoded public key for end-to-end encryption. If not provided, the MCP server will attempt to fetch it from the relay's `/public-key` endpoint. Get your public key from the Database page → Management tab → Encryption section.
|
|
98
|
-
- `MCP_USE_ENCRYPTION` - Enable end-to-end encryption (default: `true` if `MCP_PUBLIC_KEY` is set, otherwise `false`). When enabled, all SQL queries are encrypted with RSA-OAEP before being sent to the relay, ensuring the relay cannot decrypt them.
|
|
110
|
+
- `MCP_SCHEMA` - **Optional**: Schema name for RAG functions (`remember`, `search_memory`). Defaults to `public`. For recipes, set this to the recipe's schema (e.g., `rag_mcp`).
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
**Note**: This MCP server uses the **Postgres TCP protocol** with TLS encryption, just like standard Postgres clients. No HTTP or RSA-OAEP encryption is used.
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
- `list_tables` - List tables in the database (only shows whitelisted tables if `MCP_ALLOWED_TABLES` is set)
|
|
104
|
-
- `remember` - Store information in memory with automatic embedding (requires `MCP_ALLOWED_TABLES` for recipe use)
|
|
105
|
-
- `search_memory` - Search through stored memories using semantic similarity (requires `MCP_ALLOWED_TABLES` for recipe use)
|
|
106
|
-
|
|
107
|
-
**Note**: When used with recipes, `MCP_ALLOWED_TABLES` must be set to restrict access to only the recipe's tables. The relay server validates all queries against this whitelist.
|
|
108
|
-
|
|
109
|
-
## End-to-End Encryption
|
|
114
|
+
## Available Tools
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
- `query_database` - Execute SQL queries against the database
|
|
117
|
+
- `list_tables` - List tables in the database
|
|
118
|
+
- `remember` - Store information in memory with automatic embedding (uses `MCP_SCHEMA` for the target schema)
|
|
119
|
+
- `search_memory` - Search through stored memories using semantic similarity (uses `MCP_SCHEMA` for the target schema)
|
|
112
120
|
|
|
113
|
-
|
|
114
|
-
- The relay server relays encrypted queries without being able to decrypt them
|
|
115
|
-
- Only the browser (with the private key) can decrypt and execute queries
|
|
116
|
-
- This ensures complete privacy: even the relay cannot see your SQL queries
|
|
121
|
+
## Security
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
3. Set `MCP_USE_ENCRYPTION` to `"true"`
|
|
123
|
+
This MCP server uses the standard Postgres TCP protocol with:
|
|
124
|
+
- **TLS encryption**: All connections are encrypted using TLS (same as `psql` with SSL)
|
|
125
|
+
- **Password authentication**: Uses the auth token as the password
|
|
122
126
|
|
|
123
|
-
|
|
127
|
+
**Note**: With password authentication, anyone with the auth token can connect with any Postgres client and access all tables. The security model relies on:
|
|
128
|
+
- Not sharing the auth token
|
|
129
|
+
- Schema isolation (recipes operate in their own schemas)
|
|
130
|
+
- Standard Postgres access control (if you have the password, you have access)
|
|
124
131
|
|
|
125
132
|
## Requirements
|
|
126
133
|
|
|
127
134
|
- Node.js 18+
|
|
128
|
-
-
|
|
135
|
+
- Browser tab with db4.app open and relay connected
|
|
136
|
+
- Connection ID and Auth Token from the Database page
|
|
129
137
|
- For RAG features: LM Studio with embedding-capable model loaded
|
|
130
138
|
|
|
139
|
+
## Connection String Format
|
|
140
|
+
|
|
141
|
+
The connection string follows the standard Postgres URL format:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
postgres://postgres:AUTH_TOKEN@CONNECTION_ID.pg.db4.app
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Where:
|
|
148
|
+
- `postgres` is the username (fixed)
|
|
149
|
+
- `AUTH_TOKEN` is your auth token (password)
|
|
150
|
+
- `CONNECTION_ID` is your connection ID (hostname)
|
|
151
|
+
- Port defaults to 5432 (standard Postgres port)
|
|
152
|
+
|
|
131
153
|
## License
|
|
132
154
|
|
|
133
155
|
MIT
|
|
134
|
-
|
package/dist/index.js
CHANGED
|
@@ -14658,57 +14658,105 @@ var StdioServerTransport = class {
|
|
|
14658
14658
|
};
|
|
14659
14659
|
|
|
14660
14660
|
// src/index.js
|
|
14661
|
-
import
|
|
14662
|
-
var
|
|
14661
|
+
import pg from "pg";
|
|
14662
|
+
var { Client } = pg;
|
|
14663
|
+
var DEFAULT_POSTGRES_URL = process.env.MCP_POSTGRES_URL ?? null;
|
|
14663
14664
|
var DEFAULT_EMBEDDING_URL = process.env.LM_STUDIO_EMBEDDING_URL ?? "http://localhost:1234/v1/embeddings";
|
|
14664
14665
|
var DEFAULT_EMBEDDING_MODEL = process.env.LM_STUDIO_EMBEDDING_MODEL ?? "";
|
|
14665
|
-
var MCP_PUBLIC_KEY = process.env.MCP_PUBLIC_KEY ?? null;
|
|
14666
|
-
var USE_ENCRYPTION = process.env.MCP_USE_ENCRYPTION === "true" || MCP_PUBLIC_KEY !== null;
|
|
14667
14666
|
var MCP_CONNECTION_ID = process.env.MCP_CONNECTION_ID ?? null;
|
|
14667
|
+
var MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN ?? null;
|
|
14668
|
+
var MCP_SCHEMA = process.env.MCP_SCHEMA ?? "public";
|
|
14669
|
+
var clientPool = /* @__PURE__ */ new Map();
|
|
14670
|
+
function getClient(postgresUrl) {
|
|
14671
|
+
if (!postgresUrl) {
|
|
14672
|
+
throw new Error("Postgres connection URL is required. Set MCP_POSTGRES_URL or pass postgresUrl in the tool call.");
|
|
14673
|
+
}
|
|
14674
|
+
if (clientPool.has(postgresUrl)) {
|
|
14675
|
+
return clientPool.get(postgresUrl);
|
|
14676
|
+
}
|
|
14677
|
+
const client = new Client({
|
|
14678
|
+
connectionString: postgresUrl,
|
|
14679
|
+
ssl: postgresUrl.includes("db4.app") ? { rejectUnauthorized: true } : false
|
|
14680
|
+
});
|
|
14681
|
+
clientPool.set(postgresUrl, client);
|
|
14682
|
+
return client;
|
|
14683
|
+
}
|
|
14684
|
+
async function ensureConnected(client) {
|
|
14685
|
+
if (client._connected || client._ending) {
|
|
14686
|
+
return;
|
|
14687
|
+
}
|
|
14688
|
+
if (!client._connected) {
|
|
14689
|
+
const connectPromise = client.connect();
|
|
14690
|
+
const timeoutPromise = new Promise(
|
|
14691
|
+
(_, reject) => setTimeout(() => reject(new Error("Postgres connection timed out after 10 seconds")), 1e4)
|
|
14692
|
+
);
|
|
14693
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
14694
|
+
}
|
|
14695
|
+
}
|
|
14668
14696
|
var mcpServer = new McpServer({
|
|
14669
14697
|
name: "db4app-bridge",
|
|
14670
|
-
version: "0.
|
|
14698
|
+
version: "0.2.0"
|
|
14671
14699
|
});
|
|
14672
14700
|
var RunSqlSchema = external_exports.object({
|
|
14673
14701
|
sql: external_exports.string().min(1).describe("SQL statement to execute against the browser-backed Postgres engine"),
|
|
14674
14702
|
params: external_exports.array(external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()])).describe("Optional positional parameters for the SQL statement").optional(),
|
|
14675
|
-
|
|
14676
|
-
connectionId: external_exports.string().uuid().describe("Browser connection ID (
|
|
14703
|
+
postgresUrl: external_exports.string().url().describe("Override the Postgres connection URL (defaults to MCP_POSTGRES_URL). Format: postgres://postgres:AUTH_TOKEN@CONNECTION_ID.pg.db4.app").optional(),
|
|
14704
|
+
connectionId: external_exports.string().uuid().describe("Browser connection ID (used to construct connection URL if postgresUrl not provided, defaults to MCP_CONNECTION_ID)").optional(),
|
|
14705
|
+
authToken: external_exports.string().describe("Auth token for authentication (used to construct connection URL if postgresUrl not provided, defaults to MCP_AUTH_TOKEN)").optional()
|
|
14677
14706
|
});
|
|
14678
14707
|
var ListTablesSchema = external_exports.object({
|
|
14679
14708
|
schema: external_exports.string().describe("Schema name to filter by (default: public)").optional(),
|
|
14680
|
-
|
|
14681
|
-
connectionId: external_exports.string().uuid().describe("Browser connection ID (
|
|
14709
|
+
postgresUrl: external_exports.string().url().describe("Override the Postgres connection URL (defaults to MCP_POSTGRES_URL)").optional(),
|
|
14710
|
+
connectionId: external_exports.string().uuid().describe("Browser connection ID (used to construct connection URL if postgresUrl not provided)").optional(),
|
|
14711
|
+
authToken: external_exports.string().describe("Auth token for authentication (used to construct connection URL if postgresUrl not provided)").optional()
|
|
14682
14712
|
});
|
|
14683
14713
|
var RememberSchema = external_exports.object({
|
|
14684
14714
|
content: external_exports.string().min(1).describe("Information to remember and store in memory. This will be automatically embedded and indexed for semantic search."),
|
|
14685
14715
|
source: external_exports.string().describe('Optional source identifier for this memory (e.g. "conversation", "user_input", file path)').optional(),
|
|
14686
14716
|
metadata: external_exports.record(external_exports.string(), external_exports.unknown()).describe("Optional metadata to store alongside the memory (JSON object).").optional(),
|
|
14687
|
-
|
|
14717
|
+
postgresUrl: external_exports.string().url().describe("Override the Postgres connection URL (defaults to MCP_POSTGRES_URL)").optional(),
|
|
14688
14718
|
embeddingUrl: external_exports.string().url().describe("Override the LM Studio embedding endpoint (defaults to LM_STUDIO_EMBEDDING_URL or http://localhost:1234/v1/embeddings)").optional(),
|
|
14689
14719
|
embeddingModel: external_exports.string().describe("Optional model name for embedding generation (defaults to LM_STUDIO_EMBEDDING_MODEL)").optional(),
|
|
14690
|
-
connectionId: external_exports.string().uuid().describe("Browser connection ID (
|
|
14720
|
+
connectionId: external_exports.string().uuid().describe("Browser connection ID (used to construct connection URL if postgresUrl not provided)").optional(),
|
|
14721
|
+
authToken: external_exports.string().describe("Auth token for authentication (used to construct connection URL if postgresUrl not provided)").optional()
|
|
14691
14722
|
});
|
|
14692
14723
|
var SemanticSearchSchema = external_exports.object({
|
|
14693
14724
|
query: external_exports.string().min(1).describe("Natural language query for semantic search"),
|
|
14694
14725
|
embedding: external_exports.array(external_exports.number()).nonempty().describe("Embedding for the query. If not provided, will be computed automatically using LM Studio.").optional(),
|
|
14695
14726
|
topK: external_exports.number().int().positive().max(100).describe("Maximum number of matching chunks to return").default(5),
|
|
14696
14727
|
source: external_exports.string().describe("Optional source identifier to filter results by").optional(),
|
|
14697
|
-
|
|
14728
|
+
postgresUrl: external_exports.string().url().describe("Override the Postgres connection URL (defaults to MCP_POSTGRES_URL)").optional(),
|
|
14698
14729
|
embeddingUrl: external_exports.string().url().describe("Override the LM Studio embedding endpoint (used only if embedding is not provided)").optional(),
|
|
14699
14730
|
embeddingModel: external_exports.string().describe("Optional model name for embedding generation (used only if embedding is not provided)").optional(),
|
|
14700
|
-
connectionId: external_exports.string().uuid().describe("Browser connection ID (
|
|
14731
|
+
connectionId: external_exports.string().uuid().describe("Browser connection ID (used to construct connection URL if postgresUrl not provided)").optional(),
|
|
14732
|
+
authToken: external_exports.string().describe("Auth token for authentication (used to construct connection URL if postgresUrl not provided)").optional()
|
|
14701
14733
|
});
|
|
14734
|
+
function buildPostgresUrl(connectionId, authToken, providedUrl) {
|
|
14735
|
+
if (providedUrl) {
|
|
14736
|
+
return providedUrl;
|
|
14737
|
+
}
|
|
14738
|
+
const effectiveConnectionId = connectionId ?? MCP_CONNECTION_ID;
|
|
14739
|
+
const effectiveAuthToken = authToken ?? MCP_AUTH_TOKEN;
|
|
14740
|
+
if (!effectiveConnectionId || !effectiveAuthToken) {
|
|
14741
|
+
if (DEFAULT_POSTGRES_URL) {
|
|
14742
|
+
return DEFAULT_POSTGRES_URL;
|
|
14743
|
+
}
|
|
14744
|
+
throw new Error(
|
|
14745
|
+
"Postgres connection URL is required. Either provide postgresUrl, set MCP_POSTGRES_URL, or provide both connectionId and authToken (or set MCP_CONNECTION_ID and MCP_AUTH_TOKEN)."
|
|
14746
|
+
);
|
|
14747
|
+
}
|
|
14748
|
+
return `postgres://postgres:${encodeURIComponent(effectiveAuthToken)}@${effectiveConnectionId}.pg.db4.app`;
|
|
14749
|
+
}
|
|
14702
14750
|
mcpServer.registerTool(
|
|
14703
14751
|
"query_database",
|
|
14704
14752
|
{
|
|
14705
|
-
description: "Execute SQL queries against the database. Use this when you need to SELECT data, INSERT rows, UPDATE records, DELETE data, CREATE tables, or run any other SQL statement. This is the primary tool for interacting with the database schema and data.
|
|
14753
|
+
description: "Execute SQL queries against the database. Use this when you need to SELECT data, INSERT rows, UPDATE records, DELETE data, CREATE tables, or run any other SQL statement. This is the primary tool for interacting with the database schema and data.",
|
|
14706
14754
|
inputSchema: RunSqlSchema
|
|
14707
14755
|
},
|
|
14708
|
-
async ({ sql, params = [],
|
|
14756
|
+
async ({ sql, params = [], postgresUrl, connectionId, authToken }) => {
|
|
14709
14757
|
try {
|
|
14710
|
-
const
|
|
14711
|
-
const result = await executeSql(
|
|
14758
|
+
const url = buildPostgresUrl(connectionId, authToken, postgresUrl);
|
|
14759
|
+
const result = await executeSql(url, sql, params);
|
|
14712
14760
|
return formatToolResult(result);
|
|
14713
14761
|
} catch (error) {
|
|
14714
14762
|
return mcpServer.createToolError(error instanceof Error ? error.message : String(error));
|
|
@@ -14718,35 +14766,21 @@ mcpServer.registerTool(
|
|
|
14718
14766
|
mcpServer.registerTool(
|
|
14719
14767
|
"list_tables",
|
|
14720
14768
|
{
|
|
14721
|
-
description: "List tables in the database
|
|
14769
|
+
description: "List tables in the database. Use this when you need to discover what tables exist, explore the database schema, or find out what data is available before writing queries. Returns table names and their schemas.",
|
|
14722
14770
|
inputSchema: ListTablesSchema
|
|
14723
14771
|
},
|
|
14724
|
-
async ({ schema,
|
|
14725
|
-
const
|
|
14726
|
-
let sql = `
|
|
14772
|
+
async ({ schema, postgresUrl, connectionId, authToken }) => {
|
|
14773
|
+
const sql = `
|
|
14727
14774
|
SELECT table_schema, table_name
|
|
14728
14775
|
FROM information_schema.tables
|
|
14729
14776
|
WHERE table_type = 'BASE TABLE'
|
|
14730
14777
|
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
14731
14778
|
AND ($1::text IS NULL OR table_schema = $1::text)
|
|
14779
|
+
ORDER BY table_schema, table_name;
|
|
14732
14780
|
`;
|
|
14733
|
-
if (allowedTables.length > 0) {
|
|
14734
|
-
const tableFilters = allowedTables.map((table, idx) => {
|
|
14735
|
-
const [schemaName, tableName] = table.includes(".") ? table.split(".") : ["public", table];
|
|
14736
|
-
return `(table_schema = $${idx + 2}::text AND table_name = $${idx + 3}::text)`;
|
|
14737
|
-
});
|
|
14738
|
-
sql += ` AND (${tableFilters.join(" OR ")})`;
|
|
14739
|
-
}
|
|
14740
|
-
sql += ` ORDER BY table_schema, table_name;`;
|
|
14741
|
-
const params = [schema ?? null];
|
|
14742
|
-
if (allowedTables.length > 0) {
|
|
14743
|
-
allowedTables.forEach((table) => {
|
|
14744
|
-
const [schemaName, tableName] = table.includes(".") ? table.split(".") : ["public", table];
|
|
14745
|
-
params.push(schemaName, tableName);
|
|
14746
|
-
});
|
|
14747
|
-
}
|
|
14748
14781
|
try {
|
|
14749
|
-
const
|
|
14782
|
+
const url = buildPostgresUrl(connectionId, authToken, postgresUrl);
|
|
14783
|
+
const result = await executeSql(url, sql, [schema ?? null]);
|
|
14750
14784
|
return formatToolResult(result);
|
|
14751
14785
|
} catch (error) {
|
|
14752
14786
|
return mcpServer.createToolError(error instanceof Error ? error.message : String(error));
|
|
@@ -14759,18 +14793,18 @@ mcpServer.registerTool(
|
|
|
14759
14793
|
description: 'Store information in long-term memory for later retrieval. Use this when the user tells you something they want you to remember, such as personal facts, preferences, context from previous conversations, or any information that should persist across sessions. Automatically generates embeddings and makes the content searchable via semantic search. Examples: "Remember that I prefer dark mode", "Remember that Max is from Sweden", "Remember my API key is...".',
|
|
14760
14794
|
inputSchema: RememberSchema
|
|
14761
14795
|
},
|
|
14762
|
-
async ({ content, source = null, metadata = null,
|
|
14796
|
+
async ({ content, source = null, metadata = null, postgresUrl, embeddingUrl, embeddingModel, connectionId, authToken }) => {
|
|
14763
14797
|
try {
|
|
14764
14798
|
const embedding = await generateEmbedding(content, embeddingUrl, embeddingModel);
|
|
14765
|
-
const
|
|
14766
|
-
const schema = allowedTables.length > 0 && allowedTables[0].includes(".") ? allowedTables[0].split(".")[0] : "public";
|
|
14799
|
+
const schema = MCP_SCHEMA;
|
|
14767
14800
|
const sql = `
|
|
14768
14801
|
INSERT INTO ${schema}.documents (source, chunk_index, content, metadata, embedding)
|
|
14769
14802
|
VALUES ($1, $2, $3, $4::jsonb, $5::double precision[])
|
|
14770
14803
|
RETURNING id, source, chunk_index, content;
|
|
14771
14804
|
`;
|
|
14772
14805
|
const params = [source, null, content, metadata ? JSON.stringify(metadata) : null, embedding];
|
|
14773
|
-
const
|
|
14806
|
+
const url = buildPostgresUrl(connectionId, authToken, postgresUrl);
|
|
14807
|
+
const result = await executeSql(url, sql, params);
|
|
14774
14808
|
const insertedRow = result.rows[0];
|
|
14775
14809
|
return {
|
|
14776
14810
|
content: [
|
|
@@ -14797,14 +14831,13 @@ mcpServer.registerTool(
|
|
|
14797
14831
|
description: 'Search through previously stored memories and information using semantic similarity. Use this when the user asks about something you might have remembered earlier, or when you need to recall information from past conversations. Examples: "Where is Max from?", "What did I tell you about my preferences?", "What information do you have about X?". Automatically computes embeddings if not provided. Returns the most relevant stored information ranked by similarity.',
|
|
14798
14832
|
inputSchema: SemanticSearchSchema
|
|
14799
14833
|
},
|
|
14800
|
-
async ({ query, embedding, topK, source = null,
|
|
14834
|
+
async ({ query, embedding, topK, source = null, postgresUrl, embeddingUrl, embeddingModel, connectionId, authToken }) => {
|
|
14801
14835
|
try {
|
|
14802
14836
|
let queryEmbedding = embedding;
|
|
14803
14837
|
if (!queryEmbedding) {
|
|
14804
14838
|
queryEmbedding = await generateEmbedding(query, embeddingUrl, embeddingModel);
|
|
14805
14839
|
}
|
|
14806
|
-
const
|
|
14807
|
-
const schema = allowedTables.length > 0 && allowedTables[0].includes(".") ? allowedTables[0].split(".")[0] : "public";
|
|
14840
|
+
const schema = MCP_SCHEMA;
|
|
14808
14841
|
const sql = `
|
|
14809
14842
|
SELECT
|
|
14810
14843
|
id,
|
|
@@ -14820,7 +14853,8 @@ mcpServer.registerTool(
|
|
|
14820
14853
|
LIMIT $3::int;
|
|
14821
14854
|
`;
|
|
14822
14855
|
const params = [queryEmbedding, source, topK];
|
|
14823
|
-
const
|
|
14856
|
+
const url = buildPostgresUrl(connectionId, authToken, postgresUrl);
|
|
14857
|
+
const result = await executeSql(url, sql, params);
|
|
14824
14858
|
result.command = "RAG_SEMANTIC_SEARCH";
|
|
14825
14859
|
return formatToolResult(result);
|
|
14826
14860
|
} catch (error) {
|
|
@@ -14838,12 +14872,32 @@ async function generateEmbedding(text, embeddingUrl, model) {
|
|
|
14838
14872
|
}
|
|
14839
14873
|
let response;
|
|
14840
14874
|
try {
|
|
14875
|
+
const controller = new AbortController();
|
|
14876
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
14841
14877
|
response = await fetch(url, {
|
|
14842
14878
|
method: "POST",
|
|
14843
14879
|
headers: { "content-type": "application/json" },
|
|
14844
|
-
body: JSON.stringify(requestBody)
|
|
14880
|
+
body: JSON.stringify(requestBody),
|
|
14881
|
+
signal: controller.signal
|
|
14845
14882
|
});
|
|
14883
|
+
clearTimeout(timeoutId);
|
|
14846
14884
|
} catch (fetchError) {
|
|
14885
|
+
if (fetchError.name === "AbortError") {
|
|
14886
|
+
throw new Error(
|
|
14887
|
+
`Embedding request timed out after 30 seconds. This usually means:
|
|
14888
|
+
1. LM Studio is not responding (check if it's running)
|
|
14889
|
+
2. The model is not an embedding model (you need an embedding model, not a chat model)
|
|
14890
|
+
3. The embedding endpoint URL is incorrect
|
|
14891
|
+
|
|
14892
|
+
Current URL: ${url}
|
|
14893
|
+
Model: ${model || DEFAULT_EMBEDDING_MODEL || "not specified"}
|
|
14894
|
+
|
|
14895
|
+
Troubleshooting:
|
|
14896
|
+
- Make sure LM Studio headless server is running
|
|
14897
|
+
- Load an EMBEDDING model (e.g., "BAAI/bge-small-en-v1.5", "text-embedding-qwen3-embedding-4b")
|
|
14898
|
+
- Check the embedding endpoint URL (default: http://localhost:1234/v1/embeddings)`
|
|
14899
|
+
);
|
|
14900
|
+
}
|
|
14847
14901
|
const baseMessage = fetchError instanceof TypeError && fetchError.message.includes("fetch") ? `Failed to connect to LM Studio embedding endpoint at ${url}` : `Network error calling LM Studio embedding API: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`;
|
|
14848
14902
|
const suggestion = `
|
|
14849
14903
|
|
|
@@ -14873,144 +14927,26 @@ Troubleshooting:
|
|
|
14873
14927
|
}
|
|
14874
14928
|
throw new Error("Unexpected embedding response format from LM Studio");
|
|
14875
14929
|
}
|
|
14876
|
-
async function
|
|
14877
|
-
const
|
|
14878
|
-
return webcrypto.subtle.importKey(
|
|
14879
|
-
"spki",
|
|
14880
|
-
buffer,
|
|
14881
|
-
{
|
|
14882
|
-
name: "RSA-OAEP",
|
|
14883
|
-
hash: "SHA-256"
|
|
14884
|
-
},
|
|
14885
|
-
true,
|
|
14886
|
-
["encrypt"]
|
|
14887
|
-
);
|
|
14888
|
-
}
|
|
14889
|
-
async function encryptWithPublicKey(publicKey, data) {
|
|
14890
|
-
const encoder = new TextEncoder();
|
|
14891
|
-
const dataBuffer = encoder.encode(data);
|
|
14892
|
-
const encrypted = await webcrypto.subtle.encrypt(
|
|
14893
|
-
{
|
|
14894
|
-
name: "RSA-OAEP"
|
|
14895
|
-
},
|
|
14896
|
-
publicKey,
|
|
14897
|
-
dataBuffer
|
|
14898
|
-
);
|
|
14899
|
-
return Buffer.from(encrypted).toString("base64");
|
|
14900
|
-
}
|
|
14901
|
-
async function fetchPublicKey(relayUrl, connectionId) {
|
|
14930
|
+
async function executeSql(postgresUrl, sql, params = []) {
|
|
14931
|
+
const client = getClient(postgresUrl);
|
|
14902
14932
|
try {
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
|
|
14907
|
-
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
14919
|
-
const publicKeyUrl = relayUrlObj.toString();
|
|
14920
|
-
const response = await fetch(publicKeyUrl);
|
|
14921
|
-
if (!response.ok) {
|
|
14922
|
-
throw new Error(`Failed to fetch public key: ${response.status}`);
|
|
14923
|
-
}
|
|
14924
|
-
const data = await response.json();
|
|
14925
|
-
return data.publicKey;
|
|
14926
|
-
} catch (err) {
|
|
14927
|
-
throw new Error(`Failed to fetch public key from relay: ${err.message}`);
|
|
14928
|
-
}
|
|
14929
|
-
}
|
|
14930
|
-
var publicKeyCache = /* @__PURE__ */ new Map();
|
|
14931
|
-
async function getPublicKey(relayUrl, connectionId) {
|
|
14932
|
-
const cacheKey = connectionId ?? "default";
|
|
14933
|
-
if (publicKeyCache.has(cacheKey)) {
|
|
14934
|
-
return publicKeyCache.get(cacheKey);
|
|
14935
|
-
}
|
|
14936
|
-
let publicKeyString = MCP_PUBLIC_KEY;
|
|
14937
|
-
if (!publicKeyString) {
|
|
14938
|
-
if (!connectionId && !MCP_CONNECTION_ID) {
|
|
14939
|
-
throw new Error("Connection ID is required to fetch public key. Set MCP_CONNECTION_ID or pass connectionId.");
|
|
14940
|
-
}
|
|
14941
|
-
publicKeyString = await fetchPublicKey(relayUrl, connectionId ?? MCP_CONNECTION_ID);
|
|
14942
|
-
}
|
|
14943
|
-
const publicKey = await importPublicKey(publicKeyString);
|
|
14944
|
-
publicKeyCache.set(cacheKey, publicKey);
|
|
14945
|
-
return publicKey;
|
|
14946
|
-
}
|
|
14947
|
-
async function executeSql(relayUrl, sql, params, allowedTables, connectionId) {
|
|
14948
|
-
if (!relayUrl) {
|
|
14949
|
-
throw new Error("Relay URL is not set. Provide MCP_RELAY_HTTP_URL or pass relayUrl in the tool call.");
|
|
14950
|
-
}
|
|
14951
|
-
const relayUrlObj = new URL(relayUrl);
|
|
14952
|
-
const pathParts = relayUrlObj.pathname.split("/").filter(Boolean);
|
|
14953
|
-
let queryUrl = relayUrl;
|
|
14954
|
-
let effectiveConnectionId = connectionId;
|
|
14955
|
-
if (pathParts.length >= 2 && pathParts[1] === "query") {
|
|
14956
|
-
effectiveConnectionId = pathParts[0];
|
|
14957
|
-
queryUrl = relayUrl;
|
|
14958
|
-
} else if (pathParts[0] === "query" && pathParts[1]) {
|
|
14959
|
-
effectiveConnectionId = pathParts[1];
|
|
14960
|
-
queryUrl = relayUrl;
|
|
14961
|
-
} else {
|
|
14962
|
-
effectiveConnectionId = connectionId ?? MCP_CONNECTION_ID;
|
|
14963
|
-
if (!effectiveConnectionId) {
|
|
14964
|
-
throw new Error(
|
|
14965
|
-
"Connection ID is required. Either include it in MCP_RELAY_HTTP_URL as /{connectionId}/query, or set MCP_CONNECTION_ID environment variable, or pass connectionId in the tool call."
|
|
14966
|
-
);
|
|
14967
|
-
}
|
|
14968
|
-
relayUrlObj.pathname = `/${effectiveConnectionId}/query`;
|
|
14969
|
-
queryUrl = relayUrlObj.toString();
|
|
14970
|
-
}
|
|
14971
|
-
let requestBody;
|
|
14972
|
-
if (USE_ENCRYPTION) {
|
|
14973
|
-
try {
|
|
14974
|
-
const publicKey = await getPublicKey(relayUrl, effectiveConnectionId);
|
|
14975
|
-
const payloadToEncrypt = JSON.stringify({ sql, params });
|
|
14976
|
-
const encryptedPayload = await encryptWithPublicKey(publicKey, payloadToEncrypt);
|
|
14977
|
-
requestBody = {
|
|
14978
|
-
encrypted: true,
|
|
14979
|
-
encryptedPayload
|
|
14980
|
-
};
|
|
14981
|
-
if (allowedTables && allowedTables.length > 0) {
|
|
14982
|
-
requestBody.allowedTables = allowedTables;
|
|
14983
|
-
}
|
|
14984
|
-
} catch (encryptErr) {
|
|
14985
|
-
console.error("[MCP] Encryption failed, falling back to unencrypted:", encryptErr.message);
|
|
14986
|
-
requestBody = { sql, params };
|
|
14987
|
-
if (allowedTables && allowedTables.length > 0) {
|
|
14988
|
-
requestBody.allowedTables = allowedTables;
|
|
14989
|
-
}
|
|
14990
|
-
}
|
|
14991
|
-
} else {
|
|
14992
|
-
requestBody = { sql, params };
|
|
14993
|
-
if (allowedTables && allowedTables.length > 0) {
|
|
14994
|
-
requestBody.allowedTables = allowedTables;
|
|
14995
|
-
}
|
|
14996
|
-
}
|
|
14997
|
-
console.log("[MCP] Executing SQL:", { queryUrl, effectiveConnectionId, sql: sql.substring(0, 100) + "..." });
|
|
14998
|
-
const response = await fetch(queryUrl, {
|
|
14999
|
-
method: "POST",
|
|
15000
|
-
headers: { "content-type": "application/json" },
|
|
15001
|
-
body: JSON.stringify(requestBody)
|
|
15002
|
-
});
|
|
15003
|
-
if (!response.ok) {
|
|
15004
|
-
const errorData = await response.json().catch(() => ({}));
|
|
15005
|
-
const errorMessage = errorData.error ?? `Relay request failed (${response.status})`;
|
|
15006
|
-
console.error("[MCP] Relay error:", { status: response.status, statusText: response.statusText, error: errorMessage, url: queryUrl });
|
|
15007
|
-
throw new Error(errorMessage);
|
|
15008
|
-
}
|
|
15009
|
-
const payload = await response.json();
|
|
15010
|
-
if (!payload.ok) {
|
|
15011
|
-
throw new Error(payload.error ?? "Relay returned an error");
|
|
14933
|
+
await ensureConnected(client);
|
|
14934
|
+
console.log("[MCP] Executing SQL:", { postgresUrl: postgresUrl.replace(/:[^:@]+@/, ":****@"), sql: sql.substring(0, 100) + "..." });
|
|
14935
|
+
const queryPromise = client.query(sql, params);
|
|
14936
|
+
const timeoutPromise = new Promise(
|
|
14937
|
+
(_, reject) => setTimeout(() => reject(new Error("Postgres query timed out after 30 seconds")), 3e4)
|
|
14938
|
+
);
|
|
14939
|
+
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
14940
|
+
return {
|
|
14941
|
+
columns: result.fields?.map((f) => f.name) ?? [],
|
|
14942
|
+
rows: result.rows ?? [],
|
|
14943
|
+
command: result.command ?? "QUERY",
|
|
14944
|
+
rowCount: result.rowCount ?? 0
|
|
14945
|
+
};
|
|
14946
|
+
} catch (error) {
|
|
14947
|
+
console.error("[MCP] Postgres error:", error.message);
|
|
14948
|
+
throw error;
|
|
15012
14949
|
}
|
|
15013
|
-
return payload.result ?? { columns: [], rows: [], command: "QUERY", rowCount: 0 };
|
|
15014
14950
|
}
|
|
15015
14951
|
function formatToolResult(result) {
|
|
15016
14952
|
const { columns = [], rows = [], command = "QUERY", rowCount = rows.length ?? 0 } = result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "db4app-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "MCP server for db4.app - enables LLMs to interact with browser-based Postgres databases via Model Context Protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/index.js --external:node:*",
|
|
16
|
+
"build": "esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/index.js --external:node:* --external:pg --external:pg-native",
|
|
17
17
|
"lint": "eslint src --ext .js",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
19
|
"start": "node dist/index.js"
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
43
|
+
"pg": "^8.11.3",
|
|
43
44
|
"zod": "^3.25.76"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|