Resume-MCP-Server

ianand3977/Resume-MCP-Server

3.2

If you are the rightful owner of Resume-MCP-Server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to henry@mcphub.com.

This document provides a comprehensive guide to setting up and running a Model Context Protocol (MCP) server using the provided starter code.

Tools
  1. resume

    Serve your resume in plain markdown.

  2. validate

    Validates the phone number and application key.

  3. fetch

    Fetch a URL and return its content.

šŸš€ MCP Server Setup Instructions Follow the instructions below to set up your MCP server and complete the application process.

āœ… Next Steps Use the provided starter code to spin up a local MCP server.

After running /apply <TWITTER/LINKEDIN REPLY URL> you will get a application key.

Now you need to create an mcp server using the starter code given in this gist to submit your resume.

Use this command to connect Puch with your mcp server /mcp connect <SERVER URL (should be publicly accesible)>/mcp

šŸ”‘ Important: Replace the placeholder token in the code with your actual application key.

Puch will run a validation check against your Auth token (application key) and phone number šŸ“ž Validation requires both the key and your phone number, formatted as {country_code}{number} — without the + symbol. Example: 919876543210 for an Indian number.

Feed your resume to Puch: Create a tool that sends your resume in a format fit for an LLM šŸ“Ž Resume Tool Requirement: Your server must include a resume tool that:

Accepts a local file (your resume). Converts it to markdown text Submits the data to the Puch AI MCP endpoint as a string. Starter code from typing import Annotated from fastmcp import FastMCP from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair import markdownify from mcp import ErrorData, McpError from mcp.server.auth.provider import AccessToken from mcp.types import INTERNAL_ERROR, INVALID_PARAMS, TextContent from openai import BaseModel from pydantic import AnyUrl, Field import readabilipy from pathlib import Path

TOKEN = "<generated_token>" MY_NUMBER = "9189XXXXXXXX" # Insert your number {91}{Your number}

class RichToolDescription(BaseModel): description: str use_when: str side_effects: str | None

class SimpleBearerAuthProvider(BearerAuthProvider): """ A simple BearerAuthProvider that does not require any specific configuration. It allows any valid bearer token to access the MCP server. For a more complete implementation that can authenticate dynamically generated tokens, please use BearerAuthProvider with your public key or JWKS URI. """

def __init__(self, token: str):
    k = RSAKeyPair.generate()
    super().__init__(
        public_key=k.public_key, jwks_uri=None, issuer=None, audience=None
    )
    self.token = token

async def load_access_token(self, token: str) -> AccessToken | None:
    if token == self.token:
        return AccessToken(
            token=token,
            client_id="unknown",
            scopes=[],
            expires_at=None,  # No expiration for simplicity
        )
    return None

class Fetch: IGNORE_ROBOTS_TXT = True USER_AGENT = "Puch/1.0 (Autonomous)"

@classmethod
async def fetch_url(
    cls,
    url: str,
    user_agent: str,
    force_raw: bool = False,
) -> tuple[str, str]:
    """
    Fetch the URL and return the content in a form ready for the LLM, as well as a prefix string with status information.
    """
    from httpx import AsyncClient, HTTPError

    async with AsyncClient() as client:
        try:
            response = await client.get(
                url,
                follow_redirects=True,
                headers={"User-Agent": user_agent},
                timeout=30,
            )
        except HTTPError as e:
            raise McpError(
                ErrorData(
                    code=INTERNAL_ERROR, message=f"Failed to fetch {url}: {e!r}"
                )
            )
        if response.status_code >= 400:
            raise McpError(
                ErrorData(
                    code=INTERNAL_ERROR,
                    message=f"Failed to fetch {url} - status code {response.status_code}",
                )
            )

        page_raw = response.text

    content_type = response.headers.get("content-type", "")
    is_page_html = (
        "<html" in page_raw[:100] or "text/html" in content_type or not content_type
    )

    if is_page_html and not force_raw:
        return cls.extract_content_from_html(page_raw), ""

    return (
        page_raw,
        f"Content type {content_type} cannot be simplified to markdown, but here is the raw content:\n",
    )

@staticmethod
def extract_content_from_html(html: str) -> str:
    """Extract and convert HTML content to Markdown format.

    Args:
        html: Raw HTML content to process

    Returns:
        Simplified markdown version of the content
    """
    ret = readabilipy.simple_json.simple_json_from_html_string(
        html, use_readability=True
    )
    if not ret["content"]:
        return "<error>Page failed to be simplified from HTML</error>"
    content = markdownify.markdownify(
        ret["content"],
        heading_style=markdownify.ATX,
    )
    return content

mcp = FastMCP( "My MCP Server", auth=SimpleBearerAuthProvider(TOKEN), )

ResumeToolDescription = RichToolDescription( description="Serve your resume in plain markdown.", use_when="Puch (or anyone) asks for your resume; this must return raw markdown,
no extra formatting.", side_effects=None, )

@mcp.tool(description=ResumeToolDescription.model_dump_json()) async def resume() -> str: """ Return your resume exactly as markdown text.

TODO: Implement this function to:
1. Find and read your resume.
2. Convert the resume to markdown format.
3. Handle any errors gracefully.
4. Return the resume as markdown text.
"""
# TODO: Implement resume fetching logic
raise NotImplementedError("Resume tool not implemented")

@mcp.tool async def validate() -> str: """ NOTE: This tool must be present in an MCP server used by puch. """ return MY_NUMBER

FetchToolDescription = RichToolDescription( description="Fetch a URL and return its content.", use_when="Use this tool when the user provides a URL and asks for its content, or when the user wants to fetch a webpage.", side_effects="The user will receive the content of the requested URL in a simplified format, or raw HTML if requested.", )

@mcp.tool(description=FetchToolDescription.model_dump_json()) async def fetch( url: Annotated[AnyUrl, Field(description="URL to fetch")], max_length: Annotated[ int, Field( default=5000, description="Maximum number of characters to return.", gt=0, lt=1000000, ), ] = 5000, start_index: Annotated[ int, Field( default=0, description="On return output starting at this character index, useful if a previous fetch was truncated and more context is required.", ge=0, ), ] = 0, raw: Annotated[ bool, Field( default=False, description="Get the actual HTML content if the requested page, without simplification.", ), ] = False, ) -> list[TextContent]: """Fetch a URL and return its content.""" url_str = str(url).strip() if not url: raise McpError(ErrorData(code=INVALID_PARAMS, message="URL is required"))

content, prefix = await Fetch.fetch_url(url_str, Fetch.USER_AGENT, force_raw=raw)
original_length = len(content)
if start_index >= original_length:
    content = "<error>No more content available.</error>"
else:
    truncated_content = content[start_index : start_index + max_length]
    if not truncated_content:
        content = "<error>No more content available.</error>"
    else:
        content = truncated_content
        actual_content_length = len(truncated_content)
        remaining_content = original_length - (start_index + actual_content_length)
        # Only add the prompt to continue fetching if there is still remaining content
        if actual_content_length == max_length and remaining_content > 0:
            next_start = start_index + actual_content_length
            content += f"\n\n<error>Content truncated. Call the fetch tool with a start_index of {next_start} to get more content.</error>"
return [TextContent(type="text", text=f"{prefix}Contents of {url}:\n{content}")]

async def main(): await mcp.run_async( "streamable-http", host="0.0.0.0", port=8085, )

if name == "main": import asyncio

asyncio.run(main())