loki-mode 7.29.0 → 7.30.0

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.
package/README.md CHANGED
@@ -35,7 +35,7 @@
35
35
  - **Cross-project memory** -- Episodic/semantic/procedural memory with vector search; knowledge learned on one project surfaces on the next (v5.15.0+, see `memory/engine.py`)
36
36
  - **Self-hosted and private** -- Your keys, your infrastructure, no data leaves your network
37
37
  - **Legacy system healing** -- `loki heal` archaeology/stabilize/isolate/modernize/validate phases (v6.67.0, see `skills/healing.md`)
38
- - **MCP server** -- 34 tools (including ChromaDB code search) plus 3 resources and 2 prompts (`mcp/server.py`, with managed-memory and magic tools registered from `mcp/managed_tools.py` and `mcp/magic_tools.py`)
38
+ - **MCP server** -- 34 tools (including ChromaDB code search) plus 3 resources and 2 prompts (`mcp/server.py`, with magic tools registered from `mcp/magic_tools.py` and the managed-memory tool from `mcp/managed_tools.py`). Of the 34, 33 are always available; `loki_memory_redact` is registered but only succeeds when `LOKI_MANAGED_AGENTS=true` and `LOKI_MANAGED_MEMORY=true`. Launch with `loki mcp` (bootstraps the Python MCP SDK on first run).
39
39
  - **Full-stack output** -- Source code, tests, Docker Compose stacks (multi-service with healthchecks), CI/CD pipelines, audit logs
40
40
  - **Provider-agnostic** -- runs on Claude, Codex, Cline, or Aider with automatic failover (`loki-ts/src/runner/providers.ts`); no vendor lock-in. Gemini CLI deprecated v7.5.18; Antigravity CLI coming soon.
41
41
  - **Open source** -- Free for personal, internal, and academic use.
@@ -102,7 +102,7 @@ loki quick "build a landing page with a signup form"
102
102
  |--------|---------|-------|
103
103
  | **Bun (recommended)** | `bun install -g loki-mode` | Fastest startup for CLI commands. |
104
104
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` | Auto-installs Bun as a dep |
105
- | **Docker** | `docker pull asklokesh/loki-mode:7.29.0 && docker run --rm asklokesh/loki-mode:7.29.0 start prd.md` | Bun pre-installed in image |
105
+ | **Docker** | `docker pull asklokesh/loki-mode:7.30.0 && docker run --rm asklokesh/loki-mode:7.30.0 start prd.md` | Bun pre-installed in image |
106
106
  | **npm (compat)** | `npm install -g loki-mode` | Works without Bun (bash fallback). Migrate any time with `loki self-update --to bun`. |
107
107
 
108
108
  **Upgrading:**
@@ -162,7 +162,7 @@ The next major release sunsets the Bash runtime entirely. There is no firm calen
162
162
  | Method | Command |
163
163
  |--------|---------|
164
164
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` |
165
- | **Docker** | `docker pull asklokesh/loki-mode:7.29.0` |
165
+ | **Docker** | `docker pull asklokesh/loki-mode:7.30.0` |
166
166
  | **Inside Claude Code** | `claude --dangerously-skip-permissions` then type "Loki Mode" |
167
167
  | **Git clone** | `git clone https://github.com/asklokesh/loki-mode.git` |
168
168
 
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.29.0
6
+ # Loki Mode v7.30.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -398,4 +398,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
398
398
 
399
399
  ---
400
400
 
