יצירת mcp client

יצירת mcp client משלנו כדי שיתחבר לשרתי mcp שונים ויחבר את ה-LLM להכל באופן סטנדרטי.

פוסט זה הוא המשך ישיר של הפוסט הראשון בסדרה – mcp cli והפוסט השני mcp server – ללא קריאה בהם, אי אפשר יהיה להבין את הפוסט הזה. בפוסט זה אני אסביר איך יוצרים mcp client משלנו. רוב המדריכים (גם אני בהתחלה) מראים על איך משתמשים בקליינט מן המוכן. אבל אם אנו רוצים לשלב mcp באפליקציות שלנו, אנו לא יכולים להשתמש במשהו מן המוכן. אם אני רוצה למשל לבנות LLM שמבצע סריקה של מסד הנתונים שלי ומבצע פעולות בהתאם למה שהוא מוצא שם, אני לא אוכל להשתמש בקליינט של אנתרופיק למשל או אפילו לא ב-mcp cli. הם מעולים למשתמשים או למתכנתי וייב, פחות למי שרוצה לבצע עבודה יותר מורכבת.

אני מדגים עם פייתון, אבל ממש אין שום בעיה לעבוד עם Node.js או כל שפה אחרת (כן, כן, גם #C) כדי ליצור קליינט. החבילות של MCP זמינות לכל השפות.

התחלת הפרויקט

אנו נתחיל מליצור פרויקט ב-uv. אם אתם לא מכירים uv, אז באמת כדאי להכיר ובפוסט הזה אני מסביר. לא סתם הוא בוער ובוער חזק בתחום פיתוח הפייתון. נפתח תיקיה, נכנס אליה, נקליד uv init כדי ליצור פרויקט uv ואז uv venv כדי ליצור סביבה וירטואלית. אנחנו מוכנים. נתקין את המודולים של mcp וכיוון שאני רוצה להשתמש ב-LLM של openai, אז נשתמש בו. אבל הוא באמת לא חובה עבור הקליינט, כן?

uv add openai mcp

החלטה לאיזה שרת mcp להתחבר

עכשיו לכתיבה של הקליינט, כאשר אנו נכתוב קליינט שמתחבר ל-MCP Server. יש לנו כמה שרתי MCP Server. הראשון והיחיד שסקרתי עד כה הוא זה שעובד דרך stdio – כלומר במחשב שלי. אנחנו ניצמד לשיטה הזו. יש מלא דוגמאות.

מה שמעניין הוא הקוד, אם אתם עובדים עם LLM מסוג כלשהו לייצור הקוד, אפשר לקחת את הקוד הזה ולהדביק אותו ולבקש ממנו דוגמה לפי הצרכים שלכם. אני באמת ממליץ לעשות את זה ולראות בזמן אמת איך זה עובד.

אם אתם רוצים, אתם יכולים להריץ את הקוד הזה. צריך API key של openAI. יצרו אחד עם החשבון שלכם והכניסו אותו כמשתנה סביבה עם:

export OPENAI_API_KEY="sk-proj-YOUR-KEY"

העתיקו את הקוד הזה לקובץ בשם client-stdio.py:

import asyncio
import os
import sys
from typing import Optional
from contextlib import AsyncExitStack

from openai import OpenAI

# MCP imports for stdio communication
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

class MCPStdioClient:
    """
    An MCP client that connects to an MCP server using stdio communication
    and processes queries using OpenAI and the server's tools.
    """
    
    def __init__(self):
        # Initialize OpenAI client
        self.openai = OpenAI()
        
        # Initialize session and stdio objects
        self.session: Optional[ClientSession] = None
        self.stdio = None
        self.write = None
        self.exit_stack = AsyncExitStack()
        
        # Store server tools
        self.tools = []

    async def connect_to_server(self, server_script_path: str, use_uv: bool = False, server_dir: str = None) -> bool:
        """
        Connect to an MCP server via stdio
        
        Args:
            server_script_path: Path to the server script (.py or .js)
            use_uv: Whether to use uv to run the server
            server_dir: Directory to run uv from
            
        Returns:
            bool: True if connection successful, False otherwise
        """
        try:
            print(f"Connecting to MCP server at: {server_script_path}")
            
            # Determine server type (Python or Node.js)
            is_python = server_script_path.endswith('.py')
            is_js = server_script_path.endswith('.js')
            if not (is_python or is_js):
                print("Error: Server script must be a .py or .js file")
                return False

            # Configure server parameters based on whether we're using uv or not
            env = os.environ.copy()  # Use current environment with OPENAI_API_KEY
            
            if use_uv and is_python and server_dir:
                # Using uv to run the server from specified directory
                print(f"Using uv to run server from directory: {server_dir}")
                command = "uv"
                script_name = os.path.basename(server_script_path)
                args = ["--directory", server_dir, "run", script_name]
            else:
                # Using default Python or Node interpreter
                command = "python" if is_python else "node"
                args = [server_script_path]

            server_params = StdioServerParameters(
                command=command,
                args=args,
                env=env
            )

            # Connect to server
            stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
            self.stdio, self.write = stdio_transport
            self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))

            # Initialize session
            print("Initializing MCP session...")
            await self.session.initialize()

            # Fetch available tools
            response = await self.session.list_tools()
            self.tools = response.tools
            
            if self.tools:
                print(f"\nConnected to server with {len(self.tools)} tools:")
                for tool in self.tools:
                    print(f"  - {tool.name}: {tool.description}")
            else:
                print("Connected to server but no tools available")
                
            return True
            
        except Exception as e:
            print(f"Error connecting to MCP server: {str(e)}")
            import traceback
            traceback.print_exc()
            return False

    async def process_query(self, query: str) -> str:
        """
        Process a query using OpenAI and the MCP server's tools
        
        Args:
            query: The user's question or request
            
        Returns:
            OpenAI's response as a string
        """
        if not self.session:
            return "Error: Not connected to an MCP server"
            
        try:
            # Prepare initial chat messages
            messages = [
                {"role": "system", "content": "You are a helpful assistant that can access the latest blog posts from internet-israel.com."},
                {"role": "user", "content": query}
            ]

            # Convert MCP tools to OpenAI tool format
            available_tools = [{
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.inputSchema
                }
            } for tool in self.tools]

            # Initial OpenAI API call with tools
            print("Sending query to OpenAI...")
            response = self.openai.chat.completions.create(
                model="gpt-4-turbo",  # Using GPT-4 Turbo for tool use
                messages=messages,
                tools=available_tools,
                tool_choice="auto",  # Let the model decide whether to use tools
                max_tokens=1000
            )

            # Process response and handle tool calls
            final_text = []
            
            assistant_message = response.choices[0].message
            if assistant_message.content:
                final_text.append(assistant_message.content)
            
            # Handle tool calls if present
            if assistant_message.tool_calls:
                # Add assistant message to conversation
                messages.append(assistant_message)
                
                print(f"OpenAI wants to use {len(assistant_message.tool_calls)} tools")
                
                # Process each tool call
                for tool_call in assistant_message.tool_calls:
                    tool_name = tool_call.function.name
                    tool_args = tool_call.function.arguments
                    
                    # Convert string arguments to JSON if needed
                    import json
                    if isinstance(tool_args, str):
                        try:
                            tool_args = json.loads(tool_args)
                        except json.JSONDecodeError:
                            print(f"Warning: Could not parse tool args as JSON: {tool_args}")
                            tool_args = {}
                    
                    print(f"Calling tool: {tool_name} with args: {tool_args}")
                    
                    # Execute tool call through MCP server
                    try:
                        result = await self.session.call_tool(tool_name, tool_args)
                        tool_result = result.content
                        final_text.append(f"\n[Tool: {tool_name}]\n{tool_result}")
                        
                        # Add tool result to messages
                        messages.append({
                            "role": "tool",
                            "tool_call_id": tool_call.id,
                            "content": tool_result
                        })
                    except Exception as e:
                        error_msg = f"Error calling tool {tool_name}: {str(e)}"
                        print(error_msg)
                        final_text.append(f"\n[Error with tool {tool_name}]: {str(e)}")
                        
                        # Add error to messages
                        messages.append({
                            "role": "tool",
                            "tool_call_id": tool_call.id,
                            "content": f"Error: {str(e)}"
                        })
                
                # Get final response from OpenAI with tool results
                print("Getting final response from OpenAI with tool results...")
                response = self.openai.chat.completions.create(
                    model="gpt-4-turbo",
                    messages=messages,
                    max_tokens=1000
                )
                
                next_message = response.choices[0].message
                if next_message.content:
                    final_text.append(f"\n{next_message.content}")

            return "\n".join([text for text in final_text if text])
            
        except Exception as e:
            print(f"Error processing query: {str(e)}")
            import traceback
            traceback.print_exc()
            return f"Error processing query: {str(e)}"

    async def chat_loop(self):
        """Run an interactive chat loop"""
        print("\nMCP Stdio Client Started!")
        print("Type your queries about internet-israel.com blog or 'quit' to exit.")

        while True:
            try:
                query = input("\nQuery: ").strip()

                if query.lower() in ('quit', 'exit'):
                    break

                response = await self.process_query(query)
                print("\nResponse:")
                print(response)

            except Exception as e:
                print(f"\nError: {str(e)}")
                import traceback
                traceback.print_exc()

    async def cleanup(self):
        """Clean up resources"""
        if self.session:
            await self.exit_stack.aclose()
            print("Connection to MCP server closed")


