mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
better mcp server
This commit is contained in:
@@ -2,6 +2,7 @@ dist
|
||||
.env*
|
||||
/data
|
||||
.nitro
|
||||
.agents
|
||||
.output
|
||||
.vercel
|
||||
.cursor
|
||||
|
||||
@@ -19,8 +19,5 @@
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/utils",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {
|
||||
"@animate-ui": "https://animate-ui.com/r/{name}.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,32 +207,52 @@ If you're running a self-hosted Reactive Resume instance, replace `https://rxres
|
||||
|
||||
## Available Tools
|
||||
|
||||
The MCP server exposes the following tools:
|
||||
Tool names use a hierarchical `reactive_resume.*` prefix ([SEP-986](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) style) so they stay distinct when multiple MCP servers are enabled in the same client.
|
||||
|
||||
| Tool | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `list_resumes` | List all resumes with IDs, names, tags, and status. Supports filtering by tags and sorting by last updated, creation date, or name |
|
||||
| `get_resume` | Get the full data of a specific resume by ID |
|
||||
| `create_resume` | Create a new, empty resume with a name and slug. Optionally pre-fill with sample data |
|
||||
| `duplicate_resume` | Create a copy of an existing resume with a new name and slug |
|
||||
| `patch_resume` | Apply JSON Patch (RFC 6902) operations to modify a resume's data |
|
||||
| `delete_resume` | Permanently delete a resume and all associated files. **Irreversible** |
|
||||
| `lock_resume` | Lock a resume to prevent edits, patches, and deletion |
|
||||
| `unlock_resume` | Unlock a previously locked resume to re-enable editing |
|
||||
| `export_resume_pdf` | Generate a PDF from the resume and return a download URL |
|
||||
| `get_resume_screenshot` | Get a visual preview of the resume's first page as a WebP image URL |
|
||||
| `get_resume_statistics` | Get view and download statistics for a resume |
|
||||
| Tool | Description |
|
||||
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `reactive_resume.list_resumes` | List all resumes with IDs, names, tags, and status. Supports filtering by tags and sorting by last updated, creation date, or name |
|
||||
| `reactive_resume.get_resume` | Get the full data of a specific resume by ID |
|
||||
| `reactive_resume.create_resume` | Create a new, empty resume with a name and slug. Optionally pre-fill with sample data |
|
||||
| `reactive_resume.duplicate_resume` | Create a copy of an existing resume with a new name and slug |
|
||||
| `reactive_resume.patch_resume` | Apply JSON Patch (RFC 6902) operations to modify a resume's data |
|
||||
| `reactive_resume.delete_resume` | Permanently delete a resume and all associated files. **Irreversible** |
|
||||
| `reactive_resume.lock_resume` | Lock a resume to prevent edits, patches, and deletion |
|
||||
| `reactive_resume.unlock_resume` | Unlock a previously locked resume to re-enable editing |
|
||||
| `reactive_resume.export_resume_pdf` | Generate a PDF from the resume and return a download URL |
|
||||
| `reactive_resume.get_resume_screenshot` | Get a visual preview of the resume's first page as a WebP image URL |
|
||||
| `reactive_resume.get_resume_statistics` | Get view and download statistics for a resume |
|
||||
|
||||
### Breaking change (tool names)
|
||||
|
||||
Older clients may refer to unprefixed names (`list_resumes`, `get_resume`, …). Those names are no longer used; update automations and saved prompts to the `reactive_resume.*` names above.
|
||||
|
||||
## Available Resources
|
||||
|
||||
| Resource | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `resume://{id}` | The full resume data as a readable JSON resource. Lists all resumes and supports reading individual ones by ID |
|
||||
| `resume://schema` | The ResumeData JSON Schema — reference this to understand valid paths and value types for JSON Patch operations |
|
||||
Resources follow MCP conventions: **static** items appear in `resources/list`; **parameterized** access is declared in `resources/templates/list` and read via `resources/read` once you know the ID.
|
||||
|
||||
| Discovery | What you get |
|
||||
| ------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `resources/list` | Static resources only — currently **`resume://_meta/schema`** (ResumeData JSON Schema) |
|
||||
| `resources/templates/list` | **`resume://{id}`** — template for reading full resume JSON by ID (not enumerated per resume) |
|
||||
| `reactive_resume.list_resumes` (tool) | **Primary way to discover resume IDs** — resumes are not listed as separate MCP resources |
|
||||
|
||||
| URI | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------ |
|
||||
| `resume://_meta/schema` | ResumeData JSON Schema — use for valid JSON Patch paths and value types |
|
||||
| `resume://{id}` | Full resume data as JSON — use an ID from `reactive_resume.list_resumes` |
|
||||
|
||||
### Breaking change (schema URI)
|
||||
|
||||
The schema resource was previously `resume://schema`. It is now **`resume://_meta/schema`**. Update any saved prompts, automations, or client configs that referenced the old URI.
|
||||
|
||||
### Static server card (`/.well-known/mcp/server-card.json`)
|
||||
|
||||
`GET /.well-known/mcp/server-card.json` returns a JSON document ([SEP-1649](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1649)) with `serverInfo`, optional authentication metadata, and summaries of tools, resources, resource templates, and prompts. It is generated to match the live MCP server and can be used for discovery when a client cannot run a full capability scan against `/mcp/`.
|
||||
|
||||
## Available Prompts
|
||||
|
||||
Prompts are pre-built workflows that provide the AI with structured instructions and context. Each prompt embeds the resume data and schema automatically.
|
||||
Prompts are pre-built workflows that provide the AI with structured instructions and context. Each prompt embeds the resume data and the schema resource (`resume://_meta/schema`) automatically.
|
||||
|
||||
| Prompt | Description |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
@@ -286,8 +306,8 @@ Once your MCP client is connected, you can use natural language to interact with
|
||||
- "Tailor my resume for this job description: ..." (uses `tailor_resume`)
|
||||
|
||||
<Tip>
|
||||
The AI will use `get_resume` to inspect your current resume before making changes with `patch_resume`. This ensures
|
||||
the correct JSON paths are used.
|
||||
The AI will use `reactive_resume.get_resume` to inspect your current resume before making changes with
|
||||
`reactive_resume.patch_resume`. This ensures the correct JSON paths are used.
|
||||
</Tip>
|
||||
|
||||
## Troubleshooting
|
||||
@@ -297,7 +317,7 @@ Once your MCP client is connected, you can use natural language to interact with
|
||||
| "Unauthorized" with no login prompt | Your client may not support MCP OAuth discovery. Use API key mode (`x-api-key`) |
|
||||
| OAuth login opens but fails redirect/callback | Confirm your client's MCP OAuth callback settings and retry the connection |
|
||||
| "API error (401)" | Your API key is invalid or expired. Create a new one in **Settings → API Keys** |
|
||||
| "API error (404)" | The resume ID doesn't exist. Use `list_resumes` to find valid IDs |
|
||||
| "API error (404)" | The resume ID doesn't exist. Use `reactive_resume.list_resumes` to find valid IDs |
|
||||
| "API error (403)" | The resume is locked. Unlock it in the Reactive Resume dashboard |
|
||||
| Connection refused | Check that the URL is correct and the instance is running |
|
||||
| "ReferenceError: File is not defined" when using `mcp-remote` | You're running Node.js 18. `mcp-remote` requires **Node.js 20 or later** — upgrade with `nvm use 20` or `nvm alias default 20` |
|
||||
|
||||
+37
-36
@@ -35,22 +35,22 @@
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.66",
|
||||
"@ai-sdk/google": "^3.0.58",
|
||||
"@ai-sdk/openai": "^3.0.50",
|
||||
"@aws-sdk/client-s3": "^3.1024.0",
|
||||
"@ai-sdk/anthropic": "^3.0.68",
|
||||
"@ai-sdk/google": "^3.0.60",
|
||||
"@ai-sdk/openai": "^3.0.52",
|
||||
"@aws-sdk/client-s3": "^3.1027.0",
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@better-auth/api-key": "^1.5.6",
|
||||
"@better-auth/drizzle-adapter": "^1.5.6",
|
||||
"@better-auth/infra": "^0.1.13",
|
||||
"@better-auth/oauth-provider": "^1.5.6",
|
||||
"@better-auth/api-key": "^1.6.1",
|
||||
"@better-auth/drizzle-adapter": "^1.6.1",
|
||||
"@better-auth/infra": "^0.1.14",
|
||||
"@better-auth/oauth-provider": "^1.6.1",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource-variable/ibm-plex-sans": "^5.2.8",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lingui/core": "^5.9.4",
|
||||
"@lingui/react": "^5.9.4",
|
||||
"@lingui/core": "^5.9.5",
|
||||
"@lingui/react": "^5.9.5",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@monaco-editor/react": "4.8.0-rc.3",
|
||||
"@orpc/client": "^1.13.13",
|
||||
@@ -68,24 +68,24 @@
|
||||
"@tanstack/react-router-ssr-query": "^1.166.10",
|
||||
"@tanstack/react-start": "^1.167.16",
|
||||
"@tanstack/zod-adapter": "^1.166.9",
|
||||
"@tiptap/extension-highlight": "^3.22.2",
|
||||
"@tiptap/extension-table": "^3.22.2",
|
||||
"@tiptap/extension-text-align": "^3.22.2",
|
||||
"@tiptap/pm": "^3.22.2",
|
||||
"@tiptap/react": "^3.22.2",
|
||||
"@tiptap/starter-kit": "^3.22.2",
|
||||
"@uiw/color-convert": "^2.9.6",
|
||||
"@uiw/react-color-colorful": "^2.9.6",
|
||||
"ai": "^6.0.146",
|
||||
"ai-sdk-ollama": "^3.8.2",
|
||||
"@tiptap/extension-highlight": "^3.22.3",
|
||||
"@tiptap/extension-table": "^3.22.3",
|
||||
"@tiptap/extension-text-align": "^3.22.3",
|
||||
"@tiptap/pm": "^3.22.3",
|
||||
"@tiptap/react": "^3.22.3",
|
||||
"@tiptap/starter-kit": "^3.22.3",
|
||||
"@uiw/color-convert": "^2.10.1",
|
||||
"@uiw/react-color-colorful": "^2.10.1",
|
||||
"ai": "^6.0.154",
|
||||
"ai-sdk-ollama": "^3.8.3",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "^1.5.6",
|
||||
"better-auth": "^1.6.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"docx": "^9.6.1",
|
||||
"dompurify": "^3.3.3",
|
||||
"drizzle-orm": "1.0.0-beta.20",
|
||||
"drizzle-orm": "1.0.0-beta.21",
|
||||
"drizzle-zod": "1.0.0-beta.14-a36c63d",
|
||||
"es-toolkit": "^1.45.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -96,18 +96,18 @@
|
||||
"jsonrepair": "^3.13.3",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"motion": "^12.38.0",
|
||||
"nodemailer": "^8.0.4",
|
||||
"nodemailer": "^8.0.5",
|
||||
"pg": "^8.20.0",
|
||||
"puppeteer-core": "^24.40.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-hook-form": "^7.72.1",
|
||||
"react-hotkeys-hook": "^5.2.4",
|
||||
"react-resizable-panels": "^4.9.0",
|
||||
"react-window": "^2.2.7",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"shadcn": "^4.1.2",
|
||||
"react-zoom-pan-pinch": "^4.0.3",
|
||||
"shadcn": "^4.2.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sonner": "^2.0.7",
|
||||
"srvx": "^0.11.15",
|
||||
@@ -125,9 +125,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.9.4",
|
||||
"@lingui/cli": "^5.9.4",
|
||||
"@lingui/vite-plugin": "^5.9.4",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.9.5",
|
||||
"@lingui/cli": "^5.9.5",
|
||||
"@lingui/vite-plugin": "^5.9.5",
|
||||
"@rolldown/plugin-babel": "^0.2.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -138,22 +138,22 @@
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260404.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260408.1",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^4.1.2",
|
||||
"@vitest/coverage-v8": "^4.1.3",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"drizzle-kit": "1.0.0-beta.20",
|
||||
"drizzle-kit": "1.0.0-beta.21",
|
||||
"happy-dom": "^20.8.9",
|
||||
"jose": "^6.2.2",
|
||||
"knip": "^6.3.0",
|
||||
"knip": "^6.3.1",
|
||||
"nitro": "3.0.260311-beta",
|
||||
"node-addon-api": "^8.7.0",
|
||||
"node-gyp": "^12.2.0",
|
||||
"npm-check-updates": "^20.0.0",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.15",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.16",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plus": "latest",
|
||||
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.15"
|
||||
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.16"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
|
||||
"pnpm": {
|
||||
@@ -175,6 +175,7 @@
|
||||
"bcrypt",
|
||||
"esbuild",
|
||||
"msw",
|
||||
"prisma",
|
||||
"sharp"
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+1323
-1342
File diff suppressed because it is too large
Load Diff
@@ -169,7 +169,6 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
|
||||
if (!isAIEnabled)
|
||||
throw new Error(t`This feature requires AI Integration to be enabled. Please enable it in the settings.`);
|
||||
|
||||
// const arrayBuffer = await values.file.arrayBuffer();
|
||||
const base64 = await fileToBase64(values.file);
|
||||
|
||||
const mediaType =
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ModelMessage } from "ai";
|
||||
|
||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
@@ -248,30 +250,32 @@ type ParsePdfInput = z.infer<typeof aiCredentialsSchema> & {
|
||||
file: z.infer<typeof fileInputSchema>;
|
||||
};
|
||||
|
||||
type BuildResumeParsingMessagesInput = {
|
||||
systemPrompt: string;
|
||||
userPrompt: string;
|
||||
file: z.infer<typeof fileInputSchema>;
|
||||
mediaType: string;
|
||||
};
|
||||
|
||||
function buildResumeParsingMessages({
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
file,
|
||||
mediaType,
|
||||
}: {
|
||||
systemPrompt: string;
|
||||
userPrompt: string;
|
||||
file: z.infer<typeof fileInputSchema>;
|
||||
mediaType: string;
|
||||
}) {
|
||||
}: BuildResumeParsingMessagesInput): ModelMessage[] {
|
||||
return [
|
||||
{
|
||||
role: "system" as const,
|
||||
role: "system",
|
||||
content:
|
||||
systemPrompt +
|
||||
"\n\nIMPORTANT: You must return ONLY raw valid JSON. Do not return markdown, do not return explanations. Just the JSON object. Use the following JSON as a template and fill in the extracted values. For arrays, you MUST use the exact key names shown in the template (e.g. use 'description' instead of 'summary', 'website' instead of 'url'):\n\n" +
|
||||
JSON.stringify(aiExtractionTemplate, null, 2),
|
||||
},
|
||||
{
|
||||
role: "user" as const,
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text" as const, text: userPrompt },
|
||||
{ type: "file" as const, data: file.data, mediaType, filename: file.name },
|
||||
{ type: "text", text: userPrompt },
|
||||
{ type: "file", data: file.data, mediaType, filename: file.name },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -47,6 +47,7 @@ import { Route as ApiOpenapiSplatRouteImport } from "./routes/api/openapi.$";
|
||||
import { Route as ApiAuthSplatRouteImport } from "./routes/api/auth.$";
|
||||
import { Route as DotwellKnownOauthProtectedResourceSplatRouteImport } from "./routes/[.]well-known/oauth-protected-resource.$";
|
||||
import { Route as DotwellKnownOauthAuthorizationServerSplatRouteImport } from "./routes/[.]well-known/oauth-authorization-server.$";
|
||||
import { Route as DotwellKnownMcpServerCardDotjsonRouteImport } from "./routes/[.]well-known/mcp/server-card[.]json";
|
||||
import { Route as DashboardSettingsAuthenticationIndexRouteImport } from "./routes/dashboard/settings/authentication/index";
|
||||
|
||||
const SchemaDotjsonRoute = SchemaDotjsonRouteImport.update({
|
||||
@@ -248,6 +249,12 @@ const DotwellKnownOauthAuthorizationServerSplatRoute =
|
||||
path: "/$",
|
||||
getParentRoute: () => DotwellKnownOauthAuthorizationServerRoute,
|
||||
} as any);
|
||||
const DotwellKnownMcpServerCardDotjsonRoute =
|
||||
DotwellKnownMcpServerCardDotjsonRouteImport.update({
|
||||
id: "/.well-known/mcp/server-card.json",
|
||||
path: "/.well-known/mcp/server-card.json",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const DashboardSettingsAuthenticationIndexRoute =
|
||||
DashboardSettingsAuthenticationIndexRouteImport.update({
|
||||
id: "/settings/authentication/",
|
||||
@@ -278,6 +285,7 @@ export interface FileRoutesByFullPath {
|
||||
"/auth/": typeof AuthIndexRoute;
|
||||
"/dashboard/": typeof DashboardIndexRoute;
|
||||
"/mcp/": typeof McpIndexRoute;
|
||||
"/.well-known/mcp/server-card.json": typeof DotwellKnownMcpServerCardDotjsonRoute;
|
||||
"/.well-known/oauth-authorization-server/$": typeof DotwellKnownOauthAuthorizationServerSplatRoute;
|
||||
"/.well-known/oauth-protected-resource/$": typeof DotwellKnownOauthProtectedResourceSplatRoute;
|
||||
"/api/auth/$": typeof ApiAuthSplatRoute;
|
||||
@@ -315,6 +323,7 @@ export interface FileRoutesByTo {
|
||||
"/auth": typeof AuthIndexRoute;
|
||||
"/dashboard": typeof DashboardIndexRoute;
|
||||
"/mcp": typeof McpIndexRoute;
|
||||
"/.well-known/mcp/server-card.json": typeof DotwellKnownMcpServerCardDotjsonRoute;
|
||||
"/.well-known/oauth-authorization-server/$": typeof DotwellKnownOauthAuthorizationServerSplatRoute;
|
||||
"/.well-known/oauth-protected-resource/$": typeof DotwellKnownOauthProtectedResourceSplatRoute;
|
||||
"/api/auth/$": typeof ApiAuthSplatRoute;
|
||||
@@ -357,6 +366,7 @@ export interface FileRoutesById {
|
||||
"/auth/": typeof AuthIndexRoute;
|
||||
"/dashboard/": typeof DashboardIndexRoute;
|
||||
"/mcp/": typeof McpIndexRoute;
|
||||
"/.well-known/mcp/server-card.json": typeof DotwellKnownMcpServerCardDotjsonRoute;
|
||||
"/.well-known/oauth-authorization-server/$": typeof DotwellKnownOauthAuthorizationServerSplatRoute;
|
||||
"/.well-known/oauth-protected-resource/$": typeof DotwellKnownOauthProtectedResourceSplatRoute;
|
||||
"/api/auth/$": typeof ApiAuthSplatRoute;
|
||||
@@ -399,6 +409,7 @@ export interface FileRouteTypes {
|
||||
| "/auth/"
|
||||
| "/dashboard/"
|
||||
| "/mcp/"
|
||||
| "/.well-known/mcp/server-card.json"
|
||||
| "/.well-known/oauth-authorization-server/$"
|
||||
| "/.well-known/oauth-protected-resource/$"
|
||||
| "/api/auth/$"
|
||||
@@ -436,6 +447,7 @@ export interface FileRouteTypes {
|
||||
| "/auth"
|
||||
| "/dashboard"
|
||||
| "/mcp"
|
||||
| "/.well-known/mcp/server-card.json"
|
||||
| "/.well-known/oauth-authorization-server/$"
|
||||
| "/.well-known/oauth-protected-resource/$"
|
||||
| "/api/auth/$"
|
||||
@@ -477,6 +489,7 @@ export interface FileRouteTypes {
|
||||
| "/auth/"
|
||||
| "/dashboard/"
|
||||
| "/mcp/"
|
||||
| "/.well-known/mcp/server-card.json"
|
||||
| "/.well-known/oauth-authorization-server/$"
|
||||
| "/.well-known/oauth-protected-resource/$"
|
||||
| "/api/auth/$"
|
||||
@@ -508,6 +521,7 @@ export interface RootRouteChildren {
|
||||
ApiHealthRoute: typeof ApiHealthRoute;
|
||||
PrinterResumeIdRoute: typeof PrinterResumeIdRoute;
|
||||
McpIndexRoute: typeof McpIndexRoute;
|
||||
DotwellKnownMcpServerCardDotjsonRoute: typeof DotwellKnownMcpServerCardDotjsonRoute;
|
||||
ApiAuthSplatRoute: typeof ApiAuthSplatRoute;
|
||||
ApiOpenapiSplatRoute: typeof ApiOpenapiSplatRoute;
|
||||
ApiRpcSplatRoute: typeof ApiRpcSplatRoute;
|
||||
@@ -782,6 +796,13 @@ declare module "@tanstack/react-router" {
|
||||
preLoaderRoute: typeof DotwellKnownOauthAuthorizationServerSplatRouteImport;
|
||||
parentRoute: typeof DotwellKnownOauthAuthorizationServerRoute;
|
||||
};
|
||||
"/.well-known/mcp/server-card.json": {
|
||||
id: "/.well-known/mcp/server-card.json";
|
||||
path: "/.well-known/mcp/server-card.json";
|
||||
fullPath: "/.well-known/mcp/server-card.json";
|
||||
preLoaderRoute: typeof DotwellKnownMcpServerCardDotjsonRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/dashboard/settings/authentication/": {
|
||||
id: "/dashboard/settings/authentication/";
|
||||
path: "/settings/authentication";
|
||||
@@ -919,6 +940,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
ApiHealthRoute: ApiHealthRoute,
|
||||
PrinterResumeIdRoute: PrinterResumeIdRoute,
|
||||
McpIndexRoute: McpIndexRoute,
|
||||
DotwellKnownMcpServerCardDotjsonRoute: DotwellKnownMcpServerCardDotjsonRoute,
|
||||
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
||||
ApiOpenapiSplatRoute: ApiOpenapiSplatRoute,
|
||||
ApiRpcSplatRoute: ApiRpcSplatRoute,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { buildMcpServerCard } from "@/routes/mcp/-helpers/mcp-server-card";
|
||||
|
||||
/** Well-known MCP server card (SEP-1649) for static metadata when clients cannot complete a full capability scan. */
|
||||
export const Route = createFileRoute("/.well-known/mcp/server-card.json")({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async () =>
|
||||
Response.json(buildMcpServerCard(__APP_VERSION__), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=60, stale-while-revalidate=120",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
||||
import z from "zod";
|
||||
|
||||
import { jsonPatchOperationSchema } from "@/utils/resume/patch";
|
||||
|
||||
import { MCP_TOOL_NAME as T } from "./tools";
|
||||
|
||||
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`.
|
||||
*/
|
||||
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."),
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: T.getResume,
|
||||
title: "Get 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 })),
|
||||
},
|
||||
{
|
||||
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."),
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
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"),
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: T.patchResume,
|
||||
title: "Patch Resume",
|
||||
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: z
|
||||
.array(jsonPatchOperationSchema)
|
||||
.min(1)
|
||||
.describe("Array of JSON Patch (RFC 6902) operations to apply"),
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
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 })),
|
||||
},
|
||||
{
|
||||
name: T.lockResume,
|
||||
title: "Lock Resume",
|
||||
description: [
|
||||
"Lock a resume to prevent any modifications.",
|
||||
"",
|
||||
`When locked, a resume cannot be edited (${T.patchResume}), updated, or deleted.`,
|
||||
`Use \`${T.unlockResume}\` to re-enable editing.`,
|
||||
].join("\n"),
|
||||
inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })),
|
||||
},
|
||||
{
|
||||
name: T.unlockResume,
|
||||
title: "Unlock Resume",
|
||||
description: "Unlock a previously locked resume, re-enabling edits, patches, and deletion.",
|
||||
inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })),
|
||||
},
|
||||
{
|
||||
name: T.exportResumePdf,
|
||||
title: "Export Resume as PDF",
|
||||
description: [
|
||||
"Generate a PDF from the specified resume and return a download URL.",
|
||||
"",
|
||||
"The PDF is rendered using the resume's current template, layout, and design settings,",
|
||||
"then uploaded to storage. The returned URL can be shared or downloaded directly.",
|
||||
].join("\n"),
|
||||
inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })),
|
||||
},
|
||||
{
|
||||
name: T.getResumeScreenshot,
|
||||
title: "Get Resume Screenshot",
|
||||
description: [
|
||||
"Get a visual preview of the resume's first page as a WebP image URL.",
|
||||
"",
|
||||
"Screenshots are cached for up to 6 hours and regenerated automatically when the resume",
|
||||
"is updated. Returns null if the printer service is unavailable.",
|
||||
].join("\n"),
|
||||
inputSchema: toJsonSchemaCompat(z.object({ id: resumeId })),
|
||||
},
|
||||
{
|
||||
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 })),
|
||||
},
|
||||
];
|
||||
|
||||
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: "tailor_resume",
|
||||
title: "Tailor Resume for a Job",
|
||||
description:
|
||||
"Adapt a resume to match a specific job description by adjusting keywords, content, and structure for ATS optimization.",
|
||||
arguments: [
|
||||
{ name: "id", description: "Resume ID.", required: true },
|
||||
{
|
||||
name: "job_description",
|
||||
description: "The full job description or posting to tailor the resume for.",
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
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 get_resume.",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
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"],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,11 +2,15 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
import z from "zod";
|
||||
|
||||
import { MCP_TOOL_NAME as T } from "./tools";
|
||||
|
||||
// ── Shared prompt helpers ────────────────────────────────────────
|
||||
|
||||
const resumeIdArg = z
|
||||
.string()
|
||||
.describe("The ID of the resume. Use `list_resumes` to find IDs, or `create_resume` to create a new one first.");
|
||||
.describe(
|
||||
`The ID of the resume. Use \`${T.listResumes}\` to find IDs, or \`${T.createResume}\` to create a new one first.`,
|
||||
);
|
||||
|
||||
/** Embeds the resume data and JSON schema as context messages. */
|
||||
function resumeContext(id: string) {
|
||||
@@ -27,7 +31,7 @@ function resumeContext(id: string) {
|
||||
content: {
|
||||
type: "resource" as const,
|
||||
resource: {
|
||||
uri: "resume://schema",
|
||||
uri: "resume://_meta/schema",
|
||||
mimeType: "application/json",
|
||||
text: "Resume data JSON Schema — use this to understand valid paths and types for JSON Patch operations",
|
||||
},
|
||||
@@ -39,7 +43,7 @@ function resumeContext(id: string) {
|
||||
const PATCH_REFERENCE = [
|
||||
"## JSON Patch Reference",
|
||||
"",
|
||||
"Use the `patch_resume` tool for every change. Common operations:",
|
||||
`Use the \`${T.patchResume}\` tool for every change. Common operations:`,
|
||||
"",
|
||||
"| Action | Operation |",
|
||||
"|--------|-----------|",
|
||||
@@ -57,7 +61,7 @@ const PATCH_REFERENCE = [
|
||||
"- New item IDs must be valid UUIDs (format: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`).",
|
||||
"- HTML content fields (`description`, `summary.content`) must use valid HTML: `<p>`, `<ul>`/`<li>`, `<strong>`, `<em>`.",
|
||||
"- Every `website` field is an object: `{ url: string, label: string }`.",
|
||||
"- Use `get_resume_screenshot` after making visual changes (template, colors, layout) to verify the result.",
|
||||
`- Use \`${T.getResumeScreenshot}\` after making visual changes (template, colors, layout) to verify the result.`,
|
||||
].join("\n");
|
||||
|
||||
// ── Prompt Registration ──────────────────────────────────────────
|
||||
@@ -137,7 +141,7 @@ export function registerPrompts(server: McpServer) {
|
||||
"1. Start with an **overall assessment** (strengths + key areas to improve).",
|
||||
"2. Work through improvements **one section at a time**.",
|
||||
"3. For each suggestion, explain the **rationale** and show the before/after.",
|
||||
"4. Wait for my **approval** before applying changes via `patch_resume`.",
|
||||
`4. Wait for my **approval** before applying changes via \`${T.patchResume}\`.`,
|
||||
"5. Do NOT fabricate information — suggest improvements based on what exists, ask me for missing details.",
|
||||
"",
|
||||
PATCH_REFERENCE,
|
||||
@@ -183,7 +187,7 @@ export function registerPrompts(server: McpServer) {
|
||||
"5. **Section Ordering** — Reorder to put the most relevant sections first.",
|
||||
"6. **De-emphasis** — Suggest hiding sections that don't add value for this specific role.",
|
||||
"",
|
||||
"Present a summary of all proposed changes before applying. Apply via `patch_resume` only after I approve.",
|
||||
`Present a summary of all proposed changes before applying. Apply via \`${T.patchResume}\` only after I approve.`,
|
||||
"Do NOT fabricate experience or skills I don't have — only reframe existing content and ask me about gaps.",
|
||||
"",
|
||||
PATCH_REFERENCE,
|
||||
@@ -232,7 +236,7 @@ export function registerPrompts(server: McpServer) {
|
||||
"3. **Top 5 Recommendations** — Prioritized by impact, with specific actionable suggestions.",
|
||||
"4. **Strengths** — What's working well and should be preserved.",
|
||||
"",
|
||||
"This is a **read-only review**. Do NOT call `patch_resume` or make any changes.",
|
||||
`This is a **read-only review**. Do NOT call \`${T.patchResume}\` or make any changes.`,
|
||||
"Format the review as a clear, structured report.",
|
||||
].join("\n"),
|
||||
},
|
||||
|
||||
@@ -5,34 +5,12 @@ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { client } from "@/integrations/orpc/client";
|
||||
import schemaJSON from "@/schema/schema.json";
|
||||
|
||||
import { MCP_TOOL_NAME as T } from "./tools";
|
||||
|
||||
export function registerResources(server: McpServer) {
|
||||
// ── Resource: resume://{id} ──────────────────────────────────
|
||||
// Dynamic resource that exposes each resume's full data as JSON.
|
||||
// Clients can list all available resumes and read individual ones by ID.
|
||||
const resumeTemplate = new ResourceTemplate("resume://{id}", {
|
||||
list: async () => {
|
||||
const resumes = await client.resume.list();
|
||||
|
||||
return {
|
||||
resources: resumes.map(({ id, name, slug, tags, isPublic, isLocked, updatedAt }) => ({
|
||||
name,
|
||||
title: `${name} (${slug})`,
|
||||
uri: `resume://${id}`,
|
||||
mimeType: "application/json" as const,
|
||||
description: [
|
||||
isPublic ? "Public" : "Private",
|
||||
isLocked ? "Locked" : null,
|
||||
tags.length > 0 ? `Tags: ${tags.join(", ")}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" | "),
|
||||
annotations: {
|
||||
lastModified: updatedAt.toISOString(),
|
||||
},
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
// Template resource: read resume JSON by ID. Discovery is via list tool (tools), not resources/list.
|
||||
const resumeTemplate = new ResourceTemplate("resume://{id}", { list: undefined });
|
||||
|
||||
server.registerResource(
|
||||
"resume",
|
||||
@@ -42,8 +20,9 @@ export function registerResources(server: McpServer) {
|
||||
mimeType: "application/json",
|
||||
description: [
|
||||
"Full resume data as JSON, including basics, summary, sections, custom sections, and metadata.",
|
||||
"Use resume://{id} with an ID from list_resumes.",
|
||||
"This is also embedded as context in all MCP prompts (build_resume, improve_resume, etc.).",
|
||||
`Discover resume IDs with the \`${T.listResumes}\` tool, then read \`resume://{id}\` or use \`${T.getResume}\`.`,
|
||||
"Appears in `resources/templates/list`; not enumerated in `resources/list`.",
|
||||
"Embedded as context in MCP prompts (build_resume, improve_resume, etc.).",
|
||||
].join(" "),
|
||||
},
|
||||
async (uri: URL) => {
|
||||
@@ -64,13 +43,13 @@ export function registerResources(server: McpServer) {
|
||||
},
|
||||
);
|
||||
|
||||
// ── Resource: resume://schema ────────────────────────────────
|
||||
// ── Resource: resume://_meta/schema ───────────────────────────
|
||||
// Static resource containing the JSON Schema for resume data.
|
||||
// LLMs should reference this when generating JSON Patch operations
|
||||
// to ensure paths and values conform to the expected structure.
|
||||
server.registerResource(
|
||||
"resume-schema",
|
||||
"resume://schema",
|
||||
"resume://_meta/schema",
|
||||
{
|
||||
title: "Resume Data JSON Schema",
|
||||
mimeType: "application/json",
|
||||
|
||||
@@ -8,6 +8,21 @@ import { jsonPatchOperationSchema } from "@/utils/resume/patch";
|
||||
|
||||
type PatchOperation = z.infer<typeof jsonPatchOperationSchema>;
|
||||
|
||||
/** 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;
|
||||
|
||||
// ── Shared Helpers ──────────────────────────────────────────────
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
@@ -16,13 +31,15 @@ function errorMessage(error: unknown): string {
|
||||
|
||||
function errorHint(error: unknown): string {
|
||||
const msg = errorMessage(error);
|
||||
const { unlockResume, listResumes, getResume } = MCP_TOOL_NAME;
|
||||
if (msg.includes("slug already exists")) return "\n\nHint: The slug is already in use. Try a different one.";
|
||||
if (msg.includes("locked")) return "\n\nHint: This resume is locked. Use `unlock_resume` first.";
|
||||
if (msg.includes("locked")) return `\n\nHint: This resume is locked. Use \`${unlockResume}\` first.`;
|
||||
if (msg.includes("404") || msg.includes("not found"))
|
||||
return "\n\nHint: Resume not found. Use `list_resumes` to find valid IDs.";
|
||||
return `\n\nHint: Resume not found. Use \`${listResumes}\` to find valid IDs.`;
|
||||
if (msg.includes("400"))
|
||||
return "\n\nHint: Invalid request. Check the input parameters or use `get_resume` to inspect the resume structure.";
|
||||
if (msg.includes("403")) return "\n\nHint: Permission denied. The resume may be locked — use `unlock_resume` first.";
|
||||
return `\n\nHint: Invalid request. Check the input parameters or use \`${getResume}\` to inspect the resume structure.`;
|
||||
if (msg.includes("403"))
|
||||
return `\n\nHint: Permission denied. The resume may be locked — use \`${unlockResume}\` first.`;
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -50,23 +67,26 @@ function text(value: string): CallToolResult {
|
||||
|
||||
// ── Shared Zod Fragments ────────────────────────────────────────
|
||||
|
||||
const resumeIdSchema = z.string().min(1).describe("Resume ID. Use `list_resumes` to find valid IDs.");
|
||||
const T = MCP_TOOL_NAME;
|
||||
|
||||
const resumeIdSchema = z.string().min(1).describe(`Resume ID. Use \`${T.listResumes}\` to find valid IDs.`);
|
||||
|
||||
// ── Tool Registration ───────────────────────────────────────────
|
||||
|
||||
export function registerTools(server: McpServer) {
|
||||
// ── List Resumes ──────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"list_resumes",
|
||||
T.listResumes,
|
||||
{
|
||||
title: "List Resumes",
|
||||
description: [
|
||||
"List all resumes for the authenticated user.",
|
||||
"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.",
|
||||
"",
|
||||
"Use this tool first to discover resume IDs before calling other tools.",
|
||||
`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: z.object({
|
||||
@@ -95,7 +115,7 @@ export function registerTools(server: McpServer) {
|
||||
async ({ tags, sort }: { tags: string[]; sort: "lastUpdatedAt" | "createdAt" | "name" }) => {
|
||||
const resumes = await client.resume.list({ tags, sort });
|
||||
|
||||
if (resumes.length === 0) return text("No resumes found. Use `create_resume` to create one.");
|
||||
if (resumes.length === 0) return text(`No resumes found. Use \`${T.createResume}\` to create one.`);
|
||||
|
||||
return text(JSON.stringify(resumes, null, 2));
|
||||
},
|
||||
@@ -104,7 +124,7 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Get Resume ────────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"get_resume",
|
||||
T.getResume,
|
||||
{
|
||||
title: "Get Resume",
|
||||
description: [
|
||||
@@ -114,8 +134,8 @@ export function registerTools(server: McpServer) {
|
||||
"location, website), summary, picture settings, all sections (experience, education, skills,",
|
||||
"projects, etc.), custom sections, and metadata (template, layout, typography, colors).",
|
||||
"",
|
||||
"Use `list_resumes` first to find valid IDs.",
|
||||
"The `resume://schema` resource describes the full data structure.",
|
||||
`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: z.object({ id: resumeIdSchema }),
|
||||
annotations: {
|
||||
@@ -134,7 +154,7 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Create Resume ─────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"create_resume",
|
||||
T.createResume,
|
||||
{
|
||||
title: "Create Resume",
|
||||
description: [
|
||||
@@ -142,7 +162,7 @@ export function registerTools(server: McpServer) {
|
||||
"",
|
||||
"Returns the ID of the newly created resume.",
|
||||
"Set `withSampleData` to true to pre-fill with example content (useful for testing).",
|
||||
"After creating, use `get_resume` to view or `patch_resume` to populate it.",
|
||||
`After creating, use \`${T.getResume}\` to view or \`${T.patchResume}\` to populate it.`,
|
||||
].join("\n"),
|
||||
inputSchema: z.object({
|
||||
name: z.string().min(1).max(64).describe("Display name for the resume (e.g. 'Software Engineer 2026')"),
|
||||
@@ -181,7 +201,7 @@ export function registerTools(server: McpServer) {
|
||||
const id = await client.resume.create({ name, slug, tags, withSampleData });
|
||||
|
||||
return text(
|
||||
`Created resume "${name}" (ID: ${id}) with slug "${slug}".${withSampleData ? " Pre-filled with sample data." : ""}\n\nNext steps: Use \`get_resume\` to view it, or \`patch_resume\` to start editing.`,
|
||||
`Created resume "${name}" (ID: ${id}) with slug "${slug}".${withSampleData ? " Pre-filled with sample data." : ""}\n\nNext steps: Use \`${T.getResume}\` to view it, or \`${T.patchResume}\` to start editing.`,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -189,7 +209,7 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Duplicate Resume ──────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"duplicate_resume",
|
||||
T.duplicateResume,
|
||||
{
|
||||
title: "Duplicate Resume",
|
||||
description: [
|
||||
@@ -218,7 +238,7 @@ export function registerTools(server: McpServer) {
|
||||
const newId = await client.resume.duplicate({ id, name, slug, tags });
|
||||
|
||||
return text(
|
||||
`Duplicated resume as "${name}" (ID: ${newId}) with slug "${slug}".\n\nNext steps: Use \`get_resume\` to view it, or \`patch_resume\` to customize.`,
|
||||
`Duplicated resume as "${name}" (ID: ${newId}) with slug "${slug}".\n\nNext steps: Use \`${T.getResume}\` to view it, or \`${T.patchResume}\` to customize.`,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -226,14 +246,14 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Patch Resume ──────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"patch_resume",
|
||||
T.patchResume,
|
||||
{
|
||||
title: "Patch Resume",
|
||||
description: [
|
||||
"Apply JSON Patch (RFC 6902) operations to partially update a resume's data.",
|
||||
"",
|
||||
"This is the primary way to edit resume content. Use `get_resume` first to inspect the",
|
||||
"current structure, and `resume://schema` to understand valid paths and types.",
|
||||
`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.",
|
||||
"",
|
||||
@@ -250,7 +270,7 @@ export function registerTools(server: McpServer) {
|
||||
"",
|
||||
"Important: HTML content fields (description, summary.content) must use valid HTML.",
|
||||
"New items must include a valid UUID as `id` and `hidden: false`.",
|
||||
"Locked resumes cannot be patched — use `unlock_resume` first.",
|
||||
`Locked resumes cannot be patched — use \`${T.unlockResume}\` first.`,
|
||||
].join("\n"),
|
||||
inputSchema: z.object({
|
||||
id: resumeIdSchema,
|
||||
@@ -276,14 +296,14 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Delete Resume ─────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"delete_resume",
|
||||
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 `unlock_resume` first.",
|
||||
"Consider using `duplicate_resume` to create a backup before deleting.",
|
||||
`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: z.object({ id: resumeIdSchema }),
|
||||
annotations: {
|
||||
@@ -302,15 +322,15 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Lock Resume ───────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"lock_resume",
|
||||
T.lockResume,
|
||||
{
|
||||
title: "Lock Resume",
|
||||
description: [
|
||||
"Lock a resume to prevent any modifications.",
|
||||
"",
|
||||
"When locked, a resume cannot be edited (patch_resume), updated, or deleted.",
|
||||
`When locked, a resume cannot be edited (${T.patchResume}), updated, or deleted.`,
|
||||
"Useful for protecting finalized resumes from accidental changes.",
|
||||
"Use `unlock_resume` to re-enable editing.",
|
||||
`Use \`${T.unlockResume}\` to re-enable editing.`,
|
||||
].join("\n"),
|
||||
inputSchema: z.object({ id: resumeIdSchema }),
|
||||
annotations: {
|
||||
@@ -329,7 +349,7 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Unlock Resume ─────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"unlock_resume",
|
||||
T.unlockResume,
|
||||
{
|
||||
title: "Unlock Resume",
|
||||
description: "Unlock a previously locked resume, re-enabling edits, patches, and deletion.",
|
||||
@@ -350,7 +370,7 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Export Resume as PDF ──────────────────────────────────────
|
||||
server.registerTool(
|
||||
"export_resume_pdf",
|
||||
T.exportResumePdf,
|
||||
{
|
||||
title: "Export Resume as PDF",
|
||||
description: [
|
||||
@@ -377,7 +397,7 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Get Resume Screenshot ────────────────────────────────────
|
||||
server.registerTool(
|
||||
"get_resume_screenshot",
|
||||
T.getResumeScreenshot,
|
||||
{
|
||||
title: "Get Resume Screenshot",
|
||||
description: [
|
||||
@@ -408,7 +428,7 @@ export function registerTools(server: McpServer) {
|
||||
|
||||
// ── Get Resume Statistics ────────────────────────────────────
|
||||
server.registerTool(
|
||||
"get_resume_statistics",
|
||||
T.getResumeStatistics,
|
||||
{
|
||||
title: "Get Resume Statistics",
|
||||
description: [
|
||||
|
||||
+31
-20
@@ -9,26 +9,37 @@ import { registerResources } from "./-helpers/resources";
|
||||
import { registerTools } from "./-helpers/tools";
|
||||
|
||||
function createMcpServer() {
|
||||
const server = new McpServer({
|
||||
name: "reactive-resume",
|
||||
version: "1.0.0",
|
||||
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",
|
||||
},
|
||||
{
|
||||
src: "https://rxresu.me/icon/dark.svg",
|
||||
mimeType: "image/svg+xml",
|
||||
theme: "dark",
|
||||
},
|
||||
],
|
||||
});
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: "reactive-resume",
|
||||
version: __APP_VERSION__,
|
||||
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",
|
||||
},
|
||||
{
|
||||
src: "https://rxresu.me/icon/dark.svg",
|
||||
mimeType: "image/svg+xml",
|
||||
theme: "dark",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
instructions: [
|
||||
"You are connected to Reactive Resume over MCP.",
|
||||
"Authenticate with OAuth (recommended) or an API key (`x-api-key`).",
|
||||
"Discover resume IDs with `reactive_resume.list_resumes` (not `resources/list`).",
|
||||
"Read schema at `resume://_meta/schema`; read resume JSON via `resume://{id}` or `reactive_resume.get_resume`.",
|
||||
"Apply edits with JSON Patch through `reactive_resume.patch_resume`.",
|
||||
].join(" "),
|
||||
},
|
||||
);
|
||||
|
||||
registerResources(server);
|
||||
registerTools(server);
|
||||
|
||||
Reference in New Issue
Block a user