DEV Community

Cover image for MCP Series (01): What Is MCP — Why Function Calling Isn't Enough
WonderLab
WonderLab

Posted on

MCP Series (01): What Is MCP — Why Function Calling Isn't Enough

A Real Problem First

You've wired Claude into Jira — it can search tickets, create issues, update status. It runs. Your team is happy.

Three weeks later, a colleague on another team wants the same Jira tools for their Agent project.

Two options:

  1. Send them the Jira tool code to copy
  2. Tell them to install an MCP Server: 5 minutes, done

MCP was designed to make the second option the default.


What Function Calling Can and Can't Do

Function Calling (also called Tool Use) lets an LLM invoke external tools. The mechanics are straightforward:

tools = [{
    "name": "search_jira",
    "description": "Search Jira tickets",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "Search keywords"},
            "project": {"type": "string", "description": "Project key, optional"}
        },
        "required": ["query"]
    }
}]

response = client.messages.create(
    model="claude-sonnet-4-6",
    tools=tools,
    messages=[{"role": "user", "content": "Find recent P0 bugs"}]
)

if response.stop_reason == "tool_use":
    tool_call = response.content[0]
    result = search_jira(tool_call.input["query"])
    # return result to LLM to continue reasoning
Enter fullscreen mode Exit fullscreen mode

This works well. It's in production everywhere.

The structural problem: tool definitions and implementations both live in the caller.

Every project using Jira tools needs to:

  • Maintain a search_jira schema definition
  • Maintain a search_jira execution function
  • Handle auth, retries, result formatting

Three teams, three copies. The Jira API changes, three places to update.


MCP's Core Design

MCP (Model Context Protocol) moves tool definitions out of the caller and into an independent Server process.

Function Calling model:
┌─────────────────────────────────────┐
│  Your Agent code                    │
│  ├── LLM call logic                 │
│  ├── Tool schema definitions        │  ← every Agent maintains its own
│  └── Tool execution (Jira/GitHub/…) │  ← every Agent maintains its own
└─────────────────────────────────────┘

MCP model:
┌──────────────────┐     ┌─────────────────────┐
│  Host (Agent/IDE)│────▶│  MCP Server          │
│  only call logic │     │  ├── Tool schemas    │  ← maintained once
└──────────────────┘     │  └── Tool execution  │  ← maintained once
                         └─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

An MCP Server is an independent process that communicates with Hosts via a standard protocol (JSON-RPC 2.0). Any MCP-compatible Host — Claude Desktop, Claude Code, a custom Agent — connects to this Server directly. No re-implementation needed.


Same Scenario, Two Implementations

"Search Jira tickets" in both approaches:

Option A: Function Calling

# agent.py — tool schema and execution are both here

import anthropic
from jira import JIRA

jira_client = JIRA(
    server="https://your-company.atlassian.net",
    basic_auth=("user@example.com", os.environ["JIRA_API_TOKEN"])
)

JIRA_TOOLS = [{
    "name": "search_jira",
    "description": "Search Jira tickets, return title, status, priority",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string"},
            "max_results": {"type": "integer", "default": 10}
        },
        "required": ["query"]
    }
}]

def execute_tool(name: str, args: dict) -> str:
    if name == "search_jira":
        issues = jira_client.search_issues(
            f'text ~ "{args["query"]}"',
            maxResults=args.get("max_results", 10)
        )
        return "\n".join([
            f"[{i.key}] {i.fields.summary} ({i.fields.status.name})"
            for i in issues
        ])
    raise ValueError(f"Unknown tool: {name}")

def run_agent(user_input: str):
    client = anthropic.Anthropic()
    messages = [{"role": "user", "content": user_input}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            tools=JIRA_TOOLS,
            messages=messages
        )
        if response.stop_reason != "tool_use":
            return response.content[0].text

        tool_call = next(b for b in response.content if b.type == "tool_use")
        result = execute_tool(tool_call.name, tool_call.input)

        messages.extend([
            {"role": "assistant", "content": response.content},
            {"role": "user", "content": [{"type": "tool_result",
                                           "tool_use_id": tool_call.id,
                                           "content": result}]}
        ])
