Compare commits

..

98 Commits

Author SHA1 Message Date
paoloricciuti
199d57a8e3 feat: wip github webhook 2025-09-26 14:59:47 +02:00
paoloricciuti
47fa0a4382 fix: add $derived and $derived.by to wrong-property-access-state 2025-09-26 12:39:40 +02:00
paoloricciuti
4c6232a44f fix: add $derived and $derived.by to the reassignment list 2025-09-26 12:33:33 +02:00
paoloricciuti
8edbf2f36b fix: disable todos tools 2025-09-26 12:15:11 +02:00
paoloricciuti
6e54719f88 chore: update deps to fix tmcp issue 2025-09-26 10:01:45 +02:00
paoloricciuti
6b15eb0790 feat: add .$ property access autofixer 2025-09-24 15:07:00 +02:00
Paolo Ricciuti
a7041a4c5e Merge pull request #23 from sveltejs/setup-cli-project 2025-09-24 15:04:08 +02:00
paoloricciuti
023bea317f Merge branch 'main' into setup-cli-project 2025-09-24 10:45:48 +02:00
paoloricciuti
7a6cba8772 fix: add suggestion for $state variable_name 2025-09-24 10:45:15 +02:00
paoloricciuti
fd32b67442 fix: lint and check 2025-09-24 10:44:41 +02:00
paoloricciuti
0e3b1ba22f fix: right replacement for __filename 2025-09-24 10:13:19 +02:00
paoloricciuti
0aad39d076 fix: package.json setup and tsdown config 2025-09-24 10:09:42 +02:00
paoloricciuti
2fec290d54 fix: lint and check 2025-09-24 10:05:24 +02:00
paoloricciuti
4e59ef751a fix: rename project 2025-09-24 10:05:15 +02:00
paoloricciuti
c87c9e0715 feat: setup cli project 2025-09-24 09:43:06 +02:00
Paolo Ricciuti
12f8d84852 Merge pull request #22 from sveltejs/great-monorepo-restructure 2025-09-23 23:02:37 +02:00
paoloricciuti
bde37da5d5 chore: add .editorconfig, .gitattributes and renovate.json 2025-09-23 23:01:26 +02:00
paoloricciuti
a50844e388 fix: remove double gitignore 2025-09-23 22:57:14 +02:00
paoloricciuti
6a71229d56 fix: remove ts.config 2025-09-23 18:17:26 +02:00
paoloricciuti
92d8532c8a chore: add DATABASE_TOKEN 2025-09-23 18:10:35 +02:00
paoloricciuti
c8300bc62e fix: lint and typecheck 2025-09-23 18:02:10 +02:00
paoloricciuti
de78f7663f chore: add check recursive script 2025-09-23 16:56:45 +02:00
paoloricciuti
e57b76324f chore: add build script 2025-09-23 16:51:38 +02:00
paoloricciuti
09331e2c2b chore: add eslint locally 2025-09-23 16:50:58 +02:00
paoloricciuti
f1aef9ca2f chore: great monorepo restructuring 2025-09-23 16:45:05 +02:00
paoloricciuti
6c072534ea fix: use installed inspector and remove DANGEROUSLY_OMIT_AUTH 2025-09-23 11:22:26 +02:00
paoloricciuti
2f8165f1d7 chore: add issue templates 2025-09-23 09:28:36 +02:00
Paolo Ricciuti
d93d3a3507 Merge pull request #17 from sveltejs/playground-link-tool 2025-09-22 23:54:41 +02:00
paoloricciuti
039718f1a5 fix: ask the user to generate instead of generate by default 2025-09-22 23:48:24 +02:00
paoloricciuti
ac287a2c83 feat: playground-link tool 2025-09-22 15:57:59 +02:00
paoloricciuti
224d630a32 chore: add explicit extensions eslint rule 2025-09-22 15:11:38 +02:00
Paolo Ricciuti
4a9afb5ee1 Merge pull request #16 from sveltejs/restructure 2025-09-22 10:46:42 +02:00
Paolo Ricciuti
e68067e995 Merge pull request #15 from sveltejs/no-lib-eslint-rul 2025-09-22 10:35:04 +02:00
paoloricciuti
8258a1c9ba fix: lint 2025-09-22 10:33:35 +02:00
paoloricciuti
5aa2827c91 chore: add eslint to prevent importing with $lib inthe lib folder 2025-09-22 10:00:37 +02:00
paoloricciuti
a35d72cc6b fix: update deps 2025-09-22 09:44:18 +02:00
paoloricciuti
0c35883074 Merge branch 'main' into init-docs 2025-09-21 16:22:26 +02:00
paoloricciuti
d82c20acd6 fix: update to latest transport 2025-09-21 16:22:06 +02:00
paoloricciuti
cc3ea75c7f fix: lint 2025-09-21 14:52:11 +02:00
paoloricciuti
68724731c7 chore: add snippets to create autofixers and stup functions 2025-09-21 14:50:01 +02:00
paoloricciuti
bf1a4178bf fix: move handler in their own folder while keeping TS 2025-09-21 14:49:44 +02:00
paoloricciuti
050e588709 Merge branch 'main' into init-docs 2025-09-21 13:54:14 +02:00
Paolo Ricciuti
731b4f6548 Merge pull request #14 from sveltejs/close-sse-stream 2025-09-21 13:50:22 +02:00
paoloricciuti
582e0e1dea fix: close SSE stream immediately 2025-09-20 23:53:42 +02:00
Paolo Ricciuti
bb9a6e07ea Merge pull request #13 from sveltejs/import-stores-autofixer
feat: add use runes instead of stores autofixer
2025-09-20 09:19:36 +02:00
Stanislav Khromov
0d17b81948 Merge pull request #12 from sveltejs/eslint-conventions
chore: add eslint rules for naming convention and function declaration
2025-09-20 02:18:16 +02:00
paoloricciuti
81640c9a16 fix: describe filename and accept string version 2025-09-20 01:39:17 +02:00
paoloricciuti
8587bc8625 fix: wat? 2025-09-20 01:22:48 +02:00
paoloricciuti
0475e3b0f9 fix: us ts parser in svelte parser 2025-09-19 22:47:36 +02:00
paoloricciuti
4e1a42ab52 fix: remove from recommended 2025-09-19 22:21:10 +02:00
paoloricciuti
862f614afc fix: duh 2025-09-19 22:09:59 +02:00
paoloricciuti
a92ae954bd fix: nuke rules completely 2025-09-19 22:08:11 +02:00
paoloricciuti
e3b5188c6d fix: maybe this? 2025-09-19 22:02:13 +02:00
paoloricciuti
6b5f2092b5 fix: again 2025-09-19 21:56:32 +02:00
paoloricciuti
089e690f3e fix: what about this? 2025-09-19 21:53:04 +02:00
paoloricciuti
1c60e350a6 fix: what about this? 2025-09-19 20:38:30 +02:00
paoloricciuti
a93a6554b5 fix: is this the issue? 2025-09-19 20:37:07 +02:00
paoloricciuti
94f7d65db3 fix: latest plugin svelte? 2025-09-19 19:48:56 +02:00
paoloricciuti
d7492bb1cb fix: more stuff to deps? 2025-09-19 19:43:30 +02:00
paoloricciuti
e1e2bf68ae fix: move typescript eslint to the dependencies? 2025-09-19 19:39:41 +02:00
paoloricciuti
0ff628f5b4 fix: latest eslint maybe? 2025-09-19 19:32:40 +02:00
paoloricciuti
ea35d600e4 fix: lock 2025-09-19 19:18:49 +02:00
paoloricciuti
dcbcd5b690 fix: move eslint deps to dependencies 2025-09-19 19:16:38 +02:00
paoloricciuti
9f580a36ef feat: add use runes instead of stores autofixer 2025-09-19 16:28:23 +02:00
paoloricciuti
d81d6a3d95 chore: add eslint rules for naming convention and function declaration 2025-09-19 12:30:44 +02:00
paoloricciuti
f04cb139e3 chore: change setup to allow for hosted turso version 2025-09-19 12:21:47 +02:00
Stanislav Khromov
dedfd0b3b7 Move auto fixer handler 2025-09-19 01:20:04 +02:00
Stanislav Khromov
5d50518c3c types 2025-09-19 01:17:02 +02:00
Stanislav Khromov
74d2fb8f0e Update getDocumentationHandler.ts 2025-09-19 01:15:58 +02:00
Stanislav Khromov
86675ea1d7 Update index.ts 2025-09-19 01:08:03 +02:00
Stanislav Khromov
830fd73ab1 move handlers to separate directory 2025-09-19 01:06:53 +02:00
Stanislav Khromov
da995bdc69 Update index.ts 2025-09-19 01:05:19 +02:00
Stanislav Khromov
26b3986740 wip 2025-09-19 01:01:46 +02:00
Stanislav Khromov
8d53f56151 add tmcp docs 2025-09-19 00:55:36 +02:00
Paolo Ricciuti
fd355e872d Merge pull request #9 from sveltejs/task-prompt
feat: add Svelte task prompt
2025-09-19 00:38:49 +02:00
Paolo Ricciuti
d4edef1d82 Merge pull request #8 from sveltejs/set-update-autofixer
feat: add `set` or `update` stateful variables autofixer & imported runes autofixer
2025-09-19 00:38:29 +02:00
paoloricciuti
26d30c73c7 feat: add Svelte task prompt 2025-09-18 19:48:51 +02:00
paoloricciuti
13719d0646 feat: add derived with functions suggestions 2025-09-18 17:45:51 +02:00
paoloricciuti
4c8cadd585 feat: add imported runes autofixer 2025-09-18 17:07:31 +02:00
paoloricciuti
72e89af60f feat: add set or update stateful variables autofixer 2025-09-18 16:32:25 +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
Stanislav Khromov
037f830557 Merge pull request #3 from sveltejs/ci
Add GitHub Actions for type checking and linting
2025-09-17 00:03:03 +02:00
paoloricciuti
7c3eb030ce fix: run autofixers in separate walks to avoid messing with the tree traverse flow 2025-09-16 22:51:07 +02:00
paoloricciuti
2ded5711fb chore: use .js imports 2025-09-16 21:55:08 +02:00
paoloricciuti
b4259dd117 chore: copy tables from current mcp 2025-09-11 23:05:09 +02:00
88 changed files with 6583 additions and 898 deletions

