Skip to content

Commit d93d3a3

Browse files
Merge pull request #17 from sveltejs/playground-link-tool
2 parents 224d630 + 039718f commit d93d3a3

File tree

5 files changed

+117
-2
lines changed

5 files changed

+117
-2
lines changed

src/lib/mcp/handlers/prompts/svelte-task.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ This is the task you will work on:
3434
<task>
3535
${task}
3636
</task>
37+
38+
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.
3739
`,
3840
},
3941
},

src/lib/mcp/handlers/tools/get-documentation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as v from 'valibot';
44
export function get_documentation(server: SvelteMcp) {
55
server.tool(
66
{
7-
name: 'get_documentation',
7+
name: 'get-documentation',
88
description:
99
'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.',
1010
schema: v.object({
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './get-documentation.js';
22
export * from './list-sections.js';
33
export * from './svelte-autofixer.js';
4+
export * from './playground-link.js';

src/lib/mcp/handlers/tools/list-sections.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { SvelteMcp } from '../../index.js';
33
export function list_sections(server: SvelteMcp) {
44
server.tool(
55
{
6-
name: 'list_sections',
6+
name: 'list-sections',
77
description:
88
'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.',
99
},
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { SvelteMcp } from '../../index.js';
2+
import * as v from 'valibot';
3+
4+
async function compress_and_encode_text(input: string) {
5+
const reader = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip')).getReader();
6+
let buffer = '';
7+
for (;;) {
8+
const { done, value } = await reader.read();
9+
if (done) {
10+
reader.releaseLock();
11+
// Some sites like discord don't like it when links end with =
12+
return btoa(buffer).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
13+
} else {
14+
for (let i = 0; i < value.length; i++) {
15+
// decoding as utf-8 will make btoa reject the string
16+
buffer += String.fromCharCode(value[i]);
17+
}
18+
}
19+
}
20+
}
21+
22+
type File = {
23+
type: 'file';
24+
name: string;
25+
basename: string;
26+
contents: string;
27+
text: boolean;
28+
};
29+
30+
export function playground_link(server: SvelteMcp) {
31+
server.tool(
32+
{
33+
name: 'playground-link',
34+
description:
35+
'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.',
36+
schema: v.object({
37+
name: v.pipe(
38+
v.string(),
39+
v.description('The name of the Playground, it should reflect the user task'),
40+
),
41+
tailwind: v.pipe(
42+
v.boolean(),
43+
v.description(
44+
"If the code requires Tailwind CSS to work...only send true if it it's using tailwind classes in the code",
45+
),
46+
),
47+
files: v.pipe(
48+
v.record(v.string(), v.string()),
49+
v.description(
50+
"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.",
51+
),
52+
),
53+
}),
54+
outputSchema: v.object({
55+
url: v.string(),
56+
}),
57+
},
58+
async ({ files, name, tailwind }) => {
59+
const playground_base = new URL('https://svelte.dev/playground');
60+
const playground_files: File[] = [];
61+
62+
let has_app_svelte = false;
63+
64+
for (const [filename, contents] of Object.entries(files)) {
65+
if (filename === 'App.svelte') has_app_svelte = true;
66+
playground_files.push({
67+
type: 'file',
68+
name: filename,
69+
basename: filename.replace(/^.*[\\/]/, ''),
70+
contents,
71+
text: true,
72+
});
73+
}
74+
75+
if (!has_app_svelte) {
76+
return {
77+
isError: true,
78+
content: [
79+
{
80+
type: 'text',
81+
text: JSON.stringify({
82+
error: 'The files must contain an App.svelte file as the entry point',
83+
}),
84+
},
85+
],
86+
};
87+
}
88+
89+
const playground_config = {
90+
name,
91+
tailwind: tailwind ?? false,
92+
files: playground_files,
93+
};
94+
95+
playground_base.hash = await compress_and_encode_text(JSON.stringify(playground_config));
96+
97+
const content = {
98+
url: playground_base.toString(),
99+
};
100+
101+
return {
102+
content: [
103+
{
104+
type: 'text',
105+
text: JSON.stringify(content),
106+
},
107+
],
108+
structuredContent: content,
109+
};
110+
},
111+
);
112+
}

0 commit comments

Comments
 (0)