loki-mode 7.30.0 → 7.32.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 +4 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/context-tracker.py +8 -0
- package/autonomy/loki +799 -123
- package/autonomy/mcp-launch.sh +149 -36
- package/autonomy/run.sh +168 -4
- package/bin/loki +71 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +326 -1
- package/dashboard/static/index.html +105 -39
- package/docs/INSTALLATION.md +1 -1
- package/docs/competitive/replit-lovable-analysis.md +1 -1
- package/loki-ts/data/model-pricing.json +1 -0
- package/loki-ts/dist/loki.js +233 -231
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +157 -0
- package/mcp/lsp_proxy.py +61 -61
- package/mcp/server.py +35 -129
- package/package.json +1 -1
- package/providers/claude.sh +76 -19
- package/providers/model_catalog.json +9 -0
- package/skills/model-selection.md +49 -1
package/mcp/__init__.py
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
mcp/_sdk_loader.py -- shared pip-MCP-SDK FastMCP loader.
|
|
4
|
+
|
|
5
|
+
Extracted verbatim from mcp/server.py (task 566) so BOTH the main server
|
|
6
|
+
(mcp/server.py) and the LSP proxy (mcp/lsp_proxy.py) load the genuine pip
|
|
7
|
+
MCP SDK's FastMCP through one battle-tested code path. Importing this module
|
|
8
|
+
has NO side effects (no FastMCP instantiation, no sys.exit, no tool
|
|
9
|
+
registration), so consumers can pull the helpers in without booting a server.
|
|
10
|
+
|
|
11
|
+
============================================================
|
|
12
|
+
Loading the pip MCP SDK's FastMCP under a NAMESPACE COLLISION
|
|
13
|
+
============================================================
|
|
14
|
+
|
|
15
|
+
Root cause (task 562, re-applied to the proxy in task 566): this repo ships
|
|
16
|
+
a local package named `mcp/` (this module is mcp/_sdk_loader.py). That local
|
|
17
|
+
package SHADOWS the pip-installed MCP SDK, which is also named `mcp`. The two
|
|
18
|
+
cannot both own the top-level `mcp` name in one interpreter.
|
|
19
|
+
|
|
20
|
+
The pre-task code tried to sidestep this by loading mcp/server/fastmcp.py
|
|
21
|
+
directly from site-packages via importlib. That worked for the SDK's old
|
|
22
|
+
single-FILE layout, but MCP SDK 1.x ships FastMCP as a PACKAGE DIRECTORY
|
|
23
|
+
(mcp/server/fastmcp/__init__.py) whose own code does absolute imports like
|
|
24
|
+
`from mcp.types import Icon` and `from mcp.server.lowlevel import Server`.
|
|
25
|
+
Under shadowing those resolve to the LOCAL package and raise
|
|
26
|
+
`ModuleNotFoundError: No module named 'mcp.types'`, so FastMCP never loads.
|
|
27
|
+
mcp/server.py used to sys.exit(1) on that; mcp/lsp_proxy.py used to silently
|
|
28
|
+
degrade to a no-op shim, so its LSP tools never loaded for consumers.
|
|
29
|
+
|
|
30
|
+
So the real fix is NOT file-vs-directory detection: it is resolving the
|
|
31
|
+
namespace collision so the genuine SDK can import its own `mcp.*` subtree.
|
|
32
|
+
We do this by temporarily letting the REAL SDK own the `mcp` name:
|
|
33
|
+
1. snapshot + evict the local `mcp` / `mcp.*` modules from sys.modules,
|
|
34
|
+
2. drop the repo root (and "" / ".") from sys.path so the next import of
|
|
35
|
+
`mcp` resolves to site-packages, not the local package,
|
|
36
|
+
3. import the real `mcp.server.fastmcp` (and eagerly `mcp.types`), which
|
|
37
|
+
transitively caches the real `mcp.*` subtree in sys.modules,
|
|
38
|
+
4. restore the LOCAL `mcp` and `mcp.server` entries so the rest of this
|
|
39
|
+
codebase keeps using the local package for its own relative imports,
|
|
40
|
+
while the real SDK submodules (mcp.types, mcp.shared.*,
|
|
41
|
+
mcp.server.fastmcp.*, mcp.server.lowlevel.*) stay cached for the SDK's
|
|
42
|
+
runtime use. FastMCP holds direct references to its dependencies once
|
|
43
|
+
imported, so it does not re-resolve `mcp.server` by name at runtime.
|
|
44
|
+
|
|
45
|
+
This is the least-invasive fix (it does not rename the local package, which
|
|
46
|
+
mcp/__init__.py documents as intended behavior). It is inherently a bit
|
|
47
|
+
fragile because it juggles sys.modules; the regression test for this code
|
|
48
|
+
path is an END-TO-END one (start the server, complete an MCP stdio
|
|
49
|
+
handshake, list tools), not a file-exists check, because only a real
|
|
50
|
+
handshake proves FastMCP actually loaded and the subtree resolved.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import importlib
|
|
56
|
+
import logging
|
|
57
|
+
import os
|
|
58
|
+
import site
|
|
59
|
+
import sys
|
|
60
|
+
|
|
61
|
+
logger = logging.getLogger('loki-mcp-sdk-loader')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _real_mcp_search_dirs():
|
|
65
|
+
"""Ordered site directories to search for the pip MCP SDK."""
|
|
66
|
+
dirs = []
|
|
67
|
+
try:
|
|
68
|
+
dirs.extend(site.getsitepackages())
|
|
69
|
+
except AttributeError:
|
|
70
|
+
pass
|
|
71
|
+
try:
|
|
72
|
+
dirs.append(site.getusersitepackages())
|
|
73
|
+
except AttributeError:
|
|
74
|
+
pass
|
|
75
|
+
return dirs
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _mcp_sdk_present(search_dirs=None):
|
|
79
|
+
"""True if the pip MCP SDK appears installed in any site dir, accepting
|
|
80
|
+
both the legacy single-file layout and the 1.x package-directory layout.
|
|
81
|
+
|
|
82
|
+
Pure filesystem probe (no import side effects), kept standalone so the
|
|
83
|
+
both-layouts behaviour can be unit-tested against mktemp fixture dirs.
|
|
84
|
+
"""
|
|
85
|
+
if search_dirs is None:
|
|
86
|
+
search_dirs = _real_mcp_search_dirs()
|
|
87
|
+
for _site_dir in search_dirs:
|
|
88
|
+
if not _site_dir:
|
|
89
|
+
continue
|
|
90
|
+
_file_layout = os.path.join(_site_dir, "mcp", "server", "fastmcp.py")
|
|
91
|
+
_pkg_layout = os.path.join(
|
|
92
|
+
_site_dir, "mcp", "server", "fastmcp", "__init__.py"
|
|
93
|
+
)
|
|
94
|
+
if os.path.isfile(_file_layout) or os.path.isfile(_pkg_layout):
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _load_real_fastmcp():
|
|
100
|
+
"""Import the genuine pip MCP SDK's FastMCP class, resolving the local-vs-
|
|
101
|
+
SDK `mcp` namespace collision. Returns the FastMCP class, or None if the
|
|
102
|
+
SDK cannot be loaded. Restores the local `mcp`/`mcp.server` modules before
|
|
103
|
+
returning so the rest of this module keeps working unchanged.
|
|
104
|
+
"""
|
|
105
|
+
if not _mcp_sdk_present():
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
# 1. Snapshot every currently-loaded local `mcp`/`mcp.*` module so we can
|
|
109
|
+
# restore the ones this codebase depends on afterwards.
|
|
110
|
+
_saved_local = {
|
|
111
|
+
_k: _v for _k, _v in list(sys.modules.items())
|
|
112
|
+
if _k == "mcp" or _k.startswith("mcp.")
|
|
113
|
+
}
|
|
114
|
+
# 2. Evict them so the real SDK can claim the `mcp` name on import.
|
|
115
|
+
for _k in list(_saved_local):
|
|
116
|
+
del sys.modules[_k]
|
|
117
|
+
|
|
118
|
+
# 3. Drop repo-root / cwd entries from sys.path for the SDK import so
|
|
119
|
+
# `mcp` resolves to site-packages rather than the local package.
|
|
120
|
+
_repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
121
|
+
_saved_path = sys.path[:]
|
|
122
|
+
sys.path[:] = [
|
|
123
|
+
_p for _p in sys.path
|
|
124
|
+
if _p not in ("", ".")
|
|
125
|
+
and os.path.abspath(_p) != os.path.abspath(_repo_root)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
_fastmcp_cls = None
|
|
129
|
+
try:
|
|
130
|
+
_real_fastmcp = importlib.import_module("mcp.server.fastmcp")
|
|
131
|
+
# Eagerly import the subtree FastMCP touches at runtime so the real
|
|
132
|
+
# modules are cached before we restore the local `mcp` over the name.
|
|
133
|
+
importlib.import_module("mcp.types")
|
|
134
|
+
importlib.import_module("mcp.server.lowlevel")
|
|
135
|
+
_fastmcp_cls = getattr(_real_fastmcp, "FastMCP", None)
|
|
136
|
+
except Exception as _exc: # pragma: no cover - defensive
|
|
137
|
+
logger.error("Failed to import the pip MCP SDK FastMCP: %s", _exc)
|
|
138
|
+
_fastmcp_cls = None
|
|
139
|
+
finally:
|
|
140
|
+
# 4. Restore sys.path and re-pin the LOCAL `mcp` + `mcp.server` modules
|
|
141
|
+
# so this codebase's own relative/absolute imports keep resolving
|
|
142
|
+
# locally. We intentionally leave the real `mcp.types`,
|
|
143
|
+
# `mcp.shared.*`, `mcp.server.fastmcp.*`, and `mcp.server.lowlevel.*`
|
|
144
|
+
# cached for the SDK's runtime use; the local package never defined
|
|
145
|
+
# those submodules, so there is nothing to clobber.
|
|
146
|
+
sys.path[:] = _saved_path
|
|
147
|
+
for _k in ("mcp", "mcp.server"):
|
|
148
|
+
if _k in _saved_local:
|
|
149
|
+
sys.modules[_k] = _saved_local[_k]
|
|
150
|
+
for _k, _v in _saved_local.items():
|
|
151
|
+
# Restore any other purely-local submodules that the SDK import did
|
|
152
|
+
# not legitimately replace (e.g. mcp.magic_tools, mcp.tools).
|
|
153
|
+
_real = sys.modules.get(_k)
|
|
154
|
+
if _real is None:
|
|
155
|
+
sys.modules[_k] = _v
|
|
156
|
+
|
|
157
|
+
return _fastmcp_cls
|
package/mcp/lsp_proxy.py
CHANGED
|
@@ -26,10 +26,14 @@ Supported languages (suffix -> binary):
|
|
|
26
26
|
.go -> gopls
|
|
27
27
|
.rs -> rust-analyzer
|
|
28
28
|
|
|
29
|
-
Tools:
|
|
29
|
+
Tools (7):
|
|
30
30
|
lsp_find_references(file, line, character, include_declaration=False)
|
|
31
31
|
lsp_go_to_definition(file, line, character)
|
|
32
32
|
lsp_symbol_at_position(file, line, character)
|
|
33
|
+
lsp_check_exists(symbol, kind=None, ...)
|
|
34
|
+
lsp_get_diagnostics(file)
|
|
35
|
+
lsp_workspace_symbols(query, limit=20, ...)
|
|
36
|
+
lsp_find_definition_by_name(symbol, ...)
|
|
33
37
|
|
|
34
38
|
Usage:
|
|
35
39
|
python3 -m mcp.lsp_proxy # stdio mode (default)
|
|
@@ -39,14 +43,12 @@ Usage:
|
|
|
39
43
|
from __future__ import annotations
|
|
40
44
|
|
|
41
45
|
import atexit
|
|
42
|
-
import importlib.util
|
|
43
46
|
import json
|
|
44
47
|
import logging
|
|
45
48
|
import os
|
|
46
49
|
import queue
|
|
47
50
|
import shutil
|
|
48
51
|
import signal
|
|
49
|
-
import site
|
|
50
52
|
import subprocess
|
|
51
53
|
import sys
|
|
52
54
|
import threading
|
|
@@ -660,13 +662,21 @@ def _dispatch_lsp(file: str, line: int, character: int,
|
|
|
660
662
|
# ============================================================
|
|
661
663
|
# FASTMCP LOADING
|
|
662
664
|
# ============================================================
|
|
663
|
-
#
|
|
664
|
-
#
|
|
665
|
-
# importlib.util
|
|
666
|
-
#
|
|
667
|
-
#
|
|
668
|
-
#
|
|
669
|
-
#
|
|
665
|
+
# Task 566: the local `mcp/` package shadows the pip `mcp` SDK, which under
|
|
666
|
+
# MCP SDK 1.x (FastMCP shipped as a package DIRECTORY) makes a naive
|
|
667
|
+
# importlib.util load of mcp/server/fastmcp/__init__.py fail with
|
|
668
|
+
# `ModuleNotFoundError: No module named 'mcp.types'`. The previous loader
|
|
669
|
+
# here did exactly that and then SILENTLY degraded to a no-op shim, so the
|
|
670
|
+
# LSP tools never loaded for real consumers even with the SDK installed.
|
|
671
|
+
#
|
|
672
|
+
# The fix: reuse the SAME namespace-collision-resolving loader that
|
|
673
|
+
# mcp/server.py uses (now shared in mcp/_sdk_loader.py). Unlike server.py we
|
|
674
|
+
# do NOT sys.exit() when the SDK is genuinely absent -- we degrade to a no-op
|
|
675
|
+
# shim so the module imports cleanly under test, and production-without-SDK
|
|
676
|
+
# fails on `.run()` rather than at import time (silent-skip philosophy). But
|
|
677
|
+
# the degrade is now LOUD: one warning line naming the cause, never silent.
|
|
678
|
+
from mcp._sdk_loader import _load_real_fastmcp # noqa: E402
|
|
679
|
+
|
|
670
680
|
|
|
671
681
|
class _NoopFastMCP:
|
|
672
682
|
"""Fallback used when the pip `mcp` SDK is not installed. Tool
|
|
@@ -688,51 +698,21 @@ class _NoopFastMCP:
|
|
|
688
698
|
|
|
689
699
|
|
|
690
700
|
def _load_fastmcp():
|
|
691
|
-
"""
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if os.path.isfile(candidate_file):
|
|
707
|
-
spec = importlib.util.spec_from_file_location(
|
|
708
|
-
'mcp_pip_sdk_lsp.server.fastmcp', candidate_file,
|
|
709
|
-
submodule_search_locations=[],
|
|
710
|
-
)
|
|
711
|
-
if spec and spec.loader:
|
|
712
|
-
mod = importlib.util.module_from_spec(spec)
|
|
713
|
-
try:
|
|
714
|
-
spec.loader.exec_module(mod)
|
|
715
|
-
return mod.FastMCP
|
|
716
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
717
|
-
logger.debug("FastMCP file-import failed: %s", exc)
|
|
718
|
-
# Second shape: fastmcp/__init__.py package directory.
|
|
719
|
-
candidate_pkg = os.path.join(site_dir, 'mcp', 'server', 'fastmcp', '__init__.py')
|
|
720
|
-
if os.path.isfile(candidate_pkg):
|
|
721
|
-
spec = importlib.util.spec_from_file_location(
|
|
722
|
-
'mcp_pip_sdk_lsp.server.fastmcp', candidate_pkg,
|
|
723
|
-
submodule_search_locations=[
|
|
724
|
-
os.path.join(site_dir, 'mcp', 'server', 'fastmcp'),
|
|
725
|
-
],
|
|
726
|
-
)
|
|
727
|
-
if spec and spec.loader:
|
|
728
|
-
mod = importlib.util.module_from_spec(spec)
|
|
729
|
-
try:
|
|
730
|
-
spec.loader.exec_module(mod)
|
|
731
|
-
if hasattr(mod, 'FastMCP'):
|
|
732
|
-
return mod.FastMCP
|
|
733
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
734
|
-
logger.debug("FastMCP pkg-import failed: %s", exc)
|
|
735
|
-
logger.warning("MCP SDK not found; LSP proxy MCP server will not be runnable.")
|
|
701
|
+
"""Load the genuine pip MCP SDK's FastMCP via the shared loader, which
|
|
702
|
+
resolves the local-vs-SDK `mcp` namespace collision under both SDK
|
|
703
|
+
layouts. Returns the real FastMCP class on success, or `_NoopFastMCP`
|
|
704
|
+
on a genuinely-absent SDK (so import never raises). The degrade path is
|
|
705
|
+
LOUD: it logs one warning naming the cause rather than failing silently
|
|
706
|
+
the way the old importlib.util shim did."""
|
|
707
|
+
cls = _load_real_fastmcp()
|
|
708
|
+
if cls is not None:
|
|
709
|
+
return cls
|
|
710
|
+
logger.warning(
|
|
711
|
+
"MCP SDK (pip package 'mcp') not found or not importable; LSP proxy "
|
|
712
|
+
"MCP server will not be runnable. Tools are registered against a "
|
|
713
|
+
"no-op shim and mcp.run() will raise. Install with: "
|
|
714
|
+
"pip install -r mcp/requirements.txt (or: pip install mcp)."
|
|
715
|
+
)
|
|
736
716
|
return _NoopFastMCP
|
|
737
717
|
|
|
738
718
|
|
|
@@ -748,12 +728,32 @@ except Exception:
|
|
|
748
728
|
_version = 'unknown'
|
|
749
729
|
|
|
750
730
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
731
|
+
def _build_lsp_fastmcp():
|
|
732
|
+
"""Instantiate FastMCP forwarding only the optional kwargs the installed
|
|
733
|
+
SDK actually accepts. Mirrors mcp/server.py's _build_fastmcp: MCP SDK 1.x
|
|
734
|
+
FastMCP.__init__ has no `version=`/`description=` parameters (it uses
|
|
735
|
+
`instructions=`), so passing them unconditionally raises TypeError and
|
|
736
|
+
the proxy never starts. We introspect the signature and forward only
|
|
737
|
+
supported kwargs, keeping forward/backward compatibility. The no-op shim
|
|
738
|
+
accepts any kwargs, so this is safe on the degrade path too."""
|
|
739
|
+
import inspect
|
|
740
|
+
_kwargs = {}
|
|
741
|
+
try:
|
|
742
|
+
_params = inspect.signature(FastMCP.__init__).parameters
|
|
743
|
+
except (TypeError, ValueError): # pragma: no cover - defensive
|
|
744
|
+
_params = {}
|
|
745
|
+
_desc = ('Loki Mode LSP proxy: find references, go to definition, '
|
|
746
|
+
'symbol-at-position via on-PATH language servers.')
|
|
747
|
+
if "instructions" in _params:
|
|
748
|
+
_kwargs["instructions"] = _desc
|
|
749
|
+
elif "description" in _params:
|
|
750
|
+
_kwargs["description"] = _desc
|
|
751
|
+
if "version" in _params:
|
|
752
|
+
_kwargs["version"] = _version
|
|
753
|
+
return FastMCP('loki-mode-lsp-proxy', **_kwargs)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
mcp = _build_lsp_fastmcp()
|
|
757
757
|
|
|
758
758
|
|
|
759
759
|
# ============================================================
|
package/mcp/server.py
CHANGED
|
@@ -476,135 +476,20 @@ def _emit_context_relevance_signal(
|
|
|
476
476
|
# pip-installed MCP SDK, which is also named `mcp`. The two cannot both
|
|
477
477
|
# own the top-level `mcp` name in one interpreter.
|
|
478
478
|
#
|
|
479
|
-
# The
|
|
480
|
-
#
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
#
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
#
|
|
487
|
-
#
|
|
488
|
-
#
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
511
|
-
import site
|
|
512
|
-
|
|
513
|
-
|
|
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"
|
|
543
|
-
)
|
|
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
|
|
479
|
+
# The FastMCP loader that resolves this collision used to live inline here.
|
|
480
|
+
# Task 566 extracted it VERBATIM into mcp/_sdk_loader.py so the LSP proxy
|
|
481
|
+
# (mcp/lsp_proxy.py) can load FastMCP through the SAME battle-tested path
|
|
482
|
+
# instead of its old importlib.util shim, which silently degraded to a
|
|
483
|
+
# no-op under the MCP SDK 1.x package-directory layout. The shared module
|
|
484
|
+
# carries the full root-cause writeup; behavior here is unchanged (the
|
|
485
|
+
# three helpers are re-exported below under their original names so any
|
|
486
|
+
# existing `mcp.server._mcp_sdk_present` / `_load_real_fastmcp` reference
|
|
487
|
+
# keeps resolving).
|
|
488
|
+
from mcp._sdk_loader import ( # noqa: E402
|
|
489
|
+
_real_mcp_search_dirs,
|
|
490
|
+
_mcp_sdk_present,
|
|
491
|
+
_load_real_fastmcp,
|
|
492
|
+
)
|
|
608
493
|
|
|
609
494
|
|
|
610
495
|
FastMCP = _load_real_fastmcp()
|
|
@@ -652,6 +537,27 @@ def _build_fastmcp():
|
|
|
652
537
|
|
|
653
538
|
mcp = _build_fastmcp()
|
|
654
539
|
|
|
540
|
+
# Propagate the loki-mode VERSION into serverInfo.version.
|
|
541
|
+
#
|
|
542
|
+
# FastMCP 1.x exposes no `version=` kwarg (see _build_fastmcp above), so the
|
|
543
|
+
# kwarg branch there never fires on the installed SDK. FastMCP DOES forward to
|
|
544
|
+
# an underlying lowlevel `Server` (mcp._mcp_server), whose `version` attribute
|
|
545
|
+
# is what `create_initialization_options()` reads into serverInfo at the
|
|
546
|
+
# initialize handshake -- falling back to importlib.metadata.version("mcp")
|
|
547
|
+
# (the SDK's OWN pip version, e.g. 1.27.x) when it is None. That fallback is
|
|
548
|
+
# why the listing surfaced the SDK version instead of ours. Setting this
|
|
549
|
+
# attribute is the only mechanism the installed SDK exposes to override
|
|
550
|
+
# serverInfo.version; `version=` is a documented public parameter on the
|
|
551
|
+
# lowlevel Server.__init__, so this is the supported field, not an internal
|
|
552
|
+
# hack. Guarded so a future SDK that already set a version (e.g. via the
|
|
553
|
+
# _build_fastmcp kwarg branch) is left untouched.
|
|
554
|
+
try:
|
|
555
|
+
_inner = getattr(mcp, "_mcp_server", None)
|
|
556
|
+
if _inner is not None and getattr(_inner, "version", None) in (None, ""):
|
|
557
|
+
_inner.version = _version
|
|
558
|
+
except Exception: # pragma: no cover - defensive: never block server startup
|
|
559
|
+
pass
|
|
560
|
+
|
|
655
561
|
# ============================================================
|
|
656
562
|
# TOOLS - Functions Claude can call
|
|
657
563
|
# ============================================================
|
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.32.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",
|
package/providers/claude.sh
CHANGED
|
@@ -287,6 +287,65 @@ provider_get_tier_param() {
|
|
|
287
287
|
fi
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
# Canonical model-override / session-model normalization (single source of
|
|
291
|
+
# truth shared by run.sh, the dashboard, and the estimator). Trim leading and
|
|
292
|
+
# trailing whitespace, lowercase, and accept ONLY an exact allowlisted alias.
|
|
293
|
+
# Interior whitespace (e.g. "fab le") is therefore rejected rather than silently
|
|
294
|
+
# collapsed into "fable". bash 3.2 safe (no ${var,,}); uses tr for lowercasing.
|
|
295
|
+
# Echoes the canonical alias on success, or the empty string when the input is
|
|
296
|
+
# not an exact allowlisted alias.
|
|
297
|
+
loki_normalize_model_alias() {
|
|
298
|
+
local raw="$1"
|
|
299
|
+
# Trim leading/trailing whitespace (interior whitespace preserved so a
|
|
300
|
+
# value like "fab le" stays "fab le" and fails the exact-match below).
|
|
301
|
+
raw="${raw#"${raw%%[![:space:]]*}"}"
|
|
302
|
+
raw="${raw%"${raw##*[![:space:]]}"}"
|
|
303
|
+
raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
304
|
+
case "$raw" in
|
|
305
|
+
haiku|sonnet|opus|fable) printf '%s' "$raw" ;;
|
|
306
|
+
*) printf '%s' "" ;;
|
|
307
|
+
esac
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Shared cost-ceiling clamp (single source of truth for LOKI_MAX_TIER). Given a
|
|
311
|
+
# resolved model name and a tier hint, return the model clamped down to the
|
|
312
|
+
# operator's LOKI_MAX_TIER ceiling. Used by resolve_model_for_tier AND by the
|
|
313
|
+
# mid-flight model-override path in run.sh so a dashboard/CLI override cannot
|
|
314
|
+
# silently bypass the ceiling. Clamps are byte-identical by construction:
|
|
315
|
+
# sonnet-cap resolves planning/fable down to PROVIDER_MODEL_DEVELOPMENT (opus by
|
|
316
|
+
# default), opus-cap resolves fable down to opus, haiku-cap pins everything to
|
|
317
|
+
# PROVIDER_MODEL_FAST. No ceiling set -> model unchanged.
|
|
318
|
+
loki_apply_max_tier_clamp() {
|
|
319
|
+
local model="$1"
|
|
320
|
+
local tier="${2:-}"
|
|
321
|
+
local max_tier="${LOKI_MAX_TIER:-}"
|
|
322
|
+
# Normalize EXACTLY like the python ports (dashboard _clamp_to_max_tier,
|
|
323
|
+
# estimator _max_tier): trim + lowercase. Without this, a user-typed cap
|
|
324
|
+
# like "Sonnet" (settings.json maxTier exports verbatim) was silently
|
|
325
|
+
# ignored here while quote and dashboard claimed the ceiling enforced:
|
|
326
|
+
# the run would exceed the quote. Council R1 finding, v7.31.0.
|
|
327
|
+
max_tier="$(printf '%s' "$max_tier" | tr '[:upper:]' '[:lower:]' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
328
|
+
[ -z "$max_tier" ] && { printf '%s' "$model"; return; }
|
|
329
|
+
case "$max_tier" in
|
|
330
|
+
haiku)
|
|
331
|
+
model="$PROVIDER_MODEL_FAST"
|
|
332
|
+
;;
|
|
333
|
+
sonnet)
|
|
334
|
+
# Cap planning/fable down to development.
|
|
335
|
+
if [ "$tier" = "planning" ] || [ "$tier" = "fable" ] || [ "$model" = "fable" ]; then
|
|
336
|
+
model="$PROVIDER_MODEL_DEVELOPMENT"
|
|
337
|
+
fi
|
|
338
|
+
;;
|
|
339
|
+
opus)
|
|
340
|
+
# Opus is the ceiling: cap fable back to opus.
|
|
341
|
+
if [ "$model" = "fable" ]; then
|
|
342
|
+
model="opus"
|
|
343
|
+
fi
|
|
344
|
+
;;
|
|
345
|
+
esac
|
|
346
|
+
printf '%s' "$model"
|
|
347
|
+
}
|
|
348
|
+
|
|
290
349
|
# Dynamic model resolution (v6.0.0)
|
|
291
350
|
# Resolves a capability tier to a concrete model name at runtime.
|
|
292
351
|
# Respects LOKI_MAX_TIER to cap cost (e.g., maxTier=sonnet prevents opus usage).
|
|
@@ -301,35 +360,33 @@ resolve_model_for_tier() {
|
|
|
301
360
|
cheap) tier="fast" ;;
|
|
302
361
|
esac
|
|
303
362
|
|
|
304
|
-
local max_tier="${LOKI_MAX_TIER:-}"
|
|
305
363
|
local model=""
|
|
306
364
|
|
|
307
365
|
# Resolve tier to model
|
|
366
|
+
# fable) explicit top-tier advisory model (Fable 5, 2x Opus). Reached when
|
|
367
|
+
# the session is pinned to fable (LOKI_SESSION_MODEL=fable), the
|
|
368
|
+
# mid-flight override file selects fable, or the architect opt-in
|
|
369
|
+
# (LOKI_FABLE_ARCHITECT=1) pins the first iteration's tier to fable
|
|
370
|
+
# in run.sh. So the resolver honors the documented lever instead of
|
|
371
|
+
# falling through the `*` arm to opus (the model-honesty fix).
|
|
308
372
|
case "$tier" in
|
|
309
373
|
planning) model="$PROVIDER_MODEL_PLANNING" ;;
|
|
310
374
|
development) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
|
|
311
375
|
fast) model="$PROVIDER_MODEL_FAST" ;;
|
|
376
|
+
fable) model="fable" ;;
|
|
312
377
|
*) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
|
|
313
378
|
esac
|
|
314
379
|
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
model="$PROVIDER_MODEL_DEVELOPMENT"
|
|
326
|
-
fi
|
|
327
|
-
;;
|
|
328
|
-
opus)
|
|
329
|
-
# No cap needed, opus is max
|
|
330
|
-
;;
|
|
331
|
-
esac
|
|
332
|
-
fi
|
|
380
|
+
# Architect opt-in (LOKI_FABLE_ARCHITECT) is NOT applied here. It is applied
|
|
381
|
+
# in run.sh, scoped to the FIRST iteration only (the architecture pass), so
|
|
382
|
+
# a session pinned to opus does not silently route every iteration to fable.
|
|
383
|
+
# run.sh sets CURRENT_TIER=fable for that one iteration, which lands on the
|
|
384
|
+
# `fable)` arm above. Keeping the decision in run.sh is the only place that
|
|
385
|
+
# has ITERATION_COUNT, so the scoping is honest.
|
|
386
|
+
|
|
387
|
+
# Apply the shared LOKI_MAX_TIER ceiling (same clamp the run.sh override path
|
|
388
|
+
# uses, so the cost ceiling is enforced byte-identically on both paths).
|
|
389
|
+
model="$(loki_apply_max_tier_clamp "$model" "$tier")"
|
|
333
390
|
|
|
334
391
|
# Phase I (v7.5.25): when ANTHROPIC_BASE_URL is set, the user is routing
|
|
335
392
|
# Claude Code to an alt-provider (OpenRouter, Ollama, LiteLLM, self-hosted).
|
|
@@ -8,11 +8,20 @@
|
|
|
8
8
|
"latest_development": "claude-opus-4-7",
|
|
9
9
|
"latest_fast": "claude-sonnet-4-6",
|
|
10
10
|
"cli_aliases": {
|
|
11
|
+
"fable": "claude-fable-5",
|
|
11
12
|
"opus": "claude-opus-4-7",
|
|
12
13
|
"sonnet": "claude-sonnet-4-6",
|
|
13
14
|
"haiku": "claude-haiku-4-5"
|
|
14
15
|
},
|
|
15
16
|
"models": [
|
|
17
|
+
{
|
|
18
|
+
"id": "claude-fable-5",
|
|
19
|
+
"alias": "fable",
|
|
20
|
+
"tier": "advisor",
|
|
21
|
+
"context_window": 1000000,
|
|
22
|
+
"max_output": 128000,
|
|
23
|
+
"notes": "Top-tier advisory model. Pricing 2x Opus ($10/$50 per MTok). Doc-supported for architecture/root-cause; opt-in only (cost). Refuses cyber content, so NOT used for security review."
|
|
24
|
+
},
|
|
16
25
|
{
|
|
17
26
|
"id": "claude-opus-4-7",
|
|
18
27
|
"alias": "opus",
|