Compare commits

..

16 Commits

Author SHA1 Message Date
paoloricciuti
26d30c73c7 feat: add Svelte task prompt 2025-09-18 19:48:51 +02:00
Paolo Ricciuti
13c4832d1b Merge pull request #7 from sveltejs/autofixer-folders-and-tests 2025-09-18 15:42:33 +02:00
paoloricciuti
7c7a1f939f fix: restructure autofixer folders and add tests 2025-09-18 14:54:41 +02:00
Paolo Ricciuti
5becaf3b5e Merge pull request #1 from sveltejs/copy-tables-from-current-mcp 2025-09-17 23:06:36 +02:00
Stanislav Khromov
7f858929ad Merge branch 'main' into copy-tables-from-current-mcp 2025-09-17 22:34:56 +02:00
Stanislav Khromov
6b5588c2f9 Merge pull request #6 from sveltejs/add-devtools
Dev tools docs
2025-09-17 22:34:43 +02:00
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
paoloricciuti
b4259dd117 chore: copy tables from current mcp 2025-09-11 23:05:09 +02:00
25 changed files with 2527 additions and 199 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

@@ -12,11 +12,23 @@ pnpm dev
1. Set the VOYAGE_API_KEY for embeddings support
#### Optional tools
### Local dev tools
#### MCP inspector
```
docker-compose up
pnpm run inspect
```
- MCP Inspector: http://localhost:6274/ (Connect with `http://host.docker.internal:5173/mcp` + Streamable HTTP)
- http://localhost:8081/ - Adminer SQLite frontend
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

@@ -1,31 +0,0 @@
<?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";

View File