2
.cocoignore Normal file
View File

@@ -0,0 +1,2 @@
.claude
.github

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true

View File

@@ -1,2 +0,0 @@
DATABASE_URL=file:test.db
VOYAGE_API_KEY=your_actual_api_key_here

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
* text=auto eol=lf
/packages/**/test/** -linguist-detectable
/packages/**/fixtures/** -linguist-detectable

View File

@@ -0,0 +1,36 @@
name: 'Autofixer Request'
description: Request a new Autofixer for the MCP
title: '[Autofixer Request] '
labels: [enhancement, autofixer]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to request A new autofixer!
- type: textarea
id: problem
attributes:
label: Describe the problematic code
description: Please provide a clear and concise description the problem. Is much better if you can provide a code snippet the AI constantly get's wrong.
placeholder: The AI keeps messing with...
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: Describe what the autofixer should suggest
description: If you were looking at this code, what would you suggest to the AI for it to fix it?
placeholder: You should never do this, instead do that...
validations:
required: true
- type: dropdown
id: importance
attributes:
label: Importance
description: How important is this feature to you?
options:
- nice to have
- would make my life easier
- the MCP is useless to me without it
validations:
required: true

52
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: "\U0001F41E Bug report"
description: Report an issue with Svelte
title: '[Bug] '
labels: ['triage: bug']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Please provide a link to a repo or REPL that can reproduce the problem you ran into. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "need reproduction" label. If no reproduction is provided within a reasonable time-frame, the issue will be closed.
placeholder: Reproduction
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs
description: 'Please provide some logs or screenshot of the agentic workflow failing.'
render: shell
- type: input
id: mcp-client
attributes:
label: MCP Client
description: Which MCP client are you using?
render: shell
placeholder: claude-code, codex, opencode
validations:
required: true
- type: dropdown
id: severity
attributes:
label: Severity
description: Select the severity of this issue
options:
- annoyance
- minor functionality loss
- major functionality loss
- blocking all usage of the mcp
validations:
required: true

View File

@@ -0,0 +1,36 @@
name: 'Feature Request'
description: Request a new MCP feature
title: '[Feature Request] '
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to request this feature! If your feature request is complex or substantial enough to warrant in-depth discussion, maintainers may close the issue and ask you to open an [RFC](https://github.com/sveltejs/rfcs).
- type: textarea
id: problem
attributes:
label: Describe the problem
description: Please provide a clear and concise description the problem this feature would solve. The more information you can provide here, the better.
placeholder: I'm always frustrated when...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the proposed solution
description: Please provide a clear and concise description of what you would like to happen.
placeholder: I would like to see...
validations:
required: true
- type: dropdown
id: importance
attributes:
label: Importance
description: How important is this feature to you?
options:
- nice to have
- would make my life easier
- the MCP is useless to me without it
validations:
required: true

View File

@@ -33,4 +33,5 @@ jobs:
run: pnpm run check
env:
DATABASE_URL: file:test.db
DATABASE_TOKEN: dummy-key
VOYAGE_API_KEY: dummy-key

View File

@@ -34,3 +34,4 @@ jobs:
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key

View File

@@ -34,9 +34,11 @@ jobs:
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key
- name: Run tests
run: pnpm run test
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key

28
.gitignore vendored
View File

@@ -1,12 +1,16 @@
node_modules
/apps/**/node_modules
/packages/**/node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
/apps/**/.output
/apps/**/.vercel
/apps/**/.netlify
/apps/**/.wrangler
/**/.svelte-kit
/apps/**/build
/apps/**/dist
/packages/**/dist
# OS
.DS_Store
@@ -14,13 +18,25 @@ Thumbs.db
# Env
.env
/apps/**/.env
/packages/**/.env
.env.*
/apps/**/.env.*
/packages/**/.env.*
!.env.example
/apps/**/!.env.example
/packages/**/!.env.example
!.env.test
/apps/**/!.env.test
/packages/**/!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/apps/**/vite.config.js.timestamp-*
/apps/**/vite.config.ts.timestamp-*
# SQLite
*.db
/apps/**/*.db
/packages/**/*.db

View File

