mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
feat(mcp): add independent cover-letter tools (#3508)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
co-authored by
Amruth Pillai
parent
96c7142fbc
commit
e6a6bf0e6a
@@ -230,6 +230,16 @@ Tool names use canonical unprefixed `snake_case` names.
|
||||
| `lock_resume` | Lock a resume to prevent edits, patches, and deletion |
|
||||
| `unlock_resume` | Unlock a previously locked resume to re-enable editing |
|
||||
| `get_resume_statistics` | Get view and download statistics for a resume |
|
||||
| `list_cover_letters` | List independent cover letters in the cover-letter library; embedded resume cover letters are not listed here |
|
||||
| `read_cover_letter` | Read one independent library cover letter by ID |
|
||||
| `create_cover_letter` | Create an independent library cover letter, optionally linked to a resume or application |
|
||||
| `update_cover_letter` | Update an independent cover letter with revision-checked concurrency |
|
||||
| `refresh_cover_letter_style` | Refresh independent-letter styling from a resume without changing its content or template |
|
||||
| `duplicate_cover_letter` | Create an independent copy of a library cover letter |
|
||||
| `delete_cover_letter` | Permanently delete an independent library cover letter; requires its current revision |
|
||||
| `copy_embedded_cover_letter` | Copy a cover-letter item from a resume into the independent cover-letter library; leaves embedded item unchanged |
|
||||
| `export_cover_letter` | Export one independent library cover letter as versioned cover-letter JSON |
|
||||
| `import_cover_letter` | Import versioned cover-letter JSON as a new independent library letter |
|
||||
| `list_applications` | List tracked job applications. Supports stage, tag, and archived filters |
|
||||
| `read_application` | Read one full application record with contacts, follow-up details, documents, and timeline |
|
||||
| `list_application_tags` | List every distinct tag used across applications |
|
||||
@@ -252,6 +262,15 @@ Tool names use canonical unprefixed `snake_case` names.
|
||||
|
||||
Older clients may refer to prefixed or dot-separated names. Those names are no longer registered; update automations and saved prompts to the canonical names above.
|
||||
|
||||
### Independent and embedded cover letters
|
||||
|
||||
Reactive Resume has two cover-letter scopes:
|
||||
|
||||
- **Independent library letters** live in the **Cover Letters** dashboard and have their own IDs, revisions, templates, exports, and lifecycle. Use `list_cover_letters`, `read_cover_letter`, `create_cover_letter`, `update_cover_letter`, `refresh_cover_letter_style`, `duplicate_cover_letter`, `delete_cover_letter`, `export_cover_letter`, and `import_cover_letter` for this scope.
|
||||
- **Embedded letters** live as cover-letter items inside a resume's custom sections. They are part of that resume's `ResumeData`; use `read_resume` and `apply_resume_patch` to inspect or edit them. Use `copy_embedded_cover_letter` when you want to create a separate independent library copy. Copying does not remove or change the embedded item.
|
||||
|
||||
Updating or deleting an independent letter requires its latest `revision` as `expectedRevision`. This prevents concurrent MCP clients from overwriting newer edits.
|
||||
|
||||
## Available resources
|
||||
|
||||
Resources follow MCP conventions: **static** items appear in `resources/list`; **parameterized** access is declared in `resources/templates/list` and read via `resources/read` once you know the ID.
|
||||
|
||||
@@ -53,6 +53,24 @@ describe("buildMcpServerCard", () => {
|
||||
expect(names).toContain("tailor_resume_for_application");
|
||||
});
|
||||
|
||||
it("advertises independent cover-letter library tools", () => {
|
||||
const names = card.tools.map((tool) => tool.name);
|
||||
expect(names).toEqual(
|
||||
expect.arrayContaining([
|
||||
"list_cover_letters",
|
||||
"read_cover_letter",
|
||||
"create_cover_letter",
|
||||
"update_cover_letter",
|
||||
"refresh_cover_letter_style",
|
||||
"duplicate_cover_letter",
|
||||
"delete_cover_letter",
|
||||
"copy_embedded_cover_letter",
|
||||
"export_cover_letter",
|
||||
"import_cover_letter",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("declares a JSON Schema input for every tool", () => {
|
||||
for (const tool of card.tools) {
|
||||
expect(tool.inputSchema, tool.name).toBeDefined();
|
||||
@@ -92,6 +110,14 @@ describe("buildMcpServerCard", () => {
|
||||
expect(update.safeParse({ id: "app-1", archived: true }).success).toBe(true);
|
||||
});
|
||||
|
||||
it.each([{ content: "Updated" }, { recipient: "Dear Hiring Manager" }, { template: "onyx" }])(
|
||||
"accepts a partial cover-letter update without a name: %j",
|
||||
(fields) => {
|
||||
const input = { id: "letter-1", expectedRevision: 1, ...fields };
|
||||
expect(TOOL_META[MCP_TOOL_NAME.updateCoverLetter].inputSchema.parse(input)).toEqual(input);
|
||||
},
|
||||
);
|
||||
|
||||
it("accepts only http/https application source URLs", () => {
|
||||
const create = TOOL_META[MCP_TOOL_NAME.createApplication].inputSchema;
|
||||
|
||||
|
||||
@@ -13,6 +13,16 @@ export const MCP_TOOL_NAME = {
|
||||
lockResume: "lock_resume",
|
||||
unlockResume: "unlock_resume",
|
||||
getResumeStatistics: "get_resume_statistics",
|
||||
listCoverLetters: "list_cover_letters",
|
||||
readCoverLetter: "read_cover_letter",
|
||||
createCoverLetter: "create_cover_letter",
|
||||
updateCoverLetter: "update_cover_letter",
|
||||
refreshCoverLetterStyle: "refresh_cover_letter_style",
|
||||
duplicateCoverLetter: "duplicate_cover_letter",
|
||||
deleteCoverLetter: "delete_cover_letter",
|
||||
copyEmbeddedCoverLetter: "copy_embedded_cover_letter",
|
||||
exportCoverLetter: "export_cover_letter",
|
||||
importCoverLetter: "import_cover_letter",
|
||||
listApplications: "list_applications",
|
||||
readApplication: "read_application",
|
||||
listApplicationTags: "list_application_tags",
|
||||
|
||||
@@ -49,6 +49,9 @@ describe("tool annotations", () => {
|
||||
MCP_TOOL_NAME.readApplication,
|
||||
MCP_TOOL_NAME.listApplicationTags,
|
||||
MCP_TOOL_NAME.getApplicationStats,
|
||||
MCP_TOOL_NAME.listCoverLetters,
|
||||
MCP_TOOL_NAME.readCoverLetter,
|
||||
MCP_TOOL_NAME.exportCoverLetter,
|
||||
];
|
||||
for (const name of readOnlyTools) {
|
||||
const annotations = TOOL_META[name].annotations;
|
||||
@@ -88,6 +91,10 @@ describe("tool annotations", () => {
|
||||
MCP_TOOL_NAME.createApplication,
|
||||
MCP_TOOL_NAME.importApplications,
|
||||
MCP_TOOL_NAME.draftApplicationMessage,
|
||||
MCP_TOOL_NAME.createCoverLetter,
|
||||
MCP_TOOL_NAME.duplicateCoverLetter,
|
||||
MCP_TOOL_NAME.copyEmbeddedCoverLetter,
|
||||
MCP_TOOL_NAME.importCoverLetter,
|
||||
]) {
|
||||
const annotations = TOOL_META[name].annotations;
|
||||
expect(annotations.readOnlyHint, name).toBe(false);
|
||||
@@ -116,6 +123,9 @@ describe("tool annotations", () => {
|
||||
MCP_TOOL_NAME.removeApplicationDocument,
|
||||
MCP_TOOL_NAME.scoreApplicationMatch,
|
||||
MCP_TOOL_NAME.tailorResumeForApplication,
|
||||
MCP_TOOL_NAME.updateCoverLetter,
|
||||
MCP_TOOL_NAME.refreshCoverLetterStyle,
|
||||
MCP_TOOL_NAME.deleteCoverLetter,
|
||||
]) {
|
||||
expect(TOOL_META[name].annotations.readOnlyHint, name).toBe(false);
|
||||
expect(TOOL_META[name].annotations.destructiveHint, name).toBe(true);
|
||||
@@ -131,6 +141,7 @@ describe("tool annotations", () => {
|
||||
MCP_TOOL_NAME.removeApplicationDocument,
|
||||
MCP_TOOL_NAME.deleteApplication,
|
||||
MCP_TOOL_NAME.bulkDeleteApplications,
|
||||
MCP_TOOL_NAME.deleteCoverLetter,
|
||||
MCP_TOOL_NAME.autofillApplicationFromJob,
|
||||
MCP_TOOL_NAME.scoreApplicationMatch,
|
||||
MCP_TOOL_NAME.tailorResumeForApplication,
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
||||
import z from "zod";
|
||||
import { resumePatchOperationsSchema } from "@reactive-resume/ai/tools/resume-tool-contracts";
|
||||
import { applicationStatusSchema, contactSchema } from "@reactive-resume/schema/applications/data";
|
||||
import { coverLetterDocumentSchema } from "@reactive-resume/schema/cover-letter/data";
|
||||
import { templateSchema } from "@reactive-resume/schema/templates";
|
||||
import { MCP_TOOL_NAME as T } from "./mcp-tool-names";
|
||||
|
||||
const MAX_APPLICATION_DOCUMENT_BYTES = 10 * 1024 * 1024;
|
||||
@@ -48,6 +50,20 @@ const applicationIdSchema = z
|
||||
.describe(`Application ID. Use \`${T.listApplications}\` to find valid IDs.`);
|
||||
const applicationTimelineEntryIdSchema = z.string().min(1).describe("Timeline entry ID from an application response.");
|
||||
const applicationDocumentKindSchema = z.enum(["resume", "cover-letter"]);
|
||||
const coverLetterIdSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe(`Cover letter ID. Use \`${T.listCoverLetters}\` to find valid IDs.`);
|
||||
const expectedRevisionSchema = z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.describe("Revision returned by the latest cover-letter response.");
|
||||
const coverLetterEditableFieldsSchema = {
|
||||
name: z.string().min(1).max(100).describe("Cover-letter name."),
|
||||
recipient: z.string().max(20_000).optional().describe("Recipient and salutation HTML."),
|
||||
content: z.string().max(100_000).optional().describe("Cover-letter body HTML."),
|
||||
};
|
||||
const timelineDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must use YYYY-MM-DD format.");
|
||||
const httpUrlSchema = z
|
||||
.string()
|
||||
@@ -319,6 +335,129 @@ export const TOOL_META = {
|
||||
inputSchema: z.object({ id: resumeIdSchema }),
|
||||
annotations: READ_IDEMPOTENT,
|
||||
},
|
||||
[T.listCoverLetters]: {
|
||||
title: "List Cover Letters",
|
||||
description: [
|
||||
"List independent cover letters in the account's cover-letter library.",
|
||||
"These are separate from cover-letter sections embedded in resumes.",
|
||||
"Use this before other independent cover-letter tools to discover IDs.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({
|
||||
search: z.string().max(100).optional().describe("Filter by cover-letter name."),
|
||||
resumeId: z.string().min(1).optional().describe("Filter by source resume ID."),
|
||||
applicationId: z.string().min(1).optional().describe("Filter by source application ID."),
|
||||
limit: z.number().int().min(1).max(100).optional().default(20).describe("Maximum results. Default: 20."),
|
||||
offset: z.number().int().min(0).optional().default(0).describe("Number of results to skip. Default: 0."),
|
||||
}),
|
||||
annotations: READ_IDEMPOTENT,
|
||||
},
|
||||
[T.readCoverLetter]: {
|
||||
title: "Read Cover Letter",
|
||||
description: [
|
||||
"Read one independent cover letter from the cover-letter library.",
|
||||
"This does not read a cover-letter section embedded in a resume.",
|
||||
`Use \`${T.listCoverLetters}\` first to find valid IDs.`,
|
||||
].join("\n"),
|
||||
inputSchema: z.object({ id: coverLetterIdSchema }),
|
||||
annotations: READ_IDEMPOTENT,
|
||||
},
|
||||
[T.createCoverLetter]: {
|
||||
title: "Create Cover Letter",
|
||||
description: [
|
||||
"Create an independent cover letter in the cover-letter library.",
|
||||
"Optionally associate it with a resume or application; this does not add an embedded section to a resume.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({
|
||||
...coverLetterEditableFieldsSchema,
|
||||
recipient: z.string().max(20_000).optional().default(""),
|
||||
content: z.string().max(100_000).optional().default(""),
|
||||
resumeId: z.string().min(1).optional().describe("Optional source resume ID."),
|
||||
applicationId: z.string().min(1).optional().describe("Optional source application ID."),
|
||||
template: templateSchema.optional().describe("Optional template for this cover letter."),
|
||||
}),
|
||||
annotations: WRITE_NON_IDEMPOTENT,
|
||||
},
|
||||
[T.updateCoverLetter]: {
|
||||
title: "Update Cover Letter",
|
||||
description: [
|
||||
"Update an independent cover letter's name, recipient, content, or template.",
|
||||
"Pass the latest `revision` as `expectedRevision`; stale writes are rejected instead of overwriting newer edits.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({
|
||||
id: coverLetterIdSchema,
|
||||
expectedRevision: expectedRevisionSchema,
|
||||
...coverLetterEditableFieldsSchema,
|
||||
name: coverLetterEditableFieldsSchema.name.optional(),
|
||||
template: templateSchema.optional().describe("Replacement template. Omit to keep the current template."),
|
||||
}),
|
||||
annotations: { ...WRITE_NON_IDEMPOTENT, destructiveHint: true },
|
||||
},
|
||||
[T.refreshCoverLetterStyle]: {
|
||||
title: "Refresh Cover Letter Style",
|
||||
description: [
|
||||
"Refresh an independent cover letter's sender styling from a resume while preserving its content and template.",
|
||||
"Pass the latest `revision` as `expectedRevision` to prevent stale concurrent writes.",
|
||||
"This updates the independent letter; it does not modify the embedded cover letter in the resume.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({
|
||||
id: coverLetterIdSchema,
|
||||
expectedRevision: expectedRevisionSchema,
|
||||
resumeId: z.string().min(1).describe("Resume ID to copy sender styling from."),
|
||||
}),
|
||||
annotations: { ...WRITE_NON_IDEMPOTENT, destructiveHint: true },
|
||||
},
|
||||
[T.duplicateCoverLetter]: {
|
||||
title: "Duplicate Cover Letter",
|
||||
description: [
|
||||
"Create an independent copy of a cover letter in the library.",
|
||||
"The copy is separate from the original and from any embedded resume cover letter.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({ id: coverLetterIdSchema, name: z.string().min(1).max(100).optional() }),
|
||||
annotations: WRITE_NON_IDEMPOTENT,
|
||||
},
|
||||
[T.deleteCoverLetter]: {
|
||||
title: "Delete Cover Letter",
|
||||
description: [
|
||||
"Permanently delete an independent cover letter from the library.",
|
||||
"Pass the latest `revision` as `expectedRevision`; this does not delete embedded cover-letter sections.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({ id: coverLetterIdSchema, expectedRevision: expectedRevisionSchema }),
|
||||
annotations: { ...WRITE_DESTRUCTIVE, openWorldHint: true },
|
||||
},
|
||||
[T.copyEmbeddedCoverLetter]: {
|
||||
title: "Copy Embedded Cover Letter",
|
||||
description: [
|
||||
"Copy a cover-letter item embedded in a resume into the independent cover-letter library.",
|
||||
"The embedded item remains in the resume; the returned letter is a new independent library record.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({
|
||||
resumeId: z.string().min(1).describe("Resume containing the embedded cover letter."),
|
||||
sectionId: z.string().min(1).describe("Embedded cover-letter section ID."),
|
||||
itemId: z.string().min(1).describe("Embedded cover-letter item ID."),
|
||||
name: z.string().min(1).max(100).optional().describe("Optional name for the independent copy."),
|
||||
}),
|
||||
annotations: WRITE_NON_IDEMPOTENT,
|
||||
},
|
||||
[T.exportCoverLetter]: {
|
||||
title: "Export Cover Letter",
|
||||
description: [
|
||||
"Export an independent library cover letter as versioned Reactive Resume cover-letter JSON.",
|
||||
"This is not a full resume export and does not export an embedded cover letter directly.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({ id: coverLetterIdSchema }),
|
||||
annotations: READ_IDEMPOTENT,
|
||||
},
|
||||
[T.importCoverLetter]: {
|
||||
title: "Import Cover Letter",
|
||||
description: [
|
||||
"Import a versioned Reactive Resume cover-letter JSON document as a new independent library letter.",
|
||||
"Use `export_cover_letter` to obtain the accepted document format.",
|
||||
].join("\n"),
|
||||
inputSchema: z.object({
|
||||
document: coverLetterDocumentSchema.describe("Versioned independent cover-letter JSON document."),
|
||||
}),
|
||||
annotations: WRITE_NON_IDEMPOTENT,
|
||||
},
|
||||
[T.listApplications]: {
|
||||
title: "List Applications",
|
||||
description:
|
||||
|
||||
@@ -65,6 +65,18 @@ const clientMock = {
|
||||
setLocked: vi.fn(),
|
||||
statistics: { getById: vi.fn() },
|
||||
},
|
||||
coverLetters: {
|
||||
list: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
refreshStyle: vi.fn(),
|
||||
duplicate: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
copyEmbedded: vi.fn(),
|
||||
export: vi.fn(),
|
||||
import: vi.fn(),
|
||||
},
|
||||
applications: {
|
||||
list: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
@@ -201,6 +213,47 @@ describe("registerTools", () => {
|
||||
expect(names).toContain("draft_application_message");
|
||||
});
|
||||
|
||||
it("registers and routes independent cover-letter tools", async () => {
|
||||
clientMock.coverLetters.list.mockResolvedValueOnce({ items: [{ id: "letter-1" }], total: 1 });
|
||||
clientMock.coverLetters.update.mockResolvedValueOnce({ id: "letter-1", revision: 2 });
|
||||
clientMock.coverLetters.delete.mockResolvedValueOnce(undefined);
|
||||
const { server, registered } = makeFakeServer();
|
||||
registerTools(server as never, clientMock as never, new Headers());
|
||||
|
||||
expect(registered.map((item) => item.name)).toEqual(
|
||||
expect.arrayContaining([
|
||||
MCP_TOOL_NAME.listCoverLetters,
|
||||
MCP_TOOL_NAME.readCoverLetter,
|
||||
MCP_TOOL_NAME.createCoverLetter,
|
||||
MCP_TOOL_NAME.updateCoverLetter,
|
||||
MCP_TOOL_NAME.refreshCoverLetterStyle,
|
||||
MCP_TOOL_NAME.duplicateCoverLetter,
|
||||
MCP_TOOL_NAME.deleteCoverLetter,
|
||||
MCP_TOOL_NAME.copyEmbeddedCoverLetter,
|
||||
MCP_TOOL_NAME.exportCoverLetter,
|
||||
MCP_TOOL_NAME.importCoverLetter,
|
||||
]),
|
||||
);
|
||||
|
||||
const list = registered.find((item) => item.name === MCP_TOOL_NAME.listCoverLetters)!;
|
||||
const listResult = await list.handler({ search: "Acme", limit: 10, offset: 0 });
|
||||
expect(clientMock.coverLetters.list).toHaveBeenCalledWith({ search: "Acme", limit: 10, offset: 0 });
|
||||
expect(JSON.parse(listResult.content[0]!.text)).toEqual({ items: [{ id: "letter-1" }], total: 1 });
|
||||
|
||||
const update = registered.find((item) => item.name === MCP_TOOL_NAME.updateCoverLetter)!;
|
||||
await update.handler({ id: "letter-1", expectedRevision: 1, content: "Updated" });
|
||||
expect(clientMock.coverLetters.update).toHaveBeenCalledWith({
|
||||
id: "letter-1",
|
||||
expectedRevision: 1,
|
||||
content: "Updated",
|
||||
});
|
||||
|
||||
const remove = registered.find((item) => item.name === MCP_TOOL_NAME.deleteCoverLetter)!;
|
||||
const deleteResult = await remove.handler({ id: "letter-1", expectedRevision: 2 });
|
||||
expect(clientMock.coverLetters.delete).toHaveBeenCalledWith({ id: "letter-1", expectedRevision: 2 });
|
||||
expect(deleteResult.content[0]!.text).toContain("Deleted cover letter (letter-1).");
|
||||
});
|
||||
|
||||
it("lists applications as JSON", async () => {
|
||||
clientMock.applications.list.mockResolvedValueOnce([{ id: "app-1", company: "Acme", role: "Engineer" }]);
|
||||
const { server, registered } = makeFakeServer();
|
||||
@@ -372,7 +425,7 @@ describe("registerTools", () => {
|
||||
[
|
||||
"NOT_FOUND",
|
||||
undefined,
|
||||
`\`${MCP_TOOL_NAME.listResumes}\` and \`${MCP_TOOL_NAME.listApplications}\` return valid ones.`,
|
||||
`\`${MCP_TOOL_NAME.listResumes}\`, \`${MCP_TOOL_NAME.listCoverLetters}\`, and \`${MCP_TOOL_NAME.listApplications}\` return valid ones.`,
|
||||
],
|
||||
["RESUME_SLUG_ALREADY_EXISTS", 400, "The slug is already in use."],
|
||||
["FORBIDDEN", undefined, "Permission denied."],
|
||||
|
||||
@@ -42,7 +42,7 @@ function errorHint(error: unknown): string {
|
||||
// Every tool shares this handler, so the wording stays entity-agnostic: `NOT_FOUND` is
|
||||
// thrown by the application procedures too, and resume-flavoured advice misdirects there.
|
||||
if (code === "NOT_FOUND" || status === 404)
|
||||
return `\n\nHint: Not found. Check the ID — \`${listResumes}\` and \`${listApplications}\` return valid ones.`;
|
||||
return `\n\nHint: Not found. Check the ID — \`${listResumes}\`, \`${MCP_TOOL_NAME.listCoverLetters}\`, and \`${listApplications}\` return valid ones.`;
|
||||
if (code === "FORBIDDEN" || status === 403)
|
||||
return "\n\nHint: Permission denied. This account cannot access that record.";
|
||||
if (status === 400) return "\n\nHint: Invalid request. Check the input parameters against the tool's schema.";
|
||||
@@ -371,6 +371,86 @@ export function registerTools(server: McpServer, client: RouterClient<typeof rou
|
||||
}),
|
||||
);
|
||||
|
||||
// ── Independent Cover Letters ───────────────────────────────────
|
||||
server.registerTool(
|
||||
T.listCoverLetters,
|
||||
TOOL_META[T.listCoverLetters],
|
||||
withErrorHandling("listing cover letters", async (params) => json(await client.coverLetters.list(params as never))),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.readCoverLetter,
|
||||
TOOL_META[T.readCoverLetter],
|
||||
withErrorHandling("reading cover letter", async ({ id }: { id: string }) =>
|
||||
json(await client.coverLetters.getById({ id })),
|
||||
),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.createCoverLetter,
|
||||
TOOL_META[T.createCoverLetter],
|
||||
withErrorHandling("creating cover letter", async (params) =>
|
||||
json(await client.coverLetters.create(params as never)),
|
||||
),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.updateCoverLetter,
|
||||
TOOL_META[T.updateCoverLetter],
|
||||
withErrorHandling("updating cover letter", async (params) =>
|
||||
json(await client.coverLetters.update(params as never)),
|
||||
),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.refreshCoverLetterStyle,
|
||||
TOOL_META[T.refreshCoverLetterStyle],
|
||||
withErrorHandling("refreshing cover letter style", async (params) =>
|
||||
json(await client.coverLetters.refreshStyle(params as never)),
|
||||
),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.duplicateCoverLetter,
|
||||
TOOL_META[T.duplicateCoverLetter],
|
||||
withErrorHandling("duplicating cover letter", async (params) =>
|
||||
json(await client.coverLetters.duplicate(params as never)),
|
||||
),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.deleteCoverLetter,
|
||||
TOOL_META[T.deleteCoverLetter],
|
||||
withErrorHandling("deleting cover letter", async (params) => {
|
||||
await client.coverLetters.delete(params as never);
|
||||
return text(`Deleted cover letter (${(params as { id: string }).id}).`);
|
||||
}),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.copyEmbeddedCoverLetter,
|
||||
TOOL_META[T.copyEmbeddedCoverLetter],
|
||||
withErrorHandling("copying embedded cover letter", async (params) =>
|
||||
json(await client.coverLetters.copyEmbedded(params as never)),
|
||||
),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.exportCoverLetter,
|
||||
TOOL_META[T.exportCoverLetter],
|
||||
withErrorHandling("exporting cover letter", async ({ id }: { id: string }) =>
|
||||
json(await client.coverLetters.export({ id })),
|
||||
),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
T.importCoverLetter,
|
||||
TOOL_META[T.importCoverLetter],
|
||||
withErrorHandling("importing cover letter", async ({ document }: { document: unknown }) =>
|
||||
json(await client.coverLetters.import({ document } as never)),
|
||||
),
|
||||
);
|
||||
|
||||
// ── Applications ──────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
T.listApplications,
|
||||
|
||||
Reference in New Issue
Block a user