├── .env.example ├── README.md ├── docker-compose.yml ├── requirements.txt └── src └── webhook_server.py /.env.example: -------------------------------------------------------------------------------- 1 | LINE_CHANNEL_SECRET=your_channel_secret_here 2 | LINE_ACCESS_TOKEN=your_access_token_here 3 | CLOUDFLARE_TOKEN=your_cloudflare_tunnel_token_here 4 | SERVER_PORT=8000 5 | MESSAGES_FILE=/app/data/messages.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python LINE MCP Server 2 | 3 | [![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)](https://github.com/amornpan/py-mcp-line) 4 | [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org) 5 | [![MCP](https://img.shields.io/badge/MCP-1.2.0-green.svg)](https://github.com/modelcontextprotocol) 6 | [![FastAPI](https://img.shields.io/badge/FastAPI-0.104.1-teal.svg)](https://fastapi.tiangolo.com) 7 | [![License](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) 8 | 9 | A Model Context Protocol server implementation in Python that provides access to LINE Bot messages. This server enables Language Models to read and analyze LINE conversations through a standardized interface. 10 | 11 | ## Features 12 | 13 | ### Core Functionality 14 | * Asynchronous operation using Python's `asyncio` 15 | * Environment-based configuration using `python-dotenv` 16 | * Comprehensive logging system 17 | * LINE Bot webhook event handling 18 | * Message storage in JSON format 19 | * FastAPI integration for API endpoints 20 | * Pydantic models for data validation 21 | * Support for text, sticker, and image messages 22 | 23 | ## Prerequisites 24 | 25 | * Python 3.8+ 26 | * Required Python packages: 27 | * fastapi 28 | * pydantic 29 | * python-dotenv 30 | * mcp-server 31 | * line-bot-sdk 32 | * uvicorn 33 | 34 | ## Installation 35 | 36 | ```bash 37 | git clone https://github.com/amornpan/py-mcp-line.git 38 | cd py-mcp-line 39 | pip install -r requirements.txt 40 | ``` 41 | 42 | ## Project Structure 43 | 44 | ``` 45 | PY-MCP-LINE/ 46 | ├── src/ 47 | │ └── line/ 48 | │ ├── __init__.py 49 | │ └── server.py 50 | ├── data/ 51 | │ └── messages.json 52 | ├── tests/ 53 | │ ├── __init__.py 54 | │ └── test_line.py 55 | ├── .env 56 | ├── .env.example 57 | ├── .gitignore 58 | ├── README.md 59 | ├── Dockerfile 60 | └── requirements.txt 61 | ``` 62 | 63 | ### Directory Structure Explanation 64 | * `src/line/` - Main source code directory 65 | * `__init__.py` - Package initialization 66 | * `server.py` - Main server implementation 67 | * `data/` - Data storage directory 68 | * `messages.json` - Stored LINE messages 69 | * `tests/` - Test files directory 70 | * `__init__.py` - Test package initialization 71 | * `test_line.py` - LINE functionality tests 72 | * `.env` - Environment configuration file (not in git) 73 | * `.env.example` - Example environment configuration 74 | * `.gitignore` - Git ignore rules 75 | * `README.md` - Project documentation 76 | * `Dockerfile` - Docker configuration 77 | * `requirements.txt` - Project dependencies 78 | 79 | ## Configuration 80 | 81 | Create a `.env` file in the project root: 82 | 83 | ```env 84 | LINE_CHANNEL_SECRET=your_channel_secret 85 | LINE_ACCESS_TOKEN=your_access_token 86 | SERVER_PORT=8000 87 | MESSAGES_FILE=data/messages.json 88 | ``` 89 | 90 | ## API Implementation Details 91 | 92 | ### Resource Listing 93 | ```python 94 | @app.list_resources() 95 | async def list_resources() -> list[Resource] 96 | ``` 97 | * Lists available message types from the LINE Bot 98 | * Returns resources with URIs in the format `line:///data` 99 | * Includes resource descriptions and MIME types 100 | 101 | ### Resource Reading 102 | ```python 103 | @app.read_resource() 104 | async def read_resource(uri: AnyUrl) -> str 105 | ``` 106 | * Reads messages of the specified type 107 | * Accepts URIs in the format `line:///data` 108 | * Returns messages in JSON format 109 | * Supports filtering by date, user, or content 110 | 111 | ## Usage with Claude Desktop 112 | 113 | Add to your Claude Desktop configuration: 114 | 115 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 116 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 117 | 118 | ```json 119 | { 120 | "mcpServers": { 121 | "line": { 122 | "command": "python", 123 | "args": [ 124 | "server.py" 125 | ], 126 | "env": { 127 | "LINE_CHANNEL_SECRET": "your_channel_secret", 128 | "LINE_ACCESS_TOKEN": "your_access_token", 129 | "SERVER_PORT": "8000", 130 | "MESSAGES_FILE": "data/messages.json" 131 | } 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | ## Error Handling 138 | 139 | The server implements comprehensive error handling for: 140 | * Webhook validation failures 141 | * Message storage errors 142 | * Resource access errors 143 | * URI validation 144 | * LINE API response errors 145 | 146 | All errors are logged and returned with appropriate error messages. 147 | 148 | ## Security Features 149 | 150 | * Environment variable based configuration 151 | * LINE message signature validation 152 | * Proper error handling 153 | * Input validation through Pydantic 154 | 155 | ## Contact Information 156 | 157 | ### Amornpan Phornchaicharoen 158 | 159 | [![Email](https://img.shields.io/badge/Email-amornpan%40gmail.com-red?style=flat-square&logo=gmail)](mailto:amornpan@gmail.com) 160 | [![LinkedIn](https://img.shields.io/badge/LinkedIn-Amornpan-blue?style=flat-square&logo=linkedin)](https://www.linkedin.com/in/amornpan/) 161 | [![HuggingFace](https://img.shields.io/badge/🤗%20Hugging%20Face-amornpan-yellow?style=flat-square)](https://huggingface.co/amornpan) 162 | [![GitHub](https://img.shields.io/badge/GitHub-amornpan-black?style=flat-square&logo=github)](https://github.com/amornpan) 163 | 164 | Feel free to reach out to me if you have any questions about this project or would like to collaborate! 165 | 166 | --- 167 | *Made with ❤️ by Amornpan Phornchaicharoen* 168 | 169 | ## Author 170 | 171 | Amornpan Phornchaicharoen 172 | 173 | ## Requirements 174 | 175 | Create a `requirements.txt` file with: 176 | 177 | ``` 178 | fastapi>=0.104.1 179 | pydantic>=2.10.6 180 | uvicorn>=0.34.0 181 | python-dotenv>=1.0.1 182 | line-bot-sdk>=3.5.0 183 | anyio>=4.5.0 184 | mcp==1.2.0 185 | ``` 186 | 187 | These versions have been tested and verified to work together. The key components are: 188 | * `fastapi` and `uvicorn` for the API server 189 | * `pydantic` for data validation 190 | * `line-bot-sdk` for LINE Bot integration 191 | * `mcp` for Model Context Protocol implementation 192 | * `python-dotenv` for environment configuration 193 | * `anyio` for asynchronous I/O support 194 | 195 | ## Acknowledgments 196 | 197 | * LINE Developers for the LINE Messaging API 198 | * Model Context Protocol community 199 | * Python FastAPI community 200 | * Contributors to the python-dotenv project 201 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | line-webhook-server: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: line-webhook-server 9 | restart: unless-stopped 10 | ports: 11 | - "8000:8000" 12 | environment: 13 | - SERVER_PORT=8000 14 | - MESSAGES_FILE=/app/data/messages.json 15 | # เราไม่ต้องใช้ CLOUDFLARE_TOKEN ใน container แล้ว 16 | # เนื่องจากใช้ Windows Service ที่ติดตั้งแล้ว 17 | volumes: 18 | - ./data:/app/data 19 | healthcheck: 20 | test: ["CMD", "curl", "-f", "http://localhost:8000/health"] 21 | interval: 30s 22 | timeout: 10s 23 | retries: 3 24 | start_period: 5s 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.103.1 2 | line-bot-sdk==3.5.0 3 | python-dotenv==1.0.0 4 | uvicorn==0.23.2 5 | aiohttp==3.8.5 6 | pydantic==2.3.0 -------------------------------------------------------------------------------- /src/webhook_server.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, HTTPException 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from linebot.v3 import WebhookHandler 4 | from linebot.v3.messaging import Configuration, ApiClient, MessagingApi 5 | from linebot.v3.exceptions import InvalidSignatureError 6 | from dotenv import load_dotenv 7 | import os 8 | import json 9 | import logging 10 | from datetime import datetime 11 | import signal 12 | import sys 13 | 14 | # Set up logging 15 | logging.basicConfig( 16 | level=logging.INFO, 17 | format='[%(asctime)s] %(levelname)s: %(message)s', 18 | datefmt='%Y-%m-%d %H:%M:%S' 19 | ) 20 | logger = logging.getLogger(__name__) 21 | 22 | # Load environment variables 23 | load_dotenv() 24 | LINE_CHANNEL_SECRET = os.getenv('LINE_CHANNEL_SECRET') 25 | LINE_ACCESS_TOKEN = os.getenv('LINE_ACCESS_TOKEN') 26 | 27 | if not LINE_CHANNEL_SECRET or not LINE_ACCESS_TOKEN: 28 | logger.error("LINE_CHANNEL_SECRET and LINE_ACCESS_TOKEN must be set in .env file") 29 | sys.exit(1) 30 | 31 | # Initialize FastAPI app 32 | app = FastAPI(title="LINE Webhook Server", 33 | description="Receives and processes LINE webhook events", 34 | version="1.0.0") 35 | 36 | # Add CORS middleware 37 | app.add_middleware( 38 | CORSMiddleware, 39 | allow_origins=["*"], # Adjust in production 40 | allow_credentials=True, 41 | allow_methods=["*"], 42 | allow_headers=["*"], 43 | ) 44 | 45 | # Initialize LINE API 46 | configuration = Configuration(access_token=LINE_ACCESS_TOKEN) 47 | handler = WebhookHandler(LINE_CHANNEL_SECRET) 48 | line_api = MessagingApi(ApiClient(configuration)) 49 | 50 | # Define message storage path 51 | MESSAGES_FILE = os.getenv('MESSAGES_FILE', '/app/data/messages.json') 52 | 53 | def save_message(timestamp: str, user_id: str, message_type: str, content: str): 54 | """Save message to messages.json with improved error handling""" 55 | try: 56 | # Ensure directory exists 57 | os.makedirs(os.path.dirname(MESSAGES_FILE), exist_ok=True) 58 | 59 | # Load existing messages 60 | messages = {'messages': []} 61 | if os.path.exists(MESSAGES_FILE) and os.path.getsize(MESSAGES_FILE) > 0: 62 | try: 63 | with open(MESSAGES_FILE, 'r', encoding='utf-8') as f: 64 | messages = json.load(f) 65 | except json.JSONDecodeError: 66 | logger.error("Invalid JSON in messages file. Creating new file.") 67 | 68 | # Add new message 69 | messages['messages'].append({ 70 | 'timestamp': timestamp, 71 | 'user_id': user_id, 72 | 'type': message_type, 73 | 'content': content 74 | }) 75 | 76 | # Save to file (atomic write) 77 | temp_file = f"{MESSAGES_FILE}.tmp" 78 | with open(temp_file, 'w', encoding='utf-8') as f: 79 | json.dump(messages, f, indent=2, ensure_ascii=False) 80 | 81 | os.replace(temp_file, MESSAGES_FILE) 82 | logger.info("Message saved: %s", content[:30] + "..." if len(content) > 30 else content) 83 | return True 84 | except Exception as e: 85 | logger.error("Error saving message: %s", str(e)) 86 | return False 87 | 88 | @app.post("/webhook") 89 | async def webhook(request: Request): 90 | """Handle incoming webhook events from LINE""" 91 | try: 92 | # Get signature and body 93 | signature = request.headers.get('X-Line-Signature', '') 94 | body = await request.body() 95 | body_str = body.decode('utf-8') 96 | 97 | # Log request receipt 98 | logger.info("Received webhook request") 99 | 100 | # Verify signature (important security step) 101 | try: 102 | handler.handle(body_str, signature) 103 | except InvalidSignatureError: 104 | logger.warning("Invalid signature") 105 | raise HTTPException(status_code=403, detail="Invalid signature") 106 | 107 | # Process message 108 | body_json = json.loads(body_str) 109 | events = body_json.get("events", []) 110 | 111 | if not events: 112 | return {"status": "OK", "message": "No events"} 113 | 114 | # Process each event (currently only handling the first one) 115 | event = events[0] 116 | if event["type"] != "message": 117 | return {"status": "OK", "message": f"Non-message event received: {event['type']}"} 118 | 119 | # Extract message details 120 | timestamp = datetime.fromtimestamp(event["timestamp"] / 1000).strftime('%Y-%m-%d %H:%M:%S') 121 | user_id = event["source"]["userId"] 122 | message_type = event["message"]["type"] 123 | 124 | # Handle different message types 125 | if message_type == "text": 126 | content = event["message"].get("text", "") 127 | elif message_type == "sticker": 128 | sticker_id = event["message"].get("stickerId", "unknown") 129 | package_id = event["message"].get("packageId", "unknown") 130 | content = f"[Sticker: package={package_id}, sticker={sticker_id}]" 131 | elif message_type == "image": 132 | content = "[Image message]" 133 | else: 134 | content = f"[{message_type} message]" 135 | 136 | # Save message 137 | if save_message(timestamp, user_id, message_type, content): 138 | return {"status": "OK", "message": "Message processed successfully"} 139 | else: 140 | return {"status": "Error", "message": "Failed to save message"} 141 | 142 | except json.JSONDecodeError as e: 143 | logger.error("Invalid JSON: %s", str(e)) 144 | return {"status": "Error", "message": "Invalid JSON payload"} 145 | except Exception as e: 146 | error_msg = f"Webhook error: {str(e)}" 147 | logger.error(error_msg) 148 | return {"status": "Error", "message": error_msg} 149 | 150 | @app.get("/") 151 | async def root(): 152 | """Healthcheck endpoint""" 153 | return { 154 | "status": "LINE Webhook Server is running", 155 | "version": "1.0.0", 156 | "health": "OK" 157 | } 158 | 159 | @app.get("/health") 160 | async def health_check(): 161 | """Health check endpoint for monitoring""" 162 | return {"status": "healthy"} 163 | 164 | # Graceful shutdown handler 165 | def handle_exit(signum, frame): 166 | logger.info("Received shutdown signal. Exiting gracefully...") 167 | sys.exit(0) 168 | 169 | if __name__ == "__main__": 170 | # Register signal handlers for graceful shutdown 171 | signal.signal(signal.SIGTERM, handle_exit) 172 | signal.signal(signal.SIGINT, handle_exit) 173 | 174 | # Run the server 175 | import uvicorn 176 | port = int(os.getenv('SERVER_PORT', 8000)) 177 | uvicorn.run("webhook_server:app", host="0.0.0.0", port=port) --------------------------------------------------------------------------------