Add PyInstaller build support for deployment

- Add PyInstaller spec files (onefile and onedir modes)
- Add build scripts for executable creation
- Add MCP hooks for PyInstaller compatibility
- Add test script for MCP server validation
- Add Windsurf integration setup guide
- Update main.py with better error reporting
- Successfully tested MCP server works with ONEDIR build (45MB)

Features:
- Single executable deployment without Python dependency
- ONEDIR mode preserves stdio communication for MCP
- Build scripts with deployment instructions
- Windsurf config integration guide

Files added:
- mt5-quant.spec (PyInstaller spec)
- mt5-quant-onedir.spec (directory mode)
- scripts/build-executable.sh (onefile)
- scripts/build-executable-onedir.sh (directory)
- hooks/hook-mcp.py (PyInstaller hook)
- test_mcp.py (validation script)
- WINDSURF_SETUP.md (integration guide)
This commit is contained in:
Devid HW
2026-04-18 13:00:28 +07:00
parent 3a52ccf398
commit 3cfd374160
8 changed files with 674 additions and 2 deletions
+83
View File
@@ -0,0 +1,83 @@
# Windsurf MCP Integration Setup
## Quick Setup
### 1. Build Executable
```bash
bash scripts/build-executable-onedir.sh
```
### 2. Configure Windsurf
Edit `~/.windsurf/config.yaml`:
```yaml
mcpServers:
mt5-quant:
command: /Users/masdevid/jobs/mt5-mcp/dist/mt5-quant/mt5-quant
```
Or dengan environment variable:
```yaml
mcpServers:
mt5-quant:
command: /Users/masdevid/jobs/mt5-mcp/dist/mt5-quant/mt5-quant
env:
MT5_MCP_HOME: /Users/masdevid/jobs/mt5-mcp
```
### 3. Restart Windsurf
Close dan reopen Windsurf untuk load MCP server.
### 4. Verify
Di Windsurf chat, test dengan:
```
Run verify_setup
```
## Deployment ke Multiple Machines
### Build untuk Distribution
```bash
# Build
bash scripts/build-executable-onedir.sh
# Create tarball
tar -czf mt5-quant-macos-arm64.tar.gz -C dist mt5-quant
# Deploy ke remote server
scp mt5-quant-macos-arm64.tar.gz user@server:~/
ssh user@server "tar -xzf mt5-quant-macos-arm64.tar.gz -C /opt/"
ssh user@server "ln -s /opt/mt5-quant/mt5-quant /usr/local/bin/"
# Copy config
scp -r config/mt5-quant.yaml user@server:~/.config/mt5-quant/config/
```
### Target Machine Requirements
- MetaTrader 5 installed (via Wine/CrossOver)
- Config file di `~/.config/mt5-quant/config/mt5-quant.yaml`
- **NO Python required!**
### Windsurf Config di Target Machine
```yaml
mcpServers:
mt5-quant:
command: /usr/local/bin/mt5-quant
```
## Troubleshooting
### MCP server not appearing
1. Check Windsurf logs: `~/.windsurf/logs/`
2. Verify executable path is absolute
3. Test executable manually: `./dist/mt5-quant/mt5-quant --help`
### Config not found
Set `MT5_MCP_HOME` environment variable atau pastikan config di default location:
- macOS: `~/.config/mt5-quant/config/mt5-quant.yaml`
### Permission denied
```bash
chmod +x /path/to/mt5-quant
```
+18
View File
@@ -0,0 +1,18 @@
# PyInstaller hook for mcp package
# Ensures all mcp submodules are included
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
hiddenimports = collect_submodules('mcp')
datas = collect_data_files('mcp')
# Also ensure anyio dependencies are included
hiddenimports += [
'anyio',
'anyio.streams',
'anyio.streams.memory',
'anyio.streams.text',
'anyio._backends',
'anyio._backends._asyncio',
'anyio._backends._trio',
]
+131
View File
@@ -0,0 +1,131 @@
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller spec file for MT5-Quant MCP Server (onedir mode)
This version creates a directory with the executable + dependencies
Better for MCP stdio communication than onefile mode
Build:
pyinstaller mt5-quant-onedir.spec
Output:
dist/mt5-quant/ directory with executable inside
"""
import os
import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_all
block_cipher = None
# Project root - the build script runs from project root, so use cwd
root = Path(os.getcwd()).resolve()
# Collect entire mcp package (datas, binaries, hiddenimports)
mcp_datas, mcp_binaries, mcp_hiddenimports = collect_all('mcp')
# Data files to include
# Format: (source_path, dest_dir_in_bundle)
datas = [
# Include scripts directory (bash pipeline scripts)
(str(root / 'scripts'), 'scripts'),
# Include docs if needed
(str(root / 'docs'), 'docs'),
] + mcp_datas
# Hidden imports (modules that are imported dynamically or might be missed)
hiddenimports = mcp_hiddenimports + [
# MCP stdio - explicitly include for PyInstaller
'mcp.server.stdio',
'mcp.shared.memory',
'mcp.shared.session',
'mcp.server.models',
# Analytics modules (local project modules)
'analytics',
'analytics.extract',
'analytics.analyze',
'analytics.optimize_parser',
# Other dependencies
'pydantic',
'pydantic.v1',
'pydantic.v1.fields',
'pydantic.v1.main',
'pydantic_core',
'yaml',
'_yaml',
'yaml.constructor',
'yaml.representer',
'yaml.cyaml',
'anyio',
'anyio.streams',
'anyio.streams.memory',
'anyio.streams.text',
# Other stdlib that might be dynamically imported
'xml.etree.ElementTree',
'html.parser',
]
a = Analysis(
['server/main.py'],
pathex=[str(root), str(root / 'server'), str(root / 'analytics')],
binaries=mcp_binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
# Exclude unnecessary packages to reduce binary size
'matplotlib',
'PIL',
'numpy',
'pandas',
'scipy',
'tkinter',
'PyQt5',
'PyQt6',
'wx',
'test',
'unittest',
'pydoc',
'doctest',
'email',
'http.server',
'ftplib',
'telnetlib',
'ssl',
'sqlite3',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='mt5-quant',
debug=False,
bootloader_ignore_signals=False,
strip=True,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
# For onedir mode, we need to use COLLECT
# But actually, on macOS/Linux, just having the EXE without onefile=True creates a directory
# Wait, actually we need to check PyInstaller version behavior
# Modern PyInstaller creates onedir by default for EXE + COLLECT
+161
View File
@@ -0,0 +1,161 @@
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller spec file for MT5-Quant MCP Server
Build:
pyinstaller mt5-quant.spec
Output:
dist/mt5-quant (single executable)
"""
import os
import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_all
block_cipher = None
# Project root - the build script runs from project root, so use cwd
root = Path(os.getcwd()).resolve()
# Collect entire mcp package (datas, binaries, hiddenimports)
mcp_datas, mcp_binaries, mcp_hiddenimports = collect_all('mcp')
# Data files to include
# Format: (source_path, dest_dir_in_bundle)
datas = [
# Include scripts directory (bash pipeline scripts)
(str(root / 'scripts'), 'scripts'),
# Include docs if needed
(str(root / 'docs'), 'docs'),
] + mcp_datas
# Hidden imports (modules that are imported dynamically or might be missed)
# Note: mcp_hiddenimports already includes all mcp submodules from collect_all()
hiddenimports = mcp_hiddenimports + [
# MCP package - all submodules
'mcp',
'mcp.types',
'mcp.server',
'mcp.server.stdio',
'mcp.server.models',
'mcp.server.session',
'mcp.shared',
'mcp.shared.memory',
'mcp.shared.session',
'mcp.shared.exceptions',
'mcp.shared.context',
'mcp.shared.progress',
'mcp.shared.version',
'mcp.client',
'mcp.cli',
# Analytics modules (local project modules)
'analytics',
'analytics.extract',
'analytics.analyze',
'analytics.optimize_parser',
# Other dependencies
'pydantic',
'pydantic.v1',
'pydantic.v1.fields',
'pydantic.v1.main',
'pydantic_core',
'yaml',
'_yaml',
'yaml.constructor',
'yaml.representer',
'yaml.cyaml',
'anyio',
'anyio.streams',
'anyio.streams.memory',
'anyio.streams.text',
'anyio._backends',
'anyio._backends._asyncio',
# Other stdlib that might be dynamically imported
'xml.etree.ElementTree',
'html.parser',
'select',
'ssl',
'_ssl',
'certifi',
'email',
'email.parser',
'email.message',
'importlib.metadata',
]
a = Analysis(
['server/main.py'],
pathex=[str(root), str(root / 'server'), str(root / 'analytics')],
binaries=mcp_binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[str(root / 'hooks')],
hooksconfig={
'mcp': {
'include_all': True,
}
},
runtime_hooks=[],
excludes=[
# Exclude unnecessary packages to reduce binary size
'matplotlib',
'PIL',
'numpy',
'pandas',
'scipy',
'tkinter',
'PyQt5',
'PyQt6',
'wx',
'test',
'unittest',
'pydoc',
'doctest',
# 'email', # Required by pydantic -> mcp
# 'http.server',
# 'ftplib',
# 'telnetlib',
# 'ssl', # Required by anyio -> mcp
'sqlite3',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
# Create the EXE without including binaries/datas (onedir mode)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='mt5-quant',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
# Collect all files into directory (onedir mode)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=False,
upx_exclude=[],
name='mt5-quant'
)
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
# Build MT5-Quant as a directory bundle (onedir mode) using PyInstaller
# This mode preserves stdin/stdout better for MCP communication
# Output: dist/mt5-quant/ directory
set -e
# Get script directory and project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "$PROJECT_ROOT"
echo "=== MT5-Quant PyInstaller Build (ONEDIR Mode) ==="
echo "Project root: $PROJECT_ROOT"
echo ""
# Determine Python interpreter
if [ -f "$PROJECT_ROOT/.venv/bin/python3" ]; then
PYTHON="$PROJECT_ROOT/.venv/bin/python3"
elif [ -f "$PROJECT_ROOT/.venv/bin/python" ]; then
PYTHON="$PROJECT_ROOT/.venv/bin/python"
else
PYTHON="python3"
fi
echo "Using Python: $PYTHON"
echo ""
# Ensure dependencies are installed
echo "Installing dependencies..."
$PYTHON -m pip install pyinstaller mcp pyyaml typer --quiet 2>/dev/null || true
# Clean previous builds
echo "Cleaning previous builds..."
rm -rf "$PROJECT_ROOT/build" "$PROJECT_ROOT/dist"
# Build the executable in onedir mode
echo "Building executable in ONEDIR mode (this may take 1-2 minutes)..."
cd "$PROJECT_ROOT"
# Use onedir mode (no --onefile flag)
# Note: mode is controlled by the .spec file
$PYTHON -m PyInstaller \
--clean \
--noconfirm \
--distpath "$PROJECT_ROOT/dist" \
--workpath "$PROJECT_ROOT/build" \
"$PROJECT_ROOT/mt5-quant.spec"
# Report results
echo ""
echo "=== Build Complete ==="
echo ""
echo "Executable location:"
ls -la "$PROJECT_ROOT/dist/mt5-quant/" | head -10
echo ""
echo "Main executable:"
ls -lh "$PROJECT_ROOT/dist/mt5-quant/mt5-quant"
echo ""
echo "Total size:"
du -sh "$PROJECT_ROOT/dist/mt5-quant/"
echo ""
echo "To test:"
echo " ./dist/mt5-quant/mt5-quant"
echo ""
echo "To register with Claude Code:"
echo " claude mcp add MT5-Quant -- $(pwd)/dist/mt5-quant/mt5-quant"
echo ""
echo "=== Deployment to Multiple Machines ==="
echo ""
echo "1. Copy entire directory to target machines:"
echo " scp -r dist/mt5-quant/ user@server:/opt/"
echo " ssh user@server ln -s /opt/mt5-quant/mt5-quant /usr/local/bin/"
echo ""
echo "2. Or create tarball for distribution:"
echo " tar -czf mt5-quant-macos-arm64.tar.gz -C dist mt5-quant"
echo ""
echo "Requirements on target machine:"
echo " - MetaTrader 5 installed (via Wine on macOS/Linux)"
echo " - Config file at ~/.config/mt5-quant/config/mt5-quant.yaml"
echo " - NO Python installation required!"
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
# Build MT5-Quant as a single executable using PyInstaller
# Output: dist/mt5-quant
set -e
# Get script directory and project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "$PROJECT_ROOT"
echo "=== MT5-Quant PyInstaller Build ==="
echo "Project root: $PROJECT_ROOT"
echo ""
# Determine Python interpreter
if [ -f "$PROJECT_ROOT/.venv/bin/python3" ]; then
PYTHON="$PROJECT_ROOT/.venv/bin/python3"
elif [ -f "$PROJECT_ROOT/.venv/bin/python" ]; then
PYTHON="$PROJECT_ROOT/.venv/bin/python"
else
PYTHON="python3"
fi
echo "Using Python: $PYTHON"
echo ""
# Ensure dependencies are installed
echo "Installing dependencies..."
$PYTHON -m pip install pyinstaller mcp pyyaml --quiet
# Clean previous builds
echo "Cleaning previous builds..."
rm -rf "$PROJECT_ROOT/build" "$PROJECT_ROOT/dist"
# Build the executable
echo "Building executable (this may take 1-2 minutes)..."
cd "$PROJECT_ROOT"
$PYTHON -m PyInstaller \
--clean \
--noconfirm \
--distpath "$PROJECT_ROOT/dist" \
--workpath "$PROJECT_ROOT/build" \
"$PROJECT_ROOT/mt5-quant.spec"
# Report results
echo ""
echo "=== Build Complete ==="
echo ""
echo "Executable location:"
ls -lh dist/mt5-quant
echo ""
echo "Size breakdown:"
du -sh dist/ 2>/dev/null || echo " dist/: $(du -sh dist/mt5-quant 2>/dev/null | cut -f1)"
echo ""
echo "To test:"
echo " ./dist/mt5-quant --help"
echo ""
echo "To register with Claude Code:"
echo " claude mcp add MT5-Quant -- $(pwd)/dist/mt5-quant"
echo ""
echo "=== Deployment to Multiple Machines ==="
echo ""
echo "1. Copy this single binary to target machines:"
echo " scp dist/mt5-quant user@server:/usr/local/bin/"
echo " ssh user@server chmod +x /usr/local/bin/mt5-quant"
echo ""
echo "2. Copy config directory (includes mt5-quant.yaml):"
echo " scp -r config/ user@server:~/.config/mt5-quant/"
echo ""
echo "3. Or use bundled config (minimal):"
echo " MT5_MCP_HOME=/path/to/config ./mt5-quant"
echo ""
echo "4. Register on target machine:"
echo " claude mcp add MT5-Quant -- /usr/local/bin/mt5-quant"
echo ""
echo "Requirements on target machine:"
echo " - MetaTrader 5 installed (via Wine on macOS/Linux)"
echo " - Config file at ~/.config/mt5-quant/config/mt5-quant.yaml"
echo " - NO Python installation required!"
+4 -2
View File
@@ -23,8 +23,10 @@ try:
import mcp.server.stdio
import mcp.types as types
from mcp.server import Server
except ImportError:
print("ERROR: mcp package not installed. Run: pip install mcp", file=sys.stderr)
except ImportError as e:
print(f"ERROR: Failed to import MCP package: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
sys.exit(1)
# Config/ROOT_DIR resolution (priority order):
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Test script for MCP server"""
import subprocess
import json
import sys
def test_mcp_server(command):
"""Test if an MCP server responds correctly"""
print(f"Testing: {' '.join(command)}")
print("-" * 50)
proc = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1 # Line buffered
)
# Send initialize request
init_request = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test", "version": "1.0"}
}
}
print(f"Sending initialize request...")
proc.stdin.write(json.dumps(init_request) + "\n")
proc.stdin.flush()
# Read response with timeout
import select
import time
start = time.time()
response_lines = []
while time.time() - start < 5: # 5 second timeout
ready, _, _ = select.select([proc.stdout], [], [], 0.5)
if ready:
line = proc.stdout.readline()
if line:
response_lines.append(line.strip())
print(f"Received: {line.strip()}")
break
if not response_lines:
print("ERROR: No response received within 5 seconds")
proc.terminate()
proc.wait()
return False
# Parse response
try:
response = json.loads(response_lines[0])
if response.get("id") == 1 and "result" in response:
print("SUCCESS: MCP server responded correctly")
# Try to get tools list
tools_request = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
proc.stdin.write(json.dumps(tools_request) + "\n")
proc.stdin.flush()
start = time.time()
while time.time() - start < 5:
ready, _, _ = select.select([proc.stdout], [], [], 0.5)
if ready:
line = proc.stdout.readline()
if line:
print(f"Tools response: {line.strip()[:200]}...")
break
proc.terminate()
proc.wait()
return True
else:
print(f"ERROR: Unexpected response: {response}")
proc.terminate()
proc.wait()
return False
except json.JSONDecodeError as e:
print(f"ERROR: Invalid JSON response: {e}")
proc.terminate()
proc.wait()
return False
if __name__ == "__main__":
# Test Python source
print("\n=== Testing Python Source ===")
python_ok = test_mcp_server([sys.executable, "server/main.py"])
# Test executable
print("\n=== Testing PyInstaller Executable ===")
exe_ok = test_mcp_server(["./dist/mt5-quant"])
print("\n" + "=" * 50)
print(f"Python source: {'OK' if python_ok else 'FAILED'}")
print(f"PyInstaller exe: {'OK' if exe_ok else 'FAILED'}")