Skip to content

Commit 3c4445c

Browse files
feat: generate rules files when configuring the MCP (#46)
* generate rules files when configuring the MCP * change format to markdown for copilot and windsurf * fix formatting issues * avoid duplicating rules & create rules on init * add provider, org and repository to rules from the git info from the extension * replace existing codacy rules * fix tool name * only add to gitignore if the rules file doesn't exist already * Update src/commands/configureMCP.ts --------- Co-authored-by: Alejandro Rizzo <alejandro.rizzo@codacy.com>
1 parent 1815946 commit 3c4445c

File tree

2 files changed

+179
-1
lines changed

2 files changed

+179
-1
lines changed

src/commands/configureMCP.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,179 @@ import * as path from 'path'
44
import * as os from 'os'
55
import { Config } from '../common/config'
66
import { get, set } from 'lodash'
7+
import { parseGitRemote } from '../common/parseGitRemote'
8+
9+
interface Rule {
10+
when: string
11+
enforce: string[]
12+
}
13+
14+
interface RuleConfig {
15+
name: string
16+
description: string
17+
rules: Rule[]
18+
}
19+
20+
const newRulesTemplate = (provider?: string, organization?: string, repository?: string): RuleConfig => ({
21+
name: 'Codacy Rules',
22+
description: 'Configuration for AI behavior when interacting with Codacy',
23+
rules: [
24+
{
25+
when: 'using any tool that has either one of the following arguments: provider, organization or repository',
26+
enforce: [
27+
`use ${provider} for provider, ${organization} for organization and ${repository} for repository, and avoid calling 'git remote -v' unless really necessary`,
28+
],
29+
},
30+
{
31+
when: 'after ANY successful edit_file or reapply tool call',
32+
enforce: [
33+
'IMMEDIATELY run codacy_cli_analyze tool with:',
34+
'rootPath set to the workspace path',
35+
'file set to the edited file path',
36+
'If issues are found, propose fixes for them',
37+
],
38+
},
39+
],
40+
})
41+
// Function to parse the json content inside an MDC file. An MDC file contains text elements that would generate errors when parsed as JSON
42+
const parseMdcContent = (content: string): RuleConfig => {
43+
const parts = content.split('---')
44+
45+
if (parts.length < 3) {
46+
throw new Error('Invalid MDC file format: missing frontmatter')
47+
}
48+
49+
const jsonContent = parts[2].trim()
50+
51+
try {
52+
return JSON.parse(jsonContent)
53+
} catch (error) {
54+
throw new Error('Invalid JSON content in MDC file')
55+
}
56+
}
57+
58+
const convertRulesToMarkdown = (rules: RuleConfig, existingContent?: string): string => {
59+
const codacyRules: string = existingContent?.split('---').filter((part) => part.includes(rules.name))[0] || ''
60+
const newCodacyRules = `---\n# ${rules.name}\n${rules.description}\n${rules.rules
61+
.map((rule) => `## When ${rule.when}\n${rule.enforce.join('\n - ')}`)
62+
.join('\n\n')}\n---`
63+
return existingContent ? existingContent?.replace(`---${codacyRules}---`, newCodacyRules) : newCodacyRules
64+
}
65+
66+
const rulesPrefixForMdc = `---
67+
description:
68+
globs:
69+
alwaysApply: true
70+
---
71+
\n`
72+
73+
function getCorrectRulesInfo(): { path: string; format: string } {
74+
const ideInfo = getCurrentIDE()
75+
// Get the workspace folder path
76+
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
77+
if (!workspacePath) {
78+
throw new Error('No workspace folder found')
79+
}
80+
if (ideInfo === 'cursor') {
81+
return { path: path.join(workspacePath, '.cursor', 'rules', 'codacy.mdc'), format: 'mdc' }
82+
}
83+
if (ideInfo === 'windsurf') {
84+
return { path: path.join(workspacePath, '.windsurfrules'), format: 'md' }
85+
}
86+
return { path: path.join(workspacePath, '.github', 'copilot-instructions.md'), format: 'md' }
87+
}
88+
89+
const addRulesToGitignore = (rulesPath: string) => {
90+
const currentIDE = getCurrentIDE()
91+
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''
92+
const gitignorePath = path.join(workspacePath, '.gitignore')
93+
const relativeRulesPath = path.relative(workspacePath, rulesPath)
94+
const gitignoreContent = `\n\n#Ignore ${currentIDE} AI rules\n${relativeRulesPath}\n`
95+
let existingGitignore = ''
96+
97+
if (fs.existsSync(gitignorePath)) {
98+
existingGitignore = fs.readFileSync(gitignorePath, 'utf8')
99+
100+
if (!existingGitignore.split('\n').some((line) => line.trim() === relativeRulesPath.trim())) {
101+
fs.appendFileSync(gitignorePath, gitignoreContent)
102+
vscode.window.showInformationMessage(`Added ${relativeRulesPath} to .gitignore`)
103+
}
104+
} else {
105+
fs.writeFileSync(gitignorePath, gitignoreContent)
106+
vscode.window.showInformationMessage('Created .gitignore and added rules path')
107+
}
108+
}
109+
export async function createRules() {
110+
// Get git info
111+
const git = vscode.extensions.getExtension('vscode.git')?.exports.getAPI(1)
112+
const repo = git?.repositories[0]
113+
let provider, organization, repository
114+
115+
if (repo?.state.remotes[0]?.pushUrl) {
116+
const gitInfo = parseGitRemote(repo.state.remotes[0].pushUrl)
117+
provider = gitInfo.provider
118+
organization = gitInfo.organization
119+
repository = gitInfo.repository
120+
}
121+
122+
const newRules = newRulesTemplate(provider, organization, repository)
123+
124+
try {
125+
const { path: rulesPath, format } = getCorrectRulesInfo()
126+
const isMdc = format === 'mdc'
127+
const dirPath = path.dirname(rulesPath)
128+
129+
// Create directories if they don't exist
130+
if (!fs.existsSync(dirPath)) {
131+
fs.mkdirSync(dirPath, { recursive: true })
132+
}
133+
134+
if (!fs.existsSync(rulesPath)) {
135+
fs.writeFileSync(
136+
rulesPath,
137+
`${isMdc ? rulesPrefixForMdc : ''}${
138+
isMdc ? JSON.stringify(newRules, null, 2) : convertRulesToMarkdown(newRules)
139+
}`
140+
)
141+
vscode.window.showInformationMessage(`Created new rules file at ${rulesPath}`)
142+
addRulesToGitignore(rulesPath)
143+
} else {
144+
try {
145+
const existingContent = fs.readFileSync(rulesPath, 'utf8')
146+
147+
if (isMdc) {
148+
const existingRules = parseMdcContent(existingContent)
149+
const mergedRules = {
150+
...existingRules,
151+
rules: [
152+
...(existingRules.rules || []),
153+
...newRules.rules.filter(
154+
(newRule) =>
155+
!existingRules.rules?.some(
156+
(existingRule: Rule) =>
157+
existingRule.when === newRule.when &&
158+
existingRule.enforce.every((e) => newRule.enforce.includes(e))
159+
)
160+
),
161+
],
162+
}
163+
fs.writeFileSync(rulesPath, `${rulesPrefixForMdc}${JSON.stringify(mergedRules, null, 2)}`)
164+
} else {
165+
fs.writeFileSync(rulesPath, convertRulesToMarkdown(newRules, existingContent))
166+
}
167+
168+
vscode.window.showInformationMessage(`Updated rules in ${rulesPath}`)
169+
} catch (parseError) {
170+
vscode.window.showWarningMessage(`Error parsing existing rules file. Creating new one.`)
171+
fs.writeFileSync(rulesPath, JSON.stringify(newRules, null, 2))
172+
}
173+
}
174+
} catch (error: unknown) {
175+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
176+
vscode.window.showErrorMessage(`Failed to create rules: ${errorMessage}`)
177+
throw error
178+
}
179+
}
7180

