forked from didier/llmctx
-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathmcpHandler.ts
More file actions
269 lines (232 loc) · 8.78 KB
/
Copy pathmcpHandler.ts
File metadata and controls
269 lines (232 loc) · 8.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import { z } from 'zod'
import { createMcpHandler } from 'mcp-handler'
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
import { env } from '$env/dynamic/private'
import { ContentDbService } from '$lib/server/contentDb'
import { ContentDistilledDbService } from '$lib/server/contentDistilledDb'
import type { DbContent } from '$lib/types/db'
import { listSectionsHandler } from '$lib/handlers/listSectionsHandler'
import { getDocumentationHandler } from '$lib/handlers/getDocumentationHandler'
import { registerTemplatePrompts } from '$lib/mcpPrompts'
import { PRESET_CONFIGS } from '$lib/mcpPresets'
import { logAlways, logErrorAlways } from '$lib/log'
import {
cleanDocumentationPath,
extractTitleFromPath,
removeFrontmatter
} from '$lib/utils/pathUtils'
import { createSvelteDeveloperPromptWithTask } from '$lib/utils/prompts'
// Helper function to search for sections in the database
async function searchSectionInDb(query: string): Promise<DbContent | null> {
try {
// Use the searchContent method with default parameters
const result = await ContentDbService.searchContent(query, 'content')
return result
} catch (error) {
logErrorAlways(`Error searching for section "${query}":`, error)
return null
}
}
// Helper function to get title from metadata or path
function getTitleFromMetadata(
metadata: Record<string, unknown> | undefined,
fallbackPath: string
): string {
if (metadata?.title && typeof metadata.title === 'string') {
return metadata.title
}
return extractTitleFromPath(fallbackPath)
}
export const handler = createMcpHandler(
(server) => {
server.tool(
'list_sections',
'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.',
{},
listSectionsHandler
)
server.tool(
'get_documentation',
'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.',
{
section: z
.union([z.string(), z.array(z.string())])
.describe(
'The section name(s) to retrieve. Can search by title (e.g., "$state", "load functions") or file path (e.g., "docs/svelte/state.md"). Supports single string and array of strings'
)
},
getDocumentationHandler
)
// Main developer prompt with optional task parameter
server.registerPrompt(
'svelte-developer',
{
title: 'Svelte 5 Developer Assistant',
description:
'Expert-level guidance for Svelte 5 and SvelteKit development with optional task-specific focus',
argsSchema: {
task: z.string().optional().describe('Optional specific task or requirement to focus on')
}
},
({ task }) => {
const promptText = createSvelteDeveloperPromptWithTask(task)
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: promptText
}
}
]
}
}
)
// Register the template-based prompts
registerTemplatePrompts(server)
server.resource(
'svelte-doc',
new ResourceTemplate('svelte-llm://{+slug}', {
list: async () => {
const resources = []
// First, add preset resources
for (const preset of PRESET_CONFIGS) {
resources.push({
name: `📦 ${preset.title}`,
uri: `svelte-llm://${preset.id}`,
description: preset.description
})
}
const documents = await ContentDbService.getDocumentationSections()
logAlways(`Found ${documents.length} individual documents for resource listing`)
for (const doc of documents) {
const title = getTitleFromMetadata(doc.metadata, doc.path)
const cleanPath = cleanDocumentationPath(doc.path)
resources.push({
// Use title and clean path for better display, prefix with 📄 to distinguish from presets
name: `📄 ${title} (${cleanPath})`,
// Use cleaned path with prefix to avoid conflicts with preset IDs
uri: `svelte-llm://doc/${cleanPath}`,
// Add description from metadata if available
description: doc.metadata?.description as string | undefined
})
}
logAlways(
`Returning ${resources.length} total resources (${PRESET_CONFIGS.length} presets + ${documents.length} individual docs)`
)
return { resources }
},
complete: {
slug: async (query) => {
const suggestions = []
// Add preset completions first
for (const preset of PRESET_CONFIGS) {
if (
preset.id.toLowerCase().includes(query.toLowerCase()) ||
preset.title.toLowerCase().includes(query.toLowerCase())
) {
suggestions.push(preset.id)
}
}
// Then add individual document completions
const searchResults = await ContentDbService.searchAllContent(query)
const paths = searchResults.map((doc) => `doc/${cleanDocumentationPath(doc.path)}`)
suggestions.push(...paths)
logAlways(`Found ${suggestions.length} completions for query: ${query}`)
return suggestions
}
}
}),
async (uri, { slug }) => {
// If array for some reason, use the first element
const slugString = Array.isArray(slug) ? slug[0] : slug
logAlways(`Resource requested with slug: ${slugString}`)
// Check if this is a preset request
const preset = PRESET_CONFIGS.find((p) => p.id === slugString)
if (preset) {
logAlways(`Serving preset resource: ${preset.id}`)
// Get aggregated content for this preset
const content = await ContentDistilledDbService.getContentByPathPatterns(preset.patterns)
if (!content || content.trim().length === 0) {
throw new Error(
`No content found for preset: ${preset.id}. The distilled content may not be available yet.`
)
}
return {
contents: [
{
uri: uri.toString(),
type: 'text',
text: content,
metadata: {
title: preset.title,
description: preset.description,
type: 'preset',
id: preset.id
}
}
]
}
}
// Handle individual document requests (with 'doc/' prefix)
let documentSlug = slugString
if (slugString.startsWith('doc/')) {
documentSlug = slugString.substring(4) // Remove 'doc/' prefix
}
logAlways(`Serving individual document with slug: ${documentSlug}`)
// First try intelligent search (by title or partial path)
let document = await searchSectionInDb(documentSlug)
// If not found, try exact path match with cleaned path
if (!document) {
// Try to find by cleaned path - need to search all content and match cleaned paths
const allDocs = await ContentDbService.getFilteredContent()
document =
allDocs.find((doc) => cleanDocumentationPath(doc.path) === documentSlug) || null
}
// If still not found, try with the full path pattern (for backward compatibility)
if (!document && !documentSlug.startsWith('apps/svelte.dev/content/')) {
const fullPath = `apps/svelte.dev/content/docs/${documentSlug}`
document = await ContentDbService.getContentByPath(fullPath)
}
// If still not found, try direct database path match (for backward compatibility)
if (!document) {
document = await ContentDbService.getContentByPath(documentSlug)
}
if (!document) {
throw new Error(
`Document not found for slug: ${documentSlug}. Try using a document title (e.g., "$state") or a valid path.`
)
}
const title = getTitleFromMetadata(document.metadata, document.path)
const cleanPath = cleanDocumentationPath(document.path)
// Remove frontmatter from the content before returning it
const contentWithoutFrontmatter = removeFrontmatter(document.content)
logAlways(`Returning individual document: ${title} (${cleanPath})`)
return {
contents: [
{
uri: uri.toString(),
type: 'text',
text: contentWithoutFrontmatter,
// Include metadata in the response
metadata: {
title,
path: cleanPath,
originalPath: document.path,
type: 'document'
}
}
]
}
}
)
},
{},
{
maxDuration: 3600,
basePath: '/mcp',
verboseLogs: false,
redisUrl: env.REDIS_URL ? env.REDIS_URL : 'redis://127.0.0.1:6379'
}
)