fenix-mcp 0.1.0__py3-none-any.whl → 0.2.1__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.
@@ -4,7 +4,6 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import asyncio
7
- from dataclasses import dataclass
8
7
  from datetime import datetime
9
8
  from typing import Any, Dict, Iterable, List, Optional
10
9
 
@@ -94,7 +93,9 @@ class ProductivityService:
94
93
  async def stats(self) -> Dict[str, Any]:
95
94
  return await self._call(self._api.get_todo_stats) or {}
96
95
 
97
- async def search(self, query: str, *, limit: int, offset: int) -> List[Dict[str, Any]]:
96
+ async def search(
97
+ self, query: str, *, limit: int, offset: int
98
+ ) -> List[Dict[str, Any]]:
98
99
  # API atual não expõe paginação no endpoint de busca, mas mantemos
99
100
  # assinatura para possível suporte futuro.
100
101
  result = await self._call(self._api.search_todo_items, query=query)
@@ -21,13 +21,20 @@ class UserConfigService:
21
21
  payload = _strip_none(data)
22
22
  return await asyncio.to_thread(self._api.create_user_core_document, payload)
23
23
 
24
- async def list(self, *, returnContent: Optional[bool] = None, **_: Any) -> List[Dict[str, Any]]:
25
- return await asyncio.to_thread(
26
- self._api.list_user_core_documents,
27
- return_content=bool(returnContent),
28
- ) or []
24
+ async def list(
25
+ self, *, returnContent: Optional[bool] = None, **_: Any
26
+ ) -> List[Dict[str, Any]]:
27
+ return (
28
+ await asyncio.to_thread(
29
+ self._api.list_user_core_documents,
30
+ return_content=bool(returnContent),
31
+ )
32
+ or []
33
+ )
29
34
 
