Compare commits

..

47 Commits

Author SHA1 Message Date
paoloricciuti
baad760634 chore: add changeset to trigger release 2025-10-03 22:34:55 +02:00
paoloricciuti
b2275587ee fix: remove package manager from inner packages 2025-10-03 22:33:56 +02:00
Paolo Ricciuti
4964303100 chore: apply suggestions from code review
Co-authored-by: Dominik G. <dominik.goepel@gmx.de>
2025-10-03 22:31:46 +02:00
paoloricciuti
81901b2564 fix: small configs 2025-10-03 22:03:59 +02:00
paoloricciuti
5aa1aa401a Merge branch 'main' into setup-changesets 2025-10-03 21:52:45 +02:00
paoloricciuti
b5a88c454d fix: small typos 2025-10-03 16:49:24 +02:00
Paolo Ricciuti
7b5bea6549 Merge pull request #28 from sveltejs/llms-txt
feat: simplified documentation listing
2025-10-02 11:46:33 +02:00
paoloricciuti
a36d0d17a8 fix: validate response and default use_cases 2025-10-02 11:29:35 +02:00
paoloricciuti
70f14bddca feat: use endpoint to get sections 2025-10-02 08:03:03 +02:00
paoloricciuti
a281ef4b66 fix: use server.template with list and complete for docs resources 2025-09-28 12:19:43 +02:00
paoloricciuti
5bc812e4db fix: enable list-sections tool 2025-09-28 12:19:21 +02:00
paoloricciuti
82319661dd feat: add sections to prompt 2025-09-28 12:19:03 +02:00
paoloricciuti
ce0861c1ca chore: use mocked current version of sections 2025-09-28 12:18:52 +02:00
Stanislav Khromov
5dd83d151e Update utils.ts 2025-09-27 00:14:00 +02:00
Stanislav Khromov
76a35f5dc8 format 2025-09-27 00:13:37 +02:00
Stanislav Khromov
54763e0f55 Update packages/mcp-server/src/mcp/utils.ts
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2025-09-27 00:07:18 +02:00
Stanislav Khromov
01d5803b5d reduce duplication 2025-09-27 00:06:37 +02:00
Stanislav Khromov
0366bc785b Merge branch 'llms-txt' of https://github.com/sveltejs/mcp into llms-txt 2025-09-27 00:03:49 +02:00
Stanislav Khromov
6a6417d3a5 Use Promise.allSettled() 2025-09-27 00:03:40 +02:00
Stanislav Khromov
77af7ebcc6 Update packages/mcp-server/src/mcp/handlers/tools/get-documentation.ts
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2025-09-27 00:01:50 +02:00
Stanislav Khromov
b1a196497d Update list-sections.ts 2025-09-27 00:00:15 +02:00
Stanislav Khromov
fb2d19fd07 Update list-sections.ts 2025-09-26 23:59:18 +02:00
Stanislav Khromov
8328a3572b eslint 2025-09-26 23:51:58 +02:00
Stanislav Khromov
c05b6c257a eslint 2025-09-26 23:51:48 +02:00
Stanislav Khromov
7f9ea742d8 don't lint .claude dir 2025-09-26 23:48:15 +02:00
Stanislav Khromov
bf477a6ccf Update get-documentation.ts 2025-09-26 23:46:07 +02:00
Stanislav Khromov
0f5482477a Update get-documentation.ts 2025-09-26 22:44:28 +02:00
Stanislav Khromov
b774b463fe Update get-documentation.ts 2025-09-26 22:42:42 +02:00
Stanislav Khromov
c49b24d36a wip 2025-09-26 22:39:52 +02:00
Stanislav Khromov
6cb97ac11d Update get-documentation.ts 2025-09-26 22:36:30 +02:00
Stanislav Khromov
d33a374417 Update get-documentation.ts 2025-09-26 21:56:38 +02:00
Stanislav Khromov
1bb171cea7 cleanup 2025-09-26 20:44:41 +02:00
Stanislav Khromov
e314ab57b2 Update list-sections.ts 2025-09-26 20:43:04 +02:00
Stanislav Khromov
19cacf7ed9 refactor 2025-09-26 20:39:22 +02:00
Stanislav Khromov
68cf69a117 wip 2025-09-26 20:38:17 +02:00
Stanislav Khromov
917fdf63b1 enable 2025-09-26 20:34:57 +02:00
Stanislav Khromov
972cadc410 Update package.json 2025-09-26 20:30:16 +02:00
Stanislav Khromov
dc6c87ce37 Merge branch 'main' into llms-txt 2025-09-26 20:30:11 +02:00
Stanislav Khromov
e7431e9024 Update README.md 2025-09-26 20:29:55 +02:00
Stanislav Khromov
07737a8edd Add Claude Code .mcp.json config 2025-09-26 20:22:50 +02:00
Stanislav Khromov
fdb7689992 fix stdio path 2025-09-26 20:21:43 +02:00
paoloricciuti
1a283f60bc chore: disable unimplemented tools/resources 2025-09-24 22:11:21 +02:00
paoloricciuti
bb16ccca3a fix: pnpm versions and package.json 2025-09-24 16:06:32 +02:00
paoloricciuti
23ddaf9495 fix: update pnpm version 2025-09-24 15:59:34 +02:00
paoloricciuti
4228302ed0 fix: package.json's 2025-09-24 15:57:17 +02:00
Paolo Ricciuti
0bc4d75e13 chore: use the right repo name 🤦
Co-authored-by: Dominik G. <dominik.goepel@gmx.de>
2025-09-24 15:53:12 +02:00
paoloricciuti
4679549401 chore: setup changesets 2025-09-24 15:43:42 +02:00
29 changed files with 875 additions and 119 deletions

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

