Compare commits

...

83 Commits

Author SHA1 Message Date
renovate[bot]
5c0b4d41b3 chore(deps): update pnpm/action-setup action to v6 2026-07-12 13:42:08 +00:00
paoloricciuti
35b6a5a51c chore: add writing skill skill and opencode plugin skill 2026-07-10 23:49:19 +02:00
paoloricciuti
8a7763868a docs: remove confusing wording in skill docs 2026-07-06 10:07:24 +02:00
paoloricciuti
7239d7cb13 docs: add codex plugin to skills docs 2026-06-29 15:33:32 +02:00
paoloricciuti
ff78a85ddf docs: add links to plugin in setup docs 2026-06-29 15:31:16 +02:00
Paolo Ricciuti
8984bb0303 docs: codex plugin (#230) 2026-06-29 15:18:32 +02:00
Paolo Ricciuti
3fa951f1b6 docs: copilot plugin (#212) 2026-06-29 10:42:38 +02:00
github-actions[bot]
a668188dfa Version Packages (#229) 2026-06-26 18:20:51 +02:00
paoloricciuti
59dcbff136 fix: better wording for desired_svelte_version 2026-06-26 12:37:50 +02:00
prajwal
129a307df0 docs: migrate to agy (#228)
Co-authored-by: prajwal <prajwal@hopbox.in>
2026-06-24 18:16:22 +02:00
Paolo Ricciuti
d051d7ce6c chore: remove log 2026-06-12 11:12:22 +02:00
Copilot
54a84c5512 Updating Svelte version in mcp-remote app (#226)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-12 11:09:38 +02:00
Paolo Ricciuti
2e8f79038e chore: test version 2026-06-12 10:56:20 +02:00
github-actions[bot]
1b6b7589b5 Version Packages (#223) 2026-06-04 18:31:47 +02:00
Paolo Ricciuti
fea691996f chore: support declaration tags (#222) 2026-06-04 17:07:27 +02:00
github-actions[bot]
802b379f7a Version Packages (#208)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-22 12:11:32 +02:00
Paolo Ricciuti
fc391d0128 fix: remove restrictive permissions from subagent (#219) 2026-05-22 11:07:01 +02:00
Elliott Johnson
575dcc7d2b chore: Supply chain hardening (#217) 2026-05-22 08:24:32 +02:00
Elliott Johnson
88cea9e539 Revert "chore: Supply chain hardening" (#216) 2026-05-20 08:53:28 -06:00
Elliott Johnson
fdb1bc7370 chore: Supply chain hardening 2026-05-20 08:46:41 -06:00
Tee Ming
5b4d3aa68a docs: fix broken skills link (#211)
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-05-15 21:51:07 +02:00
github-actions[bot]
04c52f2b72 Version Packages (#197)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-13 20:51:10 +02:00
Paolo Ricciuti
0ea2a617ef docs: overhaul docs focus on all ai tools (#188)
Co-authored-by: jyc.dev <jycouet@gmail.com>
Co-authored-by: Tee Ming <chewteeming01@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
2026-05-13 20:50:40 +02:00
Paolo Ricciuti
484453e5f8 feat: allow stdio mcp to read the content of the file directly (#198) 2026-05-13 20:26:18 +02:00
Paolo Ricciuti
841af5b3a4 fix: handle non call expressions passed to is_rune (#201)
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 17:12:58 +02:00
Paolo Ricciuti
1ce957ac72 chore: add LICENSE 2026-04-28 14:48:50 +02:00
Paolo Ricciuti
e429cd7839 chore: remove db requirement (#196) 2026-04-20 12:50:45 +02:00
github-actions[bot]
2ded13539a Version Packages (#192)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-10 18:39:19 +02:00
paoloricciuti
96c50acae2 fix: add server export to opencode plugin 2026-04-10 18:37:49 +02:00
github-actions[bot]
4920088e5c Version Packages (#191)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-10 16:57:38 +02:00
Paolo Ricciuti
8557f0af6f fix: import ts files directly (#190) 2026-04-10 16:56:23 +02:00
renovate[bot]
972d0f0bb2 chore(deps): update dependency @vercel/analytics to v2 (#177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 10:15:32 +01:00
renovate[bot]
7bf364a9d5 chore(deps): update pnpm/action-setup action to v5 (#186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 10:14:49 +01:00
Paolo Ricciuti
d06d758b81 chore: sync agents with the opencode plugin too (#187) 2026-03-25 09:43:52 +01:00
github-actions[bot]
e5ce7437c4 Version Packages (#179)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-13 23:07:32 +01:00
Paolo Ricciuti
2f422ee190 fix: broaden checks for imported runes because LLMs are unhinged (#185) 2026-03-13 22:59:52 +01:00
paoloricciuti
14f087cd7a chore: simplify sync script/action 2026-03-13 22:22:19 +01:00
Chris Tsongas
1ef5ddf605 fix: update svelte-file-editor agent to use proper name (#183)
Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
2026-03-13 10:37:43 +01:00
github-actions[bot]
0e55ee792d chore: sync skills from svelte.dev (#178)
Co-authored-by: svelte-docs-bot[bot] <196124396+svelte-docs-bot[bot]@users.noreply.github.com>
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-03-12 18:32:21 +01:00
paoloricciuti
84ec24b6f6 fix: don't format code blocks in references 2026-03-12 18:19:43 +01:00
paoloricciuti
710cebe539 docs: opencode config link 2026-03-12 17:59:32 +01:00
paoloricciuti
b2a380c4ce docs: fix markdown blocks 2026-03-12 17:57:45 +01:00
paoloricciuti
eef0a9b4d9 docs: update opencode docs 2026-03-11 21:31:43 +01:00
Paolo Ricciuti
260b36e8af chore: undo rename for publish to registry check 2026-03-11 14:11:15 +01:00
github-actions[bot]
6df3ebe568 Version Packages (#173)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-11 13:08:21 +01:00
Momcilo Miladinovic
27a2fc5653 fix: merge user-configured svelte-file-editor agent settings (#176)
Co-authored-by: Momochiro <x@Mac.sweet.opnsense>
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-03-11 12:59:01 +01:00
Paolo Ricciuti
5cd99d8234 feat: allow enabling a specific skill in opencode plugin (#174) 2026-03-10 17:38:04 +01:00
Paolo Ricciuti
e9f19199cb feat: add sync skill docs (#172) 2026-03-09 17:49:18 +01:00
Willow (GHOST)
480b46df0a feat(opencode): mcp enabled option is passed to opencode (#171)
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-03-07 12:38:45 +01:00
jyc.dev
5f5fb27977 feat: display similar result & error at the end (#161) 2026-03-07 12:20:18 +01:00
Paolo Ricciuti
29cfa77c39 chore: restructure repo to have single source of truth for tools (#170) 2026-03-03 21:43:34 +01:00
github-actions[bot]
249923b1e5 Version Packages (#169)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 22:24:34 +01:00
Paolo Ricciuti
8c67cae90f feat: svelte-core-bestpractices skill (#162) 2026-02-27 22:19:11 +01:00
Paolo Ricciuti
4aad2e0cfe fix: rename repos in file (#168) 2026-02-27 20:01:15 +01:00
github-actions[bot]
02935b00a6 Version Packages (#164)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-22 15:05:33 +01:00
Paolo Ricciuti
1ea98adacd feat: cursor plugin (#166) 2026-02-22 15:02:31 +01:00
Paolo Ricciuti
494409cc42 fix: better subagent instructions to use MCP or skill (#163) 2026-02-11 22:57:09 +01:00
Paolo Ricciuti
46d8f6cce8 fix: only run "Publish to MCP registry" when MCP is published (#160) 2026-02-07 22:50:08 +01:00
github-actions[bot]
8dc63dca08 chore: sync skills and update documentation (#158)
Co-authored-by: paoloricciuti <26281609+paoloricciuti@users.noreply.github.com>
2026-02-06 18:41:18 +01:00
paoloricciuti
19fedcd35f docs: add instructions to default schema` 2026-02-06 18:40:55 +01:00
István Pató
b4eb5cc960 Update description for svelte-code-writer skill (#149) 2026-02-06 18:37:40 +01:00
paoloricciuti
6676fd8116 docs: add local opencode configuration 2026-02-06 18:37:03 +01:00
github-actions[bot]
a755a33a5f Version Packages (#157)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-06 17:30:56 +01:00
Paolo Ricciuti
556f96cfaf feat: allow for local opencode config (#156) 2026-02-06 17:25:33 +01:00
github-actions[bot]
77a3340c2f chore: sync AGENTS.md to opencode package and documentation (#155)
Co-authored-by: paoloricciuti <26281609+paoloricciuti@users.noreply.github.com>
2026-02-06 17:11:38 +01:00
Paolo Ricciuti
9ac8fd51e7 chore: sync AGENTS.md with opencode and docs (#154)
Co-authored-by: jyc.dev <jycouet@gmail.com>
2026-02-06 17:10:42 +01:00
paoloricciuti
c764308d79 fix: update version of claude plugin 2026-01-31 10:02:31 +01:00
paoloricciuti
01a7e6a8d3 fix: correct command for svelte language server 2026-01-31 09:52:57 +01:00
Paolo Ricciuti
d8ed686e3a chore: use @include in docs (#152) 2026-01-30 16:30:52 -05:00
github-actions[bot]
6f0390d0a9 Version Packages (#153)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-30 16:23:09 +01:00
paoloricciuti
c2c1b3e5e7 fix: actually push skills to right config path 2026-01-30 16:20:36 +01:00
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
159 changed files with 9905 additions and 2860 deletions

View File

@@ -0,0 +1,195 @@
# Glossary — Building Great Skills
The domain model for what makes a skill great. A skill exists to wrangle determinism out of a stochastic system; the root virtue is **Predictability**, and every term below is a lever on it. This is the disclosed reference for [`writing-great-skills`](SKILL.md).
The terms are grouped by axis: **Invocation** (how a skill is reached), **Information Hierarchy** (how its content is arranged), **Steering** (how the agent's runtime behaviour is shaped), and **Pruning** (how it is kept lean). Each **failure mode** lives beside the lever that cures it, tagged _failure mode_.
**Bold terms** in any definition are themselves defined in this glossary; find them by their heading.
## Predictability
The degree to which a skill makes the agent behave the same _way_ on every run — the same process, not the same output (a brainstorming skill should _predictably_ diverge; its tokens vary, its behaviour doesn't). The root virtue every other term serves — cost and maintainability are symptoms of it, not rivals.
_Avoid_: consistency, reliability, robustness, output-determinism
## Invocation
How a skill is reached — and the two loads you pay for the choice.
### Model-Invoked
A skill that keeps its **description** field, so the agent can see it and fire it autonomously — and the human can still type its name, so model-invocation always _includes_ user reach. There is no model-only state: a description only ever _adds_ agent discovery, never removes the human's. Pays a permanent **context load** on every turn in exchange for that discoverability. Reachable by other skills, because the description that makes it agent-discoverable makes it invocable. A model-invoked skill whose content is all **reference** is also one home for shared reference: another skill can invoke it, so reference needed by several skills lives in one place. Pick model-invocation only when the agent must reach the skill on its own; if it never fires except by hand, drop the description and pay no context load.
_Avoid_: ability, tool, capability
### User-Invoked
A skill with its **description** stripped — invisible to the agent and reachable only by the human typing its name (user-_only_, where **model-invoked** is user-_and-agent_). Trades agent-discoverability for zero **context load**. Because it has no description, nothing but the human can reach it: no other skill can fire it.
_Avoid_: procedure, workflow, command
### Description
The skill's machine-readable trigger, and the one **context pointer** a **model-invoked** skill is forced to keep loaded at all times. Its mere presence _is_ the invocation axis: keep it and the skill is model-invoked (and reachable by other skills); delete it and the skill is **user-invoked**, reachable only by the human. The source of a model-invoked skill's **context load**.
_Avoid_: frontmatter, summary
### Context Pointer
A reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. The **description** is the top-level context pointer (context window → skill); pointers to disclosed files are the same object one level down. Its wording, not the target, decides _when_ the agent reaches — and _how reliably_. A must-have target behind a weakly worded pointer is a variance bug: fix the wording first, and inline the material only if sharpening fails.
_Avoid_: link, reference, import
### Context Load
The cost a **model-invoked** skill imposes on the agent's context window — its **description**, always loaded, spending both tokens and attention. What **user-invoked** skills escape by having no description, and the brake on splitting into more model-invoked skills.
_Avoid_: token cost, context bloat
### Cognitive Load
The cost a **user-invoked** skill imposes on the human — what they must hold in their head: which skills exist and when to reach for each (the human is the index). What **model-invocation** removes by being agent-discoverable, and the brake on splitting into more user-invoked skills. Not a cost to minimise: it is the price of human agency, the reason some skills stay user-invoked. Spend it where human judgement matters; remove it where it does not.
_Avoid_: human index, burden, overhead
### Router Skill
A **user-invoked** skill whose job is to point at your other user-invoked skills — naming each and when to reach for it — so the human has one skill to remember instead of many. It can only hint, never fire them: user-invoked skills have no **description**, so nothing but the human can reach them. The cure for **cognitive load** when user-invoked skills multiply.
_Avoid_: dispatcher, menu, registry, index, router procedure
### Granularity
How finely you divide skills. Finer division spends one of the two loads: more **model-invoked** skills spend **context load** (more descriptions crowding the window and competing for attention); more **user-invoked** skills spend **cognitive load** (more for the human to remember and reach for). Two cuts guide the division. By **invocation**, split off a model-invoked skill where you have a distinct **leading word** to trigger it — a trigger word you actually use in your prompts. By **sequence**, split a run of **steps** where a step's **post-completion steps** need hiding, since isolating it in its own context clears what follows. Beware the reverse: merging sequences exposes each step's post-completion steps to what follows, inviting premature completion.
_Avoid_: chunking, modularity
## Information Hierarchy
How a skill's content is arranged, and how far down the ladder each piece sits.
### Information Hierarchy
A skill's content ranked by how immediately the agent needs it — a single ladder, produced by two cuts: in-file or behind a pointer, and step or reference. The rungs:
- **Steps** — in-file, primary
- **Reference**, in-file — secondary
- **Reference**, disclosed — behind a **context pointer**
A skill with no **steps** uses just the bottom two rungs — often a legitimately flat peer-set (e.g. every rule of a review on one rung), which is a fine arrangement, not a smell. The hierarchy is independent of invocation: a skill can be model- or user-invoked whether it is all steps, all reference, or both. When a skill has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip — a variance lever, not just a legibility one. Keep the top of the ladder legible; push down it whatever you can.
_Avoid_: structure, organization, layout
### Steps
The ordered actions the agent performs — when a skill has them, the primary tier of its content, and the part that earns its place in SKILL.md. Not every skill has steps: a skill can be all steps (`tdd`), all **reference** (a review), or both, independent of invocation. Every step ends on a **completion criterion**, clear or vague.
_Avoid_: workflow, instructions, choreography
### Reference
Material the agent refers to on demand — definitions, facts, parameters, examples, conditional instructions. When a skill has **steps** it is secondary to them; when a skill has none it is the entire content; or it lives outside any skill entirely — see **External Reference**. Reached via **context pointers**, and the prime candidate for **progressive disclosure**.
_Avoid_: supporting material, docs, background
### External Reference
**Reference** that lives outside the skill system — a plain file, no **description**, no **steps**, not invocable — that any skill can point at. The home for shared reference that needn't fire on its own, and the only shared home two **user-invoked** skills can use, since neither has a description and so neither can fire the other.
_Avoid_: doc, resource, knowledge base
### Progressive Disclosure
Moving **reference** down the ladder — out of SKILL.md and behind a **context pointer** — so the top stays legible. Not primarily a token optimisation; it is how the **information hierarchy** is protected. Licensed by **branching**: disclose what only some branches need, inline what every path needs, and if a pointer fires unreliably on must-have material, sharpen its wording, and pull it back inline only if that fails.
_Avoid_: lazy loading, chunking
### Co-location
Keeping the material an agent needs at once in one place — a concept's definition, rules, and caveats under a single heading, not scattered across the file — so reading one part brings its neighbours with it. The within-file companion to the **Information Hierarchy**: the hierarchy ranks _how far down_ a piece sits; co-location decides _what sits beside it_ once there. There is no formula for the right format of a body of **reference**; the test is that a skill should read like documentation written for the agent, and grouped material reads that way where scattered material does not. Distinct from **Duplication**: that repeats one meaning in two places, where scattering fragments a single meaning across many.
_Avoid_: grouping, clustering, cohesion
### Sprawl
_Failure mode._ A skill that is simply too long — too many lines in SKILL.md — independent of whether they are stale or repeated. Even an all-live, all-unique skill can sprawl. It costs readability (the agent wades through more before it can act, and attention thins across the excess), maintainability (every extra line is one more to keep **relevant**), and tokens. The cure is the **information hierarchy**: push **reference** down behind **context pointers**, and split by **branch** or sequence so each path carries only what it needs. Distinct from **sediment** (length from stale accumulation) and **duplication** (length from repeated meaning) — sprawl is length itself, whatever its cause.
_Avoid_: bloat, length, size, verbosity
## Steering
The levers that shape the agent's runtime behaviour toward **Predictability**.
### Branch
A distinct way a skill can be invoked — a case the skill handles — so different runs take different paths through it. A skill with many steps may carry many branches; a linear one has none.
_Avoid_: path, case, fork
### Leading Word
A compact concept — also called a _Leitwort_ — already living in the model's pretraining, that the agent thinks with while running the skill. It encodes a behavioural principle in the fewest possible tokens by invoking priors the model already holds (e.g. _lesson_, _proximal zone of development_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition across the skill and anchors a whole region of behaviour. Coining your own works if you define it clearly, but a made-up word recruits no priors — you pay in definition tokens what a pretrained word gives free. Reach for an existing word first.
A leading word serves **predictability** twice. In the body it anchors **execution** — the agent reaches for the same behaviour every time the concept appears, and inside flat reference it focuses attention on a class of thing to look for, recruiting the right checks each run. In the **description** it anchors **invocation** — and not only within the skill: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the skill and fires it more reliably. Word a description with the leading words you actually use when you want the skill.
_Avoid_: keyword, term, motif
### Completion Criterion
The condition that tells the agent a unit of work is done — the target it judges against. Two properties make it a lever, not just a quality. Its **clarity** (can the agent tell done from not-done?) resists **premature completion** — a vague bound ("understanding reached") lets the agent declare done and slip to the next step; this axis needs _steps_ to bite, since premature completion is a between-steps failure. Its **demand** (how much it requires) sets **legwork** — "every modified model accounted for" forces thorough work where "produce a change list" does not — and this axis is _not_ step-bound: it can bind a body of flat reference too, which is how a skill with no steps still carries an exhaustiveness bar ("every rule applied"). The strongest criteria are both checkable and exhaustive.
_Avoid_: done condition, exit condition, stopping rule
### Legwork
The work an agent does behind the scenes within a single step — reading files, exploring the codebase, making changes, digging up what it needs rather than offloading to the user. It lives below the step structure: never written as its own step, latent in the wording, controlled by the agent rather than the skill. The within-step counterpart to **post-completion steps**' across-step pull. Raised by a **leading word** (_comprehensive_, _thorough_) or a **completion criterion** that demands the work be exhaustive — including the demand axis applied to flat reference, which is what drives a skill of flat reference to cover all its rungs. Goes thin either when that demand is missing or when **premature completion** cuts the step short.
_Avoid_: scope, effort, diligence, coverage
### Post-Completion Steps
The **steps** that follow the current step. Visible, they pull the agent forward into **premature completion** — the more it sees, the stronger the tug; the defence is to hide them by splitting the sequence of steps into two.
_Avoid_: horizon, fog of war, lookahead
### Premature Completion
_Failure mode._ Ending the current step before it is genuinely done, because the agent's attention slips to being done rather than to the work. A between-steps failure: it needs **steps** to occur — a skill with no steps that quits early isn't premature completion but thin **legwork** under an unmet demand. A tug-of-war between two forces: visible **post-completion steps** (the pull forward) and the **completion criterion**'s clarity (the resistance — a sharp, checkable bar holds; a vague one gives way). Fuzziness is the necessary condition: a sharp bound resists the pull no matter how many later steps are visible, so a step that never rushes needs no defending. Two levers hold a step that does, but reach for them in order: **sharpen the bound first** — it is local and cheap. Only when the criterion is irreducibly fuzzy _and_ you actually observe the rush do you **hide the later steps** — and hiding only works across a real context boundary (a user-invoked hand-off or a subagent dispatch; an inline model-invoked call leaves the later steps in context and clears nothing). One cause of thin legwork, but distinct from it: legwork can be thin even when a step runs to full completion.
_Avoid_: premature closure, the rush, rushing, shortcutting
## Pruning
Keeping a skill lean — each remedy paired with the failure it cures.
### Single Source of Truth
The desired state where each meaning lives in exactly one authoritative place, so a change to the skill's behaviour is a change in one place. **Duplication** is its violation.
_Avoid_: home, canonical location
### Duplication
_Failure mode._ The same meaning given more than one **single source of truth**. It costs maintenance (change one place, you must change the others), costs tokens, and inflates prominence — repeating a meaning weights it on the ladder past its real rank. The accidental inverse of a **leading word**, which raises attention on purpose by repeating a token, never the meaning.
_Avoid_: repetition, redundancy
### Relevance
Whether a line still bears on what the skill does — the lens for what to keep. A line loses relevance either by never bearing on the task (mere exposition, or a **branch** that should be disclosed) or by going stale: drifting out of date as the behaviour or world it describes changes. Shorter skills are easier to keep relevant, because each line is cheaper to check. Distinct from **no-op**: relevance asks whether a line bears on the task, not whether it changes behaviour.
_Avoid_: load-bearing, staleness, freshness
### Sediment
_Failure mode._ Layers of old content that settle in a skill and are never cleared, because adding feels safe and removing feels risky — so stale and irrelevant lines accumulate and you must core down through them to find what is still live. The default fate of any skill without a pruning discipline; the slow erosion of **relevance**, as opposed to **duplication**'s repeated meaning.
_Avoid_: accretion, bloat, cruft, rot
### No-Op
_Failure mode._ An instruction that changes nothing because the model already does it by default — you pay load to tell the agent what it would do anyway. The test: does a line change behaviour versus the default? A line can be perfectly **relevant** and still be a no-op. The same priors that make a **leading word** free make a no-op worthless.
A leading word is a _technique_; No-Op is a _verdict_ on a line — and they cross. A leading word too weak to beat the default is a no-op (_be thorough_ when the agent is already thorough-ish), and the fix is a stronger word that passes the verdict (_relentless_), not a different technique. So the No-Op test — does it change behaviour versus the default? — is also how you grade whether a leading word is earning its repetitions. This is model-relative, not reader-relative: two people disagreeing over whether a line is a no-op disagree about the default, and settle it by running the skill, not by debate.
_Avoid_: redundant instruction, restating the obvious, belaboring

View File

@@ -0,0 +1,84 @@
---
name: writing-great-skills
description: Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
disable-model-invocation: true
metadata:
internal: true
---
A skill exists to wrangle determinism out of a stochastic system. **Predictability** — the agent taking the same _process_ every run, not producing the same output — is the root virtue; every lever below serves it.
**Bold terms** are defined in [`GLOSSARY.md`](GLOSSARY.md); look them up there for the full meaning.
## Invocation
Two choices, trading different costs:
- A **model-invoked** skill keeps a **description**, so the agent can fire it autonomously _and_ other skills can reach it (you can still type its name too). It contributes to **context load** — the description sits in the window every turn. Mechanics: omit `disable-model-invocation`, and write a model-facing description with rich trigger phrasing ("Use when the user wants…, mentions…").
- A **user-invoked** skill strips the description from the agent's reach: only you, typing its name, can invoke it — and no other skill can. Zero context load, but it spends **cognitive load**: _you_ are the index that must remember it exists. Mechanics: set `disable-model-invocation: true`; the `description` becomes human-facing — a one-line summary, trigger lists stripped.
Pick model-invocation only when the agent must reach the skill on its own, or another skill must. If it only ever fires by hand, make it user-invoked and pay no context load.
When user-invoked skills multiply past what you can remember, that piled-up cognitive load is cured by a **router skill**: one user-invoked skill that names the others and when to reach for each.
## Writing the description
A model-invoked **description** does two jobs — state what the skill is, and list the **branches** that should trigger it. Every word increases **context load**, so a description earns even harder pruning than the body:
- **Front-load the skill's leading word** — the description is where it does its invocation work.
- **One trigger per branch.** Synonyms that rename a single branch are **duplication** — "build features using TDD … asks for test-first development" is one branch written twice. Collapse them; keep only genuinely distinct branches.
- **Cut identity that's already in the body.** Keep the description to triggers, plus any "when another skill needs…" reach clause.
## Information hierarchy
A skill is built from two content types — **steps** and **reference** — that mix freely: a skill can be all steps, all reference, or both. The core decision is which to use and where each sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material:
1. **In-skill step** — an ordered action in `SKILL.md`, the primary tier: what the agent does, in order. Each step ends on a **completion criterion**, the condition that tells the agent the work is done. Make it _checkable_ (can the agent tell done from not-done?) and, where it matters, _exhaustive_ ("every modified model accounted for", not "produce a change list") — a vague criterion invites **premature completion**.
2. **In-skill reference** — a definition, rule, or fact in `SKILL.md`, consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung) — a fine arrangement, not a smell. _This skill is all reference._
3. **External reference** — reference pushed out of `SKILL.md` into a separate file, reached by a **context pointer**, loaded only when the pointer fires. (Spans _disclosed_ reference — a sibling file like `GLOSSARY.md`, still part of the skill — through fully **external reference** that lives outside the skill system and any skill can point at.)
A demanding completion criterion drives thorough **legwork** — the digging the agent does within the work — whether the skill has steps or not, since "every rule applied" binds flat reference just as "every step done" binds a sequence.
Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision.
**Progressive disclosure** is the move down the ladder — out of `SKILL.md` into a linked file — so the top stays legible. Mechanics: a linked `.md` file in the skill folder, named for what it holds (this skill discloses its full definitions to `GLOSSARY.md`). Some skills are used in more than one way, and each distinct way is a **branch** — different runs taking different paths through the skill. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. A **context pointer**'s _wording_, not its target, decides when and how reliably the agent reaches the material.
Where the ladder decides _how far down_ a piece sits, **co-location** decides _what sits beside it_ once there: keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it.
## When to split
**Granularity** is how finely you divide skills, and each cut spends one of the two loads, so split only when the cut earns it. Two cuts:
- **By invocation** — split off a **model-invoked** skill when you have a distinct **leading word** that should trigger it on its own, or another skill must reach it. You pay **context load** for the new always-loaded **description**, so that independent reach has to be worth it.
- **By sequence** — split a run of **steps** when the steps still ahead (a step's **post-completion steps**) tempt the agent to rush the one in front of it (**premature completion**). Keeping them out of view encourages the agent to do more **legwork** on the current task.
## Pruning
Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit.
Check every line for **relevance**: does it still bear on what the skill does?
Then hunt **no-ops** sentence by sentence, not just line by line: run the no-op test on each sentence in isolation, and when one fails, delete the whole sentence rather than trim words from it. Be aggressive — most prose that fails should go, not be rewritten.
## Leading words
A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the skill (e.g. _lesson_, _fog of war_, _tracer bullets_). Repeated throughout the text (though not necessarily - a strong leading word might only be needed once), it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds.
It serves predictability twice. In the body it anchors _execution_: the agent reaches for the same behaviour every time the word appears. In the description it anchors _invocation_: when the same word lives in your prompts, docs, and code, the agent links that shared language to the skill and fires it more reliably.
Hunt for opportunities to refactor skills to use leading words. A triad spelled out at three sites (**duplication**), a description spending a sentence to gesture at one idea — each is a passage begging to **collapse** into a single token. Examples include:
- "fast, deterministic, low-overhead" -> _tight_ — one quality restated across a phase — into a single pretrained word (a _tight_ loop).
- "a loop you believe in" -> _red_ — converts a fuzzy gate into a binary observable state (the loop goes _red_ on the bug, or it doesn't).
You win twice over: fewer tokens, _and_ a sharper hook for the agent to hang its thinking on. Assume every skill is carrying restatements that leading words retire — go find them.
## Failure modes
Use these to diagnose issues the user may be having with the skill.
- **Premature completion** — ending a step before it's genuinely done, attention slipping to _being done_. Defence, in order: sharpen the completion criterion first (cheap, local); only if it is irreducibly fuzzy _and_ you observe the rush, hide the post-completion steps by splitting (the sequence cut).
- **Duplication** — the same meaning in more than one place. Costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank.
- **Sediment** — stale layers that settle because adding feels safe and removing feels risky. The default fate of any skill without a pruning discipline.
- **Sprawl** — a skill simply too long, even when every line is live and unique. Hurts readability and maintainability and wastes tokens. The cure is the ladder: disclose **reference** behind pointers, and split by **branch** or sequence so each path carries only what it needs.
- **No-op** — a line the model already obeys by default, so you pay load to say nothing. The test: does it change behaviour versus the default? A weak leading word (_be thorough_ when the agent is already thorough-ish) is a no-op; the fix is a stronger word (_relentless_), not a different technique.

View File

@@ -0,0 +1,126 @@
---
name: writing-opencode-plugins
description: OpenCode plugins, @opencode-ai/plugin, @opencode-ai/plugin/tui, plugin hooks, custom tools, TUI routes, slots, keymaps, and packaging. Use when creating, editing, reviewing, testing, or publishing server or TUI plugins for OpenCode.
metadata:
internal: true
---
# Writing OpenCode Plugins
Use this skill to implement production-quality OpenCode plugins. Treat the repository's exported types and runtime as authoritative because plugin APIs are evolving and public docs may lag.
## Start Here
1. Decide which runtime owns the feature.
2. Read the relevant public type before writing code.
3. Find one focused in-repository example using the same API.
4. Implement the smallest target-specific module.
5. Test loading, behavior, failure, and cleanup in the owning package.
| Need | Plugin target | Import | Configuration |
| -------------------------------------------------------------------- | --------------------------- | ------------------------------ | ---------------------------------------------------------------- |
| Hooks, tools, auth, providers, model parameters, shell environment | Server | `@opencode-ai/plugin` | `opencode.json` or auto-discovered `.opencode/plugins/*.{ts,js}` |
| Commands, keybindings, routes, dialogs, slots, themes, notifications | TUI | `@opencode-ai/plugin/tui` | Explicit `tui.json` `plugin` entry |
| Both | Two target-only entrypoints | Both imports in separate files | Package exports `./server` and `./tui` |
Never export `server` and `tui` from the same module. Do not use server event hooks as a substitute for interactive TUI APIs.
## Verify The Current Contract
Read these files before implementing unfamiliar behavior:
- `packages/plugin/src/index.ts`: authoritative server plugin and hook types.
- `packages/plugin/src/tool.ts`: custom tool schema, context, permission, metadata, attachments, and result types.
- `packages/plugin/src/tui.ts`: authoritative TUI API and module types.
- `packages/opencode/specs/tui-plugins.md`: TUI loading, packaging, lifecycle, and API semantics.
- `packages/opencode/src/plugin/shared.ts`: target validation, IDs, and entrypoint resolution.
- `packages/opencode/src/plugin/loader.ts`: install, compatibility, and import behavior.
If these disagree with examples or website docs, follow exported types and runtime behavior, then update stale documentation when appropriate.
## Choose A Module Shape
Prefer the explicit module object for new server plugins:
```ts
import type { Plugin, PluginModule } from '@opencode-ai/plugin';
const server: Plugin = async ({ client, directory }, options) => ({
dispose: async () => {},
});
export default {
id: 'acme.example',
server,
} satisfies PluginModule & { id: string };
```
Legacy server-only local plugins may export a plugin function directly. In a legacy module every distinct named export is interpreted as a plugin, so do not export unrelated constants. Prefer a default module object for new code.
TUI plugins always use a default module object:
```tsx
/** @jsxImportSource @opentui/solid */
import type { TuiPlugin, TuiPluginModule } from '@opencode-ai/plugin/tui';
const tui: TuiPlugin = async (api) => {
api.ui.toast({ message: 'Plugin loaded' });
};
export default {
id: 'acme.example-tui',
tui,
} satisfies TuiPluginModule & { id: string };
```
File plugins require a stable, non-empty `id`. npm plugins may derive the ID from the package name, but an explicit namespaced ID makes state, diagnostics, and collision handling clearer.
## Engineering Rules
- Use TypeScript and `satisfies` against the public plugin type.
- Parse and validate `options`; they arrive as unvalidated `Record<string, unknown>`.
- Namespace plugin IDs, command IDs, route names, modes, slot names, and shared KV keys.
- Use the directory supplied by the plugin or tool context, not `process.cwd()`.
- Honor `AbortSignal` for long-running or cancellable work.
- Use `client.app.log()` for structured server logging instead of `console.log`.
- Request permission before sensitive or consequential custom-tool work.
- Keep notifications privacy-safe; do not expose prompts, secrets, paths, commands, or raw errors.
- Register only needed hooks and UI resources. Avoid broad event subscriptions when a specific hook exists.
- Make cleanup bounded, idempotent, and safe after partial initialization.
- Do not depend on undocumented load order to resolve ownership conflicts.
## Testing Workflow
Server plugin tests belong under `packages/opencode/test/plugin/` or the closest owning subsystem. TUI runtime tests belong under `packages/opencode/test/cli/tui/`; component-level TUI tests may belong in `packages/tui`.
Test at least:
- valid loading and target/entrypoint selection;
- configured options and malformed options;
- the observable behavior, not a duplicate of implementation logic;
- abort, failure, and partial-initialization behavior;
- cleanup or disposal;
- duplicate IDs or registrations when relevant;
- local file and npm packaging behavior when publishing.
Run tests from the package directory, never the repository root. Use `bun typecheck` from the owning package for type checking.
## Review Checklist
- The feature is in the correct server or TUI runtime.
- Module shape and import path match the target.
- Server and TUI entrypoints are separate.
- IDs and persistent keys are stable and namespaced.
- Options and external data are validated.
- Hook output mutation preserves other plugins' changes.
- Tools use context directory, permission, metadata, and abort correctly.
- TUI keybindings are mode-gated unless intentionally global.
- TUI resources and custom side effects are disposed.
- Package exports, `engines.opencode`, and config target are correct.
- Tests cover behavior and lifecycle.
## References
- [Server plugins](references/server-plugins.md): hooks, custom tools, lifecycle, and examples.
- [TUI plugins](references/tui-plugins.md): keymaps, routes, dialogs, slots, state, and lifecycle.
- [Packaging and testing](references/packaging-testing.md): config, package exports, compatibility, and test locations.

View File

@@ -0,0 +1,129 @@
# Packaging And Testing
## Local Configuration
Server plugin in `opencode.json`:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["./plugins/server.ts", ["package-name", { "key": "value" }]]
}
```
Server files under `.opencode/plugin/` or `.opencode/plugins/` are also auto-discovered. Relative configured paths resolve from the config file that declared them.
TUI plugin in `tui.json`:
```json
{
"$schema": "https://opencode.ai/tui.json",
"plugin": [["./plugins/tui.tsx", { "key": "value" }]],
"plugin_enabled": {
"acme.demo": true
}
}
```
`plugin_enabled` uses the resolved plugin ID, not its package or file spec. Persisted runtime enablement can override config.
After editing plugin or config-time files, restart OpenCode; the running session keeps its loaded configuration and modules.
## npm Package Shape
Publish separate target-only entrypoints:
```json
{
"name": "@acme/opencode-plugin",
"type": "module",
"exports": {
"./server": {
"import": "./dist/server.js",
"config": { "serverOption": true }
},
"./tui": {
"import": "./dist/tui.js",
"config": { "tuiOption": true }
}
},
"engines": {
"opencode": "^1.0.0"
},
"peerDependencies": {
"@opencode-ai/plugin": "^1.0.0"
}
}
```
- Server resolution prefers `exports["./server"]` and may fall back to `main`.
- TUI resolution requires `exports["./tui"]`; it does not use `main`.
- A package supporting both targets needs separate source and output files.
- `exports[target].config` may provide default options written during first install.
- Use `engines.opencode` to declare tested compatibility.
- npm compatibility is checked; local file plugins bypass the engine check.
- Pin package versions when reproducibility matters.
- Plugin package install runs with lifecycle scripts disabled, so do not require `postinstall`.
- Keep resolved entrypoints and theme paths inside the package directory.
Theme-only TUI packages may use `oc-themes`; consult `packages/opencode/specs/tui-plugins.md` for path and synchronization rules.
## Resolution And Identity
- npm declarations deduplicate by package name; higher-precedence/later declarations win.
- File server and TUI specs have target-specific resolution behavior.
- External modules may resolve/import in parallel, but activate sequentially.
- IDs must not collide with built-ins or other loaded plugins.
- Dynamic import failures are effectively permanent for the current process because Bun caches them.
- `--pure` or `OPENCODE_PURE` skips external plugins.
Read `packages/opencode/src/plugin/shared.ts`, `loader.ts`, and `install.ts` before changing packaging behavior.
## Test Locations
Server plugin coverage:
- `packages/opencode/test/plugin/trigger.test.ts`: hook sequencing and failures.
- `packages/opencode/test/plugin/loader-shared.test.ts`: resolution and module validation.
- `packages/opencode/test/plugin/shared.test.ts`: shared target rules.
- `packages/opencode/test/plugin/install.test.ts`: package install and config patching.
- `packages/opencode/test/plugin/install-concurrency.test.ts`: concurrent writes.
- `packages/opencode/test/plugin/auth-override.test.ts`: auth precedence.
- `packages/opencode/test/tool/registry.test.ts`: schemas, results, and attachments.
TUI plugin coverage:
- `packages/opencode/test/cli/tui/plugin-loader.test.ts`: loading and ordering.
- `packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts`: target entrypoints.
- `packages/opencode/test/cli/tui/plugin-lifecycle.test.ts`: rollback and cleanup.
- `packages/opencode/test/cli/tui/plugin-toggle.test.ts`: persisted enablement.
- `packages/opencode/test/cli/tui/plugin-add.test.ts`: runtime addition.
- `packages/opencode/test/cli/tui/plugin-install.test.ts`: installation.
- `packages/opencode/test/cli/tui/plugin-loader-pure.test.ts`: pure mode.
Use fixture helpers under `packages/opencode/test/fixture/` rather than reimplementing the loader in tests.
## Verification Commands
Run from the owning package, never the repository root:
```sh
cd packages/opencode
bun typecheck
bun test test/plugin/trigger.test.ts
bun test test/cli/tui/plugin-lifecycle.test.ts
```
Select the smallest relevant tests first, then broader plugin suites. For interactive TUI verification, follow `packages/opencode/AGENTS.md`: run `bun dev` in detached `tmux`, capture output, and explicitly stop the session.
## Publishing Checklist
- Build output is ESM-compatible and contains no source-only path aliases.
- Every advertised target has the correct package export.
- Each target module exports only its own target shape.
- Peer/runtime dependencies are classified correctly.
- `engines.opencode` matches tested versions.
- Default options are backward-compatible and validated at runtime.
- Local file, pinned npm, and bare npm specs have been considered.
- Loading, failure, cleanup, and upgrade behavior are tested.
- README examples match the exported API and config target.

View File

@@ -0,0 +1,122 @@
# Server Plugins
## Contract And Lifecycle
The public contract is `packages/plugin/src/index.ts`:
```ts
type Plugin = (input: PluginInput, options?: Record<string, unknown>) => Promise<Hooks>
```
`PluginInput` provides the SDK `client`, `project`, `directory`, `worktree`, `serverUrl`, Bun shell `$`, and experimental workspace registration.
The server runtime is `packages/opencode/src/plugin/index.ts`.
- Built-in plugins initialize before external plugins.
- External modules may resolve concurrently, but activation is sequential for deterministic hook order.
- `config` hooks run sequentially against the mutable merged config.
- `event` subscribes to location-filtered events and is fire-and-forget.
- Ordinary hooks run sequentially and share a mutable output object.
- Ordinary hook failures propagate and stop later hooks for that trigger.
- Initialization, config, and disposal failures are isolated and logged by the host.
- `dispose` runs when the per-directory plugin scope closes.
Mutate hook output in place. Preserve values contributed by earlier plugins: append arrays, merge maps, and change only fields the plugin owns.
## Hook Selection
Use the narrowest hook that expresses the behavior:
| Goal | Hook |
| ---------------------------------- | --------------------------------- |
| Observe SDK events | `event` |
| Modify merged configuration | `config` |
| Add tools | `tool` |
| Add provider authentication | `auth` |
| Add or change provider models | `provider` |
| Modify incoming user message | `chat.message` |
| Modify LLM parameters or headers | `chat.params`, `chat.headers` |
| Modify command parts | `command.execute.before` |
| Validate or rewrite tool arguments | `tool.execute.before` |
| Transform tool presentation/result | `tool.execute.after` |
| Modify model-facing tool schemas | `tool.definition` |
| Add shell environment variables | `shell.env` |
| Influence permission decisions | `permission.ask` |
| Customize compaction | `experimental.session.compacting` |
Read the complete `Hooks` interface before using experimental hooks.
## Custom Tools
Use `tool()` and Zod schemas from `tool.schema`:
```ts
import { type Plugin, tool } from "@opencode-ai/plugin"
export default (async () => ({
tool: {
lookup_issue: tool({
description: "Look up one issue by numeric ID",
args: {
id: tool.schema.number().int().positive().describe("Issue ID"),
},
async execute(args, context) {
await context.ask({
permission: "lookup_issue",
patterns: [String(args.id)],
always: ["*"],
metadata: { id: args.id },
})
context.metadata({ title: `Issue ${args.id}` })
return {
title: `Issue ${args.id}`,
output: "Result",
metadata: { id: args.id },
}
},
}),
},
})) satisfies Plugin
```
Tool rules:
- Write descriptions for the model, including when to use the tool and important constraints.
- Describe arguments individually and constrain them in the schema.
- Use `context.directory` and `context.worktree` for path resolution.
- Pass `context.abort` into cancellable I/O.
- Call `context.ask()` before performing work covered by a permission boundary.
- Use `context.metadata()` for in-progress presentation; return final metadata in the result.
- Return attachments only as declared file attachments with a MIME type and URL.
- Keep output useful and bounded. The host may truncate large results and add truncation metadata.
Plugin tools with built-in IDs take precedence, but overriding built-ins should be explicit and tested.
## Auth And Providers
Use existing built-ins as references rather than inventing OAuth behavior:
- `packages/opencode/src/plugin/azure.ts`: simple API-key prompt.
- `packages/opencode/src/plugin/xai.ts`: OAuth, refresh, and custom fetch behavior.
- `packages/opencode/src/plugin/openai/codex.ts`: auth plus chat parameter hooks.
- `packages/opencode/src/plugin/github-copilot/copilot.ts`: full auth/provider integration.
Do not log credentials, tokens, authorization codes, provider headers, or raw auth responses. Preserve provider identity and refresh semantics defined by `AuthHook`.
## Useful Examples
- `.opencode/plugins/model-task.ts`: custom subagent tool with permission, abort, metadata, and SDK calls when present in the worktree.
- `packages/plugin/src/example.ts`: minimal package example.
- `packages/opencode/test/fixture/agent-plugin.ts`: config mutation fixture.
- `packages/opencode/src/plugin/*.ts`: built-in auth/provider implementations.
## Common Failures
- Exporting constants beside legacy plugin functions: every exported value may be treated as a plugin.
- Using `process.cwd()` in a multi-directory process.
- Replacing a shared output map or array and deleting earlier plugin contributions.
- Forgetting that `event` is not awaited like ordinary hooks.
- Assuming thrown hook errors are isolated.
- Installing a missing dependency after a dynamic import failed and expecting the same process to recover; Bun caches failed imports.
- Trusting options without validation.

View File

@@ -0,0 +1,130 @@
# TUI Plugins
## Contract And Loading
The public contract is `packages/plugin/src/tui.ts`; technical behavior is documented in `packages/opencode/specs/tui-plugins.md`.
```ts
type TuiPlugin = (api: TuiPluginApi, options: Record<string, unknown> | undefined, meta: TuiPluginMeta) => Promise<void>
```
- Import from `@opencode-ai/plugin/tui`.
- Export one default `{ id?, tui }` object. Named exports are ignored by the loader.
- File plugins require an explicit non-empty ID.
- Configure TUI plugins explicitly in `tui.json`; there is no directory auto-discovery.
- JSX uses OpenTUI Solid, normally with `/** @jsxImportSource @opentui/solid */`.
- TUI packages resolve only `exports["./tui"]`; they do not fall back to package `main` or root exports.
## API Routing
| Need | API |
| -------------------------------- | ------------------------------------------ |
| Commands and shortcuts | `api.keymap.registerLayer(...)` |
| Temporary input context | `api.mode.push(...)` |
| Full-screen UI | `api.route.register(...)`, `navigate(...)` |
| Host dialogs and toast | `api.ui.dialog`, `Dialog*`, `toast(...)` |
| Reuse the host prompt | `api.ui.Prompt` |
| Inject host UI | `api.slots.register(...)` |
| Theme tokens and switching | `api.theme` |
| Synced sessions/providers/status | `api.state` |
| SDK operations | `api.client` |
| TUI event stream | `api.event.on(...)` |
| Persistent shared values | `api.kv` |
| Host-mediated notification/sound | `api.attention` |
| Extra cleanup | `api.lifecycle.onDispose(...)` |
Do not use deprecated `api.command` in new plugins. Register commands and bindings through keymap layers.
## Commands And Modes
```tsx
api.keymap.registerLayer({
mode: "base",
commands: [
{
name: "acme.demo.open",
title: "Open demo",
category: "Plugin",
namespace: "palette",
slashName: "demo",
run() {
api.route.navigate("acme.demo")
},
},
],
bindings: [{ key: "ctrl+shift+m", cmd: "acme.demo.open", desc: "Open demo" }],
})
```
Built-in modes are `base`, `modal`, and `autocomplete`. A layer without `mode` remains active across dialogs and autocomplete, so omit mode only for intentionally global behavior.
For plugin-owned full-screen interaction, push a namespaced mode inside the component and dispose it with Solid cleanup:
```tsx
import { onCleanup } from "solid-js"
const pop_mode = api.mode.push("acme.demo")
onCleanup(pop_mode)
```
## Routes, Dialogs, And Slots
- Reserve `home` and `session` for host routes.
- Namespace route names; duplicate routes are last-registration-wins.
- Unknown routes render fallback UI rather than throwing.
- Use host dialog components for standard interactions and `api.ui.dialog.replace()` for custom dialog content.
- Use route params for serializable navigation state; keep component-local transient state in Solid primitives when appropriate.
- `api.slots.register(...)` returns an assigned ID, not an unregister function.
- Slot registration and other host API resources are scope-tracked automatically.
- Read current slot names and props from `TuiHostSlotMap`, not copied lists.
## State And Persistence
- `api.tuiConfig` and `api.state` are live views, not initialization snapshots.
- `api.kv` is shared by all plugins. Prefix every key with the plugin ID.
- Check readiness where the API exposes it.
- Persist only user preferences or durable plugin state, not derived host state.
- Runtime enablement in KV overrides `tui.json` on startup.
`meta.state` is `first`, `updated`, or `same`. Use it for bounded migration or asset synchronization, not normal rendering behavior.
## Lifecycle
The host automatically scope-tracks commands, keymap resources, routes, event subscriptions, slots, pushed modes, and sound packs.
- `api.lifecycle.signal` aborts before cleanup begins.
- Use `api.lifecycle.onDispose()` for timers, sockets, file watchers, workers, or other plugin-owned resources.
- Initialization failure rolls back tracked resources and does not prevent later plugins from loading.
- Cleanup is reverse-order, awaited, idempotent, and constrained by a total five-second budget.
- Keep cleanup fast and independently safe after partial initialization.
## UI Quality
- Use `api.theme.current` tokens instead of hard-coded colors.
- Use `api.keys` to display shortcuts according to host formatting.
- Make routes responsive to terminal dimensions and usable with keyboard-only input.
- Avoid taking over global shortcuts without a strong reason.
- Prefer host dialogs, prompts, and slots over visually inconsistent reimplementations.
- Send attention through `api.attention.notify()` so the host owns focus, notification, and sound policy.
- Keep notification text privacy-safe.
## Useful Examples
- `.opencode/plugins/tui-smoke.tsx`: broad API smoke implementation.
- `packages/tui/src/feature-plugins/system/which-key.tsx`: focused keymap UI.
- `packages/tui/src/feature-plugins/system/notifications.ts`: attention behavior.
- `packages/tui/src/feature-plugins/system/diff-viewer.tsx`: route/UI integration.
- `packages/tui/src/feature-plugins/home/tips.tsx`: host slot usage.
- `packages/tui/src/feature-plugins/sidebar/context.tsx`: sidebar extension.
## Common Failures
- Expecting `.opencode/plugins` auto-discovery for TUI modules.
- Exporting `{ server, tui }` from one module.
- Omitting the default export, or relying on named exports.
- Omitting an ID for a file plugin.
- Registering an ungated keymap layer accidentally active in modal/autocomplete modes.
- Treating KV as plugin-private.
- Treating `slots.register()` as returning a disposer.
- Expecting `plugins.install()` to activate a plugin; installation and runtime addition are separate.
- Leaking timers or network resources because host tracking only covers host registrations.

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/mcp" }],
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/ai-tools" }],
"commit": false,
"fixed": [],
"linked": [],

View File

@@ -6,11 +6,11 @@
"plugins": [
{
"name": "svelte",
"source": "./plugins/svelte",
"source": "./plugins/claude/svelte",
"description": "A plugin for all things Svelte development, MCP, skills, and more.",
"lspServers": {
"svelte": {
"command": "svelte-language-server",
"command": "svelteserver",
"args": ["--stdio"],
"extensionToLanguage": {
".svelte": "svelte"

View File

@@ -0,0 +1,13 @@
{
"name": "svelte",
"owner": {
"name": "Svelte"
},
"plugins": [
{
"name": "svelte",
"source": "./plugins/cursor/svelte",
"description": "A plugin for all things Svelte development, MCP, skills, and more."
}
]
}

View File

@@ -13,12 +13,10 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.27.0
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
@@ -32,6 +30,4 @@ jobs:
- name: Run type check
run: pnpm run check
env:
DATABASE_URL: file:test.db
DATABASE_TOKEN: dummy-key
VOYAGE_API_KEY: dummy-key

View File

@@ -13,12 +13,10 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.27.0
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
@@ -32,6 +30,4 @@ jobs:
- name: Run linting
run: pnpm run lint
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key

View File

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

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Publish to MCP Registry
working-directory: packages/mcp-stdio

View File

@@ -1,26 +1,47 @@
name: Release Svelte Code Writer Skill
name: Release Svelte Skills
on:
push:
branches:
- main
paths:
- 'plugins/svelte/skills/svelte-code-writer/**'
- 'tools/skills/**'
workflow_dispatch:
permissions: {}
jobs:
release:
detect-skills:
permissions:
contents: write
# prevents this action from running on forks
if: github.repository == 'sveltejs/mcp'
name: Release Svelte Code Writer Skill
contents: read
if: github.repository == 'sveltejs/ai-tools'
name: Detect changed skills
runs-on: ubuntu-latest
outputs:
skills: ${{ steps.find-skills.outputs.skills }}
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Find all skills
id: find-skills
run: |
skills=$(ls -d tools/skills/*/ | xargs -I {} basename {} | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "skills=$skills" >> $GITHUB_OUTPUT
release:
needs: detect-skills
if: needs.detect-skills.outputs.skills != '[]'
permissions:
contents: write
name: Release ${{ matrix.skill }}
runs-on: ubuntu-latest
strategy:
matrix:
skill: ${{ fromJson(needs.detect-skills.outputs.skills) }}
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Get version from date
id: version
@@ -28,18 +49,18 @@ jobs:
- name: Create zip
run: |
cd plugins/svelte/skills
zip -r svelte-code-writer.zip svelte-code-writer/
cd tools/skills
zip -r ${{ matrix.skill }}.zip ${{ matrix.skill }}/
- name: Create Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: svelte-code-writer-v${{ steps.version.outputs.version }}
name: Svelte Code Writer Skill v${{ steps.version.outputs.version }}
tag_name: ${{ matrix.skill }}-v${{ steps.version.outputs.version }}
name: ${{ matrix.skill }} v${{ steps.version.outputs.version }}
body: |
Automated release of the Svelte Code Writer skill.
Automated release of the ${{ matrix.skill }} skill.
This release was triggered by changes to the `plugins/svelte/skills/svelte-code-writer/` directory.
files: plugins/svelte/skills/svelte-code-writer.zip
This release was triggered by changes to the `tools/skills/${{ matrix.skill }}/` directory.
files: tools/skills/${{ matrix.skill }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -13,11 +13,11 @@ jobs:
id-token: write # OpenID Connect token needed for provenance
pull-requests: write # to create pull request (changesets/action)
# prevents this action from running on forks
if: github.repository == 'sveltejs/mcp'
if: github.repository == 'sveltejs/ai-tools'
name: Release
runs-on: ${{ matrix.os }}
outputs:
published: ${{ steps.changesets.outputs.published }}
publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
strategy:
matrix:
# pseudo-matrix for convenience, NEVER use more than a single combination
@@ -25,7 +25,7 @@ jobs:
os: [ubuntu-latest]
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
@@ -52,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
@@ -64,7 +64,7 @@ jobs:
publish-mcp:
needs: release
if: needs.release.outputs.published == 'true'
if: contains(needs.release.outputs.publishedPackages, '"@sveltejs/mcp"')
uses: ./.github/workflows/publish-mcp.yml
secrets:
MCP_KEY: ${{ secrets.MCP_KEY }}

116
.github/workflows/sync-docs-skills.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: Sync Skills
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
actions: write
jobs:
sync-skills:
if: github.repository == 'sveltejs/ai-tools'
name: Sync skills from svelte.dev
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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: Clone svelte.dev
run: git clone --depth 2 https://github.com/sveltejs/svelte.dev.git "${{ runner.temp }}/svelte.dev"
- name: Discover changed skill files
id: discover
env:
SVELTE_DEV_ROOT: ${{ runner.temp }}/svelte.dev
run: |
skill_files=$(git -C "$SVELTE_DEV_ROOT" diff --name-only --diff-filter=ACMR HEAD~1 HEAD | grep '^apps/svelte.dev/content/docs/.*\.md$' | xargs -I{} grep -l '^skill: *true' "$SVELTE_DEV_ROOT/{}" || true)
echo "skill_files=$skill_files" >> "$GITHUB_OUTPUT"
- name: Sync skills
if: steps.discover.outputs.skill_files != ''
env:
SVELTE_DEV_ROOT: ${{ runner.temp }}/svelte.dev
DOCS_PREFIX: apps/svelte.dev/content/docs/
run: |
for full_path in ${{ steps.discover.outputs.skill_files }}; do
file="${full_path#$SVELTE_DEV_ROOT/}"
name=$(grep '^name: ' "$full_path" | head -1 | sed 's/^name: *//')
repo="${file#$DOCS_PREFIX}"
repo="${repo#/}"
repo="${repo%%/*}"
output_dir="tools/skills/$name"
rm -rf "$output_dir"
mkdir -p "$output_dir"
pnpm resolve-references --file "$full_path" --repo "$repo" --output "$output_dir"
done
- name: Sync plugins
if: steps.discover.outputs.skill_files != ''
run: |
pnpm sync-claude-plugin
pnpm sync-cursor-plugin
pnpm sync-opencode-plugin
pnpm generate-skill-docs
pnpm bump-plugin-versions
- name: Check for changes
id: git-check
run: |
git diff --exit-code -- tools/skills/ plugins/ packages/opencode/ documentation/docs/ || 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 from svelte.dev'
branch: chore/sync-skills
delete-branch: true
title: 'chore: sync skills from svelte.dev'
body: |
## Summary
Automatically synced skill markdown from `sveltejs/svelte.dev` into `tools/skills/`.
## Changes
- Cloned `sveltejs/svelte.dev`
- Filtered markdown files with `skill: true` frontmatter
- Rebuilt synced skill folders with `scripts/resolve-references.ts`
- Synced `plugins/claude/svelte/` (skills, agents)
- Synced `plugins/cursor/svelte/` (skills, agents, rules)
- Synced `packages/opencode/` (skills, instructions)
- Updated documentation
## Generated by
GitHub Action: Sync Skills
labels: |
chore
automated

95
.github/workflows/sync-plugins.yml vendored Normal file
View File

@@ -0,0 +1,95 @@
name: Sync Plugins
on:
push:
branches:
- main
paths:
- 'tools/**'
permissions:
contents: write
pull-requests: write
jobs:
sync-plugins:
# prevents this action from running on forks
if: github.repository == 'sveltejs/ai-tools'
name: Sync Plugins from tools/
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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 plugins
run: pnpm sync-plugins
- name: Generate skills documentation
run: pnpm generate-skill-docs
- name: Generate subagent documentation
run: pnpm generate-subagent-docs
- name: Check for changes
id: git-check
run: |
git diff --exit-code \
plugins/claude/svelte/ \
plugins/cursor/svelte/ \
packages/opencode/skills/ \
packages/opencode/instructions/ \
packages/opencode/schema.json \
documentation/docs/ \
|| 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 plugins from tools/'
branch: chore/sync-plugins
delete-branch: true
title: 'chore: sync plugins from tools/'
body: |
## Summary
Automatically synced all plugins from the `tools/` source of truth.
This PR was triggered by changes to `tools/**`.
## Changes
- Synced `plugins/claude/svelte/` (skills, agents with `permissionMode`)
- Synced `plugins/cursor/svelte/` (skills, agents, rules)
- Synced `packages/opencode/` (skills, instructions, schema)
- Updated documentation
## Generated by
GitHub Action: Sync Plugins
labels: |
chore
automated

View File

@@ -13,12 +13,10 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.27.0
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
@@ -32,13 +30,9 @@ jobs:
- name: Build project
run: pnpm run build
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key
- name: Run tests
run: pnpm run test
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key

View File

@@ -14,12 +14,12 @@ permissions:
jobs:
update-docs:
# prevents this action from running on forks
if: github.repository == 'sveltejs/mcp'
if: github.repository == 'sveltejs/ai-tools'
name: Update OpenCode JSON Schema
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
@@ -56,7 +56,7 @@ jobs:
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'docs: update opencode JSON schema'

View File

@@ -14,12 +14,12 @@ permissions:
jobs:
update-docs:
# prevents this action from running on forks
if: github.repository == 'sveltejs/mcp'
if: github.repository == 'sveltejs/ai-tools'
name: Update Prompt Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
@@ -56,7 +56,7 @@ jobs:
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'docs: update prompts documentation'

1
.npmrc
View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -10,6 +10,12 @@
"options": {
"parser": "svelte"
}
},
{
"files": "**/references/*.md",
"options": {
"embeddedLanguageFormatting": "off"
}
}
]
}

View File

@@ -83,7 +83,6 @@ Located in `src/lib/server/analyze/`:
Required environment variables:
- `DATABASE_URL`: SQLite database path (default: `file:test.db`)
- `VOYAGE_API_KEY`: API key for embeddings support (optional)
When connected to the svelte-llm MCP server, you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 [Svelte Contributors](https://github.com/sveltejs/ai-tools/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,3 +1 @@
DATABASE_URL=file:test.db
DATABASE_TOKEN=needs_to_be_set_but_it_can_be_anything
VOYAGE_API_KEY=your_actual_api_key_here

View File

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

View File

@@ -23,10 +23,6 @@
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"test:watch": "npm run test:unit -- --watch",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"inspect": "pnpm mcp-inspector"
},
"keywords": [
@@ -39,15 +35,12 @@
"devDependencies": {
"@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",
@@ -62,7 +55,6 @@
"vitest": "catalog:tooling"
},
"dependencies": {
"@sveltejs/mcp-schema": "workspace:^",
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-http": "catalog:tmcp",
"@vercel/analytics": "catalog:tooling",

View File

@@ -1,6 +1,5 @@
import { dev } from '$app/environment';
import { http_transport } from '$lib/mcp/index.js';
import { db } from '$lib/server/db/index.js';
import { redirect } from '@sveltejs/kit';
import { track } from '@vercel/analytics/server';
@@ -17,7 +16,6 @@ export async function handle({ event, resolve }) {
}
}
const mcp_response = await http_transport.respond(event.request, {
db,
// only add analytics in production
track: dev
? undefined
@@ -25,22 +23,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

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

View File

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

View File

@@ -2,45 +2,11 @@
title: Overview
---
The Svelte MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) server can help your LLM or agent of choice write better Svelte code. It works by providing documentation relevant to the task at hand, and statically analysing generated code so that it can suggest fixes and best practices.
The following pages will help you set up and use the AI tools officially maintained by the Svelte team.
## Setup
There are four tools, designed to help your agent write correct, robust Svelte code. They are designed to work together, but each can be used individually:
The setup varies based on the version of the MCP you prefer — remote or local — and your chosen MCP client (e.g. Claude Code, Codex CLI or GitHub Copilot):
- [local setup](local-setup) using `@sveltejs/mcp`
- [remote setup](remote-setup) using `https://mcp.svelte.dev/mcp`
## Usage
To get the most out of the MCP server we recommend including the following prompt in your [`AGENTS.md`](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:
## Available MCP Tools:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
```
If your MCP client supports it, we also recommend using the [svelte-task](prompts#svelte-task) prompt to instruct the LLM on the best way to use the MCP server.
- [Instructions](instructions): small prompt always injected into your session to make your agent more aware of the available tools
- [MCP Server](mcp): with tools, prompts and resources to give your agent more context, by pulling directly from the official Svelte documentation and using static analysis to correct common generative AI pitfalls
- [Skills](skills): lazy-loaded descriptions that teach your agent Svelte best practices, and how to use the [`@sveltejs/mcp` cli](cli)
- [Subagents](subagent): focused agents that can be invoked in parallel to execute atomic operations in a separate context window

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 Svelte 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,13 @@
---
title: AGENTS.md
---
To get the most out of the [MCP server](mcp) and [skills](skills) 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) or [`GEMINI.md`](https://geminicli.com/docs/cli/gemini-md/), if using Claude Code or Gemini). This will tell your agent which tools are available and when it is appropriate to use them.
> [!NOTE] This is already setup for you when using `npx sv add mcp`
<!-- prettier-ignore-start -->
````markdown
@include .generated/agents.md
````
<!-- prettier-ignore-end -->

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,3 @@
---
title: Prompts
---
This is the list of available prompts provided by the MCP server. Prompts are selected by the user and are sent as a user message. They can be useful to write repetitive instructions for the LLM on how to properly use the MCP server.
## svelte-task
This prompt should be used whenever you are asking the model to work on a Svelte-related task. It will instruct the LLM which documentation sections are available, which tools to invoke, when to invoke them, and how to interpret the results.
@@ -11,10 +5,22 @@ This prompt should be used whenever you are asking the model to work on a Svelte
<details>
<summary>Copy the prompt</summary>
```md
<!-- prettier-ignore-start -->
````markdown
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: use title and path to estimate use case, path: ai/overview
- title: Local setup, use_cases: use title and path to estimate use case, path: ai/local-setup
- title: Remote setup, use_cases: use title and path to estimate use case, path: ai/remote-setup
- title: Tools, use_cases: use title and path to estimate use case, path: ai/tools
- title: Resources, use_cases: use title and path to estimate use case, path: ai/resources
- title: Prompts, use_cases: use title and path to estimate use case, path: ai/prompts
- title: Overview, use_cases: use title and path to estimate use case, path: ai/plugin
- title: Subagent, use_cases: use title and path to estimate use case, path: ai/subagent
- title: Overview, use_cases: use title and path to estimate use case, path: ai/opencode-plugin
- title: Subagent, use_cases: use title and path to estimate use case, path: ai/opencode-subagent
- title: Overview, use_cases: use title and path to estimate use case, path: ai/skills
- title: Overview, use_cases: project setup, creating new svelte apps, scaffolding, cli tools, initializing projects, path: cli/overview
- title: Frequently asked questions, use_cases: project setup, initializing new svelte projects, troubleshooting cli installation, package manager configuration, path: cli/faq
- title: sv create, use_cases: project setup, starting new sveltekit app, initializing project, creating from playground, choosing project template, path: cli/sv-create
@@ -24,7 +30,7 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- title: devtools-json, use_cases: development setup, chrome devtools integration, browser-based editing, local development workflow, debugging setup, path: cli/devtools-json
- title: drizzle, use_cases: database setup, sql queries, orm integration, data modeling, postgresql, mysql, sqlite, server-side data access, database migrations, type-safe queries, path: cli/drizzle
- title: eslint, use_cases: code quality, linting, error detection, project setup, code standards, team collaboration, typescript projects, path: cli/eslint
- title: lucia, use_cases: authentication, login systems, user management, registration pages, session handling, auth setup, path: cli/lucia
- title: better-auth, use_cases: use title and path to estimate use case, path: cli/better-auth
- title: mcp, use_cases: use title and path to estimate use case, path: cli/mcp
- title: mdsvex, use_cases: blog, content sites, markdown rendering, documentation sites, technical writing, cms integration, article pages, path: cli/mdsvex
- title: paraglide, use_cases: internationalization, multi-language sites, i18n, translation, localization, language switching, global apps, multilingual content, path: cli/paraglide
@@ -34,6 +40,8 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- title: sveltekit-adapter, use_cases: deployment, production builds, hosting setup, choosing deployment platform, configuring adapters, static site generation, node server, vercel, cloudflare, netlify, path: cli/sveltekit-adapter
- title: tailwindcss, use_cases: project setup, styling, css framework, rapid prototyping, utility-first css, design systems, responsive design, adding tailwind to svelte, path: cli/tailwind
- title: vitest, use_cases: testing, unit tests, component testing, test setup, quality assurance, ci/cd pipelines, test-driven development, path: cli/vitest
- title: add-on, use_cases: use title and path to estimate use case, path: cli/add-on
- title: sv-utils, use_cases: use title and path to estimate use case, path: cli/sv-utils
- title: Introduction, use_cases: learning sveltekit, project setup, understanding framework basics, choosing between svelte and sveltekit, getting started with full-stack apps, path: kit/introduction
- title: Creating a project, use_cases: project setup, starting new sveltekit app, initial development environment, first-time sveltekit users, scaffolding projects, path: kit/creating-a-project
- title: Project types, use_cases: deployment, project setup, choosing adapters, ssg, spa, ssr, serverless, mobile apps, desktop apps, pwa, offline apps, browser extensions, separate backend, docker containers, path: kit/project-types
@@ -101,12 +109,6 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- title: Configuration, use_cases: project setup, configuration, adapters, deployment, build settings, environment variables, routing customization, prerendering, csp security, csrf protection, path configuration, typescript setup, path: kit/configuration
- title: Command Line Interface, use_cases: project setup, typescript configuration, generated types, ./$types imports, initial project configuration, path: kit/cli
- title: Types, use_cases: typescript, type safety, route parameters, api endpoints, load functions, form actions, generated types, jsconfig setup, path: kit/types
- title: Overview, use_cases: use title and path to estimate use case, path: mcp/overview
- title: Local setup, use_cases: use title and path to estimate use case, path: mcp/local-setup
- title: Remote setup, use_cases: use title and path to estimate use case, path: mcp/remote-setup
- title: Tools, use_cases: use title and path to estimate use case, path: mcp/tools
- title: Resources, use_cases: use title and path to estimate use case, path: mcp/resources
- title: Prompts, use_cases: use title and path to estimate use case, path: mcp/prompts
- title: Overview, use_cases: always, any svelte project, getting started, learning svelte, introduction, project setup, understanding framework basics, path: svelte/overview
- title: Getting started, use_cases: project setup, starting new svelte project, initial installation, choosing between sveltekit and vite, editor configuration, path: svelte/getting-started
- title: .svelte files, use_cases: always, any svelte project, component creation, project setup, learning svelte basics, path: svelte/svelte-files
@@ -154,6 +156,7 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- 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: Best practices, use_cases: use title and path to estimate use case, path: svelte/best-practices
- 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
@@ -204,6 +207,7 @@ This is the task you will work on:
</task>
If you are not writing the code into a file, once you have the final version of the code ask the user if it wants to generate a playground link to quickly check the code in it and if it answer yes call the `playground-link` tool and return the url to the user nicely formatted. The playground link MUST be generated only once you have the final version of the code and you are ready to share it, it MUST include an entry point file called `App.svelte` where the main component should live. If you have multiple files to include in the playground link you can include them all at the root.
```
````
<!-- prettier-ignore-end -->
</details>

View File

@@ -0,0 +1,16 @@
---
title: Overview
---
The Svelte MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) server can help your agent write better Svelte code. It works by providing relevant documentation, and statically analysing generated code so that it can suggest fixes and best practices.
## Setup
The setup varies based on the version of the MCP you prefer — remote or local — and your chosen MCP client (e.g. Claude Code, Codex CLI or GitHub Copilot):
- [local setup](local-setup) using `@sveltejs/mcp`
- [remote setup](remote-setup) using `https://mcp.svelte.dev/mcp`
## Usage
If your MCP client supports it, we also recommend using the [svelte-task](prompts#svelte-task) prompt to instruct the LLM on the best way to use the MCP server.

View File

@@ -37,7 +37,7 @@ In the Settings > Developer section, click on Edit Config. It will open the fold
## Codex CLI
Add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
You can automatically configure the MCP server using the [Codex plugin](codex-plugin) (recommended). If you prefer to configure the MCP server manually, add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
```toml
[mcp_servers.svelte]
@@ -45,15 +45,41 @@ command = "npx"
args = ["-y", "@sveltejs/mcp"]
```
## Gemini CLI
## Copilot CLI
To include the local MCP version in Gemini CLI, simply run the following command:
You can automatically configure the MCP server using the [Copilot plugin](copilot-plugin) (recommended). If you prefer to configure the MCP server manually, use the Copilot CLI to interactively add the MCP server:
```bash
gemini mcp add -t stdio -s [scope] svelte npx -y @sveltejs/mcp
/mcp add
```
The `[scope]` must be `user`, `project` or `local`.
Alternatively, create or edit `~/.copilot/mcp-config.json` and add the following configuration:
```json
{
"mcpServers": {
"svelte": {
"command": "npx",
"args": ["-y", "@sveltejs/mcp"]
}
}
}
```
## Antigravity CLI
To use the local MCP version in Antigravity CLI, create or edit `~/.gemini/config/mcp_config.json` and add the following configuration:
```json
{
"mcpServers": {
"svelte": {
"command": "npx",
"args": ["-y", "@sveltejs/mcp"]
}
}
}
```
## OpenCode
@@ -91,6 +117,8 @@ opencode mcp add
## Cursor
You can automatically configure the MCP server using the [Cursor plugin](cursor-plugin) (recommended). If you prefer to configure the MCP server manually you can:
- Open the command palette
- Select "View: Open MCP Settings"
- Click on "Add custom MCP"

View File

@@ -16,7 +16,7 @@ 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 a useful [skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview).
If you prefer you can also install the `svelte` plugin in [the Svelte Claude Code Marketplace](claude-plugin) that will give you both the remote server and useful [skills](skills).
## Claude Desktop
@@ -28,7 +28,7 @@ If you prefer you can also install the `svelte` plugin in [the Svelte Claude Cod
## Codex CLI
Add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
You can automatically configure the MCP server using the [Codex plugin](codex-plugin) (recommended). If you prefer to configure the MCP server manually, add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
```toml
experimental_use_rmcp_client = true
@@ -36,15 +36,39 @@ experimental_use_rmcp_client = true
url = "https://mcp.svelte.dev/mcp"
```
## Gemini CLI
## Copilot CLI
To use the remote MCP server with Gemini CLI, simply run the following command:
You can automatically configure the MCP server using the [Copilot plugin](copilot-plugin) (recommended). If you prefer to configure the MCP server manually, use the Copilot CLI to interactively add the MCP server:
```bash
gemini mcp add -t http -s [scope] svelte https://mcp.svelte.dev/mcp
/mcp add
```
The `[scope]` must be `user` or `project`.
Alternatively, create or edit `~/.copilot/mcp-config.json` and add the following configuration:
```json
{
"mcpServers": {
"svelte": {
"url": "https://mcp.svelte.dev/mcp"
}
}
}
```
## Antigravity CLI
To use the remote MCP version in Antigravity CLI, create or edit `~/.gemini/config/mcp_config.json` and add the following configuration:
```json
{
"mcpServers": {
"svelte": {
"url": "https://mcp.svelte.dev/mcp"
}
}
}
```
## OpenCode
@@ -82,6 +106,8 @@ opencode mcp add
## Cursor
You can automatically configure the MCP server using the [Cursor plugin](cursor-plugin) (recommended). If you prefer to configure the MCP server manually you can:
- Open the command palette
- Select "View: Open MCP Settings"
- Click on "Add custom MCP"

View File

@@ -0,0 +1,7 @@
---
title: Prompts
---
This is the list of available prompts provided by the MCP server. Prompts are selected by the user and are sent as a user message. They can be useful to write repetitive instructions for the LLM on how to properly use the MCP server.
@include .generated/prompts.md

View File

@@ -0,0 +1,78 @@
---
title: CLI
---
The `@sveltejs/mcp` npm package normally launches the local `stdio` MCP server:
```bash
npx -y @sveltejs/mcp
```
If you invoke it with a subcommand, it behaves like a regular CLI and prints the result directly in your terminal instead. This is useful for agents, scripts and quick manual checks.
## Usage
```bash
npx -y @sveltejs/mcp <command> [options]
```
Available commands:
- `list-sections`
- `get-documentation <sections>`
- `svelte-autofixer <code_or_path>`
You can learn more about the commands with
```bash
npx -y @sveltejs/mcp --help
npx -y @sveltejs/mcp <command> --help
npx -y @sveltejs/mcp --version
```
## `list-sections`
Lists all available Svelte and SvelteKit documentation sections.
```bash
npx -y @sveltejs/mcp list-sections
```
The output is a structured text list of sections, including each section's title, `use_cases`, and documentation path. This is the same catalog the MCP tool uses before calling `get-documentation`.
## `get-documentation`
Fetches the full documentation for one or more sections.
```bash
npx -y @sveltejs/mcp get-documentation 'svelte/$state'
# or
npx -y @sveltejs/mcp get-documentation 'svelte/$state,svelte/await-expressions'
```
Each section can be matched by title or by documentation path. If a section cannot be found, the CLI returns an error plus similar matches when available.
## `svelte-autofixer`
Runs the Svelte autofixer against either inline code or a file path:
```bash
npx -y @sveltejs/mcp svelte-autofixer 'src/routes/+page.svelte'
```
If the argument is an existing path, the CLI reads the file automatically. Otherwise it treats the argument as raw Svelte code.
Because most shells expand `$`, inline code should be quoted or escaped correctly. In practice, passing a file path is usually easier than passing source directly.
Available options:
- `--svelte-version <4|5>` - choose which Svelte version to validate against (defaults to `5`)
- `--async` - enable async Svelte analysis for Svelte 5 projects
The command prints an object with:
- `issues`
- `suggestions`
- `require_another_tool_call_after_fixing`
This makes it easy to use in an agentic loop: run the autofixer, apply fixes, then run it again until it reports no remaining issues or suggestions.

View File

@@ -0,0 +1,3 @@
---
title: MCP server
---

View File

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

View File

@@ -1,23 +0,0 @@
---
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 both the remote MCP server, a [skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview) 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 skill:
```bash
/plugin install svelte
```

View File

@@ -1,11 +0,0 @@
---
title: Skill
---
Claude Skills are sets of markdown files that live in your `.claude` folder (or that you can upload via the Claude web/desktop app). They are automatically loaded by Claude when it thinks they are appropriate for the current task.
With those markdown files you can steer the agent's behaviour and, in our case, teach it how to properly write Svelte 5 code. The advantage over the MCP server is that the relevant tokens are only loaded when they are needed (for example, if you ask the LLM to write a Typescript utility in a Svelte project it will not load the skill in the context).
You can find the skill inside the [`sveltejs/mcp`](https://github.com/sveltejs/mcp) repo in the [`plugins/svelte/skills`](https://github.com/sveltejs/mcp/tree/main/plugins/svelte/skills) folder. You can also download the latest zip file from the [releases page](https://github.com/sveltejs/mcp/releases?q=svelte-code-writer) to load it in the Claude web/desktop app or to extract it inside your `.claude` folder.
If you are using Claude Code you can also install it through the [Svelte marketplace](plugin).

View File

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

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

View File

@@ -1,39 +0,0 @@
---
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 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
}
}
```
...but if you prefer, you can enable only the subagent, only the MCP, 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

@@ -1,11 +0,0 @@
---
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,263 @@
## `svelte-code-writer`
CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing 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/ai-tools/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>
## `svelte-core-bestpractices`
Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more.
<a href="https://github.com/sveltejs/ai-tools/releases?q=svelte-core-bestpractices" target="_blank" rel="noopener noreferrer">Open Releases page</a>
<details>
<summary>View skill content</summary>
<!-- prettier-ignore-start -->
````markdown
## `$state`
Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable.
Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example.
## `$derived`
To compute something from state, use `$derived` rather than `$effect`:
```js
// do this
let square = $derived(num * num);
// don't do this
let square;
$effect(() => {
square = num * num;
});
```
> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`.
Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes.
If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this.
## `$effect`
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md)
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
## `$props`
Treat props as though they will change. For example, values that depend on props should usually use `$derived`:
```js
// @errors: 2451
let { type } = $props();
// do this
let color = $derived(type === 'danger' ? 'red' : 'green');
// don't do this — `color` will not update if `type` changes
let color = type === 'danger' ? 'red' : 'green';
```
## `$inspect.trace`
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
## Events
Any element attribute starting with `on` is treated as an event listener:
```svelte
<button onclick={() => {...}}>click me</button>
<!-- attribute shorthand also works -->
<button {onclick}>...</button>
<!-- so do spread attributes -->
<button {...props}>...</button>
```
If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`:
```svelte
<svelte:window onkeydown={...} />
<svelte:document onvisibilitychange={...} />
```
Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}
<p>hello {name}!</p>
{/snippet}
{@render greeting('world')}
```
> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components.
## Each blocks
Prefer to use [keyed each blocks](references/each.md) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items.
> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key.
Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example).
## Using JavaScript variables in CSS
If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive.
```svelte
<div style:--columns={columns}>...</div>
```
You can then reference `var(--columns)` inside the component's `<style>`.
## Styling child components
The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties:
```svelte
<!-- Parent.svelte -->
<Child --color="red" />
<!-- Child.svelte -->
<h1>Hello</h1>
<style>
h1 {
color: var(--color);
}
</style>
```
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
```svelte
<div>
<Child />
</div>
<style>
div :global {
h1 {
color: red;
}
}
</style>
```
## Context
Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering.
Use `createContext` rather than `setContext` and `getContext`, as it provides type safety.
## Async Svelte
If using version 5.36 or higher, you can use [await expressions](references/await-expressions.md) and [hydratable](references/hydratable.md) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
## Avoid legacy features
Always use runes mode for new code, and avoid features that have more modern replacements:
- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`)
- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution)
- use `$props` instead of `export let`, `$$props` and `$$restProps`
- use `onclick={...}` instead of `on:click={...}`
- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>`
- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>`
- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>`
- use classes with `$state` fields to share reactivity between components, instead of using stores
- use `{@attach ...}` instead of `use:action`
- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive
````
<!-- prettier-ignore-end -->
</details>

View File

@@ -0,0 +1,11 @@
---
title: Overview
---
This is the list of available skills provided by Svelte. Skills are sets of instructions that AI agents can load on-demand to help with specific tasks.
Skills are available in the Claude Code plugin, the Codex CLI plugin, the GitHub Copilot CLI plugin, and the OpenCode plugin (`@sveltejs/opencode`). They can also be manually installed in your `.claude/skills`, `.copilot/skills`, or `.opencode/skills` folder.
You can download the latest skills from the [releases page](https://github.com/sveltejs/ai-tools/releases) of the repo, or find them in the [`tools/skills`](https://github.com/sveltejs/ai-tools/tree/main/tools/skills) folder.
@include .generated/skills.md

View File

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

View File

@@ -0,0 +1,67 @@
---
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 or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.
---
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.
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
## 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,20 @@
---
title: Overview
---
Since creating, editing or analyzing a Svelte file is an atomic operation we recommend creating a subagent that your main agent can invoke whenever it needs to interact with a Svelte component. Subagents use a separate context window, allowing them to fetch documentation, iterate with [`svelte-autofixer`](tools#svelte-autofixer) and write to the filesystem without wasting context in the main agent.
Delegation should happen automatically when appropriate, but you can also explicitly request the subagent be used for Svelte-related tasks.
You can write your own or take inspiration from the one available in the [`sveltejs/ai-tools`](https://github.com/sveltejs/ai-tools/tree/main/tools/agents) repository: a specialized subagent called `svelte-file-editor` designed for creating, editing, and reviewing Svelte files.
<details>
<summary>View subagent definition</summary>
<!-- prettier-ignore-start -->
````markdown
@include .generated/subagent.md
````
<!-- prettier-ignore-end -->
</details>

View File

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

View File

@@ -0,0 +1,23 @@
---
title: Claude Code
---
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a Claude Code [plugin marketplace](https://code.claude.com/docs/en/discover-plugins).
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 — this will help 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/ai-tools
```
Then, install the Svelte plugin:
```bash
/plugin install svelte
```

View File

@@ -0,0 +1,54 @@
---
title: OpenCode
---
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 you can edit your [OpenCode config](https://opencode.ai/docs/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](mcp), [skills](skills), and the `svelte-file-editor` [subagent](subagent) configured for you.
## Configuration
By default, everything is enabled, but you can configure the plugin by adding a configuration file:
- locally, in `.opencode/svelte.json`
- globally, in `~/.config/opencode/svelte.json` (or, if you have specified the environment variable, in `$OPENCODE_CONFIG_DIR/svelte.json`)
```json
{
"$schema": "https://svelte.dev/opencode/schema.json",
"mcp": {
"type": "remote", // or "local" — defaults to remote
"enabled": true
},
"subagent": {
"enabled": true,
"agents": {
"svelte-file-editor": {
"model": "<other-model>", // defaults to the same as main agent
"temperature": 1, // defaults to unset
"top_p": 0.7, // defaults to unset
"maxSteps": 20 // defaults to unlimited
}
}
},
"skills": {
// this can be `true`, or an array of skills to enable
// e.g. ["svelte-core-bestpractices"]
"enabled": true
},
"instructions": {
"enabled": true
}
}
```

View File

@@ -0,0 +1,25 @@
---
title: Cursor
---
Cursor has a [plugin system](https://cursor.com/docs/plugins) that can bundle rules, skills, agents, commands, MCP servers, and hooks.
The Svelte plugin gives you the remote Svelte MCP server, Cursor [skills](skills), an always-on rule that tells the model how to use the Svelte MCP tools correctly, and the `svelte-file-editor` subagent for working on `.svelte` files and `.svelte.ts`/`.svelte.js` modules. The source is available in the [`sveltejs/ai-tools`](https://github.com/sveltejs/ai-tools/tree/main/plugins/cursor/svelte) repo.
## Installation
Install the plugin from the [Cursor Marketplace](https://cursor.com/marketplace/svelte) with the following command:
```
/add-plugin svelte
```
Plugins can be installed either for the current project or at user level.
Once installed, Cursor will discover the plugin components automatically:
- the Svelte MCP server is added from the plugin's `.mcp.json`
- rules and skills appear in Cursor's rules UI
- the `svelte-file-editor` agent becomes available in chat
> [!NOTE] The Cursor CLI does not support plugins yet. Plugin support in [Cloud Agents](https://cursor.com/docs/cloud-agent) is limited to MCP servers.

View File

@@ -0,0 +1,36 @@
---
title: GitHub Copilot CLI
---
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a GitHub Copilot CLI [plugin marketplace](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing).
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 — this will help save context by handling Svelte-specific tasks more efficiently.
## Installation
In VS Code, run the **Install plugin from source** command and use the repository URL:
```text
https://github.com/sveltejs/ai-tools
```
You can also add the repository as a marketplace from the Copilot CLI:
```bash
copilot plugin marketplace add sveltejs/ai-tools
```
Then, install the Svelte plugin:
```bash
copilot plugin install svelte@ai-tools
```
You can also run the same commands from an interactive Copilot CLI session:
```bash
/plugin marketplace add sveltejs/ai-tools
/plugin install svelte@ai-tools
```

View File

@@ -0,0 +1,28 @@
---
title: Codex CLI
---
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a Codex CLI [plugin marketplace](https://developers.openai.com/codex/plugins).
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 — this will help save context by handling Svelte-specific tasks more efficiently.
## Installation
Add the repository as a marketplace from the Codex CLI:
```bash
codex plugin marketplace add sveltejs/ai-tools
```
Then, open the plugin directory from an interactive Codex CLI session:
```bash
codex
/plugins
```
Choose the Svelte marketplace, select the `svelte` plugin, and install it.
Codex can read the repository's legacy-compatible `.claude-plugin/marketplace.json` marketplace file, so the same marketplace source works for both Claude Code and Codex CLI.

View File

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

View File

@@ -1,3 +1,3 @@
---
title: MCP
title: AI
---

View File

@@ -3,7 +3,7 @@
"version": "0.0.1",
"description": "The official Svelte MCP server implementation",
"type": "module",
"packageManager": "pnpm@10.27.0",
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"scripts": {
"build": "pnpm -r run build",
"dev": "pnpm --filter @sveltejs/mcp-remote run dev",
@@ -21,9 +21,19 @@
"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",
"generate-subagent-docs": "cp ./tools/agents/svelte-file-editor.md ./documentation/docs/50-subagents/.generated/subagent.md",
"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-plugins": "pnpm sync-claude-plugin && pnpm sync-cursor-plugin && pnpm sync-opencode-plugin && pnpm bump-plugin-versions",
"sync-claude-plugin": "node scripts/sync-claude-plugin.ts",
"sync-cursor-plugin": "node scripts/sync-cursor-plugin.ts",
"sync-opencode-plugin": "node scripts/sync-opencode-plugin.ts && pnpm generate-opencode-jsonschema",
"bump-plugin-versions": "node scripts/bump-plugin-versions.ts",
"postbump-plugin-versions": "pnpm format",
"resolve-references": "node scripts/resolve-references.ts",
"postresolve-references": "pnpm format"
},
"keywords": [
"svelte",

View File

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

View File

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

View File

@@ -1,82 +0,0 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { float_32_array } from './utils.js';
/**
* NOTE: if you modify a schema adding a vector column you need to manually add this
*
* CREATE INDEX IF NOT EXISTS name_of_the_index
* ON `name_of_the_table` (
* libsql_vector_idx(name_of_the_column, 'metric=cosine')
* )
*
* to the generated migration file
*/
export const distillations = sqliteTable('distillations', {
id: integer('id').primaryKey(),
preset_name: text('preset_name').notNull(),
version: text('version').notNull(),
content: text('content').notNull(),
size_kb: integer('size_kb').notNull(),
document_count: integer('document_count').notNull(),
distillation_job_id: integer('distillation_job_id').references(() => distillation_jobs.id),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const distillation_jobs = sqliteTable('distillation_jobs', {
id: integer('id').primaryKey(),
preset_name: text('preset_name').notNull(),
batch_id: text('batch_id'),
status: text('status', { enum: ['pending', 'processing', 'completed', 'failed'] }).notNull(),
model_used: text('model_used').notNull(),
total_files: integer('total_files').notNull(),
processed_files: integer('processed_files').notNull().default(0),
successful_files: integer('successful_files').notNull().default(0),
minimize_applied: integer('minimize_applied', { mode: 'boolean' }).notNull().default(false),
total_input_tokens: integer('total_input_tokens').notNull().default(0),
total_output_tokens: integer('total_output_tokens').notNull().default(0),
started_at: integer('started_at', { mode: 'timestamp' }),
completed_at: integer('completed_at', { mode: 'timestamp' }),
error_message: text('error_message'),
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const content = sqliteTable('content', {
id: integer('id').primaryKey(),
path: text('path').notNull(),
filename: text('filename').notNull(),
content: text('content').notNull(),
size_bytes: integer('size_bytes').notNull(),
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const content_distilled = sqliteTable('content_distilled', {
id: integer('id').primaryKey(),
path: text('path').notNull(),
filename: text('filename').notNull(),
content: text('content').notNull(),
size_bytes: integer('size_bytes').notNull(),
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});

View File

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

View File

@@ -17,12 +17,8 @@
".": "./src/index.ts",
"./handlers": "./src/mcp/handlers/tools/handlers.ts"
},
"peerDependencies": {
"drizzle-orm": "^0.45.0"
},
"dependencies": {
"@mcp-ui/server": "catalog:ai",
"@sveltejs/mcp-schema": "workspace:^",
"@tmcp/adapter-valibot": "catalog:tmcp",
"@tmcp/transport-in-memory": "catalog:tmcp",
"@typescript-eslint/parser": "catalog:lint",
@@ -40,7 +36,6 @@
"devDependencies": {
"@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

@@ -344,87 +344,111 @@ describe('add_autofixers_issues', () => {
});
describe('imported_runes', () => {
describe.each([{ source: 'svelte' }, { source: 'svelte/runes' }])(
'from "$source"',
({ source }) => {
describe.each(dollarless_runes)('single import ($rune)', ({ rune }) => {
it(`should add suggestions when importing '${rune}' from '${source}'`, () => {
const content = run_autofixers_on_code(`
describe.each([
{ source: 'svelte' },
{ source: 'svelte/runes' },
{ source: '@sveltejs/runes' },
{ source: '@sveltejs/vite-plugin-svelte' },
])('from "$source"', ({ source }) => {
describe.each(dollarless_runes)('single import ($rune)', ({ rune }) => {
it(`should add suggestions when importing '${rune}' from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${rune} } from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions when importing "${rune}" as the default export from '${source}'`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when importing "${rune}" as the default export from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import ${rune} from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions when importing '${rune}' as the namespace export from '${source}'`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when importing '${rune}' as the namespace export from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import * as ${rune} from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
});
it(`should add suggestions when importing multiple runes from '${source}'`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when importing multiple runes from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { onMount, state, effect } from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
`You are importing "state" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$state" directly.`,
);
expect(content.suggestions).toContain(
`You are importing "effect" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$effect" directly.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
`You are importing "state" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$state" directly.`,
);
expect(content.suggestions).toContain(
`You are importing "effect" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$effect" directly.`,
);
});
it(`should not add suggestions when importing other identifiers from '${source}'`, () => {
const content = run_autofixers_on_code(`
it(`should not add suggestions when importing other identifiers from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { onMount } from '${source}';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "onMount" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$onMount" directly.`,
);
});
},
);
expect(content.suggestions).not.toContain(
`You are importing "onMount" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$onMount" directly.`,
);
});
});
describe.each(dollarless_runes)('importing $rune from external lib', ({ rune }) => {
it(`should not add suggestions when importing from packages that are not svelte`, () => {
it(`should not add suggestions when importing from packages whose name doesn't contain svelte`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${rune} } from 'something-something';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "${rune}" from "something-something". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions with a different hint when importing from packages whose name contains svelte but it's not official`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${rune} } from 'svelte-something-something';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "${rune}" from "svelte-something-something". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
expect(content.suggestions).toContain(
`You are importing "${rune}" from "svelte-something-something". If you are trying to import runes to use them this is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly. If you are importing the function from a separate library ignore this suggestion.`,
);
});
});
it('should not add the imported_runes suggestion when importing derived from svelte/store', () => {
const content = run_autofixers_on_code(`
<script>
import { derived } from 'svelte/store';
</script>`);
expect(content.suggestions).not.toContain(
'You are importing "derived" from "svelte/store". This is not necessary, all runes are globally available. Please remove this import and use "$derived" directly.',
);
});
});
describe('derived_with_function', () => {
@@ -751,5 +775,16 @@ describe('add_autofixers_issues', () => {
`You are reading the stateful variable "$x" with a "$" prefix. Stateful variables are not stores and should be read without the "$". Please read it as a normal variable "x"`,
);
});
// https://github.com/sveltejs/ai-tools/issues/200
it('should not crash when reading $-prefixed identifier for variable initialized with member expression', () => {
expect(() =>
run_autofixers_on_code(`<div>{$x}</div>
<script>
const x = foo.bar;
</script>`),
).not.toThrow();
});
});
});

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

@@ -3,10 +3,20 @@ import type { Autofixer } from './index.js';
const dollarless_runes = base_runes.map((r) => r.replace('$', ''));
function should_suggest_for_source(source: string, rune: string) {
if (!source.includes('svelte')) {
return false;
}
if (source === 'svelte/store' && rune === 'derived') {
return false;
}
return true;
}
export const imported_runes: Autofixer = {
ImportDeclaration(node, { state, next }) {
const source = (node.source.value || node.source.raw?.slice(1, -1))?.toString();
if (source && (source === 'svelte' || source.startsWith('svelte/'))) {
if (source) {
for (const specifier of node.specifiers) {
const id =
specifier.type === 'ImportDefaultSpecifier'
@@ -16,10 +26,25 @@ export const imported_runes: Autofixer = {
: specifier.type === 'ImportSpecifier'
? specifier.imported
: null;
if (id && id.type === 'Identifier' && dollarless_runes.includes(id.name)) {
state.output.suggestions.push(
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
);
if (
id &&
id.type === 'Identifier' &&
dollarless_runes.includes(id.name) &&
should_suggest_for_source(source, id.name)
) {
if (
source === 'svelte' ||
source.startsWith('svelte/') ||
source.startsWith('@sveltejs')
) {
state.output.suggestions.push(
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
);
} else {
state.output.suggestions.push(
`You are importing "${id.name}" from "${source}". If you are trying to import runes to use them this is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly. If you are importing the function from a separate library ignore this suggestion.`,
);
}
}
}
}

View File

@@ -91,16 +91,56 @@ export async function get_documentation_handler({
}
});
const has_any_success = results.some((result) => result.success);
let final_text = results.map((r) => r.content).join('\n\n---\n\n');
const successes = results.filter((r) => r.success);
const failed_sections = sections.filter(
(s) =>
!available_sections.some(
(a) => a.title.toLowerCase() === s.toLowerCase() || a.slug === s || a.url === s,
),
);
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}`;
if (successes.length > 0 && failed_sections.length === 0) {
return successes.map((r) => r.content).join('\n\n---\n\n');
}
return final_text;
const parts: string[] = [];
if (successes.length > 0) {
parts.push(successes.map((r) => r.content).join('\n\n---\n\n'));
}
const fuzzy_results = failed_sections.map((requested) => {
const lower = requested.toLowerCase();
const matches = available_sections.filter(
(a) =>
a.title.toLowerCase().includes(lower) ||
a.slug.includes(lower) ||
lower.includes(a.slug.split('/').pop() ?? '') ||
a.use_cases.toLowerCase().includes(lower),
);
return { requested, matches };
});
const has_fuzzy = fuzzy_results.some((r) => r.matches.length > 0);
// Full list only when no successes and no fuzzy matches
if (successes.length === 0 && !has_fuzzy) {
const formatted_sections = await format_sections_list();
parts.push(`${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`);
}
// Similar results then errors
for (const { requested, matches } of fuzzy_results) {
if (matches.length > 0) {
const match_list = matches.map((m) => `- title: ${m.title}, section: ${m.slug}`).join('\n');
parts.push(
`${matches.length} similar result${matches.length > 1 ? 's' : ''} for "${requested}":\n${match_list}`,
);
}
parts.push(`Section not found: "${requested}".`);
}
return parts.join('\n\n---\n\n');
}
export function get_documentation(server: SvelteMcp) {

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/),
}),

View File

@@ -1,4 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { writeFileSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { InMemoryTransport } from '@tmcp/transport-in-memory';
import { beforeEach, describe, expect, it } from 'vitest';
import { server } from '../../index.js';
@@ -12,13 +15,18 @@ async function autofixer_tool_call(
is_error = false,
desired_svelte_version = 5,
async = false,
ctx?: { stdio?: boolean },
) {
const result = await session.callTool('svelte-autofixer', {
code,
desired_svelte_version,
filename: 'App.svelte',
async,
});
const result = await session.callTool(
'svelte-autofixer',
{
code,
desired_svelte_version,
filename: 'App.svelte',
async,
},
ctx,
);
expect(result).toBeDefined();
if (is_error) {
@@ -146,4 +154,40 @@ describe('svelte-autofixer tool', () => {
'The desired_svelte_version MUST be either 4 or 5 but received "3"',
);
});
it('should read file content from path when stdio context is true', async () => {
const tmp_file = join(tmpdir(), `svelte-autofixer-test-${Date.now()}.svelte`);
const file_content = `<script>
$state count = 0;
</script>`;
writeFileSync(tmp_file, file_content, 'utf-8');
try {
// with stdio: true, the file is read from disk and parsed, producing issues
const content = await autofixer_tool_call(tmp_file, false, 5, false, { stdio: true });
expect(content.issues.length).toBeGreaterThan(0);
expect(content.suggestions.length).toBeGreaterThan(0);
} finally {
unlinkSync(tmp_file);
}
});
it('should treat file path as code when stdio context is not set', async () => {
const tmp_file = join(tmpdir(), `svelte-autofixer-test-${Date.now()}.svelte`);
const file_content = `<script>
$state count = 0;
</script>`;
writeFileSync(tmp_file, file_content, 'utf-8');
try {
// without stdio context, the path string is treated as raw code (plain text), no issues
const content = await autofixer_tool_call(tmp_file, false, 5, false);
expect(content.issues).toHaveLength(0);
expect(content.suggestions).toHaveLength(0);
} finally {
unlinkSync(tmp_file);
}
});
});

View File

@@ -1,33 +1,47 @@
import { basename } from 'node:path';
import type { SvelteMcp } from '../../index.js';
import { tool } from 'tmcp/utils';
import * as v from 'valibot';
import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js';
import { add_compile_issues } from '../../autofixers/add-compile-issues.js';
import { add_eslint_issues } from '../../autofixers/add-eslint-issues.js';
import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js';
import { icons } from '../../icons/index.js';
import { tool } from 'tmcp/utils';
import { type SvelteMcp } from '../../index.js';
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.',
let cached_schema: ReturnType<typeof get_autofixer_schema> | null = null;
function get_autofixer_schema(stdio: boolean) {
let code = v.string();
if (stdio) {
// we only add the description if we are running in stdio, this saves a few tokens for the remote server
code = v.pipe(
v.string(),
v.description(
"The code to be processed by the autofixer. It can also be a path to a file containing the code. If the file doesn't exists the string will be treated as the code",
),
);
}
return v.object({
code,
desired_svelte_version: v.pipe(
v.union([v.string(), v.number()]),
v.description(
'The desired major svelte version as an integer (must be 4 or 5)...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.',
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.',
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()),
@@ -40,7 +54,7 @@ export async function svelte_autofixer_handler({
desired_svelte_version: desired_svelte_version_unchecked,
async,
filename: filename_or_path,
}: v.InferInput<typeof autofixer_schema>) {
}: v.InferInput<ReturnType<typeof get_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')]),
@@ -110,7 +124,11 @@ 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: autofixer_schema,
get schema() {
return (
cached_schema ?? (cached_schema = get_autofixer_schema(server.ctx.custom?.stdio ?? false))
);
},
outputSchema: autofixer_output_schema,
annotations: {
title: 'Svelte Autofixer',
@@ -129,6 +147,18 @@ 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 only do this if we know we are running in stdio mode (only stdio pass the context as true)
if (server.ctx.custom?.stdio) {
const [exists_sync, read_file] = await Promise.all([
import('node:fs').then((mod) => mod.existsSync),
import('node:fs/promises').then((mod) => mod.readFile),
]);
if (exists_sync(code)) {
code = await read_file(code, 'utf-8');
}
}
try {
const content = await svelte_autofixer_handler({
code,

View File

@@ -1,8 +1,6 @@
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
import { McpServer } from 'tmcp';
import { setup_prompts, setup_resources, setup_tools } from './handlers/index.js';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import type { Schema } from '@sveltejs/mcp-schema';
import { icons } from './icons/index.js';
export const server = new McpServer(
@@ -25,8 +23,8 @@ export const server = new McpServer(
'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code. After you correct the component call this tool again to confirm all the issues are fixed.',
},
).withContext<{
db: LibSQLDatabase<Schema>;
track?: (sessionId: string, event: string, extra?: string) => Promise<void>;
stdio?: boolean;
}>();
export type SvelteMcp = typeof server;

View File

@@ -1,17 +1,11 @@
import ts_parser from '@typescript-eslint/parser';
import type * as eslint from 'eslint';
import type { CallExpression, Identifier } from 'estree';
import type { Reference, Variable } from 'eslint-scope';
import { parseForESLint as svelte_eslint_parse } from 'svelte-eslint-parser';
import { runes } from '../constants.js';
type Scope = {
variables?: Variable[];
references?: Reference[];
childScopes?: Scope[];
};
type ScopeManager = {
globalScope: Scope;
};
type Scope = eslint.Scope.Scope;
type ScopeManager = eslint.Scope.ScopeManager;
function collect_scopes(scope: Scope, acc: Scope[] = []) {
acc.push(scope);
@@ -27,12 +21,12 @@ export function parse(code: string, file_path: string) {
parser: { ts: ts_parser, typescript: ts_parser },
});
let all_scopes: Scope[] | undefined;
let all_variables: Variable[] | undefined;
let all_references: Reference[] | undefined;
let all_variables: eslint.Scope.Variable[] | undefined;
let all_references: eslint.Scope.Reference[] | undefined;
function get_all_scopes() {
if (!all_scopes) {
all_scopes = collect_scopes(parsed.scopeManager!.globalScope);
all_scopes = collect_scopes(parsed.scopeManager!.globalScope!);
}
return all_scopes;
}
@@ -69,6 +63,8 @@ export function parse(code: string, file_path: string) {
return this.all_references.find((r) => r.identifier === id);
},
is_rune(call: CallExpression, rune?: (typeof runes)[number][]) {
// it should always be a call expression but we check just in case because sometimes it's any and TS doesn't complain
if (call.type !== 'CallExpression') return false;
if (call.callee.type !== 'Identifier' && call.callee.type !== 'MemberExpression')
return false;
const id = call.callee.type === 'Identifier' ? call.callee : call.callee.object;

View File

@@ -1,169 +1,209 @@
# @sveltejs/mcp
## 0.1.25
### Patch Changes
- fix: better wording for `desired_svelte_version` ([`59dcbff`](https://github.com/sveltejs/ai-tools/commit/59dcbff136a91efad5a0e978e8208e1b1d277f97))
## 0.1.24
### Patch Changes
- chore: support declaration tags ([#222](https://github.com/sveltejs/ai-tools/pull/222))
## 0.1.23
### Patch Changes
- feat: allow stdio mcp to read the content of the file directly ([#198](https://github.com/sveltejs/ai-tools/pull/198))
- fix: handle non call expressions passed to `is_rune` ([#201](https://github.com/sveltejs/ai-tools/pull/201))
- chore: remove db requirement ([#196](https://github.com/sveltejs/ai-tools/pull/196))
## 0.1.22
### Patch Changes
- fix: broaden checks for imported runes because LLMs are unhinged ([#185](https://github.com/sveltejs/ai-tools/pull/185))
## 0.1.21
### Patch Changes
- feat: display similar result & error at the end ([#161](https://github.com/sveltejs/ai-tools/pull/161))
## 0.1.20
### Patch Changes
- fix: turn off no-inspect in eslint for mcp ([`2245cb2`](https://github.com/sveltejs/ai-tools/commit/2245cb2dc9e2d217869b6a800795ce59ffb40c51))
## 0.1.19
### Patch Changes
- chore: update svelte ([`7447744`](https://github.com/sveltejs/mcp/commit/74477448cea44ec21684ea4d39f2c5c7133b5150))
- chore: update svelte ([`7447744`](https://github.com/sveltejs/ai-tools/commit/74477448cea44ec21684ea4d39f2c5c7133b5150))
## 0.1.18
### Patch Changes
- feat: expose playground link as MCP App ([#138](https://github.com/sveltejs/mcp/pull/138))
- feat: expose playground link as MCP App ([#138](https://github.com/sveltejs/ai-tools/pull/138))
## 0.1.17
### Patch Changes
- fix: add suggestion for snippets declared in script tag ([#132](https://github.com/sveltejs/mcp/pull/132))
- fix: add suggestion for snippets declared in script tag ([#132](https://github.com/sveltejs/ai-tools/pull/132))
## 0.1.16
### Patch Changes
- feat: expose tools as JS api + cli ([#128](https://github.com/sveltejs/mcp/pull/128))
- feat: expose tools as JS api + cli ([#128](https://github.com/sveltejs/ai-tools/pull/128))
## 0.1.15
### Patch Changes
- fix: server.json version + update publisher ([`9dfb4de`](https://github.com/sveltejs/mcp/commit/9dfb4dedb42837c40c4e660f0f816d7cf9081fc4))
- fix: server.json version + update publisher ([`9dfb4de`](https://github.com/sveltejs/ai-tools/commit/9dfb4dedb42837c40c4e660f0f816d7cf9081fc4))
## 0.1.14
### Patch Changes
- fix: improve prompt to reduce token usage ([#124](https://github.com/sveltejs/mcp/pull/124))
- fix: improve prompt to reduce token usage ([#124](https://github.com/sveltejs/ai-tools/pull/124))
## 0.1.13
### Patch Changes
- fix: revert name change and add title ([`98efa1e`](https://github.com/sveltejs/mcp/commit/98efa1e09ebcca7827b10dc6bc8e1699fc1e5171))
- fix: revert name change and add title ([`98efa1e`](https://github.com/sveltejs/ai-tools/commit/98efa1e09ebcca7827b10dc6bc8e1699fc1e5171))
## 0.1.12
### Patch Changes
- fix: update server name on mcp registry ([`60297b3`](https://github.com/sveltejs/mcp/commit/60297b3c49bf110b48908e61b5d5d902ea1bdf39))
- fix: update server name on mcp registry ([`60297b3`](https://github.com/sveltejs/ai-tools/commit/60297b3c49bf110b48908e61b5d5d902ea1bdf39))
- chore: update tmcp ([#99](https://github.com/sveltejs/mcp/pull/99))
- chore: update tmcp ([#99](https://github.com/sveltejs/ai-tools/pull/99))
## 0.1.11
### Patch Changes
- fix: add `async` parameter to `svelte-autofixer` ([#94](https://github.com/sveltejs/mcp/pull/94))
- fix: add `async` parameter to `svelte-autofixer` ([#94](https://github.com/sveltejs/ai-tools/pull/94))
- fix: install latest eslint svelte packages to support `$state.eager` ([`f6ce89f`](https://github.com/sveltejs/mcp/commit/f6ce89ff34faabc3d746a350ea347298ecfed2ec))
- fix: install latest eslint svelte packages to support `$state.eager` ([`f6ce89f`](https://github.com/sveltejs/ai-tools/commit/f6ce89ff34faabc3d746a350ea347298ecfed2ec))
## 0.1.10
### Patch Changes
- fix: add icons to `server.json` ([`02c951b`](https://github.com/sveltejs/mcp/commit/02c951baa86ac8103ffc158a202c06cfe6b15c01))
- fix: add icons to `server.json` ([`02c951b`](https://github.com/sveltejs/ai-tools/commit/02c951baa86ac8103ffc158a202c06cfe6b15c01))
- fix: add `preferred-frame-size` to UI resource ([`3fabcc0`](https://github.com/sveltejs/mcp/commit/3fabcc0f9bfee916c0deb9c2ffa931ed2168af2d))
- fix: add `preferred-frame-size` to UI resource ([`3fabcc0`](https://github.com/sveltejs/ai-tools/commit/3fabcc0f9bfee916c0deb9c2ffa931ed2168af2d))
- feat: support: `$state.eager` ([#90](https://github.com/sveltejs/mcp/pull/90))
- feat: support: `$state.eager` ([#90](https://github.com/sveltejs/ai-tools/pull/90))
## 0.1.9
### Patch Changes
- feat: return `mcp-ui` resource from `playground-link` ([#84](https://github.com/sveltejs/mcp/pull/84))
- feat: return `mcp-ui` resource from `playground-link` ([#84](https://github.com/sveltejs/ai-tools/pull/84))
- feat: suggest against js variables in css ([#78](https://github.com/sveltejs/mcp/pull/78))
- feat: suggest against js variables in css ([#78](https://github.com/sveltejs/ai-tools/pull/78))
## 0.1.8
### Patch Changes
- fix: upgrade registry publisher cli ([`5fa2baa`](https://github.com/sveltejs/mcp/commit/5fa2baa27009f01e0e4e91cee7984b81a81c1c29))
- fix: upgrade registry publisher cli ([`5fa2baa`](https://github.com/sveltejs/ai-tools/commit/5fa2baa27009f01e0e4e91cee7984b81a81c1c29))
## 0.1.7
### Patch Changes
- fix: use correct server schema version ([`579be87`](https://github.com/sveltejs/mcp/commit/579be877fa9f87f7f173450ca5bc918824d68282))
- fix: use correct server schema version ([`579be87`](https://github.com/sveltejs/ai-tools/commit/579be877fa9f87f7f173450ca5bc918824d68282))
## 0.1.6
### Patch Changes
- fix: prevent `imported_runes` suggestion from being added for libs that are not svelte ([`87af64f`](https://github.com/sveltejs/mcp/commit/87af64f4bc6d07b75640eb987a33655654363997))
- fix: prevent `imported_runes` suggestion from being added for libs that are not svelte ([`87af64f`](https://github.com/sveltejs/ai-tools/commit/87af64f4bc6d07b75640eb987a33655654363997))
- feat: add svelte icon and website url for mcp server ([#75](https://github.com/sveltejs/mcp/pull/75))
- feat: add svelte icon and website url for mcp server ([#75](https://github.com/sveltejs/ai-tools/pull/75))
- fix: use `data:` uri for local icon & add icons to tools + resources + prompts ([`cf62286`](https://github.com/sveltejs/mcp/commit/cf622869129382a97ad059bb1389f115907adc8e))
- fix: use `data:` uri for local icon & add icons to tools + resources + prompts ([`cf62286`](https://github.com/sveltejs/ai-tools/commit/cf622869129382a97ad059bb1389f115907adc8e))
## 0.1.5
### Patch Changes
- fix: widen `desired_svelte_version` validation to accommodate some clients ([`3b301d7`](https://github.com/sveltejs/mcp/commit/3b301d7d9c2f49758023408f505bc4ca79caaff4))
- fix: widen `desired_svelte_version` validation to accommodate some clients ([`3b301d7`](https://github.com/sveltejs/ai-tools/commit/3b301d7d9c2f49758023408f505bc4ca79caaff4))
- fix: minor tweaks to the prompt to allow for automatic sync ([#63](https://github.com/sveltejs/mcp/pull/63))
- fix: minor tweaks to the prompt to allow for automatic sync ([#63](https://github.com/sveltejs/ai-tools/pull/63))
- feat: `read_state_with_dollar` autofixer ([#66](https://github.com/sveltejs/mcp/pull/66))
- feat: `read_state_with_dollar` autofixer ([#66](https://github.com/sveltejs/ai-tools/pull/66))
## 0.1.4
### Patch Changes
- fix: pass secrets in action and update `mcpName` ([`044f098`](https://github.com/sveltejs/mcp/commit/044f0988b935fff39911a861a648dfb276f5831a))
- fix: pass secrets in action and update `mcpName` ([`044f098`](https://github.com/sveltejs/ai-tools/commit/044f0988b935fff39911a861a648dfb276f5831a))
## 0.1.3
### Patch Changes
- fix: use DNS to publish MCP ([#59](https://github.com/sveltejs/mcp/pull/59))
- fix: use DNS to publish MCP ([#59](https://github.com/sveltejs/ai-tools/pull/59))
## 0.1.2
### Patch Changes
- fix: publish to MCP registry (I really hope this time for real) ([`ef5241c`](https://github.com/sveltejs/mcp/commit/ef5241cbc204ad8bb84bde27db7c9d0a08280245))
- fix: publish to MCP registry (I really hope this time for real) ([`ef5241c`](https://github.com/sveltejs/ai-tools/commit/ef5241cbc204ad8bb84bde27db7c9d0a08280245))
## 0.1.1
### Patch Changes
- feat: publish mcp to registry (maybe for real this time) ([`132943d`](https://github.com/sveltejs/mcp/commit/132943db3b04dbbd322d08926c0880c990a61f5f))
- feat: publish mcp to registry (maybe for real this time) ([`132943d`](https://github.com/sveltejs/ai-tools/commit/132943db3b04dbbd322d08926c0880c990a61f5f))
## 0.1.0
### Minor Changes
- feat: publish to registry ([#45](https://github.com/sveltejs/mcp/pull/45))
- feat: publish to registry ([#45](https://github.com/sveltejs/ai-tools/pull/45))
### Patch Changes
- feat: add autofixer to tell the LLM to check if some function called in effect is assigning state #26 ([`73d7625`](https://github.com/sveltejs/mcp/commit/73d7625b3ca6a812ba91883ea668d80ff1e7c703))
- feat: add autofixer to tell the LLM to check if some function called in effect is assigning state #26 ([`73d7625`](https://github.com/sveltejs/ai-tools/commit/73d7625b3ca6a812ba91883ea668d80ff1e7c703))
- feat: add bind:this -> attachment and action -> attachment autofixer #20 ([`73d7625`](https://github.com/sveltejs/mcp/commit/73d7625b3ca6a812ba91883ea668d80ff1e7c703))
- feat: add bind:this -> attachment and action -> attachment autofixer #20 ([`73d7625`](https://github.com/sveltejs/ai-tools/commit/73d7625b3ca6a812ba91883ea668d80ff1e7c703))
## 0.0.4
### Patch Changes
- fix: allow TS `.svelte.ts` modules ([#49](https://github.com/sveltejs/mcp/pull/49))
- fix: allow TS `.svelte.ts` modules ([#49](https://github.com/sveltejs/ai-tools/pull/49))
## 0.0.3
### Patch Changes
- fix: check effect.pre in assign-in-effect ([#41](https://github.com/sveltejs/mcp/pull/41))
- fix: check effect.pre in assign-in-effect ([#41](https://github.com/sveltejs/ai-tools/pull/41))
- feat: `use_cases` documentation metadata ([#29](https://github.com/sveltejs/mcp/pull/29))
- feat: `use_cases` documentation metadata ([#29](https://github.com/sveltejs/ai-tools/pull/29))
- fix: change title names to allow for claude code to use the prompt ([`725f785`](https://github.com/sveltejs/mcp/commit/725f785766d04e9ed810a7c3f6bcfdb2e2b8234c))
- fix: change title names to allow for claude code to use the prompt ([`725f785`](https://github.com/sveltejs/ai-tools/commit/725f785766d04e9ed810a7c3f6bcfdb2e2b8234c))
- fix: enable doc tools ([`cb316c5`](https://github.com/sveltejs/mcp/commit/cb316c5b3ebc712946969d2d57236d159e796d58))
- fix: enable doc tools ([`cb316c5`](https://github.com/sveltejs/ai-tools/commit/cb316c5b3ebc712946969d2d57236d159e796d58))
## 0.0.2
### Patch Changes
- feat: latest version ([#25](https://github.com/sveltejs/mcp/pull/25))
- feat: latest version ([#25](https://github.com/sveltejs/ai-tools/pull/25))

View File

@@ -1,19 +1,19 @@
{
"name": "@sveltejs/mcp",
"version": "0.1.19",
"version": "0.1.25",
"type": "module",
"license": "MIT",
"mcpName": "dev.svelte/mcp",
"homepage": "https://github.com/sveltejs/mcp#readme",
"homepage": "https://github.com/sveltejs/ai-tools#readme",
"bugs": {
"url": "https://github.com/sveltejs/mcp/issues"
"url": "https://github.com/sveltejs/ai-tools/issues"
},
"bin": {
"svelte-mcp": "./dist/index.mjs"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/mcp.git",
"url": "git+https://github.com/sveltejs/ai-tools.git",
"path": "packages/mcp-stdio"
},
"files": [

View File

@@ -5,11 +5,11 @@
"description": "The official Svelte MCP server providing docs and autofixing tools for Svelte development",
"repository": {
"id": "1054419133",
"url": "https://github.com/sveltejs/mcp",
"url": "https://github.com/sveltejs/ai-tools",
"subfolder": "packages/mcp-stdio",
"source": "github"
},
"version": "0.1.19",
"version": "0.1.25",
"websiteUrl": "https://svelte.dev/docs/mcp/overview",
"icons": [
{
@@ -25,7 +25,7 @@
{
"registryType": "npm",
"identifier": "@sveltejs/mcp",
"version": "0.1.19",
"version": "0.1.25",
"runtimeHint": "npx",
"transport": {
"type": "stdio"

View File

@@ -14,7 +14,9 @@ const cli = sade('svelte-mcp');
cli.command('__mcp', '', { default: true }).action(() => {
const transport = new StdioTransport(server);
transport.listen();
transport.listen({
stdio: true,
});
});
cli

View File

@@ -19,5 +19,6 @@ export default defineConfig([
treeshake: true,
clean: true,
target: 'esnext',
inlineOnly: false,
},
]);

View File

@@ -1,13 +1,79 @@
# @sveltejs/opencode
## 0.1.9
### Patch Changes
- fix: remove restrictive permissions from subagent ([#219](https://github.com/sveltejs/ai-tools/pull/219))
## 0.1.8
### Patch Changes
- fix: add `server` export to opencode plugin ([`96c50ac`](https://github.com/sveltejs/ai-tools/commit/96c50acae2b4131a6c72d3579a73c44ab9158b18))
## 0.1.7
### Patch Changes
- fix: import `ts` files directly ([#190](https://github.com/sveltejs/ai-tools/pull/190))
## 0.1.6
### Patch Changes
- chore: sync skills from svelte.dev ([#178](https://github.com/sveltejs/ai-tools/pull/178))
- fix: update svelte-file-editor agent to use proper name ([#183](https://github.com/sveltejs/ai-tools/pull/183))
## 0.1.5
### Patch Changes
- fix: merge user-configured svelte-file-editor agent settings ([#176](https://github.com/sveltejs/ai-tools/pull/176))
- feat(opencode): mcp enabled option is passed to opencode ([#171](https://github.com/sveltejs/ai-tools/pull/171))
- feat: allow enabling a specific skill in opencode plugin ([#174](https://github.com/sveltejs/ai-tools/pull/174))
## 0.1.4
### Patch Changes
- feat: `svelte-core-bestpractices` skill ([#162](https://github.com/sveltejs/ai-tools/pull/162))
## 0.1.3
### Patch Changes
- fix: better subagent instructions to use MCP or skill ([#163](https://github.com/sveltejs/ai-tools/pull/163))
## 0.1.2
### Patch Changes
- feat: allow for local opencode config ([#156](https://github.com/sveltejs/ai-tools/pull/156))
## 0.1.1
### Patch Changes
- fix: actually push skills to right config path ([`c2c1b3e`](https://github.com/sveltejs/ai-tools/commit/c2c1b3e5e788b14eea17cd37a83ca55433cc4072))
## 0.1.0
### Minor Changes
- feat: distribute skills through opencode plugin ([#151](https://github.com/sveltejs/ai-tools/pull/151))
## 0.0.3
### Patch Changes
- chore: add README to opencode plugin ([`71295bc`](https://github.com/sveltejs/mcp/commit/71295bc11fb7bac6703e655f5fddead29967353c))
- chore: add README to opencode plugin ([`71295bc`](https://github.com/sveltejs/ai-tools/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))
- fix: show toast after a few seconds to avoid race condition ([`57e2d1d`](https://github.com/sveltejs/ai-tools/commit/57e2d1def1f5590d0a3dd6d269ac39f6397ffecf))

View File

@@ -36,39 +36,76 @@ The plugin injects instructions that teach the agent how to effectively use the
## Configuration
The default configuration:
Create `svelte.json` to customize how the plugin configures MCP, the Svelte subagent, instructions, and skills.
```json
{
"$schema": "https://raw.githubusercontent.com/sveltejs/mcp/refs/heads/main/packages/opencode/schema.json",
"$schema": "https://svelte.dev/opencode/schema.json",
"mcp": {
"type": "remote",
"enabled": true
},
"subagent": {
"enabled": true
"enabled": true,
"agents": {
"svelte-file-editor": {
"model": "anthropic/claude-sonnet-4-20250514",
"temperature": 0.7,
"top_p": 0.9,
"maxSteps": 20
}
}
},
"instructions": {
"enabled": true
},
"skills": {
"enabled": ["svelte-code-writer", "svelte-core-bestpractices"]
}
}
```
### Defaults
If omitted, the plugin uses these defaults:
- `mcp.type`: `"remote"`
- `mcp.enabled`: `true`
- `subagent.enabled`: `true`
- `subagent.agents`: `{}`
- `instructions.enabled`: `true`
- `skills.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 |
| Option | Type | Default | Description |
| ------------------------------------------------ | --------------------- | ---------- | ---------------------------------------------------------------------------------------------- |
| `mcp.type` | `"remote" \| "local"` | `"remote"` | Use `https://mcp.svelte.dev/mcp` (`remote`) or run `@sveltejs/mcp` via `npx` (`local`). |
| `mcp.enabled` | `boolean` | `true` | Enable or disable the Svelte MCP server entry. |
| `subagent.enabled` | `boolean` | `true` | Enable or disable registration of the `svelte-file-editor` subagent. |
| `subagent.agents.svelte-file-editor.model` | `string` | main agent | Override the model used by the Svelte file editor subagent. |
| `subagent.agents.svelte-file-editor.temperature` | `number` | unset | Set temperature for the subagent. |
| `subagent.agents.svelte-file-editor.top_p` | `number` | unset | Set top-p sampling for the subagent. |
| `subagent.agents.svelte-file-editor.maxSteps` | `number` | unlimited | Limit the number of steps the subagent can execute. |
| `instructions.enabled` | `boolean` | `true` | Enable or disable automatic instruction-file injection. |
| `skills.enabled` | `boolean \| string[]` | `true` | Enable all skills (`true`), disable all skills (`false`), or enable only specific skill names. |
### Config File Location
### Supported Skill Names
Place your configuration at one of these locations:
When using `skills.enabled` as an array, these built-in names are currently available:
- `~/.config/opencode/svelte.json` (global)
- `$OPENCODE_CONFIG_DIR/svelte.json` (if `OPENCODE_CONFIG_DIR` is set, takes priority)
- `svelte-code-writer`
- `svelte-core-bestpractices`
### Config File Locations and Precedence
The plugin reads from these files (lowest priority first, highest priority last):
- `~/.config/opencode/svelte.json`
- `$OPENCODE_CONFIG_DIR/svelte.json` (when `OPENCODE_CONFIG_DIR` is set)
- `.opencode/svelte.json` in the current project
If the same key is defined in multiple files, the later location overrides earlier ones.
## License

View File

@@ -0,0 +1,11 @@
// This file is auto-generated by scripts/sync-opencode-plugin.ts
// Do not edit manually — edit the markdown files in tools/agents/ instead.
export const agents = {
'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 or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.',
prompt:
"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.\n\nIf the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.\n\nIf the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.\n\n## Available MCP Tools\n\n### 1. list-sections\n\nLists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.\n\n### 2. get-documentation\n\nRetrieves 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.\n\n**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`\n\n### 3. svelte-autofixer\n\nAnalyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:\n\n- Using `$effect` instead of `$derived` for computations\n- Missing cleanup in effects\n- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)\n- Missing keys in `{#each}` blocks\n- And more\n\n## Workflow\n\nWhen invoked to work on a Svelte file:\n\n### 1. Gather Context (if needed)\n\nIf you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:\n\n1. Call `list-sections` to see available documentation\n2. Call `get-documentation` with relevant section names\n\n### 2. Read the Target File\n\nRead the file to understand the current implementation.\n\n### 3. Make Changes\n\nApply edits following Svelte 5 best practices:\n\n### 4. Validate Changes\n\nAfter editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.\n\n### 5. Fix Any Issues\n\nIf the autofixer reports problems, fix them and re-validate until no issues remain.\n\n## Output Format\n\nAfter completing your work, provide:\n\n1. Summary of changes made\n2. Any issues found and fixed by the autofixer\n3. Recommendations for further improvements (if any)",
},
} as const;

View File

@@ -4,6 +4,28 @@ import { homedir } from 'os';
import { join } from 'path';
import * as v from 'valibot';
// Schema for individual agent configuration
const agent_config_schema = v.object({
model: v.pipe(
v.optional(v.string()),
v.description('Model identifier for the agent (e.g., "anthropic/claude-sonnet-4-20250514")'),
),
temperature: v.pipe(
v.optional(v.number()),
v.description('Temperature setting for the agent (e.g., 0.7)'),
),
top_p: v.pipe(
v.optional(v.number()),
v.description(
'Control response diversity with the top_p option. Alternative to temperature for controlling randomness.',
),
),
maxSteps: v.pipe(
v.optional(v.number()),
v.description('Maximum number of steps the agent can take (e.g., 10)'),
),
});
const default_config = {
mcp: {
type: 'remote' as 'remote' | 'local',
@@ -11,28 +33,61 @@ const default_config = {
},
subagent: {
enabled: true,
agents: {} as Record<string, v.InferInput<typeof agent_config_schema>>,
},
instructions: {
enabled: true,
},
skills: {
enabled: true as boolean | string[],
},
};
export const config_schema = v.object({
mcp: v.optional(
v.object({
type: v.optional(v.picklist(['remote', 'local'])),
enabled: v.optional(v.boolean()),
}),
mcp: v.pipe(
v.optional(
v.object({
type: v.optional(v.picklist(['remote', 'local'])),
enabled: v.optional(v.boolean()),
}),
),
v.description(
"Configuration for the MCP. You can chose if it should be enabled or not and the transport to use 'remote' (default) and 'local'.",
),
),
subagent: v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
subagent: v.pipe(
v.optional(
v.object({
enabled: v.optional(v.boolean()),
agents: v.optional(v.record(v.string(), agent_config_schema)),
}),
),
v.description('Configuration for the subagent. You can choose if it should be enabled or not.'),
),
instructions: v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
instructions: v.pipe(
v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
),
v.description(
'Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not.',
),
),
skills: v.pipe(
v.optional(
v.object({
enabled: v.pipe(
v.optional(v.union([v.boolean(), v.array(v.string())])),
v.description(
'It can be either a boolean or an array containing the skills that you want to enable',
),
),
}),
),
v.description(
'Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills.',
),
),
});
@@ -63,8 +118,15 @@ function get_config_paths() {
}
}
// returning config_dir first so it has higher priority
return [config_dir_path, global_path];
// Project-local: ./.opencode/svelte.json (cwd)
let project_path: string | null = null;
const project_config = join(process.cwd(), '.opencode', 'svelte.json');
if (existsSync(project_config)) {
project_path = project_config;
}
// Lowest priority first, highest priority last (project overrides global)
return [global_path, config_dir_path, project_path];
}
function load_config_file(config_path: string): ConfigLoadResult {
@@ -97,18 +159,29 @@ function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
...user_config.mcp,
},
subagent: {
...default_config.subagent,
enabled: default_config.subagent.enabled,
...user_config.subagent,
agents: {
...default_config.subagent.agents,
...user_config.subagent?.agents,
},
},
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();
let merged: Partial<McpConfig> = {};
// Iterate from lowest to highest priority, merging as we go
for (const path of config_paths) {
if (path && existsSync(path)) {
const result = load_config_file(path);
@@ -117,23 +190,32 @@ export function get_mcp_config(ctx: PluginInput) {
ctx.client.tui.showToast({
body: {
title: 'Svelte: Invalid opencode plugin config',
message: `${result.parse_error}\nUsing default values`,
message: `${result.parse_error} (${path})\nSkipping this config file`,
variant: 'warning',
duration: 7000,
},
});
}, 7000);
return default_config;
continue;
}
const parsed = v.safeParse(config_schema, result.data);
if (parsed.success) {
return merge_with_defaults(parsed.output);
merged = {
mcp: { ...merged.mcp, ...parsed.output.mcp },
subagent: {
...merged.subagent,
...parsed.output.subagent,
agents: { ...merged.subagent?.agents, ...parsed.output.subagent?.agents },
},
instructions: { ...merged.instructions, ...parsed.output.instructions },
skills: { ...merged.skills, ...parsed.output.skills },
};
} else {
setTimeout(() => {
ctx.client.tui.showToast({
body: {
title: 'Svelte: Invalid opencode plugin config',
message: `${result.parse_error}\nUsing default values`,
message: `Invalid config schema (${path})\nSkipping this config file`,
variant: 'warning',
duration: 7000,
},
@@ -143,5 +225,5 @@ export function get_mcp_config(ctx: PluginInput) {
}
}
return default_config;
return merge_with_defaults(merged);
}

View File

@@ -2,7 +2,8 @@ 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';
import { agents } from './agents.ts';
import { get_mcp_config } from './config.ts';
const current_dir = dirname(fileURLToPath(import.meta.url));
@@ -12,6 +13,10 @@ export const svelte_plugin: Plugin = async (ctx) => {
input.agent ??= {};
input.mcp ??= {};
input.instructions ??= [];
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
input.skills ??= {};
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
input.skills.paths ??= [];
// 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
@@ -35,37 +40,72 @@ export const svelte_plugin: Plugin = async (ctx) => {
input.instructions.push(...instructions_paths.map((file) => join(instructions_dir, file)));
}
const skills_enabled = mcp_config.skills?.enabled;
if (skills_enabled !== false) {
const skills_dir = join(current_dir, 'skills');
if (Array.isArray(skills_enabled)) {
// only add specific skill directories by name
for (const skill_name of skills_enabled) {
const skill_path = join(skills_dir, skill_name);
// @ts-expect-error -- skills is a new opencode feature
input.skills.paths.push(skill_path);
}
} else {
// @ts-expect-error -- skills is a new opencode feature
input.skills.paths.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 (!input.mcp[svelte_mcp_name]) {
if (mcp_config.mcp?.type === 'remote') {
input.mcp[svelte_mcp_name] = {
type: 'remote',
url: 'https://mcp.svelte.dev/mcp',
enabled: mcp_config.mcp?.enabled ?? true,
};
} else {
input.mcp[svelte_mcp_name] = {
type: 'local',
command: ['npx', '-y', '@sveltejs/mcp'],
enabled: mcp_config.mcp?.enabled ?? true,
};
}
}
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,
},
};
for (const [agent_name, agent_data] of Object.entries(agents)) {
// we add the editor subagent that will be used when editing Svelte files to prevent wasting context on the main agent
const default_config: (typeof input.agent)[string] = {
color: '#ff3e00',
mode: 'subagent',
prompt: agent_data.prompt,
description: agent_data.description,
permission: {
[`${svelte_mcp_name}_*`]: 'allow',
},
};
// Get per-agent config from svelte.json (if any)
const agent_config = mcp_config.subagent?.agents?.[agent_name];
// Configure agent from svelte.json only
// Priority: svelte.json agent config > defaults
input.agent[agent_name] = {
...default_config,
...(agent_config?.model !== undefined && {
model: agent_config.model,
}),
...(agent_config?.temperature !== undefined && {
temperature: agent_config.temperature,
}),
...(agent_config?.maxSteps !== undefined && {
maxSteps: agent_config.maxSteps,
}),
...(agent_config?.top_p !== undefined && {
top_p: agent_config.top_p,
}),
};
}
}
},
};

View File

@@ -1,6 +1,6 @@
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:
## Available Svelte MCP Tools:
### 1. list-sections

View File

@@ -1,11 +1,11 @@
{
"name": "@sveltejs/opencode",
"version": "0.0.3",
"version": "0.1.9",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/sveltejs/mcp#readme",
"homepage": "https://github.com/sveltejs/ai-tools#readme",
"bugs": {
"url": "https://github.com/sveltejs/mcp/issues"
"url": "https://github.com/sveltejs/ai-tools/issues"
},
"scripts": {
"check": "tsc --noEmit",
@@ -14,11 +14,19 @@
"files": [
"index.ts",
"config.ts",
"instructions"
"agents.ts",
"instructions",
"skills"
],
"exports": {
"./server": {
"types": "./index.ts",
"import": "./index.ts"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/mcp.git",
"url": "git+https://github.com/sveltejs/ai-tools.git",
"path": "packages/opencode"
},
"publishConfig": {

View File

@@ -14,16 +14,55 @@
"type": "boolean"
}
},
"required": []
"required": [],
"description": "Configuration for the MCP. You can chose if it should be enabled or not and the transport to use 'remote' (default) and 'local'."
},
"subagent": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"agents": {
"type": "object",
"propertyNames": {
"anyOf": [
{
"enum": [
"svelte-file-editor"
]
},
{
"type": "string"
}
]
},
"additionalProperties": {
"type": "object",
"properties": {
"model": {
"type": "string",
"description": "Model identifier for the agent (e.g., \"anthropic/claude-sonnet-4-20250514\")"
},
"temperature": {
"type": "number",
"description": "Temperature setting for the agent (e.g., 0.7)"
},
"top_p": {
"type": "number",
"description": "Control response diversity with the top_p option. Alternative to temperature for controlling randomness."
},
"maxSteps": {
"type": "number",
"description": "Maximum number of steps the agent can take (e.g., 10)"
}
},
"required": []
}
}
},
"required": []
"required": [],
"description": "Configuration for the subagent. You can choose if it should be enabled or not."
},
"instructions": {
"type": "object",
@@ -32,7 +71,39 @@
"type": "boolean"
}
},
"required": []
"required": [],
"description": "Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not."
},
"skills": {
"type": "object",
"properties": {
"enabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "array",
"items": {
"anyOf": [
{
"enum": [
"svelte-code-writer",
"svelte-core-bestpractices"
]
},
{
"type": "string"
}
]
}
}
],
"description": "It can be either a boolean or an array containing the skills that you want to enable"
}
},
"required": [],
"description": "Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills."
}
},
"required": [],

View File

@@ -3,6 +3,58 @@ import { config_schema } from '../config.js';
import fs from 'node:fs';
import path from 'node:path';
const json_schema = toJsonSchema(config_schema);
// Read agent names from tools/agents/*.md files
function get_agent_names(agents_dir: string) {
if (!fs.existsSync(agents_dir)) return [];
return fs
.readdirSync(agents_dir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
.map((entry) => entry.name.replace(/\.md$/, ''));
}
function get_skill_names(skills_dir: string) {
if (!fs.existsSync(skills_dir)) return [];
return fs
.readdirSync(skills_dir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
}
const skills_dir = path.resolve('./skills');
const skill_names = get_skill_names(skills_dir);
const schema = config_schema;
const json_schema = toJsonSchema(schema);
// Post-process: inject skill name suggestions into the items schema.
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
// editors will autocomplete the known names but any string is still valid.
if (skill_names.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const enabled = (json_schema as any).properties?.skills?.properties?.enabled;
if (enabled?.anyOf) {
const array_branch = enabled.anyOf.find((s: Record<string, unknown>) => s.type === 'array');
if (array_branch) {
array_branch.items = {
anyOf: [{ enum: skill_names }, { type: 'string' }],
};
}
}
}
// Post-process: inject known agent names for intellisense
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
// editors will autocomplete the known names but any string is still valid.
const agents_dir = path.resolve('../../tools/agents');
const agent_names = get_agent_names(agents_dir);
if (agent_names.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const agents = (json_schema as any).properties?.subagent?.properties?.agents;
if (agents) {
agents.propertyNames = {
anyOf: [{ enum: agent_names }, { type: 'string' }],
};
}
}
fs.writeFileSync(path.resolve('./schema.json'), JSON.stringify(json_schema, null, '\t'));

View File

@@ -1,6 +1,6 @@
---
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.
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing 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

View File

@@ -0,0 +1,176 @@
---
name: svelte-core-bestpractices
description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more.
---
## `$state`
Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable.
Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example.
## `$derived`
To compute something from state, use `$derived` rather than `$effect`:
```js
// do this
let square = $derived(num * num);
// don't do this
let square;
$effect(() => {
square = num * num;
});
```
> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`.
Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes.
If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this.
## `$effect`
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md)
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
## `$props`
Treat props as though they will change. For example, values that depend on props should usually use `$derived`:
```js
// @errors: 2451
let { type } = $props();
// do this
let color = $derived(type === 'danger' ? 'red' : 'green');
// don't do this — `color` will not update if `type` changes
let color = type === 'danger' ? 'red' : 'green';
```
## `$inspect.trace`
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
## Events
Any element attribute starting with `on` is treated as an event listener:
```svelte
<button onclick={() => {...}}>click me</button>
<!-- attribute shorthand also works -->
<button {onclick}>...</button>
<!-- so do spread attributes -->
<button {...props}>...</button>
```
If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`:
```svelte
<svelte:window onkeydown={...} />
<svelte:document onvisibilitychange={...} />
```
Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}
<p>hello {name}!</p>
{/snippet}
{@render greeting('world')}
```
> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components.
## Each blocks
Prefer to use [keyed each blocks](references/each.md) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items.
> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key.
Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example).
## Using JavaScript variables in CSS
If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive.
```svelte
<div style:--columns={columns}>...</div>
```
You can then reference `var(--columns)` inside the component's `<style>`.
## Styling child components
The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties:
```svelte
<!-- Parent.svelte -->
<Child --color="red" />
<!-- Child.svelte -->
<h1>Hello</h1>
<style>
h1 {
color: var(--color);
}
</style>
```
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
```svelte
<div>
<Child />
</div>
<style>
div :global {
h1 {
color: red;
}
}
</style>
```
## Context
Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering.
Use `createContext` rather than `setContext` and `getContext`, as it provides type safety.
## Async Svelte
If using version 5.36 or higher, you can use [await expressions](references/await-expressions.md) and [hydratable](references/hydratable.md) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
## Avoid legacy features
Always use runes mode for new code, and avoid features that have more modern replacements:
- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`)
- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution)
- use `$props` instead of `export let`, `$$props` and `$$restProps`
- use `onclick={...}` instead of `on:click={...}`
- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>`
- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>`
- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>`
- use classes with `$state` fields to share reactivity between components, instead of using stores
- use `{@attach ...}` instead of `use:action`
- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive

View File

@@ -0,0 +1,53 @@
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
```svelte
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // will console.log when `count` or `message` change
</script>
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
```
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
```svelte
<script>
let count = $state(0);
$inspect(count).with((type, count) => {
if (type === 'update') {
debugger; // or `console.trace`, or whatever you want
}
});
</script>
<button onclick={() => count++}>Increment</button>
```
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
```svelte
<script>
import { doSomeWork } from './elsewhere';
$effect(() => {
+++// $inspect.trace must be the first statement of a function body+++
+++$inspect.trace();+++
doSomeWork();
});
</script>
```
`$inspect.trace` takes an optional first argument which will be used as the label.

View File

@@ -0,0 +1,166 @@
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
> [!NOTE]
> Attachments are available in Svelte 5.29 and newer.
```svelte
<!--- file: App.svelte --->
<script>
/** @type {import('svelte/attachments').Attachment} */
function myAttachment(element) {
console.log(element.nodeName); // 'DIV'
return () => {
console.log('cleaning up');
};
}
</script>
<div {@attach myAttachment}>...</div>
```
An element can have any number of attachments.
## Attachment factories
A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment (demo:
```svelte
<!--- file: App.svelte --->
<script>
import tippy from 'tippy.js';
let content = $state('Hello!');
/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
const tooltip = tippy(element, { content });
return tooltip.destroy;
};
}
</script>
<input bind:value={content} />
<button {@attach tooltip(content)}> Hover me </button>
```
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
## Inline attachments
Attachments can also be created inline (demo:
```svelte
<!--- file: App.svelte --->
<canvas
width={32}
height={32}
{@attach (canvas) => {
const context = canvas.getContext('2d');
$effect(() => {
context.fillStyle = color;
context.fillRect(0, 0, canvas.width, canvas.height);
});
}}
></canvas>
```
> [!NOTE]
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
## Conditional attachments
Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage:
```svelte
<div {@attach enabled && myAttachment}>...</div>
```
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
This allows you to create _wrapper components_ that augment elements (demo:
```svelte
<!--- file: Button.svelte --->
<script>
/** @type {import('svelte/elements').HTMLButtonAttributes} */
let { children, ...props } = $props();
</script>
<!-- `props` includes attachments -->
<button {...props}>
{@render children?.()}
</button>
```
```svelte
<!--- file: App.svelte --->
<script>
import tippy from 'tippy.js';
import Button from './Button.svelte';
let content = $state('Hello!');
/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
const tooltip = tippy(element, { content });
return tooltip.destroy;
};
}
</script>
<input bind:value={content} />
<Button {@attach tooltip(content)}>Hover me</Button>
```
## Controlling when attachments re-run
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
```js
// @errors: 7006 2304 2552
function foo(bar) {
return (node) => {
veryExpensiveSetupWork(node);
update(node, bar);
};
}
```
In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
```js
// @errors: 7006 2304 2552
function foo(+++getBar+++) {
return (node) => {
veryExpensiveSetupWork(node);
+++ $effect(() => {
update(node, getBar());
});+++
}
}
```
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
## Converting actions to attachments
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.

View File

@@ -0,0 +1,35 @@
To render a [snippet](snippet), use a `{@render ...}` tag.
```svelte
{#snippet sum(a, b)}
<p>{a} + {b} = {a + b}</p>
{/snippet}
{@render sum(1, 2)}
{@render sum(3, 4)}
{@render sum(5, 6)}
```
The expression can be an identifier like `sum`, or an arbitrary JavaScript expression:
```svelte
{@render (cool ? coolSnippet : lameSnippet)()}
```
## Optional snippets
If the snippet is potentially undefined — for example, because it's an incoming prop — then you can use optional chaining to only render it when it _is_ defined:
```svelte
{@render children?.()}
```
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
```svelte
{#if children}
{@render children()}
{:else}
<p>fallback content</p>
{/if}
```

Some files were not shown because too many files have changed in this diff Show More