async def main():
    # Validate command line arguments
    if len(sys.argv) < 2:
        print("Usage: uv run client-stdio.py <path_to_mcp_server_script> [--use-uv] [--server-dir=DIR]")
        return
    
    server_script = sys.argv[1]
    
    # Parse additional command line arguments
    use_uv = "--use-uv" in sys.argv
    server_dir = None
    
    for arg in sys.argv:
        if arg.startswith("--server-dir="):
            server_dir = arg.split("=", 1)[1]
    
    # If using uv but no server directory specified, extract from server script path
    if use_uv and not server_dir:
        server_dir = os.path.dirname(server_script)
        if not server_dir:
            server_dir = "."
    
    # Initialize and run client
    client = MCPStdioClient()
    
    try:
        # Connect to server
        if await client.connect_to_server(server_script, use_uv, server_dir):
            # Start interactive chat loop
            await client.chat_loop()
    finally:
        # Clean up resources
        await client.cleanup()


if __name__ == "__main__":
    asyncio.run(main())

תדאגו שיהיה לכם mcp server מוכן לפעולה. אני השתמשתי ב-mcp server שהדגמתי במאמר הקודם. ה-mcp server הקצת מסכן הזה מכיל כלי אחד שמביא את 5 הפוסטים האחרונים של הבלוג הזה. אבל הוא מעולה כדוגמה. אני מפעיל אותו באמצעות uv דרך הקוד הזה:

