Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e7431e9
Update README.md
khromov Sep 26, 2025
dc6c87c
Merge branch 'main' into llms-txt
khromov Sep 26, 2025
972cadc
Update package.json
khromov Sep 26, 2025
917fdf6
enable
khromov Sep 26, 2025
68cf69a
wip
khromov Sep 26, 2025
19cacf7
refactor
khromov Sep 26, 2025
e314ab5
Update list-sections.ts
khromov Sep 26, 2025
1bb171c
cleanup
khromov Sep 26, 2025
d33a374
Update get-documentation.ts
khromov Sep 26, 2025
6cb97ac
Update get-documentation.ts
khromov Sep 26, 2025
c49b24d
wip
khromov Sep 26, 2025
b774b46
Update get-documentation.ts
khromov Sep 26, 2025
0f54824
Update get-documentation.ts
khromov Sep 26, 2025
bf477a6
Update get-documentation.ts
khromov Sep 26, 2025
7f9ea74
don't lint .claude dir
khromov Sep 26, 2025
c05b6c2
eslint
khromov Sep 26, 2025
8328a35
eslint
khromov Sep 26, 2025
fb2d19f
Update list-sections.ts
khromov Sep 26, 2025
b1a1964
Update list-sections.ts
khromov Sep 26, 2025
77af7eb
Update packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts
khromov Sep 26, 2025
6a6417d
Use Promise.allSettled()
khromov Sep 26, 2025
0366bc7
Merge branch 'llms-txt' of https://github.com/sveltejs/mcp into llms-txt
khromov Sep 26, 2025
01d5803
reduce duplication
khromov Sep 26, 2025
54763e0
Update packages/mcp-server/src/mcp/utils.ts
khromov Sep 26, 2025
76a35f5
format
khromov Sep 26, 2025
5dd83d1
Update utils.ts
khromov Sep 26, 2025
ce0861c
chore: use mocked current version of sections
paoloricciuti Sep 28, 2025
8231966
feat: add sections to prompt
paoloricciuti Sep 28, 2025
5bc812e
fix: enable `list-sections` tool
paoloricciuti Sep 28, 2025
a281ef4
fix: use `server.template` with list and complete for docs resources
paoloricciuti Sep 28, 2025
70f14bd
feat: use endpoint to get sections
paoloricciuti Oct 2, 2025
a36d0d1
fix: validate response and default `use_cases`
paoloricciuti Oct 2, 2025
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
5 changes: 4 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ bun.lockb
# Miscellaneous
/static/
/drizzle/
/**/.svelte-kit/*
/**/.svelte-kit/*

# Claude Code
.claude/
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
3 changes: 3 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp-server/src/mcp/handlers/prompts/svelte-task.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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: [
Expand Down
59 changes: 49 additions & 10 deletions packages/mcp-server/src/mcp/handlers/resources/list-sections.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>((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,
},
],
};
Expand Down
71 changes: 66 additions & 5 deletions packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts
Original file line number Diff line number Diff line change
@@ -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())]),
Expand All @@ -17,7 +18,7 @@ export function get_documentation(server: SvelteMcp) {
),
}),
},
({ section }) => {
async ({ section }) => {
let sections: string[];

if (Array.isArray(section)) {
Expand All @@ -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}`,
Copy link
Collaborator Author

@khromov khromov Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe let's not add use_cases if it's empty, and put it at the end (so it's title, path, (maybe) use_cases)?

)
.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,
},
],
};
Expand Down
17 changes: 13 additions & 4 deletions packages/mcp-server/src/mcp/handlers/tools/list-sections.ts
Original file line number Diff line number Diff line change
@@ -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}`,
},
],
};
Expand Down
5 changes: 5 additions & 0 deletions packages/mcp-server/src/mcp/handlers/tools/prompts.ts
Original file line number Diff line number Diff line change
@@ -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.';
12 changes: 12 additions & 0 deletions packages/mcp-server/src/mcp/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -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(),
}),
);
31 changes: 31 additions & 0 deletions packages/mcp-server/src/mcp/utils.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
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`,
}));
}
11 changes: 4 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.