Files
PaperClipAI/packages/mcp-server/src/tools.ts
T
3abe9e2134 build(deps): bump zod from 3.25.76 to 4.4.3 (#11719)
Bumps [zod](https://github.com/colinhacks/zod) from 3.25.76 to 4.4.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/colinhacks/zod/releases">zod's
releases</a>.</em></p>
<blockquote>
<h2>v4.4.3</h2>
<h2>Commits:</h2>
<ul>
<li>4c2fa95ce3f3390fbc522324e406b4e9e89b88f9 docs: use Zernio primary
wordmark for gold sponsor logo</li>
<li>2aeec83eb135e3a83756e973ef44845fc5a455d2 docs: prune lapsed gold
sponsors and rebalance logo sizing</li>
<li>7391be88ac1ee5cd02057f5ccc012a1f5df4efd0 docs: prune lapsed
silver/bronze sponsors and add active ones</li>
<li>2c703322a21b4e2b12f33f49ea8430c451a68b4f docs: normalize bronze
sponsor logos to github avatar pattern</li>
<li>9195250cab0e7950efe39c3926d6c203b4b0a170 docs: remove Mintlify from
bronze sponsors (churned)</li>
<li>b8dffe9e62f17e6571e6249d05cc5102b54d94e4 docs: remove Numeric and
Speakeasy (2+ missed monthly cycles)</li>
<li>1cab69383fcdeae2a366d5e2a2fc4d8fc765d168 fix(v4): restore catch
handling for absent object keys (<a
href="https://redirect.github.com/colinhacks/zod/issues/5937">#5937</a>)
(<a
href="https://redirect.github.com/colinhacks/zod/issues/5939">#5939</a>)</li>
<li>c2be4f819064eed62c7c350a2d399b5faecd15f8 fix(v4): generalize
optin/fallback to transform; restore preprocess on absent keys (<a
href="https://redirect.github.com/colinhacks/zod/issues/5941">#5941</a>)</li>
<li>f3c9ec03ba7a28ae72d25cc295f38674bee0f559 4.4.3</li>
<li>1fb56a5c18c27102dbc92260a4007c7732a0ccca docs: document release
procedure in AGENTS.md</li>
</ul>
<h2>v4.4.2</h2>
<h2>Commits:</h2>
<ul>
<li>0c62df0ea19fd05abdf90473e9eef7eea530fab2 Clean up docs navigation
and stale labels (<a
href="https://redirect.github.com/colinhacks/zod/issues/5901">#5901</a>)</li>
<li>20cc794895cc8604fe0c87d83a5d1c3f89fad0ac chore: add security policy
and refresh tooling deps</li>
<li>6fbe07b0177efdd1bf1c0b05160e70d7a0702337 fix(docs): heading anchor
links now include the hash so it doesnt scoll all the way up, follows
navbar logic (<a
href="https://redirect.github.com/colinhacks/zod/issues/5791">#5791</a>)</li>
<li>4bbed1b1c73eca4ce9e59b1189ed236aa6c8b5bd Tighten discriminated union
option typing</li>
<li>bbac3e567e7fccfaaf7cdc97f1ce30c295e2c908 Update PR guidance for
agents</li>
<li>cf0dc942a32805c292fff59ade20a7ace980735a Merge remote-tracking
branch 'origin/main' into fix-discriminated-union-key-constraint</li>
<li>292c894a5fd2aa42e527900b83d8d7a3009a709c docs: add Zernio gold
sponsor</li>
<li>1fc9f311c28dcf80d0bb5a36b177086cbc3d8eca docs: document codec
inversion</li>
<li>1373c85da9aeff704a9762d27bc58699618aefb7 docs: remove AI disclosure
guidance</li>
<li>e20d02b473c08e3a4e557bc610b1b5fac079b649 chore: ignore triage
notes</li>
<li>e58ea4d91b1dfe8194b73508203213cbc7e9c936 docs: test Zod Mini tab
code heights</li>
<li>905761a5d127e8d5dd2ebb3bc88c75cb0b8149ff docs: document preprocess
input type narrowing</li>
<li>bf64bac850d4dee2b7dde7e64909d5d796d32043 chore: tighten test
guidance in AGENTS.md</li>
<li>8ec4e73f4c4693b6361ad591be40fb41eb8a9f95 chore: update play.ts
scratch</li>
<li>02c2baf7d0d615872fa4528a8020603b71211702 Make z.preprocess defer
optionality to inner schema (<a
href="https://redirect.github.com/colinhacks/zod/issues/5929">#5929</a>)</li>
<li>88015df8e25c44fb5385eb3ef28935119cd5edea fix(docs): drop deprecated
<code>baseUrl</code> from tsconfig</li>
<li>c59d4474e3b4cad1b323462186cf607178ce8267 4.4.2</li>
</ul>
<h2>v4.4.1</h2>
<h2>Commits:</h2>
<ul>
<li>481f7be4238c83ed58183f921b2646f340a91c6a ci: gate release publishing
on full test workflow</li>
<li>95ccab423aec720b2523c3a64cdc7e3204537cc7 test(v3): restore optional
undefined expectations</li>
<li>cede2c63739a5823d6aa5093d291e9a111da943d fix(v4): reject tuple holes
before required defaults (<a
href="https://redirect.github.com/colinhacks/zod/issues/5900">#5900</a>)</li>
<li>edd0bf0f5ada4a8dc581c259407d7bbad0a71ea7 release: 4.4.1</li>
<li>180d83d1dbe6a59260710cc8637a3dea2281ee56 docs: remove Jazz featured
sponsor</li>
</ul>
<h2>v4.4.0</h2>
<h2>4.4.0</h2>
<p>This is a minor release with a wide set of correctness and soundness
fixes. Some fixes intentionally make Zod stricter, so code that depended
on previously accepted invalid or ambiguous inputs may need small
updates.</p>
<h2>Potentially breaking bug fixes</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/colinhacks/zod/commit/1fb56a5c18c27102dbc92260a4007c7732a0ccca"><code>1fb56a5</code></a>
docs: document release procedure in AGENTS.md</li>
<li><a
href="https://github.com/colinhacks/zod/commit/f3c9ec03ba7a28ae72d25cc295f38674bee0f559"><code>f3c9ec0</code></a>
4.4.3</li>
<li><a
href="https://github.com/colinhacks/zod/commit/c2be4f819064eed62c7c350a2d399b5faecd15f8"><code>c2be4f8</code></a>
fix(v4): generalize optin/fallback to transform; restore preprocess on
absent...</li>
<li><a
href="https://github.com/colinhacks/zod/commit/1cab69383fcdeae2a366d5e2a2fc4d8fc765d168"><code>1cab693</code></a>
fix(v4): restore catch handling for absent object keys (<a
href="https://redirect.github.com/colinhacks/zod/issues/5937">#5937</a>)
(<a
href="https://redirect.github.com/colinhacks/zod/issues/5939">#5939</a>)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/b8dffe9e62f17e6571e6249d05cc5102b54d94e4"><code>b8dffe9</code></a>
docs: remove Numeric and Speakeasy (2+ missed monthly cycles)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/9195250cab0e7950efe39c3926d6c203b4b0a170"><code>9195250</code></a>
docs: remove Mintlify from bronze sponsors (churned)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/2c703322a21b4e2b12f33f49ea8430c451a68b4f"><code>2c70332</code></a>
docs: normalize bronze sponsor logos to github avatar pattern</li>
<li><a
href="https://github.com/colinhacks/zod/commit/7391be88ac1ee5cd02057f5ccc012a1f5df4efd0"><code>7391be8</code></a>
docs: prune lapsed silver/bronze sponsors and add active ones</li>
<li><a
href="https://github.com/colinhacks/zod/commit/2aeec83eb135e3a83756e973ef44845fc5a455d2"><code>2aeec83</code></a>
docs: prune lapsed gold sponsors and rebalance logo sizing</li>
<li><a
href="https://github.com/colinhacks/zod/commit/4c2fa95ce3f3390fbc522324e406b4e9e89b88f9"><code>4c2fa95</code></a>
docs: use Zernio primary wordmark for gold sponsor logo</li>
<li>Additional commits viewable in <a
href="https://github.com/colinhacks/zod/compare/v3.25.76...v4.4.3">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for zod since your current version.</p>
</details>
<br />

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Priya Raman <priya@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-21 00:04:14 -07:00

635 lines
24 KiB
TypeScript

import { z } from "zod";
import {
addIssueCommentSchema,
askUserQuestionsPayloadSchema,
checkoutIssueSchema,
createApprovalSchema,
createIssueInputSchema,
issueThreadInteractionContinuationPolicySchema,
requestCheckboxConfirmationPayloadSchema,
requestConfirmationPayloadSchema,
suggestTasksPayloadSchema,
updateIssueSchema,
upsertIssueDocumentSchema,
linkIssueApprovalSchema,
} from "@paperclipai/shared";
import { PaperclipApiClient } from "./client.js";
import { formatErrorResponse, formatTextResponse } from "./format.js";
export interface ToolDefinition {
name: string;
description: string;
schema: z.ZodObject;
execute: (input: Record<string, unknown>) => Promise<{
content: Array<{ type: "text"; text: string }>;
}>;
}
function makeTool<TSchema extends z.ZodRawShape>(
name: string,
description: string,
schema: z.ZodObject<TSchema>,
execute: (input: z.infer<typeof schema>) => Promise<unknown>,
): ToolDefinition {
return {
name,
description,
schema,
execute: async (input) => {
try {
const parsed = schema.parse(input);
return formatTextResponse(await execute(parsed));
} catch (error) {
return formatErrorResponse(error);
}
},
};
}
function parseOptionalJson(raw: string | undefined | null): unknown {
if (!raw || raw.trim().length === 0) return undefined;
return JSON.parse(raw);
}
const companyIdOptional = z.string().guid().optional().nullable();
const agentIdOptional = z.string().guid().optional().nullable();
const issueIdSchema = z.string().min(1);
const projectIdSchema = z.string().min(1);
const goalIdSchema = z.string().guid();
const approvalIdSchema = z.string().guid();
const documentKeySchema = z.string().trim().min(1).max(64);
const listIssuesSchema = z.object({
companyId: companyIdOptional,
status: z.string().optional(),
projectId: z.string().guid().optional(),
assigneeAgentId: z.string().guid().optional(),
participantAgentId: z.string().guid().optional(),
assigneeUserId: z.string().optional(),
touchedByUserId: z.string().optional(),
inboxArchivedByUserId: z.string().optional(),
unreadForUserId: z.string().optional(),
labelId: z.string().guid().optional(),
executionWorkspaceId: z.string().guid().optional(),
originKind: z.string().optional(),
originId: z.string().optional(),
includeRoutineExecutions: z.boolean().optional(),
includeLiveDescendantSummary: z.boolean().optional(),
q: z.string().optional(),
});
const listCommentsSchema = z.object({
issueId: issueIdSchema,
after: z.string().guid().optional(),
order: z.enum(["asc", "desc"]).optional(),
limit: z.number().int().positive().max(500).optional(),
});
const upsertDocumentToolSchema = z.object({
issueId: issueIdSchema,
key: documentKeySchema,
title: z.string().trim().max(200).nullable().optional(),
format: z.enum(["markdown"]).default("markdown"),
body: z.string().max(524288),
changeSummary: z.string().trim().max(500).nullable().optional(),
baseRevisionId: z.string().guid().nullable().optional(),
});
const createIssueToolSchema = z.object({
companyId: companyIdOptional,
}).merge(createIssueInputSchema);
const updateIssueToolSchema = z.object({
issueId: issueIdSchema,
}).merge(updateIssueSchema);
const checkoutIssueToolSchema = z.object({
issueId: issueIdSchema,
agentId: agentIdOptional,
expectedStatuses: checkoutIssueSchema.shape.expectedStatuses.optional(),
});
const addCommentToolSchema = z.object({
issueId: issueIdSchema,
}).merge(addIssueCommentSchema);
const createSuggestTasksToolSchema = z.object({
issueId: issueIdSchema,
idempotencyKey: z.string().trim().max(255).nullable().optional(),
sourceCommentId: z.string().guid().nullable().optional(),
sourceRunId: z.string().guid().nullable().optional(),
title: z.string().trim().max(240).nullable().optional(),
summary: z.string().trim().max(1000).nullable().optional(),
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
payload: suggestTasksPayloadSchema,
});
const createAskUserQuestionsToolSchema = z.object({
issueId: issueIdSchema,
idempotencyKey: z.string().trim().max(255).nullable().optional(),
sourceCommentId: z.string().guid().nullable().optional(),
sourceRunId: z.string().guid().nullable().optional(),
title: z.string().trim().max(240).nullable().optional(),
summary: z.string().trim().max(1000).nullable().optional(),
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
payload: askUserQuestionsPayloadSchema,
});
const createRequestConfirmationToolSchema = z.object({
issueId: issueIdSchema,
idempotencyKey: z.string().trim().max(255).nullable().optional(),
sourceCommentId: z.string().guid().nullable().optional(),
sourceRunId: z.string().guid().nullable().optional(),
title: z.string().trim().max(240).nullable().optional(),
summary: z.string().trim().max(1000).nullable().optional(),
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("none"),
payload: requestConfirmationPayloadSchema,
});
const createRequestCheckboxConfirmationToolSchema = z.object({
issueId: issueIdSchema,
idempotencyKey: z.string().trim().max(255).nullable().optional(),
sourceCommentId: z.string().guid().nullable().optional(),
sourceRunId: z.string().guid().nullable().optional(),
title: z.string().trim().max(240).nullable().optional(),
summary: z.string().trim().max(1000).nullable().optional(),
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
payload: requestCheckboxConfirmationPayloadSchema,
});
const approvalDecisionSchema = z.object({
approvalId: approvalIdSchema,
action: z.enum(["approve", "reject", "requestRevision", "resubmit"]),
decisionNote: z.string().optional(),
payloadJson: z.string().optional(),
});
const createApprovalToolSchema = z.object({
companyId: companyIdOptional,
}).merge(createApprovalSchema);
const apiRequestSchema = z.object({
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
path: z.string().min(1),
jsonBody: z.string().optional(),
});
const workspaceRuntimeControlTargetSchema = z.object({
workspaceCommandId: z.string().min(1).optional().nullable(),
runtimeServiceId: z.string().guid().optional().nullable(),
serviceIndex: z.number().int().nonnegative().optional().nullable(),
});
const issueWorkspaceRuntimeControlSchema = z.object({
issueId: issueIdSchema,
action: z.enum(["start", "stop", "restart"]),
}).merge(workspaceRuntimeControlTargetSchema);
const waitForIssueWorkspaceServiceSchema = z.object({
issueId: issueIdSchema,
runtimeServiceId: z.string().guid().optional().nullable(),
serviceName: z.string().min(1).optional().nullable(),
timeoutSeconds: z.number().int().positive().max(300).optional(),
});
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function readCurrentExecutionWorkspace(context: unknown): Record<string, unknown> | null {
if (!context || typeof context !== "object") return null;
const workspace = (context as { currentExecutionWorkspace?: unknown }).currentExecutionWorkspace;
return workspace && typeof workspace === "object" ? workspace as Record<string, unknown> : null;
}
function readWorkspaceRuntimeServices(workspace: Record<string, unknown> | null): Array<Record<string, unknown>> {
const raw = workspace?.runtimeServices;
return Array.isArray(raw)
? raw.filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === "object")
: [];
}
function selectRuntimeService(
services: Array<Record<string, unknown>>,
input: { runtimeServiceId?: string | null; serviceName?: string | null },
) {
if (input.runtimeServiceId) {
return services.find((service) => service.id === input.runtimeServiceId) ?? null;
}
if (input.serviceName) {
return services.find((service) => service.serviceName === input.serviceName) ?? null;
}
return services.find((service) => service.status === "running" || service.status === "starting")
?? services[0]
?? null;
}
async function getIssueWorkspaceRuntime(client: PaperclipApiClient, issueId: string) {
const context = await client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/heartbeat-context`);
const workspace = readCurrentExecutionWorkspace(context);
return {
context,
workspace,
runtimeServices: readWorkspaceRuntimeServices(workspace),
};
}
export function createToolDefinitions(client: PaperclipApiClient): ToolDefinition[] {
return [
makeTool(
"paperclipMe",
"Get the current authenticated Paperclip actor details",
z.object({}),
async () => client.requestJson("GET", "/agents/me"),
),
makeTool(
"paperclipInboxLite",
"Get the current authenticated agent inbox-lite assignment list",
z.object({}),
async () => client.requestJson("GET", "/agents/me/inbox-lite"),
),
makeTool(
"paperclipListAgents",
"List agents in a company",
z.object({ companyId: companyIdOptional }),
async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/agents`),
),
makeTool(
"paperclipGetAgent",
"Get a single agent by id",
z.object({ agentId: z.string().min(1), companyId: companyIdOptional }),
async ({ agentId, companyId }) => {
const qs = companyId ? `?companyId=${encodeURIComponent(companyId)}` : "";
return client.requestJson("GET", `/agents/${encodeURIComponent(agentId)}${qs}`);
},
),
makeTool(
"paperclipListIssues",
"List issues for a company with optional filters",
listIssuesSchema,
async (input) => {
const companyId = client.resolveCompanyId(input.companyId);
const params = new URLSearchParams();
for (const [key, value] of Object.entries(input)) {
if (key === "companyId" || value === undefined || value === null) continue;
params.set(key, String(value));
}
const qs = params.toString();
return client.requestJson("GET", `/companies/${companyId}/issues${qs ? `?${qs}` : ""}`);
},
),
makeTool(
"paperclipGetIssue",
"Get a single issue by UUID or identifier",
z.object({ issueId: issueIdSchema }),
async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}`),
),
makeTool(
"paperclipGetHeartbeatContext",
"Get compact heartbeat context for an issue",
z.object({ issueId: issueIdSchema, wakeCommentId: z.string().guid().optional() }),
async ({ issueId, wakeCommentId }) => {
const qs = wakeCommentId ? `?wakeCommentId=${encodeURIComponent(wakeCommentId)}` : "";
return client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/heartbeat-context${qs}`);
},
),
makeTool(
"paperclipListComments",
"List issue comments with incremental options",
listCommentsSchema,
async ({ issueId, after, order, limit }) => {
const params = new URLSearchParams();
if (after) params.set("after", after);
if (order) params.set("order", order);
if (limit) params.set("limit", String(limit));
const qs = params.toString();
return client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/comments${qs ? `?${qs}` : ""}`);
},
),
makeTool(
"paperclipGetComment",
"Get a specific issue comment by id",
z.object({ issueId: issueIdSchema, commentId: z.string().guid() }),
async ({ issueId, commentId }) =>
client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/comments/${encodeURIComponent(commentId)}`),
),
makeTool(
"paperclipListIssueApprovals",
"List approvals linked to an issue",
z.object({ issueId: issueIdSchema }),
async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/approvals`),
),
makeTool(
"paperclipListDocuments",
"List issue documents",
z.object({ issueId: issueIdSchema }),
async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/documents`),
),
makeTool(
"paperclipGetDocument",
"Get one issue document by key",
z.object({ issueId: issueIdSchema, key: documentKeySchema }),
async ({ issueId, key }) =>
client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`),
),
makeTool(
"paperclipListDocumentRevisions",
"List revisions for an issue document",
z.object({ issueId: issueIdSchema, key: documentKeySchema }),
async ({ issueId, key }) =>
client.requestJson(
"GET",
`/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}/revisions`,
),
),
makeTool(
"paperclipListProjects",
"List projects in a company",
z.object({ companyId: companyIdOptional }),
async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/projects`),
),
makeTool(
"paperclipGetProject",
"Get a project by id or company-scoped short reference",
z.object({ projectId: projectIdSchema, companyId: companyIdOptional }),
async ({ projectId, companyId }) => {
const qs = companyId ? `?companyId=${encodeURIComponent(companyId)}` : "";
return client.requestJson("GET", `/projects/${encodeURIComponent(projectId)}${qs}`);
},
),
makeTool(
"paperclipGetIssueWorkspaceRuntime",
"Get the current execution workspace and runtime services for an issue, including service URLs",
z.object({ issueId: issueIdSchema }),
async ({ issueId }) => getIssueWorkspaceRuntime(client, issueId),
),
makeTool(
"paperclipControlIssueWorkspaceServices",
"Start, stop, or restart the current issue execution workspace runtime services",
issueWorkspaceRuntimeControlSchema,
async ({ issueId, action, ...target }) => {
const runtime = await getIssueWorkspaceRuntime(client, issueId);
const workspaceId = typeof runtime.workspace?.id === "string" ? runtime.workspace.id : null;
if (!workspaceId) {
throw new Error("Issue has no current execution workspace");
}
return client.requestJson(
"POST",
`/execution-workspaces/${encodeURIComponent(workspaceId)}/runtime-services/${action}`,
{ body: target },
);
},
),
makeTool(
"paperclipWaitForIssueWorkspaceService",
"Wait until an issue execution workspace runtime service is running and has a URL when one is exposed",
waitForIssueWorkspaceServiceSchema,
async ({ issueId, runtimeServiceId, serviceName, timeoutSeconds }) => {
const deadline = Date.now() + (timeoutSeconds ?? 60) * 1000;
let latest: Awaited<ReturnType<typeof getIssueWorkspaceRuntime>> | null = null;
while (Date.now() <= deadline) {
latest = await getIssueWorkspaceRuntime(client, issueId);
const service = selectRuntimeService(latest.runtimeServices, { runtimeServiceId, serviceName });
if (service?.status === "running" && service.healthStatus !== "unhealthy") {
return {
workspace: latest.workspace,
service,
};
}
await sleep(1000);
}
return {
timedOut: true,
latestWorkspace: latest?.workspace ?? null,
latestRuntimeServices: latest?.runtimeServices ?? [],
};
},
),
makeTool(
"paperclipListGoals",
"List goals in a company",
z.object({ companyId: companyIdOptional }),
async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/goals`),
),
makeTool(
"paperclipGetGoal",
"Get a goal by id",
z.object({ goalId: goalIdSchema }),
async ({ goalId }) => client.requestJson("GET", `/goals/${encodeURIComponent(goalId)}`),
),
makeTool(
"paperclipListApprovals",
"List approvals in a company",
z.object({ companyId: companyIdOptional, status: z.string().optional() }),
async ({ companyId, status }) => {
const qs = status ? `?status=${encodeURIComponent(status)}` : "";
return client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/approvals${qs}`);
},
),
makeTool(
"paperclipCreateApproval",
"Create a board approval request, optionally linked to one or more issues",
createApprovalToolSchema,
async ({ companyId, ...body }) =>
client.requestJson("POST", `/companies/${client.resolveCompanyId(companyId)}/approvals`, {
body,
}),
),
makeTool(
"paperclipGetApproval",
"Get an approval by id",
z.object({ approvalId: approvalIdSchema }),
async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}`),
),
makeTool(
"paperclipGetApprovalIssues",
"List issues linked to an approval",
z.object({ approvalId: approvalIdSchema }),
async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}/issues`),
),
makeTool(
"paperclipListApprovalComments",
"List comments for an approval",
z.object({ approvalId: approvalIdSchema }),
async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}/comments`),
),
makeTool(
"paperclipCreateIssue",
"Create a new issue",
createIssueToolSchema,
async ({ companyId, ...body }) =>
client.requestJson("POST", `/companies/${client.resolveCompanyId(companyId)}/issues`, { body }),
),
makeTool(
"paperclipUpdateIssue",
"Patch an issue, optionally including a comment; include resume=true when intentionally requesting follow-up on resumable closed work",
updateIssueToolSchema,
async ({ issueId, ...body }) =>
client.requestJson("PATCH", `/issues/${encodeURIComponent(issueId)}`, { body }),
),
makeTool(
"paperclipCheckoutIssue",
"Checkout an issue for an agent",
checkoutIssueToolSchema,
async ({ issueId, agentId, expectedStatuses }) =>
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/checkout`, {
body: {
agentId: client.resolveAgentId(agentId),
expectedStatuses: expectedStatuses ?? ["todo", "backlog", "blocked"],
},
}),
),
makeTool(
"paperclipReleaseIssue",
"Release an issue checkout",
z.object({ issueId: issueIdSchema }),
async ({ issueId }) => client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/release`, { body: {} }),
),
makeTool(
"paperclipAddComment",
"Add a comment to an issue; include resume=true when intentionally requesting follow-up on resumable closed work",
addCommentToolSchema,
async ({ issueId, ...body }) =>
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/comments`, { body }),
),
makeTool(
"paperclipSuggestTasks",
"Create a suggest_tasks interaction on an issue",
createSuggestTasksToolSchema,
async ({ issueId, ...body }) =>
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/interactions`, {
body: {
kind: "suggest_tasks",
...body,
},
}),
),
makeTool(
"paperclipAskUserQuestions",
"Create an ask_user_questions interaction on an issue",
createAskUserQuestionsToolSchema,
async ({ issueId, ...body }) =>
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/interactions`, {
body: {
kind: "ask_user_questions",
...body,
},
}),
),
makeTool(
"paperclipRequestConfirmation",
"Create a request_confirmation interaction on an issue",
createRequestConfirmationToolSchema,
async ({ issueId, ...body }) =>
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/interactions`, {
body: {
kind: "request_confirmation",
...body,
},
}),
),
makeTool(
"paperclipRequestCheckboxConfirmation",
"Create a request_checkbox_confirmation interaction on an issue",
createRequestCheckboxConfirmationToolSchema,
async ({ issueId, ...body }) =>
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/interactions`, {
body: {
kind: "request_checkbox_confirmation",
...body,
},
}),
),
makeTool(
"paperclipUpsertIssueDocument",
"Create or update an issue document",
upsertDocumentToolSchema,
async ({ issueId, key, ...body }) =>
client.requestJson(
"PUT",
`/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`,
{ body },
),
),
makeTool(
"paperclipRestoreIssueDocumentRevision",
"Restore a prior revision of an issue document",
z.object({
issueId: issueIdSchema,
key: documentKeySchema,
revisionId: z.string().guid(),
}),
async ({ issueId, key, revisionId }) =>
client.requestJson(
"POST",
`/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}/restore`,
{ body: {} },
),
),
makeTool(
"paperclipLinkIssueApproval",
"Link an approval to an issue",
z.object({ issueId: issueIdSchema }).merge(linkIssueApprovalSchema),
async ({ issueId, approvalId }) =>
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/approvals`, {
body: { approvalId },
}),
),
makeTool(
"paperclipUnlinkIssueApproval",
"Unlink an approval from an issue",
z.object({ issueId: issueIdSchema, approvalId: approvalIdSchema }),
async ({ issueId, approvalId }) =>
client.requestJson(
"DELETE",
`/issues/${encodeURIComponent(issueId)}/approvals/${encodeURIComponent(approvalId)}`,
),
),
makeTool(
"paperclipApprovalDecision",
"Approve, reject, request revision, or resubmit an approval",
approvalDecisionSchema,
async ({ approvalId, action, decisionNote, payloadJson }) => {
const path =
action === "approve"
? `/approvals/${encodeURIComponent(approvalId)}/approve`
: action === "reject"
? `/approvals/${encodeURIComponent(approvalId)}/reject`
: action === "requestRevision"
? `/approvals/${encodeURIComponent(approvalId)}/request-revision`
: `/approvals/${encodeURIComponent(approvalId)}/resubmit`;
const body =
action === "resubmit"
? { payload: parseOptionalJson(payloadJson) ?? {} }
: { decisionNote };
return client.requestJson("POST", path, { body });
},
),
makeTool(
"paperclipAddApprovalComment",
"Add a comment to an approval",
z.object({ approvalId: approvalIdSchema, body: z.string().min(1) }),
async ({ approvalId, body }) =>
client.requestJson("POST", `/approvals/${encodeURIComponent(approvalId)}/comments`, {
body: { body },
}),
),
makeTool(
"paperclipApiRequest",
"Make a JSON request to an existing Paperclip /api endpoint for unsupported operations",
apiRequestSchema,
async ({ method, path, jsonBody }) => {
if (!path.startsWith("/") || path.includes("..")) {
throw new Error("path must start with / and be relative to /api, and must not contain '..'");
}
return client.requestJson(method, path, {
body: parseOptionalJson(jsonBody),
});
},
),
];
}