@@ -8,3 +8,4 @@ bun.lockb
# Miscellaneous
/static/
/drizzle/
/**/.svelte-kit/*

33
.vscode/mcp-snippets.code-snippets vendored Normal file
View File

@@ -0,0 +1,33 @@
{
// Place your svelte-mcp workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
"Setup Function": {
"scope": "javascript,typescript",
"prefix": "!setup-mcp",
"body": [
"import type { SvelteMcp } from '../../index.js';",
"import * as v from 'valibot';",
"",
"export function ${1:function_name}(server: SvelteMcp) {",
"\t$0",
"}",
],
"description": "Create a setup function for a tool/resource/prompt handler",
},
"Autofixer": {
"scope": "javascript,typescript",
"prefix": "!autofixer",
"body": [
"import type { Autofixer } from '.';",
"export const ${1:autofixer_name}: Autofixer = {",
"\t$0",
"};",
],
"description": "Create a setup export for an autofixer",
},
}

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,27 @@ pnpm dev
```
1. Set the VOYAGE_API_KEY for embeddings support
> [!NOTE]
> Currently to prevent having a bunch of Timeout logs on vercel we shut down the SSE channel immediately. This means that we can't use `server.log` and we are not sending `list-changed` notifications. We can use elicitation and sampling since those are sent on the same stream of the POST request
### 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

@@ -0,0 +1,4 @@
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

View File

@@ -1,11 +1,12 @@
import { defineConfig } from 'drizzle-kit';
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
if (!process.env.DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
export default defineConfig({
schema: './src/lib/server/db/schema.ts',
dialect: 'sqlite',
dbCredentials: { url: process.env.DATABASE_URL },
dialect: 'turso',
dbCredentials: { url: process.env.DATABASE_URL, authToken: process.env.DATABASE_TOKEN },
verbose: true,
strict: true,
});

View File

@@ -0,0 +1,71 @@
{
"name": "@sveltejs/mcp-remote",
"version": "0.0.1",
"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",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"lint:fix": "prettier --write . && eslint . --fix",
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"test:watch": "npm run test:unit -- --watch",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"inspect": "pnpm mcp-inspector"
},
"keywords": [
"svelte",
"tmcp",
"mcp",
"server"
],
"private": true,
"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",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@types/node": "^24.3.1",
"@typescript-eslint/parser": "^8.44.0",
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.40.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.12.3",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-eslint-parser": "^1.3.2",
"typescript": "^5.0.0",
"vite": "^7.0.4",
"vite-plugin-devtools-json": "^1.0.0",
"vitest": "^3.2.3"
},
"dependencies": {
"@sveltejs/mcp-schema": "workspace:^",
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-http": "^0.6.3",
"tmcp": "^1.14.0",
"valibot": "^1.1.0"
}
}

View File

@@ -0,0 +1,23 @@
import { http_transport } from '$lib/mcp/index.js';
import { db } from '$lib/server/db/index.js';
export async function handle({ event, resolve }) {
const mcp_response = await http_transport.respond(event.request, {
db,
});
// 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)
// so it's a waste of resources to keep a connection open that will error
// after 5 minutes making the logs dirty. For this reason if we have a response from
// the MCP server and it's a GET request we just return an empty response (it has to be
// 200 or the MCP client will complain)
if (mcp_response && event.request.method === 'GET') {
try {
await mcp_response.body?.cancel();
} catch {
// ignore
}
return new Response('', { status: 200 });
}
return mcp_response ?? resolve(event);
}

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

View File

@@ -0,0 +1,6 @@
import { server } from '@sveltejs/mcp-server';
import { HttpTransport } from '@tmcp/transport-http';
export const http_transport = new HttpTransport(server, {
cors: true,
});

View File

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,15 @@
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema.js';
// 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';
if (!DATABASE_URL) throw new Error('DATABASE_URL is not set');
if (!DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
const client = createClient({
url: DATABASE_URL,
authToken: DATABASE_TOKEN,
});
export const db = drizzle(client, { schema, logger: true });

View File

@@ -0,0 +1,2 @@
// we need to re-export from here to allow for the drizzle config to pick them up for migrations
export * from '@sveltejs/mcp-schema/schema';

View File

@@ -0,0 +1 @@
<h1>Official Svelte MCP</h1>

View File

@@ -0,0 +1,54 @@
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 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// 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
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

View File

@@ -0,0 +1,22 @@
import devtoolsJson from 'vite-plugin-devtools-json';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit(), devtoolsJson()],
// we don't have tests yet so we just comment this out for now
// test: {
// expect: { requireAssertions: true },
// projects: [
// {
// extends: './vite.config.ts',
// test: {
// name: 'server',
// environment: 'node',
// include: ['src/**/*.{test,spec}.{js,ts}'],
// exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
// },
// },
// ],
// },
});

320
docs/tmcp.md Normal file
View File

@@ -0,0 +1,320 @@
> [!WARNING]
> Unfortunately i published the 1.0 by mistake...this package is currently under heavy development so there will be breaking changes in minors...threat this `1.x` as the `0.x` of any other package. Sorry for the disservice, every breaking will be properly labeled in the PR name.
# tmcp
A lightweight, schema-agnostic Model Context Protocol (MCP) server implementation with unified API design.
## Why tmcp?
tmcp offers significant advantages over the official MCP SDK:
- **🔄 Schema Agnostic**: Works with any validation library through adapters
- **📦 No Weird Dependencies**: Minimal footprint with only essential dependencies (looking at you `express`)
- **🎯 Unified API**: Consistent, intuitive interface across all MCP capabilities
- **🔌 Extensible**: Easy to add support for new schema libraries
- **⚡ Lightweight**: No bloat, just what you need
## Supported Schema Libraries
tmcp works with all major schema validation libraries through its adapter system:
- **Zod** - `@tmcp/adapter-zod`
- **Valibot** - `@tmcp/adapter-valibot`
- **ArkType** - `@tmcp/adapter-arktype`
- **Effect Schema** - `@tmcp/adapter-effect`
- **Zod v3** - `@tmcp/adapter-zod-v3`
## Installation
```bash
pnpm install tmcp
# Choose your preferred schema library adapter
pnpm install @tmcp/adapter-zod zod
# Choose your preferred transport
pnpm install @tmcp/transport-stdio # For CLI/desktop apps
pnpm install @tmcp/transport-http # For web-based clients
```
## Quick Start
### Standard I/O Transport (CLI/Desktop)
```javascript
import { McpServer } from 'tmcp';
import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
import { StdioTransport } from '@tmcp/transport-stdio';
import { z } from 'zod';
const adapter = new ZodJsonSchemaAdapter();
const server = new McpServer(
{
name: 'my-server',
version: '1.0.0',
description: 'My awesome MCP server',
},
{
adapter,
capabilities: {
tools: { listChanged: true },
prompts: { listChanged: true },
resources: { listChanged: true },
},
},
);
// Define a tool with type-safe schema
server.tool(
{
name: 'calculate',
description: 'Perform mathematical calculations',
schema: z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
a: z.number(),
b: z.number(),
}),
},
async ({ operation, a, b }) => {
switch (operation) {
case 'add':
return a + b;
case 'subtract':
return a - b;
case 'multiply':
return a * b;
case 'divide':
return a / b;
}
},
);
// Start the server with stdio transport
const transport = new StdioTransport(server);
transport.listen();
```
### HTTP Transport (Web-based)
```javascript
import { McpServer } from 'tmcp';
import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
import { HttpTransport } from '@tmcp/transport-http';
import { z } from 'zod';
const adapter = new ZodJsonSchemaAdapter();
const server = new McpServer(/* ... same server config ... */);
// Add tools as above...
// Create HTTP transport
const transport = new HttpTransport(server);
// Use with your preferred HTTP server (Bun example)
Bun.serve({
port: 3000,
async fetch(req) {
const response = await transport.respond(req);
if (response === null) {
return new Response('Not Found', { status: 404 });
}
return response;
},
});
```
## API Reference
### McpServer
The main server class that handles MCP protocol communications.
#### Constructor
```javascript
new McpServer(serverInfo, options);
```
- `serverInfo`: Server metadata (name, version, description)
- `options`: Configuration object with adapter and capabilities
#### Methods
##### `tool(definition, handler)`
Register a tool with optional schema validation.
```javascript
server.tool(
{
name: 'tool-name',
description: 'Tool description',
schema: yourSchema, // optional
},
async (input) => {
// Tool implementation
return result;
},
);
```
##### `prompt(definition, handler)`
Register a prompt template with optional schema validation.
```javascript
server.prompt(
{
name: 'prompt-name',
description: 'Prompt description',
schema: yourSchema, // optional
complete: (arg, context) => ['completion1', 'completion2'] // optional
},
async (input) => {
// Prompt implementation
return { messages: [...] };
}
);
```
##### `resource(definition, handler)`
Register a static resource.
```javascript
server.resource(
{
name: 'resource-name',
description: 'Resource description',
uri: 'file://path/to/resource'
},
async (uri, params) => {
// Resource implementation
return { contents: [...] };
}
);
```
##### `template(definition, handler)`
Register a URI template for dynamic resources.
```javascript
server.template(
{
name: 'template-name',
description: 'Template description',
uri: 'file://path/{id}/resource',
complete: (arg, context) => ['id1', 'id2'] // optional
},
async (uri, params) => {
// Template implementation using params.id
return { contents: [...] };
}
);
```
##### `receive(request)`
Process an incoming MCP request.
```javascript
const response = server.receive(jsonRpcRequest);
```
## Advanced Examples
### Multiple Schema Libraries
```javascript
// Use different schemas for different tools
import { z } from 'zod';
import * as v from 'valibot';
server.tool(
{
name: 'zod-tool',
schema: z.object({ name: z.string() }),
},
async ({ name }) => `Hello ${name}`,
);
server.tool(
{
name: 'valibot-tool',
schema: v.object({ age: v.number() }),
},
async ({ age }) => `Age: ${age}`,
);
```
### Resource Templates with Completion
```javascript
server.template(
{
name: 'user-profile',
description: 'Get user profile by ID',
uri: 'users/{userId}/profile',
complete: (arg, context) => {
// Provide completions for userId parameter
return ['user1', 'user2', 'user3'];
},
},
async (uri, params) => {
const user = await getUserById(params.userId);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(user),
},
],
};
},
);
```
### Complex Validation
```javascript
const complexSchema = z.object({
user: z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18).max(120),
}),
preferences: z
.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean(),
})
.optional(),
tags: z.array(z.string()).default([]),
});
server.tool(
{
name: 'create-user',
description: 'Create a new user with preferences',
schema: complexSchema,
},
async (input) => {
// Input is fully typed and validated
const { user, preferences, tags } = input;
return await createUser(user, preferences, tags);
},
);
```
## Contributing
Contributions are welcome! Please see our [contributing guidelines](../../CONTRIBUTING.md) for details.
## Acknowledgments
Huge thanks to Sean O'Bannon that provided us with the `@tmcp` scope on npm.
## License
MIT © Paolo Ricciuti

