mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
further improvements to the mcp server
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user