mcplint-cli 0.1.0__py3-none-any.whl

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.
Files changed (71) hide show
  1. mcplint/__about__.py +1 -0
  2. mcplint/__init__.py +3 -0
  3. mcplint/benchmark/__init__.py +0 -0
  4. mcplint/benchmark/dataset.py +34 -0
  5. mcplint/benchmark/providers/__init__.py +0 -0
  6. mcplint/benchmark/providers/anthropic_provider.py +92 -0
  7. mcplint/benchmark/providers/base.py +26 -0
  8. mcplint/benchmark/providers/factory.py +51 -0
  9. mcplint/benchmark/providers/fake.py +26 -0
  10. mcplint/benchmark/providers/openai_provider.py +25 -0
  11. mcplint/benchmark/runner.py +43 -0
  12. mcplint/benchmark/scorer.py +147 -0
  13. mcplint/cli/__init__.py +0 -0
  14. mcplint/cli/commands/__init__.py +0 -0
  15. mcplint/cli/commands/benchmark_cmd.py +88 -0
  16. mcplint/cli/commands/compare_cmd.py +139 -0
  17. mcplint/cli/commands/fix_cmd.py +60 -0
  18. mcplint/cli/commands/inspect_cmd.py +44 -0
  19. mcplint/cli/commands/rules_cmd.py +30 -0
  20. mcplint/cli/commands/scan_cmd.py +113 -0
  21. mcplint/cli/commands/snapshot_cmd.py +33 -0
  22. mcplint/cli/main.py +49 -0
  23. mcplint/compare/__init__.py +0 -0
  24. mcplint/compare/differ.py +148 -0
  25. mcplint/config/__init__.py +0 -0
  26. mcplint/config/loader.py +36 -0
  27. mcplint/config/schema.py +40 -0
  28. mcplint/core/__init__.py +0 -0
  29. mcplint/core/engine.py +40 -0
  30. mcplint/core/registry.py +52 -0
  31. mcplint/core/rules/__init__.py +0 -0
  32. mcplint/core/rules/ambiguity.py +157 -0
  33. mcplint/core/rules/ambiguity_rules.py +109 -0
  34. mcplint/core/rules/base.py +39 -0
  35. mcplint/core/rules/builtin.py +45 -0
  36. mcplint/core/rules/completeness_rules.py +217 -0
  37. mcplint/core/rules/description_rules.py +110 -0
  38. mcplint/core/rules/safety_rules.py +139 -0
  39. mcplint/core/rules/schema_rules.py +101 -0
  40. mcplint/core/score.py +132 -0
  41. mcplint/fix/__init__.py +0 -0
  42. mcplint/fix/suggest.py +183 -0
  43. mcplint/mcp_client/__init__.py +0 -0
  44. mcplint/mcp_client/canonical.py +25 -0
  45. mcplint/mcp_client/persistence.py +17 -0
  46. mcplint/mcp_client/session.py +80 -0
  47. mcplint/mcp_client/stdio.py +17 -0
  48. mcplint/models/__init__.py +0 -0
  49. mcplint/models/benchmark.py +67 -0
  50. mcplint/models/common.py +23 -0
  51. mcplint/models/comparison.py +53 -0
  52. mcplint/models/contracts.py +39 -0
  53. mcplint/models/findings.py +44 -0
  54. mcplint/models/fixes.py +13 -0
  55. mcplint/models/score.py +25 -0
  56. mcplint/models/snapshot.py +27 -0
  57. mcplint/py.typed +0 -0
  58. mcplint/reporters/__init__.py +0 -0
  59. mcplint/reporters/benchmark_terminal.py +46 -0
  60. mcplint/reporters/comparison_terminal.py +61 -0
  61. mcplint/reporters/fix_markdown.py +34 -0
  62. mcplint/reporters/html.py +71 -0
  63. mcplint/reporters/json_reporter.py +9 -0
  64. mcplint/reporters/sarif.py +77 -0
  65. mcplint/reporters/templates/report.html.j2 +176 -0
  66. mcplint/reporters/terminal.py +52 -0
  67. mcplint_cli-0.1.0.dist-info/METADATA +392 -0
  68. mcplint_cli-0.1.0.dist-info/RECORD +71 -0
  69. mcplint_cli-0.1.0.dist-info/WHEEL +4 -0
  70. mcplint_cli-0.1.0.dist-info/entry_points.txt +2 -0
  71. mcplint_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,40 @@
