fenix-mcp 1.13.0__py3-none-any.whl → 2.0.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.
- fenix_mcp/__init__.py +1 -1
- fenix_mcp/application/tools/initialize.py +18 -56
- fenix_mcp/application/tools/intelligence.py +130 -304
- fenix_mcp/application/tools/knowledge.py +567 -257
- fenix_mcp/domain/initialization.py +11 -112
- fenix_mcp/domain/intelligence.py +57 -247
- fenix_mcp/domain/knowledge.py +56 -117
- fenix_mcp/infrastructure/fenix_api/client.py +158 -122
- fenix_mcp/interface/mcp_server.py +12 -0
- fenix_mcp-2.0.0.dist-info/METADATA +341 -0
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/RECORD +14 -14
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/WHEEL +1 -1
- fenix_mcp-1.13.0.dist-info/METADATA +0 -258
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -228,9 +228,6 @@ class FenixApiClient:
|
|
|
228
228
|
# Memories
|
|
229
229
|
# ------------------------------------------------------------------
|
|
230
230
|
|
|
231
|
-
def create_memory(self, payload: Mapping[str, Any]) -> Any:
|
|
232
|
-
return self._request("POST", "/api/memories", json=payload)
|
|
233
|
-
|
|
234
231
|
def list_memories(
|
|
235
232
|
self,
|
|
236
233
|
*,
|
|
@@ -259,129 +256,16 @@ class FenixApiClient:
|
|
|
259
256
|
def update_memory(self, memory_id: str, payload: Mapping[str, Any]) -> Any:
|
|
260
257
|
return self._request("PATCH", f"/api/memories/{memory_id}", json=payload)
|
|
261
258
|
|
|
262
|
-
def delete_memory(self, memory_id: str) -> Any:
|
|
263
|
-
return self._request("DELETE", f"/api/memories/{memory_id}")
|
|
264
|
-
|
|
265
|
-
def list_memories_by_tags(self, *, tags: str) -> Any:
|
|
266
|
-
params = self._build_params(required={"tags": tags})
|
|
267
|
-
return self._request("GET", "/api/memories/tags", params=params)
|
|
268
|
-
|
|
269
259
|
def record_memory_access(self, memory_id: str) -> Any:
|
|
270
260
|
return self._request("POST", f"/api/memories/{memory_id}/access")
|
|
271
261
|
|
|
272
|
-
def
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
262
|
+
def save_memory(self, payload: Mapping[str, Any]) -> Any:
|
|
263
|
+
"""Smart save memory - creates or updates based on semantic similarity."""
|
|
264
|
+
return self._request("POST", "/api/memories/save", json=payload)
|
|
276
265
|
|
|
277
|
-
def
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
def smart_create_memory(self, payload: Mapping[str, Any]) -> Any:
|
|
283
|
-
return self._request(
|
|
284
|
-
"POST", "/api/memory-intelligence/smart-create", json=payload
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
# ------------------------------------------------------------------
|
|
288
|
-
# Configuration: modes and rules
|
|
289
|
-
# ------------------------------------------------------------------
|
|
290
|
-
|
|
291
|
-
def list_modes(
|
|
292
|
-
self,
|
|
293
|
-
*,
|
|
294
|
-
include_rules: Optional[bool] = None,
|
|
295
|
-
return_description: Optional[bool] = None,
|
|
296
|
-
return_metadata: Optional[bool] = None,
|
|
297
|
-
) -> Any:
|
|
298
|
-
params = self._build_params(
|
|
299
|
-
optional={
|
|
300
|
-
"includeRules": include_rules,
|
|
301
|
-
"returnDescription": return_description,
|
|
302
|
-
"returnMetadata": return_metadata,
|
|
303
|
-
}
|
|
304
|
-
)
|
|
305
|
-
return self._request("GET", "/api/modes", params=params)
|
|
306
|
-
|
|
307
|
-
def get_mode(
|
|
308
|
-
self,
|
|
309
|
-
mode_id: str,
|
|
310
|
-
*,
|
|
311
|
-
return_description: Optional[bool] = None,
|
|
312
|
-
return_metadata: Optional[bool] = None,
|
|
313
|
-
) -> Any:
|
|
314
|
-
params = self._build_params(
|
|
315
|
-
optional={
|
|
316
|
-
"returnDescription": return_description,
|
|
317
|
-
"returnMetadata": return_metadata,
|
|
318
|
-
}
|
|
319
|
-
)
|
|
320
|
-
return self._request("GET", f"/api/modes/{mode_id}", params=params)
|
|
321
|
-
|
|
322
|
-
def create_mode(self, payload: Mapping[str, Any]) -> Any:
|
|
323
|
-
return self._request("POST", "/api/modes", json=payload)
|
|
324
|
-
|
|
325
|
-
def update_mode(self, mode_id: str, payload: Mapping[str, Any]) -> Any:
|
|
326
|
-
return self._request("PATCH", f"/api/modes/{mode_id}", json=payload)
|
|
327
|
-
|
|
328
|
-
def delete_mode(self, mode_id: str) -> Any:
|
|
329
|
-
return self._request("DELETE", f"/api/modes/{mode_id}")
|
|
330
|
-
|
|
331
|
-
def list_rules(
|
|
332
|
-
self,
|
|
333
|
-
*,
|
|
334
|
-
return_description: Optional[bool] = None,
|
|
335
|
-
return_metadata: Optional[bool] = None,
|
|
336
|
-
return_modes: Optional[bool] = None,
|
|
337
|
-
) -> Any:
|
|
338
|
-
params = self._build_params(
|
|
339
|
-
optional={
|
|
340
|
-
"returnDescription": return_description,
|
|
341
|
-
"returnMetadata": return_metadata,
|
|
342
|
-
"returnModes": return_modes,
|
|
343
|
-
}
|
|
344
|
-
)
|
|
345
|
-
return self._request("GET", "/api/rules", params=params)
|
|
346
|
-
|
|
347
|
-
def get_rule(
|
|
348
|
-
self,
|
|
349
|
-
rule_id: str,
|
|
350
|
-
*,
|
|
351
|
-
return_description: Optional[bool] = None,
|
|
352
|
-
return_metadata: Optional[bool] = None,
|
|
353
|
-
return_modes: Optional[bool] = None,
|
|
354
|
-
) -> Any:
|
|
355
|
-
params = self._build_params(
|
|
356
|
-
optional={
|
|
357
|
-
"returnDescription": return_description,
|
|
358
|
-
"returnMetadata": return_metadata,
|
|
359
|
-
"returnModes": return_modes,
|
|
360
|
-
}
|
|
361
|
-
)
|
|
362
|
-
return self._request("GET", f"/api/rules/{rule_id}", params=params)
|
|
363
|
-
|
|
364
|
-
def create_rule(self, payload: Mapping[str, Any]) -> Any:
|
|
365
|
-
return self._request("POST", "/api/rules", json=payload)
|
|
366
|
-
|
|
367
|
-
def update_rule(self, rule_id: str, payload: Mapping[str, Any]) -> Any:
|
|
368
|
-
return self._request("PATCH", f"/api/rules/{rule_id}", json=payload)
|
|
369
|
-
|
|
370
|
-
def delete_rule(self, rule_id: str) -> Any:
|
|
371
|
-
return self._request("DELETE", f"/api/rules/{rule_id}")
|
|
372
|
-
|
|
373
|
-
def add_mode_rule(self, mode_id: str, rule_id: str) -> Any:
|
|
374
|
-
payload = {"modeId": mode_id, "ruleId": rule_id}
|
|
375
|
-
return self._request("POST", "/api/mode-rules", json=payload)
|
|
376
|
-
|
|
377
|
-
def remove_mode_rule(self, mode_id: str, rule_id: str) -> Any:
|
|
378
|
-
return self._request("DELETE", f"/api/mode-rules/mode/{mode_id}/rule/{rule_id}")
|
|
379
|
-
|
|
380
|
-
def list_rules_by_mode(self, mode_id: str) -> Any:
|
|
381
|
-
return self._request("GET", f"/api/mode-rules/mode/{mode_id}/rules")
|
|
382
|
-
|
|
383
|
-
def list_modes_by_rule(self, rule_id: str) -> Any:
|
|
384
|
-
return self._request("GET", f"/api/mode-rules/rule/{rule_id}/modes")
|
|
266
|
+
def search_memories(self, payload: Mapping[str, Any]) -> Any:
|
|
267
|
+
"""Search memories using semantic similarity (embeddings)."""
|
|
268
|
+
return self._request("POST", "/api/memories/search", json=payload)
|
|
385
269
|
|
|
386
270
|
# ------------------------------------------------------------------
|
|
387
271
|
# Knowledge: documentation
|
|
@@ -691,3 +575,155 @@ class FenixApiClient:
|
|
|
691
575
|
|
|
692
576
|
def cancel_sprint(self, sprint_id: str) -> Any:
|
|
693
577
|
return self._request("PATCH", f"/api/sprints/{sprint_id}/cancel")
|
|
578
|
+
|
|
579
|
+
# ------------------------------------------------------------------
|
|
580
|
+
# Rules
|
|
581
|
+
# ------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
def create_rule(self, payload: Mapping[str, Any]) -> Any:
|
|
584
|
+
return self._request("POST", "/api/rules", json=payload)
|
|
585
|
+
|
|
586
|
+
def list_rules(self, **filters: Any) -> Any:
|
|
587
|
+
return self._request("GET", "/api/rules", params=_strip_none(filters))
|
|
588
|
+
|
|
589
|
+
def get_rule(self, rule_id: str) -> Any:
|
|
590
|
+
return self._request("GET", f"/api/rules/{rule_id}")
|
|
591
|
+
|
|
592
|
+
def update_rule(self, rule_id: str, payload: Mapping[str, Any]) -> Any:
|
|
593
|
+
return self._request("PATCH", f"/api/rules/{rule_id}", json=payload)
|
|
594
|
+
|
|
595
|
+
def delete_rule(self, rule_id: str) -> Any:
|
|
596
|
+
return self._request("DELETE", f"/api/rules/{rule_id}")
|
|
597
|
+
|
|
598
|
+
def list_marketplace_rules(self, **filters: Any) -> Any:
|
|
599
|
+
return self._request(
|
|
600
|
+
"GET", "/api/rules/marketplace/list", params=_strip_none(filters)
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
def search_marketplace_rules(self, *, query: str, limit: int = 20) -> Any:
|
|
604
|
+
params = self._build_params(required={"q": query}, optional={"limit": limit})
|
|
605
|
+
return self._request("GET", "/api/rules/marketplace/search", params=params)
|
|
606
|
+
|
|
607
|
+
def get_top_marketplace_rules(self, *, limit: int = 10) -> Any:
|
|
608
|
+
params = self._build_params(optional={"limit": limit})
|
|
609
|
+
return self._request("GET", "/api/rules/marketplace/top", params=params)
|
|
610
|
+
|
|
611
|
+
def fork_rule(self, rule_id: str, payload: Mapping[str, Any]) -> Any:
|
|
612
|
+
return self._request("POST", f"/api/rules/{rule_id}/fork", json=payload)
|
|
613
|
+
|
|
614
|
+
def download_rule(self, rule_id: str) -> Any:
|
|
615
|
+
return self._request("POST", f"/api/rules/{rule_id}/download")
|
|
616
|
+
|
|
617
|
+
def rate_rule(self, rule_id: str, rating: float) -> Any:
|
|
618
|
+
return self._request(
|
|
619
|
+
"POST", f"/api/rules/{rule_id}/rate", json={"rating": rating}
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
def export_rule(self, rule_id: str, format: str) -> Any:
|
|
623
|
+
return self._request("GET", f"/api/rules/{rule_id}/export/{format}")
|
|
624
|
+
|
|
625
|
+
def export_merged_rules(self, format: str) -> Any:
|
|
626
|
+
params = self._build_params(required={"format": format})
|
|
627
|
+
return self._request("GET", "/api/rules/export/merged", params=params)
|
|
628
|
+
|
|
629
|
+
# ------------------------------------------------------------------
|
|
630
|
+
# API Catalog
|
|
631
|
+
# ------------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
def list_api_catalog(self, **filters: Any) -> Any:
|
|
634
|
+
"""List API specifications with optional filters."""
|
|
635
|
+
return self._request("GET", "/api/api-catalog", params=_strip_none(filters))
|
|
636
|
+
|
|
637
|
+
def get_api_catalog(self, spec_id: str) -> Any:
|
|
638
|
+
"""Get API specification details by ID."""
|
|
639
|
+
return self._request("GET", f"/api/api-catalog/{spec_id}")
|
|
640
|
+
|
|
641
|
+
def search_api_catalog_text(
|
|
642
|
+
self,
|
|
643
|
+
*,
|
|
644
|
+
query: str,
|
|
645
|
+
limit: int = 20,
|
|
646
|
+
offset: int = 0,
|
|
647
|
+
status: Optional[str] = None,
|
|
648
|
+
tags: Optional[List[str]] = None,
|
|
649
|
+
) -> Any:
|
|
650
|
+
"""Full-text search in API specifications."""
|
|
651
|
+
params = self._build_params(
|
|
652
|
+
required={"q": query},
|
|
653
|
+
optional={
|
|
654
|
+
"limit": limit,
|
|
655
|
+
"offset": offset,
|
|
656
|
+
"status": status,
|
|
657
|
+
"tags": ",".join(tags) if tags else None,
|
|
658
|
+
},
|
|
659
|
+
)
|
|
660
|
+
return self._request("GET", "/api/api-catalog/search/apis", params=params)
|
|
661
|
+
|
|
662
|
+
def search_api_catalog_endpoints_text(
|
|
663
|
+
self,
|
|
664
|
+
*,
|
|
665
|
+
query: str,
|
|
666
|
+
limit: int = 20,
|
|
667
|
+
offset: int = 0,
|
|
668
|
+
specification_id: Optional[str] = None,
|
|
669
|
+
method: Optional[str] = None,
|
|
670
|
+
) -> Any:
|
|
671
|
+
"""Full-text search in API endpoints."""
|
|
672
|
+
params = self._build_params(
|
|
673
|
+
required={"q": query},
|
|
674
|
+
optional={
|
|
675
|
+
"limit": limit,
|
|
676
|
+
"offset": offset,
|
|
677
|
+
"specificationId": specification_id,
|
|
678
|
+
"method": method,
|
|
679
|
+
},
|
|
680
|
+
)
|
|
681
|
+
return self._request("GET", "/api/api-catalog/search/endpoints", params=params)
|
|
682
|
+
|
|
683
|
+
def search_api_catalog_semantic(
|
|
684
|
+
self,
|
|
685
|
+
*,
|
|
686
|
+
query: str,
|
|
687
|
+
limit: int = 20,
|
|
688
|
+
offset: int = 0,
|
|
689
|
+
threshold: Optional[float] = None,
|
|
690
|
+
status: Optional[str] = None,
|
|
691
|
+
tags: Optional[List[str]] = None,
|
|
692
|
+
) -> Any:
|
|
693
|
+
"""Semantic search in API specifications using embeddings."""
|
|
694
|
+
params = self._build_params(
|
|
695
|
+
required={"q": query},
|
|
696
|
+
optional={
|
|
697
|
+
"limit": limit,
|
|
698
|
+
"offset": offset,
|
|
699
|
+
"threshold": threshold,
|
|
700
|
+
"status": status,
|
|
701
|
+
"tags": ",".join(tags) if tags else None,
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
return self._request("GET", "/api/api-catalog/semantic/apis", params=params)
|
|
705
|
+
|
|
706
|
+
def search_api_catalog_endpoints_semantic(
|
|
707
|
+
self,
|
|
708
|
+
*,
|
|
709
|
+
query: str,
|
|
710
|
+
limit: int = 20,
|
|
711
|
+
offset: int = 0,
|
|
712
|
+
threshold: Optional[float] = None,
|
|
713
|
+
specification_id: Optional[str] = None,
|
|
714
|
+
method: Optional[str] = None,
|
|
715
|
+
) -> Any:
|
|
716
|
+
"""Semantic search in API endpoints using embeddings."""
|
|
717
|
+
params = self._build_params(
|
|
718
|
+
required={"q": query},
|
|
719
|
+
optional={
|
|
720
|
+
"limit": limit,
|
|
721
|
+
"offset": offset,
|
|
722
|
+
"threshold": threshold,
|
|
723
|
+
"specificationId": specification_id,
|
|
724
|
+
"method": method,
|
|
725
|
+
},
|
|
726
|
+
)
|
|
727
|
+
return self._request(
|
|
728
|
+
"GET", "/api/api-catalog/semantic/endpoints", params=params
|
|
729
|
+
)
|
|
@@ -63,6 +63,18 @@ class SimpleMcpServer:
|
|
|
63
63
|
# Notifications do not require a response
|
|
64
64
|
return None
|
|
65
65
|
|
|
66
|
+
if method == "notifications/cancelled":
|
|
67
|
+
# Client cancelled a request - no response needed
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
if method == "logging/setLevel":
|
|
71
|
+
# Acknowledge log level change request (we don't actually change anything)
|
|
72
|
+
return {
|
|
73
|
+
"jsonrpc": "2.0",
|
|
74
|
+
"id": request_id,
|
|
75
|
+
"result": {},
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
raise McpServerError(f"Unsupported method: {method}")
|
|
67
79
|
|
|
68
80
|
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fenix-mcp
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Fênix Cloud MCP server implemented in Python
|
|
5
|
+
Author: Fenix Inc
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: pydantic>=2.5
|
|
9
|
+
Requires-Dist: requests>=2.31
|
|
10
|
+
Requires-Dist: urllib3>=2.0
|
|
11
|
+
Requires-Dist: aiohttp>=3.9
|
|
12
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
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"
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="https://fenix.devshire.app/logos/logo_fenix.png" alt="Fenix MCP" width="200" />
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<strong>Fenix MCP Server</strong><br/>
|
|
28
|
+
Python MCP server for Fenix Cloud API integration
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<a href="https://pypi.org/project/fenix-mcp/"><img src="https://img.shields.io/pypi/v/fenix-mcp.svg" alt="PyPI"></a>
|
|
33
|
+
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python"></a>
|
|
34
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License"></a>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<a href="#quick-start">Quick Start</a> •
|
|
39
|
+
<a href="#installation">Installation</a> •
|
|
40
|
+
<a href="#configuration">Configuration</a> •
|
|
41
|
+
<a href="#project-structure">Structure</a>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Overview
|
|
47
|
+
|
|
48
|
+
Fenix MCP connects MCP-compatible clients (Claude Code, Cursor, Windsurf, VS Code, etc.) directly to the Fenix Cloud APIs. Every tool invocation hits the live backend—no outdated snapshots or hallucinated IDs.
|
|
49
|
+
|
|
50
|
+
**Available Tools:**
|
|
51
|
+
- `knowledge` — Documentation CRUD, work items, modes, rules
|
|
52
|
+
- `productivity` — TODO management
|
|
53
|
+
- `intelligence` — Memories and smart operations
|
|
54
|
+
- `user_config` — User configuration documents
|
|
55
|
+
- `initialize` — Personalized setup
|
|
56
|
+
- `health` — Backend health check
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pipx install fenix-mcp # Install
|
|
64
|
+
fenix-mcp --pat <your-token> # Run (STDIO mode)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
| Requirement | Version |
|
|
72
|
+
|-------------|---------|
|
|
73
|
+
| Python | 3.10+ |
|
|
74
|
+
| Fenix PAT Token | Required |
|
|
75
|
+
| MCP Client | Claude Code, Cursor, Windsurf, VS Code, etc. |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
### With pipx (recommended)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pipx install fenix-mcp
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### With pip
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install --user fenix-mcp
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Upgrade
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pipx upgrade fenix-mcp
|
|
97
|
+
# or
|
|
98
|
+
pip install --upgrade fenix-mcp
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
### MCP Client Setup
|
|
106
|
+
|
|
107
|
+
<details>
|
|
108
|
+
<summary><strong>Claude Code</strong> — ~/.claude/config.toml</summary>
|
|
109
|
+
|
|
110
|
+
```toml
|
|
111
|
+
[mcp_servers.fenix]
|
|
112
|
+
command = "fenix-mcp"
|
|
113
|
+
args = ["--pat", "your-token"]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
</details>
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary><strong>Cursor</strong> — ~/.cursor/mcp.json</summary>
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"fenix": {
|
|
125
|
+
"command": "fenix-mcp",
|
|
126
|
+
"args": ["--pat", "your-token"],
|
|
127
|
+
"disabled": false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
</details>
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary><strong>VS Code / Windsurf</strong> — settings.json</summary>
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"modelContextProtocol.mcpServers": {
|
|
141
|
+
"fenix": {
|
|
142
|
+
"command": "fenix-mcp",
|
|
143
|
+
"args": ["--pat", "your-token"]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
</details>
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary><strong>Environment Variables</strong></summary>
|
|
155
|
+
|
|
156
|
+
| Variable | Description | Default |
|
|
157
|
+
|----------|-------------|---------|
|
|
158
|
+
| `FENIX_API_URL` | Fenix Cloud API base URL | `https://fenix-api.devshire.app` |
|
|
159
|
+
| `FENIX_PAT_TOKEN` | Token (alternative to `--pat`) | empty |
|
|
160
|
+
| `FENIX_TRANSPORT_MODE` | `stdio`, `http`, or `both` | `stdio` |
|
|
161
|
+
| `FENIX_HTTP_HOST` | HTTP transport host | `127.0.0.1` |
|
|
162
|
+
| `FENIX_HTTP_PORT` | HTTP transport port | `5003` |
|
|
163
|
+
| `FENIX_LOG_LEVEL` | Log level (`DEBUG`, `INFO`, etc.) | `INFO` |
|
|
164
|
+
|
|
165
|
+
See [`.env.example`](./.env.example) for reference.
|
|
166
|
+
|
|
167
|
+
</details>
|
|
168
|
+
|
|
169
|
+
<details>
|
|
170
|
+
<summary><strong>HTTP Transport</strong></summary>
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
export FENIX_TRANSPORT_MODE=http
|
|
174
|
+
export FENIX_HTTP_PORT=5003
|
|
175
|
+
fenix-mcp --pat <your-token>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
JSON-RPC endpoint: `http://127.0.0.1:5003/jsonrpc`
|
|
179
|
+
|
|
180
|
+
Run both STDIO and HTTP:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
export FENIX_TRANSPORT_MODE=both
|
|
184
|
+
fenix-mcp --pat <your-token>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
</details>
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Project Structure
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
fenix-mcp-py/
|
|
195
|
+
├── fenix_mcp/ # Main package
|
|
196
|
+
│ ├── main.py # Entry point (CLI)
|
|
197
|
+
│ ├── application/ # Use cases and services
|
|
198
|
+
│ ├── domain/ # Business entities
|
|
199
|
+
│ ├── infrastructure/ # External integrations (API, transport)
|
|
200
|
+
│ └── interface/ # MCP protocol handlers
|
|
201
|
+
├── tests/ # Test suite
|
|
202
|
+
├── docs/ # Documentation
|
|
203
|
+
├── pyproject.toml # Project configuration
|
|
204
|
+
└── .env.example # Environment template
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
<details>
|
|
208
|
+
<summary><strong>fenix_mcp/</strong> — Package Structure</summary>
|
|
209
|
+
|
|
210
|
+
| Directory | Responsibility |
|
|
211
|
+
|-----------|----------------|
|
|
212
|
+
| `main.py` | CLI entry point, argument parsing, server bootstrap |
|
|
213
|
+
| `application/` | Use cases, handlers for each MCP tool |
|
|
214
|
+
| `domain/` | Business entities, DTOs, validation |
|
|
215
|
+
| `infrastructure/` | API client, HTTP transport, logging |
|
|
216
|
+
| `interface/` | MCP protocol implementation, tool registration |
|
|
217
|
+
|
|
218
|
+
### Architecture
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
MCP Client → interface/ → application/ → infrastructure/ → Fenix API
|
|
222
|
+
↓
|
|
223
|
+
domain/
|
|
224
|
+
(entities)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
</details>
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Tech Stack
|
|
232
|
+
|
|
233
|
+
| Layer | Technology |
|
|
234
|
+
|-------|------------|
|
|
235
|
+
| Runtime | Python 3.10+ |
|
|
236
|
+
| Validation | Pydantic 2 |
|
|
237
|
+
| HTTP | aiohttp, requests |
|
|
238
|
+
| Config | pydantic-settings |
|
|
239
|
+
| Testing | pytest, pytest-asyncio |
|
|
240
|
+
| Linting | flake8, black, mypy |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Development
|
|
245
|
+
|
|
246
|
+
### Setup
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Clone and install
|
|
250
|
+
git clone <repo>
|
|
251
|
+
cd fenix-mcp-py
|
|
252
|
+
pip install -e .[dev]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Scripts
|
|
256
|
+
|
|
257
|
+
| Command | Description |
|
|
258
|
+
|---------|-------------|
|
|
259
|
+
| `pytest` | Run tests |
|
|
260
|
+
| `pytest --cov=fenix_mcp` | Run with coverage |
|
|
261
|
+
| `black fenix_mcp/ tests/` | Format code |
|
|
262
|
+
| `flake8 fenix_mcp/ tests/` | Lint |
|
|
263
|
+
| `mypy fenix_mcp/` | Type check |
|
|
264
|
+
|
|
265
|
+
### Commit Convention
|
|
266
|
+
|
|
267
|
+
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
|
268
|
+
|
|
269
|
+
| Prefix | Description | Version Bump |
|
|
270
|
+
|--------|-------------|--------------|
|
|
271
|
+
| `fix:` | Bug fixes | Patch |
|
|
272
|
+
| `feat:` | New features | Minor |
|
|
273
|
+
| `BREAKING CHANGE:` | Breaking changes | Major |
|
|
274
|
+
| `chore:` | Maintenance | None |
|
|
275
|
+
| `docs:` | Documentation | None |
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## CI/CD
|
|
280
|
+
|
|
281
|
+
| Platform | Usage |
|
|
282
|
+
|----------|-------|
|
|
283
|
+
| GitHub Actions | Tests, lint, build on push/PR to main |
|
|
284
|
+
| Semantic Release | Auto-version based on commits |
|
|
285
|
+
| PyPI | Package distribution |
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Troubleshooting
|
|
290
|
+
|
|
291
|
+
<details>
|
|
292
|
+
<summary><code>command not found: fenix-mcp</code></summary>
|
|
293
|
+
|
|
294
|
+
Add scripts directory to PATH:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
# macOS/Linux
|
|
298
|
+
export PATH="$PATH:~/.local/bin"
|
|
299
|
+
|
|
300
|
+
# Windows
|
|
301
|
+
# Add %APPDATA%\Python\Python311\Scripts to PATH
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
</details>
|
|
305
|
+
|
|
306
|
+
<details>
|
|
307
|
+
<summary><code>401 Unauthorized</code></summary>
|
|
308
|
+
|
|
309
|
+
1. Check `--pat` or `FENIX_PAT_TOKEN` is set correctly
|
|
310
|
+
2. Regenerate token in Fenix Cloud if expired
|
|
311
|
+
|
|
312
|
+
</details>
|
|
313
|
+
|
|
314
|
+
<details>
|
|
315
|
+
<summary>Run HTTP and STDIO simultaneously</summary>
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
export FENIX_TRANSPORT_MODE=both
|
|
319
|
+
fenix-mcp --pat <your-token>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
</details>
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Security
|
|
327
|
+
|
|
328
|
+
- Store tokens securely (keychain, `pass`, `.env`)
|
|
329
|
+
- Never commit secrets to git
|
|
330
|
+
- Revoke tokens when no longer needed
|
|
331
|
+
- Use `pipx` to isolate from global Python
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Useful Links
|
|
336
|
+
|
|
337
|
+
| Resource | URL |
|
|
338
|
+
|----------|-----|
|
|
339
|
+
| PyPI | https://pypi.org/project/fenix-mcp/ |
|
|
340
|
+
| Fenix Cloud | https://fenix.devshire.app |
|
|
341
|
+
| MCP Protocol | https://modelcontextprotocol.io |
|