raise-cli 2.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.
Files changed (264) hide show
  1. raise_cli/__init__.py +38 -0
  2. raise_cli/__main__.py +30 -0
  3. raise_cli/adapters/__init__.py +91 -0
  4. raise_cli/adapters/declarative/__init__.py +26 -0
  5. raise_cli/adapters/declarative/adapter.py +267 -0
  6. raise_cli/adapters/declarative/discovery.py +94 -0
  7. raise_cli/adapters/declarative/expressions.py +150 -0
  8. raise_cli/adapters/declarative/reference/__init__.py +1 -0
  9. raise_cli/adapters/declarative/reference/github.yaml +143 -0
  10. raise_cli/adapters/declarative/schema.py +98 -0
  11. raise_cli/adapters/filesystem.py +299 -0
  12. raise_cli/adapters/mcp_bridge.py +10 -0
  13. raise_cli/adapters/mcp_confluence.py +246 -0
  14. raise_cli/adapters/mcp_jira.py +405 -0
  15. raise_cli/adapters/models.py +205 -0
  16. raise_cli/adapters/protocols.py +180 -0
  17. raise_cli/adapters/registry.py +90 -0
  18. raise_cli/adapters/sync.py +149 -0
  19. raise_cli/agents/__init__.py +14 -0
  20. raise_cli/agents/antigravity.yaml +8 -0
  21. raise_cli/agents/claude.yaml +8 -0
  22. raise_cli/agents/copilot.yaml +8 -0
  23. raise_cli/agents/copilot_plugin.py +124 -0
  24. raise_cli/agents/cursor.yaml +7 -0
  25. raise_cli/agents/roo.yaml +8 -0
  26. raise_cli/agents/windsurf.yaml +8 -0
  27. raise_cli/artifacts/__init__.py +30 -0
  28. raise_cli/artifacts/models.py +43 -0
  29. raise_cli/artifacts/reader.py +55 -0
  30. raise_cli/artifacts/renderer.py +104 -0
  31. raise_cli/artifacts/story_design.py +69 -0
  32. raise_cli/artifacts/writer.py +45 -0
  33. raise_cli/backlog/__init__.py +1 -0
  34. raise_cli/backlog/sync.py +115 -0
  35. raise_cli/cli/__init__.py +3 -0
  36. raise_cli/cli/commands/__init__.py +3 -0
  37. raise_cli/cli/commands/_resolve.py +153 -0
  38. raise_cli/cli/commands/adapters.py +362 -0
  39. raise_cli/cli/commands/artifact.py +137 -0
  40. raise_cli/cli/commands/backlog.py +333 -0
  41. raise_cli/cli/commands/base.py +31 -0
  42. raise_cli/cli/commands/discover.py +551 -0
  43. raise_cli/cli/commands/docs.py +130 -0
  44. raise_cli/cli/commands/doctor.py +177 -0
  45. raise_cli/cli/commands/gate.py +223 -0
  46. raise_cli/cli/commands/graph.py +1086 -0
  47. raise_cli/cli/commands/info.py +81 -0
  48. raise_cli/cli/commands/init.py +746 -0
  49. raise_cli/cli/commands/journal.py +167 -0
  50. raise_cli/cli/commands/mcp.py +524 -0
  51. raise_cli/cli/commands/memory.py +467 -0
  52. raise_cli/cli/commands/pattern.py +348 -0
  53. raise_cli/cli/commands/profile.py +59 -0
  54. raise_cli/cli/commands/publish.py +80 -0
  55. raise_cli/cli/commands/release.py +338 -0
  56. raise_cli/cli/commands/session.py +528 -0
  57. raise_cli/cli/commands/signal.py +410 -0
  58. raise_cli/cli/commands/skill.py +350 -0
  59. raise_cli/cli/commands/skill_set.py +145 -0
  60. raise_cli/cli/error_handler.py +158 -0
  61. raise_cli/cli/main.py +163 -0
  62. raise_cli/compat.py +66 -0
  63. raise_cli/config/__init__.py +41 -0
  64. raise_cli/config/agent_plugin.py +105 -0
  65. raise_cli/config/agent_registry.py +233 -0
  66. raise_cli/config/agents.py +120 -0
  67. raise_cli/config/ide.py +32 -0
  68. raise_cli/config/paths.py +379 -0
  69. raise_cli/config/settings.py +180 -0
  70. raise_cli/context/__init__.py +42 -0
  71. raise_cli/context/analyzers/__init__.py +16 -0
  72. raise_cli/context/analyzers/models.py +36 -0
  73. raise_cli/context/analyzers/protocol.py +43 -0
  74. raise_cli/context/analyzers/python.py +292 -0
  75. raise_cli/context/builder.py +1569 -0
  76. raise_cli/context/diff.py +213 -0
  77. raise_cli/context/extractors/__init__.py +13 -0
  78. raise_cli/context/extractors/skills.py +121 -0
  79. raise_cli/core/__init__.py +37 -0
  80. raise_cli/core/files.py +66 -0
  81. raise_cli/core/text.py +174 -0
  82. raise_cli/core/tools.py +441 -0
  83. raise_cli/discovery/__init__.py +50 -0
  84. raise_cli/discovery/analyzer.py +691 -0
  85. raise_cli/discovery/drift.py +355 -0
  86. raise_cli/discovery/scanner.py +1687 -0
  87. raise_cli/doctor/__init__.py +4 -0
  88. raise_cli/doctor/checks/__init__.py +1 -0
  89. raise_cli/doctor/checks/environment.py +110 -0
  90. raise_cli/doctor/checks/project.py +238 -0
  91. raise_cli/doctor/fix.py +80 -0
  92. raise_cli/doctor/models.py +56 -0
  93. raise_cli/doctor/protocol.py +43 -0
  94. raise_cli/doctor/registry.py +100 -0
  95. raise_cli/doctor/report.py +141 -0
  96. raise_cli/doctor/runner.py +95 -0
  97. raise_cli/engines/__init__.py +3 -0
  98. raise_cli/exceptions.py +215 -0
  99. raise_cli/gates/__init__.py +19 -0
  100. raise_cli/gates/builtin/__init__.py +1 -0
  101. raise_cli/gates/builtin/coverage.py +52 -0
  102. raise_cli/gates/builtin/lint.py +48 -0
  103. raise_cli/gates/builtin/tests.py +48 -0
  104. raise_cli/gates/builtin/types.py +48 -0
  105. raise_cli/gates/models.py +40 -0
  106. raise_cli/gates/protocol.py +41 -0
  107. raise_cli/gates/registry.py +141 -0
  108. raise_cli/governance/__init__.py +11 -0
  109. raise_cli/governance/extractor.py +412 -0
  110. raise_cli/governance/models.py +134 -0
  111. raise_cli/governance/parsers/__init__.py +35 -0
  112. raise_cli/governance/parsers/_convert.py +38 -0
  113. raise_cli/governance/parsers/adr.py +274 -0
  114. raise_cli/governance/parsers/backlog.py +356 -0
  115. raise_cli/governance/parsers/constitution.py +119 -0
  116. raise_cli/governance/parsers/epic.py +323 -0
  117. raise_cli/governance/parsers/glossary.py +316 -0
  118. raise_cli/governance/parsers/guardrails.py +345 -0
  119. raise_cli/governance/parsers/prd.py +112 -0
  120. raise_cli/governance/parsers/roadmap.py +118 -0
  121. raise_cli/governance/parsers/vision.py +116 -0
  122. raise_cli/graph/__init__.py +1 -0
  123. raise_cli/graph/backends/__init__.py +57 -0
  124. raise_cli/graph/backends/api.py +137 -0
  125. raise_cli/graph/backends/dual.py +139 -0
  126. raise_cli/graph/backends/pending.py +84 -0
  127. raise_cli/handlers/__init__.py +3 -0
  128. raise_cli/hooks/__init__.py +54 -0
  129. raise_cli/hooks/builtin/__init__.py +1 -0
  130. raise_cli/hooks/builtin/backlog.py +216 -0
  131. raise_cli/hooks/builtin/gate_bridge.py +83 -0
  132. raise_cli/hooks/builtin/jira_sync.py +127 -0
  133. raise_cli/hooks/builtin/memory.py +117 -0
  134. raise_cli/hooks/builtin/telemetry.py +72 -0
  135. raise_cli/hooks/emitter.py +184 -0
  136. raise_cli/hooks/events.py +262 -0
  137. raise_cli/hooks/protocol.py +38 -0
  138. raise_cli/hooks/registry.py +117 -0
  139. raise_cli/mcp/__init__.py +33 -0
  140. raise_cli/mcp/bridge.py +218 -0
  141. raise_cli/mcp/models.py +43 -0
  142. raise_cli/mcp/registry.py +77 -0
  143. raise_cli/mcp/schema.py +41 -0
  144. raise_cli/memory/__init__.py +58 -0
  145. raise_cli/memory/loader.py +247 -0
  146. raise_cli/memory/migration.py +241 -0
  147. raise_cli/memory/models.py +169 -0
  148. raise_cli/memory/writer.py +598 -0
  149. raise_cli/onboarding/__init__.py +103 -0
  150. raise_cli/onboarding/bootstrap.py +324 -0
  151. raise_cli/onboarding/claudemd.py +17 -0
  152. raise_cli/onboarding/conventions.py +742 -0
  153. raise_cli/onboarding/detection.py +374 -0
  154. raise_cli/onboarding/governance.py +443 -0
  155. raise_cli/onboarding/instructions.py +672 -0
  156. raise_cli/onboarding/manifest.py +201 -0
  157. raise_cli/onboarding/memory_md.py +399 -0
  158. raise_cli/onboarding/migration.py +207 -0
  159. raise_cli/onboarding/profile.py +624 -0
  160. raise_cli/onboarding/skill_conflict.py +100 -0
  161. raise_cli/onboarding/skill_manifest.py +176 -0
  162. raise_cli/onboarding/skills.py +437 -0
  163. raise_cli/onboarding/workflows.py +101 -0
  164. raise_cli/output/__init__.py +28 -0
  165. raise_cli/output/console.py +394 -0
  166. raise_cli/output/formatters/__init__.py +9 -0
  167. raise_cli/output/formatters/adapters.py +135 -0
  168. raise_cli/output/formatters/discover.py +439 -0
  169. raise_cli/output/formatters/skill.py +298 -0
  170. raise_cli/publish/__init__.py +3 -0
  171. raise_cli/publish/changelog.py +80 -0
  172. raise_cli/publish/check.py +179 -0
  173. raise_cli/publish/version.py +172 -0
  174. raise_cli/rai_base/__init__.py +22 -0
  175. raise_cli/rai_base/framework/__init__.py +7 -0
  176. raise_cli/rai_base/framework/methodology.yaml +233 -0
  177. raise_cli/rai_base/governance/__init__.py +1 -0
  178. raise_cli/rai_base/governance/architecture/__init__.py +1 -0
  179. raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
  180. raise_cli/rai_base/governance/architecture/system-context.md +34 -0
  181. raise_cli/rai_base/governance/architecture/system-design.md +24 -0
  182. raise_cli/rai_base/governance/backlog.md +8 -0
  183. raise_cli/rai_base/governance/guardrails.md +17 -0
  184. raise_cli/rai_base/governance/prd.md +25 -0
  185. raise_cli/rai_base/governance/vision.md +16 -0
  186. raise_cli/rai_base/identity/__init__.py +8 -0
  187. raise_cli/rai_base/identity/core.md +119 -0
  188. raise_cli/rai_base/identity/perspective.md +119 -0
  189. raise_cli/rai_base/memory/__init__.py +7 -0
  190. raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
  191. raise_cli/schemas/__init__.py +3 -0
  192. raise_cli/schemas/journal.py +49 -0
  193. raise_cli/schemas/session_state.py +117 -0
  194. raise_cli/session/__init__.py +5 -0
  195. raise_cli/session/bundle.py +820 -0
  196. raise_cli/session/close.py +268 -0
  197. raise_cli/session/journal.py +119 -0
  198. raise_cli/session/resolver.py +126 -0
  199. raise_cli/session/state.py +187 -0
  200. raise_cli/skills/__init__.py +44 -0
  201. raise_cli/skills/locator.py +141 -0
  202. raise_cli/skills/name_checker.py +199 -0
  203. raise_cli/skills/parser.py +145 -0
  204. raise_cli/skills/scaffold.py +212 -0
  205. raise_cli/skills/schema.py +132 -0
  206. raise_cli/skills/skillsets.py +195 -0
  207. raise_cli/skills/validator.py +197 -0
  208. raise_cli/skills_base/__init__.py +80 -0
  209. raise_cli/skills_base/contract-template.md +60 -0
  210. raise_cli/skills_base/preamble.md +37 -0
  211. raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
  212. raise_cli/skills_base/rai-debug/SKILL.md +171 -0
  213. raise_cli/skills_base/rai-discover/SKILL.md +167 -0
  214. raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
  215. raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
  216. raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
  217. raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
  218. raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
  219. raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
  220. raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
  221. raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
  222. raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
  223. raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
  224. raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
  225. raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
  226. raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  227. raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
  228. raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
  229. raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
  230. raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
  231. raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
  232. raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
  233. raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
  234. raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
  235. raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
  236. raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
  237. raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
  238. raise_cli/skills_base/rai-research/SKILL.md +143 -0
  239. raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  240. raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
  241. raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
  242. raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
  243. raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
  244. raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  245. raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
  246. raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
  247. raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
  248. raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
  249. raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
  250. raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
  251. raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
  252. raise_cli/telemetry/__init__.py +42 -0
  253. raise_cli/telemetry/schemas.py +285 -0
  254. raise_cli/telemetry/writer.py +217 -0
  255. raise_cli/tier/__init__.py +0 -0
  256. raise_cli/tier/context.py +134 -0
  257. raise_cli/viz/__init__.py +7 -0
  258. raise_cli/viz/generator.py +406 -0
  259. raise_cli-2.2.1.dist-info/METADATA +433 -0
  260. raise_cli-2.2.1.dist-info/RECORD +264 -0
  261. raise_cli-2.2.1.dist-info/WHEEL +4 -0
  262. raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
  263. raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
  264. raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,167 @@
1
+ """CLI commands for session journal — incremental memory persistence.
2
+
3
+ Provides `rai session journal add` and `rai session journal show` for
4
+ persisting and retrieving session context across compaction events.
5
+
6
+ Example:
7
+ $ rai session journal add "Use JSONL for persistence" --type decision
8
+ $ rai session journal show --last 5
9
+ $ rai session journal show --compact
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Annotated
16
+
17
+ import typer
18
+
19
+ from raise_cli.cli.error_handler import cli_error
20
+ from raise_cli.config.paths import get_personal_dir
21
+ from raise_cli.onboarding.profile import load_developer_profile
22
+ from raise_cli.schemas.journal import JournalEntryType
23
+ from raise_cli.session.journal import (
24
+ append_journal_entry,
25
+ format_journal_compact,
26
+ read_journal,
27
+ )
28
+
29
+ journal_app = typer.Typer(
30
+ name="journal",
31
+ help="Incremental session memory (decisions, insights, tasks)",
32
+ no_args_is_help=True,
33
+ )
34
+
35
+
36
+ def _resolve_session_dir(project: str | None) -> Path:
37
+ """Resolve the current session directory from the developer profile.
38
+
39
+ Args:
40
+ project: Project path override.
41
+
42
+ Returns:
43
+ Path to the per-session directory.
44
+
45
+ Raises:
46
+ typer.Exit: If no active session found.
47
+ """
48
+ profile = load_developer_profile()
49
+ if profile is None:
50
+ cli_error("No developer profile found")
51
+ raise typer.Exit(1) # unreachable, cli_error raises
52
+
53
+ project_path = Path(project).resolve() if project else Path.cwd().resolve()
54
+
55
+ # Find active session for this project
56
+ for active in profile.active_sessions:
57
+ if active.project and Path(active.project).resolve() == project_path:
58
+ session_id = active.session_id
59
+ return get_personal_dir(project_path) / "sessions" / session_id
60
+
61
+ cli_error(
62
+ "No active session for this project",
63
+ hint="Start a session first: rai session start --project .",
64
+ )
65
+ raise typer.Exit(1) # unreachable
66
+
67
+
68
+ @journal_app.command()
69
+ def add(
70
+ content: Annotated[
71
+ str,
72
+ typer.Argument(help="Content to persist"),
73
+ ],
74
+ entry_type: Annotated[
75
+ JournalEntryType,
76
+ typer.Option(
77
+ "--type",
78
+ "-t",
79
+ help="Entry type (decision, insight, task_done, note)",
80
+ ),
81
+ ] = JournalEntryType.NOTE,
82
+ tags: Annotated[
83
+ str | None,
84
+ typer.Option(
85
+ "--tags",
86
+ help="Comma-separated tags (e.g., 'arch,spike')",
87
+ ),
88
+ ] = None,
89
+ project: Annotated[
90
+ str | None,
91
+ typer.Option(
92
+ "--project",
93
+ "-p",
94
+ help="Project path",
95
+ ),
96
+ ] = None,
97
+ ) -> None:
98
+ """Add a journal entry to the current session.
99
+
100
+ Examples:
101
+ $ rai session journal add "Use JSONL for journal" --type decision
102
+ $ rai session journal add "T1 complete" --type task_done
103
+ $ rai session journal add "Compaction loses rationale" --type insight --tags "compaction,memory"
104
+ """
105
+ session_dir = _resolve_session_dir(project)
106
+ tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
107
+
108
+ result = append_journal_entry(
109
+ session_dir=session_dir,
110
+ entry_type=entry_type,
111
+ content=content,
112
+ tags=tag_list,
113
+ )
114
+
115
+ typer.echo(f"{result.id}: {entry_type.value} recorded")
116
+
117
+
118
+ @journal_app.command()
119
+ def show(
120
+ last: Annotated[
121
+ int | None,
122
+ typer.Option(
123
+ "--last",
124
+ "-n",
125
+ help="Show only the last N entries",
126
+ ),
127
+ ] = None,
128
+ compact: Annotated[
129
+ bool,
130
+ typer.Option(
131
+ "--compact",
132
+ help="Output compact format for context injection",
133
+ ),
134
+ ] = False,
135
+ project: Annotated[
136
+ str | None,
137
+ typer.Option(
138
+ "--project",
139
+ "-p",
140
+ help="Project path",
141
+ ),
142
+ ] = None,
143
+ ) -> None:
144
+ """Show journal entries for the current session.
145
+
146
+ With --compact, outputs a token-efficient format suitable for
147
+ post-compaction context injection via hooks.
148
+
149
+ Examples:
150
+ $ rai session journal show
151
+ $ rai session journal show --last 5
152
+ $ rai session journal show --compact
153
+ """
154
+ session_dir = _resolve_session_dir(project)
155
+ entries = read_journal(session_dir, last_n=last)
156
+
157
+ if compact:
158
+ typer.echo(format_journal_compact(entries))
159
+ else:
160
+ if not entries:
161
+ typer.echo("No journal entries.")
162
+ return
163
+ for entry in entries:
164
+ tag_suffix = f" [{', '.join(entry.tags)}]" if entry.tags else ""
165
+ typer.echo(
166
+ f"[{entry.id}] {entry.entry_type.value}: {entry.content}{tag_suffix}"
167
+ )
@@ -0,0 +1,524 @@
1
+ """MCP server management commands.
2
+
3
+ Provides CLI access to registered MCP servers via the McpBridge.
4
+ Token-efficient by design: `rai mcp call` invokes tools without
5
+ injecting tool schemas into agent context (ADR-042, E338).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Annotated, Any
18
+
19
+ import typer
20
+ import yaml
21
+ from rich.console import Console
22
+ from rich.table import Table
23
+
24
+ from raise_cli.hooks.emitter import create_emitter
25
+ from raise_cli.hooks.events import McpCallEvent
26
+ from raise_cli.mcp.models import McpHealthResult
27
+ from raise_cli.mcp.registry import discover_mcp_servers
28
+ from raise_cli.mcp.schema import McpServerConfig, ServerConnection
29
+
30
+ mcp_app = typer.Typer(
31
+ name="mcp",
32
+ help="Manage and invoke MCP servers registered in .raise/mcp/",
33
+ no_args_is_help=True,
34
+ )
35
+
36
+ console = Console(stderr=True)
37
+
38
+
39
+ def _lazy_bridge(
40
+ server_command: str,
41
+ server_args: list[str] | None = None,
42
+ env: dict[str, str] | None = None,
43
+ ) -> Any:
44
+ """Lazy-import McpBridge and instantiate. Avoids requiring mcp SDK at CLI startup."""
45
+ from raise_cli.mcp.bridge import McpBridge
46
+
47
+ return McpBridge(server_command=server_command, server_args=server_args, env=env)
48
+
49
+
50
+ def _lazy_bridge_error() -> type:
51
+ """Lazy-import McpBridgeError."""
52
+ from raise_cli.mcp.bridge import McpBridgeError
53
+
54
+ return McpBridgeError
55
+
56
+
57
+ def _resolve_env(config: McpServerConfig) -> dict[str, str] | None:
58
+ """Build env dict from ServerConnection.env var names."""
59
+ env_names = config.server.env
60
+ if not env_names:
61
+ return None
62
+ return {
63
+ **os.environ,
64
+ **{k: os.environ.get(k, "") for k in env_names},
65
+ }
66
+
67
+
68
+ async def _call_tool(
69
+ config: McpServerConfig, tool_name: str, arguments: dict[str, Any]
70
+ ) -> dict[str, Any]:
71
+ """Connect to MCP server, call tool, return result as dict."""
72
+ bridge = _lazy_bridge(
73
+ server_command=config.server.command,
74
+ server_args=config.server.args,
75
+ env=_resolve_env(config),
76
+ )
77
+ try:
78
+ result = await bridge.call(tool_name, arguments)
79
+ return result.model_dump()
80
+ finally:
81
+ await bridge.aclose()
82
+
83
+
84
+ def _lookup_server(
85
+ server_name: str, servers: dict[str, McpServerConfig]
86
+ ) -> McpServerConfig:
87
+ """Look up server in registry or exit with error."""
88
+ if server_name not in servers:
89
+ console.print(f"Error: Server '{server_name}' not found in registry.")
90
+ available = ", ".join(sorted(servers)) if servers else "(none)"
91
+ console.print(f"Available servers: {available}")
92
+ raise typer.Exit(1)
93
+ return servers[server_name]
94
+
95
+
96
+ @mcp_app.command("list")
97
+ def list_servers() -> None:
98
+ """List all registered MCP servers.
99
+
100
+ Shows servers configured in .raise/mcp/*.yaml with their names,
101
+ descriptions, and commands.
102
+ """
103
+ servers = discover_mcp_servers()
104
+ if not servers:
105
+ console.print("No MCP servers registered in .raise/mcp/")
106
+ return
107
+
108
+ table = Table(title="MCP Servers (.raise/mcp/)")
109
+ table.add_column("Name", style="bold")
110
+ table.add_column("Description")
111
+ table.add_column("Command")
112
+
113
+ for config in servers.values():
114
+ cmd = f"{config.server.command} {' '.join(config.server.args)}"
115
+ table.add_row(
116
+ config.name,
117
+ config.description or "",
118
+ cmd[:50] + "..." if len(cmd) > 50 else cmd,
119
+ )
120
+
121
+ Console().print(table)
122
+
123
+
124
+ @mcp_app.command()
125
+ def health(
126
+ server: Annotated[str, typer.Argument(help="Registered MCP server name")],
127
+ ) -> None:
128
+ """Check connectivity of a registered MCP server.
129
+
130
+ Connects to the server, lists tools, and reports status, latency,
131
+ and tool count.
132
+ """
133
+ servers = discover_mcp_servers()
134
+ config = _lookup_server(server, servers)
135
+
136
+ async def _check() -> McpHealthResult:
137
+ bridge = _lazy_bridge(
138
+ server_command=config.server.command,
139
+ server_args=config.server.args,
140
+ env=_resolve_env(config),
141
+ )
142
+ try:
143
+ return await bridge.health()
144
+ finally:
145
+ await bridge.aclose()
146
+
147
+ result = asyncio.run(_check())
148
+ if result.healthy:
149
+ Console().print(
150
+ f"[green]{config.name}[/green]: healthy "
151
+ f"({result.tool_count} tools, {result.latency_ms}ms)"
152
+ )
153
+ else:
154
+ console.print(f"[red]{config.name}[/red]: unhealthy — {result.message}")
155
+ raise typer.Exit(1)
156
+
157
+
158
+ @mcp_app.command()
159
+ def tools(
160
+ server: Annotated[str, typer.Argument(help="Registered MCP server name")],
161
+ ) -> None:
162
+ """List available tools on a registered MCP server.
163
+
164
+ Connects to the server and retrieves the list of tools with their
165
+ names and descriptions.
166
+ """
167
+ servers = discover_mcp_servers()
168
+ config = _lookup_server(server, servers)
169
+
170
+ async def _list() -> list[tuple[str, str]]:
171
+ bridge = _lazy_bridge(
172
+ server_command=config.server.command,
173
+ server_args=config.server.args,
174
+ env=_resolve_env(config),
175
+ )
176
+ try:
177
+ tool_list = await bridge.list_tools()
178
+ return [(t.name, t.description) for t in tool_list]
179
+ finally:
180
+ await bridge.aclose()
181
+
182
+ try:
183
+ tool_info = asyncio.run(_list())
184
+ except Exception as exc:
185
+ if isinstance(exc, _lazy_bridge_error()):
186
+ console.print(f"Error: {exc}")
187
+ raise typer.Exit(1) from exc
188
+ raise
189
+
190
+ if not tool_info:
191
+ Console().print(f"No tools found on {config.name}")
192
+ return
193
+
194
+ table = Table(title=f"Tools for {config.name}")
195
+ table.add_column("Tool", style="bold")
196
+ table.add_column("Description")
197
+ for name, desc in tool_info:
198
+ table.add_row(name, desc)
199
+ Console().print(table)
200
+
201
+
202
+ @mcp_app.command()
203
+ def call(
204
+ server: Annotated[str, typer.Argument(help="Registered MCP server name")],
205
+ tool: Annotated[str, typer.Argument(help="Tool name to invoke")],
206
+ args: Annotated[
207
+ str,
208
+ typer.Option("--args", help="Tool arguments as JSON string"),
209
+ ] = "{}",
210
+ verbose: Annotated[
211
+ bool,
212
+ typer.Option("--verbose", help="Show call details on stderr"),
213
+ ] = False,
214
+ ) -> None:
215
+ """Invoke a tool on a registered MCP server.
216
+
217
+ Looks up the server in .raise/mcp/ registry, connects via McpBridge,
218
+ calls the specified tool, and prints the result as JSON to stdout.
219
+
220
+ Example:
221
+ rai mcp call context7 resolve-library-id --args '{"query":"next.js","libraryName":"next.js"}'
222
+ """
223
+ # Discover servers
224
+ servers = discover_mcp_servers()
225
+ config = _lookup_server(server, servers)
226
+
227
+ # Parse arguments
228
+ try:
229
+ arguments: dict[str, Any] = json.loads(args)
230
+ except json.JSONDecodeError as exc:
231
+ console.print(f"Error: Invalid JSON in --args: {exc}")
232
+ raise typer.Exit(1) from exc
233
+
234
+ # Call tool with latency measurement
235
+ emitter = create_emitter()
236
+ start = time.monotonic()
237
+ try:
238
+ result = asyncio.run(_call_tool(config, tool, arguments))
239
+ elapsed_ms = int((time.monotonic() - start) * 1000)
240
+ emitter.emit(
241
+ McpCallEvent(
242
+ server=server,
243
+ tool=tool,
244
+ success=True,
245
+ latency_ms=elapsed_ms,
246
+ )
247
+ )
248
+ if verbose:
249
+ console.print(f"mcp:call {server}/{tool} — ok ({elapsed_ms}ms)")
250
+ except Exception as exc:
251
+ elapsed_ms = int((time.monotonic() - start) * 1000)
252
+ emitter.emit(
253
+ McpCallEvent(
254
+ server=server,
255
+ tool=tool,
256
+ success=False,
257
+ latency_ms=elapsed_ms,
258
+ error=str(exc),
259
+ )
260
+ )
261
+ if verbose:
262
+ console.print(f"mcp:call {server}/{tool} — error ({elapsed_ms}ms): {exc}")
263
+ console.print(f"Error: {exc}")
264
+ raise typer.Exit(1) from exc
265
+
266
+ # Output JSON to stdout (not stderr console)
267
+ stdout = Console()
268
+ stdout.print_json(json.dumps(result))
269
+
270
+
271
+ def _write_mcp_config(
272
+ *,
273
+ name: str,
274
+ server_command: str,
275
+ server_args: list[str],
276
+ env_list: list[str] | None,
277
+ tool_names: list[str],
278
+ out_dir: Path,
279
+ force: bool,
280
+ source: str = "scaffold",
281
+ ) -> Path:
282
+ """Write .raise/mcp/<name>.yaml with overwrite protection.
283
+
284
+ Shared by ``scaffold`` and ``install`` commands. Returns the written path.
285
+
286
+ Raises:
287
+ typer.Exit: If file exists and ``force`` is False.
288
+ """
289
+ out_path = out_dir / f"{name}.yaml"
290
+
291
+ if out_path.exists() and not force:
292
+ console.print(f"Error: {out_path} already exists. Use --force to overwrite.")
293
+ raise typer.Exit(1)
294
+
295
+ config_data = McpServerConfig(
296
+ name=name,
297
+ server=ServerConnection(command=server_command, args=server_args, env=env_list),
298
+ )
299
+
300
+ out_dir.mkdir(parents=True, exist_ok=True)
301
+
302
+ header = f"# Generated by: rai mcp {source} {name}\n"
303
+ if tool_names:
304
+ header += f"# Discovered tools: {', '.join(tool_names)}\n"
305
+ yaml_content = yaml.dump(
306
+ config_data.model_dump(exclude_none=True),
307
+ default_flow_style=False,
308
+ sort_keys=False,
309
+ )
310
+ out_path.write_text(header + yaml_content)
311
+ return out_path
312
+
313
+
314
+ @mcp_app.command()
315
+ def scaffold(
316
+ name: Annotated[
317
+ str, typer.Argument(help="Server name (used as filename and config name)")
318
+ ],
319
+ command: Annotated[
320
+ str, typer.Option("--command", help="Server command (e.g. 'npx', 'uvx')")
321
+ ],
322
+ args: Annotated[
323
+ str,
324
+ typer.Option("--args", help="Server arguments as space-separated string"),
325
+ ] = "",
326
+ env: Annotated[
327
+ str,
328
+ typer.Option(
329
+ "--env", help="Comma-separated env var names (e.g. 'TOKEN,API_KEY')"
330
+ ),
331
+ ] = "",
332
+ force: Annotated[
333
+ bool,
334
+ typer.Option("--force", help="Overwrite existing config file"),
335
+ ] = False,
336
+ mcp_dir: Annotated[
337
+ str,
338
+ typer.Option("--mcp-dir", help="MCP config directory", hidden=True),
339
+ ] = "",
340
+ ) -> None:
341
+ """Connect to an MCP server, introspect tools, and generate config.
342
+
343
+ Creates a .raise/mcp/<name>.yaml config file by connecting to the
344
+ server, discovering available tools, and writing a valid McpServerConfig.
345
+
346
+ Example:
347
+ rai mcp scaffold context7 --command npx --args "-y @upstash/context7-mcp"
348
+ """
349
+ out_dir = Path(mcp_dir) if mcp_dir else Path.cwd() / ".raise" / "mcp"
350
+
351
+ # Early overwrite check — fail before connecting to server
352
+ out_path = out_dir / f"{name}.yaml"
353
+ if out_path.exists() and not force:
354
+ console.print(f"Error: {out_path} already exists. Use --force to overwrite.")
355
+ raise typer.Exit(1)
356
+
357
+ server_args = args.split() if args else []
358
+ env_list = [e.strip() for e in env.split(",") if e.strip()] or None
359
+
360
+ # Connect and introspect
361
+ async def _introspect() -> list[str]:
362
+ bridge = _lazy_bridge(
363
+ server_command=command,
364
+ server_args=server_args,
365
+ )
366
+ try:
367
+ tool_list = await bridge.list_tools()
368
+ return [t.name for t in tool_list]
369
+ finally:
370
+ await bridge.aclose()
371
+
372
+ try:
373
+ tool_names = asyncio.run(_introspect())
374
+ except Exception as exc:
375
+ console.print(f"Error: {exc}")
376
+ raise typer.Exit(1) from exc
377
+
378
+ out_path = _write_mcp_config(
379
+ name=name,
380
+ server_command=command,
381
+ server_args=server_args,
382
+ env_list=env_list,
383
+ tool_names=tool_names,
384
+ out_dir=out_dir,
385
+ force=force,
386
+ source="scaffold",
387
+ )
388
+ Console().print(f"Created {out_path} ({len(tool_names)} tools discovered)")
389
+
390
+
391
+ # --- Type strategies for install ---
392
+
393
+ _TYPE_STRATEGIES: dict[str, tuple[str, list[str]]] = {
394
+ "npx": ("npx", ["-y"]),
395
+ "uvx": ("uvx", []),
396
+ }
397
+
398
+
399
+ def _build_server_config(
400
+ pkg_type: str,
401
+ package: str,
402
+ module: str | None,
403
+ ) -> tuple[str, list[str]]:
404
+ """Return (command, args) for the given package type."""
405
+ if pkg_type == "pip":
406
+ assert module is not None # validated before calling
407
+ return "python", ["-m", module]
408
+ base_cmd, base_args = _TYPE_STRATEGIES[pkg_type]
409
+ return base_cmd, [*base_args, package]
410
+
411
+
412
+ @mcp_app.command()
413
+ def install(
414
+ package: Annotated[
415
+ str, typer.Argument(help="Package identifier (e.g. '@upstash/context7-mcp')")
416
+ ],
417
+ pkg_type: Annotated[
418
+ str,
419
+ typer.Option("--type", help="Package type: uvx, npx, or pip"),
420
+ ],
421
+ name: Annotated[str, typer.Option("--name", help="Server name for config file")],
422
+ env: Annotated[
423
+ str,
424
+ typer.Option("--env", help="Comma-separated env var names"),
425
+ ] = "",
426
+ module: Annotated[
427
+ str,
428
+ typer.Option("--module", help="Python module name (required for --type pip)"),
429
+ ] = "",
430
+ force: Annotated[
431
+ bool,
432
+ typer.Option("--force", help="Overwrite existing config file"),
433
+ ] = False,
434
+ mcp_dir: Annotated[
435
+ str,
436
+ typer.Option("--mcp-dir", help="MCP config directory", hidden=True),
437
+ ] = "",
438
+ ) -> None:
439
+ """Install an MCP server package and generate config.
440
+
441
+ Installs the package (pip only), verifies connectivity via health check,
442
+ and generates .raise/mcp/<name>.yaml.
443
+
444
+ Examples:
445
+ rai mcp install @upstash/context7-mcp --type npx --name context7
446
+ rai mcp install mcp-github --type uvx --name github --env GITHUB_TOKEN
447
+ rai mcp install mcp-server-fetch --type pip --name fetch --module mcp_server_fetch
448
+ """
449
+ # Validate type
450
+ valid_types: set[str] = {"uvx", "npx", "pip"}
451
+ if pkg_type not in valid_types:
452
+ console.print(
453
+ f"Error: Invalid --type '{pkg_type}'. Must be one of: {', '.join(sorted(valid_types))}"
454
+ )
455
+ raise typer.Exit(1)
456
+
457
+ # Validate pip requires --module
458
+ if pkg_type == "pip" and not module:
459
+ console.print("Error: --module is required when --type is pip")
460
+ raise typer.Exit(1)
461
+
462
+ out_dir = Path(mcp_dir) if mcp_dir else Path.cwd() / ".raise" / "mcp"
463
+
464
+ # Early overwrite check
465
+ out_path = out_dir / f"{name}.yaml"
466
+ if out_path.exists() and not force:
467
+ console.print(f"Error: {out_path} already exists. Use --force to overwrite.")
468
+ raise typer.Exit(1)
469
+
470
+ # pip: install package first
471
+ if pkg_type == "pip":
472
+ console.print(f"Installing {package} via pip...")
473
+ pip_result = subprocess.run(
474
+ [sys.executable, "-m", "pip", "install", package],
475
+ capture_output=True,
476
+ text=True,
477
+ )
478
+ if pip_result.returncode != 0:
479
+ console.print(f"Error: pip install failed: {pip_result.stderr}")
480
+ raise typer.Exit(1)
481
+
482
+ # Build command/args from type strategy
483
+ server_command, server_args = _build_server_config(
484
+ pkg_type,
485
+ package,
486
+ module or None,
487
+ )
488
+ env_list = [e.strip() for e in env.split(",") if e.strip()] or None
489
+
490
+ # Health check — non-fatal
491
+ tool_names: list[str] = []
492
+ health_ok = True
493
+
494
+ async def _check() -> list[str]:
495
+ bridge = _lazy_bridge(
496
+ server_command=server_command,
497
+ server_args=server_args,
498
+ )
499
+ try:
500
+ tools = await bridge.list_tools()
501
+ return [t.name for t in tools]
502
+ finally:
503
+ await bridge.aclose()
504
+
505
+ try:
506
+ tool_names = asyncio.run(_check())
507
+ except Exception as exc:
508
+ health_ok = False
509
+ console.print(f"Warning: Health check failed: {exc}")
510
+
511
+ # Write config
512
+ written = _write_mcp_config(
513
+ name=name,
514
+ server_command=server_command,
515
+ server_args=server_args,
516
+ env_list=env_list,
517
+ tool_names=tool_names,
518
+ out_dir=out_dir,
519
+ force=True, # already checked above
520
+ source="install",
521
+ )
522
+
523
+ status = "healthy" if health_ok else "unhealthy (check config)"
524
+ Console().print(f"Created {written} ({len(tool_names)} tools, {status})")