@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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/yotta-dev-mcp/SKILL.md +18 -5
- package/skills/yotta-dev-mcp/references/tools.md +35 -2
- package/skills/yotta-dev-mcp/scripts/dev_common.py +13 -2
- package/skills/yotta-dev-mcp/scripts/dev_engine.py +72 -78
- package/skills/yotta-dev-mcp/scripts/dev_mcp_doctor.py +768 -0
- package/skills/yotta-dev-mcp/scripts/dev_model.py +20 -8
- package/skills/yotta-dev-mcp/scripts/dev_selftest.py +18 -7
- package/skills/yotta-dev-mcp/scripts/yotta_dev_mcp.py +98 -8
- package/skills/yotta-dev-mcp/server.json +2 -2
|
@@ -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
|
|
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({
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
384
|
-
|
|
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
|
|
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
|
-
"
|
|
390
|
-
"
|
|
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,
|
|
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.
|
|
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.
|
|
14
|
+
"version": "0.2.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|