diff --git a/.prettierignore b/.prettierignore index 611d858..b6c8873 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,4 +8,7 @@ bun.lockb # Miscellaneous /static/ /drizzle/ -/**/.svelte-kit/* \ No newline at end of file +/**/.svelte-kit/* + +# Claude Code +.claude/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index c240871..855c15c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,12 +90,12 @@ When connected to the svelte-llm MCP server, you have access to comprehensive Sv ## Available MCP Tools: -### 1. list_sections +### 1. list-sections Use this FIRST to discover all available documentation sections. Returns a structured list with titles 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 +### 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 and then use the get_documentation tool to fetch ALL documentation sections that are relevant for the users task. +After calling the list-sections tool, you MUST analyze the returned documentation sections and then use the get_documentation tool to fetch ALL documentation sections that are relevant for the users task. diff --git a/README.md b/README.md index e167ec5..ede829f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Repo for the official Svelte MCP server. ``` pnpm i -cp .env.example .env +cp apps/mcp-remote/.env.example apps/mcp-remote/.env pnpm dev ``` diff --git a/eslint.config.js b/eslint.config.js index 2552bfd..325387f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -12,6 +12,9 @@ const gitignore_path = fileURLToPath(new URL('./.gitignore', import.meta.url)); export default /** @type {import("eslint").Linter.Config} */ ([ includeIgnoreFile(gitignore_path), + { + ignores: ['.claude/**/*'], + }, js.configs.recommended, ...ts.configs.recommended, ...svelte.configs.recommended, diff --git a/package.json b/package.json index 5e5b67e..e2557b3 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "build": "pnpm -r run build", + "dev": "pnpm --filter @sveltejs/mcp-remote run dev", "check": "pnpm -r run check", "format": "prettier --write .", "lint": "prettier --check . && eslint .", diff --git a/packages/mcp-server/src/mcp/handlers/prompts/svelte-task.ts b/packages/mcp-server/src/mcp/handlers/prompts/svelte-task.ts index 958eef7..dea406c 100644 --- a/packages/mcp-server/src/mcp/handlers/prompts/svelte-task.ts +++ b/packages/mcp-server/src/mcp/handlers/prompts/svelte-task.ts @@ -1,5 +1,6 @@ import type { SvelteMcp } from '../../index.js'; import * as v from 'valibot'; +import { get_sections } from '../../utils.js'; export function setup_svelte_task(server: SvelteMcp) { server.prompt( @@ -13,8 +14,7 @@ export function setup_svelte_task(server: SvelteMcp) { }), }, async ({ task }) => { - // TODO: implement logic to fetch the available docs paths to return in the prompt - const available_docs: string[] = []; + const available_docs: string[] = (await get_sections()).map((s) => s.title); return { messages: [ diff --git a/packages/mcp-server/src/mcp/handlers/resources/list-sections.ts b/packages/mcp-server/src/mcp/handlers/resources/list-sections.ts index c36e8e3..6cd4767 100644 --- a/packages/mcp-server/src/mcp/handlers/resources/list-sections.ts +++ b/packages/mcp-server/src/mcp/handlers/resources/list-sections.ts @@ -1,22 +1,61 @@ import type { SvelteMcp } from '../../index.js'; +import { get_sections, fetch_with_timeout } from '../../utils.js'; -export function list_sections(server: SvelteMcp) { - server.resource( +export async function list_sections(server: SvelteMcp) { + const sections = await get_sections(); + + server.template( { - name: 'list-sections', - enabled: () => false, - description: - 'The list of all the available Svelte 5 and SvelteKit documentation sections in a structured format.', - uri: 'svelte://list-sections', - title: 'Svelte Documentation Section', + name: 'Svelte Doc Section', + description: 'A single documentation section', + list() { + return sections.map((section) => { + const section_name = section.slug; + const resource_name = section_name; + const resource_uri = `svelte://${section_name}.md`; + return { + name: resource_name, + description: section.use_cases, + uri: resource_uri, + title: section.title, + }; + }); + }, + complete: { + slug: (query) => { + const values = sections + .reduce((acc, section) => { + const section_name = section.slug; + const resource_name = section_name; + if (section_name.includes(query.toLowerCase())) { + acc.push(resource_name); + } + return acc; + }, []) + // there's a hard limit of 100 for completions + .slice(0, 100); + return { + completion: { + values, + }, + }; + }, + }, + uri: 'svelte://{/slug*}.md', }, - async (uri) => { + async (uri, { slug }) => { + const section = sections.find((section) => { + return slug === section.slug; + }); + if (!section) throw new Error(`Section not found: ${slug}`); + const response = await fetch_with_timeout(section.url); + const content = await response.text(); return { contents: [ { uri, type: 'text', - text: 'resource list-sections called', + text: content, }, ], }; diff --git a/packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts b/packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts index 6b3ddf0..d68bd12 100644 --- a/packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts +++ b/packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts @@ -1,13 +1,14 @@ import type { SvelteMcp } from '../../index.js'; import * as v from 'valibot'; +import { get_sections, fetch_with_timeout } from '../../utils.js'; +import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js'; export function get_documentation(server: SvelteMcp) { server.tool( { name: 'get-documentation', - enabled: () => false, 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.', + '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({ section: v.pipe( v.union([v.string(), v.array(v.string())]), @@ -17,7 +18,7 @@ export function get_documentation(server: SvelteMcp) { ), }), }, - ({ section }) => { + async ({ section }) => { let sections: string[]; if (Array.isArray(section)) { @@ -43,13 +44,73 @@ export function get_documentation(server: SvelteMcp) { sections = []; } - const sections_list = sections.length > 0 ? sections.join(', ') : 'no sections'; + const available_sections = await get_sections(); + + const settled_results = await Promise.allSettled( + sections.map(async (requested_section) => { + const matched_section = available_sections.find( + (s) => + s.title.toLowerCase() === requested_section.toLowerCase() || + s.url === requested_section, + ); + + if (matched_section) { + try { + const response = await fetch_with_timeout(matched_section.url); + if (response.ok) { + const content = await response.text(); + return { success: true, content: `## ${matched_section.title}\n\n${content}` }; + } else { + return { + success: false, + content: `## ${matched_section.title}\n\nError: Could not fetch documentation (HTTP ${response.status})`, + }; + } + } catch (error) { + return { + success: false, + content: `## ${matched_section.title}\n\nError: Failed to fetch documentation - ${error}`, + }; + } + } else { + return { + success: false, + content: `## ${requested_section}\n\nError: Section not found.`, + }; + } + }), + ); + + const results = settled_results.map((result) => { + if (result.status === 'fulfilled') { + return result.value; + } else { + return { + success: false, + content: `Error: Couldn't fetch - ${result.reason}`, + }; + } + }); + + const has_any_success = results.some((result) => result.success); + let final_text = results.map((r) => r.content).join('\n\n---\n\n'); + + if (!has_any_success) { + const formatted_sections = available_sections + .map( + (section) => + `* title: ${section.title}, use_cases: ${section.use_cases}, path: ${section.url}`, + ) + .join('\n'); + + final_text += `\n\n---\n\n${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`; + } return { content: [ { type: 'text', - text: `called for sections: ${sections_list}`, + text: final_text, }, ], }; diff --git a/packages/mcp-server/src/mcp/handlers/tools/list-sections.ts b/packages/mcp-server/src/mcp/handlers/tools/list-sections.ts index 6d35a63..ab001d9 100644 --- a/packages/mcp-server/src/mcp/handlers/tools/list-sections.ts +++ b/packages/mcp-server/src/mcp/handlers/tools/list-sections.ts @@ -1,19 +1,28 @@ import type { SvelteMcp } from '../../index.js'; +import { get_sections } from '../../utils.js'; +import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js'; export function list_sections(server: SvelteMcp) { server.tool( { name: 'list-sections', - enabled: () => false, 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.', + 'Lists all available Svelte 5 and SvelteKit documentation sections in a structured format. Returns sections as a list of "* title: [section_title], use_cases: [use_cases], 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.', }, - () => { + async () => { + const sections = await get_sections(); + const formatted_sections = sections + .map( + (section) => + `* title: ${section.title}, use_cases: ${section.use_cases}, path: ${section.url}`, + ) + .join('\n'); + return { content: [ { type: 'text', - text: 'tool list_sections called', + text: `${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`, }, ], }; diff --git a/packages/mcp-server/src/mcp/handlers/tools/prompts.ts b/packages/mcp-server/src/mcp/handlers/tools/prompts.ts new file mode 100644 index 0000000..e574058 --- /dev/null +++ b/packages/mcp-server/src/mcp/handlers/tools/prompts.ts @@ -0,0 +1,5 @@ +export const SECTIONS_LIST_INTRO = + 'List of available Svelte documentation sections and its inteneded uses:'; + +export const SECTIONS_LIST_OUTRO = + 'Use the title or path with the get-documentation tool to get more details about a specific section.'; diff --git a/packages/mcp-server/src/mcp/schemas/index.ts b/packages/mcp-server/src/mcp/schemas/index.ts new file mode 100644 index 0000000..0bdc3ce --- /dev/null +++ b/packages/mcp-server/src/mcp/schemas/index.ts @@ -0,0 +1,12 @@ +import * as v from 'valibot'; + +export const documentation_sections_schema = v.record( + v.string(), + v.object({ + metadata: v.object({ + title: v.string(), + use_cases: v.optional(v.string()), + }), + slug: v.string(), + }), +); diff --git a/packages/mcp-server/src/mcp/utils.ts b/packages/mcp-server/src/mcp/utils.ts new file mode 100644 index 0000000..0ed3ad1 --- /dev/null +++ b/packages/mcp-server/src/mcp/utils.ts @@ -0,0 +1,31 @@ +import * as v from 'valibot'; +import { documentation_sections_schema } from '../mcp/schemas/index.js'; + +export async function fetch_with_timeout( + url: string, + timeout_ms: number = 10000, +): Promise { + try { + const response = await fetch(url, { signal: AbortSignal.timeout(timeout_ms) }); + return response; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Request timed out after ${timeout_ms}ms`); + } + throw error; + } +} + +export async function get_sections() { + const sections = await fetch_with_timeout( + 'https://svelte.dev/docs/experimental/sections.json', + ).then((res) => res.json()); + const validated_sections = v.safeParse(documentation_sections_schema, sections); + if (!validated_sections.success) return []; + return Object.entries(validated_sections.output).map(([, section]) => ({ + title: section.metadata.title, + use_cases: section.metadata.use_cases ?? 'read document for use cases', + slug: section.slug, + url: `https://svelte.dev/${section.slug}/llms.txt`, + })); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a59e75..671be56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 10.1.8(eslint@9.36.0(jiti@2.6.0)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0)) + version: 2.32.0(eslint@9.36.0(jiti@2.6.0)) eslint-plugin-svelte: specifier: ^3.12.3 version: 3.12.4(eslint@9.36.0(jiti@2.6.0))(svelte@5.39.6)(ts-node@10.9.2(@types/node@24.5.2)(typescript@5.9.2)) @@ -5775,17 +5775,16 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.0)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2) eslint: 9.36.0(jiti@2.6.0) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0)): + eslint-plugin-import@2.32.0(eslint@9.36.0(jiti@2.6.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5796,7 +5795,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.36.0(jiti@2.6.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.0)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.0)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5807,8 +5806,6 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack