Skip to content

Commit ed2d16b

Browse files
MajorTalclaude
andcommitted
Add parameterized query support to run_sql and expand db docs
- MCP run_sql tool and CLI projects sql now accept optional params array - When params provided, sends JSON body { sql, params } instead of text/plain - CLI accepts --params '[1, "hello"]' flag with JSON validation - SKILL.md expanded with db.sql() return type, parameterized examples, and full db.from() chainable API documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent edece43 commit ed2d16b

12 files changed

Lines changed: 299 additions & 9 deletions

File tree

SKILL.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,18 @@ Deploy a serverless function (Node 22) to a project. Functions are invoked via H
206206
**DB access inside functions:**
207207
```typescript
208208
import { db } from '@run402/functions';
209-
const users = await db.from('users').select('*');
210-
const result = await db.sql('SELECT count(*) FROM orders');
211209
```
212210

211+
**db.sql(query, params?)** — raw SQL, returns `{ status, schema, rows, rowCount }`.
212+
- SELECT: `rows` = matching rows, `rowCount` = row count
213+
- INSERT/UPDATE/DELETE: `rows` = `[]`, `rowCount` = affected rows
214+
- Parameterized: `db.sql('SELECT * FROM t WHERE id = $1', [42])`
215+
216+
**db.from(table)** — PostgREST-style queries (service_role, bypasses RLS). Returns a plain array of row objects.
217+
Chainable read methods: `.select(cols?)`, `.eq(col, val)`, `.neq()`, `.gt()`, `.lt()`, `.gte()`, `.lte()`, `.like()`, `.ilike()`, `.in(col, [vals])`, `.order(col, { ascending? })`, `.limit(n)`, `.offset(n)`
218+
Chainable write methods: `.insert(obj | obj[])`, `.update(obj)`, `.delete()` — all return array of affected rows.
219+
Column narrowing: `.insert({...}).select('col1, col2')` returns only specified columns.
220+
213221
**Secrets:** Access via `process.env.SECRET_NAME`. Set with `set_secret`.
214222

215223
### invoke_function

cli/lib/projects.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Subcommands:
1313
list List all your projects (IDs, URLs, active marker)
1414
info <id> Show project details: REST URL, keys
1515
keys <id> Print anon_key and service_key as JSON
16-
sql <id> "<query>" [--file <path>] Run a SQL query against a project's Postgres DB
16+
sql <id> "<query>" [--file <path>] [--params '<json>'] Run a SQL query (supports parameterized queries)
1717
rest <id> <table> [params] Query a table via the REST API (PostgREST)
1818
usage <id> Show compute/storage usage for a project
1919
schema <id> Inspect the database schema
@@ -29,6 +29,7 @@ Examples:
2929
run402 projects list
3030
run402 projects info abc123
3131
run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
32+
run402 projects sql abc123 "SELECT * FROM users WHERE id = $1" --params '[42]'
3233
run402 projects sql abc123 --file setup.sql
3334
run402 projects rest abc123 users "limit=10&select=id,name"
3435
run402 projects usage abc123
@@ -121,13 +122,23 @@ async function sqlCmd(projectId, args = []) {
121122
const p = findProject(projectId);
122123
let file = null;
123124
let query = null;
125+
let paramsRaw = null;
124126
for (let i = 0; i < args.length; i++) {
125127
if (args[i] === "--file" && args[i + 1]) { file = args[++i]; }
128+
else if (args[i] === "--params" && args[i + 1]) { paramsRaw = args[++i]; }
126129
else if (!query && !args[i].startsWith("--")) { query = args[i]; }
127130
}
128131
const sql = file ? readFileSync(file, "utf-8") : query;
129132
if (!sql) { console.error(JSON.stringify({ status: "error", message: "Missing SQL query. Provide inline or use --file <path>" })); process.exit(1); }
130-
const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "text/plain" }, body: sql });
133+
let params;
134+
if (paramsRaw) {
135+
try { params = JSON.parse(paramsRaw); } catch { console.error(JSON.stringify({ status: "error", message: "Invalid JSON for --params. Expected a JSON array, e.g. '[42, \"hello\"]'" })); process.exit(1); }
136+
if (!Array.isArray(params)) { console.error(JSON.stringify({ status: "error", message: "--params must be a JSON array, e.g. '[42, \"hello\"]'" })); process.exit(1); }
137+
}
138+
const useParams = params && params.length > 0;
139+
const headers = { "Authorization": `Bearer ${p.service_key}`, "Content-Type": useParams ? "application/json" : "text/plain" };
140+
const body = useParams ? JSON.stringify({ sql, params }) : sql;
141+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers, body });
131142
console.log(JSON.stringify(await res.json(), null, 2));
132143
}
133144

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-03-28
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## Context
2+
3+
The run402 API endpoint `POST /projects/v1/admin/:id/sql` now accepts two content types:
4+
- `text/plain` — raw SQL string (existing behavior)
5+
- `application/json``{ "sql": "...", "params": [...] }` for parameterized queries (new)
6+
7+
The `@run402/functions` Lambda runtime has expanded `db.sql()` to accept params and `db.from()` with a full chainable API. The SKILL.md documentation only shows a minimal 2-line example.
8+
9+
This repo's MCP tool (`run-sql.ts`) and CLI (`projects.mjs`) currently send plain text only. They need a JSON path for params.
10+
11+
## Goals / Non-Goals
12+
13+
**Goals:**
14+
- MCP `run_sql` and CLI `projects sql` support optional parameterized queries
15+
- SKILL.md documents the full `db.sql()` and `db.from()` API matching llms.txt
16+
- Backward compatible — omitting params preserves existing plain-text behavior
17+
18+
**Non-Goals:**
19+
- Changing the API endpoint behavior (already done in run402)
20+
- Adding params support to `rest_query` (uses PostgREST, not raw SQL)
21+
- Changing the response format (already returns `{ status, schema, rows, rowCount }`)
22+
23+
## Decisions
24+
25+
**1. JSON body when params present, plain text otherwise**
26+
27+
When `params` is provided and non-empty, send `Content-Type: application/json` with `{ sql, params }`. When omitted, keep existing `text/plain` with raw SQL body. This maintains backward compatibility and avoids unnecessary format changes.
28+
29+
Alternative: Always send JSON. Rejected because it changes behavior for all callers and the API still has the text/plain path documented.
30+
31+
**2. CLI `--params` as JSON array string**
32+
33+
The CLI accepts `--params '[1, "hello"]'` as a JSON string that gets parsed. This matches common CLI patterns (e.g., `aws --cli-input-json`). Alternative: positional args after `--` separator. Rejected as harder to parse and ambiguous with mixed types.
34+
35+
**3. SKILL.md docs mirror llms.txt structure**
36+
37+
Copy the expanded db.sql() and db.from() documentation directly from the llms.txt diff. This keeps the two sources consistent and avoids documentation drift.
38+
39+
## Risks / Trade-offs
40+
41+
- [Params type safety] CLI params arrive as a JSON string — invalid JSON will fail at parse time. → Mitigation: validate with `JSON.parse()` and give a clear error message.
42+
- [SKILL.md size] Adding ~15 lines of API docs. → Acceptable; this is the primary reference for agents using functions.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## Why
2+
3+
The run402 API now supports parameterized queries on `POST /projects/v1/admin/:id/sql` (accepts JSON `{ sql, params }`) and the `@run402/functions` Lambda runtime has expanded `db.sql(query, params?)` and `db.from(table)` APIs. The MCP tool, CLI command, and SKILL.md documentation are out of sync with these backend capabilities.
4+
5+
## What Changes
6+
7+
- **MCP `run_sql` tool**: Add optional `params` array to schema. When provided, send `Content-Type: application/json` with `{ sql, params }` instead of plain text.
8+
- **CLI `projects sql` command**: Add `--params` flag accepting a JSON array string. Same JSON body behavior.
9+
- **SKILL.md**: Expand "DB access inside functions" section to document `db.sql()` return type, parameterized query support, and full `db.from()` chainable API (matching llms.txt).
10+
- OpenClaw inherits CLI changes automatically via shims.
11+
12+
## Capabilities
13+
14+
### New Capabilities
15+
- `parameterized-sql`: Add parameterized query support to the `run_sql` MCP tool and CLI `projects sql` command
16+
- `functions-db-docs`: Expand SKILL.md documentation for `db.sql()` and `db.from()` runtime helpers
17+
18+
### Modified Capabilities
19+
20+
## Impact
21+
22+
- `src/tools/run-sql.ts` — schema + handler changes (JSON body path)
23+
- `src/tools/run-sql.test.ts` — new test cases for params
24+
- `cli/lib/projects.mjs``sqlCmd` changes for `--params` flag
25+
- `SKILL.md` — expanded DB section (~15 lines replacing 2)
26+
- `sync.test.ts` — no surface changes (same tool names), but verify tests pass
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## ADDED Requirements
2+
3+
### Requirement: SKILL.md documents db.sql with params and return type
4+
The SKILL.md "DB access inside functions" section SHALL document:
5+
- `db.sql(query, params?)` signature with optional params
6+
- Return type: `{ status, schema, rows, rowCount }`
7+
- SELECT behavior: `rows` = matching rows, `rowCount` = row count
8+
- INSERT/UPDATE/DELETE behavior: `rows` = `[]`, `rowCount` = affected rows
9+
- Parameterized example: `db.sql('SELECT * FROM t WHERE id = $1', [42])`
10+
11+
#### Scenario: Agent reads SKILL.md for db.sql usage
12+
- **WHEN** an agent reads the SKILL.md functions section
13+
- **THEN** it finds documentation for parameterized queries with `db.sql(query, params?)` and understands the return type shape
14+
15+
### Requirement: SKILL.md documents db.from chainable API
16+
The SKILL.md "DB access inside functions" section SHALL document:
17+
- `db.from(table)` returns a PostgREST-style query builder (service_role, bypasses RLS) that returns a plain array of row objects
18+
- Chainable read methods: `.select(cols?)`, `.eq(col, val)`, `.neq()`, `.gt()`, `.lt()`, `.gte()`, `.lte()`, `.like()`, `.ilike()`, `.in(col, [vals])`, `.order(col, { ascending? })`, `.limit(n)`, `.offset(n)`
19+
- Chainable write methods: `.insert(obj | obj[])`, `.update(obj)`, `.delete()` — all return array of affected rows
20+
- Column narrowing: `.insert({...}).select('col1, col2')` returns only specified columns
21+
22+
#### Scenario: Agent reads SKILL.md for db.from usage
23+
- **WHEN** an agent reads the SKILL.md functions section
24+
- **THEN** it finds the full chainable API for `db.from()` including read methods, write methods, and column narrowing
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## ADDED Requirements
2+
3+
### Requirement: MCP run_sql accepts optional params array
4+
The MCP `run_sql` tool schema SHALL accept an optional `params` field of type array. When `params` is provided and non-empty, the tool SHALL send the request as `Content-Type: application/json` with body `{ "sql": "<sql>", "params": [...] }`. When `params` is omitted or empty, the tool SHALL send `Content-Type: text/plain` with the raw SQL string as body (existing behavior).
5+
6+
#### Scenario: Parameterized query via MCP
7+
- **WHEN** `run_sql` is called with `sql: "SELECT * FROM users WHERE id = $1"` and `params: [42]`
8+
- **THEN** the tool sends a POST to `/projects/v1/admin/:id/sql` with `Content-Type: application/json` and body `{"sql":"SELECT * FROM users WHERE id = $1","params":[42]}`
9+
10+
#### Scenario: Plain SQL via MCP (backward compat)
11+
- **WHEN** `run_sql` is called with `sql: "SELECT 1"` and no `params`
12+
- **THEN** the tool sends a POST with `Content-Type: text/plain` and body `SELECT 1`
13+
14+
#### Scenario: Empty params array treated as no params
15+
- **WHEN** `run_sql` is called with `sql: "SELECT 1"` and `params: []`
16+
- **THEN** the tool sends a POST with `Content-Type: text/plain` and body `SELECT 1`
17+
18+
### Requirement: CLI projects sql accepts --params flag
19+
The CLI `projects sql` command SHALL accept an optional `--params` flag whose value is a JSON array string. When provided, the CLI SHALL parse the JSON and send the request as `application/json` with `{ sql, params }`. Invalid JSON SHALL cause the command to exit with an error message.
20+
21+
#### Scenario: Parameterized query via CLI
22+
- **WHEN** user runs `run402 projects sql <id> "SELECT * FROM t WHERE id = $1" --params '[42]'`
23+
- **THEN** the CLI sends a JSON body with sql and params to the API
24+
25+
#### Scenario: Invalid params JSON
26+
- **WHEN** user runs `run402 projects sql <id> "SELECT 1" --params 'not-json'`
27+
- **THEN** the CLI exits with an error message about invalid JSON params
28+
29+
#### Scenario: CLI without params (backward compat)
30+
- **WHEN** user runs `run402 projects sql <id> "SELECT 1"` without `--params`
31+
- **THEN** the CLI sends `text/plain` with the raw SQL string
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## 1. MCP run_sql parameterized queries
2+
3+
- [x] 1.1 Add optional `params` field (z.array(z.unknown()).optional()) to `runSqlSchema` in `src/tools/run-sql.ts`
4+
- [x] 1.2 Update `handleRunSql` to send JSON body when params is non-empty, plain text otherwise
5+
- [x] 1.3 Add test cases in `src/tools/run-sql.test.ts`: parameterized query sends JSON, no params sends plain text, empty params sends plain text
6+
7+
## 2. CLI projects sql --params flag
8+
9+
- [x] 2.1 Add `--params` flag parsing to `sqlCmd` in `cli/lib/projects.mjs`
10+
- [x] 2.2 Validate JSON.parse of params value, exit with error on invalid JSON
11+
- [x] 2.3 Send JSON body with `{ sql, params }` when params provided, plain text otherwise
12+
13+
## 3. SKILL.md documentation
14+
15+
- [x] 3.1 Replace the 2-line db example (lines 206-211) with expanded `db.sql(query, params?)` docs including return type and parameterized example
16+
- [x] 3.2 Add `db.from(table)` chainable API documentation (read methods, write methods, column narrowing)
17+
18+
## 4. Verify
19+
20+
- [x] 4.1 Run `npm test` — all unit and sync tests pass
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
### Requirement: SKILL.md documents db.sql with params and return type
2+
The SKILL.md "DB access inside functions" section SHALL document:
3+
- `db.sql(query, params?)` signature with optional params
4+
- Return type: `{ status, schema, rows, rowCount }`
5+
- SELECT behavior: `rows` = matching rows, `rowCount` = row count
6+
- INSERT/UPDATE/DELETE behavior: `rows` = `[]`, `rowCount` = affected rows
7+
- Parameterized example: `db.sql('SELECT * FROM t WHERE id = $1', [42])`
8+
9+
#### Scenario: Agent reads SKILL.md for db.sql usage
10+
- **WHEN** an agent reads the SKILL.md functions section
11+
- **THEN** it finds documentation for parameterized queries with `db.sql(query, params?)` and understands the return type shape
12+
13+
### Requirement: SKILL.md documents db.from chainable API
14+
The SKILL.md "DB access inside functions" section SHALL document:
15+
- `db.from(table)` returns a PostgREST-style query builder (service_role, bypasses RLS) that returns a plain array of row objects
16+
- Chainable read methods: `.select(cols?)`, `.eq(col, val)`, `.neq()`, `.gt()`, `.lt()`, `.gte()`, `.lte()`, `.like()`, `.ilike()`, `.in(col, [vals])`, `.order(col, { ascending? })`, `.limit(n)`, `.offset(n)`
17+
- Chainable write methods: `.insert(obj | obj[])`, `.update(obj)`, `.delete()` — all return array of affected rows
18+
- Column narrowing: `.insert({...}).select('col1, col2')` returns only specified columns
19+
20+
#### Scenario: Agent reads SKILL.md for db.from usage
21+
- **WHEN** an agent reads the SKILL.md functions section
22+
- **THEN** it finds the full chainable API for `db.from()` including read methods, write methods, and column narrowing
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
### Requirement: MCP run_sql accepts optional params array
2+
The MCP `run_sql` tool schema SHALL accept an optional `params` field of type array. When `params` is provided and non-empty, the tool SHALL send the request as `Content-Type: application/json` with body `{ "sql": "<sql>", "params": [...] }`. When `params` is omitted or empty, the tool SHALL send `Content-Type: text/plain` with the raw SQL string as body (existing behavior).
3+
4+
#### Scenario: Parameterized query via MCP
5+
- **WHEN** `run_sql` is called with `sql: "SELECT * FROM users WHERE id = $1"` and `params: [42]`
6+
- **THEN** the tool sends a POST to `/projects/v1/admin/:id/sql` with `Content-Type: application/json` and body `{"sql":"SELECT * FROM users WHERE id = $1","params":[42]}`
7+
8+
#### Scenario: Plain SQL via MCP (backward compat)
9+
- **WHEN** `run_sql` is called with `sql: "SELECT 1"` and no `params`
10+
- **THEN** the tool sends a POST with `Content-Type: text/plain` and body `SELECT 1`
11+
12+
#### Scenario: Empty params array treated as no params
13+
- **WHEN** `run_sql` is called with `sql: "SELECT 1"` and `params: []`
14+
- **THEN** the tool sends a POST with `Content-Type: text/plain` and body `SELECT 1`
15+
16+
### Requirement: CLI projects sql accepts --params flag
17+
The CLI `projects sql` command SHALL accept an optional `--params` flag whose value is a JSON array string. When provided, the CLI SHALL parse the JSON and send the request as `application/json` with `{ sql, params }`. Invalid JSON SHALL cause the command to exit with an error message.
18+
19+
#### Scenario: Parameterized query via CLI
20+
- **WHEN** user runs `run402 projects sql <id> "SELECT * FROM t WHERE id = $1" --params '[42]'`
21+
- **THEN** the CLI sends a JSON body with sql and params to the API
22+
23+
#### Scenario: Invalid params JSON
24+
- **WHEN** user runs `run402 projects sql <id> "SELECT 1" --params 'not-json'`
25+
- **THEN** the CLI exits with an error message about invalid JSON params
26+
27+
#### Scenario: CLI without params (backward compat)
28+
- **WHEN** user runs `run402 projects sql <id> "SELECT 1"` without `--params`
29+
- **THEN** the CLI sends `text/plain` with the raw SQL string

0 commit comments

Comments
 (0)