mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-27 18:34:51 +10:00
62f8270b3e
commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 13:13:38 2026 +0200
chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies
commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 13:08:04 2026 +0200
fix: remove timestamp conflict guard
commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 12:11:51 2026 +0200
chore(release): v5.1.5
commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:58:35 2026 +0200
revert: compose.yml
commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:58:08 2026 +0200
refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086
commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:10:41 2026 +0200
refactor(pdf): simplify sidebar section filtering and update summary feature logic
commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:53:37 2026 +0200
chore: update pnpm-lock.yaml and turbo.json
commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:38:30 2026 +0200
fix(polyfill): add tested polyfill for Map Upsert methods
commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:23:29 2026 +0200
Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration
# Conflicts:
# packages/api/src/services/agent-url.ts
# packages/runtime-externals/package.json
commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:22:12 2026 +0200
chore: preserve branch changes before main sync
commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Mon May 18 07:50:28 2026 +0200
chore: lot of fixes for monorepo migration
commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 13:57:17 2026 +0200
chore: update knip version and refine web app routing with new SEO endpoints
commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 12:10:06 2026 +0200
refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint
commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:54:29 2026 +0200
chore: update dependencies and enhance PWA metadata in web app
commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:12:35 2026 +0200
docs: revise manifest-only pwa testing scope
commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:11:33 2026 +0200
docs: add manifest-only pwa design
commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:05:04 2026 +0200
chore(dev): simplify server proxy config
commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:04:50 2026 +0200
docs: add unsafe oauth redirect plan
commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:04:34 2026 +0200
feat(auth): add unsafe oauth redirect flag
commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:55:02 2026 +0200
docs: design unsafe oauth redirect flag
commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:22:04 2026 +0200
chore: update translation source paths
commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:09:25 2026 +0200
refactor(arch): react spa + hono migration
commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 01:10:47 2026 +0200
docs: add docker nightly tagging design
commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Thu May 14 20:05:44 2026 +0200
feat: migrate to hono spa server
342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
|
import z from "zod";
|
|
import { resumePatchOperationsSchema } from "@reactive-resume/ai/tools/resume-tool-contracts";
|
|
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 = [
|
|
{
|
|
name: T.listResumes,
|
|
title: "List Resumes",
|
|
description: [
|
|
"Primary way to discover resume IDs for this account. Resumes are not listed as MCP resources;",
|
|
"use this tool (not `resources/list`) to enumerate IDs.",
|
|
"",
|
|
"Returns an array of resume objects (without full resume data) containing:",
|
|
"id, name, slug, tags, isPublic, isLocked, createdAt, updatedAt.",
|
|
"",
|
|
`Call this before \`${T.getResume}\`, \`${T.patchResume}\`, prompts, or \`resources/read\` with \`resume://{id}\`.`,
|
|
"Results can be filtered by tags and sorted by last updated date, creation date, or name.",
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(
|
|
z.object({
|
|
tags: z
|
|
.array(z.string())
|
|
.optional()
|
|
.default([])
|
|
.describe(
|
|
"Filter resumes by tags. Only resumes matching ALL specified tags are returned. Default: no filter.",
|
|
),
|
|
sort: z
|
|
.enum(["lastUpdatedAt", "createdAt", "name"])
|
|
.optional()
|
|
.default("lastUpdatedAt")
|
|
.describe("Sort order for results. Default: lastUpdatedAt."),
|
|
}),
|
|
),
|
|
annotations: TOOL_ANNOTATIONS[T.listResumes],
|
|
},
|
|
{
|
|
name: T.listResumeTags,
|
|
title: "List Resume Tags",
|
|
description: [
|
|
"Returns a sorted list of every distinct tag used across your resumes.",
|
|
"Useful for choosing tag filters when calling list tools or keeping naming consistent.",
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(z.object({})),
|
|
annotations: TOOL_ANNOTATIONS[T.listResumeTags],
|
|
},
|
|
{
|
|
name: T.getResume,
|
|
title: "Read Resume",
|
|
description: [
|
|
"Get the full data of a specific resume by its ID.",
|
|
"",
|
|
"Returns the complete resume data as JSON, including: basics (name, headline, email, phone,",
|
|
"location, website), summary, picture settings, all sections (experience, education, skills,",
|
|
"projects, etc.), custom sections, and metadata (template, layout, typography, colors).",
|
|
"",
|
|
`Use \`${T.listResumes}\` first to find valid IDs.`,
|
|
"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.getResumeAnalysis,
|
|
title: "Get Resume Analysis",
|
|
description: [
|
|
"Returns the latest saved AI analysis for a resume (scorecard, strengths, suggestions), if any.",
|
|
"Analyses are created from the Reactive Resume web app AI flow, not from MCP.",
|
|
`Returns JSON or a short message if none exists. Use \`${T.listResumes}\` to find resume IDs.`,
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })),
|
|
annotations: TOOL_ANNOTATIONS[T.getResumeAnalysis],
|
|
},
|
|
{
|
|
name: T.createResume,
|
|
title: "Create Resume",
|
|
description: [
|
|
"Create a new, empty resume with a name and URL-friendly slug.",
|
|
"",
|
|
"Returns the ID of the newly created resume.",
|
|
"Set `withSampleData` to true to pre-fill with example content (useful for testing).",
|
|
`After creating, use \`${T.getResume}\` to view or \`${T.patchResume}\` to populate it.`,
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(
|
|
z.object({
|
|
name: z.string().min(1).max(64).describe("Display name for the resume (e.g. 'Software Engineer 2026')"),
|
|
slug: z
|
|
.string()
|
|
.min(1)
|
|
.max(64)
|
|
.describe("URL-friendly slug, must be unique across your resumes (e.g. 'software-engineer-2026')"),
|
|
tags: z
|
|
.array(z.string())
|
|
.optional()
|
|
.default([])
|
|
.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],
|
|
},
|
|
{
|
|
name: T.importResume,
|
|
title: "Import Resume",
|
|
description: [
|
|
"Create a new resume from a full ResumeData JSON object (e.g. an exported file from Reactive Resume).",
|
|
"A random name and slug are assigned automatically, like the web importer.",
|
|
`For small edits to an existing resume, prefer \`${T.patchResume}\` instead of re-importing.`,
|
|
"Large payloads may exceed MCP client message limits — in that case, use the web UI or the HTTP API.",
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(
|
|
z.object({
|
|
data: z
|
|
.unknown()
|
|
.describe("Complete ResumeData JSON (same shape as read_resume output or `resume://_meta/schema`)."),
|
|
}),
|
|
),
|
|
annotations: TOOL_ANNOTATIONS[T.importResume],
|
|
},
|
|
{
|
|
name: T.duplicateResume,
|
|
title: "Duplicate Resume",
|
|
description: [
|
|
"Create a copy of an existing resume with all its data.",
|
|
"",
|
|
"Returns the ID of the newly duplicated resume.",
|
|
"You must provide a new name and slug for the copy.",
|
|
"Useful for creating job-specific variants of a base resume.",
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(
|
|
z.object({
|
|
id: resumeId.describe("ID of the resume to duplicate"),
|
|
name: z.string().min(1).max(64).describe("Name for the duplicate"),
|
|
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],
|
|
},
|
|
{
|
|
name: T.patchResume,
|
|
title: "Apply Resume Patch",
|
|
description: [
|
|
"Apply JSON Patch (RFC 6902) operations to partially update a resume's data.",
|
|
"",
|
|
`This is the primary way to edit resume content. Use \`${T.getResume}\` first to inspect the`,
|
|
"current structure, and `resume://_meta/schema` to understand valid paths and types.",
|
|
"",
|
|
"Supported operations: add, remove, replace, move, copy, test.",
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(
|
|
z.object({
|
|
id: resumeId,
|
|
operations: resumePatchOperationsSchema,
|
|
}),
|
|
),
|
|
annotations: TOOL_ANNOTATIONS[T.patchResume],
|
|
},
|
|
{
|
|
name: T.updateResume,
|
|
title: "Update Resume (metadata)",
|
|
description: [
|
|
"Update resume metadata only: display name, URL slug, tags, and/or public visibility.",
|
|
"Does not change section content — use JSON Patch via the patch tool for body edits.",
|
|
`Locked resumes cannot be updated; use \`${T.unlockResume}\` first.`,
|
|
"Password protection cannot be set or removed via MCP; use the web app for that.",
|
|
"",
|
|
"Always returns your canonical share URL (`{app}/{username}/{slug}`). Anonymous viewers can use it only when `isPublic` is true; password protection from the web app still applies.",
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(
|
|
z.object({
|
|
id: resumeId,
|
|
name: z.string().min(1).max(64).optional().describe("Display name for the resume."),
|
|
slug: z
|
|
.string()
|
|
.min(1)
|
|
.max(64)
|
|
.optional()
|
|
.describe("URL-friendly slug; must stay unique among your resumes."),
|
|
tags: z.array(z.string()).optional().describe("Replace the resume's tags (omit to leave unchanged)."),
|
|
isPublic: z
|
|
.boolean()
|
|
.optional()
|
|
.describe(
|
|
"When true, anyone with the link can view the public resume (subject to password if set in the app).",
|
|
),
|
|
}),
|
|
),
|
|
annotations: TOOL_ANNOTATIONS[T.updateResume],
|
|
},
|
|
{
|
|
name: T.deleteResume,
|
|
title: "Delete Resume",
|
|
description: [
|
|
"Permanently delete a resume and all its associated files (screenshots, PDFs).",
|
|
"",
|
|
`This action is IRREVERSIBLE. Locked resumes cannot be deleted — use \`${T.unlockResume}\` first.`,
|
|
`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,
|
|
title: "Lock Resume",
|
|
description: [
|
|
"Lock a resume to prevent any modifications.",
|
|
"",
|
|
`When locked, a resume cannot be edited (${T.patchResume}, ${T.updateResume}), or deleted.`,
|
|
`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.getResumeStatistics,
|
|
title: "Get Resume Statistics",
|
|
description: [
|
|
"Get view and download statistics for a resume.",
|
|
"",
|
|
"Returns: isPublic (boolean), views (count), downloads (count),",
|
|
"lastViewedAt (timestamp or null), lastDownloadedAt (timestamp or null).",
|
|
].join("\n"),
|
|
inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })),
|
|
annotations: TOOL_ANNOTATIONS[T.getResumeStatistics],
|
|
},
|
|
];
|
|
|
|
const prompts = [
|
|
{
|
|
name: "build_resume",
|
|
title: "Build Resume",
|
|
description: "Guide the user step-by-step through building a resume from scratch, section by section.",
|
|
arguments: [{ name: "id", description: "Resume ID.", required: true }],
|
|
},
|
|
{
|
|
name: "improve_resume",
|
|
title: "Improve Resume",
|
|
description: "Review resume content and suggest concrete improvements to wording, impact, and structure.",
|
|
arguments: [{ name: "id", description: "Resume ID.", required: true }],
|
|
},
|
|
{
|
|
name: "review_resume",
|
|
title: "Review Resume",
|
|
description:
|
|
"Get a structured, professional critique with a scorecard and prioritized recommendations. Read-only — no changes are made.",
|
|
arguments: [{ name: "id", description: "Resume ID.", required: true }],
|
|
},
|
|
];
|
|
|
|
const resources = [
|
|
{
|
|
name: "resume-schema",
|
|
title: "Resume Data JSON Schema",
|
|
uri: "resume://_meta/schema",
|
|
description: [
|
|
"The JSON Schema describing the complete resume data structure.",
|
|
"Reference when generating JSON Patch operations so paths and value types are valid.",
|
|
].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 = [
|
|
{
|
|
name: "resume",
|
|
title: "Resume Data",
|
|
uriTemplate: "resume://{id}",
|
|
description: "Full resume data as JSON. Discover IDs with the list tool; read via resources/read or read_resume.",
|
|
mimeType: "application/json",
|
|
},
|
|
];
|
|
|
|
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,
|
|
title: "Reactive Resume",
|
|
websiteUrl: "https://rxresu.me",
|
|
description:
|
|
"Reactive Resume is a free and open-source resume builder. Use this MCP server to interact with your resume using an LLM of your choice.",
|
|
icons: [
|
|
{ src: "https://rxresu.me/icon/light.svg", mimeType: "image/svg+xml", theme: "light" as const },
|
|
{ src: "https://rxresu.me/icon/dark.svg", mimeType: "image/svg+xml", theme: "dark" as const },
|
|
],
|
|
},
|
|
tools,
|
|
prompts,
|
|
resources,
|
|
resourceTemplates,
|
|
authentication: {
|
|
required: true,
|
|
schemes: ["oauth2", "bearer"],
|
|
},
|
|
};
|
|
}
|