Follow the instructions below to set up your MCP server and complete the application process.
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 <AUTH TOKEN>
🔑 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:919876543210for 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.
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())



















Can anyone please provide me the code ? I don't know why mcp is refusing to connect