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.
- fenix_mcp/__init__.py +4 -1
- fenix_mcp/application/tool_base.py +3 -2
- fenix_mcp/application/tool_registry.py +3 -1
- fenix_mcp/application/tools/health.py +1 -3
- fenix_mcp/application/tools/initialize.py +18 -6
- fenix_mcp/application/tools/intelligence.py +47 -15
- fenix_mcp/application/tools/knowledge.py +162 -50
- fenix_mcp/application/tools/productivity.py +26 -9
- fenix_mcp/application/tools/user_config.py +18 -9
- fenix_mcp/domain/initialization.py +21 -9
- fenix_mcp/domain/intelligence.py +27 -15
- fenix_mcp/domain/knowledge.py +161 -61
- fenix_mcp/domain/productivity.py +3 -2
- fenix_mcp/domain/user_config.py +16 -7
- fenix_mcp/infrastructure/config.py +6 -2
- fenix_mcp/infrastructure/context.py +0 -1
- fenix_mcp/infrastructure/fenix_api/client.py +118 -38
- fenix_mcp/infrastructure/http_client.py +1 -1
- fenix_mcp/interface/mcp_server.py +0 -3
- fenix_mcp/interface/transports.py +19 -10
- fenix_mcp/main.py +4 -1
- {fenix_mcp-0.1.0.dist-info → fenix_mcp-0.2.1.dist-info}/METADATA +56 -6
- fenix_mcp-0.2.1.dist-info/RECORD +29 -0
- fenix_mcp-0.1.0.dist-info/RECORD +0 -29
- {fenix_mcp-0.1.0.dist-info → fenix_mcp-0.2.1.dist-info}/WHEEL +0 -0
- {fenix_mcp-0.1.0.dist-info → fenix_mcp-0.2.1.dist-info}/entry_points.txt +0 -0
- {fenix_mcp-0.1.0.dist-info → fenix_mcp-0.2.1.dist-info}/top_level.txt +0 -0
fenix_mcp/domain/productivity.py
CHANGED
|
@@ -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(
|
|
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)
|
fenix_mcp/domain/user_config.py
CHANGED
|
@@ -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(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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 =
|
|
138
|
-
|
|
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(
|
|
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 =
|
|
143
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
169
|
-
|
|
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(
|
|
199
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
401
|
-
|
|
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(
|
|
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(
|
|
423
|
-
|
|
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(
|
|
426
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
539
|
-
|
|
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(
|
|
557
|
-
|
|
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(
|
|
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(
|
|
605
|
-
|
|
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(
|
|
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")
|
|
@@ -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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
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.
|
|
204
|
-
5.
|
|
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,,
|