diff --git a/apps/registry/nitro.config.ts b/apps/registry/nitro.config.ts index 65ac893..acc78dc 100644 --- a/apps/registry/nitro.config.ts +++ b/apps/registry/nitro.config.ts @@ -3,9 +3,9 @@ import { buildHooks } from './server/hooks' // https://nitro.build/config export default defineNitroConfig({ - compatibilityDate: '2024-09-19', + compatibilityDate: 'latest', srcDir: 'server', - preset: 'cloudflare-module', + preset: 'cloudflare', hooks: buildHooks, serverAssets: [ { @@ -13,4 +13,14 @@ export default defineNitroConfig({ dir: './assets/registry', }, ], + cloudflare: { + nodeCompat: true, + deployConfig: true, + }, + unenv: { + alias: { + // https://github.com/nitrojs/nitro/issues/3170 + 'safer-buffer': 'node:buffer', + }, + }, }) diff --git a/apps/registry/package.json b/apps/registry/package.json index c19700c..bea5679 100644 --- a/apps/registry/package.json +++ b/apps/registry/package.json @@ -8,7 +8,6 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.22.0", - "mcp-handler": "^1.0.3", "shadcn-vue": "^2.3.2", "zod": "~3.25.76" }, diff --git a/apps/registry/server/routes/mcp.ts b/apps/registry/server/routes/mcp.ts index 90d5f51..a678ebf 100644 --- a/apps/registry/server/routes/mcp.ts +++ b/apps/registry/server/routes/mcp.ts @@ -1,19 +1,30 @@ +import type { H3Event } from 'h3' import type { Registry, RegistryItem } from 'shadcn-vue/schema' -import type { ZodRawShape } from 'zod' -import { fromWebHandler } from 'h3' -import { createMcpHandler } from 'mcp-handler' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { defineEventHandler, getHeader, readBody, sendRedirect } from 'h3' import { useStorage } from 'nitropack/runtime' import { z } from 'zod' const REGISTRY_STORAGE_BASE = 'assets:registry' + const REGISTRY_INDEX_FILE = 'index.json' -// Parameter Schemas -const componentParamsShape = { - component: z.string().min(1, 'component is required'), -} satisfies ZodRawShape +const SERVER_INFO = { + name: 'ai-elements-vue', + version: '1.0.0', +} + +const DOCS_REDIRECT = 'https://www.ai-elements-vue.com/overview/mcp-server' + +const getComponentInputSchema = z.object({ + component: z + .string() + .min(1, 'component is required') + .describe('Component name (e.g. "context")'), +}) -const componentParamsSchema = z.object(componentParamsShape) +type GetComponentInput = z.infer // Data Access Layer function getRegistryStorage() { @@ -73,16 +84,7 @@ async function handleListComponents() { } } -async function handleGetComponent(args: Record) { - const parsedArgs = componentParamsSchema.safeParse(args) - if (!parsedArgs.success) { - return { - content: [{ type: 'text' as const, text: `Invalid input: ${parsedArgs.error.message}` }], - isError: true, - } - } - - const { component } = parsedArgs.data +async function handleGetComponent(component: string) { const registryItem = await loadRegistryItem(component) if (!registryItem) { @@ -97,28 +99,67 @@ async function handleGetComponent(args: Record) { } } -// Main Handler -const handler = createMcpHandler( - (server) => { - server.registerTool( - 'get_ai_elements_components', - { - title: 'List AI Elements components', - description: 'Provides a list of all AI Elements components.', - }, - handleListComponents, - ) - - server.registerTool( - 'get_ai_elements_component', - { - title: 'Get AI Elements component', - description: 'Provides information about an AI Elements component.', - inputSchema: componentParamsShape, - }, - handleGetComponent, - ) - }, -) - -export default fromWebHandler(handler) +function createMcpServer() { + const server = new McpServer(SERVER_INFO) + + server.registerTool( + 'get_ai_elements_components', + { + title: 'List AI Elements components', + description: 'Provides a list of all AI Elements components.', + }, + async () => handleListComponents(), + ) + + server.registerTool( + 'get_ai_elements_component', + { + title: 'Get AI Elements component', + description: 'Provides information about an AI Elements component.', + inputSchema: getComponentInputSchema, + }, + async ({ component }: GetComponentInput) => handleGetComponent(component), + ) + + return server +} + +async function readOptionalBody(event: H3Event) { + const method = event.node.req.method + if (!method || method === 'GET' || method === 'HEAD') { + return undefined + } + + try { + return await readBody(event) + } + catch (error) { + console.warn('Failed to parse MCP request body, falling back to undefined', error) + return undefined + } +} + +export default defineEventHandler(async (event) => { + if (event.node.req.method === 'GET') { + const accept = getHeader(event, 'accept') ?? '' + if (accept.includes('text/html')) { + return sendRedirect(event, DOCS_REDIRECT) + } + } + + const server = createMcpServer() + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }) + + event.node.res.once('close', () => { + server.close().catch(error => console.error('Failed to close MCP server', error)) + if (typeof transport.close === 'function') { + transport.close().catch(error => console.error('Failed to close MCP transport', error)) + } + }) + + const body = await readOptionalBody(event) + await server.connect(transport) + await transport.handleRequest(event.node.req, event.node.res, body) +}) diff --git a/apps/registry/wrangler.toml b/apps/registry/wrangler.toml index 032c918..d1876f4 100644 --- a/apps/registry/wrangler.toml +++ b/apps/registry/wrangler.toml @@ -1,5 +1,4 @@ name = "ai-elements-vue-registry" main = ".output/server/index.mjs" -compatibility_date = "2025-09-19" -compatibility_flags = [ "nodejs_compat" ] +compatibility_date = "2025-11-26" routes = [ "registry.ai-elements-vue.com/*" ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47a27ed..ec2245a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.22.0 version: 1.22.0 - mcp-handler: - specifier: ^1.0.3 - version: 1.0.3(@modelcontextprotocol/sdk@1.22.0) shadcn-vue: specifier: ^2.3.2 version: 2.3.3(babel-plugin-macros@3.1.0)(eslint@9.37.0(jiti@2.6.1))(magicast@0.3.5)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) @@ -2250,35 +2247,6 @@ packages: '@poppinss/exception@1.2.2': resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/client@1.6.1': - resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} - engines: {node: '>=14'} - - '@redis/graph@1.1.1': - resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/json@1.0.7': - resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/search@1.2.0': - resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/time-series@1.1.0': - resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} - peerDependencies: - '@redis/client': ^1.0.0 - '@resvg/resvg-js-android-arm-eabi@2.6.2': resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} engines: {node: '>= 10'} @@ -5057,10 +5025,6 @@ packages: fuzzysort@3.1.0: resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -5951,16 +5915,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mcp-handler@1.0.3: - resolution: {integrity: sha512-AQ8I1a6wyfK0hIHeGqvjXqKHGvABDvw5dvwJvMjUiuKAUDgFsFtPExqThFOE/boOTbBKXn7fPSJ94soT0GjL+A==} - hasBin: true - peerDependencies: - '@modelcontextprotocol/sdk': ^1.17.2 - next: '>=13.0.0' - peerDependenciesMeta: - next: - optional: true - mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -7045,9 +6999,6 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - redis@4.7.1: - resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} - refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -8455,9 +8406,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -10785,32 +10733,6 @@ snapshots: '@poppinss/exception@1.2.2': {} - '@redis/bloom@1.2.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/client@1.6.1': - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - - '@redis/graph@1.1.1(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/json@1.0.7(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/search@1.2.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/time-series@1.1.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - '@resvg/resvg-js-android-arm-eabi@2.6.2': optional: true @@ -14024,8 +13946,6 @@ snapshots: fuzzysort@3.1.0: {} - generic-pool@3.9.0: {} - gensync@1.0.0-beta.2: {} geojson-vt@3.2.1: {} @@ -15009,13 +14929,6 @@ snapshots: math-intrinsics@1.1.0: {} - mcp-handler@1.0.3(@modelcontextprotocol/sdk@1.22.0): - dependencies: - '@modelcontextprotocol/sdk': 1.22.0 - chalk: 5.6.2 - commander: 11.1.0 - redis: 4.7.1 - mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -16799,15 +16712,6 @@ snapshots: dependencies: redis-errors: 1.2.0 - redis@4.7.1: - dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.6.1) - '@redis/client': 1.6.1 - '@redis/graph': 1.1.1(@redis/client@1.6.1) - '@redis/json': 1.0.7(@redis/client@1.6.1) - '@redis/search': 1.2.0(@redis/client@1.6.1) - '@redis/time-series': 1.1.0(@redis/client@1.6.1) - refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -18691,8 +18595,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yallist@5.0.0: {} yaml-eslint-parser@1.3.0: