1+ import type { H3Event } from 'h3'
12import type { Registry , RegistryItem } from 'shadcn-vue/schema'
2- import type { ZodRawShape } from 'zod '
3- import { fromWebHandler } from 'h3 '
4- import { createMcpHandler } from 'mcp-handler '
3+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js '
4+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js '
5+ import { defineEventHandler , getHeader , readBody , sendRedirect } from 'h3 '
56import { useStorage } from 'nitropack/runtime'
67import { z } from 'zod'
78
89const REGISTRY_STORAGE_BASE = 'assets:registry'
10+
911const REGISTRY_INDEX_FILE = 'index.json'
1012
11- // Parameter Schemas
12- const componentParamsShape = {
13- component : z . string ( ) . min ( 1 , 'component is required' ) ,
14- } satisfies ZodRawShape
13+ const SERVER_INFO = {
14+ name : 'ai-elements-vue' ,
15+ version : '1.0.0' ,
16+ }
17+
18+ const DOCS_REDIRECT = 'https://www.ai-elements-vue.com/overview/mcp-server'
19+
20+ const getComponentInputSchema = z . object ( {
21+ component : z
22+ . string ( )
23+ . min ( 1 , 'component is required' )
24+ . describe ( 'Component name (e.g. "context")' ) ,
25+ } )
1526
16- const componentParamsSchema = z . object ( componentParamsShape )
27+ type GetComponentInput = z . infer < typeof getComponentInputSchema >
1728
1829// Data Access Layer
1930function getRegistryStorage ( ) {
@@ -73,16 +84,7 @@ async function handleListComponents() {
7384 }
7485}
7586
76- async function handleGetComponent ( args : Record < string , unknown > ) {
77- const parsedArgs = componentParamsSchema . safeParse ( args )
78- if ( ! parsedArgs . success ) {
79- return {
80- content : [ { type : 'text' as const , text : `Invalid input: ${ parsedArgs . error . message } ` } ] ,
81- isError : true ,
82- }
83- }
84-
85- const { component } = parsedArgs . data
87+ async function handleGetComponent ( component : string ) {
8688 const registryItem = await loadRegistryItem ( component )
8789
8890 if ( ! registryItem ) {
@@ -97,28 +99,67 @@ async function handleGetComponent(args: Record<string, unknown>) {
9799 }
98100}
99101
100- // Main Handler
101- const handler = createMcpHandler (
102- ( server ) => {
103- server . registerTool (
104- 'get_ai_elements_components' ,
105- {
106- title : 'List AI Elements components' ,
107- description : 'Provides a list of all AI Elements components.' ,
108- } ,
109- handleListComponents ,
110- )
111-
112- server . registerTool (
113- 'get_ai_elements_component' ,
114- {
115- title : 'Get AI Elements component' ,
116- description : 'Provides information about an AI Elements component.' ,
117- inputSchema : componentParamsShape ,
118- } ,
119- handleGetComponent ,
120- )
121- } ,
122- )
123-
124- export default fromWebHandler ( handler )
102+ function createMcpServer ( ) {
103+ const server = new McpServer ( SERVER_INFO )
104+
105+ server . registerTool (
106+ 'get_ai_elements_components' ,
107+ {
108+ title : 'List AI Elements components' ,
109+ description : 'Provides a list of all AI Elements components.' ,
110+ } ,
111+ async ( ) => handleListComponents ( ) ,
112+ )
113+
114+ server . registerTool (
115+ 'get_ai_elements_component' ,
116+ {
117+ title : 'Get AI Elements component' ,
118+ description : 'Provides information about an AI Elements component.' ,
119+ inputSchema : getComponentInputSchema ,
120+ } ,
121+ async ( { component } : GetComponentInput ) => handleGetComponent ( component ) ,
122+ )
123+
124+ return server
125+ }
126+
127+ async function readOptionalBody ( event : H3Event ) {
128+ const method = event . node . req . method
129+ if ( ! method || method === 'GET' || method === 'HEAD' ) {
130+ return undefined
131+ }
132+
133+ try {
134+ return await readBody ( event )
135+ }
136+ catch ( error ) {
137+ console . warn ( 'Failed to parse MCP request body, falling back to undefined' , error )
138+ return undefined
139+ }
140+ }
141+
142+ export default defineEventHandler ( async ( event ) => {
143+ if ( event . node . req . method === 'GET' ) {
144+ const accept = getHeader ( event , 'accept' ) ?? ''
145+ if ( accept . includes ( 'text/html' ) ) {
146+ return sendRedirect ( event , DOCS_REDIRECT )
147+ }
148+ }
149+
150+ const server = createMcpServer ( )
151+ const transport = new StreamableHTTPServerTransport ( {
152+ sessionIdGenerator : undefined ,
153+ } )
154+
155+ event . node . res . once ( 'close' , ( ) => {
156+ server . close ( ) . catch ( error => console . error ( 'Failed to close MCP server' , error ) )
157+ if ( typeof transport . close === 'function' ) {
158+ transport . close ( ) . catch ( error => console . error ( 'Failed to close MCP transport' , error ) )
159+ }
160+ } )
161+
162+ const body = await readOptionalBody ( event )
163+ await server . connect ( transport )
164+ await transport . handleRequest ( event . node . req , event . node . res , body )
165+ } )
0 commit comments