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.
@@ -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 find_similar_memories(self, payload: Mapping[str, Any]) -> Any:
273
- return self._request(
274
- "POST", "/api/memory-intelligence/similarity", json=payload
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 consolidate_memories(self, payload: Mapping[str, Any]) -> Any:
278
- return self._request(
279
- "POST", "/api/memory-intelligence/consolidate", json=payload
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 |