@yottameta/yotta-dev-mcp-plugin 0.2.0 → 0.2.2

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.
@@ -30,9 +30,14 @@ def _resolve_python_import(source_rel, node):
30
30
  if node.level > 1:
31
31
  parts = parts[:-(node.level - 1)] if len(parts) >= node.level - 1 else []
32
32
  prefix = "/".join(parts)
33
+ if not node.module:
34
+ return [
35
+ (prefix + "/" + alias.name.replace(".", "/")).strip("/")
36
+ for alias in node.names
37
+ ]
33
38
  module = node.module or ""
34
39
  target = (prefix + "/" + module.replace(".", "/")).strip("/")
35
- return [target + ".py" if target else source_rel]
40
+ return [target] if target else [source_rel]
36
41
  if node.module:
37
42
  return [node.module]
38
43
  return []
@@ -47,7 +52,12 @@ def _repo_map_python(path, rel):
47
52
  for node in ast.walk(tree):
48
53
  if isinstance(node, (ast.Import, ast.ImportFrom)):
49
54
  for target in _resolve_python_import(rel, node):
50
- imports.append({"source": rel, "target": target, "line": getattr(node, "lineno", 1)})
55
+ imports.append({
56
+ "source": rel,
57
+ "target": target,
58
+ "line": getattr(node, "lineno", 1),
59
+ "relative": isinstance(node, ast.ImportFrom) and bool(node.level),
60
+ })
51
61
  return imports
52
62
 
53
63
  def _repo_map_js(path, rel):
@@ -77,7 +87,7 @@ def _resolve_relative_module(raw_target, source_rel, module_set, root=None):
77
87
  return base, "file"
78
88
  return None, None
79
89
 
80
- def _classify_python_import(raw_target, source_rel, module_set):
90
+ def _classify_python_import(raw_target, source_rel, module_set, relative=False):
81
91
  if raw_target.endswith(".py"):
82
92
  if raw_target in module_set:
83
93
  return "internal", raw_target
@@ -91,7 +101,7 @@ def _classify_python_import(raw_target, source_rel, module_set):
91
101
  for candidate in (base + ".py", base + "/__init__.py"):
92
102
  if candidate in module_set:
93
103
  return "internal", candidate
94
- return "external", raw_target
104
+ return ("unresolved" if relative else "external"), raw_target
95
105
 
96
106
  def _classify_js_import(raw_target, source_rel, module_set, root=None):
97
107
  if raw_target.startswith("."):
@@ -265,19 +275,21 @@ def system_model(path, max_files=2000, contract_file=None):
265
275
  suffix = file_path.suffix.lower()
266
276
  if suffix == ".py":
267
277
  raw_imports = _repo_map_python(file_path, rel)
268
- classifier = lambda raw: _classify_python_import(raw, rel, module_set) # noqa: E731
269
278
  elif suffix in JS_EXTS:
270
279
  raw_imports = _repo_map_js(file_path, rel)
271
- classifier = lambda raw: _classify_js_import(raw, rel, module_set, root) # noqa: E731
272
280
  else:
273
281
  raw_imports = []
274
- classifier = None
275
282
  module_imports = []
276
283
  for raw in raw_imports:
277
284
  target_raw = raw["target"]
278
285
  if target_raw == rel:
279
286
  continue