1
+ """Typed schema for mcplint.yaml."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from mcplint.core.rules.ambiguity import DEFAULT_AMBIGUITY_THRESHOLD
8
+ from mcplint.core.rules.completeness_rules import DEFAULT_MAX_DESCRIPTION_CHARACTERS
9
+ from mcplint.models.findings import Severity
10
+
11
+
12
+ class ThresholdsConfig(BaseModel):
13
+ ambiguity: float = Field(default=DEFAULT_AMBIGUITY_THRESHOLD, ge=0.0, le=1.0)
14
+ max_description_characters: int = Field(default=DEFAULT_MAX_DESCRIPTION_CHARACTERS, gt=0)
15
+
16
+
17
+ class IgnoreEntry(BaseModel):
18
+ tool: str
19
+ rules: list[str] = Field(default_factory=list)
20
+
21
+
22
+ class BenchmarkConfig(BaseModel):
23
+ provider: str | None = None
24
+ model: str | None = None
25
+ runs: int = Field(default=3, ge=1)
26
+
27
+
28
+ class MCPLintConfig(BaseModel):
29
+ severity: dict[str, Severity] = Field(default_factory=dict)
30
+ thresholds: ThresholdsConfig = Field(default_factory=ThresholdsConfig)
31
+ ignore: list[IgnoreEntry] = Field(default_factory=list)
32
+ benchmark: BenchmarkConfig | None = None
33
+
34
+ def ignored_rule_ids_for_tool(self, tool_name: str, all_rule_ids: set[str]) -> set[str]:
35
+ ignored: set[str] = set()
36
+ for entry in self.ignore:
37
+ if entry.tool != tool_name:
38
+ continue
39
+ ignored |= set(entry.rules) if entry.rules else all_rule_ids
40
+ return ignored
File without changes
mcplint/core/engine.py ADDED
@@ -0,0 +1,40 @@
1
+ """Pure function that runs every registered rule over every tool in a snapshot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from mcplint.core.registry import RuleRegistry
8
+ from mcplint.core.rules.base import RuleContext
9
+ from mcplint.models.common import ArtifactMetadata
10
+ from mcplint.models.findings import Finding, LintReport
11
+ from mcplint.models.snapshot import MCPServerSnapshot
12
+
13
+ if TYPE_CHECKING:
14
+ from mcplint.config.schema import MCPLintConfig
15
+
16
+ REPORT_SCHEMA_VERSION = "1.0"
17
+
18
+
19
+ def lint_snapshot(
20
+ snapshot: MCPServerSnapshot,
21
+ registry: RuleRegistry,
22
+ config: MCPLintConfig | None = None,
23
+ ) -> LintReport:
24
+ context = RuleContext(snapshot=snapshot)
25
+ all_rule_ids = {rule.id for rule in registry.all()}
26
+ findings: list[Finding] = []
27
+ for tool in snapshot.tools:
28
+ ignored = config.ignored_rule_ids_for_tool(tool.name, all_rule_ids) if config else set()
29
+ for rule in registry.all():
30
+ if rule.id in ignored:
31
+ continue
32
+ for finding in rule.check(tool, context):
33
+ if config and rule.id in config.severity:
34
+ finding = finding.model_copy(update={"severity": config.severity[rule.id]})
35
+ findings.append(finding)
36
+ return LintReport(
37
+ metadata=ArtifactMetadata.create(schema_version=REPORT_SCHEMA_VERSION),
38
+ server_name=snapshot.server_name,
39
+ findings=findings,
40
+ )
@@ -0,0 +1,52 @@
1
+ """Collects built-in and plugin rules for the lint engine to run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import entry_points
6
+ from typing import TYPE_CHECKING
7
+
8
+ from mcplint.core.rules.base import Rule
9
+
10
+ if TYPE_CHECKING:
11
+ from mcplint.config.schema import MCPLintConfig
12
+
13
+
14
+ class RuleRegistry:
15
+ def __init__(self) -> None:
16
+ self._rules: dict[str, Rule] = {}
17
+
18
+ def register(self, rule: Rule) -> None:
19
+ if rule.id in self._rules:
20
+ raise ValueError(f"Rule '{rule.id}' is already registered")
21
+ self._rules[rule.id] = rule
22
+
23
+ def get(self, rule_id: str) -> Rule | None:
24
+ return self._rules.get(rule_id)
25
+
26
+ def all(self) -> list[Rule]:
27
+ return [self._rules[key] for key in sorted(self._rules)]
28
+
29
+ def load_entry_point_plugins(self) -> None:
30
+ for entry_point in entry_points(group="mcplint.rules"):
31
+ rule_cls = entry_point.load()
32
+ self.register(rule_cls())
33
+
34
+ @classmethod
35
+ def with_builtin_rules(cls, config: MCPLintConfig | None = None) -> RuleRegistry:
36
+ from mcplint.core.rules.ambiguity_rules import (
37
+ AmbiguousToolOverlapRule,
38
+ MissingToolDistinctionRule,
39
+ )
40
+ from mcplint.core.rules.builtin import BUILTIN_RULES
41
+ from mcplint.core.rules.completeness_rules import ExcessiveDescriptionLengthRule
42
+
43
+ registry = cls()
44
+ for rule_cls in BUILTIN_RULES:
45
+ rule = rule_cls()
46
+ if config is not None:
47
+ if isinstance(rule, AmbiguousToolOverlapRule | MissingToolDistinctionRule):
48
+ rule.threshold = config.thresholds.ambiguity
49
+ elif isinstance(rule, ExcessiveDescriptionLengthRule):
50
+ rule.max_characters = config.thresholds.max_description_characters
51
+ registry.register(rule)
52
+ return registry
File without changes
@@ -0,0 +1,157 @@
1
+ """Cross-tool semantic ambiguity engine.
2
+
3
+ Computes a 0-1 ambiguity score between every pair of tools from normalised
4
+ token overlap, name similarity, description similarity, and input-schema
5
+ (parameter name) similarity, plus optional sentence-transformer embeddings
6
+ when the `semantic` extra is installed. Every flagged pair carries structured
7
+ evidence (shared verbs/entities/parameters, absent distinctions) so the
8
+ result is inspectable rather than an opaque score.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from dataclasses import dataclass, field
15
+
16
+ from mcplint.core.rules.safety_rules import READ_VERBS, WRITE_VERBS, first_word
17
+ from mcplint.models.contracts import ToolContract
18
+
19
+ DEFAULT_AMBIGUITY_THRESHOLD = 0.55
20
+
21
+ _STOPWORDS = frozenset(
22
+ {
23
+ "a",
24
+ "an",
25
+ "the",
26
+ "of",
27
+ "for",
28
+ "to",
29
+ "and",
30
+ "or",
31
+ "in",
32
+ "on",
33
+ "by",
34
+ "with",
35
+ "is",
36
+ "are",
37
+ "this",
38
+ "that",
39
+ "it",
40
+ "its",
41
+ "your",
42
+ "you",
43
+ "from",
44
+ "as",
45
+ "at",
46
+ "be",
47
+ "will",
48
+ "if",
49
+ "not",
50
+ "can",
51
+ "does",
52
+ "do",
53
+ }
54
+ )
55
+ _MANY_HINTS = frozenset({"list", "search", "find", "query"})
56
+ _ONE_HINTS = frozenset({"get", "fetch", "show", "view"})
57
+
58
+
59
+ def _tokens(text: str, *, drop_stopwords: bool) -> set[str]:
60
+ cleaned = re.sub(r"[^a-z0-9\s]", " ", text.lower())
61
+ words = [w for w in cleaned.split() if w]
62
+ if drop_stopwords:
63
+ words = [w for w in words if w not in _STOPWORDS]
64
+ return set(words)
65
+
66
+
67
+ def _jaccard(a: set[str], b: set[str]) -> float:
68
+ if not a and not b:
69
+ return 0.0
70
+ union = a | b
71
+ if not union:
72
+ return 0.0
73
+ return len(a & b) / len(union)
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class AmbiguityEvidence:
78
+ shared_verbs: tuple[str, ...] = ()
79
+ shared_entities: tuple[str, ...] = ()
80
+ overlapping_parameters: tuple[str, ...] = ()
81
+ absent_exact_vs_search_distinction: bool = False
82
+ absent_one_vs_many_distinction: bool = False
83
+ absent_read_vs_write_distinction: bool = False
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class AmbiguityPairResult:
88
+ tool_a: str
89
+ tool_b: str
90
+ score: float
91
+ name_similarity: float
92
+ description_similarity: float
93
+ schema_similarity: float
94
+ evidence: AmbiguityEvidence = field(default_factory=AmbiguityEvidence)
95
+
96
+ def has_missing_distinction(self) -> bool:
97
+ return (
98
+ self.evidence.absent_exact_vs_search_distinction
99
+ or self.evidence.absent_one_vs_many_distinction
100
+ or self.evidence.absent_read_vs_write_distinction
101
+ )
102
+
103
+
104
+ def compute_ambiguity(tool_a: ToolContract, tool_b: ToolContract) -> AmbiguityPairResult:
105
+ name_tokens_a = _tokens(tool_a.name.replace("_", " ").replace("-", " "), drop_stopwords=False)
106
+ name_tokens_b = _tokens(tool_b.name.replace("_", " ").replace("-", " "), drop_stopwords=False)
107
+ name_similarity = _jaccard(name_tokens_a, name_tokens_b)
108
+
109
+ desc_tokens_a = _tokens(tool_a.description or "", drop_stopwords=True)
110
+ desc_tokens_b = _tokens(tool_b.description or "", drop_stopwords=True)
111
+ description_similarity = _jaccard(desc_tokens_a, desc_tokens_b)
112
+
113
+ schema_similarity = _jaccard(tool_a.parameter_names(), tool_b.parameter_names())
114
+
115
+ score = 0.25 * name_similarity + 0.45 * description_similarity + 0.30 * schema_similarity
116
+
117
+ verb_a, verb_b = first_word(tool_a.name), first_word(tool_b.name)
118
+ action_verbs = READ_VERBS | WRITE_VERBS
119
+ shared_verbs = tuple(sorted({verb_a} & {verb_b} & action_verbs))
120
+ shared_entities = tuple(sorted((name_tokens_a & name_tokens_b) - action_verbs))
121
+ overlapping_parameters = tuple(sorted(tool_a.parameter_names() & tool_b.parameter_names()))
122
+
123
+ combined_description = f"{tool_a.description or ''} {tool_b.description or ''}".lower()
124
+
125
+ absent_exact_vs_search = (
126
+ {verb_a, verb_b} & _ONE_HINTS
127
+ and {verb_a, verb_b} & _MANY_HINTS
128
+ and "exact" not in combined_description
129
+ )
130
+ one_vs_many_keywords = ("single", "one or more", "list of", "multiple")
131
+ absent_one_vs_many = (
132
+ bool({verb_a, verb_b} & _ONE_HINTS)
133
+ and bool({verb_a, verb_b} & _MANY_HINTS)
134
+ and not any(kw in combined_description for kw in one_vs_many_keywords)
135
+ )
136
+ read_only_a = bool(tool_a.annotations.read_only_hint)
137
+ read_only_b = bool(tool_b.annotations.read_only_hint)
138
+ absent_read_vs_write = read_only_a != read_only_b and "read-only" not in combined_description
139
+
140
+ evidence = AmbiguityEvidence(
141
+ shared_verbs=shared_verbs,
142
+ shared_entities=shared_entities,
143
+ overlapping_parameters=overlapping_parameters,
144
+ absent_exact_vs_search_distinction=bool(absent_exact_vs_search),
145
+ absent_one_vs_many_distinction=bool(absent_one_vs_many),
146
+ absent_read_vs_write_distinction=absent_read_vs_write,
147
+ )
148
+
149
+ return AmbiguityPairResult(
150
+ tool_a=tool_a.name,
151
+ tool_b=tool_b.name,
152
+ score=round(score, 4),
153
+ name_similarity=round(name_similarity, 4),
154
+ description_similarity=round(description_similarity, 4),
155
+ schema_similarity=round(schema_similarity, 4),
156
+ evidence=evidence,
157
+ )
@@ -0,0 +1,109 @@
1
+ """Rules built on top of the cross-tool ambiguity engine (`ambiguity.py`).
2
+
3
+ Both rules iterate every *other* tool in the snapshot but only emit a
4
+ finding once per unordered pair (when `tool.name < other.name`), since the
5
+ lint engine calls `check()` once per tool and pairs would otherwise be
6
+ reported twice.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from mcplint.core.rules.ambiguity import DEFAULT_AMBIGUITY_THRESHOLD, compute_ambiguity
12
+ from mcplint.core.rules.base import Rule, RuleContext
13
+ from mcplint.models.contracts import SourceLocation, ToolContract
14
+ from mcplint.models.findings import Finding, Severity
15
+
16
+
17
+ class AmbiguousToolOverlapRule(Rule):
18
+ id = "ambiguous-tool-overlap"
19
+ title = "Ambiguous tool overlap"
20
+ description = (
21
+ "Flags pairs of tools whose name, description, and parameters overlap "
22
+ "enough that an agent could plausibly pick either one for the same task."
23
+ )
24
+ default_severity = Severity.WARNING
25
+ tags = ("ambiguity",)
26
+ threshold = DEFAULT_AMBIGUITY_THRESHOLD
27
+
28
+ def check(self, tool: ToolContract, context: RuleContext) -> list[Finding]:
29
+ findings = []
30
+ for other in context.snapshot.tools:
31
+ if other.name <= tool.name:
32
+ continue
33
+ result = compute_ambiguity(tool, other)
34
+ if result.score < self.threshold:
35
+ continue
36
+ evidence_parts = [
37
+ f"ambiguity score {result.score:.2f} (threshold {self.threshold:.2f})"
38
+ ]
39
+ if result.evidence.shared_verbs:
40
+ evidence_parts.append(f"shared verbs: {', '.join(result.evidence.shared_verbs)}")
41
+ if result.evidence.shared_entities:
42
+ evidence_parts.append(
43
+ f"shared entities: {', '.join(result.evidence.shared_entities)}"
44
+ )
45
+ if result.evidence.overlapping_parameters:
46
+ evidence_parts.append(
47
+ f"overlapping parameters: {', '.join(result.evidence.overlapping_parameters)}"
48
+ )
49
+ findings.append(
50
+ Finding(
51
+ rule_id=self.id,
52
+ severity=self.default_severity,
53
+ message=f"Tools '{tool.name}' and '{other.name}' are semantically ambiguous.",
54
+ evidence="; ".join(evidence_parts),
55
+ location=SourceLocation(tool_name=tool.name, json_path="$.description"),
56
+ remediation=(
57
+ f"Clarify how '{tool.name}' differs from '{other.name}': when an "
58
+ "agent should pick one over the other."
59
+ ),
60
+ confidence=min(0.9, 0.4 + result.score / 2),
61
+ )
62
+ )
63
+ return findings
64
+
65
+
66
+ class MissingToolDistinctionRule(Rule):
67
+ id = "missing-tool-distinction"
68
+ title = "Missing tool distinction"
69
+ description = (
70
+ "For ambiguous tool pairs, flags the absence of an explicit "
71
+ "exact-vs-search, one-vs-many, or read-vs-write distinction."
72
+ )
73
+ default_severity = Severity.INFO
74
+ tags = ("ambiguity",)
75
+ threshold = DEFAULT_AMBIGUITY_THRESHOLD
76
+
77
+ def check(self, tool: ToolContract, context: RuleContext) -> list[Finding]:
78
+ findings = []
79
+ for other in context.snapshot.tools:
80
+ if other.name <= tool.name:
81
+ continue
82
+ result = compute_ambiguity(tool, other)
83
+ if result.score < self.threshold or not result.has_missing_distinction():
84
+ continue
85
+ missing = []
86
+ if result.evidence.absent_exact_vs_search_distinction:
87
+ missing.append("exact-vs-search")
88
+ if result.evidence.absent_one_vs_many_distinction:
89
+ missing.append("one-vs-many")
90
+ if result.evidence.absent_read_vs_write_distinction:
91
+ missing.append("read-vs-write")
92
+ findings.append(
93
+ Finding(
94
+ rule_id=self.id,
95
+ severity=self.default_severity,
96
+ message=(
97
+ f"Tools '{tool.name}' and '{other.name}' don't state a "
98
+ f"{'/'.join(missing)} distinction."
99
+ ),
100
+ evidence=f"missing distinction(s): {', '.join(missing)}",
101
+ location=SourceLocation(tool_name=tool.name, json_path="$.description"),
102
+ remediation=(
103
+ f'Add a sentence like "Use {tool.name} when ... Use {other.name} '
104
+ f'when ..." to both descriptions.'
105
+ ),
106
+ confidence=0.5,
107
+ )
108
+ )
109
+ return findings
@@ -0,0 +1,39 @@
1
+ """The Rule contract every deterministic and plugin rule implements."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import ClassVar
7
+
8
+ from pydantic import BaseModel, ConfigDict
9
+
10
+ from mcplint.models.contracts import ToolContract
11
+ from mcplint.models.findings import Finding, RuleMetadata, Severity
12
+ from mcplint.models.snapshot import MCPServerSnapshot
13
+
14
+
15
+ class RuleContext(BaseModel):
16
+ model_config = ConfigDict(frozen=True)
17
+
18
+ snapshot: MCPServerSnapshot
19
+
20
+
21
+ class Rule(ABC):
22
+ id: ClassVar[str]
23
+ title: ClassVar[str]
24
+ description: ClassVar[str]
25
+ default_severity: ClassVar[Severity]
26
+ tags: ClassVar[tuple[str, ...]] = ()
27
+
28
+ @abstractmethod
29
+ def check(self, tool: ToolContract, context: RuleContext) -> list[Finding]: ...
30
+
31
+ @classmethod
32
+ def metadata(cls) -> RuleMetadata:
33
+ return RuleMetadata(
34
+ id=cls.id,
35
+ title=cls.title,
36
+ description=cls.description,
37
+ default_severity=cls.default_severity,
38
+ tags=list(cls.tags),
39
+ )
@@ -0,0 +1,45 @@
1
+ """Registry of all built-in rule classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from mcplint.core.rules.ambiguity_rules import AmbiguousToolOverlapRule, MissingToolDistinctionRule
6
+ from mcplint.core.rules.base import Rule
7
+ from mcplint.core.rules.completeness_rules import (
8
+ ExcessiveDescriptionLengthRule,
9
+ MissingReturnSemanticsRule,
10
+ UndefinedDomainTermRule,
11
+ UndocumentedErrorBehaviourRule,
12
+ UndocumentedRequiredConstraintRule,
13
+ )
14
+ from mcplint.core.rules.description_rules import (
15
+ DescriptionRepeatsNameRule,
16
+ MissingToolDescriptionRule,
17
+ VagueToolDescriptionRule,
18
+ )
19
+ from mcplint.core.rules.safety_rules import (
20
+ DestructiveToolWithoutWarningRule,
21
+ StateChangingToolMarkedReadOnlyRule,
22
+ ToolNameActionConflictRule,
23
+ )
24
+ from mcplint.core.rules.schema_rules import (
25
+ MissingParameterDescriptionRule,
26
+ SchemaDescriptionTypeConflictRule,
27
+ )
28
+
29
+ BUILTIN_RULES: list[type[Rule]] = [
30
+ MissingToolDescriptionRule,
31
+ DescriptionRepeatsNameRule,
32
+ VagueToolDescriptionRule,
33
+ MissingParameterDescriptionRule,
34
+ MissingReturnSemanticsRule,
35
+ UndocumentedErrorBehaviourRule,
36
+ UndocumentedRequiredConstraintRule,
37
+ SchemaDescriptionTypeConflictRule,
38
+ ToolNameActionConflictRule,
39
+ DestructiveToolWithoutWarningRule,
40
+ StateChangingToolMarkedReadOnlyRule,
41
+ AmbiguousToolOverlapRule,
42
+ MissingToolDistinctionRule,
43
+ ExcessiveDescriptionLengthRule,
44
+ UndefinedDomainTermRule,
45
+ ]