Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 5 additions & 0 deletions .changeset/smooth-bats-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

feat(cli): add new add-on `mcp` to configure your project
1 change: 1 addition & 0 deletions documentation/docs/20-commands/20-sv-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ You can select multiple space-separated add-ons from [the list below](#Official-
- [`drizzle`](drizzle)
- [`eslint`](eslint)
- [`lucia`](lucia)
- [`mcp`](mcp)
- [`mdsvex`](mdsvex)
- [`paraglide`](paraglide)
- [`playwright`](playwright)
Expand Down
33 changes: 33 additions & 0 deletions documentation/docs/30-add-ons/17-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: mcp
---

[Svelte MCP](/docs/mcp/overview) can help your LLM write better Svelte code.

## Usage

```sh
npx sv add mcp
```

## What you get

- A good mcp configuration for your project depending on your IDE

## Options

### ide

The IDE you want to use like `'claude-code'`, `'cursor'`, `'gemini'`, `'opencode'`, `'vscode'`, `'other'`.

```sh
npx sv add mcp=ide:cursor,vscode
```

### setup

The setup you want to use.

```sh
npx sv add mcp=setup:local
```
5 changes: 4 additions & 1 deletion packages/addons/_config/official.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import eslint from '../eslint/index.ts';
import lucia from '../lucia/index.ts';
import mdsvex from '../mdsvex/index.ts';
import paraglide from '../paraglide/index.ts';
import mcp from '../mcp/index.ts';
import playwright from '../playwright/index.ts';
import prettier from '../prettier/index.ts';
import storybook from '../storybook/index.ts';
Expand All @@ -26,6 +27,7 @@ type OfficialAddons = {
mdsvex: Addon<any>;
paraglide: Addon<any>;
storybook: Addon<any>;
mcp: Addon<any>;
};

// The order of addons here determines the order they are displayed inside the CLI
Expand All @@ -42,7 +44,8 @@ export const officialAddons: OfficialAddons = {
lucia,
mdsvex,
paraglide,
storybook
storybook,
mcp
};

export function getAddonDetails(id: string): AddonWithoutExplicitArgs {
Expand Down
10 changes: 9 additions & 1 deletion packages/addons/_tests/_setup/suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export function setupTest<Addons extends AddonMap>(
kinds: Array<AddonTestCase<Addons>['kind']>;
filter?: (addonTestCase: AddonTestCase<Addons>) => boolean;
browser?: boolean;
preInstallAddon?: (o: {
addonTestCase: AddonTestCase<Addons>;
cwd: string;
}) => Promise<void> | void;
}
) {
const test = vitest.test.extend<Fixtures>({} as any);
Expand Down Expand Up @@ -85,13 +89,17 @@ export function setupTest<Addons extends AddonMap>(
})
);

