diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..b8befec --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,14 @@ +{ + "servers": { + "GmailMCPServer": { + "command": "uv", + "args": [ + "run", + "main.py" + ], + "cwd": "/home/matteo/Documents/Dev/Personal/hal/MCP_and_tools/GMailMCP", + "type": "stdio" + } + }, + "inputs": [] +} \ No newline at end of file diff --git a/CodeFirst_Libraries/PydanticAI/email_agent.py b/CodeFirst_Libraries/PydanticAI/email_agent.py new file mode 100644 index 0000000..364457e --- /dev/null +++ b/CodeFirst_Libraries/PydanticAI/email_agent.py @@ -0,0 +1,88 @@ +import os +import asyncio +import smtplib +from email.message import EmailMessage +from dotenv import load_dotenv +from pydantic_ai import Agent, RunContext +import nest_asyncio + +# Apply nest_asyncio to allow nested event loops if necessary +nest_asyncio.apply() + +# --- Configuration --- +load_dotenv() + +# --- Agent Definition --- +agent = Agent( + 'openai:gpt-5-nano', + system_prompt='You are a helpful email assistant. You can send emails using the defined tools.', + deps_type=str # We can use deps to pass dependencies if needed, or simple type + +) + +# --- Tool Definition --- +@agent.tool +def send_email(ctx: RunContext[str], recipient: str, subject: str, body: str) -> str: + """ + Send an email using Gmail SMTP. + + Args: + recipient: The email address receiving the email. + subject: The subject/title of the email. + body: The body content of the email. + + Returns: + A success message or error description. + """ + sender = os.getenv("GMAIL_SENDER") or os.getenv("GMAIL_USERNAME") or "mmarcolinionline@gmail.com" # Fallback/Configuration + password = os.getenv("GMAIL_PASSWORD") + + if not password: + return "Error: GMAIL_PASSWORD not set in environment variables." + + print(f"DEBUG: Attempting to send email to {recipient} with subject '{subject}'") + + msg = EmailMessage() + msg.set_content(body) + msg["Subject"] = subject + msg["From"] = sender + msg["To"] = recipient + + try: + with smtplib.SMTP("smtp.gmail.com", 587) as server: + server.starttls() + server.login(sender, password) + server.send_message(msg) + return f"Email sent successfully to {recipient}" + except Exception as e: + return f"Error sending email: {str(e)}" + +# --- Execution --- +async def main(): + print("Starting Email Agent...") + print("Ensure GMAIL_PASSWORD is set in your .env file.") + + messages = [] + + while True: + try: + user_input = input("\nUser (or 'q' to quit): ") + if user_input.lower() in ["q", "quit", "exit"]: + break + + # Run the agent + result = await agent.run(user_input, message_history=messages) + + print(f"Agent: {result.data}") + + # Update history + messages.extend(result.new_messages()) + + except KeyboardInterrupt: + break + except Exception as e: + print(f"Error: {e}") + break + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/MCP_and_tools/GMailMCP/.env.example b/MCP_and_tools/GMailMCP/.env.example new file mode 100644 index 0000000..8e64e79 --- /dev/null +++ b/MCP_and_tools/GMailMCP/.env.example @@ -0,0 +1,4 @@ +GMAIL_PASSWORD=your_app_password_here +# If your sender email is different from the account associated with the password, +# you might need to adjust authentication or use the primary account. +# For IMAP, the email_address argument must match the account for this password. diff --git a/MCP_and_tools/GMailMCP/.python-version b/MCP_and_tools/GMailMCP/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/MCP_and_tools/GMailMCP/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/MCP_and_tools/GMailMCP/README.md b/MCP_and_tools/GMailMCP/README.md new file mode 100644 index 0000000..3db93f6 --- /dev/null +++ b/MCP_and_tools/GMailMCP/README.md @@ -0,0 +1,49 @@ +# Gmail MCP Server + +This is a Model Context Protocol (MCP) server that provides Gmail integration. + +## Tools + +1. `send_email(sender, recipient, title, body)`: Sends an email using Gmail SMTP. +2. `get_recent_emails(email_address)`: Retrieves the titles of the last 10 emails from the inbox. + +## Setup + +1. Create a `.env` file based on `.env.example`: + ```bash + cp .env.example .env + ``` +2. Add your Gmail App Password to `.env`. You can generate one in your Google Account settings (Security > 2-Step Verification > App passwords). + +## Usage + +Run the server using `uv`: + +```bash +uv run main.py +``` + +## VS Code Configuration (mcp.json) + +To use this server with VS Code's MCP client, add the following configuration to your MCP settings file (typically located at `~/.config/Code/User/globalStorage/mcp-servers.json` or accessible via the command palette "MCP: Configure Servers"): + +```json +{ + "mcpServers": { + "gmail-mcp": { + "command": "uv", + "args": [ + "--directory", + "/absolute/path/to/hal/MCP_and_tools/GMailMCP", + "run", + "main.py" + ], + "env": { + "GMAIL_PASSWORD": "your-app-password-here" + } + } + } +} +``` + +> **Note:** Replace `/absolute/path/to/hal/MCP_and_tools/GMailMCP` with the actual full path to this directory on your machine. You can also move the sensitive `GMAIL_PASSWORD` env var here if you prefer not to rely on the `.env` file when running via VS Code, though the server code loads `.env` by default. \ No newline at end of file diff --git a/MCP_and_tools/GMailMCP/main.py b/MCP_and_tools/GMailMCP/main.py new file mode 100644 index 0000000..f0a20b1 --- /dev/null +++ b/MCP_and_tools/GMailMCP/main.py @@ -0,0 +1,110 @@ +from fastmcp import FastMCP +import smtplib +import imaplib +import email +from email.header import decode_header +from email.message import EmailMessage +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Initialize FastMCP +mcp = FastMCP("Gmail Integration") + +@mcp.tool() +def send_email(sender: str, recipient: str, title: str, body: str) -> str: + """ + Send an email using Gmail SMTP. + + Args: + sender: The email address sending the email. + recipient: The email address receiving the email. + title: The subject/title of the email. + body: The body content of the email. + """ + password = os.getenv("GMAIL_PASSWORD") + if not password: + raise ValueError("GMAIL_PASSWORD not set in environment variables.") + + # Create the email + msg = EmailMessage() + msg.set_content(body) + msg["Subject"] = title + msg["From"] = sender + msg["To"] = recipient + + try: + # Connect to Gmail SMTP server + # Note: This requires an App Password if 2FA is enabled + with smtplib.SMTP("smtp.gmail.com", 587) as server: + server.starttls() + server.login(sender, password) + server.send_message(msg) + return f"Email sent successfully to {recipient}" + except Exception as e: + return f"Error sending email: {str(e)}" + +@mcp.tool() +def get_recent_emails(email_address: str) -> list[str]: + """ + Retrieve the titles of the last 10 emails from the inbox. + + Args: + email_address: The email address to check (must match GMAIL_PASSWORD account). + """ + password = os.getenv("GMAIL_PASSWORD") + if not password: + raise ValueError("GMAIL_PASSWORD not set in environment variables.") + + try: + # Connect to Gmail IMAP server + mail = imaplib.IMAP4_SSL("imap.gmail.com") + mail.login(email_address, password) + + # Select the 'inbox' + mail.select("inbox") + + # Search for all emails + status, messages = mail.search(None, "ALL") + if status != "OK": + return ["Error retrieving emails"] + + # Get the list of email IDs + email_ids = messages[0].split() + + # Get the last 10 email IDs (or fewer if less than 10 exist) + last_10_ids = email_ids[-10:] if len(email_ids) >= 10 else email_ids + + titles = [] + # Fetch in reverse order (newest first) + for e_id in reversed(last_10_ids): + status, msg_data = mail.fetch(e_id, "(RFC822)") + if status != "OK": + continue + + for response_part in msg_data: + if isinstance(response_part, tuple): + msg = email.message_from_bytes(response_part[1]) + subject = msg["Subject"] + if subject: + decoded_list = decode_header(subject) + subject_str = "" + for decoded_part, encoding in decoded_list: + if isinstance(decoded_part, bytes): + subject_str += decoded_part.decode(encoding if encoding else "utf-8", errors="ignore") + else: + subject_str += decoded_part + titles.append(subject_str) + else: + titles.append("(No Subject)") + + mail.logout() + return titles + except Exception as e: + return [f"Error retrieving emails: {str(e)}"] + +if __name__ == "__main__": + mcp.run() + diff --git a/MCP_and_tools/GMailMCP/pyproject.toml b/MCP_and_tools/GMailMCP/pyproject.toml new file mode 100644 index 0000000..9b03418 --- /dev/null +++ b/MCP_and_tools/GMailMCP/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "gmail-mcp" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastmcp>=2.14.4", + "python-dotenv>=1.2.1", +] diff --git a/MCP_and_tools/SendEmailWithComposio/.python-version b/MCP_and_tools/SendEmailWithComposio/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/MCP_and_tools/SendEmailWithComposio/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/MCP_and_tools/SendEmailWithComposio/README.md b/MCP_and_tools/SendEmailWithComposio/README.md new file mode 100644 index 0000000..e69de29 diff --git a/MCP_and_tools/SendEmailWithComposio/pyproject.toml b/MCP_and_tools/SendEmailWithComposio/pyproject.toml new file mode 100644 index 0000000..5248d15 --- /dev/null +++ b/MCP_and_tools/SendEmailWithComposio/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "sendemailwithcomposio" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "composio>=0.10.10", + "composio-langchain>=0.10.10", + "composio-openai-agents>=0.10.10", + "dotenv>=0.9.9", + "langchain>=1.2.7", + "langchain-community>=0.4.1", + "langchain-openai>=1.1.7", + "openai-agents>=0.7.0", +] diff --git a/MCP_and_tools/SendEmailWithComposio/sendMail.py b/MCP_and_tools/SendEmailWithComposio/sendMail.py new file mode 100644 index 0000000..7107242 --- /dev/null +++ b/MCP_and_tools/SendEmailWithComposio/sendMail.py @@ -0,0 +1,55 @@ +import os + +from dotenv import load_dotenv +import asyncio +from composio import Composio +from agents import Agent, Runner +from composio_openai_agents import OpenAIAgentsProvider + +# Load environment variables +load_dotenv() + +if not os.environ.get("OPENAI_API_KEY"): + print("WARNING: OPENAI_API_KEY not found in environment variables.") + +composio = Composio(api_key=os.environ.get("COMPOSIO_API_KEY"), provider=OpenAIAgentsProvider()) + +# Id of the user in your system +externalUserId = "c3e00703-0478-4974-8873-bb6b6586f8bf" + +# Create an auth config for gmail from the dashboard or programmatically +auth_config_id = os.environ.get("AUTH_CONFIG_ID") +connection_request = composio.connected_accounts.link( + user_id=externalUserId, + auth_config_id=auth_config_id, +) + +# Redirect user to the OAuth flow +redirect_url = connection_request.redirect_url + +print( + f"Please authorize the app by visiting this URL: {redirect_url}" +) # Print the redirect url to the user + +# Wait for the connection to be established +connected_account = connection_request.wait_for_connection() +print( + f"Connection established successfully! Connected account id: {connected_account.id}" +) + +# Get Gmail tools that are pre-configured +tools = composio.tools.get(user_id=externalUserId, tools=["GMAIL_SEND_EMAIL"]) + +agent = Agent( + name="Email Manager", instructions="You are a helpful assistant", tools=tools +) + +# Run the agent +async def main(): + result = await Runner.run( + starting_agent=agent, + input="Send an email to mmarcolinishop@gmail.com with the subject 'Hello from composio 👋🏻' and the body 'Congratulations on sending your first email using AI Agents and Composio!'", + ) + print(result.final_output) + +asyncio.run(main()) diff --git a/MCP_and_tools/Skills_and_Tools/GmailSkill/SKILL.md b/MCP_and_tools/Skills_and_Tools/GmailSkill/SKILL.md new file mode 100644 index 0000000..a577c96 --- /dev/null +++ b/MCP_and_tools/Skills_and_Tools/GmailSkill/SKILL.md @@ -0,0 +1,42 @@ +# Gmail Skill + +This skill allows you to send emails via Gmail SMTP and retrieve the last 10 emails from the inbox using IMAP. + +## Requirements + +- Python 3.12+ +- `uv` package manager +- A `.env` file containing `GMAIL_PASSWORD` (App Password if 2FA is enabled). + +## Environment Setup + +Ensure you have a `.env` file in the root of your workspace or in this directory with: +```bash +GMAIL_PASSWORD=your_app_password +``` + +## Usage + +You can execute the script using `uv run`. + +### Send an Email + +```bash +uv run scripts/manage_emails.py send \ + --sender "your_email@gmail.com" \ + --recipient "recipient@example.com" \ + --title "Hello from VS Code" \ + --body "This is a test email sent from the Gmail Skill." +``` + +### Get Recent 10 Emails + +```bash +uv run scripts/manage_emails.py get-recent \ + --email-address "your_email@gmail.com" +``` + +## Capabilities + +- **Send Email**: Sends an email with subject and body. +- **Get Recent Emails**: Retrieves list of subjects for the last 10 emails in the Inbox. diff --git a/MCP_and_tools/Skills_and_Tools/GmailSkill/pyproject.toml b/MCP_and_tools/Skills_and_Tools/GmailSkill/pyproject.toml new file mode 100644 index 0000000..ed72c6a --- /dev/null +++ b/MCP_and_tools/Skills_and_Tools/GmailSkill/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "gmail-skill" +version = "0.1.0" +description = "A skill to manage Gmail operations" +readme = "SKILL.md" +requires-python = ">=3.10" +dependencies = [ + "python-dotenv", +] + +[tool.uv] +package = false diff --git a/MCP_and_tools/Skills_and_Tools/GmailSkill/scripts/manage_emails.py b/MCP_and_tools/Skills_and_Tools/GmailSkill/scripts/manage_emails.py new file mode 100644 index 0000000..a671be3 --- /dev/null +++ b/MCP_and_tools/Skills_and_Tools/GmailSkill/scripts/manage_emails.py @@ -0,0 +1,132 @@ +import argparse +import smtplib +import imaplib +import email +from email.header import decode_header +from email.message import EmailMessage +import os +import sys +from dotenv import load_dotenv +from pathlib import Path + +# Load environment variables +# Try loading from the current directory, and also specific locations +load_dotenv() # Defaults +env_path = Path(__file__).parent / ".env" +load_dotenv(dotenv_path=env_path) + +def send_email(sender: str, recipient: str, title: str, body: str) -> str: + """ + Send an email using Gmail SMTP. + """ + password = os.getenv("GMAIL_PASSWORD") + if not password: + print("Error: GMAIL_PASSWORD not set in environment variables.", file=sys.stderr) + return "Error: GMAIL_PASSWORD not set" + + # Create the email + msg = EmailMessage() + msg.set_content(body) + msg["Subject"] = title + msg["From"] = sender + msg["To"] = recipient + + try: + # Connect to Gmail SMTP server + with smtplib.SMTP("smtp.gmail.com", 587) as server: + server.starttls() + server.login(sender, password) + server.send_message(msg) + return f"Email sent successfully to {recipient}" + except Exception as e: + return f"Error sending email: {str(e)}" + +def get_recent_emails(email_address: str) -> list[str]: + """ + Retrieve the titles of the last 10 emails from the inbox. + """ + password = os.getenv("GMAIL_PASSWORD") + if not password: + print("Error: GMAIL_PASSWORD not set in environment variables.", file=sys.stderr) + return ["Error: GMAIL_PASSWORD not set"] + + try: + # Connect to Gmail IMAP server + mail = imaplib.IMAP4_SSL("imap.gmail.com") + mail.login(email_address, password) + + # Select the 'inbox' + mail.select("inbox") + + # Search for all emails + status, messages = mail.search(None, "ALL") + if status != "OK": + return ["Error retrieving emails: IMAP search failed"] + + # Get the list of email IDs + email_ids = messages[0].split() + + # Get the last 10 email IDs (or fewer if less than 10 exist) + last_10_ids = email_ids[-10:] if len(email_ids) >= 10 else email_ids + + titles = [] + # Fetch in reverse order (newest first) + for e_id in reversed(last_10_ids): + status, msg_data = mail.fetch(e_id, "(RFC822)") + if status != "OK": + continue + + for response_part in msg_data: + if isinstance(response_part, tuple): + msg = email.message_from_bytes(response_part[1]) + subject = msg["Subject"] + if subject: + decoded_list = decode_header(subject) + subject_str = "" + for decoded_part, encoding in decoded_list: + if isinstance(decoded_part, bytes): + subject_str += decoded_part.decode(encoding if encoding else "utf-8", errors="ignore") + else: + subject_str += decoded_part + titles.append(subject_str) + else: + titles.append("(No Subject)") + + mail.logout() + # Print titles for the CLI output + for title in titles: + print(f"- {title}") + return titles + except Exception as e: + err = f"Error retrieving emails: {str(e)}" + print(err, file=sys.stderr) + return [err] + +def main(): + parser = argparse.ArgumentParser(description="Gmail Actions Skill") + subparsers = parser.add_subparsers(dest="command", help="Command to execute") + + # Send email command + send_parser = subparsers.add_parser("send", help="Send an email") + send_parser.add_argument("--sender", required=True, help="Sender email address") + send_parser.add_argument("--recipient", required=True, help="Recipient email address") + send_parser.add_argument("--title", required=True, help="Email subject") + send_parser.add_argument("--body", required=True, help="Email body") + + # Get recent emails command + get_parser = subparsers.add_parser("get-recent", help="Get recent 10 emails") + get_parser.add_argument("--email-address", required=True, help="Email address to check") + + args = parser.parse_args() + + if args.command == "send": + result = send_email(args.sender, args.recipient, args.title, args.body) + print(result) + elif args.command == "get-recent": + get_recent_emails(args.email_address) + else: + parser.print_help() + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/MCP_and_tools/Skills_and_Tools/README.md b/MCP_and_tools/Skills_and_Tools/README.md new file mode 100644 index 0000000..ce218cf --- /dev/null +++ b/MCP_and_tools/Skills_and_Tools/README.md @@ -0,0 +1 @@ +LLM Agent Skills (Modular AI Capabilities) Introduced in 2025, these are portable, version-controlled folders containing instructions, scripts, and resources that large language models (LLMs) can load to perform specialized tasks. Components: A skill consists of a SKILL.md file (metadata/instructions) and optional /scripts, /references, and /assets folders.Discovery & Loading: Agents (like Claude Code or Copilot) automatically discover skills from local or project directories and load them only when relevant.Use Cases:Tool Use: Enabling agents to execute Bash or Python scripts for complex tasks.Domain Expertise: Specialized knowledge, such as legal review or data analysis.Skill Chaining: Using the output of one skill as the input for another (e.g., Researcher \(\rightarrow \) Writer).Key Platforms: Supported by Claude, GitHub Copilot (VS Code), and Cursor.  \ No newline at end of file diff --git a/README.md b/README.md index e387795..df857ea 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,16 @@ LlamaIndex: While known for RAG, it is now a top-tier agent framework for data-h Mem0: Provides a "long-term memory" layer for agents, allowing them to remember user preferences and past interactions across different sessions. -5. Observability & Debugging +5. Skills and tools +Key Aspects of Agent Skills: +Definition & Structure: A skill is a folder containing a SKILL.md file (metadata and instructions) and optional scripts or resources. The tool is the code that the skill might require to execute some actions +Purpose: They provide "procedural knowledge" rather than general intelligence, enabling agents to execute specific workflows. +Portability & Modularity: Skills are designed to work across different AI agent platforms, allowing for easy sharing and reusability. +Example: A skill could be a set of instructions instructing an AI to create a summary of a document in a specific corporate template. +Agent Skills vs. Other AI Concepts: +vs. MCP (Model Context Protocol): MCP is the "plumbing" (connecting to databases), while skills are the "expertise" (how to use that data). + +6. Observability & Debugging You cannot fix an agent if you don't know where its reasoning went wrong. LangSmith: Essential for tracing exactly what an agent "thought" at each step.