Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This PR was made with Abdullah's Truffle! 🍄
96 changes: 96 additions & 0 deletions truffile/resources/app-store/turkish-airlines/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import annotations

import os
from pathlib import Path
import time
from typing import Any

from config import (
TURKISH_AIRLINES_AUTH_TOKEN_ENDPOINT,
TURKISH_AIRLINES_OAUTH_APP_VAR_KEY,
TURKISH_AIRLINES_OAUTH_RESOURCE,
resolve_token_file,
)
from remote_mcp_compat import load_remote_mcp

RemoteMcpOAuth = load_remote_mcp().RemoteMcpOAuth


REFRESH_SKEW_SECONDS = 300


class TurkishAirlinesAuth(RemoteMcpOAuth):
def __init__(self, token_file: Path | None = None, read_only: bool = False) -> None:
resolved = Path(token_file) if token_file is not None else resolve_token_file()
env_access_token = str(os.getenv("TURKISH_AIRLINES_ACCESS_TOKEN", "") or "").strip()
super().__init__(
resolved,
app_var_key=TURKISH_AIRLINES_OAUTH_APP_VAR_KEY,
env_access_token=env_access_token,
default_token_endpoint=TURKISH_AIRLINES_AUTH_TOKEN_ENDPOINT,
default_resource=TURKISH_AIRLINES_OAUTH_RESOURCE,
read_only=read_only,
)

def get_access_token(self) -> str:
if self.env_access_token:
return self.env_access_token
return self.ensure_access_token_fresh()

def ensure_access_token_fresh(self) -> str:
payload = self.get_oauth_payload()
if not isinstance(payload, dict):
return ""
if self._needs_refresh(payload):
if self._read_only:
return self.token_from_payload(payload)
try:
return self.refresh_access_token()
except Exception:
reloaded = self.get_oauth_payload()
if isinstance(reloaded, dict) and not self._needs_refresh(reloaded):
return self.token_from_payload(reloaded)
raise
return self.token_from_payload(payload)

def refresh_access_token(self) -> str:
token = super().refresh_access_token()
payload = self.get_oauth_payload()
if isinstance(payload, dict):
self._set_expiry_fields(payload)
self.save_oauth_payload(payload)
return token

@staticmethod
def _set_expiry_fields(payload: dict[str, Any]) -> None:
expires_in = payload.get("expires_in")
try:
seconds = int(expires_in)
except (TypeError, ValueError):
return
if seconds > 0:
payload["expires_at"] = int(time.time()) + seconds

@staticmethod
def _needs_refresh(payload: dict[str, Any]) -> bool:
expires_at = _expiry_epoch(payload)
if expires_at is None:
return False
return expires_at - time.time() <= REFRESH_SKEW_SECONDS


def _expiry_epoch(payload: dict[str, Any]) -> float | None:
for key in ("expires_at", "expiresAt", "expiry", "expires"):
raw = payload.get(key)
if raw is None:
continue
if isinstance(raw, (int, float)):
return float(raw)
value = str(raw).strip()
if not value:
continue
try:
return float(value)
except ValueError:
pass
return None
54 changes: 54 additions & 0 deletions truffile/resources/app-store/turkish-airlines/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

DEFAULT_TOKEN_FILE = Path("/root/.turkish-airlines-oauth/tokens.json")
DEFAULT_DATA_DIR = Path(os.getenv("TURKISH_AIRLINES_DATA_DIR", "/root/.turkish-airlines-oauth")).expanduser()

