loki-mode 7.28.2 → 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/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.28.2'
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.28.2",
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.