From e3b0eff1a04f1b6ba72bf2ce4135e9617c99ab73 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Wed, 29 Oct 2025 21:20:57 +0000 Subject: [PATCH 1/7] feat(mcp): add server --- layer/package.json | 4 +- layer/server/api/mcp/get-page.get.ts | 52 +++ layer/server/api/mcp/list-pages.get.ts | 45 +++ layer/server/api/mcp/search-content.get.ts | 49 +++ layer/server/routes/mcp.ts | 80 ++++ layer/server/utils/mcp.ts | 32 ++ pnpm-lock.yaml | 416 +++++++++++++++++++++ 7 files changed, 677 insertions(+), 1 deletion(-) create mode 100644 layer/server/api/mcp/get-page.get.ts create mode 100644 layer/server/api/mcp/list-pages.get.ts create mode 100644 layer/server/api/mcp/search-content.get.ts create mode 100644 layer/server/routes/mcp.ts create mode 100644 layer/server/utils/mcp.ts diff --git a/layer/package.json b/layer/package.json index 4de85e706..6a350e49a 100644 --- a/layer/package.json +++ b/layer/package.json @@ -25,6 +25,7 @@ "@iconify-json/lucide": "^1.2.69", "@iconify-json/simple-icons": "^1.2.54", "@iconify-json/vscode-icons": "^1.2.32", + "@modelcontextprotocol/sdk": "^1.0.4", "@nuxt/content": "https://pkg.pr.new/@nuxt/content@dd854d5", "@nuxt/image": "^1.11.0", "@nuxt/kit": "^4.1.3", @@ -43,7 +44,8 @@ "pkg-types": "^2.3.0", "scule": "^1.3.0", "tailwindcss": "^4.1.14", - "ufo": "^1.6.1" + "ufo": "^1.6.1", + "zod": "^3.24.1" }, "peerDependencies": { "better-sqlite3": "12.x", diff --git a/layer/server/api/mcp/get-page.get.ts b/layer/server/api/mcp/get-page.get.ts new file mode 100644 index 000000000..461c7012b --- /dev/null +++ b/layer/server/api/mcp/get-page.get.ts @@ -0,0 +1,52 @@ +import { z } from 'zod' +import { queryCollection } from '@nuxt/content/server' +import type { Collections } from '@nuxt/content' +import { stringify } from 'minimark/stringify' + +const querySchema = z.object({ + path: z.string().describe('The page path (e.g., /en/getting-started/installation)'), +}) + +export default defineCachedEventHandler(async (event) => { + const { path } = await getValidatedQuery(event, querySchema.parse) + const config = useRuntimeConfig(event).public + + const siteUrl = config.site?.url || 'http://localhost:3000' + const availableLocales = getAvailableLocales(config) + const collectionName = config.i18n?.locales + ? getCollectionFromPath(path, availableLocales) + : 'docs' + + const page = await queryCollection(event, collectionName as keyof Collections) + .where('path', '=', path) + .select('title', 'path', 'description', 'body') + .first() + + if (!page) { + throw createError({ + statusCode: 404, + statusMessage: 'Page not found', + }) + } + + if (page.body.value[0]?.[0] !== 'h1') { + page.body.value.unshift(['blockquote', {}, page.description]) + page.body.value.unshift(['h1', {}, page.title]) + } + + const content = stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' }) + + return { + title: page.title, + path: page.path, + description: page.description, + content, + url: `${siteUrl}${page.path}`, + } +}, { + maxAge: 60 * 60, + getKey: (event) => { + const query = getQuery(event) + return `mcp-get-page-${query.path}` + }, +}) diff --git a/layer/server/api/mcp/list-pages.get.ts b/layer/server/api/mcp/list-pages.get.ts new file mode 100644 index 000000000..901440843 --- /dev/null +++ b/layer/server/api/mcp/list-pages.get.ts @@ -0,0 +1,45 @@ +import { z } from 'zod' +import { queryCollection } from '@nuxt/content/server' +import type { Collections } from '@nuxt/content' + +const querySchema = z.object({ + locale: z.string().optional().describe('Language code (e.g., "en", "fr")'), +}) + +export default defineCachedEventHandler(async (event) => { + const { locale } = await getValidatedQuery(event, querySchema.parse) + const config = useRuntimeConfig(event).public + + const siteUrl = config.site?.url || 'http://localhost:3000' + const availableLocales = getAvailableLocales(config) + const collections = getCollectionsToQuery(locale, availableLocales) + + const allPages = await Promise.all( + collections.map(async (collectionName) => { + try { + const pages = await queryCollection(event, collectionName as keyof Collections) + .select('title', 'path', 'description') + .all() + + return pages.map(page => ({ + title: page.title, + path: page.path, + description: page.description, + locale: collectionName.replace('docs_', ''), + url: `${siteUrl}${page.path}`, + })) + } + catch { + return [] + } + }), + ) + + return allPages.flat() +}, { + maxAge: 60 * 60, + getKey: (event) => { + const query = getQuery(event) + return `mcp-list-pages-${query.locale || 'all'}` + }, +}) diff --git a/layer/server/api/mcp/search-content.get.ts b/layer/server/api/mcp/search-content.get.ts new file mode 100644 index 000000000..8450ecaf8 --- /dev/null +++ b/layer/server/api/mcp/search-content.get.ts @@ -0,0 +1,49 @@ +import { z } from 'zod' +import { queryCollection } from '@nuxt/content/server' +import type { Collections } from '@nuxt/content' + +const querySchema = z.object({ + query: z.string().describe('Search query'), + locale: z.string().optional().describe('Language code (e.g., "en", "fr")'), + limit: z.number().optional().default(10).describe('Maximum number of results'), +}) + +export default defineCachedEventHandler(async (event) => { + const { query, locale, limit } = await getValidatedQuery(event, querySchema.parse) + + const config = useRuntimeConfig(event).public + const siteUrl = config.site?.url || 'http://localhost:3000' + const availableLocales = getAvailableLocales(config) + const collections = getCollectionsToQuery(locale, availableLocales) + + const allResults = await Promise.all( + collections.map(async (collectionName) => { + try { + const pages = await queryCollection(event, collectionName as keyof Collections) + .where('title', 'LIKE', `%${query}%`) + .select('title', 'path', 'description') + .limit(Number(limit)) + .all() + + return pages.map(page => ({ + title: page.title, + path: page.path, + description: page.description, + locale: collectionName.replace('docs_', ''), + url: `${siteUrl}${page.path}`, + })) + } + catch { + return [] + } + }), + ) + + return allResults.flat().slice(0, Number(limit)) +}, { + maxAge: 60 * 60, + getKey: (event) => { + const query = getQuery(event) + return `mcp-search-content-${query.query}-${query.locale || 'all'}` + }, +}) diff --git a/layer/server/routes/mcp.ts b/layer/server/routes/mcp.ts new file mode 100644 index 000000000..5cabb653f --- /dev/null +++ b/layer/server/routes/mcp.ts @@ -0,0 +1,80 @@ +import { z } from 'zod' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import type { H3Event } from 'h3' + +function createServer(event: H3Event) { + const config = useRuntimeConfig(event).public + + const siteName = config.site?.name || 'Docus Documentation' + const availableLocales = getAvailableLocales(config) + + const server = new McpServer({ + name: siteName, + version: '1.0.0', + }) + + server.tool( + 'list_pages', + 'Lists all available documentation pages with their titles, paths, and descriptions', + { + locale: availableLocales.length > 0 + ? z.enum(availableLocales as [string, ...string[]]).optional() + : z.string().optional(), + }, + async (params) => { + const result = await $fetch('/api/mcp/list-pages', { query: params }) + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } + }, + ) + + server.tool( + 'search', + 'Search documentation pages by query string (searches in title and description)', + { + query: z.string().min(1).describe('Search query'), + locale: availableLocales.length > 0 + ? z.enum(availableLocales as [string, ...string[]]).optional() + : z.string().optional(), + limit: z.number().min(1).max(20).optional().default(10).describe('Maximum number of results'), + }, + async (params) => { + const result = await $fetch('/api/mcp/search-content', { query: params }) + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } + }, + ) + + server.tool( + 'get_page', + 'Retrieves the full markdown content of a specific documentation page by path', + { + path: z.string().describe('The page path (e.g., /en/getting-started/installation)'), + }, + async (params) => { + const result = await $fetch('/api/mcp/get-page', { query: params }) + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } + }, + ) + + return server +} + +export default defineEventHandler(async (event) => { + if (getHeader(event, 'accept')?.includes('text/html')) { + return sendRedirect(event, '/') + } + + const server = createServer(event) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }) + + event.node.res.on('close', () => { + transport.close() + server.close() + }) + + await server.connect(transport) + const body = await readBody(event) + await transport.handleRequest(event.node.req, event.node.res, body) +}) diff --git a/layer/server/utils/mcp.ts b/layer/server/utils/mcp.ts new file mode 100644 index 000000000..7b4012d99 --- /dev/null +++ b/layer/server/utils/mcp.ts @@ -0,0 +1,32 @@ +import type { LocaleObject } from '@nuxtjs/i18n' + +export function getAvailableLocales(config: { i18n?: { locales?: Array }, docus?: { filteredLocales?: LocaleObject[] } }): string[] { + if (config.docus?.filteredLocales) { + return config.docus.filteredLocales.map(locale => locale.code) + } + + return config.i18n?.locales + ? config.i18n.locales.map(locale => typeof locale === 'string' ? locale : locale.code) + : [] +} + +export function getCollectionsToQuery(locale: string | undefined, availableLocales: string[]): string[] { + if (locale && availableLocales.includes(locale)) { + return [`docs_${locale}`] + } + + return availableLocales.length > 0 + ? availableLocales.map(l => `docs_${l}`) + : ['docs'] +} + +export function getCollectionFromPath(path: string, availableLocales: string[]): string { + const pathSegments = path.split('/').filter(Boolean) + const firstSegment = pathSegments[0] + + if (firstSegment && availableLocales.includes(firstSegment)) { + return `docs_${firstSegment}` + } + + return 'docs' +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb140fb84..1fbfceea0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ importers: '@iconify-json/vscode-icons': specifier: ^1.2.32 version: 1.2.32 + '@modelcontextprotocol/sdk': + specifier: ^1.0.4 + version: 1.20.2 '@nuxt/content': specifier: https://pkg.pr.new/@nuxt/content@dd854d5 version: https://pkg.pr.new/@nuxt/content@dd854d5(better-sqlite3@12.4.1)(magicast@0.3.5) @@ -187,6 +190,9 @@ importers: ufo: specifier: ^1.6.1 version: 1.6.1 + zod: + specifier: ^3.24.1 + version: 3.25.76 packages: @@ -948,6 +954,10 @@ packages: peerDependencies: rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + '@modelcontextprotocol/sdk@1.20.2': + resolution: {integrity: sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2716,6 +2726,10 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -2917,6 +2931,10 @@ packages: blob-to-buffer@1.2.9: resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -2965,6 +2983,10 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + c12@3.3.0: resolution: {integrity: sha512-K9ZkuyeJQeqLEyqldbYLG3wjqwpw4BVaAqvmxq3GYKK0b1A/yYQdIcJxkzAOWcNVWhJpRXAPfZFueekiY/L8Dw==} peerDependencies: @@ -2985,6 +3007,14 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -3173,6 +3203,14 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + conventional-changelog-angular@8.0.0: resolution: {integrity: sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA==} engines: {node: '>=18'} @@ -3249,6 +3287,14 @@ packages: cookie-es@2.0.0: resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -3263,6 +3309,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -3520,6 +3570,10 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -3627,9 +3681,21 @@ packages: errx@0.1.0: resolution: {integrity: sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.25.11: resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} engines: {node: '>=18'} @@ -3827,6 +3893,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -3839,6 +3909,16 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -3897,6 +3977,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -3934,6 +4018,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -3982,9 +4070,17 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-port-please@3.2.0: resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -4058,6 +4154,10 @@ packages: resolution: {integrity: sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==} engines: {node: '>=20'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4080,6 +4180,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -4268,6 +4372,10 @@ packages: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + ipx@2.1.1: resolution: {integrity: sha512-XuM9FEGOT+/45mfAWZ5ykwkZ/oE7vWpd1iWjRffMWlwAYIRzb/xD6wZhQ4BzmPMX6Ov5dqK0wUyD0OEN9oWT6g==} hasBin: true @@ -4360,6 +4468,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -4696,6 +4807,10 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -4741,10 +4856,18 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + meow@13.2.0: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4968,6 +5091,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -5114,6 +5241,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + ofetch@1.4.1: resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} @@ -5296,6 +5427,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@6.0.0: resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} engines: {node: '>=18'} @@ -5327,6 +5461,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -5579,6 +5717,10 @@ packages: protocols@2.0.2: resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -5593,6 +5735,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -5609,6 +5755,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -5788,6 +5938,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -5881,6 +6035,22 @@ packages: shiki@3.14.0: resolution: {integrity: sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -6282,6 +6452,10 @@ packages: resolution: {integrity: sha512-wQ531tuWvB6oK+pchHIu5lHe5f5wpSCqB8Kf4dWQRbOYc9HTge7JL0G4Qd44bh6QuJCccIzL3bugb8GI0MwHrg==} engines: {node: '>=20'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + type-level-regexp@0.1.17: resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} @@ -6379,6 +6553,10 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin-auto-import@20.2.0: resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==} engines: {node: '>=14'} @@ -6523,6 +6701,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vaul-vue@0.4.1: resolution: {integrity: sha512-A6jOWOZX5yvyo1qMn7IveoWN91mJI5L3BUKsIwkg6qrTGgHs1Sb1JF/vyLJgnbN1rH4OOOxFbtqL9A46bOyGUQ==} peerDependencies: @@ -7680,6 +7862,23 @@ snapshots: json5: 2.2.3 rollup: 4.52.4 + '@modelcontextprotocol/sdk@1.20.2': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.1 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -9749,6 +9948,11 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -9938,6 +10142,20 @@ snapshots: blob-to-buffer@1.2.9: {} + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -9990,6 +10208,8 @@ snapshots: esbuild: 0.25.11 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + c12@3.3.0(magicast@0.3.5): dependencies: chokidar: 4.0.3 @@ -10026,6 +10246,16 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camelize@1.0.1: {} @@ -10222,6 +10452,12 @@ snapshots: consola@3.4.2: {} + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + conventional-changelog-angular@8.0.0: dependencies: compare-func: 2.0.0 @@ -10306,6 +10542,10 @@ snapshots: cookie-es@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.0.2: {} copy-anything@3.0.5: @@ -10318,6 +10558,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + crc-32@1.2.2: {} crc32-stream@6.0.0: @@ -10545,6 +10790,12 @@ snapshots: dotenv@17.2.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -10638,8 +10889,16 @@ snapshots: errx@0.1.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.25.11: optionalDependencies: '@esbuild/aix-ppc64': 0.25.11 @@ -10905,6 +11164,10 @@ snapshots: eventsource-parser@3.0.6: {} + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -10934,6 +11197,42 @@ snapshots: expand-template@2.0.3: {} + express-rate-limit@7.5.1(express@5.1.0): + dependencies: + express: 5.1.0 + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} extend@3.0.2: {} @@ -10982,6 +11281,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up-simple@1.0.1: {} find-up@5.0.0: @@ -11040,6 +11350,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forwarded@0.2.0: {} + fraction.js@4.3.7: {} framer-motion@12.23.12: @@ -11065,8 +11377,26 @@ snapshots: get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-port-please@3.2.0: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@8.0.1: {} get-stream@9.0.1: @@ -11160,6 +11490,8 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.3.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -11191,6 +11523,8 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -11464,6 +11798,8 @@ snapshots: ip-address@10.0.1: {} + ipaddr.js@1.9.1: {} + ipx@2.1.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.1): dependencies: '@fastify/accept-negotiator': 1.1.0 @@ -11570,6 +11906,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -11875,6 +12213,8 @@ snapshots: marky@1.3.0: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -11996,8 +12336,12 @@ snapshots: mdn-data@2.12.2: {} + media-typer@1.1.0: {} + meow@13.2.0: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -12299,6 +12643,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} netmask@2.0.2: {} @@ -12722,6 +13068,8 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + ofetch@1.4.1: dependencies: destr: 2.0.5 @@ -13017,6 +13365,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + path-type@6.0.0: {} pathe@1.1.2: {} @@ -13035,6 +13385,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.0: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -13267,6 +13619,11 @@ snapshots: protocols@2.0.2: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 @@ -13289,6 +13646,10 @@ snapshots: punycode@2.3.1: {} + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -13301,6 +13662,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -13638,6 +14006,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-applescript@7.1.0: {} run-async@4.0.6: {} @@ -13771,6 +14149,34 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@4.1.0: {} simple-concat@1.0.1: {} @@ -14198,6 +14604,12 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + type-level-regexp@0.1.17: {} typedarray@0.0.6: {} @@ -14320,6 +14732,8 @@ snapshots: universal-user-agent@7.0.3: {} + unpipe@1.0.0: {} + unplugin-auto-import@20.2.0(@nuxt/kit@4.1.3(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3))): dependencies: local-pkg: 1.1.2 @@ -14474,6 +14888,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vary@1.1.2: {} + vaul-vue@0.4.1(reka-ui@2.5.1(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)): dependencies: '@vueuse/core': 10.11.1(vue@3.5.22(typescript@5.9.3)) From e6ce37261814319e121045394eeed61805c196ef Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Thu, 30 Oct 2025 16:53:01 +0000 Subject: [PATCH 2/7] remove search tool --- layer/server/api/mcp/search-content.get.ts | 49 ---------------------- layer/server/routes/mcp.ts | 16 ------- 2 files changed, 65 deletions(-) delete mode 100644 layer/server/api/mcp/search-content.get.ts diff --git a/layer/server/api/mcp/search-content.get.ts b/layer/server/api/mcp/search-content.get.ts deleted file mode 100644 index 8450ecaf8..000000000 --- a/layer/server/api/mcp/search-content.get.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { z } from 'zod' -import { queryCollection } from '@nuxt/content/server' -import type { Collections } from '@nuxt/content' - -const querySchema = z.object({ - query: z.string().describe('Search query'), - locale: z.string().optional().describe('Language code (e.g., "en", "fr")'), - limit: z.number().optional().default(10).describe('Maximum number of results'), -}) - -export default defineCachedEventHandler(async (event) => { - const { query, locale, limit } = await getValidatedQuery(event, querySchema.parse) - - const config = useRuntimeConfig(event).public - const siteUrl = config.site?.url || 'http://localhost:3000' - const availableLocales = getAvailableLocales(config) - const collections = getCollectionsToQuery(locale, availableLocales) - - const allResults = await Promise.all( - collections.map(async (collectionName) => { - try { - const pages = await queryCollection(event, collectionName as keyof Collections) - .where('title', 'LIKE', `%${query}%`) - .select('title', 'path', 'description') - .limit(Number(limit)) - .all() - - return pages.map(page => ({ - title: page.title, - path: page.path, - description: page.description, - locale: collectionName.replace('docs_', ''), - url: `${siteUrl}${page.path}`, - })) - } - catch { - return [] - } - }), - ) - - return allResults.flat().slice(0, Number(limit)) -}, { - maxAge: 60 * 60, - getKey: (event) => { - const query = getQuery(event) - return `mcp-search-content-${query.query}-${query.locale || 'all'}` - }, -}) diff --git a/layer/server/routes/mcp.ts b/layer/server/routes/mcp.ts index 5cabb653f..45235bbd5 100644 --- a/layer/server/routes/mcp.ts +++ b/layer/server/routes/mcp.ts @@ -28,22 +28,6 @@ function createServer(event: H3Event) { }, ) - server.tool( - 'search', - 'Search documentation pages by query string (searches in title and description)', - { - query: z.string().min(1).describe('Search query'), - locale: availableLocales.length > 0 - ? z.enum(availableLocales as [string, ...string[]]).optional() - : z.string().optional(), - limit: z.number().min(1).max(20).optional().default(10).describe('Maximum number of results'), - }, - async (params) => { - const result = await $fetch('/api/mcp/search-content', { query: params }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - }, - ) - server.tool( 'get_page', 'Retrieves the full markdown content of a specific documentation page by path', From aeada3ecd3f8b241e7b65cbd999ccb742e985ce2 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Fri, 31 Oct 2025 13:29:48 +0000 Subject: [PATCH 3/7] temporary ts error ignore --- layer/server/api/mcp/get-page.get.ts | 1 + layer/server/api/mcp/list-pages.get.ts | 1 + layer/server/routes/mcp.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/layer/server/api/mcp/get-page.get.ts b/layer/server/api/mcp/get-page.get.ts index 461c7012b..035e7c694 100644 --- a/layer/server/api/mcp/get-page.get.ts +++ b/layer/server/api/mcp/get-page.get.ts @@ -11,6 +11,7 @@ export default defineCachedEventHandler(async (event) => { const { path } = await getValidatedQuery(event, querySchema.parse) const config = useRuntimeConfig(event).public + // @ts-expect-error - FIXME: This should be typed const siteUrl = config.site?.url || 'http://localhost:3000' const availableLocales = getAvailableLocales(config) const collectionName = config.i18n?.locales diff --git a/layer/server/api/mcp/list-pages.get.ts b/layer/server/api/mcp/list-pages.get.ts index 901440843..a986d278c 100644 --- a/layer/server/api/mcp/list-pages.get.ts +++ b/layer/server/api/mcp/list-pages.get.ts @@ -10,6 +10,7 @@ export default defineCachedEventHandler(async (event) => { const { locale } = await getValidatedQuery(event, querySchema.parse) const config = useRuntimeConfig(event).public + // @ts-expect-error - FIXME: This should be typed const siteUrl = config.site?.url || 'http://localhost:3000' const availableLocales = getAvailableLocales(config) const collections = getCollectionsToQuery(locale, availableLocales) diff --git a/layer/server/routes/mcp.ts b/layer/server/routes/mcp.ts index 45235bbd5..3a4797c77 100644 --- a/layer/server/routes/mcp.ts +++ b/layer/server/routes/mcp.ts @@ -6,6 +6,7 @@ import type { H3Event } from 'h3' function createServer(event: H3Event) { const config = useRuntimeConfig(event).public + // @ts-expect-error - FIXME: This should be typed const siteName = config.site?.name || 'Docus Documentation' const availableLocales = getAvailableLocales(config) From 134ccf2536c3db0ad98fd7bde8b85fbb676cba22 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Tue, 4 Nov 2025 13:16:01 +0000 Subject: [PATCH 4/7] add mcp via a module --- docs/nuxt.config.ts | 3 + layer/modules/mcp/index.ts | 82 +++++++++++++++++++ .../mcp/runtime/server/api}/get-page.get.ts | 14 ++-- .../mcp/runtime/server/api}/list-pages.get.ts | 4 +- .../mcp/runtime/server/handler.ts} | 24 +++--- .../mcp/runtime/server/utils.ts} | 7 +- layer/nuxt.config.ts | 1 + 7 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 layer/modules/mcp/index.ts rename layer/{server/api/mcp => modules/mcp/runtime/server/api}/get-page.get.ts (68%) rename layer/{server/api/mcp => modules/mcp/runtime/server/api}/list-pages.get.ts (88%) rename layer/{server/routes/mcp.ts => modules/mcp/runtime/server/handler.ts} (73%) rename layer/{server/utils/mcp.ts => modules/mcp/runtime/server/utils.ts} (80%) diff --git a/docs/nuxt.config.ts b/docs/nuxt.config.ts index d9aa0ea00..de8c33845 100644 --- a/docs/nuxt.config.ts +++ b/docs/nuxt.config.ts @@ -25,6 +25,9 @@ export default defineNuxtConfig({ description: 'Write beautiful docs with Markdown.', }, }, + mcp: { + name: 'Docus documentation', + }, studio: { route: '/admin', repository: { diff --git a/layer/modules/mcp/index.ts b/layer/modules/mcp/index.ts new file mode 100644 index 000000000..0dc5d71ff --- /dev/null +++ b/layer/modules/mcp/index.ts @@ -0,0 +1,82 @@ +import { defineNuxtModule, addServerHandler, useLogger, createResolver } from '@nuxt/kit' +import { defu } from 'defu' + +const { resolve } = createResolver(import.meta.url) + +export interface ModuleOptions { + /** + * Enable or disable the MCP server + * @default true + */ + enabled?: boolean + /** + * The route path for the MCP server endpoint + * @default '/mcp' + */ + route?: string + /** + * URL to redirect to when a browser accesses the MCP endpoint + * @default '/' + */ + redirectTo?: string + /** + * The name of the MCP server + * @default Site name from site config or 'Docus Documentation' + */ + name?: string + /** + * The version of the MCP server + * @default '1.0.0' + */ + version?: string +} + +export default defineNuxtModule({ + meta: { + name: 'docus-mcp', + configKey: 'mcp', + }, + defaults: { + enabled: true, + route: '/mcp', + redirectTo: '/', + name: '', + version: '1.0.0', + }, + async setup(options, nuxt) { + const logger = useLogger('docus:mcp') + + nuxt.options.runtimeConfig.mcp = defu( + nuxt.options.runtimeConfig.mcp, + { + enabled: options.enabled, + route: options.route, + redirectTo: options.redirectTo, + name: options.name, + version: options.version, + }, + ) + + if (!options.enabled) { + logger.info('MCP server is disabled') + return + } + + logger.info(`MCP server enabled at route: ${options.route}`) + + addServerHandler({ + route: '/.docus-mcp/list-pages', + handler: resolve('./runtime/server/api/list-pages.get.ts'), + }) + + addServerHandler({ + route: '/.docus-mcp/get-page', + handler: resolve('./runtime/server/api/get-page.get.ts'), + }) + + addServerHandler({ + route: options.route, + handler: resolve('./runtime/server/handler.ts'), + }) + }, +}) diff --git a/layer/server/api/mcp/get-page.get.ts b/layer/modules/mcp/runtime/server/api/get-page.get.ts similarity index 68% rename from layer/server/api/mcp/get-page.get.ts rename to layer/modules/mcp/runtime/server/api/get-page.get.ts index 035e7c694..d18e13128 100644 --- a/layer/server/api/mcp/get-page.get.ts +++ b/layer/modules/mcp/runtime/server/api/get-page.get.ts @@ -2,6 +2,7 @@ import { z } from 'zod' import { queryCollection } from '@nuxt/content/server' import type { Collections } from '@nuxt/content' import { stringify } from 'minimark/stringify' +import { getAvailableLocales, getCollectionFromPath } from '../utils' const querySchema = z.object({ path: z.string().describe('The page path (e.g., /en/getting-started/installation)'), @@ -11,8 +12,7 @@ export default defineCachedEventHandler(async (event) => { const { path } = await getValidatedQuery(event, querySchema.parse) const config = useRuntimeConfig(event).public - // @ts-expect-error - FIXME: This should be typed - const siteUrl = config.site?.url || 'http://localhost:3000' + const siteUrl = import.meta.dev ? 'http://localhost:3000' : getRequestURL(event).origin const availableLocales = getAvailableLocales(config) const collectionName = config.i18n?.locales ? getCollectionFromPath(path, availableLocales) @@ -30,12 +30,14 @@ export default defineCachedEventHandler(async (event) => { }) } - if (page.body.value[0]?.[0] !== 'h1') { - page.body.value.unshift(['blockquote', {}, page.description]) - page.body.value.unshift(['h1', {}, page.title]) + const body = page.body as { value: unknown[] } + + if (Array.isArray(body.value) && body.value[0] && Array.isArray(body.value[0]) && body.value[0][0] !== 'h1') { + body.value.unshift(['blockquote', {}, page.description]) + body.value.unshift(['h1', {}, page.title]) } - const content = stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' }) + const content = stringify({ ...body, type: 'minimark' } as Parameters[0], { format: 'markdown/html' }) return { title: page.title, diff --git a/layer/server/api/mcp/list-pages.get.ts b/layer/modules/mcp/runtime/server/api/list-pages.get.ts similarity index 88% rename from layer/server/api/mcp/list-pages.get.ts rename to layer/modules/mcp/runtime/server/api/list-pages.get.ts index a986d278c..ca14f39fa 100644 --- a/layer/server/api/mcp/list-pages.get.ts +++ b/layer/modules/mcp/runtime/server/api/list-pages.get.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { queryCollection } from '@nuxt/content/server' import type { Collections } from '@nuxt/content' +import { getCollectionsToQuery, getAvailableLocales } from '../utils' const querySchema = z.object({ locale: z.string().optional().describe('Language code (e.g., "en", "fr")'), @@ -10,8 +11,7 @@ export default defineCachedEventHandler(async (event) => { const { locale } = await getValidatedQuery(event, querySchema.parse) const config = useRuntimeConfig(event).public - // @ts-expect-error - FIXME: This should be typed - const siteUrl = config.site?.url || 'http://localhost:3000' + const siteUrl = import.meta.dev ? 'http://localhost:3000' : getRequestURL(event).origin const availableLocales = getAvailableLocales(config) const collections = getCollectionsToQuery(locale, availableLocales) diff --git a/layer/server/routes/mcp.ts b/layer/modules/mcp/runtime/server/handler.ts similarity index 73% rename from layer/server/routes/mcp.ts rename to layer/modules/mcp/runtime/server/handler.ts index 3a4797c77..be3f9cfce 100644 --- a/layer/server/routes/mcp.ts +++ b/layer/modules/mcp/runtime/server/handler.ts @@ -1,18 +1,16 @@ import { z } from 'zod' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { getAvailableLocales } from './utils' import type { H3Event } from 'h3' -function createServer(event: H3Event) { - const config = useRuntimeConfig(event).public - - // @ts-expect-error - FIXME: This should be typed - const siteName = config.site?.name || 'Docus Documentation' - const availableLocales = getAvailableLocales(config) +function createMcpServer(event: H3Event) { + const { name, version } = useRuntimeConfig(event).mcp + const availableLocales = getAvailableLocales(useRuntimeConfig(event).public) const server = new McpServer({ - name: siteName, - version: '1.0.0', + name, + version, }) server.tool( @@ -24,7 +22,7 @@ function createServer(event: H3Event) { : z.string().optional(), }, async (params) => { - const result = await $fetch('/api/mcp/list-pages', { query: params }) + const result = await $fetch('/.docus-mcp/list-pages', { query: params }) return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } }, ) @@ -36,7 +34,7 @@ function createServer(event: H3Event) { path: z.string().describe('The page path (e.g., /en/getting-started/installation)'), }, async (params) => { - const result = await $fetch('/api/mcp/get-page', { query: params }) + const result = await $fetch('/.docus-mcp/get-page', { query: params }) return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } }, ) @@ -45,11 +43,13 @@ function createServer(event: H3Event) { } export default defineEventHandler(async (event) => { + const { redirectTo } = useRuntimeConfig(event).mcp + if (getHeader(event, 'accept')?.includes('text/html')) { - return sendRedirect(event, '/') + return sendRedirect(event, redirectTo) } - const server = createServer(event) + const server = createMcpServer(event) const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }) diff --git a/layer/server/utils/mcp.ts b/layer/modules/mcp/runtime/server/utils.ts similarity index 80% rename from layer/server/utils/mcp.ts rename to layer/modules/mcp/runtime/server/utils.ts index 7b4012d99..2f1569bf7 100644 --- a/layer/server/utils/mcp.ts +++ b/layer/modules/mcp/runtime/server/utils.ts @@ -1,6 +1,11 @@ import type { LocaleObject } from '@nuxtjs/i18n' -export function getAvailableLocales(config: { i18n?: { locales?: Array }, docus?: { filteredLocales?: LocaleObject[] } }): string[] { +type ConfigWithLocales = { + i18n?: { locales?: Array } + docus?: { filteredLocales?: LocaleObject[] } +} + +export function getAvailableLocales(config: ConfigWithLocales): string[] { if (config.docus?.filteredLocales) { return config.docus.filteredLocales.map(locale => locale.code) } diff --git a/layer/nuxt.config.ts b/layer/nuxt.config.ts index 4e5b1c03b..dfe134613 100644 --- a/layer/nuxt.config.ts +++ b/layer/nuxt.config.ts @@ -7,6 +7,7 @@ export default defineNuxtConfig({ resolve('./modules/config'), resolve('./modules/routing'), resolve('./modules/css'), + resolve('./modules/mcp'), '@nuxt/ui', '@nuxt/content', '@nuxt/image', From 085ca75544c217355c400d0fca0f7858597a5a53 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Tue, 4 Nov 2025 15:19:48 +0000 Subject: [PATCH 5/7] add zod to docs --- docs/package.json | 3 ++- layer/modules/mcp/runtime/server/handler.ts | 5 +++-- pnpm-lock.yaml | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/package.json b/docs/package.json index a32d459f3..ca67fe968 100644 --- a/docs/package.json +++ b/docs/package.json @@ -16,7 +16,8 @@ "docus": "workspace:*", "nuxt-studio": "https://pkg.pr.new/nuxt-content/studio/nuxt-studio@dc78b20", "tailwindcss": "^4.1.14", - "unist-util-visit": "^5.0.0" + "unist-util-visit": "^5.0.0", + "zod": "^3.24.1" }, "devDependencies": { "nuxt": "4.1.3" diff --git a/layer/modules/mcp/runtime/server/handler.ts b/layer/modules/mcp/runtime/server/handler.ts index be3f9cfce..838381b29 100644 --- a/layer/modules/mcp/runtime/server/handler.ts +++ b/layer/modules/mcp/runtime/server/handler.ts @@ -5,8 +5,9 @@ import { getAvailableLocales } from './utils' import type { H3Event } from 'h3' function createMcpServer(event: H3Event) { - const { name, version } = useRuntimeConfig(event).mcp - const availableLocales = getAvailableLocales(useRuntimeConfig(event).public) + const runtimeConfig = useRuntimeConfig(event) + const { name, version } = runtimeConfig.mcp + const availableLocales = getAvailableLocales(runtimeConfig.public) const server = new McpServer({ name, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef7e09c52..0898165fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: unist-util-visit: specifier: ^5.0.0 version: 5.0.0 + zod: + specifier: ^3.24.1 + version: 3.25.76 devDependencies: nuxt: specifier: 4.1.3 From 392bda25414260a0ba6641aaa2b3365d1ed91254 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Tue, 4 Nov 2025 15:31:28 +0000 Subject: [PATCH 6/7] use external mcp-module --- docs/server/mcp/tools/example.ts | 20 +++++ docs/tsconfig.json | 18 +++- layer/modules/mcp/index.ts | 82 ------------------ layer/modules/mcp/runtime/server/handler.ts | 66 --------------- layer/nuxt.config.ts | 2 +- layer/package.json | 2 +- .../api/.docus-mcp}/get-page.get.ts | 2 +- .../api/.docus-mcp}/list-pages.get.ts | 2 +- layer/server/mcp/tools/get-page.ts | 15 ++++ layer/server/mcp/tools/list-pages.ts | 15 ++++ layer/server/routes/raw/[...slug].md.get.ts | 2 +- .../utils.ts => server/utils/content.ts} | 0 pnpm-lock.yaml | 84 ++++++++++++++++--- 13 files changed, 145 insertions(+), 165 deletions(-) create mode 100644 docs/server/mcp/tools/example.ts delete mode 100644 layer/modules/mcp/index.ts delete mode 100644 layer/modules/mcp/runtime/server/handler.ts rename layer/{modules/mcp/runtime/server/api => server/api/.docus-mcp}/get-page.get.ts (95%) rename layer/{modules/mcp/runtime/server/api => server/api/.docus-mcp}/list-pages.get.ts (94%) create mode 100644 layer/server/mcp/tools/get-page.ts create mode 100644 layer/server/mcp/tools/list-pages.ts rename layer/{modules/mcp/runtime/server/utils.ts => server/utils/content.ts} (100%) diff --git a/docs/server/mcp/tools/example.ts b/docs/server/mcp/tools/example.ts new file mode 100644 index 000000000..383d88db7 --- /dev/null +++ b/docs/server/mcp/tools/example.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' + +// delete this file before merging +export default defineMcpTool({ + name: 'example_tool', + description: 'An example custom tool to test auto-detection', + paramsSchema: { + message: z.string().describe('A message to echo back'), + }, + handler: async (params) => { + return { + content: [ + { + type: 'text', + text: `Echo: ${params.message}`, + }, + ], + } + }, +}) diff --git a/docs/tsconfig.json b/docs/tsconfig.json index eb97e3f0e..6ae5970c7 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -1,3 +1,17 @@ { - "extends": "./.nuxt/tsconfig.json" -} \ No newline at end of file + "files": [], + "references": [ + { + "path": "./.nuxt/tsconfig.app.json" + }, + { + "path": "./.nuxt/tsconfig.server.json" + }, + { + "path": "./.nuxt/tsconfig.shared.json" + }, + { + "path": "./.nuxt/tsconfig.node.json" + } + ] +} diff --git a/layer/modules/mcp/index.ts b/layer/modules/mcp/index.ts deleted file mode 100644 index 0dc5d71ff..000000000 --- a/layer/modules/mcp/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { defineNuxtModule, addServerHandler, useLogger, createResolver } from '@nuxt/kit' -import { defu } from 'defu' - -const { resolve } = createResolver(import.meta.url) - -export interface ModuleOptions { - /** - * Enable or disable the MCP server - * @default true - */ - enabled?: boolean - /** - * The route path for the MCP server endpoint - * @default '/mcp' - */ - route?: string - /** - * URL to redirect to when a browser accesses the MCP endpoint - * @default '/' - */ - redirectTo?: string - /** - * The name of the MCP server - * @default Site name from site config or 'Docus Documentation' - */ - name?: string - /** - * The version of the MCP server - * @default '1.0.0' - */ - version?: string -} - -export default defineNuxtModule({ - meta: { - name: 'docus-mcp', - configKey: 'mcp', - }, - defaults: { - enabled: true, - route: '/mcp', - redirectTo: '/', - name: '', - version: '1.0.0', - }, - async setup(options, nuxt) { - const logger = useLogger('docus:mcp') - - nuxt.options.runtimeConfig.mcp = defu( - nuxt.options.runtimeConfig.mcp, - { - enabled: options.enabled, - route: options.route, - redirectTo: options.redirectTo, - name: options.name, - version: options.version, - }, - ) - - if (!options.enabled) { - logger.info('MCP server is disabled') - return - } - - logger.info(`MCP server enabled at route: ${options.route}`) - - addServerHandler({ - route: '/.docus-mcp/list-pages', - handler: resolve('./runtime/server/api/list-pages.get.ts'), - }) - - addServerHandler({ - route: '/.docus-mcp/get-page', - handler: resolve('./runtime/server/api/get-page.get.ts'), - }) - - addServerHandler({ - route: options.route, - handler: resolve('./runtime/server/handler.ts'), - }) - }, -}) diff --git a/layer/modules/mcp/runtime/server/handler.ts b/layer/modules/mcp/runtime/server/handler.ts deleted file mode 100644 index 838381b29..000000000 --- a/layer/modules/mcp/runtime/server/handler.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { z } from 'zod' -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' -import { getAvailableLocales } from './utils' -import type { H3Event } from 'h3' - -function createMcpServer(event: H3Event) { - const runtimeConfig = useRuntimeConfig(event) - const { name, version } = runtimeConfig.mcp - const availableLocales = getAvailableLocales(runtimeConfig.public) - - const server = new McpServer({ - name, - version, - }) - - server.tool( - 'list_pages', - 'Lists all available documentation pages with their titles, paths, and descriptions', - { - locale: availableLocales.length > 0 - ? z.enum(availableLocales as [string, ...string[]]).optional() - : z.string().optional(), - }, - async (params) => { - const result = await $fetch('/.docus-mcp/list-pages', { query: params }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - }, - ) - - server.tool( - 'get_page', - 'Retrieves the full markdown content of a specific documentation page by path', - { - path: z.string().describe('The page path (e.g., /en/getting-started/installation)'), - }, - async (params) => { - const result = await $fetch('/.docus-mcp/get-page', { query: params }) - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } - }, - ) - - return server -} - -export default defineEventHandler(async (event) => { - const { redirectTo } = useRuntimeConfig(event).mcp - - if (getHeader(event, 'accept')?.includes('text/html')) { - return sendRedirect(event, redirectTo) - } - - const server = createMcpServer(event) - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - }) - - event.node.res.on('close', () => { - transport.close() - server.close() - }) - - await server.connect(transport) - const body = await readBody(event) - await transport.handleRequest(event.node.req, event.node.res, body) -}) diff --git a/layer/nuxt.config.ts b/layer/nuxt.config.ts index dfe134613..ded809a19 100644 --- a/layer/nuxt.config.ts +++ b/layer/nuxt.config.ts @@ -7,11 +7,11 @@ export default defineNuxtConfig({ resolve('./modules/config'), resolve('./modules/routing'), resolve('./modules/css'), - resolve('./modules/mcp'), '@nuxt/ui', '@nuxt/content', '@nuxt/image', '@nuxtjs/robots', + '@hrcd/mcp', 'nuxt-og-image', 'nuxt-llms', () => { diff --git a/layer/package.json b/layer/package.json index 6a350e49a..a528d5d98 100644 --- a/layer/package.json +++ b/layer/package.json @@ -22,10 +22,10 @@ "README.md" ], "dependencies": { + "@hrcd/mcp": "^0.0.4", "@iconify-json/lucide": "^1.2.69", "@iconify-json/simple-icons": "^1.2.54", "@iconify-json/vscode-icons": "^1.2.32", - "@modelcontextprotocol/sdk": "^1.0.4", "@nuxt/content": "https://pkg.pr.new/@nuxt/content@dd854d5", "@nuxt/image": "^1.11.0", "@nuxt/kit": "^4.1.3", diff --git a/layer/modules/mcp/runtime/server/api/get-page.get.ts b/layer/server/api/.docus-mcp/get-page.get.ts similarity index 95% rename from layer/modules/mcp/runtime/server/api/get-page.get.ts rename to layer/server/api/.docus-mcp/get-page.get.ts index d18e13128..61fefd8da 100644 --- a/layer/modules/mcp/runtime/server/api/get-page.get.ts +++ b/layer/server/api/.docus-mcp/get-page.get.ts @@ -2,7 +2,7 @@ import { z } from 'zod' import { queryCollection } from '@nuxt/content/server' import type { Collections } from '@nuxt/content' import { stringify } from 'minimark/stringify' -import { getAvailableLocales, getCollectionFromPath } from '../utils' +import { getAvailableLocales, getCollectionFromPath } from '../../utils/content' const querySchema = z.object({ path: z.string().describe('The page path (e.g., /en/getting-started/installation)'), diff --git a/layer/modules/mcp/runtime/server/api/list-pages.get.ts b/layer/server/api/.docus-mcp/list-pages.get.ts similarity index 94% rename from layer/modules/mcp/runtime/server/api/list-pages.get.ts rename to layer/server/api/.docus-mcp/list-pages.get.ts index ca14f39fa..74858ebe3 100644 --- a/layer/modules/mcp/runtime/server/api/list-pages.get.ts +++ b/layer/server/api/.docus-mcp/list-pages.get.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { queryCollection } from '@nuxt/content/server' import type { Collections } from '@nuxt/content' -import { getCollectionsToQuery, getAvailableLocales } from '../utils' +import { getCollectionsToQuery, getAvailableLocales } from '../../utils/content' const querySchema = z.object({ locale: z.string().optional().describe('Language code (e.g., "en", "fr")'), diff --git a/layer/server/mcp/tools/get-page.ts b/layer/server/mcp/tools/get-page.ts new file mode 100644 index 000000000..ec205b6eb --- /dev/null +++ b/layer/server/mcp/tools/get-page.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' + +export default defineMcpTool({ + name: 'get_page', + description: 'Retrieves the full markdown content of a specific documentation page by path', + inputSchema: { + path: z.string().describe('The page path (e.g., /en/getting-started/installation)'), + }, + handler: async (params) => { + const result = await $fetch('/api/.docus-mcp/get-page', { query: params }) + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + } + }, +}) diff --git a/layer/server/mcp/tools/list-pages.ts b/layer/server/mcp/tools/list-pages.ts new file mode 100644 index 000000000..4826686a6 --- /dev/null +++ b/layer/server/mcp/tools/list-pages.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' + +export default defineMcpTool({ + name: 'list_pages', + description: 'Lists all available documentation pages with their titles, paths, and descriptions', + inputSchema: { + locale: z.string().optional().describe('The locale to filter pages by'), + }, + handler: async (params) => { + const result = await $fetch('/api/.docus-mcp/list-pages', { query: params }) + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + } + }, +}) diff --git a/layer/server/routes/raw/[...slug].md.get.ts b/layer/server/routes/raw/[...slug].md.get.ts index 15eaab411..7866f79c9 100644 --- a/layer/server/routes/raw/[...slug].md.get.ts +++ b/layer/server/routes/raw/[...slug].md.get.ts @@ -1,6 +1,6 @@ import { withLeadingSlash } from 'ufo' import { stringify } from 'minimark/stringify' -import { queryCollection } from '@nuxt/content/nitro' +import { queryCollection } from '@nuxt/content/server' import type { Collections } from '@nuxt/content' export default eventHandler(async (event) => { diff --git a/layer/modules/mcp/runtime/server/utils.ts b/layer/server/utils/content.ts similarity index 100% rename from layer/modules/mcp/runtime/server/utils.ts rename to layer/server/utils/content.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0898165fa..14e279bd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,9 @@ importers: layer: dependencies: + '@hrcd/mcp': + specifier: ^0.0.4 + version: 0.0.4(magicast@0.3.5)(zod@3.25.76) '@iconify-json/lucide': specifier: ^1.2.69 version: 1.2.69 @@ -127,9 +130,6 @@ importers: '@iconify-json/vscode-icons': specifier: ^1.2.32 version: 1.2.32 - '@modelcontextprotocol/sdk': - specifier: ^1.0.4 - version: 1.20.2 '@nuxt/content': specifier: https://pkg.pr.new/@nuxt/content@dd854d5 version: https://pkg.pr.new/@nuxt/content@dd854d5(better-sqlite3@12.4.1)(magicast@0.3.5) @@ -644,6 +644,11 @@ packages: '@floating-ui/vue@1.1.9': resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==} + '@hrcd/mcp@0.0.4': + resolution: {integrity: sha512-ws8dy76Kki6iH98JmZ2pbYSP6VBX4eFsFE4p3P9xijH4j5tQPid3Em7p9JfDnfoLLBOU6bkH3b0UxLL0YeeC+g==} + peerDependencies: + zod: ^3.24.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -957,9 +962,14 @@ packages: peerDependencies: rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 - '@modelcontextprotocol/sdk@1.20.2': - resolution: {integrity: sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==} + '@modelcontextprotocol/sdk@1.21.1': + resolution: {integrity: sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ==} engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2768,9 +2778,20 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alien-signals@3.0.0: resolution: {integrity: sha512-JHoRJf18Y6HN4/KZALr3iU+0vW9LKG+8FMThQlbn4+gv8utsLIkwpomjElGPccGeNwh0FI2HN6BLnyFLo6OyLQ==} @@ -3957,6 +3978,9 @@ packages: fast-npm-meta@0.4.7: resolution: {integrity: sha512-aZU3i3eRcSb2NCq8i6N6IlyiTyF6vqAqzBGl2NBF6ngNx/GIqfYbkLDIKZ4z4P0o/RmtsFnVqHwdrSm13o4tnQ==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -4594,6 +4618,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -5056,6 +5083,7 @@ packages: motion-v@1.7.2: resolution: {integrity: sha512-h2qfae2LUMLw5KIjQF5cT+r0MrLwP4AFDMOisyp25x/oDI3PHgjLHJrhHx77q8iBNegk4llt5p6deC12EJ5fvQ==} + deprecated: deprecate peerDependencies: '@vueuse/core': '>=10.0.0' vue: '>=3.0.0' @@ -5896,6 +5924,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -7539,6 +7571,20 @@ snapshots: - '@vue/composition-api' - vue + '@hrcd/mcp@0.0.4(magicast@0.3.5)(zod@3.25.76)': + dependencies: + '@modelcontextprotocol/sdk': 1.21.1 + '@nuxt/kit': 4.2.0(magicast@0.3.5) + chokidar: 4.0.3 + defu: 6.1.4 + pathe: 2.0.3 + tinyglobby: 0.2.15 + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - magicast + - supports-color + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -7872,9 +7918,10 @@ snapshots: json5: 2.2.3 rollup: 4.52.4 - '@modelcontextprotocol/sdk@1.20.2': + '@modelcontextprotocol/sdk@1.21.1': dependencies: - ajv: 6.12.6 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 @@ -8206,7 +8253,7 @@ snapshots: '@iconify/utils': 3.0.2 '@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.3)) '@nuxt/devtools-kit': 2.6.5(magicast@0.3.5)(vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@nuxt/kit': 4.1.3(magicast@0.3.5) + '@nuxt/kit': 4.2.0(magicast@0.3.5) consola: 3.4.2 local-pkg: 1.1.2 mlly: 1.8.0 @@ -10039,6 +10086,10 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.76 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -10046,6 +10097,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + alien-signals@3.0.0: {} ansi-regex@5.0.1: {} @@ -11321,6 +11379,8 @@ snapshots: fast-npm-meta@0.4.7: {} + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -12067,6 +12127,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -12943,7 +13005,7 @@ snapshots: nuxt-site-config-kit@3.2.9(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)): dependencies: - '@nuxt/kit': 4.1.3(magicast@0.3.5) + '@nuxt/kit': 4.2.0(magicast@0.3.5) pkg-types: 2.3.0 site-config-stack: 3.2.9(vue@3.5.22(typescript@5.9.3)) std-env: 3.10.0 @@ -12954,7 +13016,7 @@ snapshots: nuxt-site-config@3.2.9(h3@1.15.4)(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)): dependencies: - '@nuxt/kit': 4.1.3(magicast@0.3.5) + '@nuxt/kit': 4.2.0(magicast@0.3.5) h3: 1.15.4 nuxt-site-config-kit: 3.2.9(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)) pathe: 2.0.3 @@ -14008,6 +14070,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} From 0f563a0c5803b1e4b611cdb4bc48197b805c5568 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Wed, 12 Nov 2025 13:29:00 +0000 Subject: [PATCH 7/7] refactor(get-page): use `/raw` --- layer/server/api/.docus-mcp/get-page.get.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/layer/server/api/.docus-mcp/get-page.get.ts b/layer/server/api/.docus-mcp/get-page.get.ts index 61fefd8da..6558afb9a 100644 --- a/layer/server/api/.docus-mcp/get-page.get.ts +++ b/layer/server/api/.docus-mcp/get-page.get.ts @@ -1,7 +1,6 @@ import { z } from 'zod' import { queryCollection } from '@nuxt/content/server' import type { Collections } from '@nuxt/content' -import { stringify } from 'minimark/stringify' import { getAvailableLocales, getCollectionFromPath } from '../../utils/content' const querySchema = z.object({ @@ -20,7 +19,7 @@ export default defineCachedEventHandler(async (event) => { const page = await queryCollection(event, collectionName as keyof Collections) .where('path', '=', path) - .select('title', 'path', 'description', 'body') + .select('title', 'path', 'description') .first() if (!page) { @@ -30,14 +29,14 @@ export default defineCachedEventHandler(async (event) => { }) } - const body = page.body as { value: unknown[] } - - if (Array.isArray(body.value) && body.value[0] && Array.isArray(body.value[0]) && body.value[0][0] !== 'h1') { - body.value.unshift(['blockquote', {}, page.description]) - body.value.unshift(['h1', {}, page.title]) - } - - const content = stringify({ ...body, type: 'minimark' } as Parameters[0], { format: 'markdown/html' }) + const content = await $fetch(`/raw${path}.md`, { + baseURL: siteUrl, + }).catch(() => { + throw createError({ + statusCode: 404, + statusMessage: 'Raw content not found', + }) + }) return { title: page.title,