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
17 changed files with 242 additions and 112 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

@@ -1,4 +1,3 @@
DATABASE_URL=file:test.db
DATABASE_TOKEN=needs_to_be_set_but_it_can_be_anything
VOYAGE_API_KEY=your_actual_api_key_here
GITHUB_WEBHOOK_SECRET=some_secret
VOYAGE_API_KEY=your_actual_api_key_here

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,10 +62,10 @@
"vitest": "^3.2.3"
},
"dependencies": {
"@libsql/client": "^0.14.0",
"@sveltejs/mcp-schema": "workspace:^",
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-http": "^0.6.3",
"tmcp": "^1.14.0",
"valibot": "^1.1.0"
"tmcp": "^1.14.0"
}
}

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,32 +0,0 @@
import * as v from 'valibot';
// not the full schema but it contains the information we need
export const github_webhook_schema = v.object({
action: v.union([v.literal('closed')]),
pull_request: v.object({
patch_url: v.string(),
merged: v.boolean(),
user: v.object({
login: v.string(),
}),
}),
});
export const github_content_schema = v.object({
name: v.string(),
path: v.string(),
sha: v.string(),
size: v.number(),
url: v.string(),
html_url: v.string(),
git_url: v.string(),
download_url: v.nullable(v.string()),
type: v.literal('file'),
content: v.string(),
encoding: v.literal('base64'),
_links: v.object({
self: v.string(),
git: v.string(),
html: v.string(),
}),
});

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

@@ -1,54 +0,0 @@
import { github_content_schema, github_webhook_schema } from '$lib/schemas/index.js';
import * as v from 'valibot';
export async function POST({ request, fetch }) {
const body = await request.json();
// TODO add secret validation
const validated_pull_request = v.safeParse(github_webhook_schema, body);
if (!validated_pull_request.success) {
return new Response('Invalid payload', { status: 400 });
}
const { pull_request } = validated_pull_request.output;
if (!pull_request.merged) {
return new Response(null, { status: 204 });
}
const patch = await fetch(pull_request.patch_url);
if (!patch.ok) {
return new Response('Failed to fetch patch', { status: 500 });
}
const patch_text = await patch.text();
const files = [
...patch_text.matchAll(
/^diff --git\sa\/(?<file>.+?)\sb\/\1\n(?:(?<action>deleted|new)\sfile mode)?/gm,
),
].map((res) => ({ file: res.groups!.file, action: res.groups?.action ?? 'modified' }));
for (const file of files) {
if (file.action === 'deleted') {
// delete path from db
continue;
}
const new_file_content = await fetch(
`https://api.github.com/repos/sveltejs/svelte.dev/contents/${file.file}`,
);
if (!new_file_content.ok) {
// push file in queue and try again later?
continue;
}
const new_file_json = await new_file_content.json();
const validated_content = v.safeParse(github_content_schema, new_file_json);
if (!validated_content.success) {
// push file in queue and try again later?
continue;
}
const content = Buffer.from(validated_content.output.content, 'base64').toString('utf-8');
// save content and distilled content in the db
console.log({ content, file: file.file });
}
return new Response(null, { status: 204 });
}

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

@@ -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;

33
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
@@ -62,9 +65,6 @@ importers:
tmcp:
specifier: ^1.14.0
version: 1.14.0(typescript@5.9.2)
valibot:
specifier: ^1.1.0
version: 1.1.0(typescript@5.9.2)
devDependencies:
'@eslint/compat':
specifier: ^1.3.2
@@ -72,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)
@@ -114,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
@@ -196,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:
@@ -3222,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'}
@@ -5778,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
@@ -5799,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
@@ -5810,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
@@ -6733,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: {}