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 +3 -3
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +63 -17
- package/autonomy/mcp-launch.sh +282 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/competitive/emergence-others-analysis.md +1 -1
- package/docs/competitive/replit-lovable-analysis.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +186 -35
- package/package.json +1 -1
- package/templates/simple-todo-app.md +3 -0
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
401
|
+
**v7.30.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
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
|
-
#
|
|
4740
|
-
#
|
|
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
|
-
#
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
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
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
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 (
|
|
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 (
|
|
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 |
|
|
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
|
---
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -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.
|
|
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=
|
|
790
|
+
//# debugId=F467D70D7895117C64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -467,39 +467,156 @@ def _emit_context_relevance_signal(
|
|
|
467
467
|
thread.start()
|
|
468
468
|
|
|
469
469
|
|
|
470
|
-
#
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if
|
|
502
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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.
|
|
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.
|