Skip to content

Commit 88e49e8

Browse files
authored
Merge pull request #39 from codacy/mcp-server-setup-CY-7423
feature: Setup codacy MCP server CY-7423
2 parents 2f2bd39 + e35a73d commit 88e49e8

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed

package.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@
3939
"icon": "resources/icons/codacy-logo.svg",
4040
"when": "Codacy:RepositoryManagerStateContext == NeedsAuthentication"
4141
},
42+
{
43+
"id": "codacy:mcp",
44+
"name": "Codacy MCP Server",
45+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor",
46+
"icon": "$(gear)",
47+
"initialSize": 2
48+
},
4249
{
4350
"id": "codacy:prSummary",
4451
"name": "Pull request",
@@ -75,6 +82,16 @@
7582
"contents": "You have not yet signed in with Codacy\n[Sign in](command:codacy.signIn)",
7683
"when": "Codacy:RepositoryManagerStateContext == NeedsAuthentication"
7784
},
85+
{
86+
"view": "codacy:mcp",
87+
"contents": "Enable your Cursor AI Chat to talk to Codacy's Cloud API \n[Add Codacy MCP Server](command:codacy.configureMCP)",
88+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor && !codacy:mcpConfigured"
89+
},
90+
{
91+
"view": "codacy:mcp",
92+
"contents": "MCP Server is enabled\n[Reset MCP Server](command:codacy.configureMCP.reset)",
93+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor && codacy:mcpConfigured"
94+
},
7895
{
7996
"view": "codacy:statuses",
8097
"contents": "No repositories open.",
@@ -171,6 +188,26 @@
171188
"command": "codacy.issue.seeDetails",
172189
"title": "See issue details",
173190
"category": "Codacy commands"
191+
},
192+
{
193+
"command": "codacy.configureMCP",
194+
"title": "Configure Codacy MCP Server",
195+
"category": "Codacy commands",
196+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor"
197+
},
198+
{
199+
"command": "codacy.configureMCP.reset",
200+
"title": "Reset Codacy MCP Server",
201+
"category": "Codacy commands",
202+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor && codacy:mcpConfigured"
203+
},
204+
{
205+
"command": "codacy.configureMCP",
206+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor && !codacy:mcpConfigured"
207+
},
208+
{
209+
"command": "codacy.configureMCP.reset",
210+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor && codacy:mcpConfigured"
174211
}
175212
],
176213
"menus": {
@@ -186,6 +223,14 @@
186223
{
187224
"command": "codacy.pr.checkout",
188225
"when": "false"
226+
},
227+
{
228+
"command": "codacy.configureMCP",
229+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor && !codacy:mcpConfigured"
230+
},
231+
{
232+
"command": "codacy.configureMCP.reset",
233+
"when": "Codacy:RepositoryManagerStateContext != NeedsAuthentication && codacy:isCursor && codacy:mcpConfigured"
189234
}
190235
],
191236
"view/title": [

src/commands/configureMCP.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as vscode from 'vscode'
2+
import * as fs from 'fs'
3+
import * as path from 'path'
4+
import * as os from 'os'
5+
6+
export function isMCPConfigured(): boolean {
7+
try {
8+
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json')
9+
if (!fs.existsSync(mcpPath)) {
10+
return false
11+
}
12+
13+
const config = JSON.parse(fs.readFileSync(mcpPath, 'utf8'))
14+
return config?.mcpServers?.codacy !== undefined
15+
} catch (error) {
16+
// If there's any error reading or parsing the file, assume it's not configured
17+
return false
18+
}
19+
}
20+
21+
export async function configureMCP() {
22+
try {
23+
// Get the Codacy API token from extension settings
24+
const config = vscode.workspace.getConfiguration('codacy')
25+
const apiToken = config.get<string>('apiToken')
26+
27+
if (!apiToken) {
28+
throw new Error('Codacy API token not found in settings')
29+
}
30+
31+
// Create .cursor directory if it doesn't exist
32+
const cursorDir = path.join(os.homedir(), '.cursor')
33+
if (!fs.existsSync(cursorDir)) {
34+
fs.mkdirSync(cursorDir)
35+
}
36+
37+
const mcpPath = path.join(cursorDir, 'mcp.json')
38+
39+
// Prepare the Codacy server configuration
40+
const codacyServer = {
41+
command: 'npx',
42+
args: ['-y', '@codacy/codacy-mcp@latest'],
43+
env: {
44+
CODACY_ACCOUNT_TOKEN: apiToken,
45+
},
46+
}
47+
48+
// Read existing configuration if it exists
49+
let mcpConfig = { mcpServers: {} }
50+
if (fs.existsSync(mcpPath)) {
51+
try {
52+
const existingConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8'))
53+
mcpConfig = {
54+
mcpServers: {
55+
...(existingConfig.mcpServers || {}),
56+
codacy: codacyServer,
57+
},
58+
}
59+
} catch (parseError) {
60+
// If the existing file is invalid JSON, we'll create a new one
61+
mcpConfig = {
62+
mcpServers: {
63+
codacy: codacyServer,
64+
},
65+
}
66+
}
67+
} else {
68+
mcpConfig = {
69+
mcpServers: {
70+
codacy: codacyServer,
71+
},
72+
}
73+
}
74+
75+
fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2))
76+
77+
vscode.window.showInformationMessage('Codacy MCP server added successfully')
78+
} catch (error: unknown) {
79+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
80+
vscode.window.showErrorMessage(`Failed to configure MCP server: ${errorMessage}`)
81+
}
82+
}

src/extension.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +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'
2021

2122
/**
2223
* Helper function to register all extension commands
@@ -87,6 +88,13 @@ export async function activate(context: vscode.ExtensionContext) {
8788
Logger.appendLine('Codacy extension activated')
8889
context.subscriptions.push(Logger)
8990

91+
// Set initial context values
92+
await vscode.commands.executeCommand(
93+
'setContext',
94+
'codacy:isCursor',
95+
vscode.env.appName.toLowerCase().includes('cursor')
96+
)
97+
9098
Config.init(context)
9199

92100
initializeApi()
@@ -170,6 +178,31 @@ export async function activate(context: vscode.ExtensionContext) {
170178
vscode.commands.registerCommand('codacy.pr.toggleCoverage', (item: { onClick: () => void }) => {
171179
item.onClick()
172180
})
181+
182+
// Register MCP commands
183+
const updateMCPState = () => {
184+
const isConfigured = isMCPConfigured()
185+
vscode.commands.executeCommand('setContext', 'codacy:mcpConfigured', isConfigured)
186+
}
187+
188+
// Update initially
189+
updateMCPState()
190+
191+
// Register configure command
192+
context.subscriptions.push(
193+
vscode.commands.registerCommand('codacy.configureMCP', async () => {
194+
await configureMCP()
195+
updateMCPState()
196+
})
197+
)
198+
199+
// Register reset command
200+
context.subscriptions.push(
201+
vscode.commands.registerCommand('codacy.configureMCP.reset', async () => {
202+
await configureMCP()
203+
updateMCPState()
204+
})
205+
)
173206
}
174207

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

src/test/mocks/MockExtensionContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class MockExtensionContext implements ExtensionContext {
4141
extensionRuntime: any
4242
extension: any
4343
isNewInstall: any
44+
languageModelAccessInformation: any
4445

4546
constructor() {
4647
this.extensionPath = path.resolve(__dirname, '..')

0 commit comments

Comments
 (0)