Compare commits

..

4 Commits

Author SHA1 Message Date
jycouet
543fff6207 add remult entities 2025-09-30 08:20:51 +02:00
jycouet
9de930dddc @libsql/client needs to be in deps 2025-09-30 07:46:11 +02:00
Stanislav Khromov
07737a8edd Add Claude Code .mcp.json config 2025-09-26 20:22:50 +02:00
Stanislav Khromov
fdb7689992 fix stdio path 2025-09-26 20:21:43 +02:00
16 changed files with 245 additions and 76 deletions

View File

@@ -1,8 +1,9 @@
{
"mcpServers": {
"svelte-llm": {
"type": "http",
"url": "https://svelte-llm.stanislav.garden/mcp/mcp"
"svelte": {
"type": "stdio",
"command": "node",
"args": ["packages/mcp-stdio/dist/index.js"]
}
}
}

2
.vscode/mcp.json vendored
View File

@@ -3,7 +3,7 @@
"Svelte MCP": {
"type": "stdio",
"command": "node",
"args": ["dist/lib/stdio.js"]
"args": ["packages/mcp-stdio/dist/index.js"]
}
},
"inputs": []

View File

@@ -39,7 +39,6 @@
"devDependencies": {
"@eslint/compat": "^1.3.2",
"@eslint/js": "^9.36.0",
"@libsql/client": "^0.14.0",
"@modelcontextprotocol/inspector": "^0.16.7",
"@sveltejs/adapter-vercel": "^5.6.3",
"@sveltejs/kit": "^2.22.0",
@@ -53,6 +52,7 @@
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"remult": "3.3.0-next.1",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-eslint-parser": "^1.3.2",
@@ -62,6 +62,7 @@
"vitest": "^3.2.3"
},
"dependencies": {
"@libsql/client": "^0.14.0",
"@sveltejs/mcp-schema": "workspace:^",
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-http": "^0.6.3",

View File

@@ -1,9 +1,10 @@
import { http_transport } from '$lib/mcp/index.js';
import { db } from '$lib/server/db/index.js';
import { db, entities } from '$lib/server/db/index.js';
export async function handle({ event, resolve }) {
const mcp_response = await http_transport.respond(event.request, {
db,
entities,
});
// we are deploying on vercel the SSE connection will timeout after 5 minutes...for
// the moment we are not sending back any notifications (logs, or list changed notifications)

View File

@@ -1,9 +1,15 @@
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema.js';
import { remult, SqlDatabase } from 'remult';
import { TursoDataProvider } from 'remult/remult-turso';
// let's disable it for the moment...i can't figure out a way to make it wotk with eslint
// eslint-disable-next-line import/extensions
import { DATABASE_TOKEN, DATABASE_URL } from '$env/static/private';
import { Content } from '../../../shared/entities/Content.js';
import { ContentDistilled } from '../../../shared/entities/ContentDistilled.js';
import { Distillation } from '../../../shared/entities/Distillation.js';
import { DistillationJob } from '../../../shared/entities/DistillationJob.js';
if (!DATABASE_URL) throw new Error('DATABASE_URL is not set');
if (!DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
@@ -13,3 +19,13 @@ const client = createClient({
});
export const db = drizzle(client, { schema, logger: true });
remult.dataProvider = new SqlDatabase(new TursoDataProvider(client));
SqlDatabase.LogToConsole = true;
export const entities = {
Content,
ContentDistilled,
Distillation,
DistillationJob,
};

View File

@@ -0,0 +1,19 @@
import { Fields, type FieldOptions } from 'remult';
export function FieldVector<entityType = unknown>(
...options: (FieldOptions<entityType, number[]> & { dimensions?: number })[]
) {
const dimensions = options[0].dimensions ?? 1024;
return Fields.object<entityType, number[]>(
{
valueConverter: {
fieldTypeInDb: `F32_BLOB(${dimensions})`,
toDb: (val) => JSON.stringify(val),
toDbSql: (val) => `vector32(${val})`,
fromDb: (val: Buffer) => Array.from(new Float32Array(val)),
},
},
...options,
);
}

View File

@@ -0,0 +1,35 @@
import { Entity, Fields } from 'remult';
import { FieldVector } from '../FieldVector';
@Entity<Content>('content', {
allowApiCrud: true,
dbName: 'content',
})
export class Content {
@Fields.integer()
id!: number;
@Fields.string()
path!: string;
@Fields.string()
filename!: string;
@Fields.string()
content!: string;
@Fields.integer()
size_bytes!: number;
@FieldVector({ allowNull: true })
embeddings: number[] | null = null;
@Fields.string()
metadata = '{}';
@Fields.integer()
created_at!: number;
@Fields.integer()
updated_at!: number;
}

View File

@@ -0,0 +1,35 @@
import { Entity, Fields } from 'remult';
import { FieldVector } from '../FieldVector';
@Entity<ContentDistilled>('content_distilled', {
allowApiCrud: true,
dbName: 'content_distilled',
})
export class ContentDistilled {
@Fields.integer()
id!: number;
@Fields.string()
path!: string;
@Fields.string()
filename!: string;
@Fields.string()
content!: string;
@Fields.integer()
size_bytes!: number;
@FieldVector({ allowNull: true })
embeddings: number[] | null = null;
@Fields.string()
metadata = '{}';
@Fields.integer()
created_at!: number;
@Fields.integer()
updated_at!: number;
}

View File

@@ -0,0 +1,36 @@
import { Entity, Field, Fields } from "remult"
import { Relations } from "remult"
import { DistillationJob } from "./DistillationJob.js"
@Entity<Distillation>("distillations", {
allowApiCrud: true,
dbName: "distillations",
})
export class Distillation {
@Fields.integer()
id!: number
@Fields.string()
preset_name!: string
@Fields.string()
version!: string
@Fields.string()
content!: string
@Fields.integer()
size_kb!: number
@Fields.integer()
document_count!: number
@Fields.integer({ allowNull: true })
distillation_job_id?: number
@Relations.toOne(() => DistillationJob, { field: "distillation_job_id" })
distillation_job?: DistillationJob
@Fields.integer()
created_at!: number
}

View File

@@ -0,0 +1,64 @@
import { Entity, Fields } from 'remult';
import { Relations } from 'remult';
import { Distillation } from './Distillation.js';
@Entity<DistillationJob>('distillation_jobs', {
allowApiCrud: true,
dbName: 'distillation_jobs',
})
export class DistillationJob {
@Fields.integer()
id!: number;
@Fields.string()
preset_name!: string;
@Fields.string({ allowNull: true })
batch_id?: string;
@Fields.string()
status!: string;
@Fields.string()
model_used!: string;
@Fields.integer()
total_files!: number;
@Fields.integer()
processed_files = 0;
@Fields.integer()
successful_files = 0;
@Fields.boolean()
minimize_applied = false;
@Fields.integer()
total_input_tokens = 0;
@Fields.integer()
total_output_tokens = 0;
@Fields.integer({ allowNull: true })
started_at?: number;
@Fields.integer({ allowNull: true })
completed_at?: number;
@Fields.string({ allowNull: true })
error_message?: string;
@Fields.string()
metadata = '{}';
@Fields.integer()
created_at!: number;
@Fields.integer()
updated_at!: number;
// Relations toMany
@Relations.toMany(() => Distillation)
distillations?: Distillation[];
}

View File

@@ -9,7 +9,8 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"experimentalDecorators": true
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files

View File

@@ -36,6 +36,7 @@
"@sveltejs/kit": "^2.42.2",
"@types/eslint-scope": "^8.3.2",
"@types/estree": "^1.0.8",
"@typescript-eslint/types": "^8.44.0"
"@typescript-eslint/types": "^8.44.0",
"remult": "3.3.0-next.1"
}
}

View File

@@ -117,36 +117,6 @@ describe('add_autofixers_issues', () => {
);
});
});
it('should add a suggestion when calling a function inside an effect', () => {
const content = run_autofixers_on_code(`
<script>
import { fetch_data } from './data.js';
$effect(() => {
fetch_data();
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are calling the function \`fetch_data\` inside an $effect. Please check if the function is reassigning a stateful variable because that's considered malpractice and check if it could use \`$derived\` instead. Ignore this suggestion if you are sure this function is not assigning any stateful variable or if you can't check if it does.`,
);
});
it('should add a suggestion when calling a function inside an effect (with non identifier callee)', () => {
const content = run_autofixers_on_code(`
<script>
import { fetch_data } from './data.js';
$effect(() => {
fetch_data.fetch();
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are calling a function inside an $effect. Please check if the function is reassigning a stateful variable because that's considered malpractice and check if it could use \`$derived\` instead. Ignore this suggestion if you are sure this function is not assigning any stateful variable or if you can't check if it does.`,
);
});
});
with_possible_inits('($init)', ({ init }) => {

View File

@@ -1,10 +1,4 @@
import type {
AssignmentExpression,
CallExpression,
Identifier,
Node,
UpdateExpression,
} from 'estree';
import type { AssignmentExpression, Identifier, Node, UpdateExpression } from 'estree';
import type { Autofixer, AutofixerState } from './index.js';
import { left_most_id } from '../ast/utils.js';
import type { AST } from 'svelte-eslint-parser';
@@ -33,9 +27,9 @@ function run_if_in_effect(
}
}
function assign_or_update_visitor(
function visitor(
node: UpdateExpression | AssignmentExpression,
{ state, path, next }: Context<Node | AST.SvelteNode, AutofixerState>,
{ state, path }: Context<Node | AST.SvelteNode, AutofixerState>,
) {
run_if_in_effect(path, state, () => {
function check_if_stateful_id(id: Identifier) {
@@ -64,25 +58,9 @@ function assign_or_update_visitor(
}
}
});
next();
}
function call_expression_visitor(
node: CallExpression,
{ state, path, next }: Context<Node | AST.SvelteNode, AutofixerState>,
) {
run_if_in_effect(path, state, () => {
const function_name =
node.callee.type === 'Identifier' ? `the function \`${node.callee.name}\`` : 'a function';
state.output.suggestions.push(
`You are calling ${function_name} inside an $effect. Please check if the function is reassigning a stateful variable because that's considered malpractice and check if it could use \`$derived\` instead. Ignore this suggestion if you are sure this function is not assigning any stateful variable or if you can't check if it does.`,
);
});
next();
}
export const assign_in_effect: Autofixer = {
UpdateExpression: assign_or_update_visitor,
AssignmentExpression: assign_or_update_visitor,
CallExpression: call_expression_visitor,
UpdateExpression: visitor,
AssignmentExpression: visitor,
};

View File

@@ -3,6 +3,7 @@ import { McpServer } from 'tmcp';
import { setup_prompts, setup_resources, setup_tools } from './handlers/index.js';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import type { Schema } from '@sveltejs/mcp-schema';
import type { ClassType } from 'remult';
export const server = new McpServer(
{
@@ -21,7 +22,7 @@ export const server = new McpServer(
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.',
},
).withContext<{ db: LibSQLDatabase<Schema> }>();
).withContext<{ db: LibSQLDatabase<Schema>; entities: Record<string, ClassType<unknown>> }>();
export type SvelteMcp = typeof server;

30
pnpm-lock.yaml generated
View File

@@ -25,7 +25,7 @@ importers:
version: 10.1.8(eslint@9.36.0(jiti@2.6.0))
eslint-plugin-import:
specifier: ^2.32.0
version: 2.32.0(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0))
version: 2.32.0(eslint@9.36.0(jiti@2.6.0))
eslint-plugin-svelte:
specifier: ^3.12.3
version: 3.12.4(eslint@9.36.0(jiti@2.6.0))(svelte@5.39.6)(ts-node@10.9.2(@types/node@24.5.2)(typescript@5.9.2))
@@ -50,6 +50,9 @@ importers:
apps/mcp-remote:
dependencies:
'@libsql/client':
specifier: ^0.14.0
version: 0.14.0
'@sveltejs/mcp-schema':
specifier: workspace:^
version: link:../../packages/mcp-schema
@@ -69,9 +72,6 @@ importers:
'@eslint/js':
specifier: ^9.36.0
version: 9.36.0
'@libsql/client':
specifier: ^0.14.0
version: 0.14.0
'@modelcontextprotocol/inspector':
specifier: ^0.16.7
version: 0.16.8(@types/node@24.5.2)(typescript@5.9.2)
@@ -111,6 +111,9 @@ importers:
prettier-plugin-svelte:
specifier: ^3.3.3
version: 3.4.0(prettier@3.6.2)(svelte@5.39.6)
remult:
specifier: 3.3.0-next.1
version: 3.3.0-next.1
svelte:
specifier: ^5.0.0
version: 5.39.6
@@ -193,6 +196,9 @@ importers:
'@typescript-eslint/types':
specifier: ^8.44.0
version: 8.44.1
remult:
specifier: 3.3.0-next.1
version: 3.3.0-next.1
packages/mcp-stdio:
dependencies:
@@ -3219,6 +3225,9 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
remult@3.3.0-next.1:
resolution: {integrity: sha512-41psdKqvkch17x/lOT4fJ9iE5Ee1MchZ1yrlWGCV8oSsXhdp9H8/eIay2LPp62c3YBx9CgcZgpwwXVmRnzoWdw==}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@@ -5775,17 +5784,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.0)):
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.0)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
eslint: 9.36.0(jiti@2.6.0)
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0)):
eslint-plugin-import@2.32.0(eslint@9.36.0(jiti@2.6.0)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -5796,7 +5804,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.36.0(jiti@2.6.0)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.0))
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.0))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -5807,8 +5815,6 @@ snapshots:
semver: 6.3.1
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -6730,6 +6736,10 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
remult@3.3.0-next.1:
dependencies:
tslib: 2.8.1
require-directory@2.1.1: {}
resolve-from@4.0.0: {}