Compare commits

..

13 Commits

Author SHA1 Message Date
Stanislav Khromov
dab61bdb3c Update README.md 2025-09-17 01:40:43 +02:00
Stanislav Khromov
0719e06f6a Update README.md 2025-09-17 01:40:31 +02:00
Stanislav Khromov
7346b0387a Update bypass.php 2025-09-17 01:39:04 +02:00
Stanislav Khromov
3af6c887e9 Update bypass.php 2025-09-17 01:38:10 +02:00
Stanislav Khromov
d91a4f2b51 wip 2025-09-17 01:36:54 +02:00
Stanislav Khromov
ec2e12f7f2 Update docker-compose.yml 2025-09-17 01:10:21 +02:00
Stanislav Khromov
7805fb8a24 set up mcp-inspector 2025-09-17 01:06:18 +02:00
Stanislav Khromov
5280f37a05 Update docker-compose.yml 2025-09-17 01:02:02 +02:00
Stanislav Khromov
621c6cf3c7 Update package.json 2025-09-17 00:57:45 +02:00
Stanislav Khromov
5772110214 Update docker-compose.yml 2025-09-17 00:54:32 +02:00
Stanislav Khromov
b171f6a19f Update docker-compose.yml 2025-09-17 00:50:13 +02:00
Stanislav Khromov
3882733a66 Update docker-compose.yml 2025-09-17 00:44:15 +02:00
Stanislav Khromov
e178374683 Create docker-compose.yml 2025-09-17 00:43:23 +02:00
17 changed files with 155 additions and 2260 deletions

1
.gitignore vendored
View File

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

10
.vscode/mcp.json vendored
View File

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

View File

@@ -12,23 +12,11 @@ pnpm dev
1. Set the VOYAGE_API_KEY for embeddings support
### Local dev tools
#### MCP inspector
#### Optional tools
```
pnpm run inspect
docker-compose up
```
Then visit http://localhost:6274/
- Transport type: `Streamable HTTP`
- http://localhost:5173/mcp
#### Database inspector
```
pnpm run db:studio
```
https://local.drizzle.studio/
- MCP Inspector: http://localhost:6274/ (Connect with `http://host.docker.internal:5173/mcp` + Streamable HTTP)
- http://localhost:8081/ - Adminer SQLite frontend

31
bypass.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
function adminer_object() {
include_once "plugins/login-sqlite.php";
class NoAuthSqlite extends Adminer {
function login($login, $password) {
return true;
}
function credentials() {
return array('/tmp/test.db', '', '');
}
function database() {
return '/tmp/test.db';
}
function loginForm() {
echo '<input type="hidden" name="auth[driver]" value="sqlite">';
echo '<input type="hidden" name="auth[server]" value="/tmp/test.db">';
echo '<input type="hidden" name="auth[username]" value="">';
echo '<input type="hidden" name="auth[password]" value="">';
echo '<input type="hidden" name="auth[db]" value="/tmp/test.db">';
echo '<p><input type="submit" value="Connect to SQLite Database"></p>';
}
}
return new NoAuthSqlite;
}
include "adminer.php";

25
docker-compose.yml Normal file
View File

@@ -0,0 +1,25 @@
services:
mcp-inspector:
image: ghcr.io/modelcontextprotocol/inspector:latest
ports:
- '6274:6274'
- '6277:6277'
environment:
- DANGEROUSLY_OMIT_AUTH=true
- HOST=0.0.0.0
restart: unless-stopped
container_name: mcp-inspector-sveltejs-mcp
adminer:
image: adminer:4.6.2
ports:
- '8081:8080'
volumes:
- ./test.db:/tmp/test.db
- ./bypass.php:/var/www/html/bypass.php
environment:
- ADMINER_DEFAULT_SERVER=sqlite:/tmp/test.db
- ADMINER_DESIGN=hydra
restart: unless-stopped
container_name: adminer-sveltejs-mcp
command: ["php", "-S", "[::]:8080", "-t", "/var/www/html", "/var/www/html/bypass.php"]

View File

