@@ -1377,9 +1377,13 @@ class FlowMcpCli {
13771377 const results = groupSchemas
13781378 . map ( ( { main, file } ) => {
13791379 try {
1380- const { status, messages } = FlowMCP . validateMain ( { main } )
1380+ const normalizedMain = FlowMcpCli . #normalizeMainForValidation( { main } )
1381+ const { status, messages } = FlowMCP . validateMain ( { 'main' : normalizedMain } )
13811382 const namespace = main [ 'namespace' ] || 'unknown'
1382- const entry = { file, namespace, status, messages }
1383+ const toolCount = Object . keys ( main [ 'tools' ] || main [ 'routes' ] || { } ) . length
1384+ const resourceCount = Object . keys ( main [ 'resources' ] || { } ) . length
1385+ const skillCount = ( main [ 'skills' ] || [ ] ) . length
1386+ const entry = { file, namespace, status, messages, 'tools' : toolCount , 'resources' : resourceCount , 'skills' : skillCount }
13831387
13841388 return entry
13851389 } catch ( err ) {
@@ -1432,9 +1436,13 @@ class FlowMcpCli {
14321436 const results = schemas
14331437 . map ( ( { main, file } ) => {
14341438 try {
1435- const { status, messages } = FlowMCP . validateMain ( { main } )
1439+ const normalizedMain = FlowMcpCli . #normalizeMainForValidation( { main } )
1440+ const { status, messages } = FlowMCP . validateMain ( { 'main' : normalizedMain } )
14361441 const namespace = main [ 'namespace' ] || 'unknown'
1437- const entry = { file, namespace, status, messages }
1442+ const toolCount = Object . keys ( main [ 'tools' ] || main [ 'routes' ] || { } ) . length
1443+ const resourceCount = Object . keys ( main [ 'resources' ] || { } ) . length
1444+ const skillCount = ( main [ 'skills' ] || [ ] ) . length
1445+ const entry = { file, namespace, status, messages, 'tools' : toolCount , 'resources' : resourceCount , 'skills' : skillCount }
14381446
14391447 return entry
14401448 } catch ( err ) {
@@ -1471,6 +1479,169 @@ class FlowMcpCli {
14711479 }
14721480
14731481
1482+ static async migrate ( { schemaPath, cwd, all = false , dryRun = false } ) {
1483+ const { initialized, error : initError , fix : initFix } = await FlowMcpCli . #requireInit( )
1484+ if ( ! initialized ) {
1485+ const result = FlowMcpCli . #error( { 'error' : initError , 'fix' : initFix } )
1486+
1487+ return { result }
1488+ }
1489+
1490+ const { status : validStatus , messages : validMessages } = FlowMcpCli . validationMigrate ( { schemaPath, all } )
1491+ if ( ! validStatus ) {
1492+ const result = { 'status' : false , 'messages' : validMessages }
1493+
1494+ return { result }
1495+ }
1496+
1497+ const routesKeyPattern = / ( [ ' " ] ) r o u t e s \1( \s * : ) / g
1498+ const versionPattern = / ( [ ' " ] v e r s i o n [ ' " ] ) \s * : \s * [ ' " ] 2 \. \d + \. \d + [ ' " ] / g
1499+
1500+ let filePaths = [ ]
1501+
1502+ if ( all ) {
1503+ const dirPath = schemaPath || cwd
1504+ const resolvedDir = resolve ( dirPath )
1505+
1506+ let dirStat
1507+ try {
1508+ dirStat = await stat ( resolvedDir )
1509+ } catch {
1510+ const result = FlowMcpCli . #error( { 'error' : `Path not found: ${ dirPath } ` } )
1511+
1512+ return { result }
1513+ }
1514+
1515+ if ( ! dirStat . isDirectory ( ) ) {
1516+ const result = FlowMcpCli . #error( { 'error' : `Path is not a directory: ${ dirPath } . Use --all with a directory.` } )
1517+
1518+ return { result }
1519+ }
1520+
1521+ const { files } = await FlowMcpCli . #findSchemaFiles( { dirPath : resolvedDir } )
1522+ filePaths = files
1523+ } else {
1524+ const resolvedPath = resolve ( schemaPath )
1525+
1526+ let pathStat
1527+ try {
1528+ pathStat = await stat ( resolvedPath )
1529+ } catch {
1530+ const result = FlowMcpCli . #error( { 'error' : `Path not found: ${ schemaPath } ` } )
1531+
1532+ return { result }
1533+ }
1534+
1535+ if ( pathStat . isDirectory ( ) ) {
1536+ const { files } = await FlowMcpCli . #findSchemaFiles( { dirPath : resolvedPath } )
1537+ filePaths = files
1538+ } else {
1539+ filePaths = [ resolvedPath ]
1540+ }
1541+ }
1542+
1543+ if ( filePaths . length === 0 ) {
1544+ const result = {
1545+ 'status' : true ,
1546+ 'total' : 0 ,
1547+ 'migrated' : 0 ,
1548+ 'skipped' : 0 ,
1549+ 'failed' : 0 ,
1550+ dryRun,
1551+ 'results' : [ ]
1552+ }
1553+
1554+ return { result }
1555+ }
1556+
1557+ const results = [ ]
1558+ let migrated = 0
1559+ let skipped = 0
1560+ let failed = 0
1561+
1562+ const processFile = async ( filePath ) => {
1563+ try {
1564+ const content = await readFile ( filePath , 'utf-8' )
1565+ const hasRoutes = routesKeyPattern . test ( content )
1566+ routesKeyPattern . lastIndex = 0
1567+ const hasV2Version = versionPattern . test ( content )
1568+ versionPattern . lastIndex = 0
1569+
1570+ if ( ! hasRoutes && ! hasV2Version ) {
1571+ skipped += 1
1572+ results . push ( { 'file' : filePath , 'action' : 'skipped' , 'reason' : 'No v2 routes or version found' } )
1573+
1574+ return
1575+ }
1576+
1577+ if ( dryRun ) {
1578+ migrated += 1
1579+ const changes = [ ]
1580+ if ( hasRoutes ) { changes . push ( 'routes -> tools' ) }
1581+ if ( hasV2Version ) { changes . push ( 'version 2.x.x -> 3.0.0' ) }
1582+ results . push ( { 'file' : filePath , 'action' : 'migrated' , 'reason' : `[dry-run] Would apply: ${ changes . join ( ', ' ) } ` } )
1583+
1584+ return
1585+ }
1586+
1587+ let updatedContent = content
1588+ updatedContent = updatedContent . replace ( routesKeyPattern , '$1tools$1$2' )
1589+ routesKeyPattern . lastIndex = 0
1590+ updatedContent = updatedContent . replace ( versionPattern , `$1: '3.0.0'` )
1591+ versionPattern . lastIndex = 0
1592+
1593+ await writeFile ( filePath , updatedContent , 'utf-8' )
1594+ migrated += 1
1595+ results . push ( { 'file' : filePath , 'action' : 'migrated' , 'reason' : 'Successfully migrated to v3' } )
1596+ } catch ( err ) {
1597+ failed += 1
1598+ results . push ( { 'file' : filePath , 'action' : 'failed' , 'reason' : err . message } )
1599+ }
1600+ }
1601+
1602+ await Promise . all (
1603+ filePaths
1604+ . map ( ( filePath ) => {
1605+ const promise = processFile ( filePath )
1606+
1607+ return promise
1608+ } )
1609+ )
1610+
1611+ const total = migrated + skipped + failed
1612+ const result = {
1613+ 'status' : failed === 0 ,
1614+ total,
1615+ migrated,
1616+ skipped,
1617+ failed,
1618+ dryRun,
1619+ results
1620+ }
1621+
1622+ return { result }
1623+ }
1624+
1625+
1626+ static validationMigrate ( { schemaPath, all } ) {
1627+ const struct = { 'status' : false , 'messages' : [ ] }
1628+
1629+ if ( ! all && ( schemaPath === undefined || schemaPath === null ) ) {
1630+ struct [ 'messages' ] . push ( 'schemaPath: Missing value. Provide a path or use --all.' )
1631+ } else if ( ! all && typeof schemaPath !== 'string' ) {
1632+ struct [ 'messages' ] . push ( 'schemaPath: Must be a string.' )
1633+ }
1634+
1635+ if ( struct [ 'messages' ] . length > 0 ) {
1636+ return struct
1637+ }
1638+
1639+ struct [ 'status' ] = true
1640+
1641+ return struct
1642+ }
1643+
1644+
14741645 static async test ( { schemaPath, route, cwd, group, all } ) {
14751646 const { initialized, error : initError , fix : initFix } = await FlowMcpCli . #requireInit( )
14761647 if ( ! initialized ) {
@@ -2579,6 +2750,7 @@ class FlowMcpCli {
25792750 namespace,
25802751 tags,
25812752 score,
2753+ 'type' : tool [ 'type' ] || 'tool' ,
25822754 'add' : `${ appConfig [ 'cliCommand' ] } add ${ toolName } `
25832755 }
25842756
@@ -3952,8 +4124,9 @@ class FlowMcpCli {
39524124 const filePath = join ( schemasBaseDir , schemaRef )
39534125 const { main } = await FlowMcpCli . #loadSchema( { filePath } )
39544126
3955- if ( main && main [ 'routes' ] ) {
3956- Object . entries ( main [ 'routes' ] )
4127+ const toolEntries = main ? ( main [ 'tools' ] || main [ 'routes' ] ) : null
4128+ if ( main && toolEntries ) {
4129+ Object . entries ( toolEntries )
39574130 . forEach ( ( [ routeName , routeConfig ] ) => {
39584131 const routeDescription = routeConfig [ 'description' ] || ''
39594132 const toolRef = `${ schemaRef } ::${ routeName } `
@@ -3970,7 +4143,51 @@ class FlowMcpCli {
39704143 namespace,
39714144 'description' : routeDescription ,
39724145 'tags' : main [ 'tags' ] || [ ] ,
3973- 'schemaName' : main [ 'name' ] || ''
4146+ 'schemaName' : main [ 'name' ] || '' ,
4147+ 'type' : 'tool'
4148+ } )
4149+ } )
4150+ }
4151+
4152+ if ( main && main [ 'resources' ] ) {
4153+ Object . entries ( main [ 'resources' ] )
4154+ . forEach ( ( [ resourceName , resourceConfig ] ) => {
4155+ const resourceDescription = resourceConfig [ 'description' ] || ''
4156+ const toolRef = `${ schemaRef } ::resource::${ resourceName } `
4157+ const toolName = `${ resourceName } _${ main [ 'namespace' ] || namespace } `
4158+
4159+ tools . push ( {
4160+ toolRef,
4161+ toolName,
4162+ schemaRef,
4163+ 'routeName' : resourceName ,
4164+ namespace,
4165+ 'description' : resourceDescription ,
4166+ 'tags' : main [ 'tags' ] || [ ] ,
4167+ 'schemaName' : main [ 'name' ] || '' ,
4168+ 'type' : 'resource'
4169+ } )
4170+ } )
4171+ }
4172+
4173+ if ( main && main [ 'skills' ] ) {
4174+ main [ 'skills' ]
4175+ . forEach ( ( skillDef ) => {
4176+ const skillName = skillDef [ 'name' ] || 'unknown'
4177+ const skillDescription = skillDef [ 'description' ] || ''
4178+ const toolRef = `${ schemaRef } ::skill::${ skillName } `
4179+ const toolName = `${ skillName } _${ main [ 'namespace' ] || namespace } `
4180+
4181+ tools . push ( {
4182+ toolRef,
4183+ toolName,
4184+ schemaRef,
4185+ 'routeName' : skillName ,
4186+ namespace,
4187+ 'description' : skillDescription ,
4188+ 'tags' : main [ 'tags' ] || [ ] ,
4189+ 'schemaName' : main [ 'name' ] || '' ,
4190+ 'type' : 'skill'
39744191 } )
39754192 } )
39764193 }
@@ -5389,10 +5606,10 @@ Note: Run "${cmd} init" first. This is the only interactive command.
53895606
53905607
53915608 static #getAllTests( { main } ) {
5392- const routes = main [ 'routes' ] || { }
5609+ const tools = main [ 'tools' ] || main [ 'routes' ] || { }
53935610 const tests = [ ]
53945611
5395- Object . entries ( routes )
5612+ Object . entries ( tools )
53965613 . forEach ( ( [ routeName , routeConfig ] ) => {
53975614 const routeTests = routeConfig [ 'tests' ] || [ ]
53985615
@@ -5505,7 +5722,7 @@ Note: Run "${cmd} init" first. This is the only interactive command.
55055722
55065723 static #prepareServerTool( { main, handlerMap, serverParams, routeName } ) {
55075724 const namespace = main [ 'namespace' ] || 'unknown'
5508- const routes = main [ 'routes' ] || { }
5725+ const routes = main [ 'tools' ] || main [ ' routes' ] || { }
55095726 const routeConfig = routes [ routeName ]
55105727
55115728 if ( ! routeConfig ) {
@@ -5943,6 +6160,26 @@ Note: Run "${cmd} init" first. This is the only interactive command.
59436160 }
59446161
59456162
6163+ static async #findSchemaFiles( { dirPath } ) {
6164+ const entries = await readdir ( dirPath , { recursive : true } )
6165+ const files = entries
6166+ . filter ( ( entry ) => {
6167+ const ext = extname ( entry )
6168+ const isSchema = ext === '.mjs' || ext === '.js'
6169+
6170+ return isSchema
6171+ } )
6172+ . map ( ( entry ) => {
6173+ const fullPath = join ( dirPath , entry )
6174+
6175+ return fullPath
6176+ } )
6177+ . sort ( )
6178+
6179+ return { files }
6180+ }
6181+
6182+
59466183 static async #removeToolSchema( { toolName, cwd } ) {
59476184 const filePath = join ( cwd , appConfig [ 'localConfigDirName' ] , 'tools' , `${ toolName } .json` )
59486185
@@ -5954,6 +6191,22 @@ Note: Run "${cmd} init" first. This is the only interactive command.
59546191 return { 'removed' : false }
59556192 }
59566193 }
6194+
6195+
6196+ static #normalizeMainForValidation( { main } ) {
6197+ if ( ! main ) {
6198+ return main
6199+ }
6200+
6201+ if ( main [ 'tools' ] && ! main [ 'routes' ] ) {
6202+ const { tools, resources, skills, ...rest } = main
6203+ const normalizedMain = { ...rest , 'routes' : tools }
6204+
6205+ return normalizedMain
6206+ }
6207+
6208+ return main
6209+ }
59576210}
59586211
59596212
0 commit comments