Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib/mcp/handlers/prompts/svelte-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ This is the task you will work on:
<task>
${task}
</task>

If you are not writing the code into a file, once you have the final version of the code ask the user if it wants to generate a playground link to quickly check the code in it and if it answer yes call the \`playground-link\` tool and return the url to the user nicely formatted. The playground link MUST be generated only once you have the final version of the code and you are ready to share it, it MUST include an entry point file called \`App.svelte\` where the main component should live. If you have multiple files to include in the playground link you can include them all at the root.
`,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/lib/mcp/handlers/tools/get-documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions src/lib/mcp/handlers/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './get-documentation.js';
export * from './list-sections.js';
export * from './svelte-autofixer.js';
export * from './playground-link.js';
2 changes: 1 addition & 1 deletion src/lib/mcp/handlers/tools/list-sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
},
Expand Down
112 changes: 112 additions & 0 deletions src/lib/mcp/handlers/tools/playground-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { SvelteMcp } from '../../index.js';
import * as v from 'valibot';

async function compress_and_encode_text(input: string) {
const reader = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip')).getReader();
let buffer = '';
for (;;) {
const { done, value } = await reader.read();
if (done) {
reader.releaseLock();
// Some sites like discord don't like it when links end with =
return btoa(buffer).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
} else {
for (let i = 0; i < value.length; i++) {
// decoding as utf-8 will make btoa reject the string
buffer += String.fromCharCode(value[i]);
}
}
}
}

type File = {
type: 'file';
name: string;
basename: string;
contents: string;
text: boolean;
};

export function playground_link(server: SvelteMcp) {
server.tool(
{
name: 'playground-link',
description:
'Generates a Playground link given a Svelte code snippet. Once you have the final version of the code you want to send to the user, ALWAYS ask the user if it wants a playground link to allow it to quickly check the code in the playground before calling this tool. NEVER use this tool if you have written the component to a file in the user project. The playground accept multiple files so if are importing from other files just include them all at the root level.',
schema: v.object({
name: v.pipe(
v.string(),
v.description('The name of the Playground, it should reflect the user task'),
),
tailwind: v.pipe(
v.boolean(),
v.description(
"If the code requires Tailwind CSS to work...only send true if it it's using tailwind classes in the code",
),
),
files: v.pipe(
v.record(v.string(), v.string()),
v.description(
"An object where all the keys are the filenames (with extensions) and the values are the file content. For example: { 'Component.svelte': '<script>...</script>', 'utils.js': 'export function ...' }. The playground accept multiple files so if are importing from other files just include them all at the root level.",
),
),
}),
outputSchema: v.object({
url: v.string(),
}),
},
async ({ files, name, tailwind }) => {
const playground_base = new URL('https://svelte.dev/playground');
const playground_files: File[] = [];

let has_app_svelte = false;

for (const [filename, contents] of Object.entries(files)) {
if (filename === 'App.svelte') has_app_svelte = true;
playground_files.push({
type: 'file',
name: filename,
basename: filename.replace(/^.*[\\/]/, ''),
contents,
text: true,
});
}

if (!has_app_svelte) {
return {
isError: true,
content: [
{
type: 'text',
text: JSON.stringify({
error: 'The files must contain an App.svelte file as the entry point',
}),
},
],
};
}

const playground_config = {
name,
tailwind: tailwind ?? false,
files: playground_files,
};

playground_base.hash = await compress_and_encode_text(JSON.stringify(playground_config));

const content = {
url: playground_base.toString(),
};

return {
content: [
{
type: 'text',
text: JSON.stringify(content),
},
],
structuredContent: content,
};
},
);
}