11
.changeset/config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/mcp" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@@ -0,0 +1,5 @@
---
'@sveltejs/mcp': patch
---
feat: latest version

View File

@@ -18,7 +18,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
version: 10.17.1
- name: Setup Node.js
uses: actions/setup-node@v4

View File

@@ -18,7 +18,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
version: 10.17.1
- name: Setup Node.js
uses: actions/setup-node@v4

58
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Release
on:
push:
branches:
- main
permissions: {}
jobs:
release:
permissions:
contents: write # to create release (changesets/action)
id-token: write # OpenID Connect token needed for provenance
pull-requests: write # to create pull request (changesets/action)
# prevents this action from running on forks
if: github.repository == 'sveltejs/mcp'
name: Release
runs-on: ${{ matrix.os }}
strategy:
matrix:
# pseudo-matrix for convenience, NEVER use more than a single combination
node: [24]
os: [ubuntu-latest]
steps:
- name: checkout
uses: actions/checkout@v5
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node }}
package-manager-cache: false # pnpm is not installed yet
- name: install pnpm
shell: bash
run: |
PNPM_VER=$(jq -r '.packageManager | if .[0:5] == "pnpm@" then .[5:] else "packageManager in package.json does not start with pnpm@\n" | halt_error(1) end' package.json)
echo installing pnpm version $PNPM_VER
npm i -g pnpm@$PNPM_VER
- uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node }}
package-manager-cache: true # caches pnpm via packageManager field in package.json
- name: install
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: publint
run: pnpm check:publint
- name: Create Release Pull Request or Publish to npm
id: changesets
# pinned for security, always review third party action code before updating
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3
with:
# This expects you to have a script called release which does a build for your packages and calls changeset publish
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true

View File

@@ -18,7 +18,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
version: 10.17.1
- name: Setup Node.js
uses: actions/setup-node@v4

View File

