11import { resolve } from 'node:path'
22import { promises as fs } from 'node:fs'
3- import { homedir } from 'node:os'
43import type { NetlifyAPI } from '@netlify/api'
54
65import { chalk , log , logAndThrowError , type APIError } from '../../utils/command-helpers.js'
76import { normalizeRepoUrl } from '../../utils/normalize-repo-url.js'
87import { runGit } from '../../utils/run-git.js'
98import { startSpinner } from '../../lib/spinner.js'
10- import { getContextConsumers , type ConsumerConfig } from '../../recipes/ai-context/context.js'
11- import execa from '../../utils/execa.js'
12- import { version } from '../../utils/command-helpers.js'
9+ import { detectIDE } from '../../recipes/ai-context/index.js'
10+ import { type ConsumerConfig } from '../../recipes/ai-context/context.js'
11+ import {
12+ generateMcpConfig ,
13+ configureMcpForVSCode ,
14+ configureMcpForCursor ,
15+ configureMcpForWindsurf ,
16+ showGenericMcpConfig
17+ } from '../../utils/mcp-utils.js'
1318import type BaseCommand from '../base-command.js'
1419import type { SiteInfo } from '../../utils/types.js'
1520import inquirer from 'inquirer'
1621
22+ /**
23+ * Project information interface for AI projects
24+ */
1725interface ProjectInfo {
1826 success : boolean
1927 projectId : string
2028 prompt : string
2129}
2230
23- // Check if a command belongs to a known IDE (reusing ai-context logic)
24- const getConsumerKeyFromCommand = ( command : string , consumers : ConsumerConfig [ ] ) : string | null => {
25- const match = consumers . find (
26- ( consumer ) => consumer . consumerProcessCmd && command . includes ( consumer . consumerProcessCmd ) ,
27- )
28- return match ? match . key : null
29- }
30-
31- // Get command and parent PID (same logic as ai-context)
32- const getCommandAndParentPID = async (
33- pid : number ,
34- ) : Promise < {
35- parentPID : number
36- command : string
37- consumerKey : string | null
38- } > => {
39- const { stdout } = await execa ( 'ps' , [ '-p' , String ( pid ) , '-o' , 'ppid=,comm=' ] )
40- const output = stdout . trim ( )
41- const spaceIndex = output . indexOf ( ' ' )
42- const parentPID = output . substring ( 0 , spaceIndex )
43- const command = output . substring ( spaceIndex + 1 ) . toLowerCase ( )
44-
45- const consumers = await getContextConsumers ( version ) // Use current CLI version
46-
47- return {
48- parentPID : Number ( parentPID ) ,
49- command,
50- consumerKey : getConsumerKeyFromCommand ( command , consumers ) ,
51- }
52- }
53-
54- // Detect IDE by walking up process tree (same logic as ai-context)
55- const detectIDE = async ( ) : Promise < ConsumerConfig | null > => {
56- if ( process . env . AI_CONTEXT_SKIP_DETECTION === 'true' ) {
57- return null
58- }
59-
60- const ppid = process . ppid
61- let result : Awaited < ReturnType < typeof getCommandAndParentPID > >
62- try {
63- result = await getCommandAndParentPID ( ppid )
64- while ( result . parentPID !== 1 && ! result . consumerKey ) {
65- result = await getCommandAndParentPID ( result . parentPID )
66- }
67- } catch {
68- // Process detection failed
69- return null
70- }
71-
72- if ( result . consumerKey ) {
73- const consumers = await getContextConsumers ( version )
74- const contextConsumer = consumers . find ( ( consumer ) => consumer . key === result . consumerKey )
75- if ( contextConsumer ) {
76- return contextConsumer
77- }
78- }
79-
80- return null
81- }
82-
83- // Generate MCP configuration for the detected IDE
84- const generateMcpConfig = ( ide : ConsumerConfig ) : Record < string , unknown > => {
85- const configs : Record < string , Record < string , unknown > > = {
86- vscode : {
87- servers : {
88- netlify : {
89- type : 'stdio' ,
90- command : 'npx' ,
91- args : [ '-y' , '@netlify/mcp' ] ,
92- } ,
93- } ,
94- } ,
95- cursor : {
96- mcpServers : {
97- netlify : {
98- command : 'npx' ,
99- args : [ '-y' , '@netlify/mcp' ] ,
100- } ,
101- } ,
102- } ,
103- windsurf : {
104- mcpServers : {
105- netlify : {
106- command : 'npx' ,
107- args : [ '-y' , '@netlify/mcp' ] ,
108- } ,
109- } ,
110- } ,
111- }
112-
113- return (
114- configs [ ide . key ] ?? {
115- mcpServers : {
116- netlify : {
117- command : 'npx' ,
118- args : [ '-y' , '@netlify/mcp' ] ,
119- } ,
120- } ,
121- }
122- )
123- }
124-
125- // VS Code specific MCP configuration
126- const configureMcpForVSCode = async ( config : Record < string , unknown > , projectPath : string ) : Promise < void > => {
127- const vscodeDirPath = resolve ( projectPath , '.vscode' )
128- const configPath = resolve ( vscodeDirPath , 'mcp.json' )
129-
130- try {
131- // Create .vscode directory if it doesn't exist
132- await fs . mkdir ( vscodeDirPath , { recursive : true } )
133-
134- // Write or update mcp.json
135- let existingConfig : Record < string , unknown > = { }
136- try {
137- const existingContent = await fs . readFile ( configPath , 'utf-8' )
138- existingConfig = JSON . parse ( existingContent ) as Record < string , unknown >
139- } catch {
140- // File doesn't exist or is invalid JSON
141- }
142-
143- const updatedConfig = { ...existingConfig , ...config }
144-
145- await fs . writeFile ( configPath , JSON . stringify ( updatedConfig , null , 2 ) , 'utf-8' )
146- log ( `${ chalk . green ( '✅' ) } VS Code MCP configuration saved to ${ chalk . cyan ( '.vscode/mcp.json' ) } ` )
147- } catch ( error ) {
148- throw new Error ( `Failed to configure VS Code MCP: ${ error instanceof Error ? error . message : 'Unknown error' } ` )
149- }
150- }
151-
152- // Cursor specific MCP configuration
153- const configureMcpForCursor = async ( config : Record < string , unknown > , projectPath : string ) : Promise < void > => {
154- const configPath = resolve ( projectPath , '.cursor' , 'mcp.json' )
155-
156- try {
157- await fs . mkdir ( resolve ( projectPath , '.cursor' ) , { recursive : true } )
158- await fs . writeFile ( configPath , JSON . stringify ( config , null , 2 ) , 'utf-8' )
159- log ( `${ chalk . green ( '✅' ) } Cursor MCP configuration saved to ${ chalk . cyan ( '.cursor/mcp.json' ) } ` )
160- } catch ( error ) {
161- throw new Error ( `Failed to configure Cursor MCP: ${ error instanceof Error ? error . message : 'Unknown error' } ` )
162- }
163- }
164-
165- // Windsurf specific MCP configuration
166- const configureMcpForWindsurf = async ( config : Record < string , unknown > , _projectPath : string ) : Promise < void > => {
167- const windsurfDirPath = resolve ( homedir ( ) , '.codeium' , 'windsurf' )
168- const configPath = resolve ( windsurfDirPath , 'mcp_config.json' )
169-
170- try {
171- // Create .codeium/windsurf directory if it doesn't exist
172- await fs . mkdir ( windsurfDirPath , { recursive : true } )
173-
174- // Read existing config or create new one
175- let existingConfig : Record < string , unknown > = { }
176- try {
177- const existingContent = await fs . readFile ( configPath , 'utf-8' )
178- existingConfig = JSON . parse ( existingContent ) as Record < string , unknown >
179- } catch {
180- // File doesn't exist or is invalid JSON
181- }
182-
183- // Merge mcpServers from both configs
184- const existingServers = ( existingConfig . mcpServers as Record < string , unknown > | undefined ) ?? { }
185- const newServers = ( config . mcpServers as Record < string , unknown > | undefined ) ?? { }
186-
187- const updatedConfig = {
188- ...existingConfig ,
189- mcpServers : {
190- ...existingServers ,
191- ...newServers ,
192- } ,
193- }
194-
195- await fs . writeFile ( configPath , JSON . stringify ( updatedConfig , null , 2 ) , 'utf-8' )
196- log ( `${ chalk . green ( '✅' ) } Windsurf MCP configuration saved` )
197- log ( `${ chalk . gray ( '💡' ) } Restart Windsurf to activate the MCP server` )
198- } catch ( error ) {
199- throw new Error ( `Failed to configure Windsurf MCP: ${ error instanceof Error ? error . message : 'Unknown error' } ` )
200- }
201- }
202-
203- // Generic MCP configuration display
204- const showGenericMcpConfig = ( config : Record < string , unknown > , ideName : string ) : void => {
205- log ( `\n${ chalk . yellow ( '📋 Manual configuration required' ) } ` )
206- log ( `Please add the following configuration to your ${ ideName } settings:` )
207- log ( `\n${ chalk . gray ( '--- Configuration ---' ) } ` )
208- log ( JSON . stringify ( config , null , 2 ) )
209- log ( `${ chalk . gray ( '--- End Configuration ---' ) } \n` )
210- }
211-
21231// Trigger IDE-specific MCP configuration
21332const triggerMcpConfiguration = async ( ide : ConsumerConfig , projectPath : string ) : Promise < boolean > => {
21433 log ( `\n${ chalk . blue ( '🔧 MCP Configuration for' ) } ${ chalk . cyan ( ide . presentedName ) } ` )
@@ -272,9 +91,6 @@ const fetchProjectInfo = async (url: string): Promise<ProjectInfo> => {
27291 } ,
27392 } )
27493
275- if ( ! response . ok ) {
276- throw new Error ( `Failed to fetch project information: ${ response . statusText } ` )
277- }
27894 const data = ( await response . text ( ) ) as unknown as string
27995 const parsedData = JSON . parse ( data ) as unknown as ProjectInfo
28096 return parsedData
@@ -286,7 +102,7 @@ const fetchProjectInfo = async (url: string): Promise<ProjectInfo> => {
286102const getRepoUrlFromProjectId = async ( api : NetlifyAPI , projectId : string ) : Promise < string > => {
287103 try {
288104 const siteInfo = ( await api . getSite ( { siteId : projectId } ) ) as SiteInfo
289- const repoUrl = SiteInfo . build_settings ?. repo_url
105+ const repoUrl = siteInfo . build_settings ?. repo_url
290106
291107 if ( ! repoUrl ) {
292108 throw new Error ( `No repository URL found for project ID: ${ projectId } ` )
@@ -410,7 +226,9 @@ export const initWithAiRules = async (hash: string, command: BaseCommand): Promi
410226 } else {
411227 log ( chalk . yellowBright ( `🔧 Step 2: Manual MCP Configuration` ) )
412228 log ( ` ${ chalk . cyan ( detectedIDE . key ) } detected - MCP setup was skipped` )
413- log ( ` ${ chalk . gray ( 'You can configure MCP manually later for enhanced AI capabilities' ) } ` )
229+ log ( ` ${ chalk . gray ( 'You can configure MCP manually later for enhanced AI capabilities:' ) } ` )
230+ log ( ` ${ chalk . gray ( 'Documentation:' ) } ${ chalk . cyan ( 'https://docs.netlify.com/welcome/build-with-ai/netlify-mcp-server/' ) } ` )
231+
414232 }
415233 log ( )
416234 }
0 commit comments