Compare commits

...

36 Commits

Author SHA1 Message Date
github-actions[bot]
cdfbb907b6 Version Packages (#147)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-30 16:03:12 +01:00
Paolo Ricciuti
2eb2b18008 feat: distribute skills through opencode plugin (#151) 2026-01-30 15:52:35 +01:00
renovate[bot]
e8554f3d8f chore(deps): update all non-major dependencies (#137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
2026-01-27 22:19:34 +01:00
Christopher Harrison
0213bd951f Add Copilot CLI setup instructions (#148) 2026-01-27 22:05:30 +01:00
paoloricciuti
2245cb2dc9 fix: turn off no-inspect in eslint for mcp 2026-01-27 16:25:30 +01:00
renovate[bot]
2a5f7d6314 chore(deps): update dependency @mcp-ui/server to v6 (#146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
2026-01-27 16:11:09 +01:00
paoloricciuti
8518e627d3 chore: remove logs 2026-01-23 18:41:14 +01:00
paoloricciuti
9e824da9e2 chore: better debug logs 2026-01-23 18:32:54 +01:00
paoloricciuti
b38f2c11da fix: debug log 2026-01-23 18:28:37 +01:00
paoloricciuti
ea37c7120c chore: use new disableSee option in tmcp 2026-01-23 07:58:36 +01:00
paoloricciuti
099e939f79 fix: remove log duh 2026-01-22 23:50:37 +01:00
paoloricciuti
69b36eefdc fix: return 405 for SSE stream instead of closing immediately 2026-01-22 23:47:59 +01:00
github-actions[bot]
15ad554f53 Version Packages (#145)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-19 15:56:05 +01:00
paoloricciuti
74477448ce chore: update svelte 2026-01-19 15:52:46 +01:00
github-actions[bot]
6f538265d1 Version Packages (#144)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-17 15:21:24 +01:00
paoloricciuti
71295bc11f chore: add README to opencode plugin 2026-01-17 15:20:11 +01:00
Rich Harris
b5040ff5cf docs: tweak Claude Code documentation (#143)
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-01-17 01:04:32 +01:00
github-actions[bot]
45c961417f Version Packages (#141)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-16 19:26:42 +01:00
paoloricciuti
57e2d1def1 fix: show toast after a few seconds to avoid race condition 2026-01-16 19:21:42 +01:00
Paolo Ricciuti
398a703580 feat: opencode plugin (#142)
Co-authored-by: Rich Harris <hello@rich-harris.dev>
2026-01-16 12:41:02 -05:00
Paolo Ricciuti
75c802a115 feat: add lsp config for claude plugin (#140) 2026-01-15 15:00:38 +01:00
Paolo Ricciuti
53a634cdb0 feat: expose playground link as MCP App (#138) 2026-01-15 14:59:46 +01:00
paoloricciuti
bcdc33e7a5 fix: automatically accept edits in the svelte file editor agent 2026-01-05 22:18:46 +01:00
github-actions[bot]
825ae33427 Version Packages (#136)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-05 09:43:10 +01:00
Paolo Ricciuti
ccf940cc45 feat: claude skill + marketplace (#110)
Co-authored-by: Stanislav Khromov <stanislav.khromov+github@gmail.com>
2026-01-04 23:00:03 +01:00
Paolo Ricciuti
b2f195fb7b fix: add suggestion for snippets declared in script tag (#132) 2026-01-04 12:00:33 +01:00
renovate[bot]
d8e4b18bff chore(deps): update pnpm to v10.27.0 (#133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 15:40:19 +01:00
renovate[bot]
6a2198b433 chore(deps): update dependency globals to v17 (#134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 15:40:10 +01:00
renovate[bot]
2ce60c6110 chore(deps): update pnpm to v10.26.2 (#131)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 14:26:52 +01:00
github-actions[bot]
0f8987fdcf Version Packages (#130)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-22 00:18:38 +01:00
Paolo Ricciuti
42911e2631 feat: expose tools as JS api + cli (#128) 2025-12-22 00:16:36 +01:00
paoloricciuti
655eb85eba fix: use pnpm pack for pkg.pr.new publish 2025-12-19 18:26:45 +01:00
renovate[bot]
89403a7d0c chore(deps): update pnpm to v10.26.1 (#129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 17:23:59 +01:00
paoloricciuti
3747623d55 chore: setup pkg.pr.new 2025-12-18 20:00:30 +01:00
github-actions[bot]
c1f230455f Version Packages (#127)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-17 19:48:37 +01:00
paoloricciuti
9dfb4dedb4 fix: server.json version + update publisher 2025-12-17 19:46:59 +01:00
61 changed files with 3740 additions and 1760 deletions

View File

@@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["!@sveltejs/mcp"]
"ignore": ["!@sveltejs/mcp", "!@sveltejs/opencode"]
}

View File

@@ -0,0 +1,22 @@
{
"name": "svelte",
"owner": {
"name": "Svelte"
},
"plugins": [
{
"name": "svelte",
"source": "./plugins/svelte",
"description": "A plugin for all things Svelte development, MCP, skills, and more.",
"lspServers": {
"svelte": {
"command": "svelte-language-server",
"args": ["--stdio"],
"extensionToLanguage": {
".svelte": "svelte"
}
}
}
}
]
}

View File

@@ -18,10 +18,10 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.26.0
version: 10.28.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '24'
cache: 'pnpm'

View File

@@ -18,10 +18,10 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.26.0
version: 10.28.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '24'
cache: 'pnpm'

View File

@@ -0,0 +1,23 @@
name: Publish Any Commit
on: [push, pull_request]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- run: corepack enable
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- run: pnpm dlx pkg-pr-new publish --compact './packages/mcp-stdio' './packages/opencode' --pnpm

View File

@@ -21,11 +21,11 @@ jobs:
MCP_KEY: ${{ secrets.MCP_KEY }}
run: |
NAME=mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz
# Download MCP Publisher pinned to v1.3.10 using latest https for security and save it to a file named mcp-publisher.tar.gz
curl --proto '=https' --proto-redir '=https' --tlsv1.2 -fL "https://github.com/modelcontextprotocol/registry/releases/download/v1.3.10/$NAME" -O
# Download MCP Publisher pinned to v1.4.0 using latest https for security and save it to a file named mcp-publisher.tar.gz
curl --proto '=https' --proto-redir '=https' --tlsv1.2 -fL "https://github.com/modelcontextprotocol/registry/releases/download/v1.4.0/$NAME" -O
# Verify the SHA256 checksum of the downloaded file
sha256sum --ignore-missing -c ./checksums/registry_1.3.10_checksums.txt
sha256sum --ignore-missing -c ./checksums/registry_1.4.0_checksums.txt
# Extract the tarball
mkdir tmp

View File

@@ -0,0 +1,45 @@
name: Release Svelte Code Writer Skill
on:
push:
branches:
- main
paths:
- 'plugins/svelte/skills/svelte-code-writer/**'
workflow_dispatch:
permissions: {}
jobs:
release:
permissions:
contents: write
# prevents this action from running on forks
if: github.repository == 'sveltejs/mcp'
name: Release Svelte Code Writer Skill
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v6
- name: Get version from date
id: version
run: echo "version=$(date +'%Y.%m.%d-%H%M%S')" >> $GITHUB_OUTPUT
- name: Create zip
run: |
cd plugins/svelte/skills
zip -r svelte-code-writer.zip svelte-code-writer/
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: svelte-code-writer-v${{ steps.version.outputs.version }}
name: Svelte Code Writer Skill v${{ steps.version.outputs.version }}
body: |
Automated release of the Svelte Code Writer skill.
This release was triggered by changes to the `plugins/svelte/skills/svelte-code-writer/` directory.
files: plugins/svelte/skills/svelte-code-writer.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -29,7 +29,7 @@ jobs:
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@v6
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node }}
package-manager-cache: false # pnpm is not installed yet
@@ -39,10 +39,11 @@ jobs:
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@v6
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node }}
package-manager-cache: true # caches pnpm via packageManager field in package.json
cache: 'pnpm'
- name: install
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: build
@@ -51,7 +52,7 @@ jobs:
- 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
uses: changesets/action@c48e67d110a68bc90ccf1098e9646092baacaa87 # v1.6.0
with:
# This expects you to have a script called changeset:version version that calls changeset version and updated what it needs to be updated
version: pnpm changeset:version

View File

@@ -0,0 +1,84 @@
name: Sync OpenCode Skills
on:
push:
branches:
- main
paths:
- 'plugins/svelte/skills/**'
permissions:
contents: write
pull-requests: write
jobs:
sync-skills:
# prevents this action from running on forks
if: github.repository == 'sveltejs/mcp'
name: Sync Skills to OpenCode Package and Update Docs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
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
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Sync skills folder
run: pnpm sync-opencode-skills
- name: Generate skills documentation
run: pnpm generate-skill-docs
- name: Check for changes
id: git-check
run: |
git diff --exit-code packages/opencode/skills documentation/docs/60-skills/10-skills.md || echo "changed=true" >> $GITHUB_OUTPUT
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync skills and update documentation'
branch: chore/sync-opencode-skills
delete-branch: true
title: 'chore: sync skills and update documentation'
body: |
## Summary
Automatically synced skills and updated documentation.
This PR was triggered by changes to the skills folder in `plugins/svelte/skills/`.
## Changes
- Synced `packages/opencode/skills/` with latest skill definitions
- Updated `documentation/docs/60-skills/10-skills.md` with latest skill documentation
## Generated by
GitHub Action: Sync OpenCode Skills
labels: |
chore
documentation
automated

View File

@@ -18,10 +18,10 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.26.0
version: 10.28.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '24'
cache: 'pnpm'

View File

@@ -0,0 +1,78 @@
name: Update OpenCode JSON Schema
on:
push:
branches:
- main
paths:
- 'packages/opencode/config.ts'
permissions:
contents: write
pull-requests: write
jobs:
update-docs:
# prevents this action from running on forks
if: github.repository == 'sveltejs/mcp'
name: Update OpenCode JSON Schema
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
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
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Generate opencode JSON schema
run: pnpm generate-opencode-jsonschema
- name: Check for changes
id: git-check
run: |
git diff --exit-code packages/opencode/schema.json || echo "changed=true" >> $GITHUB_OUTPUT
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'docs: update opencode JSON schema'
branch: docs/update-opencode-jsonschema
delete-branch: true
title: 'docs: update opencode JSON schema'
body: |
## Summary
Automatically generated update for OpenCode JSON schema.
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.ts`.
## Changes
- Updated `packages/opencode/schema.json` with latest JSON schema
## Generated by
GitHub Action: Update OpenCode JSON Schema
labels: |
automated

View File

@@ -24,7 +24,7 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: false # pnpm is not installed yet
@@ -37,10 +37,11 @@ jobs:
npm i -g pnpm@$PNPM_VER
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@v6
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
@@ -55,7 +56,7 @@ jobs:
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'docs: update prompts documentation'

View File

@@ -12,4 +12,6 @@ bun.lockb
# Claude Code
.claude/
.changeset/
.changeset/
/packages/opencode/schema.json

View File

@@ -25,22 +25,5 @@ export async function handle({ event, resolve }) {
await track(event, { session_id, ...(extra ? { extra } : {}) });
},
});
// 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 {
return mcp_response;
} finally {
try {
await mcp_response.body?.cancel();
} catch {
// ignore
}
}
}
return mcp_response ?? resolve(event);
}

View File

@@ -4,4 +4,9 @@ import { HttpTransport } from '@tmcp/transport-http';
export const http_transport = new HttpTransport(server, {
cors: true,
path: '/mcp',
// 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.
disableSse: true,
});

View File

@@ -45,6 +45,27 @@ command = "npx"
args = ["-y", "@sveltejs/mcp"]
```
## Copilot CLI
Use the Copilot CLI to interactively add the MCP server:
```bash
/mcp add
```
Alternatively, create or edit `~/.copilot/mcp-config.json` and add the following configuration:
```json
{
"mcpServers": {
"svelte": {
"command": "npx",
"args": ["-y", "@sveltejs/mcp"]
}
}
}
```
## Gemini CLI
To include the local MCP version in Gemini CLI, simply run the following command:
@@ -57,7 +78,7 @@ The `[scope]` must be `user`, `project` or `local`.
## OpenCode
Run the command:
You can automatically configure the MCP server using the [OpenCode plugin](opencode-plugin) (recommended). If you prefer to configure the MCP server manually, run:
```bash
opencode mcp add

View File

@@ -16,6 +16,8 @@ claude mcp add -t http -s [scope] svelte https://mcp.svelte.dev/mcp
You can choose your preferred `scope` (it must be `user`, `project` or `local`) and `name`.
If you prefer you can also install the `svelte` plugin in [the Svelte Claude Code Marketplace](plugin) that will give you both the remote server and useful [skills](skills).
## Claude Desktop
- Open Settings > Connectors
@@ -34,6 +36,26 @@ experimental_use_rmcp_client = true
url = "https://mcp.svelte.dev/mcp"
```
## Copilot CLI
Use the Copilot CLI to interactively add the MCP server:
```bash
/mcp add
```
Alternatively, create or edit `~/.copilot/mcp-config.json` and add the following configuration:
```json
{
"mcpServers": {
"svelte": {
"url": "https://mcp.svelte.dev/mcp"
}
}
}
```
## Gemini CLI
To use the remote MCP server with Gemini CLI, simply run the following command:
@@ -46,7 +68,7 @@ The `[scope]` must be `user` or `project`.
## OpenCode
Run the command:
You can automatically configure the MCP server using the [OpenCode plugin](opencode-plugin) (recommended). If you prefer to configure the MCP server manually, run:
```bash
opencode mcp add

View File

@@ -0,0 +1,3 @@
---
title: Claude Code Plugin
---

View File

@@ -0,0 +1,23 @@
---
title: Overview
---
The open source [repository](https://github.com/sveltejs/mcp) containing the code for the MCP server is also a Claude Code Marketplace plugin.
The marketplace allows you to install the `svelte` plugin which will give you the remote MCP server, [skills](skills) to instruct the LLM on how to properly write Svelte 5 code, and a specialized agent for editing Svelte files.
If possible, we recommend that you instruct the LLM to execute MCP calls with the agent (you can explicitly mention an agent in your message to delegate work to it) when creating or editing `.svelte` files or `.svelte.ts`/`.svelte.js` modules as it helps save context by handling Svelte-specific tasks more efficiently.
## Installation
To add the repository as a marketplace, launch Claude Code and type the following:
```bash
/plugin marketplace add sveltejs/mcp
```
Then, install the Svelte plugin:
```bash
/plugin install svelte
```

View File

@@ -0,0 +1,11 @@
---
title: Subagent
---
The Svelte plugin includes a specialized subagent called `svelte-file-editor` designed for creating, editing, and reviewing Svelte files.
## Benefits
The subagent has access to its own context window, allowing it to fetch the documentation, iterate with the `svelte-autofixer` tool and write to the file system without wasting context in the main agent.
The delegation should happen automatically when appropriate, but you can also explicitly request the subagent be used for Svelte-related tasks.

View File

@@ -0,0 +1,3 @@
---
title: OpenCode Plugin
---

View File

@@ -0,0 +1,42 @@
---
title: Overview
---
OpenCode has a [plugin system](https://opencode.ai/docs/plugins/) that allows developers to add MCP servers, agents and commands programmatically. Svelte has an OpenCode plugin published under `@sveltejs/opencode`.
## Installation
To install the plugin in OpenCode you can edit your [OpenCode config]() (either the global or the local one), adding `@sveltejs/opencode` to the list of plugins.
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@sveltejs/opencode"]
}
```
That's it! You now have the Svelte MCP server, [skills](skills), and the [file editor subagent](opencode-subagent) configured for you.
## Configuration
The default configuration for the Svelte OpenCode plugin looks like this...
```json
{
"$schema": "https://raw.githubusercontent.com/sveltejs/mcp/refs/heads/main/packages/opencode/schema.json",
"mcp": {
"type": "remote",
"enabled": true
},
"subagent": {
"enabled": true
},
"skills": {
"enabled": true
}
}
```
...but if you prefer, you can enable only the subagent, only the MCP, only the skills, or configure the kind of MCP server you want to use (`local` or `remote`).
You can place this file in `~/.config/opencode/svelte.json` or, if you have an `OPENCODE_CONFIG_DIR` environment variable specified, at `$OPENCODE_CONFIG_DIR/svelte.json`.

View File

@@ -0,0 +1,11 @@
---
title: Subagent
---
The Svelte plugin includes a specialized subagent called `svelte-file-editor` designed for creating, editing, and reviewing Svelte files.
## Benefits
The subagent has access to its own context window, allowing it to fetch the documentation, iterate with the `svelte-autofixer` tool and write to the file system without wasting context in the main agent.
The delegation should happen automatically when appropriate, but you can also explicitly request the subagent be used for Svelte-related tasks.

View File

@@ -0,0 +1,86 @@
---
title: Overview
---
This is the list of available skills provided by the Svelte MCP package. Skills are sets of instructions that AI agents can load on-demand to help with specific tasks.
Skills are available in both the Claude Code plugin (installed via the marketplace) and the OpenCode plugin (`@sveltejs/opencode`). They can also be manually installed in your `.claude/skills/` or `.opencode/skills/` folder.
You can download the latest skills from the [releases page](https://github.com/sveltejs/mcp/releases) or find them in the [`plugins/svelte/skills`](https://github.com/sveltejs/mcp/tree/main/plugins/svelte/skills) folder.
## `svelte-code-writer`
CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating or editing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
<a href="https://github.com/sveltejs/mcp/releases?q=svelte-code-writer" target="_blank" rel="noopener noreferrer">Open Releases page</a>
<details>
<summary>View skill content</summary>
<!-- prettier-ignore-start -->
````markdown
# Svelte 5 Code Writer
## CLI Tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
```bash
npx @sveltejs/mcp list-sections
```
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
```
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
**Example:**
```bash
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
```
Analyzes Svelte code and suggests fixes for common issues.
**Options:**
- `--async` - Enable async Svelte mode (default: false)
- `--svelte-version` - Target version: 4 or 5 (default: 5)
**Examples:**
```bash
# Analyze inline code (escape $ as \$)
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
# Analyze a file
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
# Target Svelte 4
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
```
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
## Workflow
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component
````
<!-- prettier-ignore-end -->
</details>

View File

@@ -0,0 +1,3 @@
---
title: Skills
---

View File

@@ -3,7 +3,7 @@
"version": "0.0.1",
"description": "The official Svelte MCP server implementation",
"type": "module",
"packageManager": "pnpm@10.26.0",
"packageManager": "pnpm@10.28.2",
"scripts": {
"build": "pnpm -r run build",
"dev": "pnpm --filter @sveltejs/mcp-remote run dev",
@@ -18,11 +18,14 @@
"test": "npm run test:unit -- --run",
"test:watch": "npm run test:unit -- --watch",
"inspect": "pnpm mcp-inspector",
"generate-opencode-jsonschema": "pnpm --filter @sveltejs/opencode run generate-schema",
"generate-summaries": "pnpm --filter @sveltejs/mcp-server run generate-summaries",
"generate-prompt-docs": "node --import node-resolve-ts/register scripts/update-docs-prompts.ts",
"generate-skill-docs": "node --import node-resolve-ts/register scripts/update-docs-skills.ts",
"debug:generate-summaries": "pnpm --filter @sveltejs/mcp-server run debug:generate-summaries",
"release": "pnpm --filter @sveltejs/mcp run build && changeset publish",
"changeset:version": "changeset version && pnpm --filter @sveltejs/mcp run update:version && git add --all"
"changeset:version": "changeset version && pnpm --filter @sveltejs/mcp run update:version && git add --all",
"sync-opencode-skills": "rm -rf packages/opencode/skills && cp -r plugins/svelte/skills packages/opencode/skills"
},
"keywords": [
"svelte",

View File

@@ -14,7 +14,8 @@
"debug:generate-summaries": "DEBUG_MODE=1 node scripts/generate-summaries.ts --experimental-strip-types"
},
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./handlers": "./src/mcp/handlers/tools/handlers.ts"
},
"peerDependencies": {
"drizzle-orm": "^0.45.0"

View File

@@ -35,6 +35,7 @@ function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
'svelte/prefer-writable-derived': 'warn',
'svelte/require-event-dispatcher-types': 'warn',
'svelte/require-store-reactive-access': 'warn',
'svelte/no-inspect': 'off',
},
languageOptions: {

View File

@@ -5,110 +5,137 @@ import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js';
import { icons } from '../../icons/index.js';
import { tool } from 'tmcp/utils';
const get_documentation_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., "cli/overview"). Supports single string and array of strings',
),
),
});
export async function get_documentation_handler({
section,
}: v.InferInput<typeof get_documentation_schema>) {
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.slug === requested_section ||
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 = await format_sections_list();
final_text += `\n\n---\n\n${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
}
return final_text;
}
export function get_documentation(server: SvelteMcp) {
server.tool(
{
name: 'get-documentation',
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., "cli/overview"). 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. Before calling this tool, try to implement Svelte components using your own knowledge and the `svelte-autofixer` tool, since calling this tool is token intensive.',
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., "cli/overview"). Supports single string and array of strings',
),
),
}),
schema: get_documentation_schema,
annotations: {
title: 'Get Documentation',
destructiveHint: false,
readOnlyHint: true,
openWorldHint: false,
},
icons,
},
async ({ section }) => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'get-documentation');
}
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.slug === requested_section ||
s.url === requested_section,
try {
const content = await get_documentation_handler({ section });
return tool.text(content);
} catch (e) {
const error = e as Error;
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(
server.ctx.sessionId,
'get-documentation-error',
error.message,
);
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 = await format_sections_list();
final_text += `\n\n---\n\n${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
return tool.error(error.message);
}
return tool.text(final_text);
},
);
}

View File

@@ -0,0 +1,4 @@
export { get_documentation_handler } from './get-documentation.js';
export { list_sections_handler } from './list-sections.js';
export { svelte_autofixer_handler } from './svelte-autofixer.js';
export { playground_link_handler } from './playground-link.js';

View File

@@ -1,4 +1,4 @@
export * from './get-documentation.js';
export * from './list-sections.js';
export * from './svelte-autofixer.js';
export * from './playground-link.js';
export { get_documentation } from './get-documentation.js';
export { list_sections } from './list-sections.js';
export { svelte_autofixer } from './svelte-autofixer.js';
export { playground_link } from './playground-link.js';

View File

@@ -4,21 +4,44 @@ import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js';
import { icons } from '../../icons/index.js';
import { tool } from 'tmcp/utils';
export async function list_sections_handler() {
const formatted_sections = await format_sections_list();
return `${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
}
export function list_sections(server: SvelteMcp) {
server.tool(
{
name: 'list-sections',
description:
'Lists all available Svelte 5 and SvelteKit documentation sections in a structured format. Each section includes a "use_cases" field that describes WHEN this documentation would be useful. You should carefully analyze the use_cases field to determine which sections are relevant for the user\'s query. The use_cases contain comma-separated keywords describing project types (e.g., "e-commerce", "blog"), features (e.g., "authentication", "forms"), components (e.g., "slider", "modal"), development stages (e.g., "deployment", "testing"), or "always" for fundamental concepts. Match these use_cases against the user\'s intent - for example, if building an e-commerce site, fetch sections with use_cases containing "e-commerce", "product listings", "shopping cart", etc. If building a slider, look for "slider", "carousel", "animation", etc. Returns sections as "* title: [section_title], use_cases: [use_cases], path: [file_path]". Always run list-sections FIRST for any Svelte query, then analyze ALL use_cases to identify relevant sections, and finally use get_documentation to fetch ALL relevant sections at once.',
annotations: {
title: 'List Sections',
destructiveHint: false,
readOnlyHint: true,
openWorldHint: false,
},
icons,
},
async () => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'list-sections');
}
const formatted_sections = await format_sections_list();
return tool.text(`${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`);
try {
const content = await list_sections_handler();
return tool.text(content);
} catch (e) {
const error = e as Error;
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(
server.ctx.sessionId,
'list-sections-error',
error.message,
);
}
return tool.error(error.message);
}
},
);
}

View File

@@ -58,7 +58,7 @@ describe('playground-link tool', () => {
type: 'resource',
resource: expect.objectContaining({
uri: 'ui://svelte/playground-link',
mimeType: 'text/uri-list',
mimeType: 'text/html;profile=mcp-app',
_meta: { 'mcpui.dev/ui-preferred-frame-size': ['100%', '1200px'] },
text: expect.stringMatching(/^https:\/\/svelte\.dev\/playground\/embed#H4sIA/),
}),
@@ -67,6 +67,24 @@ describe('playground-link tool', () => {
);
});
it('should have tool _meta with resource URI for MCP Apps hosts', async () => {
const tools = await session.listTools();
const playground_tool = tools.tools.find((t) => t.name === 'playground-link');
expect(playground_tool).toBeDefined();
expect(playground_tool?._meta).toStrictEqual({
ui: { resourceUri: 'ui://svelte/playground-link' },
});
});
it('should expose a resource for MCP Apps hosts', async () => {
const resources = await session.listResources();
const playground_resource = resources.resources.find(
(r) => r.uri === 'ui://svelte/playground-link',
);
expect(playground_resource).toBeDefined();
expect(playground_resource?.name).toBe('playground-link-ui');
});
it('should not create a playground link if App.svelte is missing', async () => {
const result = await session.callTool<{ url: string }>('playground-link', {
name: 'My Playground',

View File

@@ -1,8 +1,8 @@
import type { SvelteMcp } from '../../index.js';
import * as v from 'valibot';
import { icons } from '../../icons/index.js';
import { createUIResource } from '@mcp-ui/server';
import { tool } from 'tmcp/utils';
import * as v from 'valibot';
import { icons } from '../../icons/index.js';
import type { SvelteMcp } from '../../index.js';
async function compress_and_encode_text(input: string) {
const reader = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip')).getReader();
@@ -30,98 +30,232 @@ type File = {
text: boolean;
};
const playground_link_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.",
),
),
});
const playground_link_output_schema = v.object({
url: v.string(),
});
export async function playground_link_handler({
files,
name,
tailwind,
}: v.InferInput<typeof playground_link_schema>) {
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) {
throw new 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 url = playground_base.toString();
// use the embed path to have a cleaner UI for mcp-ui
playground_base.pathname = '/playground/embed';
return {
url,
iframe_url: playground_base.toString(),
};
}
// Create the UI resource for MCP Apps hosts (with adapter)
// This will be registered as a resource that MCP Apps hosts can fetch
const playground_ui_resource = createUIResource({
uri: 'ui://svelte/playground-link',
encoding: 'text',
resourceProps: {
_meta: {
ui: {
csp: {
connectDomains: ['https://svelte.dev'],
resourceDomains: ['https://svelte.dev'],
frameDomains: ['https://svelte.dev'],
baseUriDomains: ['https://svelte.dev'],
},
},
},
},
content: {
type: 'rawHtml',
// This is a placeholder HTML - the actual iframe URL will be set per-request
// MCP Apps hosts receive the tool input/output via postMessage
htmlString: `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; }
iframe { width: 100%; height: 100%; border: none; display: none; }
.loading { display: flex; align-items: center; justify-content: center; height: 100%; font-family: system-ui, sans-serif; color: #666; }
</style>
</head>
<body>
<div class="loading" id="loading">Loading playground...</div>
<iframe id="playground" allow="clipboard-write"></iframe>
<script>
function size_changed() {
const width = document.body.scrollWidth;
window.parent.postMessage({
jsonrpc: '2.0',
method: 'ui/notifications/size-changed',
params: {
width,
height: 800
}
}, '*');
}
// Signal that the widget is ready
window.parent.postMessage({ type: 'ui-lifecycle-iframe-ready' }, '*');
// Listen for render data from the adapter (for MCP Apps hosts)
window.addEventListener('message', (event) => {
if (event.data.type === 'ui-lifecycle-iframe-render-data') {
const renderData = event.data.payload.renderData || {};
const toolOutput = renderData.toolOutput;
// The tool output contains the iframe URL
if (toolOutput && toolOutput.structuredContent && toolOutput.structuredContent.url) {
const iframe = document.getElementById('playground');
const loading = document.getElementById('loading');
// Convert the URL to embed URL
const embedUrl = toolOutput.structuredContent.url.replace('/playground#', '/playground/embed#');
iframe.src = embedUrl;
iframe.style.display = 'block';
iframe.addEventListener("load", () => {
size_changed();
});
loading.style.display = 'none';
}
}
});
</script>
</body>
</html>`,
},
uiMetadata: {
'preferred-frame-size': ['100%', '1200px'],
},
adapters: {
mcpApps: { enabled: true },
},
});
export function playground_link(server: SvelteMcp) {
// Register the UI resource so MCP Apps hosts can fetch it
server.resource(
{
name: 'playground-link-ui',
description: 'UI resource for the Svelte Playground widget',
uri: playground_ui_resource.resource.uri,
icons,
},
() => {
return {
contents: [playground_ui_resource.resource],
};
},
);
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(),
}),
schema: playground_link_schema,
outputSchema: playground_link_output_schema,
annotations: {
title: 'Playground Link',
destructiveHint: false,
readOnlyHint: true,
openWorldHint: false,
},
icons,
// For MCP Apps hosts - points to the registered resource
_meta: {
ui: {
resourceUri: playground_ui_resource.resource.uri,
},
},
},
async ({ files, name, tailwind }) => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link');
}
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) {
try {
const result = await playground_link_handler({ files, name, tailwind });
return {
content: [
{
type: 'text',
text: JSON.stringify({ url: result.url }),
},
// Embedded resource for MCP-UI hosts (no adapter, uses externalUrl)
createUIResource({
uri: 'ui://svelte/playground-link',
content: {
type: 'externalUrl',
iframeUrl: result.iframe_url,
},
uiMetadata: {
'preferred-frame-size': ['100%', '1200px'],
},
encoding: 'text',
}),
],
structuredContent: { url: result.url },
};
} catch (e) {
const error = e as Error;
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link-no-app-svelte');
await server.ctx.custom?.track?.(
server.ctx.sessionId,
'playground-link-error',
error.message,
);
}
return tool.error('The files must contain an App.svelte file as the entry point');
return tool.error(error.message);
}
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(),
};
// use the embed path to have a cleaner UI for mcp-ui
playground_base.pathname = '/playground/embed';
return {
content: [
{
type: 'text',
text: JSON.stringify(content),
},
createUIResource({
uri: 'ui://svelte/playground-link',
content: {
type: 'externalUrl',
iframeUrl: playground_base.toString(),
},
uiMetadata: {
'preferred-frame-size': ['100%', '1200px'],
},
encoding: 'text',
}),
],
structuredContent: content,
};
},
);
}

View File

@@ -53,6 +53,18 @@ describe('svelte-autofixer tool', () => {
);
});
it('should add suggestions for snippets declared in script tag', async () => {
const content = await autofixer_tool_call(`<script>
{#snippet my_snippet()}
some content
{/snippet}
</script>`);
expect(content.issues.length).toBeGreaterThan(0);
expect(content.suggestions).toContain(
"The code can't be compiled because a Javascript parse error. The error suggests you have a `{#snippet ...}` block inside the `<script>` tag. Snippets are template syntax and should be declared in the markup section of the component, not in the script. Move the snippet outside of the `<script>` tag. Snippets declared in the markup can also be accessed in the script tag in case you need them.",
);
});
it('should error out if async is true with a version less than 5', async () => {
const content = await autofixer_tool_call(
`<script>

View File

@@ -7,6 +7,102 @@ import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js
import { icons } from '../../icons/index.js';
import { tool } from 'tmcp/utils';
const autofixer_schema = v.object({
code: v.string(),
desired_svelte_version: v.pipe(
v.union([v.string(), v.number()]),
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.',
),
),
async: v.pipe(
v.optional(v.boolean()),
v.description(
'If true the code is an async component/module and might use await in the markup or top-level awaits in the script tag. If possible check the svelte.config.js/svelte.config.ts to check if the option is enabled otherwise asks the user if they prefer using it or not. You can only use this option if the version is 5.',
),
),
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.',
),
),
});
const autofixer_output_schema = v.object({
issues: v.array(v.string()),
suggestions: v.array(v.string()),
require_another_tool_call_after_fixing: v.boolean(),
});
export async function svelte_autofixer_handler({
code,
desired_svelte_version: desired_svelte_version_unchecked,
async,
filename: filename_or_path,
}: v.InferInput<typeof autofixer_schema>) {
// we validate manually because some clients don't support union in the input schema (looking at you cursor)
const parsed_version = v.safeParse(
v.union([v.literal(4), v.literal(5), v.literal('4'), v.literal('5')]),
desired_svelte_version_unchecked,
);
if (parsed_version.success === false) {
throw new Error(
`The desired_svelte_version MUST be either 4 or 5 but received "${desired_svelte_version_unchecked}"`,
);
}
const desired_svelte_version = parsed_version.output;
if (async && +desired_svelte_version < 5) {
throw new Error('The async option can only be used with Svelte version 5 or higher.');
}
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, async);
add_autofixers_issues(content, code, +desired_svelte_version, filename, async);
await add_eslint_issues(content, code, +desired_svelte_version, filename, async);
} catch (e: unknown) {
const error = e as Error & { start?: { line: number; column: number }; frame?: string };
content.issues.push(
`${error.message} at line ${error.start?.line}, column ${error.start?.column}`,
);
if (error.message.includes('js_parse_error')) {
// Check if the error frame contains template syntax that was incorrectly placed in the script tag
if (error.frame?.includes('{#snippet')) {
content.suggestions.push(
"The code can't be compiled because a Javascript parse error. The error suggests you have a `{#snippet ...}` block inside the `<script>` tag. Snippets are template syntax and should be declared in the markup section of the component, not in the script. Move the snippet outside of the `<script>` tag. Snippets declared in the markup can also be accessed in the script tag in case you need them.",
);
} else {
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)`.",
);
}
} else if (error.message.includes('css_expected_identifier')) {
content.suggestions.push(
"The code can't be compiled because a valid CSS identifier is expected. This sometimes means you are trying to use a variable in CSS like this: `color: {my_color}` but Svelte doesn't support that. You can use inline CSS variables for that `<div style:--color={my_color}></div>` and then use the variable as usual in CSS with `color: var(--color)`.",
);
}
}
if (content.issues.length > 0 || content.suggestions.length > 0) {
content.require_another_tool_call_after_fixing = true;
}
return content;
}
export function svelte_autofixer(server: SvelteMcp) {
server.tool(
{
@@ -14,32 +110,8 @@ export function svelte_autofixer(server: SvelteMcp) {
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.string(), v.number()]),
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.',
),
),
async: v.pipe(
v.optional(v.boolean()),
v.description(
'If true the code is an async component/module and might use await in the markup or top-level awaits in the script tag. If possible check the svelte.config.js/svelte.config.ts to check if the option is enabled otherwise asks the user if they prefer using it or not. You can only use this option if the version is 5.',
),
),
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(),
}),
schema: autofixer_schema,
outputSchema: autofixer_output_schema,
annotations: {
title: 'Svelte Autofixer',
destructiveHint: false,
@@ -57,63 +129,25 @@ export function svelte_autofixer(server: SvelteMcp) {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer');
}
// we validate manually because some clients don't support union in the input schema (looking at you cursor)
const parsed_version = v.safeParse(
v.union([v.literal(4), v.literal(5), v.literal('4'), v.literal('5')]),
desired_svelte_version_unchecked,
);
if (parsed_version.success === false) {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer-wrong-version');
}
return tool.error(
`The desired_svelte_version MUST be either 4 or 5 but received "${desired_svelte_version_unchecked}"`,
);
}
const desired_svelte_version = parsed_version.output;
if (async && +desired_svelte_version < 5) {
return tool.error('The async option can only be used with Svelte version 5 or higher.');
}
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, async);
add_autofixers_issues(content, code, +desired_svelte_version, filename, async);
await add_eslint_issues(content, code, +desired_svelte_version, filename, async);
} 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)`.",
);
} else if (error.message.includes('css_expected_identifier')) {
content.suggestions.push(
"The code can't be compiled because a valid CSS identifier is expected. This sometimes means you are trying to use a variable in CSS like this: `color: {my_color}` but Svelte doesn't support that. You can use inline CSS variables for that `<div style:--color={my_color}></div>` and then use the variable as usual in CSS with `color: var(--color)`.",
const content = await svelte_autofixer_handler({
code,
desired_svelte_version: desired_svelte_version_unchecked,
async,
filename: filename_or_path,
});
return tool.structured(content);
} catch (e) {
const error = e as Error;
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(
server.ctx.sessionId,
'svelte-autofixer-error',
error.message,
);
}
return tool.error(error.message);
}
if (content.issues.length > 0 || content.suggestions.length > 0) {
content.require_another_tool_call_after_fixing = true;
}
return tool.structured(content);
},
);
}

