mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 10:50:38 +09:00
Add standalone Paperclip MCP server package
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
08fea10ce1
commit
8cdba3ce18
12 changed files with 902 additions and 0 deletions
413
packages/mcp-server/src/tools.ts
Normal file
413
packages/mcp-server/src/tools.ts
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
addIssueCommentSchema,
|
||||
checkoutIssueSchema,
|
||||
createIssueSchema,
|
||||
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.AnyZodObject;
|
||||
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().uuid().optional().nullable();
|
||||
const agentIdOptional = z.string().uuid().optional().nullable();
|
||||
const issueIdSchema = z.string().min(1);
|
||||
const projectIdSchema = z.string().min(1);
|
||||
const goalIdSchema = z.string().uuid();
|
||||
const approvalIdSchema = z.string().uuid();
|
||||
const documentKeySchema = z.string().trim().min(1).max(64);
|
||||
|
||||
const listIssuesSchema = z.object({
|
||||
companyId: companyIdOptional,
|
||||
status: z.string().optional(),
|
||||
projectId: z.string().uuid().optional(),
|
||||
assigneeAgentId: z.string().uuid().optional(),
|
||||
participantAgentId: z.string().uuid().optional(),
|
||||
assigneeUserId: z.string().optional(),
|
||||
touchedByUserId: z.string().optional(),
|
||||
inboxArchivedByUserId: z.string().optional(),
|
||||
unreadForUserId: z.string().optional(),
|
||||
labelId: z.string().uuid().optional(),
|
||||
executionWorkspaceId: z.string().uuid().optional(),
|
||||
originKind: z.string().optional(),
|
||||
originId: z.string().optional(),
|
||||
includeRoutineExecutions: z.boolean().optional(),
|
||||
q: z.string().optional(),
|
||||
});
|
||||
|
||||
const listCommentsSchema = z.object({
|
||||
issueId: issueIdSchema,
|
||||
after: z.string().uuid().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().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
const createIssueToolSchema = z.object({
|
||||
companyId: companyIdOptional,
|
||||
}).merge(createIssueSchema);
|
||||
|
||||
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 approvalDecisionSchema = z.object({
|
||||
approvalId: approvalIdSchema,
|
||||
action: z.enum(["approve", "reject", "requestRevision", "resubmit"]),
|
||||
decisionNote: z.string().optional(),
|
||||
payloadJson: z.string().optional(),
|
||||
});
|
||||
|
||||
const apiRequestSchema = z.object({
|
||||
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
||||
path: z.string().min(1),
|
||||
jsonBody: z.string().optional(),
|
||||
});
|
||||
|
||||
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().uuid().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().uuid() }),
|
||||
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(
|
||||
"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(
|
||||
"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",
|
||||
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",
|
||||
addCommentToolSchema,
|
||||
async ({ issueId, ...body }) =>
|
||||
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/comments`, { 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().uuid(),
|
||||
}),
|
||||
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("/")) {
|
||||
throw new Error("path must start with / and be relative to /api");
|
||||
}
|
||||
return client.requestJson(method, path, {
|
||||
body: parseOptionalJson(jsonBody),
|
||||
});
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue