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.
- raise_cli/__init__.py +38 -0
- raise_cli/__main__.py +30 -0
- raise_cli/adapters/__init__.py +91 -0
- raise_cli/adapters/declarative/__init__.py +26 -0
- raise_cli/adapters/declarative/adapter.py +267 -0
- raise_cli/adapters/declarative/discovery.py +94 -0
- raise_cli/adapters/declarative/expressions.py +150 -0
- raise_cli/adapters/declarative/reference/__init__.py +1 -0
- raise_cli/adapters/declarative/reference/github.yaml +143 -0
- raise_cli/adapters/declarative/schema.py +98 -0
- raise_cli/adapters/filesystem.py +299 -0
- raise_cli/adapters/mcp_bridge.py +10 -0
- raise_cli/adapters/mcp_confluence.py +246 -0
- raise_cli/adapters/mcp_jira.py +405 -0
- raise_cli/adapters/models.py +205 -0
- raise_cli/adapters/protocols.py +180 -0
- raise_cli/adapters/registry.py +90 -0
- raise_cli/adapters/sync.py +149 -0
- raise_cli/agents/__init__.py +14 -0
- raise_cli/agents/antigravity.yaml +8 -0
- raise_cli/agents/claude.yaml +8 -0
- raise_cli/agents/copilot.yaml +8 -0
- raise_cli/agents/copilot_plugin.py +124 -0
- raise_cli/agents/cursor.yaml +7 -0
- raise_cli/agents/roo.yaml +8 -0
- raise_cli/agents/windsurf.yaml +8 -0
- raise_cli/artifacts/__init__.py +30 -0
- raise_cli/artifacts/models.py +43 -0
- raise_cli/artifacts/reader.py +55 -0
- raise_cli/artifacts/renderer.py +104 -0
- raise_cli/artifacts/story_design.py +69 -0
- raise_cli/artifacts/writer.py +45 -0
- raise_cli/backlog/__init__.py +1 -0
- raise_cli/backlog/sync.py +115 -0
- raise_cli/cli/__init__.py +3 -0
- raise_cli/cli/commands/__init__.py +3 -0
- raise_cli/cli/commands/_resolve.py +153 -0
- raise_cli/cli/commands/adapters.py +362 -0
- raise_cli/cli/commands/artifact.py +137 -0
- raise_cli/cli/commands/backlog.py +333 -0
- raise_cli/cli/commands/base.py +31 -0
- raise_cli/cli/commands/discover.py +551 -0
- raise_cli/cli/commands/docs.py +130 -0
- raise_cli/cli/commands/doctor.py +177 -0
- raise_cli/cli/commands/gate.py +223 -0
- raise_cli/cli/commands/graph.py +1086 -0
- raise_cli/cli/commands/info.py +81 -0
- raise_cli/cli/commands/init.py +746 -0
- raise_cli/cli/commands/journal.py +167 -0
- raise_cli/cli/commands/mcp.py +524 -0
- raise_cli/cli/commands/memory.py +467 -0
- raise_cli/cli/commands/pattern.py +348 -0
- raise_cli/cli/commands/profile.py +59 -0
- raise_cli/cli/commands/publish.py +80 -0
- raise_cli/cli/commands/release.py +338 -0
- raise_cli/cli/commands/session.py +528 -0
- raise_cli/cli/commands/signal.py +410 -0
- raise_cli/cli/commands/skill.py +350 -0
- raise_cli/cli/commands/skill_set.py +145 -0
- raise_cli/cli/error_handler.py +158 -0
- raise_cli/cli/main.py +163 -0
- raise_cli/compat.py +66 -0
- raise_cli/config/__init__.py +41 -0
- raise_cli/config/agent_plugin.py +105 -0
- raise_cli/config/agent_registry.py +233 -0
- raise_cli/config/agents.py +120 -0
- raise_cli/config/ide.py +32 -0
- raise_cli/config/paths.py +379 -0
- raise_cli/config/settings.py +180 -0
- raise_cli/context/__init__.py +42 -0
- raise_cli/context/analyzers/__init__.py +16 -0
- raise_cli/context/analyzers/models.py +36 -0
- raise_cli/context/analyzers/protocol.py +43 -0
- raise_cli/context/analyzers/python.py +292 -0
- raise_cli/context/builder.py +1569 -0
- raise_cli/context/diff.py +213 -0
- raise_cli/context/extractors/__init__.py +13 -0
- raise_cli/context/extractors/skills.py +121 -0
- raise_cli/core/__init__.py +37 -0
- raise_cli/core/files.py +66 -0
- raise_cli/core/text.py +174 -0
- raise_cli/core/tools.py +441 -0
- raise_cli/discovery/__init__.py +50 -0
- raise_cli/discovery/analyzer.py +691 -0
- raise_cli/discovery/drift.py +355 -0
- raise_cli/discovery/scanner.py +1687 -0
- raise_cli/doctor/__init__.py +4 -0
- raise_cli/doctor/checks/__init__.py +1 -0
- raise_cli/doctor/checks/environment.py +110 -0
- raise_cli/doctor/checks/project.py +238 -0
- raise_cli/doctor/fix.py +80 -0
- raise_cli/doctor/models.py +56 -0
- raise_cli/doctor/protocol.py +43 -0
- raise_cli/doctor/registry.py +100 -0
- raise_cli/doctor/report.py +141 -0
- raise_cli/doctor/runner.py +95 -0
- raise_cli/engines/__init__.py +3 -0
- raise_cli/exceptions.py +215 -0
- raise_cli/gates/__init__.py +19 -0
- raise_cli/gates/builtin/__init__.py +1 -0
- raise_cli/gates/builtin/coverage.py +52 -0
- raise_cli/gates/builtin/lint.py +48 -0
- raise_cli/gates/builtin/tests.py +48 -0
- raise_cli/gates/builtin/types.py +48 -0
- raise_cli/gates/models.py +40 -0
- raise_cli/gates/protocol.py +41 -0
- raise_cli/gates/registry.py +141 -0
- raise_cli/governance/__init__.py +11 -0
- raise_cli/governance/extractor.py +412 -0
- raise_cli/governance/models.py +134 -0
- raise_cli/governance/parsers/__init__.py +35 -0
- raise_cli/governance/parsers/_convert.py +38 -0
- raise_cli/governance/parsers/adr.py +274 -0
- raise_cli/governance/parsers/backlog.py +356 -0
- raise_cli/governance/parsers/constitution.py +119 -0
- raise_cli/governance/parsers/epic.py +323 -0
- raise_cli/governance/parsers/glossary.py +316 -0
- raise_cli/governance/parsers/guardrails.py +345 -0
- raise_cli/governance/parsers/prd.py +112 -0
- raise_cli/governance/parsers/roadmap.py +118 -0
- raise_cli/governance/parsers/vision.py +116 -0
- raise_cli/graph/__init__.py +1 -0
- raise_cli/graph/backends/__init__.py +57 -0
- raise_cli/graph/backends/api.py +137 -0
- raise_cli/graph/backends/dual.py +139 -0
- raise_cli/graph/backends/pending.py +84 -0
- raise_cli/handlers/__init__.py +3 -0
- raise_cli/hooks/__init__.py +54 -0
- raise_cli/hooks/builtin/__init__.py +1 -0
- raise_cli/hooks/builtin/backlog.py +216 -0
- raise_cli/hooks/builtin/gate_bridge.py +83 -0
- raise_cli/hooks/builtin/jira_sync.py +127 -0
- raise_cli/hooks/builtin/memory.py +117 -0
- raise_cli/hooks/builtin/telemetry.py +72 -0
- raise_cli/hooks/emitter.py +184 -0
- raise_cli/hooks/events.py +262 -0
- raise_cli/hooks/protocol.py +38 -0
- raise_cli/hooks/registry.py +117 -0
- raise_cli/mcp/__init__.py +33 -0
- raise_cli/mcp/bridge.py +218 -0
- raise_cli/mcp/models.py +43 -0
- raise_cli/mcp/registry.py +77 -0
- raise_cli/mcp/schema.py +41 -0
- raise_cli/memory/__init__.py +58 -0
- raise_cli/memory/loader.py +247 -0
- raise_cli/memory/migration.py +241 -0
- raise_cli/memory/models.py +169 -0
- raise_cli/memory/writer.py +598 -0
- raise_cli/onboarding/__init__.py +103 -0
- raise_cli/onboarding/bootstrap.py +324 -0
- raise_cli/onboarding/claudemd.py +17 -0
- raise_cli/onboarding/conventions.py +742 -0
- raise_cli/onboarding/detection.py +374 -0
- raise_cli/onboarding/governance.py +443 -0
- raise_cli/onboarding/instructions.py +672 -0
- raise_cli/onboarding/manifest.py +201 -0
- raise_cli/onboarding/memory_md.py +399 -0
- raise_cli/onboarding/migration.py +207 -0
- raise_cli/onboarding/profile.py +624 -0
- raise_cli/onboarding/skill_conflict.py +100 -0
- raise_cli/onboarding/skill_manifest.py +176 -0
- raise_cli/onboarding/skills.py +437 -0
- raise_cli/onboarding/workflows.py +101 -0
- raise_cli/output/__init__.py +28 -0
- raise_cli/output/console.py +394 -0
- raise_cli/output/formatters/__init__.py +9 -0
- raise_cli/output/formatters/adapters.py +135 -0
- raise_cli/output/formatters/discover.py +439 -0
- raise_cli/output/formatters/skill.py +298 -0
- raise_cli/publish/__init__.py +3 -0
- raise_cli/publish/changelog.py +80 -0
- raise_cli/publish/check.py +179 -0
- raise_cli/publish/version.py +172 -0
- raise_cli/rai_base/__init__.py +22 -0
- raise_cli/rai_base/framework/__init__.py +7 -0
- raise_cli/rai_base/framework/methodology.yaml +233 -0
- raise_cli/rai_base/governance/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
- raise_cli/rai_base/governance/architecture/system-context.md +34 -0
- raise_cli/rai_base/governance/architecture/system-design.md +24 -0
- raise_cli/rai_base/governance/backlog.md +8 -0
- raise_cli/rai_base/governance/guardrails.md +17 -0
- raise_cli/rai_base/governance/prd.md +25 -0
- raise_cli/rai_base/governance/vision.md +16 -0
- raise_cli/rai_base/identity/__init__.py +8 -0
- raise_cli/rai_base/identity/core.md +119 -0
- raise_cli/rai_base/identity/perspective.md +119 -0
- raise_cli/rai_base/memory/__init__.py +7 -0
- raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
- raise_cli/schemas/__init__.py +3 -0
- raise_cli/schemas/journal.py +49 -0
- raise_cli/schemas/session_state.py +117 -0
- raise_cli/session/__init__.py +5 -0
- raise_cli/session/bundle.py +820 -0
- raise_cli/session/close.py +268 -0
- raise_cli/session/journal.py +119 -0
- raise_cli/session/resolver.py +126 -0
- raise_cli/session/state.py +187 -0
- raise_cli/skills/__init__.py +44 -0
- raise_cli/skills/locator.py +141 -0
- raise_cli/skills/name_checker.py +199 -0
- raise_cli/skills/parser.py +145 -0
- raise_cli/skills/scaffold.py +212 -0
- raise_cli/skills/schema.py +132 -0
- raise_cli/skills/skillsets.py +195 -0
- raise_cli/skills/validator.py +197 -0
- raise_cli/skills_base/__init__.py +80 -0
- raise_cli/skills_base/contract-template.md +60 -0
- raise_cli/skills_base/preamble.md +37 -0
- raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
- raise_cli/skills_base/rai-debug/SKILL.md +171 -0
- raise_cli/skills_base/rai-discover/SKILL.md +167 -0
- raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
- raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
- raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
- raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
- raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
- raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
- raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
- raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
- raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
- raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
- raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
- raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
- raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
- raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
- raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
- raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
- raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
- raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
- raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
- raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
- raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
- raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
- raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
- raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
- raise_cli/skills_base/rai-research/SKILL.md +143 -0
- raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
- raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
- raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
- raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
- raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
- raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
- raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
- raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
- raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
- raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
- raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
- raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
- raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
- raise_cli/telemetry/__init__.py +42 -0
- raise_cli/telemetry/schemas.py +285 -0
- raise_cli/telemetry/writer.py +217 -0
- raise_cli/tier/__init__.py +0 -0
- raise_cli/tier/context.py +134 -0
- raise_cli/viz/__init__.py +7 -0
- raise_cli/viz/generator.py +406 -0
- raise_cli-2.2.1.dist-info/METADATA +433 -0
- raise_cli-2.2.1.dist-info/RECORD +264 -0
- raise_cli-2.2.1.dist-info/WHEEL +4 -0
- raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
- raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
- 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})")
|