mcp-appium 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+
11
+ # Les capabilities décrivent une application et un appareil précis : elles
12
+ # n'ont rien à faire dans un dépôt public. L'exemple vit dans examples/.
13
+ /appium-caps.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Julien Becheny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.5
2
+ Name: mcp-appium
3
+ Version: 0.1.0
4
+ Summary: Serveur MCP qui donne à un assistant les éléments réels d'une application Appium, au lieu de le laisser les inventer.
5
+ Project-URL: Homepage, https://github.com/julien-becheny/mcp-appium
6
+ Project-URL: Issues, https://github.com/julien-becheny/mcp-appium/issues
7
+ Author: Julien Becheny
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: android,appium,ios,llm,mcp,mobile-testing,test-automation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: appium-python-client>=3.0.0
21
+ Requires-Dist: lxml>=4.9.0
22
+ Requires-Dist: mcp>=2.0.0
23
+ Requires-Dist: requests>=2.28.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.5.0; extra == 'dev'
27
+ Provides-Extra: image
28
+ Requires-Dist: pillow>=10.0.0; extra == 'image'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # mcp-appium
32
+
33
+ Un assistant qui écrit des tests mobiles invente des sélecteurs. Il propose
34
+ `accessibility_id=bouton_valider` parce que c'est ce qu'un développeur aurait
35
+ écrit, et le test échoue parce que l'application expose autre chose.
36
+
37
+ Ce serveur MCP lui donne **l'écran réel**.
38
+
39
+ ```
40
+ pip install mcp-appium
41
+ ```
42
+
43
+ ## Ce qu'il fait
44
+
45
+ Onze outils, exposés à l'assistant via le Model Context Protocol :
46
+
47
+ | Outil | Rôle |
48
+ |---|---|
49
+ | `connect_to_session` | Se rattache à une session Appium **déjà ouverte** |
50
+ | `get_page_source` | L'arbre de l'écran, simplifié ou brut |
51
+ | `find_elements` | Recherche par sélecteur ou par texte |
52
+ | `suggest_locators` | Des sélecteurs qui existent, classés par robustesse |
53
+ | `get_element_info` | Attributs, position, état d'un élément |
54
+ | `screenshot` | L'écran, réduit avant envoi |
55
+ | `tap_element` | Clic, avec vérification que l'écran a bougé |
56
+ | `type_text` | Saisie dans un champ |
57
+ | `go_back` | Retour arrière |
58
+ | `get_session_info` | Plateforme, appareil, identifiant de session |
59
+ | `close_session` | Libère l'appareil, **si ce serveur a ouvert la session** |
60
+
61
+ Android, iOS, iPadOS et Windows.
62
+
63
+ ## Le cas courant : observer une session existante
64
+
65
+ Un test tourne, il échoue sur un élément. Tu demandes à l'assistant ce que
66
+ l'écran contient vraiment.
67
+
68
+ ```
69
+ connect_to_session()
70
+ ```
71
+
72
+ Sans argument, le serveur cherche une session active sur
73
+ `http://127.0.0.1:4723` et s'y rattache. **Il ne crée rien, ne redémarre rien**,
74
+ et aucune configuration n'est nécessaire.
75
+
76
+ C'est le mode à privilégier : l'assistant voit exactement ce que le test voit,
77
+ au moment où il le voit.
78
+
79
+ ## Créer une session
80
+
81
+ Si aucune session n'existe, le serveur peut en ouvrir une. Il lui faut alors des
82
+ capabilities, déclarées dans `appium-caps.json` à la racine de ton projet :
83
+
84
+ ```json
85
+ {
86
+ "platformName": "Android",
87
+ "automationName": "UiAutomator2",
88
+ "appPackage": "com.exemple.app",
89
+ "appActivity": ".MainActivity"
90
+ }
91
+ ```
92
+
93
+ Les clés sont préfixées par `appium:` automatiquement quand il le faut.
94
+
95
+ Plusieurs plateformes dans le même fichier :
96
+
97
+ ```json
98
+ {
99
+ "android": { "platformName": "Android", "automationName": "UiAutomator2", "appPackage": "com.exemple.app" },
100
+ "ios": { "platformName": "iOS", "automationName": "XCUITest", "bundleId": "com.exemple.app" }
101
+ }
102
+ ```
103
+
104
+ La variable `MCP_APPIUM_PLATFORM` choisit laquelle. À défaut, la première
105
+ déclarée. Deux autres variables existent : `MCP_APPIUM_CAPS` pour passer le JSON
106
+ directement, et `MCP_APPIUM_CAPS_FILE` pour désigner un autre fichier.
107
+
108
+ ## Déclarer le serveur
109
+
110
+ Dans VS Code, `.vscode/mcp.json` :
111
+
112
+ ```json
113
+ {
114
+ "servers": {
115
+ "appium": {
116
+ "type": "stdio",
117
+ "command": "mcp-appium"
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ Le format est le même pour les autres clients MCP : une commande, transport
124
+ standard.
125
+
126
+ ## Le parti pris qui compte : borner les sorties
127
+
128
+ Un arbre de vue Appium brut dépasse couramment les cinquante mille caractères.
129
+ Envoyé tel quel, il sature la fenêtre de contexte du modèle avant de lui avoir
130
+ appris quoi que ce soit. Pire : ce qui entre dans le contexte y reste, et se
131
+ repaie à chaque échange suivant de la conversation.
132
+
133
+ Toutes les sorties sont donc plafonnées, et le serveur le dit quand il coupe :
134
+
135
+ - arbre simplifié à 400 lignes, avec les seuls attributs qui servent à cibler ;
136
+ - source brute à 40 000 caractères ;
137
+ - 15 éléments détaillés au maximum dans une recherche ;
138
+ - captures réduites à 1280 pixels de large.
139
+
140
+ Un outil d'inspection qui ne borne pas ses sorties est inutilisable en
141
+ conversation, quelle que soit la qualité de ce qu'il expose.
142
+
143
+ ## Deux autres partis pris
144
+
145
+ **Un tap vérifie son effet.** `tap_element` compare l'écran avant et après, et
146
+ signale explicitement un clic resté sans conséquence. Un élément désactivé ou
147
+ recouvert répond à `click()` sans rien faire : sans cette vérification,
148
+ l'assistant croit avoir avancé et enchaîne dans le vide.
149
+
150
+ **Une session ne se ferme que si on l'a ouverte.** `close_session` libère
151
+ l'appareil quand le serveur a créé la session, et se contente de s'en détacher
152
+ sinon. Fermer la session d'un test en cours couperait ce test.
153
+
154
+ **`suggest_locators` classe par robustesse.** L'identifiant d'accessibilité
155
+ d'abord, le XPath sur le texte en dernier, avec la mention qu'il cassera au
156
+ prochain changement de libellé.
157
+
158
+ ## Ce qu'il ne fait pas
159
+
160
+ Il **n'écrit pas de tests** et n'impose aucun framework. Il expose l'état de
161
+ l'application, l'assistant fait le reste avec les outils que tu utilises déjà.
162
+
163
+ Il ne dépend d'**aucun service d'IA**. Ni clé d'API, ni compte, ni appel sortant :
164
+ le seul réseau qu'il touche est ton serveur Appium local.
165
+
166
+ Il ne **remplace pas Appium Inspector** pour l'exploration manuelle. Il sert à
167
+ donner ces informations à un modèle, ce qu'une interface graphique ne sait pas
168
+ faire.
169
+
170
+ ## Licence
171
+
172
+ MIT.
@@ -0,0 +1,142 @@
1
+ # mcp-appium
2
+
3
+ Un assistant qui écrit des tests mobiles invente des sélecteurs. Il propose
4
+ `accessibility_id=bouton_valider` parce que c'est ce qu'un développeur aurait
5
+ écrit, et le test échoue parce que l'application expose autre chose.
6
+
7
+ Ce serveur MCP lui donne **l'écran réel**.
8
+
9
+ ```
10
+ pip install mcp-appium
11
+ ```
12
+
13
+ ## Ce qu'il fait
14
+
15
+ Onze outils, exposés à l'assistant via le Model Context Protocol :
16
+
17
+ | Outil | Rôle |
18
+ |---|---|
19
+ | `connect_to_session` | Se rattache à une session Appium **déjà ouverte** |
20
+ | `get_page_source` | L'arbre de l'écran, simplifié ou brut |
21
+ | `find_elements` | Recherche par sélecteur ou par texte |
22
+ | `suggest_locators` | Des sélecteurs qui existent, classés par robustesse |
23
+ | `get_element_info` | Attributs, position, état d'un élément |
24
+ | `screenshot` | L'écran, réduit avant envoi |
25
+ | `tap_element` | Clic, avec vérification que l'écran a bougé |
26
+ | `type_text` | Saisie dans un champ |
27
+ | `go_back` | Retour arrière |
28
+ | `get_session_info` | Plateforme, appareil, identifiant de session |
29
+ | `close_session` | Libère l'appareil, **si ce serveur a ouvert la session** |
30
+
31
+ Android, iOS, iPadOS et Windows.
32
+
33
+ ## Le cas courant : observer une session existante
34
+
35
+ Un test tourne, il échoue sur un élément. Tu demandes à l'assistant ce que
36
+ l'écran contient vraiment.
37
+
38
+ ```
39
+ connect_to_session()
40
+ ```
41
+
42
+ Sans argument, le serveur cherche une session active sur
43
+ `http://127.0.0.1:4723` et s'y rattache. **Il ne crée rien, ne redémarre rien**,
44
+ et aucune configuration n'est nécessaire.
45
+
46
+ C'est le mode à privilégier : l'assistant voit exactement ce que le test voit,
47
+ au moment où il le voit.
48
+
49
+ ## Créer une session
50
+
51
+ Si aucune session n'existe, le serveur peut en ouvrir une. Il lui faut alors des
52
+ capabilities, déclarées dans `appium-caps.json` à la racine de ton projet :
53
+
54
+ ```json
55
+ {
56
+ "platformName": "Android",
57
+ "automationName": "UiAutomator2",
58
+ "appPackage": "com.exemple.app",
59
+ "appActivity": ".MainActivity"
60
+ }
61
+ ```
62
+
63
+ Les clés sont préfixées par `appium:` automatiquement quand il le faut.
64
+
65
+ Plusieurs plateformes dans le même fichier :
66
+
67
+ ```json
68
+ {
69
+ "android": { "platformName": "Android", "automationName": "UiAutomator2", "appPackage": "com.exemple.app" },
70
+ "ios": { "platformName": "iOS", "automationName": "XCUITest", "bundleId": "com.exemple.app" }
71
+ }
72
+ ```
73
+
74
+ La variable `MCP_APPIUM_PLATFORM` choisit laquelle. À défaut, la première
75
+ déclarée. Deux autres variables existent : `MCP_APPIUM_CAPS` pour passer le JSON
76
+ directement, et `MCP_APPIUM_CAPS_FILE` pour désigner un autre fichier.
77
+
78
+ ## Déclarer le serveur
79
+
80
+ Dans VS Code, `.vscode/mcp.json` :
81
+
82
+ ```json
83
+ {
84
+ "servers": {
85
+ "appium": {
86
+ "type": "stdio",
87
+ "command": "mcp-appium"
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Le format est le même pour les autres clients MCP : une commande, transport
94
+ standard.
95
+
96
+ ## Le parti pris qui compte : borner les sorties
97
+
98
+ Un arbre de vue Appium brut dépasse couramment les cinquante mille caractères.
99
+ Envoyé tel quel, il sature la fenêtre de contexte du modèle avant de lui avoir
100
+ appris quoi que ce soit. Pire : ce qui entre dans le contexte y reste, et se
101
+ repaie à chaque échange suivant de la conversation.
102
+
103
+ Toutes les sorties sont donc plafonnées, et le serveur le dit quand il coupe :
104
+
105
+ - arbre simplifié à 400 lignes, avec les seuls attributs qui servent à cibler ;
106
+ - source brute à 40 000 caractères ;
107
+ - 15 éléments détaillés au maximum dans une recherche ;
108
+ - captures réduites à 1280 pixels de large.
109
+
110
+ Un outil d'inspection qui ne borne pas ses sorties est inutilisable en
111
+ conversation, quelle que soit la qualité de ce qu'il expose.
112
+
113
+ ## Deux autres partis pris
114
+
115
+ **Un tap vérifie son effet.** `tap_element` compare l'écran avant et après, et
116
+ signale explicitement un clic resté sans conséquence. Un élément désactivé ou
117
+ recouvert répond à `click()` sans rien faire : sans cette vérification,
118
+ l'assistant croit avoir avancé et enchaîne dans le vide.
119
+
120
+ **Une session ne se ferme que si on l'a ouverte.** `close_session` libère
121
+ l'appareil quand le serveur a créé la session, et se contente de s'en détacher
122
+ sinon. Fermer la session d'un test en cours couperait ce test.
123
+
124
+ **`suggest_locators` classe par robustesse.** L'identifiant d'accessibilité
125
+ d'abord, le XPath sur le texte en dernier, avec la mention qu'il cassera au
126
+ prochain changement de libellé.
127
+
128
+ ## Ce qu'il ne fait pas
129
+
130
+ Il **n'écrit pas de tests** et n'impose aucun framework. Il expose l'état de
131
+ l'application, l'assistant fait le reste avec les outils que tu utilises déjà.
132
+
133
+ Il ne dépend d'**aucun service d'IA**. Ni clé d'API, ni compte, ni appel sortant :
134
+ le seul réseau qu'il touche est ton serveur Appium local.
135
+
136
+ Il ne **remplace pas Appium Inspector** pour l'exploration manuelle. Il sert à
137
+ donner ces informations à un modèle, ce qu'une interface graphique ne sait pas
138
+ faire.
139
+
140
+ ## Licence
141
+
142
+ MIT.
@@ -0,0 +1,23 @@
1
+ {
2
+ "android": {
3
+ "platformName": "Android",
4
+ "automationName": "UiAutomator2",
5
+ "appPackage": "com.exemple.app",
6
+ "appActivity": ".MainActivity",
7
+ "noReset": true,
8
+ "newCommandTimeout": 300
9
+ },
10
+ "ios": {
11
+ "platformName": "iOS",
12
+ "automationName": "XCUITest",
13
+ "bundleId": "com.exemple.app",
14
+ "udid": "00000000-0000000000000000",
15
+ "noReset": true,
16
+ "newCommandTimeout": 300
17
+ },
18
+ "windows": {
19
+ "platformName": "Windows",
20
+ "automationName": "Windows",
21
+ "app": "Root"
22
+ }
23
+ }
@@ -0,0 +1,51 @@
1
+ """Essai manuel du serveur contre un Appium réel. Hors suite de tests : il exige
2
+ un appareil connecté et une application installée.
3
+
4
+ .venv\\Scripts\\python.exe essai_reel.py
5
+ """
6
+
7
+ # Standard library
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).parent / "src"))
12
+
13
+ # Local
14
+ from mcp_appium import caps, server
15
+
16
+
17
+ def titre(texte: str) -> None:
18
+ print(f"\n{'=' * 70}\n{texte}\n{'=' * 70}")
19
+
20
+
21
+ titre("1. Chargement des capabilities")
22
+ chargees = caps.load()
23
+ print(f"{len(chargees)} capabilities, plateforme = {chargees.get('platformName')}")
24
+
25
+ titre("2. Connexion")
26
+ print(server.connect_to_session())
27
+
28
+ titre("3. Session")
29
+ print(server.get_session_info())
30
+
31
+ titre("4. Arbre de l'ecran, simplifie")
32
+ arbre = server.get_page_source()
33
+ lignes = arbre.splitlines()
34
+ print(f"{len(lignes)} lignes, {len(arbre)} caracteres")
35
+ print("\n".join(lignes[:25]))
36
+
37
+ titre("5. Suggestions de selecteurs")
38
+ print(server.suggest_locators("E-mail")[:1200])
39
+
40
+ titre("6. Recherche par texte")
41
+ print(server.find_elements("E-mail")[:900])
42
+
43
+ titre("7. Recherche par selecteur")
44
+ print(server.find_elements("//android.widget.TextView")[:600])
45
+
46
+ titre("8. Capture")
47
+ image = server.screenshot()
48
+ print(f"type={type(image).__name__}, {len(image.data)} octets")
49
+
50
+ titre("9. Fermeture")
51
+ print(server.close_session())
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-appium"
7
+ version = "0.1.0"
8
+ description = "Serveur MCP qui donne à un assistant les éléments réels d'une application Appium, au lieu de le laisser les inventer."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Julien Becheny" }]
13
+ keywords = ["mcp", "appium", "test-automation", "mobile-testing", "android", "ios", "llm"]
14
+
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Software Development :: Testing",
24
+ ]
25
+
26
+ dependencies = [
27
+ # SDK 2 : FastMCP y est devenu MCPServer, l'API 1.x ne convient pas.
28
+ "mcp>=2.0.0",
29
+ "Appium-Python-Client>=3.0.0",
30
+ "lxml>=4.9.0",
31
+ "requests>=2.28.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ # Réduction des captures avant envoi au modèle. Sans Pillow, l'image part
36
+ # en pleine résolution : cela fonctionne, cela coûte plus de contexte.
37
+ image = ["Pillow>=10.0.0"]
38
+ dev = ["pytest>=7.0.0", "ruff>=0.5.0"]
39
+
40
+ [project.scripts]
41
+ mcp-appium = "mcp_appium.server:main"
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/julien-becheny/mcp-appium"
45
+ Issues = "https://github.com/julien-becheny/mcp-appium/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/mcp_appium"]
49
+
50
+ [tool.ruff]
51
+ line-length = 100
@@ -0,0 +1,8 @@
1
+ """Serveur MCP donnant à un assistant les éléments réels d'une application Appium.
2
+
3
+ Volontairement vide : importer ce paquet ne doit pas charger Appium, lxml et le
4
+ protocole MCP. Le serveur vit dans ``mcp_appium.server``, la configuration dans
5
+ ``mcp_appium.caps``, qui ne dépend que de la bibliothèque standard.
6
+ """
7
+
8
+ __version__ = "0.1.0"
@@ -0,0 +1,95 @@
1
+ """Chargement des capabilities Appium, pour la création de session.
2
+
3
+ Le cas courant est le rattachement à une session déjà ouverte : aucune
4
+ capability n'est alors nécessaire. Ce module ne sert qu'au cas inverse,
5
+ quand le serveur doit démarrer la session lui-même.
6
+
7
+ Trois sources, par ordre de priorité :
8
+
9
+ 1. la variable d'environnement ``MCP_APPIUM_CAPS``, qui contient un objet JSON ;
10
+ 2. le fichier désigné par ``MCP_APPIUM_CAPS_FILE`` ;
11
+ 3. ``appium-caps.json`` dans le répertoire courant.
12
+
13
+ Le fichier peut décrire une plateforme unique, ou plusieurs indexées par nom.
14
+ Dans le second cas, ``MCP_APPIUM_PLATFORM`` choisit laquelle, avec repli sur
15
+ la première déclarée.
16
+ """
17
+
18
+ # Standard library
19
+ import json
20
+ import os
21
+ from pathlib import Path
22
+ from typing import Optional
23
+
24
+ DEFAULT_FILENAME = "appium-caps.json"
25
+
26
+ # Une capability Appium est toujours préfixée, sauf les quelques clés W3C.
27
+ _W3C_KEYS = {"platformName", "browserName", "browserVersion", "acceptInsecureCerts"}
28
+
29
+
30
+ class CapabilitiesIntrouvables(RuntimeError):
31
+ """Aucune capability déclarée : la session ne peut pas être créée."""
32
+
33
+
34
+ def _normalise(caps: dict) -> dict:
35
+ """Préfixe les clés non W3C par ``appium:``, comme l'exige Appium 2."""
36
+ return {
37
+ k if (k in _W3C_KEYS or ":" in k) else f"appium:{k}": v
38
+ for k, v in caps.items()
39
+ }
40
+
41
+
42
+ def _looks_like_multi(data: dict) -> bool:
43
+ """Un dictionnaire de plateformes ne contient que des dictionnaires."""
44
+ return bool(data) and all(isinstance(v, dict) for v in data.values())
45
+
46
+
47
+ def _select(data: dict, platform: Optional[str]) -> dict:
48
+ if not _looks_like_multi(data):
49
+ return data
50
+
51
+ if platform and platform in data:
52
+ return data[platform]
53
+
54
+ if platform:
55
+ connus = ", ".join(sorted(data))
56
+ raise CapabilitiesIntrouvables(
57
+ f"Plateforme '{platform}' absente du fichier. Déclarées : {connus}."
58
+ )
59
+
60
+ return next(iter(data.values()))
61
+
62
+
63
+ def _read_source() -> tuple[dict, str]:
64
+ """Rend les capabilities brutes et l'origine, pour les messages d'erreur."""
65
+ inline = os.environ.get("MCP_APPIUM_CAPS")
66
+ if inline:
67
+ return json.loads(inline), "MCP_APPIUM_CAPS"
68
+
69
+ declared = os.environ.get("MCP_APPIUM_CAPS_FILE")
70
+ chemin = Path(declared) if declared else Path.cwd() / DEFAULT_FILENAME
71
+
72
+ if not chemin.is_file():
73
+ raise CapabilitiesIntrouvables(
74
+ f"Aucune capability trouvée. Attendu : {chemin}, ou la variable "
75
+ "MCP_APPIUM_CAPS. Le rattachement à une session existante, lui, "
76
+ "n'en demande aucune."
77
+ )
78
+
79
+ return json.loads(chemin.read_text(encoding="utf-8")), str(chemin)
80
+
81
+
82
+ def load(platform: Optional[str] = None) -> dict:
83
+ """Rend les capabilities prêtes à être passées à Appium.
84
+
85
+ Args:
86
+ platform: nom de la plateforme dans un fichier multi-plateformes.
87
+ À défaut, la variable ``MCP_APPIUM_PLATFORM`` est consultée.
88
+ """
89
+ data, origine = _read_source()
90
+
91
+ if not isinstance(data, dict):
92
+ raise CapabilitiesIntrouvables(f"{origine} ne décrit pas un objet JSON.")
93
+
94
+ choisi = platform or os.environ.get("MCP_APPIUM_PLATFORM") or None
95
+ return _normalise(_select(data, choisi))