@soulbatical/tetra-dev-toolkit 1.20.9 → 1.20.10

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.
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Code Quality Check: MCP Tool Documentation
3
+ *
4
+ * Ensures all MCP tool definitions have proper descriptions.
5
+ * Scans backend-mcp/src/ for tool definitions and verifies:
6
+ *
7
+ * 1. Every tool has a non-empty `description` field
8
+ * 2. Descriptions are meaningful (>20 chars, not just the tool name)
9
+ * 3. Tool names follow naming convention (snake_case)
10
+ *
11
+ * Severity: high — tools without descriptions are unusable for AI clients
12
+ */
13
+
14
+ import { readdir, readFile, stat } from 'node:fs/promises'
15
+ import { join, relative, extname } from 'node:path'
16
+
17
+ export const meta = {
18
+ id: 'mcp-tool-docs',
19
+ name: 'MCP Tool Documentation',
20
+ category: 'codeQuality',
21
+ severity: 'high',
22
+ description: 'Ensures all MCP tool definitions have meaningful descriptions and follow naming conventions'
23
+ }
24
+
25
+ // ── Patterns ──────────────────────────────────────────
26
+
27
+ // Match tool definition objects: { name: "...", description: "..." }
28
+ // Handles both inline and multi-line definitions
29
+ const TOOL_NAME_RE = /name:\s*["'`]([^"'`]+)["'`]/g
30
+ const TOOL_DESC_RE = /description:\s*["'`]([^"'`]*)["'`]/g
31
+
32
+ // snake_case check
33
+ const SNAKE_CASE_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
34
+
35
+ // ── Scanner ───────────────────────────────────────────
36
+
37
+ async function collectFiles(dir) {
38
+ const files = []
39
+ try {
40
+ const entries = await readdir(dir, { withFileTypes: true })
41
+ for (const entry of entries) {
42
+ const fullPath = join(dir, entry.name)
43
+ if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') {
44
+ files.push(...await collectFiles(fullPath))
45
+ } else if (['.ts', '.tsx', '.js'].includes(extname(entry.name))) {
46
+ files.push(fullPath)
47
+ }
48
+ }
49
+ } catch {
50
+ // Directory doesn't exist
51
+ }
52
+ return files
53
+ }
54
+
55
+ function extractTools(content) {
56
+ const tools = []
57
+
58
+ // Strategy 1: Find name/description pairs in tool definition objects
59
+ // Look for patterns like: { name: "tool_name", description: "..." }
60
+ const nameMatches = [...content.matchAll(TOOL_NAME_RE)]
61
+
62
+ for (const nameMatch of nameMatches) {
63
+ const name = nameMatch[1]
64
+ const namePos = nameMatch.index
65
+
66
+ // Look for a description within 500 chars of the name
67
+ const searchWindow = content.slice(Math.max(0, namePos - 200), namePos + 500)
68
+ const descMatch = searchWindow.match(/description:\s*["'`]([^"'`]*)["'`]/)
69
+
70
+ tools.push({
71
+ name,
72
+ description: descMatch ? descMatch[1] : null,
73
+ line: content.slice(0, namePos).split('\n').length,
74
+ })
75
+ }
76
+
77
+ return tools
78
+ }
79
+
80
+ function validateTool(tool) {
81
+ const violations = []
82
+
83
+ // Check name is snake_case
84
+ if (!SNAKE_CASE_RE.test(tool.name)) {
85
+ violations.push({
86
+ rule: 'tool-naming',
87
+ severity: 'medium',
88
+ message: `Tool "${tool.name}" should use snake_case naming`,
89
+ })
90
+ }
91
+
92
+ // Check description exists
93
+ if (!tool.description) {
94
+ violations.push({
95
+ rule: 'missing-description',
96
+ severity: 'high',
97
+ message: `Tool "${tool.name}" has no description — AI clients won't know what it does`,
98
+ })
99
+ return violations
100
+ }
101
+
102
+ // Check description is meaningful
103
+ if (tool.description.length < 20) {
104
+ violations.push({
105
+ rule: 'short-description',
106
+ severity: 'high',
107
+ message: `Tool "${tool.name}" description is too short (${tool.description.length} chars, min 20) — "${tool.description}"`,
108
+ })
109
+ }
110
+
111
+ // Check description isn't just the tool name
112
+ if (tool.description.toLowerCase().replace(/[_\s-]/g, '') === tool.name.toLowerCase().replace(/[_\s-]/g, '')) {
113
+ violations.push({
114
+ rule: 'lazy-description',
115
+ severity: 'high',
116
+ message: `Tool "${tool.name}" description just repeats the name — write what it does`,
117
+ })
118
+ }
119
+
120
+ return violations
121
+ }
122
+
123
+ // ── Main check ────────────────────────────────────────
124
+
125
+ export async function run(config, projectRoot) {
126
+ const result = {
127
+ passed: true,
128
+ findings: [],
129
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
130
+ details: { files: {}, totalTools: 0, totalViolations: 0, toolsWithoutDesc: 0 }
131
+ }
132
+
133
+ // Find MCP tool directories
134
+ const mcpDirs = [
135
+ join(projectRoot, 'backend-mcp', 'src', 'tools'),
136
+ join(projectRoot, 'backend-mcp', 'src'),
137
+ join(projectRoot, 'mcp', 'src', 'tools'),
138
+ join(projectRoot, 'src', 'tools'),
139
+ ]
140
+
141
+ let allFiles = []
142
+ for (const dir of mcpDirs) {
143
+ try {
144
+ await stat(dir)
145
+ allFiles.push(...await collectFiles(dir))
146
+ } catch {
147
+ // Directory doesn't exist, skip
148
+ }
149
+ }
150
+
151
+ if (allFiles.length === 0) return result // Not an MCP project
152
+
153
+ for (const filePath of allFiles) {
154
+ const content = await readFile(filePath, 'utf8')
155
+ const tools = extractTools(content)
156
+
157
+ if (tools.length === 0) continue
158
+
159
+ result.details.totalTools += tools.length
160
+ const fileViolations = []
161
+
162
+ for (const tool of tools) {
163
+ const violations = validateTool(tool)
164
+ if (violations.length > 0) {
165
+ fileViolations.push(...violations.map(v => ({ ...v, tool: tool.name, line: tool.line })))
166
+ if (violations.some(v => v.rule === 'missing-description')) {
167
+ result.details.toolsWithoutDesc++
168
+ }
169
+ }
170
+ }
171
+
172
+ if (fileViolations.length > 0) {
173
+ const rel = relative(projectRoot, filePath)
174
+ result.details.files[rel] = fileViolations
175
+ result.details.totalViolations += fileViolations.length
176
+
177
+ for (const v of fileViolations) {
178
+ result.summary[v.severity] = (result.summary[v.severity] || 0) + 1
179
+ result.summary.total++
180
+ }
181
+
182
+ result.findings.push({
183
+ type: `MCP tool docs in ${rel}`,
184
+ severity: fileViolations.some(v => v.severity === 'high') ? 'high' : 'medium',
185
+ message: `${fileViolations.length} issue(s): ${[...new Set(fileViolations.map(v => v.rule))].join(', ')}`,
186
+ files: fileViolations.map(v => `L${v.line}: [${v.rule}] ${v.message}`)
187
+ })
188
+ }
189
+ }
190
+
191
+ result.passed = result.summary.high === 0 && result.summary.critical === 0
192
+ return result
193
+ }
@@ -22,6 +22,7 @@ export * as rpcGeneratorOrigin from './supabase/rpc-generator-origin.js'
22
22
  export * as uiTheming from './codeQuality/ui-theming.js'
23
23
  export * as barrelImportDetector from './codeQuality/barrel-import-detector.js'
24
24
  export * as typescriptStrictness from './codeQuality/typescript-strictness.js'
25
+ export * as mcpToolDocs from './codeQuality/mcp-tool-docs.js'
25
26
 
26
27
  // Health checks (project ecosystem)
27
28
  export * as health from './health/index.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.20.9",
3
+ "version": "1.20.10",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },