further improvements to the mcp server

This commit is contained in:
Amruth Pillai
2026-04-09 10:06:50 +02:00
parent 1b266ba7ac
commit 85e0b0a96d
5 changed files with 148 additions and 83 deletions
@@ -123,7 +123,7 @@ export function ResumeAnalysisSectionBuilder() {
</div>
</div>
{!analysis && !isPending && (
{analysisQuery.isFetched && !analysis && !isPending && (
<div className="rounded-md border border-dashed p-3">
<p className="max-w-xs text-sm text-muted-foreground">
<Trans>Run your first analysis to get a scorecard, strengths, and prioritized suggestions.</Trans>
+42 -1
View File
@@ -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://<id>\` 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,
+14
View File
@@ -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;
@@ -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<McpRegisteredToolName, ToolAnnotations> = {
[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,
},
};
+16 -81
View File
@@ -6,22 +6,12 @@ import z from "zod";
import { client } from "@/integrations/orpc/client";
import { jsonPatchOperationSchema } from "@/utils/resume/patch";
type PatchOperation = z.infer<typeof jsonPatchOperationSchema>;
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<typeof jsonPatchOperationSchema>;
// ── 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 });