1+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
2+ import { existsSync } from 'node:fs'
13import fs from 'node:fs/promises'
24import path from 'node:path'
35
46import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
57import type { ViteDevServer } from 'vite'
6- import type { SiteConfig } from 'vitepress'
8+ import type { DefaultTheme , SiteConfig , SiteData } from 'vitepress'
79
810type Awaitable < T > = T | PromiseLike < T > ;
911
10- export default function mcp ( mcpServer : McpServer , viteServer : ViteDevServer ) : Awaitable < void | McpServer > {
12+ export default async function mcp ( mcpServer : McpServer , viteServer : ViteDevServer ) : Awaitable < void | McpServer > {
1113 const vitepress = ( viteServer . config as any ) . vitepress as SiteConfig
12- console . log ( 'Setting up MCP server for Vue I18n docs...' , vitepress . userConfig . themeConfig )
13- // mcpServer.resource('contents', 'vue-i18n://contents', async (uri) => {
14- mcpServer . resource ( 'contents' , 'vue-i18n://contents' , {
15- uri : 'vue-i18n://contents' ,
16- name : 'Vue I18n Contents' ,
17- description : 'List of Contents for Vue I18n' ,
14+ const docRootDir = vitepress . srcDir
15+ const docFiles = ( vitepress . pages || [ ] ) . map ( page => path . resolve ( docRootDir , page ) ) . filter ( file => existsSync ( file ) )
16+ console . log ( 'VitePress pages:' , docFiles )
17+ const themeConfig = vitepress . site . themeConfig as DefaultTheme . Config
18+ console . log ( 'VitePress site config:' , vitepress )
19+ // console.log('Setting up MCP server for Vue I18n docs...', themeConfig.sidebar)
20+ const sidebarDirs = await getSidebarDirNames ( docRootDir )
21+ console . log ( 'Sidebar:' , themeConfig . sidebar , sidebarDirs )
22+ // for (const [key, value] of Object.entries(themeConfig.sidebar || {})) {
23+ // console.log(`Sidebar item: ${key}`)
24+ // const items = (themeConfig.sidebar || {})[key] as DefaultTheme.SidebarItem[]
25+ // for (const item of items) {
26+ // console.log(item.text, item)
27+ // }
28+ // }
29+
30+ // @ts -expect-error -- FIXME:
31+ mcpServer . resource ( 'contents' , 'vue-i18n://docs' , {
32+ uri : 'vue-i18n://docs' ,
33+ name : 'Vue I18n Documentation top' ,
34+ description : 'Vue I18n documentation root, provides categories and links to documentation pages.' ,
1835 } , async ( uri ) => {
19- const filePath = path . resolve ( import . meta. dirname , './dist/llms.txt' )
20- const content = await fs . readFile ( filePath , 'utf-8' )
36+ const content = renderMarkdownTop ( vitepress . site , getSideBar ( sidebarDirs , themeConfig . sidebar || { } ) )
2137 return {
2238 contents : [
2339 {
@@ -27,5 +43,82 @@ export default function mcp(mcpServer: McpServer, viteServer: ViteDevServer): Aw
2743 ]
2844 }
2945 } )
46+
47+ const pageUri = 'vue-i18n://docs{page}'
48+ const pageTempalte = new ResourceTemplate ( pageUri , { list : undefined } )
49+ mcpServer . resource ( 'pages' , pageTempalte , {
50+ uri : pageUri ,
51+ name : 'Vue I18n Documentation page' ,
52+ description : 'Vue I18n documentation page content' ,
53+ } , async ( uri , params ) => {
54+ console . log ( 'Fetching page:' , uri , params )
55+ const p = Array . isArray ( params . page ) ? params . page [ 0 ] : params . page
56+ const pagePath = path . resolve ( docRootDir , p )
57+ const file = await fs . readFile ( pagePath , 'utf-8' )
58+ return {
59+ contents : [ {
60+ uri : uri . href ,
61+ text : file ,
62+ } ]
63+ }
64+ } )
65+
3066 return mcpServer
3167}
68+
69+ function renderMarkdownTop ( site : SiteData , sidebar : ReturnType < typeof getSideBar > ) {
70+ return `# ${ site . title }
71+
72+ ${ site . description }
73+
74+ ## Table of Contents
75+
76+ ${ Object . entries ( sidebar ) . map ( ( [ _caegory , items ] ) => {
77+ const buf = [ ] as string [ ]
78+ items . reduce ( ( acc , item ) => {
79+ acc . push ( `### ${ item . text } ` , '' )
80+ const items = ( item . items || [ ] ) as DefaultTheme . SidebarItem [ ]
81+ const itemBuf = [ ] as string [ ]
82+ for ( const subItem of items ) {
83+ if ( subItem . link ) {
84+ itemBuf . push ( `- [${ subItem . text } ](${ subItem . link . endsWith ( '.md' ) ? subItem . link : `${ subItem . link } .md` } )` )
85+ }
86+ }
87+ itemBuf . push ( '' )
88+
89+ if ( itemBuf . length ) {
90+ acc . push ( itemBuf . join ( '\n' ) )
91+ }
92+ return acc
93+ } , buf )
94+ return buf . join ( '\n' )
95+ } ) . join ( '\n\n' )
96+ }
97+ `
98+ }
99+
100+ function getSideBar ( sidebarDirs : string [ ] , sideBar : DefaultTheme . Sidebar ) {
101+ return Object . keys ( sideBar ) . reduce ( ( acc , key ) => {
102+ const category = key . split ( '/' ) . filter ( Boolean ) [ 0 ]
103+ if ( sidebarDirs . includes ( category ) ) {
104+ acc [ category ] = sideBar [ key ] || [ ]
105+ }
106+ return acc
107+ } , { } as { [ key : string ] : DefaultTheme . SidebarItem [ ] } )
108+ }
109+
110+ const EXCLUDE_DIR_NAMES = [
111+ '.vitepress' ,
112+ 'index.md' ,
113+ 'public' ,
114+ ]
115+
116+ async function getSidebarDirNames ( root : string ) : Promise < string [ ] > {
117+ const dirs = await fs . readdir ( root , { withFileTypes : true } ) ;
118+ return dirs . reduce ( ( acc , dir ) => {
119+ if ( dir . isDirectory ( ) && ! EXCLUDE_DIR_NAMES . includes ( dir . name ) ) {
120+ acc . push ( dir . name )
121+ }
122+ return acc
123+ } , [ ] as string [ ] )
124+ }
0 commit comments