Skip to content

Commit 655bb5b

Browse files
committed
fix(mcp): use /raw fetching instead of rawbody
1 parent ab75d2c commit 655bb5b

File tree

7 files changed

+119
-116
lines changed

7 files changed

+119
-116
lines changed

content.config.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -216,26 +216,23 @@ export default defineContentConfig({
216216
source: [docsV5Source, examplesV5Source],
217217
schema: z.object({
218218
titleTemplate: z.string().optional(),
219-
links: z.array(Button),
220-
rawbody: z.string()
219+
links: z.array(Button)
221220
})
222221
}),
223222
docsv4: defineCollection({
224223
type: 'page',
225224
source: [docsV4Source, examplesV4Source],
226225
schema: z.object({
227226
titleTemplate: z.string().optional(),
228-
links: z.array(Button),
229-
rawbody: z.string()
227+
links: z.array(Button)
230228
})
231229
}),
232230
docsv3: defineCollection({
233231
type: 'page',
234232
source: [docsV3Source, examplesV3Source],
235233
schema: z.object({
236234
titleTemplate: z.string().optional(),
237-
links: z.array(Button),
238-
rawbody: z.string()
235+
links: z.array(Button)
239236
})
240237
}),
241238
blog: defineCollection({
@@ -247,8 +244,7 @@ export default defineContentConfig({
247244
date: z.string().date(),
248245
draft: z.boolean().default(false),
249246
category: z.enum(['Release', 'Tutorial', 'Announcement', 'Article']),
250-
tags: z.array(z.string()),
251-
rawbody: z.string()
247+
tags: z.array(z.string())
252248
})
253249
}),
254250
landing: defineCollection({
@@ -279,8 +275,7 @@ export default defineContentConfig({
279275
logoIcon: z.string(),
280276
category: z.string(),
281277
nitroPreset: z.string(),
282-
website: z.string().url(),
283-
rawbody: z.string()
278+
website: z.string().url()
284279
})
285280
}),
286281
manualSponsors: defineCollection({

server/mcp/prompts/deployment-guide.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default defineMcpPrompt({
1010
const event = useEvent()
1111

1212
const deployProviders = await queryCollection(event, 'deploy')
13-
.select('title', 'path', 'description', 'rawbody', 'logoSrc', 'logoIcon', 'category', 'nitroPreset', 'website', 'sponsor')
13+
.select('title', 'path', 'description')
1414
.all()
1515

1616
const allProviders = deployProviders?.map(p => ({
@@ -24,20 +24,12 @@ export default defineMcpPrompt({
2424
p.title.toLowerCase().includes(provider.toLowerCase())
2525
)
2626

27-
let providerDetails = null
27+
let providerContent: string | null = null
2828
if (matchingProvider) {
29-
providerDetails = {
30-
title: matchingProvider.title,
31-
path: matchingProvider.path,
32-
description: matchingProvider.description,
33-
content: matchingProvider.rawbody,
34-
logoSrc: matchingProvider.logoSrc,
35-
logoIcon: matchingProvider.logoIcon,
36-
category: matchingProvider.category,
37-
nitroPreset: matchingProvider.nitroPreset,
38-
website: matchingProvider.website,
39-
sponsor: matchingProvider.sponsor,
40-
url: `https://nuxt.com${matchingProvider.path}`
29+
try {
30+
providerContent = await $fetch<string>(`/raw${matchingProvider.path}.md`)
31+
} catch {
32+
providerContent = null
4133
}
4234
}
4335

@@ -47,7 +39,7 @@ export default defineMcpPrompt({
4739
role: 'user' as const,
4840
content: {
4941
type: 'text' as const,
50-
text: `Help me deploy my Nuxt application to ${provider}. ${providerDetails ? `Here are the deployment instructions: ${JSON.stringify(providerDetails, null, 2)}` : `Here are all available providers: ${JSON.stringify(allProviders, null, 2)}`}`
42+
text: `Help me deploy my Nuxt application to ${provider}. ${providerContent ? `Here are the deployment instructions:\n\n${providerContent}` : `Here are all available providers: ${JSON.stringify(allProviders, null, 2)}`}`
5143
}
5244
}
5345
]

server/mcp/tools/get-blog-post.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { z } from 'zod'
2-
import { queryCollection } from '@nuxt/content/server'
32

43
export default defineMcpTool({
54
description: `Retrieves the full content and details of a specific Nuxt blog post.
@@ -13,32 +12,24 @@ WHEN NOT TO USE: If you don't know the exact path and need to search/discover, u
1312
1413
EXAMPLES: "/blog/v4", "/blog/nuxt3", "/blog/nuxt-on-the-edge"`,
1514
inputSchema: {
16-
path: z.string().describe('The path to the blog post (e.g., /blog/v4)')
15+
path: z.string().describe('The path to the blog post (e.g., /blog/v4)'),
16+
sections: z.array(z.string()).optional().describe('Specific h2 section titles to return. If omitted, returns full content.')
1717
},
1818
cache: '1h',
19-
async handler({ path }) {
20-
const event = useEvent()
19+
async handler({ path, sections }) {
20+
try {
21+
const fullContent = await $fetch<string>(`/raw${path}.md`)
2122

22-
const post = await queryCollection(event, 'blog')
23-
.where('path', '=', path)
24-
.select('title', 'path', 'description', 'rawbody', 'date', 'category', 'tags', 'authors', 'image')
25-
.first()
23+
let content = fullContent
24+
if (sections?.length) {
25+
content = extractSections(fullContent, sections)
26+
}
2627

27-
if (!post) {
28-
return errorResult('Blog post not found')
28+
return {
29+
content: [{ type: 'text' as const, text: content }]
30+
}
31+
} catch (error) {
32+
return errorResult(`Blog post not found: ${error}`)
2933
}
30-
31-
return jsonResult({
32-
title: post.title,
33-
path: post.path,
34-
description: post.description,
35-
content: post.rawbody,
36-
date: post.date,
37-
category: post.category,
38-
tags: post.tags,
39-
authors: post.authors,
40-
image: post.image,
41-
url: `https://nuxt.com${post.path}`
42-
})
4334
}
4435
})
Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { z } from 'zod'
2-
import { queryCollection } from '@nuxt/content/server'
32

43
export default defineMcpTool({
54
description: `Retrieves detailed deployment instructions and setup guide for a specific hosting provider.
@@ -13,33 +12,24 @@ WHEN NOT TO USE: If the user is asking about options or comparing providers, use
1312
1413
EXAMPLES: "/deploy/vercel", "/deploy/cloudflare", "/deploy/netlify", "/deploy/aws", "/deploy/node-server"`,
1514
inputSchema: {
16-
path: z.string().describe('The path to the deploy provider (e.g., /deploy/vercel)')
15+
path: z.string().describe('The path to the deploy provider (e.g., /deploy/vercel)'),
16+
sections: z.array(z.string()).optional().describe('Specific h2 section titles to return. If omitted, returns full content.')
1717
},
1818
cache: '1h',
19-
async handler({ path }) {
20-
const event = useEvent()
19+
async handler({ path, sections }) {
20+
try {
21+
const fullContent = await $fetch<string>(`/raw${path}.md`)
2122

22-
const provider = await queryCollection(event, 'deploy')
23-
.where('path', '=', path)
24-
.select('title', 'path', 'description', 'rawbody', 'logoSrc', 'logoIcon', 'category', 'nitroPreset', 'website', 'sponsor')
25-
.first()
23+
let content = fullContent
24+
if (sections?.length) {
25+
content = extractSections(fullContent, sections)
26+
}
2627

27-
if (!provider) {
28-
return errorResult('Deploy provider not found')
28+
return {
29+
content: [{ type: 'text' as const, text: content }]
30+
}
31+
} catch (error) {
32+
return errorResult(`Deploy provider not found: ${error}`)
2933
}
30-
31-
return jsonResult({
32-
title: provider.title,
33-
path: provider.path,
34-
description: provider.description,
35-
content: provider.rawbody,
36-
logoSrc: provider.logoSrc,
37-
logoIcon: provider.logoIcon,
38-
category: provider.category,
39-
nitroPreset: provider.nitroPreset,
40-
website: provider.website,
41-
sponsor: provider.sponsor,
42-
url: `https://nuxt.com${provider.path}`
43-
})
4434
}
4535
})

server/mcp/tools/get-documentation-page.ts

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { queryCollection } from '@nuxt/content/server'
21
import { z } from 'zod'
32

43
export default defineMcpTool({
@@ -31,30 +30,24 @@ Common Issues:
3130
- "/docs/4.x/guide/going-further/debugging" - debugging
3231
- "/docs/4.x/guide/going-further/error-handling" - errors`,
3332
inputSchema: {
34-
path: z.string().describe('The path to the documentation page (e.g., /docs/4.x/getting-started/introduction)')
33+
path: z.string().describe('The path to the documentation page (e.g., /docs/4.x/getting-started/introduction)'),
34+
sections: z.array(z.string()).optional().describe('Specific h2 section titles to return (e.g., ["Usage", "API"]). If omitted, returns full documentation.')
3535
},
3636
cache: '30m',
37-
async handler({ path }) {
38-
const event = useEvent()
39-
const docsVersion = path.includes('/docs/5.x') ? 'docsv5' : path.includes('/docs/4.x') ? 'docsv4' : 'docsv3'
40-
41-
const page = await queryCollection(event, docsVersion)
42-
.where('path', '=', path)
43-
.select('title', 'path', 'description', 'rawbody', 'links')
44-
.first()
45-
46-
if (!page) {
47-
return errorResult('Documentation page not found')
37+
async handler({ path, sections }) {
38+
try {
39+
const fullContent = await $fetch<string>(`/raw${path}.md`)
40+
41+
let content = fullContent
42+
if (sections?.length) {
43+
content = extractSections(fullContent, sections)
44+
}
45+
46+
return {
47+
content: [{ type: 'text' as const, text: content }]
48+
}
49+
} catch (error) {
50+
return errorResult(`Documentation page not found: ${error}`)
4851
}
49-
50-
return jsonResult({
51-
title: page.title,
52-
path: page.path,
53-
description: page.description,
54-
content: page.rawbody,
55-
version: page.path.includes('/docs/5.x') ? '5.x' : page.path.includes('/docs/4.x') ? '4.x' : '3.x',
56-
links: page.links,
57-
url: `https://nuxt.com${page.path}`
58-
})
5952
}
6053
})
Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
import { z } from 'zod'
2-
import { queryCollection } from '@nuxt/content/server'
32

43
export default defineMcpTool({
54
description: 'Gets the getting started guide for Nuxt. Parameters: version (enum, optional) - Nuxt version.',
65
inputSchema: {
76
// TODO: add '5.x' when Nuxt 5 is released
8-
version: z.enum(['3.x', '4.x']).optional().default('4.x').describe('Nuxt version')
7+
version: z.enum(['3.x', '4.x']).optional().default('4.x').describe('Nuxt version'),
8+
sections: z.array(z.string()).optional().describe('Specific h2 section titles to return. If omitted, returns full guide.')
99
},
1010
cache: '30m',
11-
async handler({ version }) {
12-
const event = useEvent()
11+
async handler({ version, sections }) {
1312
const path = `/docs/${version}/getting-started/introduction`
14-
const docsVersion = version === '4.x' ? 'docsv4' : 'docsv3'
1513

16-
const page = await queryCollection(event, docsVersion)
17-
.where('path', '=', path)
18-
.select('title', 'path', 'description', 'rawbody', 'links')
19-
.first()
14+
try {
15+
const fullContent = await $fetch<string>(`/raw${path}.md`)
2016

21-
if (!page) {
22-
return errorResult('Getting started guide not found')
23-
}
17+
let content = fullContent
18+
if (sections?.length) {
19+
content = extractSections(fullContent, sections)
20+
}
2421

25-
return jsonResult({
26-
title: page.title,
27-
path: page.path,
28-
description: page.description,
29-
content: page.rawbody,
30-
version: version,
31-
links: page.links,
32-
url: `https://nuxt.com${page.path}`
33-
})
22+
return {
23+
content: [{ type: 'text' as const, text: content }]
24+
}
25+
} catch (error) {
26+
return errorResult(`Getting started guide not found: ${error}`)
27+
}
3428
}
3529
})

server/utils/mcp.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Extract specific h2 sections from markdown content.
3+
* Always includes the title (h1) and description (first blockquote).
4+
*/
5+
export function extractSections(markdown: string, sectionTitles: string[]): string {
6+
const lines = markdown.split('\n')
7+
const result: string[] = []
8+
9+
const normalizedTitles = sectionTitles.map(t => t.toLowerCase().trim())
10+
11+
let inHeader = true
12+
for (const line of lines) {
13+
if (inHeader) {
14+
result.push(line)
15+
if (line.startsWith('>') && result.length > 1) {
16+
result.push('')
17+
inHeader = false
18+
}
19+
continue
20+
}
21+
break
22+
}
23+
24+
let currentSection: string | null = null
25+
let sectionContent: string[] = []
26+
27+
for (const line of lines) {
28+
if (line.startsWith('## ')) {
29+
if (currentSection && normalizedTitles.includes(currentSection.toLowerCase())) {
30+
result.push(...sectionContent)
31+
result.push('')
32+
}
33+
currentSection = line.replace('## ', '').trim()
34+
sectionContent = [line]
35+
continue
36+
}
37+
38+
if (currentSection) {
39+
sectionContent.push(line)
40+
}
41+
}
42+
43+
if (currentSection && normalizedTitles.includes(currentSection.toLowerCase())) {
44+
result.push(...sectionContent)
45+
}
46+
47+
return result.join('\n').trim()
48+
}

0 commit comments

Comments
 (0)