@@ -1,25 +0,0 @@
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,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 --host",
"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,42 +0,0 @@
import type { Node } from 'estree';
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 }
>;
export const assign_in_effect: Autofixer = {
AssignmentExpression(node, { path, state }) {
const in_effect = path.findLast(
(node) =>
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === '$effect',
);
if (
in_effect &&
in_effect.type === 'CallExpression' &&
(in_effect.callee.type === 'Identifier' || in_effect.callee.type === 'MemberExpression')
) {
if (state.parsed.is_rune(in_effect, ['$effect', '$effect.pre'])) {
if (node.left.type === 'Identifier') {
const reference = state.parsed.find_reference_by_id(node.left);
const definition = reference?.resolved?.defs[0];
if (definition && definition.type === 'Variable') {
const init = definition.node.init;
if (
init?.type === 'CallExpression' &&
state.parsed.is_rune(init, ['$state', '$state.raw'])
) {
state.output.suggestions.push(
`The stateful variable "${node.left.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,
);
}
}
}
}
}
},
};

View File

@@ -0,0 +1,113 @@
import { describe, expect, it } from 'vitest';
import { add_autofixers_issues } from './add-autofixers-issues';
describe('add_autofixers_issues', () => {
describe('assign_in_effect', () => {
it('should add suggestions when assigning to a stateful variable inside an effect', () => {
const content = { issues: [], suggestions: [] };
const code = `
<script>
const count = $state(0);
$effect(() => {
count = 43;
});
</script>`;
add_autofixers_issues(content, code, 5);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it('should add a suggestion for each variable assigned within an effect', () => {
const content = { issues: [], suggestions: [] };
const code = `
<script>
const count = $state(0);
const count2 = $state(0);
$effect(() => {
count = 43;
count2 = 44;
});
</script>`;
add_autofixers_issues(content, code, 5);
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
expect(content.suggestions).toContain(
'The stateful variable "count2" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it('should not add a suggestion for variables that are not assigned within an effect', () => {
const content = { issues: [], suggestions: [] };
const code = `
<script>
const count = $state(0);
</script>
<button onclick={() => count = 43}>Increment</button>
`;
add_autofixers_issues(content, code, 5);
expect(content.suggestions).not.toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it("should not add a suggestions for variables that are assigned within an effect but aren't stateful", () => {
const content = { issues: [], suggestions: [] };
const code = `
<script>
const count = 0;
$effect(() => {
count = 43;
});
</script>`;
add_autofixers_issues(content, code, 5);
expect(content.suggestions).not.toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it('should add a suggestion for variables that are assigned within an effect with an update', () => {
const content = { issues: [], suggestions: [] };
const code = `
<script>
let count = $state(0);
$effect(() => {
count++;
});
</script>
`;
add_autofixers_issues(content, code, 5);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it('should add a suggestion for variables that are mutated within an effect', () => {
const content = { issues: [], suggestions: [] };
const code = `
<script>
let count = $state({ value: 0 });
$effect(() => {
count.value = 42;
});
</script>
`;
add_autofixers_issues(content, code, 5);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
});
});

View File

@@ -0,0 +1,22 @@
import { parse } from '../../parse/parse.js';
import { walk } from '../../index.js';
import type { Node } from 'estree';
import * as autofixers from './visitors/index.js';
export function add_autofixers_issues(
content: { issues: string[]; suggestions: string[] },
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
) {
const parsed = parse(code, filename);
// 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,
);
}
}

View File

@@ -0,0 +1,20 @@
import { compile } from 'svelte/compiler';
export function add_compile_issues(
content: { issues: string[]; suggestions: string[] },
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
) {
const compilation_result = compile(code, {
filename: filename || 'Component.svelte',
generate: false,
runes: desired_svelte_version >= 5,
});
for (const warning of compilation_result.warnings) {
content.issues.push(
`${warning.message} at line ${warning.start?.line}, column ${warning.start?.column}`,
);
}
}

View File

@@ -0,0 +1,90 @@
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,
},
},
},
];
}
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,
},
}),
}));
}
export async function add_eslint_issues(
content: { issues: string[]; suggestions: string[] },
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
) {
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}`,
);
}
}
}

View File

@@ -0,0 +1,16 @@
import type { Identifier, MemberExpression } from 'estree';
/**
* Gets the left-most identifier of a member expression or identifier.
*/
export function left_most_id(expression: MemberExpression | Identifier) {
while (expression.type === 'MemberExpression') {
expression = expression.object as MemberExpression | Identifier;
}
if (expression.type !== 'Identifier') {
return null;
}
return expression;
}

View File

@@ -0,0 +1,62 @@
import type { AssignmentExpression, Identifier, Node, UpdateExpression } from 'estree';
import type { Autofixer, AutofixerState } from '.';
import { left_most_id } from '../ast/utils';
import type { SvelteNode } from 'svelte-eslint-parser/lib/ast';
import type { Context } from 'zimmerframe';
function run_if_in_effect(path: (Node | SvelteNode)[], state: AutofixerState, to_run: () => void) {
const in_effect = path.findLast(
(node) =>
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === '$effect',
);
if (
in_effect &&
in_effect.type === 'CallExpression' &&
(in_effect.callee.type === 'Identifier' || in_effect.callee.type === 'MemberExpression')
) {
if (state.parsed.is_rune(in_effect, ['$effect', '$effect.pre'])) {
to_run();
}
}
}
function visitor(
node: UpdateExpression | AssignmentExpression,
{ state, path }: Context<Node | SvelteNode, AutofixerState>,
) {
run_if_in_effect(path, state, () => {
function check_if_stateful_id(id: Identifier) {
const reference = state.parsed.find_reference_by_id(id);
const definition = reference?.resolved?.defs[0];
if (definition && definition.type === 'Variable') {
const init = definition.node.init;
if (
init?.type === 'CallExpression' &&
state.parsed.is_rune(init, ['$state', '$state.raw'])
) {
state.output.suggestions.push(
`The stateful variable "${id.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,
);
}
}
}
const variable = node.type === 'UpdateExpression' ? node.argument : node.left;
if (variable.type === 'Identifier') {
check_if_stateful_id(variable);
} else if (variable.type === 'MemberExpression') {
const object = left_most_id(variable);
if (object) {
check_if_stateful_id(object);
}
}
});
}
export const assign_in_effect: Autofixer = {
UpdateExpression: visitor,
AssignmentExpression: visitor,
};

View File

@@ -0,0 +1,14 @@
import type { Node } from 'estree';
import type { AST } from 'svelte-eslint-parser';
import type { Visitors } from 'zimmerframe';
import type { ParseResult } from '../../../parse/parse.js';
export type AutofixerState = {
output: { issues: string[]; suggestions: string[] };
parsed: ParseResult;
desired_svelte_version: number;
};
export type Autofixer = Visitors<Node | AST.SvelteNode, AutofixerState>;
export * from './assign-in-effect.js';

View File

@@ -1,12 +1,11 @@
import { walk } from '../index.js';
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
import { HttpTransport } from '@tmcp/transport-http';
import { StdioTransport } from '@tmcp/transport-stdio';
import type { Node } from 'estree';
import { McpServer } from 'tmcp';
import * as v from 'valibot';
import { parse } from '../server/analyze/parse.js';
import * as autofixers from './autofixers.js';
import { add_autofixers_issues } from './autofixers/add-autofixers-issues.js';
import { add_compile_issues } from './autofixers/add-compile-issues.js';
import { add_eslint_issues } from './autofixers/add-eslint-issues.js';
const server = new McpServer(
{
@@ -23,7 +22,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 +34,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 +54,27 @@ 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 {
add_compile_issues(content, code, desired_svelte_version, filename);
const parsed = parse(code, filename ?? 'Component.svelte');
add_autofixers_issues(content, code, desired_svelte_version, filename);
// 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);
await add_eslint_issues(content, code, desired_svelte_version, filename);
} 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 {
@@ -70,6 +89,46 @@ server.tool(
},
);
server.prompt(
{
name: 'svelte-task-prompt',
title: 'Svelte Task Prompt',
description:
'Use this Prompt to ask for any svelte related task. It will automatically instruct the LLM on how to best use the autofixer and how to query for documentation pages.',
schema: v.object({
task: v.pipe(v.string(), v.description('The task to be performed')),
}),
},
async ({ task }) => {
// TODO: implement logic to fetch the available docs paths to return in the prompt
const available_docs: string[] = [];
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool \`get_documentation\` with one of the following paths:
<available-docs-paths>
${JSON.stringify(available_docs, null, 2)}
</available-docs-paths>
Every time you write a Svelte component or a Svelte module you MUST invoke the \`svelte-autofixer\` tool providing the code. The tool will return a list of issues or suggestions. If there are any issues or suggestions you MUST fix them and call the tool again with the updated code. You MUST keep doing this until the tool returns no issues or suggestions. Only then you can return the code to the user.
This is the task you will work on:
<task>
${task}
</task>
`,
},
},
],
};
},
);
export const http_transport = new HttpTransport(server, {
cors: true,
});

View File

@@ -12,9 +12,80 @@ import { float_32_array } from './utils';
* to the generated migration file
*/
// 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', {
export const distillations = sqliteTable('distillations', {
id: integer('id').primaryKey(),
text: text('text'),
vector: float_32_array('vector', { dimensions: 3 }),
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()),
});

View File

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

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();

View File

@@ -1,50 +0,0 @@
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,21 +1 @@
<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}

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"]
}

View File

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