Skip to content

Commit a183342

Browse files
jycouetmanuel3108
andauthored
feat(cli): add new add-on mcp to configure your project (#735)
* don't display empty array next step * add in list * feat(cli): add new add-on `mcp` to configure your project * v0 doc * update types * cleanup * using client & matching other questions styles in sv * default to remote * inline local remote link Co-authored-by: Manuel <30698007+manuel3108@users.noreply.github.com> * clean code * list mcp in list of adders --------- Co-authored-by: Manuel <30698007+manuel3108@users.noreply.github.com>
1 parent 3d5f4de commit a183342

File tree

8 files changed

+234
-4
lines changed

8 files changed

+234
-4
lines changed

.changeset/smooth-bats-beam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'sv': patch
3+
---
4+
5+
feat(cli): add new add-on `mcp` to configure your project

documentation/docs/20-commands/20-sv-add.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ You can select multiple space-separated add-ons from [the list below](#Official-
3131
- [`drizzle`](drizzle)
3232
- [`eslint`](eslint)
3333
- [`lucia`](lucia)
34+
- [`mcp`](mcp)
3435
- [`mdsvex`](mdsvex)
3536
- [`paraglide`](paraglide)
3637
- [`playwright`](playwright)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: mcp
3+
---
4+
5+
[Svelte MCP](/docs/mcp/overview) can help your LLM write better Svelte code.
6+
7+
## Usage
8+
9+
```sh
10+
npx sv add mcp
11+
```
12+
13+
## What you get
14+
15+
- A good mcp configuration for your project depending on your IDE
16+
17+
## Options
18+
19+
### ide
20+
21+
The IDE you want to use like `'claude-code'`, `'cursor'`, `'gemini'`, `'opencode'`, `'vscode'`, `'other'`.
22+
23+
```sh
24+
npx sv add mcp=ide:cursor,vscode
25+
```
26+
27+
### setup
28+
29+
The setup you want to use.
30+
31+
```sh
32+
npx sv add mcp=setup:local
33+
```

packages/addons/_config/official.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import eslint from '../eslint/index.ts';
66
import lucia from '../lucia/index.ts';
77
import mdsvex from '../mdsvex/index.ts';
88
import paraglide from '../paraglide/index.ts';
9+
import mcp from '../mcp/index.ts';
910
import playwright from '../playwright/index.ts';
1011
import prettier from '../prettier/index.ts';
1112
import storybook from '../storybook/index.ts';
@@ -26,6 +27,7 @@ type OfficialAddons = {
2627
mdsvex: Addon<any>;
2728
paraglide: Addon<any>;
2829
storybook: Addon<any>;
30+
mcp: Addon<any>;
2931
};
3032

3133
// The order of addons here determines the order they are displayed inside the CLI
@@ -42,7 +44,8 @@ export const officialAddons: OfficialAddons = {
4244
lucia,
4345
mdsvex,
4446
paraglide,
45-
storybook
47+
storybook,
48+
mcp
4649
};
4750

4851
export function getAddonDetails(id: string): AddonWithoutExplicitArgs {

packages/addons/_tests/_setup/suite.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export function setupTest<Addons extends AddonMap>(
3535
kinds: Array<AddonTestCase<Addons>['kind']>;
3636
filter?: (addonTestCase: AddonTestCase<Addons>) => boolean;
3737
browser?: boolean;
38+
preInstallAddon?: (o: {
39+
addonTestCase: AddonTestCase<Addons>;
40+
cwd: string;
41+
}) => Promise<void> | void;
3842
}
3943
) {
4044
const test = vitest.test.extend<Fixtures>({} as any);
@@ -85,13 +89,17 @@ export function setupTest<Addons extends AddonMap>(
8589
})
8690
);
8791

88-
for (const { variant, kind } of testCases) {
92+
for (const addonTestCase of testCases) {
93+
const { variant, kind } = addonTestCase;
8994
const cwd = create({ testId: `${kind.type}-${variant}`, variant });
9095

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

100+
if (options?.preInstallAddon) {
101+
await options.preInstallAddon({ addonTestCase, cwd });
102+
}
95103
const { pnpmBuildDependencies } = await installAddon({
96104
cwd,
97105
addons,

packages/addons/_tests/mcp/test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect } from '@playwright/test';
2+
import { setupTest } from '../_setup/suite.ts';
3+
import mcp from '../../mcp/index.ts';
4+
import fs from 'node:fs';
5+
import path from 'node:path';
6+
7+
const { test, testCases } = setupTest(
8+
{ mcp },
9+
{
10+
kinds: [
11+
{
12+
type: 'default',
13+
options: {
14+
mcp: { ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], setup: 'local' }
15+
}
16+
}
17+
],
18+
browser: false,
19+
// test only one as it's not depending on project variants
20+
filter: (addonTestCase) => addonTestCase.variant === 'kit-ts',
21+
preInstallAddon: ({ cwd }) => {
22+
// prepare an existing file
23+
fs.mkdirSync(path.resolve(cwd, `.cursor`));
24+
fs.writeFileSync(
25+
path.resolve(cwd, `.cursor/mcp.json`),
26+
JSON.stringify(
27+
{
28+
mcpServers: {
29+
svelte: { some: 'thing' },
30+
anotherMCP: {}
31+
}
32+
},
33+
null,
34+
2
35+
),
36+
{ encoding: 'utf8' }
37+
);
38+
}
39+
}
40+
);
41+
42+
test.concurrent.for(testCases)('mcp $variant', (testCase, ctx) => {
43+
const cwd = ctx.cwd(testCase);
44+
45+
const cursorPath = path.resolve(cwd, `.cursor/mcp.json`);
46+
const cursorMcpContent = fs.readFileSync(cursorPath, 'utf8');
47+
48+
// should keep other MCPs
49+
expect(cursorMcpContent).toContain(`anotherMCP`);
50+
// should have the svelte level
51+
expect(cursorMcpContent).toContain(`svelte`);
52+
// should have local conf
53+
expect(cursorMcpContent).toContain(`@sveltejs/mcp`);
54+
// should remove old svelte config
55+
expect(cursorMcpContent).not.toContain(`thing`);
56+
});

packages/addons/mcp/index.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
2+
import { parseJson } from '@sveltejs/cli-core/parsers';
3+
4+
const options = defineAddonOptions()
5+
.add('ide', {
6+
question: 'Which client would you like to use?',
7+
type: 'multiselect',
8+
default: [],
9+
options: [
10+
{ value: 'claude-code', label: 'claude code' },
11+
{ value: 'cursor', label: 'Cursor' },
12+
{ value: 'gemini', label: 'Gemini' },
13+
{ value: 'opencode', label: 'opencode' },
14+
{ value: 'vscode', label: 'VSCode' },
15+
{ value: 'other', label: 'Other' }
16+
],
17+
required: true
18+
})
19+
.add('setup', {
20+
question: 'What setup you want to use?',
21+
type: 'select',
22+
default: 'remote',
23+
options: [
24+
{ value: 'local', label: 'Local', hint: 'will use stdio' },
25+
{ value: 'remote', label: 'Remote', hint: 'will use a remote endpoint' }
26+
],
27+
required: true
28+
})
29+
.build();
30+
31+
export default defineAddon({
32+
id: 'mcp',
33+
shortDescription: 'Svelte MCP',
34+
homepage: 'https://svelte.dev/docs/mcp',
35+
options,
36+
run: ({ sv, options }) => {
37+
const getLocalConfig = (o?: { type?: 'stdio' | 'local'; env?: boolean }) => {
38+
return {
39+
...(o?.type ? { type: o.type } : {}),
40+
command: 'npx',
41+
args: ['-y', '@sveltejs/mcp'],
42+
...(o?.env ? { env: {} } : {})
43+
};
44+
};
45+
const getRemoteConfig = (o?: { type?: 'http' | 'remote' }) => {
46+
return {
47+
...(o?.type ? { type: o.type } : {}),
48+
url: 'https://mcp.svelte.dev/mcp'
49+
};
50+
};
51+
52+
const configurator: Record<
53+
(typeof options.ide)[number],
54+
| {
55+
schema?: string;
56+
mcpServersKey?: string;
57+
filePath: string;
58+
typeLocal?: 'stdio' | 'local';
59+
typeRemote?: 'http' | 'remote';
60+
env?: boolean;
61+
}
62+
| { other: true }
63+
> = {
64+
'claude-code': {
65+
filePath: '.mcp.json',
66+
typeLocal: 'stdio',
67+
typeRemote: 'http',
68+
env: true
69+
},
70+
cursor: {
71+
filePath: '.cursor/mcp.json'
72+
},
73+
gemini: {
74+
filePath: '.gemini/settings.json'
75+
},
76+
opencode: {
77+
schema: 'https://opencode.ai/config.json',
78+
mcpServersKey: 'mcp',
79+
filePath: 'opencode.json',
80+
typeLocal: 'local',
81+
typeRemote: 'remote'
82+
},
83+
vscode: {
84+
mcpServersKey: 'servers',
85+
filePath: '.vscode/mcp.json'
86+
},
87+
other: {
88+
other: true
89+
}
90+
};
91+
92+
for (const ide of options.ide) {
93+
const value = configurator[ide];
94+
if ('other' in value) continue;
95+
96+
const { mcpServersKey, filePath, typeLocal, typeRemote, env, schema } = value;
97+
sv.file(filePath, (content) => {
98+
const { data, generateCode } = parseJson(content);
99+
if (schema) {
100+
data['$schema'] = schema;
101+
}
102+
const key = mcpServersKey || 'mcpServers';
103+
data[key] ??= {};
104+
data[key].svelte =
105+
options.setup === 'local'
106+
? getLocalConfig({ type: typeLocal, env })
107+
: getRemoteConfig({ type: typeRemote });
108+
return generateCode();
109+
});
110+
}
111+
},
112+
nextSteps({ highlighter, options }) {
113+
const steps = [];
114+
115+
if (options.ide.includes('other')) {
116+
steps.push(
117+
`For other clients: ${highlighter.website(`https://svelte.dev/docs/mcp/${options.setup}-setup#Other-clients`)}`
118+
);
119+
}
120+
121+
return steps;
122+
}
123+
});

packages/cli/commands/add/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -589,10 +589,11 @@ export async function runAddCommand(
589589
const nextSteps = selectedAddons
590590
.map(({ addon }) => {
591591
if (!addon.nextSteps) return;
592-
let addonMessage = `${pc.green(addon.id)}:\n`;
593-
594592
const options = official[addon.id];
595593
const addonNextSteps = addon.nextSteps({ ...workspace, options, highlighter });
594+
if (addonNextSteps.length === 0) return;
595+
596+
let addonMessage = `${pc.green(addon.id)}:\n`;
596597
addonMessage += ` - ${addonNextSteps.join('\n - ')}`;
597598
return addonMessage;
598599
})

0 commit comments

Comments
 (0)