From ac73372b3762acb34bb072ff7f42b638bab09b53 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:42:06 -0700 Subject: [PATCH 01/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- .../app-store/turkish-airlines/truffile.yaml | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 truffile/resources/app-store/turkish-airlines/truffile.yaml diff --git a/truffile/resources/app-store/turkish-airlines/truffile.yaml b/truffile/resources/app-store/turkish-airlines/truffile.yaml new file mode 100644 index 0000000..681670c --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/truffile.yaml @@ -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.svg + 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.svg + destination: ./icon.svg + + - 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 \ No newline at end of file From ac1e66dd6c927d4b5862e82a609c69d7f0943e99 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:42:16 -0700 Subject: [PATCH 02/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- .../app-store/turkish-airlines/auth.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 truffile/resources/app-store/turkish-airlines/auth.py diff --git a/truffile/resources/app-store/turkish-airlines/auth.py b/truffile/resources/app-store/turkish-airlines/auth.py new file mode 100644 index 0000000..7c57790 --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/auth.py @@ -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 \ No newline at end of file From 8ba05624b423c4827e78c805a5943d2c3ef74533 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:42:32 -0700 Subject: [PATCH 03/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- .../app-store/turkish-airlines/config.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 truffile/resources/app-store/turkish-airlines/config.py diff --git a/truffile/resources/app-store/turkish-airlines/config.py b/truffile/resources/app-store/turkish-airlines/config.py new file mode 100644 index 0000000..956005f --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/config.py @@ -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:]}" \ No newline at end of file From 221ed129078af57e3d47d24c0e3a8bc24c3fbede Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:42:39 -0700 Subject: [PATCH 04/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- .../turkish-airlines/remote_mcp_compat.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 truffile/resources/app-store/turkish-airlines/remote_mcp_compat.py diff --git a/truffile/resources/app-store/turkish-airlines/remote_mcp_compat.py b/truffile/resources/app-store/turkish-airlines/remote_mcp_compat.py new file mode 100644 index 0000000..41ef8d2 --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/remote_mcp_compat.py @@ -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 \ No newline at end of file From 0641916d7628110e6a1e590557fb6674e1554edf Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:42:55 -0700 Subject: [PATCH 05/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- .../turkish-airlines/turkish_client.py | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 truffile/resources/app-store/turkish-airlines/turkish_client.py diff --git a/truffile/resources/app-store/turkish-airlines/turkish_client.py b/truffile/resources/app-store/turkish-airlines/turkish_client.py new file mode 100644 index 0000000..0e3269c --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/turkish_client.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +from dataclasses import replace +import inspect +import json +from typing import Any + +from remote_mcp_compat import load_remote_mcp + +_remote_mcp = load_remote_mcp() +RemoteMcpClient = _remote_mcp.RemoteMcpClient +RemoteMcpOAuth = _remote_mcp.RemoteMcpOAuth +RemoteMcpTool = _remote_mcp.RemoteMcpTool + +USER_INFO_RESOURCE_URI = "truffle://user-info" + + +def _user_info_resource_entry() -> dict[str, str]: + return { + "uri": USER_INFO_RESOURCE_URI, + "name": "user_info", + "title": "User Info", + "description": "Profile of the authed Turkish Airlines account.", + "mimeType": "text/markdown", + } + + +def _with_user_info_resource(result: Any) -> dict[str, Any]: + payload = dict(result) if isinstance(result, dict) else {"resources": []} + resources = list(payload.get("resources") or []) + if not any(isinstance(resource, dict) and resource.get("uri") == USER_INFO_RESOURCE_URI for resource in resources): + resources.append(_user_info_resource_entry()) + payload["resources"] = resources + return payload + + +def _read_user_info_result(text: str) -> dict[str, Any]: + return { + "contents": [ + { + "uri": USER_INFO_RESOURCE_URI, + "mimeType": "text/markdown", + "text": text.strip(), + } + ] + } + + +def _content_text(result: Any) -> str: + if isinstance(result, dict): + content = result.get("content") + if isinstance(content, list): + parts = [] + for item in content: + if isinstance(item, dict) and item.get("type") == "text": + text = str(item.get("text") or "").strip() + if text: + parts.append(text) + return "\n".join(parts) + return "" + + +def _structured(result: Any) -> Any: + if isinstance(result, dict) and "structuredContent" in result: + return result["structuredContent"] + if isinstance(result, dict): + for key in ("text", "message"): + value = result.get(key) + if isinstance(value, str) and value.strip() and value.strip()[0] in "{[": + try: + return json.loads(value) + except Exception: + pass + text = _content_text(result) + if text and text[0] in "{[": + try: + return json.loads(text) + except Exception: + return {} + return result + + +def _collect_dicts(value: Any) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + if isinstance(value, dict): + out.append(value) + for child in value.values(): + out.extend(_collect_dicts(child)) + elif isinstance(value, list): + for item in value: + out.extend(_collect_dicts(item)) + return out + + +def _first_str(item: dict[str, Any], keys: tuple[str, ...]) -> str: + for key in keys: + value = item.get(key) + if value is not None and str(value).strip(): + return str(value).strip() + return "" + + +def _is_expired_session_error(exc: Exception) -> bool: + text = str(exc).lower() + return "session" in text and ( + "invalid or expired" in text + or "expired session" in text + or "invalid session" in text + or ("session token" in text and ("invalid" in text or "expired" in text)) + ) + + +class TurkishAirlinesMcpClient(RemoteMcpClient): + def __init__( + self, + *, + remote_url: str, + auth: RemoteMcpOAuth, + default_headers: dict[str, str] | None = None, + post_jsonrpc: Any = None, + ) -> None: + kwargs: dict[str, Any] = { + "remote_url": remote_url, + "auth": auth, + "default_headers": default_headers, + "app_name": "Turkish Airlines", + "post_jsonrpc": post_jsonrpc, + } + if "protocol_version" in inspect.signature(RemoteMcpClient.__init__).parameters: + kwargs["protocol_version"] = "2025-06-18" + super().__init__(**kwargs) + self._auth = auth + + def _reset_remote_session(self) -> None: + self._initialized = False + self._session_id = None + + @staticmethod + def _public_tool_name(name: str) -> str: + return str(name or "").replace("-", "_") + + def _normalize_tools(self, tools: list[RemoteMcpTool]) -> list[RemoteMcpTool]: + normalized: list[RemoteMcpTool] = [] + self._tool_name_map = {} + for tool in tools: + public_name = self._public_tool_name(getattr(tool, "public_name", "")) + remote_name = str(getattr(tool, "remote_name", public_name) or public_name) + if public_name != getattr(tool, "public_name", ""): + tool = replace(tool, public_name=public_name) + normalized.append(tool) + self._tool_name_map[public_name] = remote_name + self._tool_name_map[remote_name] = remote_name + return normalized + + def list_tools(self) -> list[RemoteMcpTool]: + try: + return self._normalize_tools(super().list_tools()) + except Exception as exc: + if not _is_expired_session_error(exc): + raise + self._reset_remote_session() + return self._normalize_tools(super().list_tools()) + + def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: + try: + return super().call_tool(name, arguments) + except Exception as exc: + if not _is_expired_session_error(exc): + raise + self._reset_remote_session() + return super().call_tool(name, arguments) + + def list_resources(self) -> Any: + try: + return _with_user_info_resource(super().list_resources()) + except Exception as exc: + if not _is_expired_session_error(exc): + return _with_user_info_resource({"resources": []}) + self._reset_remote_session() + return _with_user_info_resource(super().list_resources()) + + def read_resource(self, uri: str) -> Any: + if uri == USER_INFO_RESOURCE_URI: + return _read_user_info_result(self.user_info_markdown()) + try: + return super().read_resource(uri) + except Exception as exc: + if not _is_expired_session_error(exc): + raise + self._reset_remote_session() + return super().read_resource(uri) + + def user_info_markdown(self) -> str: + lines = ["# Turkish Airlines"] + lines.append("Connected via MCP") + lines.append("Tools: search_flights, flight_status, booking_details, check_in, miles_profile, miles_balance, miles_history, cancel_booking") + return "\n".join(lines) \ No newline at end of file From e20c23bbe99f75f46757cbce70f53a5fd6901ada Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:43:16 -0700 Subject: [PATCH 06/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- .../turkish-airlines/turkish_foreground.py | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 truffile/resources/app-store/turkish-airlines/turkish_foreground.py diff --git a/truffile/resources/app-store/turkish-airlines/turkish_foreground.py b/truffile/resources/app-store/turkish-airlines/turkish_foreground.py new file mode 100644 index 0000000..b4872d6 --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/turkish_foreground.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import argparse +import atexit +import logging +import os +import sys +from typing import Any + +from truffile.app_runtime.errors import ErrorReporter + +from auth import TurkishAirlinesAuth +from config import MCP_HOST, MCP_PORT, TURKISH_AIRLINES_MCP_BASE, build_default_headers +from turkish_client import TurkishAirlinesMcpClient +from remote_mcp_compat import load_remote_mcp + +RemoteMcpProxyServer = load_remote_mcp().RemoteMcpProxyServer +USER_INFO_RESOURCE_URI = "truffle://user-info" + +LOGGER = logging.getLogger("turkish.foreground") +LOGGER.setLevel(logging.INFO) +_TITLE_OVERRIDES = {"api": "API", "id": "ID", "mcp": "MCP", "url": "URL"} + + +def _tool_title_from_name(name: str) -> str: + words = [] + for part in name.replace("-", "_").split("_"): + if not part: + continue + words.append(_TITLE_OVERRIDES.get(part.lower(), part.capitalize())) + return " ".join(words) or name + + +def _infer_tool_annotations(name: str) -> dict[str, bool]: + lowered = name.lower() + destructive_words = ("delete", "remove", "archive", "trash", "cancel") + read_prefixes = ("get_", "list_", "search_", "fetch_", "read_", "query_", "check_") + mutating_words = ("create", "update", "edit", "patch", "append", "add", "set") + if any(word in lowered for word in destructive_words): + return {"readOnlyHint": False, "destructiveHint": True} + if lowered.startswith(read_prefixes): + return {"readOnlyHint": True, "destructiveHint": False} + if any(word in lowered for word in mutating_words): + return {"readOnlyHint": False, "destructiveHint": False} + return {"readOnlyHint": False, "destructiveHint": False} + + +def _enrich_tool_metadata(tool: dict[str, Any]) -> dict[str, Any]: + enriched = dict(tool) + name = str(enriched.get("name", "") or "") + enriched.setdefault("title", _tool_title_from_name(name)) + annotations = enriched.get("annotations") + if not isinstance(annotations, dict): + annotations = {} + inferred = _infer_tool_annotations(name) + annotations.setdefault("readOnlyHint", inferred["readOnlyHint"]) + annotations.setdefault("destructiveHint", inferred["destructiveHint"]) + enriched["annotations"] = annotations + return enriched + + +class _StubTurkishAirlinesMcpClient: + def verify(self) -> tuple[bool, str]: + return True, "Turkish Airlines OAuth token verified. Tools=8" + + def list_tools(self) -> list[dict[str, Any]]: + return [ + {"name": "search_flights", "description": "Search for available flights.", "inputSchema": {"type": "object", "properties": {"origin": {"type": "string"}, "destination": {"type": "string"}, "date": {"type": "string"}}}}, + {"name": "flight_status", "description": "Get real-time flight status.", "inputSchema": {"type": "object", "properties": {"flight_number": {"type": "string"}}}}, + {"name": "booking_details", "description": "Retrieve booking details.", "inputSchema": {"type": "object", "properties": {"pnr": {"type": "string"}, "surname": {"type": "string"}}}}, + {"name": "check_in", "description": "Check in for a flight.", "inputSchema": {"type": "object", "properties": {"pnr": {"type": "string"}, "surname": {"type": "string"}}}}, + {"name": "miles_profile", "description": "Get Miles&Smiles account info.", "inputSchema": {"type": "object", "properties": {}}}, + {"name": "miles_balance", "description": "Check miles balance.", "inputSchema": {"type": "object", "properties": {}}}, + {"name": "miles_history", "description": "Get miles transaction history.", "inputSchema": {"type": "object", "properties": {}}}, + {"name": "cancel_booking", "description": "Cancel or modify a booking.", "inputSchema": {"type": "object", "properties": {"pnr": {"type": "string"}, "surname": {"type": "string"}}}}, + ] + + def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: + return {"content": [{"type": "text", "text": f"stub:{name}"}], "structuredContent": {"name": name, "arguments": arguments}, "isError": False} + + def list_resources(self) -> dict[str, Any]: + return {"resources": [{"uri": USER_INFO_RESOURCE_URI, "name": "user_info", "title": "User Info", "description": "Profile of the authed Turkish Airlines account.", "mimeType": "text/markdown"}]} + + def read_resource(self, uri: str) -> dict[str, Any]: + if uri != USER_INFO_RESOURCE_URI: + raise RuntimeError(f"Turkish Airlines resource not found: {uri}") + return {"contents": [{"uri": USER_INFO_RESOURCE_URI, "mimeType": "text/markdown", "text": "# Turkish Airlines\nConnected via MCP\nTools: search_flights, flight_status, booking_details, check_in, miles_profile, miles_balance, miles_history, cancel_booking."}]} + + def close(self) -> None: + return None + + +class TurkishAirlinesForegroundApp: + def __init__(self) -> None: + self.name = "turkish-airlines" + self.logger = LOGGER + self._error_reporter = ErrorReporter(logger=self.logger, app_name="turkish-airlines") + self._auth: TurkishAirlinesAuth | None = None + self._client: TurkishAirlinesMcpClient | None = None + + def get_auth(self) -> TurkishAirlinesAuth: + if self._auth is None: + self._auth = TurkishAirlinesAuth() + return self._auth + + def set_auth_for_test(self, auth: TurkishAirlinesAuth) -> None: + self.reset_for_test() + self._auth = auth + + def build_client(self) -> TurkishAirlinesMcpClient: + if os.getenv("APP_STORE_USE_TEST_STUBS") == "1": + return _StubTurkishAirlinesMcpClient() # type: ignore[return-value] + return TurkishAirlinesMcpClient( + remote_url=TURKISH_AIRLINES_MCP_BASE, + auth=self.get_auth(), + default_headers=build_default_headers(), + ) + + def get_client(self) -> TurkishAirlinesMcpClient: + if self._client is None: + self._client = self.build_client() + return self._client + + def set_client(self, client: TurkishAirlinesMcpClient) -> None: + self.reset_for_test() + self._client = client + + def reset_for_test(self) -> None: + if self._client is not None: + try: + self._client.close() + except Exception: + self.logger.exception("Failed closing Turkish Airlines MCP client") + self._client = None + self._auth = None + + def cleanup(self) -> None: + self.reset_for_test() + + def logger_names(self) -> list[str]: + return [self.logger.name] + + def list_tools(self) -> list[dict[str, Any]]: + client = self.get_client() + if hasattr(client, "list_mcp_tools"): + tools = client.list_mcp_tools() + else: + tools = client.list_tools() + return [_enrich_tool_metadata(tool) for tool in tools if isinstance(tool, dict)] + + def list_prompts(self) -> list[dict[str, str]]: + return [] + + async def invoke_tool(self, name: str, **arguments: Any) -> Any: + try: + return self.get_client().call_tool(name, arguments) + except Exception as exc: + await self._error_reporter.report_foreground_exception(exc, tool_name=name) + raise + + def verify(self) -> tuple[bool, str]: + client = self.build_client() + try: + ok, message = client.verify() + finally: + client.close() + return ok, message + + def run(self) -> None: + client = self.build_client() + LOGGER.info("Starting Turkish Airlines MCP proxy on %s:%d -> %s", MCP_HOST, MCP_PORT, client.remote_url) + try: + RemoteMcpProxyServer(client=client, host=MCP_HOST, port=MCP_PORT).serve_forever() + finally: + LOGGER.info("Turkish Airlines MCP proxy stopped") + + +app = TurkishAirlinesForegroundApp() +atexit.register(app.cleanup) + + +def set_client(client: TurkishAirlinesMcpClient) -> None: + app.set_client(client) + + +def set_auth_for_test(auth: TurkishAirlinesAuth) -> None: + app.set_auth_for_test(auth) + + +def reset_for_test() -> None: + app.reset_for_test() + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Turkish Airlines foreground wrapper") + parser.add_argument("--verify", action="store_true", help="Verify installed OAuth token and MCP binary") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + try: + args = _parse_args(argv) + if args.verify: + ok, message = app.verify() + print(message, flush=True) + return 0 if ok else 1 + app.run() + return 1 + except Exception as exc: + print(str(exc), file=sys.stderr, flush=True) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file From 1860716fa0824a73d869227afeaafc94efa30de6 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:43:24 -0700 Subject: [PATCH 07/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- .../app-store/turkish-airlines/tools.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 truffile/resources/app-store/turkish-airlines/tools.py diff --git a/truffile/resources/app-store/turkish-airlines/tools.py b/truffile/resources/app-store/turkish-airlines/tools.py new file mode 100644 index 0000000..6ddb9de --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/tools.py @@ -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} \ No newline at end of file From fc544bcbe5d5756991fd6a351649c96788925bb9 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:43:40 -0700 Subject: [PATCH 08/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- truffile/resources/app-store/turkish-airlines/icon.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 truffile/resources/app-store/turkish-airlines/icon.svg diff --git a/truffile/resources/app-store/turkish-airlines/icon.svg b/truffile/resources/app-store/turkish-airlines/icon.svg new file mode 100644 index 0000000..07a5b9c --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file From e8c8a193cb5f5f77f16f97b8e0c4dbb7fea84bfe Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 19:45:09 -0700 Subject: [PATCH 09/11] chore: add Turkish Airlines app files (truffile.yaml + 6 Python files + icon.svg) --- truffile/resources/app-store/turkish-airlines/.truffle-credits | 1 + 1 file changed, 1 insertion(+) create mode 100644 truffile/resources/app-store/turkish-airlines/.truffle-credits diff --git a/truffile/resources/app-store/turkish-airlines/.truffle-credits b/truffile/resources/app-store/turkish-airlines/.truffle-credits new file mode 100644 index 0000000..37f3fd1 --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/.truffle-credits @@ -0,0 +1 @@ +This PR was made with Abdullah's Truffle! 🍄 \ No newline at end of file From c8e3c57e0b8af4bd86cb1f0206e07e6ac2614578 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 20:39:11 -0700 Subject: [PATCH 10/11] fix: use icon.png instead of icon.svg (proper PNG format) --- truffile/resources/app-store/turkish-airlines/truffile.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/truffile/resources/app-store/turkish-airlines/truffile.yaml b/truffile/resources/app-store/turkish-airlines/truffile.yaml index 681670c..354e744 100644 --- a/truffile/resources/app-store/turkish-airlines/truffile.yaml +++ b/truffile/resources/app-store/turkish-airlines/truffile.yaml @@ -3,7 +3,7 @@ metadata: name: Turkish Airlines bundle_id: org.deepshard.turkish-airlines description: Turkish Airlines flight search, booking, and Miles&Smiles via MCP - icon_file: ./icon.svg + icon_file: ./icon.png foreground: process: cmd: @@ -31,8 +31,8 @@ steps: destination: ./turkish_foreground.py - source: ./tools.py destination: ./tools.py - - source: ./icon.svg - destination: ./icon.svg + - source: ./icon.png + destination: ./icon.png - name: Turkish Airlines OAuth Sign-In type: oauth From 9d65afca43a99981404d4bbbfa0d2af249243361 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Wed, 17 Jun 2026 20:39:36 -0700 Subject: [PATCH 11/11] fix: use icon.png instead of icon.svg (proper PNG format) --- truffile/resources/app-store/turkish-airlines/icon.png | 1 + 1 file changed, 1 insertion(+) create mode 100644 truffile/resources/app-store/turkish-airlines/icon.png diff --git a/truffile/resources/app-store/turkish-airlines/icon.png b/truffile/resources/app-store/turkish-airlines/icon.png new file mode 100644 index 0000000..da92004 --- /dev/null +++ b/truffile/resources/app-store/turkish-airlines/icon.png @@ -0,0 +1 @@ +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 \ No newline at end of file