280
- kind, target = classifier(target_raw)
287
+ if suffix == ".py":
288
+ kind, target = _classify_python_import(
289
+ target_raw, rel, module_set, relative=raw.get("relative", False)
290
+ )
291
+ else:
292
+ kind, target = _classify_js_import(target_raw, rel, module_set, root)
281
293
  entry = {
282
294
  "source": rel,
283
295
  "target": target,
@@ -15,7 +15,8 @@ from pathlib import Path
15
15
 
16
16
  from dev_architecture import architecture_review
17
17
  from dev_common import (
18
- EVIDENCE_LIMIT, VERIFY_REQUIRED_INSTALLED_FILES,
18
+ EVIDENCE_LIMIT, LEAN_INSTALL_MARKERS, VERIFY_REQUIRED_INSTALLED_ASSETS,
19
+ VERIFY_REQUIRED_INSTALLED_FILES,
19
20
  VERIFY_REQUIRED_SOURCE_FILES, VERIFY_WRITE_GATES, _frontmatter_version,
20
21
  _read_text,
21
22
  )
@@ -44,26 +45,30 @@ def _function_default(source_text, function_name, parameter_name):
44
45
  def _string_constant(node):
45
46
  return isinstance(node, ast.Constant) and isinstance(node.value, str)
46
47
 
48
+ def _is_lean_install(root):
49
+ return any((root / marker).is_file() for marker in LEAN_INSTALL_MARKERS)
50
+
47
51
  def _protocol_tool_contracts(source_text):
48
52
  tree = ast.parse(source_text)
49
53
  function = None
50
54
  for node in tree.body:
51
- if isinstance(node, ast.FunctionDef) and node.name == "mcp_tools":
55
+ # v0.2.2 起工具清单字面量在 _all_tools();mcp_tools(profile) 只做分档过滤。
56
+ if isinstance(node, ast.FunctionDef) and node.name in ("_all_tools", "mcp_tools"):
52
57
  function = node
53
58
  break
54
59
  if function is None:
55
- raise ValueError("mcp_tools() not found")
60
+ raise ValueError("tool list function not found")
56
61
  list_node = None
57
62
  for node in ast.walk(function):
58
63
  if isinstance(node, ast.Return) and isinstance(node.value, ast.List):
59
64
  list_node = node.value
60
65
  break
61
66
  if list_node is None:
62
- raise ValueError("mcp_tools() does not return a literal tool list")
67
+ raise ValueError("tool list function does not return a literal tool list")
63
68
  contracts = []
64
69
  for element in list_node.elts:
65
70
  if not isinstance(element, ast.Dict):
66
- raise ValueError("mcp_tools() contains a non-literal tool entry")
71
+ raise ValueError("tool list contains a non-literal tool entry")
67
72
  values = {}
68
73
  for key, value in zip(element.keys, element.values):
69
74
  if _string_constant(key):
@@ -380,8 +385,14 @@ def self_test(path, mode="auto", allow_execute=False, timeout=120):
380
385
  else:
381
386
  mode = "source"
382
387
 
383
- required = (VERIFY_REQUIRED_SOURCE_FILES if mode == "source"
384
- else VERIFY_REQUIRED_INSTALLED_FILES)
388
+ if mode == "source":
389
+ required = list(VERIFY_REQUIRED_SOURCE_FILES)
390
+ else:
391
+ required = list(VERIFY_REQUIRED_INSTALLED_FILES)
392
+ # Lean distribution strips assets/ by platform policy; only require
393
+ # the banner when the copy is not lean or it already ships assets/.
394
+ if not _is_lean_install(root) or (root / "assets").is_dir():
395
+ required.extend(VERIFY_REQUIRED_INSTALLED_ASSETS)
385
396
  missing = [rel for rel in required if not (root / rel).is_file()]
386
397
  files_check = {
387
398
  "id": "files",
@@ -25,6 +25,25 @@ MCP_PROTOCOL_MODERN = "2026-07-28"
25
25
  MCP_PROTOCOL_LEGACY = "2025-11-25"
26
26
  SERVER_INFO = {"name": TOOL_NAME, "version": VERSION}
27
27
 
28
+ # 工具分组(v0.2.2):与元忆 `--tools core|full` 同口径。core 是给「需要在上下文里
29
+ # 常驻工具表」的宿主用小档(5 工具,schema 远低于 8000 字符硬上限);full 保持 18 工具。
30
+ CORE_TOOL_NAMES = (
31
+ "repo_map",
32
+ "find_code",
33
+ "review_code",
34
+ "review_diff",
35
+ "verify_change",
36
+ )
37
+ TOOL_PROFILES = ("core", "full")
38
+ DEFAULT_TOOL_PROFILE = "full"
39
+
40
+
41
+ def normalize_tool_profile(value):
42
+ profile = str(value or "").strip().lower() or DEFAULT_TOOL_PROFILE
43
+ if profile not in TOOL_PROFILES:
44
+ raise ValueError("未知工具分组: %s(可用: core / full)" % value)
45
+ return profile
46
+
28
47
 
29
48
  def _text_result(payload):
30
49
  return {
@@ -63,7 +82,7 @@ TOOL_HANDLERS = {
63
82
  }
64
83
 
65
84
 
66
- def mcp_tools():
85
+ def _all_tools():
67
86
  return [
68
87
  {
69
88
  "name": "repo_map",
@@ -386,14 +405,24 @@ def mcp_tools():
386
405
  {
387
406
  "name": "mcp_doctor",
388
407
  "description": (
389
- "Inspect installed skills and MCP JSON configuration files for "
390
- "versions and obvious configuration issues. Read-only."
408
+ "Broad, read-only health check for installed skills and MCP client "
409
+ "configuration. Discovers Codex, Cursor, WorkBuddy, OpenCode, Claude "
410
+ "Code and other known hosts (plus project-level configs), parses JSON, "
411
+ "JSONC and a narrow TOML subset, and always returns a coverage report "
412
+ "with checked / missing / unsupported / unverified hosts. Only server "
413
+ "names are returned; commands, args and env values are never included. "
414
+ "A clean result applies only to the covered scope: read coverage and "
415
+ "coverage_confidence before treating it as all-clear."
391
416
  ),
392
417
  "inputSchema": {
393
418
  "type": "object",
394
419
  "properties": {
395
420
  "skills_dirs": {"type": "array", "items": {"type": "string"}},
396
421
  "config_paths": {"type": "array", "items": {"type": "string"}},
422
+ "include_defaults": {
423
+ "type": "boolean",
424
+ "description": "Also scan the built-in host registry when config_paths is supplied",
425
+ },
397
426
  },
398
427
  "additionalProperties": False,
399
428
  },
@@ -511,6 +540,15 @@ def mcp_tools():
511
540
  ]
512
541
 
513
542
 
543
+ def mcp_tools(profile=None):
544
+ """按工具分档返回 tools/list 内容(core 5 个 / full 18 个)。"""
545
+ tools = _all_tools()
546
+ if normalize_tool_profile(profile) == "core":
547
+ by_name = {tool["name"]: tool for tool in tools}
548
+ return [by_name[name] for name in CORE_TOOL_NAMES]
549
+ return tools
550
+
551
+
514
552
  def _req_version(params):
515
553
  meta = (params or {}).get("_meta") or {}
516
554
  return meta.get("io.modelcontextprotocol/protocolVersion")
@@ -538,7 +576,7 @@ def _unsupported_version(rid, protocol_version):
538
576
  }
539
577
 
540
578
 
541
- def handle_message(msg):
579
+ def handle_message(msg, tool_profile=None):
542
580
  if not isinstance(msg, dict) or msg.get("jsonrpc") != "2.0":
543
581
  rid = msg.get("id") if isinstance(msg, dict) else None
544
582
  return {"jsonrpc": "2.0", "id": rid,
@@ -568,7 +606,9 @@ def handle_message(msg):
568
606
  "verify_change for change-centered evidence, self_test for integrity "
569
607
  "checks, and run_adapter to probe or explicitly run optional local "
570
608
  "architecture adapters. Tools are offline and read-only unless an "
571
- "explicit write or execute flag is set."
609
+ "explicit write or execute flag is set. Current tool profile: "
610
+ + normalize_tool_profile(tool_profile)
611
+ + " (start with --tools core|full)."
572
612
  ),
573
613
  }, (3600000, "public")),
574
614
  }
@@ -578,11 +618,20 @@ def handle_message(msg):
578
618
  return {
579
619
  "jsonrpc": "2.0",
580
620
  "id": rid,
581
- "result": _modern_ok({"tools": mcp_tools()}, (300000, "public")),
621
+ "result": _modern_ok({"tools": mcp_tools(tool_profile)}, (300000, "public")),
582
622
  }
583
623
  if method == "tools/call":
584
624
  name = params.get("name")
585
625
  arguments = params.get("arguments") or {}
626
+ if (normalize_tool_profile(tool_profile) == "core"
627
+ and name not in CORE_TOOL_NAMES):
628
+ return {
629
+ "jsonrpc": "2.0",
630
+ "id": rid,
631
+ "result": _modern_ok(_tool_error(
632
+ "工具 %s 属于 full 分组,当前以 --tools core 启动。"
633
+ "需要完整工具集时请用 --tools full 重新启动。" % name)),
634
+ }
586
635
  handler = TOOL_HANDLERS.get(name)
587
636
  if not handler:
588
637
  return {
@@ -616,10 +665,16 @@ def handle_message(msg):
616
665
  if method == "ping":
617
666
  return {"jsonrpc": "2.0", "id": rid, "result": {}}
618
667
  if method == "tools/list":
619
- return {"jsonrpc": "2.0", "id": rid, "result": {"tools": mcp_tools()}}
668
+ return {"jsonrpc": "2.0", "id": rid,
669
+ "result": {"tools": mcp_tools(tool_profile)}}
620
670
  if method == "tools/call":
621
671
  name = params.get("name")
622
672
  arguments = params.get("arguments") or {}
673
+ if (normalize_tool_profile(tool_profile) == "core"
674
+ and name not in CORE_TOOL_NAMES):
675
+ return {"jsonrpc": "2.0", "id": rid, "result": _tool_error(
676
+ "工具 %s 属于 full 分组,当前以 --tools core 启动。"
677
+ "需要完整工具集时请用 --tools full 重新启动。" % name)}
623
678
  handler = TOOL_HANDLERS.get(name)
624
679
  if not handler:
625
680
  return {
@@ -632,7 +687,42 @@ def handle_message(msg):
632
687
  "error": {"code": -32601, "message": "Method not found: " + str(method)}}
633
688
 
634
689
 
690
+ def parse_args(argv):
691
+ """解析启动参数:`--tools core|full`(默认 full)、`--version`、`--help`。"""
692
+ profile = DEFAULT_TOOL_PROFILE
693
+ index = 0
694
+ while index < len(argv):
695
+ arg = argv[index]
696
+ if arg == "--tools":
697
+ index += 1
698
+ if index >= len(argv):
699
+ sys.stderr.write("用法错误: --tools 需要一个值(core / full)\n")
700
+ raise SystemExit(2)
701
+ raw = argv[index]
702
+ elif arg.startswith("--tools="):
703
+ raw = arg.split("=", 1)[1]
704
+ elif arg in ("--version", "-v"):
705
+ print("%s %s" % (TOOL_NAME, VERSION))
706
+ raise SystemExit(0)
707
+ elif arg in ("--help", "-h"):
708
+ print("用法: yotta-dev-mcp [--tools core|full]")
709
+ print(" core: 5 工具(%s)" % " / ".join(CORE_TOOL_NAMES))
710
+ print(" full: 18 工具(默认)")
711
+ raise SystemExit(0)
712
+ else:
713
+ sys.stderr.write("用法错误: 未知参数 %s\n" % arg)
714
+ raise SystemExit(2)
715
+ try:
716
+ profile = normalize_tool_profile(raw)
717
+ except ValueError as exc:
718
+ sys.stderr.write("用法错误: %s\n" % exc)
719
+ raise SystemExit(2)
720
+ index += 1
721
+ return profile
722
+
723
+
635
724
  def main():
725
+ tool_profile = parse_args(sys.argv[1:])
636
726
  try:
637
727
  sys.stdin.reconfigure(encoding="utf-8")
638
728
  sys.stdout.reconfigure(encoding="utf-8")
@@ -649,7 +739,7 @@ def main():
649
739
  response = {"jsonrpc": "2.0", "id": None,
650
740
  "error": {"code": -32700, "message": "parse error"}}
651
741
  else:
652
- response = handle_message(message)
742
+ response = handle_message(message, tool_profile)
653
743
  if response is not None:
654
744
  sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
655
745
  sys.stdout.flush()
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/YottaMeta/yotta-dev-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.2.0",
9
+ "version": "0.2.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@yottameta/yotta-dev-mcp",
14
- "version": "0.2.0",
14
+ "version": "0.2.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }