allye-mcp 1.0.2__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.
Files changed (69) hide show
  1. allye_mcp/__init__.py +11 -0
  2. allye_mcp/application/formatters/__init__.py +40 -0
  3. allye_mcp/application/formatters/api_catalog.py +169 -0
  4. allye_mcp/application/formatters/board.py +21 -0
  5. allye_mcp/application/formatters/doc.py +301 -0
  6. allye_mcp/application/formatters/skill.py +174 -0
  7. allye_mcp/application/formatters/sprint.py +31 -0
  8. allye_mcp/application/formatters/work.py +73 -0
  9. allye_mcp/application/presenters.py +24 -0
  10. allye_mcp/application/prompt_base.py +76 -0
  11. allye_mcp/application/prompt_registry.py +39 -0
  12. allye_mcp/application/prompts/__init__.py +57 -0
  13. allye_mcp/application/prompts/api.py +64 -0
  14. allye_mcp/application/prompts/bug.py +44 -0
  15. allye_mcp/application/prompts/docs.py +39 -0
  16. allye_mcp/application/prompts/docs_create.py +42 -0
  17. allye_mcp/application/prompts/docs_search.py +37 -0
  18. allye_mcp/application/prompts/feature.py +44 -0
  19. allye_mcp/application/prompts/memory.py +67 -0
  20. allye_mcp/application/prompts/mine.py +37 -0
  21. allye_mcp/application/prompts/task.py +42 -0
  22. allye_mcp/application/prompts/todo.py +42 -0
  23. allye_mcp/application/prompts/todo_add.py +38 -0
  24. allye_mcp/application/tool_base.py +242 -0
  25. allye_mcp/application/tool_registry.py +39 -0
  26. allye_mcp/application/tools/__init__.py +41 -0
  27. allye_mcp/application/tools/api_catalog.py +148 -0
  28. allye_mcp/application/tools/boards.py +124 -0
  29. allye_mcp/application/tools/docs.py +395 -0
  30. allye_mcp/application/tools/health.py +28 -0
  31. allye_mcp/application/tools/initialize.py +195 -0
  32. allye_mcp/application/tools/intelligence.py +441 -0
  33. allye_mcp/application/tools/productivity.py +336 -0
  34. allye_mcp/application/tools/skills.py +363 -0
  35. allye_mcp/application/tools/sprints.py +123 -0
  36. allye_mcp/application/tools/team.py +173 -0
  37. allye_mcp/application/tools/user_config.py +183 -0
  38. allye_mcp/application/tools/work_items.py +590 -0
  39. allye_mcp/domain/_service_base.py +81 -0
  40. allye_mcp/domain/api_catalog.py +44 -0
  41. allye_mcp/domain/boards.py +182 -0
  42. allye_mcp/domain/docs.py +153 -0
  43. allye_mcp/domain/initialization.py +80 -0
  44. allye_mcp/domain/intelligence.py +119 -0
  45. allye_mcp/domain/productivity.py +212 -0
  46. allye_mcp/domain/skills.py +104 -0
  47. allye_mcp/domain/sprints.py +155 -0
  48. allye_mcp/domain/team.py +65 -0
  49. allye_mcp/domain/user_config.py +80 -0
  50. allye_mcp/domain/work_items.py +265 -0
  51. allye_mcp/infrastructure/allye_api/client.py +1341 -0
  52. allye_mcp/infrastructure/auth/__init__.py +26 -0
  53. allye_mcp/infrastructure/auth/jwks_client.py +78 -0
  54. allye_mcp/infrastructure/auth/jwt_validator.py +150 -0
  55. allye_mcp/infrastructure/auth/oauth_client_credentials.py +98 -0
  56. allye_mcp/infrastructure/auth/strategies.py +126 -0
  57. allye_mcp/infrastructure/config.py +90 -0
  58. allye_mcp/infrastructure/context.py +85 -0
  59. allye_mcp/infrastructure/http_client.py +101 -0
  60. allye_mcp/infrastructure/logging.py +93 -0
  61. allye_mcp/infrastructure/request_context.py +113 -0
  62. allye_mcp/interface/mcp_server.py +340 -0
  63. allye_mcp/interface/transports.py +394 -0
  64. allye_mcp/main.py +123 -0
  65. allye_mcp-1.0.2.dist-info/METADATA +345 -0
  66. allye_mcp-1.0.2.dist-info/RECORD +69 -0
  67. allye_mcp-1.0.2.dist-info/WHEEL +5 -0
  68. allye_mcp-1.0.2.dist-info/entry_points.txt +2 -0
  69. allye_mcp-1.0.2.dist-info/top_level.txt +1 -0
