feat(requests): P2 — agent MCP tools for unified requests/inbox (RFC) #56
@@ -1162,9 +1162,17 @@ describe("createServer()", () => {
|
||||
test("registers all tools (count is stable across registerXxxTools wiring)", () => {
|
||||
const server = createServer() as unknown as { registeredToolNames: string[] };
|
||||
const names = server.registeredToolNames;
|
||||
expect(names.length).toBe(89);
|
||||
expect(names.length).toBe(96);
|
||||
// create_issue (Gitea bug-filing) must be wired into the default surface.
|
||||
expect(names).toContain("create_issue");
|
||||
// Unified requests/inbox tools (RFC P2) — all 7 wired into the surface.
|
||||
expect(names).toContain("create_request");
|
||||
expect(names).toContain("list_inbox");
|
||||
expect(names).toContain("check_requests");
|
||||
expect(names).toContain("get_request");
|
||||
expect(names).toContain("respond_request");
|
||||
expect(names).toContain("add_request_message");
|
||||
expect(names).toContain("cancel_request");
|
||||
// Names must be unique — a duplicate registration would indicate a
|
||||
// copy-paste mistake in one of the registerXxxTools() calls.
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Unit tests for the unified requests / inbox tools (src/tools/requests.ts).
|
||||
*
|
||||
* fetch is mocked globally (no real HTTP). Each test asserts the handler hits
|
||||
* the right path + method + body and returns the standard MCP envelope. Mirrors
|
||||
* the fetch-mock convention in index.test.ts / issues.test.ts.
|
||||
*/
|
||||
|
||||
import { PLATFORM_URL } from "../api.js";
|
||||
import {
|
||||
handleCreateRequest,
|
||||
handleListInbox,
|
||||
handleCheckRequests,
|
||||
handleGetRequest,
|
||||
handleRespondRequest,
|
||||
handleAddRequestMessage,
|
||||
handleCancelRequest,
|
||||
} from "../tools/requests.js";
|
||||
|
||||
function mockFetch(payload: unknown, ok = true, status = 200) {
|
||||
const body = typeof payload === "string" ? payload : JSON.stringify(payload);
|
||||
return jest.fn().mockResolvedValue({
|
||||
ok,
|
||||
status,
|
||||
headers: { get: () => null },
|
||||
text: jest.fn().mockResolvedValue(body),
|
||||
});
|
||||
}
|
||||
|
||||
function mockFetchSequence(responses: Array<{ payload: unknown; ok?: boolean; status?: number }>) {
|
||||
const fn = jest.fn();
|
||||
for (const r of responses) {
|
||||
fn.mockResolvedValueOnce({
|
||||
ok: r.ok ?? true,
|
||||
status: r.status ?? 200,
|
||||
headers: { get: () => null },
|
||||
text: jest.fn().mockResolvedValue(JSON.stringify(r.payload)),
|
||||
});
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
function bodyOf(result: { content: { type: string; text: string }[] }) {
|
||||
return JSON.parse(result.content[0].text);
|
||||
}
|
||||
|
||||
afterEach(() => jest.restoreAllMocks());
|
||||
|
||||
describe("create_request", () => {
|
||||
it("POSTs a task to the requester workspace's /requests with the full body", async () => {
|
||||
global.fetch = mockFetch({ request_id: "req-1", status: "pending" }) as unknown as typeof fetch;
|
||||
const res = await handleCreateRequest({
|
||||
workspace_id: "ws-1",
|
||||
kind: "task",
|
||||
recipient_type: "agent",
|
||||
recipient_id: "ws-2",
|
||||
title: "do the thing",
|
||||
detail: "with care",
|
||||
priority: 5,
|
||||
});
|
||||
|
||||
const call = (global.fetch as jest.Mock).mock.calls[0];
|
||||
expect(call[0]).toBe(`${PLATFORM_URL}/workspaces/ws-1/requests`);
|
||||
expect(call[1].method).toBe("POST");
|
||||
const sent = JSON.parse(call[1].body);
|
||||
expect(sent).toEqual({
|
||||
kind: "task",
|
||||
recipient_type: "agent",
|
||||
recipient_id: "ws-2",
|
||||
title: "do the thing",
|
||||
detail: "with care",
|
||||
priority: 5,
|
||||
});
|
||||
expect(bodyOf(res).request_id).toBe("req-1");
|
||||
});
|
||||
|
||||
it("POSTs an approval addressed to a user", async () => {
|
||||
global.fetch = mockFetch({ request_id: "req-2", status: "pending" }) as unknown as typeof fetch;
|
||||
await handleCreateRequest({
|
||||
workspace_id: "ws-9",
|
||||
kind: "approval",
|
||||
recipient_type: "user",
|
||||
recipient_id: "user-7",
|
||||
title: "approve deploy",
|
||||
});
|
||||
const sent = JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body);
|
||||
expect(sent.kind).toBe("approval");
|
||||
expect(sent.recipient_type).toBe("user");
|
||||
expect(sent.recipient_id).toBe("user-7");
|
||||
});
|
||||
});
|
||||
|
||||
describe("list_inbox vs check_requests", () => {
|
||||
it("list_inbox GETs the recipient inbox path with a status filter", async () => {
|
||||
global.fetch = mockFetch([{ request_id: "req-1" }]) as unknown as typeof fetch;
|
||||
await handleListInbox({ workspace_id: "ws-1", status: "pending" });
|
||||
const url = (global.fetch as jest.Mock).mock.calls[0][0];
|
||||
expect(url).toBe(`${PLATFORM_URL}/workspaces/ws-1/requests/inbox?status=pending`);
|
||||
expect((global.fetch as jest.Mock).mock.calls[0][1].method).toBe("GET");
|
||||
});
|
||||
|
||||
it("check_requests GETs the OUTGOING /requests path (not the inbox)", async () => {
|
||||
global.fetch = mockFetch([{ request_id: "req-2" }]) as unknown as typeof fetch;
|
||||
await handleCheckRequests({ workspace_id: "ws-1" });
|
||||
const url = (global.fetch as jest.Mock).mock.calls[0][0];
|
||||
expect(url).toBe(`${PLATFORM_URL}/workspaces/ws-1/requests`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get_request", () => {
|
||||
it("GETs the per-workspace request path (agent auth scope)", async () => {
|
||||
global.fetch = mockFetch({ request: { request_id: "req-1" }, messages: [] }) as unknown as typeof fetch;
|
||||
const res = await handleGetRequest({ workspace_id: "ws-1", request_id: "req-1" });
|
||||
const url = (global.fetch as jest.Mock).mock.calls[0][0];
|
||||
expect(url).toBe(`${PLATFORM_URL}/workspaces/ws-1/requests/req-1`);
|
||||
expect(bodyOf(res).request.request_id).toBe("req-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("respond_request", () => {
|
||||
it("POSTs the terminal action with responder_type=agent, responder_id=workspace_id", async () => {
|
||||
global.fetch = mockFetch({ status: "done", request_id: "req-1" }) as unknown as typeof fetch;
|
||||
await handleRespondRequest({ workspace_id: "ws-1", request_id: "req-1", action: "done" });
|
||||
const call = (global.fetch as jest.Mock).mock.calls[0];
|
||||
expect(call[0]).toBe(`${PLATFORM_URL}/workspaces/ws-1/requests/req-1/respond`);
|
||||
expect(call[1].method).toBe("POST");
|
||||
const sent = JSON.parse(call[1].body);
|
||||
expect(sent).toEqual({ action: "done", responder_type: "agent", responder_id: "ws-1" });
|
||||
});
|
||||
|
||||
it("also posts a thread message when `message` is supplied, returning both results", async () => {
|
||||
global.fetch = mockFetchSequence([
|
||||
{ payload: { status: "approved", request_id: "req-1" } },
|
||||
{ payload: { status: "created", request_id: "req-1", message_id: "m-1" } },
|
||||
]) as unknown as typeof fetch;
|
||||
const res = await handleRespondRequest({
|
||||
workspace_id: "ws-1",
|
||||
request_id: "req-1",
|
||||
action: "approved",
|
||||
message: "looks good",
|
||||
});
|
||||
const calls = (global.fetch as jest.Mock).mock.calls;
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[1][0]).toBe(`${PLATFORM_URL}/workspaces/ws-1/requests/req-1/messages`);
|
||||
const msgBody = JSON.parse(calls[1][1].body);
|
||||
expect(msgBody).toEqual({ body: "looks good", author_type: "agent", author_id: "ws-1" });
|
||||
const out = bodyOf(res);
|
||||
expect(out.respond.status).toBe("approved");
|
||||
expect(out.message.message_id).toBe("m-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("add_request_message", () => {
|
||||
it("POSTs the thread message with author_type=agent, author_id=workspace_id", async () => {
|
||||
global.fetch = mockFetch({ status: "created", request_id: "req-1", message_id: "m-9" }) as unknown as typeof fetch;
|
||||
await handleAddRequestMessage({ workspace_id: "ws-1", request_id: "req-1", body: "need more info" });
|
||||
const call = (global.fetch as jest.Mock).mock.calls[0];
|
||||
expect(call[0]).toBe(`${PLATFORM_URL}/workspaces/ws-1/requests/req-1/messages`);
|
||||
expect(call[1].method).toBe("POST");
|
||||
const sent = JSON.parse(call[1].body);
|
||||
expect(sent).toEqual({ body: "need more info", author_type: "agent", author_id: "ws-1" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel_request", () => {
|
||||
it("POSTs the cancel path for the requester workspace", async () => {
|
||||
global.fetch = mockFetch({ status: "cancelled", request_id: "req-1" }) as unknown as typeof fetch;
|
||||
const res = await handleCancelRequest({ workspace_id: "ws-1", request_id: "req-1" });
|
||||
const call = (global.fetch as jest.Mock).mock.calls[0];
|
||||
expect(call[0]).toBe(`${PLATFORM_URL}/workspaces/ws-1/requests/req-1/cancel`);
|
||||
expect(call[1].method).toBe("POST");
|
||||
expect(bodyOf(res).status).toBe("cancelled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error passthrough", () => {
|
||||
it("surfaces a non-2xx platform error in the envelope (HTTP <code>)", async () => {
|
||||
global.fetch = mockFetch("boom", false, 500) as unknown as typeof fetch;
|
||||
const res = await handleCreateRequest({
|
||||
workspace_id: "ws-1",
|
||||
kind: "task",
|
||||
recipient_type: "agent",
|
||||
recipient_id: "ws-2",
|
||||
title: "x",
|
||||
});
|
||||
expect(bodyOf(res).error).toContain("HTTP 500");
|
||||
});
|
||||
});
|
||||
+16
-1
@@ -27,6 +27,7 @@ import { registerApprovalTools } from "./tools/approvals.js";
|
||||
import { registerDiscoveryTools } from "./tools/discovery.js";
|
||||
import { registerRemoteAgentTools } from "./tools/remote_agents.js";
|
||||
import { registerIssueTools } from "./tools/issues.js";
|
||||
import { registerRequestTools } from "./tools/requests.js";
|
||||
import { registerManagementTools } from "./tools/management/index.js";
|
||||
|
||||
// Re-exports so existing importers (tests, SDK consumers) keep working.
|
||||
@@ -231,6 +232,16 @@ export {
|
||||
giteaApiUrl,
|
||||
defaultIssueRepo,
|
||||
} from "./tools/issues.js";
|
||||
export {
|
||||
registerRequestTools,
|
||||
handleCreateRequest,
|
||||
handleListInbox,
|
||||
handleCheckRequests,
|
||||
handleGetRequest,
|
||||
handleRespondRequest,
|
||||
handleAddRequestMessage,
|
||||
handleCancelRequest,
|
||||
} from "./tools/requests.js";
|
||||
export { mgmtCall, mgmtGet, managementUrl } from "./tools/management/client.js";
|
||||
export { registerCpAdminTools, handleListOrgs, handleGetOrg, cpUrl, cpConfigured } from "./tools/management/cp_admin.js";
|
||||
|
||||
@@ -264,6 +275,9 @@ export function createServer() {
|
||||
// host and an agent on the workspace surface both observe bugs worth
|
||||
// tracking). The tool name is unique, so it is safe in both registries.
|
||||
registerIssueTools(srv);
|
||||
// Unified requests/inbox tools (RFC P2) — registered in BOTH modes, same
|
||||
// as create_issue: an agent on either surface can raise/answer requests.
|
||||
registerRequestTools(srv);
|
||||
return srv;
|
||||
}
|
||||
|
||||
@@ -280,6 +294,7 @@ export function createServer() {
|
||||
registerDiscoveryTools(srv);
|
||||
registerRemoteAgentTools(srv);
|
||||
registerIssueTools(srv);
|
||||
registerRequestTools(srv);
|
||||
|
||||
return srv;
|
||||
}
|
||||
@@ -347,7 +362,7 @@ async function main() {
|
||||
mode: "management",
|
||||
});
|
||||
} else {
|
||||
logInfo("Molecule AI MCP server running on stdio (89 tools available)", { transport: "stdio", toolCount: 89 });
|
||||
logInfo("Molecule AI MCP server running on stdio (96 tools available)", { transport: "stdio", toolCount: 96 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Unified requests / inbox tools — RFC "unified-requests-inbox", Phase 2.
|
||||
*
|
||||
* These are the AGENT-FACING MCP tools for the requests subsystem: the one
|
||||
* primitive that generalizes "tasks" (agent → user/agent asks) and "approvals"
|
||||
* (the gate) into a single inbox keyed by `kind` ∈ {task, approval}, where both
|
||||
* the requester and the recipient may be a user OR another agent.
|
||||
*
|
||||
* Responding is ASYNCHRONOUS: a requester is never blocked. It raises a request
|
||||
* (`create_request`), keeps working, and later picks up the answer with
|
||||
* `check_requests`. A recipient sees incoming work via `list_inbox` and acts on
|
||||
* it with `respond_request` / `add_request_message`.
|
||||
*
|
||||
* Every tool acts AS a workspace (the agent), mirroring the approvals tools
|
||||
* which all take `workspace_id`. The Phase-1 workspace-server registers the
|
||||
* agent-side action verbs under the per-workspace, workspace-token-auth prefix
|
||||
* `/workspaces/:id/requests/...` (the bare `/requests/:requestId/...` paths are
|
||||
* AdminAuth-gated for the canvas user — NOT reachable with a workspace token),
|
||||
* so EVERY tool below — including get/respond/messages/cancel — routes through
|
||||
* `/workspaces/{workspace_id}/requests/...`. See
|
||||
* workspace-server/internal/router/router.go (the `wsAuth` group) and
|
||||
* handlers/requests.go for the contract.
|
||||
*
|
||||
* The pre-existing approval tools (create_approval, decide_approval, …) are
|
||||
* left untouched — they keep working against the old /approvals endpoints; the
|
||||
* formal shim/deprecation is a later phase (P5).
|
||||
*/
|
||||
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
||||
import { validate } from "../utils/validation.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schemas
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CreateRequestSchema = z.object({
|
||||
workspace_id: z.string().describe("Acting workspace (the requesting agent)"),
|
||||
kind: z.enum(["task", "approval"]).describe("task = please do X; approval = please approve X"),
|
||||
recipient_type: z.enum(["user", "agent"]).describe("Whether the recipient is a user or another agent"),
|
||||
recipient_id: z
|
||||
.string()
|
||||
.describe("Recipient id — a workspace id for an agent recipient, or a user id for a user recipient"),
|
||||
title: z.string().describe("Short one-line summary of what is being asked"),
|
||||
detail: z.string().optional().describe("Full detail / context for the request"),
|
||||
priority: z.number().int().optional().describe("Optional integer priority (higher = more urgent)"),
|
||||
});
|
||||
export type CreateRequestParams = z.infer<typeof CreateRequestSchema>;
|
||||
|
||||
const ListInboxSchema = z.object({
|
||||
workspace_id: z.string().describe("Acting workspace (the recipient agent)"),
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional status filter, e.g. pending | info_requested | done | rejected | approved | cancelled"),
|
||||
});
|
||||
export type ListInboxParams = z.infer<typeof ListInboxSchema>;
|
||||
|
||||
const CheckRequestsSchema = z.object({
|
||||
workspace_id: z.string().describe("Acting workspace (the requesting agent)"),
|
||||
status: z.string().optional().describe("Optional status filter (see list_inbox)"),
|
||||
});
|
||||
export type CheckRequestsParams = z.infer<typeof CheckRequestsSchema>;
|
||||
|
||||
const GetRequestSchema = z.object({
|
||||
workspace_id: z.string().describe("Acting workspace (the agent making the call; used for auth scope)"),
|
||||
request_id: z.string().describe("Request id to fetch"),
|
||||
});
|
||||
export type GetRequestParams = z.infer<typeof GetRequestSchema>;
|
||||
|
||||
const RespondRequestSchema = z.object({
|
||||
workspace_id: z.string().describe("Acting workspace (the responding agent)"),
|
||||
request_id: z.string().describe("Request id to respond to"),
|
||||
action: z
|
||||
.enum(["done", "rejected", "approved"])
|
||||
.describe("Terminal action — must be valid for the request's kind (task → done/rejected; approval → approved/rejected)"),
|
||||
message: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional note posted to the request's More-Info thread alongside the response"),
|
||||
});
|
||||
export type RespondRequestParams = z.infer<typeof RespondRequestSchema>;
|
||||
|
||||
const AddRequestMessageSchema = z.object({
|
||||
workspace_id: z.string().describe("Acting workspace (the agent authoring the message)"),
|
||||
request_id: z.string().describe("Request id whose thread to append to"),
|
||||
body: z.string().describe("Message text. If the author is the recipient, this flips the request to info_requested"),
|
||||
});
|
||||
export type AddRequestMessageParams = z.infer<typeof AddRequestMessageSchema>;
|
||||
|
||||
const CancelRequestSchema = z.object({
|
||||
workspace_id: z.string().describe("Acting workspace (the requester withdrawing the request)"),
|
||||
request_id: z.string().describe("Request id to cancel/withdraw"),
|
||||
});
|
||||
export type CancelRequestParams = z.infer<typeof CancelRequestSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function handleCreateRequest(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||
const p = validate(args, CreateRequestSchema);
|
||||
const data = await apiCall("POST", `/workspaces/${p.workspace_id}/requests`, {
|
||||
kind: p.kind,
|
||||
recipient_type: p.recipient_type,
|
||||
recipient_id: p.recipient_id,
|
||||
title: p.title,
|
||||
detail: p.detail,
|
||||
priority: p.priority,
|
||||
});
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleListInbox(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||
const p = validate(args, ListInboxSchema);
|
||||
const qs = p.status ? `?status=${encodeURIComponent(p.status)}` : "";
|
||||
const data = await platformGet(`/workspaces/${p.workspace_id}/requests/inbox${qs}`);
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleCheckRequests(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||
const p = validate(args, CheckRequestsSchema);
|
||||
const qs = p.status ? `?status=${encodeURIComponent(p.status)}` : "";
|
||||
const data = await platformGet(`/workspaces/${p.workspace_id}/requests${qs}`);
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleGetRequest(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||
const p = validate(args, GetRequestSchema);
|
||||
const data = await platformGet(`/workspaces/${p.workspace_id}/requests/${p.request_id}`);
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleRespondRequest(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||
const p = validate(args, RespondRequestSchema);
|
||||
const data = await apiCall(
|
||||
"POST",
|
||||
`/workspaces/${p.workspace_id}/requests/${p.request_id}/respond`,
|
||||
{ action: p.action, responder_type: "agent", responder_id: p.workspace_id }
|
||||
);
|
||||
// If a note was supplied, post it to the More-Info thread too. The response
|
||||
// envelope returns both results so the caller sees each outcome (no silent
|
||||
// drop if the thread post fails).
|
||||
if (p.message && p.message.trim().length > 0) {
|
||||
const msg = await apiCall(
|
||||
"POST",
|
||||
`/workspaces/${p.workspace_id}/requests/${p.request_id}/messages`,
|
||||
{ body: p.message, author_type: "agent", author_id: p.workspace_id }
|
||||
);
|
||||
return toMcpResult({ respond: data, message: msg });
|
||||
}
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleAddRequestMessage(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||
const p = validate(args, AddRequestMessageSchema);
|
||||
const data = await apiCall(
|
||||
"POST",
|
||||
`/workspaces/${p.workspace_id}/requests/${p.request_id}/messages`,
|
||||
{ body: p.body, author_type: "agent", author_id: p.workspace_id }
|
||||
);
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
export async function handleCancelRequest(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||
const p = validate(args, CancelRequestSchema);
|
||||
const data = await apiCall("POST", `/workspaces/${p.workspace_id}/requests/${p.request_id}/cancel`);
|
||||
return toMcpResult(data);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function registerRequestTools(srv: McpServer) {
|
||||
srv.tool(
|
||||
"create_request",
|
||||
"Raise a request (a task or an approval) addressed to a user or another agent. " +
|
||||
"kind='task' asks someone to DO something; kind='approval' asks someone to APPROVE something. " +
|
||||
"Asynchronous: you are not blocked — poll for the answer later with check_requests.",
|
||||
{
|
||||
workspace_id: z.string().describe("Acting workspace (the requesting agent)"),
|
||||
kind: z.enum(["task", "approval"]).describe("task = please do X; approval = please approve X"),
|
||||
recipient_type: z.enum(["user", "agent"]).describe("Whether the recipient is a user or another agent"),
|
||||
recipient_id: z
|
||||
.string()
|
||||
.describe("Recipient id — a workspace id for an agent recipient, or a user id for a user recipient"),
|
||||
title: z.string().describe("Short one-line summary of what is being asked"),
|
||||
detail: z.string().optional().describe("Full detail / context for the request"),
|
||||
priority: z.number().int().optional().describe("Optional integer priority (higher = more urgent)"),
|
||||
},
|
||||
handleCreateRequest
|
||||
);
|
||||
|
||||
srv.tool(
|
||||
"list_inbox",
|
||||
"List requests addressed TO this agent (its inbox) — the incoming tasks/approvals it should act on. " +
|
||||
"Optionally filter by status (e.g. pending).",
|
||||
{
|
||||
workspace_id: z.string().describe("Acting workspace (the recipient agent)"),
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional status filter, e.g. pending | info_requested | done | rejected | approved | cancelled"),
|
||||
},
|
||||
handleListInbox
|
||||
);
|
||||
|
||||
srv.tool(
|
||||
"check_requests",
|
||||
"Check the status of requests this agent RAISED (the async pickup of responses). " +
|
||||
"Use after create_request to see whether a recipient has responded.",
|
||||
{
|
||||
workspace_id: z.string().describe("Acting workspace (the requesting agent)"),
|
||||
status: z.string().optional().describe("Optional status filter (see list_inbox)"),
|
||||
},
|
||||
handleCheckRequests
|
||||
);
|
||||
|
||||
srv.tool(
|
||||
"get_request",
|
||||
"Get a single request plus its full More-Info message thread.",
|
||||
{
|
||||
workspace_id: z.string().describe("Acting workspace (the agent making the call; used for auth scope)"),
|
||||
request_id: z.string().describe("Request id to fetch"),
|
||||
},
|
||||
handleGetRequest
|
||||
);
|
||||
|
||||
srv.tool(
|
||||
"respond_request",
|
||||
"Respond to a request addressed to this agent with a terminal action " +
|
||||
"(done | rejected | approved — must be valid for the request's kind). " +
|
||||
"Optionally include a message, which is also posted to the request's thread.",
|
||||
{
|
||||
workspace_id: z.string().describe("Acting workspace (the responding agent)"),
|
||||
request_id: z.string().describe("Request id to respond to"),
|
||||
action: z
|
||||
.enum(["done", "rejected", "approved"])
|
||||
.describe("Terminal action — task → done/rejected; approval → approved/rejected"),
|
||||
message: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional note posted to the request's More-Info thread alongside the response"),
|
||||
},
|
||||
handleRespondRequest
|
||||
);
|
||||
|
||||
srv.tool(
|
||||
"add_request_message",
|
||||
"Add a message to a request's More-Info thread (e.g. to ask the requester for clarification). " +
|
||||
"When the author is the recipient, this flips the request to info_requested.",
|
||||
{
|
||||
workspace_id: z.string().describe("Acting workspace (the agent authoring the message)"),
|
||||
request_id: z.string().describe("Request id whose thread to append to"),
|
||||
body: z.string().describe("Message text"),
|
||||
},
|
||||
handleAddRequestMessage
|
||||
);
|
||||
|
||||
srv.tool(
|
||||
"cancel_request",
|
||||
"Withdraw (cancel) a request this agent previously raised.",
|
||||
{
|
||||
workspace_id: z.string().describe("Acting workspace (the requester withdrawing the request)"),
|
||||
request_id: z.string().describe("Request id to cancel/withdraw"),
|
||||
},
|
||||
handleCancelRequest
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user