30
- async def get(self, doc_id: str, *, returnContent: Optional[bool] = None, **_: Any) -> Dict[str, Any]:
35
+ async def get(
36
+ self, doc_id: str, *, returnContent: Optional[bool] = None, **_: Any
37
+ ) -> Dict[str, Any]:
31
38
  return await asyncio.to_thread(
32
39
  self._api.get_user_core_document,
33
40
  doc_id,
@@ -36,7 +43,9 @@ class UserConfigService:
36
43
 
37
44
  async def update(self, doc_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
38
45
  payload = _strip_none(data)
39
- return await asyncio.to_thread(self._api.update_user_core_document, doc_id, payload)
46
+ return await asyncio.to_thread(
47
+ self._api.update_user_core_document, doc_id, payload
48
+ )
40
49
 
41
50
  async def delete(self, doc_id: str) -> None:
42
51
  await asyncio.to_thread(self._api.delete_user_core_document, doc_id)
@@ -17,14 +17,18 @@ class Settings(BaseSettings):
17
17
  default="https://fenix-api-production-7619.up.railway.app",
18
18
  description="Base URL for the Fênix Cloud API.",
19
19
  )
20
- http_timeout: float = Field(default=30.0, description="Default HTTP timeout in seconds.")
20
+ http_timeout: float = Field(
21
+ default=30.0, description="Default HTTP timeout in seconds."
22
+ )
21
23
  log_level: str = Field(default="INFO", description="Root log level.")
22
24
  transport_mode: Literal["stdio", "http", "both"] = Field(
23
25
  default="stdio",
24
26
  description="Active transport mode: stdio, http or both.",
25
27
  )
26
28
  http_host: str = Field(default="127.0.0.1", description="HTTP bind host.")
27
- http_port: int = Field(default=3000, description="HTTP port when running with network transport.")
29
+ http_port: int = Field(
30
+ default=3000, description="HTTP port when running with network transport."
31
+ )
28
32
 
29
33
  model_config = SettingsConfigDict(
30
34
  populate_by_name=True,
@@ -17,4 +17,3 @@ class AppContext:
17
17
  settings: Settings
18
18
  logger: Logger
19
19
  api_client: FenixApiClient
20
-
@@ -20,7 +20,9 @@ def _to_query_value(value: Any) -> Any:
20
20
 
21
21
 
22
22
  def _strip_none(data: Mapping[str, Any]) -> Dict[str, Any]:
23
- return {key: _to_query_value(value) for key, value in data.items() if value is not None}
23
+ return {
24
+ key: _to_query_value(value) for key, value in data.items() if value is not None
25
+ }
24
26
 
25
27
 
26
28
  @dataclass(slots=True)
@@ -103,7 +105,9 @@ class FenixApiClient:
103
105
  json: Optional[Mapping[str, Any]] = None,
104
106
  headers: Optional[Mapping[str, str]] = None,
105
107
  ) -> Any:
106
- response = self._http.request(method, endpoint, params=params, json=json, headers=headers)
108
+ response = self._http.request(
109
+ method, endpoint, params=params, json=json, headers=headers
110
+ )
107
111
  if response.status_code == 204:
108
112
  return None
109
113
 
@@ -114,7 +118,9 @@ class FenixApiClient:
114
118
 
115
119
  if not response.ok:
116
120
  message = payload.get("message") or payload.get("error") or response.text
117
- raise FenixApiError(f"HTTP {response.status_code} calling {endpoint}: {message}")
121
+ raise FenixApiError(
122
+ f"HTTP {response.status_code} calling {endpoint}: {message}"
123
+ )
118
124
 
119
125
  return payload.get("data", payload)
120
126
 
@@ -134,13 +140,27 @@ class FenixApiClient:
134
140
 
135
141
  def list_core_documents(self, *, return_content: bool = False) -> Any:
136
142
  params = self._build_params(optional={"returnContent": return_content})
137
- headers = {"x-mcp-token": self.core_documents_token} if self.core_documents_token else None
138
- return self._request("GET", "/api/core-documents/mcp/all", params=params, headers=headers)
143
+ headers = (
144
+ {"x-mcp-token": self.core_documents_token}
145
+ if self.core_documents_token
146
+ else None
147
+ )
148
+ return self._request(
149
+ "GET", "/api/core-documents/mcp/all", params=params, headers=headers
150
+ )
139
151
 
140
- def get_core_document_by_name(self, name: str, *, return_content: bool = False) -> Any:
152
+ def get_core_document_by_name(
153
+ self, name: str, *, return_content: bool = False
154
+ ) -> Any:
141
155
  params = self._build_params(optional={"returnContent": return_content})
142
- headers = {"x-mcp-token": self.core_documents_token} if self.core_documents_token else None
143
- return self._request("GET", f"/api/core-documents/mcp/{name}", params=params, headers=headers)
156
+ headers = (
157
+ {"x-mcp-token": self.core_documents_token}
158
+ if self.core_documents_token
159
+ else None
160
+ )
161
+ return self._request(
162
+ "GET", f"/api/core-documents/mcp/{name}", params=params, headers=headers
163
+ )
144
164
 
145
165
  def list_core_documents_auth(self, *, return_content: bool = False) -> Any:
146
166
  params = self._build_params(optional={"returnContent": return_content})
@@ -158,15 +178,23 @@ class FenixApiClient:
158
178
  params = self._build_params(optional={"returnContent": return_content})
159
179
  return self._request("GET", "/api/user-core-documents", params=params)
160
180
 
161
- def get_user_core_document(self, document_id: str, *, return_content: bool = False) -> Any:
181
+ def get_user_core_document(
182
+ self, document_id: str, *, return_content: bool = False
183
+ ) -> Any:
162
184
  params = self._build_params(optional={"returnContent": return_content})
163
- return self._request("GET", f"/api/user-core-documents/{document_id}", params=params)
185
+ return self._request(
186
+ "GET", f"/api/user-core-documents/{document_id}", params=params
187
+ )
164
188
 
165
189
  def create_user_core_document(self, payload: Mapping[str, Any]) -> Any:
166
190
  return self._request("POST", "/api/user-core-documents", json=payload)
167
191
 
168
- def update_user_core_document(self, document_id: str, payload: Mapping[str, Any]) -> Any:
169
- return self._request("PATCH", f"/api/user-core-documents/{document_id}", json=payload)
192
+ def update_user_core_document(
193
+ self, document_id: str, payload: Mapping[str, Any]
194
+ ) -> Any:
195
+ return self._request(
196
+ "PATCH", f"/api/user-core-documents/{document_id}", json=payload
197
+ )
170
198
 
171
199
  def delete_user_core_document(self, document_id: str) -> Any:
172
200
  return self._request("DELETE", f"/api/user-core-documents/{document_id}")
@@ -195,8 +223,12 @@ class FenixApiClient:
195
223
  def update_todo_item_status(self, item_id: str, payload: Mapping[str, Any]) -> Any:
196
224
  return self._request("PATCH", f"/api/todo-items/{item_id}/status", json=payload)
197
225
 
198
- def update_todo_item_priority(self, item_id: str, payload: Mapping[str, Any]) -> Any:
199
- return self._request("PATCH", f"/api/todo-items/{item_id}/priority", json=payload)
226
+ def update_todo_item_priority(
227
+ self, item_id: str, payload: Mapping[str, Any]
228
+ ) -> Any:
229
+ return self._request(
230
+ "PATCH", f"/api/todo-items/{item_id}/priority", json=payload
231
+ )
200
232
 
201
233
  def get_todo_stats(self) -> Any:
202
234
  return self._request("GET", "/api/todo-items/stats")
@@ -245,7 +277,9 @@ class FenixApiClient:
245
277
  include_content: bool = True,
246
278
  include_metadata: bool = True,
247
279
  ) -> Any:
