diff --git a/src/lib/mcp/handlers/prompts/svelte-task.ts b/src/lib/mcp/handlers/prompts/svelte-task.ts
index 2c7d9f4..958eef7 100644
--- a/src/lib/mcp/handlers/prompts/svelte-task.ts
+++ b/src/lib/mcp/handlers/prompts/svelte-task.ts
@@ -34,6 +34,8 @@ 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.
`,
},
},
diff --git a/src/lib/mcp/handlers/tools/get-documentation.ts b/src/lib/mcp/handlers/tools/get-documentation.ts
index a3f7b19..9c3ea81 100644
--- a/src/lib/mcp/handlers/tools/get-documentation.ts
+++ b/src/lib/mcp/handlers/tools/get-documentation.ts
@@ -4,7 +4,7 @@ import * as v from 'valibot';
export function get_documentation(server: SvelteMcp) {
server.tool(
{
- name: 'get_documentation',
+ name: 'get-documentation',
description:
'Retrieves full documentation content for Svelte 5 or SvelteKit sections. Supports flexible search by title (e.g., "$state", "routing") or file path (e.g., "docs/svelte/state.md"). Can accept a single section name or an array of sections. Before running this, make sure to analyze the users query, as well as the output from list_sections (which should be called first). Then ask for ALL relevant sections the user might require. For example, if the user asks to build anything interactive, you will need to fetch all relevant runes, and so on.',
schema: v.object({
diff --git a/src/lib/mcp/handlers/tools/index.ts b/src/lib/mcp/handlers/tools/index.ts
index 1069185..6414054 100644
--- a/src/lib/mcp/handlers/tools/index.ts
+++ b/src/lib/mcp/handlers/tools/index.ts
@@ -1,3 +1,4 @@
export * from './get-documentation.js';
export * from './list-sections.js';
export * from './svelte-autofixer.js';
+export * from './playground-link.js';
diff --git a/src/lib/mcp/handlers/tools/list-sections.ts b/src/lib/mcp/handlers/tools/list-sections.ts
index c821455..bbc3243 100644
--- a/src/lib/mcp/handlers/tools/list-sections.ts
+++ b/src/lib/mcp/handlers/tools/list-sections.ts
@@ -3,7 +3,7 @@ import type { SvelteMcp } from '../../index.js';
export function list_sections(server: SvelteMcp) {
server.tool(
{
- name: 'list_sections',
+ name: 'list-sections',
description:
'Lists all available Svelte 5 and SvelteKit documentation sections in a structured format. Returns sections as a list of "* title: [section_title], path: [file_path]" - you can use either the title or path when querying a specific section via the get_documentation tool. Always run list_sections first for any query related to Svelte development to discover available content.',
},
diff --git a/src/lib/mcp/handlers/tools/playground-link.ts b/src/lib/mcp/handlers/tools/playground-link.ts
new file mode 100644
index 0000000..f2e8bf3
--- /dev/null
+++ b/src/lib/mcp/handlers/tools/playground-link.ts
@@ -0,0 +1,112 @@
+import type { SvelteMcp } from '../../index.js';
+import * as v from 'valibot';
+
+async function compress_and_encode_text(input: string) {
+ const reader = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip')).getReader();
+ let buffer = '';
+ for (;;) {
+ const { done, value } = await reader.read();
+ if (done) {
+ reader.releaseLock();
+ // Some sites like discord don't like it when links end with =
+ return btoa(buffer).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
+ } else {
+ for (let i = 0; i < value.length; i++) {
+ // decoding as utf-8 will make btoa reject the string
+ buffer += String.fromCharCode(value[i]);
+ }
+ }
+ }
+}
+
+type File = {
+ type: 'file';
+ name: string;
+ basename: string;
+ contents: string;
+ text: boolean;
+};
+
+export function playground_link(server: SvelteMcp) {
+ server.tool(
+ {
+ name: 'playground-link',
+ description:
+ 'Generates a Playground link given a Svelte code snippet. Once you have the final version of the code you want to send to the user, ALWAYS ask the user if it wants a playground link to allow it to quickly check the code in the playground before calling this tool. NEVER use this tool if you have written the component to a file in the user project. The playground accept multiple files so if are importing from other files just include them all at the root level.',
+ schema: v.object({
+ name: v.pipe(
+ v.string(),
+ v.description('The name of the Playground, it should reflect the user task'),
+ ),
+ tailwind: v.pipe(
+ v.boolean(),
+ v.description(
+ "If the code requires Tailwind CSS to work...only send true if it it's using tailwind classes in the code",
+ ),
+ ),
+ files: v.pipe(
+ v.record(v.string(), v.string()),
+ v.description(
+ "An object where all the keys are the filenames (with extensions) and the values are the file content. For example: { 'Component.svelte': '', 'utils.js': 'export function ...' }. The playground accept multiple files so if are importing from other files just include them all at the root level.",
+ ),
+ ),
+ }),
+ outputSchema: v.object({
+ url: v.string(),
+ }),
+ },
+ async ({ files, name, tailwind }) => {
+ const playground_base = new URL('https://svelte.dev/playground');
+ const playground_files: File[] = [];
+
+ let has_app_svelte = false;
+
+ for (const [filename, contents] of Object.entries(files)) {
+ if (filename === 'App.svelte') has_app_svelte = true;
+ playground_files.push({
+ type: 'file',
+ name: filename,
+ basename: filename.replace(/^.*[\\/]/, ''),
+ contents,
+ text: true,
+ });
+ }
+
+ if (!has_app_svelte) {
+ return {
+ isError: true,
+ content: [
+ {
+ type: 'text',
+ text: JSON.stringify({
+ error: 'The files must contain an App.svelte file as the entry point',
+ }),
+ },
+ ],
+ };
+ }
+
+ const playground_config = {
+ name,
+ tailwind: tailwind ?? false,
+ files: playground_files,
+ };
+
+ playground_base.hash = await compress_and_encode_text(JSON.stringify(playground_config));
+
+ const content = {
+ url: playground_base.toString(),
+ };
+
+ return {
+ content: [
+ {
+ type: 'text',
+ text: JSON.stringify(content),
+ },
+ ],
+ structuredContent: content,
+ };
+ },
+ );
+}