@yugabytedb/meko-mcp 1.2.0 → 2.0.0
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 +28 -19
- package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
- package/assets/hooks-handlers/lib/capture.js +209 -144
- package/assets/hooks-handlers/lib/checkpoint-timer.js +23 -11
- package/assets/hooks-handlers/session-start.sh +6 -2
- package/assets/scripts/meko-statusline.sh +119 -0
- package/assets/scripts/package-desktop-skill.sh +27 -24
- package/assets/skills/meko-mcp-tools/SKILL.md +42 -50
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +75 -150
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +30 -36
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +6 -22
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +9 -8
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +35 -42
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -92
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +5 -9
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +1 -81
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +100 -107
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +41 -11
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +71 -150
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +12 -18
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +12 -15
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +35 -42
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -92
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +5 -9
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +1 -81
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +198 -0
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
- package/bin/create-meko-setup.mjs +29 -0
- package/lib/clients/claude-code.mjs +133 -6
- package/lib/clients/claude-desktop.mjs +37 -12
- package/lib/clients/codex.mjs +306 -5
- package/lib/clients/common.mjs +41 -1
- package/lib/clients/cursor.mjs +75 -19
- package/lib/config.mjs +26 -0
- package/lib/installer.mjs +24 -0
- package/lib/mcp-transport.mjs +5 -0
- package/lib/validate.mjs +0 -1
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +8 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
package/lib/clients/cursor.mjs
CHANGED
|
@@ -16,8 +16,11 @@ import {
|
|
|
16
16
|
writeJsonFile,
|
|
17
17
|
} from "../config.mjs";
|
|
18
18
|
import {
|
|
19
|
+
MEKO_MCP_USER_AGENT,
|
|
19
20
|
getInstallerVersion,
|
|
21
|
+
installStableTree,
|
|
20
22
|
isDanglingSymlink,
|
|
23
|
+
isPackagedInstall,
|
|
21
24
|
markerIsStale,
|
|
22
25
|
readInstallerMarker,
|
|
23
26
|
resolveHooksRoot,
|
|
@@ -31,7 +34,6 @@ const SKILLS = [{ name: "meko-mcp-tools" }];
|
|
|
31
34
|
|
|
32
35
|
const HOOKS = [
|
|
33
36
|
{ event: "sessionStart", script: "cursor-session-start.sh", timeout: 15000 },
|
|
34
|
-
{ event: "beforeSubmitPrompt", script: "before-submit-prompt.sh", matcher: "UserPromptSubmit", timeout: 2000 },
|
|
35
37
|
{ event: "preCompact", script: "pre-compact.sh" },
|
|
36
38
|
{ event: "sessionEnd", script: "session-end.sh", timeout: 30000 },
|
|
37
39
|
];
|
|
@@ -40,6 +42,9 @@ const HOOKS = [
|
|
|
40
42
|
* @returns {object}
|
|
41
43
|
*/
|
|
42
44
|
export function createCursorAdapter(customPaths = {}) {
|
|
45
|
+
const resolveHooksRootPath = customPaths.resolveHooksRoot ?? resolveHooksRoot;
|
|
46
|
+
const resolveSkillSourcePath = customPaths.resolveSkillSource ?? resolveSkillSource;
|
|
47
|
+
const packagedInstallDetector = customPaths.isPackagedInstall ?? isPackagedInstall;
|
|
43
48
|
const paths = {
|
|
44
49
|
getMcpPath: customPaths.getMcpPath ?? getCursorMcpPath,
|
|
45
50
|
getHooksPath: customPaths.getHooksPath ?? getCursorHooksPath,
|
|
@@ -85,6 +90,7 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
85
90
|
if (dryRun) {
|
|
86
91
|
log.dim(` Would update ${mcpPath}:`);
|
|
87
92
|
log.dim(` mcpServers.${name}.url = ${url}`);
|
|
93
|
+
log.dim(` mcpServers.${name}.headers.User-Agent = ${MEKO_MCP_USER_AGENT}`);
|
|
88
94
|
if (apiKey) {
|
|
89
95
|
log.dim(" mcpServers.<name>.headers.Authorization = <set>");
|
|
90
96
|
}
|
|
@@ -95,16 +101,18 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
95
101
|
if (!cfg.mcpServers || typeof cfg.mcpServers !== "object") {
|
|
96
102
|
cfg.mcpServers = {};
|
|
97
103
|
}
|
|
98
|
-
const server = { url };
|
|
104
|
+
const server = { url, headers: { "User-Agent": MEKO_MCP_USER_AGENT } };
|
|
99
105
|
if (apiKey) {
|
|
100
|
-
server.headers =
|
|
106
|
+
server.headers.Authorization = `Bearer ${apiKey}`;
|
|
101
107
|
}
|
|
102
108
|
cfg.mcpServers[name] = server;
|
|
103
109
|
writeJsonFile(mcpPath, cfg);
|
|
104
110
|
},
|
|
105
111
|
setHookEnvironment({ url, apiKey, dryRun, log, scope, projectRoot, cursorHookMode }) {
|
|
106
112
|
const mode = cursorHookMode || "native";
|
|
113
|
+
const installedHooksRoot = getCursorHooksInstallRoot(paths, scope, projectRoot);
|
|
107
114
|
if (dryRun) {
|
|
115
|
+
log.dim(` Would copy Cursor hook scripts to ${installedHooksRoot}`);
|
|
108
116
|
if (mode === "native") {
|
|
109
117
|
log.dim(` Would configure Cursor hooks at ${paths.getHooksPath(scope, projectRoot)}`);
|
|
110
118
|
}
|
|
@@ -114,11 +122,27 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
114
122
|
return;
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
installStableTree(resolveHooksRootPath(), installedHooksRoot);
|
|
126
|
+
|
|
117
127
|
if (mode === "native") {
|
|
118
|
-
installNativeCursorHooks({
|
|
128
|
+
installNativeCursorHooks({
|
|
129
|
+
scope,
|
|
130
|
+
projectRoot,
|
|
131
|
+
url,
|
|
132
|
+
apiKey,
|
|
133
|
+
paths,
|
|
134
|
+
handlersRoot: installedHooksRoot,
|
|
135
|
+
});
|
|
119
136
|
}
|
|
120
137
|
if (mode === "claude-compat") {
|
|
121
|
-
installClaudeCompatHooks({
|
|
138
|
+
installClaudeCompatHooks({
|
|
139
|
+
scope,
|
|
140
|
+
projectRoot,
|
|
141
|
+
url,
|
|
142
|
+
apiKey,
|
|
143
|
+
paths,
|
|
144
|
+
handlersRoot: installedHooksRoot,
|
|
145
|
+
});
|
|
122
146
|
}
|
|
123
147
|
},
|
|
124
148
|
installSkills({ dryRun, log, scope, projectRoot }) {
|
|
@@ -132,7 +156,7 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
132
156
|
|
|
133
157
|
mkdirSync(skillDir, { recursive: true });
|
|
134
158
|
for (const skill of SKILLS) {
|
|
135
|
-
const source =
|
|
159
|
+
const source = resolveSkillSourcePath(skill.name);
|
|
136
160
|
const destination = join(skillDir, skill.name);
|
|
137
161
|
|
|
138
162
|
if (existsSync(destination)) {
|
|
@@ -172,7 +196,9 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
172
196
|
removePath(destination);
|
|
173
197
|
}
|
|
174
198
|
|
|
175
|
-
const strategy = symlinkOrCopy(source, destination
|
|
199
|
+
const strategy = symlinkOrCopy(source, destination, {
|
|
200
|
+
preferCopy: packagedInstallDetector(),
|
|
201
|
+
});
|
|
176
202
|
// Only stamp a marker when we materialized a real copy. Writing into
|
|
177
203
|
// a symlinked source dir would mutate the shipped asset tree, which
|
|
178
204
|
// is confusing at best and a permission error at worst. Symlinks
|
|
@@ -217,10 +243,20 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
217
243
|
async validateConfig({ serverName, scope, projectRoot }) {
|
|
218
244
|
const mcpPath = paths.getMcpPath(scope, projectRoot);
|
|
219
245
|
const cfg = readJsonFile(mcpPath);
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
const entry = cfg?.mcpServers?.[serverName];
|
|
247
|
+
if (!entry) {
|
|
248
|
+
return { ok: false, message: `Server "${serverName}" not found in ${mcpPath}.` };
|
|
249
|
+
}
|
|
250
|
+
const hasUserAgent = Object.keys(entry.headers ?? {}).some(
|
|
251
|
+
(key) => key.toLowerCase() === "user-agent",
|
|
252
|
+
);
|
|
253
|
+
if (!hasUserAgent) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
message: `Server "${serverName}" in ${mcpPath} is missing the User-Agent header required by the Cloud WAF.`,
|
|
257
|
+
};
|
|
222
258
|
}
|
|
223
|
-
return { ok:
|
|
259
|
+
return { ok: true, message: `Server "${serverName}" is registered in ${mcpPath}.` };
|
|
224
260
|
},
|
|
225
261
|
};
|
|
226
262
|
}
|
|
@@ -245,7 +281,22 @@ function ensureEnvBlock(cfg, url, apiKey) {
|
|
|
245
281
|
}
|
|
246
282
|
}
|
|
247
283
|
|
|
248
|
-
function
|
|
284
|
+
function getCursorHooksInstallRoot(paths, scope, projectRoot) {
|
|
285
|
+
return join(
|
|
286
|
+
dirname(paths.getHooksPath(scope, projectRoot)),
|
|
287
|
+
"meko-hooks",
|
|
288
|
+
"hooks-handlers",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function installNativeCursorHooks({
|
|
293
|
+
scope,
|
|
294
|
+
projectRoot,
|
|
295
|
+
url,
|
|
296
|
+
apiKey,
|
|
297
|
+
paths,
|
|
298
|
+
handlersRoot,
|
|
299
|
+
}) {
|
|
249
300
|
const hooksPath = paths.getHooksPath(scope, projectRoot);
|
|
250
301
|
const cfg = readJsonFile(hooksPath);
|
|
251
302
|
if (!cfg.version) cfg.version = 1;
|
|
@@ -253,7 +304,6 @@ function installNativeCursorHooks({ scope, projectRoot, url, apiKey, paths }) {
|
|
|
253
304
|
cfg.hooks = {};
|
|
254
305
|
}
|
|
255
306
|
|
|
256
|
-
const handlersRoot = resolveHooksRoot();
|
|
257
307
|
pruneNativeCursorMekoHooks(cfg.hooks);
|
|
258
308
|
for (const hook of HOOKS) {
|
|
259
309
|
const scriptPath = join(handlersRoot, hook.script);
|
|
@@ -281,14 +331,20 @@ function installNativeCursorHooks({ scope, projectRoot, url, apiKey, paths }) {
|
|
|
281
331
|
// will be pruned; the user must re-run the Cursor installer to restore
|
|
282
332
|
// them. pruneMekoHookEntries emits a warning on any removal so the user
|
|
283
333
|
// has a chance to notice before hooks silently disappear.
|
|
284
|
-
function installClaudeCompatHooks({
|
|
334
|
+
function installClaudeCompatHooks({
|
|
335
|
+
scope,
|
|
336
|
+
projectRoot,
|
|
337
|
+
url,
|
|
338
|
+
apiKey,
|
|
339
|
+
paths,
|
|
340
|
+
handlersRoot,
|
|
341
|
+
}) {
|
|
285
342
|
const settingsPath = paths.getClaudeCompatSettingsPath(scope, projectRoot);
|
|
286
343
|
const cfg = readJsonFile(settingsPath);
|
|
287
344
|
if (!cfg.hooks || typeof cfg.hooks !== "object") {
|
|
288
345
|
cfg.hooks = {};
|
|
289
346
|
}
|
|
290
347
|
|
|
291
|
-
const handlersRoot = resolveHooksRoot();
|
|
292
348
|
const compatMap = {
|
|
293
349
|
SessionStart: { script: "session-start.sh", timeout: 15000 },
|
|
294
350
|
PreCompact: { script: "pre-compact.sh" },
|
|
@@ -354,9 +410,7 @@ function upsertClaudeCompatHook(hooksObj, event, entry) {
|
|
|
354
410
|
}
|
|
355
411
|
|
|
356
412
|
function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
|
|
357
|
-
const handlersRoot =
|
|
358
|
-
const scripts = ["session-start.sh", "cursor-session-start.sh", "before-submit-prompt.sh", "pre-compact.sh", "session-end.sh"]
|
|
359
|
-
.map((name) => join(handlersRoot, name));
|
|
413
|
+
const handlersRoot = getCursorHooksInstallRoot(paths, scope, projectRoot);
|
|
360
414
|
|
|
361
415
|
if (mode === "native") {
|
|
362
416
|
const hooksPath = paths.getHooksPath(scope, projectRoot);
|
|
@@ -366,7 +420,7 @@ function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
|
|
|
366
420
|
if (!Array.isArray(cfg.hooks[event])) continue;
|
|
367
421
|
cfg.hooks[event] = cfg.hooks[event].filter((item) => {
|
|
368
422
|
if (!item?.command) return true;
|
|
369
|
-
return !
|
|
423
|
+
return !isMekoHookCommand(item.command);
|
|
370
424
|
});
|
|
371
425
|
}
|
|
372
426
|
writeJsonFile(hooksPath, cfg);
|
|
@@ -389,7 +443,7 @@ function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
|
|
|
389
443
|
if (!Array.isArray(group?.hooks)) return group;
|
|
390
444
|
const filtered = group.hooks.filter((item) => {
|
|
391
445
|
if (!item?.command) return true;
|
|
392
|
-
return !
|
|
446
|
+
return !isMekoHookCommand(item.command);
|
|
393
447
|
});
|
|
394
448
|
return { ...group, hooks: filtered };
|
|
395
449
|
}).filter((group) => {
|
|
@@ -400,6 +454,8 @@ function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
|
|
|
400
454
|
writeJsonFile(settingsPath, cfg);
|
|
401
455
|
}
|
|
402
456
|
}
|
|
457
|
+
|
|
458
|
+
removePath(dirname(handlersRoot));
|
|
403
459
|
}
|
|
404
460
|
|
|
405
461
|
/**
|
package/lib/config.mjs
CHANGED
|
@@ -143,6 +143,32 @@ export function getCodexMekoEnvFilePath() {
|
|
|
143
143
|
return join(codexHome, "meko-env.sh");
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Return the stable directory the Codex hook scripts are copied into at
|
|
148
|
+
* install time.
|
|
149
|
+
*
|
|
150
|
+
* Why a dedicated copy instead of referencing the shipped asset root
|
|
151
|
+
* directly: when the installer runs via `npx @yugabytedb/meko-mcp`, the
|
|
152
|
+
* asset root resolves to an ephemeral npm cache dir
|
|
153
|
+
* (`~/.npm/_npx/<hash>/node_modules/@yugabytedb/meko-mcp/assets/...`).
|
|
154
|
+
* npx garbage-collects those hashed dirs, so a hooks.json that pins the
|
|
155
|
+
* npx path becomes a dangling pointer on the next npx run — Codex then
|
|
156
|
+
* spawns `bash '<missing script>'`, which exits 127 ("command not found")
|
|
157
|
+
* and surfaces as a failed SessionStart hook + the misleading "could not
|
|
158
|
+
* reach Meko" fallback. Copying the scripts under $CODEX_HOME gives the
|
|
159
|
+
* hook a home-dir-stable target that outlives the npx cache.
|
|
160
|
+
*
|
|
161
|
+
* The `hooks-handlers` leaf is preserved so the command string still
|
|
162
|
+
* matches `isMekoHookCommand`'s prune regex — that lets a reinstall
|
|
163
|
+
* auto-heal any stale npx-pinned entry left by an older installer.
|
|
164
|
+
*
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
export function getCodexHooksInstallRoot() {
|
|
168
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
169
|
+
return join(codexHome, "meko-hooks", "hooks-handlers");
|
|
170
|
+
}
|
|
171
|
+
|
|
146
172
|
/**
|
|
147
173
|
* Return path to the shared agent-skills directory used by Codex
|
|
148
174
|
* (and other agent runtimes that follow the convention).
|
package/lib/installer.mjs
CHANGED
|
@@ -33,6 +33,7 @@ export async function install(options) {
|
|
|
33
33
|
skipSkills = false,
|
|
34
34
|
skipValidation = false,
|
|
35
35
|
skipCanary = false,
|
|
36
|
+
installStatusline = false,
|
|
36
37
|
dryRun = false,
|
|
37
38
|
verbose = false,
|
|
38
39
|
yes = false,
|
|
@@ -50,6 +51,7 @@ export async function install(options) {
|
|
|
50
51
|
|
|
51
52
|
let totalSteps = 3; // register + env/hooks + validate
|
|
52
53
|
if (!skipSkills) totalSteps += 1;
|
|
54
|
+
if (installStatusline && typeof adapter.installStatusline === "function") totalSteps += 1;
|
|
53
55
|
if (skipValidation) totalSteps -= 1;
|
|
54
56
|
|
|
55
57
|
banner(`Meko Setup for ${adapter.displayName}`);
|
|
@@ -171,6 +173,28 @@ export async function install(options) {
|
|
|
171
173
|
currentStep++;
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
if (installStatusline && typeof adapter.installStatusline === "function") {
|
|
177
|
+
try {
|
|
178
|
+
adapter.installStatusline({
|
|
179
|
+
scope,
|
|
180
|
+
dryRun,
|
|
181
|
+
verbose,
|
|
182
|
+
log: LOG,
|
|
183
|
+
settingsPath,
|
|
184
|
+
projectRoot,
|
|
185
|
+
});
|
|
186
|
+
stepOk(
|
|
187
|
+
currentStep,
|
|
188
|
+
totalSteps,
|
|
189
|
+
dryRun ? "Wiring datapack statusline (dry-run)." : "Datapack statusline wired.",
|
|
190
|
+
);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
stepFail(currentStep, totalSteps, "Failed to wire datapack statusline.");
|
|
193
|
+
warn(`Non-critical: ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
currentStep++;
|
|
196
|
+
}
|
|
197
|
+
|
|
174
198
|
if (!skipValidation) {
|
|
175
199
|
if (dryRun) {
|
|
176
200
|
dim("Would validate config, MCP connectivity, and tool availability.");
|
package/lib/mcp-transport.mjs
CHANGED
|
@@ -39,6 +39,11 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
|
|
|
39
39
|
const headers = {
|
|
40
40
|
"Content-Type": "application/json",
|
|
41
41
|
Accept: "application/json, text/event-stream",
|
|
42
|
+
// Cloud Meko sits behind a WAF that 403s requests with no User-Agent
|
|
43
|
+
// (Node's http/https send none by default). This transport powers install
|
|
44
|
+
// validation and migration against cloud URLs, so it hits the same WAF as
|
|
45
|
+
// the capture hooks — send an explicit UA so those calls aren't rejected.
|
|
46
|
+
"User-Agent": "meko-installer/1.0 (+https://github.com/yugabyte/meko-mcp-server)",
|
|
42
47
|
};
|
|
43
48
|
if (apiKey) {
|
|
44
49
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
package/lib/validate.mjs
CHANGED
package/package.json
CHANGED
|
@@ -22,6 +22,10 @@ const PAYLOAD = [
|
|
|
22
22
|
from: join(repoRoot, "skills", "skills", "meko-mcp-tools-desktop"),
|
|
23
23
|
to: join(assetsDir, "skills", "meko-mcp-tools-desktop"),
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
from: join(repoRoot, "skills", "skills", "meko-select-datapack-desktop"),
|
|
27
|
+
to: join(assetsDir, "skills", "meko-select-datapack-desktop"),
|
|
28
|
+
},
|
|
25
29
|
{
|
|
26
30
|
from: join(repoRoot, "skills", "hooks-handlers"),
|
|
27
31
|
to: join(assetsDir, "hooks-handlers"),
|
|
@@ -30,6 +34,10 @@ const PAYLOAD = [
|
|
|
30
34
|
from: join(repoRoot, "skills", "scripts", "package-desktop-skill.sh"),
|
|
31
35
|
to: join(assetsDir, "scripts", "package-desktop-skill.sh"),
|
|
32
36
|
},
|
|
37
|
+
{
|
|
38
|
+
from: join(repoRoot, "skills", "scripts", "meko-statusline.sh"),
|
|
39
|
+
to: join(assetsDir, "scripts", "meko-statusline.sh"),
|
|
40
|
+
},
|
|
33
41
|
];
|
|
34
42
|
|
|
35
43
|
const cleaning = process.argv.includes("--clean");
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Read-Only vs Write Query Execution Patterns
|
|
2
|
-
|
|
3
|
-
**WRONG:** `db_run_write_query(scope="write", query="SELECT * FROM users")` — requires write scope unnecessarily, triggers guardrail validation, returns `{"rows_affected": -1}` instead of data.
|
|
4
|
-
**WRONG:** `db_run_read_only_query(scope="read", query="INSERT INTO ...")` — fails: "cannot execute INSERT in a read-only transaction."
|
|
5
|
-
|
|
6
|
-
## How Each Tool Works
|
|
7
|
-
|
|
8
|
-
**`db_run_read_only_query`:** `BEGIN READ ONLY` → execute → `ROLLBACK`. Returns JSON array of row objects.
|
|
9
|
-
|
|
10
|
-
**`db_run_write_query`:** Guardrail validation → execute → `COMMIT`. Returns `{"rows_affected": N}`.
|
|
11
|
-
|
|
12
|
-
## When to Use db_summarize_database
|
|
13
|
-
|
|
14
|
-
Use instead of writing your own `information_schema` query when you need a table overview:
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
db_summarize_database(scope="read", schema="public")
|
|
18
|
-
-- Returns table names, column schemas, AND row counts in one call
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## SQL Best Practices
|
|
22
|
-
|
|
23
|
-
**Schema qualification:** For non-public schemas, use `analytics."monthly_revenue"` and pass `schema="analytics"` to `db_summarize_database`.
|
|
24
|
-
|
|
25
|
-
**String escaping:** Queries are passed directly to `cur.execute()` — no parameter placeholders. Escape values in SQL: `WHERE name = 'O''Brien'`.
|
|
26
|
-
|
|
27
|
-
**LIMIT on exploratory queries:**
|
|
28
|
-
```
|
|
29
|
-
db_run_read_only_query(scope="read", query="SELECT * FROM large_table LIMIT 25")
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**Batch large writes:** Due to the 1000-row INSERT cap, split large inserts. Or use `INSERT ... SELECT` for unlimited rows from an existing table.
|
|
33
|
-
|
|
34
|
-
**One statement per call:**
|
|
35
|
-
```
|
|
36
|
-
WRONG: "CREATE TABLE a (id INT); CREATE TABLE b (id INT);"
|
|
37
|
-
RIGHT: Two separate db_run_write_query calls
|
|
38
|
-
```
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Write Query Guardrails and Blocked Operations
|
|
2
|
-
|
|
3
|
-
**WRONG:** `"INSERT INTO a (...); INSERT INTO b (...)"` → "Multi-statement queries not allowed." Send one statement per call.
|
|
4
|
-
**WRONG:** `"DROP SCHEMA analytics CASCADE"` → "DROP SCHEMA is not allowed."
|
|
5
|
-
**WRONG:** `"\\d users"` → psql meta-commands not supported. Use `information_schema` queries via `db_run_read_only_query`.
|
|
6
|
-
|
|
7
|
-
## Blocked Operations
|
|
8
|
-
|
|
9
|
-
**Database/schema destruction:** `DROP DATABASE`, `DROP SCHEMA`, `ALTER DATABASE`, `CREATE DATABASE`
|
|
10
|
-
**Role/privilege manipulation:** `GRANT`, `REVOKE`, `CREATE/ALTER/DROP ROLE`, `CREATE/ALTER/DROP USER`
|
|
11
|
-
**Filesystem/code execution:** `COPY TO/FROM`, `LOAD`, `DO $$`, `CREATE EXTENSION`
|
|
12
|
-
**Server configuration:** `ALTER SYSTEM`, `RESET ALL`
|
|
13
|
-
**Dangerous functions:** `pg_sleep`, `pg_read_file`, `pg_write_file`, `lo_import`, `lo_export`, `dblink`
|
|
14
|
-
**Structural rules:** One SQL statement per call (multi-statement rejected). No psql `\` commands.
|
|
15
|
-
|
|
16
|
-
## Configurable Limits
|
|
17
|
-
|
|
18
|
-
**Bulk INSERT row cap** (default 1000, env: `YB_MCP_MAX_INSERT_ROWS`): Counts parenthesized tuples in VALUES. Does NOT apply to `INSERT ... SELECT`. Split large inserts into batches of 1000 or fewer.
|
|
19
|
-
|
|
20
|
-
**WHERE clause enforcement** (off by default):
|
|
21
|
-
- `YB_MCP_REQUIRE_WHERE_ON_UPDATE=true` → rejects `UPDATE` without top-level `WHERE`
|
|
22
|
-
- `YB_MCP_REQUIRE_WHERE_ON_DELETE=true` → rejects `DELETE` without top-level `WHERE`
|
|
23
|
-
|
|
24
|
-
A `WHERE` inside a subquery does NOT satisfy the check.
|
|
25
|
-
|
|
26
|
-
## What IS Allowed
|
|
27
|
-
|
|
28
|
-
`INSERT INTO ... VALUES`, `INSERT INTO ... SELECT` (no row limit), `UPDATE ... SET ... WHERE`, `DELETE FROM ... WHERE`, `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, `CREATE INDEX`, `TRUNCATE`, `CALL procedure_name(...)`.
|
|
29
|
-
|
|
30
|
-
SQL comments are stripped before guardrail checks — patterns cannot be hidden in comments.
|