248
- params = self._build_params(required={"content": include_content, "metadata": include_metadata})
280
+ params = self._build_params(
281
+ required={"content": include_content, "metadata": include_metadata}
282
+ )
249
283
  return self._request("GET", f"/api/memories/{memory_id}", params=params)
250
284
 
251
285
  def update_memory(self, memory_id: str, payload: Mapping[str, Any]) -> Any:
@@ -262,13 +296,19 @@ class FenixApiClient:
262
296
  return self._request("POST", f"/api/memories/{memory_id}/access")
263
297
 
264
298
  def find_similar_memories(self, payload: Mapping[str, Any]) -> Any:
265
- return self._request("POST", "/api/memory-intelligence/similarity", json=payload)
299
+ return self._request(
300
+ "POST", "/api/memory-intelligence/similarity", json=payload
301
+ )
266
302
 
267
303
  def consolidate_memories(self, payload: Mapping[str, Any]) -> Any:
268
- return self._request("POST", "/api/memory-intelligence/consolidate", json=payload)
304
+ return self._request(
305
+ "POST", "/api/memory-intelligence/consolidate", json=payload
306
+ )
269
307
 
270
308
  def smart_create_memory(self, payload: Mapping[str, Any]) -> Any:
271
- return self._request("POST", "/api/memory-intelligence/smart-create", json=payload)
309
+ return self._request(
310
+ "POST", "/api/memory-intelligence/smart-create", json=payload
311
+ )
272
312
 
273
313
  # ------------------------------------------------------------------
274
314
  # Configuration: modes and rules
@@ -377,12 +417,16 @@ class FenixApiClient:
377
417
  return self._request("GET", "/api/documentation", params=_strip_none(filters))
378
418
 
379
419
  def get_documentation_item(self, item_id: str, **filters: Any) -> Any:
380
- return self._request("GET", f"/api/documentation/{item_id}", params=_strip_none(filters))
420
+ return self._request(
421
+ "GET", f"/api/documentation/{item_id}", params=_strip_none(filters)
422
+ )
381
423
 
382
424
  def create_documentation_item(self, payload: Mapping[str, Any]) -> Any:
383
425
  return self._request("POST", "/api/documentation", json=payload)
384
426
 
385
- def update_documentation_item(self, item_id: str, payload: Mapping[str, Any]) -> Any:
427
+ def update_documentation_item(
428
+ self, item_id: str, payload: Mapping[str, Any]
429
+ ) -> Any:
386
430
  return self._request("PATCH", f"/api/documentation/{item_id}", json=payload)
387
431
 
388
432
  def delete_documentation_item(self, item_id: str) -> Any:
@@ -397,8 +441,12 @@ class FenixApiClient:
397
441
  def get_documentation_full_tree(self) -> Any:
398
442
  return self._request("GET", "/api/documentation/tree")
399
443
 
400
- def search_documentation_items(self, *, query: str, team_id: str, limit: int) -> Any:
401
- params = self._build_params(required={"q": query, "team_id": team_id, "limit": limit})
444
+ def search_documentation_items(
445
+ self, *, query: str, team_id: str, limit: int
446
+ ) -> Any:
447
+ params = self._build_params(
448
+ required={"q": query, "team_id": team_id, "limit": limit}
449
+ )
402
450
  return self._request("GET", "/api/documentation/search", params=params)
403
451
 
404
452
  def list_documentation_roots(self, *, team_id: str) -> Any:
@@ -414,16 +462,26 @@ class FenixApiClient:
414
462
  return self._request("GET", "/api/documentation/analytics", params=params)
