From 5443f3e5e9760708edc33d8885f506bd6ec6d23f Mon Sep 17 00:00:00 2001 From: Morten Harders Date: Fri, 6 Nov 2026 13:52:46 +0100 Subject: [PATCH] feat(mcp): allow agent writes on Lerd connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lerd projects and services cannot be edited, so agents could never change data on them. Their right-click menu now has 'Allow agents to change data…' (with a confirmation) and 'Make read-only for agents'. The choice is kept by connection name in config.toml (agent_writes), which the MCP server reads on every call, and saving Settings keeps it. --- README.md | 2 +- app.go | 16 +++++++++++++++- frontend/src/actions.ts | 15 +++++++++++++++ frontend/wailsjs/go/main/App.d.ts | 2 ++ frontend/wailsjs/go/main/App.js | 4 ++++ frontend/wailsjs/go/models.ts | 2 ++ internal/config/config.go | 2 ++ internal/manager/manager.go | 6 ++++++ 8 files changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32842ca..e79ef8b 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ dbro mcp --http # streamable H claude mcp add --transport http dbro http://127.0.0.1:7811/mcp ``` -Tools: `list_connections`, `list_databases`, `list_tables`, `describe_table`, `query` (always in a read-only transaction) and `execute` (only where *Allow agents to change data* is on). Agent statements appear in the app's SQL log marked `[mcp]`. +Tools: `list_connections`, `list_databases`, `list_tables`, `describe_table`, `query` (always in a read-only transaction) and `execute` (only where *Allow agents to change data* is on: in Edit for saved connections, in the right-click menu for Lerd projects and services). Agent statements appear in the app's SQL log marked `[mcp]`. ## Headless mode diff --git a/app.go b/app.go index 1833cc4..fd66112 100644 --- a/app.go +++ b/app.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "sync" "time" @@ -192,7 +193,7 @@ func (a *App) SaveThemeAs(name string, colors map[string]string) (string, error) // SaveSettings stores settings (theme changes go through SetTheme). func (a *App) SaveSettings(s config.Settings) error { cur, _ := config.LoadSettings() - s.Theme, s.FirstRunDone, s.OmarchyAsked = cur.Theme, cur.FirstRunDone, cur.OmarchyAsked + s.Theme, s.FirstRunDone, s.OmarchyAsked, s.AgentWrites = cur.Theme, cur.FirstRunDone, cur.OmarchyAsked, cur.AgentWrites return config.SaveSettings(s) } @@ -880,3 +881,16 @@ func (a *App) Restart() error { runtime.Quit(a.ctx) return nil } + +// SetAgentWrites lets agents change data on a lerd connection, or makes it read-only again. +func (a *App) SetAgentWrites(name string, allow bool) error { + s, err := config.LoadSettings() + if err != nil { + return err + } + s.AgentWrites = slices.DeleteFunc(s.AgentWrites, func(n string) bool { return n == name }) + if allow { + s.AgentWrites = append(s.AgentWrites, name) + } + return config.SaveSettings(s) +} diff --git a/frontend/src/actions.ts b/frontend/src/actions.ts index ad6c43f..8df4179 100644 --- a/frontend/src/actions.ts +++ b/frontend/src/actions.ts @@ -25,12 +25,27 @@ export function connectionMenu(name: string, t: Tree): MenuItem[] { { separator: true, label: '' }, { label: 'New database…', disabled: dialect(name) === 'sqlite', action: () => newDatabase(name) }, { separator: true, label: '' }, + ...(manual ? [] : [ + c?.allowAgentWrites + ? { label: 'Make read-only for agents', action: () => setAgentWrites(name, false) } + : { label: 'Allow agents to change data…', action: () => setAgentWrites(name, true) }, + { separator: true, label: '' }, + ]), { label: 'Edit…', disabled: !manual, action: () => editConnection(name) }, { label: 'Duplicate…', disabled: !manual, action: () => editConnection(name, true) }, { label: 'Delete', disabled: !manual, danger: true, action: () => deleteConnection(name) }, ]; } +/** setAgentWrites switches MCP writes on a lerd connection; saved connections do it in Edit. */ +async function setAgentWrites(name: string, allow: boolean) { + if (allow && !(await confirm('Allow agents to change data', `Agents connected over MCP can then run INSERT, UPDATE, DELETE and schema changes on ${displayName(name)}.`, 'Allow', true))) return; + await Go.SetAgentWrites(name, allow).catch((e) => showError(e, 'Could not save')); + await tree.reload(); + if (state.selection?.conn === name) emit('select', state.selection); + toast(`${displayName(name)}: agents ${allow ? 'can change data' : 'are read-only'}`); +} + export function databaseMenu(conn: string, dbName: string, t: Tree): MenuItem[] { const sel: Selection = { kind: 'database', conn, db: dbName }; return [ diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 8e1ed3c..bc5a5ed 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -105,6 +105,8 @@ export function SaveSnippet(arg1:history.Snippet):Promise; export function SaveThemeAs(arg1:string,arg2:Record):Promise; +export function SetAgentWrites(arg1:string,arg2:boolean):Promise; + export function SetTheme(arg1:string):Promise; export function Snippets():Promise>; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 0c6d02e..a3a7d24 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -198,6 +198,10 @@ export function SaveThemeAs(arg1, arg2) { return window['go']['main']['App']['SaveThemeAs'](arg1, arg2); } +export function SetAgentWrites(arg1, arg2) { + return window['go']['main']['App']['SetAgentWrites'](arg1, arg2); +} + export function SetTheme(arg1) { return window['go']['main']['App']['SetTheme'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 3c46449..623e10e 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -94,6 +94,7 @@ export namespace config { firstRunDone: boolean; queryTimeout: number; checkUpdates: boolean; + agentWrites: string[]; static createFrom(source: any = {}) { return new Settings(source); @@ -109,6 +110,7 @@ export namespace config { this.firstRunDone = source["firstRunDone"]; this.queryTimeout = source["queryTimeout"]; this.checkUpdates = source["checkUpdates"]; + this.agentWrites = source["agentWrites"]; } } diff --git a/internal/config/config.go b/internal/config/config.go index 5b52733..2801d80 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,6 +59,8 @@ type Settings struct { FirstRunDone bool `toml:"first_run_done,omitempty" json:"firstRunDone"` QueryTimeout int `toml:"query_timeout,omitempty" json:"queryTimeout"` // seconds CheckUpdates bool `toml:"check_updates" json:"checkUpdates"` // look for a new release on startup + // AgentWrites names the lerd connections agents may change; they cannot be edited like saved ones. + AgentWrites []string `toml:"agent_writes,omitempty" json:"agentWrites"` } // Dir returns the config directory, honoring DBRO_CONFIG_DIR and XDG_CONFIG_HOME. diff --git a/internal/manager/manager.go b/internal/manager/manager.go index c617630..802822d 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "slices" "sort" "strings" "sync" @@ -80,6 +81,11 @@ func (m *Manager) Connections() ([]config.Connection, []string) { warn = append(warn, "lerd: "+err.Error()) } out = append(out, lerd...) + for i := range out { + if out[i].Source == "lerd" || out[i].Source == "lerd-project" { + out[i].AllowAgentWrites = slices.Contains(s.AgentWrites, out[i].Name) + } + } } rank := map[string]int{"manual": 0, "laravel": 0, "lerd-project": 1, "lerd": 2} sort.SliceStable(out, func(i, j int) bool { return rank[out[i].Source] < rank[out[j].Source] })