View File

@@ -5,15 +5,17 @@ import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
import svelteConfig from './apps/mcp-remote/svelte.config.js';
import eslint_plugin_import from 'eslint-plugin-import';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
const gitignore_path = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
export default /** @type {import("eslint").Linter.Config} */ ([
includeIgnoreFile(gitignore_path),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
eslint_plugin_import.flatConfigs.recommended,
prettier,
...svelte.configs.prettier,
{
@@ -24,6 +26,27 @@ export default ts.config(
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off',
'@typescript-eslint/naming-convention': [
'error',
{
selector: ['variableLike'],
format: ['snake_case', 'UPPER_CASE'],
leadingUnderscore: 'allow',
},
],
'func-style': ['error', 'declaration', { allowTypeAnnotation: true }],
'import/no-unresolved': 'off', // this doesn't work well with typescript path mapping
'import/extensions': [
'error',
'ignorePackages',
{
js: 'always',
mjs: 'always',
cjs: 'always',
ts: 'always',
svelte: 'always',
},
],
},
},
{
@@ -37,4 +60,4 @@ export default ts.config(
},
},
},
);
]);

View File

@@ -1,27 +1,18 @@
{
"name": "@sveltejs/mcp",
"name": "@sveltejs/mcp-mono",
"version": "0.0.1",
"description": "The official Svelte MCP server implementation",
"type": "module",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"build": "pnpm -r run build",
"check": "pnpm -r run check",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"lint:fix": "prettier --write . && eslint . --fix",
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"test:watch": "npm run test:unit -- --watch",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
"inspect": "pnpm mcp-inspector"
},
"keywords": [
"svelte",
@@ -31,42 +22,20 @@
],
"private": true,
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@libsql/client": "^0.14.0",
"@sveltejs/adapter-vercel": "^5.6.3",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@types/eslint-scope": "^8.3.2",
"@types/estree": "^1.0.8",
"@types/node": "^24.3.1",
"@typescript-eslint/types": "^8.43.0",
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.40.0",
"eslint": "^9.18.0",
"@eslint/compat": "^1.3.2",
"@eslint/js": "^9.36.0",
"@modelcontextprotocol/inspector": "^0.16.7",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-svelte": "^3.12.3",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^7.0.4",
"vite-plugin-devtools-json": "^1.0.0",
"typescript-eslint": "^8.44.1",
"vitest": "^3.2.3"
},
"dependencies": {
"@tmcp/adapter-valibot": "^0.1.4",
"@tmcp/transport-http": "^0.6.0",
"@tmcp/transport-stdio": "^0.1.3",
"@typescript-eslint/parser": "^8.43.0",
"svelte-eslint-parser": "^1.3.2",
"tmcp": "^1.12.2",
"valibot": "^1.1.0",
"zimmerframe": "^1.1.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"

View File

@@ -0,0 +1,20 @@
{
"name": "@sveltejs/mcp-schema",
"version": "0.0.1",
"private": true,
"description": "",
"main": "index.js",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js",
"./schema": "./src/schema.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.15.1",
"type": "module",
"dependencies": {
"drizzle-orm": "^0.40.1"
}
}

View File

@@ -0,0 +1,8 @@
/**
* @import * as schema from './schema.js'
*/
export * from './schema.js';
/**
* @typedef {typeof schema} Schema
*/

View File

@@ -0,0 +1,82 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { float_32_array } from './utils.js';
/**
* NOTE: if you modify a schema adding a vector column you need to manually add this
*
* CREATE INDEX IF NOT EXISTS name_of_the_index
* ON `name_of_the_table` (
* libsql_vector_idx(name_of_the_column, 'metric=cosine')
* )
*
* to the generated migration file
*/
export const distillations = sqliteTable('distillations', {
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' }).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' }).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' }).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

@@ -1,19 +1,22 @@
import { sql, Column } from 'drizzle-orm';
/**
* @import { Column } from 'drizzle-orm';
*/
import { sql } from 'drizzle-orm';
import { customType } from 'drizzle-orm/sqlite-core';
/**
* Helper function to convert an array of embeddings into a format that can be inserted into a LibSQL vector column.
* @param arr The embeddings array.
* @param {number[]} arr The embeddings array.
*/
export function vector(arr: number[]) {
export function vector(arr) {
return sql`vector32(${JSON.stringify(arr)})`;
}
/**
* Helper function to calculate the distance between a vector column and an array of embeddings and return it as a columns.
* @param column The drizzle column representing the vector.
* @param arr The embeddings array.
* @param as The name of the returned column. Default is 'distance'.
* @param {Column} column The drizzle column representing the vector.
* @param {number} arr The embeddings array.
* @param {string} as The name of the returned column. Default is 'distance'.
*
* @example
* await db.select({
@@ -25,26 +28,38 @@ export function vector(arr: number[]) {
* .orderBy(sql`distance`)
* .execute();
*/
export function distance(column: Column, arr: number[], as = 'distance') {
return sql<number>`vector_distance_cos(${column}, vector32(${JSON.stringify(arr)}))`.as(as);
export function distance(column, arr, as = 'distance') {
return /** @type {typeof sql<number>} */ (
sql
)`CASE ${column} ISNULL WHEN 1 THEN 1 ELSE vector_distance_cos(${column}, vector32(${JSON.stringify(arr)})) END`.as(
as,
);
}
/**
* Custom drizzle type to use the LibSQL vector column type.
*/
export const float_32_array = customType<{
export const float_32_array = /** @type {typeof customType<{
data: number[];
config: { dimensions: number };
configRequired: true;
driverData: Buffer;
}>({
}>} */ (customType)({
dataType(config) {
return `F32_BLOB(${config.dimensions})`;
},
fromDriver(value: Buffer) {
/**
* @param {Buffer} value
*/
fromDriver(value) {
return Array.from(new Float32Array(value.buffer));
},
toDriver(value: number[]) {
/**
*
* @param {number[]} value
* @returns
*/
toDriver(value) {
return vector(value);
},
});

View File

@@ -0,0 +1,41 @@
{
"name": "@sveltejs/mcp-server",
"version": "0.0.1",
"private": true,
"description": "",
"main": "index.js",
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.15.1",
"type": "module",
"scripts": {
"test": "vitest"
},
"exports": {
".": "./src/index.ts"
},
"peerDependencies": {
"drizzle-orm": "^0.40.0"
},
"dependencies": {
"@sveltejs/mcp-schema": "workspace:^",
"@tmcp/adapter-valibot": "^0.1.4",
"@typescript-eslint/parser": "^8.44.0",
"eslint": "^9.36.0",
"eslint-plugin-svelte": "^3.12.3",
"svelte": "^5.39.2",
"svelte-eslint-parser": "^1.3.2",
"tmcp": "^1.13.0",
"typescript-eslint": "^8.44.0",
"valibot": "^1.1.0",
"vitest": "^3.2.4",
"zimmerframe": "^1.1.4"
},
"devDependencies": {
"@sveltejs/kit": "^2.42.2",
"@types/eslint-scope": "^8.3.2",
"@types/estree": "^1.0.8",
"@typescript-eslint/types": "^8.44.0"
}
}

View File

@@ -0,0 +1,23 @@
export const base_runes = [
'$state',
'$effect',
'$derived',
'$inspect',
'$props',
'$bindable',
'$host',
] as const;
export const nested_runes = [
'$state.raw',
'$state.snapshot',
'$effect.pre',
'$effect.tracking',
'$effect.pending',
'$effect.root',
'$derived.by',
'$inspect.trace',
'$props.id',
] as const;
export const runes = [...base_runes, ...nested_runes] as const;

View File

@@ -0,0 +1 @@
export { server, type SvelteMcp } from './mcp/index.js';

View File

@@ -0,0 +1,471 @@
import { describe, expect, it } from 'vitest';
import { add_autofixers_issues } from './add-autofixers-issues.js';
import { base_runes } from '../../constants.js';
const dollarless_runes = base_runes.map((r) => ({ rune: r.replace('$', '') }));
function run_autofixers_on_code(code: string, desired_svelte_version = 5) {
const content = { issues: [], suggestions: [] };
add_autofixers_issues(content, code, desired_svelte_version);
return content;
}
function with_possible_inits(title: string, fn: (args: { init: string }) => void) {
describe.each([
{ init: '$state' },
{ init: '$state.raw' },
{ init: '$derived' },
{ init: '$derived.by' },
])(title, fn);
}
describe('add_autofixers_issues', () => {
describe('assign_in_effect', () => {
with_possible_inits('($init)', ({ init }) => {
it(`should add suggestions when assigning to a stateful variable inside an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}(0);
$effect(() => {
count = 43;
});
</script>`);
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 = run_autofixers_on_code(`
<script>
const count = $state(0);
const count2 = $state(0);
$effect(() => {
count = 43;
count2 = 44;
});
</script>`);
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 = run_autofixers_on_code(`
<script>
const count = ${init}(0);
</script>
<button onclick={() => count = 43}>Increment</button>
`);
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 = run_autofixers_on_code(`
<script>
const count = 0;
$effect(() => {
count = 43;
});
</script>`);
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 = run_autofixers_on_code(`
<script>
let count = ${init}(0);
$effect(() => {
count++;
});
</script>
`);
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 = run_autofixers_on_code(`
<script>
let count = ${init}({ value: 0 });
$effect(() => {
count.value = 42;
});
</script>
`);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
});
});
with_possible_inits('($init)', ({ init }) => {
describe.each([{ method: 'set' }, { method: 'update' }])(
'wrong_property_access_state ($method)',
({ method }) => {
it(`should add suggestions when using .${method}() on a stateful variable with a literal init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}(0);
function update_count() {
count.${method}(43);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with an array init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}([0]);
function update_count() {
count.${method}([1]);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable (${init}({}))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}({ value: 0 });
function update_count() {
count.${method}({ value: 43 });
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable (${init}(new Class()))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}(new Class());
function update_count() {
count.${method}(new Class());
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable (${init}(variable_name))`, () => {
const content = run_autofixers_on_code(`
<script>
const { init } = $props();
const count = ${init}(init);
function update_count() {
count.${method}(43);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should not add suggestions when using .${method} on a stateful variable if it's not a method call`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}({});
function update_count() {
console.log(count.${method});
}
</script>`);
expect(content.suggestions).not.toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
},
);
describe.each([{ property: '$' }])(
'wrong_property_access_state property ($property)',
async ({ property }) => {
it(`should add suggestions when reading .${property} on a stateful variable with a literal init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}(0);
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them.`,
);
});
it(`should add suggestions when reading .${property} on a stateful variable with an array init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}([1]);
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them.`,
);
});
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable (${init}({}))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}({ value: 0 });
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable (${init}(new Class()))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}(new Class());
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable (${init}(variable_name))`, () => {
const content = run_autofixers_on_code(`
<script>
const { init } = $props();
const count = ${init}(init);
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
},
);
});
describe('imported_runes', () => {
describe.each([{ source: 'svelte' }, { source: 'svelte/runes' }])(
'from "$source"',
({ source }) => {
describe.each(dollarless_runes)('single import ($rune)', ({ rune }) => {
it(`should add suggestions when importing '${rune}' from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${rune} } from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions when importing "${rune}" as the default export from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import ${rune} from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions when importing '${rune}' as the namespace export from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import * as ${rune} from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
});
it(`should add suggestions when importing multiple runes from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { onMount, state, effect } from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
`You are importing "state" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$state" directly.`,
);
expect(content.suggestions).toContain(
`You are importing "effect" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$effect" directly.`,
);
});
it(`should not add suggestions when importing other identifiers from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { onMount } from '${source}';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "onMount" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$onMount" directly.`,
);
});
},
);
});
describe('derived_with_function', () => {
it(`should add suggestions when using a function as the first argument to $derived`, () => {
const content = run_autofixers_on_code(`
<script>
const value = $derived(() => {
return 43;
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'You are passing a function to $derived when declaring "value" but $derived expects an expression. You can use $derived.by instead.',
);
});
it(`should add suggestions when using a function as the first argument to $derived in classes`, () => {
const content = run_autofixers_on_code(`
<script>
class Double {
value = $derived(() => 43);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'You are passing a function to $derived when declaring "value" but $derived expects an expression. You can use $derived.by instead.',
);
});
it(`should add suggestions when using a function as the first argument to $derived in classes constructors`, () => {
const content = run_autofixers_on_code(`
<script>
class Double {
value;
constructor(){
this.value = $derived(function() { return 44; });
}
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'You are passing a function to $derived when declaring "value" but $derived expects an expression. You can use $derived.by instead.',
);
});
it(`should add suggestions when using a function as the first argument to $derived without the declaring part if it's not an identifier`, () => {
const content = run_autofixers_on_code(`
<script>
const { destructured } = $derived(() => 43);
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'You are passing a function to $derived but $derived expects an expression. You can use $derived.by instead.',
);
});
it(`should add suggestions when using a function as the first argument to $derived.by`, () => {
const content = run_autofixers_on_code(`
<script>
const { destructured } = $derived.by(() => 43);
</script>`);
expect(content.suggestions).not.toContain(
'You are passing a function to $derived but $derived expects an expression. You can use $derived.by instead.',
);
});
});
describe('use_runes_instead_of_store', () => {
describe.each([{ import: 'derived' }, { import: 'writable' }, { import: 'readable' }])(
'importing $import from svelte/store',
({ import: imported }) => {
it(`should add suggestions when importing '${imported}' from 'svelte/store'`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${imported} } from 'svelte/store';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${imported}" from "svelte/store". Unless the user specifically asked for stores or it's required because some library/component requires a store as input consider using runes like \`$state\` or \`$derived\` instead, all runes are globally available.`,
);
});
},
);
it(`should not add suggestions when importing other identifiers from 'svelte/store'`, () => {
const content = run_autofixers_on_code(`
<script>
import { get } from 'svelte/store';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "get" from "svelte/store". Unless the user specifically asked for stores or it's required because some library/component requires a store as input consider using runes like \`$state\` or \`$derived\` instead, all runes are globally available.`,
);
});
});
});

View File

@@ -0,0 +1,22 @@
import { parse } from '../../parse/parse.js';
import { walk } from '../../mcp/autofixers/ast/walk.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,93 @@
import { ESLint } from 'eslint';
import svelte_parser from 'svelte-eslint-parser';
import svelte from 'eslint-plugin-svelte';
import type { Config } from '@sveltejs/kit';
import ts from 'typescript-eslint';
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: {
extraFileExtensions: ['.svelte'],
parser: ts.parser,
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,32 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { walk as _walk } from 'zimmerframe';
export type WalkParams<
T extends {
type: string;
},
U extends Record<string, any> | null,
> = Parameters<typeof _walk<T, U>>;
export function walk<
T extends {
type: string;
},
U extends Record<string, any> | null,
>(...args: WalkParams<T, U>) {
const [node, state, visitors] = args;
const visited = new WeakSet();
return _walk<T, U>(node, state, {
_(node, ctx) {
if (visited.has(node)) return;
visited.add(node);
if (visitors._) {
const ret = visitors._(node, ctx);
if (ret) return ret;
} else {
ctx.next();
}
},
...visitors,
});
}

View File

@@ -0,0 +1,66 @@
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';
import type { Context } from 'zimmerframe';
function run_if_in_effect(
path: (Node | AST.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 | AST.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', '$derived', '$derived.by'])
) {
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,41 @@
import type { Identifier, PrivateIdentifier } from 'estree';
import type { Autofixer } from './index.js';
export const derived_with_function: Autofixer = {
CallExpression(node, { state, path }) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === '$derived' &&
state.parsed.is_rune(node, ['$derived']) &&
(node.arguments[0]?.type === 'ArrowFunctionExpression' ||
node.arguments[0]?.type === 'FunctionExpression')
) {
const parent = path[path.length - 1];
let variable_id: Identifier | PrivateIdentifier | undefined;
if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
// const something = $derived(...)
variable_id = parent.id;
} else if (parent?.type === 'PropertyDefinition') {
// class X { something = $derived(...) }
variable_id =
parent.key.type === 'Identifier'
? parent.key
: parent.key.type === 'PrivateIdentifier'
? parent.key
: undefined;
} else if (parent?.type === 'AssignmentExpression') {
// this.something = $derived(...)
variable_id =
parent.left.type === 'MemberExpression'
? parent.left.property.type === 'Identifier'
? parent.left.property
: undefined
: undefined;
}
state.output.suggestions.push(
`You are passing a function to $derived ${variable_id ? `when declaring "${variable_id.name}" ` : ''}but $derived expects an expression. You can use $derived.by instead.`,
);
}
},
};

View File

@@ -0,0 +1,28 @@
import { base_runes } from '../../../constants.js';
import type { Autofixer } from './index.js';
const dollarless_runes = base_runes.map((r) => r.replace('$', ''));
export const imported_runes: Autofixer = {
ImportDeclaration(node, { state, next }) {
const source = (node.source.value || node.source.raw?.slice(1, -1))?.toString();
if (source && source.startsWith('svelte')) {
for (const specifier of node.specifiers) {
const id =
specifier.type === 'ImportDefaultSpecifier'
? specifier.local
: specifier.type === 'ImportNamespaceSpecifier'
? specifier.local
: specifier.type === 'ImportSpecifier'
? specifier.imported
: null;
if (id && id.type === 'Identifier' && dollarless_runes.includes(id.name)) {
state.output.suggestions.push(
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
);
}
}
}
next();
},
};

View File

@@ -0,0 +1,18 @@
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';
export * from './wrong-property-access-state.js';
export * from './imported-runes.js';
export * from './derived-with-function.js';
export * from './use-runes-instead-of-store.js';

View File

@@ -0,0 +1,21 @@
import type { Autofixer } from './index.js';
export const use_runes_instead_of_store: Autofixer = {
ImportDeclaration(node, { state, next }) {
const source = (node.source.value || node.source.raw?.slice(1, -1))?.toString();
if (source && source === 'svelte/store') {
for (const specifier of node.specifiers) {
if (
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
['derived', 'writable', 'readable'].includes(specifier.imported.name)
) {
state.output.suggestions.push(
`You are importing "${specifier.imported.name}" from "svelte/store". Unless the user specifically asked for stores or it's required because some library/component requires a store as input consider using runes like \`$state\` or \`$derived\` instead, all runes are globally available.`,
);
}
}
}
next();
},
};

View File

@@ -0,0 +1,41 @@
import type { Autofixer } from './index.js';
import { left_most_id } from '../ast/utils.js';
const UPDATE_PROPERTIES = new Set(['set', 'update', '$']);
const METHODS = new Set(['set', 'update']);
export const wrong_property_access_state: Autofixer = {
MemberExpression(node, { state, next, path }) {
const parent = path[path.length - 1];
let is_property = false;
if (
node.property.type === 'Identifier' &&
((is_property = !METHODS.has(node.property.name)) ||
(parent?.type === 'CallExpression' && parent.callee === node)) &&
UPDATE_PROPERTIES.has(node.property.name)
) {
const id = left_most_id(node);
if (id) {
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', '$derived', '$derived.by'])
) {
let suggestion = is_property
? `You are trying to read the stateful variable "${id.name}" using "${node.property.name}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them.`
: `You are trying to update the stateful variable "${id.name}" using "${node.property.name}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`;
const argument = init.arguments[0];
if (!argument || (argument.type !== 'Literal' && argument.type !== 'ArrayExpression')) {
suggestion += ` However I can't verify if "${id.name}" is a state variable of an object or a class with a "${node.property.name}" ${is_property ? 'property' : 'method'} on it. Please verify that before updating the code to use a normal ${is_property ? 'access' : 'assignment'}`;
}
state.output.suggestions.push(suggestion);
}
}
}
}
next();
},
};

