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())












Hi @ArjitJ ... Could you please confirm if my application is successful? I'm confused if my application is submitted or not because it's not showing any message like "Your application is submitted." I already tried to ask in Puch chat it's unable to confirm, rather suggesting to contact Puch AI directly. Would be pleased to hear from you...