415
463
 
416
464
  def move_documentation_item(self, item_id: str, payload: Mapping[str, Any]) -> Any:
417
- return self._request("PATCH", f"/api/documentation/{item_id}/move", json=payload)
465
+ return self._request(
466
+ "PATCH", f"/api/documentation/{item_id}/move", json=payload
467
+ )
418
468
 
419
469
  def publish_documentation_item(self, item_id: str) -> Any:
420
470
  return self._request("PATCH", f"/api/documentation/{item_id}/publish")
421
471
 
422
- def create_documentation_version(self, item_id: str, payload: Mapping[str, Any]) -> Any:
423
- return self._request("POST", f"/api/documentation/{item_id}/version", json=payload)
472
+ def create_documentation_version(
473
+ self, item_id: str, payload: Mapping[str, Any]
474
+ ) -> Any:
475
+ return self._request(
476
+ "POST", f"/api/documentation/{item_id}/version", json=payload
477
+ )
424
478
 
425
- def duplicate_documentation_item(self, item_id: str, payload: Mapping[str, Any]) -> Any:
426
- return self._request("POST", f"/api/documentation/{item_id}/duplicate", json=payload)
479
+ def duplicate_documentation_item(
480
+ self, item_id: str, payload: Mapping[str, Any]
481
+ ) -> Any:
482
+ return self._request(
483
+ "POST", f"/api/documentation/{item_id}/duplicate", json=payload
484
+ )
427
485
 
428
486
  # ------------------------------------------------------------------
429
487
  # Knowledge: work items
@@ -449,7 +507,9 @@ class FenixApiClient:
449
507
  return self._request("GET", "/api/work-items/backlog", params=params)
450
508
 
451
509
  def search_work_items(self, *, query: str, team_id: str, limit: int) -> Any:
452
- params = self._build_params(required={"q": query, "team_id": team_id, "limit": limit})
510
+ params = self._build_params(
511
+ required={"q": query, "team_id": team_id, "limit": limit}
512
+ )
453
513
  return self._request("GET", "/api/work-items/search", params=params)
454
514
 
455
515
  def get_work_items_analytics(self, *, team_id: str) -> Any:
@@ -458,7 +518,9 @@ class FenixApiClient:
458
518
 
459
519
  def get_work_items_velocity(self, *, team_id: str, sprints_count: int) -> Any:
460
520
  params = self._build_params(required={"sprints_count": sprints_count})
461
- return self._request("GET", f"/api/work-items/velocity/{team_id}", params=params)
521
+ return self._request(
522
+ "GET", f"/api/work-items/velocity/{team_id}", params=params
523
+ )
462
524
 
463
525
  def list_work_items_by_sprint(self, *, sprint_id: str) -> Any:
464
526
  return self._request("GET", f"/api/work-items/by-sprint/{sprint_id}")
@@ -513,7 +575,9 @@ class FenixApiClient:
513
575
  return self._request("GET", "/api/work-boards/favorites")
514
576
 
515
577
  def search_work_boards(self, *, query: str, team_id: str, limit: int) -> Any:
516
- params = self._build_params(required={"q": query, "team_id": team_id, "limit": limit})
578
+ params = self._build_params(
579
+ required={"q": query, "team_id": team_id, "limit": limit}
580
+ )
517
581
  return self._request("GET", "/api/work-boards/search", params=params)
518
582
 
519
583
  def list_recent_work_boards(self, *, limit: int) -> Any:
@@ -535,8 +599,12 @@ class FenixApiClient:
535
599
  def list_work_board_columns(self, board_id: str) -> Any:
536
600
  return self._request("GET", f"/api/work-boards/{board_id}/columns")
537
601
 
538
- def toggle_work_board_favorite(self, board_id: str, payload: Mapping[str, Any]) -> Any:
539
- return self._request("PATCH", f"/api/work-boards/{board_id}/favorite", json=payload)
602
+ def toggle_work_board_favorite(
603
+ self, board_id: str, payload: Mapping[str, Any]
604
+ ) -> Any:
605
+ return self._request(
606
+ "PATCH", f"/api/work-boards/{board_id}/favorite", json=payload
607
+ )
540
608
 
541
609
  def clone_work_board(self, board_id: str, payload: Mapping[str, Any]) -> Any:
542
610
  return self._request("POST", f"/api/work-boards/{board_id}/clone", json=payload)
