Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4157f26
feat(init): add Netlify MCP server for Claude Code
domitriusclark Oct 20, 2026
51f308a
feat(init): set up MCP for the agents init detects
domitriusclark Oct 20, 2026
0d3de81
feat(init): remember MCP agent choices per user
domitriusclark Oct 20, 2026
433ef8f
test(init): use .cmd fake agent binaries on Windows
domitriusclark Oct 20, 2026
bb3ddc1
fix(init): install skills in new and empty folders
domitriusclark Oct 20, 2026
059e415
fix(init): only write MCP config when a person picks it
domitriusclark Oct 20, 2026
f6a8241
fix(init): re-offer agents that kept another MCP server
domitriusclark Oct 20, 2026
8fc3f35
feat(init): set up the Netlify MCP server per project
domitriusclark Oct 21, 2026
535189f
fix(init): read older claude mcp list output and flag commands left t…
domitriusclark Oct 21, 2026
404f7ab
feat(init): add the MCP server for the agent running init
domitriusclark Oct 21, 2026
8875f87
fix(init): name where Claude's MCP entry is saved
domitriusclark Oct 21, 2026
73eae05
fix(init): never overwrite or go user-wide by default
domitriusclark Oct 21, 2026
b70b3e3
fix(init): keep agent runs from touching saved MCP choices
domitriusclark Oct 21, 2026
b3fe95a
fix(init): address MCP setup review round three
domitriusclark Oct 22, 2026
d7c3ed7
test(init): skip relative-PATH tests when temp is on another drive
domitriusclark Oct 22, 2026
5c7d7c4
fix(init): address MCP setup review round four
domitriusclark Oct 23, 2026
cdde3f9
fix(init): check every Netlify entry at user scope
domitriusclark Oct 26, 2026
49efae0
refactor(init): drop unrequested MCP edge cases
domitriusclark Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/commands/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ netlify init
- `force` (*boolean*) - Reinitialize CI hooks if the linked project is already configured to use CI
- `git-remote-name` (*string*) - Name of Git remote to use. e.g. "origin"
- `manual` (*boolean*) - Manually configure a git remote for CI
- `mcp-scope` (*project | user*) - Add the Netlify MCP server for this project only, or for your user across all projects
- `reset-context` (*boolean*) - Replace locally edited Netlify skills with the latest release, and migrate or delete edited copies under renamed or deprecated names
- `reset-mcp-setup` (*boolean*) - Forget your saved Netlify MCP server choices and ask which AI coding agents to set up
- `skip-mcp-setup` (*boolean*) - Skip adding the Netlify MCP server to your AI coding agents
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
- `skip-agent-setup` (*boolean*) - Skip installing Netlify skills for AI coding agents into the project
- `skip-agent-setup` (*boolean*) - Skip installing Netlify skills and adding the Netlify MCP server for AI coding agents


<!-- AUTO-GENERATED-CONTENT:END -->
23 changes: 21 additions & 2 deletions src/commands/init/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OptionValues } from 'commander'
import { Option, OptionValues } from 'commander'
import terminalLink from 'terminal-link'