@@ -4,15 +4,10 @@
"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",
"dev": "vite dev --host",
"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",
@@ -26,8 +21,7 @@
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"inspect": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector"
"db:studio": "drizzle-kit studio"
},
"keywords": [
"svelte",
@@ -40,7 +34,6 @@
"@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,15 +1,10 @@
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 | AST.SvelteNode,
{
output: { issues: string[]; suggestions: string[] };
parsed: ParseResult;
desired_svelte_version: number;
}
Node,
{ output: { issues: string[]; suggestions: string[] }; parsed: ParseResult }
>;
export const assign_in_effect: Autofixer = {

View File

@@ -1,70 +0,0 @@
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,8 +7,6 @@ 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(
{
@@ -25,7 +23,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. After you correct the component call this tool again to confirm all the issues are fixed.',
'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',
},
);
@@ -37,18 +35,11 @@ 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.array(v.string()),
suggestions: v.array(v.string()),
require_another_tool_call_after_fixing: v.boolean(),
issues: v.optional(v.array(v.string())),
suggestions: v.optional(v.array(v.string())),
}),
annotations: {
title: 'Svelte Autofixer',
@@ -57,61 +48,14 @@ server.tool(
openWorldHint: false,
},
},
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
async ({ code, filename }) => {
const content: { issues: string[]; suggestions: string[] } = { issues: [], suggestions: [] };
const compilation_result = compile(code, {
filename: filename || 'Component.svelte',
generate: false,
runes: desired_svelte_version >= 5,
});
const parsed = parse(code, filename ?? 'Component.svelte');
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;
// 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);
}
return {

View File

@@ -12,80 +12,9 @@ import { float_32_array } from './utils';
* to the generated migration file
*/
export const distillations = sqliteTable('distillations', {
// this is just an example of a vector table...we can change this with the docs table later
export const vector_table = sqliteTable('vector_table', {
id: integer('id').primaryKey(),
preset_name: text('preset_name').notNull(),
version: text('version').notNull(),
content: text('content').notNull(),
size_kb: integer('size_kb').notNull(),
document_count: integer('document_count').notNull(),
distillation_job_id: integer('distillation_job_id').references(() => distillation_jobs.id),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const distillation_jobs = sqliteTable('distillation_jobs', {
id: integer('id').primaryKey(),
preset_name: text('preset_name').notNull(),
batch_id: text('batch_id'),
status: text('status', { enum: ['pending', 'processing', 'completed', 'failed'] }).notNull(),
model_used: text('model_used').notNull(),
total_files: integer('total_files').notNull(),
processed_files: integer('processed_files').notNull().default(0),
successful_files: integer('successful_files').notNull().default(0),
minimize_applied: integer('minimize_applied', { mode: 'boolean' }).notNull().default(false),
total_input_tokens: integer('total_input_tokens').notNull().default(0),
total_output_tokens: integer('total_output_tokens').notNull().default(0),
started_at: integer('started_at', { mode: 'timestamp' }),
completed_at: integer('completed_at', { mode: 'timestamp' }),
error_message: text('error_message'),
metadata: text('metadata', { mode: 'json' })
.$type<Record<string, unknown>>()
.notNull()
.default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const content = sqliteTable('content', {
id: integer('id').primaryKey(),
path: text('path').notNull(),
filename: text('filename').notNull(),
content: text('content').notNull(),
size_bytes: integer('size_bytes').notNull(),
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
metadata: text('metadata', { mode: 'json' })
.$type<Record<string, unknown>>()
.notNull()
.default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const content_distilled = sqliteTable('content_distilled', {
id: integer('id').primaryKey(),
path: text('path').notNull(),
filename: text('filename').notNull(),
content: text('content').notNull(),
size_bytes: integer('size_bytes').notNull(),
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
metadata: text('metadata', { mode: 'json' })
.$type<Record<string, unknown>>()
.notNull()
.default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
text: text('text'),
vector: float_32_array('vector', { dimensions: 3 }),
});

View File

@@ -26,9 +26,7 @@ export function vector(arr: number[]) {
* .execute();
*/
export function distance(column: Column, arr: number[], as = 'distance') {
return sql<number>`CASE ${column} ISNULL WHEN 1 THEN 1 ELSE vector_distance_cos(${column}, vector32(${JSON.stringify(arr)})) END`.as(
as,
);
return sql<number>`vector_distance_cos(${column}, vector32(${JSON.stringify(arr)}))`.as(as);
}
/**

View File

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

View File

@@ -0,0 +1,50 @@
import { VOYAGE_API_KEY } from '$env/static/private';
import { db } from '$lib/server/db/index.js';
import { vector_table } from '$lib/server/db/schema.js';
import { distance, vector } from '$lib/server/db/utils.js';
import { sql } from 'drizzle-orm';
async function get_embeddings(text: string) {
const result = await fetch('https://api.voyageai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${VOYAGE_API_KEY}`,
},
body: JSON.stringify({
input: [text],
model: 'voyage-3.5',
}),
}).then((res) => res.json());
return result.data[0].embedding as number[];
}
export async function load({ url: { searchParams } }) {
const sentence = searchParams.get('sentence');
if (!sentence) return { top: [], sentence: '' };
const top = await db
.select({
id: vector_table.id,
text: vector_table.text,
distance: distance(vector_table.vector, await get_embeddings(sentence)),
})
.from(vector_table)
.orderBy(sql`distance`)
.execute();
return { top, sentence };
}
export const actions = {
async default({ request }) {
const data = await request.formData();
const text = data.get('text')?.toString();
const embeddings = await get_embeddings(text ?? '');
if (text && embeddings) {
await db
.insert(vector_table)
.values({ text, vector: vector(embeddings) })
.execute();
}
},
};

View File

@@ -1 +1,21 @@
<script lang="ts">
import { enhance } from '$app/forms';
let { data } = $props();
</script>
<h1>Official Svelte MCP</h1>
<form method="POST" use:enhance>
<textarea name="text" rows="4" cols="50" placeholder="Enter text to store in the vector database"
></textarea>
<br />
<button>Submit</button>
</form>
Comparing with
<pre>{data.sentence}</pre>
{#each data.top as item (item.id)}
<p>{item.text} - {item.distance}</p>
{/each}

View File

@@ -1,21 +0,0 @@
{
"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"]
}

View File

@@ -18,4 +18,7 @@ export default defineConfig({
},
],
},
server: {
allowedHosts: ['host.docker.internal'],
},
});