Manual arxiv integration

Time to get our hands dirty. In this module, we'll build integrations the way most people do it today: manually. This is intentionally painful. By the end, you'll understand exactly what MCP eliminates.

Our DevTools AI Assistant needs to search research papers on arXiv. We need two tool functions: search_papers() to find papers by topic, and extract_info() to get details about a specific paper. Let's write them.

04-tool-use-arxiv.py
python
def search_papers(topic: str, max_results: int = 5) -> List[str]:
    """Search for papers on arXiv based on a topic."""
    client = arxiv.Client()
    search = arxiv.Search(
        query=topic,
        max_results=max_results,
        sort_by=arxiv.SortCriterion.Relevance
    )
    papers_dir = os.path.join("papers", topic)
    os.makedirs(papers_dir, exist_ok=True)

    papers_data = []
    paper_ids = []
    for result in client.results(search):
        paper_id = result.entry_id.split("/")[-1]
        papers_data.append({
            "id": paper_id,
            "title": result.title,
            "authors": [a.name for a in result.authors],
            "summary": result.summary,
            "pdf_url": result.pdf_url,
            "published": str(result.published),
        })
        paper_ids.append(paper_id)

    with open(os.path.join(papers_dir, "papers_info.json"), "w") as f:
        json.dump(papers_data, f, indent=2)
    return paper_ids

def extract_info(paper_id: str) -> str:
    """Extract information about a specific paper."""
    for root, dirs, files in os.walk("papers"):
        if "papers_info.json" in files:
            with open(os.path.join(root, "papers_info.json")) as f:
                papers = json.load(f)
                for paper in papers:
                    if paper["id"] == paper_id:
                        return json.dumps(paper, indent=2)
    return f"No saved information for paper {paper_id}."

Two straightforward functions for searching papers and extracting info. About 40 lines. This is the easy part.

Those functions are clean and useful. But to let an LLM call them, we need boilerplate. A lot of it. Let's count the layers of glue code.

04-tool-use-arxiv.py, Boilerplate #1: JSON Schemas
python
# Hand-written JSON schemas duplicating type info from function signatures
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_papers",
            "description": "Search for papers on arXiv based on a topic",
            "parameters": {
                "type": "object",
                "properties": {
                    "topic": {"type": "string", "description": "The topic"},
                    "max_results": {"type": "integer", "description": "Max papers"}
                },
                "required": ["topic"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "extract_info",
            "description": "Extract information about a specific paper",
            "parameters": {
                "type": "object",
                "properties": {
                    "paper_id": {"type": "string", "description": "The paper ID"}
                },
                "required": ["paper_id"]
            }
        }
    }
]

~40 lines of JSON schemas that duplicate what the function signatures and docstrings already say.

04-tool-use-arxiv.py, Boilerplate #2 & #3
python
# Boilerplate #2: Name-to-function mapping
mapping_tool_function = {
    "search_papers": search_papers,
    "extract_info": extract_info,
}

# Boilerplate #3: Execute dispatcher
def execute_tool(tool_name, tool_args):
    """Route tool calls to the right function."""
    func = mapping_tool_function.get(tool_name)
    if not func:
        return f"Unknown tool: {tool_name}"
    result = func(**tool_args)
    if result is None:
        return "Action completed."
    elif isinstance(result, list):
        return json.dumps(result)
    elif isinstance(result, dict):
        return json.dumps(result, indent=2)
    return str(result)

A mapping dict + a dispatcher function. Same pattern repeated for every integration.

Let's count the boilerplate scorecard for arXiv alone: - Tool functions (actual logic): ~40 lines - JSON schemas: ~40 lines - Name-to-function mapping: ~4 lines - Execute dispatcher: ~18 lines - LLM tool-call loop: ~46 lines - Total glue code: ~108 lines that have nothing to do with arXiv The actual arXiv logic is 40 lines. The glue code is 108 lines. More than 2.5x the useful code.

Quiz: Quiz

Loading practice…