mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-07-03 19:19:25 +08:00
Compare commits
132 Commits
add-devtoo
...
setup-chan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baad760634 | ||
|
|
b2275587ee | ||
|
|
4964303100 | ||
|
|
81901b2564 | ||
|
|
5aa1aa401a | ||
|
|
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 | ||
|
|
9f580a36ef | ||
|
|
d81d6a3d95 | ||
|
|
f04cb139e3 | ||
|
|
dedfd0b3b7 | ||
|
|
5d50518c3c | ||
|
|
74d2fb8f0e | ||
|
|
86675ea1d7 | ||
|
|
830fd73ab1 | ||
|
|
da995bdc69 | ||
|
|
26b3986740 | ||
|
|
8d53f56151 | ||
|
|
fd355e872d | ||
|
|
d4edef1d82 | ||
|
|
26d30c73c7 | ||
|
|
13719d0646 | ||
|
|
4c8cadd585 | ||
|
|
72e89af60f | ||
|
|
13c4832d1b | ||
|
|
7c7a1f939f | ||
|
|
5becaf3b5e | ||
|
|
7f858929ad | ||
|
|
6b5588c2f9 | ||
|
|
b4259dd117 |
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": []
|
||||
}
|
||||
5
.changeset/shy-peaches-laugh.md
Normal file
5
.changeset/shy-peaches-laugh.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@sveltejs/mcp': patch
|
||||
---
|
||||
|
||||
feat: latest version
|
||||
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: publint
|
||||
run: pnpm check:publint
|
||||
|
||||
- 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
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
DATABASE_URL=file:test.db
|
||||
DATABASE_TOKEN=needs_to_be_set_but_it_can_be_anything
|
||||
VOYAGE_API_KEY=your_actual_api_key_here
|
||||
@@ -1,11 +1,12 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
if (!process.env.DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/lib/server/db/schema.ts',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: { url: process.env.DATABASE_URL },
|
||||
dialect: 'turso',
|
||||
dbCredentials: { url: process.env.DATABASE_URL, authToken: process.env.DATABASE_TOKEN },
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
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"
|
||||
}
|
||||
}
|
||||
23
apps/mcp-remote/src/hooks.server.ts
Normal file
23
apps/mcp-remote/src/hooks.server.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { http_transport } from '$lib/mcp/index.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
const mcp_response = await http_transport.respond(event.request, {
|
||||
db,
|
||||
});
|
||||
// we are deploying on vercel the SSE connection will timeout after 5 minutes...for
|
||||
// the moment we are not sending back any notifications (logs, or list changed notifications)
|
||||
// so it's a waste of resources to keep a connection open that will error
|
||||
// after 5 minutes making the logs dirty. For this reason if we have a response from
|
||||
// the MCP server and it's a GET request we just return an empty response (it has to be
|
||||
// 200 or the MCP client will complain)
|
||||
if (mcp_response && event.request.method === 'GET') {
|
||||
try {
|
||||
await mcp_response.body?.cancel();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return new Response('', { status: 200 });
|
||||
}
|
||||
return mcp_response ?? resolve(event);
|
||||
}
|
||||
|
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';
|
||||
1
apps/mcp-remote/src/routes/+page.svelte
Normal file
1
apps/mcp-remote/src/routes/+page.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Official Svelte MCP</h1>
|
||||
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
|
||||
@@ -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(
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
]);
|
||||
|
||||
65
package.json
65
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,43 +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",
|
||||
"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
|
||||
*/
|
||||
82
packages/mcp-schema/src/schema.js
Normal file
82
packages/mcp-schema/src/schema.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { float_32_array } from './utils.js';
|
||||
|
||||
/**
|
||||
* NOTE: if you modify a schema adding a vector column you need to manually add this
|
||||
*
|
||||
* CREATE INDEX IF NOT EXISTS name_of_the_index
|
||||
* ON `name_of_the_table` (
|
||||
* libsql_vector_idx(name_of_the_column, 'metric=cosine')
|
||||
* )
|
||||
*
|
||||
* to the generated migration file
|
||||
*/
|
||||
|
||||
export const distillations = sqliteTable('distillations', {
|
||||
id: integer('id').primaryKey(),
|
||||
preset_name: text('preset_name').notNull(),
|
||||
version: text('version').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_kb: integer('size_kb').notNull(),
|
||||
document_count: integer('document_count').notNull(),
|
||||
distillation_job_id: integer('distillation_job_id').references(() => distillation_jobs.id),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const distillation_jobs = sqliteTable('distillation_jobs', {
|
||||
id: integer('id').primaryKey(),
|
||||
preset_name: text('preset_name').notNull(),
|
||||
batch_id: text('batch_id'),
|
||||
status: text('status', { enum: ['pending', 'processing', 'completed', 'failed'] }).notNull(),
|
||||
model_used: text('model_used').notNull(),
|
||||
total_files: integer('total_files').notNull(),
|
||||
processed_files: integer('processed_files').notNull().default(0),
|
||||
successful_files: integer('successful_files').notNull().default(0),
|
||||
minimize_applied: integer('minimize_applied', { mode: 'boolean' }).notNull().default(false),
|
||||
total_input_tokens: integer('total_input_tokens').notNull().default(0),
|
||||
total_output_tokens: integer('total_output_tokens').notNull().default(0),
|
||||
started_at: integer('started_at', { mode: 'timestamp' }),
|
||||
completed_at: integer('completed_at', { mode: 'timestamp' }),
|
||||
error_message: text('error_message'),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const content = sqliteTable('content', {
|
||||
id: integer('id').primaryKey(),
|
||||
path: text('path').notNull(),
|
||||
filename: text('filename').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_bytes: integer('size_bytes').notNull(),
|
||||
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const content_distilled = sqliteTable('content_distilled', {
|
||||
id: integer('id').primaryKey(),
|
||||
path: text('path').notNull(),
|
||||
filename: text('filename').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_bytes: integer('size_bytes').notNull(),
|
||||
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
@@ -1,19 +1,22 @@
|
||||
import { sql, Column } from 'drizzle-orm';
|
||||
/**
|
||||
* @import { Column } from 'drizzle-orm';
|
||||
*/
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { customType } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
/**
|
||||
* Helper function to convert an array of embeddings into a format that can be inserted into a LibSQL vector column.
|
||||
* @param arr The embeddings array.
|
||||
* @param {number[]} arr The embeddings array.
|
||||
*/
|
||||
export function vector(arr: number[]) {
|
||||
export function vector(arr) {
|
||||
return sql`vector32(${JSON.stringify(arr)})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate the distance between a vector column and an array of embeddings and return it as a columns.
|
||||
* @param column The drizzle column representing the vector.
|
||||
* @param arr The embeddings array.
|
||||
* @param as The name of the returned column. Default is 'distance'.
|
||||
* @param {Column} column The drizzle column representing the vector.
|
||||
* @param {number} arr The embeddings array.
|
||||
* @param {string} as The name of the returned column. Default is 'distance'.
|
||||
*
|
||||
* @example
|
||||
* await db.select({
|
||||
@@ -25,26 +28,38 @@ export function vector(arr: number[]) {
|
||||
* .orderBy(sql`distance`)
|
||||
* .execute();
|
||||
*/
|
||||
export function distance(column: Column, arr: number[], as = 'distance') {
|
||||
return sql<number>`vector_distance_cos(${column}, vector32(${JSON.stringify(arr)}))`.as(as);
|
||||
export function distance(column, arr, as = 'distance') {
|
||||
return /** @type {typeof sql<number>} */ (
|
||||
sql
|
||||
)`CASE ${column} ISNULL WHEN 1 THEN 1 ELSE vector_distance_cos(${column}, vector32(${JSON.stringify(arr)})) END`.as(
|
||||
as,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom drizzle type to use the LibSQL vector column type.
|
||||
*/
|
||||
export const float_32_array = customType<{
|
||||
export const float_32_array = /** @type {typeof customType<{
|
||||
data: number[];
|
||||
config: { dimensions: number };
|
||||
configRequired: true;
|
||||
driverData: Buffer;
|
||||
}>({
|
||||
}>} */ (customType)({
|
||||
dataType(config) {
|
||||
return `F32_BLOB(${config.dimensions})`;
|
||||
},
|
||||
fromDriver(value: Buffer) {
|
||||
/**
|
||||
* @param {Buffer} value
|
||||
*/
|
||||
fromDriver(value) {
|
||||
return Array.from(new Float32Array(value.buffer));
|
||||
},
|
||||
toDriver(value: number[]) {
|
||||
/**
|
||||
*
|
||||
* @param {number[]} value
|
||||
* @returns
|
||||
*/
|
||||
toDriver(value) {
|
||||
return vector(value);
|
||||
},
|
||||
});
|
||||
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"
|
||||
}
|
||||
}
|
||||
23
packages/mcp-server/src/constants.ts
Normal file
23
packages/mcp-server/src/constants.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const base_runes = [
|
||||
'$state',
|
||||
'$effect',
|
||||
'$derived',
|
||||
'$inspect',
|
||||
'$props',
|
||||
'$bindable',
|
||||
'$host',
|
||||
] as const;
|
||||
|
||||
export const nested_runes = [
|
||||
'$state.raw',
|
||||
'$state.snapshot',
|
||||
'$effect.pre',
|
||||
'$effect.tracking',
|
||||
'$effect.pending',
|
||||
'$effect.root',
|
||||
'$derived.by',
|
||||
'$inspect.trace',
|
||||
'$props.id',
|
||||
] as const;
|
||||
|
||||
export const runes = [...base_runes, ...nested_runes] as const;
|
||||
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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { parse } from '../../parse/parse.js';
|
||||
import { walk } from '../../mcp/autofixers/ast/walk.js';
|
||||
import type { Node } from 'estree';
|
||||
import * as autofixers from './visitors/index.js';
|
||||
|
||||
export function add_autofixers_issues(
|
||||
content: { issues: string[]; suggestions: string[] },
|
||||
code: string,
|
||||
desired_svelte_version: number,
|
||||
filename = 'Component.svelte',
|
||||
) {
|
||||
const parsed = parse(code, filename);
|
||||
|
||||
// Run each autofixer separately to avoid interrupting logic flow
|
||||
for (const autofixer of Object.values(autofixers)) {
|
||||
walk(
|
||||
parsed.ast as unknown as Node,
|
||||
{ output: content, parsed, desired_svelte_version },
|
||||
autofixer,
|
||||
);
|
||||
}
|
||||
}
|
||||
20
packages/mcp-server/src/mcp/autofixers/add-compile-issues.ts
Normal file
20
packages/mcp-server/src/mcp/autofixers/add-compile-issues.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { compile } from 'svelte/compiler';
|
||||
|
||||
export function add_compile_issues(
|
||||
content: { issues: string[]; suggestions: string[] },
|
||||
code: string,
|
||||
desired_svelte_version: number,
|
||||
filename = 'Component.svelte',
|
||||
) {
|
||||
const compilation_result = compile(code, {
|
||||
filename: filename || 'Component.svelte',
|
||||
generate: false,
|
||||
runes: desired_svelte_version >= 5,
|
||||
});
|
||||
|
||||
for (const warning of compilation_result.warnings) {
|
||||
content.issues.push(
|
||||
`${warning.message} at line ${warning.start?.line}, column ${warning.start?.column}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
@@ -48,7 +51,7 @@ function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
|
||||
];
|
||||
}
|
||||
|
||||
export function get_linter(version: number) {
|
||||
function get_linter(version: number) {
|
||||
if (version < 5) {
|
||||
return (svelte_4_linter ??= new ESLint({
|
||||
overrideConfigFile: true,
|
||||
@@ -68,3 +71,23 @@ export function get_linter(version: number) {
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function add_eslint_issues(
|
||||
content: { issues: string[]; suggestions: string[] },
|
||||
code: string,
|
||||
desired_svelte_version: number,
|
||||
filename = 'Component.svelte',
|
||||
) {
|
||||
const eslint = get_linter(desired_svelte_version);
|
||||
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });
|
||||
|
||||
for (const message of results[0]?.messages ?? []) {
|
||||
if (message.severity === 2) {
|
||||
content.issues.push(`${message.message} at line ${message.line}, column ${message.column}`);
|
||||
} else if (message.severity === 1) {
|
||||
content.suggestions.push(
|
||||
`${message.message} at line ${message.line}, column ${message.column}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/mcp-server/src/mcp/autofixers/ast/utils.ts
Normal file
16
packages/mcp-server/src/mcp/autofixers/ast/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Identifier, MemberExpression } from 'estree';
|
||||
|
||||
/**
|
||||
* Gets the left-most identifier of a member expression or identifier.
|
||||
*/
|
||||
export function left_most_id(expression: MemberExpression | Identifier) {
|
||||
while (expression.type === 'MemberExpression') {
|
||||
expression = expression.object as MemberExpression | Identifier;
|
||||
}
|
||||
|
||||
if (expression.type !== 'Identifier') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { AssignmentExpression, Identifier, Node, UpdateExpression } from 'estree';
|
||||
import type { Autofixer, AutofixerState } from './index.js';
|
||||
import { left_most_id } from '../ast/utils.js';
|
||||
import type { AST } from 'svelte-eslint-parser';
|
||||
import type { Context } from 'zimmerframe';
|
||||
|
||||
function run_if_in_effect(
|
||||
path: (Node | AST.SvelteNode)[],
|
||||
state: AutofixerState,
|
||||
to_run: () => void,
|
||||
) {
|
||||
const in_effect = path.findLast(
|
||||
(node) =>
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === '$effect',
|
||||
);
|
||||
|
||||
if (
|
||||
in_effect &&
|
||||
in_effect.type === 'CallExpression' &&
|
||||
(in_effect.callee.type === 'Identifier' || in_effect.callee.type === 'MemberExpression')
|
||||
) {
|
||||
if (state.parsed.is_rune(in_effect, ['$effect', '$effect.pre'])) {
|
||||
to_run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function visitor(
|
||||
node: UpdateExpression | AssignmentExpression,
|
||||
{ state, path }: Context<Node | AST.SvelteNode, AutofixerState>,
|
||||
) {
|
||||
run_if_in_effect(path, state, () => {
|
||||
function check_if_stateful_id(id: Identifier) {
|
||||
const reference = state.parsed.find_reference_by_id(id);
|
||||
const definition = reference?.resolved?.defs[0];
|
||||
if (definition && definition.type === 'Variable') {
|
||||
const init = definition.node.init;
|
||||
if (
|
||||
init?.type === 'CallExpression' &&
|
||||
state.parsed.is_rune(init, ['$state', '$state.raw', '$derived', '$derived.by'])
|
||||
) {
|
||||
state.output.suggestions.push(
|
||||
`The stateful variable "${id.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const variable = node.type === 'UpdateExpression' ? node.argument : node.left;
|
||||
|
||||
if (variable.type === 'Identifier') {
|
||||
check_if_stateful_id(variable);
|
||||
} else if (variable.type === 'MemberExpression') {
|
||||
const object = left_most_id(variable);
|
||||
if (object) {
|
||||
check_if_stateful_id(object);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const assign_in_effect: Autofixer = {
|
||||
UpdateExpression: visitor,
|
||||
AssignmentExpression: visitor,
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Identifier, PrivateIdentifier } from 'estree';
|
||||
import type { Autofixer } from './index.js';
|
||||
|
||||
export const derived_with_function: Autofixer = {
|
||||
CallExpression(node, { state, path }) {
|
||||
if (
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === '$derived' &&
|
||||
state.parsed.is_rune(node, ['$derived']) &&
|
||||
(node.arguments[0]?.type === 'ArrowFunctionExpression' ||
|
||||
node.arguments[0]?.type === 'FunctionExpression')
|
||||
) {
|
||||
const parent = path[path.length - 1];
|
||||
let variable_id: Identifier | PrivateIdentifier | undefined;
|
||||
if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
||||
// const something = $derived(...)
|
||||
variable_id = parent.id;
|
||||
} else if (parent?.type === 'PropertyDefinition') {
|
||||
// class X { something = $derived(...) }
|
||||
variable_id =
|
||||
parent.key.type === 'Identifier'
|
||||
? parent.key
|
||||
: parent.key.type === 'PrivateIdentifier'
|
||||
? parent.key
|
||||
: undefined;
|
||||
} else if (parent?.type === 'AssignmentExpression') {
|
||||
// this.something = $derived(...)
|
||||
variable_id =
|
||||
parent.left.type === 'MemberExpression'
|
||||
? parent.left.property.type === 'Identifier'
|
||||
? parent.left.property
|
||||
: undefined
|
||||
: undefined;
|
||||
}
|
||||
|
||||
state.output.suggestions.push(
|
||||
`You are passing a function to $derived ${variable_id ? `when declaring "${variable_id.name}" ` : ''}but $derived expects an expression. You can use $derived.by instead.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { base_runes } from '../../../constants.js';
|
||||
import type { Autofixer } from './index.js';
|
||||
|
||||
const dollarless_runes = base_runes.map((r) => r.replace('$', ''));
|
||||
|
||||
export const imported_runes: Autofixer = {
|
||||
ImportDeclaration(node, { state, next }) {
|
||||
const source = (node.source.value || node.source.raw?.slice(1, -1))?.toString();
|
||||
if (source && source.startsWith('svelte')) {
|
||||
for (const specifier of node.specifiers) {
|
||||
const id =
|
||||
specifier.type === 'ImportDefaultSpecifier'
|
||||
? specifier.local
|
||||
: specifier.type === 'ImportNamespaceSpecifier'
|
||||
? specifier.local
|
||||
: specifier.type === 'ImportSpecifier'
|
||||
? specifier.imported
|
||||
: null;
|
||||
if (id && id.type === 'Identifier' && dollarless_runes.includes(id.name)) {
|
||||
state.output.suggestions.push(
|
||||
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
},
|
||||
};
|
||||
18
packages/mcp-server/src/mcp/autofixers/visitors/index.ts
Normal file
18
packages/mcp-server/src/mcp/autofixers/visitors/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Node } from 'estree';
|
||||
import type { AST } from 'svelte-eslint-parser';
|
||||
import type { Visitors } from 'zimmerframe';
|
||||
import type { ParseResult } from '../../../parse/parse.js';
|
||||
|
||||
export type AutofixerState = {
|
||||
output: { issues: string[]; suggestions: string[] };
|
||||
parsed: ParseResult;
|
||||
desired_svelte_version: number;
|
||||
};
|
||||
|
||||
export type Autofixer = Visitors<Node | AST.SvelteNode, AutofixerState>;
|
||||
|
||||
export * from './assign-in-effect.js';
|
||||
export * from './wrong-property-access-state.js';
|
||||
export * from './imported-runes.js';
|
||||
export * from './derived-with-function.js';
|
||||
export * from './use-runes-instead-of-store.js';
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Autofixer } from './index.js';
|
||||
|
||||
export const use_runes_instead_of_store: Autofixer = {
|
||||
ImportDeclaration(node, { state, next }) {
|
||||
const source = (node.source.value || node.source.raw?.slice(1, -1))?.toString();
|
||||
if (source && source === 'svelte/store') {
|
||||
for (const specifier of node.specifiers) {
|
||||
if (
|
||||
specifier.type === 'ImportSpecifier' &&
|
||||
specifier.imported.type === 'Identifier' &&
|
||||
['derived', 'writable', 'readable'].includes(specifier.imported.name)
|
||||
) {
|
||||
state.output.suggestions.push(
|
||||
`You are importing "${specifier.imported.name}" from "svelte/store". Unless the user specifically asked for stores or it's required because some library/component requires a store as input consider using runes like \`$state\` or \`$derived\` instead, all runes are globally available.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
@@ -2,6 +2,7 @@ import ts_parser from '@typescript-eslint/parser';
|
||||
import type { CallExpression, Identifier } from 'estree';
|
||||
import type { Reference, Variable } from 'eslint-scope';
|
||||
import { parseForESLint as svelte_eslint_parse } from 'svelte-eslint-parser';
|
||||
import { runes } from '../constants.js';
|
||||
|
||||
type Scope = {
|
||||
variables?: Variable[];
|
||||
@@ -18,25 +19,6 @@ function collect_scopes(scope: Scope, acc: Scope[] = []) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const runes = [
|
||||
'$state',
|
||||
'$state.raw',
|
||||
'$state.snapshot',
|
||||
'$effect',
|
||||
'$effect.pre',
|
||||
'$effect.tracking',
|
||||
'$effect.pending',
|
||||
'$effect.root',
|
||||
'$derived',
|
||||
'$derived.by',
|
||||
'$inspect',
|
||||
'$inspect.trace',
|
||||
'$props',
|
||||
'$props.id',
|
||||
'$bindable',
|
||||
'$host',
|
||||
] as const;
|
||||
|
||||
export type ParseResult = ReturnType<typeof parse>;
|
||||
|
||||
export function parse(code: string, file_path: string) {
|
||||
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.1",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/sveltejs/mcp#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/sveltejs/mcp/issues"
|
||||
},
|
||||
"bin": {
|
||||
"svelte-mcp": "./dist/index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sveltejs/mcp.git",
|
||||
"path": "packages/mcp-stdio"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown && 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({});
|
||||
3264
pnpm-lock.yaml
generated
3264
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,47 +0,0 @@
|
||||
import type { Node } from 'estree';
|
||||
import type { AST } from 'svelte-eslint-parser';
|
||||
import type { Visitors } from 'zimmerframe';
|
||||
import type { ParseResult } from '../server/analyze/parse.js';
|
||||
|
||||
export type Autofixer = Visitors<
|
||||
Node | AST.SvelteNode,
|
||||
{
|
||||
output: { issues: string[]; suggestions: string[] };
|
||||
parsed: ParseResult;
|
||||
desired_svelte_version: number;
|
||||
}
|
||||
>;
|
||||
|
||||
export const assign_in_effect: Autofixer = {
|
||||
AssignmentExpression(node, { path, state }) {
|
||||
const in_effect = path.findLast(
|
||||
(node) =>
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === '$effect',
|
||||
);
|
||||
if (
|
||||
in_effect &&
|
||||
in_effect.type === 'CallExpression' &&
|
||||
(in_effect.callee.type === 'Identifier' || in_effect.callee.type === 'MemberExpression')
|
||||
) {
|
||||
if (state.parsed.is_rune(in_effect, ['$effect', '$effect.pre'])) {
|
||||
if (node.left.type === 'Identifier') {
|
||||
const reference = state.parsed.find_reference_by_id(node.left);
|
||||
const definition = reference?.resolved?.defs[0];
|
||||
if (definition && definition.type === 'Variable') {
|
||||
const init = definition.node.init;
|
||||
if (
|
||||
init?.type === 'CallExpression' &&
|
||||
state.parsed.is_rune(init, ['$state', '$state.raw'])
|
||||
) {
|
||||
state.output.suggestions.push(
|
||||
`The stateful variable "${node.left.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
import { walk } from '../index.js';
|
||||
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
|
||||
import { HttpTransport } from '@tmcp/transport-http';
|
||||
import { StdioTransport } from '@tmcp/transport-stdio';
|
||||
import type { Node } from 'estree';
|
||||
import { McpServer } from 'tmcp';
|
||||
import * as v from 'valibot';
|
||||
import { parse } from '../server/analyze/parse.js';
|
||||
import * as autofixers from './autofixers.js';
|
||||
import { get_linter } from './eslint.js';
|
||||
import { compile } from 'svelte/compiler';
|
||||
|
||||
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 {
|
||||
// compile without generating to get warnings and errors
|
||||
|
||||
const compilation_result = compile(code, {
|
||||
filename: filename || 'Component.svelte',
|
||||
generate: false,
|
||||
runes: desired_svelte_version >= 5,
|
||||
});
|
||||
|
||||
for (const warning of compilation_result.warnings) {
|
||||
content.issues.push(
|
||||
`${warning.message} at line ${warning.start?.line}, column ${warning.start?.column}`,
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = parse(code, filename ?? 'Component.svelte');
|
||||
|
||||
// Run each autofixer separately to avoid interrupting logic flow
|
||||
for (const autofixer of Object.values(autofixers)) {
|
||||
walk(
|
||||
parsed.ast as unknown as Node,
|
||||
{ output: content, parsed, desired_svelte_version },
|
||||
autofixer,
|
||||
);
|
||||
}
|
||||
|
||||
const eslint = get_linter(desired_svelte_version);
|
||||
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });
|
||||
|
||||
for (const message of results[0].messages) {
|
||||
if (message.severity === 2) {
|
||||
content.issues.push(
|
||||
`${message.message} at line ${message.line}, column ${message.column}`,
|
||||
);
|
||||
} else if (message.severity === 1) {
|
||||
content.suggestions.push(
|
||||
`${message.message} at line ${message.line}, column ${message.column}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} 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,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const http_transport = new HttpTransport(server, {
|
||||
cors: true,
|
||||
});
|
||||
export const stdio_transport = new StdioTransport(server);
|
||||
@@ -1,10 +0,0 @@
|
||||
import { DATABASE_URL } from '$env/static/private';
|
||||
import { createClient } from '@libsql/client';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
import * as schema from './schema';
|
||||
|
||||
if (!DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
|
||||
const client = createClient({ url: DATABASE_URL });
|
||||
|
||||
export const db = drizzle(client, { schema, logger: true });
|
||||
@@ -1,20 +0,0 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { float_32_array } from './utils';
|
||||
|
||||
/**
|
||||
* NOTE: if you modify a schema adding a vector column you need to manually add this
|
||||
*
|
||||
* CREATE INDEX IF NOT EXISTS name_of_the_index
|
||||
* ON `name_of_the_table` (
|
||||
* libsql_vector_idx(name_of_the_column, 'metric=cosine')
|
||||
* )
|
||||
*
|
||||
* to the generated migration file
|
||||
*/
|
||||
|
||||
// this is just an example of a vector table...we can change this with the docs table later
|
||||
export const vector_table = sqliteTable('vector_table', {
|
||||
id: integer('id').primaryKey(),
|
||||
text: text('text'),
|
||||
vector: float_32_array('vector', { dimensions: 3 }),
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { stdio_transport } from './mcp/index.js';
|
||||
|
||||
stdio_transport.listen();
|
||||
@@ -1,50 +0,0 @@
|
||||
import { VOYAGE_API_KEY } from '$env/static/private';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { vector_table } from '$lib/server/db/schema.js';
|
||||
import { distance, vector } from '$lib/server/db/utils.js';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
async function get_embeddings(text: string) {
|
||||
const result = await fetch('https://api.voyageai.com/v1/embeddings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${VOYAGE_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input: [text],
|
||||
model: 'voyage-3.5',
|
||||
}),
|
||||
}).then((res) => res.json());
|
||||
|
||||
return result.data[0].embedding as number[];
|
||||
}
|
||||
|
||||
export async function load({ url: { searchParams } }) {
|
||||
const sentence = searchParams.get('sentence');
|
||||
if (!sentence) return { top: [], sentence: '' };
|
||||
const top = await db
|
||||
.select({
|
||||
id: vector_table.id,
|
||||
text: vector_table.text,
|
||||
distance: distance(vector_table.vector, await get_embeddings(sentence)),
|
||||
})
|
||||
.from(vector_table)
|
||||
.orderBy(sql`distance`)
|
||||
.execute();
|
||||
return { top, sentence };
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async default({ request }) {
|
||||
const data = await request.formData();
|
||||
const text = data.get('text')?.toString();
|
||||
const embeddings = await get_embeddings(text ?? '');
|
||||
if (text && embeddings) {
|
||||
await db
|
||||
.insert(vector_table)
|
||||
.values({ text, vector: vector(embeddings) })
|
||||
.execute();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<h1>Official Svelte MCP</h1>
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<textarea name="text" rows="4" cols="50" placeholder="Enter text to store in the vector database"
|
||||
></textarea>
|
||||
<br />
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
|
||||
Comparing with
|
||||
<pre>{data.sentence}</pre>
|
||||
|
||||
{#each data.top as item (item.id)}
|
||||
<p>{item.text} - {item.distance}</p>
|
||||
{/each}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/lib/stdio.ts", "src/lib/mcp/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/routes/**/*", "src/app.html", "src/hooks.server.ts"]
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import devtoolsJson from 'vite-plugin-devtools-json';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit(), devtoolsJson()],
|
||||
test: {
|
||||
expect: { requireAssertions: true },
|
||||
projects: [
|
||||
{
|
||||
extends: './vite.config.ts',
|
||||
test: {
|
||||
name: 'server',
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user