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 });