Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7a1df24
Added github action
diego-ferrand Oct 22, 2025
f5f4a74
Fixed build.py typo
diego-ferrand Oct 22, 2025
8d8fb3f
Set branch to deploy
diego-ferrand Oct 22, 2025
01caa83
Set branch to deploy
diego-ferrand Oct 22, 2025
90d324d
Remove public runner
diego-ferrand Oct 22, 2025
6f7b9fb
Added branch to artifact deploy
diego-ferrand Oct 22, 2025
38a3c82
Remove dist from git ignore
diego-ferrand Oct 22, 2025
5f98c6a
[skip ci] Update all binary artifacts
actions-user Oct 22, 2025
dec1dc9
add docker build
diego-ferrand Oct 23, 2025
d2635ab
Merge branch 'GITHUB_ACTIONS' of https://github.com/PerfectoCode/perf…
diego-ferrand Oct 23, 2025
03753a6
[skip ci] Update all binary artifacts
actions-user Oct 23, 2025
1898261
Add branch deploy
diego-ferrand Oct 23, 2025
28bb0c8
Merge branch 'GITHUB_ACTIONS' of https://github.com/PerfectoCode/perf…
diego-ferrand Oct 23, 2025
40c53e5
[skip ci] Update all binary artifacts
actions-user Oct 23, 2025
d6e3bea
Removed push trigger
diego-ferrand Oct 24, 2025
99b56b5
[skip ci] Update all binary artifacts
actions-user Oct 24, 2025
9bd8b11
Removed another push condition
diego-ferrand Oct 24, 2025
f894762
Merge branch 'GITHUB_ACTIONS' of https://github.com/PerfectoCode/perf…
diego-ferrand Oct 24, 2025
5e08688
[skip ci] Update all binary artifacts
actions-user Oct 24, 2025
8c66b62
Merge branch 'main' of https://github.com/PerfectoCode/perfecto-mcp i…
diego-ferrand Oct 24, 2025
262db89
Fix workflow filename: build.yaml -> build.yml
diego-ferrand Oct 24, 2025
b47552c
Merge branch 'GITHUB_ACTIONS' of https://github.com/PerfectoCode/perf…
diego-ferrand Oct 24, 2025
8977aa9
[skip ci] Update all binary artifacts
actions-user Oct 24, 2025
698e027
Merge branch 'main' of https://github.com/PerfectoCode/perfecto-mcp i…
diego-ferrand Oct 24, 2025
e6b3209
Merge branch 'GITHUB_ACTIONS' of https://github.com/PerfectoCode/perf…
diego-ferrand Oct 24, 2025
fd5b6ed
Remove branch from pipeline
diego-ferrand Oct 24, 2025
308e5a5
Merge branch 'main' of https://github.com/PerfectoCode/perfecto-mcp i…
diego-ferrand Oct 27, 2025
ab503cd
Apply same logic from bzm-mcp
diego-ferrand Jan 12, 2026
4276515
Merge branch 'main' of https://github.com/PerfectoCode/perfecto-mcp i…
diego-ferrand Jan 12, 2026
0189fec
Added github workflow
diego-ferrand Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
- os: ubuntu-24.04
name: linux
arch: amd64
- os: ubuntu-24.04-arm
name: linux
arch: arm64
- os: windows-latest
name: windows
arch: amd64
Expand Down Expand Up @@ -114,14 +117,22 @@ jobs:
echo "Copied Windows AMD64 binary"
fi
if [ -d "artifacts/perfecto-mcp-macos-arm64" ]; then
cp artifacts/perfecto-mcp-macos-arm64/* dist/
chmod +x dist/perfecto-mcp-macos-arm64
echo "Copied macOS ARM64 binary"
cp -r artifacts/perfecto-mcp-macos-arm64/* dist/
if [ -d "dist/perfecto-mcp-arm64.app" ]; then
echo "Found macOS ARM64 .app bundle"
else
chmod +x dist/perfecto-mcp-macos-arm64 2>/dev/null || true
echo "Copied macOS ARM64 binary"
fi
fi
if [ -d "artifacts/perfecto-mcp-macos-amd64" ]; then
cp artifacts/perfecto-mcp-macos-amd64/* dist/
chmod +x dist/perfecto-mcp-macos-amd64
echo "Copied macOS AMD64 binary"
cp -r artifacts/perfecto-mcp-macos-amd64/* dist/
if [ -d "dist/perfecto-mcp-amd64.app" ]; then
echo "Found macOS AMD64 .app bundle"
else
chmod +x dist/perfecto-mcp-macos-amd64 2>/dev/null || true
echo "Copied macOS AMD64 binary"
fi
fi

echo "Final dist/ contents:"
Expand Down
150 changes: 135 additions & 15 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"""Build script for creating PyInstaller binary."""
import os
import platform
import shutil
import subprocess
import tomllib
from datetime import date
from pathlib import Path
Expand All @@ -11,6 +13,12 @@
sep = os.pathsep


def clean_build():
build_dir = Path('build')
if build_dir.exists():
shutil.rmtree(build_dir)


def build_version_file():
pyproject = Path(__file__).parent / "pyproject.toml"
with open(pyproject, "rb") as f:
Expand Down Expand Up @@ -57,38 +65,150 @@ def build_version_file():
f.write(TEMPLATE.strip())


def build():
"""Build the binary using PyInstaller."""
system = platform.system().lower()
suffix = '.exe' if system == 'windows' else ''
arch = platform.machine().lower()

# Map architecture names to Docker-compatible format
def normalize_architecture(arch: str) -> str:
if arch in ['x86_64', 'amd64']:
arch = 'amd64'
return 'amd64'
elif arch in ['aarch64', 'arm64']:
arch = 'arm64'
return 'arm64'
elif arch.startswith('arm'):
arch = 'arm64' # Assume ARM64 for Docker compatibility
return 'arm64'
return arch

system = "macos" if system == 'darwin' else system
name = f'perfecto-mcp-{system}-{arch}{suffix}'

icon = 'app.ics' if system == 'macos' else 'app.ico'
def normalize_system_name(system: str) -> str:
return "macos" if system == 'darwin' else system


def get_binary_name(system: str, arch: str) -> str:
suffix = '.exe' if system == 'windows' else ''
return f'perfecto-mcp-{system}-{arch}{suffix}'


def get_icon_file(system: str) -> str:
return 'app.icns' if system == 'macos' else 'app.ico'


def run_pyinstaller(name: str, icon: str):
PyInstaller.__main__.run([
'main.py',
'--onefile',
'--version-file=version_info.txt',
f'--add-data=pyproject.toml{sep}.',
f'--add-data=resources/app.png{sep}resources',
f'--add-data=resources{sep}resources',
f'--name={name}',
f'--icon={icon}',
'--clean',
'--noconfirm',
])


def build():
clean_build()

system = normalize_system_name(platform.system().lower())
arch = normalize_architecture(platform.machine().lower())
name = get_binary_name(system, arch)
icon = get_icon_file(system)

run_pyinstaller(name, icon)
clean_build()

if system == "macos":
create_app_bundle(name, arch, dist_dir=Path("dist"))
elif system == "linux":
create_sha256_checksum(name, dist_dir=Path("dist"))


def create_app_directory_structure(app_path: Path) -> Path:
macos_path = app_path / "Contents" / "MacOS"
macos_path.mkdir(parents=True, exist_ok=True)
return macos_path


def copy_binary_to_app(binary_path: Path, target_path: Path):
if not binary_path.exists():
raise FileNotFoundError(f"Binary not found: {binary_path}")
shutil.copy2(binary_path, target_path)
os.chmod(target_path, 0o755)


def create_launcher_script(launcher_path: Path):
launcher_content = """#!/bin/bash
set -e

BIN_DIR="$(cd "$(dirname "$0")" && pwd)"
BIN="$BIN_DIR/perfecto-mcp"

if [ -t 1 ]; then
exec "$BIN" "$@"
else
open -a Terminal "$BIN"
fi
"""
with open(launcher_path, "w", encoding="utf-8") as f:
f.write(launcher_content)
os.chmod(launcher_path, 0o755)


def create_info_plist(plist_path: Path):
info_plist_content = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>launcher.sh</string>

<key>CFBundleIdentifier</key>
<string>com.perfecto.mcp</string>

<key>CFBundleName</key>
<string>Perfecto MCP</string>

<key>CFBundlePackageType</key>
<string>APPL</string>
</dict>
</plist>
"""
with open(plist_path, "w", encoding="utf-8") as f:
f.write(info_plist_content)


def create_app_bundle(binary_name: str, arch: str, dist_dir: Path):
app_name = f"perfecto-mcp-{arch}.app"
app_path = dist_dir / app_name
contents_path = app_path / "Contents"

macos_path = create_app_directory_structure(app_path)

binary_path = dist_dir / binary_name
copy_binary_to_app(binary_path, macos_path / "perfecto-mcp")

create_launcher_script(macos_path / "launcher.sh")
create_info_plist(contents_path / "Info.plist")

binary_path.unlink()
print(f"Created {app_name} in {dist_dir}")


def create_sha256_checksum(binary_name: str, dist_dir: Path):
binary_path = dist_dir / binary_name
checksum_path = dist_dir / f"{binary_name}.sha256"

if not binary_path.exists():
raise FileNotFoundError(f"Binary not found: {binary_path}")

with open(checksum_path, "w") as f:
subprocess.run(
["sha256sum", binary_name],
cwd=dist_dir,
stdout=f,
check=True,
)

print(f"Created {checksum_path.name} in {dist_dir}")


if __name__ == "__main__":
build_version_file()
build()
build()
18 changes: 18 additions & 0 deletions config/version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import importlib.metadata
import os
import subprocess
import sys
import tomllib
from pathlib import Path
Expand Down Expand Up @@ -27,9 +28,26 @@ def get_executable():
else:
return os.path.join(os.path.abspath(Path(__file__).parent.parent), "main.py")


def get_bundle_executable():
executable_path = os.path.realpath(get_executable())
if sys.platform == "darwin":
translocated_path = executable_path
result = subprocess.check_output(
['/usr/bin/security', 'translocate-original-path', translocated_path],
stderr=subprocess.STDOUT
)
original_path = result.decode('utf-8').split('\n')[-2].strip()

return os.path.realpath(os.path.join(original_path, "..", "..", ".."))
else:
return executable_path


def is_uvx():
return "\\uv\\cache\\" in sys.prefix

__version__ = get_version()
__executable__ = get_executable()
__bundle__ = get_bundle_executable()
__uvx__ = is_uvx()
13 changes: 10 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from config.perfecto import SECURITY_TOKEN_FILE_ENV_NAME, SECURITY_TOKEN_ENV_NAME, PERFECTO_CLOUD_NAME_ENV_NAME, \
GITHUB
from config.token import PerfectoToken, PerfectoTokenError
from config.version import __version__, __executable__, __uvx__, get_version
from config.version import __version__, __executable__, __bundle__, __uvx__, get_version
from server import register_tools

PERFECTO_SECURITY_TOKEN_FILE_NAME = "perfecto-security-token.txt"
Expand Down Expand Up @@ -38,7 +38,10 @@ def get_token() -> PerfectoToken:
is_docker = os.getenv('MCP_DOCKER', 'false').lower() == 'true'
token = None

local_security_token_file = os.path.join(os.path.dirname(__executable__), PERFECTO_SECURITY_TOKEN_FILE_NAME)
if sys.platform == "darwin" and __bundle__.endswith(".app"):
local_security_token_file = os.path.join(os.path.dirname(__bundle__), PERFECTO_SECURITY_TOKEN_FILE_NAME)
else:
local_security_token_file = os.path.join(os.path.dirname(__executable__), PERFECTO_SECURITY_TOKEN_FILE_NAME)
if not PERFECTO_SECURITY_TOKEN_FILE_PATH and os.path.exists(local_security_token_file):
PERFECTO_SECURITY_TOKEN_FILE_PATH = local_security_token_file

Expand Down Expand Up @@ -119,7 +122,11 @@ def main():
else:
perfecto_environment_str = f"{PERFECTO_CLOUD_NAME}"

command = "uvx" if __uvx__ else __executable__
if sys.platform == "darwin" and __bundle__.endswith(".app"):
command_path = os.path.join(__bundle__, "Contents", "MacOS", "perfecto-mcp")
else:
command_path = __executable__
command = "uvx" if __uvx__ else command_path
args = ["--mcp"]
if __uvx__:
args = [
Expand Down