neural-loom 0.2.48 → 0.2.49
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +4 -0
- package/.next/build-manifest.json +3 -3
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/diagnostics/route-bundle-stats.json +1 -1
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/routes-manifest.json +24 -0
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
- package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
- package/.next/server/app/api/context/route.js.nft.json +1 -1
- package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
- package/.next/server/app/api/recents/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/connections/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/sql/connections/route/build-manifest.json +9 -0
- package/.next/server/app/api/sql/connections/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/sql/connections/route.js +6 -0
- package/.next/server/app/api/sql/connections/route.js.map +5 -0
- package/.next/server/app/api/sql/connections/route.js.nft.json +1 -0
- package/.next/server/app/api/sql/connections/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/sql/mcp/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/sql/mcp/route/build-manifest.json +9 -0
- package/.next/server/app/api/sql/mcp/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/sql/mcp/route.js +6 -0
- package/.next/server/app/api/sql/mcp/route.js.map +5 -0
- package/.next/server/app/api/sql/mcp/route.js.nft.json +1 -0
- package/.next/server/app/api/sql/mcp/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/sql/query/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/sql/query/route/build-manifest.json +9 -0
- package/.next/server/app/api/sql/query/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/sql/query/route.js +6 -0
- package/.next/server/app/api/sql/query/route.js.map +5 -0
- package/.next/server/app/api/sql/query/route.js.nft.json +1 -0
- package/.next/server/app/api/sql/query/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/sql/schema/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/sql/schema/route/build-manifest.json +9 -0
- package/.next/server/app/api/sql/schema/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/sql/schema/route.js +6 -0
- package/.next/server/app/api/sql/schema/route.js.map +5 -0
- package/.next/server/app/api/sql/schema/route.js.nft.json +1 -0
- package/.next/server/app/api/sql/schema/route_client-reference-manifest.js +3 -0
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/page/react-loadable-manifest.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +4 -0
- package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js +86 -0
- package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__00l5btc._.js +74 -0
- package/.next/server/chunks/[root-of-the-server]__00l5btc._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__06men8g._.js +74 -0
- package/.next/server/chunks/[root-of-the-server]__06men8g._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0_o7_jj._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0_o7_jj._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js +74 -0
- package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_sql_connections_route_actions_11cib8~.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_sql_connections_route_actions_11cib8~.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_sql_mcp_route_actions_0wihfps.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_sql_mcp_route_actions_0wihfps.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_sql_query_route_actions_11x3q3c.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_sql_query_route_actions_11x3q3c.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_sql_schema_route_actions_015okwb.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_sql_schema_route_actions_015okwb.js.map +1 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/03f4bqk_d65hs.js +6 -0
- package/.next/static/chunks/{15zf~gqr_i8s1.js → 0~6gm~u8rgtpd.js} +1 -1
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/.next/types/routes.d.ts +5 -1
- package/.next/types/validator.ts +36 -0
- package/bin/cli.js +76 -0
- package/package.json +2 -2
- package/src/app/api/sql/connections/route.ts +60 -0
- package/src/app/api/sql/mcp/route.ts +27 -0
- package/src/app/api/sql/query/route.ts +56 -0
- package/src/app/api/sql/schema/route.ts +42 -0
- package/src/app/components/DbExplorer.tsx +1279 -0
- package/src/app/components/IdeLayout.tsx +27 -1
- package/src/lib/sql/ConnectionManager.ts +207 -0
- package/src/lib/sql/DbAdapter.ts +62 -0
- package/src/lib/sql/McpServer.ts +278 -0
- package/src/lib/sql/adapters/MysqlAdapter.ts +120 -0
- package/src/lib/sql/adapters/PostgresAdapter.ts +139 -0
- package/src/lib/sql/adapters/SqliteAdapter.ts +111 -0
- package/.next/static/chunks/0bclk7hf35ac4.js +0 -1
- /package/.next/static/{V3HKuGO63wx4G9xwdUktP → BoncS4SNz6CDpSdR8oY-K}/_buildManifest.js +0 -0
- /package/.next/static/{V3HKuGO63wx4G9xwdUktP → BoncS4SNz6CDpSdR8oY-K}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{V3HKuGO63wx4G9xwdUktP → BoncS4SNz6CDpSdR8oY-K}/_ssgManifest.js +0 -0
|
@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from "react";
|
|
|
4
4
|
import TerminalConsole from "./TerminalConsole";
|
|
5
5
|
import DirectoryBrowserModal from "./DirectoryBrowserModal";
|
|
6
6
|
import MarkdownPreview from "./MarkdownPreview";
|
|
7
|
+
import DbExplorer from "./DbExplorer";
|
|
7
8
|
|
|
8
9
|
interface FileNode {
|
|
9
10
|
name: string;
|
|
@@ -156,7 +157,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
|
|
|
156
157
|
|
|
157
158
|
const [editorMode, setEditorMode] = useState<'edit' | 'split' | 'preview'>('edit');
|
|
158
159
|
const [explorerVisible, setExplorerVisible] = useState(true);
|
|
159
|
-
const [activeTab, setActiveTab] = useState<"explorer" | "git" | "analytics" | "auditor" | "browser">("explorer");
|
|
160
|
+
const [activeTab, setActiveTab] = useState<"explorer" | "git" | "analytics" | "auditor" | "browser" | "database">("explorer");
|
|
160
161
|
const [browserUrlInput, setBrowserUrlInput] = useState("http://localhost:3000");
|
|
161
162
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
162
163
|
|
|
@@ -1008,6 +1009,26 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
|
|
|
1008
1009
|
>
|
|
1009
1010
|
🌐
|
|
1010
1011
|
</div>
|
|
1012
|
+
|
|
1013
|
+
{/* Database Explorer Tab */}
|
|
1014
|
+
<div
|
|
1015
|
+
onClick={() => {
|
|
1016
|
+
if (activeTab === "database") {
|
|
1017
|
+
setExplorerVisible(!explorerVisible);
|
|
1018
|
+
} else {
|
|
1019
|
+
setActiveTab("database");
|
|
1020
|
+
setExplorerVisible(true);
|
|
1021
|
+
}
|
|
1022
|
+
}}
|
|
1023
|
+
style={{
|
|
1024
|
+
...sidebarIconStyle,
|
|
1025
|
+
backgroundColor: activeTab === "database" && explorerVisible ? "rgba(255,255,255,0.08)" : "transparent",
|
|
1026
|
+
borderLeft: activeTab === "database" && explorerVisible ? "2px solid var(--primary)" : "2px solid transparent",
|
|
1027
|
+
}}
|
|
1028
|
+
title="Database Explorer & SQL Client"
|
|
1029
|
+
>
|
|
1030
|
+
🗄️
|
|
1031
|
+
</div>
|
|
1011
1032
|
</div>
|
|
1012
1033
|
|
|
1013
1034
|
<div
|
|
@@ -1030,6 +1051,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
|
|
|
1030
1051
|
{activeTab === "analytics" && "Telemetry & Cost"}
|
|
1031
1052
|
{activeTab === "auditor" && "Session Auditor"}
|
|
1032
1053
|
{activeTab === "browser" && "Browser Preview"}
|
|
1054
|
+
{activeTab === "database" && "Database Explorer"}
|
|
1033
1055
|
</span>
|
|
1034
1056
|
{activeTab === "explorer" && (
|
|
1035
1057
|
<div style={{ display: "flex", gap: "0.25rem" }}>
|
|
@@ -1591,6 +1613,10 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
|
|
|
1591
1613
|
</div>
|
|
1592
1614
|
</div>
|
|
1593
1615
|
)}
|
|
1616
|
+
|
|
1617
|
+
{activeTab === "database" && (
|
|
1618
|
+
<DbExplorer workspaceRoot={workspaceRoot} />
|
|
1619
|
+
)}
|
|
1594
1620
|
</div>
|
|
1595
1621
|
</div>
|
|
1596
1622
|
<div
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getConfigDir } from "../config";
|
|
5
|
+
import { DbAdapter } from "./DbAdapter";
|
|
6
|
+
import { SqliteAdapter } from "./adapters/SqliteAdapter";
|
|
7
|
+
import { PostgresAdapter } from "./adapters/PostgresAdapter";
|
|
8
|
+
import { MysqlAdapter } from "./adapters/MysqlAdapter";
|
|
9
|
+
|
|
10
|
+
export interface DbConnectionProfile {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
type: "sqlite" | "postgres" | "mysql";
|
|
14
|
+
filepath?: string; // sqlite
|
|
15
|
+
host?: string; // pg, mysql
|
|
16
|
+
port?: number; // pg, mysql
|
|
17
|
+
database?: string; // pg, mysql
|
|
18
|
+
username?: string; // pg, mysql
|
|
19
|
+
password?: string; // pg, mysql
|
|
20
|
+
ssl?: boolean; // pg
|
|
21
|
+
readOnly?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SqlConfigSchema {
|
|
25
|
+
connections: { [id: string]: DbConnectionProfile };
|
|
26
|
+
activeConnection: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ConnectionManager {
|
|
30
|
+
private static activeInstances = new Map<string, DbAdapter>();
|
|
31
|
+
|
|
32
|
+
private static getGlobalConfigPath(): string {
|
|
33
|
+
return path.join(getConfigDir(), "global-sql.json");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private static getWorkspaceConfigPath(workspaceRoot?: string): string | null {
|
|
37
|
+
if (!workspaceRoot) return null;
|
|
38
|
+
return path.join(workspaceRoot, ".ai", "neural-loom", "sql-config.json");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private static loadConfig(filePath: string): SqlConfigSchema {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(filePath)) {
|
|
44
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
45
|
+
return JSON.parse(content);
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`[ConnectionManager] Failed to load config from ${filePath}:`, err);
|
|
49
|
+
}
|
|
50
|
+
return { connections: {}, activeConnection: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private static saveConfig(filePath: string, config: SqlConfigSchema): void {
|
|
54
|
+
try {
|
|
55
|
+
const dir = path.dirname(filePath);
|
|
56
|
+
if (!fs.existsSync(dir)) {
|
|
57
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf8");
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`[ConnectionManager] Failed to save config to ${filePath}:`, err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public static async getProfiles(workspaceRoot?: string): Promise<{
|
|
66
|
+
global: DbConnectionProfile[];
|
|
67
|
+
workspace: DbConnectionProfile[];
|
|
68
|
+
activeId: string | null;
|
|
69
|
+
isActiveGlobal: boolean;
|
|
70
|
+
}> {
|
|
71
|
+
const globalConfig = this.loadConfig(this.getGlobalConfigPath());
|
|
72
|
+
const globalProfiles = Object.values(globalConfig.connections);
|
|
73
|
+
|
|
74
|
+
let workspaceProfiles: DbConnectionProfile[] = [];
|
|
75
|
+
let activeId = globalConfig.activeConnection;
|
|
76
|
+
let isActiveGlobal = true;
|
|
77
|
+
|
|
78
|
+
if (workspaceRoot) {
|
|
79
|
+
const wsConfigPath = this.getWorkspaceConfigPath(workspaceRoot);
|
|
80
|
+
if (wsConfigPath) {
|
|
81
|
+
const wsConfig = this.loadConfig(wsConfigPath);
|
|
82
|
+
workspaceProfiles = Object.values(wsConfig.connections);
|
|
83
|
+
if (wsConfig.activeConnection) {
|
|
84
|
+
activeId = wsConfig.activeConnection;
|
|
85
|
+
isActiveGlobal = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
global: globalProfiles,
|
|
92
|
+
workspace: workspaceProfiles,
|
|
93
|
+
activeId,
|
|
94
|
+
isActiveGlobal
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public static async saveProfile(
|
|
99
|
+
profile: DbConnectionProfile,
|
|
100
|
+
isGlobal: boolean,
|
|
101
|
+
workspaceRoot?: string
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const filePath = isGlobal
|
|
104
|
+
? this.getGlobalConfigPath()
|
|
105
|
+
: this.getWorkspaceConfigPath(workspaceRoot);
|
|
106
|
+
|
|
107
|
+
if (!filePath) {
|
|
108
|
+
throw new Error("Invalid workspace root path for saving connection profile.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const config = this.loadConfig(filePath);
|
|
112
|
+
config.connections[profile.id] = profile;
|
|
113
|
+
this.saveConfig(filePath, config);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public static async deleteProfile(
|
|
117
|
+
profileId: string,
|
|
118
|
+
isGlobal: boolean,
|
|
119
|
+
workspaceRoot?: string
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const filePath = isGlobal
|
|
122
|
+
? this.getGlobalConfigPath()
|
|
123
|
+
: this.getWorkspaceConfigPath(workspaceRoot);
|
|
124
|
+
|
|
125
|
+
if (!filePath) {
|
|
126
|
+
throw new Error("Invalid workspace root path for deleting connection profile.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const config = this.loadConfig(filePath);
|
|
130
|
+
if (config.connections[profileId]) {
|
|
131
|
+
delete config.connections[profileId];
|
|
132
|
+
if (config.activeConnection === profileId) {
|
|
133
|
+
config.activeConnection = null;
|
|
134
|
+
}
|
|
135
|
+
this.saveConfig(filePath, config);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Close any active instance
|
|
139
|
+
const cacheKey = `${isGlobal ? "global" : "ws"}:${profileId}`;
|
|
140
|
+
const active = this.activeInstances.get(cacheKey);
|
|
141
|
+
if (active) {
|
|
142
|
+
await active.disconnect().catch(console.error);
|
|
143
|
+
this.activeInstances.delete(cacheKey);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public static async setActiveProfile(
|
|
148
|
+
profileId: string | null,
|
|
149
|
+
isGlobal: boolean,
|
|
150
|
+
workspaceRoot?: string
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
const filePath = isGlobal
|
|
153
|
+
? this.getGlobalConfigPath()
|
|
154
|
+
: this.getWorkspaceConfigPath(workspaceRoot);
|
|
155
|
+
|
|
156
|
+
if (!filePath) {
|
|
157
|
+
throw new Error("Invalid workspace root path for setting active profile.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const config = this.loadConfig(filePath);
|
|
161
|
+
config.activeConnection = profileId;
|
|
162
|
+
this.saveConfig(filePath, config);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public static createAdapter(profile: DbConnectionProfile, workspaceRoot?: string): DbAdapter {
|
|
166
|
+
switch (profile.type) {
|
|
167
|
+
case "sqlite":
|
|
168
|
+
return new SqliteAdapter(profile, workspaceRoot);
|
|
169
|
+
case "postgres":
|
|
170
|
+
return new PostgresAdapter(profile);
|
|
171
|
+
case "mysql":
|
|
172
|
+
return new MysqlAdapter(profile);
|
|
173
|
+
default:
|
|
174
|
+
throw new Error(`Unsupported database type: ${(profile as any).type}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public static async getActiveAdapter(workspaceRoot?: string): Promise<{ adapter: DbAdapter; profile: DbConnectionProfile } | null> {
|
|
179
|
+
const { global, workspace, activeId, isActiveGlobal } = await this.getProfiles(workspaceRoot);
|
|
180
|
+
if (!activeId) return null;
|
|
181
|
+
|
|
182
|
+
const cacheKey = `${isActiveGlobal ? "global" : "ws"}:${activeId}`;
|
|
183
|
+
const cached = this.activeInstances.get(cacheKey);
|
|
184
|
+
if (cached) {
|
|
185
|
+
// Find matching profile
|
|
186
|
+
const list = isActiveGlobal ? global : workspace;
|
|
187
|
+
const profile = list.find(p => p.id === activeId);
|
|
188
|
+
if (profile) return { adapter: cached, profile };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Load connection profile
|
|
192
|
+
const list = isActiveGlobal ? global : workspace;
|
|
193
|
+
const profile = list.find(p => p.id === activeId);
|
|
194
|
+
if (!profile) return null;
|
|
195
|
+
|
|
196
|
+
const adapter = this.createAdapter(profile, workspaceRoot);
|
|
197
|
+
this.activeInstances.set(cacheKey, adapter);
|
|
198
|
+
return { adapter, profile };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public static async closeAll(): Promise<void> {
|
|
202
|
+
for (const [, instance] of this.activeInstances.entries()) {
|
|
203
|
+
await instance.disconnect().catch(console.error);
|
|
204
|
+
}
|
|
205
|
+
this.activeInstances.clear();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
export interface TableSchema {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string; // 'table' or 'view'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ColumnSchema {
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
nullable: boolean;
|
|
11
|
+
defaultValue?: string | null;
|
|
12
|
+
isPrimaryKey: boolean;
|
|
13
|
+
isForeignKey: boolean;
|
|
14
|
+
referencedTable?: string | null;
|
|
15
|
+
referencedColumn?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QueryResult {
|
|
19
|
+
rows: any[];
|
|
20
|
+
fields?: string[];
|
|
21
|
+
affectedRows?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export abstract class DbAdapter {
|
|
25
|
+
protected config: any;
|
|
26
|
+
|
|
27
|
+
constructor(config: any) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
abstract connect(): Promise<void>;
|
|
32
|
+
abstract query(sql: string, params?: any[]): Promise<QueryResult>;
|
|
33
|
+
abstract listTables(): Promise<TableSchema[]>;
|
|
34
|
+
abstract describeTable(tableName: string): Promise<ColumnSchema[]>;
|
|
35
|
+
abstract disconnect(): Promise<void>;
|
|
36
|
+
|
|
37
|
+
async getSchemaSummary(): Promise<string> {
|
|
38
|
+
try {
|
|
39
|
+
const tables = await this.listTables();
|
|
40
|
+
let summary = `### Database Schema Summary\n\n`;
|
|
41
|
+
if (tables.length === 0) {
|
|
42
|
+
return summary + "*No tables found in this database.*";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const t of tables) {
|
|
46
|
+
summary += `#### Table: \`${t.name}\` (${t.type})\n`;
|
|
47
|
+
const columns = await this.describeTable(t.name);
|
|
48
|
+
summary += `| Column | Type | Nullable | Key | References |\n`;
|
|
49
|
+
summary += `|---|---|---|---|---|\n`;
|
|
50
|
+
for (const col of columns) {
|
|
51
|
+
const keyType = col.isPrimaryKey ? "PK" : col.isForeignKey ? "FK" : "";
|
|
52
|
+
const ref = col.isForeignKey ? `\`${col.referencedTable}.${col.referencedColumn}\`` : "";
|
|
53
|
+
summary += `| \`${col.name}\` | \`${col.type}\` | ${col.nullable ? "Yes" : "No"} | ${keyType} | ${ref} |\n`;
|
|
54
|
+
}
|
|
55
|
+
summary += `\n`;
|
|
56
|
+
}
|
|
57
|
+
return summary;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return `Failed to reflect schema summary: ${err instanceof Error ? err.message : String(err)}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
import { ConnectionManager } from "./ConnectionManager";
|
|
4
|
+
|
|
5
|
+
export class McpServer {
|
|
6
|
+
private workspaceRoot?: string;
|
|
7
|
+
|
|
8
|
+
constructor(workspaceRoot?: string) {
|
|
9
|
+
this.workspaceRoot = workspaceRoot;
|
|
10
|
+
// Redirect console.log to stderr so that it doesn't corrupt stdout JSON-RPC stream
|
|
11
|
+
console.log = (...args) => {
|
|
12
|
+
console.error("[LOG]", ...args);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public start(): void {
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout,
|
|
20
|
+
terminal: false
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
rl.on("line", async (line) => {
|
|
24
|
+
if (!line.trim()) return;
|
|
25
|
+
try {
|
|
26
|
+
const message = JSON.parse(line);
|
|
27
|
+
if (message.method) {
|
|
28
|
+
const response = await this.handleMessage(message);
|
|
29
|
+
if (response) {
|
|
30
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
this.sendError(null, -32700, "Parse error: " + (err instanceof Error ? err.message : String(err)));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.error("[McpServer] SQL Model Context Protocol Server listening on stdio...");
|
|
39
|
+
if (this.workspaceRoot) {
|
|
40
|
+
console.error(`[McpServer] Workspace Root Context resolved to: ${this.workspaceRoot}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async handleMessage(message: any): Promise<any> {
|
|
45
|
+
const { id, method, params } = message;
|
|
46
|
+
|
|
47
|
+
switch (method) {
|
|
48
|
+
case "initialize":
|
|
49
|
+
return {
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
id,
|
|
52
|
+
result: {
|
|
53
|
+
protocolVersion: "2024-11-05",
|
|
54
|
+
capabilities: {
|
|
55
|
+
tools: {}
|
|
56
|
+
},
|
|
57
|
+
serverInfo: {
|
|
58
|
+
name: "neural-loom-sql-mcp",
|
|
59
|
+
version: "1.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
case "notifications/initialized":
|
|
65
|
+
return null;
|
|
66
|
+
|
|
67
|
+
case "tools/list":
|
|
68
|
+
return {
|
|
69
|
+
jsonrpc: "2.0",
|
|
70
|
+
id,
|
|
71
|
+
result: {
|
|
72
|
+
tools: [
|
|
73
|
+
{
|
|
74
|
+
name: "sql_list_connections",
|
|
75
|
+
description: "Lists all configured global and workspace database connection profiles, and identifies which one is active.",
|
|
76
|
+
inputSchema: { type: "object", properties: {} }
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "sql_list_tables",
|
|
80
|
+
description: "Lists all tables and views in the currently active database.",
|
|
81
|
+
inputSchema: { type: "object", properties: {} }
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "sql_describe_table",
|
|
85
|
+
description: "Provides column names, types, primary keys, foreign keys, and nullability information for a specific table.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
table: { type: "string", description: "The name of the table to describe." }
|
|
90
|
+
},
|
|
91
|
+
required: ["table"]
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "sql_get_schema_summary",
|
|
96
|
+
description: "Returns a complete Markdown representation of the database schema, detailing all tables, columns, constraints, and relationships.",
|
|
97
|
+
inputSchema: { type: "object", properties: {} }
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "sql_query",
|
|
101
|
+
description: "Executes a SQL query on the active database connection. Select queries return matching rows; modifying queries return affected row count. If readOnly is set on the connection, modifying statements will fail.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
sql: { type: "string", description: "The SQL statement to execute." }
|
|
106
|
+
},
|
|
107
|
+
required: ["sql"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
case "tools/call":
|
|
115
|
+
return await this.handleToolCall(id, params);
|
|
116
|
+
|
|
117
|
+
default:
|
|
118
|
+
return {
|
|
119
|
+
jsonrpc: "2.0",
|
|
120
|
+
id,
|
|
121
|
+
error: {
|
|
122
|
+
code: -32601,
|
|
123
|
+
message: `Method not found: ${method}`
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async handleRequest(req: any): Promise<void> {
|
|
130
|
+
const response = await this.handleMessage(req);
|
|
131
|
+
if (response) {
|
|
132
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async handleToolCall(id: any, params: any): Promise<void> {
|
|
137
|
+
const toolName = params.name;
|
|
138
|
+
const args = params.arguments || {};
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (toolName === "sql_list_connections") {
|
|
142
|
+
const res = await ConnectionManager.getProfiles(this.workspaceRoot);
|
|
143
|
+
const activeProfile = res.isActiveGlobal
|
|
144
|
+
? res.global.find(p => p.id === res.activeId)
|
|
145
|
+
: res.workspace.find(p => p.id === res.activeId);
|
|
146
|
+
|
|
147
|
+
const responseText = `Active Connection Profile: ${activeProfile ? `${activeProfile.name} (${activeProfile.type})` : "None"}\n\n` +
|
|
148
|
+
`Global Profiles:\n` + (res.global.length > 0 ? res.global.map(p => `- [${p.id}] ${p.name} (${p.type}) ${res.activeId === p.id && res.isActiveGlobal ? "(Active)" : ""}`).join("\n") : "None") + "\n\n" +
|
|
149
|
+
`Workspace Profiles:\n` + (res.workspace.length > 0 ? res.workspace.map(p => `- [${p.id}] ${p.name} (${p.type}) ${res.activeId === p.id && !res.isActiveGlobal ? "(Active)" : ""}`).join("\n") : "None");
|
|
150
|
+
|
|
151
|
+
this.sendToolResult(id, responseText);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const active = await ConnectionManager.getActiveAdapter(this.workspaceRoot);
|
|
156
|
+
if (!active) {
|
|
157
|
+
this.sendToolResult(
|
|
158
|
+
id,
|
|
159
|
+
"Error: No active database connection is selected. Please configure and activate a database connection in the NeuralLoom workspace dashboard UI.",
|
|
160
|
+
true
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { adapter, profile } = active;
|
|
166
|
+
|
|
167
|
+
switch (toolName) {
|
|
168
|
+
case "sql_list_tables": {
|
|
169
|
+
const tables = await adapter.listTables();
|
|
170
|
+
const listText = tables.length > 0
|
|
171
|
+
? tables.map(t => `- \`${t.name}\` (${t.type})`).join("\n")
|
|
172
|
+
: "*No tables found in the active database.*";
|
|
173
|
+
this.sendToolResult(id, `Active Database: ${profile.name} (${profile.type})\n\n### Tables:\n${listText}`);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case "sql_describe_table": {
|
|
178
|
+
const table = args.table;
|
|
179
|
+
if (!table) {
|
|
180
|
+
this.sendToolResult(id, "Error: Missing required argument 'table'.", true);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const columns = await adapter.describeTable(table);
|
|
184
|
+
let text = `### Table Structure: \`${table}\` (Active Connection: ${profile.name})\n\n`;
|
|
185
|
+
text += `| Column | Type | Nullable | Key | References |\n`;
|
|
186
|
+
text += `|---|---|---|---|---|\n`;
|
|
187
|
+
for (const col of columns) {
|
|
188
|
+
const key = col.isPrimaryKey ? "PK" : col.isForeignKey ? "FK" : "";
|
|
189
|
+
const ref = col.isForeignKey ? `\`${col.referencedTable}.${col.referencedColumn}\`` : "";
|
|
190
|
+
text += `| \`${col.name}\` | \`${col.type}\` | ${col.nullable ? "Yes" : "No"} | ${key} | ${ref} |\n`;
|
|
191
|
+
}
|
|
192
|
+
this.sendToolResult(id, text);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case "sql_get_schema_summary": {
|
|
197
|
+
const summary = await adapter.getSchemaSummary();
|
|
198
|
+
this.sendToolResult(id, summary);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case "sql_query": {
|
|
203
|
+
const sql = args.sql;
|
|
204
|
+
if (!sql || !sql.trim()) {
|
|
205
|
+
this.sendToolResult(id, "Error: Missing required argument 'sql'.", true);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const clean = sql.trim().toLowerCase();
|
|
210
|
+
const isMutation = clean.startsWith("insert") ||
|
|
211
|
+
clean.startsWith("update") ||
|
|
212
|
+
clean.startsWith("delete") ||
|
|
213
|
+
clean.startsWith("drop") ||
|
|
214
|
+
clean.startsWith("alter") ||
|
|
215
|
+
clean.startsWith("create") ||
|
|
216
|
+
clean.startsWith("truncate") ||
|
|
217
|
+
clean.startsWith("replace");
|
|
218
|
+
|
|
219
|
+
if (profile.readOnly && isMutation) {
|
|
220
|
+
this.sendToolResult(id, "Error: Access Denied. This database connection is configured as Read-Only.", true);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const res = await adapter.query(sql);
|
|
225
|
+
if (res.affectedRows !== undefined) {
|
|
226
|
+
this.sendToolResult(id, `Query completed successfully. Affected rows: ${res.affectedRows}`);
|
|
227
|
+
} else {
|
|
228
|
+
this.sendToolResult(id, JSON.stringify(res.rows, null, 2));
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
default:
|
|
234
|
+
this.sendError(id, -32601, `Tool not implemented: ${toolName}`);
|
|
235
|
+
}
|
|
236
|
+
} catch (err: any) {
|
|
237
|
+
this.sendToolResult(id, `Exception executing tool: ${err.message || String(err)}`, true);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private sendResponse(id: any, result: any): void {
|
|
242
|
+
const response = {
|
|
243
|
+
jsonrpc: "2.0",
|
|
244
|
+
id,
|
|
245
|
+
result
|
|
246
|
+
};
|
|
247
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private sendToolResult(id: any, text: string, isError = false): void {
|
|
251
|
+
const response = {
|
|
252
|
+
jsonrpc: "2.0",
|
|
253
|
+
id,
|
|
254
|
+
result: {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: "text",
|
|
258
|
+
text
|
|
259
|
+
}
|
|
260
|
+
],
|
|
261
|
+
isError
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private sendError(id: any, code: number, message: string): void {
|
|
268
|
+
const response = {
|
|
269
|
+
jsonrpc: "2.0",
|
|
270
|
+
id,
|
|
271
|
+
error: {
|
|
272
|
+
code,
|
|
273
|
+
message
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
277
|
+
}
|
|
278
|
+
}
|