Skip to main content
Glama

MCP Doc Server

service.ts9.72 kB
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js' import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' import { join } from 'path' import { getSiteMap, url2LocalPath, getMDXFileContent, DOC_ROUTE_BASE_PATH_MAPPING, } from './utils/mdx.js' import { postMultiSearch } from './utils/search.js' import { z } from 'zod' const DOCS_MD_DIR = join(process.cwd(), '.docs') const DOCS_DIST_DIR = join(process.cwd(), '.docs') interface SearchResult { path: string title?: string score?: number content?: string } interface DocInfo { path: string content: string } interface CodeSuggestion { type: string description: string code: string } export const createMcpServer = () => { const server = new McpServer({ name: 'MDX Documentation Server', version: '1.0.0', }) const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }) // server.resource( // 'docs', // new ResourceTemplate('docs://{path}', { // list: async () => { // const siteMapFile = join(DOCS_DIST_DIR, 'sitemap.xml') // const siteMapContent = await getSiteMap(siteMapFile) // return { // resources: siteMapContent.map((file: { loc: string }) => { // const encodedPath = encodeURIComponent(file.loc) // return { // name: file.loc, // uri: `docs://${encodedPath}`, // description: file.loc, // } // }), // } // }, // }), // async (uri, { path }) => { // const decodedPath = decodeURIComponent(`${path}`) // const localPath = url2LocalPath(decodedPath) // if (!localPath) { // throw new Error(`Invalid URL: ${uri.href}`) // } // const mdxFile = await getMDXFileContent(join(DOCS_MD_DIR, localPath)) // return { // contents: [ // { // uri: uri.href, // text: mdxFile, // }, // ], // } // } // ) server.tool( 'search-docs', 'search for documentation by query', { query: z.string().describe('search query'), }, async ({ query }) => { try { const res = await postMultiSearch(query) return { content: [{ type: 'text', text: JSON.stringify(res) }], } } catch (err: unknown) { const error = err as Error return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], isError: true, } } } ) // server.tool( // 'get-doc-structure', // { // category: z.string().optional().default(''), // }, // async ({ category }) => { // try { // const siteMapFile = join(DOCS_DIST_DIR, 'sitemap.xml') // const siteMapContent = await getSiteMap(siteMapFile) // let filteredContent = siteMapContent // if (category) { // filteredContent = siteMapContent.filter((item: { loc: string }) => // item.loc.toLowerCase().includes(category.toLowerCase()) // ) // } // const structure = filteredContent.map((item: { loc: string }) => { // const parts = item.loc.split('/') // return { // path: item.loc, // name: parts[parts.length - 1] || parts[parts.length - 2], // level: parts.length - 1, // category: parts.length > 1 ? parts[0] : '根目录', // uri: `docs://${encodeURIComponent(item.loc)}`, // text: item, // } // }) // return { // content: [ // { // type: 'text', // text: JSON.stringify( // { // total: structure.length, // categories: [ // ...new Set( // structure.map( // (item: { category: string }) => item.category // ) // ), // ], // structure, // }, // null, // 2 // ), // }, // ], // } // } catch (err: unknown) { // const error = err as Error // return { // content: [ // { // type: 'text', // text: `获取文档结构时出错: ${error.message}`, // }, // ], // isError: true, // } // } // } // ) server.tool( 'list-docs', 'list documents by category', { category: z .string() .nullable() .optional() .default(null) .describe('document category'), limit: z .number() .nullable() .optional() .default(0) .describe('number of documents to return'), sortBy: z .enum(['path', 'name']) .nullable() .optional() .default('name') .describe('sort order(path or name)'), }, async ({ category, limit, sortBy }) => { try { const actualLimit = limit ?? 0 console.log('actualLimit 1', actualLimit, 'limit', limit) const actualSortBy = sortBy ?? 'name' const siteMapFile = join(DOCS_DIST_DIR, 'sitemap.xml') const siteMapContent = await getSiteMap(siteMapFile) // filter categories const filteredDocs = [] if (category) { const filteredByCategory = siteMapContent.filter( (item: { loc: string }) => item.loc.toLowerCase().includes(category.toLowerCase()) ) filteredDocs.push(...filteredByCategory) } else { filteredDocs.push(...siteMapContent) } // 格式化文档列表 let docsList = filteredDocs.map( (item: { loc: string; category?: string }) => { const parts = item.loc.split('/') const lastPartIndex = parts.length - 1 const lastPart = lastPartIndex >= 0 ? parts[lastPartIndex] : '' const decodedPath = decodeURIComponent(`${item.loc}`) const localPath = url2LocalPath(decodedPath) return { path: item.loc, name: lastPart, category: item.category, depth: parts.length, uri: `docs://${localPath}`, localPath: localPath, } } ) // 排序 if (actualSortBy === 'name') { docsList.sort((a: { name: string }, b: { name: any }) => a.name.localeCompare(b.name) ) } else { docsList.sort((a: { path: string }, b: { path: any }) => a.path.localeCompare(b.path) ) } // 限制结果数量 if (actualLimit > 0) { docsList = docsList.slice(0, actualLimit) } console.log('docsList', docsList.length, 'actualLimit', actualLimit) return { content: [ { type: 'text', text: JSON.stringify( { categories: Object.keys(DOC_ROUTE_BASE_PATH_MAPPING), total: docsList.length, docs: docsList, }, null, 2 ), }, ], } } catch (err: unknown) { const error = err as Error return { content: [ { type: 'text', text: `获取文档列表时出错: ${error.message}`, }, ], isError: true, } } } ) server.tool( 'get-doc-content', 'get document markdown content by URI', { uri: z.string().describe('document URI'), }, async ({ uri }) => { try { // 解析URI if (!uri.startsWith('docs://')) { throw new Error('Invalid URI format, must start with docs://') } const encodedPath = uri.replace('docs://', '') // const decodedPath = decodeURIComponent(encodedPath) // const localPath = url2LocalPath(decodedPath) const localPath = encodedPath if (!localPath) { throw new Error(`Invalid local path: ${uri}`) } // 获取文档内容 const mdxContent = await getMDXFileContent(join(DOCS_MD_DIR, localPath)) return { content: [ { type: 'text', text: mdxContent, }, ], } } catch (err: unknown) { const error = err as Error return { content: [ { type: 'text', text: `Error when retrieving document content: ${error.message}`, }, ], isError: true, } } } ) server.prompt('doc', { question: z.string() }, ({ question }) => ({ messages: [ { role: 'user', content: { type: 'text', text: `Please read this question: ${question}.\n\nThen extract the key words and query by #search-docs tool. You can get document URI from response. \n\nThen get document content by #get-doc-content tool. You can get document content from response.\n\nFinally, please give me a summary of the document content and the code suggestion if any.`, }, }, ], })) return { server, transport, setupServer: async () => { await server.connect(transport) }, } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Shengwang-Community/doc-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server