uv --directory /Users/spare10/local/demo-mcp-server run main.py

אני לא נדרש להפעיל אותו, רק לזכור איך להפעיל אותו. כשאני קורא ל-mcp client שלי, אני צריך להעביר לו את רשימת השרתים שהוא מחובר אליהם. כן, בדיוק כמו ה-mcp cli שהוא בסופו של דבר גם קליינט. רק אחד שבנוי יותר טוב מהקקמייקה בן 150 השורות שנתתי לעיל. איך אני מפעיל את השרת? מהתיקיה שיש בה את הקובץ:

uv run client-stdio.py /Users/spare10/local/demo-mcp-server/main.py --use-uv

אם לא שכחתי והצבתי את ה-OPENAI_API_KEY כמשתנה סביבה, אני אראה שיש לי קליינט שגם יש בו את הכלי של get_feed!

Connected to server with 1 tools:
  - get_feed: Fetch latest blog posts from internet-israel.com

Args:
    limit: Number of feed items to return (default 5)


MCP Stdio Client Started!
Type your queries about internet-israel.com blog or 'quit' to exit.

Query:

או קיי – איך זה עובד?

איך הנס הזה קרה? כאמור הקליינט מחבר את ה-LLM לרשימת שרתים. במקרה הזה שרתי stdio. הקליינט נוצר באמצעות

client = MCPStdioClient()

ואז:

if await client.connect_to_server(server_script, use_uv, server_dir):

וזהו, ה-mcp client מפעיל את השרתים ויחד עם ה-LLM הוא מאפשר את הכל. עכשיו אנחנו בעצם יכולים לעבוד על הקליינט שלנו לפי הצרכים שלנו. פה בניתי דוגמה פשוטה שעובדת עם ה-CLI. כמובן שאנחנו לא חייבים לעשות כן. כל אחד והקליינט שלו. ברוב הפעמים זה יהיה קליינט שבכלל לא מדבר עם משתמש אלא עם קלט שמגיע ממקומות אחרים.

אני לא רוצה לנתח את הקוד כי כבר זה פחות מעניין בעולם של היום. מה שמעניין במיוחד מבחינתנו הוא לראות איך הקוד עובד. יצירת הקליינט, קבלת המידע על השרתים וחיבור של הכל באמצעות ה-SDK של ה-mcp. ה-MCPStdioClient יודע בעצם להתחבר לשרתים ולחבר בינם לבין פעולות ולדעת להפעיל אותם לפי הוראותיו של ה-LLM. מה שחשוב הוא למצוא דוגמה טובה שעובדת, לתת אותה ל-LLM שכותב לכם את הקוד ולומר לו ״תעשה ככה״ ואז לעבוד על הקוד שלו כדי לראות שהוא לא עושה שטויות ולהבין באמת אם המידע עובר ולאן.

המשתמש שולח שאילתה לקליינט.
הקליינט מעביר את השאילתה ל-LLM עם רשימת הכלים הזמינים.
ה-LLM מחליט אם להפעיל כלי ומחזיר הוראה לקליינט.
הקליינט מפעיל את הכלי דרך ה-MCP Server.
התוצאה חוזרת ל-LLM, שמשלב אותה בתגובה הסופית.

כניסה אל מאחורי הקלעים

