Compare commits

...

9 Commits

Author SHA1 Message Date
Stanislav Khromov
684d7effef wip 2025-09-17 22:33:50 +02:00
Paolo Ricciuti
a0f8abb632 Merge pull request #5 from sveltejs/eslint-versions 2025-09-17 16:25:16 +02:00
paoloricciuti
f85e4612ac fix: use compiler to get warnings, enable more rules 2025-09-17 16:25:04 +02:00
paoloricciuti
11aceaf047 fix: use snake case and lazily create eslint instances 2025-09-17 15:05:12 +02:00
baseballyama
e515b9eaf9 wip 2025-09-17 21:00:58 +09:00
paoloricciuti
ffafb8a492 fix: lint 2025-09-17 01:35:56 +02:00
paoloricciuti
cba6ab8f4b feat: run eslint on code to get basic rules 2025-09-17 01:30:15 +02:00
paoloricciuti
052331a80e feat: labeled statement autofixer 2025-09-17 01:30:15 +02:00
paoloricciuti
887137e794 feat: setup stdio server build 2025-09-17 01:30:15 +02:00
10 changed files with 2186 additions and 16 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ vite.config.ts.timestamp-*
# SQLite
*.db
dist

10
.vscode/mcp.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"servers": {
"Svelte MCP": {
"type": "stdio",
"command": "node",
"args": ["dist/lib/stdio.js"]
}
},
"inputs": []
}

View File

