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.
Files changed (3) hide show
  1. package/README.md +63 -42
  2. package/dist/index.js +118 -182
  3. 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. Add this to the `mcpServers` object in your `mcp.json`:
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
- "MCP_RELAY_HTTP_URL": "http://localhost:8787/query",
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
- "MCP_ALLOWED_TABLES": "rag_mcp.documents",
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**: Replace `YOUR_PUBLIC_KEY_HERE` with your actual public key from the Database page → Management tab → Encryption section.
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
- 3. **Install an Embedding Model** (Required for RAG features):
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
- 4. Restart LM Studio to load the configuration.
68
+ 5. Restart LM Studio to load the configuration.
60
69
 
61
70
  ### With Claude Desktop
62
71
 
63
- Add to your `claude_desktop_config.json`:
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
- "MCP_RELAY_HTTP_URL": "http://localhost:8787/query",
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
- "MCP_ALLOWED_TABLES": "rag_mcp.documents",
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**: Replace `YOUR_PUBLIC_KEY_HERE` with your actual public key from the Database page → Management tab → Encryption section.
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
- - `MCP_RELAY_HTTP_URL` - HTTP relay endpoint (default: `http://localhost:8787/query`)
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
- - `MCP_ALLOWED_TABLES` - **Required for recipes**: Comma-separated list of allowed tables (e.g., `"schema.table1,schema.table2"`). The relay validates all SQL queries against this whitelist. If not set, the MCP server can access all tables (for direct use, not recommended for recipes).
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
- ## Available Tools
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
- - `query_database` - Execute SQL queries (respects `MCP_ALLOWED_TABLES` if set)
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
- This MCP server supports end-to-end encryption of SQL queries using RSA-OAEP (2048-bit). When encryption is enabled:
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
- - SQL queries are encrypted in the MCP server using the browser's public key
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
- To enable encryption:
119
- 1. Get your public key from the Database page Management tab Encryption section
120
- 2. Set `MCP_PUBLIC_KEY` to your public key (base64 string)
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
- If `MCP_PUBLIC_KEY` is not provided, the MCP server will automatically fetch it from the relay's `/public-key` endpoint.
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
- - Postgres WASM relay running (default: `http://localhost:8787/query`)
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 { webcrypto } from "node:crypto";
14662
- var DEFAULT_RELAY_URL = process.env.MCP_RELAY_HTTP_URL ?? process.env.MCP_PROXY_HTTP_URL ?? "http://localhost:8787/query";
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.1.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
- relayUrl: external_exports.string().url().describe("Override the HTTP relay endpoint (defaults to MCP_RELAY_HTTP_URL or http://localhost:8787/query)").optional(),
14676
- connectionId: external_exports.string().uuid().describe("Browser connection ID (required for multi-user support, defaults to MCP_CONNECTION_ID)").optional()
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
- relayUrl: external_exports.string().url().describe("Override the HTTP relay endpoint (defaults to MCP_RELAY_HTTP_URL or http://localhost:8787/query)").optional(),
14681
- connectionId: external_exports.string().uuid().describe("Browser connection ID (required for multi-user support, defaults to MCP_CONNECTION_ID)").optional()
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
- relayUrl: external_exports.string().url().describe("Override the HTTP relay endpoint (defaults to MCP_RELAY_HTTP_URL or http://localhost:8787/query)").optional(),
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 (required for multi-user support, defaults to MCP_CONNECTION_ID)").optional()
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
- relayUrl: external_exports.string().url().describe("Override the HTTP relay endpoint (defaults to MCP_RELAY_HTTP_URL or http://localhost:8787/query)").optional(),
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 (required for multi-user support, defaults to MCP_CONNECTION_ID)").optional()
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. Note: This tool respects table permissions - you can only access tables specified in MCP_ALLOWED_TABLES.",
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 = [], relayUrl, connectionId }) => {
14756
+ async ({ sql, params = [], postgresUrl, connectionId, authToken }) => {
14709
14757
  try {
14710
- const allowedTables = process.env.MCP_ALLOWED_TABLES ? process.env.MCP_ALLOWED_TABLES.split(",").map((t) => t.trim()) : [];
14711
- const result = await executeSql(relayUrl ?? DEFAULT_RELAY_URL, sql, params, allowedTables, connectionId);
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 that you have access to. 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. Note: Only shows tables you have permission to access.",
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, relayUrl, connectionId }) => {
14725
- const allowedTables = process.env.MCP_ALLOWED_TABLES ? process.env.MCP_ALLOWED_TABLES.split(",").map((t) => t.trim()) : [];
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 result = await executeSql(relayUrl ?? DEFAULT_RELAY_URL, sql, params, allowedTables, connectionId);
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, relayUrl, embeddingUrl, embeddingModel, connectionId }) => {
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 allowedTables = process.env.MCP_ALLOWED_TABLES ? process.env.MCP_ALLOWED_TABLES.split(",").map((t) => t.trim()) : [];
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 result = await executeSql(relayUrl ?? DEFAULT_RELAY_URL, sql, params, allowedTables, connectionId);
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, relayUrl, embeddingUrl, embeddingModel, connectionId }) => {
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 allowedTables = process.env.MCP_ALLOWED_TABLES ? process.env.MCP_ALLOWED_TABLES.split(",").map((t) => t.trim()) : [];
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 result = await executeSql(relayUrl ?? DEFAULT_RELAY_URL, sql, params, allowedTables, connectionId);
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 importPublicKey(publicKeyString) {
14877
- const buffer = Buffer.from(publicKeyString, "base64");
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
- const relayUrlObj = new URL(relayUrl);
14904
- const pathParts = relayUrlObj.pathname.split("/").filter(Boolean);
14905
- let effectiveConnectionId = connectionId;
14906
- if (pathParts.length >= 2 && (pathParts[1] === "query" || pathParts[1] === "public-key")) {
14907
- effectiveConnectionId = pathParts[0];
14908
- } else if (pathParts[0] === "query" && pathParts[1]) {
14909
- effectiveConnectionId = pathParts[1];
14910
- } else if (pathParts[0] === "public-key" && pathParts[1]) {
14911
- effectiveConnectionId = pathParts[1];
14912
- } else if (!effectiveConnectionId) {
14913
- effectiveConnectionId = MCP_CONNECTION_ID;
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
- relayUrlObj.pathname = `/${effectiveConnectionId}/public-key`;
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.5",
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": {