@@ -1,8 +1,9 @@
{
"mcpServers": {
"svelte-llm": {
"type": "http",
"url": "https://svelte-llm.stanislav.garden/mcp/mcp"
"svelte": {
"type": "stdio",
"command": "node",
"args": ["packages/mcp-stdio/dist/index.js"]
}
}
}

View File

@@ -8,4 +8,7 @@ bun.lockb
# Miscellaneous
/static/
/drizzle/
/**/.svelte-kit/*
/**/.svelte-kit/*
# Claude Code
.claude/

2
.vscode/mcp.json vendored
View File

@@ -3,7 +3,7 @@
"Svelte MCP": {
"type": "stdio",
"command": "node",
"args": ["dist/lib/stdio.js"]
"args": ["packages/mcp-stdio/dist/index.js"]
}
},
"inputs": []

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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,

View File

@@ -3,16 +3,20 @@
"version": "0.0.1",
"description": "The official Svelte MCP server implementation",
"type": "module",
"packageManager": "pnpm@10.17.1",
"scripts": {
"build": "pnpm -r run build",
"dev": "pnpm --filter @sveltejs/mcp-remote run dev",
"check": "pnpm -r run check",
"check:publint": "pnpm -r run check:publint",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"lint:fix": "prettier --write . && eslint . --fix",
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"test:watch": "npm run test:unit -- --watch",
"inspect": "pnpm mcp-inspector"
"inspect": "pnpm mcp-inspector",
"release": "pnpm --filter @sveltejs/mcp run build && changeset publish"
},
"keywords": [
"svelte",
@@ -22,9 +26,11 @@
],
"private": true,
"devDependencies": {
"@changesets/cli": "^2.29.7",
"@eslint/compat": "^1.3.2",
"@eslint/js": "^9.36.0",
"@modelcontextprotocol/inspector": "^0.16.7",
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.32.0",
@@ -32,6 +38,7 @@
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"publint": "^0.3.13",
"typescript": "^5.0.0",
"typescript-eslint": "^8.44.1",
"vitest": "^3.2.3"

View File

@@ -12,7 +12,6 @@
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.15.1",
"type": "module",
"dependencies": {
"drizzle-orm": "^0.40.1"

View File

@@ -7,7 +7,6 @@
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.15.1",
"type": "module",
"scripts": {
"test": "vitest"

View File

@@ -117,36 +117,6 @@ describe('add_autofixers_issues', () => {
);
});
});
it('should add a suggestion when calling a function inside an effect', () => {
const content = run_autofixers_on_code(`
<script>
import { fetch_data } from './data.js';
$effect(() => {
fetch_data();
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are calling the function \`fetch_data\` inside an $effect. Please check if the function is reassigning a stateful variable because that's considered malpractice and check if it could use \`$derived\` instead. Ignore this suggestion if you are sure this function is not assigning any stateful variable or if you can't check if it does.`,
);
});
it('should add a suggestion when calling a function inside an effect (with non identifier callee)', () => {
const content = run_autofixers_on_code(`
<script>
import { fetch_data } from './data.js';
$effect(() => {
fetch_data.fetch();
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are calling a function inside an $effect. Please check if the function is reassigning a stateful variable because that's considered malpractice and check if it could use \`$derived\` instead. Ignore this suggestion if you are sure this function is not assigning any stateful variable or if you can't check if it does.`,
);
});
});
with_possible_inits('($init)', ({ init }) => {

View File

@@ -1,10 +1,4 @@
import type {
AssignmentExpression,
CallExpression,
Identifier,
Node,
UpdateExpression,
} from 'estree';
import type { AssignmentExpression, Identifier, Node, UpdateExpression } from 'estree';
import type { Autofixer, AutofixerState } from './index.js';
import { left_most_id } from '../ast/utils.js';
import type { AST } from 'svelte-eslint-parser';
@@ -33,9 +27,9 @@ function run_if_in_effect(
}
}
function assign_or_update_visitor(
function visitor(
node: UpdateExpression | AssignmentExpression,
{ state, path, next }: Context<Node | AST.SvelteNode, AutofixerState>,
{ state, path }: Context<Node | AST.SvelteNode, AutofixerState>,
) {
run_if_in_effect(path, state, () => {
function check_if_stateful_id(id: Identifier) {
@@ -64,25 +58,9 @@ function assign_or_update_visitor(
}
}
});
next();
}
function call_expression_visitor(
node: CallExpression,
{ state, path, next }: Context<Node | AST.SvelteNode, AutofixerState>,
) {
run_if_in_effect(path, state, () => {
const function_name =
node.callee.type === 'Identifier' ? `the function \`${node.callee.name}\`` : 'a function';
state.output.suggestions.push(
`You are calling ${function_name} inside an $effect. Please check if the function is reassigning a stateful variable because that's considered malpractice and check if it could use \`$derived\` instead. Ignore this suggestion if you are sure this function is not assigning any stateful variable or if you can't check if it does.`,
);
});
next();
}
export const assign_in_effect: Autofixer = {
UpdateExpression: assign_or_update_visitor,
AssignmentExpression: assign_or_update_visitor,
CallExpression: call_expression_visitor,
UpdateExpression: visitor,
AssignmentExpression: visitor,
};

View File

@@ -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: [
@@ -22,7 +22,7 @@ export function setup_svelte_task(server: SvelteMcp) {
role: 'user',
content: {
type: 'text',
text: `You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool \`get_documentation\` with one of the following paths:
text: `You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool \`get-documentation\` with one of the following paths:
<available-docs-paths>
${JSON.stringify(available_docs, null, 2)}
</available-docs-paths>

View File

@@ -0,0 +1,64 @@
import type { SvelteMcp } from '../../index.js';
import { get_sections, fetch_with_timeout } from '../../utils.js';
export async function list_sections(server: SvelteMcp) {
const sections = await get_sections();
server.template(
{
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, { 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: content,
},
],
};
},
);
}

View File

@@ -1 +1 @@
export * from './list-sections.js';
export * from './doc-section.js';

View File

@@ -1,25 +0,0 @@
import type { SvelteMcp } from '../../index.js';
export function list_sections(server: SvelteMcp) {
server.resource(
{
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',
},
async (uri) => {
return {
contents: [
{
uri,
type: 'text',
text: 'resource list-sections called',
},
],
};
},
);
}

View File

@@ -1,5 +1,7 @@
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(
@@ -7,7 +9,7 @@ export function get_documentation(server: SvelteMcp) {
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 +19,7 @@ export function get_documentation(server: SvelteMcp) {
),
}),
},
({ section }) => {
async ({ section }) => {
let sections: string[];
if (Array.isArray(section)) {
@@ -43,13 +45,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,
},
],
};

View File

@@ -1,4 +1,6 @@
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(
@@ -6,14 +8,22 @@ export function list_sections(server: SvelteMcp) {
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}`,
},
],
};

View File

@@ -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.';

View File

@@ -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(),
}),
);

View File

@@ -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`,
}));
}

View File

@@ -22,10 +22,11 @@
"access": "public"
},
"scripts": {
"build": "tsdown",
"build": "tsdown && publint",
"dev": "tsdown --watch",
"test": "vitest",
"check": "tsc --noEmit"
"check": "tsc --noEmit",
"check:publint": "publint --strict"
},
"devDependencies": {
"@sveltejs/mcp-server": "workspace:^",

572
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff