Compare commits

..

39 Commits

Author SHA1 Message Date
paoloricciuti
77c1728ab1 Merge branch 'main' into js-api-and-cli 2025-12-19 18:26:58 +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
83e9d6583a Merge branch 'main' into js-api-and-cli 2025-12-18 20:21:42 +01:00
paoloricciuti
3747623d55 chore: setup pkg.pr.new 2025-12-18 20:00:30 +01:00
paoloricciuti
5d8a85c2e3 fix: move to catalog 2025-12-18 19:27:36 +01:00
paoloricciuti
eac75d3629 fix: types exports 2025-12-18 19:23:25 +01:00
paoloricciuti
59abf83bdb feat: expose tools as JS api + cli 2025-12-18 18:43:53 +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
github-actions[bot]
b891e4860b Version Packages (#126)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-17 18:36:08 +01:00
Paolo Ricciuti
c9e8508dd9 fix: changesets 2025-12-17 18:31:39 +01:00
github-actions[bot]
9896406aff docs: update prompt documentation (#125)
Co-authored-by: paoloricciuti <26281609+paoloricciuti@users.noreply.github.com>
2025-12-17 18:29:54 +01:00
Paolo Ricciuti
b01ae9069b fix: typo again 2025-12-17 18:27:20 +01:00
Paolo Ricciuti
b6db495242 fix: changesets 2025-12-17 18:19:08 +01:00
Paolo Ricciuti
3926310107 fix: typo in documentation tool name in svelte_task 2025-12-17 18:17:30 +01:00
Paolo Ricciuti
848627549f fix: improve prompt to reduce token usage (#124)
Co-authored-by: Stanislav Khromov <stanislav.khromov+github@gmail.com>
2025-12-17 18:13:07 +01:00
renovate[bot]
283164ca7e chore(deps): update peter-evans/create-pull-request action to v8 (#121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 16:48:55 +01:00
renovate[bot]
c6ee414f62 chore(deps): update all non-major dependencies (#119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 16:48:38 +01:00
Matheus Ferraz
ea5bdf66dc chore(docs): fix tool name typo (#120) 2025-12-13 01:32:19 +01:00
paoloricciuti
7d707202d1 fix: update tmcp 2025-12-06 10:44:04 +01:00
renovate[bot]
f0daadfbd0 chore(deps): update all non-major dependencies (#117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 23:52:14 +01:00
github-actions[bot]
15a7774da7 Version Packages (#118)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-05 22:34:53 +01:00
paoloricciuti
98efa1e09e fix: revert name change and add title 2025-12-05 22:26:33 +01:00
github-actions[bot]
e20cf2974d Version Packages (#100)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-03 10:37:26 +01:00
paoloricciuti
b2ee968a3f chore: update publisher 2025-12-03 10:36:46 +01:00
paoloricciuti
60297b3c49 fix: update server name on mcp registry 2025-12-03 10:34:39 +01:00
renovate[bot]
39076da8ce chore(deps): update dependency @eslint/compat to v2 (#111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 10:34:15 +01:00
renovate[bot]
fba733646a chore(deps): update all non-major dependencies (#112)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 10:02:16 +01:00
renovate[bot]
7fcd4705a5 chore(deps): update actions/checkout action to v6 (#114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 10:02:00 +01:00
renovate[bot]
af7d341ba5 chore(deps): update pnpm to v10.22.0 (#107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 15:29:04 +01:00
Lucas Terracino
52546551ff Update valid scope options in remote setup documentation for Gemini CLI (#108) 2025-11-12 23:01:54 +01:00
renovate[bot]
1f0a5f1519 chore(deps): update all non-major dependencies (#98)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
2025-11-11 12:39:11 +01:00
Scott Wu
0bf04bad2e docs: add reference to GEMINI.md and CLI change (#106) 2025-11-11 12:19:12 +01:00
Paolo Ricciuti
f001918925 chore: add initialize analytics event (#104) 2025-11-04 11:21:49 +01:00
paoloricciuti
67487c324a fix: remove log 2025-11-04 11:03:23 +01:00
jyc.dev
5beeef5543 moving to pnpm catalogs (#101) 2025-11-01 00:10:54 +01:00
Willow (GHOST)
e1a03fdb85 docs: update zed docs to use svelte-mcp extension (#102) 2025-10-31 17:11:46 +01:00
Paolo Ricciuti
384c1fd209 chore: update tmcp (#99) 2025-10-30 09:34:03 +01:00
40 changed files with 1551 additions and 973 deletions

View File

@@ -0,0 +1,5 @@
---
'@sveltejs/mcp': patch
---
feat: expose tools as JS api + cli

View File

@@ -1,5 +0,0 @@
---
'@sveltejs/mcp': patch
---
chore: update tmcp

View File

@@ -13,12 +13,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.20.0
version: 10.26.1
- name: Setup Node.js
uses: actions/setup-node@v6

View File

@@ -13,12 +13,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.20.0
version: 10.26.1
- name: Setup Node.js
uses: actions/setup-node@v6

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@v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- run: pnpm dlx pkg-pr-new publish --compact './packages/mcp-stdio' --pnpm

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Publish to MCP Registry
working-directory: packages/mcp-stdio
@@ -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.3 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.3/$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.3_checksums.txt
sha256sum --ignore-missing -c ./checksums/registry_1.4.0_checksums.txt
# Extract the tarball
mkdir tmp

View File

@@ -25,7 +25,7 @@ jobs:
os: [ubuntu-latest]
steps:
- name: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0

View File

@@ -13,12 +13,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.20.0
version: 10.26.1
- name: Setup Node.js
uses: actions/setup-node@v6

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -55,7 +55,7 @@ jobs:
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'docs: update prompts documentation'

View File

@@ -37,35 +37,35 @@
],
"private": true,
"devDependencies": {
"@eslint/compat": "^1.3.2",
"@eslint/js": "^9.36.0",
"@libsql/client": "^0.15.0",
"@modelcontextprotocol/inspector": "^0.17.0",
"@sveltejs/adapter-vercel": "^6.0.0",
"@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.31.0",
"drizzle-orm": "^0.44.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": "^4.0.0"
"@eslint/compat": "catalog:lint",
"@eslint/js": "catalog:lint",
"@libsql/client": "catalog:orm",
"@modelcontextprotocol/inspector": "catalog:ai",
"@sveltejs/adapter-vercel": "catalog:svelte",
"@sveltejs/kit": "catalog:svelte",
"@sveltejs/vite-plugin-svelte": "catalog:svelte",
"@types/node": "catalog:tooling",
"@typescript-eslint/parser": "catalog:lint",
"drizzle-kit": "catalog:orm",
"drizzle-orm": "catalog:orm",
"eslint-config-prettier": "catalog:lint",
"eslint-plugin-svelte": "catalog:lint",
"globals": "catalog:lint",
"prettier": "catalog:lint",
"prettier-plugin-svelte": "catalog:lint",
"svelte": "catalog:svelte",
"svelte-check": "catalog:svelte",
"svelte-eslint-parser": "catalog:lint",
"typescript": "catalog:tooling",
"vite": "catalog:tooling",
"vite-plugin-devtools-json": "catalog:tooling",
"vitest": "catalog:tooling"
},
"dependencies": {
"@sveltejs/mcp-schema": "workspace:^",
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-http": "catalog:mcp",
"@vercel/analytics": "^1.5.0",
"tmcp": "catalog:mcp"
"@tmcp/transport-http": "catalog:tmcp",
"@vercel/analytics": "catalog:tooling",
"tmcp": "catalog:tmcp"
}
}

View File

@@ -5,9 +5,6 @@ import { redirect } from '@sveltejs/kit';
import { track } from '@vercel/analytics/server';
export async function handle({ event, resolve }) {
if (event.request.method === 'POST') {
console.log(await event.request.clone().text());
}
if (event.request.method === 'GET') {
const accept = event.request.headers.get('accept');
if (accept) {
@@ -24,8 +21,8 @@ export async function handle({ event, resolve }) {
// only add analytics in production
track: dev
? undefined
: async (session_id, event, slug) => {
await track(event, { session_id, ...(slug ? { slug } : {}) });
: async (session_id, event, extra) => {
await track(event, { session_id, ...(extra ? { extra } : {}) });
},
});
// we are deploying on vercel the SSE connection will timeout after 5 minutes...for

View File

@@ -13,7 +13,9 @@ The setup varies based on the version of the MCP you prefer — remote or local
## Usage
To get the most out of the MCP server we recommend including the following prompt in your [`AGENTS.md`](https://agents.md) (or [`CLAUDE.md`](https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports), if using Claude Code). This will tell the LLM which tools are available and when it's appropriate to use them.
To get the most out of the MCP server we recommend including the following prompt in your [`AGENTS.md`](https://agents.md) (or [`CLAUDE.md`](https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports), if using Claude Code. Or [`GEMINI.md`](https://geminicli.com/docs/cli/gemini-md/), if using GEMINI). This will tell the LLM which tools are available and when it's appropriate to use them.
> [!NOTE] This is already setup for you when using `npx sv add mcp`
```md
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:

View File

@@ -110,6 +110,12 @@ It will open a file with your MCP servers where you can add the following config
## Zed
Install the [Svelte MCP Server extension](https://zed.dev/extensions/svelte-mcp).
<details>
<summary>Configure Manually</summary>
- Open the command palette
- Search and select "agent:open settings"
- In settings panel look for `Model Context Protocol (MCP) Servers`
@@ -127,6 +133,8 @@ It will open a popup with MCP server config where you can add the following conf
}
```
</details>
## Other clients
If we didn't include the MCP client you are using, refer to their documentation for `stdio` servers and use `npx` as the command and `-y @sveltejs/mcp` as the arguments.

View File

@@ -42,7 +42,7 @@ To use the remote MCP server with Gemini CLI, simply run the following command:
gemini mcp add -t http -s [scope] svelte https://mcp.svelte.dev/mcp
```
The `[scope]` must be `user`, `project` or `local`.
The `[scope]` must be `user` or `project`.
## OpenCode

View File

@@ -12,7 +12,7 @@ This prompt should be used whenever you are asking the model to work on a Svelte
<summary>Copy the prompt</summary>
```md
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:
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. However: before invoking the `get-documentation` tool, try to answer the users query using your own knowledge and the `svelte-autofixer` tool. Be mindful of how many section you request, since it is token-intensive!
<available-docs>
- title: Overview, use_cases: project setup, creating new svelte apps, scaffolding, cli tools, initializing projects, path: cli/overview
@@ -153,6 +153,7 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- title: Context, use_cases: shared state, avoiding prop drilling, component communication, theme providers, user context, authentication state, configuration sharing, deeply nested components, path: svelte/context
- title: Lifecycle hooks, use_cases: component initialization, cleanup tasks, timers, subscriptions, dom measurements, chat windows, autoscroll features, migration from svelte 4, path: svelte/lifecycle-hooks
- title: Imperative component API, use_cases: project setup, client-side rendering, server-side rendering, ssr, hydration, testing, programmatic component creation, tooltips, dynamic mounting, path: svelte/imperative-component-api
- title: Hydratable data, use_cases: use title and path to estimate use case, path: svelte/hydratable
- title: Testing, use_cases: testing, quality assurance, unit tests, integration tests, component tests, e2e tests, vitest setup, playwright setup, test automation, path: svelte/testing
- title: TypeScript, use_cases: typescript setup, type safety, component props typing, generic components, wrapper components, dom type augmentation, project configuration, path: svelte/typescript
- title: Custom elements, use_cases: web components, custom elements, component library, design system, framework-agnostic components, embedding svelte in non-svelte apps, shadow dom, path: svelte/custom-elements
@@ -192,6 +193,8 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
</available-docs>
These are the available documentation sections that `list-sections` will return, you do not need to call it again.
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:

View File

@@ -7,13 +7,20 @@ import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './apps/mcp-remote/svelte.config.js';
import eslint_plugin_import from 'eslint-plugin-import';
import { configs as pnpm } from 'eslint-plugin-pnpm';
const gitignore_path = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default /** @type {import("eslint").Linter.Config} */ ([
includeIgnoreFile(gitignore_path),
{
ignores: ['.claude/**/*', '.changeset/*'],
ignores: [
'.claude/**/*',
'.changeset/*',
'.github/**/*.yml',
'.github/**/*.yaml',
'**/pnpm-lock.yaml',
],
},
js.configs.recommended,
...ts.configs.recommended,
@@ -74,4 +81,16 @@ export default /** @type {import("eslint").Linter.Config} */ ([
},
},
},
{
name: 'pnpm/exclude-some-rules',
files: ['**/*.json', '**/*.yaml', '**/*.yml', 'pnpm-workspace.yaml'],
rules: {
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'func-style': 'off',
},
},
...pnpm.json,
...pnpm.yaml,
]);

View File

@@ -3,7 +3,7 @@
"version": "0.0.1",
"description": "The official Svelte MCP server implementation",
"type": "module",
"packageManager": "pnpm@10.20.0",
"packageManager": "pnpm@10.26.1",
"scripts": {
"build": "pnpm -r run build",
"dev": "pnpm --filter @sveltejs/mcp-remote run dev",
@@ -12,6 +12,8 @@
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"lint:fix": "prettier --write . && eslint . --fix",
"lint:inspect": "pnpm dlx @eslint/config-inspector",
"node:inspect": "pnpm dlx node-modules-inspector",
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"test:watch": "npm run test:unit -- --watch",
@@ -30,27 +32,24 @@
],
"private": true,
"devDependencies": {
"@changesets/cli": "^2.29.7",
"@eslint/compat": "^1.3.2",
"@eslint/js": "^9.36.0",
"@modelcontextprotocol/inspector": "^0.17.0",
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-svelte": "^3.12.3",
"globals": "^16.0.0",
"node-resolve-ts": "^1.0.2",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"publint": "^0.3.13",
"typescript": "^5.0.0",
"typescript-eslint": "^8.44.1",
"vitest": "^4.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
"@changesets/cli": "catalog:tooling",
"@eslint/compat": "catalog:lint",
"@eslint/js": "catalog:lint",
"@modelcontextprotocol/inspector": "catalog:ai",
"@sveltejs/adapter-vercel": "catalog:svelte",
"@svitejs/changesets-changelog-github-compact": "catalog:tooling",
"eslint": "catalog:lint",
"eslint-config-prettier": "catalog:lint",
"eslint-plugin-import": "catalog:lint",
"eslint-plugin-pnpm": "catalog:lint",
"eslint-plugin-svelte": "catalog:lint",
"globals": "catalog:lint",
"node-resolve-ts": "catalog:tooling",
"prettier": "catalog:lint",
"prettier-plugin-svelte": "catalog:lint",
"publint": "catalog:tooling",
"typescript": "catalog:tooling",
"typescript-eslint": "catalog:lint",
"vitest": "catalog:tooling"
}
}

View File

@@ -14,6 +14,6 @@
"license": "ISC",
"type": "module",
"dependencies": {
"drizzle-orm": "^0.44.0"
"drizzle-orm": "catalog:orm"
}
}

View File

@@ -14,33 +14,35 @@
"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.44.0"
"drizzle-orm": "^0.45.0"
},
"dependencies": {
"@mcp-ui/server": "^5.12.0",
"@mcp-ui/server": "catalog:ai",
"@sveltejs/mcp-schema": "workspace:^",
"@tmcp/adapter-valibot": "catalog:mcp",
"@typescript-eslint/parser": "^8.44.0",
"eslint": "^9.36.0",
"eslint-plugin-svelte": "^3.12.5",
"svelte": "^5.39.2",
"svelte-eslint-parser": "^1.4.0",
"tmcp": "catalog:mcp",
"ts-blank-space": "^0.6.2",
"typescript-eslint": "^8.44.0",
"valibot": "^1.1.0",
"vitest": "^4.0.0",
"zimmerframe": "^1.1.4"
"@tmcp/adapter-valibot": "catalog:tmcp",
"@tmcp/transport-in-memory": "catalog:tmcp",
"@typescript-eslint/parser": "catalog:lint",
"eslint": "catalog:lint",
"eslint-plugin-svelte": "catalog:lint",
"svelte": "catalog:svelte",
"svelte-eslint-parser": "catalog:lint",
"tmcp": "catalog:tmcp",
"ts-blank-space": "catalog:tooling",
"typescript-eslint": "catalog:lint",
"valibot": "catalog:tooling",
"vitest": "catalog:tooling",
"zimmerframe": "catalog:tooling"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.68.0",
"@sveltejs/kit": "^2.42.2",
"@types/eslint-scope": "^8.3.2",
"@types/estree": "^1.0.8",
"@typescript-eslint/types": "^8.44.0",
"dotenv": "^17.2.3"
"@anthropic-ai/sdk": "catalog:ai",
"@sveltejs/kit": "catalog:svelte",
"@types/eslint-scope": "catalog:lint",
"@types/estree": "catalog:tooling",
"@typescript-eslint/types": "catalog:lint",
"dotenv": "catalog:tooling"
}
}

View File

@@ -2,19 +2,22 @@ import type { SvelteMcp } from '../../index.js';
import * as v from 'valibot';
import { format_sections_list } from '../../utils.js';
import { icons } from '../../icons/index.js';
import { prompt } from 'tmcp/utils';
/**
* Function that actually generates the prompt string. You can use this in the MCP server handler to generate the prompt, it can accept arguments
* if needed (it will always be invoked manually so it's up to you to provide the arguments).
*/
function svelte_task(available_docs: string, task: string) {
return `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:
return `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. However: before invoking the \`get-documentation\` tool, try to answer the users query using your own knowledge and the \`svelte-autofixer\` tool. Be mindful of how many section you request, since it is token-intensive!
<available-docs>
${available_docs}
</available-docs>
These are the available documentation sections that \`list-sections\` will return, you do not need to call it again.
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:
@@ -73,17 +76,7 @@ export function setup_svelte_task(server: SvelteMcp) {
}
const available_docs = await format_sections_list();
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: svelte_task(available_docs, task),
},
},
],
};
return prompt.text(svelte_task(available_docs, task));
},
);
}

View File

@@ -1,6 +1,7 @@
import type { SvelteMcp } from '../../index.js';
import { get_sections, fetch_with_timeout } from '../../utils.js';
import { icons } from '../../icons/index.js';
import { resource } from 'tmcp/utils';
export async function list_sections(server: SvelteMcp) {
const sections = await get_sections();
@@ -59,15 +60,7 @@ export async function list_sections(server: SvelteMcp) {
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,
},
],
};
return resource.text(uri, content);
},
);
}

View File

@@ -3,118 +3,139 @@ import * as v from 'valibot';
import { get_sections, fetch_with_timeout, format_sections_list } from '../../utils.js';
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.',
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',
),
),
}),
'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: 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 {
content: [
{
type: 'text',
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

@@ -2,6 +2,13 @@ import type { SvelteMcp } from '../../index.js';
import { format_sections_list } from '../../utils.js';
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(
@@ -9,22 +16,32 @@ export function list_sections(server: SvelteMcp) {
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 {
content: [
{
type: 'text',
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

@@ -0,0 +1,84 @@
import { InMemoryTransport } from '@tmcp/transport-in-memory';
import { beforeEach, describe, expect, it } from 'vitest';
import { server } from '../../index.js';
const transport = new InMemoryTransport(server);
let session: ReturnType<typeof transport.session>;
describe('playground-link tool', () => {
beforeEach(async () => {
session = transport.session();
await session.initialize(
'2025-06-18',
{},
{
name: 'test-client',
version: '1.0.0',
},
);
});
it('should create a playground link if App.svelte is present', async () => {
const result = await session.callTool<{ url: string }>('playground-link', {
name: 'My Playground',
tailwind: false,
files: {
'App.svelte': `Hi there!`,
},
});
expect(result.structuredContent).toBeDefined();
expect(result.structuredContent?.url).toBeDefined();
// Verify URL structure rather than exact match (gzip compression can vary by platform)
expect(result.structuredContent?.url).toMatch(/^https:\/\/svelte\.dev\/playground#H4sIA/);
expect(result.structuredContent?.url).toContain('svelte.dev/playground');
});
it('should have a content with the stringified version of structured content and an ui resource', async () => {
const result = await session.callTool<{ url: string }>('playground-link', {
name: 'My Playground',
tailwind: false,
files: {
'App.svelte': `Hi there!`,
},
});
expect(result.structuredContent).toBeDefined();
expect(result.content).toStrictEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'text',
text: JSON.stringify(result.structuredContent),
}),
]),
);
// Verify resource structure without exact URL match (gzip compression can vary by platform)
expect(result.content).toStrictEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'resource',
resource: expect.objectContaining({
uri: 'ui://svelte/playground-link',
mimeType: 'text/uri-list',
_meta: { 'mcpui.dev/ui-preferred-frame-size': ['100%', '1200px'] },
text: expect.stringMatching(/^https:\/\/svelte\.dev\/playground\/embed#H4sIA/),
}),
}),
]),
);
});
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',
tailwind: false,
files: {
'Something.svelte': `Hi there!`,
},
});
expect(result.isError).toBe(true);
expect(result.content?.[0]).toStrictEqual({
type: 'text',
text: 'The files must contain an App.svelte file as the entry point',
});
});
});

View File

@@ -2,6 +2,7 @@ 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';
async function compress_and_encode_text(input: string) {
const reader = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip')).getReader();
@@ -29,107 +30,126 @@ 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(),
};
}
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(),
}),
schema: playground_link_schema,
outputSchema: playground_link_output_schema,
annotations: {
title: 'Playground Link',
destructiveHint: false,
readOnlyHint: true,
openWorldHint: false,
},
icons,
},
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) {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link-no-app-svelte');
}
try {
const result = await playground_link_handler({ files, name, tailwind });
return {
isError: true,
content: [
{
type: 'text',
text: JSON.stringify({
error: 'The files must contain an App.svelte file as the entry point',
}),
text: JSON.stringify({ url: result.url }),
},
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-error',
error.message,
);
}
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

@@ -1,12 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { InMemoryTransport } from '@tmcp/transport-in-memory';
import { beforeEach, describe, expect, it } from 'vitest';
import { server } from '../../index.js';
/**
* Small utility to create a JSON-RPC request without having to always specify as const
*/
function request<const T>(request: T) {
return request;
}
const transport = new InMemoryTransport(server);
let session: ReturnType<typeof transport.session>;
async function autofixer_tool_call(
code: string,
@@ -14,51 +13,34 @@ async function autofixer_tool_call(
desired_svelte_version = 5,
async = false,
) {
const result = await server.receive({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'svelte-autofixer',
arguments: {
code,
desired_svelte_version,
filename: 'App.svelte',
async,
},
},
const result = await session.callTool('svelte-autofixer', {
code,
desired_svelte_version,
filename: 'App.svelte',
async,
});
expect(result).toBeDefined();
expect(result.result).toBeDefined();
if (is_error) {
return result.result;
return result as any;
}
expect(result.result.structuredContent).toBeDefined();
return result.result.structuredContent;
expect(result.structuredContent).toBeDefined();
return result.structuredContent as any;
}
describe('svelte-autofixer tool', () => {
beforeEach(async () => {
const initialize_request = request({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2025-06-18',
capabilities: {
roots: { listChanged: true },
},
clientInfo: {
name: 'test-client',
version: '1.0.0',
},
},
});
session = transport.session();
await server.receive(initialize_request, {
sessionId: 'svelte-autofixer-session',
});
session = transport.session();
await session.initialize(
'2025-06-18',
{},
{
name: 'test-client',
version: '1.0.0',
},
);
});
it('should add suggestions for js parse errors', async () => {

View File

@@ -5,6 +5,96 @@ 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';
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 } };
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)`.",
);
}
}
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(
@@ -13,32 +103,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,
@@ -56,85 +122,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 {
isError: true,
content: [
{
type: 'text',
text: `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 {
isError: true,
content: [
{
type: 'text',
text: `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 {
content: [
{
type: 'text',
text: JSON.stringify(content),
},
],
structuredContent: content,
};
},
);
}

View File

@@ -26,7 +26,7 @@ export const server = new McpServer(
},
).withContext<{
db: LibSQLDatabase<Schema>;
track?: (sessionId: string, event: string, slug?: string) => Promise<void>;
track?: (sessionId: string, event: string, extra?: string) => Promise<void>;
}>();
export type SvelteMcp = typeof server;
@@ -34,3 +34,8 @@ export type SvelteMcp = typeof server;
setup_tools(server);
setup_resources(server);
setup_prompts(server);
server.on('initialize', async ({ clientInfo: client_info }) => {
if (!server.ctx.custom?.track || !server.ctx.sessionId) return;
server.ctx.custom.track(server.ctx.sessionId, 'initialize', client_info.name);
});

View File

@@ -1,5 +1,31 @@
# @sveltejs/mcp
## 0.1.15
### Patch Changes
- fix: server.json version + update publisher ([`9dfb4de`](https://github.com/sveltejs/mcp/commit/9dfb4dedb42837c40c4e660f0f816d7cf9081fc4))
## 0.1.14
### Patch Changes
- fix: improve prompt to reduce token usage ([#124](https://github.com/sveltejs/mcp/pull/124))
## 0.1.13
### Patch Changes
- fix: revert name change and add title ([`98efa1e`](https://github.com/sveltejs/mcp/commit/98efa1e09ebcca7827b10dc6bc8e1699fc1e5171))
## 0.1.12
### Patch Changes
- fix: update server name on mcp registry ([`60297b3`](https://github.com/sveltejs/mcp/commit/60297b3c49bf110b48908e61b5d5d902ea1bdf39))
- chore: update tmcp ([#99](https://github.com/sveltejs/mcp/pull/99))
## 0.1.11
### Patch Changes

View File

@@ -1,25 +0,0 @@
704f15b3ccba80990fbaaeb858e2e893120dea3ebb9340e04a36065987e089c5 mcp-publisher_darwin_amd64.tar.gz
9eadadeb80998cd5b29e009dd3959634730d3754b9e201c0a3bb39dd6ae85933 mcp-publisher_darwin_amd64.tar.gz.sbom.json
f80265eadc6b052d2885a7dfa47a1ecc7ca95926c143197ced747317793dcc11 mcp-publisher_darwin_arm64.tar.gz
f063c462512eed8f2ce9610a6a125534cb552060ddb5364d8d49b32196416fbe mcp-publisher_darwin_arm64.tar.gz.sbom.json
1113b9d6bf59b000966c4f17752cf87b51db03dcc5482721421fd843ce3bf048 mcp-publisher_linux_amd64.tar.gz
d3e46855b3b906aa84571b0d16384ad6074fd08709108a47d724908e91947b9d mcp-publisher_linux_amd64.tar.gz.sbom.json
34be13ec07490ab1250194d56820af222b49b85db6062ff848d33f9cf6eb41ce mcp-publisher_linux_arm64.tar.gz
a1ff3c26d53007c98301b747f97e5e241bfdb860b6a761db8627c9ba9ba71d88 mcp-publisher_linux_arm64.tar.gz.sbom.json
9ba8b538744652c4837d08b7b83ae85d2dbea98c897cfcf4d90835df4fe075b3 mcp-publisher_windows_amd64.tar.gz
924a0adcebd9360aed4ee5959e664e4e99b8259ed871ac91be5ace5657afba7d mcp-publisher_windows_amd64.tar.gz.sbom.json
a3eeab18ee6fd1d76d7ec153779ebe1bb404d7ea561f117781174e4396d90565 mcp-publisher_windows_arm64.tar.gz
9351f3acc39f89402ce5f72be1b45f3bf1b16f8441c97496195188f276e6c227 mcp-publisher_windows_arm64.tar.gz.sbom.json
9767b6f2afe1b60c597d4c66468d491be3466b25de8b4d4e7a21e61950856ef7 registry-1.3.3.tar.gz
a7867ff2c15905a765dc77636d86241ea04a17d0a90f086913a3cea4fe899213 registry_darwin_amd64.tar.gz
f0ed44b6ebdfcba40daefe7ca556f385de0cbae95dca3077c406f75cdb6abb9b registry_darwin_amd64.tar.gz.sbom.json
b4d93d6fc2d831d287cde79953822b434cb8406eea4f38f180628df227f6569e registry_darwin_arm64.tar.gz
dde0ed7f350eda98eb6f474d19aa0e4c717dd9f120609949acf05d7986cf6fcc registry_darwin_arm64.tar.gz.sbom.json
6772a573828f3482a192c88dac4223ee71ff4f521c8819480a194757e46a8272 registry_linux_amd64.tar.gz
ac177354d5f43f86bdf7f36738837a2792ba136198209eb61d4d4dd9ab408853 registry_linux_amd64.tar.gz.sbom.json
61c1fb9126977e0694d5aad8990a9d4c2d0e933e6aa469789559fad3865e8b7f registry_linux_arm64.tar.gz
9b81dfe2c81c49f7731072b8c217f05d609f7094ea94aaf9491f33880f2dec7f registry_linux_arm64.tar.gz.sbom.json
7fb5752de50a841614c6990f20807820bda33a72eaf7be51ae6cbd4e9acb2b87 registry_windows_amd64.tar.gz
fd44139231d4725c75b2a5a96fb670d3137f628db91c8281f6348e842792d514 registry_windows_amd64.tar.gz.sbom.json
a85ba7eb1a06a9e668b8a2097e00ab010e454a2afeb4e29ebeb7b702a31cccd3 registry_windows_arm64.tar.gz
93503683d32a2971b741e87f2879cb2a3f41acd184c618e04d17a98926915f2d 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.11",
"version": "0.1.15",
"type": "module",
"license": "MIT",
"mcpName": "dev.svelte/mcp",
@@ -9,7 +9,7 @@
"url": "https://github.com/sveltejs/mcp/issues"
},
"bin": {
"svelte-mcp": "./dist/index.js"
"svelte-mcp": "./dist/index.mjs"
},
"repository": {
"type": "git",
@@ -19,6 +19,12 @@
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/handlers.d.mts",
"import": "./dist/handlers.mjs"
}
},
"publishConfig": {
"access": "public"
},
@@ -32,15 +38,16 @@
},
"devDependencies": {
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-stdio": "catalog:mcp",
"@types/node": "^22.15.17",
"publint": "^0.3.13",
"tsdown": "^0.15.0",
"typescript": "^5.8.3",
"vitest": "^4.0.0"
"@tmcp/transport-stdio": "catalog:tmcp",
"@types/node": "catalog:tooling",
"publint": "catalog:tooling",
"tsdown": "catalog:tooling",
"typescript": "catalog:tooling",
"vitest": "catalog:tooling"
},
"dependencies": {
"eslint": "^9.36.0",
"tmcp": "catalog:mcp"
"eslint": "catalog:lint",
"sade": "catalog:tooling",
"tmcp": "catalog:tmcp"
}
}

View File

@@ -1,6 +1,7 @@
{
"$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",
"repository": {
"id": "1054419133",
@@ -8,7 +9,7 @@
"subfolder": "packages/mcp-stdio",
"source": "github"
},
"version": "0.1.11",
"version": "0.1.15",
"websiteUrl": "https://svelte.dev/docs/mcp/overview",
"icons": [
{
@@ -24,7 +25,7 @@
{
"registryType": "npm",
"identifier": "@sveltejs/mcp",
"version": "0.1.11",
"version": "0.1.15",
"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,7 +13,9 @@ 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',

1187
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,61 @@
packages:
- './packages/*'
- './apps/*'
useNodeVersion: 22.19.0
- ./packages/*
- ./apps/*
catalogs:
mcp:
tmcp: '^1.16.0'
'@tmcp/transport-http': '^0.8.0'
'@tmcp/transport-stdio': '^0.4.0'
'@tmcp/adapter-valibot': '^0.1.4'
ai:
'@anthropic-ai/sdk': ^0.71.0
'@mcp-ui/server': ^5.12.0
'@modelcontextprotocol/inspector': ^0.18.0
lint:
'@eslint/compat': ^2.0.0
'@eslint/js': ^9.36.0
'@types/eslint-scope': ^8.3.2
'@typescript-eslint/parser': ^8.44.0
'@typescript-eslint/types': ^8.44.0
eslint: ^9.36.0
eslint-config-prettier: ^10.0.1
eslint-plugin-import: ^2.32.0
eslint-plugin-pnpm: ^1.3.0
eslint-plugin-svelte: ^3.12.5
globals: ^16.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
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-check: ^4.0.0
tmcp:
'@tmcp/adapter-valibot': ^0.1.4
'@tmcp/transport-http': ^0.8.3
'@tmcp/transport-in-memory': ^0.0.5
'@tmcp/transport-stdio': ^0.4.0
tmcp: ^1.19.0
tooling:
'@changesets/cli': ^2.29.7
'@svitejs/changesets-changelog-github-compact': ^1.2.0
'@types/estree': ^1.0.8
'@types/node': ^24.3.1
'@vercel/analytics': ^1.5.0
dotenv: ^17.2.3
node-resolve-ts: ^1.0.2
publint: ^0.3.13
sade: 1.8.1
ts-blank-space: ^0.6.2
tsdown: ^0.18.0
typescript: ^5.0.0
valibot: ^1.1.0
vite: ^7.0.4
vite-plugin-devtools-json: ^1.0.0
vitest: ^4.0.0
zimmerframe: ^1.1.4
useNodeVersion: 22.19.0