401
- **v7.29.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
401
+ **v7.30.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.29.0
1
+ 7.30.0
package/autonomy/loki CHANGED
@@ -702,6 +702,13 @@ show_help() {
702
702
  show_landing() {
703
703
  local version
704
704
  version=$(get_version)
705
+ # v7.30.0 (item 5): strip color when stdout is not a TTY (piped/redirected)
706
+ # in addition to the global NO_COLOR honoring above, so captured landing
707
+ # output is clean. Local overrides only; the rest of the CLI is untouched.
708
+ local BOLD="$BOLD" CYAN="$CYAN" NC="$NC"
709
+ if [ ! -t 1 ]; then
710
+ BOLD=''; CYAN=''; NC=''
711
+ fi
705
712
  echo -e "${BOLD}Loki Mode v$version${NC} - the spec-driven builder that verifies its own work."
706
713
  echo ""
707
714
  echo -e "First time here? ${CYAN}loki doctor${NC} checks your setup (an AI provider CLI is required)."
@@ -709,6 +716,7 @@ show_landing() {
709
716
  echo "Get started:"
710
717
  echo -e " ${CYAN}loki start ./prd.md${NC} Build from a spec (PRD file, GitHub issue, or no arg)"
711
718
  echo -e " ${CYAN}loki demo${NC} Build a sample todo app end to end (real run)"
719
+ echo -e " ${CYAN}loki web${NC} Open the visual builder to input a spec and watch agents build"
712
720
  echo -e " ${CYAN}loki dashboard start${NC} Start the live run monitor (then: loki dashboard open)"
713
721
  echo ""
714
722
  echo -e "Need help? ${CYAN}loki help${NC} lists every command."
@@ -4736,8 +4744,10 @@ cmd_web_stop() {
4736
4744
  rm -f "$pids_file" 2>/dev/null || true
4737
4745
  fi
4738
4746
 
4739
- # Global cleanup: kill ALL loki-related processes regardless of CWD
4740
- # This ensures "loki web stop" from anywhere cleans up everything
4747
+ # Companion cleanup: stop the dashboard server that the web UI runs
4748
+ # alongside. Scoped to the dashboard, NOT to every loki process: the
4749
+ # documented intent of "loki web stop" is to stop the Purple Lab web UI
4750
+ # session, not to reap unrelated builds.
4741
4751
 
4742
4752
  # Kill any dashboard server (by process name and by port)
4743
4753
  local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
@@ -4762,21 +4772,14 @@ cmd_web_stop() {
4762
4772
  kill -0 "$dash_port_pid" 2>/dev/null && kill -9 "$dash_port_pid" 2>/dev/null || true
4763
4773
  fi
4764
4774
 
4765
- # Kill any loki run.sh orchestrator processes (status monitors, resource monitors)
4766
- local run_pids
4767
- run_pids=$(pgrep -f "loki-run-\|status-monitor\|resource-monitor" 2>/dev/null || true)
4768
- if [ -n "$run_pids" ]; then
4769
- for rpid in $run_pids; do
4770
- kill "$rpid" 2>/dev/null || true
4771
- done
4772
- sleep 1
4773
- for rpid in $run_pids; do
4774
- kill -0 "$rpid" 2>/dev/null && kill -9 "$rpid" 2>/dev/null || true
4775
- done
4776
- if [ "$stopped" = true ]; then
4777
- echo "Background processes cleaned up"
4778
- fi
4779
- fi
4775
+ # FIX-563 (v7.30.0): do NOT blanket-kill loki-run-* orchestrators here.
4776
+ # The prior unscoped `pgrep -f "loki-run-..."` reaped EVERY orchestrator on
4777
+ # the machine, including foreign `loki start` sessions launched from other
4778
+ # terminals/CWDs that the web UI never owned. Purple Lab's own build
4779
+ # processes are already reaped authoritatively above via child-pids.json
4780
+ # (the only PIDs this session actually started). Foreign builds survive,
4781
+ # mirroring the cwd-scoped dashboard-stop pattern (FIX rationale: a user
4782
+ # invoking "loki web stop" expects the web UI gone, not their other builds).
4780
4783
 
4781
4784
  # Clean up all PID files globally
4782
4785
  rm -f "${LOKI_DIR}/dashboard/dashboard.pid" 2>/dev/null || true
@@ -9082,6 +9085,14 @@ cmd_sandbox() {
9082
9085
  # caller then falls back to a no-number confirm). Never fabricates a number.
9083
9086
  emit_demo_estimate() {
9084
9087
  local prd_path="$1"
9088
+ # v7.30.0 (item 5): the demo estimate always prints before spending, incl.
9089
+ # non-TTY (--dry-run / --yes piped), so gate color on a TTY here too (the
9090
+ # global vars are only blanked on NO_COLOR). Mirrors provider-offer.sh and
9091
+ # quickstart.sh. Local overrides only.
9092
+ local BOLD="$BOLD" YELLOW="$YELLOW" DIM="$DIM" NC="$NC"
9093
+ if [ ! -t 1 ]; then
9094
+ BOLD=''; YELLOW=''; DIM=''; NC=''
9095
+ fi
9085
9096
  local plan_json=""
9086
9097
  plan_json=$(LOKI_COMPLEXITY=simple show_prd_plan "$prd_path" "true" "false" 2>/dev/null) || plan_json=""
9087
9098
 
@@ -9160,6 +9171,16 @@ cmd_demo() {
9160
9171
  return 0
9161
9172
  fi
9162
9173
 
9174
+ # v7.30.0 (item 5): demo emits user-facing color (header, dry-run block,
9175
+ # cancel/refuse messages) on paths that always run even when piped/non-TTY,
9176
+ # so gate color on a TTY here (the global vars are only blanked on NO_COLOR).
9177
+ # Placed AFTER the --help early return so help output is untouched. Local
9178
+ # overrides only; mirrors provider-offer.sh / quickstart.sh.
9179
+ local BOLD="$BOLD" CYAN="$CYAN" YELLOW="$YELLOW" GREEN="$GREEN" DIM="$DIM" RED="$RED" NC="$NC"
9180
+ if [ ! -t 1 ]; then
9181
+ BOLD=''; CYAN=''; YELLOW=''; GREEN=''; DIM=''; RED=''; NC=''
9182
+ fi
9183
+
9163
9184
  local version
9164
9185
  version=$(get_version)
9165
9186
  local demo_prd="$SKILL_DIR/templates/simple-todo-app.md"
@@ -11636,6 +11657,28 @@ cmd_grill() {
11636
11657
  return $?
11637
11658
  }
11638
11659
 
11660
+ # ---------------------------------------------------------------------------
11661
+ # loki mcp - launch the MCP (Model Context Protocol) server
11662
+ #
11663
+ # Thin dispatcher that sources autonomy/mcp-launch.sh and delegates to
11664
+ # mcp_launch_main(). The launcher checks for python3 + the MCP SDK and, when
11665
+ # the SDK is missing, offers a consent-gated bootstrap into a project-local
11666
+ # virtualenv (.loki/mcp-venv) before exec'ing the server over stdio. This
11667
+ # closes the fresh-npm-consumer gap where the only bin was `loki` and the
11668
+ # Python MCP dependencies were never installed (task 562).
11669
+ # ---------------------------------------------------------------------------
11670
+ cmd_mcp() {
11671
+ local mcp_mod="$_LOKI_SCRIPT_DIR/mcp-launch.sh"
11672
+ if [ ! -f "$mcp_mod" ]; then
11673
+ echo -e "${RED}Error: mcp launcher not found at $mcp_mod${NC}" >&2
11674
+ return 3
11675
+ fi
11676
+ # shellcheck source=/dev/null
11677
+ source "$mcp_mod"
11678
+ mcp_launch_main "$@"
11679
+ return $?
11680
+ }
11681
+
11639
11682
  cmd_heal_help() {
11640
11683
  echo -e "${BOLD}loki heal${NC} - Legacy system healing (v6.67.0)"
11641
11684
  echo ""
@@ -13822,6 +13865,9 @@ main() {
13822
13865
  grill)
13823
13866
  cmd_grill "$@"
13824
13867
  ;;
13868
+ mcp)
13869
+ cmd_mcp "$@"
13870
+ ;;
13825
13871
  migrate)
13826
13872
  cmd_migrate "$@"
13827
13873
  ;;
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env bash
2
+ # mcp-launch.sh -- launch the Loki Mode MCP server, bootstrapping its Python
3
+ # dependencies on first run (task 562).
4
+ #
5
+ # Why this exists: a fresh `npm install -g loki-mode` ships mcp/server.py and
6
+ # mcp/requirements.txt but installs NO Python packages and exposes only the
7
+ # `loki` bin. So `python3 -m mcp.server` exits because the MCP SDK is absent.
8
+ # `loki mcp` closes that gap: it checks for python3 + the MCP SDK, and when the
9
+ # SDK is missing it offers a consent-gated bootstrap into a project-local
10
+ # virtualenv (.loki/mcp-venv), then execs the server over stdio using THAT
11
+ # venv's python so the SDK is actually importable.
12
+ #
13
+ # Design (least-invasive, honest):
14
+ # * Venv location: <user-cwd>/.loki/mcp-venv (the project Loki is invoked in,
15
+ # NOT the install root). Project-local, no global site-packages pollution,
16
+ # no sudo, no curl-pipe-bash, no root-owned writes under a global install.
17
+ # Removing the project's .loki fully uninstalls. Override with
18
+ # LOKI_MCP_VENV=/abs/path. Honors LOKI_DIR (defaults to .loki).
19
+ # * The server is launched with PYTHONPATH set to the install root (NOT by
20
+ # cd-ing into it) so the user's cwd is preserved: mcp/server.py resolves the
21
+ # project .loki from os.getcwd(). Without PYTHONPATH, `import mcp` from an
22
+ # arbitrary cwd resolves to the pip MCP SDK's own `mcp` package (zero Loki
23
+ # tools); PYTHONPATH puts the install root first so the LOCAL mcp/server.py
24
+ # wins, while server.py's namespace juggle still hands the real SDK its own
25
+ # `mcp.*` subtree.
26
+ # * The ONLY command run on the user's behalf is, after explicit consent:
27
+ # <venv>/bin/pip install -r mcp/requirements.txt
28
+ # The exact command is printed before it runs.
29
+ # * Non-interactive / CI: NEVER install. Print the manual command to stderr
30
+ # and exit 2 (mirrors autonomy/provider-offer.sh gate semantics).
31
+ # * Opt-out: LOKI_NO_INSTALL_OFFER=1 -> never prompt, print manual command,
32
+ # exit 2. --yes / LOKI_ASSUME_YES / LOKI_AUTO_CONFIRM=true -> auto-accept.
33
+ #
34
+ # Self-containment: depends only on bash builtins + python3 on PATH. Defines
35
+ # its own colors so it behaves identically whether sourced by autonomy/loki or
36
+ # run standalone.
37
+
38
+ # Guard against double-source.
39
+ if [ -n "${_LOKI_MCP_LAUNCH_SOURCED:-}" ]; then
40
+ return 0 2>/dev/null || true
41
+ fi
42
+ _LOKI_MCP_LAUNCH_SOURCED=1
43
+
44
+ # --- Self-contained colors (honor NO_COLOR) --------------------------------
45
+ if [ -n "${NO_COLOR:-}" ] || [ ! -t 1 ]; then
46
+ _ML_RED=''; _ML_YELLOW=''; _ML_BOLD=''; _ML_NC=''
47
+ else
48
+ _ML_RED=$'\033[0;31m'
49
+ _ML_YELLOW=$'\033[1;33m'
50
+ _ML_BOLD=$'\033[1m'
51
+ _ML_NC=$'\033[0m'
52
+ fi
53
+
54
+ # Repo root = parent of the directory holding this script (autonomy/..).
55
+ _ml_repo_root() {
56
+ local self_dir
57
+ self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
58
+ (cd "$self_dir/.." && pwd)
59
+ }
60
+
61
+ # _ml_assume_yes: true when the user opted into unattended confirmation.
62
+ _ml_assume_yes() {
63
+ [ "${LOKI_ASSUME_YES:-}" = "1" ] && return 0
64
+ [ "${LOKI_AUTO_CONFIRM:-}" = "true" ] && return 0
65
+ return 1
66
+ }
67
+
68
+ # _ml_non_interactive: true when we must NEVER prompt (non-TTY or CI).
69
+ _ml_non_interactive() {
70
+ [ ! -t 0 ] && return 0
71
+ [ ! -t 1 ] && return 0
72
+ [ -n "${CI:-}" ] && return 0
73
+ return 1
74
+ }
75
+
76
+ # _ml_python: echo the best base python3 for creating the venv, or empty.
77
+ _ml_python() {
78
+ local p
79
+ for p in python3 python3.12 python3.11; do
80
+ if command -v "$p" >/dev/null 2>&1; then
81
+ printf '%s' "$p"
82
+ return 0
83
+ fi
84
+ done
85
+ return 1
86
+ }
87
+
88
+ # _ml_sdk_importable <python> <root>: true (0) only if the real MCP SDK's
89
+ # FastMCP can actually be CONSTRUCTED -- not merely that the SDK files exist on
90
+ # disk. A file-exists check is a false positive: under the local-vs-SDK `mcp`
91
+ # namespace collision the package-dir FastMCP can be present yet fail to import
92
+ # (`No module named 'mcp.types'`). We delegate to mcp/server.py's own
93
+ # `--check-sdk` probe, which runs the exact loader the server uses and exits 0
94
+ # only when FastMCP loaded.
95
+ #
96
+ # Critical: we set PYTHONPATH to the install root and DO NOT cd into it, so the
97
+ # probe exercises the SAME module resolution as the real launch (which preserves
98
+ # the user's cwd). The redirect of stdin from /dev/null is insurance: if the
99
+ # pip SDK's own `mcp.server` were ever reached, its stub starts a stdio receive
100
+ # loop; the EOF makes it exit instead of hanging.
101
+ _ml_sdk_importable() {
102
+ local py="$1" root="$2"
103
+ PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" \
104
+ "$py" -m mcp.server --check-sdk </dev/null >/dev/null 2>&1
105
+ }
106
+
107
+ # _ml_print_manual <root> <venv>: print the honest manual install commands.
108
+ # The venv lives in the user's project (.loki/mcp-venv by default), while
109
+ # requirements.txt is shipped under the install root.
110
+ _ml_print_manual() {
111
+ local root="$1" venv="$2"
112
+ printf 'Install the MCP server dependencies manually:\n' >&2
113
+ printf ' python3 -m venv %s\n' "$venv" >&2
114
+ printf ' %s/bin/pip install -r %s/mcp/requirements.txt\n' "$venv" "$root" >&2
115
+ printf ' PYTHONPATH=%s %s/bin/python -m mcp.server\n' "$root" "$venv" >&2
116
+ }
117
+
118
+ _ml_help() {
119
+ cat <<'EOF'
120
+ Loki Mode -- launch the MCP (Model Context Protocol) server
121
+
122
+ Usage: loki mcp [--transport stdio|http] [--port N] [--help]
123
+
124
+ Starts the Loki Mode MCP server so MCP-aware clients (Claude Code, IDEs)
125
+ can call Loki's tools (memory, task queue, code search, build management).
126
+
127
+ On first run, if the Python MCP SDK is not installed, Loki offers to create
128
+ a project-local virtualenv at .loki/mcp-venv and install mcp/requirements.txt
129
+ into it (with your consent). The exact pip command is printed before it runs.
130
+ It then launches the server using that venv's python.
131
+
132
+ Options:
133
+ --transport stdio|http Transport to use (default: stdio).
134
+ --port N Port for http transport (default: 8421).
135
+ --help, -h Show this help and exit.
136
+
137
+ Environment:
138
+ LOKI_MCP_VENV=/abs/path Use a custom venv location instead of .loki/mcp-venv.
139
+ LOKI_NO_INSTALL_OFFER=1 Never prompt to install; print the manual command.
140
+ --yes / LOKI_ASSUME_YES=1 Auto-accept the dependency install.
141
+
142
+ Behavior in non-interactive / CI shells: never installs. Prints the manual
143
+ install command to stderr and exits 2.
144
+ EOF
145
+ }
146
+
147
+ # mcp_launch_main: dispatcher invoked by cmd_mcp() (autonomy/loki) or directly.
148
+ mcp_launch_main() {
149
+ # Parse only flags we own; everything else is forwarded to the server.
150
+ local arg
151
+ for arg in "$@"; do
152
+ case "$arg" in
153
+ --help|-h|help)
154
+ _ml_help
155
+ return 0
156
+ ;;
157
+ esac
158
+ done
159
+
160
+ local root
161
+ root="$(_ml_repo_root)"
162
+
163
+ # 1. python3 presence.
164
+ local base_py
165
+ if ! base_py="$(_ml_python)"; then
166
+ printf '%sNo python3 found on PATH.%s The MCP server needs Python 3.\n' "$_ML_RED" "$_ML_NC" >&2
167
+ printf 'Install Python 3 (https://www.python.org/downloads), then re-run: loki mcp\n' >&2
168
+ return 2
169
+ fi
170
+
171
+ # 2. venv location. Lives in the USER'S project (their cwd), NOT the install
172
+ # root: $root may be a root-owned global npm prefix where we must never
173
+ # write. LOKI_DIR (default .loki) keeps this consistent with every other
174
+ # .loki artifact; LOKI_MCP_VENV overrides outright.
175
+ local venv="${LOKI_MCP_VENV:-$PWD/${LOKI_DIR:-.loki}/mcp-venv}"
176
+ local venv_py="$venv/bin/python"
177
+
178
+ # 3. If the venv already has the SDK, use it directly. The server is launched
179
+ # with PYTHONPATH=$root (NOT by cd-ing) so the user's cwd is preserved for
180
+ # .loki resolution; see _ml_sdk_importable for why.
181
+ # Known narrow residual: if the user's cwd itself contains a Python
182
+ # package literally named mcp/ with a server submodule, python -m puts
183
+ # the cwd ahead of PYTHONPATH and that package wins. Essentially never
184
+ # true for real projects; documented rather than fought.
185
+ if [ -x "$venv_py" ] && _ml_sdk_importable "$venv_py" "$root"; then
186
+ exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" -m mcp.server "$@"
187
+ fi
188
+
189
+ # 4. If the BASE python already has the SDK (e.g. user pip-installed it),
190
+ # use it -- no venv needed.
191
+ if _ml_sdk_importable "$base_py" "$root"; then
192
+ exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$base_py" -m mcp.server "$@"
193
+ fi
194
+
195
+ # 5. SDK missing. Decide whether we may bootstrap.
196
+ if [ "${LOKI_NO_INSTALL_OFFER:-}" = "1" ]; then
197
+ printf '%sMCP SDK not installed.%s\n' "$_ML_YELLOW" "$_ML_NC" >&2
198
+ _ml_print_manual "$root" "$venv"
199
+ return 2
200
+ fi
201
+
202
+ if _ml_non_interactive; then
203
+ printf '%sMCP SDK not installed%s and this is a non-interactive shell, so Loki will not install it automatically.\n' "$_ML_YELLOW" "$_ML_NC" >&2
204
+ _ml_print_manual "$root" "$venv"
205
+ return 2
206
+ fi
207
+
208
+ # 6. Interactive TTY: offer the consent-gated bootstrap.
209
+ local answer=""
210
+ if _ml_assume_yes; then
211
+ answer="y"
212
+ else
213
+ printf '\n'
214
+ printf 'The MCP server needs Python dependencies that are not installed.\n'
215
+ printf 'Loki can create a project-local virtualenv and install them:\n'
216
+ printf ' python3 -m venv %s\n' "$venv"
217
+ printf ' %s/bin/pip install -r %s/mcp/requirements.txt\n' "$venv" "$root"
218
+ printf '\n'
219
+ printf 'Nothing is installed globally and no sudo is used. Proceed? [Y/n] '
220
+ read -r answer || answer="n"
221
+ fi
222
+
223
+ case "$answer" in
224
+ ""|y|Y|yes|YES) ;;
225
+ *)
226
+ printf 'Skipped.\n'
227
+ _ml_print_manual "$root" "$venv"
228
+ return 2
229
+ ;;
230
+ esac
231
+
232
+ # 7. Create the venv if needed. Ensure the parent .loki exists in the user's
233
+ # project first (never write under the install root).
234
+ if [ ! -x "$venv_py" ]; then
235
+ local venv_parent
236
+ venv_parent="$(dirname "$venv")"
237
+ if [ ! -d "$venv_parent" ] && ! mkdir -p "$venv_parent"; then
238
+ printf '%sCannot create %s (no write access).%s\n' "$_ML_RED" "$venv_parent" "$_ML_NC" >&2
239
+ _ml_print_manual "$root" "$venv"
240
+ return 2
241
+ fi
242
+ printf 'Creating virtualenv (%s) ...\n' "$venv"
243
+ if ! "$base_py" -m venv "$venv"; then
244
+ printf '%sFailed to create virtualenv at %s.%s\n' "$_ML_RED" "$venv" "$_ML_NC" >&2
245
+ _ml_print_manual "$root" "$venv"
246
+ return 2
247
+ fi
248
+ fi
249
+
250
+ # 8. Install requirements into the venv.
251
+ local req="$root/mcp/requirements.txt"
252
+ if [ ! -f "$req" ]; then
253
+ printf '%smcp/requirements.txt not found at %s.%s\n' "$_ML_RED" "$req" "$_ML_NC" >&2
254
+ return 2
255
+ fi
256
+ printf 'Installing MCP dependencies (%s/bin/pip install -r %s) ...\n' "$venv" "$req"
257
+ local code=0
258
+ "$venv/bin/pip" install -r "$req" || code=$?
259
+ if [ "$code" -ne 0 ]; then
260
+ printf '%sInstall failed (pip exited %s).%s You can retry manually:\n' "$_ML_RED" "$code" "$_ML_NC" >&2
261
+ _ml_print_manual "$root" "$venv"
262
+ return 2
263
+ fi
264
+
265
+ # 9. Verify, then exec the server using the venv python (critical: the
266
+ # site-packages walk in server.py only finds the SDK if we run the
267
+ # venv's interpreter, not the ambient python3). PYTHONPATH=$root keeps the
268
+ # user's cwd intact for .loki resolution; see _ml_sdk_importable.
269
+ if ! _ml_sdk_importable "$venv_py" "$root"; then
270
+ printf '%sDependencies installed but the MCP SDK still is not importable.%s\n' "$_ML_RED" "$_ML_NC" >&2
271
+ _ml_print_manual "$root" "$venv"
272
+ return 2
273
+ fi
274
+ printf "%sMCP dependencies ready. Launching server over stdio ...%s\n" "$_ML_BOLD" "$_ML_NC" >&2
275
+ exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" -m mcp.server "$@"
276
+ }
277
+
278
+ # Executed directly (tests, manual): run the dispatcher.
279
+ # When sourced by autonomy/loki, this block does not run.
280
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
281
+ mcp_launch_main "$@"
282
+ fi
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.29.0"
10
+ __version__ = "7.30.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.29.0
5
+ **Version:** v7.30.0
6
6
 
7
7
  ---
8
8
 
@@ -297,7 +297,7 @@ Developers who value open-source tooling, speed, and terminal-native workflows.
297
297
  | **Benchmark (SWE-bench)** | N/A | N/A | 80.9% (Opus 4.5) | 56.8% (Pro) | Configurable (uses any model) |
298
298
  | **CLI Interface** | No (Python API) | No (web UI) | Yes | Yes | Yes |
299
299
  | **IDE Integration** | No | No | Yes (VS Code, JetBrains) | No | Yes (VS Code extension) |
300
- | **MCP Support** | No | No | Yes | Yes | Yes (15 tools) |
300
+ | **MCP Support** | No | No | Yes | Yes | Yes (34 tools) |
301
301
  | **Cost (Heavy Use)** | Enterprise contract | $100/mo | $200/mo or API | $20-200/mo or API | $0 + API costs |
302
302
  | **Context Window** | Model-dependent | N/A | 200K tokens | Model-dependent | Model-dependent |
303
303
 
@@ -368,7 +368,7 @@ Replit Agent has evolved rapidly through four major versions:
368
368
  | Figma | MCP + Import | Builder.io plugin | No |
369
369
  | GitHub | Yes | Bidirectional sync | Full Git workflow |
370
370
  | Slack/Notion/Linear | Native connectors | No | Via MCP (extensible) |
371
- | Custom MCP | Yes | No | Yes (15 MCP tools) |
371
+ | Custom MCP | Yes | No | Yes (34 MCP tools) |
372
372
  | Salesforce | Native connector | No | No |
373
373
  | BigQuery/Snowflake | Native connectors | No | No |
374
374
 
@@ -544,7 +544,7 @@ When positioning Loki Mode against Replit and Lovable, emphasize:
544
544
  | Dashboard active sessions | Monitoring only | Build-along interface |
545
545
  | Figma integration | None | MCP connector live |
546
546
  | Starter templates | 13 PRD templates | +5 full-stack starter kits |
547
- | MCP connectors | 15 tools | 25+ tools with common services |
547
+ | MCP connectors | 34 tools | 25+ tools with common services |
548
548
  | Cost transparency | Token tracking exists | Real-time cost dashboard |
549
549
 
550
550
  ---
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var u8=Object.defineProperty;var c8=($)=>$;function p8($,Q){this[$]=c8.bind(null,Q)}var g=($,Q)=>{for(var Z in Q)u8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:p8.bind(Q,Z)})};var k=($,Q)=>()=>($&&(Q=$($=0)),Q);var X1=import.meta.require;var R$={};g(R$,{lokiDir:()=>P,homeLokiDir:()=>o1,findRepoRootForVersion:()=>d1,REPO_ROOT:()=>v});import{resolve as n,dirname as l1}from"path";import{fileURLToPath as l8}from"url";import{existsSync as L1}from"fs";import{homedir as d8}from"os";function o8(){let $=F$;for(let Q=0;Q<6;Q++){if(L1(n($,"VERSION"))&&L1(n($,"autonomy/run.sh")))return $;let Z=l1($);if(Z===$)break;$=Z}return n(F$,"..","..","..")}function d1($){let Q=$;for(let Z=0;Z<6;Z++){if(L1(n(Q,"VERSION"))&&L1(n(Q,"autonomy/run.sh")))return Q;let z=l1(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o1(){return n(d8(),".loki")}var F$,v;var h=k(()=>{F$=l1(l8(import.meta.url));v=o8()});import{readFileSync as n8}from"fs";import{resolve as a8,dirname as s8}from"path";import{fileURLToPath as t8}from"url";function k1(){if($1!==null)return $1;let $="7.29.0";if(typeof $==="string"&&$.length>0)return $1=$,$1;try{let Q=s8(t8(import.meta.url)),Z=d1(Q);$1=n8(a8(Z,"VERSION"),"utf-8").trim()}catch{$1="unknown"}return $1}var $1=null;var n1=k(()=>{h()});var x$={};g(x$,{runOrThrow:()=>r8,run:()=>j,commandVersion:()=>e8,commandExists:()=>f,ShellError:()=>a1});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,K;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}K=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[H,X,q]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:H,stderr:X,exitCode:q}}finally{if(z)clearTimeout(z);if(K)clearTimeout(K)}}async function r8($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=i8($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function i8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function e8($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a1;var d=k(()=>{a1=class a1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return $7?"":$}var $7,T,S,I,XZ,w,R,b,J;var c=k(()=>{$7=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),XZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),b=a("\x1B[2m"),J=a("\x1B[0m")});import{existsSync as W7}from"fs";async function Q1(){if(B1!==void 0)return B1;let $="/opt/homebrew/bin/python3.12";if(W7($))return B1=$,$;let Q=await f("python3.12");if(Q)return B1=Q,Q;let Z=await f("python3");return B1=Z,Z}async function Z1($,Q={}){let Z=await Q1();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B1;var H1=k(()=>{d()});var o$={};g(o$,{runStatus:()=>D7});import{existsSync as y,readFileSync as q1,readdirSync as f$,statSync as u$}from"fs";import{resolve as D,basename as L7}from"path";import{homedir as k7}from"os";async function F7(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
2
+ var u8=Object.defineProperty;var c8=($)=>$;function p8($,Q){this[$]=c8.bind(null,Q)}var g=($,Q)=>{for(var Z in Q)u8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:p8.bind(Q,Z)})};var k=($,Q)=>()=>($&&(Q=$($=0)),Q);var X1=import.meta.require;var R$={};g(R$,{lokiDir:()=>P,homeLokiDir:()=>o1,findRepoRootForVersion:()=>d1,REPO_ROOT:()=>v});import{resolve as n,dirname as l1}from"path";import{fileURLToPath as l8}from"url";import{existsSync as L1}from"fs";import{homedir as d8}from"os";function o8(){let $=F$;for(let Q=0;Q<6;Q++){if(L1(n($,"VERSION"))&&L1(n($,"autonomy/run.sh")))return $;let Z=l1($);if(Z===$)break;$=Z}return n(F$,"..","..","..")}function d1($){let Q=$;for(let Z=0;Z<6;Z++){if(L1(n(Q,"VERSION"))&&L1(n(Q,"autonomy/run.sh")))return Q;let z=l1(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o1(){return n(d8(),".loki")}var F$,v;var h=k(()=>{F$=l1(l8(import.meta.url));v=o8()});import{readFileSync as n8}from"fs";import{resolve as a8,dirname as s8}from"path";import{fileURLToPath as t8}from"url";function k1(){if($1!==null)return $1;let $="7.30.0";if(typeof $==="string"&&$.length>0)return $1=$,$1;try{let Q=s8(t8(import.meta.url)),Z=d1(Q);$1=n8(a8(Z,"VERSION"),"utf-8").trim()}catch{$1="unknown"}return $1}var $1=null;var n1=k(()=>{h()});var x$={};g(x$,{runOrThrow:()=>r8,run:()=>j,commandVersion:()=>e8,commandExists:()=>f,ShellError:()=>a1});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,K;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}K=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[H,X,q]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:H,stderr:X,exitCode:q}}finally{if(z)clearTimeout(z);if(K)clearTimeout(K)}}async function r8($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=i8($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function i8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function e8($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a1;var d=k(()=>{a1=class a1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return $7?"":$}var $7,T,S,I,XZ,w,R,b,J;var c=k(()=>{$7=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),XZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),b=a("\x1B[2m"),J=a("\x1B[0m")});import{existsSync as W7}from"fs";async function Q1(){if(B1!==void 0)return B1;let $="/opt/homebrew/bin/python3.12";if(W7($))return B1=$,$;let Q=await f("python3.12");if(Q)return B1=Q,Q;let Z=await f("python3");return B1=Z,Z}async function Z1($,Q={}){let Z=await Q1();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B1;var H1=k(()=>{d()});var o$={};g(o$,{runStatus:()=>D7});import{existsSync as y,readFileSync as q1,readdirSync as f$,statSync as u$}from"fs";import{resolve as D,basename as L7}from"path";import{homedir as k7}from"os";async function F7(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -787,4 +787,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
787
787
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
788
788
  `),process.stderr.write(f8),2}}v$();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var d3=await l3(Bun.argv.slice(2));process.exit(d3);
789
789
 
790
- //# debugId=E90B805A356D223A64756E2164756E21
790
+ //# debugId=F467D70D7895117C64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.29.0'
60
+ __version__ = '7.30.0'
package/mcp/server.py CHANGED
@@ -467,39 +467,156 @@ def _emit_context_relevance_signal(
467
467
  thread.start()
468
468
 
469
469
 
470
- # BUG #3 FIX: The local mcp/ package shadows the pip-installed mcp SDK.
471
- # Load FastMCP directly from site-packages using importlib.util to bypass
472
- # Python's package name resolution entirely (avoids infinite recursion).
473
- import importlib.util
470
+ # ============================================================
471
+ # Loading the pip MCP SDK's FastMCP under a NAMESPACE COLLISION
472
+ # ============================================================
473
+ #
474
+ # Root cause (task 562): this repo ships a local package named `mcp/`
475
+ # (this very file is mcp/server.py). That local package SHADOWS the
476
+ # pip-installed MCP SDK, which is also named `mcp`. The two cannot both
477
+ # own the top-level `mcp` name in one interpreter.
478
+ #
479
+ # The pre-task code tried to sidestep this by loading mcp/server/fastmcp.py
480
+ # directly from site-packages via importlib. That worked for the SDK's old
481
+ # single-FILE layout, but MCP SDK 1.x ships FastMCP as a PACKAGE DIRECTORY
482
+ # (mcp/server/fastmcp/__init__.py) whose own code does absolute imports like
483
+ # `from mcp.types import Icon` and `from mcp.server.lowlevel import Server`.
484
+ # Under shadowing those resolve to the LOCAL package and raise
485
+ # `ModuleNotFoundError: No module named 'mcp.types'`, so FastMCP never loads
486
+ # and the server exits. (lsp_proxy.py has the same latent failure but
487
+ # silently degrades to a no-op shim.)
488
+ #
489
+ # So the real fix is NOT file-vs-directory detection: it is resolving the
490
+ # namespace collision so the genuine SDK can import its own `mcp.*` subtree.
491
+ # We do this by temporarily letting the REAL SDK own the `mcp` name:
492
+ # 1. snapshot + evict the local `mcp` / `mcp.*` modules from sys.modules,
493
+ # 2. drop the repo root (and "" / ".") from sys.path so the next import of
494
+ # `mcp` resolves to site-packages, not the local package,
495
+ # 3. import the real `mcp.server.fastmcp` (and eagerly `mcp.types`), which
496
+ # transitively caches the real `mcp.*` subtree in sys.modules,
497
+ # 4. restore the LOCAL `mcp` and `mcp.server` entries so the rest of this
498
+ # codebase keeps using the local package for its own relative imports,
499
+ # while the real SDK submodules (mcp.types, mcp.shared.*,
500
+ # mcp.server.fastmcp.*, mcp.server.lowlevel.*) stay cached for the SDK's
501
+ # runtime use. FastMCP holds direct references to its dependencies once
502
+ # imported, so it does not re-resolve `mcp.server` by name at runtime.
503
+ #
504
+ # This is the least-invasive fix (it does not rename the local package, which
505
+ # mcp/__init__.py documents as intended behavior). It is inherently a bit
506
+ # fragile because it juggles sys.modules; the regression test for this code
507
+ # path is an END-TO-END one (start the server, complete an MCP stdio
508
+ # handshake, list tools), not a file-exists check, because only a real
509
+ # handshake proves FastMCP actually loaded and the subtree resolved.
510
+ import importlib
474
511
  import site
475
512
 
476
- _fastmcp_found = False
477
- _search_paths = []
478
- try:
479
- _search_paths.extend(site.getsitepackages())
480
- except AttributeError:
481
- pass
482
- try:
483
- _search_paths.append(site.getusersitepackages())
484
- except AttributeError:
485
- pass
486
513
 
487
- for _site_dir in _search_paths:
488
- _fastmcp_path = os.path.join(_site_dir, "mcp", "server", "fastmcp.py")
489
- if os.path.isfile(_fastmcp_path):
490
- _spec = importlib.util.spec_from_file_location(
491
- "mcp_pip_sdk.server.fastmcp", _fastmcp_path,
492
- submodule_search_locations=[]
514
+ def _real_mcp_search_dirs():
515
+ """Ordered site directories to search for the pip MCP SDK."""
516
+ dirs = []
517
+ try:
518
+ dirs.extend(site.getsitepackages())
519
+ except AttributeError:
520
+ pass
521
+ try:
522
+ dirs.append(site.getusersitepackages())
523
+ except AttributeError:
524
+ pass
525
+ return dirs
526
+
527
+
528
+ def _mcp_sdk_present(search_dirs=None):
529
+ """True if the pip MCP SDK appears installed in any site dir, accepting
530
+ both the legacy single-file layout and the 1.x package-directory layout.
531
+
532
+ Pure filesystem probe (no import side effects), kept standalone so the
533
+ both-layouts behaviour can be unit-tested against mktemp fixture dirs.
534
+ """
535
+ if search_dirs is None:
536
+ search_dirs = _real_mcp_search_dirs()
537
+ for _site_dir in search_dirs:
538
+ if not _site_dir:
539
+ continue
540
+ _file_layout = os.path.join(_site_dir, "mcp", "server", "fastmcp.py")
541
+ _pkg_layout = os.path.join(
542
+ _site_dir, "mcp", "server", "fastmcp", "__init__.py"
493
543
  )
494
- if _spec and _spec.loader:
495
- _fastmcp_mod = importlib.util.module_from_spec(_spec)
496
- _spec.loader.exec_module(_fastmcp_mod)
497
- FastMCP = _fastmcp_mod.FastMCP
498
- _fastmcp_found = True
499
- break
500
-
501
- if not _fastmcp_found:
502
- logger.error("MCP SDK (pip package 'mcp') not found in site-packages. Install with: pip install mcp")
544
+ if os.path.isfile(_file_layout) or os.path.isfile(_pkg_layout):
545
+ return True
546
+ return False
547
+
548
+
549
+ def _load_real_fastmcp():
550
+ """Import the genuine pip MCP SDK's FastMCP class, resolving the local-vs-
551
+ SDK `mcp` namespace collision. Returns the FastMCP class, or None if the
552
+ SDK cannot be loaded. Restores the local `mcp`/`mcp.server` modules before
553
+ returning so the rest of this module keeps working unchanged.
554
+ """
555
+ if not _mcp_sdk_present():
556
+ return None
557
+
558
+ # 1. Snapshot every currently-loaded local `mcp`/`mcp.*` module so we can
559
+ # restore the ones this codebase depends on afterwards.
560
+ _saved_local = {
561
+ _k: _v for _k, _v in list(sys.modules.items())
562
+ if _k == "mcp" or _k.startswith("mcp.")
563
+ }
564
+ # 2. Evict them so the real SDK can claim the `mcp` name on import.
565
+ for _k in list(_saved_local):
566
+ del sys.modules[_k]
567
+
568
+ # 3. Drop repo-root / cwd entries from sys.path for the SDK import so
569
+ # `mcp` resolves to site-packages rather than the local package.
570
+ _repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
571
+ _saved_path = sys.path[:]
572
+ sys.path[:] = [
573
+ _p for _p in sys.path
574
+ if _p not in ("", ".")
575
+ and os.path.abspath(_p) != os.path.abspath(_repo_root)
576
+ ]
577
+
578
+ _fastmcp_cls = None
579
+ try:
580
+ _real_fastmcp = importlib.import_module("mcp.server.fastmcp")
581
+ # Eagerly import the subtree FastMCP touches at runtime so the real
582
+ # modules are cached before we restore the local `mcp` over the name.
583
+ importlib.import_module("mcp.types")
584
+ importlib.import_module("mcp.server.lowlevel")
585
+ _fastmcp_cls = getattr(_real_fastmcp, "FastMCP", None)
586
+ except Exception as _exc: # pragma: no cover - defensive
587
+ logger.error("Failed to import the pip MCP SDK FastMCP: %s", _exc)
588
+ _fastmcp_cls = None
589
+ finally:
590
+ # 4. Restore sys.path and re-pin the LOCAL `mcp` + `mcp.server` modules
591
+ # so this codebase's own relative/absolute imports keep resolving
592
+ # locally. We intentionally leave the real `mcp.types`,
593
+ # `mcp.shared.*`, `mcp.server.fastmcp.*`, and `mcp.server.lowlevel.*`
594
+ # cached for the SDK's runtime use; the local package never defined
595
+ # those submodules, so there is nothing to clobber.
596
+ sys.path[:] = _saved_path
597
+ for _k in ("mcp", "mcp.server"):
598
+ if _k in _saved_local:
599
+ sys.modules[_k] = _saved_local[_k]
600
+ for _k, _v in _saved_local.items():
601
+ # Restore any other purely-local submodules that the SDK import did
602
+ # not legitimately replace (e.g. mcp.magic_tools, mcp.tools).
603
+ _real = sys.modules.get(_k)
604
+ if _real is None:
605
+ sys.modules[_k] = _v
606
+
607
+ return _fastmcp_cls
608
+
609
+
610
+ FastMCP = _load_real_fastmcp()
611
+
612
+ if FastMCP is None:
613
+ logger.error(
614
+ "MCP SDK (pip package 'mcp') not found or not importable. "
615
+ "Install it, then re-run. The simplest path is: 'loki mcp', which "
616
+ "creates a managed virtualenv at .loki/mcp-venv and installs "
617
+ "mcp/requirements.txt for you. To install manually: "
618
+ "pip install -r mcp/requirements.txt (or: pip install mcp)."
619
+ )
503
620
  sys.exit(1)
504
621
 
505
622
  # Read version from VERSION file instead of hardcoding
@@ -509,12 +626,31 @@ try:
509
626
  except Exception:
510
627
  _version = "unknown"
511
628
 
512
- # Initialize FastMCP server
513
- mcp = FastMCP(
514
- "loki-mode",
515
- version=_version,
516
- description="Loki Mode autonomous agent orchestration"
517
- )
629
+ # Initialize FastMCP server.
630
+ #
631
+ # Task 562: pass only kwargs the installed SDK actually accepts. MCP SDK 1.x
632
+ # FastMCP.__init__ has no `version=`/`description=` parameters (it uses
633
+ # `instructions=`), so passing them raises TypeError and the server never
634
+ # starts. We introspect the signature and forward only supported optional
635
+ # kwargs, keeping forward/backward compatibility across SDK versions.
636
+ def _build_fastmcp():
637
+ import inspect
638
+ _kwargs = {}
639
+ try:
640
+ _params = inspect.signature(FastMCP.__init__).parameters
641
+ except (TypeError, ValueError): # pragma: no cover - defensive
642
+ _params = {}
643
+ _desc = "Loki Mode autonomous agent orchestration"
644
+ if "instructions" in _params:
645
+ _kwargs["instructions"] = _desc
646
+ elif "description" in _params:
647
+ _kwargs["description"] = _desc
648
+ if "version" in _params:
649
+ _kwargs["version"] = _version
650
+ return FastMCP("loki-mode", **_kwargs)
651
+
652
+
653
+ mcp = _build_fastmcp()
518
654
 
519
655
  # ============================================================
520
656
  # TOOLS - Functions Claude can call
@@ -2462,8 +2598,23 @@ def main():
2462
2598
  help='Transport mechanism (default: stdio)')
2463
2599
  parser.add_argument('--port', type=int, default=8421,
2464
2600
  help='Port for HTTP transport (default: 8421)')
2601
+ parser.add_argument('--check-sdk', action='store_true',
2602
+ help=('Probe only: exit 0 if the MCP SDK loaded and the '
2603
+ 'server object built, non-zero otherwise. Used by '
2604
+ '`loki mcp` to verify a venv before launching. '
2605
+ 'Does not start a server.'))
2465
2606
  args = parser.parse_args()
2466
2607
 
2608
+ # --check-sdk: if we reached here, the module-level loader already imported
2609
+ # FastMCP and built `mcp` (otherwise the module would have sys.exit(1)'d at
2610
+ # import). So reaching main() with a live `mcp` object means the SDK is
2611
+ # genuinely importable. Report success and exit without starting a server.
2612
+ if args.check_sdk:
2613
+ if mcp is not None:
2614
+ print("MCP SDK OK", file=sys.stderr)
2615
+ sys.exit(0)
2616
+ sys.exit(1)
2617
+
2467
2618
  # Register cleanup to prevent file handle leaks on shutdown/restart
2468
2619
  atexit.register(cleanup_mcp_singletons)
2469
2620
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.29.0",
4
+ "version": "7.30.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -15,3 +15,6 @@ index.html in a browser runs the whole app.
15
15
 
16
16
  Keep the whole thing minimal and readable. The goal is a quick end-to-end
17
17
  build that finishes fast, not a production system.
18
+
19
+ A basic automated check or verification step confirms that add, list, toggle,
20
+ and delete behave as described before the build is called done.