From 85e0b0a96dc9869b485e4d30f6b9bb06f28c6d4a Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Thu, 9 Apr 2026 10:06:50 +0200 Subject: [PATCH] further improvements to the mcp server --- .../right/sections/resume-analysis.tsx | 2 +- src/routes/mcp/-helpers/mcp-server-card.ts | 43 +++++++- src/routes/mcp/-helpers/mcp-tool-names.ts | 14 +++ src/routes/mcp/-helpers/tool-annotations.ts | 75 ++++++++++++++ src/routes/mcp/-helpers/tools.ts | 97 +++---------------- 5 files changed, 148 insertions(+), 83 deletions(-) create mode 100644 src/routes/mcp/-helpers/mcp-tool-names.ts create mode 100644 src/routes/mcp/-helpers/tool-annotations.ts diff --git a/src/routes/builder/$resumeId/-sidebar/right/sections/resume-analysis.tsx b/src/routes/builder/$resumeId/-sidebar/right/sections/resume-analysis.tsx index 9fdff07be..4839b2014 100644 --- a/src/routes/builder/$resumeId/-sidebar/right/sections/resume-analysis.tsx +++ b/src/routes/builder/$resumeId/-sidebar/right/sections/resume-analysis.tsx @@ -123,7 +123,7 @@ export function ResumeAnalysisSectionBuilder() { - {!analysis && !isPending && ( + {analysisQuery.isFetched && !analysis && !isPending && (

Run your first analysis to get a scorecard, strengths, and prioritized suggestions. diff --git a/src/routes/mcp/-helpers/mcp-server-card.ts b/src/routes/mcp/-helpers/mcp-server-card.ts index cd4cc06f4..f59b9f9f9 100644 --- a/src/routes/mcp/-helpers/mcp-server-card.ts +++ b/src/routes/mcp/-helpers/mcp-server-card.ts @@ -3,13 +3,17 @@ import z from "zod"; import { jsonPatchOperationSchema } from "@/utils/resume/patch"; -import { MCP_TOOL_NAME as T } from "./tools"; +import { MCP_TOOL_NAME as T } from "./mcp-tool-names"; +import { TOOL_ANNOTATIONS } from "./tool-annotations"; const resumeId = z.string().min(1).describe("Resume ID."); /** * Static MCP server card (SEP-1649 / well-known `mcp/server-card.json`). * Kept in sync with `registerTools`, `registerResources`, and `registerPrompts`. + * + * Some registries only surface the `resources` array in their UI, not `resourceTemplates`. + * The parameterized resume URI is therefore duplicated here so discovery matches the live template. */ export function buildMcpServerCard(appVersion: string) { const tools = [ @@ -42,6 +46,7 @@ export function buildMcpServerCard(appVersion: string) { .describe("Sort order for results. Default: lastUpdatedAt."), }), ), + annotations: TOOL_ANNOTATIONS[T.listResumes], }, { name: T.getResume, @@ -57,6 +62,7 @@ export function buildMcpServerCard(appVersion: string) { "The `resume://_meta/schema` resource describes the full data structure for JSON Patch paths.", ].join("\n"), inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })), + annotations: TOOL_ANNOTATIONS[T.getResume], }, { name: T.createResume, @@ -84,6 +90,7 @@ export function buildMcpServerCard(appVersion: string) { withSampleData: z.boolean().optional().default(false).describe("Pre-fill with sample data. Default: false."), }), ), + annotations: TOOL_ANNOTATIONS[T.createResume], }, { name: T.duplicateResume, @@ -103,6 +110,7 @@ export function buildMcpServerCard(appVersion: string) { tags: z.array(z.string()).optional().default([]).describe("Tags for the duplicate"), }), ), + annotations: TOOL_ANNOTATIONS[T.duplicateResume], }, { name: T.patchResume, @@ -124,6 +132,7 @@ export function buildMcpServerCard(appVersion: string) { .describe("Array of JSON Patch (RFC 6902) operations to apply"), }), ), + annotations: TOOL_ANNOTATIONS[T.patchResume], }, { name: T.deleteResume, @@ -135,6 +144,7 @@ export function buildMcpServerCard(appVersion: string) { `Consider using \`${T.duplicateResume}\` to create a backup before deleting.`, ].join("\n"), inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })), + annotations: TOOL_ANNOTATIONS[T.deleteResume], }, { name: T.lockResume, @@ -146,12 +156,14 @@ export function buildMcpServerCard(appVersion: string) { `Use \`${T.unlockResume}\` to re-enable editing.`, ].join("\n"), inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })), + annotations: TOOL_ANNOTATIONS[T.lockResume], }, { name: T.unlockResume, title: "Unlock Resume", description: "Unlock a previously locked resume, re-enabling edits, patches, and deletion.", inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })), + annotations: TOOL_ANNOTATIONS[T.unlockResume], }, { name: T.exportResumePdf, @@ -163,6 +175,7 @@ export function buildMcpServerCard(appVersion: string) { "then uploaded to storage. The returned URL can be shared or downloaded directly.", ].join("\n"), inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })), + annotations: TOOL_ANNOTATIONS[T.exportResumePdf], }, { name: T.getResumeScreenshot, @@ -174,6 +187,7 @@ export function buildMcpServerCard(appVersion: string) { "is updated. Returns null if the printer service is unavailable.", ].join("\n"), inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })), + annotations: TOOL_ANNOTATIONS[T.getResumeScreenshot], }, { name: T.getResumeStatistics, @@ -185,6 +199,7 @@ export function buildMcpServerCard(appVersion: string) { "lastViewedAt (timestamp or null), lastDownloadedAt (timestamp or null).", ].join("\n"), inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })), + annotations: TOOL_ANNOTATIONS[T.getResumeStatistics], }, ]; @@ -235,6 +250,17 @@ export function buildMcpServerCard(appVersion: string) { ].join(" "), mimeType: "application/json", }, + { + name: "resume", + title: "Resume Data", + uri: "resume://{id}", + description: [ + "Full resume JSON for one resume. Substitute a real ID for `{id}` (UUID from your account).", + "On the wire this is a resource template (`resources/templates/list`), not a row in `resources/list`.", + `Discover IDs with \`${T.listResumes}\`; read via \`resources/read\` on e.g. \`resume://\` or use \`${T.getResume}\`.`, + ].join(" "), + mimeType: "application/json", + }, ]; const resourceTemplates = [ @@ -248,6 +274,21 @@ export function buildMcpServerCard(appVersion: string) { ]; return { + /** + * Optional session fields for gateways. OAuth is primary; API key is optional for clients that support custom headers. + */ + configurationSchema: { + type: "object", + properties: { + apiKey: { + type: "string", + title: "API key", + description: + "Optional. Create a key under Account → API Keys. Forwarded as the x-api-key header when not using OAuth.", + "x-from": { header: "x-api-key" }, + }, + }, + }, serverInfo: { name: "reactive-resume", version: appVersion, diff --git a/src/routes/mcp/-helpers/mcp-tool-names.ts b/src/routes/mcp/-helpers/mcp-tool-names.ts new file mode 100644 index 000000000..3b2f3660f --- /dev/null +++ b/src/routes/mcp/-helpers/mcp-tool-names.ts @@ -0,0 +1,14 @@ +/** Hierarchical MCP tool names (SEP-986-style namespacing). */ +export const MCP_TOOL_NAME = { + listResumes: "reactive_resume.list_resumes", + getResume: "reactive_resume.get_resume", + createResume: "reactive_resume.create_resume", + duplicateResume: "reactive_resume.duplicate_resume", + patchResume: "reactive_resume.patch_resume", + deleteResume: "reactive_resume.delete_resume", + lockResume: "reactive_resume.lock_resume", + unlockResume: "reactive_resume.unlock_resume", + exportResumePdf: "reactive_resume.export_resume_pdf", + getResumeScreenshot: "reactive_resume.get_resume_screenshot", + getResumeStatistics: "reactive_resume.get_resume_statistics", +} as const; diff --git a/src/routes/mcp/-helpers/tool-annotations.ts b/src/routes/mcp/-helpers/tool-annotations.ts new file mode 100644 index 000000000..4c1d3c12b --- /dev/null +++ b/src/routes/mcp/-helpers/tool-annotations.ts @@ -0,0 +1,75 @@ +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]; + +/** Tool behavior hints for MCP `tools/list` and the static server card. */ +export const TOOL_ANNOTATIONS: Record = { + [MCP_TOOL_NAME.listResumes]: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + [MCP_TOOL_NAME.getResume]: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + [MCP_TOOL_NAME.createResume]: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + [MCP_TOOL_NAME.duplicateResume]: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + [MCP_TOOL_NAME.patchResume]: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + [MCP_TOOL_NAME.deleteResume]: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: false, + }, + [MCP_TOOL_NAME.lockResume]: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + [MCP_TOOL_NAME.unlockResume]: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + [MCP_TOOL_NAME.exportResumePdf]: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + [MCP_TOOL_NAME.getResumeScreenshot]: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + [MCP_TOOL_NAME.getResumeStatistics]: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, +}; diff --git a/src/routes/mcp/-helpers/tools.ts b/src/routes/mcp/-helpers/tools.ts index 1fd534f6a..8ae72ef30 100644 --- a/src/routes/mcp/-helpers/tools.ts +++ b/src/routes/mcp/-helpers/tools.ts @@ -6,22 +6,12 @@ import z from "zod"; import { client } from "@/integrations/orpc/client"; import { jsonPatchOperationSchema } from "@/utils/resume/patch"; -type PatchOperation = z.infer; +import { MCP_TOOL_NAME } from "./mcp-tool-names"; +import { TOOL_ANNOTATIONS } from "./tool-annotations"; -/** Hierarchical MCP tool names (SEP-986-style namespacing). */ -export const MCP_TOOL_NAME = { - listResumes: "reactive_resume.list_resumes", - getResume: "reactive_resume.get_resume", - createResume: "reactive_resume.create_resume", - duplicateResume: "reactive_resume.duplicate_resume", - patchResume: "reactive_resume.patch_resume", - deleteResume: "reactive_resume.delete_resume", - lockResume: "reactive_resume.lock_resume", - unlockResume: "reactive_resume.unlock_resume", - exportResumePdf: "reactive_resume.export_resume_pdf", - getResumeScreenshot: "reactive_resume.get_resume_screenshot", - getResumeStatistics: "reactive_resume.get_resume_statistics", -} as const; +export { MCP_TOOL_NAME } from "./mcp-tool-names"; + +type PatchOperation = z.infer; // ── Shared Helpers ────────────────────────────────────────────── @@ -103,12 +93,7 @@ export function registerTools(server: McpServer) { .default("lastUpdatedAt") .describe("Sort order for results. Default: lastUpdatedAt."), }), - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.listResumes], }, withErrorHandling( "listing resumes", @@ -138,12 +123,7 @@ export function registerTools(server: McpServer) { "The `resume://_meta/schema` resource describes the full data structure for JSON Patch paths.", ].join("\n"), inputSchema: z.object({ id: resumeIdSchema }), - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.getResume], }, withErrorHandling("getting resume", async ({ id }: { id: string }) => { const resume = await client.resume.getById({ id }); @@ -178,12 +158,7 @@ export function registerTools(server: McpServer) { .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: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.createResume], }, withErrorHandling( "creating resume", @@ -225,12 +200,7 @@ export function registerTools(server: McpServer) { 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: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.duplicateResume], }, withErrorHandling( "duplicating resume", @@ -279,12 +249,7 @@ export function registerTools(server: McpServer) { .min(1) .describe("Array of JSON Patch (RFC 6902) operations to apply"), }), - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.patchResume], }, withErrorHandling("patching resume", async ({ id, operations }: { id: string; operations: PatchOperation[] }) => { const resume = await client.resume.patch({ id, operations }); @@ -306,12 +271,7 @@ export function registerTools(server: McpServer) { `Consider using \`${T.duplicateResume}\` to create a backup before deleting.`, ].join("\n"), inputSchema: z.object({ id: resumeIdSchema }), - annotations: { - readOnlyHint: false, - destructiveHint: true, - idempotentHint: true, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.deleteResume], }, withErrorHandling("deleting resume", async ({ id }: { id: string }) => { await client.resume.delete({ id }); @@ -333,12 +293,7 @@ export function registerTools(server: McpServer) { `Use \`${T.unlockResume}\` to re-enable editing.`, ].join("\n"), inputSchema: z.object({ id: resumeIdSchema }), - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.lockResume], }, withErrorHandling("locking resume", async ({ id }: { id: string }) => { await client.resume.setLocked({ id, isLocked: true }); @@ -354,12 +309,7 @@ export function registerTools(server: McpServer) { title: "Unlock Resume", description: "Unlock a previously locked resume, re-enabling edits, patches, and deletion.", inputSchema: z.object({ id: resumeIdSchema }), - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.unlockResume], }, withErrorHandling("unlocking resume", async ({ id }: { id: string }) => { await client.resume.setLocked({ id, isLocked: false }); @@ -381,12 +331,7 @@ export function registerTools(server: McpServer) { "PDF generation may take a few seconds depending on the resume's complexity.", ].join("\n"), inputSchema: z.object({ id: resumeIdSchema }), - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.exportResumePdf], }, withErrorHandling("exporting resume as PDF", async ({ id }: { id: string }) => { const { url } = await client.printer.printResumeAsPDF({ id }); @@ -408,12 +353,7 @@ export function registerTools(server: McpServer) { "Use this after making changes to visually verify the result.", ].join("\n"), inputSchema: z.object({ id: resumeIdSchema }), - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.getResumeScreenshot], }, withErrorHandling("getting resume screenshot", async ({ id }: { id: string }) => { const { url } = await client.printer.getResumeScreenshot({ id }); @@ -438,12 +378,7 @@ export function registerTools(server: McpServer) { "lastViewedAt (timestamp or null), lastDownloadedAt (timestamp or null).", ].join("\n"), inputSchema: z.object({ id: resumeIdSchema }), - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + annotations: TOOL_ANNOTATIONS[T.getResumeStatistics], }, withErrorHandling("getting resume statistics", async ({ id }: { id: string }) => { const stats = await client.resume.statistics.getById({ id });