8181
function getCurrentIDE(): string {
9182
const isCursor = vscode.env.appName.toLowerCase().includes('cursor')
@@ -105,6 +278,7 @@ export async function configureMCP() {
105278
fs.writeFileSync(filePath, JSON.stringify(modifiedConfig, null, 2))
106279

107280
vscode.window.showInformationMessage('Codacy MCP server added successfully')
281+
await createRules()
108282
} catch (error: unknown) {
109283
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
110284
vscode.window.showErrorMessage(`Failed to configure MCP server: ${errorMessage}`)

src/extension.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { Account } from './codacy/Account'
1717
import Telemetry from './common/telemetry'
1818
import { decorateWithCoverage } from './views/coverage'
1919
import { APIState, Repository as GitRepository } from './git/git'
20-
import { configureMCP, isMCPConfigured } from './commands/configureMCP'
20+
import { configureMCP, createRules, isMCPConfigured } from './commands/configureMCP'
2121

2222
/**
2323
* Helper function to register all extension commands
@@ -204,6 +204,10 @@ export async function activate(context: vscode.ExtensionContext) {
204204
updateMCPState()
205205
})
206206
)
207+
208+
if (isMCPConfigured()) {
209+
await createRules()
210+
}
207211
}
208212

209213
// This method is called when your extension is deactivated

0 commit comments

Comments
 (0)