@@ -553,8 +621,12 @@ class FenixApiClient:
553
621
  def get_work_board_column(self, column_id: str) -> Any:
554
622
  return self._request("GET", f"/api/work-boards/columns/{column_id}")
555
623
 
556
- def update_work_board_column(self, column_id: str, payload: Mapping[str, Any]) -> Any:
557
- return self._request("PATCH", f"/api/work-boards/columns/{column_id}", json=payload)
624
+ def update_work_board_column(
625
+ self, column_id: str, payload: Mapping[str, Any]
626
+ ) -> Any:
627
+ return self._request(
628
+ "PATCH", f"/api/work-boards/columns/{column_id}", json=payload
629
+ )
558
630
 
559
631
  def delete_work_board_column(self, column_id: str) -> Any:
560
632
  return self._request("DELETE", f"/api/work-boards/columns/{column_id}")
@@ -586,7 +658,9 @@ class FenixApiClient:
586
658
  return self._request("GET", f"/api/sprints/recent/{team_id}", params=params)
587
659
 
588
660
  def search_sprints(self, *, query: str, team_id: str, limit: int) -> Any:
589
- params = self._build_params(required={"q": query, "team_id": team_id, "limit": limit})
661
+ params = self._build_params(
662
+ required={"q": query, "team_id": team_id, "limit": limit}
663
+ )
590
664
  return self._request("GET", "/api/sprints/search", params=params)
591
665
 
592
666
  def get_active_sprint(self, *, team_id: str) -> Any:
@@ -601,8 +675,12 @@ class FenixApiClient:
601
675
  def get_sprint_work_items(self, sprint_id: str) -> Any:
602
676
  return self._request("GET", f"/api/sprints/{sprint_id}/work-items")
603
677
 
604
- def add_work_items_to_sprint(self, sprint_id: str, payload: Mapping[str, Any]) -> Any:
605
- return self._request("POST", f"/api/sprints/{sprint_id}/work-items", json=payload)
678
+ def add_work_items_to_sprint(
679
+ self, sprint_id: str, payload: Mapping[str, Any]
680
+ ) -> Any:
681
+ return self._request(
682
+ "POST", f"/api/sprints/{sprint_id}/work-items", json=payload
683
+ )
606
684
 
607
685
  def remove_work_items_from_sprint(self, payload: Mapping[str, Any]) -> Any:
608
686
  return self._request("DELETE", "/api/sprints/work-items", json=payload)
@@ -617,7 +695,9 @@ class FenixApiClient:
617
695
  return self._request("PATCH", f"/api/sprints/{sprint_id}/start", json=payload)
618
696
 
619
697
  def complete_sprint(self, sprint_id: str, payload: Mapping[str, Any]) -> Any:
620
- return self._request("PATCH", f"/api/sprints/{sprint_id}/complete", json=payload)
698
+ return self._request(
699
+ "PATCH", f"/api/sprints/{sprint_id}/complete", json=payload
700
+ )
621
701
 
622
702
  def cancel_sprint(self, sprint_id: str) -> Any:
623
703
  return self._request("PATCH", f"/api/sprints/{sprint_id}/cancel")
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
 
6
6
  import logging
7
7
  from dataclasses import dataclass, field
8
- from typing import Any, Dict, Mapping, Optional
8
+ from typing import Any, Mapping, Optional
9
9
 
10
10
  import requests
11
11
  from requests import Response, Session
@@ -3,9 +3,6 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- import asyncio
7
- import json
8
- import sys
9
6
  import uuid
10
7
  from dataclasses import dataclass
11
8
  from typing import Any, Dict, Optional
@@ -19,8 +19,7 @@ from fenix_mcp.infrastructure.config import Settings
19
19
  class Transport(Protocol):
20
20
  name: str
21
21
 
22
- async def serve_forever(self) -> None:
23
- ...
22
+ async def serve_forever(self) -> None: ...
24
23
 
25
24
 
26
25
  class StdIoTransport:
@@ -47,7 +46,9 @@ class StdIoTransport:
47
46
  try:
48
47
  request = json.loads(line)
49
48
  except json.JSONDecodeError as exc: # pragma: no cover - defensive
50
- self._logger.warning("Invalid JSON received on STDIO", extra={"error": str(exc)})
49
+ self._logger.warning(
50
+ "Invalid JSON received on STDIO", extra={"error": str(exc)}
51
+ )
51
52
  continue
52
53
 
53
54
  try:
@@ -109,14 +110,16 @@ class HttpTransport:
109
110
  site = web.TCPSite(runner, host=self._host, port=self._port)
110
111
  try:
111
112
  await site.start()
112
- except Exception as exc: # pragma: no cover - defensive
113
+ except Exception: # pragma: no cover - defensive
113
114
  self._logger.exception(
114
115
  "Failed to bind HTTP transport",
115
116
  extra={"host": self._host, "port": self._port},
116
117
  )
117
118
  raise
118
119
 
119
- self._logger.info("HTTP transport listening", extra={"host": self._host, "port": self._port})
120
+ self._logger.info(
121
+ "HTTP transport listening", extra={"host": self._host, "port": self._port}
122
+ )
120
123
  self._runner = runner
121
124
 
122
125
  async def _cleanup(self) -> None:
@@ -126,7 +129,9 @@ class HttpTransport:
126
129
 
127
130
  def _with_cors(self, response: web.StreamResponse) -> web.StreamResponse:
128
131
  response.headers.setdefault("Access-Control-Allow-Origin", "*")
129
- response.headers.setdefault("Access-Control-Allow-Headers", "Content-Type, Authorization")
132
+ response.headers.setdefault(
133
+ "Access-Control-Allow-Headers", "Content-Type, Authorization"
134
+ )
130
135
  response.headers.setdefault("Access-Control-Allow-Methods", "POST, OPTIONS")
131
136
  return response
132
137
 
@@ -142,9 +147,11 @@ class HttpTransport:
142
147
  return self._with_cors(web.Response(status=204))
143
148
 
144
149
  async def _handle_jsonrpc(self, request: web.Request) -> web.StreamResponse:
145
- auth_header = request.headers.get('Authorization') or request.headers.get('authorization')
146
- if auth_header and auth_header.lower().startswith('bearer '):
147
- token = auth_header.split(' ', 1)[1].strip()
150
+ auth_header = request.headers.get("Authorization") or request.headers.get(
151
+ "authorization"
152
+ )
153
+ if auth_header and auth_header.lower().startswith("bearer "):
154
+ token = auth_header.split(" ", 1)[1].strip()
148
155
  if token:
149
156
  self._server.set_personal_access_token(token)
150
157
 
@@ -219,7 +226,9 @@ class TransportFactory:
219
226
  transports.append(http_transport)
220
227
 
221
228
  if not transports:
222
- raise ValueError("No transport configured. Check FENIX_TRANSPORT_MODE env var.")
229
+ raise ValueError(
230
+ "No transport configured. Check FENIX_TRANSPORT_MODE env var."
231
+ )
223
232
 
224
233
  if len(transports) == 1:
225
234
  return transports[0]
fenix_mcp/main.py CHANGED
@@ -16,6 +16,7 @@ from fenix_mcp.infrastructure.fenix_api.client import FenixApiClient
16
16
  from fenix_mcp.interface.mcp_server import build_server
17
17
  from fenix_mcp.interface.transports import TransportFactory
18
18
 
19
+
19
20
  def _normalize_pat(token: str | None) -> str | None:
20
21
  if token is None:
21
22
  return None
@@ -63,7 +64,9 @@ async def _run_async(pat_token: str | None) -> None:
63
64
 
64
65
  async with AsyncExitStack() as stack:
65
66
  server = build_server(context=context)
66
- transport = await TransportFactory(settings, logger=logger).create(stack=stack, server=server)
67
+ transport = await TransportFactory(settings, logger=logger).create(
68
+ stack=stack, server=server
69
+ )
67
70
  logger.info("Fênix MCP server started (mode=%s)", transport.name)
68
71
  await transport.serve_forever()
69
72
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fenix-mcp
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Fênix Cloud MCP server implemented in Python
5
5
  Author: Fenix Inc
6
6
  Requires-Python: >=3.10
@@ -13,6 +13,11 @@ Requires-Dist: pydantic-settings>=2.0
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: pytest>=7.4; extra == "dev"
15
15
  Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
16
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
17
+ Requires-Dist: black>=23.0; extra == "dev"
18
+ Requires-Dist: flake8>=6.0; extra == "dev"
19
+ Requires-Dist: mypy>=1.0; extra == "dev"
20
+ Requires-Dist: twine>=4.0; extra == "dev"
16
21
 
17
22
  # Fênix MCP — Live Access to Fênix Cloud Data
18
23
 