allye_mcp/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ # SPDX-FileCopyrightText: 2024 Bruno Fernandes
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Fênix Cloud MCP Server (Python edition).
6
+ """
7
+
8
+ __all__ = ["__version__"]
9
+
10
+
11
+ __version__ = "1.0.2"
@@ -0,0 +1,40 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Formatter helpers extracted from legacy monolithic tool."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from .api_catalog import _format_api_detail, _format_api_item, _format_api_search_result
7
+ from .board import _format_board
8
+ from .doc import (
9
+ _extract_name_count,
10
+ _format_doc,
11
+ _format_doc_analytics,
12
+ _format_doc_tree_flat,
13
+ _format_doc_tree_nested,
14
+ )
15
+ from .skill import (
16
+ _export_skill_format,
17
+ _export_skills_merged_format,
18
+ _format_marketplace_skill,
19
+ _format_skill,
20
+ )
21
+ from .sprint import _format_sprint
22
+ from .work import _format_work
23
+
24
+ __all__ = [
25
+ "_format_work",
26
+ "_format_board",
27
+ "_format_sprint",
28
+ "_format_doc",
29
+ "_format_doc_tree_flat",
30
+ "_format_doc_tree_nested",
31
+ "_extract_name_count",
32
+ "_format_doc_analytics",
33
+ "_format_skill",
34
+ "_format_marketplace_skill",
35
+ "_export_skill_format",
36
+ "_export_skills_merged_format",
37
+ "_format_api_search_result",
38
+ "_format_api_item",
39
+ "_format_api_detail",
40
+ ]
@@ -0,0 +1,169 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List
6
+
7
+
8
+ def _format_api_search_result(api: Dict[str, Any]) -> str:
9
+ """Format an API search result with similarity and matched endpoint."""
10
+ lines: List[str] = []
11
+
12
+ title = api.get("title", "Untitled API")
13
+ slug = api.get("slug", "")
14
+ similarity = api.get("similarity")
15
+
16
+ # Title with similarity
17
+ if similarity is not None:
18
+ pct = float(similarity) * 100
19
+ lines.append(f"🔗 **{title}** ({pct:.0f}% match)")
20
+ else:
21
+ lines.append(f"🔗 **{title}**")
22
+
23
+ lines.append(f"Slug: {slug}")
24
+
25
+ # Status
26
+ status = api.get("lifecycle_status", "unknown")
27
+ lines.append(f"Status: {status}")
28
+
29
+ # Description (truncated)
30
+ desc = api.get("description")
31
+ if desc:
32
+ truncated = desc[:150] + "..." if len(desc) > 150 else desc
33
+ lines.append(f"Description: {truncated}")
34
+
35
+ # Team info
36
+ team = api.get("team")
37
+ if isinstance(team, dict):
38
+ lines.append(f"Team: {team.get('name', 'Unknown')}")
39
+
40
+ # Matched endpoint (for semantic search)
41
+ matched = api.get("matchedEndpoint")
42
+ if matched:
43
+ method = matched.get("method", "").upper()
44
+ path = matched.get("path", "")
45
+ summary = matched.get("summary", "")
46
+ lines.append(f"Best match: **{method} {path}**")
47
+ if summary:
48
+ lines.append(f" └─ {summary}")
49
+
50
+ # Current version
51
+ version = api.get("current_version")
52
+ if isinstance(version, dict):
53
+ lines.append(f"Version: {version.get('version', 'N/A')}")
54
+
55
+ return "\n".join(lines)
56
+
57
+
58
+ def _format_api_item(api: Dict[str, Any]) -> str:
59
+ """Format an API item for listing."""
60
+ lines: List[str] = []
61
+
62
+ title = api.get("title", "Untitled API")
63
+ slug = api.get("slug", "")
64
+ status = api.get("lifecycle_status", "unknown")
65
+
66
+ lines.append(f"📦 **{title}**")
67
+ lines.append(f"ID: {api.get('id', 'N/A')}")
68
+ lines.append(f"Slug: {slug}")
69
+ lines.append(f"Status: {status}")
70
+
71
+ # Tags
72
+ tags = api.get("tags", [])
73
+ if tags:
74
+ lines.append(f"Tags: {', '.join(tags)}")
75
+
76
+ # Team
77
+ team = api.get("team")
78
+ if isinstance(team, dict):
79
+ lines.append(f"Team: {team.get('name', 'Unknown')}")
80
+
81
+ # Version
82
+ version = api.get("current_version")
83
+ if isinstance(version, dict):
84
+ lines.append(f"Version: {version.get('version', 'N/A')}")
85
+
86
+ return "\n".join(lines)
87
+
88
+
89
+ def _format_api_detail(api: Dict[str, Any]) -> str:
90
+ """Format detailed API specification view."""
91
+ lines: List[str] = []
92
+
93
+ title = api.get("title", "Untitled API")
94
+ slug = api.get("slug", "")
95
+
96
+ lines.append(f"📚 **{title}**")
97
+ lines.append("")
98
+ lines.append(f"ID: {api.get('id', 'N/A')}")
99
+ lines.append(f"Slug: {slug}")
100
+ lines.append(f"Status: {api.get('lifecycle_status', 'unknown')}")
101
+
102
+ # Description
103
+ desc = api.get("description")
104
+ if desc:
105
+ lines.append("")
106
+ lines.append("**Description:**")
107
+ lines.append(desc)
108
+
109
+ # Tags
110
+ tags = api.get("tags", [])
111
+ if tags:
112
+ lines.append("")
113
+ lines.append(f"Tags: {', '.join(tags)}")
114
+
115
+ # Team
116
+ team = api.get("team")
117
+ if isinstance(team, dict):
118
+ lines.append(f"Team: {team.get('name', 'Unknown')}")
119
+
120
+ # Current version
121
+ version = api.get("current_version")
122
+ if isinstance(version, dict):
123
+ lines.append("")
124
+ lines.append("**Current Version:**")
125
+ lines.append(f"- Version: {version.get('version', 'N/A')}")
126
+ lines.append(f"- OpenAPI: {version.get('openapi_version', 'N/A')}")
127
+
128
+ # Endpoints from current version
129
+ endpoints = version.get("endpoint_embeddings", [])
130
+ if endpoints:
131
+ lines.append(f"- Endpoints: {len(endpoints)}")
132
+ lines.append("")
133
+ lines.append("**Endpoints:**")
134
+ for ep in endpoints[:10]: # Limit to 10
135
+ method = ep.get("method", "").upper()
136
+ path = ep.get("path", "")
137
+ summary = ep.get("summary", "")
138
+ if summary:
139
+ lines.append(f"- **{method}** {path} - {summary}")
140
+ else:
141
+ lines.append(f"- **{method}** {path}")
142
+ if len(endpoints) > 10:
143
+ lines.append(f" ... and {len(endpoints) - 10} more endpoints")
144
+
145
+ # Environments
146
+ environments = api.get("environments", [])
147
+ if environments:
148
+ lines.append("")
149
+ lines.append("**Environments:**")
150
+ for env in environments:
151
+ name = env.get("name", "Unnamed")
152
+ url = env.get("base_url", "")
153
+ lines.append(f"- {name}: {url}")
154
+
155
+ # Permissions
156
+ permissions = api.get("permissions", {})
157
+ if permissions:
158
+ lines.append("")
159
+ lines.append("**Your Permissions:**")
160
+ if permissions.get("canEdit"):
161
+ lines.append("- ✅ Edit")
162
+ if permissions.get("canDelete"):
163
+ lines.append("- ✅ Delete")
164
+ if permissions.get("canPublish"):
165
+ lines.append("- ✅ Publish")
166
+ if permissions.get("canManageVersions"):
167
+ lines.append("- ✅ Manage Versions")
168
+
169
+ return "\n".join(lines)
@@ -0,0 +1,21 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ def _format_board(board: Dict[str, Any], header: Optional[str] = None) -> str:
9
+ lines: List[str] = []
10
+ if header:
11
+ lines.append(header)
12
+ lines.append("")
13
+ lines.extend(
14
+ [
15
+ f"🗂️ **{board.get('name', 'Unnamed')}**",
16
+ f"ID: {board.get('id', 'N/A')}",
17
+ f"Team: {board.get('team_id', 'N/A')}",
18
+ f"Columns: {len(board.get('columns', []))}",
19
+ ]
20
+ )
21
+ return "\n".join(lines)
@@ -0,0 +1,301 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from allye_mcp.domain._service_base import _format_date
8
+
9
+
10
+ def _format_doc_tree_flat(data: Any) -> str:
11
+ """Format a flat list of docs (with parent_id) into a visual tree."""
12
+ items: List[Dict[str, Any]] = []
13
+ if isinstance(data, list):
14
+ items = data
15
+ elif isinstance(data, dict):
16
+ nested = data.get("data") or data.get("children") or data.get("items")
17
+ if isinstance(nested, list):
18
+ items = nested
19
+ else:
20
+ # Single root dict — treat as nested tree
21
+ return _format_doc_tree_nested(data)
22
+
23
+ if not items:
24
+ return "(empty)"
25
+
26
+ # Build parent→children map
27
+ by_id: Dict[str, Dict[str, Any]] = {}
28
+ children_map: Dict[Optional[str], List[str]] = {}
29
+ for item in items:
30
+ item_id = item.get("id", "")
31
+ by_id[item_id] = item
32
+ parent = item.get("parent_id")
33
+ children_map.setdefault(parent, []).append(item_id)
34
+
35
+ # Find roots (parent_id is None or not present in by_id)
36
+ root_ids = [
37
+ item_id
38
+ for item_id, item in by_id.items()
39
+ if item.get("parent_id") is None or item.get("parent_id") not in by_id
40
+ ]
41
+
42
+ lines: List[str] = []
43
+
44
+ def _render_node(
45
+ item_id: str, prefix: str, connector: str, child_prefix: str
46
+ ) -> None:
47
+ item = by_id.get(item_id, {})
48
+ emoji = item.get("emoji") or item.get("emote") or ""
49
+ title = item.get("title") or item.get("name") or "Untitled"
50
+ doc_type = item.get("doc_type") or item.get("type") or ""
51
+ iid = item.get("id", "")
52
+
53
+ label = f"{emoji} {title}".strip()
54
+ if doc_type:
55
+ label += f" ({doc_type})"
56
+ label += f" — ID: {iid}"
57
+ lines.append(f"{prefix}{connector}{label}")
58
+
59
+ child_ids = children_map.get(item_id, [])
60
+ for i, cid in enumerate(child_ids):
61
+ is_last_child = i == len(child_ids) - 1
62
+ c = "└── " if is_last_child else "├── "
63
+ cp = child_prefix + (" " if is_last_child else "│ ")
64
+ _render_node(cid, child_prefix, c, cp)
65
+
66
+ for i, rid in enumerate(root_ids):
67
+ _render_node(rid, "", "", "")
68
+ if i < len(root_ids) - 1:
69
+ lines.append("")
70
+
71
+ return "\n".join(lines)
72
+
73
+
74
+ def _format_doc_tree_nested(data: Any) -> str:
75
+ """Format a nested dict with 'children' arrays into a visual tree."""
76
+ if not data:
77
+ return "(empty)"
78
+
79
+ # If data is a list, delegate to flat formatter
80
+ if isinstance(data, list):
81
+ return _format_doc_tree_flat(data)
82
+
83
+ if not isinstance(data, dict):
84
+ return "(empty)"
85
+
86
+ # Unwrap if the tree is inside a 'data', 'children', or 'items' key
87
+ # and the root dict itself has no 'title'/'id' (i.e., it's just a wrapper)
88
+ if not data.get("id") and not data.get("title"):
89
+ nested = data.get("data") or data.get("children") or data.get("items")
90
+ if isinstance(nested, list):
91
+ return _format_doc_tree_flat(nested)
92
+ if isinstance(nested, dict):
93
+ data = nested
94
+
95
+ if not data.get("id") and not data.get("title"):
96
+ return "(empty)"
97
+
98
+ lines: List[str] = []
99
+
100
+ def _render(
101
+ node: Dict[str, Any], prefix: str, connector: str, child_prefix: str
102
+ ) -> None:
103
+ emoji = node.get("emoji") or node.get("emote") or ""
104
+ title = node.get("title") or node.get("name") or "Untitled"
105
+ doc_type = node.get("doc_type") or node.get("type") or ""
106
+ iid = node.get("id", "")
107
+
108
+ label = f"{emoji} {title}".strip()
109
+ if doc_type:
110
+ label += f" ({doc_type})"
111
+ label += f" — ID: {iid}"
112
+ lines.append(f"{prefix}{connector}{label}")
113
+
114
+ children = node.get("children", [])
115
+ if isinstance(children, list):
116
+ valid = [c for c in children if isinstance(c, dict)]
117
+ for i, child in enumerate(valid):
118
+ is_last_child = i == len(valid) - 1
119
+ c = "└── " if is_last_child else "├── "
120
+ cp = child_prefix + (" " if is_last_child else "│ ")
121
+ _render(child, child_prefix, c, cp)
122
+
123
+ _render(data, "", "", "")
124
+ return "\n".join(lines)
125
+
126
+
127
+ def _extract_name_count(item: Dict[str, Any]) -> tuple[str, Any]:
128
+ """Extract a (name, count) pair from a Prisma groupBy-style dict.
129
+
130
+ Prisma groupBy returns fields like {doc_type: "page", _count: 5}
131
+ or {author_user_id: "uuid", _count: 3}.
132
+ """
133
+ # Try known count keys (Prisma uses _count)
134
+ count = None
135
+ for key in ("_count", "count", "total", "value"):
136
+ val = item.get(key)
137
+ if val is not None and isinstance(val, (int, float)):
138
+ count = val
139
+ break
140
+ # _count can also be a dict like {author_user_id: 3}
141
+ raw_count = item.get("_count")
142
+ if count is None and isinstance(raw_count, dict):
143
+ for v in raw_count.values():
144
+ if isinstance(v, (int, float)):
145
+ count = v
146
+ break
147
+
148
+ # Try known name keys
149
+ name = None
150
+ for key in (
151
+ "doc_type",
152
+ "type",
153
+ "name",
154
+ "author",
155
+ "author_user_id",
156
+ "label",
157
+ "title",
158
+ ):
159
+ val = item.get(key)
160
+ if val is not None and isinstance(val, str) and val:
161
+ name = val
162
+ break
163
+
164
+ # Fallback: first string value
165
+ if name is None:
166
+ for key, val in item.items():
167
+ if key.startswith("_"):
168
+ continue
169
+ if isinstance(val, str) and val:
170
+ name = val
171
+ break
172
+
173
+ return (name or "?", count if count is not None else "?")
174
+
175
+
176
+ def _format_doc_analytics(analytics: Dict[str, Any]) -> str:
177
+ """Format doc analytics into a readable summary."""
178
+ lines = ["📊 **Documentation Analytics**", ""]
179
+
180
+ # Simple scalar fields
181
+ simple_keys = {
182
+ "total",
183
+ "published",
184
+ "drafts",
185
+ "inReview",
186
+ "in_review",
187
+ "archived",
188
+ "folders",
189
+ "pages",
190
+ "guides",
191
+ }
192
+ for key, value in analytics.items():
193
+ if key in simple_keys and not isinstance(value, (dict, list)):
194
+ lines.append(f"- **{key}**: {value}")
195
+
196
+ # byType
197
+ by_type = analytics.get("byType")
198
+ if by_type is None:
199
+ by_type = analytics.get("by_type")
200
+ if isinstance(by_type, dict):
201
+ parts = [f"{k}: {v}" for k, v in by_type.items()]
202
+ if parts:
203
+ lines.append(f"- **By type**: {', '.join(parts)}")
204
+ elif isinstance(by_type, list):
205
+ parts = []
206
+ for item in by_type:
207
+ if isinstance(item, dict):
208
+ name, count = _extract_name_count(item)
209
+ parts.append(f"{name}: {count}")
210
+ if parts:
211
+ lines.append(f"- **By type**: {', '.join(parts)}")
212
+
213
+ # byAuthor
214
+ by_author = analytics.get("byAuthor")
215
+ if by_author is None:
216
+ by_author = analytics.get("by_author")
217
+ if isinstance(by_author, list):
218
+ author_parts = []
219
+ for item in by_author:
220
+ if isinstance(item, dict):
221
+ # Prisma groupBy returns {author_user_id, _count}
222
+ # Try nested author object first, then fall back to ID
223
+ author_obj = item.get("author")
224
+ if isinstance(author_obj, dict):
225
+ name = author_obj.get("name") or author_obj.get("email") or "?"
226
+ else:
227
+ uid = item.get("author_user_id") or ""
228
+ name = uid[:8] + "…" if len(uid) > 8 else uid or "?"
229
+ _, count = _extract_name_count(item)
230
+ author_parts.append(f"{name} ({count})")
231
+ if author_parts:
232
+ lines.append("")
233
+ lines.append("**By author:**")
234
+ for part in author_parts[:10]:
235
+ lines.append(f"- {part}")
236
+ if len(author_parts) > 10:
237
+ lines.append(f" ... and {len(author_parts) - 10} more")
238
+
239
+ # recentlyUpdated
240
+ recent = analytics.get("recentlyUpdated")
241
+ if recent is None:
242
+ recent = analytics.get("recently_updated")
243
+ if isinstance(recent, list) and recent:
244
+ lines.append("")
245
+ lines.append("**Recently updated:**")
246
+ for item in recent[:10]:
247
+ if isinstance(item, dict):
248
+ emoji = item.get("emoji") or item.get("emote") or ""
249
+ title = item.get("title") or item.get("name") or "Untitled"
250
+ updated = _format_date(item.get("updated_at") or item.get("updatedAt"))
251
+ label = f"{emoji} {title}".strip()
252
+ lines.append(f"- {label} ({updated})")
253
+ if len(recent) > 10:
254
+ lines.append(f" ... and {len(recent) - 10} more")
255
+
256
+ # Catch any remaining keys not yet handled
257
+ handled = simple_keys | {
258
+ "byType",
259
+ "by_type",
260
+ "byAuthor",
261
+ "by_author",
262
+ "recentlyUpdated",
263
+ "recently_updated",
264
+ }
265
+ for key, value in analytics.items():
266
+ if key not in handled and not isinstance(value, (dict, list)):
267
+ lines.append(f"- **{key}**: {value}")
268
+
269
+ return "\n".join(lines)
270
+
271
+
272
+ def _format_doc(
273
+ doc: Dict[str, Any],
274
+ *,
275
+ header: Optional[str] = None,
276
+ show_content: bool = False,
277
+ ) -> str:
278
+ lines: List[str] = []
279
+ if header:
280
+ lines.append(header)
281
+ lines.append("")
282
+ lines.extend(
283
+ [
284
+ f"📄 **{doc.get('title') or doc.get('name', 'Untitled')}**",
285
+ f"ID: {doc.get('id', 'N/A')}",
286
+ f"Status: {doc.get('status', 'N/A')}",
287
+ f"Team: {doc.get('team_id', 'N/A')}",
288
+ ]
289
+ )
290
+ if doc.get("updated_at") or doc.get("updatedAt"):
291
+ lines.append(
292
+ f"Updated at: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
293
+ )
294
+
295
+ # Show content only when explicitly requested (e.g., doc_get)
296
+ if show_content and doc.get("content"):
297
+ lines.append("")
298
+ lines.append("**Content:**")
299
+ lines.append(str(doc.get("content") or ""))
300
+
301
+ return "\n".join(lines)