permisapi-mcp 0.1.0__py3-none-any.whl

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,22 @@
1
+ """permisapi-mcp : serveur MCP pour PermisAPI.
2
+
3
+ Permet a Claude Desktop, Cursor, Windsurf, ou tout client MCP-compatible
4
+ de consulter les permis de construire France en langage naturel.
5
+
6
+ Usage :
7
+ pip install permisapi-mcp
8
+
9
+ # Configure Claude Desktop avec :
10
+ # {
11
+ # "mcpServers": {
12
+ # "permisapi": {
13
+ # "command": "permisapi-mcp",
14
+ # "env": {"PERMISAPI_KEY": "pk_live_..."}
15
+ # }
16
+ # }
17
+ # }
18
+
19
+ Voir docs/MCP_SETUP.md pour le guide complet.
20
+ """
21
+
22
+ __version__ = "0.1.0"
@@ -0,0 +1,85 @@
1
+ """Serveur MCP stdio pour PermisAPI.
2
+
3
+ Implemente le Model Context Protocol (Anthropic, novembre 2024) en
4
+ mode stdio pour Claude Desktop / Cursor / Windsurf / autres clients
5
+ MCP-compatibles.
6
+
7
+ Lance via la commande `permisapi-mcp` (entry point pyproject.toml).
8
+ La cle PermisAPI est lue depuis l'env PERMISAPI_KEY au demarrage.
9
+
10
+ Architecture :
11
+ - Imports `mcp` lib (Anthropic) au runtime, gracieux si absent
12
+ - Tools list construit a partir de tools.TOOL_SCHEMAS
13
+ - Dispatch via tools.call_tool() (logique testable separement)
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import logging
19
+ import sys
20
+
21
+ logger = logging.getLogger("permisapi-mcp")
22
+ logging.basicConfig(
23
+ level="INFO",
24
+ format="%(asctime)s %(levelname)-5s [permisapi-mcp] %(message)s",
25
+ stream=sys.stderr, # IMPORTANT : MCP stdio utilise stdout pour le protocole
26
+ )
27
+
28
+
29
+ def main() -> None:
30
+ """Entry point synchrone (appele par `permisapi-mcp` script)."""
31
+ try:
32
+ asyncio.run(_run_server())
33
+ except KeyboardInterrupt:
34
+ logger.info("Arret demande, bye.")
35
+ except Exception: # noqa: BLE001
36
+ logger.exception("Erreur fatale")
37
+ sys.exit(1)
38
+
39
+
40
+ async def _run_server() -> None:
41
+ """Cree le serveur MCP et lance le transport stdio."""
42
+ try:
43
+ from mcp.server import Server
44
+ from mcp.server.stdio import stdio_server
45
+ from mcp.types import TextContent, Tool
46
+ except ImportError as exc:
47
+ logger.error(
48
+ "Le package `mcp` n'est pas installe. Lance "
49
+ "`pip install permisapi-mcp` pour tout installer correctement. "
50
+ "Erreur : %s",
51
+ exc,
52
+ )
53
+ sys.exit(2)
54
+
55
+ from permisapi_mcp.tools import TOOL_SCHEMAS, call_tool
56
+
57
+ app = Server("permisapi")
58
+
59
+ @app.list_tools()
60
+ async def list_tools() -> list[Tool]:
61
+ return [
62
+ Tool(
63
+ name=t["name"],
64
+ description=t["description"],
65
+ inputSchema=t["inputSchema"],
66
+ )
67
+ for t in TOOL_SCHEMAS
68
+ ]
69
+
70
+ @app.call_tool()
71
+ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextContent]:
72
+ result_text = await call_tool(name, arguments or {})
73
+ return [TextContent(type="text", text=result_text)]
74
+
75
+ logger.info("permisapi-mcp pret. Connecte via stdio.")
76
+ async with stdio_server() as (read_stream, write_stream):
77
+ await app.run(
78
+ read_stream,
79
+ write_stream,
80
+ app.create_initialization_options(),
81
+ )
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
permisapi_mcp/tools.py ADDED
@@ -0,0 +1,378 @@
1
+ """Tools metier : appels a l'API PermisAPI, independants du protocole MCP.
2
+
3
+ Cette couche est testable sans avoir le `mcp` package installe : on
4
+ mock juste httpx ou on utilise MockTransport. Le server.py construit
5
+ les Tool MCP a partir d'ici.
6
+
7
+ 6 tools exposes :
8
+ 1. search_permits : GET /v1/permits avec filtres
9
+ 2. get_permit_details : GET /v1/permits/{num_pa}
10
+ 3. find_dvf_neighbors : GET /v1/permits/{num_pa}/dvf
11
+ 4. get_mdb_score : GET /v1/permits/{num_pa}/score
12
+ 5. get_plu_zoning : GET /v1/permits/{num_pa}/plu
13
+ 6. get_risks : GET /v1/permits/{num_pa}/risks
14
+
15
+ Securite : la cle API du user est lue depuis l'env PERMISAPI_KEY au
16
+ demarrage du serveur, jamais transmise via les arguments d'un tool.
17
+ Pas de risque qu'un LLM puisse "leaker" la cle dans une reponse.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ from typing import Any
24
+
25
+ import httpx
26
+
27
+ PERMISAPI_BASE = os.environ.get("PERMISAPI_BASE_URL", "https://api.permisapi.fr")
28
+ HTTP_TIMEOUT = 15.0
29
+
30
+
31
+ class PermisapiError(Exception):
32
+ """Erreur retournee par l'API PermisAPI (4xx / 5xx)."""
33
+
34
+ def __init__(self, status_code: int, detail: str):
35
+ self.status_code = status_code
36
+ self.detail = detail
37
+ super().__init__(f"PermisAPI {status_code} : {detail}")
38
+
39
+
40
+ def _api_key() -> str:
41
+ """Lit la cle PermisAPI depuis l'env. Raise si absente."""
42
+ key = os.environ.get("PERMISAPI_KEY")
43
+ if not key:
44
+ raise RuntimeError(
45
+ "PERMISAPI_KEY non configuree. Ajoute-la dans la config "
46
+ "MCP de ton client (Claude Desktop, Cursor, etc). Obtiens "
47
+ "une cle gratuite sur https://permisapi.fr/#pricing"
48
+ )
49
+ return key
50
+
51
+
52
+ async def _http_get(
53
+ path: str,
54
+ params: dict[str, Any] | None = None,
55
+ *,
56
+ client: httpx.AsyncClient | None = None,
57
+ ) -> dict[str, Any]:
58
+ """Helper GET sur PermisAPI avec auth + gestion d'erreur."""
59
+ headers = {
60
+ "X-API-Key": _api_key(),
61
+ "Accept": "application/json",
62
+ "User-Agent": "permisapi-mcp/0.1.0",
63
+ }
64
+ own_client = client is None
65
+ c = client or httpx.AsyncClient(timeout=HTTP_TIMEOUT)
66
+ try:
67
+ resp = await c.get(f"{PERMISAPI_BASE}{path}", params=params, headers=headers)
68
+ finally:
69
+ if own_client:
70
+ await c.aclose()
71
+
72
+ if resp.status_code >= 400:
73
+ try:
74
+ payload = resp.json()
75
+ detail = payload.get("detail") or payload.get("message") or resp.text
76
+ except (ValueError, AttributeError):
77
+ detail = resp.text[:200]
78
+ raise PermisapiError(resp.status_code, str(detail))
79
+
80
+ return resp.json()
81
+
82
+
83
+ # ----------------------------------------------------------------------------
84
+ # Tool definitions (JSON Schema for inputs)
85
+ # ----------------------------------------------------------------------------
86
+
87
+
88
+ TOOL_SCHEMAS: list[dict[str, Any]] = [
89
+ {
90
+ "name": "search_permits",
91
+ "description": (
92
+ "Recherche des permis de construire France avec filtres "
93
+ "combinables : departement, commune, type de permis, etat, "
94
+ "dates, surface min, SIREN demandeur. Retourne une page de "
95
+ "permits avec leurs infos de base. Plan Free OK."
96
+ ),
97
+ "inputSchema": {
98
+ "type": "object",
99
+ "properties": {
100
+ "dep_code": {
101
+ "type": "string",
102
+ "description": "Code departement INSEE 2 chars (ex 75 Paris, 33 Bordeaux). Requis pour Free/Explorer.",
103
+ },
104
+ "comm_code": {
105
+ "type": "string",
106
+ "description": "Code commune INSEE 5 chars (ex 75116 Paris 16e).",
107
+ },
108
+ "permit_type": {
109
+ "type": "string",
110
+ "enum": [
111
+ "PC_LOGEMENT", "PC_LOCAUX",
112
+ "DP_LOGEMENT", "DP_LOCAUX",
113
+ "PA", "PD",
114
+ ],
115
+ },
116
+ "etat_pa": {
117
+ "type": "integer",
118
+ "description": "Etat permis : 1=Accorde, 2=Tacite, 3=Refuse, 4=Irrecevable, 5=Retrait, 6=Acheve.",
119
+ "minimum": 1, "maximum": 6,
120
+ },
121
+ "date_from": {"type": "string", "description": "YYYY-MM-DD."},
122
+ "date_to": {"type": "string", "description": "YYYY-MM-DD."},
123
+ "min_superficie": {
124
+ "type": "integer", "description": "Surface terrain min en m².",
125
+ },
126
+ "siren_dem": {
127
+ "type": "string",
128
+ "description": "SIREN du demandeur (9 chiffres).",
129
+ },
130
+ "limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20},
131
+ },
132
+ },
133
+ },
134
+ {
135
+ "name": "get_permit_details",
136
+ "description": (
137
+ "Recupere tous les details d'un permis a partir de son "
138
+ "identifiant Sitadel (num_pa) : adresse complete, demandeur, "
139
+ "dates, surface, parcelle cadastre, lat/lng. Plan Free OK."
140
+ ),
141
+ "inputSchema": {
142
+ "type": "object",
143
+ "properties": {
144
+ "num_pa": {
145
+ "type": "string",
146
+ "description": "Identifiant Sitadel unique (ex PC07404021K1).",
147
+ },
148
+ },
149
+ "required": ["num_pa"],
150
+ },
151
+ },
152
+ {
153
+ "name": "find_dvf_neighbors",
154
+ "description": (
155
+ "Pour un permis, retourne les top transactions immobilieres "
156
+ "DVF voisines (5 ans glissants). Permet d'estimer la valeur "
157
+ "fonciere du quartier. Plan Pro+ uniquement."
158
+ ),
159
+ "inputSchema": {
160
+ "type": "object",
161
+ "properties": {
162
+ "num_pa": {"type": "string"},
163
+ "limit": {"type": "integer", "minimum": 1, "maximum": 5, "default": 3},
164
+ "type_local": {
165
+ "type": "string",
166
+ "description": "CSV des types : 1=Maison, 2=Appartement, 3=Dependance, 4=Local commercial.",
167
+ },
168
+ "min_year": {"type": "integer", "minimum": 2014, "maximum": 2100},
169
+ },
170
+ "required": ["num_pa"],
171
+ },
172
+ },
173
+ {
174
+ "name": "get_mdb_score",
175
+ "description": (
176
+ "Calcule le Score Opportunite Marchand de Biens v0.1 pour "
177
+ "un permis (note 0-100 + tier low/medium/high/premium + "
178
+ "breakdown de 7 signaux ponderes). Plan Pro+ uniquement."
179
+ ),
180
+ "inputSchema": {
181
+ "type": "object",
182
+ "properties": {
183
+ "num_pa": {"type": "string"},
184
+ },
185
+ "required": ["num_pa"],
186
+ },
187
+ },
188
+ {
189
+ "name": "get_plu_zoning",
190
+ "description": (
191
+ "Retourne le zonage urbanisme PLU au point geocode du permis "
192
+ "(UA/UB urbain, AU a urbaniser, A agricole, N naturelle) avec "
193
+ "verdict booleen constructible et raison juridique. Source "
194
+ "Geoportail de l'Urbanisme. Plan Pro+ uniquement."
195
+ ),
196
+ "inputSchema": {
197
+ "type": "object",
198
+ "properties": {
199
+ "num_pa": {"type": "string"},
200
+ },
201
+ "required": ["num_pa"],
202
+ },
203
+ },
204
+ {
205
+ "name": "get_risks",
206
+ "description": (
207
+ "Risques naturels et technologiques (inondation, seisme, "
208
+ "argile, ICPE proches) connus sur la commune du permis. "
209
+ "Score agrege 0-100 + tier. Source Georisques BRGM. Plan "
210
+ "Pro+ uniquement."
211
+ ),
212
+ "inputSchema": {
213
+ "type": "object",
214
+ "properties": {
215
+ "num_pa": {"type": "string"},
216
+ },
217
+ "required": ["num_pa"],
218
+ },
219
+ },
220
+ ]
221
+
222
+
223
+ # ----------------------------------------------------------------------------
224
+ # Tool implementations
225
+ # ----------------------------------------------------------------------------
226
+
227
+
228
+ def _validate_num_pa(num_pa: Any) -> str:
229
+ """Defense en profondeur cote MCP : valide que num_pa est string et
230
+ safe (pas d'injection URL).
231
+ """
232
+ if not isinstance(num_pa, str):
233
+ raise ValueError(f"num_pa doit etre une string, recu : {type(num_pa).__name__}")
234
+ if not num_pa or len(num_pa) > 50:
235
+ raise ValueError("num_pa doit faire entre 1 et 50 caracteres")
236
+ # Whitelist : alphanumeric + space + - + _
237
+ for c in num_pa:
238
+ if not (c.isalnum() or c in " -_"):
239
+ raise ValueError(f"num_pa contient un caractere interdit : {c!r}")
240
+ return num_pa
241
+
242
+
243
+ async def search_permits(
244
+ arguments: dict[str, Any],
245
+ *,
246
+ client: httpx.AsyncClient | None = None,
247
+ ) -> dict[str, Any]:
248
+ """GET /v1/permits avec filtres."""
249
+ params: dict[str, Any] = {}
250
+ for k in (
251
+ "dep_code", "comm_code", "permit_type", "etat_pa",
252
+ "date_from", "date_to", "min_superficie", "siren_dem",
253
+ ):
254
+ v = arguments.get(k)
255
+ if v is not None and v != "":
256
+ params[k] = v
257
+ limit = arguments.get("limit", 20)
258
+ params["limit"] = max(1, min(50, int(limit)))
259
+ return await _http_get("/v1/permits", params=params, client=client)
260
+
261
+
262
+ async def get_permit_details(
263
+ arguments: dict[str, Any],
264
+ *,
265
+ client: httpx.AsyncClient | None = None,
266
+ ) -> dict[str, Any]:
267
+ """GET /v1/permits/{num_pa}."""
268
+ num_pa = _validate_num_pa(arguments.get("num_pa"))
269
+ return await _http_get(f"/v1/permits/{num_pa}", client=client)
270
+
271
+
272
+ async def find_dvf_neighbors(
273
+ arguments: dict[str, Any],
274
+ *,
275
+ client: httpx.AsyncClient | None = None,
276
+ ) -> dict[str, Any]:
277
+ """GET /v1/permits/{num_pa}/dvf."""
278
+ num_pa = _validate_num_pa(arguments.get("num_pa"))
279
+ params: dict[str, Any] = {}
280
+ if "limit" in arguments and arguments["limit"] is not None:
281
+ params["limit"] = max(1, min(5, int(arguments["limit"])))
282
+ if arguments.get("type_local"):
283
+ params["type_local"] = arguments["type_local"]
284
+ if arguments.get("min_year") is not None:
285
+ params["min_year"] = int(arguments["min_year"])
286
+ return await _http_get(
287
+ f"/v1/permits/{num_pa}/dvf", params=params or None, client=client
288
+ )
289
+
290
+
291
+ async def get_mdb_score(
292
+ arguments: dict[str, Any],
293
+ *,
294
+ client: httpx.AsyncClient | None = None,
295
+ ) -> dict[str, Any]:
296
+ """GET /v1/permits/{num_pa}/score."""
297
+ num_pa = _validate_num_pa(arguments.get("num_pa"))
298
+ return await _http_get(f"/v1/permits/{num_pa}/score", client=client)
299
+
300
+
301
+ async def get_plu_zoning(
302
+ arguments: dict[str, Any],
303
+ *,
304
+ client: httpx.AsyncClient | None = None,
305
+ ) -> dict[str, Any]:
306
+ """GET /v1/permits/{num_pa}/plu."""
307
+ num_pa = _validate_num_pa(arguments.get("num_pa"))
308
+ return await _http_get(f"/v1/permits/{num_pa}/plu", client=client)
309
+
310
+
311
+ async def get_risks(
312
+ arguments: dict[str, Any],
313
+ *,
314
+ client: httpx.AsyncClient | None = None,
315
+ ) -> dict[str, Any]:
316
+ """GET /v1/permits/{num_pa}/risks."""
317
+ num_pa = _validate_num_pa(arguments.get("num_pa"))
318
+ return await _http_get(f"/v1/permits/{num_pa}/risks", client=client)
319
+
320
+
321
+ # ----------------------------------------------------------------------------
322
+ # Dispatch
323
+ # ----------------------------------------------------------------------------
324
+
325
+
326
+ TOOL_HANDLERS = {
327
+ "search_permits": search_permits,
328
+ "get_permit_details": get_permit_details,
329
+ "find_dvf_neighbors": find_dvf_neighbors,
330
+ "get_mdb_score": get_mdb_score,
331
+ "get_plu_zoning": get_plu_zoning,
332
+ "get_risks": get_risks,
333
+ }
334
+
335
+
336
+ async def call_tool(
337
+ name: str,
338
+ arguments: dict[str, Any],
339
+ *,
340
+ client: httpx.AsyncClient | None = None,
341
+ ) -> str:
342
+ """Dispatch un appel de tool MCP vers la bonne fonction handler.
343
+
344
+ Retourne du texte JSON-encoded (le format attendu par MCP TextContent).
345
+ Les erreurs sont retournees comme texte d'erreur, pas raised, car
346
+ MCP attend toujours du contenu (pas une exception).
347
+ """
348
+ handler = TOOL_HANDLERS.get(name)
349
+ if handler is None:
350
+ return json.dumps({"error": f"Tool inconnu : {name}"}, ensure_ascii=False)
351
+
352
+ try:
353
+ result = await handler(arguments, client=client)
354
+ return json.dumps(result, ensure_ascii=False, indent=2)
355
+ except PermisapiError as exc:
356
+ return json.dumps(
357
+ {
358
+ "error": "permisapi_error",
359
+ "status_code": exc.status_code,
360
+ "detail": exc.detail,
361
+ },
362
+ ensure_ascii=False,
363
+ )
364
+ except ValueError as exc:
365
+ return json.dumps(
366
+ {"error": "validation", "detail": str(exc)},
367
+ ensure_ascii=False,
368
+ )
369
+ except RuntimeError as exc:
370
+ return json.dumps(
371
+ {"error": "config", "detail": str(exc)},
372
+ ensure_ascii=False,
373
+ )
374
+ except Exception as exc: # noqa: BLE001
375
+ return json.dumps(
376
+ {"error": "internal", "detail": f"{type(exc).__name__}: {exc}"},
377
+ ensure_ascii=False,
378
+ )
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: permisapi-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server pour PermisAPI : permettez a Claude / ChatGPT / Cursor de consulter les permis de construire France en langage naturel.
5
+ Project-URL: Homepage, https://permisapi.fr/mcp
6
+ Project-URL: Documentation, https://permisapi.fr/mcp
7
+ Project-URL: Setup, https://github.com/Evan-Crx/permisapi/blob/master/docs/MCP_SETUP.md
8
+ Author-email: Evan Caroux <evan@permisapi.fr>
9
+ License: MIT
10
+ Keywords: anthropic,chatgpt,claude,construire,france,mcp,permis,real-estate,sitadel
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Natural Language :: French
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: mcp>=1.0.0
22
+ Requires-Dist: permisapi-client>=0.6.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
25
+ Requires-Dist: pytest>=7.4; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # permisapi-mcp
29
+
30
+ Serveur **MCP** (Model Context Protocol, Anthropic) pour [PermisAPI](https://permisapi.fr).
31
+
32
+ Permet à **Claude Desktop**, **Cursor**, **Windsurf** ou tout client MCP-compatible de consulter les **311 000 permis de construire France** en langage naturel.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install permisapi-mcp
38
+ ```
39
+
40
+ Vous avez besoin d'une clé API PermisAPI (gratuite pour commencer) :
41
+ [https://permisapi.fr/#pricing](https://permisapi.fr/#pricing)
42
+
43
+ ## Configuration Claude Desktop
44
+
45
+ Éditez `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) ou `%APPDATA%\Claude\claude_desktop_config.json` (Windows) :
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "permisapi": {
51
+ "command": "permisapi-mcp",
52
+ "env": {
53
+ "PERMISAPI_KEY": "pk_live_VOTRE_CLE"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ Redémarrez Claude Desktop. Vous pouvez maintenant demander :
61
+
62
+ > *« Liste les permis de logement déposés à Bordeaux ce mois avec un score MDB > 70 »*
63
+ >
64
+ > *« Trouve-moi des opportunités MDB autour de la rue de Passy à Paris »*
65
+ >
66
+ > *« Quel est le zonage PLU du permis PC07404021K1 ? »*
67
+
68
+ ## Configuration Cursor / Windsurf / autres clients
69
+
70
+ Voir le guide complet : [https://permisapi.fr/mcp](https://permisapi.fr/mcp)
71
+
72
+ ## Tools disponibles (6)
73
+
74
+ | Tool | Endpoint | Plan requis |
75
+ |---|---|:---:|
76
+ | `search_permits` | GET /v1/permits | Free |
77
+ | `get_permit_details` | GET /v1/permits/{num_pa} | Free |
78
+ | `find_dvf_neighbors` | GET /v1/permits/{num_pa}/dvf | Pro |
79
+ | `get_mdb_score` | GET /v1/permits/{num_pa}/score | Pro |
80
+ | `get_plu_zoning` | GET /v1/permits/{num_pa}/plu | Pro |
81
+ | `get_risks` | GET /v1/permits/{num_pa}/risks | Pro |
82
+
83
+ ## Sécurité
84
+
85
+ - La clé API reste **côté user** (env var locale, jamais transmise au LLM)
86
+ - Le LLM voit uniquement les arguments des tools (pas la clé)
87
+ - Validation stricte des inputs (regex sur `num_pa`, ranges Pydantic)
88
+ - Pas de side-effects : tous les tools sont **read-only** (GET uniquement)
89
+
90
+ ## Licence
91
+
92
+ MIT.
93
+
94
+ ## Support
95
+
96
+ evan@permisapi.fr — réponse 24-72h.
@@ -0,0 +1,7 @@
1
+ permisapi_mcp/__init__.py,sha256=w827Sv18pA1gI-2GxfRkPnziVbIPfFbv6165oipUZfg,539
2
+ permisapi_mcp/server.py,sha256=Gb7Nsya1kmyzTjSs_34bE8c_flX1bwugdddEzE0-mB4,2639
3
+ permisapi_mcp/tools.py,sha256=uEp63BfEM_rFPcsOTp0rw9PwgEPQX2QMaJGXajKzIMg,13357
4
+ permisapi_mcp-0.1.0.dist-info/METADATA,sha256=LSq1STXBhcI-N-7yPVtqAbcInTZWFS1OWxXmL6eMy-0,3246
5
+ permisapi_mcp-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ permisapi_mcp-0.1.0.dist-info/entry_points.txt,sha256=LOkP_9LtZ3k-jL4_svD3dJ2F4fQsm4lGSGkguj497XE,60
7
+ permisapi_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ permisapi-mcp = permisapi_mcp.server:main