| 1 | diff --git a/internal/webui/webmcp.js b/internal/webui/webmcp.js |
| 2 | index 921568f..f118447 100644 |
| 3 | --- a/internal/webui/webmcp.js |
| 4 | +++ b/internal/webui/webmcp.js |
| 5 | @@ -145,6 +145,8 @@ |
| 6 | object({agent: pubkey}, ["agent"]), reads, (input, signal) => query("browseagent", input, signal)); |
| 7 | register("tiny.list_callbacks", "List event callbacks: id, owner, host, filter, paused state, failures and last delivery. Members and agents see their own; the owner and moderators see every callback. Uses your connected signer.", |
| 8 | object(), reads, (input, signal) => manage("listcallbacks", [], signal)); |
| 9 | + register("tiny.list_join_requests", "List access requests from people who asked to join without an invite: pubkey, reason, time asked, status (pending, approved or denied), who decided and when. Pending requests come first. Uses your connected signer; owner or moderator.", |
| 10 | + object(), reads, (input, signal) => manage("listjoinrequests", [], signal)); |
| 11 | |
| 12 | function open(path) { |
| 13 | const url = new URL(local(path), location.href); |
| 14 | @@ -232,6 +234,10 @@ |
| 15 | object({agent: pubkey}, ["agent"]), "resumeagent", input => [input.agent]); |
| 16 | control("tiny.revoke_agent", "Revoke an agent's grant. The agent loses its role at once; only a new grant restores it.", |
| 17 | object({agent: pubkey}, ["agent"]), "revokeagent", input => [input.agent]); |
| 18 | + control("tiny.approve_join", "Approve an access request: the key becomes a member, the relay's member list and add-user record follow, and the request is marked approved. Owner or moderator.", |
| 19 | + object({pubkey}, ["pubkey"]), "approvejoin", input => [input.pubkey]); |
| 20 | + control("tiny.deny_join", "Deny an access request. The key stays outside the relay and may ask again. Owner or moderator.", |
| 21 | + object({pubkey}, ["pubkey"]), "denyjoin", input => [input.pubkey]); |
| 22 | control("tiny.pause_callback", "Pause an event callback by id. Deliveries stop until it is resumed. The callback's owner, the relay owner and moderators may do this.", |
| 23 | object({id}, ["id"]), "pausecallback", input => [input.id]); |
| 24 | control("tiny.resume_callback", "Resume a paused event callback by id and clear its failure count.", |
| 25 | diff --git a/scripts/webmcp.test.mjs b/scripts/webmcp.test.mjs |
| 26 | index 2461c3c..aa7009c 100644 |
| 27 | --- a/scripts/webmcp.test.mjs |
| 28 | +++ b/scripts/webmcp.test.mjs |
| 29 | @@ -238,6 +238,36 @@ test("agent tools list through the signer, read through the session and control |
| 30 | } |
| 31 | }); |
| 32 | |
| 33 | +test("access request tools list through the signer and decide by pubkey", async () => { |
| 34 | + const calls = []; |
| 35 | + const asker = "b".repeat(64); |
| 36 | + const {tools} = await browser({path: "/r/work/manage/people", signedFetch: async (url, method, body) => { |
| 37 | + calls.push({url, method, body: JSON.parse(body)}); |
| 38 | + const request = JSON.parse(body); |
| 39 | + if (request.method === "listjoinrequests") { |
| 40 | + return new Response(JSON.stringify({result: [{pubkey: asker, reason: "Met you at the meetup", requested_at: 1700000000, status: "pending", decided_by: "", decided_at: 0}]})); |
| 41 | + } |
| 42 | + return new Response(JSON.stringify({result: {pubkey: asker, status: request.method === "approvejoin" ? "approved" : "denied"}})); |
| 43 | + }}); |
| 44 | + const list = tools.get("tiny.list_join_requests"); |
| 45 | + assert.equal(list.annotations.readOnlyHint, true); |
| 46 | + const listed = await list.execute({}); |
| 47 | + assert.equal(listed.structuredContent.result[0].pubkey, asker); |
| 48 | + assert.equal(listed.structuredContent.result[0].status, "pending"); |
| 49 | + assert.equal(calls[0].url, "/r/work/manage/rpc"); |
| 50 | + assert.equal(calls[0].method, "POST"); |
| 51 | + assert.deepEqual(calls[0].body, {method: "listjoinrequests", params: []}); |
| 52 | + for (const [name, method, status] of [["tiny.approve_join", "approvejoin", "approved"], ["tiny.deny_join", "denyjoin", "denied"]]) { |
| 53 | + const tool = tools.get(name); |
| 54 | + assert.equal(tool.annotations.consequentialHint, true); |
| 55 | + assert.deepEqual([...tool.inputSchema.required], ["pubkey"]); |
| 56 | + assert.equal(tool.inputSchema.properties.pubkey.pattern, "^[0-9a-f]{64}$"); |
| 57 | + const result = await tool.execute({pubkey: asker}); |
| 58 | + assert.deepEqual(calls.at(-1).body, {method, params: [asker]}); |
| 59 | + assert.equal(result.structuredContent.result.status, status); |
| 60 | + } |
| 61 | +}); |
| 62 | + |
| 63 | test("callback tools list through the signer and control by id", async () => { |
| 64 | const calls = []; |
| 65 | const {tools} = await browser({path: "/r/work/manage/agents", signedFetch: async (url, method, body) => { |