building a better mcp server

This commit is contained in:
Amruth Pillai
2026-02-09 23:09:14 +01:00
parent 8e060890d3
commit 833b8343ac
7 changed files with 945 additions and 509 deletions
+242
View File
@@ -0,0 +1,242 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import z from "zod";
// ── 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.");
/** Embeds the resume data and JSON schema as context messages. */
function resumeContext(id: string) {
return [
{
role: "user" as const,
content: {
type: "resource" as const,
resource: {
uri: `resume://${id}`,
mimeType: "application/json",
text: "Current resume data",
},
},
},
{
role: "user" as const,
content: {
type: "resource" as const,
resource: {
uri: "resume://schema",
mimeType: "application/json",
text: "Resume data JSON Schema — use this to understand valid paths and types for JSON Patch operations",
},
},
},
];
}
const PATCH_REFERENCE = [
"## JSON Patch Reference",
"",
"Use the `patch_resume` tool for every change. Common operations:",
"",
"| Action | Operation |",
"|--------|-----------|",
'| Change name | `{ "op": "replace", "path": "/basics/name", "value": "Jane Doe" }` |',
'| Update headline | `{ "op": "replace", "path": "/basics/headline", "value": "Senior Engineer" }` |',
'| Replace summary | `{ "op": "replace", "path": "/summary/content", "value": "<p>Experienced...</p>" }` |',
'| Add experience | `{ "op": "add", "path": "/sections/experience/items/-", "value": { ...full item } }` |',
'| Remove skill at index 2 | `{ "op": "remove", "path": "/sections/skills/items/2" }` |',
'| Update specific field | `{ "op": "replace", "path": "/sections/experience/items/0/company", "value": "New Corp" }` |',
'| Change template | `{ "op": "replace", "path": "/metadata/template", "value": "bronzor" }` |',
'| Change primary color | `{ "op": "replace", "path": "/metadata/design/colors/primary", "value": "rgba(37, 99, 235, 1)" }` |',
'| Hide a section | `{ "op": "replace", "path": "/sections/interests/hidden", "value": true }` |',
"",
"Rules:",
"- 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.",
].join("\n");
// ── Prompt Registration ──────────────────────────────────────────
export function registerPrompts(server: McpServer) {
// ── Build Resume ─────────────────────────────────────────────
server.registerPrompt(
"build_resume",
{
title: "Build Resume",
description: "Guide the user step-by-step through building a resume from scratch, section by section.",
argsSchema: { id: resumeIdArg },
},
async ({ id }) => ({
messages: [
...resumeContext(id),
{
role: "user" as const,
content: {
type: "text" as const,
text: [
"You are an expert resume writer. Help me build my resume step by step.",
"",
"## Process",
"",
"1. **Basics** — Ask for: full name, headline/job title, email, phone, location, website.",
"2. **Summary** — Help me write a compelling 2-3 sentence professional summary.",
"3. **Experience** — Walk through each role: company, position, period, key accomplishments.",
"4. **Education** — Degree, school, graduation date, relevant coursework/honors.",
"5. **Skills** — Categorize technical and soft skills with proficiency levels.",
"6. **Other sections** — Projects, certifications, languages, volunteer work, etc.",
"7. **Design** — Offer to adjust template, typography, and color scheme.",
"",
"For each section, ask targeted questions, draft the content, and wait for my approval before applying.",
"Do NOT fabricate any information — only use what I provide or explicitly ask you to generate.",
"",
PATCH_REFERENCE,
"",
"The resume data and schema are attached above. Let's begin!",
].join("\n"),
},
},
],
}),
);
// ── Improve Resume ───────────────────────────────────────────
server.registerPrompt(
"improve_resume",
{
title: "Improve Resume",
description: "Review resume content and suggest concrete improvements to wording, impact, and structure.",
argsSchema: { id: resumeIdArg },
},
async ({ id }) => ({
messages: [
...resumeContext(id),
{
role: "user" as const,
content: {
type: "text" as const,
text: [
"You are an expert resume writer and career coach. Review my resume and help me improve it.",
"",
"## Analysis Framework",
"",
"Go through each section and identify:",
"- **Weak bullet points** — lacking metrics, impact, or specificity",
"- **Passive voice** — replace with strong action verbs (Led, Built, Designed, Increased...)",
"- **Vague descriptions** — make concrete with specific technologies, team sizes, outcomes",
"- **Missing quantification** — add numbers, percentages, dollar amounts where possible",
"- **Structural issues** — inconsistent formatting, poor section ordering, missing sections",
"- **Redundancies** — repetitive content that dilutes impact",
"",
"## Process",
"",
"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`.",
"5. Do NOT fabricate information — suggest improvements based on what exists, ask me for missing details.",
"",
PATCH_REFERENCE,
].join("\n"),
},
},
],
}),
);
// ── Tailor Resume for a Job ──────────────────────────────────
server.registerPrompt(
"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.",
argsSchema: {
id: resumeIdArg,
job_description: z.string().min(1).describe("The full job description or posting to tailor the resume for."),
},
},
async ({ id, job_description }) => ({
messages: [
...resumeContext(id),
{
role: "user" as const,
content: {
type: "text" as const,
text: [
"You are an expert resume writer specializing in ATS optimization and job targeting.",
"",
"## Target Job Description",
"",
job_description,
"",
"## Process",
"",
"1. **Gap Analysis** — Extract required skills, qualifications, keywords, and technologies from the job description. Compare against my resume and list what's aligned, what's missing, and what's irrelevant.",
"2. **Headline & Summary** — Suggest adjustments to match the target role.",
"3. **Experience** — Reword bullet points to highlight relevant accomplishments using keywords from the JD.",
"4. **Skills** — Update with missing keywords that I actually possess (ask me to confirm).",
"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.",
"Do NOT fabricate experience or skills I don't have — only reframe existing content and ask me about gaps.",
"",
PATCH_REFERENCE,
].join("\n"),
},
},
],
}),
);
// ── Review Resume ────────────────────────────────────────────
server.registerPrompt(
"review_resume",
{
title: "Review Resume",
description:
"Get a structured, professional critique with a scorecard and prioritized recommendations. Read-only — no changes are made.",
argsSchema: { id: resumeIdArg },
},
async ({ id }) => ({
messages: [
...resumeContext(id),
{
role: "user" as const,
content: {
type: "text" as const,
text: [
"You are a professional resume reviewer and career advisor. Provide a thorough critique.",
"",
"## Evaluation Dimensions (score each 1-10)",
"",
"| Dimension | What to evaluate |",
"|-----------|-----------------|",
"| **Completeness** | Are all important sections filled in? Any critical gaps? |",
"| **Impact** | Do bullet points demonstrate results with metrics and outcomes? |",
"| **Clarity** | Is the writing clear, concise, and free of unnecessary jargon? |",
"| **Formatting** | Is the layout consistent? Are sections well-organized? |",
"| **ATS Compatibility** | Will it parse well through Applicant Tracking Systems? |",
"| **Keywords** | Are relevant industry keywords present and naturally integrated? |",
"| **Length** | Is the resume an appropriate length for the experience level? |",
"",
"## Output Format",
"",
"1. **Scorecard** — Score each dimension (1-10) with a brief justification.",
"2. **Overall Score** — Weighted average of all dimensions.",
"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.",
"Format the review as a clear, structured report.",
].join("\n"),
},
},
],
}),
);
}
+92
View File
@@ -0,0 +1,92 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { client } from "@/integrations/orpc/client";
import schemaJSON from "@/schema/schema.json";
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(),
},
})),
};
},
});
server.registerResource(
"resume",
resumeTemplate,
{
title: "Resume Data",
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.).",
].join(" "),
},
async (uri: URL) => {
const id = uri.href.replace(/^resume:\/\//, "");
if (!id) throw new Error("Invalid resume URI — expected format: resume://{id}");
const resume = await client.resume.getById({ id });
return {
contents: [
{
uri: uri.href,
mimeType: "application/json" as const,
text: JSON.stringify(resume.data, null, 2),
},
],
};
},
);
// ── Resource: resume://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",
{
title: "Resume Data JSON Schema",
mimeType: "application/json",
description: [
"The JSON Schema describing the complete resume data structure.",
"Reference this when generating JSON Patch operations to ensure paths and value types are valid.",
"Covers: basics, summary, picture, sections (experience, education, skills, etc.),",
"custom sections, and metadata (template, layout, typography, colors, CSS).",
].join(" "),
},
async (uri: URL) => ({
contents: [
{
uri: uri.href,
mimeType: "application/json" as const,
text: JSON.stringify(schemaJSON, null, 2),
},
],
}),
);
}
+432
View File
@@ -0,0 +1,432 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import z from "zod";
import { client } from "@/integrations/orpc/client";
import { jsonPatchOperationSchema } from "@/utils/resume/patch";
type PatchOperation = z.infer<typeof jsonPatchOperationSchema>;
// ── Shared Helpers ──────────────────────────────────────────────
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function errorHint(error: unknown): string {
const msg = errorMessage(error);
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("404") || msg.includes("not found"))
return "\n\nHint: Resume not found. Use `list_resumes` 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 "";
}
/**
* Wraps an async tool handler with consistent error formatting.
* On success, returns the handler's result directly.
* On failure, returns `{ isError: true, content: [{ type: "text", text }] }` with actionable hints.
*/
function withErrorHandling<T>(label: string, handler: (params: T) => Promise<CallToolResult>) {
return async (params: T): Promise<CallToolResult> => {
try {
return await handler(params);
} catch (error) {
return {
isError: true,
content: [{ type: "text", text: `Error ${label}: ${errorMessage(error)}${errorHint(error)}` }],
};
}
};
}
function text(value: string): CallToolResult {
return { content: [{ type: "text", text: value }] };
}
// ── Shared Zod Fragments ────────────────────────────────────────
const resumeIdSchema = z.string().min(1).describe("Resume ID. Use `list_resumes` to find valid IDs.");
// ── Tool Registration ───────────────────────────────────────────
export function registerTools(server: McpServer) {
// ── List Resumes ──────────────────────────────────────────────
server.registerTool(
"list_resumes",
{
title: "List Resumes",
description: [
"List all resumes for the authenticated user.",
"",
"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.",
"Results can be filtered by tags and sorted by last updated date, creation date, or name.",
].join("\n"),
inputSchema: 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: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
},
withErrorHandling(
"listing resumes",
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.");
return text(JSON.stringify(resumes, null, 2));
},
),
);
// ── Get Resume ────────────────────────────────────────────────
server.registerTool(
"get_resume",
{
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 `list_resumes` first to find valid IDs.",
"The `resume://schema` resource describes the full data structure.",
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
},
withErrorHandling("getting resume", async ({ id }: { id: string }) => {
const resume = await client.resume.getById({ id });
return text(JSON.stringify(resume.data, null, 2));
}),
);
// ── Create Resume ─────────────────────────────────────────────
server.registerTool(
"create_resume",
{
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 `get_resume` to view or `patch_resume` 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')"),
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: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
},
withErrorHandling(
"creating resume",
async ({
name,
slug,
tags,
withSampleData,
}: {
name: string;
slug: string;
tags: string[];
withSampleData: boolean;
}) => {
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.`,
);
},
),
);
// ── Duplicate Resume ──────────────────────────────────────────
server.registerTool(
"duplicate_resume",
{
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: z.object({
id: resumeIdSchema.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: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
},
withErrorHandling(
"duplicating resume",
async ({ id, name, slug, tags }: { id: string; name: string; slug: string; tags: string[] }) => {
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.`,
);
},
),
);
// ── Patch Resume ──────────────────────────────────────────────
server.registerTool(
"patch_resume",
{
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.",
"",
"Supported operations: add, remove, replace, move, copy, test.",
"",
"Common path examples:",
" /basics/name — Change the name",
" /basics/headline — Change the headline",
" /summary/content — Replace summary (HTML string)",
" /sections/experience/items/- — Append a new experience item",
" /sections/experience/items/0/company — Update first experience's company",
" /sections/skills/items/- — Append a new skill",
" /metadata/template — Change the template (e.g. 'azurill', 'bronzor', 'onyx')",
" /metadata/design/colors/primary — Change the primary color (rgba string)",
" /sections/interests/hidden — Hide/show a section",
"",
"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.",
].join("\n"),
inputSchema: z.object({
id: resumeIdSchema,
operations: z
.array(jsonPatchOperationSchema)
.min(1)
.describe("Array of JSON Patch (RFC 6902) operations to apply"),
}),
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
},
withErrorHandling("patching resume", async ({ id, operations }: { id: string; operations: PatchOperation[] }) => {
const resume = await client.resume.patch({ id, operations });
const summary = operations.map((op) => `${op.op} ${op.path}`).join(", ");
return text(`Applied ${operations.length} operation(s) to "${resume.name}": ${summary}`);
}),
);
// ── Delete Resume ─────────────────────────────────────────────
server.registerTool(
"delete_resume",
{
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.",
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
},
},
withErrorHandling("deleting resume", async ({ id }: { id: string }) => {
await client.resume.delete({ id });
return text(`Successfully deleted resume (${id}) and all associated files.`);
}),
);
// ── Lock Resume ───────────────────────────────────────────────
server.registerTool(
"lock_resume",
{
title: "Lock Resume",
description: [
"Lock a resume to prevent any modifications.",
"",
"When locked, a resume cannot be edited (patch_resume), updated, or deleted.",
"Useful for protecting finalized resumes from accidental changes.",
"Use `unlock_resume` to re-enable editing.",
].join("\n"),
inputSchema: z.object({ id: resumeIdSchema }),
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
},
withErrorHandling("locking resume", async ({ id }: { id: string }) => {
await client.resume.setLocked({ id, isLocked: true });
return text(`Resume (${id}) is now locked. It cannot be edited, patched, or deleted until unlocked.`);
}),
);
// ── Unlock Resume ─────────────────────────────────────────────
server.registerTool(
"unlock_resume",
{
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,
},
},
withErrorHandling("unlocking resume", async ({ id }: { id: string }) => {
await client.resume.setLocked({ id, isLocked: false });
return text(`Resume (${id}) is now unlocked. It can be edited, patched, and deleted.`);
}),
);
// ── Export Resume as PDF ──────────────────────────────────────
server.registerTool(
"export_resume_pdf",
{
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.",
"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,
},
},
withErrorHandling("exporting resume as PDF", async ({ id }: { id: string }) => {
const { url } = await client.printer.printResumeAsPDF({ id });
return text(`PDF generated successfully.\n\nDownload URL: ${url}`);
}),
);
// ── Get Resume Screenshot ────────────────────────────────────
server.registerTool(
"get_resume_screenshot",
{
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.",
"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,
},
},
withErrorHandling("getting resume screenshot", async ({ id }: { id: string }) => {
const { url } = await client.printer.getResumeScreenshot({ id });
if (!url) return text("Screenshot could not be generated. The printer service may be unavailable.");
return text(
`Resume screenshot URL: ${url}\n\nThis is a WebP image of the first page. Open the URL to view the current visual state of the resume.`,
);
}),
);
// ── Get Resume Statistics ────────────────────────────────────
server.registerTool(
"get_resume_statistics",
{
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: z.object({ id: resumeIdSchema }),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
},
withErrorHandling("getting resume statistics", async ({ id }: { id: string }) => {
const stats = await client.resume.statistics.getById({ id });
return text(JSON.stringify(stats, null, 2));
}),
);
}
+68
View File
@@ -0,0 +1,68 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { createFileRoute } from "@tanstack/react-router";
import { registerPrompts } from "./-helpers/prompts";
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",
},
],
});
registerResources(server);
registerTools(server);
registerPrompts(server);
return server;
}
export const Route = createFileRoute("/mcp/")({
server: {
handlers: {
ANY: async ({ request }) => {
try {
const apiKey = request.headers.get("x-api-key");
if (!apiKey) throw new Error("Unauthorized");
const server = createMcpServer();
const transport = new WebStandardStreamableHTTPServerTransport({
enableJsonResponse: true,
});
await server.connect(transport);
return await transport.handleRequest(request);
} catch (error) {
console.error(`Error handling request: ${error instanceof Error ? error.message : String(error)}`);
return Response.json({
id: null,
jsonrpc: "2.0",
error: {
code: -32603,
message: `Error handling request: ${error instanceof Error ? error.message : String(error)}`,
},
});
}
},
},
},
});