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.
- package/.env.example +8 -0
- package/.github/CODEOWNERS +6 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +7 -0
- package/.vscode/settings.json +4 -0
- package/ARCHITECTURE.md +1429 -0
- package/CHANGELOG.md +167 -0
- package/CONTRIBUTING.md +327 -0
- package/LICENSE +201 -0
- package/README.md +1028 -0
- package/SECURITY.md +248 -0
- package/VISUAL_GUIDE.md +437 -0
- package/apps/vscode/IMPLEMENTATION.md +480 -0
- package/apps/vscode/README.md +248 -0
- package/apps/vscode/package.json +381 -0
- package/apps/vscode/resources/icon.png +0 -0
- package/apps/vscode/resources/icon.svg +5 -0
- package/apps/vscode/src/commands/browseRegistry.ts +211 -0
- package/apps/vscode/src/commands/configureClaudeDesktop.ts +332 -0
- package/apps/vscode/src/commands/convert.ts +82 -0
- package/apps/vscode/src/commands/convertCurrentRepo.ts +109 -0
- package/apps/vscode/src/commands/convertFromUrl.ts +138 -0
- package/apps/vscode/src/commands/index.ts +121 -0
- package/apps/vscode/src/commands/validate.ts +197 -0
- package/apps/vscode/src/extension.ts +464 -0
- package/apps/vscode/src/global.d.ts +36 -0
- package/apps/vscode/src/test/extension.test.ts +73 -0
- package/apps/vscode/src/utils/file-generator.ts +529 -0
- package/apps/vscode/src/utils/github-api.ts +335 -0
- package/apps/vscode/src/utils/index.ts +29 -0
- package/apps/vscode/src/utils/mcp-config.ts +334 -0
- package/apps/vscode/src/utils/storage.ts +87 -0
- package/apps/vscode/src/views/McpServersTreeView.ts +160 -0
- package/apps/vscode/src/views/OutputChannelView.ts +195 -0
- package/apps/vscode/src/views/StatusBarItem.ts +251 -0
- package/apps/vscode/src/views/ToolsExplorerView.ts +314 -0
- package/apps/vscode/src/views/historyProvider.ts +75 -0
- package/apps/vscode/src/views/index.ts +12 -0
- package/apps/vscode/src/views/resultsPanel.ts +330 -0
- package/apps/vscode/src/webviews/ConversionPanel.ts +350 -0
- package/apps/vscode/src/webviews/ToolDetailsPanel.ts +448 -0
- package/apps/vscode/src/webviews/index.ts +9 -0
- package/apps/vscode/src/webviews/webview-ui/styles.ts +492 -0
- package/apps/vscode/tsconfig.json +20 -0
- package/apps/web/PLAYGROUND_GUIDE.md +499 -0
- package/apps/web/README.md +505 -0
- package/apps/web/app/api/convert/route.ts +100 -0
- package/apps/web/app/api/convert/stream/route.ts +198 -0
- package/apps/web/app/api/deploy/route.ts +157 -0
- package/apps/web/app/api/edge/route.ts +308 -0
- package/apps/web/app/api/export-docker/route.ts +284 -0
- package/apps/web/app/api/generate-openapi/route.ts +119 -0
- package/apps/web/app/api/mcp/[serverId]/route.ts +263 -0
- package/apps/web/app/api/playground/connect/route.ts +143 -0
- package/apps/web/app/api/playground/disconnect/route.ts +78 -0
- package/apps/web/app/api/playground/execute/route.ts +135 -0
- package/apps/web/app/api/playground/sessions/route.ts +103 -0
- package/apps/web/app/api/playground/tools/route.ts +117 -0
- package/apps/web/app/api/playground/v2/connect/route.ts +96 -0
- package/apps/web/app/api/playground/v2/disconnect/route.ts +88 -0
- package/apps/web/app/api/playground/v2/health/route.ts +80 -0
- package/apps/web/app/api/playground/v2/prompts/route.ts +160 -0
- package/apps/web/app/api/playground/v2/resources/route.ts +159 -0
- package/apps/web/app/api/playground/v2/sessions/route.ts +184 -0
- package/apps/web/app/api/playground/v2/tools/route.ts +167 -0
- package/apps/web/app/api/stream/route.ts +232 -0
- package/apps/web/app/batch/BatchConvertClient.tsx +190 -0
- package/apps/web/app/batch/page.tsx +37 -0
- package/apps/web/app/convert/page.tsx +269 -0
- package/apps/web/app/dashboard/page.tsx +380 -0
- package/apps/web/app/globals.css +622 -0
- package/apps/web/app/layout.tsx +120 -0
- package/apps/web/app/manifest.ts +31 -0
- package/apps/web/app/opengraph-image.tsx +112 -0
- package/apps/web/app/page.old.tsx +924 -0
- package/apps/web/app/page.tsx +77 -0
- package/apps/web/app/playground/page.tsx +306 -0
- package/apps/web/app/playground/v2/error.tsx +163 -0
- package/apps/web/app/playground/v2/layout.tsx +58 -0
- package/apps/web/app/playground/v2/loading.tsx +152 -0
- package/apps/web/app/playground/v2/page.tsx +644 -0
- package/apps/web/app/playground/v2/providers.tsx +214 -0
- package/apps/web/app/playground/v2/use-shortcuts.ts +209 -0
- package/apps/web/app/playground/v2/use-url-state.ts +296 -0
- package/apps/web/app/providers.tsx +22 -0
- package/apps/web/app/sitemap.ts +32 -0
- package/apps/web/app/twitter-image.tsx +112 -0
- package/apps/web/components/BranchSelector.tsx +401 -0
- package/apps/web/components/ClaudeConfigExport.tsx +226 -0
- package/apps/web/components/Features.tsx +84 -0
- package/apps/web/components/Footer.tsx +119 -0
- package/apps/web/components/GenerationProgress.tsx +248 -0
- package/apps/web/components/GithubUrlInput.tsx +483 -0
- package/apps/web/components/Header.tsx +175 -0
- package/apps/web/components/Hero.tsx +117 -0
- package/apps/web/components/HowItWorks.tsx +119 -0
- package/apps/web/components/InstallBanner.tsx +158 -0
- package/apps/web/components/Logo.tsx +116 -0
- package/apps/web/components/ParticleBackground.tsx +105 -0
- package/apps/web/components/Playground.tsx +472 -0
- package/apps/web/components/PlaygroundToolTester.tsx +410 -0
- package/apps/web/components/ProductCards.tsx +179 -0
- package/apps/web/components/SplitView.tsx +194 -0
- package/apps/web/components/ToolFilter.tsx +260 -0
- package/apps/web/components/ToolList.tsx +325 -0
- package/apps/web/components/batch/BatchConvert.tsx +785 -0
- package/apps/web/components/batch/index.ts +7 -0
- package/apps/web/components/convert/ConfigTabs.tsx +230 -0
- package/apps/web/components/convert/ConversionResult.tsx +482 -0
- package/apps/web/components/convert/InlinePlayground.tsx +259 -0
- package/apps/web/components/convert/LoadingSteps.tsx +311 -0
- package/apps/web/components/convert/OneClickInstall.tsx +224 -0
- package/apps/web/components/convert/ToolCard.tsx +189 -0
- package/apps/web/components/convert/TryInPlayground.tsx +242 -0
- package/apps/web/components/convert/index.ts +12 -0
- package/apps/web/components/deploy/DeployButton.tsx +369 -0
- package/apps/web/components/deploy/index.ts +7 -0
- package/apps/web/components/docker/DockerExport.tsx +690 -0
- package/apps/web/components/docker/index.ts +7 -0
- package/apps/web/components/install/OneClickInstall.tsx +676 -0
- package/apps/web/components/install/index.ts +7 -0
- package/apps/web/components/playground/CapabilityTabs.tsx +150 -0
- package/apps/web/components/playground/ConnectionStatusV2.tsx +322 -0
- package/apps/web/components/playground/EmptyStates.tsx +305 -0
- package/apps/web/components/playground/ExecutionLog.tsx +260 -0
- package/apps/web/components/playground/ExecutionLogV2.tsx +378 -0
- package/apps/web/components/playground/JsonViewer.tsx +388 -0
- package/apps/web/components/playground/PlaygroundLayout.tsx +244 -0
- package/apps/web/components/playground/PromptsPanel.tsx +385 -0
- package/apps/web/components/playground/ResourcesPanel.tsx +378 -0
- package/apps/web/components/playground/SchemaForm.tsx +477 -0
- package/apps/web/components/playground/ServerStatus.tsx +151 -0
- package/apps/web/components/playground/ShareButton.tsx +239 -0
- package/apps/web/components/playground/ToolsPanel.tsx +309 -0
- package/apps/web/components/playground/TransportConfigurator.tsx +563 -0
- package/apps/web/components/playground/index.ts +74 -0
- package/apps/web/components/playground/types.ts +202 -0
- package/apps/web/components/streaming/StreamingProgress.tsx +441 -0
- package/apps/web/components/streaming/index.ts +7 -0
- package/apps/web/components/ui/badge.tsx +42 -0
- package/apps/web/components/ui/button.tsx +88 -0
- package/apps/web/components/ui/card.tsx +75 -0
- package/apps/web/components/ui/code-block.tsx +122 -0
- package/apps/web/components/ui/index.ts +12 -0
- package/apps/web/components/ui/input.tsx +55 -0
- package/apps/web/components/ui/tabs.tsx +61 -0
- package/apps/web/hooks/index.ts +85 -0
- package/apps/web/hooks/types.ts +1173 -0
- package/apps/web/hooks/use-conversion.ts +133 -0
- package/apps/web/hooks/use-execution-history.ts +376 -0
- package/apps/web/hooks/use-generation-progress.ts +147 -0
- package/apps/web/hooks/use-local-storage.ts +88 -0
- package/apps/web/hooks/use-mcp-client.ts +623 -0
- package/apps/web/hooks/use-mcp-connection.ts +500 -0
- package/apps/web/hooks/use-mcp-execution.ts +282 -0
- package/apps/web/hooks/use-mcp-prompts.ts +441 -0
- package/apps/web/hooks/use-mcp-resources.ts +430 -0
- package/apps/web/hooks/use-mcp-tools.ts +540 -0
- package/apps/web/hooks/use-playground-store.ts +299 -0
- package/apps/web/hooks/use-playground.ts +184 -0
- package/apps/web/hooks/use-streaming-conversion.ts +227 -0
- package/apps/web/hooks/useBatchConversion.ts +271 -0
- package/apps/web/hooks/useDockerConfig.ts +161 -0
- package/apps/web/hooks/usePlatformDetection.ts +80 -0
- package/apps/web/hooks/useStreaming.ts +199 -0
- package/apps/web/lib/api/errors.ts +386 -0
- package/apps/web/lib/api/index.ts +137 -0
- package/apps/web/lib/api/logger.ts +187 -0
- package/apps/web/lib/api/middleware.ts +364 -0
- package/apps/web/lib/api/openapi.ts +977 -0
- package/apps/web/lib/api/session-manager.ts +594 -0
- package/apps/web/lib/api/types.ts +433 -0
- package/apps/web/lib/api/validation.ts +523 -0
- package/apps/web/lib/constants.ts +114 -0
- package/apps/web/lib/mcp/client.ts +1137 -0
- package/apps/web/lib/mcp/events.ts +651 -0
- package/apps/web/lib/mcp/index.ts +347 -0
- package/apps/web/lib/mcp/logger.ts +428 -0
- package/apps/web/lib/mcp/metrics.ts +703 -0
- package/apps/web/lib/mcp/retry.ts +616 -0
- package/apps/web/lib/mcp/session-manager.ts +779 -0
- package/apps/web/lib/mcp/transports.ts +988 -0
- package/apps/web/lib/mcp/types.ts +594 -0
- package/apps/web/lib/mcp-client-enhanced.ts +871 -0
- package/apps/web/lib/mcp-client.ts +778 -0
- package/apps/web/lib/mcp-errors.ts +489 -0
- package/apps/web/lib/mcp-sandbox.ts +593 -0
- package/apps/web/lib/mcp-testing.ts +428 -0
- package/apps/web/lib/mcp-types.ts +448 -0
- package/apps/web/lib/playground-store.tsx +1147 -0
- package/apps/web/lib/utils.ts +439 -0
- package/apps/web/next-env.d.ts +5 -0
- package/apps/web/next.config.js +23 -0
- package/apps/web/package.json +55 -0
- package/apps/web/postcss.config.js +6 -0
- package/apps/web/public/.well-known/ai-plugin.json +17 -0
- package/apps/web/public/logo.svg +6 -0
- package/apps/web/public/robots.txt +22 -0
- package/apps/web/public/schema.json +27 -0
- package/apps/web/tailwind.config.js +26 -0
- package/apps/web/tailwind.config.ts +123 -0
- package/apps/web/tsconfig.json +20 -0
- package/apps/web/types/deploy.ts +139 -0
- package/apps/web/types/index.ts +247 -0
- package/apps/web/vercel.json +39 -0
- package/eslint.config.mjs +23 -0
- package/llms.txt +102 -0
- package/mkdocs/docs/api/core.md +318 -0
- package/mkdocs/docs/api/index.md +128 -0
- package/mkdocs/docs/api/mcp-server.md +301 -0
- package/mkdocs/docs/api/openapi-parser.md +254 -0
- package/mkdocs/docs/assets/logo.svg +7 -0
- package/mkdocs/docs/changelog.md +118 -0
- package/mkdocs/docs/cli/generate.md +148 -0
- package/mkdocs/docs/cli/index.md +52 -0
- package/mkdocs/docs/cli/inspect.md +164 -0
- package/mkdocs/docs/cli/serve.md +136 -0
- package/mkdocs/docs/concepts/classification.md +254 -0
- package/mkdocs/docs/concepts/how-it-works.md +299 -0
- package/mkdocs/docs/concepts/index.md +77 -0
- package/mkdocs/docs/concepts/mcp-protocol.md +362 -0
- package/mkdocs/docs/concepts/tool-types.md +382 -0
- package/mkdocs/docs/contributing/architecture.md +262 -0
- package/mkdocs/docs/contributing/development.md +245 -0
- package/mkdocs/docs/contributing/index.md +73 -0
- package/mkdocs/docs/contributing/testing.md +320 -0
- package/mkdocs/docs/getting-started/configuration.md +235 -0
- package/mkdocs/docs/getting-started/index.md +54 -0
- package/mkdocs/docs/getting-started/installation.md +145 -0
- package/mkdocs/docs/getting-started/quickstart.md +160 -0
- package/mkdocs/docs/guides/batch.md +375 -0
- package/mkdocs/docs/guides/claude-desktop.md +227 -0
- package/mkdocs/docs/guides/cursor.md +188 -0
- package/mkdocs/docs/guides/custom-tools.md +367 -0
- package/mkdocs/docs/guides/index.md +78 -0
- package/mkdocs/docs/guides/private-repos.md +221 -0
- package/mkdocs/docs/guides/vscode.md +247 -0
- package/mkdocs/docs/index.md +175 -0
- package/mkdocs/docs/reference/config.md +223 -0
- package/mkdocs/docs/reference/env.md +192 -0
- package/mkdocs/docs/reference/index.md +102 -0
- package/mkdocs/docs/reference/tools.md +309 -0
- package/mkdocs/docs/stylesheets/extra.css +231 -0
- package/mkdocs/mkdocs.yml +204 -0
- package/mkdocs/overrides/.gitkeep +1 -0
- package/mkdocs/overrides/main.html +7 -0
- package/mkdocs/python-deps.txt +7 -0
- package/mkdocs/vercel.json +11 -0
- package/package.json +63 -0
- package/packages/core/package.json +61 -0
- package/packages/core/src/__tests__/bitbucket-client.test.ts +366 -0
- package/packages/core/src/__tests__/cli.test.ts +235 -0
- package/packages/core/src/__tests__/code-extractor.test.ts +378 -0
- package/packages/core/src/__tests__/docker-generator.test.ts +255 -0
- package/packages/core/src/__tests__/github-client.test.ts +390 -0
- package/packages/core/src/__tests__/gitlab-client.test.ts +319 -0
- package/packages/core/src/__tests__/go-extractor.test.ts +351 -0
- package/packages/core/src/__tests__/graphql-extractor.test.ts +330 -0
- package/packages/core/src/__tests__/java-extractor.test.ts +497 -0
- package/packages/core/src/__tests__/plugins.test.ts +467 -0
- package/packages/core/src/__tests__/readme-extractor.test.ts +258 -0
- package/packages/core/src/__tests__/redis-cache.test.ts +307 -0
- package/packages/core/src/__tests__/rust-extractor.test.ts +252 -0
- package/packages/core/src/__tests__/streaming.test.ts +251 -0
- package/packages/core/src/additional-extractors.ts +333 -0
- package/packages/core/src/cache/cache-interface.ts +179 -0
- package/packages/core/src/cache/index.ts +210 -0
- package/packages/core/src/cache/redis-cache.ts +291 -0
- package/packages/core/src/cache/upstash-cache.ts +379 -0
- package/packages/core/src/cache.ts +251 -0
- package/packages/core/src/cli.ts +822 -0
- package/packages/core/src/code-extractor.ts +696 -0
- package/packages/core/src/docker-generator.ts +470 -0
- package/packages/core/src/edge-compatible.ts +491 -0
- package/packages/core/src/extractors/go-extractor.ts +791 -0
- package/packages/core/src/extractors/index.ts +9 -0
- package/packages/core/src/extractors/java-extractor.ts +937 -0
- package/packages/core/src/extractors/rust-extractor.ts +744 -0
- package/packages/core/src/github-client.ts +319 -0
- package/packages/core/src/go-generator.ts +356 -0
- package/packages/core/src/graphql-extractor.ts +358 -0
- package/packages/core/src/index.ts +797 -0
- package/packages/core/src/langchain-exporter.ts +617 -0
- package/packages/core/src/language-parsers.ts +1114 -0
- package/packages/core/src/mcp-introspector.ts +279 -0
- package/packages/core/src/monorepo-detector.ts +378 -0
- package/packages/core/src/plugins/index.ts +370 -0
- package/packages/core/src/plugins/registry.ts +404 -0
- package/packages/core/src/plugins/types.ts +215 -0
- package/packages/core/src/providers/base-provider.ts +246 -0
- package/packages/core/src/providers/bitbucket-client.ts +464 -0
- package/packages/core/src/providers/gitlab-client.ts +388 -0
- package/packages/core/src/providers/index.ts +176 -0
- package/packages/core/src/python-generator.ts +260 -0
- package/packages/core/src/queue/index.ts +100 -0
- package/packages/core/src/queue/memory-queue.ts +445 -0
- package/packages/core/src/queue/redis-queue.ts +578 -0
- package/packages/core/src/queue/types.ts +251 -0
- package/packages/core/src/readme-extractor.ts +409 -0
- package/packages/core/src/schema-generator.ts +638 -0
- package/packages/core/src/streaming.ts +999 -0
- package/packages/core/src/types.ts +289 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/core/tsup.config.ts +25 -0
- package/packages/mcp-server/README.md +297 -0
- package/packages/mcp-server/package.json +55 -0
- package/packages/mcp-server/src/__tests__/mcp-server.test.ts +177 -0
- package/packages/mcp-server/src/__tests__/tools.test.ts +217 -0
- package/packages/mcp-server/src/index.ts +1206 -0
- package/packages/mcp-server/src/prompts/index.ts +601 -0
- package/packages/mcp-server/src/tools/export-docker.ts +362 -0
- package/packages/mcp-server/src/tools/generate-openapi.ts +162 -0
- package/packages/mcp-server/src/tools/monitor-mcp-server.ts +448 -0
- package/packages/mcp-server/src/tools/stream-convert.ts +398 -0
- package/packages/mcp-server/src/tools/test-mcp-tool.ts +531 -0
- package/packages/mcp-server/tsconfig.json +12 -0
- package/packages/mcp-server/tsup.config.ts +14 -0
- package/packages/openapi-parser/package-lock.json +3028 -0
- package/packages/openapi-parser/package.json +41 -0
- package/packages/openapi-parser/src/analyzer.ts +700 -0
- package/packages/openapi-parser/src/asyncapi-parser.ts +475 -0
- package/packages/openapi-parser/src/cli.ts +302 -0
- package/packages/openapi-parser/src/generator.ts +570 -0
- package/packages/openapi-parser/src/generators/express-analyzer.ts +649 -0
- package/packages/openapi-parser/src/generators/fastapi-analyzer.ts +960 -0
- package/packages/openapi-parser/src/generators/index.ts +200 -0
- package/packages/openapi-parser/src/generators/nextjs-analyzer.ts +768 -0
- package/packages/openapi-parser/src/generators/openapi-builder.ts +527 -0
- package/packages/openapi-parser/src/generators/types.ts +298 -0
- package/packages/openapi-parser/src/graphql-parser.ts +462 -0
- package/packages/openapi-parser/src/grpc-parser.ts +649 -0
- package/packages/openapi-parser/src/har-parser.ts +723 -0
- package/packages/openapi-parser/src/index.ts +635 -0
- package/packages/openapi-parser/src/insomnia-parser.ts +614 -0
- package/packages/openapi-parser/src/parser.ts +231 -0
- package/packages/openapi-parser/src/postman-parser.ts +611 -0
- package/packages/openapi-parser/src/ref-resolver.ts +313 -0
- package/packages/openapi-parser/src/transformer.ts +459 -0
- package/packages/openapi-parser/tests/generators/express.test.ts +209 -0
- package/packages/openapi-parser/tests/generators/fastapi.test.ts +236 -0
- package/packages/openapi-parser/tests/generators/nextjs.test.ts +273 -0
- package/packages/openapi-parser/tests/parsers.test.ts +847 -0
- package/packages/openapi-parser/tsconfig.json +9 -0
- package/packages/openapi-parser/tsup.config.ts +11 -0
- package/packages/registry/package.json +59 -0
- package/packages/registry/src/cli.ts +456 -0
- package/packages/registry/src/index.ts +44 -0
- package/packages/registry/src/popular/github.json +47 -0
- package/packages/registry/src/popular/index.ts +55 -0
- package/packages/registry/src/popular/linear.json +42 -0
- package/packages/registry/src/popular/notion.json +42 -0
- package/packages/registry/src/popular/openai.json +40 -0
- package/packages/registry/src/popular/resend.json +38 -0
- package/packages/registry/src/popular/slack.json +42 -0
- package/packages/registry/src/popular/stripe.json +163 -0
- package/packages/registry/src/popular/supabase.json +42 -0
- package/packages/registry/src/popular/twilio.json +40 -0
- package/packages/registry/src/popular/vercel.json +40 -0
- package/packages/registry/src/registry.ts +492 -0
- package/packages/registry/src/storage.ts +334 -0
- package/packages/registry/src/types.ts +275 -0
- package/packages/registry/src/updater.ts +208 -0
- package/packages/registry/tsconfig.json +10 -0
- package/packages/registry/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +3 -0
- package/scripts/build-docs.sh +16 -0
- package/server.json +9 -0
- package/templates/Dockerfile.python.template +60 -0
- package/templates/Dockerfile.typescript.template +60 -0
- package/templates/docker-compose.template.yml +68 -0
- package/tests/fixtures/express-app/index.js +34 -0
- package/tests/fixtures/express-app/routes/posts.js +43 -0
- package/tests/fixtures/express-app/routes/users.js +58 -0
- package/tests/fixtures/fastapi-app/main.py +125 -0
- package/tests/fixtures/fastapi-app/routes/admin.py +42 -0
- package/tests/fixtures/graphql/simple-schema.graphql +65 -0
- package/tests/fixtures/mocks/github-api-responses.json +63 -0
- package/tests/fixtures/nextjs-app/app/api/posts/route.ts +55 -0
- package/tests/fixtures/nextjs-app/app/api/users/[id]/route.ts +63 -0
- package/tests/fixtures/nextjs-app/app/api/users/route.ts +44 -0
- package/tests/fixtures/nextjs-app/pages/api/health.ts +28 -0
- package/tests/fixtures/openapi/petstore.yaml +179 -0
- package/tests/integration/langchain-export.test.ts +405 -0
- package/tests/integration/openapi-conversion.test.ts +221 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +32 -0
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playground Store - Shared state management for playground integration
|
|
3
|
+
* @copyright 2024-2026 nirholas
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { createContext, useContext, useCallback, useReducer, useEffect, ReactNode } from 'react';
|
|
10
|
+
import type { Tool, ConversionResult } from '@/types';
|
|
11
|
+
import { safeJsonParse } from '@/lib/utils';
|
|
12
|
+
|
|
13
|
+
// ===== Types =====
|
|
14
|
+
|
|
15
|
+
export interface PlaygroundState {
|
|
16
|
+
/** Generated TypeScript MCP server code */
|
|
17
|
+
generatedCode: string | null;
|
|
18
|
+
/** Generated Python MCP server code */
|
|
19
|
+
generatedPythonCode: string | null;
|
|
20
|
+
/** Extracted tools from conversion */
|
|
21
|
+
tools: Tool[];
|
|
22
|
+
/** Repository name */
|
|
23
|
+
repoName: string | null;
|
|
24
|
+
/** Repository URL */
|
|
25
|
+
repoUrl: string | null;
|
|
26
|
+
/** Session ID for server connection */
|
|
27
|
+
sessionId: string | null;
|
|
28
|
+
/** Last conversion timestamp */
|
|
29
|
+
lastConversion: Date | null;
|
|
30
|
+
/** Full conversion result for reference */
|
|
31
|
+
conversionResult: ConversionResult | null;
|
|
32
|
+
/** Error state */
|
|
33
|
+
error: PlaygroundError | null;
|
|
34
|
+
/** Loading state */
|
|
35
|
+
isLoading: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PlaygroundError {
|
|
39
|
+
type: 'syntax' | 'server' | 'execution' | 'network' | 'unknown';
|
|
40
|
+
message: string;
|
|
41
|
+
details?: string;
|
|
42
|
+
recoverable: boolean;
|
|
43
|
+
retryCount?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// API interfaces for tool execution
|
|
47
|
+
export interface ExecuteToolRequest {
|
|
48
|
+
generatedCode: string;
|
|
49
|
+
toolName: string;
|
|
50
|
+
toolParams: Record<string, unknown>;
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ExecuteToolResponse {
|
|
55
|
+
success: boolean;
|
|
56
|
+
result?: unknown;
|
|
57
|
+
error?: string;
|
|
58
|
+
sessionId: string;
|
|
59
|
+
executionTime: number;
|
|
60
|
+
logs?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type PlaygroundAction =
|
|
64
|
+
| { type: 'SET_CONVERSION_RESULT'; payload: ConversionResult }
|
|
65
|
+
| { type: 'SET_CODE'; payload: { typescript: string; python?: string } }
|
|
66
|
+
| { type: 'SET_TOOLS'; payload: Tool[] }
|
|
67
|
+
| { type: 'SET_SESSION_ID'; payload: string }
|
|
68
|
+
| { type: 'SET_ERROR'; payload: PlaygroundError | null }
|
|
69
|
+
| { type: 'SET_LOADING'; payload: boolean }
|
|
70
|
+
| { type: 'CLEAR_STATE' }
|
|
71
|
+
| { type: 'LOAD_FROM_STORAGE'; payload: Partial<PlaygroundState> }
|
|
72
|
+
| { type: 'INCREMENT_RETRY' };
|
|
73
|
+
|
|
74
|
+
interface PlaygroundContextValue {
|
|
75
|
+
state: PlaygroundState;
|
|
76
|
+
dispatch: React.Dispatch<PlaygroundAction>;
|
|
77
|
+
|
|
78
|
+
// Convenience methods
|
|
79
|
+
setConversionResult: (result: ConversionResult) => void;
|
|
80
|
+
setCode: (typescript: string, python?: string) => void;
|
|
81
|
+
setError: (error: PlaygroundError | null) => void;
|
|
82
|
+
clearState: () => void;
|
|
83
|
+
|
|
84
|
+
// URL helpers
|
|
85
|
+
generateShareableLink: () => string;
|
|
86
|
+
loadFromUrl: (params: URLSearchParams) => Promise<void>;
|
|
87
|
+
|
|
88
|
+
// Gist helpers
|
|
89
|
+
loadFromGist: (gistId: string) => Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ===== Initial State =====
|
|
93
|
+
|
|
94
|
+
const initialState: PlaygroundState = {
|
|
95
|
+
generatedCode: null,
|
|
96
|
+
generatedPythonCode: null,
|
|
97
|
+
tools: [],
|
|
98
|
+
repoName: null,
|
|
99
|
+
repoUrl: null,
|
|
100
|
+
sessionId: null,
|
|
101
|
+
lastConversion: null,
|
|
102
|
+
conversionResult: null,
|
|
103
|
+
error: null,
|
|
104
|
+
isLoading: false,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// ===== Storage Keys =====
|
|
108
|
+
|
|
109
|
+
const STORAGE_KEY = 'playground-state';
|
|
110
|
+
const SESSION_KEY = 'playground-session';
|
|
111
|
+
|
|
112
|
+
// ===== Reducer =====
|
|
113
|
+
|
|
114
|
+
function playgroundReducer(state: PlaygroundState, action: PlaygroundAction): PlaygroundState {
|
|
115
|
+
switch (action.type) {
|
|
116
|
+
case 'SET_CONVERSION_RESULT': {
|
|
117
|
+
const result = action.payload;
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
generatedCode: result.code,
|
|
121
|
+
generatedPythonCode: result.pythonCode || null,
|
|
122
|
+
tools: result.tools,
|
|
123
|
+
repoName: result.name,
|
|
124
|
+
repoUrl: result.repository?.url || null,
|
|
125
|
+
lastConversion: new Date(),
|
|
126
|
+
conversionResult: result,
|
|
127
|
+
error: null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'SET_CODE':
|
|
132
|
+
return {
|
|
133
|
+
...state,
|
|
134
|
+
generatedCode: action.payload.typescript,
|
|
135
|
+
generatedPythonCode: action.payload.python || null,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
case 'SET_TOOLS':
|
|
139
|
+
return {
|
|
140
|
+
...state,
|
|
141
|
+
tools: action.payload,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
case 'SET_SESSION_ID':
|
|
145
|
+
return {
|
|
146
|
+
...state,
|
|
147
|
+
sessionId: action.payload,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
case 'SET_ERROR':
|
|
151
|
+
return {
|
|
152
|
+
...state,
|
|
153
|
+
error: action.payload,
|
|
154
|
+
isLoading: false,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
case 'SET_LOADING':
|
|
158
|
+
return {
|
|
159
|
+
...state,
|
|
160
|
+
isLoading: action.payload,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
case 'CLEAR_STATE':
|
|
164
|
+
return {
|
|
165
|
+
...initialState,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
case 'LOAD_FROM_STORAGE':
|
|
169
|
+
return {
|
|
170
|
+
...state,
|
|
171
|
+
...action.payload,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
case 'INCREMENT_RETRY':
|
|
175
|
+
return {
|
|
176
|
+
...state,
|
|
177
|
+
error: state.error
|
|
178
|
+
? { ...state.error, retryCount: (state.error.retryCount || 0) + 1 }
|
|
179
|
+
: null,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
default:
|
|
183
|
+
return state;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ===== Context =====
|
|
188
|
+
|
|
189
|
+
const PlaygroundContext = createContext<PlaygroundContextValue | null>(null);
|
|
190
|
+
|
|
191
|
+
// ===== Provider =====
|
|
192
|
+
|
|
193
|
+
interface PlaygroundProviderProps {
|
|
194
|
+
children: ReactNode;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function PlaygroundProvider({ children }: PlaygroundProviderProps) {
|
|
198
|
+
const [state, dispatch] = useReducer(playgroundReducer, initialState);
|
|
199
|
+
|
|
200
|
+
// Load from localStorage on mount
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (typeof window === 'undefined') return;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
206
|
+
if (stored) {
|
|
207
|
+
const parsed = safeJsonParse<Partial<PlaygroundState>>(stored, {});
|
|
208
|
+
if (parsed.lastConversion) {
|
|
209
|
+
parsed.lastConversion = new Date(parsed.lastConversion);
|
|
210
|
+
}
|
|
211
|
+
dispatch({ type: 'LOAD_FROM_STORAGE', payload: parsed });
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.warn('Failed to load playground state from storage:', error);
|
|
215
|
+
}
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
// Persist to localStorage on state change
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (typeof window === 'undefined') return;
|
|
221
|
+
if (!state.generatedCode && !state.tools.length) return;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const toStore = {
|
|
225
|
+
generatedCode: state.generatedCode,
|
|
226
|
+
generatedPythonCode: state.generatedPythonCode,
|
|
227
|
+
tools: state.tools,
|
|
228
|
+
repoName: state.repoName,
|
|
229
|
+
repoUrl: state.repoUrl,
|
|
230
|
+
lastConversion: state.lastConversion?.toISOString(),
|
|
231
|
+
conversionResult: state.conversionResult,
|
|
232
|
+
};
|
|
233
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn('Failed to persist playground state:', error);
|
|
236
|
+
}
|
|
237
|
+
}, [state.generatedCode, state.generatedPythonCode, state.tools, state.repoName, state.repoUrl, state.lastConversion, state.conversionResult]);
|
|
238
|
+
|
|
239
|
+
// Convenience methods
|
|
240
|
+
const setConversionResult = useCallback((result: ConversionResult) => {
|
|
241
|
+
dispatch({ type: 'SET_CONVERSION_RESULT', payload: result });
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
const setCode = useCallback((typescript: string, python?: string) => {
|
|
245
|
+
dispatch({ type: 'SET_CODE', payload: { typescript, python } });
|
|
246
|
+
}, []);
|
|
247
|
+
|
|
248
|
+
const setError = useCallback((error: PlaygroundError | null) => {
|
|
249
|
+
dispatch({ type: 'SET_ERROR', payload: error });
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
252
|
+
const clearState = useCallback(() => {
|
|
253
|
+
dispatch({ type: 'CLEAR_STATE' });
|
|
254
|
+
if (typeof window !== 'undefined') {
|
|
255
|
+
window.localStorage.removeItem(STORAGE_KEY);
|
|
256
|
+
window.localStorage.removeItem(SESSION_KEY);
|
|
257
|
+
}
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
260
|
+
// Generate a shareable link with base64-encoded code
|
|
261
|
+
const generateShareableLink = useCallback(() => {
|
|
262
|
+
if (typeof window === 'undefined' || !state.generatedCode) {
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const baseUrl = window.location.origin;
|
|
267
|
+
|
|
268
|
+
// For large code, we truncate and suggest using gist
|
|
269
|
+
if (state.generatedCode.length > 10000) {
|
|
270
|
+
console.warn('Code too large for URL encoding. Consider using a Gist.');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const encoded = btoa(encodeURIComponent(state.generatedCode));
|
|
275
|
+
const params = new URLSearchParams();
|
|
276
|
+
params.set('code', encoded);
|
|
277
|
+
|
|
278
|
+
if (state.repoName) {
|
|
279
|
+
params.set('name', state.repoName);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return `${baseUrl}/playground?${params.toString()}`;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('Failed to generate shareable link:', error);
|
|
285
|
+
return '';
|
|
286
|
+
}
|
|
287
|
+
}, [state.generatedCode, state.repoName]);
|
|
288
|
+
|
|
289
|
+
// Load code from URL parameters
|
|
290
|
+
const loadFromUrl = useCallback(async (params: URLSearchParams) => {
|
|
291
|
+
dispatch({ type: 'SET_LOADING', payload: true });
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// Check for base64-encoded code
|
|
295
|
+
const encodedCode = params.get('code');
|
|
296
|
+
if (encodedCode) {
|
|
297
|
+
// Validate base64 encoding
|
|
298
|
+
if (!isValidBase64(encodedCode)) {
|
|
299
|
+
dispatch({
|
|
300
|
+
type: 'SET_ERROR',
|
|
301
|
+
payload: {
|
|
302
|
+
type: 'syntax',
|
|
303
|
+
message: 'Invalid code parameter',
|
|
304
|
+
details: 'The code parameter is not valid base64 encoding',
|
|
305
|
+
recoverable: false,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const decoded = decodeURIComponent(atob(encodedCode));
|
|
313
|
+
|
|
314
|
+
// Validate the decoded content looks like code
|
|
315
|
+
if (!decoded.trim() || decoded.length < 10) {
|
|
316
|
+
dispatch({
|
|
317
|
+
type: 'SET_ERROR',
|
|
318
|
+
payload: {
|
|
319
|
+
type: 'syntax',
|
|
320
|
+
message: 'Invalid code content',
|
|
321
|
+
details: 'The shared code appears to be empty or too short',
|
|
322
|
+
recoverable: false,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const name = params.get('name') || 'Shared Code';
|
|
329
|
+
|
|
330
|
+
dispatch({
|
|
331
|
+
type: 'SET_CODE',
|
|
332
|
+
payload: { typescript: decoded },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Try to extract tools from the code
|
|
336
|
+
const extractedTools = extractToolsFromCode(decoded);
|
|
337
|
+
if (extractedTools.length > 0) {
|
|
338
|
+
dispatch({ type: 'SET_TOOLS', payload: extractedTools });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
342
|
+
return;
|
|
343
|
+
} catch (decodeError) {
|
|
344
|
+
dispatch({
|
|
345
|
+
type: 'SET_ERROR',
|
|
346
|
+
payload: {
|
|
347
|
+
type: 'syntax',
|
|
348
|
+
message: 'Failed to decode shared code',
|
|
349
|
+
details: decodeError instanceof Error ? decodeError.message : 'Invalid encoding',
|
|
350
|
+
recoverable: false,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Check for Gist ID
|
|
358
|
+
const gistId = params.get('gist');
|
|
359
|
+
if (gistId) {
|
|
360
|
+
// Validate Gist ID format (alphanumeric, 20-32 chars typically)
|
|
361
|
+
if (!isValidGistId(gistId)) {
|
|
362
|
+
dispatch({
|
|
363
|
+
type: 'SET_ERROR',
|
|
364
|
+
payload: {
|
|
365
|
+
type: 'syntax',
|
|
366
|
+
message: 'Invalid Gist ID',
|
|
367
|
+
details: 'The Gist ID format is invalid. It should be alphanumeric.',
|
|
368
|
+
recoverable: false,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await loadFromGistInternal(gistId, dispatch);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
381
|
+
dispatch({
|
|
382
|
+
type: 'SET_ERROR',
|
|
383
|
+
payload: {
|
|
384
|
+
type: 'network',
|
|
385
|
+
message: 'Failed to load code from URL',
|
|
386
|
+
details: errorMessage,
|
|
387
|
+
recoverable: true,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}, []);
|
|
392
|
+
|
|
393
|
+
// Load code from GitHub Gist
|
|
394
|
+
const loadFromGist = useCallback(async (gistId: string) => {
|
|
395
|
+
// Validate Gist ID before making API call
|
|
396
|
+
if (!isValidGistId(gistId)) {
|
|
397
|
+
dispatch({
|
|
398
|
+
type: 'SET_ERROR',
|
|
399
|
+
payload: {
|
|
400
|
+
type: 'syntax',
|
|
401
|
+
message: 'Invalid Gist ID',
|
|
402
|
+
details: 'The Gist ID format is invalid. It should be alphanumeric.',
|
|
403
|
+
recoverable: false,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
await loadFromGistInternal(gistId, dispatch);
|
|
409
|
+
}, []);
|
|
410
|
+
|
|
411
|
+
const value: PlaygroundContextValue = {
|
|
412
|
+
state,
|
|
413
|
+
dispatch,
|
|
414
|
+
setConversionResult,
|
|
415
|
+
setCode,
|
|
416
|
+
setError,
|
|
417
|
+
clearState,
|
|
418
|
+
generateShareableLink,
|
|
419
|
+
loadFromUrl,
|
|
420
|
+
loadFromGist,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<PlaygroundContext.Provider value={value}>
|
|
425
|
+
{children}
|
|
426
|
+
</PlaygroundContext.Provider>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ===== Hook =====
|
|
431
|
+
|
|
432
|
+
export function usePlaygroundStore(): PlaygroundContextValue {
|
|
433
|
+
const context = useContext(PlaygroundContext);
|
|
434
|
+
if (!context) {
|
|
435
|
+
throw new Error('usePlaygroundStore must be used within a PlaygroundProvider');
|
|
436
|
+
}
|
|
437
|
+
return context;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ===== Validation Helpers =====
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Validate base64 encoding
|
|
444
|
+
*/
|
|
445
|
+
function isValidBase64(str: string): boolean {
|
|
446
|
+
if (!str || str.length === 0) return false;
|
|
447
|
+
|
|
448
|
+
// Check for valid base64 characters
|
|
449
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
450
|
+
if (!base64Regex.test(str)) return false;
|
|
451
|
+
|
|
452
|
+
// Try to decode to verify
|
|
453
|
+
try {
|
|
454
|
+
atob(str);
|
|
455
|
+
return true;
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Validate GitHub Gist ID format
|
|
463
|
+
* Gist IDs are alphanumeric strings, typically 20-32 characters
|
|
464
|
+
*/
|
|
465
|
+
function isValidGistId(gistId: string): boolean {
|
|
466
|
+
if (!gistId || typeof gistId !== 'string') return false;
|
|
467
|
+
|
|
468
|
+
// Gist IDs are alphanumeric, typically 20-32 chars (but can vary)
|
|
469
|
+
// They don't contain special characters
|
|
470
|
+
const gistIdRegex = /^[a-f0-9]{20,40}$/i;
|
|
471
|
+
return gistIdRegex.test(gistId);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Validate GitHub URL format
|
|
476
|
+
*/
|
|
477
|
+
export function isValidGitHubUrl(url: string): boolean {
|
|
478
|
+
if (!url || typeof url !== 'string') return false;
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const parsed = new URL(url.startsWith('http') ? url : `https://${url}`);
|
|
482
|
+
const isGithub = parsed.hostname === 'github.com' || parsed.hostname === 'www.github.com';
|
|
483
|
+
if (!isGithub) return false;
|
|
484
|
+
|
|
485
|
+
// Check for owner/repo pattern
|
|
486
|
+
const pathParts = parsed.pathname.split('/').filter(Boolean);
|
|
487
|
+
return pathParts.length >= 2;
|
|
488
|
+
} catch {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Parse GitHub URL to extract owner and repo
|
|
495
|
+
*/
|
|
496
|
+
export function parseGitHubUrl(url: string): { owner: string; repo: string } | null {
|
|
497
|
+
if (!isValidGitHubUrl(url)) return null;
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const parsed = new URL(url.startsWith('http') ? url : `https://${url}`);
|
|
501
|
+
const pathParts = parsed.pathname.split('/').filter(Boolean);
|
|
502
|
+
if (pathParts.length >= 2) {
|
|
503
|
+
return {
|
|
504
|
+
owner: pathParts[0],
|
|
505
|
+
repo: pathParts[1].replace(/\.git$/, ''),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
// ignore
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ===== Helper Functions =====
|
|
515
|
+
|
|
516
|
+
async function loadFromGistInternal(
|
|
517
|
+
gistId: string,
|
|
518
|
+
dispatch: React.Dispatch<PlaygroundAction>
|
|
519
|
+
): Promise<void> {
|
|
520
|
+
dispatch({ type: 'SET_LOADING', payload: true });
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const response = await fetch(`https://api.github.com/gists/${gistId}`);
|
|
524
|
+
|
|
525
|
+
if (!response.ok) {
|
|
526
|
+
if (response.status === 404) {
|
|
527
|
+
throw new Error('Gist not found. It may have been deleted or is private.');
|
|
528
|
+
}
|
|
529
|
+
if (response.status === 403) {
|
|
530
|
+
throw new Error('Rate limited by GitHub API. Please try again in a few minutes.');
|
|
531
|
+
}
|
|
532
|
+
throw new Error(`Failed to fetch Gist: ${response.status} ${response.statusText}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const gist = await response.json();
|
|
536
|
+
const files = Object.values(gist.files) as Array<{
|
|
537
|
+
filename: string;
|
|
538
|
+
content: string;
|
|
539
|
+
language?: string;
|
|
540
|
+
}>;
|
|
541
|
+
|
|
542
|
+
// Look for TypeScript or Python files
|
|
543
|
+
const tsFile = files.find(f => f.filename.endsWith('.ts') || f.language === 'TypeScript');
|
|
544
|
+
const pyFile = files.find(f => f.filename.endsWith('.py') || f.language === 'Python');
|
|
545
|
+
|
|
546
|
+
if (!tsFile && !pyFile) {
|
|
547
|
+
throw new Error('No TypeScript or Python file found in Gist. Please share a Gist containing a .ts or .py file.');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
dispatch({
|
|
551
|
+
type: 'SET_CODE',
|
|
552
|
+
payload: {
|
|
553
|
+
typescript: tsFile?.content || '',
|
|
554
|
+
python: pyFile?.content,
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Try to extract tools from the code
|
|
559
|
+
const code = tsFile?.content || pyFile?.content || '';
|
|
560
|
+
const extractedTools = extractToolsFromCode(code);
|
|
561
|
+
if (extractedTools.length > 0) {
|
|
562
|
+
dispatch({ type: 'SET_TOOLS', payload: extractedTools });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
568
|
+
dispatch({
|
|
569
|
+
type: 'SET_ERROR',
|
|
570
|
+
payload: {
|
|
571
|
+
type: 'network',
|
|
572
|
+
message: 'Failed to load Gist',
|
|
573
|
+
details: errorMessage,
|
|
574
|
+
recoverable: true,
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Extract tool definitions from MCP server code.
|
|
582
|
+
* Supports multiple patterns for both TypeScript and Python MCP servers.
|
|
583
|
+
*/
|
|
584
|
+
function extractToolsFromCode(code: string): Tool[] {
|
|
585
|
+
const tools: Tool[] = [];
|
|
586
|
+
const seenNames = new Set<string>();
|
|
587
|
+
|
|
588
|
+
// Extract TypeScript tools
|
|
589
|
+
extractTypeScriptTools(code, tools, seenNames);
|
|
590
|
+
|
|
591
|
+
// Extract Python tools
|
|
592
|
+
extractPythonTools(code, tools, seenNames);
|
|
593
|
+
|
|
594
|
+
return tools;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Extract TypeScript MCP tool definitions
|
|
599
|
+
*/
|
|
600
|
+
function extractTypeScriptTools(code: string, tools: Tool[], seenNames: Set<string>): void {
|
|
601
|
+
// Pattern 1: server.tool("name", "description", schema, handler)
|
|
602
|
+
const pattern1 = /server\.tool\(\s*(['"`])(\w+)\1\s*,\s*(['"`])([\s\S]*?)\3\s*,\s*(\{[\s\S]*?\})\s*,/g;
|
|
603
|
+
|
|
604
|
+
// Pattern 2: server.tool({ name: "...", description: "...", inputSchema: {...} })
|
|
605
|
+
const pattern2 = /server\.tool\(\s*\{[\s\S]*?name\s*:\s*(['"`])(\w+)\1[\s\S]*?description\s*:\s*(['"`])([\s\S]*?)\3[\s\S]*?inputSchema\s*:\s*(\{[\s\S]*?\})\s*[\s\S]*?\}\s*\)/g;
|
|
606
|
+
|
|
607
|
+
// Pattern 3: .tool("name", { description, inputSchema })
|
|
608
|
+
const pattern3 = /\.tool\(\s*(['"`])(\w+)\1\s*,\s*\{[\s\S]*?description\s*:\s*(['"`])([\s\S]*?)\3/g;
|
|
609
|
+
|
|
610
|
+
let match;
|
|
611
|
+
|
|
612
|
+
// Process Pattern 1
|
|
613
|
+
while ((match = pattern1.exec(code)) !== null) {
|
|
614
|
+
const [, , name, , description, schemaStr] = match;
|
|
615
|
+
if (seenNames.has(name)) continue;
|
|
616
|
+
seenNames.add(name);
|
|
617
|
+
|
|
618
|
+
tools.push({
|
|
619
|
+
name,
|
|
620
|
+
description: cleanDescription(description),
|
|
621
|
+
inputSchema: parseSchemaString(schemaStr),
|
|
622
|
+
source: { type: 'code', file: 'loaded' },
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Process Pattern 2
|
|
627
|
+
while ((match = pattern2.exec(code)) !== null) {
|
|
628
|
+
const [, , name, , description, schemaStr] = match;
|
|
629
|
+
if (seenNames.has(name)) continue;
|
|
630
|
+
seenNames.add(name);
|
|
631
|
+
|
|
632
|
+
tools.push({
|
|
633
|
+
name,
|
|
634
|
+
description: cleanDescription(description),
|
|
635
|
+
inputSchema: parseSchemaString(schemaStr),
|
|
636
|
+
source: { type: 'code', file: 'loaded' },
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Process Pattern 3 (partial - no schema)
|
|
641
|
+
while ((match = pattern3.exec(code)) !== null) {
|
|
642
|
+
const [, , name, , description] = match;
|
|
643
|
+
if (seenNames.has(name)) continue;
|
|
644
|
+
seenNames.add(name);
|
|
645
|
+
|
|
646
|
+
tools.push({
|
|
647
|
+
name,
|
|
648
|
+
description: cleanDescription(description),
|
|
649
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
650
|
+
source: { type: 'code', file: 'loaded' },
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Pattern 4: Look for z.object schemas with tool names nearby
|
|
655
|
+
const zodPattern = /const\s+(\w+)Schema\s*=\s*z\.object\(\s*(\{[\s\S]*?\})\s*\)/g;
|
|
656
|
+
const zodSchemas = new Map<string, string>();
|
|
657
|
+
|
|
658
|
+
while ((match = zodPattern.exec(code)) !== null) {
|
|
659
|
+
const [, schemaName, schemaBody] = match;
|
|
660
|
+
zodSchemas.set(schemaName.toLowerCase(), schemaBody);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Try to match Zod schemas to tools that don't have schemas yet
|
|
664
|
+
for (const tool of tools) {
|
|
665
|
+
if (Object.keys(tool.inputSchema.properties || {}).length === 0) {
|
|
666
|
+
const schemaBody = zodSchemas.get(tool.name.toLowerCase()) ||
|
|
667
|
+
zodSchemas.get(tool.name.replace(/_/g, '').toLowerCase());
|
|
668
|
+
if (schemaBody) {
|
|
669
|
+
tool.inputSchema = parseZodSchema(schemaBody);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Extract Python MCP tool definitions
|
|
677
|
+
*/
|
|
678
|
+
function extractPythonTools(code: string, tools: Tool[], seenNames: Set<string>): void {
|
|
679
|
+
// Pattern 1: @server.tool() or @mcp.tool() decorator
|
|
680
|
+
const decoratorPattern = /@(?:server|mcp)\.tool\s*\(\s*\)\s*\n\s*(?:async\s+)?def\s+(\w+)\s*\(([\s\S]*?)\)\s*(?:->[\s\S]*?)?:\s*\n\s*(?:['"`]{3}([\s\S]*?)['"`]{3}|['"`]([\s\S]*?)['"`])?/g;
|
|
681
|
+
|
|
682
|
+
// Pattern 2: @tool decorator (from mcp.server.tool)
|
|
683
|
+
const toolDecoratorPattern = /@tool\s*\(\s*(?:name\s*=\s*)?(['"`])?(\w+)\1?\s*(?:,\s*description\s*=\s*(['"`])([\s\S]*?)\3)?\s*\)\s*\n\s*(?:async\s+)?def\s+(\w+)/g;
|
|
684
|
+
|
|
685
|
+
// Pattern 3: server.add_tool or mcp.add_tool
|
|
686
|
+
const addToolPattern = /(?:server|mcp)\.add_tool\(\s*['"`](\w+)['"`]\s*,\s*(?:description\s*=\s*)?['"`]([\s\S]*?)['"`]/g;
|
|
687
|
+
|
|
688
|
+
let match;
|
|
689
|
+
|
|
690
|
+
// Process Pattern 1 (decorator with docstring)
|
|
691
|
+
while ((match = decoratorPattern.exec(code)) !== null) {
|
|
692
|
+
const [, funcName, params, tripleQuoteDoc, singleQuoteDoc] = match;
|
|
693
|
+
if (seenNames.has(funcName)) continue;
|
|
694
|
+
seenNames.add(funcName);
|
|
695
|
+
|
|
696
|
+
const description = tripleQuoteDoc || singleQuoteDoc || `Tool: ${funcName}`;
|
|
697
|
+
|
|
698
|
+
tools.push({
|
|
699
|
+
name: funcName,
|
|
700
|
+
description: cleanDescription(description),
|
|
701
|
+
inputSchema: parsePythonParams(params),
|
|
702
|
+
source: { type: 'python-mcp', file: 'loaded' },
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Process Pattern 2 (tool decorator with name)
|
|
707
|
+
while ((match = toolDecoratorPattern.exec(code)) !== null) {
|
|
708
|
+
const [, , decoratorName, , description, funcName] = match;
|
|
709
|
+
const name = decoratorName || funcName;
|
|
710
|
+
if (seenNames.has(name)) continue;
|
|
711
|
+
seenNames.add(name);
|
|
712
|
+
|
|
713
|
+
tools.push({
|
|
714
|
+
name,
|
|
715
|
+
description: cleanDescription(description || `Tool: ${name}`),
|
|
716
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
717
|
+
source: { type: 'python-mcp', file: 'loaded' },
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Process Pattern 3 (add_tool)
|
|
722
|
+
while ((match = addToolPattern.exec(code)) !== null) {
|
|
723
|
+
const [, name, description] = match;
|
|
724
|
+
if (seenNames.has(name)) continue;
|
|
725
|
+
seenNames.add(name);
|
|
726
|
+
|
|
727
|
+
tools.push({
|
|
728
|
+
name,
|
|
729
|
+
description: cleanDescription(description),
|
|
730
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
731
|
+
source: { type: 'python-mcp', file: 'loaded' },
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Pattern 4: Look for Pydantic models that might be tool inputs
|
|
736
|
+
const pydanticPattern = /class\s+(\w+)(?:Input|Params|Args|Request)\s*\((?:BaseModel|TypedDict)\):\s*([\s\S]*?)(?=\n(?:class|def|@|\Z))/g;
|
|
737
|
+
const pydanticModels = new Map<string, string>();
|
|
738
|
+
|
|
739
|
+
while ((match = pydanticPattern.exec(code)) !== null) {
|
|
740
|
+
const [, modelName, modelBody] = match;
|
|
741
|
+
pydanticModels.set(modelName.toLowerCase().replace(/input|params|args|request/i, ''), modelBody);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Try to match Pydantic models to tools
|
|
745
|
+
for (const tool of tools) {
|
|
746
|
+
if (Object.keys(tool.inputSchema.properties || {}).length === 0) {
|
|
747
|
+
const modelBody = pydanticModels.get(tool.name.toLowerCase()) ||
|
|
748
|
+
pydanticModels.get(tool.name.replace(/_/g, '').toLowerCase());
|
|
749
|
+
if (modelBody) {
|
|
750
|
+
tool.inputSchema = parsePydanticModel(modelBody);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Clean and normalize description text
|
|
758
|
+
*/
|
|
759
|
+
function cleanDescription(desc: string): string {
|
|
760
|
+
if (!desc) return '';
|
|
761
|
+
return desc
|
|
762
|
+
.trim()
|
|
763
|
+
.replace(/\\n/g, ' ')
|
|
764
|
+
.replace(/\s+/g, ' ')
|
|
765
|
+
.replace(/^['"`]+|['"`]+$/g, '')
|
|
766
|
+
.trim();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Parse a JSON-like schema string into an inputSchema object
|
|
771
|
+
*/
|
|
772
|
+
function parseSchemaString(schemaStr: string): Tool['inputSchema'] {
|
|
773
|
+
const schema: Tool['inputSchema'] = {
|
|
774
|
+
type: 'object',
|
|
775
|
+
properties: {},
|
|
776
|
+
required: [],
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
// Try direct JSON parse first (for well-formed schemas)
|
|
781
|
+
const parsed = JSON.parse(schemaStr);
|
|
782
|
+
if (parsed.properties) {
|
|
783
|
+
schema.properties = parsed.properties;
|
|
784
|
+
}
|
|
785
|
+
if (parsed.required) {
|
|
786
|
+
schema.required = parsed.required;
|
|
787
|
+
}
|
|
788
|
+
return schema;
|
|
789
|
+
} catch {
|
|
790
|
+
// Fall back to regex-based extraction
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Extract properties using regex
|
|
794
|
+
const propsMatch = schemaStr.match(/properties\s*:\s*\{([\s\S]*?)\}(?=\s*,?\s*(?:required|additionalProperties|\}))/);
|
|
795
|
+
if (propsMatch) {
|
|
796
|
+
const propsStr = propsMatch[1];
|
|
797
|
+
|
|
798
|
+
// Match individual property definitions
|
|
799
|
+
const propPattern = /(\w+)\s*:\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g;
|
|
800
|
+
let propMatch;
|
|
801
|
+
|
|
802
|
+
while ((propMatch = propPattern.exec(propsStr)) !== null) {
|
|
803
|
+
const [, propName, propDef] = propMatch;
|
|
804
|
+
const prop: { type: string; description?: string; enum?: string[]; default?: unknown } = {
|
|
805
|
+
type: 'string' // default type
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// Extract type
|
|
809
|
+
const typeMatch = propDef.match(/type\s*:\s*['"`]?(\w+)['"`]?/);
|
|
810
|
+
if (typeMatch) {
|
|
811
|
+
prop.type = typeMatch[1];
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Extract description
|
|
815
|
+
const descMatch = propDef.match(/description\s*:\s*(['"`])([\s\S]*?)\1/);
|
|
816
|
+
if (descMatch) {
|
|
817
|
+
prop.description = descMatch[2];
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Extract enum values
|
|
821
|
+
const enumMatch = propDef.match(/enum\s*:\s*\[([\s\S]*?)\]/);
|
|
822
|
+
if (enumMatch) {
|
|
823
|
+
const enumValues = enumMatch[1].match(/['"`]([^'"`]+)['"`]/g);
|
|
824
|
+
if (enumValues) {
|
|
825
|
+
prop.enum = enumValues.map(v => v.replace(/['"`]/g, ''));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Extract default value
|
|
830
|
+
const defaultMatch = propDef.match(/default\s*:\s*(['"`]?)([^,}\s]+)\1/);
|
|
831
|
+
if (defaultMatch) {
|
|
832
|
+
const val = defaultMatch[2];
|
|
833
|
+
prop.default = val === 'true' ? true : val === 'false' ? false : isNaN(Number(val)) ? val : Number(val);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
schema.properties![propName] = prop;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Extract required array
|
|
841
|
+
const requiredMatch = schemaStr.match(/required\s*:\s*\[([\s\S]*?)\]/);
|
|
842
|
+
if (requiredMatch) {
|
|
843
|
+
const requiredItems = requiredMatch[1].match(/['"`](\w+)['"`]/g);
|
|
844
|
+
if (requiredItems) {
|
|
845
|
+
schema.required = requiredItems.map(r => r.replace(/['"`]/g, ''));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return schema;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Parse Zod schema definition to extract properties
|
|
854
|
+
*/
|
|
855
|
+
function parseZodSchema(schemaBody: string): Tool['inputSchema'] {
|
|
856
|
+
const schema: Tool['inputSchema'] = {
|
|
857
|
+
type: 'object',
|
|
858
|
+
properties: {},
|
|
859
|
+
required: [],
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// Match Zod property definitions
|
|
863
|
+
const propPattern = /(\w+)\s*:\s*z\.(string|number|boolean|array|object|enum)\s*\(\s*(?:\[([\s\S]*?)\])?\s*\)(?:\.describe\(\s*(['"`])([\s\S]*?)\4\s*\))?(?:\.optional\(\))?(?:\.default\(([^)]+)\))?/g;
|
|
864
|
+
let match;
|
|
865
|
+
|
|
866
|
+
while ((match = propPattern.exec(schemaBody)) !== null) {
|
|
867
|
+
const [fullMatch, propName, zodType, enumValues, , description, defaultValue] = match;
|
|
868
|
+
|
|
869
|
+
const prop: { type: string; description?: string; enum?: string[]; default?: unknown } = {
|
|
870
|
+
type: zodType === 'array' ? 'array' : zodType === 'enum' ? 'string' : zodType,
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
if (description) {
|
|
874
|
+
prop.description = description;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (enumValues) {
|
|
878
|
+
const values = enumValues.match(/['"`]([^'"`]+)['"`]/g);
|
|
879
|
+
if (values) {
|
|
880
|
+
prop.enum = values.map(v => v.replace(/['"`]/g, ''));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (defaultValue !== undefined) {
|
|
885
|
+
const val = defaultValue.trim();
|
|
886
|
+
prop.default = val === 'true' ? true : val === 'false' ? false : isNaN(Number(val)) ? val.replace(/['"`]/g, '') : Number(val);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
schema.properties![propName] = prop;
|
|
890
|
+
|
|
891
|
+
// If not optional, add to required
|
|
892
|
+
if (!fullMatch.includes('.optional()')) {
|
|
893
|
+
schema.required!.push(propName);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return schema;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Parse Python function parameters to extract schema
|
|
902
|
+
*/
|
|
903
|
+
function parsePythonParams(params: string): Tool['inputSchema'] {
|
|
904
|
+
const schema: Tool['inputSchema'] = {
|
|
905
|
+
type: 'object',
|
|
906
|
+
properties: {},
|
|
907
|
+
required: [],
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
if (!params || params.trim() === '' || params.trim() === 'self') {
|
|
911
|
+
return schema;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Split parameters, handling nested types
|
|
915
|
+
const paramList = splitPythonParams(params);
|
|
916
|
+
|
|
917
|
+
for (const param of paramList) {
|
|
918
|
+
const trimmed = param.trim();
|
|
919
|
+
if (!trimmed || trimmed === 'self' || trimmed === 'cls' || trimmed.startsWith('*')) {
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Match: name: Type = default or name: Type or name = default or just name
|
|
924
|
+
const paramMatch = trimmed.match(/^(\w+)\s*(?::\s*([\w\[\],\s|]+))?\s*(?:=\s*(.+))?$/);
|
|
925
|
+
if (!paramMatch) continue;
|
|
926
|
+
|
|
927
|
+
const [, paramName, typeHint, defaultValue] = paramMatch;
|
|
928
|
+
|
|
929
|
+
const prop: { type: string; description?: string; enum?: string[]; default?: unknown } = {
|
|
930
|
+
type: 'string' // Default type
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// Convert Python type hints to JSON Schema types
|
|
934
|
+
if (typeHint) {
|
|
935
|
+
prop.type = pythonTypeToJsonType(typeHint.trim());
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (defaultValue !== undefined && defaultValue.trim() !== 'None') {
|
|
939
|
+
const val = defaultValue.trim();
|
|
940
|
+
if (val === 'True') {
|
|
941
|
+
prop.default = true;
|
|
942
|
+
} else if (val === 'False') {
|
|
943
|
+
prop.default = false;
|
|
944
|
+
} else if (!isNaN(Number(val))) {
|
|
945
|
+
prop.default = Number(val);
|
|
946
|
+
} else {
|
|
947
|
+
prop.default = val.replace(/^['"`]|['"`]$/g, '');
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
schema.properties![paramName] = prop;
|
|
952
|
+
|
|
953
|
+
// If no default value (and not None), it's required
|
|
954
|
+
if (defaultValue === undefined) {
|
|
955
|
+
schema.required!.push(paramName);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return schema;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Split Python parameters handling nested brackets
|
|
964
|
+
*/
|
|
965
|
+
function splitPythonParams(params: string): string[] {
|
|
966
|
+
const result: string[] = [];
|
|
967
|
+
let current = '';
|
|
968
|
+
let depth = 0;
|
|
969
|
+
|
|
970
|
+
for (const char of params) {
|
|
971
|
+
if (char === '[' || char === '(') {
|
|
972
|
+
depth++;
|
|
973
|
+
current += char;
|
|
974
|
+
} else if (char === ']' || char === ')') {
|
|
975
|
+
depth--;
|
|
976
|
+
current += char;
|
|
977
|
+
} else if (char === ',' && depth === 0) {
|
|
978
|
+
result.push(current);
|
|
979
|
+
current = '';
|
|
980
|
+
} else {
|
|
981
|
+
current += char;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (current.trim()) {
|
|
986
|
+
result.push(current);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return result;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Convert Python type hint to JSON Schema type
|
|
994
|
+
*/
|
|
995
|
+
function pythonTypeToJsonType(typeHint: string): string {
|
|
996
|
+
const normalized = typeHint.toLowerCase().replace(/\s/g, '');
|
|
997
|
+
|
|
998
|
+
if (normalized.includes('str')) return 'string';
|
|
999
|
+
if (normalized.includes('int')) return 'integer';
|
|
1000
|
+
if (normalized.includes('float')) return 'number';
|
|
1001
|
+
if (normalized.includes('bool')) return 'boolean';
|
|
1002
|
+
if (normalized.includes('list') || normalized.includes('array')) return 'array';
|
|
1003
|
+
if (normalized.includes('dict') || normalized.includes('object')) return 'object';
|
|
1004
|
+
if (normalized.includes('none') || normalized.includes('null')) return 'null';
|
|
1005
|
+
|
|
1006
|
+
return 'string'; // Default fallback
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Parse Pydantic model body to extract properties
|
|
1011
|
+
*/
|
|
1012
|
+
function parsePydanticModel(modelBody: string): Tool['inputSchema'] {
|
|
1013
|
+
const schema: Tool['inputSchema'] = {
|
|
1014
|
+
type: 'object',
|
|
1015
|
+
properties: {},
|
|
1016
|
+
required: [],
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// Match Pydantic field definitions
|
|
1020
|
+
// Pattern: field_name: Type = Field(...) or field_name: Type = default
|
|
1021
|
+
const fieldPattern = /(\w+)\s*:\s*([\w\[\],\s|]+)\s*(?:=\s*(?:Field\(([\s\S]*?)\)|([^#\n]+)))?/g;
|
|
1022
|
+
let match;
|
|
1023
|
+
|
|
1024
|
+
while ((match = fieldPattern.exec(modelBody)) !== null) {
|
|
1025
|
+
const [, fieldName, typeHint, fieldArgs, defaultValue] = match;
|
|
1026
|
+
|
|
1027
|
+
// Skip private fields
|
|
1028
|
+
if (fieldName.startsWith('_')) continue;
|
|
1029
|
+
|
|
1030
|
+
const prop: { type: string; description?: string; enum?: string[]; default?: unknown } = {
|
|
1031
|
+
type: pythonTypeToJsonType(typeHint.trim()),
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
// Parse Field() arguments
|
|
1035
|
+
if (fieldArgs) {
|
|
1036
|
+
const descMatch = fieldArgs.match(/description\s*=\s*(['"`])([\s\S]*?)\1/);
|
|
1037
|
+
if (descMatch) {
|
|
1038
|
+
prop.description = descMatch[2];
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const defaultMatch = fieldArgs.match(/default\s*=\s*([^,)]+)/);
|
|
1042
|
+
if (defaultMatch) {
|
|
1043
|
+
const val = defaultMatch[1].trim();
|
|
1044
|
+
if (val !== '...' && val !== 'None') {
|
|
1045
|
+
prop.default = val === 'True' ? true : val === 'False' ? false :
|
|
1046
|
+
isNaN(Number(val)) ? val.replace(/['"`]/g, '') : Number(val);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// If default is ... or not present in Field(), it's required
|
|
1051
|
+
if (!fieldArgs.includes('default=') || fieldArgs.includes('default=...')) {
|
|
1052
|
+
schema.required!.push(fieldName);
|
|
1053
|
+
}
|
|
1054
|
+
} else if (defaultValue !== undefined) {
|
|
1055
|
+
// Handle inline default values
|
|
1056
|
+
const val = defaultValue.trim();
|
|
1057
|
+
if (val && val !== 'None') {
|
|
1058
|
+
prop.default = val === 'True' ? true : val === 'False' ? false :
|
|
1059
|
+
isNaN(Number(val)) ? val.replace(/['"`]/g, '') : Number(val);
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
// No default = required
|
|
1063
|
+
schema.required!.push(fieldName);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
schema.properties![fieldName] = prop;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return schema;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ===== Error Helpers =====
|
|
1073
|
+
|
|
1074
|
+
export function createPlaygroundError(
|
|
1075
|
+
type: PlaygroundError['type'],
|
|
1076
|
+
message: string,
|
|
1077
|
+
details?: string,
|
|
1078
|
+
recoverable = true
|
|
1079
|
+
): PlaygroundError {
|
|
1080
|
+
return {
|
|
1081
|
+
type,
|
|
1082
|
+
message,
|
|
1083
|
+
details,
|
|
1084
|
+
recoverable,
|
|
1085
|
+
retryCount: 0,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export function isSyntaxError(error: PlaygroundError | null): boolean {
|
|
1090
|
+
return error?.type === 'syntax';
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export function isServerError(error: PlaygroundError | null): boolean {
|
|
1094
|
+
return error?.type === 'server';
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export function isRecoverable(error: PlaygroundError | null): boolean {
|
|
1098
|
+
return error?.recoverable ?? false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// ===== Analytics Helpers (Optional) =====
|
|
1102
|
+
|
|
1103
|
+
export interface PlaygroundAnalytics {
|
|
1104
|
+
trackToolExecution: (toolName: string, success: boolean, executionTime: number) => void;
|
|
1105
|
+
trackError: (error: PlaygroundError) => void;
|
|
1106
|
+
trackShare: (method: 'url' | 'gist') => void;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
export function createAnalyticsTracker(): PlaygroundAnalytics {
|
|
1110
|
+
return {
|
|
1111
|
+
trackToolExecution: (toolName, success, executionTime) => {
|
|
1112
|
+
if (typeof window !== 'undefined' && 'gtag' in window) {
|
|
1113
|
+
(window as unknown as { gtag: (...args: unknown[]) => void }).gtag('event', 'playground_tool_execution', {
|
|
1114
|
+
tool_name: toolName,
|
|
1115
|
+
success,
|
|
1116
|
+
execution_time: executionTime,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
// Also log to console in development
|
|
1120
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1121
|
+
console.log('[Analytics] Tool execution:', { toolName, success, executionTime });
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
trackError: (error) => {
|
|
1125
|
+
if (typeof window !== 'undefined' && 'gtag' in window) {
|
|
1126
|
+
(window as unknown as { gtag: (...args: unknown[]) => void }).gtag('event', 'playground_error', {
|
|
1127
|
+
error_type: error.type,
|
|
1128
|
+
error_message: error.message,
|
|
1129
|
+
recoverable: error.recoverable,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1133
|
+
console.log('[Analytics] Error:', error);
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
trackShare: (method) => {
|
|
1137
|
+
if (typeof window !== 'undefined' && 'gtag' in window) {
|
|
1138
|
+
(window as unknown as { gtag: (...args: unknown[]) => void }).gtag('event', 'playground_share', {
|
|
1139
|
+
method,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1143
|
+
console.log('[Analytics] Share:', method);
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
};
|
|
1147
|
+
}
|