github-to-mcp-monorepo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/.env.example +8 -0
  2. package/.github/CODEOWNERS +6 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +5 -0
  6. package/.prettierrc +7 -0
  7. package/.vscode/settings.json +4 -0
  8. package/ARCHITECTURE.md +1429 -0
  9. package/CHANGELOG.md +167 -0
  10. package/CONTRIBUTING.md +327 -0
  11. package/LICENSE +201 -0
  12. package/README.md +1028 -0
  13. package/SECURITY.md +248 -0
  14. package/VISUAL_GUIDE.md +437 -0
  15. package/apps/vscode/IMPLEMENTATION.md +480 -0
  16. package/apps/vscode/README.md +248 -0
  17. package/apps/vscode/package.json +381 -0
  18. package/apps/vscode/resources/icon.png +0 -0
  19. package/apps/vscode/resources/icon.svg +5 -0
  20. package/apps/vscode/src/commands/browseRegistry.ts +211 -0
  21. package/apps/vscode/src/commands/configureClaudeDesktop.ts +332 -0
  22. package/apps/vscode/src/commands/convert.ts +82 -0
  23. package/apps/vscode/src/commands/convertCurrentRepo.ts +109 -0
  24. package/apps/vscode/src/commands/convertFromUrl.ts +138 -0
  25. package/apps/vscode/src/commands/index.ts +121 -0
  26. package/apps/vscode/src/commands/validate.ts +197 -0
  27. package/apps/vscode/src/extension.ts +464 -0
  28. package/apps/vscode/src/global.d.ts +36 -0
  29. package/apps/vscode/src/test/extension.test.ts +73 -0
  30. package/apps/vscode/src/utils/file-generator.ts +529 -0
  31. package/apps/vscode/src/utils/github-api.ts +335 -0
  32. package/apps/vscode/src/utils/index.ts +29 -0
  33. package/apps/vscode/src/utils/mcp-config.ts +334 -0
  34. package/apps/vscode/src/utils/storage.ts +87 -0
  35. package/apps/vscode/src/views/McpServersTreeView.ts +160 -0
  36. package/apps/vscode/src/views/OutputChannelView.ts +195 -0
  37. package/apps/vscode/src/views/StatusBarItem.ts +251 -0
  38. package/apps/vscode/src/views/ToolsExplorerView.ts +314 -0
  39. package/apps/vscode/src/views/historyProvider.ts +75 -0
  40. package/apps/vscode/src/views/index.ts +12 -0
  41. package/apps/vscode/src/views/resultsPanel.ts +330 -0
  42. package/apps/vscode/src/webviews/ConversionPanel.ts +350 -0
  43. package/apps/vscode/src/webviews/ToolDetailsPanel.ts +448 -0
  44. package/apps/vscode/src/webviews/index.ts +9 -0
  45. package/apps/vscode/src/webviews/webview-ui/styles.ts +492 -0
  46. package/apps/vscode/tsconfig.json +20 -0
  47. package/apps/web/PLAYGROUND_GUIDE.md +499 -0
  48. package/apps/web/README.md +505 -0
  49. package/apps/web/app/api/convert/route.ts +100 -0
  50. package/apps/web/app/api/convert/stream/route.ts +198 -0
  51. package/apps/web/app/api/deploy/route.ts +157 -0
  52. package/apps/web/app/api/edge/route.ts +308 -0
  53. package/apps/web/app/api/export-docker/route.ts +284 -0
  54. package/apps/web/app/api/generate-openapi/route.ts +119 -0
  55. package/apps/web/app/api/mcp/[serverId]/route.ts +263 -0
  56. package/apps/web/app/api/playground/connect/route.ts +143 -0
  57. package/apps/web/app/api/playground/disconnect/route.ts +78 -0
  58. package/apps/web/app/api/playground/execute/route.ts +135 -0
  59. package/apps/web/app/api/playground/sessions/route.ts +103 -0
  60. package/apps/web/app/api/playground/tools/route.ts +117 -0
  61. package/apps/web/app/api/playground/v2/connect/route.ts +96 -0
  62. package/apps/web/app/api/playground/v2/disconnect/route.ts +88 -0
  63. package/apps/web/app/api/playground/v2/health/route.ts +80 -0
  64. package/apps/web/app/api/playground/v2/prompts/route.ts +160 -0
  65. package/apps/web/app/api/playground/v2/resources/route.ts +159 -0
  66. package/apps/web/app/api/playground/v2/sessions/route.ts +184 -0
  67. package/apps/web/app/api/playground/v2/tools/route.ts +167 -0
  68. package/apps/web/app/api/stream/route.ts +232 -0
  69. package/apps/web/app/batch/BatchConvertClient.tsx +190 -0
  70. package/apps/web/app/batch/page.tsx +37 -0
  71. package/apps/web/app/convert/page.tsx +269 -0
  72. package/apps/web/app/dashboard/page.tsx +380 -0
  73. package/apps/web/app/globals.css +622 -0
  74. package/apps/web/app/layout.tsx +120 -0
  75. package/apps/web/app/manifest.ts +31 -0
  76. package/apps/web/app/opengraph-image.tsx +112 -0
  77. package/apps/web/app/page.old.tsx +924 -0
  78. package/apps/web/app/page.tsx +77 -0
  79. package/apps/web/app/playground/page.tsx +306 -0
  80. package/apps/web/app/playground/v2/error.tsx +163 -0
  81. package/apps/web/app/playground/v2/layout.tsx +58 -0
  82. package/apps/web/app/playground/v2/loading.tsx +152 -0
  83. package/apps/web/app/playground/v2/page.tsx +644 -0
  84. package/apps/web/app/playground/v2/providers.tsx +214 -0
  85. package/apps/web/app/playground/v2/use-shortcuts.ts +209 -0
  86. package/apps/web/app/playground/v2/use-url-state.ts +296 -0
  87. package/apps/web/app/providers.tsx +22 -0
  88. package/apps/web/app/sitemap.ts +32 -0
  89. package/apps/web/app/twitter-image.tsx +112 -0
  90. package/apps/web/components/BranchSelector.tsx +401 -0
  91. package/apps/web/components/ClaudeConfigExport.tsx +226 -0
  92. package/apps/web/components/Features.tsx +84 -0
  93. package/apps/web/components/Footer.tsx +119 -0
  94. package/apps/web/components/GenerationProgress.tsx +248 -0
  95. package/apps/web/components/GithubUrlInput.tsx +483 -0
  96. package/apps/web/components/Header.tsx +175 -0
  97. package/apps/web/components/Hero.tsx +117 -0
  98. package/apps/web/components/HowItWorks.tsx +119 -0
  99. package/apps/web/components/InstallBanner.tsx +158 -0
  100. package/apps/web/components/Logo.tsx +116 -0
  101. package/apps/web/components/ParticleBackground.tsx +105 -0
  102. package/apps/web/components/Playground.tsx +472 -0
  103. package/apps/web/components/PlaygroundToolTester.tsx +410 -0
  104. package/apps/web/components/ProductCards.tsx +179 -0
  105. package/apps/web/components/SplitView.tsx +194 -0
  106. package/apps/web/components/ToolFilter.tsx +260 -0
  107. package/apps/web/components/ToolList.tsx +325 -0
  108. package/apps/web/components/batch/BatchConvert.tsx +785 -0
  109. package/apps/web/components/batch/index.ts +7 -0
  110. package/apps/web/components/convert/ConfigTabs.tsx +230 -0
  111. package/apps/web/components/convert/ConversionResult.tsx +482 -0
  112. package/apps/web/components/convert/InlinePlayground.tsx +259 -0
  113. package/apps/web/components/convert/LoadingSteps.tsx +311 -0
  114. package/apps/web/components/convert/OneClickInstall.tsx +224 -0
  115. package/apps/web/components/convert/ToolCard.tsx +189 -0
  116. package/apps/web/components/convert/TryInPlayground.tsx +242 -0
  117. package/apps/web/components/convert/index.ts +12 -0
  118. package/apps/web/components/deploy/DeployButton.tsx +369 -0
  119. package/apps/web/components/deploy/index.ts +7 -0
  120. package/apps/web/components/docker/DockerExport.tsx +690 -0
  121. package/apps/web/components/docker/index.ts +7 -0
  122. package/apps/web/components/install/OneClickInstall.tsx +676 -0
  123. package/apps/web/components/install/index.ts +7 -0
  124. package/apps/web/components/playground/CapabilityTabs.tsx +150 -0
  125. package/apps/web/components/playground/ConnectionStatusV2.tsx +322 -0
  126. package/apps/web/components/playground/EmptyStates.tsx +305 -0
  127. package/apps/web/components/playground/ExecutionLog.tsx +260 -0
  128. package/apps/web/components/playground/ExecutionLogV2.tsx +378 -0
  129. package/apps/web/components/playground/JsonViewer.tsx +388 -0
  130. package/apps/web/components/playground/PlaygroundLayout.tsx +244 -0
  131. package/apps/web/components/playground/PromptsPanel.tsx +385 -0
  132. package/apps/web/components/playground/ResourcesPanel.tsx +378 -0
  133. package/apps/web/components/playground/SchemaForm.tsx +477 -0
  134. package/apps/web/components/playground/ServerStatus.tsx +151 -0
  135. package/apps/web/components/playground/ShareButton.tsx +239 -0
  136. package/apps/web/components/playground/ToolsPanel.tsx +309 -0
  137. package/apps/web/components/playground/TransportConfigurator.tsx +563 -0
  138. package/apps/web/components/playground/index.ts +74 -0
  139. package/apps/web/components/playground/types.ts +202 -0
  140. package/apps/web/components/streaming/StreamingProgress.tsx +441 -0
  141. package/apps/web/components/streaming/index.ts +7 -0
  142. package/apps/web/components/ui/badge.tsx +42 -0
  143. package/apps/web/components/ui/button.tsx +88 -0
  144. package/apps/web/components/ui/card.tsx +75 -0
  145. package/apps/web/components/ui/code-block.tsx +122 -0
  146. package/apps/web/components/ui/index.ts +12 -0
  147. package/apps/web/components/ui/input.tsx +55 -0
  148. package/apps/web/components/ui/tabs.tsx +61 -0
  149. package/apps/web/hooks/index.ts +85 -0
  150. package/apps/web/hooks/types.ts +1173 -0
  151. package/apps/web/hooks/use-conversion.ts +133 -0
  152. package/apps/web/hooks/use-execution-history.ts +376 -0
  153. package/apps/web/hooks/use-generation-progress.ts +147 -0
  154. package/apps/web/hooks/use-local-storage.ts +88 -0
  155. package/apps/web/hooks/use-mcp-client.ts +623 -0
  156. package/apps/web/hooks/use-mcp-connection.ts +500 -0
  157. package/apps/web/hooks/use-mcp-execution.ts +282 -0
  158. package/apps/web/hooks/use-mcp-prompts.ts +441 -0
  159. package/apps/web/hooks/use-mcp-resources.ts +430 -0
  160. package/apps/web/hooks/use-mcp-tools.ts +540 -0
  161. package/apps/web/hooks/use-playground-store.ts +299 -0
  162. package/apps/web/hooks/use-playground.ts +184 -0
  163. package/apps/web/hooks/use-streaming-conversion.ts +227 -0
  164. package/apps/web/hooks/useBatchConversion.ts +271 -0
  165. package/apps/web/hooks/useDockerConfig.ts +161 -0
  166. package/apps/web/hooks/usePlatformDetection.ts +80 -0
  167. package/apps/web/hooks/useStreaming.ts +199 -0
  168. package/apps/web/lib/api/errors.ts +386 -0
  169. package/apps/web/lib/api/index.ts +137 -0
  170. package/apps/web/lib/api/logger.ts +187 -0
  171. package/apps/web/lib/api/middleware.ts +364 -0
  172. package/apps/web/lib/api/openapi.ts +977 -0
  173. package/apps/web/lib/api/session-manager.ts +594 -0
  174. package/apps/web/lib/api/types.ts +433 -0
  175. package/apps/web/lib/api/validation.ts +523 -0
  176. package/apps/web/lib/constants.ts +114 -0
  177. package/apps/web/lib/mcp/client.ts +1137 -0
  178. package/apps/web/lib/mcp/events.ts +651 -0
  179. package/apps/web/lib/mcp/index.ts +347 -0
  180. package/apps/web/lib/mcp/logger.ts +428 -0
  181. package/apps/web/lib/mcp/metrics.ts +703 -0
  182. package/apps/web/lib/mcp/retry.ts +616 -0
  183. package/apps/web/lib/mcp/session-manager.ts +779 -0
  184. package/apps/web/lib/mcp/transports.ts +988 -0
  185. package/apps/web/lib/mcp/types.ts +594 -0
  186. package/apps/web/lib/mcp-client-enhanced.ts +871 -0
  187. package/apps/web/lib/mcp-client.ts +778 -0
  188. package/apps/web/lib/mcp-errors.ts +489 -0
  189. package/apps/web/lib/mcp-sandbox.ts +593 -0
  190. package/apps/web/lib/mcp-testing.ts +428 -0
  191. package/apps/web/lib/mcp-types.ts +448 -0
  192. package/apps/web/lib/playground-store.tsx +1147 -0
  193. package/apps/web/lib/utils.ts +439 -0
  194. package/apps/web/next-env.d.ts +5 -0
  195. package/apps/web/next.config.js +23 -0
  196. package/apps/web/package.json +55 -0
  197. package/apps/web/postcss.config.js +6 -0
  198. package/apps/web/public/.well-known/ai-plugin.json +17 -0
  199. package/apps/web/public/logo.svg +6 -0
  200. package/apps/web/public/robots.txt +22 -0
  201. package/apps/web/public/schema.json +27 -0
  202. package/apps/web/tailwind.config.js +26 -0
  203. package/apps/web/tailwind.config.ts +123 -0
  204. package/apps/web/tsconfig.json +20 -0
  205. package/apps/web/types/deploy.ts +139 -0
  206. package/apps/web/types/index.ts +247 -0
  207. package/apps/web/vercel.json +39 -0
  208. package/eslint.config.mjs +23 -0
  209. package/llms.txt +102 -0
  210. package/mkdocs/docs/api/core.md +318 -0
  211. package/mkdocs/docs/api/index.md +128 -0
  212. package/mkdocs/docs/api/mcp-server.md +301 -0
  213. package/mkdocs/docs/api/openapi-parser.md +254 -0
  214. package/mkdocs/docs/assets/logo.svg +7 -0
  215. package/mkdocs/docs/changelog.md +118 -0
  216. package/mkdocs/docs/cli/generate.md +148 -0
  217. package/mkdocs/docs/cli/index.md +52 -0
  218. package/mkdocs/docs/cli/inspect.md +164 -0
  219. package/mkdocs/docs/cli/serve.md +136 -0
  220. package/mkdocs/docs/concepts/classification.md +254 -0
  221. package/mkdocs/docs/concepts/how-it-works.md +299 -0
  222. package/mkdocs/docs/concepts/index.md +77 -0
  223. package/mkdocs/docs/concepts/mcp-protocol.md +362 -0
  224. package/mkdocs/docs/concepts/tool-types.md +382 -0
  225. package/mkdocs/docs/contributing/architecture.md +262 -0
  226. package/mkdocs/docs/contributing/development.md +245 -0
  227. package/mkdocs/docs/contributing/index.md +73 -0
  228. package/mkdocs/docs/contributing/testing.md +320 -0
  229. package/mkdocs/docs/getting-started/configuration.md +235 -0
  230. package/mkdocs/docs/getting-started/index.md +54 -0
  231. package/mkdocs/docs/getting-started/installation.md +145 -0
  232. package/mkdocs/docs/getting-started/quickstart.md +160 -0
  233. package/mkdocs/docs/guides/batch.md +375 -0
  234. package/mkdocs/docs/guides/claude-desktop.md +227 -0
  235. package/mkdocs/docs/guides/cursor.md +188 -0
  236. package/mkdocs/docs/guides/custom-tools.md +367 -0
  237. package/mkdocs/docs/guides/index.md +78 -0
  238. package/mkdocs/docs/guides/private-repos.md +221 -0
  239. package/mkdocs/docs/guides/vscode.md +247 -0
  240. package/mkdocs/docs/index.md +175 -0
  241. package/mkdocs/docs/reference/config.md +223 -0
  242. package/mkdocs/docs/reference/env.md +192 -0
  243. package/mkdocs/docs/reference/index.md +102 -0
  244. package/mkdocs/docs/reference/tools.md +309 -0
  245. package/mkdocs/docs/stylesheets/extra.css +231 -0
  246. package/mkdocs/mkdocs.yml +204 -0
  247. package/mkdocs/overrides/.gitkeep +1 -0
  248. package/mkdocs/overrides/main.html +7 -0
  249. package/mkdocs/python-deps.txt +7 -0
  250. package/mkdocs/vercel.json +11 -0
  251. package/package.json +63 -0
  252. package/packages/core/package.json +61 -0
  253. package/packages/core/src/__tests__/bitbucket-client.test.ts +366 -0
  254. package/packages/core/src/__tests__/cli.test.ts +235 -0
  255. package/packages/core/src/__tests__/code-extractor.test.ts +378 -0
  256. package/packages/core/src/__tests__/docker-generator.test.ts +255 -0
  257. package/packages/core/src/__tests__/github-client.test.ts +390 -0
  258. package/packages/core/src/__tests__/gitlab-client.test.ts +319 -0
  259. package/packages/core/src/__tests__/go-extractor.test.ts +351 -0
  260. package/packages/core/src/__tests__/graphql-extractor.test.ts +330 -0
  261. package/packages/core/src/__tests__/java-extractor.test.ts +497 -0
  262. package/packages/core/src/__tests__/plugins.test.ts +467 -0
  263. package/packages/core/src/__tests__/readme-extractor.test.ts +258 -0
  264. package/packages/core/src/__tests__/redis-cache.test.ts +307 -0
  265. package/packages/core/src/__tests__/rust-extractor.test.ts +252 -0
  266. package/packages/core/src/__tests__/streaming.test.ts +251 -0
  267. package/packages/core/src/additional-extractors.ts +333 -0
  268. package/packages/core/src/cache/cache-interface.ts +179 -0
  269. package/packages/core/src/cache/index.ts +210 -0
  270. package/packages/core/src/cache/redis-cache.ts +291 -0
  271. package/packages/core/src/cache/upstash-cache.ts +379 -0
  272. package/packages/core/src/cache.ts +251 -0
  273. package/packages/core/src/cli.ts +822 -0
  274. package/packages/core/src/code-extractor.ts +696 -0
  275. package/packages/core/src/docker-generator.ts +470 -0
  276. package/packages/core/src/edge-compatible.ts +491 -0
  277. package/packages/core/src/extractors/go-extractor.ts +791 -0
  278. package/packages/core/src/extractors/index.ts +9 -0
  279. package/packages/core/src/extractors/java-extractor.ts +937 -0
  280. package/packages/core/src/extractors/rust-extractor.ts +744 -0
  281. package/packages/core/src/github-client.ts +319 -0
  282. package/packages/core/src/go-generator.ts +356 -0
  283. package/packages/core/src/graphql-extractor.ts +358 -0
  284. package/packages/core/src/index.ts +797 -0
  285. package/packages/core/src/langchain-exporter.ts +617 -0
  286. package/packages/core/src/language-parsers.ts +1114 -0
  287. package/packages/core/src/mcp-introspector.ts +279 -0
  288. package/packages/core/src/monorepo-detector.ts +378 -0
  289. package/packages/core/src/plugins/index.ts +370 -0
  290. package/packages/core/src/plugins/registry.ts +404 -0
  291. package/packages/core/src/plugins/types.ts +215 -0
  292. package/packages/core/src/providers/base-provider.ts +246 -0
  293. package/packages/core/src/providers/bitbucket-client.ts +464 -0
  294. package/packages/core/src/providers/gitlab-client.ts +388 -0
  295. package/packages/core/src/providers/index.ts +176 -0
  296. package/packages/core/src/python-generator.ts +260 -0
  297. package/packages/core/src/queue/index.ts +100 -0
  298. package/packages/core/src/queue/memory-queue.ts +445 -0
  299. package/packages/core/src/queue/redis-queue.ts +578 -0
  300. package/packages/core/src/queue/types.ts +251 -0
  301. package/packages/core/src/readme-extractor.ts +409 -0
  302. package/packages/core/src/schema-generator.ts +638 -0
  303. package/packages/core/src/streaming.ts +999 -0
  304. package/packages/core/src/types.ts +289 -0
  305. package/packages/core/tsconfig.json +9 -0
  306. package/packages/core/tsup.config.ts +25 -0
  307. package/packages/mcp-server/README.md +297 -0
  308. package/packages/mcp-server/package.json +55 -0
  309. package/packages/mcp-server/src/__tests__/mcp-server.test.ts +177 -0
  310. package/packages/mcp-server/src/__tests__/tools.test.ts +217 -0
  311. package/packages/mcp-server/src/index.ts +1206 -0
  312. package/packages/mcp-server/src/prompts/index.ts +601 -0
  313. package/packages/mcp-server/src/tools/export-docker.ts +362 -0
  314. package/packages/mcp-server/src/tools/generate-openapi.ts +162 -0
  315. package/packages/mcp-server/src/tools/monitor-mcp-server.ts +448 -0
  316. package/packages/mcp-server/src/tools/stream-convert.ts +398 -0
  317. package/packages/mcp-server/src/tools/test-mcp-tool.ts +531 -0
  318. package/packages/mcp-server/tsconfig.json +12 -0
  319. package/packages/mcp-server/tsup.config.ts +14 -0
  320. package/packages/openapi-parser/package-lock.json +3028 -0
  321. package/packages/openapi-parser/package.json +41 -0
  322. package/packages/openapi-parser/src/analyzer.ts +700 -0
  323. package/packages/openapi-parser/src/asyncapi-parser.ts +475 -0
  324. package/packages/openapi-parser/src/cli.ts +302 -0
  325. package/packages/openapi-parser/src/generator.ts +570 -0
  326. package/packages/openapi-parser/src/generators/express-analyzer.ts +649 -0
  327. package/packages/openapi-parser/src/generators/fastapi-analyzer.ts +960 -0
  328. package/packages/openapi-parser/src/generators/index.ts +200 -0
  329. package/packages/openapi-parser/src/generators/nextjs-analyzer.ts +768 -0
  330. package/packages/openapi-parser/src/generators/openapi-builder.ts +527 -0
  331. package/packages/openapi-parser/src/generators/types.ts +298 -0
  332. package/packages/openapi-parser/src/graphql-parser.ts +462 -0
  333. package/packages/openapi-parser/src/grpc-parser.ts +649 -0
  334. package/packages/openapi-parser/src/har-parser.ts +723 -0
  335. package/packages/openapi-parser/src/index.ts +635 -0
  336. package/packages/openapi-parser/src/insomnia-parser.ts +614 -0
  337. package/packages/openapi-parser/src/parser.ts +231 -0
  338. package/packages/openapi-parser/src/postman-parser.ts +611 -0
  339. package/packages/openapi-parser/src/ref-resolver.ts +313 -0
  340. package/packages/openapi-parser/src/transformer.ts +459 -0
  341. package/packages/openapi-parser/tests/generators/express.test.ts +209 -0
  342. package/packages/openapi-parser/tests/generators/fastapi.test.ts +236 -0
  343. package/packages/openapi-parser/tests/generators/nextjs.test.ts +273 -0
  344. package/packages/openapi-parser/tests/parsers.test.ts +847 -0
  345. package/packages/openapi-parser/tsconfig.json +9 -0
  346. package/packages/openapi-parser/tsup.config.ts +11 -0
  347. package/packages/registry/package.json +59 -0
  348. package/packages/registry/src/cli.ts +456 -0
  349. package/packages/registry/src/index.ts +44 -0
  350. package/packages/registry/src/popular/github.json +47 -0
  351. package/packages/registry/src/popular/index.ts +55 -0
  352. package/packages/registry/src/popular/linear.json +42 -0
  353. package/packages/registry/src/popular/notion.json +42 -0
  354. package/packages/registry/src/popular/openai.json +40 -0
  355. package/packages/registry/src/popular/resend.json +38 -0
  356. package/packages/registry/src/popular/slack.json +42 -0
  357. package/packages/registry/src/popular/stripe.json +163 -0
  358. package/packages/registry/src/popular/supabase.json +42 -0
  359. package/packages/registry/src/popular/twilio.json +40 -0
  360. package/packages/registry/src/popular/vercel.json +40 -0
  361. package/packages/registry/src/registry.ts +492 -0
  362. package/packages/registry/src/storage.ts +334 -0
  363. package/packages/registry/src/types.ts +275 -0
  364. package/packages/registry/src/updater.ts +208 -0
  365. package/packages/registry/tsconfig.json +10 -0
  366. package/packages/registry/tsup.config.ts +11 -0
  367. package/pnpm-workspace.yaml +3 -0
  368. package/scripts/build-docs.sh +16 -0
  369. package/server.json +9 -0
  370. package/templates/Dockerfile.python.template +60 -0
  371. package/templates/Dockerfile.typescript.template +60 -0
  372. package/templates/docker-compose.template.yml +68 -0
  373. package/tests/fixtures/express-app/index.js +34 -0
  374. package/tests/fixtures/express-app/routes/posts.js +43 -0
  375. package/tests/fixtures/express-app/routes/users.js +58 -0
  376. package/tests/fixtures/fastapi-app/main.py +125 -0
  377. package/tests/fixtures/fastapi-app/routes/admin.py +42 -0
  378. package/tests/fixtures/graphql/simple-schema.graphql +65 -0
  379. package/tests/fixtures/mocks/github-api-responses.json +63 -0
  380. package/tests/fixtures/nextjs-app/app/api/posts/route.ts +55 -0
  381. package/tests/fixtures/nextjs-app/app/api/users/[id]/route.ts +63 -0
  382. package/tests/fixtures/nextjs-app/app/api/users/route.ts +44 -0
  383. package/tests/fixtures/nextjs-app/pages/api/health.ts +28 -0
  384. package/tests/fixtures/openapi/petstore.yaml +179 -0
  385. package/tests/integration/langchain-export.test.ts +405 -0
  386. package/tests/integration/openapi-conversion.test.ts +221 -0
  387. package/tsconfig.json +18 -0
  388. package/vitest.config.ts +32 -0