אבל רגע, איך ה-LLM יודע איזה כלים יש ואיך הוא נותן להפעיל את הכלים? במאמרים קודמים התייחסנו אל זה בתוך ״קסם״. אפשר להמשיך ולהתייחס אל זה בתור וודו אפר אבל אם רוצים להעמיק קצת, עכשיו, כשאנחנו כותבים את הקליינט אפשר לראות איך זה קורה. בגדול, חלק גדול מהמודלים (מ-openAI ועד grok) תומכים ב-tools. ה-LLM מקבל את רשימת הכלים ובפלט שלו מורה לקליינט להפעיל אותם. אני אדגים מהדוקומנטציה של openAI. כשאנחנו שולחים שאילתה ל-openAI, אנחנו שולחים אותה עם רשימת כלים שה-LLM יכול להפעיל, אנחנו שולחים משהו כזה:

from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "name": "get_feed",
    "description": "Get last posts of internet-israel.com.",
    "parameters": {
        "type": "object",
        "properties": {
            "number": {
                "type": "int",
                "description": "Number of posts required"
            }
        },
        "additionalProperties": False
    }
}]

response = client.responses.create(
    model="gpt-4.1",
    input=[{"role": "user", "content": "What are the last post in internet israel?"}],
    tools=tools
)

print(response.output)

אם ה-LLM מזהה שיש צורך בהפעלת כלי, וכאן הוא בהחלט יזהה, נקבל פלט כזה מה-openAI:

[{
    "type": "function_call",
    "id": "fc_12345xyz",
    "call_id": "call_12345xyz",
    "name": "get_feed",
    "arguments": "{\"number\":1}"
}]

האחריות שלי מי שקורה היא לקרוא לכלי ולספק את התשובה ואז לחכות לתשובה נוספת מה-LLM. עד ה-mcp היינו צריכים לעשות את זה ידנית ובאופן שונה מ-LLM אחד ל-LLM אחר. כי כל LLM עובד קצת אחרת. אז הייתי קורא ל-openAI ושולח לו את ה-tools בפורמט שהוא מבקש, לקבל את הפעלת ה-tools בפורמט שלו ואז להריץ אותם ולשלוח לו את התשובה. דבר שבאמת מצריך מאמץ וקוד למימוש כל הסיפור הזה. ואם משתנה גרסה או API או חלילה וחס אני רוצה להשתמש ב-LLM אחר? בהצלחה לי.

אבל הגדולה ב-mcp זה שמדובר בסטנדרט. עכשיו אני לא צריך לכתוב מימוש כזה אלא לספק לקליינט את השרת. השרת עצמו מצהיר על הפעולות שהוא יכול לעשות וכמה קליינטים יכולים לעשות שימוש באותו שרת. מה זה אומר? זה אומר שאם יש לי שירותים שונים שאני רוצה לחשוף החוצה, במקום שכל מי שצורך אותם יצטרך לשבור את הראש על לבנות להם tools ולשלוח אותם ל-LLM הרלוונטי ואז לבנות את ההרצה שלהם – אני פשוט בונה שרת שמייצא את הפעולות שלי כדי שכל שירות/קליינט יוכל להשתמש בו וזה מאד חזק בחברות ובשירותים שונים. אם אתם לא עובדים גדולה זה גם מעניין אתכם, כי יש המון המון שירותים שונים שאתם יכולים להשתמש בהם במוצרים שלכם ולא לפחד כל פעם שיהיה איזה שינוי ב-LLM בסוג של ה-tools שישבור לכם את הפונקציונליות. סטנדרטיזציה היא באמת שקט נפשי.

בפוסט הזה למדנו כיצד לבנות MCP Client – כלומר הבנו מה שני המודולים העיקריים שמייצרים mcp כזה. התחברנו לשרת MCP שיצרתי בדוגמה הקודמת ולהשתמש ב-LLM כדי להפעיל כלים בצורה חכמה וגמישה. ראינו כיצד הסטנדרט של MCP מפשט את העבודה עם מודלים שונים, חוסך זמן, ומאפשר לנו לבנות אפליקציות מתקדמות בקלות. עכשיו זה הזמן שלכם – קחו את הקוד, הזינו אותו לקופיילוט שלכם או ל-LLM שלכם או אפילו לשחק ידנית. הדוגמה שנתתי כאן פשוטה, אבל היא מצוינת להתחלה.

בפוסט הבא נתחבר ל-MCP Server מרחוק ונסתכל לעומק על איך הוא עושה את זה.

פוסטים נוספים שכדאי לקרוא

רספברי פיי

הרצת גו על רספברי פיי

עולם הרספברי פיי והמייקרים ניתן לתפעול בכל שפה – לא רק פייתון או C – כאן אני מסביר על גו

גלילה לראש העמוד