View File

@@ -0,0 +1,22 @@
import type { SvelteMcp } from '../index.js';
import * as prompts from './prompts/index.js';
import * as tools from './tools/index.js';
import * as resources from './resources/index.js';
export function setup_tools(server: SvelteMcp) {
for (const tool in tools) {
tools[tool as keyof typeof tools](server);
}
}
export function setup_prompts(server: SvelteMcp) {
for (const prompt in prompts) {
prompts[prompt as keyof typeof prompts](server);
}
}
export function setup_resources(server: SvelteMcp) {
for (const resource in resources) {
resources[resource as keyof typeof resources](server);
}
}

View File

@@ -0,0 +1 @@
export * from './svelte-task.js';

View File

@@ -0,0 +1,46 @@
import type { SvelteMcp } from '../../index.js';
import * as v from 'valibot';
export function setup_svelte_task(server: SvelteMcp) {
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>
If you are not writing the code into a file, once you have the final version of the code ask the user if it wants to generate a playground link to quickly check the code in it and if it answer yes call the \`playground-link\` tool and return the url to the user nicely formatted. The playground link MUST be generated only once you have the final version of the code and you are ready to share it, it MUST include an entry point file called \`App.svelte\` where the main component should live. If you have multiple files to include in the playground link you can include them all at the root.
`,
},
},
],
};
},
);
}