@@ -0,0 +1,410 @@
1
+ /**
2
+ * PlaygroundToolTester Component - Test individual tools with dynamic input forms
3
+ * @copyright 2024-2026 nirholas
4
+ * @license MIT
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import { useState, useCallback, useMemo } from 'react';
10
+ import { motion, AnimatePresence } from 'framer-motion';
11
+ import { Play, Loader2, AlertCircle, Check, Copy, ChevronDown, Clock } from 'lucide-react';
12
+ import { Button } from '@/components/ui/button';
13
+ import { Input } from '@/components/ui/input';
14
+ import type { Tool } from '@/types';
15
+ import { copyToClipboard } from '@/lib/utils';
16
+ import type { ExecuteToolResult } from '@/hooks/use-mcp-execution';
17
+
18
+ interface PlaygroundToolTesterProps {
19
+ tool: Tool;
20
+ /** Handler for executing the tool. If not provided, mock execution is used in demo mode */
21
+ onExecute?: (tool: Tool, params: Record<string, unknown>) => Promise<ExecuteToolResult | unknown>;
22
+ /** Whether we're in demo mode (not connected to real server) */
23
+ isDemoMode?: boolean;
24
+ /** Whether an execution is currently in progress */
25
+ isExecuting?: boolean;
26
+ className?: string;
27
+ }
28
+
29
+ type InputFieldType = 'string' | 'number' | 'boolean' | 'array' | 'object';
30
+
31
+ interface SchemaProperty {
32
+ type: string;
33
+ description?: string;
34
+ enum?: string[];
35
+ default?: unknown;
36
+ items?: { type: string };
37
+ }
38
+
39
+ export default function PlaygroundToolTester({
40
+ tool,
41
+ onExecute,
42
+ isDemoMode = false,
43
+ isExecuting: externalIsExecuting,
44
+ className = '',
45
+ }: PlaygroundToolTesterProps) {
46
+ const [params, setParams] = useState<Record<string, unknown>>({});
47
+ const [internalIsExecuting, setInternalIsExecuting] = useState(false);
48
+ const [result, setResult] = useState<unknown>(null);
49
+ const [error, setError] = useState<string | null>(null);
50
+ const [executionTime, setExecutionTime] = useState<number | null>(null);
51
+ const [copied, setCopied] = useState(false);
52
+ const [showSchema, setShowSchema] = useState(false);
53
+
54
+ // Use external executing state if provided, otherwise use internal
55
+ const isExecuting = externalIsExecuting ?? internalIsExecuting;
56
+
57
+ // Get properties from input schema
58
+ const properties = useMemo(() => {
59
+ return tool.inputSchema?.properties || {};
60
+ }, [tool]);
61
+
62
+ const requiredFields = useMemo(() => {
63
+ return new Set(tool.inputSchema?.required || []);
64
+ }, [tool]);
65
+
66
+ // Initialize default values
67
+ const initializeParams = useCallback(() => {
68
+ const initial: Record<string, unknown> = {};
69
+ Object.entries(properties).forEach(([key, prop]) => {
70
+ const property = prop as SchemaProperty;
71
+ if (property.default !== undefined) {
72
+ initial[key] = property.default;
73
+ } else if (property.enum && property.enum.length > 0) {
74
+ initial[key] = property.enum[0];
75
+ }
76
+ });
77
+ setParams(initial);
78
+ }, [properties]);
79
+
80
+ const handleParamChange = useCallback((key: string, value: unknown) => {
81
+ setParams(prev => ({ ...prev, [key]: value }));
82
+ }, []);
83
+
84
+ const handleExecute = useCallback(async () => {
85
+ setInternalIsExecuting(true);
86
+ setError(null);
87
+ setResult(null);
88
+ setExecutionTime(null);
89
+
90
+ const startTime = Date.now();
91
+
92
+ try {
93
+ if (!onExecute || isDemoMode) {
94
+ // Mock execution for demo mode
95
+ await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 400));
96
+
97
+ const mockResult = {
98
+ success: true,
99
+ tool: tool.name,
100
+ params: params,
101
+ response: {
102
+ message: `[Demo] Successfully executed ${tool.name}`,
103
+ timestamp: new Date().toISOString(),
104
+ data: Object.keys(params).length > 0 ? params : { example: 'result' },
105
+ },
106
+ };
107
+ setResult(mockResult);
108
+ setExecutionTime(Date.now() - startTime);
109
+ } else {
110
+ // Real execution via the provided handler
111
+ const response = await onExecute(tool, params);
112
+
113
+ // Handle ExecuteToolResult type
114
+ if (response && typeof response === 'object' && 'success' in response) {
115
+ const execResult = response as ExecuteToolResult;
116
+ if (!execResult.success && execResult.error) {
117
+ setError(execResult.error);
118
+ } else {
119
+ setResult(execResult.result ?? execResult);
120
+ }
121
+ setExecutionTime(execResult.executionTime || (Date.now() - startTime));
122
+ } else {
123
+ setResult(response);
124
+ setExecutionTime(Date.now() - startTime);
125
+ }
126
+ }
127
+ } catch (err) {
128
+ setError(err instanceof Error ? err.message : 'Execution failed');
129
+ setExecutionTime(Date.now() - startTime);
130
+ } finally {
131
+ setInternalIsExecuting(false);
132
+ }
133
+ }, [tool, params, onExecute, isDemoMode]);
134
+
135
+ const handleCopyResult = useCallback(async () => {
136
+ if (result) {
137
+ const success = await copyToClipboard(JSON.stringify(result, null, 2));
138
+ if (success) {
139
+ setCopied(true);
140
+ setTimeout(() => setCopied(false), 2000);
141
+ }
142
+ }
143
+ }, [result]);
144
+
145
+ const handleReset = useCallback(() => {
146
+ setParams({});
147
+ setResult(null);
148
+ setError(null);
149
+ setExecutionTime(null);
150
+ initializeParams();
151
+ }, [initializeParams]);
152
+
153
+ // Validate required fields
154
+ const isValid = useMemo(() => {
155
+ for (const field of requiredFields) {
156
+ const value = params[field];
157
+ if (value === undefined || value === '' || value === null) {
158
+ return false;
159
+ }
160
+ }
161
+ return true;
162
+ }, [params, requiredFields]);
163
+
164
+ const renderField = useCallback((key: string, prop: SchemaProperty) => {
165
+ const isRequired = requiredFields.has(key);
166
+ const value = params[key];
167
+
168
+ // Enum field - render as select
169
+ if (prop.enum && prop.enum.length > 0) {
170
+ return (
171
+ <div key={key} className="space-y-1.5">
172
+ <label className="text-sm font-medium text-neutral-300 flex items-center gap-1">
173
+ {key}
174
+ {isRequired && <span className="text-red-400">*</span>}
175
+ </label>
176
+ {prop.description && (
177
+ <p className="text-xs text-neutral-500">{prop.description}</p>
178
+ )}
179
+ <select
180
+ value={value as string || ''}
181
+ onChange={(e) => handleParamChange(key, e.target.value)}
182
+ className="w-full h-10 px-3 rounded-lg border border-neutral-700 bg-black text-white text-sm focus:outline-none focus:ring-2 focus:ring-white/10"
183
+ >
184
+ <option value="">Select...</option>
185
+ {prop.enum.map((opt) => (
186
+ <option key={opt} value={opt}>{opt}</option>
187
+ ))}
188
+ </select>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ // Boolean field - render as checkbox
194
+ if (prop.type === 'boolean') {
195
+ return (
196
+ <div key={key} className="space-y-1.5">
197
+ <label className="flex items-center gap-2 cursor-pointer">
198
+ <input
199
+ type="checkbox"
200
+ checked={value as boolean || false}
201
+ onChange={(e) => handleParamChange(key, e.target.checked)}
202
+ className="w-4 h-4 rounded border-neutral-700 bg-black text-white focus:ring-white/20"
203
+ />
204
+ <span className="text-sm font-medium text-neutral-300">
205
+ {key}
206
+ {isRequired && <span className="text-red-400 ml-1">*</span>}
207
+ </span>
208
+ </label>
209
+ {prop.description && (
210
+ <p className="text-xs text-neutral-500 ml-6">{prop.description}</p>
211
+ )}
212
+ </div>
213
+ );
214
+ }
215
+
216
+ // Number field
217
+ if (prop.type === 'number' || prop.type === 'integer') {
218
+ return (
219
+ <div key={key} className="space-y-1.5">
220
+ <label className="text-sm font-medium text-neutral-300 flex items-center gap-1">
221
+ {key}
222
+ {isRequired && <span className="text-red-400">*</span>}
223
+ </label>
224
+ {prop.description && (
225
+ <p className="text-xs text-neutral-500">{prop.description}</p>
226
+ )}
227
+ <Input
228
+ type="number"
229
+ value={value as number || ''}
230
+ onChange={(e) => handleParamChange(key, e.target.value ? Number(e.target.value) : undefined)}
231
+ placeholder={`Enter ${key}...`}
232
+ />
233
+ </div>
234
+ );
235
+ }
236
+
237
+ // Array or object field - render as textarea
238
+ if (prop.type === 'array' || prop.type === 'object') {
239
+ return (
240
+ <div key={key} className="space-y-1.5">
241
+ <label className="text-sm font-medium text-neutral-300 flex items-center gap-1">
242
+ {key}
243
+ {isRequired && <span className="text-red-400">*</span>}
244
+ <span className="text-xs text-neutral-500 font-normal">({prop.type})</span>
245
+ </label>
246
+ {prop.description && (
247
+ <p className="text-xs text-neutral-500">{prop.description}</p>
248
+ )}
249
+ <textarea
250
+ value={typeof value === 'object' ? JSON.stringify(value, null, 2) : (value as string || '')}
251
+ onChange={(e) => {
252
+ try {
253
+ const parsed = JSON.parse(e.target.value);
254
+ handleParamChange(key, parsed);
255
+ } catch {
256
+ handleParamChange(key, e.target.value);
257
+ }
258
+ }}
259
+ placeholder={`Enter ${prop.type === 'array' ? '["item1", "item2"]' : '{"key": "value"}'}`}
260
+ rows={3}
261
+ className="w-full px-3 py-2 rounded-lg border border-neutral-700 bg-black text-white text-sm font-mono focus:outline-none focus:ring-2 focus:ring-white/10 resize-y"
262
+ />
263
+ </div>
264
+ );
265
+ }
266
+
267
+ // Default string field
268
+ return (
269
+ <div key={key} className="space-y-1.5">
270
+ <label className="text-sm font-medium text-neutral-300 flex items-center gap-1">
271
+ {key}
272
+ {isRequired && <span className="text-red-400">*</span>}
273
+ </label>
274
+ {prop.description && (
275
+ <p className="text-xs text-neutral-500">{prop.description}</p>
276
+ )}
277
+ <Input
278
+ type="text"
279
+ value={value as string || ''}
280
+ onChange={(e) => handleParamChange(key, e.target.value)}
281
+ placeholder={`Enter ${key}...`}
282
+ />
283
+ </div>
284
+ );
285
+ }, [params, requiredFields, handleParamChange]);
286
+
287
+ return (
288
+ <div className={`rounded-xl border border-neutral-800 bg-neutral-900/50 overflow-hidden ${className}`}>
289
+ {/* Tool header */}
290
+ <div className="p-4 border-b border-neutral-800">
291
+ <div className="flex items-start justify-between gap-4">
292
+ <div>
293
+ <h3 className="font-semibold text-white">{tool.name}</h3>
294
+ <p className="text-sm text-neutral-400 mt-1">{tool.description}</p>
295
+ </div>
296
+ <button
297
+ onClick={() => setShowSchema(!showSchema)}
298
+ className="flex items-center gap-1 text-xs text-neutral-500 hover:text-white transition-colors"
299
+ >
300
+ Schema
301
+ <ChevronDown className={`w-3 h-3 transition-transform ${showSchema ? 'rotate-180' : ''}`} />
302
+ </button>
303
+ </div>
304
+
305
+ <AnimatePresence>
306
+ {showSchema && (
307
+ <motion.div
308
+ initial={{ height: 0, opacity: 0 }}
309
+ animate={{ height: 'auto', opacity: 1 }}
310
+ exit={{ height: 0, opacity: 0 }}
311
+ className="overflow-hidden"
312
+ >
313
+ <pre className="mt-3 p-3 bg-black/50 rounded-lg text-xs text-neutral-400 overflow-x-auto">
314
+ {JSON.stringify(tool.inputSchema, null, 2)}
315
+ </pre>
316
+ </motion.div>
317
+ )}
318
+ </AnimatePresence>
319
+ </div>
320
+
321
+ {/* Input form */}
322
+ <div className="p-4 space-y-4">
323
+ {Object.keys(properties).length === 0 ? (
324
+ <p className="text-sm text-neutral-500 italic">This tool has no input parameters</p>
325
+ ) : (
326
+ Object.entries(properties).map(([key, prop]) =>
327
+ renderField(key, prop as SchemaProperty)
328
+ )
329
+ )}
330
+ </div>
331
+
332
+ {/* Actions */}
333
+ <div className="p-4 border-t border-neutral-800 flex items-center gap-3">
334
+ <Button
335
+ onClick={handleExecute}
336
+ disabled={!isValid || isExecuting}
337
+ loading={isExecuting}
338
+ leftIcon={<Play className="w-4 h-4" />}
339
+ >
340
+ {isExecuting ? 'Executing...' : 'Execute'}
341
+ </Button>
342
+ <Button
343
+ variant="outline"
344
+ onClick={handleReset}
345
+ disabled={isExecuting}
346
+ >
347
+ Reset
348
+ </Button>
349
+ </div>
350
+
351
+ {/* Result/Error */}
352
+ <AnimatePresence>
353
+ {(result || error) && (
354
+ <motion.div
355
+ initial={{ height: 0, opacity: 0 }}
356
+ animate={{ height: 'auto', opacity: 1 }}
357
+ exit={{ height: 0, opacity: 0 }}
358
+ className="overflow-hidden"
359
+ >
360
+ <div className={`p-4 border-t ${error ? 'border-red-500/30 bg-red-500/10' : 'border-green-500/30 bg-green-500/10'}`}>
361
+ <div className="flex items-start justify-between gap-2 mb-2">
362
+ <div className="flex items-center gap-2 flex-wrap">
363
+ {error ? (
364
+ <>
365
+ <AlertCircle className="w-4 h-4 text-red-400" />
366
+ <span className="text-sm font-medium text-red-400">Error</span>
367
+ </>
368
+ ) : (
369
+ <>
370
+ <Check className="w-4 h-4 text-green-400" />
371
+ <span className="text-sm font-medium text-green-400">Success</span>
372
+ </>
373
+ )}
374
+ {executionTime !== null && (
375
+ <span className="flex items-center gap-1 text-xs text-neutral-500">
376
+ <Clock className="w-3 h-3" />
377
+ {executionTime}ms
378
+ </span>
379
+ )}
380
+ {isDemoMode && (
381
+ <span className="px-1.5 py-0.5 text-xs rounded bg-yellow-500/20 text-yellow-400">
382
+ Demo
383
+ </span>
384
+ )}
385
+ </div>
386
+ {result !== null && result !== undefined && (
387
+ <Button
388
+ variant="ghost"
389
+ size="sm"
390
+ onClick={handleCopyResult}
391
+ leftIcon={copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
392
+ >
393
+ {copied ? 'Copied!' : 'Copy'}
394
+ </Button>
395
+ )}
396
+ </div>
397
+ <pre className="p-3 bg-black/30 rounded-lg text-xs overflow-x-auto">
398
+ {error ? (
399
+ <span className="text-red-300">{error}</span>
400
+ ) : (
401
+ <span className="text-neutral-300">{JSON.stringify(result, null, 2)}</span>
402
+ )}
403
+ </pre>
404
+ </div>
405
+ </motion.div>
406
+ )}
407
+ </AnimatePresence>
408
+ </div>
409
+ );
410
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Product Cards Component - Landing page feature cards
3
+ * @copyright 2024-2026 nirholas
4
+ * @license MIT
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import { motion } from 'framer-motion';
10
+ import { ArrowRight, Github, Terminal, FileJson, BookOpen, Layers, Play, Sparkles, Cloud } from 'lucide-react';
11
+ import Link from 'next/link';
12
+
13
+ const PRODUCTS = [
14
+ {
15
+ id: 'convert',
16
+ icon: Github,
17
+ title: 'Convert Repository',
18
+ description: 'Transform any GitHub repo into a fully functional MCP server. Extracts tools from README, code, and API specs.',
19
+ href: '/convert',
20
+ badge: 'Popular',
21
+ badgeColor: 'bg-green-500/20 text-green-300',
22
+ features: ['Auto-detection', 'TypeScript & Python', 'Ready configs'],
23
+ },
24
+ {
25
+ id: 'playground',
26
+ icon: Play,
27
+ title: 'Interactive Playground',
28
+ description: 'Test your generated MCP tools in real-time. Execute tools, view results, and debug before deployment.',
29
+ href: '/playground',
30
+ badge: 'Try It',
31
+ badgeColor: 'bg-orange-500/20 text-orange-300',
32
+ features: ['Live testing', 'Real-time results', 'Debug mode'],
33
+ },
34
+ {
35
+ id: 'dashboard',
36
+ icon: Cloud,
37
+ title: 'Cloud Deploy',
38
+ description: 'One-click deploy your MCP server to the cloud. Get instant endpoints, usage analytics, and API management.',
39
+ href: '/dashboard',
40
+ badge: 'New',
41
+ badgeColor: 'bg-pink-500/20 text-pink-300',
42
+ features: ['Instant deploy', 'Usage dashboard', 'API keys'],
43
+ },
44
+ {
45
+ id: 'batch',
46
+ icon: Layers,
47
+ title: 'Batch Convert',
48
+ description: 'Convert multiple repositories at once. Perfect for teams or creating comprehensive tool collections.',
49
+ href: '/batch',
50
+ badge: 'New',
51
+ badgeColor: 'bg-purple-500/20 text-purple-300',
52
+ features: ['Multiple repos', 'Parallel processing', 'Bulk export'],
53
+ },
54
+ {
55
+ id: 'vscode',
56
+ icon: Sparkles,
57
+ title: 'VS Code Extension',
58
+ description: 'Convert repos directly from VS Code. Right-click any GitHub URL or use the command palette.',
59
+ href: 'https://marketplace.visualstudio.com/items?itemName=nirholas.github-to-mcp',
60
+ badge: 'Extension',
61
+ badgeColor: 'bg-blue-500/20 text-blue-300',
62
+ features: ['One-click convert', 'Inline results', 'Auto-config'],
63
+ external: true,
64
+ },
65
+ {
66
+ id: 'cli',
67
+ icon: Terminal,
68
+ title: 'CLI Tool',
69
+ description: 'Use the command-line tool for local development and CI/CD integration. Full control over the conversion process.',
70
+ href: 'https://github.com/nirholas/github-to-mcp#cli',
71
+ badge: 'Dev',
72
+ badgeColor: 'bg-cyan-500/20 text-cyan-300',
73
+ features: ['npm install', 'CI/CD ready', 'Batch processing'],
74
+ external: true,
75
+ },
76
+ {
77
+ id: 'api',
78
+ icon: FileJson,
79
+ title: 'REST API',
80
+ description: 'Programmatic access to the conversion engine. Build custom integrations and automated workflows.',
81
+ href: 'https://github.com/nirholas/github-to-mcp#api',
82
+ badge: null,
83
+ badgeColor: '',
84
+ features: ['JSON responses', 'Rate limited', 'OpenAPI spec'],
85
+ external: true,
86
+ },
87
+ {
88
+ id: 'docs',
89
+ icon: BookOpen,
90
+ title: 'Documentation',
91
+ description: 'Learn how to use github-to-mcp effectively. Guides, examples, and best practices.',
92
+ href: 'https://github.com/nirholas/github-to-mcp#readme',
93
+ badge: null,
94
+ badgeColor: '',
95
+ features: ['Getting started', 'Examples', 'FAQ'],
96
+ external: true,
97
+ },
98
+ ];
99
+
100
+ export default function ProductCards() {
101
+ return (
102
+ <section className="py-16">
103
+ <div className="text-center mb-12">
104
+ <motion.h2
105
+ initial={{ opacity: 0, y: 20 }}
106
+ whileInView={{ opacity: 1, y: 0 }}
107
+ viewport={{ once: true }}
108
+ className="text-3xl md:text-4xl font-bold text-white mb-4"
109
+ >
110
+ All Tools & Features
111
+ </motion.h2>
112
+ <motion.p
113
+ initial={{ opacity: 0, y: 20 }}
114
+ whileInView={{ opacity: 1, y: 0 }}
115
+ viewport={{ once: true }}
116
+ transition={{ delay: 0.1 }}
117
+ className="text-lg text-neutral-400 max-w-2xl mx-auto"
118
+ >
119
+ Convert GitHub repos to MCP servers, test interactively, and deploy
120
+ </motion.p>
121
+ </div>
122
+
123
+ <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
124
+ {PRODUCTS.map((product, index) => {
125
+ const CardWrapper = product.external ? 'a' : Link;
126
+ const cardProps = product.external
127
+ ? { href: product.href, target: '_blank', rel: 'noopener noreferrer' }
128
+ : { href: product.href };
129
+
130
+ return (
131
+ <motion.div
132
+ key={product.id}
133
+ initial={{ opacity: 0, y: 20 }}
134
+ whileInView={{ opacity: 1, y: 0 }}
135
+ viewport={{ once: true }}
136
+ transition={{ delay: index * 0.1 }}
137
+ >
138
+ <CardWrapper
139
+ {...cardProps}
140
+ className="group block h-full rounded-xl border border-neutral-800 p-6 bg-neutral-900/50 backdrop-blur-sm hover:border-neutral-600 hover:bg-neutral-900/80 transition-all"
141
+ >
142
+ <div className="flex items-start justify-between mb-4">
143
+ <div className="w-12 h-12 rounded-xl bg-white/5 border border-neutral-800 flex items-center justify-center group-hover:border-neutral-600 group-hover:bg-white/10 transition-all">
144
+ <product.icon className="w-6 h-6 text-white" />
145
+ </div>
146
+ {product.badge && (
147
+ <span className={`px-2.5 py-1 text-xs font-medium rounded-full ${product.badgeColor}`}>
148
+ {product.badge}
149
+ </span>
150
+ )}
151
+ </div>
152
+
153
+ <h3 className="text-xl font-semibold text-white mb-2 group-hover:text-white transition-colors flex items-center gap-2">
154
+ {product.title}
155
+ <ArrowRight className="w-4 h-4 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
156
+ </h3>
157
+
158
+ <p className="text-neutral-400 text-sm mb-4 leading-relaxed">
159
+ {product.description}
160
+ </p>
161
+
162
+ <div className="flex flex-wrap gap-2">
163
+ {product.features.map((feature) => (
164
+ <span
165
+ key={feature}
166
+ className="px-2 py-1 text-xs bg-white/5 border border-neutral-800 rounded text-neutral-400"
167
+ >
168
+ {feature}
169
+ </span>
170
+ ))}
171
+ </div>
172
+ </CardWrapper>
173
+ </motion.div>
174
+ );
175
+ })}
176
+ </div>
177
+ </section>
178
+ );
179
+ }