db4app-mcp-server 0.1.4 → 0.1.6
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 +61 -40
- package/dist/index.js +86 -180
- 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
|
{
|
|
@@ -35,32 +42,36 @@ npx db4app-mcp-server
|
|
|
35
42
|
"db4app-mcp-server"
|
|
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
|
{
|
|
@@ -72,63 +83,73 @@ Add to your `claude_desktop_config.json`:
|
|
|
72
83
|
"db4app-mcp-server"
|
|
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,98 @@ 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._ending || client._ending === void 0) {
|
|
14686
|
+
await client.connect();
|
|
14687
|
+
}
|
|
14688
|
+
}
|
|
14668
14689
|
var mcpServer = new McpServer({
|
|
14669
14690
|
name: "db4app-bridge",
|
|
14670
|
-
version: "0.
|
|
14691
|
+
version: "0.2.0"
|
|
14671
14692
|
});
|
|
14672
14693
|
var RunSqlSchema = external_exports.object({
|
|
14673
14694
|
sql: external_exports.string().min(1).describe("SQL statement to execute against the browser-backed Postgres engine"),
|
|
14674
14695
|
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 (
|
|
14696
|
+
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(),
|
|
14697
|
+
connectionId: external_exports.string().uuid().describe("Browser connection ID (used to construct connection URL if postgresUrl not provided, defaults to MCP_CONNECTION_ID)").optional(),
|
|
14698
|
+
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
14699
|
});
|
|
14678
14700
|
var ListTablesSchema = external_exports.object({
|
|
14679
14701
|
schema: external_exports.string().describe("Schema name to filter by (default: public)").optional(),
|
|
14680
|
-
|
|
14681
|
-
connectionId: external_exports.string().uuid().describe("Browser connection ID (
|
|
14702
|
+
postgresUrl: external_exports.string().url().describe("Override the Postgres connection URL (defaults to MCP_POSTGRES_URL)").optional(),
|
|
14703
|
+
connectionId: external_exports.string().uuid().describe("Browser connection ID (used to construct connection URL if postgresUrl not provided)").optional(),
|
|
14704
|
+
authToken: external_exports.string().describe("Auth token for authentication (used to construct connection URL if postgresUrl not provided)").optional()
|
|
14682
14705
|
});
|
|
14683
14706
|
var RememberSchema = external_exports.object({
|
|
14684
14707
|
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
14708
|
source: external_exports.string().describe('Optional source identifier for this memory (e.g. "conversation", "user_input", file path)').optional(),
|
|
14686
14709
|
metadata: external_exports.record(external_exports.string(), external_exports.unknown()).describe("Optional metadata to store alongside the memory (JSON object).").optional(),
|
|
14687
|
-
|
|
14710
|
+
postgresUrl: external_exports.string().url().describe("Override the Postgres connection URL (defaults to MCP_POSTGRES_URL)").optional(),
|
|
14688
14711
|
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
14712
|
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 (
|
|
14713
|
+
connectionId: external_exports.string().uuid().describe("Browser connection ID (used to construct connection URL if postgresUrl not provided)").optional(),
|
|
14714
|
+
authToken: external_exports.string().describe("Auth token for authentication (used to construct connection URL if postgresUrl not provided)").optional()
|
|
14691
14715
|
});
|
|
14692
14716
|
var SemanticSearchSchema = external_exports.object({
|
|
14693
14717
|
query: external_exports.string().min(1).describe("Natural language query for semantic search"),
|
|
14694
14718
|
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
14719
|
topK: external_exports.number().int().positive().max(100).describe("Maximum number of matching chunks to return").default(5),
|
|
14696
14720
|
source: external_exports.string().describe("Optional source identifier to filter results by").optional(),
|
|
14697
|
-
|
|
14721
|
+
postgresUrl: external_exports.string().url().describe("Override the Postgres connection URL (defaults to MCP_POSTGRES_URL)").optional(),
|
|
14698
14722
|
embeddingUrl: external_exports.string().url().describe("Override the LM Studio embedding endpoint (used only if embedding is not provided)").optional(),
|
|
14699
14723
|
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 (
|
|
14724
|
+
connectionId: external_exports.string().uuid().describe("Browser connection ID (used to construct connection URL if postgresUrl not provided)").optional(),
|
|
14725
|
+
authToken: external_exports.string().describe("Auth token for authentication (used to construct connection URL if postgresUrl not provided)").optional()
|
|
14701
14726
|
});
|
|
14727
|
+
function buildPostgresUrl(connectionId, authToken, providedUrl) {
|
|
14728
|
+
if (providedUrl) {
|
|
14729
|
+
return providedUrl;
|
|
14730
|
+
}
|
|
14731
|
+
const effectiveConnectionId = connectionId ?? MCP_CONNECTION_ID;
|
|
14732
|
+
const effectiveAuthToken = authToken ?? MCP_AUTH_TOKEN;
|
|
14733
|
+
if (!effectiveConnectionId || !effectiveAuthToken) {
|
|
14734
|
+
if (DEFAULT_POSTGRES_URL) {
|
|
14735
|
+
return DEFAULT_POSTGRES_URL;
|
|
14736
|
+
}
|
|
14737
|
+
throw new Error(
|
|
14738
|
+
"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)."
|
|
14739
|
+
);
|
|
14740
|
+
}
|
|
14741
|
+
return `postgres://postgres:${encodeURIComponent(effectiveAuthToken)}@${effectiveConnectionId}.pg.db4.app`;
|
|
14742
|
+
}
|
|
14702
14743
|
mcpServer.registerTool(
|
|
14703
14744
|
"query_database",
|
|
14704
14745
|
{
|
|
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.
|
|
14746
|
+
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
14747
|
inputSchema: RunSqlSchema
|
|
14707
14748
|
},
|
|
14708
|
-
async ({ sql, params = [],
|
|
14749
|
+
async ({ sql, params = [], postgresUrl, connectionId, authToken }) => {
|
|
14709
14750
|
try {
|
|
14710
|
-
const
|
|
14711
|
-
const result = await executeSql(
|
|
14751
|
+
const url = buildPostgresUrl(connectionId, authToken, postgresUrl);
|
|
14752
|
+
const result = await executeSql(url, sql, params);
|
|
14712
14753
|
return formatToolResult(result);
|
|
14713
14754
|
} catch (error) {
|
|
14714
14755
|
return mcpServer.createToolError(error instanceof Error ? error.message : String(error));
|
|
@@ -14718,35 +14759,21 @@ mcpServer.registerTool(
|
|
|
14718
14759
|
mcpServer.registerTool(
|
|
14719
14760
|
"list_tables",
|
|
14720
14761
|
{
|
|
14721
|
-
description: "List tables in the database
|
|
14762
|
+
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
14763
|
inputSchema: ListTablesSchema
|
|
14723
14764
|
},
|
|
14724
|
-
async ({ schema,
|
|
14725
|
-
const
|
|
14726
|
-
let sql = `
|
|
14765
|
+
async ({ schema, postgresUrl, connectionId, authToken }) => {
|
|
14766
|
+
const sql = `
|
|
14727
14767
|
SELECT table_schema, table_name
|
|
14728
14768
|
FROM information_schema.tables
|
|
14729
14769
|
WHERE table_type = 'BASE TABLE'
|
|
14730
14770
|
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
14731
14771
|
AND ($1::text IS NULL OR table_schema = $1::text)
|
|
14772
|
+
ORDER BY table_schema, table_name;
|
|
14732
14773
|
`;
|
|
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
14774
|
try {
|
|
14749
|
-
const
|
|
14775
|
+
const url = buildPostgresUrl(connectionId, authToken, postgresUrl);
|
|
14776
|
+
const result = await executeSql(url, sql, [schema ?? null]);
|
|
14750
14777
|
return formatToolResult(result);
|
|
14751
14778
|
} catch (error) {
|
|
14752
14779
|
return mcpServer.createToolError(error instanceof Error ? error.message : String(error));
|
|
@@ -14759,18 +14786,18 @@ mcpServer.registerTool(
|
|
|
14759
14786
|
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
14787
|
inputSchema: RememberSchema
|
|
14761
14788
|
},
|
|
14762
|
-
async ({ content, source = null, metadata = null,
|
|
14789
|
+
async ({ content, source = null, metadata = null, postgresUrl, embeddingUrl, embeddingModel, connectionId, authToken }) => {
|
|
14763
14790
|
try {
|
|
14764
14791
|
const embedding = await generateEmbedding(content, embeddingUrl, embeddingModel);
|
|
14765
|
-
const
|
|
14766
|
-
const schema = allowedTables.length > 0 && allowedTables[0].includes(".") ? allowedTables[0].split(".")[0] : "public";
|
|
14792
|
+
const schema = MCP_SCHEMA;
|
|
14767
14793
|
const sql = `
|
|
14768
14794
|
INSERT INTO ${schema}.documents (source, chunk_index, content, metadata, embedding)
|
|
14769
14795
|
VALUES ($1, $2, $3, $4::jsonb, $5::double precision[])
|
|
14770
14796
|
RETURNING id, source, chunk_index, content;
|
|
14771
14797
|
`;
|
|
14772
14798
|
const params = [source, null, content, metadata ? JSON.stringify(metadata) : null, embedding];
|
|
14773
|
-
const
|
|
14799
|
+
const url = buildPostgresUrl(connectionId, authToken, postgresUrl);
|
|
14800
|
+
const result = await executeSql(url, sql, params);
|
|
14774
14801
|
const insertedRow = result.rows[0];
|
|
14775
14802
|
return {
|
|
14776
14803
|
content: [
|
|
@@ -14797,14 +14824,13 @@ mcpServer.registerTool(
|
|
|
14797
14824
|
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
14825
|
inputSchema: SemanticSearchSchema
|
|
14799
14826
|
},
|
|
14800
|
-
async ({ query, embedding, topK, source = null,
|
|
14827
|
+
async ({ query, embedding, topK, source = null, postgresUrl, embeddingUrl, embeddingModel, connectionId, authToken }) => {
|
|
14801
14828
|
try {
|
|
14802
14829
|
let queryEmbedding = embedding;
|
|
14803
14830
|
if (!queryEmbedding) {
|
|
14804
14831
|
queryEmbedding = await generateEmbedding(query, embeddingUrl, embeddingModel);
|
|
14805
14832
|
}
|
|
14806
|
-
const
|
|
14807
|
-
const schema = allowedTables.length > 0 && allowedTables[0].includes(".") ? allowedTables[0].split(".")[0] : "public";
|
|
14833
|
+
const schema = MCP_SCHEMA;
|
|
14808
14834
|
const sql = `
|
|
14809
14835
|
SELECT
|
|
14810
14836
|
id,
|
|
@@ -14820,7 +14846,8 @@ mcpServer.registerTool(
|
|
|
14820
14846
|
LIMIT $3::int;
|
|
14821
14847
|
`;
|
|
14822
14848
|
const params = [queryEmbedding, source, topK];
|
|
14823
|
-
const
|
|
14849
|
+
const url = buildPostgresUrl(connectionId, authToken, postgresUrl);
|
|
14850
|
+
const result = await executeSql(url, sql, params);
|
|
14824
14851
|
result.command = "RAG_SEMANTIC_SEARCH";
|
|
14825
14852
|
return formatToolResult(result);
|
|
14826
14853
|
} catch (error) {
|
|
@@ -14873,143 +14900,22 @@ Troubleshooting:
|
|
|
14873
14900
|
}
|
|
14874
14901
|
throw new Error("Unexpected embedding response format from LM Studio");
|
|
14875
14902
|
}
|
|
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) {
|
|
14903
|
+
async function executeSql(postgresUrl, sql, params = []) {
|
|
14904
|
+
const client = getClient(postgresUrl);
|
|
14902
14905
|
try {
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
|
|
14907
|
-
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
if (!effectiveConnectionId) {
|
|
14916
|
-
throw new Error("Connection ID is required to fetch public key. Include it in MCP_RELAY_HTTP_URL as /{connectionId}/query or set MCP_CONNECTION_ID");
|
|
14917
|
-
}
|
|
14918
|
-
const basePath = relayUrlObj.pathname.replace(/\/.*$/, "") || "";
|
|
14919
|
-
relayUrlObj.pathname = `${basePath}/${effectiveConnectionId}/public-key`;
|
|
14920
|
-
const publicKeyUrl = relayUrlObj.toString();
|
|
14921
|
-
const response = await fetch(publicKeyUrl);
|
|
14922
|
-
if (!response.ok) {
|
|
14923
|
-
throw new Error(`Failed to fetch public key: ${response.status}`);
|
|
14924
|
-
}
|
|
14925
|
-
const data = await response.json();
|
|
14926
|
-
return data.publicKey;
|
|
14927
|
-
} catch (err) {
|
|
14928
|
-
throw new Error(`Failed to fetch public key from relay: ${err.message}`);
|
|
14929
|
-
}
|
|
14930
|
-
}
|
|
14931
|
-
var publicKeyCache = /* @__PURE__ */ new Map();
|
|
14932
|
-
async function getPublicKey(relayUrl, connectionId) {
|
|
14933
|
-
const cacheKey = connectionId ?? "default";
|
|
14934
|
-
if (publicKeyCache.has(cacheKey)) {
|
|
14935
|
-
return publicKeyCache.get(cacheKey);
|
|
14936
|
-
}
|
|
14937
|
-
let publicKeyString = MCP_PUBLIC_KEY;
|
|
14938
|
-
if (!publicKeyString) {
|
|
14939
|
-
if (!connectionId && !MCP_CONNECTION_ID) {
|
|
14940
|
-
throw new Error("Connection ID is required to fetch public key. Set MCP_CONNECTION_ID or pass connectionId.");
|
|
14941
|
-
}
|
|
14942
|
-
publicKeyString = await fetchPublicKey(relayUrl, connectionId ?? MCP_CONNECTION_ID);
|
|
14943
|
-
}
|
|
14944
|
-
const publicKey = await importPublicKey(publicKeyString);
|
|
14945
|
-
publicKeyCache.set(cacheKey, publicKey);
|
|
14946
|
-
return publicKey;
|
|
14947
|
-
}
|
|
14948
|
-
async function executeSql(relayUrl, sql, params, allowedTables, connectionId) {
|
|
14949
|
-
if (!relayUrl) {
|
|
14950
|
-
throw new Error("Relay URL is not set. Provide MCP_RELAY_HTTP_URL or pass relayUrl in the tool call.");
|
|
14951
|
-
}
|
|
14952
|
-
const relayUrlObj = new URL(relayUrl);
|
|
14953
|
-
const pathParts = relayUrlObj.pathname.split("/").filter(Boolean);
|
|
14954
|
-
let queryUrl = relayUrl;
|
|
14955
|
-
let effectiveConnectionId = connectionId;
|
|
14956
|
-
if (pathParts.length >= 2 && pathParts[1] === "query") {
|
|
14957
|
-
effectiveConnectionId = pathParts[0];
|
|
14958
|
-
queryUrl = relayUrl;
|
|
14959
|
-
} else if (pathParts[0] === "query" && pathParts[1]) {
|
|
14960
|
-
effectiveConnectionId = pathParts[1];
|
|
14961
|
-
queryUrl = relayUrl;
|
|
14962
|
-
} else {
|
|
14963
|
-
effectiveConnectionId = connectionId ?? MCP_CONNECTION_ID;
|
|
14964
|
-
if (!effectiveConnectionId) {
|
|
14965
|
-
throw new Error(
|
|
14966
|
-
"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."
|
|
14967
|
-
);
|
|
14968
|
-
}
|
|
14969
|
-
const basePath = relayUrlObj.pathname.replace(/\/.*$/, "") || "";
|
|
14970
|
-
relayUrlObj.pathname = `${basePath}/${effectiveConnectionId}/query`;
|
|
14971
|
-
queryUrl = relayUrlObj.toString();
|
|
14972
|
-
}
|
|
14973
|
-
let requestBody;
|
|
14974
|
-
if (USE_ENCRYPTION) {
|
|
14975
|
-
try {
|
|
14976
|
-
const publicKey = await getPublicKey(relayUrl, effectiveConnectionId);
|
|
14977
|
-
const payloadToEncrypt = JSON.stringify({ sql, params });
|
|
14978
|
-
const encryptedPayload = await encryptWithPublicKey(publicKey, payloadToEncrypt);
|
|
14979
|
-
requestBody = {
|
|
14980
|
-
encrypted: true,
|
|
14981
|
-
encryptedPayload
|
|
14982
|
-
};
|
|
14983
|
-
if (allowedTables && allowedTables.length > 0) {
|
|
14984
|
-
requestBody.allowedTables = allowedTables;
|
|
14985
|
-
}
|
|
14986
|
-
} catch (encryptErr) {
|
|
14987
|
-
console.error("[MCP] Encryption failed, falling back to unencrypted:", encryptErr.message);
|
|
14988
|
-
requestBody = { sql, params };
|
|
14989
|
-
if (allowedTables && allowedTables.length > 0) {
|
|
14990
|
-
requestBody.allowedTables = allowedTables;
|
|
14991
|
-
}
|
|
14992
|
-
}
|
|
14993
|
-
} else {
|
|
14994
|
-
requestBody = { sql, params };
|
|
14995
|
-
if (allowedTables && allowedTables.length > 0) {
|
|
14996
|
-
requestBody.allowedTables = allowedTables;
|
|
14997
|
-
}
|
|
14998
|
-
}
|
|
14999
|
-
const response = await fetch(queryUrl, {
|
|
15000
|
-
method: "POST",
|
|
15001
|
-
headers: { "content-type": "application/json" },
|
|
15002
|
-
body: JSON.stringify(requestBody)
|
|
15003
|
-
});
|
|
15004
|
-
if (!response.ok) {
|
|
15005
|
-
const errorData = await response.json().catch(() => ({}));
|
|
15006
|
-
throw new Error(errorData.error ?? `Relay request failed (${response.status})`);
|
|
15007
|
-
}
|
|
15008
|
-
const payload = await response.json();
|
|
15009
|
-
if (!payload.ok) {
|
|
15010
|
-
throw new Error(payload.error ?? "Relay returned an error");
|
|
14906
|
+
await ensureConnected(client);
|
|
14907
|
+
console.log("[MCP] Executing SQL:", { postgresUrl: postgresUrl.replace(/:[^:@]+@/, ":****@"), sql: sql.substring(0, 100) + "..." });
|
|
14908
|
+
const result = await client.query(sql, params);
|
|
14909
|
+
return {
|
|
14910
|
+
columns: result.fields?.map((f) => f.name) ?? [],
|
|
14911
|
+
rows: result.rows ?? [],
|
|
14912
|
+
command: result.command ?? "QUERY",
|
|
14913
|
+
rowCount: result.rowCount ?? 0
|
|
14914
|
+
};
|
|
14915
|
+
} catch (error) {
|
|
14916
|
+
console.error("[MCP] Postgres error:", error.message);
|
|
14917
|
+
throw error;
|
|
15011
14918
|
}
|
|
15012
|
-
return payload.result ?? { columns: [], rows: [], command: "QUERY", rowCount: 0 };
|
|
15013
14919
|
}
|
|
15014
14920
|
function formatToolResult(result) {
|
|
15015
14921
|
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.6",
|
|
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": {
|