View File

@@ -0,0 +1 @@
export * from './list-sections.js';

View File

@@ -0,0 +1,25 @@
import type { SvelteMcp } from '../../index.js';
export function list_sections(server: SvelteMcp) {
server.resource(
{
name: 'list-sections',
enabled: () => false,
description:
'The list of all the available Svelte 5 and SvelteKit documentation sections in a structured format.',
uri: 'svelte://list-sections',
title: 'Svelte Documentation Section',
},
async (uri) => {
return {
contents: [
{
uri,
type: 'text',
text: 'resource list-sections called',
},
],
};
},
);
}

View File

@@ -0,0 +1,58 @@
import type { SvelteMcp } from '../../index.js';
import * as v from 'valibot';
export function get_documentation(server: SvelteMcp) {
server.tool(
{
name: 'get-documentation',
enabled: () => false,
description:
'Retrieves full documentation content for Svelte 5 or SvelteKit sections. Supports flexible search by title (e.g., "$state", "routing") or file path (e.g., "docs/svelte/state.md"). Can accept a single section name or an array of sections. Before running this, make sure to analyze the users query, as well as the output from list_sections (which should be called first). Then ask for ALL relevant sections the user might require. For example, if the user asks to build anything interactive, you will need to fetch all relevant runes, and so on.',
schema: v.object({
section: v.pipe(
v.union([v.string(), v.array(v.string())]),
v.description(
'The section name(s) to retrieve. Can search by title (e.g., "$state", "load functions") or file path (e.g., "docs/svelte/state.md"). Supports single string and array of strings',
),
),
}),
},
({ section }) => {
let sections: string[];
if (Array.isArray(section)) {
sections = section.filter((s): s is string => typeof s === 'string');
} else if (
typeof section === 'string' &&
section.trim().startsWith('[') &&
section.trim().endsWith(']')
) {
try {
const parsed = JSON.parse(section);
if (Array.isArray(parsed)) {
sections = parsed.filter((s): s is string => typeof s === 'string');
} else {
sections = [section];
}
} catch {
sections = [section];
}
} else if (typeof section === 'string') {
sections = [section];
} else {
sections = [];
}
const sections_list = sections.length > 0 ? sections.join(', ') : 'no sections';
return {
content: [
{
type: 'text',
text: `called for sections: ${sections_list}`,
},
],
};
},
);
}