@@ -11,3 +11,24 @@ pnpm dev
```
1. Set the VOYAGE_API_KEY for embeddings support
### Local dev tools
#### MCP inspector
```
pnpm run inspect
```
Then visit http://localhost:6274/
- Transport type: `Streamable HTTP`
- http://localhost:5173/mcp
#### Database inspector
```
pnpm run db:studio
```
https://local.drizzle.studio/

View File

@@ -4,10 +4,15 @@
"description": "The official Svelte MCP server implementation",
"type": "module",
"main": "src/index.js",
"bin": {
"svelte-mcp": "./dist/lib/stdio.js"
},
"scripts": {
"start": "node src/index.js",
"dev": "vite dev",
"build": "vite build",
"build:mcp": "tsc --project tsconfig.build.json",
"prepublishOnly": "pnpm build:mcp",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -21,7 +26,8 @@
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
"db:studio": "drizzle-kit studio",
"inspect": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector"
},
"keywords": [
"svelte",
@@ -34,6 +40,7 @@
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@libsql/client": "^0.14.0",
"@modelcontextprotocol/inspector": "^0.16.7",
"@sveltejs/adapter-vercel": "^5.6.3",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",

1983
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,15 @@
import type { Node } from 'estree';
import type { AST } from 'svelte-eslint-parser';
import type { Visitors } from 'zimmerframe';
import type { ParseResult } from '../server/analyze/parse.js';
export type Autofixer = Visitors<
Node,
{ output: { issues: string[]; suggestions: string[] }; parsed: ParseResult }
Node | AST.SvelteNode,
{
output: { issues: string[]; suggestions: string[] };
parsed: ParseResult;
desired_svelte_version: number;
}
>;
export const assign_in_effect: Autofixer = {

70
src/lib/mcp/eslint.ts Normal file
View File

@@ -0,0 +1,70 @@
import { ESLint } from 'eslint';
import svelte_parser from 'svelte-eslint-parser';
import svelte from 'eslint-plugin-svelte';
import type { Config } from '@sveltejs/kit';
let svelte_5_linter: ESLint | undefined;
let svelte_4_linter: ESLint | undefined;
function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
return [
...svelte.configs.recommended,
{
files: ['*.svelte'],
rules: {
'no-self-assign': 'warn',
'svelte/infinite-reactive-loop': 'warn',
'svelte/no-dupe-else-if-blocks': 'warn',
'svelte/no-dupe-on-directives': 'warn',
'svelte/no-dupe-style-properties': 'warn',
'svelte/no-dupe-use-directives': 'warn',
'svelte/no-object-in-text-mustaches': 'warn',
'svelte/no-raw-special-elements': 'warn',
'svelte/no-reactive-functions': 'warn',
'svelte/no-reactive-literals': 'warn',
'svelte/no-store-async': 'warn',
'svelte/no-svelte-internal': 'warn',
'svelte/no-unnecessary-state-wrap': 'warn',
'svelte/no-unused-props': 'warn',
'svelte/no-unused-svelte-ignore': 'warn',
'svelte/no-useless-children-snippet': 'warn',
'svelte/no-useless-mustaches': 'warn',
'svelte/prefer-svelte-reactivity': 'warn',
'svelte/prefer-writable-derived': 'warn',
'svelte/require-event-dispatcher-types': 'warn',
'svelte/require-store-reactive-access': 'warn',
},
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
parser: svelte_parser,
parserOptions: {
svelteConfig: svelte_config,
},
},
},
];
}
export function get_linter(version: number) {
if (version < 5) {
return (svelte_4_linter ??= new ESLint({
overrideConfigFile: true,
baseConfig: base_config({
compilerOptions: {
runes: false,
},
}),
}));
}
return (svelte_5_linter ??= new ESLint({
overrideConfigFile: true,
baseConfig: base_config({
compilerOptions: {
runes: true,
},
}),
}));
}

View File

@@ -7,6 +7,8 @@ import { McpServer } from 'tmcp';
import * as v from 'valibot';
import { parse } from '../server/analyze/parse.js';
import * as autofixers from './autofixers.js';
import { get_linter } from './eslint.js';
import { compile } from 'svelte/compiler';
const server = new McpServer(
{
@@ -23,7 +25,7 @@ const server = new McpServer(
completions: {},
},
instructions:
'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code',
'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code. After you correct the component call this tool again to confirm all the issues are fixed.',
},
);
@@ -35,11 +37,18 @@ server.tool(
'Given a svelte component or module returns a list of suggestions to fix any issues it has. This tool MUST be used whenever the user is asking to write svelte code before sending the code back to the user',
schema: v.object({
code: v.string(),
desired_svelte_version: v.pipe(
v.union([v.literal(4), v.literal(5)]),
v.description(
'The desired svelte version...if possible read this from the package.json of the user project, otherwise use some hint from the wording (if the user asks for runes it wants version 5). Default to 5 in case of doubt.',
),
),
filename: v.optional(v.string()),
}),
outputSchema: v.object({
issues: v.optional(v.array(v.string())),
suggestions: v.optional(v.array(v.string())),
issues: v.array(v.string()),
suggestions: v.array(v.string()),
require_another_tool_call_after_fixing: v.boolean(),
}),
annotations: {
title: 'Svelte Autofixer',
@@ -48,14 +57,61 @@ server.tool(
openWorldHint: false,
},
},
async ({ code, filename }) => {
const content: { issues: string[]; suggestions: string[] } = { issues: [], suggestions: [] };
async ({ code, filename, desired_svelte_version }) => {
const content: {
issues: string[];
suggestions: string[];
require_another_tool_call_after_fixing: boolean;
} = { issues: [], suggestions: [], require_another_tool_call_after_fixing: false };
try {
// compile without generating to get warnings and errors
const parsed = parse(code, filename ?? 'Component.svelte');
const compilation_result = compile(code, {
filename: filename || 'Component.svelte',
generate: false,
runes: desired_svelte_version >= 5,
});
// Run each autofixer separately to avoid interrupting logic flow
for (const autofixer of Object.values(autofixers)) {
walk(parsed.ast as unknown as Node, { output: content, parsed }, autofixer);
for (const warning of compilation_result.warnings) {
content.issues.push(
`${warning.message} at line ${warning.start?.line}, column ${warning.start?.column}`,
);
}
const parsed = parse(code, filename ?? 'Component.svelte');
// Run each autofixer separately to avoid interrupting logic flow
for (const autofixer of Object.values(autofixers)) {
walk(
parsed.ast as unknown as Node,
{ output: content, parsed, desired_svelte_version },
autofixer,
);
}
const eslint = get_linter(desired_svelte_version);
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });
for (const message of results[0].messages) {
if (message.severity === 2) {
content.issues.push(
`${message.message} at line ${message.line}, column ${message.column}`,
);
} else if (message.severity === 1) {
content.suggestions.push(
`${message.message} at line ${message.line}, column ${message.column}`,
);
}
}
} catch (e: unknown) {
const error = e as Error & { start?: { line: number; column: number } };
content.issues.push(
`${error.message} at line ${error.start?.line}, column ${error.start?.column}`,
);
}
if (content.issues.length > 0 || content.suggestions.length > 0) {
content.require_another_tool_call_after_fixing = true;
}
return {

4
src/lib/stdio.ts Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env node
import { stdio_transport } from './mcp/index.js';
stdio_transport.listen();

21
tsconfig.build.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/lib/stdio.ts", "src/lib/mcp/**/*"],
"exclude": ["node_modules", "dist", "src/routes/**/*", "src/app.html", "src/hooks.server.ts"]
}