MCP_HOST = os.getenv("MCP_HOST", "0.0.0.0")
MCP_PORT = int(os.getenv("MCP_PORT", "8000"))
TURKISH_AIRLINES_MCP_BASE = os.getenv("TURKISH_AIRLINES_MCP_BASE", "https://mcp.turkishtechlab.com/mcp").strip()
TURKISH_AIRLINES_AUTH_TOKEN_ENDPOINT = os.getenv("TURKISH_AIRLINES_AUTH_TOKEN_ENDPOINT", "https://mcp.turkishtechlab.com/token").strip()
TURKISH_AIRLINES_OAUTH_RESOURCE = os.getenv("TURKISH_AIRLINES_OAUTH_RESOURCE", "https://mcp.turkishtechlab.com").strip()
TURKISH_AIRLINES_USER_AGENT = os.getenv("TURKISH_AIRLINES_USER_AGENT", "Truffle/1.0").strip()
TURKISH_AIRLINES_API_BASE = os.getenv("TURKISH_AIRLINES_API_BASE", "https://api.turkishairlines.com").strip().rstrip("/")
TURKISH_AIRLINES_OAUTH_APP_VAR_KEY = "turkish_airlines_oauth_state"


def resolve_token_file() -> Path:
token_file_raw = str(os.getenv("TURKISH_AIRLINES_OAUTH_TOKEN_FILE", "")).strip()
return Path(token_file_raw) if token_file_raw else DEFAULT_TOKEN_FILE


def load_oauth_token_payload() -> dict[str, Any]:
token_path = resolve_token_file()
if not token_path.exists():
raise RuntimeError(f"Turkish Airlines OAuth token file not found: {token_path}")
try:
payload = json.loads(token_path.read_text(encoding="utf-8"))
except Exception as exc:
raise RuntimeError(f"failed to read Turkish Airlines OAuth token file: {exc}") from exc
if not isinstance(payload, dict):
raise RuntimeError("Turkish Airlines OAuth token payload must be a JSON object")
return payload


def load_access_token() -> str:
payload = load_oauth_token_payload()
token = str(payload.get("access_token", "") or "").strip()
if not token:
raise RuntimeError("Turkish Airlines OAuth token payload missing access_token")
return token


def build_default_headers() -> dict[str, str]:
return {"User-Agent": TURKISH_AIRLINES_USER_AGENT} if TURKISH_AIRLINES_USER_AGENT else {}


def mask_token(raw: str) -> str:
if len(raw) <= 10:
return "*" * len(raw)
return f"{raw[:4]}...{raw[-4:]}"
1 change: 1 addition & 0 deletions truffile/resources/app-store/turkish-airlines/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions truffile/resources/app-store/turkish-airlines/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions truffile/resources/app-store/turkish-airlines/remote_mcp_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

import importlib.util
import sys
from pathlib import Path
from types import ModuleType


def load_remote_mcp() -> ModuleType:
try:
from truffile.app_runtime import remote_mcp

return remote_mcp
except (ImportError, ModuleNotFoundError):
pass

loaded_app_runtime = sys.modules.get("app_runtime")
loaded_truffile_runtime = sys.modules.get("truffile.app_runtime")
if loaded_app_runtime is loaded_truffile_runtime:
sys.modules.pop("app_runtime", None)

try:
from app_runtime import remote_mcp

return remote_mcp
except (ImportError, ModuleNotFoundError):
pass

source = _find_pyfw_remote_mcp()
if source is None:
raise ModuleNotFoundError("No module named 'truffile.app_runtime.remote_mcp'")

module_name = "truffile.app_runtime.remote_mcp"
spec = importlib.util.spec_from_file_location(module_name, source)
if spec is None or spec.loader is None:
raise ModuleNotFoundError("Could not load truffile.app_runtime.remote_mcp")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module


