Skip to content
Open
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/remove-mcp-addon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': minor
---

feat(ai-tools): replace `mcp` add-on with `ai-tools` add-on that includes both MCP and skills setup
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
packages/sv/src/cli/tests/snapshots/*
packages/sv-utils/src/tests/**/output.ts
packages/sv-utils/src/tests/**/output.ts
packages/sv/src/create/shared/+skills/*
2 changes: 1 addition & 1 deletion documentation/docs/20-commands/20-sv-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ Prevents installing dependencies

## Official add-ons

- [`ai-tools`](ai-tools)
- [`better-auth`](better-auth)
- [`drizzle`](drizzle)
- [`eslint`](eslint)
- [`mcp`](mcp)
- [`mdsvex`](mdsvex)
- [`paraglide`](paraglide)
- [`playwright`](playwright)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
---
title: mcp
title: ai-tools
---

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

## Usage

```sh
npx sv add mcp
npx sv add ai-tools
```

## What you get

- An MCP configuration for [local](https://svelte.dev/docs/ai/local-setup) or [remote](https://svelte.dev/docs/ai/remote-setup) setup
- A [README for agents](https://agents.md/) to help you use the MCP server effectively
- [Skills](https://svelte.dev/docs/ai/skills) for clients that support them (claude code, opencode)

## Options

Expand All @@ -22,13 +23,13 @@ npx sv add mcp
The IDE you want to use like `'claude-code'`, `'cursor'`, `'gemini'`, `'opencode'`, `'vscode'`, `'other'`.

```sh
npx sv add mcp="ide:cursor,vscode"
npx sv add ai-tools="ide:cursor,vscode"
```

### setup

The setup you want to use.

```sh
npx sv add mcp="setup:local"
npx sv add ai-tools="setup:local"
```
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,26 @@ const options = defineAddonOptions()
required: true,
condition: ({ ide }) => !(ide.length === 1 && ide.includes('opencode'))
})
.add('skills', {
question: 'Do you want to install skills?',
type: 'select',
default: 'files',
options: [
{ value: 'files', label: 'Add files to the project' },
{
value: 'none',
label: 'Skip',
hint: 'for Claude Code you can install the plugin instead: /plugin install svelte'
}
],
condition: ({ ide }) => ide.some((i) => i !== 'opencode' && i !== 'other')
})
.build();

export default defineAddon({
id: 'mcp',
shortDescription: 'Svelte MCP',
homepage: 'https://svelte.dev/docs/mcp',
id: 'ai-tools',
shortDescription: 'Svelte AI Tools',
homepage: 'https://svelte.dev/docs/ai',
options,
run: ({ sv, options }) => {
const getLocalConfig = (o?: {
Expand Down Expand Up @@ -72,14 +86,19 @@ export default defineAddon({
};
agentPath: string;
configPath: string;
skillsPath?: string;
agentsPath?: string;
agentExtension?: string;
customData?: Record<string, any>;
extraFiles?: Array<{ path: string; data: Record<string, any> }>;
}
| { other: true }
> = {
'claude-code': {
agentPath: 'CLAUDE.md',
agentPath: '.claude/CLAUDE.md',
configPath: '.mcp.json',
skillsPath: '.claude/skills',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Claude plugin also ships with skills but we can't really add it for them I think...what can we do in this case to avoid adding duplicate stuff?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it would be great that claude code allow plugins like in open code.

I did something like:
image

agentsPath: '.claude/agents',
mcpOptions: {
typeLocal: 'stdio',
typeRemote: 'http',
Expand All @@ -89,11 +108,13 @@ export default defineAddon({
cursor: {
agentPath: 'AGENTS.md',
configPath: '.cursor/mcp.json',
agentsPath: '.cursor/agents',
mcpOptions: {}
},
gemini: {
agentPath: 'GEMINI.md',
configPath: '.gemini/settings.json',
agentsPath: '.gemini/agents',
schema:
'https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json',
mcpOptions: {}
Expand All @@ -115,6 +136,8 @@ export default defineAddon({
vscode: {
agentPath: 'AGENTS.md',
configPath: '.vscode/mcp.json',
agentsPath: '.github/agents',
agentExtension: '.agent.md',
mcpOptions: {
serversKey: 'servers'
}
Expand All @@ -127,16 +150,29 @@ export default defineAddon({
const filesAdded: string[] = [];
const filesExistingAlready: string[] = [];

const sharedFiles = getSharedFiles().filter((file) => file.include.includes('mcp'));
const agentFile = sharedFiles.find((file) => file.name === 'AGENTS.md');
const sharedFiles = getSharedFiles();
const mcpFiles = sharedFiles.filter((file) => file.include.includes('mcp'));
const skillFiles = sharedFiles.filter((file) => file.include.includes('skills'));
const agentFiles = sharedFiles.filter((file) => file.include.includes('agents'));
const agentFile = mcpFiles.find((file) => file.name === 'AGENTS.md');

for (const ide of options.ide) {
const value = configurator[ide];

if (value === undefined) continue;
if ('other' in value) continue;

const { mcpOptions, agentPath, configPath, schema, customData, extraFiles } = value;
const {
mcpOptions,
agentPath,
configPath,
skillsPath,
agentsPath,
agentExtension,
schema,
customData,
extraFiles
} = value;

// We only add the agent file if it's not already added
if (!filesAdded.includes(agentPath)) {
Expand Down Expand Up @@ -184,12 +220,42 @@ export default defineAddon({
);
}
}

// Add skills for clients that support them (not opencode - plugin handles it)
if (skillsPath && options.skills === 'files') {
for (const file of skillFiles) {
const filePath = `${skillsPath}/${file.name}`;
sv.file(filePath, (content) => {
if (content) {
filesExistingAlready.push(filePath);
return false;
}
return file.contents;
});
}
}

// Add sub-agents for clients that support them (not opencode - plugin handles it)
if (agentsPath) {
for (const file of agentFiles) {
const ext = agentExtension ?? '.md';
const name = file.name.replace(/\.md$/, ext);
const filePath = `${agentsPath}/${name}`;
sv.file(filePath, (content) => {
if (content) {
filesExistingAlready.push(filePath);
return false;
}
return file.contents;
});
}
}
}

if (filesExistingAlready.length > 0) {
log.warn(
`${filesExistingAlready.map((path) => color.path(path)).join(', ')} already exists, we didn't touch ${filesExistingAlready.length > 1 ? 'them' : 'it'}. ` +
`See ${color.website('https://svelte.dev/docs/mcp/overview#Usage')} for manual setup.`
`See ${color.website('https://svelte.dev/docs/ai')} for manual setup.`
);
}
},
Expand Down
6 changes: 3 additions & 3 deletions packages/sv/src/addons/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Addon, AddonDefinition } from '../core/config.ts';
import aiTools from './ai-tools.ts';
import betterAuth from './better-auth.ts';
import drizzle from './drizzle.ts';
import eslint from './eslint.ts';
import mcp from './mcp.ts';
import mdsvex from './mdsvex.ts';
import paraglide from './paraglide.ts';
import playwright from './playwright.ts';
Expand All @@ -24,7 +24,7 @@ type OfficialAddons = {
mdsvex: Addon<any>;
paraglide: Addon<any>;
storybook: Addon<any>;
mcp: Addon<any>;
aiTools: Addon<any>;
};

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

export function getAddonDetails(id: string): AddonDefinition {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import fs from 'node:fs';
import path from 'node:path';
import { expect } from 'vitest';
import mcp from '../../mcp.ts';
import aiTools from '../../ai-tools.ts';
import { setupTest } from '../_setup/suite.ts';

const { test, testCases } = setupTest(
{ mcp },
{ 'ai-tools': aiTools },
{
kinds: [
{
type: 'default-local',
options: {
mcp: { ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], setup: 'local' }
'ai-tools': {
ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'],
setup: 'local',
skills: 'files'
}
}
},
{
type: 'default-remote',
options: {
mcp: { ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], setup: 'remote' }
'ai-tools': {
ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'],
setup: 'remote',
skills: 'files'
}
}
}
],
Expand Down Expand Up @@ -45,12 +53,12 @@ const { test, testCases } = setupTest(
}
);

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

const getContent = (filePath: string) => {
const cursorPath = path.resolve(cwd, filePath);
return fs.readFileSync(cursorPath, 'utf8');
const fullPath = path.resolve(cwd, filePath);
return fs.readFileSync(fullPath, 'utf8');
};

const cursorMcpContent = getContent(`.cursor/mcp.json`);
Expand Down Expand Up @@ -214,4 +222,22 @@ test.concurrent.for(testCases)('mcp $kind.type $variant', (testCase, ctx) => {
}
`);
}

// skills should be installed for claude-code only (opencode uses plugin)
const claudeSkillsDir = path.resolve(cwd, '.claude/skills');
expect(fs.existsSync(claudeSkillsDir)).toBe(true);
expect(fs.existsSync(path.resolve(claudeSkillsDir, 'svelte-code-writer/SKILL.md'))).toBe(true);
expect(fs.existsSync(path.resolve(claudeSkillsDir, 'svelte-core-bestpractices/SKILL.md'))).toBe(
true
);

// opencode should NOT have skills (plugin handles it)
expect(fs.existsSync(path.resolve(cwd, '.opencode/skills'))).toBe(false);

// sub-agents should be installed for all clients except opencode
expect(fs.existsSync(path.resolve(cwd, '.claude/agents/svelte-file-editor.md'))).toBe(true);
expect(fs.existsSync(path.resolve(cwd, '.cursor/agents/svelte-file-editor.md'))).toBe(true);
expect(fs.existsSync(path.resolve(cwd, '.gemini/agents/svelte-file-editor.md'))).toBe(true);
expect(fs.existsSync(path.resolve(cwd, '.github/agents/svelte-file-editor.agent.md'))).toBe(true);
expect(fs.existsSync(path.resolve(cwd, '.opencode/agents'))).toBe(false);
});
30 changes: 29 additions & 1 deletion packages/sv/src/cli/tests/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('cli', () => {
'better-auth=demo:password,github',
'mdsvex',
'paraglide=languageTags:en,es+demo:yes',
'mcp=ide:claude-code,cursor,gemini,opencode,vscode,other+setup:local'
'ai-tools=ide:claude-code,cursor,gemini,opencode,vscode,other+setup:local+skills:files'
// 'storybook' // No storybook addon during tests!
]
},
Expand Down Expand Up @@ -93,10 +93,29 @@ describe('cli', () => {
projectName
);
const relativeFiles = fs.readdirSync(testOutputPath, { recursive: true }) as string[];

// Files from ai-tools repo (skills, agents) change independently -
// snapshot only file listings, not content
const aiToolsFiles: Record<string, string[]> = {};
const aiToolsPattern = /[\\/](skills|agents)[\\/]/;

for (const relativeFile of relativeFiles) {
if (!fs.statSync(path.resolve(testOutputPath, relativeFile)).isFile()) continue;
if (['.svg', '.env'].some((ext) => relativeFile.endsWith(ext))) continue;

const normalized = relativeFile.replace(/\\/g, '/');

// Group ai-tools files by directory for manifest comparison
if (aiToolsPattern.test(normalized)) {
const match = normalized.match(/(.+\/(?:skills|agents))\/(.*)/);
if (match) {
const [, base, rest] = match;
aiToolsFiles[base] ??= [];
aiToolsFiles[base].push(rest);
}
continue;
}

let generated = fs.readFileSync(path.resolve(testOutputPath, relativeFile), 'utf-8');
if (relativeFile === 'package.json') {
const { data: generatedPackageJson } = parse.json(generated);
Expand All @@ -119,6 +138,15 @@ describe('cli', () => {
);
}

// Compare ai-tools file listings against sv-files-snapshots.md manifests
for (const [dir, files] of Object.entries(aiToolsFiles)) {
const manifest = files.sort().join('\n') + '\n';
await expect(manifest).toMatchFileSnapshot(
path.resolve(snapPath, dir, 'sv-files-snapshots.md'),
`ai-tools manifest "${dir}" does not match snapshot`
);
}

if (projectName === 'create-with-all-addons' && process.platform !== 'win32') {
await exec('pnpm', ['install', '--no-frozen-lockfile'], {
nodeOptions: { stdio: 'pipe', cwd: testOutputPath }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

- **Language**: TypeScript
- **Package Manager**: npm
- **Add-ons**: prettier, eslint, vitest, playwright, tailwindcss, sveltekit-adapter, drizzle, better-auth, mdsvex, paraglide, mcp
- **Add-ons**: prettier, eslint, vitest, playwright, tailwindcss, sveltekit-adapter, drizzle, better-auth, mdsvex, paraglide, ai-tools

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
svelte-file-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
svelte-code-writer/SKILL.md
svelte-core-bestpractices/SKILL.md
svelte-core-bestpractices/references/$inspect.md
svelte-core-bestpractices/references/@attach.md
svelte-core-bestpractices/references/@render.md
svelte-core-bestpractices/references/await-expressions.md
svelte-core-bestpractices/references/bind.md
svelte-core-bestpractices/references/each.md
svelte-core-bestpractices/references/hydratable.md
svelte-core-bestpractices/references/snippet.md
svelte-core-bestpractices/references/svelte-reactivity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
svelte-file-editor.md
Loading
Loading