Compare commits

...

2 Commits

Author SHA1 Message Date
paoloricciuti
48b756a21d fix: lint 2025-10-24 08:23:29 +02:00
paoloricciuti
0054d13b7e feat: analytics 2025-10-24 08:06:33 +02:00
12 changed files with 86 additions and 9 deletions

View File

@@ -65,6 +65,7 @@
"@sveltejs/mcp-schema": "workspace:^", "@sveltejs/mcp-schema": "workspace:^",
"@sveltejs/mcp-server": "workspace:^", "@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-http": "^0.7.1", "@tmcp/transport-http": "^0.7.1",
"@vercel/analytics": "^1.5.0",
"tmcp": "^1.15.3" "tmcp": "^1.15.3"
} }
} }

View File

@@ -1,6 +1,8 @@
import { dev } from '$app/environment';
import { http_transport } from '$lib/mcp/index.js'; import { http_transport } from '$lib/mcp/index.js';
import { db } from '$lib/server/db/index.js'; import { db } from '$lib/server/db/index.js';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { track } from '@vercel/analytics/server';
export async function handle({ event, resolve }) { export async function handle({ event, resolve }) {
if (event.request.method === 'POST') { if (event.request.method === 'POST') {
@@ -19,6 +21,12 @@ export async function handle({ event, resolve }) {
} }
const mcp_response = await http_transport.respond(event.request, { const mcp_response = await http_transport.respond(event.request, {
db, db,
// only add analytics in production
track: dev
? undefined
: async (session_id, event, slug) => {
await track(event, { session_id, ...(slug ? { slug } : {}) });
},
}); });
// we are deploying on vercel the SSE connection will timeout after 5 minutes...for // we are deploying on vercel the SSE connection will timeout after 5 minutes...for
// the moment we are not sending back any notifications (logs, or list changed notifications) // the moment we are not sending back any notifications (logs, or list changed notifications)

View File

@@ -1,8 +1,6 @@
import { createClient } from '@libsql/client'; import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql'; import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema.js'; import * as schema from './schema.js';
// let's disable it for the moment...i can't figure out a way to make it wotk with eslint
// eslint-disable-next-line import/extensions
import { DATABASE_TOKEN, DATABASE_URL } from '$env/static/private'; import { DATABASE_TOKEN, DATABASE_URL } from '$env/static/private';
if (!DATABASE_URL) throw new Error('DATABASE_URL is not set'); if (!DATABASE_URL) throw new Error('DATABASE_URL is not set');
if (!DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set'); if (!DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');

View File

@@ -48,13 +48,17 @@ export default /** @type {import("eslint").Linter.Config} */ ([
'import/no-unresolved': 'off', // this doesn't work well with typescript path mapping 'import/no-unresolved': 'off', // this doesn't work well with typescript path mapping
'import/extensions': [ 'import/extensions': [
'error', 'error',
'ignorePackages',
{ {
ignorePackages: true,
pattern: {
js: 'always', js: 'always',
mjs: 'always', mjs: 'always',
cjs: 'always', cjs: 'always',
ts: 'always', ts: 'always',
svelte: 'always', svelte: 'always',
svg: 'always',
json: 'always',
},
}, },
], ],
}, },

View File

@@ -68,6 +68,9 @@ export function setup_svelte_task(server: SvelteMcp) {
icons, icons,
}, },
async ({ task }) => { async ({ task }) => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-task');
}
const available_docs = await format_sections_list(); const available_docs = await format_sections_list();
return { return {

View File

@@ -46,6 +46,13 @@ export async function list_sections(server: SvelteMcp) {
icons, icons,
}, },
async (uri, { slug }) => { async (uri, { slug }) => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(
server.ctx.sessionId,
'svelte-doc-section',
Array.isArray(slug) ? slug.join(',') : slug,
);
}
const section = sections.find((section) => { const section = sections.find((section) => {
return slug === section.slug; return slug === section.slug;
}); });

View File

@@ -21,6 +21,9 @@ export function get_documentation(server: SvelteMcp) {
icons, icons,
}, },
async ({ section }) => { async ({ section }) => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'get-documentation');
}
let sections: string[]; let sections: string[];
if (Array.isArray(section)) { if (Array.isArray(section)) {

View File

@@ -12,6 +12,9 @@ export function list_sections(server: SvelteMcp) {
icons, icons,
}, },
async () => { async () => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'list-sections');
}
const formatted_sections = await format_sections_list(); const formatted_sections = await format_sections_list();
return { return {

View File

@@ -59,6 +59,9 @@ export function playground_link(server: SvelteMcp) {
icons, icons,
}, },
async ({ files, name, tailwind }) => { async ({ files, name, tailwind }) => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link');
}
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[] = [];
@@ -76,6 +79,9 @@ export function playground_link(server: SvelteMcp) {
} }
if (!has_app_svelte) { if (!has_app_svelte) {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link-no-app-svelte');
}
return { return {
isError: true, isError: true,
content: [ content: [

View File

@@ -46,12 +46,18 @@ export function svelte_autofixer(server: SvelteMcp) {
filename: filename_or_path, filename: filename_or_path,
desired_svelte_version: desired_svelte_version_unchecked, desired_svelte_version: desired_svelte_version_unchecked,
}) => { }) => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
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) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer-wrong-version');
}
return { return {
isError: true, isError: true,
content: [ content: [

View File

@@ -24,7 +24,10 @@ export const server = new McpServer(
instructions: instructions:
'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code. After you correct the component call this tool again to confirm all the issues are fixed.', 'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code. After you correct the component call this tool again to confirm all the issues are fixed.',
}, },
).withContext<{ db: LibSQLDatabase<Schema> }>(); ).withContext<{
db: LibSQLDatabase<Schema>;
track?: (sessionId: string, event: string, slug?: string) => Promise<void>;
}>();
export type SvelteMcp = typeof server; export type SvelteMcp = typeof server;

35
pnpm-lock.yaml generated
View File

@@ -71,6 +71,9 @@ importers:
'@tmcp/transport-http': '@tmcp/transport-http':
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1(tmcp@1.15.3(typescript@5.9.2)) version: 0.7.1(tmcp@1.15.3(typescript@5.9.2))
'@vercel/analytics':
specifier: ^1.5.0
version: 1.5.0(@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(tsx@4.20.6)))(svelte@5.39.6)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(tsx@4.20.6)))(react@18.3.1)(svelte@5.39.6)
tmcp: tmcp:
specifier: ^1.15.3 specifier: ^1.15.3
version: 1.15.3(typescript@5.9.2) version: 1.15.3(typescript@5.9.2)
@@ -1685,6 +1688,32 @@ packages:
peerDependencies: peerDependencies:
valibot: ^1.1.0 valibot: ^1.1.0
'@vercel/analytics@1.5.0':
resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==}
peerDependencies:
'@remix-run/react': ^2
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
react: ^18 || ^19 || ^19.0.0-rc
svelte: '>= 4'
vue: ^3
vue-router: ^4
peerDependenciesMeta:
'@remix-run/react':
optional: true
'@sveltejs/kit':
optional: true
next:
optional: true
react:
optional: true
svelte:
optional: true
vue:
optional: true
vue-router:
optional: true
'@vercel/nft@0.30.1': '@vercel/nft@0.30.1':
resolution: {integrity: sha512-2mgJZv4AYBFkD/nJ4QmiX5Ymxi+AisPLPcS/KPXVqniyQNqKXX+wjieAbDXQP3HcogfEbpHoRMs49Cd4pfkk8g==} resolution: {integrity: sha512-2mgJZv4AYBFkD/nJ4QmiX5Ymxi+AisPLPcS/KPXVqniyQNqKXX+wjieAbDXQP3HcogfEbpHoRMs49Cd4pfkk8g==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -5429,6 +5458,12 @@ snapshots:
dependencies: dependencies:
valibot: 1.1.0(typescript@5.9.2) valibot: 1.1.0(typescript@5.9.2)
'@vercel/analytics@1.5.0(@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(tsx@4.20.6)))(svelte@5.39.6)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(tsx@4.20.6)))(react@18.3.1)(svelte@5.39.6)':
optionalDependencies:
'@sveltejs/kit': 2.43.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(tsx@4.20.6)))(svelte@5.39.6)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(tsx@4.20.6))
react: 18.3.1
svelte: 5.39.6
'@vercel/nft@0.30.1(rollup@4.52.2)': '@vercel/nft@0.30.1(rollup@4.52.2)':
dependencies: dependencies:
'@mapbox/node-pre-gyp': 2.0.0 '@mapbox/node-pre-gyp': 2.0.0