Skip to content

Commit 7d2cda3

Browse files
MajorTalclaude
andcommitted
Add promote_user and demote_user tools (MCP, CLI, OpenClaw)
New admin tools to manage the project_admin role. promote_user sets is_admin=true, demote_user sets is_admin=false — both by email, using service_key auth. Closes #18. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e8c2242 commit 7d2cda3

13 files changed

Lines changed: 515 additions & 4 deletions

File tree

cli/lib/projects.mjs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Subcommands:
2020
rls <id> <template> <tables_json> Apply Row-Level Security policies
2121
delete <id> Delete a project and remove it from local state
2222
pin <id> Pin a project (prevents expiry/GC)
23+
promote-user <id> <email> Promote a user to project_admin role
24+
demote-user <id> <email> Demote a user from project_admin role
2325
2426
Examples:
2527
run402 projects quote
@@ -190,6 +192,32 @@ async function pin(projectId) {
190192
console.log(JSON.stringify(data, null, 2));
191193
}
192194

195+
async function promoteUser(projectId, email) {
196+
if (!email) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects promote-user <project_id> <email>" })); process.exit(1); }
197+
const p = findProject(projectId);
198+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/promote-user`, {
199+
method: "POST",
200+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
201+
body: JSON.stringify({ email }),
202+
});
203+
const data = await res.json();
204+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
205+
console.log(JSON.stringify(data, null, 2));
206+
}
207+
208+
async function demoteUser(projectId, email) {
209+
if (!email) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects demote-user <project_id> <email>" })); process.exit(1); }
210+
const p = findProject(projectId);
211+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/demote-user`, {
212+
method: "POST",
213+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
214+
body: JSON.stringify({ email }),
215+
});
216+
const data = await res.json();
217+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
218+
console.log(JSON.stringify(data, null, 2));
219+
}
220+
193221
async function deleteProject(projectId) {
194222
const p = findProject(projectId);
195223
const res = await fetch(`${API}/projects/v1/${projectId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${p.service_key}` } });
@@ -220,7 +248,9 @@ export async function run(sub, args) {
220248
case "schema": await schema(args[0]); break;
221249
case "rls": await rls(args[0], args[1], args[2]); break;
222250
case "delete": await deleteProject(args[0]); break;
223-
case "pin": await pin(args[0]); break;
251+
case "pin": await pin(args[0]); break;
252+
case "promote-user": await promoteUser(args[0], args[1]); break;
253+
case "demote-user": await demoteUser(args[0], args[1]); break;
224254
default:
225255
console.error(`Unknown subcommand: ${sub}\n`);
226256
console.log(HELP);
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-30
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## Context
2+
3+
The Run402 gateway added a `project_admin` role with two admin endpoints for promoting/demoting users. These endpoints follow the same pattern as other admin endpoints (`/projects/v1/admin/:id/...`) and require `service_key` auth. Currently, these endpoints are listed in `IGNORED_ENDPOINTS` in sync.test.ts with the comment "not yet exposed as CLI/MCP tools."
4+
5+
All three interfaces (MCP, CLI, OpenClaw) need to be extended. The existing `set_secret` tool is the closest pattern — a simple admin endpoint with service_key auth, project_id + one additional parameter.
6+
7+
## Goals / Non-Goals
8+
9+
**Goals:**
10+
- Expose `promote-user` and `demote-user` across all three interfaces (MCP, CLI, OpenClaw)
11+
- Follow existing tool/command patterns exactly (same error handling, auth, output format)
12+
- Keep sync.test.ts passing with the new entries
13+
14+
**Non-Goals:**
15+
- Adding `--admin` flag to CLI auth signup (the issue mentions this but there is no existing `auth signup` CLI command or MCP signup tool — this would be a separate change)
16+
- Listing or viewing project admin users (no endpoint exists for this)
17+
- Changing the auth or keystore system
18+
19+
## Decisions
20+
21+
**One MCP tool file per action**`promote-user.ts` and `demote-user.ts` as separate files, matching the project convention (each tool is its own file). Alternative: a single `user-role.ts` with two exports. Rejected because every existing tool follows the one-file-per-action pattern.
22+
23+
**CLI subcommands under `projects`**`promote-user` and `demote-user` as subcommands of `run402 projects`, matching the endpoint path structure (`/projects/v1/admin/:id/...`). The CLI convention is that admin operations on a project live under `projects:*`.
24+
25+
**service_key auth, not allowance auth** — These endpoints require service_key (stored in keystore per project), not wallet-based allowance auth. This matches other admin endpoints like `sql`, `schema`, `usage`.
26+
27+
**No separate OpenClaw module** — OpenClaw's `projects.mjs` is already a thin re-export of CLI's `projects.mjs`. Since the new subcommands are added directly to the CLI's run() switch, OpenClaw gets them for free with no changes needed.
28+
29+
## Risks / Trade-offs
30+
31+
**[Risk] Email not found returns error** → The API may return 404 or similar if the email doesn't correspond to a signed-up user. The tools will surface this via `formatApiError` in MCP and JSON error output in CLI, giving actionable feedback.
32+
33+
**[Risk] Demoting the last admin** → The gateway should handle this constraint, not the client tools. No client-side validation needed.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## Why
2+
3+
The Run402 gateway now supports a `project_admin` role — app-level admins who can manage secrets from the browser. Two new admin endpoints (`promote-user`, `demote-user`) exist in the API but have no MCP tools, CLI commands, or OpenClaw shims. The signup endpoint also now accepts `is_admin` but the CLI doesn't expose that flag. Without tooling, developers can't manage project admins through any of the three interfaces.
4+
5+
## What Changes
6+
7+
- **New MCP tools**: `promote_user(project_id, email)` and `demote_user(project_id, email)` — call the admin endpoints with service_key auth
8+
- **New CLI commands**: `run402 projects promote-user` and `run402 projects demote-user` — same endpoints, CLI output format
9+
- **New OpenClaw shims**: thin re-exports of the CLI commands
10+
- **CLI enhancement**: `--admin` flag on `run402 auth signup` — passes `is_admin: true` in the signup request body (requires service_key)
11+
- **MCP enhancement**: optional `is_admin` parameter on the existing signup flow (if an MCP signup tool exists; otherwise skip)
12+
- **sync.test.ts**: add both tools to the `SURFACE` array and remove them from `IGNORED_ENDPOINTS`
13+
14+
## Capabilities
15+
16+
### New Capabilities
17+
- `user-role-management`: Promote and demote project users to/from the `project_admin` role via MCP, CLI, and OpenClaw
18+
19+
### Modified Capabilities
20+
<!-- No existing specs are changing at the requirement level -->
21+
22+
## Impact
23+
24+
- **MCP server** (`src/tools/`): two new tool files (`promote-user.ts`, `demote-user.ts`)
25+
- **CLI** (`cli/lib/projects.mjs`): two new subcommands (`promote-user`, `demote-user`)
26+
- **OpenClaw** (`openclaw/scripts/projects.mjs`): re-export the new CLI subcommands
27+
- **sync.test.ts**: update `SURFACE` array (add entries) and `IGNORED_ENDPOINTS` (remove the two endpoints)
28+
- **API endpoints used**: `POST /projects/v1/admin/:id/promote-user`, `POST /projects/v1/admin/:id/demote-user` — both require service_key auth
29+
- **No new dependencies** — uses existing `apiRequest`, `getProject`, `formatApiError` patterns
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
## ADDED Requirements
2+
3+
### Requirement: MCP promote_user tool
4+
The system SHALL provide a `promote_user` MCP tool that sets a user's `is_admin` flag to `true` for a given project. The tool SHALL accept `project_id` (string) and `email` (string) parameters. It SHALL use `service_key` auth from the local keystore. It SHALL call `POST /projects/v1/admin/:id/promote-user` with `{ email }` in the request body. It SHALL return a success message including the email and project_id, or an error via `formatApiError`.
5+
6+
#### Scenario: Successfully promote a user
7+
- **WHEN** `promote_user` is called with a valid `project_id` and `email` of an existing user
8+
- **THEN** the tool calls the promote-user endpoint with service_key auth and returns a success text response
9+
10+
#### Scenario: Project not in keystore
11+
- **WHEN** `promote_user` is called with a `project_id` not present in the local keystore
12+
- **THEN** the tool returns `projectNotFound` error without making an API call
13+
14+
#### Scenario: API returns error
15+
- **WHEN** the promote-user endpoint returns a non-OK response (e.g. 404 user not found)
16+
- **THEN** the tool returns the formatted error via `formatApiError`
17+
18+
### Requirement: MCP demote_user tool
19+
The system SHALL provide a `demote_user` MCP tool that sets a user's `is_admin` flag to `false` for a given project. The tool SHALL accept `project_id` (string) and `email` (string) parameters. It SHALL use `service_key` auth from the local keystore. It SHALL call `POST /projects/v1/admin/:id/demote-user` with `{ email }` in the request body. It SHALL return a success message including the email and project_id, or an error via `formatApiError`.
20+
21+
#### Scenario: Successfully demote a user
22+
- **WHEN** `demote_user` is called with a valid `project_id` and `email` of an existing admin user
23+
- **THEN** the tool calls the demote-user endpoint with service_key auth and returns a success text response
24+
25+
#### Scenario: Project not in keystore
26+
- **WHEN** `demote_user` is called with a `project_id` not present in the local keystore
27+
- **THEN** the tool returns `projectNotFound` error without making an API call
28+
29+
#### Scenario: API returns error
30+
- **WHEN** the demote-user endpoint returns a non-OK response
31+
- **THEN** the tool returns the formatted error via `formatApiError`
32+
33+
### Requirement: CLI promote-user subcommand
34+
The system SHALL provide a `run402 projects promote-user <id> <email>` CLI subcommand. It SHALL look up the project via `findProject`, call the same API endpoint as the MCP tool, and output JSON to stdout. On error, it SHALL print JSON to stderr and exit with code 1.
35+
36+
#### Scenario: CLI promote-user success
37+
- **WHEN** `run402 projects promote-user <id> <email>` is run with a valid project and email
38+
- **THEN** the command outputs the API response as JSON to stdout
39+
40+
#### Scenario: CLI promote-user project not found
41+
- **WHEN** the project_id is not in the local keystore
42+
- **THEN** the command prints an error and exits with code 1
43+
44+
### Requirement: CLI demote-user subcommand
45+
The system SHALL provide a `run402 projects demote-user <id> <email>` CLI subcommand. It SHALL look up the project via `findProject`, call the same API endpoint as the MCP tool, and output JSON to stdout. On error, it SHALL print JSON to stderr and exit with code 1.
46+
47+
#### Scenario: CLI demote-user success
48+
- **WHEN** `run402 projects demote-user <id> <email>` is run with a valid project and email
49+
- **THEN** the command outputs the API response as JSON to stdout
50+
51+
#### Scenario: CLI demote-user project not found
52+
- **WHEN** the project_id is not in the local keystore
53+
- **THEN** the command prints an error and exits with code 1
54+
55+
### Requirement: OpenClaw commands via re-export
56+
The OpenClaw `projects.mjs` SHALL continue to re-export the CLI `projects.mjs` run function, which means the new `promote-user` and `demote-user` subcommands are automatically available with no additional OpenClaw changes.
57+
58+
#### Scenario: OpenClaw promote-user available
59+
- **WHEN** OpenClaw dispatches `projects promote-user <id> <email>`
60+
- **THEN** the CLI's promote-user handler is invoked via the re-export
61+
62+
### Requirement: sync.test.ts surface entries
63+
The `SURFACE` array in `sync.test.ts` SHALL include entries for `promote_user` and `demote_user` with their correct MCP tool names, CLI commands (`projects:promote-user`, `projects:demote-user`), OpenClaw commands, and API endpoints. The two endpoints SHALL be removed from `IGNORED_ENDPOINTS`.
64+
65+
#### Scenario: Sync test passes with new tools
66+
- **WHEN** `npm run test:sync` is executed
67+
- **THEN** the test passes with the new promote_user and demote_user entries in SURFACE and removed from IGNORED_ENDPOINTS
68+
69+
### Requirement: MCP tool registration
70+
Both `promote_user` and `demote_user` tools SHALL be registered in `src/index.ts` following the existing pattern (import schema + handler, register with `server.tool()`).
71+
72+
#### Scenario: Tools appear in MCP tool list
73+
- **WHEN** the MCP server starts
74+
- **THEN** both `promote_user` and `demote_user` appear in the tool list with their Zod schemas
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## 1. MCP Tools
2+
3+
- [x] 1.1 Create `src/tools/promote-user.ts` — export `promoteUserSchema` and `handlePromoteUser` following the `set-secret.ts` pattern (service_key auth, `formatApiError`, `projectNotFound`)
4+
- [x] 1.2 Create `src/tools/demote-user.ts` — export `demoteUserSchema` and `handleDemoteUser` following the same pattern
5+
- [x] 1.3 Register both tools in `src/index.ts` — import schemas and handlers, add `server.tool()` calls
6+
7+
## 2. CLI Commands
8+
9+
- [x] 2.1 Add `promote-user` and `demote-user` subcommands to `cli/lib/projects.mjs` — add handler functions and switch cases in `run()`
10+
- [x] 2.2 Update the HELP text in `cli/lib/projects.mjs` to document the new subcommands
11+
12+
## 3. Sync Test
13+
14+
- [x] 3.1 Add `promote_user` and `demote_user` entries to the `SURFACE` array in `sync.test.ts`
15+
- [x] 3.2 Remove `POST /projects/v1/admin/:id/promote-user` and `POST /projects/v1/admin/:id/demote-user` from `IGNORED_ENDPOINTS`
16+
17+
## 4. Tests & Verification
18+
19+
- [x] 4.1 Create unit tests for the MCP tools (`src/tools/promote-user.test.ts`, `src/tools/demote-user.test.ts`) — test success, project not found, and API error cases
20+
- [x] 4.2 Run `npm test` to verify all tests pass (sync test, unit tests)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
### Requirement: MCP promote_user tool
2+
The system SHALL provide a `promote_user` MCP tool that sets a user's `is_admin` flag to `true` for a given project. The tool SHALL accept `project_id` (string) and `email` (string) parameters. It SHALL use `service_key` auth from the local keystore. It SHALL call `POST /projects/v1/admin/:id/promote-user` with `{ email }` in the request body. It SHALL return a success message including the email and project_id, or an error via `formatApiError`.
3+
4+
#### Scenario: Successfully promote a user
5+
- **WHEN** `promote_user` is called with a valid `project_id` and `email` of an existing user
6+
- **THEN** the tool calls the promote-user endpoint with service_key auth and returns a success text response
7+
8+
#### Scenario: Project not in keystore
9+
- **WHEN** `promote_user` is called with a `project_id` not present in the local keystore
10+
- **THEN** the tool returns `projectNotFound` error without making an API call
11+
12+
#### Scenario: API returns error
13+
- **WHEN** the promote-user endpoint returns a non-OK response (e.g. 404 user not found)
14+
- **THEN** the tool returns the formatted error via `formatApiError`
15+
16+
### Requirement: MCP demote_user tool
17+
The system SHALL provide a `demote_user` MCP tool that sets a user's `is_admin` flag to `false` for a given project. The tool SHALL accept `project_id` (string) and `email` (string) parameters. It SHALL use `service_key` auth from the local keystore. It SHALL call `POST /projects/v1/admin/:id/demote-user` with `{ email }` in the request body. It SHALL return a success message including the email and project_id, or an error via `formatApiError`.
18+
19+
#### Scenario: Successfully demote a user
20+
- **WHEN** `demote_user` is called with a valid `project_id` and `email` of an existing admin user
21+
- **THEN** the tool calls the demote-user endpoint with service_key auth and returns a success text response
22+
23+
#### Scenario: Project not in keystore
24+
- **WHEN** `demote_user` is called with a `project_id` not present in the local keystore
25+
- **THEN** the tool returns `projectNotFound` error without making an API call
26+
27+
#### Scenario: API returns error
28+
- **WHEN** the demote-user endpoint returns a non-OK response
29+
- **THEN** the tool returns the formatted error via `formatApiError`
30+
31+
### Requirement: CLI promote-user subcommand
32+
The system SHALL provide a `run402 projects promote-user <id> <email>` CLI subcommand. It SHALL look up the project via `findProject`, call the same API endpoint as the MCP tool, and output JSON to stdout. On error, it SHALL print JSON to stderr and exit with code 1.
33+
34+
#### Scenario: CLI promote-user success
35+
- **WHEN** `run402 projects promote-user <id> <email>` is run with a valid project and email
36+
- **THEN** the command outputs the API response as JSON to stdout
37+
38+
#### Scenario: CLI promote-user project not found
39+
- **WHEN** the project_id is not in the local keystore
40+
- **THEN** the command prints an error and exits with code 1
41+
42+
### Requirement: CLI demote-user subcommand
43+
The system SHALL provide a `run402 projects demote-user <id> <email>` CLI subcommand. It SHALL look up the project via `findProject`, call the same API endpoint as the MCP tool, and output JSON to stdout. On error, it SHALL print JSON to stderr and exit with code 1.
44+
45+
#### Scenario: CLI demote-user success
46+
- **WHEN** `run402 projects demote-user <id> <email>` is run with a valid project and email
47+
- **THEN** the command outputs the API response as JSON to stdout
48+
49+
#### Scenario: CLI demote-user project not found
50+
- **WHEN** the project_id is not in the local keystore
51+
- **THEN** the command prints an error and exits with code 1
52+
53+
### Requirement: OpenClaw commands via re-export
54+
The OpenClaw `projects.mjs` SHALL continue to re-export the CLI `projects.mjs` run function, which means the new `promote-user` and `demote-user` subcommands are automatically available with no additional OpenClaw changes.
55+
56+
#### Scenario: OpenClaw promote-user available
57+
- **WHEN** OpenClaw dispatches `projects promote-user <id> <email>`
58+
- **THEN** the CLI's promote-user handler is invoked via the re-export
59+
60+
### Requirement: sync.test.ts surface entries
61+
The `SURFACE` array in `sync.test.ts` SHALL include entries for `promote_user` and `demote_user` with their correct MCP tool names, CLI commands (`projects:promote-user`, `projects:demote-user`), OpenClaw commands, and API endpoints. The two endpoints SHALL be removed from `IGNORED_ENDPOINTS`.
62+
63+
#### Scenario: Sync test passes with new tools
64+
- **WHEN** `npm run test:sync` is executed
65+
- **THEN** the test passes with the new promote_user and demote_user entries in SURFACE and removed from IGNORED_ENDPOINTS
66+
67+
### Requirement: MCP tool registration
68+
Both `promote_user` and `demote_user` tools SHALL be registered in `src/index.ts` following the existing pattern (import schema + handler, register with `server.tool()`).
69+
70+
#### Scenario: Tools appear in MCP tool list
71+
- **WHEN** the MCP server starts
72+
- **THEN** both `promote_user` and `demote_user` appear in the tool list with their Zod schemas

src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ import { listSubdomainsSchema, handleListSubdomains } from "./tools/list-subdoma
4646
import { archiveProjectSchema, handleArchiveProject } from "./tools/archive-project.js";
4747
import { pinProjectSchema, handlePinProject } from "./tools/pin-project.js";
4848

49+
// New tools — user role management
50+
import { promoteUserSchema, handlePromoteUser } from "./tools/promote-user.js";
51+
import { demoteUserSchema, handleDemoteUser } from "./tools/demote-user.js";
52+
4953
// New tools — billing
5054
import { checkBalanceSchema, handleCheckBalance } from "./tools/check-balance.js";
5155
import { listProjectsSchema, handleListProjects } from "./tools/list-projects.js";
@@ -332,6 +336,20 @@ server.tool(
332336
async (args) => handlePinProject(args),
333337
);
334338

339+
server.tool(
340+
"promote_user",
341+
"Promote a user to project_admin role by email. Admins can manage secrets from the browser. Requires service_key.",
342+
promoteUserSchema,
343+
async (args) => handlePromoteUser(args),
344+
);
345+
346+
server.tool(
347+
"demote_user",
348+
"Demote a user from project_admin role by email. Reverts to default authenticated role. Requires service_key.",
349+
demoteUserSchema,
350+
async (args) => handleDemoteUser(args),
351+
);
352+
335353
// ─── Billing & allowance tools ───────────────────────────────────────────────
336354

337355
server.tool(

0 commit comments

Comments
 (0)