refactor: ponytail audit

This commit is contained in:
Amruth Pillai
2026-07-27 20:26:16 +02:00
parent bb1fb3a7d6
commit 9110e86997
157 changed files with 1082 additions and 2177 deletions
-1
View File
@@ -1,4 +1,3 @@
export { buildMcpServerCard } from "./mcp-server-card";
export { MCP_TOOL_NAME } from "./mcp-tool-names";
export { registerPrompts } from "./prompts";
export { registerResources } from "./resources";
+3 -3
View File
@@ -98,9 +98,9 @@ describe("buildMcpServerCard", () => {
true,
);
expect(autofill.safeParse({ sourceUrl: "http://example.com/job" }).success).toBe(true);
expect(create.safeParse({ company: "Acme", role: "Engineer", sourceUrl: "ftp://example.com/job" }).success).toBe(
false,
);
const invalidUrl = create.safeParse({ company: "Acme", role: "Engineer", sourceUrl: "ftp://example.com/job" });
expect(invalidUrl.success).toBe(false);
if (!invalidUrl.success) expect(invalidUrl.error.issues[0]?.message).toBe("URL must use http or https.");
expect(autofill.safeParse({ sourceUrl: "javascript:alert(1)" }).success).toBe(false);
});
+3 -3
View File
@@ -72,7 +72,7 @@ export function registerPrompts(server: McpServer) {
description: "Guide the user step-by-step through building a resume from scratch, section by section.",
argsSchema: { id: resumeIdArg },
},
async ({ id }) => ({
({ id }) => ({
messages: [
...resumeContext(id),
{
@@ -113,7 +113,7 @@ export function registerPrompts(server: McpServer) {
description: "Review resume content and suggest concrete improvements to wording, impact, and structure.",
argsSchema: { id: resumeIdArg },
},
async ({ id }) => ({
({ id }) => ({
messages: [
...resumeContext(id),
{
@@ -158,7 +158,7 @@ export function registerPrompts(server: McpServer) {
"Get a structured, professional critique with a scorecard and prioritized recommendations. Read-only — no changes are made.",
argsSchema: { id: resumeIdArg },
},
async ({ id }) => ({
({ id }) => ({
messages: [
...resumeContext(id),
{
+1 -1
View File
@@ -58,7 +58,7 @@ export function registerResources(server: McpServer, client: RouterClient<typeof
"custom sections, and metadata (template, layout, typography, colors, CSS).",
].join(" "),
},
async (uri: URL) => ({
(uri: URL) => ({
contents: [
{
uri: uri.href,
+11 -11
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { MCP_TOOL_NAME } from "./mcp-tool-names";
import { TOOL_ANNOTATIONS } from "./tool-annotations";
import { TOOL_META } from "./tool-meta";
describe("MCP_TOOL_NAME", () => {
it("uses canonical unprefixed snake_case tool names", () => {
@@ -29,10 +29,10 @@ describe("MCP_TOOL_NAME", () => {
});
});
describe("TOOL_ANNOTATIONS", () => {
describe("tool annotations", () => {
it("provides annotations for every registered tool", () => {
for (const name of Object.values(MCP_TOOL_NAME)) {
expect(TOOL_ANNOTATIONS[name]).toBeDefined();
expect(TOOL_META[name].annotations).toBeDefined();
}
});
@@ -49,7 +49,7 @@ describe("TOOL_ANNOTATIONS", () => {
MCP_TOOL_NAME.getApplicationStats,
];
for (const name of readOnlyTools) {
const annotations = TOOL_ANNOTATIONS[name];
const annotations = TOOL_META[name].annotations;
expect(annotations.readOnlyHint, name).toBe(true);
expect(annotations.destructiveHint, name).toBe(false);
expect(annotations.idempotentHint, name).toBe(true);
@@ -57,14 +57,14 @@ describe("TOOL_ANNOTATIONS", () => {
});
it("marks PDF download URL generation as read-only but non-idempotent", () => {
const annotations = TOOL_ANNOTATIONS[MCP_TOOL_NAME.downloadResumePdf];
const annotations = TOOL_META[MCP_TOOL_NAME.downloadResumePdf].annotations;
expect(annotations.readOnlyHint).toBe(true);
expect(annotations.idempotentHint).toBe(false);
expect(annotations.destructiveHint).toBe(false);
});
it("marks deleteResume as destructive (but still idempotent)", () => {
const annotations = TOOL_ANNOTATIONS[MCP_TOOL_NAME.deleteResume];
const annotations = TOOL_META[MCP_TOOL_NAME.deleteResume].annotations;
expect(annotations.destructiveHint).toBe(true);
expect(annotations.idempotentHint).toBe(true);
expect(annotations.readOnlyHint).toBe(false);
@@ -72,7 +72,7 @@ describe("TOOL_ANNOTATIONS", () => {
it("marks application delete tools as destructive", () => {
for (const name of [MCP_TOOL_NAME.deleteApplication, MCP_TOOL_NAME.bulkDeleteApplications]) {
const annotations = TOOL_ANNOTATIONS[name];
const annotations = TOOL_META[name].annotations;
expect(annotations.readOnlyHint, name).toBe(false);
expect(annotations.destructiveHint, name).toBe(true);
}
@@ -86,7 +86,7 @@ describe("TOOL_ANNOTATIONS", () => {
MCP_TOOL_NAME.patchResume,
MCP_TOOL_NAME.updateResume,
]) {
const annotations = TOOL_ANNOTATIONS[name];
const annotations = TOOL_META[name].annotations;
expect(annotations.readOnlyHint, name).toBe(false);
expect(annotations.idempotentHint, name).toBe(false);
expect(annotations.destructiveHint, name).toBe(false);
@@ -95,7 +95,7 @@ describe("TOOL_ANNOTATIONS", () => {
it("marks lockResume / unlockResume as idempotent and non-destructive", () => {
for (const name of [MCP_TOOL_NAME.lockResume, MCP_TOOL_NAME.unlockResume]) {
const annotations = TOOL_ANNOTATIONS[name];
const annotations = TOOL_META[name].annotations;
expect(annotations.idempotentHint, name).toBe(true);
expect(annotations.destructiveHint, name).toBe(false);
expect(annotations.readOnlyHint, name).toBe(false);
@@ -103,11 +103,11 @@ describe("TOOL_ANNOTATIONS", () => {
});
it("marks only job-posting autofill as open-world", () => {
expect(TOOL_ANNOTATIONS[MCP_TOOL_NAME.autofillApplicationFromJob].openWorldHint).toBe(true);
expect(TOOL_META[MCP_TOOL_NAME.autofillApplicationFromJob].annotations.openWorldHint).toBe(true);
});
it("declares no tools as open-world by default", () => {
for (const [name, annotations] of Object.entries(TOOL_ANNOTATIONS)) {
for (const [name, { annotations }] of Object.entries(TOOL_META)) {
if (name === MCP_TOOL_NAME.autofillApplicationFromJob) continue;
expect(annotations.openWorldHint).toBe(false);
}
-79
View File
@@ -1,79 +0,0 @@
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
import { MCP_TOOL_NAME } from "./mcp-tool-names";
type McpRegisteredToolName = (typeof MCP_TOOL_NAME)[keyof typeof MCP_TOOL_NAME];
// ponytail: 5 distinct annotation shapes shared across 14 tools
const READ_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
};
const READ_NON_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
};
const READ_OPEN_WORLD_NON_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
};
const WRITE_NON_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
};
const WRITE_DESTRUCTIVE: ToolAnnotations = {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
};
const WRITE_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
};
/** Tool behavior hints for MCP `tools/list` and the static server card. */
export const TOOL_ANNOTATIONS: Record<McpRegisteredToolName, ToolAnnotations> = {
[MCP_TOOL_NAME.listResumes]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.listResumeTags]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.getResume]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.getResumeAnalysis]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.downloadResumePdf]: READ_NON_IDEMPOTENT,
[MCP_TOOL_NAME.createResume]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.importResume]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.duplicateResume]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.patchResume]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.updateResume]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.deleteResume]: WRITE_DESTRUCTIVE,
[MCP_TOOL_NAME.lockResume]: WRITE_IDEMPOTENT,
[MCP_TOOL_NAME.unlockResume]: WRITE_IDEMPOTENT,
[MCP_TOOL_NAME.getResumeStatistics]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.listApplications]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.readApplication]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.listApplicationTags]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.getApplicationStats]: READ_IDEMPOTENT,
[MCP_TOOL_NAME.createApplication]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.updateApplication]: WRITE_IDEMPOTENT,
[MCP_TOOL_NAME.addApplicationNote]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.updateApplicationTimelineEntry]: WRITE_IDEMPOTENT,
[MCP_TOOL_NAME.deleteApplicationTimelineEntry]: WRITE_DESTRUCTIVE,
[MCP_TOOL_NAME.deleteApplication]: WRITE_DESTRUCTIVE,
[MCP_TOOL_NAME.bulkUpdateApplications]: WRITE_IDEMPOTENT,
[MCP_TOOL_NAME.bulkDeleteApplications]: WRITE_DESTRUCTIVE,
[MCP_TOOL_NAME.importApplications]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.attachApplicationDocument]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.removeApplicationDocument]: WRITE_IDEMPOTENT,
[MCP_TOOL_NAME.autofillApplicationFromJob]: READ_OPEN_WORLD_NON_IDEMPOTENT,
[MCP_TOOL_NAME.scoreApplicationMatch]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.tailorResumeForApplication]: WRITE_NON_IDEMPOTENT,
[MCP_TOOL_NAME.draftApplicationMessage]: READ_NON_IDEMPOTENT,
};
+71 -42
View File
@@ -2,13 +2,49 @@
* Canonical tool metadata (title, description, inputSchema, annotations) declared once.
* Consumed by both `registerTools` (raw Zod) and `buildMcpServerCard` (toJsonSchemaCompat).
*/
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 { MCP_TOOL_NAME as T } from "./mcp-tool-names";
import { TOOL_ANNOTATIONS } from "./tool-annotations";
const MAX_APPLICATION_DOCUMENT_BYTES = 10 * 1024 * 1024;
const READ_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
};
const READ_NON_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
};
const READ_OPEN_WORLD_NON_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
};
const WRITE_NON_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
};
const WRITE_DESTRUCTIVE: ToolAnnotations = {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
};
const WRITE_IDEMPOTENT: ToolAnnotations = {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
};
// ponytail: shared schema fragment; exported so server-card can re-use without re-importing
const resumeIdSchema = z.string().min(1).describe(`Resume ID. Use \`${T.listResumes}\` to find valid IDs.`);
@@ -22,14 +58,7 @@ const timelineDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must us
const httpUrlSchema = z
.string()
.trim()
.refine((value) => {
try {
const parsed = new URL(value);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}, "URL must use http or https.");
.pipe(z.url({ protocol: /^https?$/, error: "URL must use http or https." }));
const pdfBase64Schema = z
.string()
.min(1)
@@ -98,7 +127,7 @@ export const TOOL_META = {
.default("lastUpdatedAt")
.describe("Sort order for results. Default: lastUpdatedAt."),
}),
annotations: TOOL_ANNOTATIONS[T.listResumes],
annotations: READ_IDEMPOTENT,
},
[T.listResumeTags]: {
title: "List Resume Tags",
@@ -107,7 +136,7 @@ export const TOOL_META = {
"Useful for choosing tag filters when calling list tools or keeping naming consistent.",
].join("\n"),
inputSchema: z.object({}),
annotations: TOOL_ANNOTATIONS[T.listResumeTags],
annotations: READ_IDEMPOTENT,
},
[T.getResume]: {
title: "Read Resume",
@@ -122,7 +151,7 @@ export const TOOL_META = {
"The `resume://_meta/schema` resource describes the full data structure for JSON Patch paths.",
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: TOOL_ANNOTATIONS[T.getResume],
annotations: READ_IDEMPOTENT,
},
[T.getResumeAnalysis]: {
title: "Get Resume Analysis",
@@ -132,7 +161,7 @@ export const TOOL_META = {
`Returns JSON or a short message if none exists. Use \`${T.listResumes}\` to find resume IDs.`,
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: TOOL_ANNOTATIONS[T.getResumeAnalysis],
annotations: READ_IDEMPOTENT,
},
[T.downloadResumePdf]: {
title: "Download Resume PDF",
@@ -143,7 +172,7 @@ export const TOOL_META = {
`Use \`${T.listResumes}\` first to find valid IDs.`,
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: TOOL_ANNOTATIONS[T.downloadResumePdf],
annotations: READ_NON_IDEMPOTENT,
},
[T.createResume]: {
title: "Create Resume",
@@ -168,7 +197,7 @@ export const TOOL_META = {
.describe("Tags to categorize the resume (e.g. ['tech', 'senior'])"),
withSampleData: z.boolean().optional().default(false).describe("Pre-fill with sample data. Default: false."),
}),
annotations: TOOL_ANNOTATIONS[T.createResume],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.importResume]: {
title: "Import Resume",
@@ -183,7 +212,7 @@ export const TOOL_META = {
.unknown()
.describe("Complete ResumeData JSON (same shape as `read_resume` body or `resume://_meta/schema`)."),
}),
annotations: TOOL_ANNOTATIONS[T.importResume],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.duplicateResume]: {
title: "Duplicate Resume",
@@ -200,7 +229,7 @@ export const TOOL_META = {
slug: z.string().min(1).max(64).describe("URL-friendly slug for the duplicate (must be unique)"),
tags: z.array(z.string()).optional().default([]).describe("Tags for the duplicate"),
}),
annotations: TOOL_ANNOTATIONS[T.duplicateResume],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.patchResume]: {
title: "Apply Resume Patch",
@@ -231,7 +260,7 @@ export const TOOL_META = {
id: resumeIdSchema,
operations: resumePatchOperationsSchema,
}),
annotations: TOOL_ANNOTATIONS[T.patchResume],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.updateResume]: {
title: "Update Resume (metadata)",
@@ -255,7 +284,7 @@ export const TOOL_META = {
"When true, anyone with the link can view the public resume (subject to password if set in the app).",
),
}),
annotations: TOOL_ANNOTATIONS[T.updateResume],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.deleteResume]: {
title: "Delete Resume",
@@ -266,7 +295,7 @@ export const TOOL_META = {
`Consider using \`${T.duplicateResume}\` to create a backup before deleting.`,
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: TOOL_ANNOTATIONS[T.deleteResume],
annotations: WRITE_DESTRUCTIVE,
},
[T.lockResume]: {
title: "Lock Resume",
@@ -278,13 +307,13 @@ export const TOOL_META = {
`Use \`${T.unlockResume}\` to re-enable editing.`,
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: TOOL_ANNOTATIONS[T.lockResume],
annotations: WRITE_IDEMPOTENT,
},
[T.unlockResume]: {
title: "Unlock Resume",
description: "Unlock a previously locked resume, re-enabling edits, patches, and deletion.",
inputSchema: z.object({ id: resumeIdSchema }),
annotations: TOOL_ANNOTATIONS[T.unlockResume],
annotations: WRITE_IDEMPOTENT,
},
[T.getResumeStatistics]: {
title: "Get Resume Statistics",
@@ -295,7 +324,7 @@ export const TOOL_META = {
"lastViewedAt (timestamp or null), lastDownloadedAt (timestamp or null).",
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: TOOL_ANNOTATIONS[T.getResumeStatistics],
annotations: READ_IDEMPOTENT,
},
[T.listApplications]: {
title: "List Applications",
@@ -306,31 +335,31 @@ export const TOOL_META = {
tags: z.array(z.string()).optional().default([]),
includeArchived: z.boolean().optional().default(false),
}),
annotations: TOOL_ANNOTATIONS[T.listApplications],
annotations: READ_IDEMPOTENT,
},
[T.readApplication]: {
title: "Read Application",
description: "Read one full job application, including contacts, document URLs, follow-up details, and timeline.",
inputSchema: z.object({ id: applicationIdSchema }),
annotations: TOOL_ANNOTATIONS[T.readApplication],
annotations: READ_IDEMPOTENT,
},
[T.listApplicationTags]: {
title: "List Application Tags",
description: "Return every distinct tag used across job applications.",
inputSchema: z.object({}),
annotations: TOOL_ANNOTATIONS[T.listApplicationTags],
annotations: READ_IDEMPOTENT,
},
[T.getApplicationStats]: {
title: "Get Application Stats",
description: "Return aggregate application counts by pipeline stage and source for insights.",
inputSchema: z.object({}),
annotations: TOOL_ANNOTATIONS[T.getApplicationStats],
annotations: READ_IDEMPOTENT,
},
[T.createApplication]: {
title: "Create Application",
description: "Create a tracked job application. Company and role are required.",
inputSchema: createApplicationSchema,
annotations: TOOL_ANNOTATIONS[T.createApplication],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.updateApplication]: {
title: "Update Application",
@@ -341,7 +370,7 @@ export const TOOL_META = {
...applicationMutableFieldsSchema,
archived: z.boolean().optional().describe("Whether the application is hidden from active views."),
}),
annotations: TOOL_ANNOTATIONS[T.updateApplication],
annotations: WRITE_IDEMPOTENT,
},
[T.addApplicationNote]: {
title: "Add Application Note",
@@ -351,7 +380,7 @@ export const TOOL_META = {
text: z.string().min(1),
date: timelineDateSchema.optional().describe("Optional note date in YYYY-MM-DD format."),
}),
annotations: TOOL_ANNOTATIONS[T.addApplicationNote],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.updateApplicationTimelineEntry]: {
title: "Update Application Timeline Entry",
@@ -364,19 +393,19 @@ export const TOOL_META = {
text: z.string().min(1).optional().describe("Replacement note text. Only note entries can change text."),
})
.refine((value) => value.date !== undefined || value.text !== undefined, "Provide date or text to update."),
annotations: TOOL_ANNOTATIONS[T.updateApplicationTimelineEntry],
annotations: WRITE_IDEMPOTENT,
},
[T.deleteApplicationTimelineEntry]: {
title: "Delete Application Timeline Entry",
description: "Delete a note or older stage entry. The current stage entry cannot be deleted.",
inputSchema: z.object({ id: applicationIdSchema, entryId: applicationTimelineEntryIdSchema }),
annotations: TOOL_ANNOTATIONS[T.deleteApplicationTimelineEntry],
annotations: WRITE_DESTRUCTIVE,
},
[T.deleteApplication]: {
title: "Delete Application",
description: "Permanently delete one job application and its owned uploaded documents.",
inputSchema: z.object({ id: applicationIdSchema }),
annotations: TOOL_ANNOTATIONS[T.deleteApplication],
annotations: WRITE_DESTRUCTIVE,
},
[T.bulkUpdateApplications]: {
title: "Bulk Update Applications",
@@ -387,19 +416,19 @@ export const TOOL_META = {
archived: z.boolean().optional(),
addTags: z.array(z.string()).optional(),
}),
annotations: TOOL_ANNOTATIONS[T.bulkUpdateApplications],
annotations: WRITE_IDEMPOTENT,
},
[T.bulkDeleteApplications]: {
title: "Bulk Delete Applications",
description: "Permanently delete multiple applications.",
inputSchema: z.object({ ids: z.array(z.string()).min(1) }),
annotations: TOOL_ANNOTATIONS[T.bulkDeleteApplications],
annotations: WRITE_DESTRUCTIVE,
},
[T.importApplications]: {
title: "Import Applications",
description: "Bulk-create application rows parsed from CSV or another source. Maximum 500 items.",
inputSchema: z.object({ items: z.array(createApplicationSchema).min(1).max(500) }),
annotations: TOOL_ANNOTATIONS[T.importApplications],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.attachApplicationDocument]: {
title: "Attach Application Document",
@@ -411,13 +440,13 @@ export const TOOL_META = {
contentType: z.literal("application/pdf"),
dataBase64: pdfBase64Schema,
}),
annotations: TOOL_ANNOTATIONS[T.attachApplicationDocument],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.removeApplicationDocument]: {
title: "Remove Application Document",
description: "Remove a sent resume or cover-letter PDF from an application.",
inputSchema: z.object({ id: applicationIdSchema, kind: applicationDocumentKindSchema }),
annotations: TOOL_ANNOTATIONS[T.removeApplicationDocument],
annotations: WRITE_IDEMPOTENT,
},
[T.autofillApplicationFromJob]: {
title: "Autofill Application From Job",
@@ -427,24 +456,24 @@ export const TOOL_META = {
sourceUrl: httpUrlSchema.optional(),
jobDescription: z.string().max(20_000).optional(),
}),
annotations: TOOL_ANNOTATIONS[T.autofillApplicationFromJob],
annotations: READ_OPEN_WORLD_NON_IDEMPOTENT,
},
[T.scoreApplicationMatch]: {
title: "Score Application Match",
description: "Score the linked resume against the application's job description and persist match metadata.",
inputSchema: z.object({ id: applicationIdSchema }),
annotations: TOOL_ANNOTATIONS[T.scoreApplicationMatch],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.tailorResumeForApplication]: {
title: "Tailor Resume For Application",
description: "Create and link a tailored copy of the application's linked resume.",
inputSchema: z.object({ id: applicationIdSchema }),
annotations: TOOL_ANNOTATIONS[T.tailorResumeForApplication],
annotations: WRITE_NON_IDEMPOTENT,
},
[T.draftApplicationMessage]: {
title: "Draft Application Message",
description: "Draft either a cover letter or recruiter follow-up from application and resume context.",
inputSchema: z.object({ id: applicationIdSchema, kind: z.enum(["cover-letter", "follow-up"]) }),
annotations: TOOL_ANNOTATIONS[T.draftApplicationMessage],
annotations: READ_NON_IDEMPOTENT,
},
} as const;
+2 -1
View File
@@ -21,7 +21,8 @@ vi.mock("@reactive-resume/env/server", () => ({
},
}));
const { MCP_TOOL_NAME, registerTools } = await import("./tools");
const { MCP_TOOL_NAME } = await import("./mcp-tool-names");
const { registerTools } = await import("./tools");
type ToolHandler = (input: Record<string, unknown>) => Promise<{
content: Array<{ type: "text"; text: string }>;
-2
View File
@@ -12,8 +12,6 @@ import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
import { MCP_TOOL_NAME } from "./mcp-tool-names";
import { TOOL_META } from "./tool-meta";
export { MCP_TOOL_NAME } from "./mcp-tool-names";
type PatchOperation = z.infer<typeof resumePatchOperationsSchema>[number];
// ── Shared Helpers ─────────────────────────────────────────────