mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-07-05 12:12:00 +08:00
Compare commits
5 Commits
@sveltejs/
...
js-api-and
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77c1728ab1 | ||
|
|
83e9d6583a | ||
|
|
5d8a85c2e3 | ||
|
|
eac75d3629 | ||
|
|
59abf83bdb |
5
.changeset/moody-rules-relate.md
Normal file
5
.changeset/moody-rules-relate.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@sveltejs/mcp': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: expose tools as JS api + cli
|
||||||
@@ -14,7 +14,8 @@
|
|||||||
"debug:generate-summaries": "DEBUG_MODE=1 node scripts/generate-summaries.ts --experimental-strip-types"
|
"debug:generate-summaries": "DEBUG_MODE=1 node scripts/generate-summaries.ts --experimental-strip-types"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts",
|
||||||
|
"./handlers": "./src/mcp/handlers/tools/handlers.ts"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"drizzle-orm": "^0.45.0"
|
"drizzle-orm": "^0.45.0"
|
||||||
|
|||||||
@@ -5,26 +5,18 @@ import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js';
|
|||||||
import { icons } from '../../icons/index.js';
|
import { icons } from '../../icons/index.js';
|
||||||
import { tool } from 'tmcp/utils';
|
import { tool } from 'tmcp/utils';
|
||||||
|
|
||||||
export function get_documentation(server: SvelteMcp) {
|
const get_documentation_schema = v.object({
|
||||||
server.tool(
|
|
||||||
{
|
|
||||||
name: 'get-documentation',
|
|
||||||
description:
|
|
||||||
'Retrieves full documentation content for Svelte 5 or SvelteKit sections. Supports flexible search by title (e.g., "$state", "routing") or file path (e.g., "cli/overview"). Can accept a single section name or an array of sections. Before running this, make sure to analyze the users query, as well as the output from list-sections (which should be called first). Then ask for ALL relevant sections the user might require. For example, if the user asks to build anything interactive, you will need to fetch all relevant runes, and so on. Before calling this tool, try to implement Svelte components using your own knowledge and the `svelte-autofixer` tool, since calling this tool is token intensive.',
|
|
||||||
schema: v.object({
|
|
||||||
section: v.pipe(
|
section: v.pipe(
|
||||||
v.union([v.string(), v.array(v.string())]),
|
v.union([v.string(), v.array(v.string())]),
|
||||||
v.description(
|
v.description(
|
||||||
'The section name(s) to retrieve. Can search by title (e.g., "$state", "load functions") or file path (e.g., "cli/overview"). Supports single string and array of strings',
|
'The section name(s) to retrieve. Can search by title (e.g., "$state", "load functions") or file path (e.g., "cli/overview"). Supports single string and array of strings',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
});
|
||||||
icons,
|
|
||||||
},
|
export async function get_documentation_handler({
|
||||||
async ({ section }) => {
|
section,
|
||||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
}: v.InferInput<typeof get_documentation_schema>) {
|
||||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'get-documentation');
|
|
||||||
}
|
|
||||||
let sections: string[];
|
let sections: string[];
|
||||||
|
|
||||||
if (Array.isArray(section)) {
|
if (Array.isArray(section)) {
|
||||||
@@ -108,7 +100,42 @@ export function get_documentation(server: SvelteMcp) {
|
|||||||
final_text += `\n\n---\n\n${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
|
final_text += `\n\n---\n\n${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tool.text(final_text);
|
return final_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_documentation(server: SvelteMcp) {
|
||||||
|
server.tool(
|
||||||
|
{
|
||||||
|
name: 'get-documentation',
|
||||||
|
description:
|
||||||
|
'Retrieves full documentation content for Svelte 5 or SvelteKit sections. Supports flexible search by title (e.g., "$state", "routing") or file path (e.g., "cli/overview"). Can accept a single section name or an array of sections. Before running this, make sure to analyze the users query, as well as the output from list-sections (which should be called first). Then ask for ALL relevant sections the user might require. For example, if the user asks to build anything interactive, you will need to fetch all relevant runes, and so on. Before calling this tool, try to implement Svelte components using your own knowledge and the `svelte-autofixer` tool, since calling this tool is token intensive.',
|
||||||
|
schema: get_documentation_schema,
|
||||||
|
annotations: {
|
||||||
|
title: 'Get Documentation',
|
||||||
|
destructiveHint: false,
|
||||||
|
readOnlyHint: true,
|
||||||
|
openWorldHint: false,
|
||||||
|
},
|
||||||
|
icons,
|
||||||
|
},
|
||||||
|
async ({ section }) => {
|
||||||
|
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||||
|
await server.ctx.custom?.track?.(server.ctx.sessionId, 'get-documentation');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const content = await get_documentation_handler({ section });
|
||||||
|
return tool.text(content);
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error;
|
||||||
|
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||||
|
await server.ctx.custom?.track?.(
|
||||||
|
server.ctx.sessionId,
|
||||||
|
'get-documentation-error',
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tool.error(error.message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
4
packages/mcp-server/src/mcp/handlers/tools/handlers.ts
Normal file
4
packages/mcp-server/src/mcp/handlers/tools/handlers.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { get_documentation_handler } from './get-documentation.js';
|
||||||
|
export { list_sections_handler } from './list-sections.js';
|
||||||
|
export { svelte_autofixer_handler } from './svelte-autofixer.js';
|
||||||
|
export { playground_link_handler } from './playground-link.js';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './get-documentation.js';
|
export { get_documentation } from './get-documentation.js';
|
||||||
export * from './list-sections.js';
|
export { list_sections } from './list-sections.js';
|
||||||
export * from './svelte-autofixer.js';
|
export { svelte_autofixer } from './svelte-autofixer.js';
|
||||||
export * from './playground-link.js';
|
export { playground_link } from './playground-link.js';
|
||||||
|
|||||||
@@ -4,21 +4,44 @@ import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js';
|
|||||||
import { icons } from '../../icons/index.js';
|
import { icons } from '../../icons/index.js';
|
||||||
import { tool } from 'tmcp/utils';
|
import { tool } from 'tmcp/utils';
|
||||||
|
|
||||||
|
export async function list_sections_handler() {
|
||||||
|
const formatted_sections = await format_sections_list();
|
||||||
|
|
||||||
|
return `${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function list_sections(server: SvelteMcp) {
|
export function list_sections(server: SvelteMcp) {
|
||||||
server.tool(
|
server.tool(
|
||||||
{
|
{
|
||||||
name: 'list-sections',
|
name: 'list-sections',
|
||||||
description:
|
description:
|
||||||
'Lists all available Svelte 5 and SvelteKit documentation sections in a structured format. Each section includes a "use_cases" field that describes WHEN this documentation would be useful. You should carefully analyze the use_cases field to determine which sections are relevant for the user\'s query. The use_cases contain comma-separated keywords describing project types (e.g., "e-commerce", "blog"), features (e.g., "authentication", "forms"), components (e.g., "slider", "modal"), development stages (e.g., "deployment", "testing"), or "always" for fundamental concepts. Match these use_cases against the user\'s intent - for example, if building an e-commerce site, fetch sections with use_cases containing "e-commerce", "product listings", "shopping cart", etc. If building a slider, look for "slider", "carousel", "animation", etc. Returns sections as "* title: [section_title], use_cases: [use_cases], path: [file_path]". Always run list-sections FIRST for any Svelte query, then analyze ALL use_cases to identify relevant sections, and finally use get_documentation to fetch ALL relevant sections at once.',
|
'Lists all available Svelte 5 and SvelteKit documentation sections in a structured format. Each section includes a "use_cases" field that describes WHEN this documentation would be useful. You should carefully analyze the use_cases field to determine which sections are relevant for the user\'s query. The use_cases contain comma-separated keywords describing project types (e.g., "e-commerce", "blog"), features (e.g., "authentication", "forms"), components (e.g., "slider", "modal"), development stages (e.g., "deployment", "testing"), or "always" for fundamental concepts. Match these use_cases against the user\'s intent - for example, if building an e-commerce site, fetch sections with use_cases containing "e-commerce", "product listings", "shopping cart", etc. If building a slider, look for "slider", "carousel", "animation", etc. Returns sections as "* title: [section_title], use_cases: [use_cases], path: [file_path]". Always run list-sections FIRST for any Svelte query, then analyze ALL use_cases to identify relevant sections, and finally use get_documentation to fetch ALL relevant sections at once.',
|
||||||
|
annotations: {
|
||||||
|
title: 'List Sections',
|
||||||
|
destructiveHint: false,
|
||||||
|
readOnlyHint: true,
|
||||||
|
openWorldHint: false,
|
||||||
|
},
|
||||||
icons,
|
icons,
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'list-sections');
|
await server.ctx.custom?.track?.(server.ctx.sessionId, 'list-sections');
|
||||||
}
|
}
|
||||||
const formatted_sections = await format_sections_list();
|
try {
|
||||||
|
const content = await list_sections_handler();
|
||||||
return tool.text(`${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`);
|
return tool.text(content);
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error;
|
||||||
|
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||||
|
await server.ctx.custom?.track?.(
|
||||||
|
server.ctx.sessionId,
|
||||||
|
'list-sections-error',
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tool.error(error.message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,7 @@ type File = {
|
|||||||
text: boolean;
|
text: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function playground_link(server: SvelteMcp) {
|
const playground_link_schema = v.object({
|
||||||
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(
|
name: v.pipe(
|
||||||
v.string(),
|
v.string(),
|
||||||
v.description('The name of the Playground, it should reflect the user task'),
|
v.description('The name of the Playground, it should reflect the user task'),
|
||||||
@@ -53,16 +47,17 @@ export function playground_link(server: SvelteMcp) {
|
|||||||
"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.",
|
"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({
|
|
||||||
|
const playground_link_output_schema = v.object({
|
||||||
url: v.string(),
|
url: v.string(),
|
||||||
}),
|
});
|
||||||
icons,
|
|
||||||
},
|
export async function playground_link_handler({
|
||||||
async ({ files, name, tailwind }) => {
|
files,
|
||||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
name,
|
||||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link');
|
tailwind,
|
||||||
}
|
}: v.InferInput<typeof playground_link_schema>) {
|
||||||
const playground_base = new URL('https://svelte.dev/playground');
|
const playground_base = new URL('https://svelte.dev/playground');
|
||||||
const playground_files: File[] = [];
|
const playground_files: File[] = [];
|
||||||
|
|
||||||
@@ -80,11 +75,7 @@ export function playground_link(server: SvelteMcp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!has_app_svelte) {
|
if (!has_app_svelte) {
|
||||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
throw new Error('The files must contain an App.svelte file as the entry point');
|
||||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link-no-app-svelte');
|
|
||||||
}
|
|
||||||
|
|
||||||
return tool.error('The files must contain an App.svelte file as the entry point');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const playground_config = {
|
const playground_config = {
|
||||||
@@ -95,24 +86,50 @@ export function playground_link(server: SvelteMcp) {
|
|||||||
|
|
||||||
playground_base.hash = await compress_and_encode_text(JSON.stringify(playground_config));
|
playground_base.hash = await compress_and_encode_text(JSON.stringify(playground_config));
|
||||||
|
|
||||||
const content = {
|
const url = playground_base.toString();
|
||||||
url: playground_base.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// use the embed path to have a cleaner UI for mcp-ui
|
// use the embed path to have a cleaner UI for mcp-ui
|
||||||
playground_base.pathname = '/playground/embed';
|
playground_base.pathname = '/playground/embed';
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
iframe_url: playground_base.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playground_link(server: SvelteMcp) {
|
||||||
|
server.tool(
|
||||||
|
{
|
||||||
|
name: 'playground-link',
|
||||||
|
description:
|
||||||
|
'Generates a Playground link given a Svelte code snippet. Once you have the final version of the code you want to send to the user, ALWAYS ask the user if it wants a playground link to allow it to quickly check the code in the playground before calling this tool. NEVER use this tool if you have written the component to a file in the user project. The playground accept multiple files so if are importing from other files just include them all at the root level.',
|
||||||
|
schema: playground_link_schema,
|
||||||
|
outputSchema: playground_link_output_schema,
|
||||||
|
annotations: {
|
||||||
|
title: 'Playground Link',
|
||||||
|
destructiveHint: false,
|
||||||
|
readOnlyHint: true,
|
||||||
|
openWorldHint: false,
|
||||||
|
},
|
||||||
|
icons,
|
||||||
|
},
|
||||||
|
async ({ files, name, tailwind }) => {
|
||||||
|
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||||
|
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await playground_link_handler({ files, name, tailwind });
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify(content),
|
text: JSON.stringify({ url: result.url }),
|
||||||
},
|
},
|
||||||
createUIResource({
|
createUIResource({
|
||||||
uri: 'ui://svelte/playground-link',
|
uri: 'ui://svelte/playground-link',
|
||||||
content: {
|
content: {
|
||||||
type: 'externalUrl',
|
type: 'externalUrl',
|
||||||
iframeUrl: playground_base.toString(),
|
iframeUrl: result.iframe_url,
|
||||||
},
|
},
|
||||||
uiMetadata: {
|
uiMetadata: {
|
||||||
'preferred-frame-size': ['100%', '1200px'],
|
'preferred-frame-size': ['100%', '1200px'],
|
||||||
@@ -120,8 +137,19 @@ export function playground_link(server: SvelteMcp) {
|
|||||||
encoding: 'text',
|
encoding: 'text',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
structuredContent: content,
|
structuredContent: { url: result.url },
|
||||||
};
|
};
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error;
|
||||||
|
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||||
|
await server.ctx.custom?.track?.(
|
||||||
|
server.ctx.sessionId,
|
||||||
|
'playground-link-error',
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tool.error(error.message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,7 @@ import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js
|
|||||||
import { icons } from '../../icons/index.js';
|
import { icons } from '../../icons/index.js';
|
||||||
import { tool } from 'tmcp/utils';
|
import { tool } from 'tmcp/utils';
|
||||||
|
|
||||||
export function svelte_autofixer(server: SvelteMcp) {
|
const autofixer_schema = v.object({
|
||||||
server.tool(
|
|
||||||
{
|
|
||||||
name: 'svelte-autofixer',
|
|
||||||
title: 'Svelte Autofixer',
|
|
||||||
description:
|
|
||||||
'Given a svelte component or module returns a list of suggestions to fix any issues it has. This tool MUST be used whenever the user is asking to write svelte code before sending the code back to the user',
|
|
||||||
schema: v.object({
|
|
||||||
code: v.string(),
|
code: v.string(),
|
||||||
desired_svelte_version: v.pipe(
|
desired_svelte_version: v.pipe(
|
||||||
v.union([v.string(), v.number()]),
|
v.union([v.string(), v.number()]),
|
||||||
@@ -34,39 +27,27 @@ export function svelte_autofixer(server: SvelteMcp) {
|
|||||||
'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.',
|
'The filename of the component if available, it MUST be only the Component name with .svelte or .svelte.ts extension and not the entire path.',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
});
|
||||||
outputSchema: v.object({
|
|
||||||
|
const autofixer_output_schema = v.object({
|
||||||
issues: v.array(v.string()),
|
issues: v.array(v.string()),
|
||||||
suggestions: v.array(v.string()),
|
suggestions: v.array(v.string()),
|
||||||
require_another_tool_call_after_fixing: v.boolean(),
|
require_another_tool_call_after_fixing: v.boolean(),
|
||||||
}),
|
});
|
||||||
annotations: {
|
|
||||||
title: 'Svelte Autofixer',
|
export async function svelte_autofixer_handler({
|
||||||
destructiveHint: false,
|
|
||||||
readOnlyHint: true,
|
|
||||||
openWorldHint: false,
|
|
||||||
},
|
|
||||||
icons,
|
|
||||||
},
|
|
||||||
async ({
|
|
||||||
code,
|
code,
|
||||||
filename: filename_or_path,
|
|
||||||
desired_svelte_version: desired_svelte_version_unchecked,
|
desired_svelte_version: desired_svelte_version_unchecked,
|
||||||
async,
|
async,
|
||||||
}) => {
|
filename: filename_or_path,
|
||||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
}: v.InferInput<typeof autofixer_schema>) {
|
||||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer');
|
|
||||||
}
|
|
||||||
// we validate manually because some clients don't support union in the input schema (looking at you cursor)
|
// we validate manually because some clients don't support union in the input schema (looking at you cursor)
|
||||||
const parsed_version = v.safeParse(
|
const parsed_version = v.safeParse(
|
||||||
v.union([v.literal(4), v.literal(5), v.literal('4'), v.literal('5')]),
|
v.union([v.literal(4), v.literal(5), v.literal('4'), v.literal('5')]),
|
||||||
desired_svelte_version_unchecked,
|
desired_svelte_version_unchecked,
|
||||||
);
|
);
|
||||||
if (parsed_version.success === false) {
|
if (parsed_version.success === false) {
|
||||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
throw new Error(
|
||||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer-wrong-version');
|
|
||||||
}
|
|
||||||
return tool.error(
|
|
||||||
`The desired_svelte_version MUST be either 4 or 5 but received "${desired_svelte_version_unchecked}"`,
|
`The desired_svelte_version MUST be either 4 or 5 but received "${desired_svelte_version_unchecked}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,7 +55,7 @@ export function svelte_autofixer(server: SvelteMcp) {
|
|||||||
const desired_svelte_version = parsed_version.output;
|
const desired_svelte_version = parsed_version.output;
|
||||||
|
|
||||||
if (async && +desired_svelte_version < 5) {
|
if (async && +desired_svelte_version < 5) {
|
||||||
return tool.error('The async option can only be used with Svelte version 5 or higher.');
|
throw new Error('The async option can only be used with Svelte version 5 or higher.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const content: {
|
const content: {
|
||||||
@@ -112,8 +93,54 @@ export function svelte_autofixer(server: SvelteMcp) {
|
|||||||
if (content.issues.length > 0 || content.suggestions.length > 0) {
|
if (content.issues.length > 0 || content.suggestions.length > 0) {
|
||||||
content.require_another_tool_call_after_fixing = true;
|
content.require_another_tool_call_after_fixing = true;
|
||||||
}
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function svelte_autofixer(server: SvelteMcp) {
|
||||||
|
server.tool(
|
||||||
|
{
|
||||||
|
name: 'svelte-autofixer',
|
||||||
|
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,
|
||||||
|
outputSchema: autofixer_output_schema,
|
||||||
|
annotations: {
|
||||||
|
title: 'Svelte Autofixer',
|
||||||
|
destructiveHint: false,
|
||||||
|
readOnlyHint: true,
|
||||||
|
openWorldHint: false,
|
||||||
|
},
|
||||||
|
icons,
|
||||||
|
},
|
||||||
|
async ({
|
||||||
|
code,
|
||||||
|
filename: filename_or_path,
|
||||||
|
desired_svelte_version: desired_svelte_version_unchecked,
|
||||||
|
async,
|
||||||
|
}) => {
|
||||||
|
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||||
|
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const content = await svelte_autofixer_handler({
|
||||||
|
code,
|
||||||
|
desired_svelte_version: desired_svelte_version_unchecked,
|
||||||
|
async,
|
||||||
|
filename: filename_or_path,
|
||||||
|
});
|
||||||
return tool.structured(content);
|
return tool.structured(content);
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error;
|
||||||
|
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||||
|
await server.ctx.custom?.track?.(
|
||||||
|
server.ctx.sessionId,
|
||||||
|
'svelte-autofixer-error',
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tool.error(error.message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/handlers.d.mts",
|
||||||
|
"import": "./dist/handlers.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
@@ -41,6 +47,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint": "catalog:lint",
|
"eslint": "catalog:lint",
|
||||||
|
"sade": "catalog:tooling",
|
||||||
"tmcp": "catalog:tmcp"
|
"tmcp": "catalog:tmcp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
packages/mcp-stdio/src/handlers.ts
Normal file
6
packages/mcp-stdio/src/handlers.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export {
|
||||||
|
list_sections_handler as listSections,
|
||||||
|
get_documentation_handler as getDocumentation,
|
||||||
|
svelte_autofixer_handler as svelteAutofixer,
|
||||||
|
playground_link_handler as playgroundLink,
|
||||||
|
} from '@sveltejs/mcp-server/handlers';
|
||||||
@@ -1,7 +1,65 @@
|
|||||||
#! /usr/bin/env node
|
#! /usr/bin/env node
|
||||||
import { server } from '@sveltejs/mcp-server';
|
import { server } from '@sveltejs/mcp-server';
|
||||||
|
import {
|
||||||
|
list_sections_handler,
|
||||||
|
get_documentation_handler,
|
||||||
|
svelte_autofixer_handler,
|
||||||
|
} from '@sveltejs/mcp-server/handlers';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
import { StdioTransport } from '@tmcp/transport-stdio';
|
import { StdioTransport } from '@tmcp/transport-stdio';
|
||||||
|
import sade from 'sade';
|
||||||
|
|
||||||
const transport = new StdioTransport(server);
|
const cli = sade('svelte-mcp');
|
||||||
|
|
||||||
transport.listen();
|
cli.command('__mcp', '', { default: true }).action(() => {
|
||||||
|
const transport = new StdioTransport(server);
|
||||||
|
transport.listen();
|
||||||
|
});
|
||||||
|
|
||||||
|
cli
|
||||||
|
.command('list-sections')
|
||||||
|
.describe('List all the available documentation sections')
|
||||||
|
.action(async () => {
|
||||||
|
console.log(await list_sections_handler());
|
||||||
|
});
|
||||||
|
|
||||||
|
cli
|
||||||
|
.command('get-documentation <sections>')
|
||||||
|
.describe('Get documentation for specified sections, separated by commas')
|
||||||
|
.action(async (sections) => {
|
||||||
|
console.log(await get_documentation_handler({ section: sections.split(',') }));
|
||||||
|
});
|
||||||
|
|
||||||
|
cli
|
||||||
|
.command('svelte-autofixer <code_or_path>')
|
||||||
|
.describe(
|
||||||
|
'Detect and suggest fixes for Svelte code issues, because the terminal will substitute variables `$` should be correctly escaped',
|
||||||
|
)
|
||||||
|
.option('--async', 'Wether the project is using async svelte or not', false)
|
||||||
|
.option('--svelte-version', 'Which version of svelte to use...it can be 4 or 5', 5)
|
||||||
|
.action(async (code_or_path, { async, 'svelte-version': version }) => {
|
||||||
|
let code = code_or_path;
|
||||||
|
|
||||||
|
let is_path = false;
|
||||||
|
|
||||||
|
if (existsSync(code_or_path)) {
|
||||||
|
console.log('Detected file path, reading file...');
|
||||||
|
code = await readFile(code_or_path, 'utf-8');
|
||||||
|
is_path = true;
|
||||||
|
} else {
|
||||||
|
console.log('File not found, treating input as code...');
|
||||||
|
}
|
||||||
|
|
||||||
|
const desired_svelte_version = +version;
|
||||||
|
|
||||||
|
const result = await svelte_autofixer_handler({
|
||||||
|
code,
|
||||||
|
async: Boolean(async),
|
||||||
|
desired_svelte_version,
|
||||||
|
filename: is_path ? code_or_path : undefined,
|
||||||
|
});
|
||||||
|
console.log(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
cli.parse(process.argv);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown';
|
|||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
entry: ['./src/index.ts'],
|
entry: ['./src/index.ts', './src/handlers.ts'],
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
define: {
|
define: {
|
||||||
// some eslint-plugin-svelte code expects __filename to exists but in an ESM environment it does not.
|
// some eslint-plugin-svelte code expects __filename to exists but in an ESM environment it does not.
|
||||||
@@ -13,7 +13,9 @@ export default defineConfig([
|
|||||||
// the require would fail once executed in a project without eslint installed.
|
// the require would fail once executed in a project without eslint installed.
|
||||||
external: ['eslint'],
|
external: ['eslint'],
|
||||||
publint: true,
|
publint: true,
|
||||||
dts: false,
|
dts: {
|
||||||
|
eager: true,
|
||||||
|
},
|
||||||
treeshake: true,
|
treeshake: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -128,6 +128,9 @@ catalogs:
|
|||||||
publint:
|
publint:
|
||||||
specifier: ^0.3.13
|
specifier: ^0.3.13
|
||||||
version: 0.3.13
|
version: 0.3.13
|
||||||
|
sade:
|
||||||
|
specifier: 1.8.1
|
||||||
|
version: 1.8.1
|
||||||
ts-blank-space:
|
ts-blank-space:
|
||||||
specifier: ^0.6.2
|
specifier: ^0.6.2
|
||||||
version: 0.6.2
|
version: 0.6.2
|
||||||
@@ -384,6 +387,9 @@ importers:
|
|||||||
eslint:
|
eslint:
|
||||||
specifier: catalog:lint
|
specifier: catalog:lint
|
||||||
version: 9.36.0(jiti@2.6.0)
|
version: 9.36.0(jiti@2.6.0)
|
||||||
|
sade:
|
||||||
|
specifier: catalog:tooling
|
||||||
|
version: 1.8.1
|
||||||
tmcp:
|
tmcp:
|
||||||
specifier: catalog:tmcp
|
specifier: catalog:tmcp
|
||||||
version: 1.19.0(typescript@5.9.2)
|
version: 1.19.0(typescript@5.9.2)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ catalogs:
|
|||||||
dotenv: ^17.2.3
|
dotenv: ^17.2.3
|
||||||
node-resolve-ts: ^1.0.2
|
node-resolve-ts: ^1.0.2
|
||||||
publint: ^0.3.13
|
publint: ^0.3.13
|
||||||
|
sade: 1.8.1
|
||||||
ts-blank-space: ^0.6.2
|
ts-blank-space: ^0.6.2
|
||||||
tsdown: ^0.18.0
|
tsdown: ^0.18.0
|
||||||
typescript: ^5.0.0
|
typescript: ^5.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user