View File

@@ -1,5 +1,41 @@
# @sveltejs/mcp
## 0.1.20
### Patch Changes
- fix: turn off no-inspect in eslint for mcp ([`2245cb2`](https://github.com/sveltejs/mcp/commit/2245cb2dc9e2d217869b6a800795ce59ffb40c51))
## 0.1.19
### Patch Changes
- chore: update svelte ([`7447744`](https://github.com/sveltejs/mcp/commit/74477448cea44ec21684ea4d39f2c5c7133b5150))
## 0.1.18
### Patch Changes
- feat: expose playground link as MCP App ([#138](https://github.com/sveltejs/mcp/pull/138))
## 0.1.17
### Patch Changes
- fix: add suggestion for snippets declared in script tag ([#132](https://github.com/sveltejs/mcp/pull/132))
## 0.1.16
### Patch Changes
- feat: expose tools as JS api + cli ([#128](https://github.com/sveltejs/mcp/pull/128))
## 0.1.15
### Patch Changes
- fix: server.json version + update publisher ([`9dfb4de`](https://github.com/sveltejs/mcp/commit/9dfb4dedb42837c40c4e660f0f816d7cf9081fc4))
## 0.1.14
### Patch Changes

View File

@@ -1,25 +0,0 @@
aada17479e04faa55e380ae1ee0de3c5a5d86b4c0ad5dd4b64f68426f87c84e0 mcp-publisher_darwin_amd64.tar.gz
d426c953218a61699bb47d1dae6f0b89807313f082d650f243b8a91bc7f448f3 mcp-publisher_darwin_amd64.tar.gz.sbom.json
75a14ff2570bca0eba656dee1d4ad4b2955de67e97aacc5884bcab1793d8dd2d mcp-publisher_darwin_arm64.tar.gz
988adc169f9ce4cded2b08ab0df7c1738f8ea77dccb581a234f7e036ad7b478e mcp-publisher_darwin_arm64.tar.gz.sbom.json
dc97c003df75d40134b9bf003077ac9057187d64f8d601c4f2457980712dbb62 mcp-publisher_linux_amd64.tar.gz
49e63cd1c1ade8988f6c6f679b3172ab28b5ae6ef92243a4f8a78c58150853c7 mcp-publisher_linux_amd64.tar.gz.sbom.json
094e70c73ff671da69b138b448c68336f582e9245355d469c8372a48639bbc6c mcp-publisher_linux_arm64.tar.gz
3888ef7db7a77efeb3cfb711527a00ed93188ff3fce05468a6308ae75e2bb913 mcp-publisher_linux_arm64.tar.gz.sbom.json
43a1f3ca680350dd898697c93e4e0a1489c2b638dc946c4ef496a815c0e46d53 mcp-publisher_windows_amd64.tar.gz
512ac8c24275845bcc9c4c4faf4f08d950de3585e13d283e1aa48c567735ad73 mcp-publisher_windows_amd64.tar.gz.sbom.json
739347d69d4979faf06365843c49ede92e16975cd1afe3ef627e9511459fa30b mcp-publisher_windows_arm64.tar.gz
b8ecb1cf98caef446447a1ad89f7259699632f09bd19d600c89fd800aa3c7698 mcp-publisher_windows_arm64.tar.gz.sbom.json
5b4d676da2a574ee0d3cfd752d9d3a663e675d48d928706dda0084271ded1cb5 registry-1.3.10.tar.gz
d5140c2730991dfc236305d3e6eddb6c05da9325d9803b1eaee50c06953d5991 registry_darwin_amd64.tar.gz
fd1380e0d308a1702d040427603d203a9407603e1156cd23743a3074e1313b06 registry_darwin_amd64.tar.gz.sbom.json
db22e6b0f518b1ccaf44f749babc28d70d2573ba5641b06ff085430826eac688 registry_darwin_arm64.tar.gz
4415141c7b490b1c7d9643e799df5a2885f3292b4c1c3e4c71f2841aae52fdeb registry_darwin_arm64.tar.gz.sbom.json
8fbb607254b412513eeec45c89fb3051fe9ce623196f6361ded1d335a5348566 registry_linux_amd64.tar.gz
c256554e7060cbfc92494f9b22c4e857b0606ca57479a29fa61649917c63c974 registry_linux_amd64.tar.gz.sbom.json
86ff45f0c5afc674e9b30ec47815ab54401d716b12d70f27bd73f72d5b5ad2f8 registry_linux_arm64.tar.gz
0ccb7a3d856a7951bea5d5e63314459b365199577f573c2fbcd11e7e98b9b94c registry_linux_arm64.tar.gz.sbom.json
f56d75a2569e6e37f44d8f7bede0bd01f8056130f8eab58b1211d55d43fa08fe registry_windows_amd64.tar.gz
d941a82dd69937edff791b4f84b61006d78802280fb0c06d15c07a5d90d6e0a0 registry_windows_amd64.tar.gz.sbom.json
b2e6fff3cd9bab76d4431294b2844e1e9a399d45728e5459492b21367da62453 registry_windows_arm64.tar.gz
1954a54f47048e1ac25c92196440f0c2b1ba2357e01720d5b14e6faf43be46b2 registry_windows_arm64.tar.gz.sbom.json

View File

@@ -0,0 +1,25 @@
eb5f89b76fc45a97070fa481eb03977584a78e3dff2781402d43482f114e4d6a mcp-publisher_darwin_amd64.tar.gz
29fc1b46a6f6be2580129369a9b681204b11b425d9a44147d79c8c658c7b8474 mcp-publisher_darwin_amd64.tar.gz.sbom.json
9eddbbb95efd54b9503f6c0668f43bab3f04c856946d3c7164f6daead232402f mcp-publisher_darwin_arm64.tar.gz
a8349b0ea7916f34cf4ee4e1ced5b91bc1ded6d100312cbb2095da018710da04 mcp-publisher_darwin_arm64.tar.gz.sbom.json
c4b402b43a85166c3f840641ca1c9e6de5bfa1cf533c22576d663ccbda0711bb mcp-publisher_linux_amd64.tar.gz
6f21cf917055be885f16b93154e06379540236599cfad6af4404a8323bde74b7 mcp-publisher_linux_amd64.tar.gz.sbom.json
ba5d486f86b2cef48ea506e8314d901a5169dcd56a5d6e9daf18d41244316235 mcp-publisher_linux_arm64.tar.gz
8a0096a407b916ac7270732df017a26f6112c73a066252f6556b8956c492a0b4 mcp-publisher_linux_arm64.tar.gz.sbom.json
59ee8c4a997f94794db8db13f8809666631686a70a8d89a9f0fea993f9aede0f mcp-publisher_windows_amd64.tar.gz
57d211fd9181f698d126bd773b55c98b92454d19b1e32e77860766179a8a2e8e mcp-publisher_windows_amd64.tar.gz.sbom.json
1410952b0a5968cbe89590e7b4ee6105147ef7267cf0cd50095c9bec2ee3b0d7 mcp-publisher_windows_arm64.tar.gz
6cb93e118a89ed1419135bfbaa7401bd3b7a7c5680a0d8fd7c78728f9d860630 mcp-publisher_windows_arm64.tar.gz.sbom.json
ebc17c3b7a5b86f9c036acf1d44fb904bb363bad0ac1ac37b7979eb17cf3d218 registry-1.4.0.tar.gz
5fffe8b078513fa5fbb625a213d164bb391c7c85e216e541cab789517bc6365b registry_darwin_amd64.tar.gz
10a61cf4173d8b5be63044af0a10e6c809eebc1006c0c1643753a252db808ddd registry_darwin_amd64.tar.gz.sbom.json
437746a1045f093266ad7298a47be41dc44cc33ecaeec145449c4eefeddf8880 registry_darwin_arm64.tar.gz
4c0cb24bcef0658540fb044d771294efd662edecd2f7fae7b1ca7ca2ae68f83a registry_darwin_arm64.tar.gz.sbom.json
bd77fafcc881714a63a375a9bdb53a761a2b8e367a9d2759835126c993df2356 registry_linux_amd64.tar.gz
d62a461b174089fbcaa4f1ac096bca491d18bc7f70e93ce0824fe89dbe42e974 registry_linux_amd64.tar.gz.sbom.json
38cb9e6112ff11544ba8ec88c5c0f44d4c851504ec2e33d497e25616a6f7a21e registry_linux_arm64.tar.gz
84e5a004929e7231ae35350a1fe8fb08668fc05183e13d1d78004abf08a25f3b registry_linux_arm64.tar.gz.sbom.json
77ca9243d2f744f282b39d07625f802d77f593850a0392debabe407643a8579c registry_windows_amd64.tar.gz
bb043e8a6a8d187ffab8987d36d0018024115d9217af6d2ddd233f94aea880ea registry_windows_amd64.tar.gz.sbom.json
e4ee50e05a95f288b874f5e24c7716a5032548feeabc83b715193298cec06890 registry_windows_arm64.tar.gz
49f189b615bce3d09ea24ae5d80e0816ac69225b3c8901dd7be19ef9ca06830c registry_windows_arm64.tar.gz.sbom.json

View File

@@ -1,6 +1,6 @@
{
"name": "@sveltejs/mcp",
"version": "0.1.14",
"version": "0.1.20",
"type": "module",
"license": "MIT",
"mcpName": "dev.svelte/mcp",
@@ -19,6 +19,12 @@
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/handlers.d.mts",
"import": "./dist/handlers.mjs"
}
},
"publishConfig": {
"access": "public"
},
@@ -41,6 +47,7 @@
},
"dependencies": {
"eslint": "catalog:lint",
"sade": "catalog:tooling",
"tmcp": "catalog:tmcp"
}
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "dev.svelte/mcp",
"title": "Svelte MCP",
"description": "The official Svelte MCP server providing docs and autofixing tools for Svelte development",
@@ -9,7 +9,7 @@
"subfolder": "packages/mcp-stdio",
"source": "github"
},
"version": "0.1.14",
"version": "0.1.20",
"websiteUrl": "https://svelte.dev/docs/mcp/overview",
"icons": [
{
@@ -25,7 +25,7 @@
{
"registryType": "npm",
"identifier": "@sveltejs/mcp",
"version": "0.1.14",
"version": "0.1.20",
"runtimeHint": "npx",
"transport": {
"type": "stdio"

View File

@@ -0,0 +1,6 @@
export {
list_sections_handler as listSections,
get_documentation_handler as getDocumentation,
svelte_autofixer_handler as svelteAutofixer,
playground_link_handler as playgroundLink,
} from '@sveltejs/mcp-server/handlers';

View File

@@ -1,7 +1,65 @@
#! /usr/bin/env node
import { server } from '@sveltejs/mcp-server';
import {
list_sections_handler,
get_documentation_handler,
svelte_autofixer_handler,
} from '@sveltejs/mcp-server/handlers';
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { StdioTransport } from '@tmcp/transport-stdio';
import sade from 'sade';
const transport = new StdioTransport(server);
const cli = sade('svelte-mcp');
transport.listen();
cli.command('__mcp', '', { default: true }).action(() => {
const transport = new StdioTransport(server);
transport.listen();
});
cli
.command('list-sections')
.describe('List all the available documentation sections')
.action(async () => {
console.log(await list_sections_handler());
});
cli
.command('get-documentation <sections>')
.describe('Get documentation for specified sections, separated by commas')
.action(async (sections) => {
console.log(await get_documentation_handler({ section: sections.split(',') }));
});
cli
.command('svelte-autofixer <code_or_path>')
.describe(
'Detect and suggest fixes for Svelte code issues, because the terminal will substitute variables `$` should be correctly escaped',
)
.option('--async', 'Wether the project is using async svelte or not', false)
.option('--svelte-version', 'Which version of svelte to use...it can be 4 or 5', 5)
.action(async (code_or_path, { async, 'svelte-version': version }) => {
let code = code_or_path;
let is_path = false;
if (existsSync(code_or_path)) {
console.log('Detected file path, reading file...');
code = await readFile(code_or_path, 'utf-8');
is_path = true;
} else {
console.log('File not found, treating input as code...');
}
const desired_svelte_version = +version;
const result = await svelte_autofixer_handler({
code,
async: Boolean(async),
desired_svelte_version,
filename: is_path ? code_or_path : undefined,
});
console.log(result);
});
cli.parse(process.argv);

View File

@@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: ['./src/index.ts'],
entry: ['./src/index.ts', './src/handlers.ts'],
platform: 'node',
define: {
// some eslint-plugin-svelte code expects __filename to exists but in an ESM environment it does not.
@@ -13,9 +13,12 @@ export default defineConfig([
// the require would fail once executed in a project without eslint installed.
external: ['eslint'],
publint: true,
dts: false,
dts: {
eager: true,
},
treeshake: true,
clean: true,
target: 'esnext',
inlineOnly: false,
},
]);

View File

@@ -0,0 +1,19 @@
# @sveltejs/opencode
## 0.1.0
### Minor Changes
- feat: distribute skills through opencode plugin ([#151](https://github.com/sveltejs/mcp/pull/151))
## 0.0.3
### Patch Changes
- chore: add README to opencode plugin ([`71295bc`](https://github.com/sveltejs/mcp/commit/71295bc11fb7bac6703e655f5fddead29967353c))
## 0.0.2
### Patch Changes
- fix: show toast after a few seconds to avoid race condition ([`57e2d1d`](https://github.com/sveltejs/mcp/commit/57e2d1def1f5590d0a3dd6d269ac39f6397ffecf))

View File

@@ -0,0 +1,75 @@
# @sveltejs/opencode
OpenCode plugin for Svelte that provides the Svelte MCP server, a specialized file editor subagent and instruction files.
## Installation
Add `@sveltejs/opencode` to your OpenCode config (either global or local):
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@sveltejs/opencode"]
}
```
That's it! You now have the Svelte MCP server and the file editor subagent configured automatically.
## Features
### Svelte MCP Server
The plugin automatically configures the [Svelte MCP server](https://mcp.svelte.dev) which provides:
- **list-sections** - Discover available Svelte 5 and SvelteKit documentation sections
- **get-documentation** - Retrieve full documentation content for specific sections
- **svelte-autofixer** - Analyze Svelte code and get issues/suggestions
- **playground-link** - Generate Svelte Playground links with provided code
### Svelte File Editor Subagent
A specialized subagent (`svelte-file-editor`) that is automatically used when creating, editing, or reviewing `.svelte`, `.svelte.ts`, or `.svelte.js` files. It fetches relevant documentation and validates code using the Svelte MCP server tools.
### Agent Instructions
The plugin injects instructions that teach the agent how to effectively use the Svelte MCP tools.
## Configuration
The default configuration:
```json
{
"$schema": "https://raw.githubusercontent.com/sveltejs/mcp/refs/heads/main/packages/opencode/schema.json",
"mcp": {
"type": "remote",
"enabled": true
},
"subagent": {
"enabled": true
},
"instructions": {
"enabled": true
}
}
```
### Configuration Options
| Option | Type | Default | Description |
| ---------------------- | ----------------------- | ---------- | -------------------------------------------------------------------------------- |
| `mcp.type` | `"remote"` \| `"local"` | `"remote"` | Use the remote server at `mcp.svelte.dev` or run locally via `npx @sveltejs/mcp` |
| `mcp.enabled` | `boolean` | `true` | Enable/disable the MCP server |
| `subagent.enabled` | `boolean` | `true` | Enable/disable the Svelte file editor subagent |
| `instructions.enabled` | `boolean` | `true` | Enable/disable agent instructions injection |
### Config File Location
Place your configuration at one of these locations:
- `~/.config/opencode/svelte.json` (global)
- `$OPENCODE_CONFIG_DIR/svelte.json` (if `OPENCODE_CONFIG_DIR` is set, takes priority)
## License
MIT

159
packages/opencode/config.ts Normal file
View File

@@ -0,0 +1,159 @@
import type { PluginInput } from '@opencode-ai/plugin';
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import * as v from 'valibot';
const default_config = {
mcp: {
type: 'remote' as 'remote' | 'local',
enabled: true,
},
subagent: {
enabled: true,
},
instructions: {
enabled: true,
},
skills: {
enabled: true,
},
};
export const config_schema = v.object({
mcp: v.optional(
v.object({
type: v.optional(v.picklist(['remote', 'local'])),
enabled: v.optional(v.boolean()),
}),
),
subagent: v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
),
instructions: v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
),
skills: v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
),
});
export type McpConfig = v.InferInput<typeof config_schema>;
const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode');
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, 'svelte.json');
interface ConfigLoadResult {
data: Record<string, unknown> | null;
parse_error?: string;
}
function get_config_paths() {
// Global: ~/.config/opencode/svelte.json
let global_path: string | null = null;
if (existsSync(GLOBAL_CONFIG_PATH)) {
global_path = GLOBAL_CONFIG_PATH;
}
// Custom config directory: $OPENCODE_CONFIG_DIR/svelte.json
let config_dir_path: string | null = null;
const opencode_config_dir = process.env.OPENCODE_CONFIG_DIR;
if (opencode_config_dir) {
const config_json = join(opencode_config_dir, 'svelte.json');
if (existsSync(config_json)) {
config_dir_path = config_json;
}
}
// returning config_dir first so it has higher priority
return [config_dir_path, global_path];
}
function load_config_file(config_path: string): ConfigLoadResult {
let file_content: string;
try {
file_content = readFileSync(config_path, 'utf-8');
} catch {
// File doesn't exist or can't be read
return { data: null };
}
try {
const parsed = JSON.parse(file_content);
if (parsed === undefined || parsed === null) {
return { data: null, parse_error: 'Config file is empty or invalid' };
}
return { data: parsed };
} catch (error: unknown) {
return {
data: null,
parse_error: error instanceof Error ? error.message : 'Failed to parse config',
};
}
}
function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
return {
mcp: {
...default_config.mcp,
...user_config.mcp,
},
subagent: {
...default_config.subagent,
...user_config.subagent,
},
instructions: {
...default_config.instructions,
...user_config.instructions,
},
skills: {
...default_config.skills,
...user_config.skills,
},
};
}
export function get_mcp_config(ctx: PluginInput) {
const config_paths = get_config_paths();
for (const path of config_paths) {
if (path && existsSync(path)) {
const result = load_config_file(path);
if (result.parse_error) {
setTimeout(() => {
ctx.client.tui.showToast({
body: {
title: 'Svelte: Invalid opencode plugin config',
message: `${result.parse_error}\nUsing default values`,
variant: 'warning',
duration: 7000,
},
});
}, 7000);
return default_config;
}
const parsed = v.safeParse(config_schema, result.data);
if (parsed.success) {
return merge_with_defaults(parsed.output);
} else {
setTimeout(() => {
ctx.client.tui.showToast({
body: {
title: 'Svelte: Invalid opencode plugin config',
message: `${result.parse_error}\nUsing default values`,
variant: 'warning',
duration: 7000,
},
});
}, 7000);
}
}
}
return default_config;
}

View File

@@ -0,0 +1,80 @@
import type { Plugin } from '@opencode-ai/plugin';
import { readdir } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { get_mcp_config } from './config.js';
const current_dir = dirname(fileURLToPath(import.meta.url));
export const svelte_plugin: Plugin = async (ctx) => {
return {
async config(input) {
input.agent ??= {};
input.mcp ??= {};
input.instructions ??= [];
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
input.skills ??= [];
// by default we use svelte as the name for the svelte MCP server
let svelte_mcp_name = 'svelte';
// we loop over every mcp server to see if any of them is already the svelte MCP server
for (const name in input.mcp) {
const mcp = input.mcp[name];
if (
(mcp?.type === 'remote' && mcp.url.includes('https://mcp.svelte.dev/mcp')) ||
(mcp?.type === 'local' &&
mcp.command.some((cmd: string) => cmd.includes('@sveltejs/mcp')))
) {
// if we found the svelte MCP server, we store its name and break
svelte_mcp_name = name;
break;
}
}
const mcp_config = get_mcp_config(ctx);
if (mcp_config.instructions?.enabled !== false) {
const instructions_dir = join(current_dir, 'instructions');
const instructions_paths = await readdir(instructions_dir);
input.instructions.push(...instructions_paths.map((file) => join(instructions_dir, file)));
}
if (mcp_config.skills?.enabled !== false) {
const skills_dir = join(current_dir, 'skills');
// @ts-expect-error -- skills is a new opencode feature
input.skills.push(skills_dir);
}
// if the user doesn't have the MCP server already we add one based on config
if (!input.mcp[svelte_mcp_name] && mcp_config.mcp?.enabled !== false) {
if (mcp_config.mcp?.type === 'remote') {
input.mcp[svelte_mcp_name] = {
type: 'remote',
url: 'https://mcp.svelte.dev/mcp',
};
} else {
input.mcp[svelte_mcp_name] = {
type: 'local',
command: ['npx', '-y', '@sveltejs/mcp'],
};
}
}
if (mcp_config.subagent?.enabled !== false) {
// we add the editor subagent that will be used when editing Svelte files to prevent wasting context on the main agent
input.agent['svelte-file-editor'] = {
color: '#ff3e00',
mode: 'subagent',
prompt: `You are a specialized Svelte coder. Always use the tools from the svelte MCP server to fetch documentation with \`get_documentation\` and validating the code with \`svelte_autofixer\`. If the autofixer returns any issue or suggestions solve them before summarizing the changes for the main agent.`,
description:
'Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server. Fetches relevant documentation and validates code using the Svelte MCP server tools.',
permission: {
bash: 'ask',
edit: 'allow',
webfetch: 'ask',
},
tools: {
[`${svelte_mcp_name}_*`]: true,
},
};
}
},
};
};

View File

@@ -0,0 +1,23 @@
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
## Available MCP Tools:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.

View File

@@ -0,0 +1,36 @@
{
"name": "@sveltejs/opencode",
"version": "0.1.0",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/sveltejs/mcp#readme",
"bugs": {
"url": "https://github.com/sveltejs/mcp/issues"
},
"scripts": {
"check": "tsc --noEmit",
"generate-schema": "node --import node-resolve-ts/register scripts/generate-schema.ts"
},
"files": [
"index.ts",
"config.ts",
"instructions",
"skills"
],
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/mcp.git",
"path": "packages/opencode"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"valibot": "catalog:tooling"
},
"devDependencies": {
"@opencode-ai/plugin": "catalog:ai",
"@valibot/to-json-schema": "catalog:tooling",
"@types/node": "catalog:tooling"
}
}

View File

@@ -0,0 +1,49 @@
{
"type": "object",
"properties": {
"mcp": {
"type": "object",
"properties": {
"type": {
"enum": [
"remote",
"local"
]
},
"enabled": {
"type": "boolean"
}
},
"required": []
},
"subagent": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": []
},
"instructions": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": []
},
"skills": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": []
}
},
"required": [],
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@@ -0,0 +1,8 @@
import { toJsonSchema } from '@valibot/to-json-schema';
import { config_schema } from '../config.js';
import fs from 'node:fs';
import path from 'node:path';
const json_schema = toJsonSchema(config_schema);
fs.writeFileSync(path.resolve('./schema.json'), JSON.stringify(json_schema, null, '\t'));

View File

@@ -0,0 +1,66 @@
---
name: svelte-code-writer
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating or editing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
---
# Svelte 5 Code Writer
## CLI Tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
```bash
npx @sveltejs/mcp list-sections
```
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
```
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
**Example:**
```bash
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
```
Analyzes Svelte code and suggests fixes for common issues.
**Options:**
- `--async` - Enable async Svelte mode (default: false)
- `--svelte-version` - Target version: 4 or 5 (default: 5)
**Examples:**
```bash
# Analyze inline code (escape $ as \$)
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
# Analyze a file
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
# Target Svelte 4
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
```
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
## Workflow
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["index.ts", "config.ts", "scripts/*"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,17 @@
{
"name": "svelte",
"description": "A plugin for all things related to Svelte development, MCP, skills, and more.",
"version": "1.0.0",
"author": {
"name": "Svelte"
},
"lspServers": {
"svelte": {
"command": "svelte-language-server",
"args": ["--stdio"],
"extensionToLanguage": {
".svelte": "svelte"
}
}
}
}

8
plugins/svelte/.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"svelte": {
"type": "http",
"url": "https://mcp.svelte.dev/mcp"
}
}
}

View File

@@ -0,0 +1,64 @@
---
name: svelte-file-editor
description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server. Fetches relevant documentation and validates code using the Svelte MCP server tools.
permissionMode: acceptEdits
---
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
## Available MCP Tools
### 1. list-sections
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.
### 2. get-documentation
Retrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after `list-sections` to fetch relevant docs for the task at hand.
**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`
### 3. svelte-autofixer
Analyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:
- Using `$effect` instead of `$derived` for computations
- Missing cleanup in effects
- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)
- Missing keys in `{#each}` blocks
- And more
## Workflow
When invoked to work on a Svelte file:
### 1. Gather Context (if needed)
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
1. Call `list-sections` to see available documentation
2. Call `get-documentation` with relevant section names
### 2. Read the Target File
Read the file to understand the current implementation.
### 3. Make Changes
Apply edits following Svelte 5 best practices:
### 4. Validate Changes
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
### 5. Fix Any Issues
If the autofixer reports problems, fix them and re-validate until no issues remain.
## Output Format
After completing your work, provide:
1. Summary of changes made
2. Any issues found and fixed by the autofixer
3. Recommendations for further improvements (if any)

View File

@@ -0,0 +1,66 @@
---
name: svelte-code-writer
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating or editing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
---
# Svelte 5 Code Writer
## CLI Tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
```bash
npx @sveltejs/mcp list-sections
```
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
```
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
**Example:**
```bash
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
```
Analyzes Svelte code and suggests fixes for common issues.
**Options:**
- `--async` - Enable async Svelte mode (default: false)
- `--svelte-version` - Target version: 4 or 5 (default: 5)
**Examples:**
```bash
# Analyze inline code (escape $ as \$)
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
# Analyze a file
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
# Target Svelte 4
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
```
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
## Workflow
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component

3212
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,9 @@ packages:
catalogs:
ai:
'@anthropic-ai/sdk': ^0.71.0
'@mcp-ui/server': ^5.12.0
'@modelcontextprotocol/inspector': ^0.18.0
'@mcp-ui/server': ^6.0.0
'@modelcontextprotocol/inspector': ^0.19.0
'@opencode-ai/plugin': ^1.1.44
lint:
'@eslint/compat': ^2.0.0
'@eslint/js': ^9.36.0
@@ -18,24 +19,24 @@ catalogs:
eslint-plugin-import: ^2.32.0
eslint-plugin-pnpm: ^1.3.0
eslint-plugin-svelte: ^3.12.5
globals: ^16.0.0
globals: ^17.0.0
prettier: ^3.4.2
prettier-plugin-svelte: ^3.3.3
svelte-eslint-parser: ^1.4.0
typescript-eslint: ^8.44.0
orm:
'@libsql/client': ^0.15.0
'@libsql/client': ^0.17.0
drizzle-kit: ^0.31.0
drizzle-orm: ^0.45.0
svelte:
'@sveltejs/adapter-vercel': ^6.0.0
'@sveltejs/kit': ^2.42.2
'@sveltejs/vite-plugin-svelte': ^6.0.0
svelte: ^5.39.2
svelte: ^5.47.0
svelte-check: ^4.0.0
tmcp:
'@tmcp/adapter-valibot': ^0.1.4
'@tmcp/transport-http': ^0.8.3
'@tmcp/transport-http': ^0.8.4
'@tmcp/transport-in-memory': ^0.0.5
'@tmcp/transport-stdio': ^0.4.0
tmcp: ^1.19.0
@@ -44,14 +45,16 @@ catalogs:
'@svitejs/changesets-changelog-github-compact': ^1.2.0
'@types/estree': ^1.0.8
'@types/node': ^24.3.1
'@valibot/to-json-schema': ^1.5.0
'@vercel/analytics': ^1.5.0
dotenv: ^17.2.3
node-resolve-ts: ^1.0.2
publint: ^0.3.13
ts-blank-space: ^0.6.2
tsdown: ^0.18.0
sade: 1.8.1
ts-blank-space: ^0.7.0
tsdown: ^0.20.0
typescript: ^5.0.0
valibot: ^1.1.0
valibot: ^1.2.0
vite: ^7.0.4
vite-plugin-devtools-json: ^1.0.0
vitest: ^4.0.0

View File

@@ -0,0 +1,100 @@
import fs from 'node:fs/promises';
import path from 'node:path';
interface SkillFrontmatter {
name: string;
description: string;
}
function get_backtick_fence(content: string): string {
const backtick_pattern = /`{3,}/g;
let max_backticks = 3;
let match;
while ((match = backtick_pattern.exec(content)) !== null) {
max_backticks = Math.max(max_backticks, match[0].length);
}
return '`'.repeat(max_backticks + 1);
}
function parse_frontmatter(
content: string,
): { frontmatter: SkillFrontmatter; body: string } | null {
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) return null;
const frontmatter_str = match[1];
const body = match[2];
if (!frontmatter_str || body === undefined) return null;
const frontmatter: Record<string, string> = {};
for (const line of frontmatter_str.split('\n')) {
const [key, ...value_parts] = line.split(':');
if (key && value_parts.length > 0) {
frontmatter[key.trim()] = value_parts.join(':').trim();
}
}
return {
frontmatter: frontmatter as unknown as SkillFrontmatter,
body: body.trim(),
};
}
let content = `---
title: Overview
---
This is the list of available skills provided by the Svelte MCP package. Skills are sets of instructions that AI agents can load on-demand to help with specific tasks.
Skills are available in both the Claude Code plugin (installed via the marketplace) and the OpenCode plugin (\`@sveltejs/opencode\`). They can also be manually installed in your \`.claude/skills/\` or \`.opencode/skills/\` folder.
You can download the latest skills from the [releases page](https://github.com/sveltejs/mcp/releases) or find them in the [\`plugins/svelte/skills\`](https://github.com/sveltejs/mcp/tree/main/plugins/svelte/skills) folder.
`;
const skills_dir = './plugins/svelte/skills';
const skill_dirs = (await fs.readdir(skills_dir)).filter((name) => !name.startsWith('.'));
for (const skill_name of skill_dirs) {
const skill_path = path.join(skills_dir, skill_name, 'SKILL.md');
try {
const skill_content = await fs.readFile(skill_path, 'utf-8');
const parsed = parse_frontmatter(skill_content);
if (!parsed) {
console.warn(`Warning: Could not parse frontmatter for ${skill_name}`);
continue;
}
const { frontmatter, body } = parsed;
const fence = get_backtick_fence(body);
content += `## \`${frontmatter.name}\`
${frontmatter.description}
<a href="https://github.com/sveltejs/mcp/releases?q=${frontmatter.name}" target="_blank" rel="noopener noreferrer">Open Releases page</a>
<details>
<summary>View skill content</summary>
<!-- prettier-ignore-start -->
${fence}markdown
${body}
${fence}
<!-- prettier-ignore-end -->
</details>
`;
} catch {
console.warn(`Warning: Could not read skill at ${skill_path}`);
}
}
await fs.writeFile('./documentation/docs/60-skills/10-skills.md', content.trim() + '\n');