def _find_pyfw_remote_mcp() -> Path | None:
here = Path(__file__).resolve()
for parent in here.parents:
candidate = parent / "dev" / "pyfw" / "python" / "app_runtime" / "remote_mcp.py"
if candidate.exists():
return candidate
return None
28 changes: 28 additions & 0 deletions truffile/resources/app-store/turkish-airlines/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Static Turkish Airlines MCP tool metadata for default-app publishing."""

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class ToolDefinition:
name: str
title: str
description: str
icon: str
annotations: dict[str, bool]


TOOLS = [
ToolDefinition(name="search_flights", title="Search Flights", description="Search for available Turkish Airlines flights by origin, destination, date, and passengers.", icon="airplane", annotations={"readOnlyHint": True, "destructiveHint": False}),
ToolDefinition(name="flight_status", title="Flight Status", description="Get real-time status of a Turkish Airlines flight by flight number.", icon="airplane-clock", annotations={"readOnlyHint": True, "destructiveHint": False}),
ToolDefinition(name="booking_details", title="Booking Details", description="Retrieve booking details using PNR and surname.", icon="clipboard", annotations={"readOnlyHint": True, "destructiveHint": False}),
ToolDefinition(name="check_in", title="Check In", description="Check in for a Turkish Airlines flight using PNR and surname.", icon="door-open", annotations={"readOnlyHint": False, "destructiveHint": False}),
ToolDefinition(name="miles_profile", title="Miles&Smiles Profile", description="Get Miles&Smiles account information and member details.", icon="badge", annotations={"readOnlyHint": True, "destructiveHint": False}),
ToolDefinition(name="miles_balance", title="Miles Balance", description="Check current Miles&Smiles balance and status.", icon="coins", annotations={"readOnlyHint": True, "destructiveHint": False}),
ToolDefinition(name="miles_history", title="Miles History", description="Get Miles&Smiles transaction history and point movements.", icon="list", annotations={"readOnlyHint": True, "destructiveHint": False}),
ToolDefinition(name="cancel_booking", title="Cancel Booking", description="Cancel or modify a Turkish Airlines booking.", icon="x-circle", annotations={"readOnlyHint": False, "destructiveHint": True}),
]

TOOLS_BY_NAME = {tool.name: tool for tool in TOOLS}
61 changes: 61 additions & 0 deletions truffile/resources/app-store/turkish-airlines/truffile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
truffile_version: 2
metadata:
name: Turkish Airlines
bundle_id: org.deepshard.turkish-airlines
description: Turkish Airlines flight search, booking, and Miles&Smiles via MCP
icon_file: ./icon.png
foreground:
process:
cmd:
- python
- turkish_foreground.py
working_directory: /
environment:
PYTHONUNBUFFERED: "1"
TURKISH_AIRLINES_MCP_BASE: https://mcp.turkishtechlab.com/mcp

steps:
- name: Copy app files
type: files
update_policy: run_on_update
files:
- source: ./auth.py
destination: ./auth.py
- source: ./config.py
destination: ./config.py
- source: ./remote_mcp_compat.py
destination: ./remote_mcp_compat.py
- source: ./turkish_client.py
destination: ./turkish_client.py
- source: ./turkish_foreground.py
destination: ./turkish_foreground.py
- source: ./tools.py
destination: ./tools.py
- source: ./icon.png
destination: ./icon.png

- name: Turkish Airlines OAuth Sign-In
type: oauth
update_policy: run_on_update
update_check: python ./turkish_foreground.py --verify
provider: remote_mcp
description: Sign in to Turkish Airlines to connect flights, bookings, and Miles&Smiles via MCP.
redirect_uri: https://truffle.net/api/oauth/callback
scopes: []
dynamic_client_registration: true
registration_endpoint: https://mcp.turkishtechlab.com/register
resource_metadata_endpoint: https://mcp.turkishtechlab.com/.well-known/oauth-authorization-server
oauth_resource: https://mcp.turkishtechlab.com
client_name: Truffle
auth_endpoint: https://mcp.turkishtechlab.com/authorize
token_endpoint: https://mcp.turkishtechlab.com/token
client_id_env: TURKISH_AIRLINES_CLIENT_ID
client_secret_env: null
token_output_file: /root/.turkish-airlines-oauth/tokens.json
token_file_env_name: TURKISH_AIRLINES_OAUTH_TOKEN_FILE
app_var_key: turkish_airlines_oauth_state

- name: Verify token
type: bash
update_policy: run_on_update
run: python ./turkish_foreground.py --verify
Loading