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.
- mcp_appium-0.1.0/.gitignore +13 -0
- mcp_appium-0.1.0/LICENSE +21 -0
- mcp_appium-0.1.0/PKG-INFO +172 -0
- mcp_appium-0.1.0/README.md +142 -0
- mcp_appium-0.1.0/examples/appium-caps.json +23 -0
- mcp_appium-0.1.0/examples/essai_manuel.py +51 -0
- mcp_appium-0.1.0/pyproject.toml +51 -0
- mcp_appium-0.1.0/src/mcp_appium/__init__.py +8 -0
- mcp_appium-0.1.0/src/mcp_appium/caps.py +95 -0
- mcp_appium-0.1.0/src/mcp_appium/server.py +823 -0
- mcp_appium-0.1.0/tests/test_caps.py +115 -0
|
@@ -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
|
mcp_appium-0.1.0/LICENSE
ADDED
|
@@ -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))
|