Enter fullscreen mode Exit fullscreen mode

Option B: MCP Server

# jira_mcp_server.py — standalone process, usable by any Host

import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from jira import JIRA

jira_client = JIRA(
    server="https://your-company.atlassian.net",
    basic_auth=("user@example.com", os.environ["JIRA_API_TOKEN"])
)

server = Server("jira-tools")

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [Tool(
        name="search_jira",
        description="Search Jira tickets, return title, status, priority",
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string"},
                "max_results": {"type": "integer", "default": 10}
            },
            "required": ["query"]
        }
    )]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "search_jira":
        issues = jira_client.search_issues(
            f'text ~ "{arguments["query"]}"',
            maxResults=arguments.get("max_results", 10)
        )
        result = "\n".join([
            f"[{i.key}] {i.fields.summary} ({i.fields.status.name})"
            for i in issues
        ])
        return [TextContent(type="text", text=result)]

if __name__ == "__main__":
    asyncio.run(stdio_server(server))
Enter fullscreen mode Exit fullscreen mode
# agent.py — only Agent logic remains, no Jira code at all

# Tool discovery and invocation handled by the MCP framework
# No Jira-related code needed here
Enter fullscreen mode Exit fullscreen mode

The second team onboards:

# install
pip install jira-mcp-server

# add to Claude Code settings — tools become available immediately
# zero changes to Agent code
Enter fullscreen mode Exit fullscreen mode

Core Comparison

Dimension           Function Calling          MCP
──────────────────────────────────────────────────────────────────
Tool definition     Inside caller code        Independent Server process
Reuse method        Copy code                 Install / connect Server
Cross-project sync  Manual, prone to drift    Server version is canonical
Cross-model use     Configure per model       One Server, any Host
State management    Stateless                 Server maintains state
Deployment cost     No extra process          Must manage Server process
Right scale         1-2 projects use a tool   Many projects share tools
Enter fullscreen mode Exit fullscreen mode

MCP is a standardization layer on top of Function Calling; they work together. Inside an MCP Server, tools are still invoked through the Function Calling mechanism between the Host and the LLM.


When to Use MCP vs Function Calling

Function Calling is enough when:

  • Only one project uses the tool; no reuse planned
  • The tool is simple — 10-20 lines of logic
  • You're prototyping; no overhead needed
  • Tool logic is tightly coupled to Agent logic; separation adds nothing

An MCP Server makes sense when:

  • The tool runs in multiple Agent projects (Jira, GitHub, databases)
  • The tool has state (connection pools, auth sessions, caches)
  • Claude Desktop or Claude Code users need to install and use it directly
  • The team wants a shared internal tool standard across projects

Decision questions:

Will more than 2 projects use this tool?
  → Yes → consider MCP Server
  → No  → Function Calling is fine

Does the tool need to maintain connection state?
  → Yes → MCP Server (process manages state across calls)
  → No  → keep going

Do non-engineers need to configure and use this tool?
  → Yes → MCP Server (see Claude Desktop ecosystem)
  → No  → Function Calling is fine
Enter fullscreen mode Exit fullscreen mode

Summary

Function Calling already solves 'can the LLM call a tool.' MCP solves how tools get standardized and reused: pull tool definitions out of every Agent project, put them in a standalone Server, and any MCP-compatible Host connects without reimplementing anything.

The tradeoff is process management complexity. The gain is a single maintained implementation that all dependents consume.

The next article goes into the protocol itself: the Host / Client / Server three-layer model, the JSON-RPC message format, and a walkthrough of a real communication session using MCP Inspector.


References


Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.

Find more useful knowledge and interesting products on my Homepage

Top comments (0)