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,988 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Transport Abstraction - Factory for creating MCP transports
|
|
3
|
+
* @copyright 2024-2026 nirholas
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
8
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
9
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
10
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
TransportConfig,
|
|
14
|
+
StdioTransportConfig,
|
|
15
|
+
SseTransportConfig,
|
|
16
|
+
StreamableHttpTransportConfig,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
import {
|
|
19
|
+
McpTransportError,
|
|
20
|
+
McpErrorCode,
|
|
21
|
+
McpError,
|
|
22
|
+
isStdioTransportConfig,
|
|
23
|
+
isSseTransportConfig,
|
|
24
|
+
isStreamableHttpTransportConfig,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
import type { Logger } from './logger.js';
|
|
27
|
+
import { createNoopLogger } from './logger.js';
|
|
28
|
+
import type { McpEventEmitter } from './events.js';
|
|
29
|
+
import type { RetryConfig } from './retry.js';
|
|
30
|
+
import { retry, CircuitBreaker } from './retry.js';
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Enhanced Transport Types
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Transport health status
|
|
38
|
+
*/
|
|
39
|
+
export interface TransportHealthStatus {
|
|
40
|
+
readonly healthy: boolean;
|
|
41
|
+
readonly lastCheck: Date;
|
|
42
|
+
readonly consecutiveFailures: number;
|
|
43
|
+
readonly latencyMs: number | null;
|
|
44
|
+
readonly error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Transport wrapper with enhanced capabilities
|
|
49
|
+
*/
|
|
50
|
+
export interface EnhancedTransport {
|
|
51
|
+
readonly transport: Transport;
|
|
52
|
+
readonly config: TransportConfig;
|
|
53
|
+
readonly createdAt: Date;
|
|
54
|
+
readonly healthStatus: TransportHealthStatus;
|
|
55
|
+
|
|
56
|
+
/** Check if transport is healthy */
|
|
57
|
+
checkHealth(): Promise<TransportHealthStatus>;
|
|
58
|
+
|
|
59
|
+
/** Close and cleanup the transport */
|
|
60
|
+
close(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Configuration for transport with reconnection support
|
|
65
|
+
*/
|
|
66
|
+
export interface ReconnectingTransportConfig {
|
|
67
|
+
readonly transportConfig: TransportConfig;
|
|
68
|
+
readonly retryConfig?: Partial<RetryConfig>;
|
|
69
|
+
readonly healthCheckIntervalMs?: number;
|
|
70
|
+
readonly autoReconnect?: boolean;
|
|
71
|
+
readonly logger?: Logger;
|
|
72
|
+
readonly events?: McpEventEmitter;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Connection pool configuration
|
|
77
|
+
*/
|
|
78
|
+
export interface ConnectionPoolConfig {
|
|
79
|
+
readonly maxConnections: number;
|
|
80
|
+
readonly minConnections: number;
|
|
81
|
+
readonly acquireTimeoutMs: number;
|
|
82
|
+
readonly idleTimeoutMs: number;
|
|
83
|
+
readonly healthCheckIntervalMs: number;
|
|
84
|
+
readonly logger?: Logger;
|
|
85
|
+
readonly events?: McpEventEmitter;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Connection pool entry
|
|
90
|
+
*/
|
|
91
|
+
interface PoolEntry {
|
|
92
|
+
transport: EnhancedTransport;
|
|
93
|
+
inUse: boolean;
|
|
94
|
+
lastUsed: Date;
|
|
95
|
+
acquiredAt: Date | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Transport Factory
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates an MCP transport based on the provided configuration.
|
|
104
|
+
*
|
|
105
|
+
* @param config - Transport configuration specifying the type and parameters
|
|
106
|
+
* @returns A Transport instance ready to be connected
|
|
107
|
+
* @throws {McpTransportError} If the transport type is unsupported or configuration is invalid
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* // Create stdio transport
|
|
112
|
+
* const stdioTransport = createTransport({
|
|
113
|
+
* type: 'stdio',
|
|
114
|
+
* command: 'npx',
|
|
115
|
+
* args: ['tsx', 'server.ts'],
|
|
116
|
+
* });
|
|
117
|
+
*
|
|
118
|
+
* // Create streamable HTTP transport
|
|
119
|
+
* const httpTransport = createTransport({
|
|
120
|
+
* type: 'streamable-http',
|
|
121
|
+
* url: 'http://localhost:3000/mcp',
|
|
122
|
+
* });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function createTransport(config: TransportConfig): Transport {
|
|
126
|
+
if (isStdioTransportConfig(config)) {
|
|
127
|
+
return createStdioTransport(config);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isSseTransportConfig(config)) {
|
|
131
|
+
return createSseTransport(config);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isStreamableHttpTransportConfig(config)) {
|
|
135
|
+
return createStreamableHttpTransport(config);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// TypeScript should prevent this, but handle it for runtime safety
|
|
139
|
+
throw new McpError(
|
|
140
|
+
McpErrorCode.UnsupportedTransport,
|
|
141
|
+
`Unsupported transport type: ${(config as TransportConfig).type}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Stdio Transport
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Creates a stdio transport for spawning local MCP server processes.
|
|
151
|
+
*
|
|
152
|
+
* @param config - Stdio transport configuration
|
|
153
|
+
* @returns StdioClientTransport instance
|
|
154
|
+
* @throws {McpTransportError} If the configuration is invalid
|
|
155
|
+
*/
|
|
156
|
+
function createStdioTransport(config: StdioTransportConfig): StdioClientTransport {
|
|
157
|
+
if (!config.command || typeof config.command !== 'string') {
|
|
158
|
+
throw new McpTransportError('Stdio transport requires a valid command string');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
return new StdioClientTransport({
|
|
163
|
+
command: config.command,
|
|
164
|
+
args: config.args ? [...config.args] : undefined,
|
|
165
|
+
env: config.env ? { ...config.env } : undefined,
|
|
166
|
+
cwd: config.cwd,
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new McpTransportError(
|
|
170
|
+
`Failed to create stdio transport: ${error instanceof Error ? error.message : String(error)}`,
|
|
171
|
+
error
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// SSE Transport
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates an SSE transport for connecting to legacy HTTP+SSE MCP servers.
|
|
182
|
+
*
|
|
183
|
+
* @param config - SSE transport configuration
|
|
184
|
+
* @returns SSEClientTransport instance
|
|
185
|
+
* @throws {McpTransportError} If the configuration is invalid
|
|
186
|
+
* @deprecated SSE transport is deprecated in favor of streamable-http
|
|
187
|
+
*/
|
|
188
|
+
function createSseTransport(config: SseTransportConfig): SSEClientTransport {
|
|
189
|
+
if (!config.url || typeof config.url !== 'string') {
|
|
190
|
+
throw new McpTransportError('SSE transport requires a valid URL string');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let url: URL;
|
|
194
|
+
try {
|
|
195
|
+
url = new URL(config.url);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new McpTransportError(
|
|
198
|
+
`Invalid SSE URL: ${config.url}`,
|
|
199
|
+
error
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Validate URL protocol
|
|
204
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
205
|
+
throw new McpTransportError(
|
|
206
|
+
`SSE transport requires http or https URL, got: ${url.protocol}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const options = config.headers
|
|
212
|
+
? {
|
|
213
|
+
requestInit: {
|
|
214
|
+
headers: { ...config.headers },
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
: undefined;
|
|
218
|
+
|
|
219
|
+
return new SSEClientTransport(url, options);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new McpTransportError(
|
|
222
|
+
`Failed to create SSE transport: ${error instanceof Error ? error.message : String(error)}`,
|
|
223
|
+
error
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Streamable HTTP Transport
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Creates a Streamable HTTP transport for connecting to modern MCP servers.
|
|
234
|
+
*
|
|
235
|
+
* @param config - Streamable HTTP transport configuration
|
|
236
|
+
* @returns StreamableHTTPClientTransport instance
|
|
237
|
+
* @throws {McpTransportError} If the configuration is invalid
|
|
238
|
+
*/
|
|
239
|
+
function createStreamableHttpTransport(
|
|
240
|
+
config: StreamableHttpTransportConfig
|
|
241
|
+
): StreamableHTTPClientTransport {
|
|
242
|
+
if (!config.url || typeof config.url !== 'string') {
|
|
243
|
+
throw new McpTransportError('Streamable HTTP transport requires a valid URL string');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let url: URL;
|
|
247
|
+
try {
|
|
248
|
+
url = new URL(config.url);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new McpTransportError(
|
|
251
|
+
`Invalid Streamable HTTP URL: ${config.url}`,
|
|
252
|
+
error
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Validate URL protocol
|
|
257
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
258
|
+
throw new McpTransportError(
|
|
259
|
+
`Streamable HTTP transport requires http or https URL, got: ${url.protocol}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const options: {
|
|
265
|
+
requestInit?: RequestInit;
|
|
266
|
+
sessionId?: string;
|
|
267
|
+
} = {};
|
|
268
|
+
|
|
269
|
+
if (config.headers) {
|
|
270
|
+
options.requestInit = {
|
|
271
|
+
headers: { ...config.headers },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (config.sessionId) {
|
|
276
|
+
options.sessionId = config.sessionId;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return new StreamableHTTPClientTransport(
|
|
280
|
+
url,
|
|
281
|
+
Object.keys(options).length > 0 ? options : undefined
|
|
282
|
+
);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new McpTransportError(
|
|
285
|
+
`Failed to create Streamable HTTP transport: ${error instanceof Error ? error.message : String(error)}`,
|
|
286
|
+
error
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Transport Utilities
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Validates a transport configuration without creating the transport.
|
|
297
|
+
*
|
|
298
|
+
* @param config - Transport configuration to validate
|
|
299
|
+
* @returns True if the configuration is valid
|
|
300
|
+
* @throws {McpTransportError} If the configuration is invalid
|
|
301
|
+
*/
|
|
302
|
+
export function validateTransportConfig(config: TransportConfig): boolean {
|
|
303
|
+
if (!config || typeof config !== 'object') {
|
|
304
|
+
throw new McpTransportError('Transport configuration must be an object');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!config.type || typeof config.type !== 'string') {
|
|
308
|
+
throw new McpTransportError('Transport configuration must have a type');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
switch (config.type) {
|
|
312
|
+
case 'stdio':
|
|
313
|
+
if (!config.command || typeof config.command !== 'string') {
|
|
314
|
+
throw new McpTransportError('Stdio transport requires a valid command string');
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
|
|
318
|
+
case 'sse':
|
|
319
|
+
case 'streamable-http':
|
|
320
|
+
if (!config.url || typeof config.url !== 'string') {
|
|
321
|
+
throw new McpTransportError(`${config.type} transport requires a valid URL string`);
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const url = new URL(config.url);
|
|
325
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
326
|
+
throw new McpTransportError(
|
|
327
|
+
`${config.type} transport requires http or https URL, got: ${url.protocol}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (error instanceof McpTransportError) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
throw new McpTransportError(`Invalid URL: ${config.url}`, error);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
default:
|
|
339
|
+
throw new McpError(
|
|
340
|
+
McpErrorCode.UnsupportedTransport,
|
|
341
|
+
`Unsupported transport type: ${(config as TransportConfig).type}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Gets the display name for a transport type.
|
|
350
|
+
*
|
|
351
|
+
* @param type - Transport type
|
|
352
|
+
* @returns Human-readable transport name
|
|
353
|
+
*/
|
|
354
|
+
export function getTransportDisplayName(type: TransportConfig['type']): string {
|
|
355
|
+
switch (type) {
|
|
356
|
+
case 'stdio':
|
|
357
|
+
return 'Standard I/O (Local Process)';
|
|
358
|
+
case 'sse':
|
|
359
|
+
return 'Server-Sent Events (Legacy HTTP)';
|
|
360
|
+
case 'streamable-http':
|
|
361
|
+
return 'Streamable HTTP';
|
|
362
|
+
default:
|
|
363
|
+
return 'Unknown Transport';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Checks if a transport type is supported in the current environment.
|
|
369
|
+
*
|
|
370
|
+
* @param type - Transport type to check
|
|
371
|
+
* @returns True if the transport type is supported
|
|
372
|
+
*/
|
|
373
|
+
export function isTransportSupported(type: TransportConfig['type']): boolean {
|
|
374
|
+
switch (type) {
|
|
375
|
+
case 'stdio':
|
|
376
|
+
// Stdio transport requires Node.js process spawning
|
|
377
|
+
return typeof globalThis.process !== 'undefined';
|
|
378
|
+
|
|
379
|
+
case 'sse':
|
|
380
|
+
case 'streamable-http':
|
|
381
|
+
// HTTP transports are supported in all environments with fetch
|
|
382
|
+
return typeof globalThis.fetch !== 'undefined';
|
|
383
|
+
|
|
384
|
+
default:
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Enhanced Transport Wrapper
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Creates an enhanced transport with health checking capabilities
|
|
395
|
+
*/
|
|
396
|
+
export function createEnhancedTransport(
|
|
397
|
+
config: TransportConfig,
|
|
398
|
+
logger: Logger = createNoopLogger()
|
|
399
|
+
): EnhancedTransport {
|
|
400
|
+
const transport = createTransport(config);
|
|
401
|
+
const createdAt = new Date();
|
|
402
|
+
|
|
403
|
+
let healthStatus: TransportHealthStatus = {
|
|
404
|
+
healthy: true,
|
|
405
|
+
lastCheck: createdAt,
|
|
406
|
+
consecutiveFailures: 0,
|
|
407
|
+
latencyMs: null,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const checkHealth = async (): Promise<TransportHealthStatus> => {
|
|
411
|
+
const startTime = performance.now();
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
// For HTTP transports, we can do a lightweight check
|
|
415
|
+
if (isSseTransportConfig(config) || isStreamableHttpTransportConfig(config)) {
|
|
416
|
+
const controller = new AbortController();
|
|
417
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const response = await fetch(config.url, {
|
|
421
|
+
method: 'HEAD',
|
|
422
|
+
signal: controller.signal,
|
|
423
|
+
});
|
|
424
|
+
clearTimeout(timeoutId);
|
|
425
|
+
|
|
426
|
+
const latencyMs = performance.now() - startTime;
|
|
427
|
+
healthStatus = {
|
|
428
|
+
healthy: response.ok,
|
|
429
|
+
lastCheck: new Date(),
|
|
430
|
+
consecutiveFailures: response.ok ? 0 : healthStatus.consecutiveFailures + 1,
|
|
431
|
+
latencyMs,
|
|
432
|
+
error: response.ok ? undefined : `HTTP ${response.status}`,
|
|
433
|
+
};
|
|
434
|
+
} catch (fetchError) {
|
|
435
|
+
clearTimeout(timeoutId);
|
|
436
|
+
throw fetchError;
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
// For stdio, we assume healthy if transport exists
|
|
440
|
+
// Actual health would be checked via ping through the client
|
|
441
|
+
healthStatus = {
|
|
442
|
+
healthy: true,
|
|
443
|
+
lastCheck: new Date(),
|
|
444
|
+
consecutiveFailures: 0,
|
|
445
|
+
latencyMs: performance.now() - startTime,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
logger.debug('Transport health check passed', { data: { latencyMs: healthStatus.latencyMs } });
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const latencyMs = performance.now() - startTime;
|
|
452
|
+
healthStatus = {
|
|
453
|
+
healthy: false,
|
|
454
|
+
lastCheck: new Date(),
|
|
455
|
+
consecutiveFailures: healthStatus.consecutiveFailures + 1,
|
|
456
|
+
latencyMs,
|
|
457
|
+
error: error instanceof Error ? error.message : String(error),
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
logger.warn('Transport health check failed', {
|
|
461
|
+
data: {
|
|
462
|
+
errorMessage: healthStatus.error,
|
|
463
|
+
consecutiveFailures: healthStatus.consecutiveFailures,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return healthStatus;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const close = async (): Promise<void> => {
|
|
472
|
+
try {
|
|
473
|
+
await transport.close?.();
|
|
474
|
+
logger.debug('Transport closed');
|
|
475
|
+
} catch (error) {
|
|
476
|
+
logger.error('Error closing transport', {
|
|
477
|
+
data: { errorMessage: error instanceof Error ? error.message : String(error) },
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
transport,
|
|
484
|
+
config,
|
|
485
|
+
createdAt,
|
|
486
|
+
get healthStatus() {
|
|
487
|
+
return healthStatus;
|
|
488
|
+
},
|
|
489
|
+
checkHealth,
|
|
490
|
+
close,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ============================================================================
|
|
495
|
+
// Reconnecting Transport
|
|
496
|
+
// ============================================================================
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Transport wrapper with automatic reconnection support
|
|
500
|
+
*/
|
|
501
|
+
export class ReconnectingTransport {
|
|
502
|
+
private _transport: EnhancedTransport | null = null;
|
|
503
|
+
private _config: ReconnectingTransportConfig;
|
|
504
|
+
private _logger: Logger;
|
|
505
|
+
private _events?: McpEventEmitter;
|
|
506
|
+
private _circuitBreaker: CircuitBreaker;
|
|
507
|
+
private _healthCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
508
|
+
private _closed = false;
|
|
509
|
+
private _reconnecting = false;
|
|
510
|
+
private _connectionState: 'disconnected' | 'connecting' | 'connected' | 'error' = 'disconnected';
|
|
511
|
+
|
|
512
|
+
constructor(config: ReconnectingTransportConfig) {
|
|
513
|
+
this._config = config;
|
|
514
|
+
this._logger = config.logger ?? createNoopLogger();
|
|
515
|
+
this._events = config.events;
|
|
516
|
+
this._circuitBreaker = new CircuitBreaker({
|
|
517
|
+
failureThreshold: 5,
|
|
518
|
+
resetTimeoutMs: 30000,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get the underlying transport
|
|
524
|
+
*/
|
|
525
|
+
get transport(): Transport | null {
|
|
526
|
+
return this._transport?.transport ?? null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Check if currently connected
|
|
531
|
+
*/
|
|
532
|
+
get isConnected(): boolean {
|
|
533
|
+
return this._transport !== null && this._transport.healthStatus.healthy;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Check if closed
|
|
538
|
+
*/
|
|
539
|
+
get isClosed(): boolean {
|
|
540
|
+
return this._closed;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Connect the transport with retry support
|
|
545
|
+
*/
|
|
546
|
+
async connect(): Promise<Transport> {
|
|
547
|
+
if (this._closed) {
|
|
548
|
+
throw new McpTransportError('Transport is closed');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const previousState = this._connectionState;
|
|
552
|
+
this._connectionState = 'connecting';
|
|
553
|
+
|
|
554
|
+
this._events?.emit({
|
|
555
|
+
type: 'connection:stateChange',
|
|
556
|
+
previousState: previousState,
|
|
557
|
+
currentState: 'connecting',
|
|
558
|
+
timestamp: new Date(),
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const retryOptions: Partial<RetryConfig> = {
|
|
562
|
+
maxAttempts: this._config.retryConfig?.maxAttempts ?? 3,
|
|
563
|
+
initialDelayMs: this._config.retryConfig?.initialDelayMs ?? 1000,
|
|
564
|
+
maxDelayMs: this._config.retryConfig?.maxDelayMs ?? 30000,
|
|
565
|
+
backoffMultiplier: this._config.retryConfig?.backoffMultiplier ?? 2,
|
|
566
|
+
jitter: this._config.retryConfig?.jitter ?? true,
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const result = await retry(
|
|
570
|
+
async () => {
|
|
571
|
+
// Use circuit breaker to guard connection attempts
|
|
572
|
+
return await this._circuitBreaker.execute(async () => {
|
|
573
|
+
const enhancedTransport = createEnhancedTransport(
|
|
574
|
+
this._config.transportConfig,
|
|
575
|
+
this._logger
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// Verify transport is healthy
|
|
579
|
+
const health = await enhancedTransport.checkHealth();
|
|
580
|
+
if (!health.healthy) {
|
|
581
|
+
throw new McpTransportError(`Transport health check failed: ${health.error}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return enhancedTransport;
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
retryOptions
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
if (!result.success || !result.value) {
|
|
591
|
+
this._connectionState = 'error';
|
|
592
|
+
this._events?.emit({
|
|
593
|
+
type: 'connection:stateChange',
|
|
594
|
+
previousState: 'connecting',
|
|
595
|
+
currentState: 'error',
|
|
596
|
+
timestamp: new Date(),
|
|
597
|
+
});
|
|
598
|
+
throw result.error ?? new McpTransportError('Failed to connect');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this._transport = result.value;
|
|
602
|
+
this._connectionState = 'connected';
|
|
603
|
+
|
|
604
|
+
this._events?.emit({
|
|
605
|
+
type: 'connection:stateChange',
|
|
606
|
+
previousState: 'connecting',
|
|
607
|
+
currentState: 'connected',
|
|
608
|
+
timestamp: new Date(),
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Start health check interval
|
|
612
|
+
if (this._config.healthCheckIntervalMs && this._config.healthCheckIntervalMs > 0) {
|
|
613
|
+
this._startHealthCheck();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return this._transport.transport;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Reconnect the transport
|
|
621
|
+
*/
|
|
622
|
+
async reconnect(): Promise<Transport> {
|
|
623
|
+
if (this._closed) {
|
|
624
|
+
throw new McpTransportError('Transport is closed');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (this._reconnecting) {
|
|
628
|
+
throw new McpTransportError('Reconnection already in progress');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
this._reconnecting = true;
|
|
632
|
+
const previousState = this._connectionState;
|
|
633
|
+
|
|
634
|
+
this._events?.emit({
|
|
635
|
+
type: 'connection:reconnecting',
|
|
636
|
+
attempt: 1,
|
|
637
|
+
maxAttempts: this._config.retryConfig?.maxAttempts ?? 3,
|
|
638
|
+
delayMs: this._config.retryConfig?.initialDelayMs ?? 1000,
|
|
639
|
+
timestamp: new Date(),
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
// Close existing transport
|
|
644
|
+
if (this._transport) {
|
|
645
|
+
await this._transport.close();
|
|
646
|
+
this._transport = null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Connect again
|
|
650
|
+
return await this.connect();
|
|
651
|
+
} catch (error) {
|
|
652
|
+
this._connectionState = 'error';
|
|
653
|
+
this._events?.emit({
|
|
654
|
+
type: 'connection:stateChange',
|
|
655
|
+
previousState,
|
|
656
|
+
currentState: 'error',
|
|
657
|
+
timestamp: new Date(),
|
|
658
|
+
});
|
|
659
|
+
throw error;
|
|
660
|
+
} finally {
|
|
661
|
+
this._reconnecting = false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Close the transport
|
|
667
|
+
*/
|
|
668
|
+
async close(): Promise<void> {
|
|
669
|
+
this._closed = true;
|
|
670
|
+
this._stopHealthCheck();
|
|
671
|
+
|
|
672
|
+
const previousState = this._connectionState;
|
|
673
|
+
|
|
674
|
+
if (this._transport) {
|
|
675
|
+
await this._transport.close();
|
|
676
|
+
this._transport = null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
this._connectionState = 'disconnected';
|
|
680
|
+
this._events?.emit({
|
|
681
|
+
type: 'connection:stateChange',
|
|
682
|
+
previousState,
|
|
683
|
+
currentState: 'disconnected',
|
|
684
|
+
timestamp: new Date(),
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
this._events?.emit({
|
|
688
|
+
type: 'connection:closed',
|
|
689
|
+
reason: 'manual',
|
|
690
|
+
wasClean: true,
|
|
691
|
+
timestamp: new Date(),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Get current health status
|
|
697
|
+
*/
|
|
698
|
+
async checkHealth(): Promise<TransportHealthStatus | null> {
|
|
699
|
+
return this._transport?.checkHealth() ?? null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private _startHealthCheck(): void {
|
|
703
|
+
this._stopHealthCheck();
|
|
704
|
+
|
|
705
|
+
this._healthCheckInterval = setInterval(async () => {
|
|
706
|
+
if (this._closed || !this._transport) return;
|
|
707
|
+
|
|
708
|
+
const health = await this._transport.checkHealth();
|
|
709
|
+
|
|
710
|
+
if (!health.healthy && this._config.autoReconnect !== false) {
|
|
711
|
+
this._logger.warn('Health check failed, attempting reconnect', {
|
|
712
|
+
data: { consecutiveFailures: health.consecutiveFailures },
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
await this.reconnect();
|
|
717
|
+
} catch (error) {
|
|
718
|
+
this._logger.error('Reconnect failed', {
|
|
719
|
+
data: { errorMessage: error instanceof Error ? error.message : String(error) },
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
this._events?.emit({
|
|
723
|
+
type: 'connection:error',
|
|
724
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
725
|
+
recoverable: true,
|
|
726
|
+
timestamp: new Date(),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}, this._config.healthCheckIntervalMs);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private _stopHealthCheck(): void {
|
|
734
|
+
if (this._healthCheckInterval) {
|
|
735
|
+
clearInterval(this._healthCheckInterval);
|
|
736
|
+
this._healthCheckInterval = null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ============================================================================
|
|
742
|
+
// Connection Pool
|
|
743
|
+
// ============================================================================
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Connection pool for managing multiple transports
|
|
747
|
+
*/
|
|
748
|
+
export class TransportPool {
|
|
749
|
+
private _config: ConnectionPoolConfig;
|
|
750
|
+
private _transportConfig: TransportConfig;
|
|
751
|
+
private _pool: PoolEntry[] = [];
|
|
752
|
+
private _waitQueue: Array<{
|
|
753
|
+
resolve: (transport: EnhancedTransport) => void;
|
|
754
|
+
reject: (error: Error) => void;
|
|
755
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
756
|
+
}> = [];
|
|
757
|
+
private _logger: Logger;
|
|
758
|
+
private _events?: McpEventEmitter;
|
|
759
|
+
private _healthCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
760
|
+
private _closed = false;
|
|
761
|
+
|
|
762
|
+
constructor(transportConfig: TransportConfig, config: Partial<ConnectionPoolConfig> = {}) {
|
|
763
|
+
this._transportConfig = transportConfig;
|
|
764
|
+
this._config = {
|
|
765
|
+
maxConnections: config.maxConnections ?? 10,
|
|
766
|
+
minConnections: config.minConnections ?? 1,
|
|
767
|
+
acquireTimeoutMs: config.acquireTimeoutMs ?? 30000,
|
|
768
|
+
idleTimeoutMs: config.idleTimeoutMs ?? 60000,
|
|
769
|
+
healthCheckIntervalMs: config.healthCheckIntervalMs ?? 30000,
|
|
770
|
+
logger: config.logger,
|
|
771
|
+
events: config.events,
|
|
772
|
+
};
|
|
773
|
+
this._logger = this._config.logger ?? createNoopLogger();
|
|
774
|
+
this._events = this._config.events;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Initialize the pool with minimum connections
|
|
779
|
+
*/
|
|
780
|
+
async initialize(): Promise<void> {
|
|
781
|
+
if (this._closed) {
|
|
782
|
+
throw new McpTransportError('Pool is closed');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const promises: Promise<void>[] = [];
|
|
786
|
+
|
|
787
|
+
for (let i = 0; i < this._config.minConnections; i++) {
|
|
788
|
+
promises.push(this._createConnection());
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
await Promise.all(promises);
|
|
792
|
+
this._startHealthCheck();
|
|
793
|
+
|
|
794
|
+
this._logger.info('Connection pool initialized', {
|
|
795
|
+
data: {
|
|
796
|
+
minConnections: this._config.minConnections,
|
|
797
|
+
maxConnections: this._config.maxConnections,
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Get the current pool size
|
|
804
|
+
*/
|
|
805
|
+
get size(): number {
|
|
806
|
+
return this._pool.length;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Get the number of available connections
|
|
811
|
+
*/
|
|
812
|
+
get available(): number {
|
|
813
|
+
return this._pool.filter(entry => !entry.inUse).length;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Get the number of in-use connections
|
|
818
|
+
*/
|
|
819
|
+
get inUse(): number {
|
|
820
|
+
return this._pool.filter(entry => entry.inUse).length;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Acquire a transport from the pool
|
|
825
|
+
*/
|
|
826
|
+
async acquire(): Promise<EnhancedTransport> {
|
|
827
|
+
if (this._closed) {
|
|
828
|
+
throw new McpTransportError('Pool is closed');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Try to find an available healthy connection
|
|
832
|
+
const available = this._pool.find(entry => !entry.inUse && entry.transport.healthStatus.healthy);
|
|
833
|
+
|
|
834
|
+
if (available) {
|
|
835
|
+
available.inUse = true;
|
|
836
|
+
available.acquiredAt = new Date();
|
|
837
|
+
this._logger.debug('Acquired connection from pool', { data: { poolSize: this.size } });
|
|
838
|
+
return available.transport;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Create a new connection if under max
|
|
842
|
+
if (this._pool.length < this._config.maxConnections) {
|
|
843
|
+
await this._createConnection();
|
|
844
|
+
|
|
845
|
+
const newEntry = this._pool.find(entry => !entry.inUse);
|
|
846
|
+
if (newEntry) {
|
|
847
|
+
newEntry.inUse = true;
|
|
848
|
+
newEntry.acquiredAt = new Date();
|
|
849
|
+
this._logger.debug('Created new connection', { data: { poolSize: this.size } });
|
|
850
|
+
return newEntry.transport;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Wait for a connection to become available
|
|
855
|
+
return new Promise<EnhancedTransport>((resolve, reject) => {
|
|
856
|
+
const timeoutId = setTimeout(() => {
|
|
857
|
+
const index = this._waitQueue.findIndex(item => item.resolve === resolve);
|
|
858
|
+
if (index !== -1) {
|
|
859
|
+
this._waitQueue.splice(index, 1);
|
|
860
|
+
}
|
|
861
|
+
reject(new McpTransportError('Acquire timeout exceeded'));
|
|
862
|
+
}, this._config.acquireTimeoutMs);
|
|
863
|
+
|
|
864
|
+
this._waitQueue.push({ resolve, reject, timeoutId });
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Release a transport back to the pool
|
|
870
|
+
*/
|
|
871
|
+
release(transport: EnhancedTransport): void {
|
|
872
|
+
const entry = this._pool.find(e => e.transport === transport);
|
|
873
|
+
|
|
874
|
+
if (!entry) {
|
|
875
|
+
this._logger.warn('Attempted to release unknown transport');
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
entry.inUse = false;
|
|
880
|
+
entry.lastUsed = new Date();
|
|
881
|
+
entry.acquiredAt = null;
|
|
882
|
+
|
|
883
|
+
this._logger.debug('Released connection to pool', { data: { poolSize: this.size, available: this.available } });
|
|
884
|
+
|
|
885
|
+
// Process wait queue
|
|
886
|
+
if (this._waitQueue.length > 0) {
|
|
887
|
+
const waiter = this._waitQueue.shift();
|
|
888
|
+
if (waiter) {
|
|
889
|
+
clearTimeout(waiter.timeoutId);
|
|
890
|
+
entry.inUse = true;
|
|
891
|
+
entry.acquiredAt = new Date();
|
|
892
|
+
waiter.resolve(entry.transport);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Close all connections and shutdown the pool
|
|
899
|
+
*/
|
|
900
|
+
async close(): Promise<void> {
|
|
901
|
+
this._closed = true;
|
|
902
|
+
this._stopHealthCheck();
|
|
903
|
+
|
|
904
|
+
// Reject all waiters
|
|
905
|
+
for (const waiter of this._waitQueue) {
|
|
906
|
+
clearTimeout(waiter.timeoutId);
|
|
907
|
+
waiter.reject(new McpTransportError('Pool is closing'));
|
|
908
|
+
}
|
|
909
|
+
this._waitQueue = [];
|
|
910
|
+
|
|
911
|
+
// Close all connections
|
|
912
|
+
const closePromises = this._pool.map(entry => entry.transport.close());
|
|
913
|
+
await Promise.allSettled(closePromises);
|
|
914
|
+
|
|
915
|
+
this._pool = [];
|
|
916
|
+
this._logger.info('Connection pool closed');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private async _createConnection(): Promise<void> {
|
|
920
|
+
const transport = createEnhancedTransport(this._transportConfig, this._logger);
|
|
921
|
+
await transport.checkHealth();
|
|
922
|
+
|
|
923
|
+
this._pool.push({
|
|
924
|
+
transport,
|
|
925
|
+
inUse: false,
|
|
926
|
+
lastUsed: new Date(),
|
|
927
|
+
acquiredAt: null,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private _startHealthCheck(): void {
|
|
932
|
+
this._stopHealthCheck();
|
|
933
|
+
|
|
934
|
+
this._healthCheckInterval = setInterval(async () => {
|
|
935
|
+
if (this._closed) return;
|
|
936
|
+
|
|
937
|
+
const now = Date.now();
|
|
938
|
+
const toRemove: PoolEntry[] = [];
|
|
939
|
+
|
|
940
|
+
for (const entry of this._pool) {
|
|
941
|
+
// Skip in-use connections
|
|
942
|
+
if (entry.inUse) continue;
|
|
943
|
+
|
|
944
|
+
// Check for idle timeout
|
|
945
|
+
const idleTime = now - entry.lastUsed.getTime();
|
|
946
|
+
if (idleTime > this._config.idleTimeoutMs && this._pool.length > this._config.minConnections) {
|
|
947
|
+
toRemove.push(entry);
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Check health
|
|
952
|
+
const health = await entry.transport.checkHealth();
|
|
953
|
+
if (!health.healthy && health.consecutiveFailures >= 3) {
|
|
954
|
+
toRemove.push(entry);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Remove unhealthy/idle connections
|
|
959
|
+
for (const entry of toRemove) {
|
|
960
|
+
const index = this._pool.indexOf(entry);
|
|
961
|
+
if (index !== -1) {
|
|
962
|
+
this._pool.splice(index, 1);
|
|
963
|
+
await entry.transport.close();
|
|
964
|
+
this._logger.debug('Removed connection from pool', { data: { reason: 'unhealthy or idle' } });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Ensure minimum connections
|
|
969
|
+
while (this._pool.length < this._config.minConnections && !this._closed) {
|
|
970
|
+
try {
|
|
971
|
+
await this._createConnection();
|
|
972
|
+
} catch (error) {
|
|
973
|
+
this._logger.warn('Failed to create connection for pool', {
|
|
974
|
+
data: { errorMessage: error instanceof Error ? error.message : String(error) },
|
|
975
|
+
});
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}, this._config.healthCheckIntervalMs);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
private _stopHealthCheck(): void {
|
|
983
|
+
if (this._healthCheckInterval) {
|
|
984
|
+
clearInterval(this._healthCheckInterval);
|
|
985
|
+
this._healthCheckInterval = null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|