@@ -132,17 +137,61 @@ Set `FENIX_TRANSPORT_MODE=both` to run STDIO and HTTP simultaneously. The defaul
132
137
 
133
138
  > Copy `.env.example` to `.env` for easier customization.
134
139
 
135
- ## 🧪 Local Testing
140
+
141
+ ## 🧪 Development
142
+
143
+ ### Local Testing
136
144
 
137
145
  ```bash
146
+ # Install development dependencies
138
147
  pip install -e .[dev]
148
+
149
+ # Run tests
139
150
  pytest
151
+
152
+ # Run with coverage
153
+ pytest --cov=fenix_mcp --cov-report=html
154
+
155
+ # Run linting
156
+ flake8 fenix_mcp/ tests/
157
+ black --check fenix_mcp/ tests/
158
+
159
+ # Run type checking
160
+ mypy fenix_mcp/
161
+
162
+ # Format code
163
+ black fenix_mcp/ tests/
140
164
  ```
141
165
 
166
+ ### Pre-commit Hooks (Optional)
167
+
168
+ ```bash
169
+ # Install pre-commit
170
+ pip install pre-commit
171
+
172
+ # Install hooks
173
+ pre-commit install
174
+
175
+ # Run on all files
176
+ pre-commit run --all-files
177
+ ```
178
+
179
+ ### Commit Convention
180
+
181
+ This project follows [Conventional Commits](https://www.conventionalcommits.org/):
182
+
183
+ - `fix:` - Bug fixes (patch version bump)
184
+ - `feat:` - New features (minor version bump)
185
+ - `BREAKING CHANGE:` - Breaking changes (major version bump)
186
+ - `chore:` - Maintenance tasks
187
+ - `docs:` - Documentation changes
188
+ - `test:` - Test additions/changes
189
+
142
190
  ## 🔄 Automation
143
191
 
144
- - **CI (GitHub Actions)** – runs on pushes and pull requests targeting `main`. It installs dependencies, runs `pytest`, builds the distribution artifacts, and uploads them as workflow artifacts.
145
- - **Publish workflow** – push a tag `v*` (or trigger the "Publish" workflow manually) to build the package and, if `PYPI_API_TOKEN` is set in repository secrets, upload artifacts to PyPI via `twine`.
192
+ - **CI (GitHub Actions)** – runs on pushes and pull requests targeting `main`. It installs dependencies, runs tests on Python 3.11, enforces flake8/black/mypy, generates coverage, builds the distribution (`python -m build`) and, on pushes, uploads artifacts for debugging.
193
+
194
+ - **Semantic Release** – after the CI job succeeds on `main`, the workflow installs the required `semantic-release` plugins and runs `npx semantic-release`. Conventional Commits decide the next version, `scripts/bump_version.py` updates `fenix_mcp.__version__`, the build artifacts are regenerated, and release notes/assets are published to GitHub and PyPI (using `PYPI_API_TOKEN`). If no eligible commit (`feat`, `fix`, or `BREAKING CHANGE`) exists since the last tag, no new release is produced.
146
195
 
147
196
  ## 🧰 Available Tools
148
197
 
@@ -200,8 +249,9 @@ STDIO stays active for MCP clients; HTTP will listen on `FENIX_HTTP_HOST:FENIX_H
200
249
  1. Fork the repository
201
250
  2. Create a branch: `git checkout -b feat/my-feature`
202
251
  3. Install dev dependencies: `pip install -e .[dev]`
203
- 4. Run `pytest`
204
- 5. Open a Pull Request describing your changes
252
+ 4. Use Conventional Commits (`feat:`, `fix:`, or `BREAKING CHANGE:`) so Semantic Release can infer the next version.
253
+ 5. Run `pytest`
254
+ 6. Open a Pull Request describing your changes
205
255
 
206
256
  ## 📄 License
207
257
 
@@ -0,0 +1,29 @@
1
+ fenix_mcp/__init__.py,sha256=LF6isSdae_j8s4tpM4kkMDDkxA1ezezbFtp2fukuWm8,615
2
+ fenix_mcp/main.py,sha256=iJV-9btNMDJMObvcn7wBQdbLLKjkYCQ1ANGEwHGHlMU,2857
3
+ fenix_mcp/application/presenters.py,sha256=fGME54PdCDhTBhXO-JUB9yLdBHiE1aeXLTC2fCuxnxM,689
4
+ fenix_mcp/application/tool_base.py,sha256=qUcb46qx9gHQfrSHgj4RD4NCHW-OIvKQdR5G9uxZ5l4,1316
5
+ fenix_mcp/application/tool_registry.py,sha256=bPT5g8GfxG_qu28R1WaDOZHvtmG6TPDvZi8eWj1T9xE,1250
6
+ fenix_mcp/application/tools/__init__.py,sha256=Gi1YvYh-KdL9HD8gLVrknHrxiKKEOhHBEZ02KBXJaKQ,796
7
+ fenix_mcp/application/tools/health.py,sha256=m5DxhoRbdwl6INzd6PISxv1NAv-ljCrezsr773VB0wE,834
8
+ fenix_mcp/application/tools/initialize.py,sha256=F3coSBzeVinrXBzIfuifGgfNwpireWoi4lgUn_s6fCk,4627
9
+ fenix_mcp/application/tools/intelligence.py,sha256=Tst0ydvaLgxx3GC1vyfNK3iWf2Nf46_A-DZGtgsMD-Q,12090
10
+ fenix_mcp/application/tools/knowledge.py,sha256=4ClGoFRqyFIPuzzg2DAg-w2eMvTP37mH0THXDGftinw,44634
11
+ fenix_mcp/application/tools/productivity.py,sha256=2IMkNdZ-Kd1CFAO7geruAVjtf_BWoDdbnwkl76vhtC8,9973
12
+ fenix_mcp/application/tools/user_config.py,sha256=8mPOZuwszO0TapxgrA7Foe15VQE874_mvfQYlGzyv4Y,6230
13
+ fenix_mcp/domain/initialization.py,sha256=LmDbTIfFtScfKMothKiCyEi4K0f8hEtsZcigpW5zd88,7345
14
+ fenix_mcp/domain/intelligence.py,sha256=8t4cgCsGwt8ouabVYrajp3-M90DepLnP_qZPNYnGCzU,4725
15
+ fenix_mcp/domain/knowledge.py,sha256=fKQOTt20u5aa5Yo7gPeQ1Qxa_K5pBxn1yn8FEfOFltM,20241
16
+ fenix_mcp/domain/productivity.py,sha256=nmHRuVJGRRu1s4eMoAv8vXHKsSauCPl-FvFx3I_yCTE,6661
17
+ fenix_mcp/domain/user_config.py,sha256=LzBDCk31gLMtKHTbBmYb9VoFPHDW6OydpmDXeHHd0Mw,1642
18
+ fenix_mcp/infrastructure/config.py,sha256=zhJ3hhsP-bRfICcdq8rIDh5NGDe_u7AGpcgjcc2U1nY,1908
19
+ fenix_mcp/infrastructure/context.py,sha256=kiDiamiPbHZpTGyZMylcQwtLhfaDXrxAkWSst_DWQNw,470
20
+ fenix_mcp/infrastructure/http_client.py,sha256=QLIPhGYR_cBQGsbIO4RTR6ksyvkQt-OKHQU1JhPyap8,2470
21
+ fenix_mcp/infrastructure/logging.py,sha256=bHrWlSi_0HshRe3--BK_5nzUszW-gh37q6jsd0ShS2Y,1371
22
+ fenix_mcp/infrastructure/fenix_api/client.py,sha256=9gYNTxky5i3bjTiiRXdnld6ZCvkTtf0Xdlm0FDtQi24,28239
23
+ fenix_mcp/interface/mcp_server.py,sha256=5UM2NJuNbwHkmCEprIFataJ5nFZiO8efTtP_oW3_iX0,2331
24
+ fenix_mcp/interface/transports.py,sha256=PxdhfjH8UMl03f7nuCLc-M6tMx6-Y-btVz_mSqXKrSI,8138
25
+ fenix_mcp-0.2.1.dist-info/METADATA,sha256=BS8uIDZk1lYVlyS_VnosK8ftqNB95kKiBPxSV2vDRUE,7260
26
+ fenix_mcp-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ fenix_mcp-0.2.1.dist-info/entry_points.txt,sha256=o52x_YHBupEd-1Z1GSfUjv3gJrx5_I-EkHhCgt1WBaE,49
28
+ fenix_mcp-0.2.1.dist-info/top_level.txt,sha256=2G1UtKpwjaIGQyE7sRoHecxaGLeuexfjrOUjv9DDKh4,10
29
+ fenix_mcp-0.2.1.dist-info/RECORD,,