import BaseCommand from '../base-command.js'
Expand All @@ -11,7 +11,23 @@ export const createInitCommand = (program: BaseCommand) =>
)
.option('-m, --manual', 'Manually configure a git remote for CI')
.option('--git-remote-name <name>', 'Name of Git remote to use. e.g. "origin"')
.option('--skip-agent-setup', 'Skip installing Netlify skills for AI coding agents into the project')
.option(
'--skip-agent-setup',
'Skip installing Netlify skills and adding the Netlify MCP server for AI coding agents',
)
.option('--skip-mcp-setup', 'Skip adding the Netlify MCP server to your AI coding agents')
.addOption(
new Option(
'--mcp-scope <scope>',
'Add the Netlify MCP server for this project only, or for your user across all projects',
)
.choices(['project', 'user'])
.default('project'),
)
.option(
'--reset-mcp-setup',
'Forget your saved Netlify MCP server choices and ask which AI coding agents to set up',
)
.option(
'--reset-context',
'Replace locally edited Netlify skills with the latest release, and migrate or delete edited copies under renamed or deprecated names',
Expand All @@ -26,6 +42,9 @@ For more information about getting started with Netlify CLI, see ${terminalLink(
const { init } = await import('./init.js')
await init(options, command, {
setupAgentSkills: !options.skipAgentSetup,
setupAgentMcp: !options.skipAgentSetup && !options.skipMcpSetup,
resetMcpSetup: Boolean(options.resetMcpSetup),
mcpScope: options.mcpScope === 'user' ? 'user' : 'project',
resetContext: Boolean(options.resetContext),
})
})
77 changes: 71 additions & 6 deletions src/commands/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type BaseCommand from '../base-command.js'
import { link } from '../link/link.js'
import { sitesCreate } from '../sites/sites-create.js'
import type { LocalState, SiteInfo } from '../../utils/types.js'
import { findProjectSetupBlocker } from '../../utils/init/agent-location.js'
import { type McpScope, setupAgentMcp } from '../../utils/init/agent-mcp.js'
import { setupAgentSkills } from '../../utils/init/agent-skills.js'
import { getBuildSettings, saveNetlifyToml } from '../../utils/init/utils.js'
import { type InitExitCode, LINKED_EXISTING_SITE_EXIT_CODE, LINKED_NEW_SITE_EXIT_CODE } from './constants.js'
Expand Down Expand Up @@ -212,12 +214,14 @@ type InitExtraOptions = {
customizeExitMessage?: InitExitMessageCustomizer | undefined
exitAfterConfiguringRepo?: boolean | undefined
setupAgentSkills?: boolean | undefined
setupAgentMcp?: boolean | undefined
resetContext?: boolean | undefined
resetMcpSetup?: boolean | undefined
mcpScope?: McpScope | undefined
}

const installAgentSkills = async (command: BaseCommand, reset: boolean): Promise<void> => {
log()
const result = await setupAgentSkills({ workingDir: command.netlify.repositoryRoot, reset })
const installAgentSkills = async (workingDir: string, reset: boolean): Promise<void> => {
const result = await setupAgentSkills({ workingDir, reset })
await track('sites_agentSkillsSetup', {
installed: result.installed,
directories: result.directories,
Expand All @@ -227,21 +231,78 @@ const installAgentSkills = async (command: BaseCommand, reset: boolean): Promise
})
}

const addAgentMcp = async (command: BaseCommand, reset: boolean, scope: McpScope): Promise<void> => {
const { repositoryRoot, state } = command.netlify
const {
selection,
agents,
failed = false,
} = await setupAgentMcp({
workingDir: repositoryRoot,
store: state,
scope,
reset,
})
await track('sites_agentMcpSetup', { selection, agents, failed, scope, resetMcpSetup: reset })
}

const setUpAgents = async (
command: BaseCommand,
{
skills,
mcp,
reset,
resetMcp,
mcpScope,
}: { skills: boolean; mcp: boolean; reset: boolean; resetMcp: boolean; mcpScope: McpScope },
): Promise<void> => {
if (!skills && !mcp) {
return
}
const { repositoryRoot } = command.netlify
log()
const blocker = await findProjectSetupBlocker(repositoryRoot)
Comment thread
domitriusclark marked this conversation as resolved.
Comment thread
domitriusclark marked this conversation as resolved.
const skillsBlocked = skills && blocker !== undefined
const mcpBlocked = mcp && blocker !== undefined && (blocker.place === 'root' || mcpScope === 'project')
if (blocker && (skillsBlocked || mcpBlocked)) {
const skipped = [skillsBlocked && 'Netlify skills', mcpBlocked && 'the Netlify MCP server']
.filter(Boolean)
.join(' and ')
log(`Skipped setting up ${skipped}: ${blocker.reason}.`)
log(
` Run ${chalk.cyanBright.bold(`${netlifyCommand()} init`)} from your project folder to set up ${skipped} there.`,
)
await track('sites_agentSetupSkipped', { skills: skillsBlocked, mcp: mcpBlocked })
}
if (skills && !skillsBlocked) {
await installAgentSkills(repositoryRoot, reset)
}
if (mcp && !mcpBlocked) {
await addAgentMcp(command, resetMcp, mcpScope)
}
}

export const init = async (
options: OptionValues,
command: BaseCommand,
{
customizeExitMessage,
exitAfterConfiguringRepo = false,
setupAgentSkills: shouldSetupAgentSkills = false,
setupAgentMcp: shouldSetupAgentMcp = false,
resetContext = false,
resetMcpSetup = false,
mcpScope = 'project',
}: InitExtraOptions = {},
): Promise<SiteInfo> => {
command.setAnalyticsPayload({
manual: options.manual,
force: options.force,
skipAgentSetup: options.skipAgentSetup,
skipMcpSetup: options.skipMcpSetup,
resetContext: options.resetContext,
resetMcpSetup: options.resetMcpSetup,
mcpScope,
})

const { repositoryRoot, state } = command.netlify
Expand All @@ -253,9 +314,13 @@ export const init = async (
// Add .netlify to .gitignore file
await ensureNetlifyIgnore(repositoryRoot)

if (shouldSetupAgentSkills) {
await installAgentSkills(command, resetContext)
}
await setUpAgents(command, {
skills: shouldSetupAgentSkills,
mcp: shouldSetupAgentMcp,
reset: resetContext,
resetMcp: resetMcpSetup,
mcpScope,
})

const repoUrl = getRepoUrl(existingSiteInfo)
if (repoUrl && !options.force) {
Expand Down
30 changes: 30 additions & 0 deletions src/utils/init/agent-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { promises as fs } from 'node:fs'
import { homedir } from 'node:os'
import path from 'node:path'

const realpathOrSelf = async (target: string): Promise<string> => {
try {
return await fs.realpath(target)
} catch {
return path.resolve(target)
}
}

export interface ProjectSetupBlocker {
place: 'home' | 'root'
reason: string
}

export const findProjectSetupBlocker = async (
root: string,
home: string = homedir(),
): Promise<ProjectSetupBlocker | undefined> => {
const resolvedRoot = await realpathOrSelf(root)
if (resolvedRoot === (await realpathOrSelf(home))) {
return { place: 'home', reason: 'this is your home directory, where they would apply to every project' }
}
if (resolvedRoot === path.parse(resolvedRoot).root) {
return { place: 'root', reason: 'this is the root of the file system' }
}
return undefined
}
Loading
Loading