mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-07-04 03:19:38 +08:00
Compare commits
130 Commits
import-sto
...
@sveltejs/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f296e5277 | ||
|
|
121395e98e | ||
|
|
8e7c881838 | ||
|
|
91c396e675 | ||
|
|
baad760634 | ||
|
|
b2275587ee | ||
|
|
4964303100 | ||
|
|
81901b2564 | ||
|
|
5aa1aa401a | ||
|
|
4201627f53 | ||
|
|
9a70fbe3aa | ||
|
|
dc16a42c65 | ||
|
|
3b50014b09 | ||
|
|
e9214bc470 | ||
|
|
3106305902 | ||
|
|
3c14872068 | ||
|
|
216a470bd2 | ||
|
|
b5a88c454d | ||
|
|
7b5bea6549 | ||
|
|
a36d0d17a8 | ||
|
|
70f14bddca | ||
|
|
a281ef4b66 | ||
|
|
5bc812e4db | ||
|
|
82319661dd | ||
|
|
ce0861c1ca | ||
|
|
5dd83d151e | ||
|
|
76a35f5dc8 | ||
|
|
54763e0f55 | ||
|
|
01d5803b5d | ||
|
|
0366bc785b | ||
|
|
6a6417d3a5 | ||
|
|
77af7ebcc6 | ||
|
|
b1a196497d | ||
|
|
fb2d19fd07 | ||
|
|
8328a3572b | ||
|
|
c05b6c257a | ||
|
|
7f9ea742d8 | ||
|
|
bf477a6ccf | ||
|
|
0f5482477a | ||
|
|
b774b463fe | ||
|
|
c49b24d36a | ||
|
|
6cb97ac11d | ||
|
|
d33a374417 | ||
|
|
1bb171cea7 | ||
|
|
e314ab57b2 | ||
|
|
19cacf7ed9 | ||
|
|
68cf69a117 | ||
|
|
917fdf63b1 | ||
|
|
972cadc410 | ||
|
|
dc6c87ce37 | ||
|
|
e7431e9024 | ||
|
|
07737a8edd | ||
|
|
fdb7689992 | ||
|
|
47fa0a4382 | ||
|
|
4c6232a44f | ||
|
|
8edbf2f36b | ||
|
|
6e54719f88 | ||
|
|
1a283f60bc | ||
|
|
bb16ccca3a | ||
|
|
23ddaf9495 | ||
|
|
4228302ed0 | ||
|
|
0bc4d75e13 | ||
|
|
4679549401 | ||
|
|
6b15eb0790 | ||
|
|
a7041a4c5e | ||
|
|
023bea317f | ||
|
|
7a6cba8772 | ||
|
|
fd32b67442 | ||
|
|
0e3b1ba22f | ||
|
|
0aad39d076 | ||
|
|
2fec290d54 | ||
|
|
4e59ef751a | ||
|
|
c87c9e0715 | ||
|
|
12f8d84852 | ||
|
|
bde37da5d5 | ||
|
|
a50844e388 | ||
|
|
6a71229d56 | ||
|
|
92d8532c8a | ||
|
|
c8300bc62e | ||
|
|
de78f7663f | ||
|
|
e57b76324f | ||
|
|
09331e2c2b | ||
|
|
f1aef9ca2f | ||
|
|
6c072534ea | ||
|
|
2f8165f1d7 | ||
|
|
d93d3a3507 | ||
|
|
039718f1a5 | ||
|
|
ac287a2c83 | ||
|
|
224d630a32 | ||
|
|
4a9afb5ee1 | ||
|
|
e68067e995 | ||
|
|
8258a1c9ba | ||
|
|
5aa2827c91 | ||
|
|
a35d72cc6b | ||
|
|
0c35883074 | ||
|
|
d82c20acd6 | ||
|
|
cc3ea75c7f | ||
|
|
68724731c7 | ||
|
|
bf1a4178bf | ||
|
|
050e588709 | ||
|
|
731b4f6548 | ||
|
|
582e0e1dea | ||
|
|
bb9a6e07ea | ||
|
|
0d17b81948 | ||
|
|
81640c9a16 | ||
|
|
8587bc8625 | ||
|
|
0475e3b0f9 | ||
|
|
4e1a42ab52 | ||
|
|
862f614afc | ||
|
|
a92ae954bd | ||
|
|
e3b5188c6d | ||
|
|
6b5f2092b5 | ||
|
|
089e690f3e | ||
|
|
1c60e350a6 | ||
|
|
a93a6554b5 | ||
|
|
94f7d65db3 | ||
|
|
d7492bb1cb | ||
|
|
e1e2bf68ae | ||
|
|
0ff628f5b4 | ||
|
|
ea35d600e4 | ||
|
|
dcbcd5b690 | ||
|
|
d81d6a3d95 | ||
|
|
dedfd0b3b7 | ||
|
|
5d50518c3c | ||
|
|
74d2fb8f0e | ||
|
|
86675ea1d7 | ||
|
|
830fd73ab1 | ||
|
|
da995bdc69 | ||
|
|
26b3986740 | ||
|
|
8d53f56151 |
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/mcp" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
2
.cocoignore
Normal file
2
.cocoignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.claude
|
||||
.github
|
||||
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
|
||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
/packages/**/test/** -linguist-detectable
|
||||
/packages/**/fixtures/** -linguist-detectable
|
||||
36
.github/ISSUE_TEMPLATE/autofixer_request.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/autofixer_request.yml
vendored
Normal 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
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
|
||||
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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
|
||||
3
.github/workflows/check.yml
vendored
3
.github/workflows/check.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
version: 10.17.1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -33,4 +33,5 @@ jobs:
|
||||
run: pnpm run check
|
||||
env:
|
||||
DATABASE_URL: file:test.db
|
||||
DATABASE_TOKEN: dummy-key
|
||||
VOYAGE_API_KEY: dummy-key
|
||||
|
||||
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
version: 10.17.1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -34,3 +34,4 @@ jobs:
|
||||
env:
|
||||
DATABASE_URL: file:test.db
|
||||
VOYAGE_API_KEY: dummy-key
|
||||
DATABASE_TOKEN: dummy-key
|
||||
|
||||
58
.github/workflows/release.yml
vendored
Normal file
58
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
permissions:
|
||||
contents: write # to create release (changesets/action)
|
||||
id-token: write # OpenID Connect token needed for provenance
|
||||
pull-requests: write # to create pull request (changesets/action)
|
||||
# prevents this action from running on forks
|
||||
if: github.repository == 'sveltejs/mcp'
|
||||
name: Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# pseudo-matrix for convenience, NEVER use more than a single combination
|
||||
node: [24]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
package-manager-cache: false # pnpm is not installed yet
|
||||
- name: install pnpm
|
||||
shell: bash
|
||||
run: |
|
||||
PNPM_VER=$(jq -r '.packageManager | if .[0:5] == "pnpm@" then .[5:] else "packageManager in package.json does not start with pnpm@\n" | halt_error(1) end' package.json)
|
||||
echo installing pnpm version $PNPM_VER
|
||||
npm i -g pnpm@$PNPM_VER
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
package-manager-cache: true # caches pnpm via packageManager field in package.json
|
||||
- name: install
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
|
||||
- name: build
|
||||
run: pnpm run --filter ./packages/mcp-stdio/ build
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
# pinned for security, always review third party action code before updating
|
||||
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3
|
||||
with:
|
||||
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
||||
publish: pnpm release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
version: 10.17.1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -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
|
||||
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -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,14 +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
|
||||
dist
|
||||
/apps/**/*.db
|
||||
/packages/**/*.db
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"svelte-llm": {
|
||||
"type": "http",
|
||||
"url": "https://svelte-llm.stanislav.garden/mcp/mcp"
|
||||
"svelte": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["packages/mcp-stdio/dist/index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,7 @@ bun.lockb
|
||||
# Miscellaneous
|
||||
/static/
|
||||
/drizzle/
|
||||
/**/.svelte-kit/*
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
33
.vscode/mcp-snippets.code-snippets
vendored
Normal file
33
.vscode/mcp-snippets.code-snippets
vendored
Normal 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",
|
||||
},
|
||||
}
|
||||
2
.vscode/mcp.json
vendored
2
.vscode/mcp.json
vendored
@@ -3,7 +3,7 @@
|
||||
"Svelte MCP": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["dist/lib/stdio.js"]
|
||||
"args": ["packages/mcp-stdio/dist/index.js"]
|
||||
}
|
||||
},
|
||||
"inputs": []
|
||||
|
||||
@@ -90,12 +90,12 @@ When connected to the svelte-llm MCP server, you have access to comprehensive Sv
|
||||
|
||||
## Available MCP Tools:
|
||||
|
||||
### 1. list_sections
|
||||
### 1. list-sections
|
||||
|
||||
Use this FIRST to discover all available documentation sections. Returns a structured list with titles and paths.
|
||||
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||
|
||||
### 2. get_documentation
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||
After calling the list_sections tool, you MUST analyze the returned documentation sections and then use the get_documentation tool to fetch ALL documentation sections that are relevant for the users task.
|
||||
After calling the list-sections tool, you MUST analyze the returned documentation sections and then use the get_documentation tool to fetch ALL documentation sections that are relevant for the users task.
|
||||
|
||||
@@ -6,12 +6,15 @@ Repo for the official Svelte MCP server.
|
||||
|
||||
```
|
||||
pnpm i
|
||||
cp .env.example .env
|
||||
cp apps/mcp-remote/.env.example apps/mcp-remote/.env
|
||||
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
|
||||
|
||||
70
apps/mcp-remote/package.json
Normal file
70
apps/mcp-remote/package.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
35
apps/mcp-remote/src/hooks.server.ts
Normal file
35
apps/mcp-remote/src/hooks.server.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { http_transport } from '$lib/mcp/index.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
if (event.request.method === 'GET') {
|
||||
const accept = event.request.headers.get('accept');
|
||||
if (accept) {
|
||||
const accepts = accept.split(',');
|
||||
if (!accepts.includes('text/event-stream')) {
|
||||
// the request it's a browser request, not an MCP client request
|
||||
// it means someone probably opened it from the docs...we should redirect to the docs
|
||||
redirect(302, 'https://svelte.dev/docs/mcp/overview');
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
0
apps/mcp-remote/src/lib/index.ts
Normal file
0
apps/mcp-remote/src/lib/index.ts
Normal file
6
apps/mcp-remote/src/lib/mcp/index.ts
Normal file
6
apps/mcp-remote/src/lib/mcp/index.ts
Normal 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,
|
||||
});
|
||||
15
apps/mcp-remote/src/lib/server/db/index.ts
Normal file
15
apps/mcp-remote/src/lib/server/db/index.ts
Normal 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 });
|
||||
2
apps/mcp-remote/src/lib/server/db/schema.ts
Normal file
2
apps/mcp-remote/src/lib/server/db/schema.ts
Normal 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';
|
||||
19
apps/mcp-remote/tsconfig.json
Normal file
19
apps/mcp-remote/tsconfig.json
Normal 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
|
||||
}
|
||||
22
apps/mcp-remote/vite.config.ts
Normal file
22
apps/mcp-remote/vite.config.ts
Normal 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
320
docs/tmcp.md
Normal 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
|
||||
44
documentation/docs/10-introduction/10-overview.md
Normal file
44
documentation/docs/10-introduction/10-overview.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
The Svelte MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) server can help your LLM or agent of choice write better Svelte code. It works by providing documentation relevant to the task at hand, and statically analysing generated code so that it can suggest fixes and best practices.
|
||||
|
||||
## Setup
|
||||
|
||||
The setup varies based on the version of the MCP you prefer — remote or local — and your chosen MCP client (e.g. Claude Code, Codex CLI or GitHub Copilot):
|
||||
|
||||
- [local setup](local-setup) using `@sveltejs/mcp`
|
||||
- [remote setup](remote-setup) using [mcp.svelte.dev/mcp](https://mcp.svelte.dev/mcp)
|
||||
|
||||
## Usage
|
||||
|
||||
To get the most out of the MCP server we recommend including the following prompt in your `AGENTS.md` (or `CLAUDE.md`, if using Claude Code). This will tell the LLM which tools are available and when it's appropriate to use them.
|
||||
|
||||
```md
|
||||
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
## Available MCP Tools:
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||
|
||||
### 3. svelte-autofixer
|
||||
|
||||
Analyzes Svelte code and returns issues and suggestions.
|
||||
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||
|
||||
### 4. playground-link
|
||||
|
||||
Generates a Svelte Playground link with the provided code.
|
||||
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||
```
|
||||
|
||||
If your MCP client supports it, we also recommend using the [svelte-task](prompts#svelte-task) prompt to instruct the LLM on the best way to use the MCP server.
|
||||
3
documentation/docs/10-introduction/index.md
Normal file
3
documentation/docs/10-introduction/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
113
documentation/docs/20-setup/20-local-setup.md
Normal file
113
documentation/docs/20-setup/20-local-setup.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: Local setup
|
||||
---
|
||||
|
||||
The local (or stdio) version of the MCP server is available via the [`@sveltejs/mcp`](https://www.npmjs.com/package/@sveltejs/mcp) npm package. You can either install it globally and then reference it in your configuration or run it with `npx`:
|
||||
|
||||
```bash
|
||||
npx -y @sveltejs/mcp
|
||||
```
|
||||
|
||||
Here's how to set it up in some common MCP clients:
|
||||
|
||||
## Claude Code
|
||||
|
||||
To include the local MCP version in Claude Code, simply run the following command:
|
||||
|
||||
```bash
|
||||
claude mcp add -t stdio -s [scope] svelte npx -y @sveltejs/mcp
|
||||
```
|
||||
|
||||
The `[scope]` must be `user`, `project` or `local`.
|
||||
|
||||
## Claude Desktop
|
||||
|
||||
In the Settings > Developer section, click on Edit Config. It will open the folder with a `claude_desktop_config.json` file in it. Edit the file to include the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"svelte": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@sveltejs/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Codex CLI
|
||||
|
||||
Add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
|
||||
|
||||
```toml
|
||||
[mcp_servers.svelte]
|
||||
command = "npx"
|
||||
args = ["-y", "@sveltejs/mcp"]
|
||||
```
|
||||
|
||||
## Gemini CLI
|
||||
|
||||
To include the local MCP version in Gemini CLI, simply run the following command:
|
||||
|
||||
```bash
|
||||
gemini mcp add -t stdio -s [scope] svelte npx -y @sveltejs/mcp
|
||||
```
|
||||
|
||||
The `[scope]` must be `user`, `project` or `local`.
|
||||
|
||||
## OpenCode
|
||||
|
||||
Run the command:
|
||||
|
||||
```bash
|
||||
opencode mcp add
|
||||
```
|
||||
|
||||
and follow the instructions, selecting 'Local' under the 'Select MCP server type' prompt:
|
||||
|
||||
```bash
|
||||
opencode mcp add
|
||||
|
||||
┌ Add MCP server
|
||||
│
|
||||
◇ Enter MCP server name
|
||||
│ svelte
|
||||
│
|
||||
◇ Select MCP server type
|
||||
│ Local
|
||||
│
|
||||
◆ Enter command to run
|
||||
│ npx -y @sveltejs/mcp
|
||||
```
|
||||
|
||||
## VS Code
|
||||
|
||||
- Open the command palette
|
||||
- Select "MCP: Add Server..."
|
||||
- Select "Command (stdio)"
|
||||
- Insert `npx -y @sveltejs/mcp` in the input and press `Enter`
|
||||
- When prompted for a name, insert `svelte`
|
||||
- Select if you want to add it as a `Global` or `Workspace` MCP server
|
||||
|
||||
## Cursor
|
||||
|
||||
- Open the command palette
|
||||
- Select "View: Open MCP Settings"
|
||||
- Click on "Add custom MCP"
|
||||
|
||||
It will open a file with your MCP servers where you can add the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"svelte": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@sveltejs/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Other clients
|
||||
|
||||
If we didn't include the MCP client you are using, refer to their documentation for `stdio` servers and use `npx` as the command and `-y @sveltejs/mcp` as the arguments.
|
||||
101
documentation/docs/20-setup/30-remote-setup.md
Normal file
101
documentation/docs/20-setup/30-remote-setup.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Remote setup
|
||||
---
|
||||
|
||||
The remote version of the MCP server is available on `https://mcp.svelte.dev/mcp`.
|
||||
|
||||
Here's how to set it up in some common MCP clients:
|
||||
|
||||
## Claude Code
|
||||
|
||||
To include the remote MCP version in Claude Code, simply run the following command:
|
||||
|
||||
```bash
|
||||
claude mcp add -t http -s [scope] svelte https://mcp.svelte.dev/mcp
|
||||
```
|
||||
|
||||
You can choose your preferred `scope` (it must be `user`, `project` or `local`) and `name`.
|
||||
|
||||
## Claude Desktop
|
||||
|
||||
- Open Settings > Connectors
|
||||
- Click on Add Custom Connector
|
||||
- When prompted for a name, enter `svelte`
|
||||
- Under the Remote MCP server URL input, use `https://mcp.svelte.dev/mcp`
|
||||
- Click Add
|
||||
|
||||
## Codex CLI
|
||||
|
||||
Add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
|
||||
|
||||
```toml
|
||||
experimental_use_rmcp_client = true
|
||||
[mcp_servers.svelte]
|
||||
url = "https://mcp.svelte.dev/mcp"
|
||||
```
|
||||
|
||||
## Gemini CLI
|
||||
|
||||
To use the remote MCP server with Gemini CLI, simply run the following command:
|
||||
|
||||
```bash
|
||||
gemini mcp add -t http -s [scope] svelte https://mcp.svelte.dev/mcp
|
||||
```
|
||||
|
||||
The `[scope]` must be `user`, `project` or `local`.
|
||||
|
||||
## OpenCode
|
||||
|
||||
Run the command:
|
||||
|
||||
```bash
|
||||
opencode mcp add
|
||||
```
|
||||
|
||||
and follow the instructions, selecting 'Remote' under the 'Select MCP server type' prompt:
|
||||
|
||||
```bash
|
||||
opencode mcp add
|
||||
|
||||
┌ Add MCP server
|
||||
│
|
||||
◇ Enter MCP server name
|
||||
│ svelte
|
||||
│
|
||||
◇ Select MCP server type
|
||||
│ Remote
|
||||
│
|
||||
◇ Enter MCP server URL
|
||||
│ https://mcp.svelte.dev/mcp
|
||||
```
|
||||
|
||||
## VS Code
|
||||
|
||||
- Open the command palette
|
||||
- Select "MCP: Add Server..."
|
||||
- Select "HTTP (HTTP or Server-Sent-Events)"
|
||||
- Insert `https://mcp.svelte.dev/mcp` in the input and press `Enter`
|
||||
- Insert your preferred name
|
||||
- Select if you want to add it as a `Global` or `Workspace` MCP server
|
||||
|
||||
## Cursor
|
||||
|
||||
- Open the command palette
|
||||
- Select "View: Open MCP Settings"
|
||||
- Click on "Add custom MCP"
|
||||
|
||||
It will open a file with your MCP servers where you can add the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"svelte": {
|
||||
"url": "https://mcp.svelte.dev/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Other clients
|
||||
|
||||
If we didn't include the MCP client you are using, refer to their documentation for `remote` servers and use `https://mcp.svelte.dev/mcp` as the URL.
|
||||
3
documentation/docs/20-setup/index.md
Normal file
3
documentation/docs/20-setup/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Setup
|
||||
---
|
||||
21
documentation/docs/30-capabilities/10-tools.md
Normal file
21
documentation/docs/30-capabilities/10-tools.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Tools
|
||||
---
|
||||
|
||||
The following tools are provided by the MCP server to the model, which can decide to call one or more of them during a session:
|
||||
|
||||
## list-sections
|
||||
|
||||
Provides a list of all the available documentation sections.
|
||||
|
||||
## get-documentation
|
||||
|
||||
Allows the model to get the full (and up-to-date) documentation for the requested sections directly from [svelte.dev/docs](/docs).
|
||||
|
||||
## svelte-autofixer
|
||||
|
||||
Uses static analysis to provide suggestions for the generated code. It should be invoked in a loop by the model until all issues and suggestions are resolved.
|
||||
|
||||
## playground-link
|
||||
|
||||
Generates an ephemeral playground link with the generated code. It's useful when the generated code is not written to a file in your project and you want to quickly test the generated solution. The code is not stored anywhere except the URL itself (which will often, as a consequence, be quite large).
|
||||
9
documentation/docs/30-capabilities/20-resources.md
Normal file
9
documentation/docs/30-capabilities/20-resources.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Resources
|
||||
---
|
||||
|
||||
This is the list of available resources provided by the MCP server. Resources are included by the user (not by the LLM) and are useful if you want to include specific knowledge in your session. For example, if you know that the component will need to use transitions you can include the transition documentation directly without asking the LLM to do it for you.
|
||||
|
||||
## doc-section
|
||||
|
||||
This dynamic resource allows you to add every section of the Svelte documentation as a resource. The URI looks like this `svelte://slug-of-the-docs.md` and the returned resource will contain the `llms.txt` version of the specific page you selected.
|
||||
26
documentation/docs/30-capabilities/30-prompts.md
Normal file
26
documentation/docs/30-capabilities/30-prompts.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Prompts
|
||||
---
|
||||
|
||||
This is the list of available prompts provided by the MCP server. Prompts are selected by the user and are sent as a user message. They can be useful to write repetitive instructions for the LLM on how to properly use the MCP server.
|
||||
|
||||
## svelte-task
|
||||
|
||||
This prompt should be used whenever you are asking the model to work on some Svelte-related task. It will instruct the LLM on which documentation sections are available, which tool to invoke, when to invoke it, and how to interpret the result. It will ask you for the description of the task and the returned value will look like this:
|
||||
|
||||
```
|
||||
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>
|
||||
[all available docs]
|
||||
</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>
|
||||
[your task here]
|
||||
</task>
|
||||
|
||||
If you are not writing the code into a file, once you have the final version of the code ask the user if they want to generate a playground link to quickly check the code in it and if they 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.
|
||||
```
|
||||
3
documentation/docs/30-capabilities/index.md
Normal file
3
documentation/docs/30-capabilities/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Capabilities
|
||||
---
|
||||
3
documentation/docs/index.md
Normal file
3
documentation/docs/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: MCP
|
||||
---
|
||||
@@ -5,15 +5,20 @@ 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),
|
||||
{
|
||||
ignores: ['.claude/**/*'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
eslint_plugin_import.flatConfigs.recommended,
|
||||
prettier,
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
@@ -24,6 +29,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 +63,4 @@ export default ts.config(
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
]);
|
||||
|
||||
66
package.json
66
package.json
@@ -1,33 +1,22 @@
|
||||
{
|
||||
"name": "@sveltejs/mcp",
|
||||
"name": "@sveltejs/mcp-mono",
|
||||
"version": "0.0.1",
|
||||
"description": "The official Svelte MCP server implementation",
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"svelte-mcp": "./dist/lib/stdio.js"
|
||||
},
|
||||
"packageManager": "pnpm@10.17.1",
|
||||
"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",
|
||||
"build": "pnpm -r run build",
|
||||
"dev": "pnpm --filter @sveltejs/mcp-remote run dev",
|
||||
"check": "pnpm -r run check",
|
||||
"check:publint": "pnpm -r run check:publint",
|
||||
"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": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector"
|
||||
"inspect": "pnpm mcp-inspector",
|
||||
"release": "pnpm --filter @sveltejs/mcp run build && changeset publish"
|
||||
},
|
||||
"keywords": [
|
||||
"svelte",
|
||||
@@ -37,44 +26,23 @@
|
||||
],
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@eslint/js": "^9.36.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/eslint-scope": "^8.3.2",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/node": "^24.3.1",
|
||||
"@typescript-eslint/types": "^8.43.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"eslint": "^9.18.0",
|
||||
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
|
||||
"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",
|
||||
"publint": "^0.3.13",
|
||||
"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"
|
||||
|
||||
19
packages/mcp-schema/package.json
Normal file
19
packages/mcp-schema/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.40.1"
|
||||
}
|
||||
}
|
||||
8
packages/mcp-schema/src/index.js
Normal file
8
packages/mcp-schema/src/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @import * as schema from './schema.js'
|
||||
*/
|
||||
export * from './schema.js';
|
||||
|
||||
/**
|
||||
* @typedef {typeof schema} Schema
|
||||
*/
|
||||
@@ -1,5 +1,5 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { float_32_array } from './utils';
|
||||
import { float_32_array } from './utils.js';
|
||||
|
||||
/**
|
||||
* NOTE: if you modify a schema adding a vector column you need to manually add this
|
||||
@@ -40,10 +40,7 @@ export const distillation_jobs = sqliteTable('distillation_jobs', {
|
||||
started_at: integer('started_at', { mode: 'timestamp' }),
|
||||
completed_at: integer('completed_at', { mode: 'timestamp' }),
|
||||
error_message: text('error_message'),
|
||||
metadata: text('metadata', { mode: 'json' })
|
||||
.$type<Record<string, unknown>>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
@@ -59,10 +56,7 @@ export const content = sqliteTable('content', {
|
||||
content: text('content').notNull(),
|
||||
size_bytes: integer('size_bytes').notNull(),
|
||||
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
|
||||
metadata: text('metadata', { mode: 'json' })
|
||||
.$type<Record<string, unknown>>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
@@ -78,10 +72,7 @@ export const content_distilled = sqliteTable('content_distilled', {
|
||||
content: text('content').notNull(),
|
||||
size_bytes: integer('size_bytes').notNull(),
|
||||
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
|
||||
metadata: text('metadata', { mode: 'json' })
|
||||
.$type<Record<string, unknown>>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
@@ -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,8 +28,10 @@ export function vector(arr: number[]) {
|
||||
* .orderBy(sql`distance`)
|
||||
* .execute();
|
||||
*/
|
||||
export function distance(column: Column, arr: number[], as = 'distance') {
|
||||
return sql<number>`CASE ${column} ISNULL WHEN 1 THEN 1 ELSE vector_distance_cos(${column}, vector32(${JSON.stringify(arr)})) END`.as(
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -34,19 +39,27 @@ export function distance(column: Column, arr: number[], as = 'distance') {
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
});
|
||||
40
packages/mcp-server/package.json
Normal file
40
packages/mcp-server/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@sveltejs/mcp-server",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
1
packages/mcp-server/src/index.ts
Normal file
1
packages/mcp-server/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { server, type SvelteMcp } from './mcp/index.js';
|
||||
@@ -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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parse } from '../../parse/parse.js';
|
||||
import { walk } from '../../index.js';
|
||||
import { walk } from '../../mcp/autofixers/ast/walk.js';
|
||||
import type { Node } from 'estree';
|
||||
import * as autofixers from './visitors/index.js';
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
|
||||
@@ -41,6 +42,8 @@ function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
|
||||
sourceType: 'module',
|
||||
parser: svelte_parser,
|
||||
parserOptions: {
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig: svelte_config,
|
||||
},
|
||||
},
|
||||
@@ -78,7 +81,7 @@ export async function add_eslint_issues(
|
||||
const eslint = get_linter(desired_svelte_version);
|
||||
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });
|
||||
|
||||
for (const message of results[0].messages) {
|
||||
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) {
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { AssignmentExpression, Identifier, Node, UpdateExpression } from 'estree';
|
||||
import type { Autofixer, AutofixerState } from '.';
|
||||
import type { Autofixer, AutofixerState } from './index.js';
|
||||
import { left_most_id } from '../ast/utils.js';
|
||||
import type { SvelteNode } from 'svelte-eslint-parser/lib/ast';
|
||||
import type { AST } from 'svelte-eslint-parser';
|
||||
import type { Context } from 'zimmerframe';
|
||||
|
||||
function run_if_in_effect(path: (Node | SvelteNode)[], state: AutofixerState, to_run: () => void) {
|
||||
function run_if_in_effect(
|
||||
path: (Node | AST.SvelteNode)[],
|
||||
state: AutofixerState,
|
||||
to_run: () => void,
|
||||
) {
|
||||
const in_effect = path.findLast(
|
||||
(node) =>
|
||||
node.type === 'CallExpression' &&
|
||||
@@ -25,7 +29,7 @@ function run_if_in_effect(path: (Node | SvelteNode)[], state: AutofixerState, to
|
||||
|
||||
function visitor(
|
||||
node: UpdateExpression | AssignmentExpression,
|
||||
{ state, path }: Context<Node | SvelteNode, AutofixerState>,
|
||||
{ state, path }: Context<Node | AST.SvelteNode, AutofixerState>,
|
||||
) {
|
||||
run_if_in_effect(path, state, () => {
|
||||
function check_if_stateful_id(id: Identifier) {
|
||||
@@ -35,7 +39,7 @@ function visitor(
|
||||
const init = definition.node.init;
|
||||
if (
|
||||
init?.type === 'CallExpression' &&
|
||||
state.parsed.is_rune(init, ['$state', '$state.raw'])
|
||||
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.`,
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Identifier, PrivateIdentifier } from 'estree';
|
||||
import type { Autofixer } from '.';
|
||||
import type { Autofixer } from './index.js';
|
||||
|
||||
export const derived_with_function: Autofixer = {
|
||||
CallExpression(node, { state, path }) {
|
||||
@@ -7,15 +7,15 @@ export const derived_with_function: Autofixer = {
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === '$derived' &&
|
||||
state.parsed.is_rune(node, ['$derived']) &&
|
||||
(node.arguments[0].type === 'ArrowFunctionExpression' ||
|
||||
node.arguments[0].type === 'FunctionExpression')
|
||||
(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') {
|
||||
if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
||||
// const something = $derived(...)
|
||||
variable_id = parent.id;
|
||||
} else if (parent.type === 'PropertyDefinition') {
|
||||
} else if (parent?.type === 'PropertyDefinition') {
|
||||
// class X { something = $derived(...) }
|
||||
variable_id =
|
||||
parent.key.type === 'Identifier'
|
||||
@@ -23,7 +23,7 @@ export const derived_with_function: Autofixer = {
|
||||
: parent.key.type === 'PrivateIdentifier'
|
||||
? parent.key
|
||||
: undefined;
|
||||
} else if (parent.type === 'AssignmentExpression') {
|
||||
} else if (parent?.type === 'AssignmentExpression') {
|
||||
// this.something = $derived(...)
|
||||
variable_id =
|
||||
parent.left.type === 'MemberExpression'
|
||||
@@ -1,5 +1,5 @@
|
||||
import { base_runes } from '../../../constants.js';
|
||||
import type { Autofixer } from '.';
|
||||
import type { Autofixer } from './index.js';
|
||||
|
||||
const dollarless_runes = base_runes.map((r) => r.replace('$', ''));
|
||||
|
||||
@@ -12,7 +12,7 @@ export type AutofixerState = {
|
||||
export type Autofixer = Visitors<Node | AST.SvelteNode, AutofixerState>;
|
||||
|
||||
export * from './assign-in-effect.js';
|
||||
export * from './set-or-update-state.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';
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Autofixer } from '.';
|
||||
import type { Autofixer } from './index.js';
|
||||
|
||||
export const use_runes_instead_of_store: Autofixer = {
|
||||
ImportDeclaration(node, { state, next }) {
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
22
packages/mcp-server/src/mcp/handlers/index.ts
Normal file
22
packages/mcp-server/src/mcp/handlers/index.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
1
packages/mcp-server/src/mcp/handlers/prompts/index.ts
Normal file
1
packages/mcp-server/src/mcp/handlers/prompts/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './svelte-task.js';
|
||||
46
packages/mcp-server/src/mcp/handlers/prompts/svelte-task.ts
Normal file
46
packages/mcp-server/src/mcp/handlers/prompts/svelte-task.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { SvelteMcp } from '../../index.js';
|
||||
import * as v from 'valibot';
|
||||
import { get_sections } from '../../utils.js';
|
||||
|
||||
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 }) => {
|
||||
const available_docs: string[] = (await get_sections()).map((s) => s.title);
|
||||
|
||||
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.
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { SvelteMcp } from '../../index.js';
|
||||
import { get_sections, fetch_with_timeout } from '../../utils.js';
|
||||
|
||||
export async function list_sections(server: SvelteMcp) {
|
||||
const sections = await get_sections();
|
||||
|
||||
server.template(
|
||||
{
|
||||
name: 'Svelte Doc Section',
|
||||
description: 'A single documentation section',
|
||||
list() {
|
||||
return sections.map((section) => {
|
||||
const section_name = section.slug;
|
||||
const resource_name = section_name;
|
||||
const resource_uri = `svelte://${section_name}.md`;
|
||||
return {
|
||||
name: resource_name,
|
||||
description: section.use_cases,
|
||||
uri: resource_uri,
|
||||
title: section.title,
|
||||
};
|
||||
});
|
||||
},
|
||||
complete: {
|
||||
slug: (query) => {
|
||||
const values = sections
|
||||
.reduce<string[]>((acc, section) => {
|
||||
const section_name = section.slug;
|
||||
const resource_name = section_name;
|
||||
if (section_name.includes(query.toLowerCase())) {
|
||||
acc.push(resource_name);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
// there's a hard limit of 100 for completions
|
||||
.slice(0, 100);
|
||||
return {
|
||||
completion: {
|
||||
values,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
uri: 'svelte://{/slug*}.md',
|
||||
},
|
||||
async (uri, { slug }) => {
|
||||
const section = sections.find((section) => {
|
||||
return slug === section.slug;
|
||||
});
|
||||
if (!section) throw new Error(`Section not found: ${slug}`);
|
||||
const response = await fetch_with_timeout(section.url);
|
||||
const content = await response.text();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
1
packages/mcp-server/src/mcp/handlers/resources/index.ts
Normal file
1
packages/mcp-server/src/mcp/handlers/resources/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './doc-section.js';
|
||||
120
packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts
Normal file
120
packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { SvelteMcp } from '../../index.js';
|
||||
import * as v from 'valibot';
|
||||
import { get_sections, fetch_with_timeout } from '../../utils.js';
|
||||
import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js';
|
||||
|
||||
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',
|
||||
),
|
||||
),
|
||||
}),
|
||||
},
|
||||
async ({ 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 available_sections = await get_sections();
|
||||
|
||||
const settled_results = await Promise.allSettled(
|
||||
sections.map(async (requested_section) => {
|
||||
const matched_section = available_sections.find(
|
||||
(s) =>
|
||||
s.title.toLowerCase() === requested_section.toLowerCase() ||
|
||||
s.url === requested_section,
|
||||
);
|
||||
|
||||
if (matched_section) {
|
||||
try {
|
||||
const response = await fetch_with_timeout(matched_section.url);
|
||||
if (response.ok) {
|
||||
const content = await response.text();
|
||||
return { success: true, content: `## ${matched_section.title}\n\n${content}` };
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${matched_section.title}\n\nError: Could not fetch documentation (HTTP ${response.status})`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${matched_section.title}\n\nError: Failed to fetch documentation - ${error}`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${requested_section}\n\nError: Section not found.`,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const results = settled_results.map((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return result.value;
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `Error: Couldn't fetch - ${result.reason}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const has_any_success = results.some((result) => result.success);
|
||||
let final_text = results.map((r) => r.content).join('\n\n---\n\n');
|
||||
|
||||
if (!has_any_success) {
|
||||
const formatted_sections = available_sections
|
||||
.map(
|
||||
(section) =>
|
||||
`* title: ${section.title}, use_cases: ${section.use_cases}, path: ${section.url}`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
final_text += `\n\n---\n\n${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: final_text,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
4
packages/mcp-server/src/mcp/handlers/tools/index.ts
Normal file
4
packages/mcp-server/src/mcp/handlers/tools/index.ts
Normal 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';
|
||||
32
packages/mcp-server/src/mcp/handlers/tools/list-sections.ts
Normal file
32
packages/mcp-server/src/mcp/handlers/tools/list-sections.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { SvelteMcp } from '../../index.js';
|
||||
import { get_sections } from '../../utils.js';
|
||||
import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.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], use_cases: [use_cases], 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.',
|
||||
},
|
||||
async () => {
|
||||
const sections = await get_sections();
|
||||
const formatted_sections = sections
|
||||
.map(
|
||||
(section) =>
|
||||
`* title: ${section.title}, use_cases: ${section.use_cases}, path: ${section.url}`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
112
packages/mcp-server/src/mcp/handlers/tools/playground-link.ts
Normal file
112
packages/mcp-server/src/mcp/handlers/tools/playground-link.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
5
packages/mcp-server/src/mcp/handlers/tools/prompts.ts
Normal file
5
packages/mcp-server/src/mcp/handlers/tools/prompts.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const SECTIONS_LIST_INTRO =
|
||||
'List of available Svelte documentation sections and its inteneded uses:';
|
||||
|
||||
export const SECTIONS_LIST_OUTRO =
|
||||
'Use the title or path with the get-documentation tool to get more details about a specific section.';
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
30
packages/mcp-server/src/mcp/index.ts
Normal file
30
packages/mcp-server/src/mcp/index.ts
Normal 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);
|
||||
12
packages/mcp-server/src/mcp/schemas/index.ts
Normal file
12
packages/mcp-server/src/mcp/schemas/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as v from 'valibot';
|
||||
|
||||
export const documentation_sections_schema = v.record(
|
||||
v.string(),
|
||||
v.object({
|
||||
metadata: v.object({
|
||||
title: v.string(),
|
||||
use_cases: v.optional(v.string()),
|
||||
}),
|
||||
slug: v.string(),
|
||||
}),
|
||||
);
|
||||
31
packages/mcp-server/src/mcp/utils.ts
Normal file
31
packages/mcp-server/src/mcp/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as v from 'valibot';
|
||||
import { documentation_sections_schema } from '../mcp/schemas/index.js';
|
||||
|
||||
export async function fetch_with_timeout(
|
||||
url: string,
|
||||
timeout_ms: number = 10000,
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(timeout_ms) });
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error(`Request timed out after ${timeout_ms}ms`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function get_sections() {
|
||||
const sections = await fetch_with_timeout(
|
||||
'https://svelte.dev/docs/experimental/sections.json',
|
||||
).then((res) => res.json());
|
||||
const validated_sections = v.safeParse(documentation_sections_schema, sections);
|
||||
if (!validated_sections.success) return [];
|
||||
return Object.entries(validated_sections.output).map(([, section]) => ({
|
||||
title: section.metadata.title,
|
||||
use_cases: section.metadata.use_cases ?? 'read document for use cases',
|
||||
slug: section.slug,
|
||||
url: `https://svelte.dev/${section.slug}/llms.txt`,
|
||||
}));
|
||||
}
|
||||
@@ -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);
|
||||
7
packages/mcp-stdio/CHANGELOG.md
Normal file
7
packages/mcp-stdio/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# @sveltejs/mcp
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: latest version ([#25](https://github.com/sveltejs/mcp/pull/25))
|
||||
16
packages/mcp-stdio/README.md
Normal file
16
packages/mcp-stdio/README.md
Normal 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
|
||||
```
|
||||
43
packages/mcp-stdio/package.json
Normal file
43
packages/mcp-stdio/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@sveltejs/mcp",
|
||||
"version": "0.0.2",
|
||||
"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 && publint",
|
||||
"dev": "tsdown --watch",
|
||||
"test": "vitest",
|
||||
"check": "tsc --noEmit",
|
||||
"check:publint": "publint --strict"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
7
packages/mcp-stdio/src/index.ts
Normal file
7
packages/mcp-stdio/src/index.ts
Normal 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();
|
||||
5
packages/mcp-stdio/tsconfig.json
Normal file
5
packages/mcp-stdio/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
21
packages/mcp-stdio/tsdown.config.ts
Normal file
21
packages/mcp-stdio/tsdown.config.ts
Normal 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',
|
||||
},
|
||||
]);
|
||||
3
packages/mcp-stdio/vitest.config.ts
Normal file
3
packages/mcp-stdio/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({});
|
||||
3267
pnpm-lock.yaml
generated
3267
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
packages:
|
||||
- '*'
|
||||
- './packages/*'
|
||||
- './apps/*'
|
||||
|
||||
useNodeVersion: 22.19.0
|
||||
|
||||
19
renovate.json
Normal file
19
renovate.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
describe('add_autofixers_issues', () => {
|
||||
describe('assign_in_effect', () => {
|
||||
it(`should add suggestions when assigning to a stateful variable inside an effect`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
const count = $state(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 = $state(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 = $state(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 = $state({ 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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([{ method: 'set' }, { method: 'update' }])(
|
||||
'set_or_update_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 = $state(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 = $state([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 ($state({}))`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
const count = $state({ 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 ($state(new Class()))`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
const count = $state(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 ($state(variable_name))`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
const { init } = $props();
|
||||
const count = $state(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 = $state({});
|
||||
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('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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { Autofixer } from '.';
|
||||
import { left_most_id } from '../ast/utils.js';
|
||||
|
||||
const UPDATE_PROPERTIES = ['set', 'update'];
|
||||
|
||||
export const set_or_update_state: Autofixer = {
|
||||
MemberExpression(node, { state, next, path }) {
|
||||
const parent = path[path.length - 1];
|
||||
if (
|
||||
parent.type === 'CallExpression' &&
|
||||
parent.callee === node &&
|
||||
node.property.type === 'Identifier' &&
|
||||
UPDATE_PROPERTIES.includes(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'])
|
||||
) {
|
||||
let suggestion = `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}" method on it. Please verify that before updating the code to use a normal assignment`;
|
||||
}
|
||||
state.output.suggestions.push(suggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
},
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
|
||||
import { HttpTransport } from '@tmcp/transport-http';
|
||||
import { StdioTransport } from '@tmcp/transport-stdio';
|
||||
import { McpServer } from 'tmcp';
|
||||
import * as v from 'valibot';
|
||||
import { add_autofixers_issues } from './autofixers/add-autofixers-issues.js';
|
||||
import { add_compile_issues } from './autofixers/add-compile-issues.js';
|
||||
import { add_eslint_issues } from './autofixers/add-eslint-issues.js';
|
||||
|
||||
const server = new McpServer(
|
||||
{
|
||||
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.',
|
||||
},
|
||||
);
|
||||
|
||||
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.description(
|
||||
'The desired svelte version...if possible read this from the package.json of the user project, otherwise use some hint from the wording (if the user asks for runes it wants version 5). Default to 5 in case of doubt.',
|
||||
),
|
||||
),
|
||||
filename: v.optional(v.string()),
|
||||
}),
|
||||
outputSchema: v.object({
|
||||
issues: v.array(v.string()),
|
||||
suggestions: v.array(v.string()),
|
||||
require_another_tool_call_after_fixing: v.boolean(),
|
||||
}),
|
||||
annotations: {
|
||||
title: 'Svelte Autofixer',
|
||||
destructiveHint: false,
|
||||
readOnlyHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
},
|
||||
async ({ code, filename, desired_svelte_version }) => {
|
||||
const content: {
|
||||
issues: string[];
|
||||
suggestions: string[];
|
||||
require_another_tool_call_after_fixing: boolean;
|
||||
} = { issues: [], suggestions: [], require_another_tool_call_after_fixing: false };
|
||||
try {
|
||||
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 (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,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.prompt(
|
||||
{
|
||||
name: 'svelte-task-prompt',
|
||||
title: 'Svelte Task Prompt',
|
||||
description:
|
||||
'Use this Prompt to ask for any svelte related task. It will automatically instruct the LLM on how to best use the autofixer and how to query for documentation pages.',
|
||||
schema: v.object({
|
||||
task: v.pipe(v.string(), v.description('The task to be performed')),
|
||||
}),
|
||||
},
|
||||
async ({ task }) => {
|
||||
// TODO: implement logic to fetch the available docs paths to return in the prompt
|
||||
const available_docs: string[] = [];
|
||||
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool \`get_documentation\` with one of the following paths:
|
||||
<available-docs-paths>
|
||||
${JSON.stringify(available_docs, null, 2)}
|
||||
</available-docs-paths>
|
||||
|
||||
Every time you write a Svelte component or a Svelte module you MUST invoke the \`svelte-autofixer\` tool providing the code. The tool will return a list of issues or suggestions. If there are any issues or suggestions you MUST fix them and call the tool again with the updated code. You MUST keep doing this until the tool returns no issues or suggestions. Only then you can return the code to the user.
|
||||
|
||||
This is the task you will work on:
|
||||
|
||||
<task>
|
||||
${task}
|
||||
</task>
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const http_transport = new HttpTransport(server, {
|
||||
cors: true,
|
||||
});
|
||||
export const stdio_transport = new StdioTransport(server);
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createClient } from '@libsql/client';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
import * as schema from './schema';
|
||||
|
||||
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');
|
||||
|
||||
const client = createClient({
|
||||
url: process.env.DATABASE_URL,
|
||||
authToken: process.env.DATABASE_TOKEN,
|
||||
});
|
||||
|
||||
export const db = drizzle(client, { schema, logger: true });
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { stdio_transport } from './mcp/index.js';
|
||||
|
||||
stdio_transport.listen();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user