View File

@@ -0,0 +1,4 @@
export * from './get-documentation.js';
export * from './list-sections.js';
export * from './svelte-autofixer.js';
export * from './playground-link.js';

View File

@@ -0,0 +1,22 @@
import type { SvelteMcp } from '../../index.js';
export function list_sections(server: SvelteMcp) {
server.tool(
{
name: 'list-sections',
enabled: () => false,
description:
'Lists all available Svelte 5 and SvelteKit documentation sections in a structured format. Returns sections as a list of "* title: [section_title], path: [file_path]" - you can use either the title or path when querying a specific section via the get_documentation tool. Always run list_sections first for any query related to Svelte development to discover available content.',
},
() => {
return {
content: [
{
type: 'text',
text: 'tool list_sections called',
},
],
};
},
);
}

View File

@@ -0,0 +1,112 @@
import type { SvelteMcp } from '../../index.js';
import * as v from 'valibot';
async function compress_and_encode_text(input: string) {
const reader = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip')).getReader();
let buffer = '';
for (;;) {
const { done, value } = await reader.read();
if (done) {
reader.releaseLock();
// Some sites like discord don't like it when links end with =
return btoa(buffer).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
} else {
for (let i = 0; i < value.length; i++) {
// decoding as utf-8 will make btoa reject the string
buffer += String.fromCharCode(value[i]!);
}
}
}
}
type File = {
type: 'file';
name: string;
basename: string;
contents: string;
text: boolean;
};
export function playground_link(server: SvelteMcp) {
server.tool(
{
name: 'playground-link',
description:
'Generates a Playground link given a Svelte code snippet. Once you have the final version of the code you want to send to the user, ALWAYS ask the user if it wants a playground link to allow it to quickly check the code in the playground before calling this tool. NEVER use this tool if you have written the component to a file in the user project. The playground accept multiple files so if are importing from other files just include them all at the root level.',
schema: v.object({
name: v.pipe(
v.string(),
v.description('The name of the Playground, it should reflect the user task'),
),
tailwind: v.pipe(
v.boolean(),
v.description(
"If the code requires Tailwind CSS to work...only send true if it it's using tailwind classes in the code",
),
),
files: v.pipe(
v.record(v.string(), v.string()),
v.description(
"An object where all the keys are the filenames (with extensions) and the values are the file content. For example: { 'Component.svelte': '<script>...</script>', 'utils.js': 'export function ...' }. The playground accept multiple files so if are importing from other files just include them all at the root level.",
),
),
}),
outputSchema: v.object({
url: v.string(),
}),
},
async ({ files, name, tailwind }) => {
const playground_base = new URL('https://svelte.dev/playground');
const playground_files: File[] = [];
let has_app_svelte = false;
for (const [filename, contents] of Object.entries(files)) {
if (filename === 'App.svelte') has_app_svelte = true;
playground_files.push({
type: 'file',
name: filename,
basename: filename.replace(/^.*[\\/]/, ''),
contents,
text: true,
});
}
if (!has_app_svelte) {
return {
isError: true,
content: [
{
type: 'text',
text: JSON.stringify({
error: 'The files must contain an App.svelte file as the entry point',
}),
},
],
};
}
const playground_config = {
name,
tailwind: tailwind ?? false,
files: playground_files,
};
playground_base.hash = await compress_and_encode_text(JSON.stringify(playground_config));
const content = {
url: playground_base.toString(),
};
return {
content: [
{
type: 'text',
text: JSON.stringify(content),
},
],
structuredContent: content,
};
},
);
}

View File

@@ -0,0 +1,86 @@
import { basename } from 'node:path';
import type { SvelteMcp } from '../../index.js';
import * as v from 'valibot';
import { add_compile_issues } from '../../autofixers/add-compile-issues.js';
import { add_eslint_issues } from '../../autofixers/add-eslint-issues.js';
import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js';
export function svelte_autofixer(server: SvelteMcp) {
server.tool(
{
name: 'svelte-autofixer',
title: 'Svelte Autofixer',
description:
'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.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.pipe(
v.optional(v.string()),
v.description(
'The filename of the component if available, it MUST be only the Component name with .svelte or .svelte.ts extension and not the entire path.',
),
),
}),
outputSchema: v.object({
issues: v.array(v.string()),
suggestions: v.array(v.string()),
require_another_tool_call_after_fixing: v.boolean(),
}),
annotations: {
title: 'Svelte Autofixer',
destructiveHint: false,
readOnlyHint: true,
openWorldHint: false,
},
},
async ({ code, filename: filename_or_path, 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 {
// just in case the LLM sends a full path we extract the filename...it's not really needed
// but it's nice to have a filename in the errors
const filename = filename_or_path ? basename(filename_or_path) : 'Component.svelte';
add_compile_issues(content, code, +desired_svelte_version, filename);
add_autofixers_issues(content, code, +desired_svelte_version, filename);
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 (error.message.includes('js_parse_error')) {
content.suggestions.push(
"The code can't be compiled because a Javascript parse error. In case you are using runes like this `$state variable_name = 3;` or `$derived variable_name = 3 * count` that's not how runes are used. You need to use them as function calls without importing them: `const variable_name = $state(3)` and `const variable_name = $derived(3 * count)`.",
);
}
}
if (content.issues.length > 0 || content.suggestions.length > 0) {
content.require_another_tool_call_after_fixing = true;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(content),
},
],
structuredContent: content,
};
},
);
}

View File

@@ -0,0 +1,30 @@
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
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';
export const server = new McpServer(
{
name: 'Svelte MCP',
version: '0.0.1',
description: 'The official Svelte MCP server implementation',
},
{
adapter: new ValibotJsonSchemaAdapter(),
capabilities: {
tools: {},
prompts: {},
resources: {},
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.',
},
).withContext<{ db: LibSQLDatabase<Schema> }>();
export type SvelteMcp = typeof server;
setup_tools(server);
setup_resources(server);
setup_prompts(server);

View File

@@ -1,6 +1,6 @@
import type { TSESTree } from '@typescript-eslint/types';
import { describe, expect, it } from 'vitest';
import { walk } from '$lib/index.js';
import { walk } from '../mcp/autofixers/ast/walk.js';
import { parse, type ParseResult } from './parse.js';
// ----------------------------------------------------------------------
@@ -57,8 +57,8 @@ function assert_svelte_file(result: ParseResult) {
const name_var = variable_declaration_from_id(result, declaration_id);
expect(Array.isArray(name_var.defs)).toBe(true);
expect(name_var.defs.length).toBeGreaterThan(0);
expect(name_var.defs[0].type).toBe('Variable');
expect(name_var.defs[0].name && name_var.defs[0].name.name).toBe('name');
expect(name_var.defs[0]?.type).toBe('Variable');
expect(name_var.defs[0]?.name && name_var.defs[0].name.name).toBe('name');
const references_to_name = all_references.filter((rf) => rf.resolved === name_var);
expect(references_to_name.length).toBeGreaterThan(0);
@@ -76,7 +76,7 @@ function assert_sveltejs_file(result: ParseResult) {
const v_var = variable_declaration_from_id(result, declaration_id);
expect(Array.isArray(v_var.defs)).toBe(true);
expect(v_var.defs.length).toBeGreaterThan(0);
expect(v_var.defs[0].type).toBeTruthy();
expect(v_var.defs[0]?.type).toBeTruthy();
const references_to_v = all_references.filter((rf) => rf.resolved === v_var);
expect(references_to_v.length).toBeGreaterThanOrEqual(2);

View File

@@ -2,6 +2,7 @@ import ts_parser from '@typescript-eslint/parser';
import type { CallExpression, Identifier } from 'estree';
import type { Reference, Variable } from 'eslint-scope';
import { parseForESLint as svelte_eslint_parse } from 'svelte-eslint-parser';
import { runes } from '../constants.js';
type Scope = {
variables?: Variable[];
@@ -18,25 +19,6 @@ function collect_scopes(scope: Scope, acc: Scope[] = []) {
return acc;
}
const runes = [
'$state',
'$state.raw',
'$state.snapshot',
'$effect',
'$effect.pre',
'$effect.tracking',
'$effect.pending',
'$effect.root',
'$derived',
'$derived.by',
'$inspect',
'$inspect.trace',
'$props',
'$props.id',
'$bindable',
'$host',
] as const;
export type ParseResult = ReturnType<typeof parse>;
export function parse(code: string, file_path: string) {

View File

@@ -0,0 +1,16 @@
# @sveltejs/mcp
The CLI version of the Svelte MCP.
You can run it directly with
```bash
npx @sveltejs/mcp
```
or install it and then run it
```bash
pnpm i @sveltejs/mcp
pnpm svelte-mcp
```

View File

@@ -0,0 +1,42 @@
{
"name": "@sveltejs/mcp",
"version": "0.0.1",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/sveltejs/mcp#readme",
"bugs": {
"url": "https://github.com/sveltejs/mcp/issues"
},
"bin": {
"svelte-mcp": "./dist/index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/mcp.git",
"path": "packages/mcp-stdio"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"test": "vitest",
"check": "tsc --noEmit"
},
"devDependencies": {
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-stdio": "^0.3.1",
"@types/node": "^22.15.17",
"publint": "^0.3.13",
"tsdown": "^0.11.9",
"typescript": "^5.8.3",
"vitest": "^3.1.3"
},
"dependencies": {
"eslint": "^9.36.0"
}
}

View File

@@ -0,0 +1,7 @@
#! /usr/bin/env node
import { server } from '@sveltejs/mcp-server';
import { StdioTransport } from '@tmcp/transport-stdio';
const transport = new StdioTransport(server);
transport.listen();

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,21 @@
import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: ['./src/index.ts'],
platform: 'node',
define: {
// some eslint-plugin-svelte code expects __filename to exists but in an ESM environment it does not.
__filename: 'import.meta.filename',
},
// we need eslint at runtime but the bundler doesn't bundle `require`'s which `eslint-plugin-svelte` uses to require
// `eslint/use-at-your-own-risk`. If we didn't have `eslint` as an actual dependency and didn't externalize it
// the require would fail once executed in a project without eslint installed.
external: ['eslint'],
publint: true,
dts: false,
treeshake: true,
clean: true,
target: 'esnext',
},
]);

View File

@@ -0,0 +1,3 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({});

4622
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
packages:
- '*'
- './packages/*'
- './apps/*'
useNodeVersion: 22.19.0

19
renovate.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": [
"config:recommended",
":preserveSemverRanges",
"group:allNonMajor",
":semanticCommitTypeAll(chore)"
],
"pin": {
"enabled": false
},
"ignoreDeps": ["@types/node", "esbuild", "rollup", "typescript"],
"packageRules": [
{
"matchPackageNames": ["vite"],
"matchUpdateTypes": ["major"],
"enabled": false
}
]
}

View File

@@ -1,5 +0,0 @@
import { http_transport } from '$lib/mcp';
export async function handle({ event, resolve }) {
return (await http_transport.respond(event.request)) ?? resolve(event);
}

View File

@@ -1,55 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { walk as _walk } from 'zimmerframe';
import type { Autofixer } from './mcp/autofixers';
export type WalkParams<
T extends {
type: string;
},
U extends Record<string, any> | null,
> = Parameters<typeof _walk<T, U>>;
export function walk<
T extends {
type: string;
},
U extends Record<string, any> | null,
>(...args: WalkParams<T, U>) {
const [node, state, visitors] = args;
const visited = new WeakSet();
return _walk<T, U>(node, state, {
_(node, ctx) {
if (visited.has(node)) return;
visited.add(node);
if (visitors._) {
const ret = visitors._(node, ctx);
if (ret) return ret;
} else {
ctx.next();
}
},
...visitors,
});
}
export function mix_visitors(autofixers: Record<string, Autofixer>): Autofixer {
const visitors: Autofixer = {};
for (const key in autofixers) {
const new_visitors = autofixers[key];
for (const node_type in new_visitors) {
if (node_type in visitors) {
const existing_visitor = visitors[node_type as keyof typeof visitors]!;
const new_visitor = new_visitors[node_type as keyof typeof visitors]!;
visitors[node_type as keyof typeof visitors] = (node, ctx) => {
(existing_visitor as any)(node, ctx);
(new_visitor as any)(node, ctx);
};
} else {
visitors[node_type as keyof typeof visitors] = new_visitors[
node_type as keyof typeof visitors
] as never;
}
}
}
return visitors;
}

View File

@@ -1,42 +0,0 @@
import type { Node } from 'estree';
import type { Visitors } from 'zimmerframe';
import type { ParseResult } from '$lib/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

@@ -1,73 +0,0 @@
import { mix_visitors, walk } from '$lib';
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';
const server = new McpServer(
{
name: 'Svelte MCP',
version: '0.0.1',
description: 'The official Svelte MCP server implementation',
},
{
adapter: new ValibotJsonSchemaAdapter(),
capabilities: {
tools: {},
prompts: {},
resources: {},
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',
},
);
server.tool(
{
name: 'svelte-autofixer',
title: 'Svelte Autofixer',
description:
'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(),
filename: v.optional(v.string()),
}),
outputSchema: v.object({
issues: v.optional(v.array(v.string())),
suggestions: v.optional(v.array(v.string())),
}),
annotations: {
title: 'Svelte Autofixer',
destructiveHint: false,
readOnlyHint: true,
openWorldHint: false,
},
},
async ({ code, filename }) => {
const content: { issues: string[]; suggestions: string[] } = { issues: [], suggestions: [] };
const parsed = parse(code, filename ?? 'Component.svelte');
walk(parsed.ast as unknown as Node, { output: content, parsed }, mix_visitors(autofixers));
return {
content: [
{
type: 'text',
text: JSON.stringify(content),
},
],
structuredContent: content,
};
},
);
export const http_transport = new HttpTransport(server, {
cors: true,
});
export const stdio_transport = new StdioTransport(server);

View File

@@ -1,10 +0,0 @@
import { DATABASE_URL } from '$env/static/private';
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema';
if (!DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = createClient({ url: DATABASE_URL });
export const db = drizzle(client, { schema, logger: true });

View File

@@ -1,20 +0,0 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { float_32_array } from './utils';
/**
* NOTE: if you modify a schema adding a vector column you need to manually add this
*
* CREATE INDEX IF NOT EXISTS name_of_the_index
* ON `name_of_the_table` (
* libsql_vector_idx(name_of_the_column, 'metric=cosine')
* )
*
* 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', {
id: integer('id').primaryKey(),
text: text('text'),
vector: float_32_array('vector', { dimensions: 3 }),
});

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 +0,0 @@
<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,19 +1,21 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"module": "nodenext",
"target": "esnext",
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"noEmit": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"checkJs": true,
"allowJs": true,
// Recommended Options
"strict": true,
"moduleResolution": "bundler"
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": 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
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

View File

@@ -1,21 +0,0 @@
import devtoolsJson from 'vite-plugin-devtools-json';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit(), devtoolsJson()],
test: {
expect: { requireAssertions: true },
projects: [
{
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
},
},
],
},
});