for (const { variant, kind } of testCases) {
for (const addonTestCase of testCases) {
const { variant, kind } = addonTestCase;
const cwd = create({ testId: `${kind.type}-${variant}`, variant });

// test metadata
const metaPath = path.resolve(cwd, 'meta.json');
fs.writeFileSync(metaPath, JSON.stringify({ variant, kind }, null, '\t'), 'utf8');

if (options?.preInstallAddon) {
await options.preInstallAddon({ addonTestCase, cwd });
}
const { pnpmBuildDependencies } = await installAddon({
cwd,
addons,
Expand Down
56 changes: 56 additions & 0 deletions packages/addons/_tests/mcp/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect } from '@playwright/test';
import { setupTest } from '../_setup/suite.ts';
import mcp from '../../mcp/index.ts';
import fs from 'node:fs';
import path from 'node:path';

const { test, testCases } = setupTest(
{ mcp },
{
kinds: [
{
type: 'default',
options: {
mcp: { ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], setup: 'local' }
}
}
],
browser: false,
// test only one as it's not depending on project variants
filter: (addonTestCase) => addonTestCase.variant === 'kit-ts',
preInstallAddon: ({ cwd }) => {
// prepare an existing file
fs.mkdirSync(path.resolve(cwd, `.cursor`));
fs.writeFileSync(
path.resolve(cwd, `.cursor/mcp.json`),
JSON.stringify(
{
mcpServers: {
svelte: { some: 'thing' },
anotherMCP: {}
}
},
null,
2
),
{ encoding: 'utf8' }
);
}
}
);

test.concurrent.for(testCases)('mcp $variant', (testCase, ctx) => {
const cwd = ctx.cwd(testCase);

const cursorPath = path.resolve(cwd, `.cursor/mcp.json`);
const cursorMcpContent = fs.readFileSync(cursorPath, 'utf8');

// should keep other MCPs
expect(cursorMcpContent).toContain(`anotherMCP`);
// should have the svelte level
expect(cursorMcpContent).toContain(`svelte`);
// should have local conf
expect(cursorMcpContent).toContain(`@sveltejs/mcp`);
// should remove old svelte config
expect(cursorMcpContent).not.toContain(`thing`);
});
123 changes: 123 additions & 0 deletions packages/addons/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
import { parseJson } from '@sveltejs/cli-core/parsers';

const options = defineAddonOptions()
.add('ide', {
question: 'Which client would you like to use?',
type: 'multiselect',
default: [],
options: [
{ value: 'claude-code', label: 'claude code' },
{ value: 'cursor', label: 'Cursor' },
{ value: 'gemini', label: 'Gemini' },
{ value: 'opencode', label: 'opencode' },
{ value: 'vscode', label: 'VSCode' },
{ value: 'other', label: 'Other' }
],
required: true
})
.add('setup', {
question: 'What setup you want to use?',
type: 'select',
default: 'remote',
options: [
{ value: 'local', label: 'Local', hint: 'will use stdio' },
{ value: 'remote', label: 'Remote', hint: 'will use a remote endpoint' }
],
required: true
})
.build();

export default defineAddon({
id: 'mcp',
shortDescription: 'Svelte MCP',
homepage: 'https://svelte.dev/docs/mcp',
options,
run: ({ sv, options }) => {
const getLocalConfig = (o?: { type?: 'stdio' | 'local'; env?: boolean }) => {
return {
...(o?.type ? { type: o.type } : {}),
command: 'npx',
args: ['-y', '@sveltejs/mcp'],
...(o?.env ? { env: {} } : {})
};
};
const getRemoteConfig = (o?: { type?: 'http' | 'remote' }) => {
return {
...(o?.type ? { type: o.type } : {}),
url: 'https://mcp.svelte.dev/mcp'
};
};

const configurator: Record<
(typeof options.ide)[number],
| {
schema?: string;
mcpServersKey?: string;
filePath: string;
typeLocal?: 'stdio' | 'local';
typeRemote?: 'http' | 'remote';
env?: boolean;
}
| { other: true }
> = {
'claude-code': {
filePath: '.mcp.json',
typeLocal: 'stdio',
typeRemote: 'http',
env: true
},
cursor: {
filePath: '.cursor/mcp.json'
},
gemini: {
filePath: '.gemini/settings.json'
},
opencode: {
schema: 'https://opencode.ai/config.json',
mcpServersKey: 'mcp',
filePath: 'opencode.json',
typeLocal: 'local',
typeRemote: 'remote'
},
vscode: {
mcpServersKey: 'servers',
filePath: '.vscode/mcp.json'
},
other: {
other: true
}
};

for (const ide of options.ide) {
const value = configurator[ide];
if ('other' in value) continue;

const { mcpServersKey, filePath, typeLocal, typeRemote, env, schema } = value;
sv.file(filePath, (content) => {
const { data, generateCode } = parseJson(content);
if (schema) {
data['$schema'] = schema;
}
const key = mcpServersKey || 'mcpServers';
data[key] ??= {};
data[key].svelte =
options.setup === 'local'
? getLocalConfig({ type: typeLocal, env })
: getRemoteConfig({ type: typeRemote });
return generateCode();
});
}
},
nextSteps({ highlighter, options }) {
const steps = [];

if (options.ide.includes('other')) {
steps.push(
`For other clients: ${highlighter.website(`https://svelte.dev/docs/mcp/${options.setup}-setup#Other-clients`)}`
);
}

return steps;
}
});
5 changes: 3 additions & 2 deletions packages/cli/commands/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,10 +589,11 @@ export async function runAddCommand(
const nextSteps = selectedAddons
.map(({ addon }) => {
if (!addon.nextSteps) return;
let addonMessage = `${pc.green(addon.id)}:\n`;

const options = official[addon.id];
const addonNextSteps = addon.nextSteps({ ...workspace, options, highlighter });
if (addonNextSteps.length === 0) return;

let addonMessage = `${pc.green(addon.id)}:\n`;
addonMessage += ` - ${addonNextSteps.join('\n - ')}`;
return addonMessage;
})
Expand Down
Loading