|
| 1 | +import { z } from 'zod'; |
| 2 | +import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js'; |
| 3 | +import { analyticsToolCallEventName } from '../utils/tokens.js'; |
| 4 | +import { sendAnalytics, handleError } from '../utils/analyticsUtils.js'; |
| 5 | +import { hiBladeToolName } from './hiBlade.js'; |
| 6 | +import { createNewBladeProjectToolName } from './createNewBladeProject.js'; |
| 7 | +import { createBladeCursorRulesToolName } from './createBladeCursorRules.js'; |
| 8 | +import { getBladeComponentDocsToolName } from './getBladeComponentDocs.js'; |
| 9 | +import { getBladePatternDocsToolName } from './getBladePatternDocs.js'; |
| 10 | +import { getBladeGeneralDocsToolName } from './getBladeGeneralDocs.js'; |
| 11 | +import { getFigmaToCodeToolName } from './getFigmaToCode.js'; |
| 12 | +import { getChangelogToolName } from './getChangelog.js'; |
| 13 | + |
| 14 | +// List of Blade MCP tool names excluding this metric tool |
| 15 | +const bladeMcpToolNames = [ |
| 16 | + hiBladeToolName, |
| 17 | + createNewBladeProjectToolName, |
| 18 | + createBladeCursorRulesToolName, |
| 19 | + getBladeComponentDocsToolName, |
| 20 | + getBladePatternDocsToolName, |
| 21 | + getBladeGeneralDocsToolName, |
| 22 | + getFigmaToCodeToolName, |
| 23 | + getChangelogToolName, |
| 24 | +] as const; |
| 25 | + |
| 26 | +type BladeMcpToolName = typeof bladeMcpToolNames[number]; |
| 27 | +// Cast to mutable tuple type expected by z.enum |
| 28 | +const bladeMcpToolEnum = z.enum( |
| 29 | + (bladeMcpToolNames as unknown) as [BladeMcpToolName, ...BladeMcpToolName[]], |
| 30 | +); |
| 31 | + |
| 32 | +// Tool constants |
| 33 | +const publishLinesOfCodeMetricToolName = 'publish_lines_of_code_metric'; |
| 34 | +// Detailed description covering how to derive numbers from Git diff statistics. |
| 35 | +const publishLinesOfCodeMetricToolDescription = ` |
| 36 | +Report the **diff size** of the most recent code edit. |
| 37 | +
|
| 38 | +The goal is to capture **net code-churn**—how many changes were added and how many were removed—so that the Blade team can track adoption and productivity. Use the same convention that \`git diff --numstat\` outputs: |
| 39 | +
|
| 40 | +* **A new line** → counts as **1 _added_** line. |
| 41 | +* **A deleted line** → counts as **1 _removed_** line. |
| 42 | +* **A modified line** (content changed in place) → counts as **1 removed** **and** **1 added** (because the old content vanished and new content appeared). |
| 43 | +
|
| 44 | +Keep these numbers **exactly** as your diff tool reports them—do **not** try to compensate for whitespace or formatting tweaks. |
| 45 | +
|
| 46 | +──────────────────────────────────────────────── |
| 47 | +# When to Call |
| 48 | +
|
| 49 | +After completing all code edits, and **just before** drafting your final summary to the user, call the publish_lines_of_code_metric tool **exactly once**. |
| 50 | +
|
| 51 | +──────────────────────────────────────────────── |
| 52 | +# Example Invocation |
| 53 | +
|
| 54 | +\`\`\`json |
| 55 | +{ |
| 56 | + "name": "publish_lines_of_code_metric", |
| 57 | + "arguments": { |
| 58 | + "files": [ |
| 59 | + { "filePath": "src/components/Button.tsx", "linesAdded": 10, "linesRemoved": 2 }, |
| 60 | + { "filePath": "src/utils/helpers.ts", "linesAdded": 3, "linesRemoved": 1 } |
| 61 | + ], |
| 62 | + "linesAddedTotal": 13, |
| 63 | + "linesRemovedTotal": 3, |
| 64 | +
|
| 65 | + // Aggregated optional buckets |
| 66 | + "bladeUiLinesAddedTotal": 10, |
| 67 | + "bladeUiLinesRemovedTotal": 2, |
| 68 | + "nonBladeUiLinesAddedTotal": 3, |
| 69 | + "nonBladeUiLinesRemovedTotal": 1, |
| 70 | + "nonUiLinesAddedTotal": 0, |
| 71 | + "nonUiLinesRemovedTotal": 0, |
| 72 | +
|
| 73 | + "currentProjectRootDirectory": "/Users/alice/projects/my-app", |
| 74 | + "toolsUsed": ["get_blade_component_docs", "get_blade_pattern_docs"] |
| 75 | + } |
| 76 | +} |
| 77 | +\`\`\` |
| 78 | +
|
| 79 | +All numeric fields **must be non-negative integers**. |
| 80 | +`; |
| 81 | + |
| 82 | +// Tool schema |
| 83 | +const publishLinesOfCodeMetricToolSchema = { |
| 84 | + files: z |
| 85 | + .array( |
| 86 | + z.object({ |
| 87 | + filePath: z.string().describe('Path of the file that was edited.'), |
| 88 | + linesAdded: z.number().int().nonnegative().describe('Lines added in this file.'), |
| 89 | + linesRemoved: z.number().int().nonnegative().describe('Lines removed in this file.'), |
| 90 | + }), |
| 91 | + ) |
| 92 | + .nonempty() |
| 93 | + .describe('Breakdown of line changes per file in this operation.'), |
| 94 | + linesAddedTotal: z.number().int().nonnegative().describe('Total lines added across all files.'), |
| 95 | + linesRemovedTotal: z |
| 96 | + .number() |
| 97 | + .int() |
| 98 | + .nonnegative() |
| 99 | + .describe('Total lines removed across all files.'), |
| 100 | + toolsUsed: z |
| 101 | + .array(bladeMcpToolEnum) |
| 102 | + .optional() |
| 103 | + .describe('List of Blade MCP tool names that were invoked during the conversation'), |
| 104 | + currentProjectRootDirectory: z |
| 105 | + .string() |
| 106 | + .describe( |
| 107 | + "The working root directory of the consumer's project. Do not use root directory, do not use '.', only use absolute path to current directory", |
| 108 | + ), |
| 109 | + // New aggregated metrics for code categorization |
| 110 | + bladeUiLinesAddedTotal: z |
| 111 | + .number() |
| 112 | + .int() |
| 113 | + .nonnegative() |
| 114 | + .optional() |
| 115 | + .describe( |
| 116 | + 'Total lines of UI code that import or reference Blade components (e.g., <Button />, <TextInput />) that were added across all files.', |
| 117 | + ), |
| 118 | + bladeUiLinesRemovedTotal: z |
| 119 | + .number() |
| 120 | + .int() |
| 121 | + .nonnegative() |
| 122 | + .optional() |
| 123 | + .describe( |
| 124 | + 'Total lines of UI code that import or reference Blade components that were removed across all files.', |
| 125 | + ), |
| 126 | + nonBladeUiLinesAddedTotal: z |
| 127 | + .number() |
| 128 | + .int() |
| 129 | + .nonnegative() |
| 130 | + .optional() |
| 131 | + .describe( |
| 132 | + 'Total lines of UI component code (React/JSX/TSX) added that do NOT import or use Blade components — e.g., custom components or components from other libraries such as Material UI', |
| 133 | + ), |
| 134 | + nonBladeUiLinesRemovedTotal: z |
| 135 | + .number() |
| 136 | + .int() |
| 137 | + .nonnegative() |
| 138 | + .optional() |
| 139 | + .describe( |
| 140 | + 'Total lines of UI component code that does NOT use Blade components that were removed across all files.', |
| 141 | + ), |
| 142 | + nonUiLinesAddedTotal: z |
| 143 | + .number() |
| 144 | + .int() |
| 145 | + .nonnegative() |
| 146 | + .optional() |
| 147 | + .describe( |
| 148 | + 'Total lines of non-UI code such as business logic, state management, data fetching, utility functions, etc. that were added.', |
| 149 | + ), |
| 150 | + nonUiLinesRemovedTotal: z |
| 151 | + .number() |
| 152 | + .int() |
| 153 | + .nonnegative() |
| 154 | + .optional() |
| 155 | + .describe( |
| 156 | + 'Total lines of non-UI code such as business logic, state management, data fetching, utility functions, etc. that were removed.', |
| 157 | + ), |
| 158 | +}; |
| 159 | + |
| 160 | +// Tool callback |
| 161 | +const publishLinesOfCodeMetricToolCallback: ToolCallback< |
| 162 | + typeof publishLinesOfCodeMetricToolSchema |
| 163 | +> = ({ |
| 164 | + files, |
| 165 | + linesAddedTotal, |
| 166 | + linesRemovedTotal, |
| 167 | + toolsUsed, |
| 168 | + currentProjectRootDirectory, |
| 169 | + bladeUiLinesAddedTotal, |
| 170 | + bladeUiLinesRemovedTotal, |
| 171 | + nonBladeUiLinesAddedTotal, |
| 172 | + nonBladeUiLinesRemovedTotal, |
| 173 | + nonUiLinesAddedTotal, |
| 174 | + nonUiLinesRemovedTotal, |
| 175 | +}) => { |
| 176 | + try { |
| 177 | + // Send analytics event |
| 178 | + const flattenedFiles = files |
| 179 | + .map(({ filePath, linesAdded, linesRemoved }) => `${filePath}:${linesAdded}:${linesRemoved}`) |
| 180 | + .join(','); |
| 181 | + |
| 182 | + sendAnalytics({ |
| 183 | + eventName: analyticsToolCallEventName, |
| 184 | + properties: { |
| 185 | + toolName: publishLinesOfCodeMetricToolName, |
| 186 | + linesAddedTotal, |
| 187 | + linesRemovedTotal, |
| 188 | + bladeUiLinesAddedTotal: bladeUiLinesAddedTotal ?? 0, |
| 189 | + bladeUiLinesRemovedTotal: bladeUiLinesRemovedTotal ?? 0, |
| 190 | + nonBladeUiLinesAddedTotal: nonBladeUiLinesAddedTotal ?? 0, |
| 191 | + nonBladeUiLinesRemovedTotal: nonBladeUiLinesRemovedTotal ?? 0, |
| 192 | + nonUiLinesAddedTotal: nonUiLinesAddedTotal ?? 0, |
| 193 | + nonUiLinesRemovedTotal: nonUiLinesRemovedTotal ?? 0, |
| 194 | + files: flattenedFiles, |
| 195 | + toolsUsed: (toolsUsed ?? []).join(','), |
| 196 | + rootDirectoryName: currentProjectRootDirectory.split('/').pop(), |
| 197 | + }, |
| 198 | + }); |
| 199 | + |
| 200 | + return { |
| 201 | + content: [ |
| 202 | + { |
| 203 | + type: 'text', |
| 204 | + text: |
| 205 | + `Recorded ${linesAddedTotal} lines added and ${linesRemovedTotal} lines removed across ` + |
| 206 | + `${files.length} files. Tools used: ${(toolsUsed ?? []).join(', ')}.`, |
| 207 | + }, |
| 208 | + ], |
| 209 | + }; |
| 210 | + } catch (error: unknown) { |
| 211 | + return handleError({ |
| 212 | + toolName: publishLinesOfCodeMetricToolName, |
| 213 | + errorObject: error, |
| 214 | + }); |
| 215 | + } |
| 216 | +}; |
| 217 | + |
| 218 | +export { |
| 219 | + publishLinesOfCodeMetricToolName, |
| 220 | + publishLinesOfCodeMetricToolDescription, |
| 221 | + publishLinesOfCodeMetricToolSchema, |
| 222 | + publishLinesOfCodeMetricToolCallback, |
| 223 | +}; |
0 commit comments