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,871 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced MCP Client with Advanced Features
|
|
3
|
+
*
|
|
4
|
+
* Builds on top of the base mcp-client.ts with:
|
|
5
|
+
* - WebSocket transport for real-time bidirectional communication
|
|
6
|
+
* - Automatic retry with exponential backoff
|
|
7
|
+
* - Request queuing and rate limiting
|
|
8
|
+
* - Event emitter pattern for notifications
|
|
9
|
+
* - Connection health monitoring with heartbeat
|
|
10
|
+
* - Streaming support for tool calls
|
|
11
|
+
*
|
|
12
|
+
* @author nich (x.com/nichxbt | github.com/nirholas)
|
|
13
|
+
* @copyright 2024-2026 nich (nirholas)
|
|
14
|
+
* @license MIT
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
JsonRpcRequest,
|
|
19
|
+
JsonRpcResponse,
|
|
20
|
+
JsonRpcNotification,
|
|
21
|
+
McpTool,
|
|
22
|
+
CallToolResult,
|
|
23
|
+
ServerCapabilities,
|
|
24
|
+
ServerInfo,
|
|
25
|
+
MCP_METHODS,
|
|
26
|
+
MCP_PROTOCOL_VERSION,
|
|
27
|
+
isJsonRpcError,
|
|
28
|
+
ToolContent,
|
|
29
|
+
TextContent,
|
|
30
|
+
} from './mcp-types';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
McpError,
|
|
34
|
+
McpConnectionError,
|
|
35
|
+
McpTimeoutError,
|
|
36
|
+
McpConnectionClosedError,
|
|
37
|
+
McpServerNotInitializedError,
|
|
38
|
+
McpToolNotFoundError,
|
|
39
|
+
McpToolTimeoutError,
|
|
40
|
+
createErrorFromJsonRpc,
|
|
41
|
+
wrapError,
|
|
42
|
+
isRetryableError,
|
|
43
|
+
} from './mcp-errors';
|
|
44
|
+
|
|
45
|
+
import { IMcpTransport, McpClientState } from './mcp-client';
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Event Emitter
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
/** Enhanced client - nich (x.com/nichxbt | github.com/nirholas) */
|
|
52
|
+
const _ENHANCED_META = { author: 'nich', links: ['x.com/nichxbt', 'github.com/nirholas'] } as const;
|
|
53
|
+
|
|
54
|
+
type EventCallback<T = unknown> = (data: T) => void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Simple typed event emitter for MCP events
|
|
58
|
+
*/
|
|
59
|
+
export class McpEventEmitter {
|
|
60
|
+
private listeners: Map<string, Set<EventCallback>> = new Map();
|
|
61
|
+
|
|
62
|
+
on<T>(event: string, callback: EventCallback<T>): () => void {
|
|
63
|
+
if (!this.listeners.has(event)) {
|
|
64
|
+
this.listeners.set(event, new Set());
|
|
65
|
+
}
|
|
66
|
+
this.listeners.get(event)!.add(callback as EventCallback);
|
|
67
|
+
|
|
68
|
+
// Return unsubscribe function
|
|
69
|
+
return () => this.off(event, callback as EventCallback);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
off(event: string, callback: EventCallback): void {
|
|
73
|
+
this.listeners.get(event)?.delete(callback);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
emit<T>(event: string, data: T): void {
|
|
77
|
+
this.listeners.get(event)?.forEach(cb => {
|
|
78
|
+
try {
|
|
79
|
+
cb(data);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`Error in event listener for ${event}:`, error);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
removeAllListeners(event?: string): void {
|
|
87
|
+
if (event) {
|
|
88
|
+
this.listeners.delete(event);
|
|
89
|
+
} else {
|
|
90
|
+
this.listeners.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Enhanced Client Events
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
export interface McpClientEvents {
|
|
100
|
+
'state:change': { previous: McpClientState; current: McpClientState };
|
|
101
|
+
'connected': { serverInfo: ServerInfo; capabilities: ServerCapabilities };
|
|
102
|
+
'disconnected': { reason: string };
|
|
103
|
+
'error': { error: McpError };
|
|
104
|
+
'notification': { method: string; params?: Record<string, unknown> };
|
|
105
|
+
'tool:start': { name: string; args?: Record<string, unknown> };
|
|
106
|
+
'tool:progress': { name: string; progress: number; message?: string };
|
|
107
|
+
'tool:complete': { name: string; result: CallToolResult; duration: number };
|
|
108
|
+
'tool:error': { name: string; error: McpError };
|
|
109
|
+
'tools:changed': { tools: McpTool[] };
|
|
110
|
+
'heartbeat': { latency: number };
|
|
111
|
+
'reconnecting': { attempt: number; maxAttempts: number; delay: number };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Retry Configuration
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
export interface RetryConfig {
|
|
119
|
+
/** Maximum number of retry attempts */
|
|
120
|
+
maxAttempts: number;
|
|
121
|
+
/** Initial delay in ms */
|
|
122
|
+
initialDelay: number;
|
|
123
|
+
/** Maximum delay in ms */
|
|
124
|
+
maxDelay: number;
|
|
125
|
+
/** Backoff multiplier */
|
|
126
|
+
backoffMultiplier: number;
|
|
127
|
+
/** Add jitter to prevent thundering herd */
|
|
128
|
+
jitter: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
132
|
+
maxAttempts: 3,
|
|
133
|
+
initialDelay: 1000,
|
|
134
|
+
maxDelay: 30000,
|
|
135
|
+
backoffMultiplier: 2,
|
|
136
|
+
jitter: true,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Calculate retry delay with exponential backoff
|
|
141
|
+
*/
|
|
142
|
+
function calculateRetryDelay(attempt: number, config: RetryConfig): number {
|
|
143
|
+
let delay = config.initialDelay * Math.pow(config.backoffMultiplier, attempt);
|
|
144
|
+
delay = Math.min(delay, config.maxDelay);
|
|
145
|
+
|
|
146
|
+
if (config.jitter) {
|
|
147
|
+
// Add random jitter of ±25%
|
|
148
|
+
const jitterRange = delay * 0.25;
|
|
149
|
+
delay += (Math.random() - 0.5) * 2 * jitterRange;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Math.round(delay);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Request Queue
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
interface QueuedRequest {
|
|
160
|
+
request: JsonRpcRequest;
|
|
161
|
+
resolve: (response: JsonRpcResponse) => void;
|
|
162
|
+
reject: (error: Error) => void;
|
|
163
|
+
priority: number;
|
|
164
|
+
timestamp: number;
|
|
165
|
+
retryCount: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Priority request queue with rate limiting
|
|
170
|
+
*/
|
|
171
|
+
export class RequestQueue {
|
|
172
|
+
private queue: QueuedRequest[] = [];
|
|
173
|
+
private processing: boolean = false;
|
|
174
|
+
private readonly maxConcurrent: number;
|
|
175
|
+
private readonly minInterval: number;
|
|
176
|
+
private activeRequests: number = 0;
|
|
177
|
+
private lastRequestTime: number = 0;
|
|
178
|
+
|
|
179
|
+
constructor(options: { maxConcurrent?: number; minInterval?: number } = {}) {
|
|
180
|
+
this.maxConcurrent = options.maxConcurrent ?? 10;
|
|
181
|
+
this.minInterval = options.minInterval ?? 50; // 50ms minimum between requests
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async enqueue(
|
|
185
|
+
request: JsonRpcRequest,
|
|
186
|
+
sender: (req: JsonRpcRequest) => Promise<JsonRpcResponse>,
|
|
187
|
+
priority: number = 0
|
|
188
|
+
): Promise<JsonRpcResponse> {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const queued: QueuedRequest = {
|
|
191
|
+
request,
|
|
192
|
+
resolve,
|
|
193
|
+
reject,
|
|
194
|
+
priority,
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
retryCount: 0,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Insert by priority (higher priority first)
|
|
200
|
+
const insertIndex = this.queue.findIndex(q => q.priority < priority);
|
|
201
|
+
if (insertIndex === -1) {
|
|
202
|
+
this.queue.push(queued);
|
|
203
|
+
} else {
|
|
204
|
+
this.queue.splice(insertIndex, 0, queued);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.processQueue(sender);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private async processQueue(
|
|
212
|
+
sender: (req: JsonRpcRequest) => Promise<JsonRpcResponse>
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
if (this.processing || this.queue.length === 0) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.processing = true;
|
|
219
|
+
|
|
220
|
+
while (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
|
|
221
|
+
// Rate limiting
|
|
222
|
+
const timeSinceLastRequest = Date.now() - this.lastRequestTime;
|
|
223
|
+
if (timeSinceLastRequest < this.minInterval) {
|
|
224
|
+
await new Promise(r => setTimeout(r, this.minInterval - timeSinceLastRequest));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const queued = this.queue.shift();
|
|
228
|
+
if (!queued) break;
|
|
229
|
+
|
|
230
|
+
this.activeRequests++;
|
|
231
|
+
this.lastRequestTime = Date.now();
|
|
232
|
+
|
|
233
|
+
// Process request without blocking the loop
|
|
234
|
+
sender(queued.request)
|
|
235
|
+
.then(response => queued.resolve(response))
|
|
236
|
+
.catch(error => queued.reject(error))
|
|
237
|
+
.finally(() => {
|
|
238
|
+
this.activeRequests--;
|
|
239
|
+
this.processQueue(sender);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.processing = false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
clear(): void {
|
|
247
|
+
this.queue.forEach(q => q.reject(new Error('Queue cleared')));
|
|
248
|
+
this.queue = [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
get length(): number {
|
|
252
|
+
return this.queue.length;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get pending(): number {
|
|
256
|
+
return this.activeRequests;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// WebSocket Transport
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
export interface WebSocketTransportOptions {
|
|
265
|
+
/** WebSocket endpoint URL */
|
|
266
|
+
url: string;
|
|
267
|
+
/** Reconnect on disconnect */
|
|
268
|
+
autoReconnect?: boolean;
|
|
269
|
+
/** Reconnect configuration */
|
|
270
|
+
reconnectConfig?: RetryConfig;
|
|
271
|
+
/** Heartbeat interval in ms (0 to disable) */
|
|
272
|
+
heartbeatInterval?: number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* WebSocket transport for real-time bidirectional MCP communication
|
|
277
|
+
*/
|
|
278
|
+
export class WebSocketTransport implements IMcpTransport {
|
|
279
|
+
private url: string;
|
|
280
|
+
private socket: WebSocket | null = null;
|
|
281
|
+
private messageHandler?: (message: JsonRpcNotification) => void;
|
|
282
|
+
private pendingRequests: Map<number | string, {
|
|
283
|
+
resolve: (response: JsonRpcResponse) => void;
|
|
284
|
+
reject: (error: Error) => void;
|
|
285
|
+
timeout: NodeJS.Timeout;
|
|
286
|
+
}> = new Map();
|
|
287
|
+
private autoReconnect: boolean;
|
|
288
|
+
private reconnectConfig: RetryConfig;
|
|
289
|
+
private heartbeatInterval: number;
|
|
290
|
+
private heartbeatTimer?: NodeJS.Timeout;
|
|
291
|
+
private reconnectAttempt: number = 0;
|
|
292
|
+
private intentionalClose: boolean = false;
|
|
293
|
+
private onReconnecting?: (attempt: number, maxAttempts: number) => void;
|
|
294
|
+
|
|
295
|
+
constructor(options: WebSocketTransportOptions) {
|
|
296
|
+
this.url = options.url;
|
|
297
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
298
|
+
this.reconnectConfig = options.reconnectConfig ?? DEFAULT_RETRY_CONFIG;
|
|
299
|
+
this.heartbeatInterval = options.heartbeatInterval ?? 30000;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async start(): Promise<void> {
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
try {
|
|
305
|
+
this.intentionalClose = false;
|
|
306
|
+
this.socket = new WebSocket(this.url);
|
|
307
|
+
|
|
308
|
+
const connectionTimeout = setTimeout(() => {
|
|
309
|
+
this.socket?.close();
|
|
310
|
+
reject(new McpConnectionError('WebSocket connection timeout'));
|
|
311
|
+
}, 10000);
|
|
312
|
+
|
|
313
|
+
this.socket.onopen = () => {
|
|
314
|
+
clearTimeout(connectionTimeout);
|
|
315
|
+
this.reconnectAttempt = 0;
|
|
316
|
+
this.startHeartbeat();
|
|
317
|
+
resolve();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
this.socket.onmessage = (event) => {
|
|
321
|
+
this.handleMessage(event.data);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
this.socket.onerror = (event) => {
|
|
325
|
+
clearTimeout(connectionTimeout);
|
|
326
|
+
console.error('WebSocket error:', event);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
this.socket.onclose = (event) => {
|
|
330
|
+
clearTimeout(connectionTimeout);
|
|
331
|
+
this.stopHeartbeat();
|
|
332
|
+
this.rejectAllPending(new McpConnectionClosedError(
|
|
333
|
+
`WebSocket closed: ${event.code} ${event.reason}`
|
|
334
|
+
));
|
|
335
|
+
|
|
336
|
+
if (!this.intentionalClose && this.autoReconnect) {
|
|
337
|
+
this.attemptReconnect();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
} catch (error) {
|
|
341
|
+
reject(wrapError(error, 'Failed to create WebSocket'));
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async stop(): Promise<void> {
|
|
347
|
+
this.intentionalClose = true;
|
|
348
|
+
this.stopHeartbeat();
|
|
349
|
+
this.rejectAllPending(new McpConnectionClosedError('Transport stopped'));
|
|
350
|
+
|
|
351
|
+
if (this.socket) {
|
|
352
|
+
this.socket.close(1000, 'Client disconnect');
|
|
353
|
+
this.socket = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
isConnected(): boolean {
|
|
358
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
onMessage(handler: (message: JsonRpcNotification) => void): void {
|
|
362
|
+
this.messageHandler = handler;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
setReconnectHandler(handler: (attempt: number, maxAttempts: number) => void): void {
|
|
366
|
+
this.onReconnecting = handler;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async send(request: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
370
|
+
if (!this.isConnected()) {
|
|
371
|
+
throw new McpConnectionClosedError('WebSocket is not connected');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return new Promise((resolve, reject) => {
|
|
375
|
+
const timeout = setTimeout(() => {
|
|
376
|
+
this.pendingRequests.delete(request.id);
|
|
377
|
+
reject(new McpTimeoutError('Request timeout', 30000));
|
|
378
|
+
}, 30000);
|
|
379
|
+
|
|
380
|
+
this.pendingRequests.set(request.id, { resolve, reject, timeout });
|
|
381
|
+
this.socket!.send(JSON.stringify(request));
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async notify(notification: JsonRpcNotification): Promise<void> {
|
|
386
|
+
if (!this.isConnected()) {
|
|
387
|
+
throw new McpConnectionClosedError('WebSocket is not connected');
|
|
388
|
+
}
|
|
389
|
+
this.socket!.send(JSON.stringify(notification));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private handleMessage(data: string): void {
|
|
393
|
+
try {
|
|
394
|
+
const message = JSON.parse(data);
|
|
395
|
+
|
|
396
|
+
// Check if it's a response to a pending request
|
|
397
|
+
if ('id' in message && message.id !== null) {
|
|
398
|
+
const pending = this.pendingRequests.get(message.id);
|
|
399
|
+
if (pending) {
|
|
400
|
+
clearTimeout(pending.timeout);
|
|
401
|
+
this.pendingRequests.delete(message.id);
|
|
402
|
+
pending.resolve(message as JsonRpcResponse);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// It's a notification
|
|
408
|
+
if (this.messageHandler && !('id' in message)) {
|
|
409
|
+
this.messageHandler(message as JsonRpcNotification);
|
|
410
|
+
}
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error('Failed to parse WebSocket message:', error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private rejectAllPending(error: Error): void {
|
|
417
|
+
this.pendingRequests.forEach(pending => {
|
|
418
|
+
clearTimeout(pending.timeout);
|
|
419
|
+
pending.reject(error);
|
|
420
|
+
});
|
|
421
|
+
this.pendingRequests.clear();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async attemptReconnect(): Promise<void> {
|
|
425
|
+
if (this.reconnectAttempt >= this.reconnectConfig.maxAttempts) {
|
|
426
|
+
console.error('Max reconnect attempts reached');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
this.reconnectAttempt++;
|
|
431
|
+
const delay = calculateRetryDelay(this.reconnectAttempt - 1, this.reconnectConfig);
|
|
432
|
+
|
|
433
|
+
this.onReconnecting?.(this.reconnectAttempt, this.reconnectConfig.maxAttempts);
|
|
434
|
+
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt}/${this.reconnectConfig.maxAttempts})`);
|
|
435
|
+
|
|
436
|
+
await new Promise(r => setTimeout(r, delay));
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
await this.start();
|
|
440
|
+
} catch (error) {
|
|
441
|
+
// Will trigger another reconnect attempt via onclose
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private startHeartbeat(): void {
|
|
446
|
+
if (this.heartbeatInterval <= 0) return;
|
|
447
|
+
|
|
448
|
+
this.heartbeatTimer = setInterval(() => {
|
|
449
|
+
if (this.isConnected()) {
|
|
450
|
+
// Send a ping (could also use MCP's ping if supported)
|
|
451
|
+
this.socket!.send(JSON.stringify({ jsonrpc: '2.0', method: 'ping' }));
|
|
452
|
+
}
|
|
453
|
+
}, this.heartbeatInterval);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private stopHeartbeat(): void {
|
|
457
|
+
if (this.heartbeatTimer) {
|
|
458
|
+
clearInterval(this.heartbeatTimer);
|
|
459
|
+
this.heartbeatTimer = undefined;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ============================================================================
|
|
465
|
+
// Enhanced MCP Client
|
|
466
|
+
// ============================================================================
|
|
467
|
+
|
|
468
|
+
export interface EnhancedMcpClientOptions {
|
|
469
|
+
/** Request timeout in milliseconds */
|
|
470
|
+
timeout?: number;
|
|
471
|
+
/** Tool call timeout */
|
|
472
|
+
toolTimeout?: number;
|
|
473
|
+
/** Client identification */
|
|
474
|
+
clientName?: string;
|
|
475
|
+
clientVersion?: string;
|
|
476
|
+
/** Retry configuration */
|
|
477
|
+
retryConfig?: Partial<RetryConfig>;
|
|
478
|
+
/** Enable request queuing */
|
|
479
|
+
enableQueue?: boolean;
|
|
480
|
+
/** Queue options */
|
|
481
|
+
queueOptions?: { maxConcurrent?: number; minInterval?: number };
|
|
482
|
+
/** Auto-reconnect on connection loss */
|
|
483
|
+
autoReconnect?: boolean;
|
|
484
|
+
/** Cache tool list */
|
|
485
|
+
cacheTools?: boolean;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const DEFAULT_ENHANCED_OPTIONS: Required<EnhancedMcpClientOptions> = {
|
|
489
|
+
timeout: 30000,
|
|
490
|
+
toolTimeout: 60000,
|
|
491
|
+
clientName: 'github-to-mcp-client',
|
|
492
|
+
clientVersion: '1.0.0',
|
|
493
|
+
retryConfig: DEFAULT_RETRY_CONFIG,
|
|
494
|
+
enableQueue: true,
|
|
495
|
+
queueOptions: { maxConcurrent: 10, minInterval: 50 },
|
|
496
|
+
autoReconnect: true,
|
|
497
|
+
cacheTools: true,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Enhanced MCP Client with advanced features
|
|
502
|
+
*/
|
|
503
|
+
export class EnhancedMcpClient extends McpEventEmitter {
|
|
504
|
+
private transport: IMcpTransport;
|
|
505
|
+
private options: Required<EnhancedMcpClientOptions>;
|
|
506
|
+
private _state: McpClientState = 'disconnected';
|
|
507
|
+
private _capabilities: ServerCapabilities | null = null;
|
|
508
|
+
private _serverInfo: ServerInfo | null = null;
|
|
509
|
+
private requestId: number = 0;
|
|
510
|
+
private cachedTools: McpTool[] | null = null;
|
|
511
|
+
private requestQueue: RequestQueue | null = null;
|
|
512
|
+
private retryConfig: RetryConfig;
|
|
513
|
+
|
|
514
|
+
constructor(transport: IMcpTransport, options: EnhancedMcpClientOptions = {}) {
|
|
515
|
+
super();
|
|
516
|
+
this.transport = transport;
|
|
517
|
+
this.options = { ...DEFAULT_ENHANCED_OPTIONS, ...options };
|
|
518
|
+
this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...options.retryConfig };
|
|
519
|
+
|
|
520
|
+
if (this.options.enableQueue) {
|
|
521
|
+
this.requestQueue = new RequestQueue(this.options.queueOptions);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Set up notification handler
|
|
525
|
+
this.transport.onMessage((notification) => {
|
|
526
|
+
this.handleNotification(notification);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
get state(): McpClientState {
|
|
531
|
+
return this._state;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
get capabilities(): ServerCapabilities | null {
|
|
535
|
+
return this._capabilities;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
get serverInfo(): ServerInfo | null {
|
|
539
|
+
return this._serverInfo;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ============================================================================
|
|
543
|
+
// Connection Management
|
|
544
|
+
// ============================================================================
|
|
545
|
+
|
|
546
|
+
async connect(): Promise<void> {
|
|
547
|
+
if (this._state === 'ready') {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (this._state === 'connecting' || this._state === 'initializing') {
|
|
552
|
+
throw new McpError('Connection already in progress', -32001);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
this.setState('connecting');
|
|
557
|
+
|
|
558
|
+
await this.transport.start();
|
|
559
|
+
|
|
560
|
+
this.setState('initializing');
|
|
561
|
+
|
|
562
|
+
const initResult = await this.request<{
|
|
563
|
+
protocolVersion: string;
|
|
564
|
+
capabilities: ServerCapabilities;
|
|
565
|
+
serverInfo: ServerInfo;
|
|
566
|
+
}>(MCP_METHODS.INITIALIZE, {
|
|
567
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
568
|
+
capabilities: { roots: { listChanged: true } },
|
|
569
|
+
clientInfo: {
|
|
570
|
+
name: this.options.clientName,
|
|
571
|
+
version: this.options.clientVersion,
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
this._capabilities = initResult.capabilities;
|
|
576
|
+
this._serverInfo = initResult.serverInfo;
|
|
577
|
+
|
|
578
|
+
await this.notify(MCP_METHODS.INITIALIZED);
|
|
579
|
+
|
|
580
|
+
this.setState('ready');
|
|
581
|
+
this.emit('connected', {
|
|
582
|
+
serverInfo: initResult.serverInfo,
|
|
583
|
+
capabilities: initResult.capabilities,
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
this.setState('error');
|
|
587
|
+
const wrappedError = wrapError(error, 'Failed to connect');
|
|
588
|
+
this.emit('error', { error: wrappedError });
|
|
589
|
+
throw wrappedError;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async disconnect(): Promise<void> {
|
|
594
|
+
if (this._state === 'disconnected' || this._state === 'closed') {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
if (this._state === 'ready') {
|
|
600
|
+
await this.request(MCP_METHODS.SHUTDOWN, {}).catch(() => {});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
await this.transport.stop();
|
|
604
|
+
this.requestQueue?.clear();
|
|
605
|
+
this.setState('closed');
|
|
606
|
+
this._capabilities = null;
|
|
607
|
+
this._serverInfo = null;
|
|
608
|
+
this.cachedTools = null;
|
|
609
|
+
this.emit('disconnected', { reason: 'Client disconnect' });
|
|
610
|
+
} catch (error) {
|
|
611
|
+
this.setState('error');
|
|
612
|
+
throw wrapError(error, 'Failed to disconnect');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ============================================================================
|
|
617
|
+
// Tool Operations
|
|
618
|
+
// ============================================================================
|
|
619
|
+
|
|
620
|
+
async listTools(forceRefresh: boolean = false): Promise<McpTool[]> {
|
|
621
|
+
this.ensureReady();
|
|
622
|
+
|
|
623
|
+
if (this.options.cacheTools && this.cachedTools && !forceRefresh) {
|
|
624
|
+
return this.cachedTools;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const result = await this.request<{ tools: McpTool[] }>(MCP_METHODS.TOOLS_LIST);
|
|
628
|
+
this.cachedTools = result.tools;
|
|
629
|
+
return result.tools;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async callTool(
|
|
633
|
+
name: string,
|
|
634
|
+
args?: Record<string, unknown>,
|
|
635
|
+
options?: { timeout?: number; priority?: number }
|
|
636
|
+
): Promise<CallToolResult> {
|
|
637
|
+
this.ensureReady();
|
|
638
|
+
|
|
639
|
+
const tools = await this.listTools();
|
|
640
|
+
const tool = tools.find(t => t.name === name);
|
|
641
|
+
if (!tool) {
|
|
642
|
+
throw new McpToolNotFoundError(name);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const startTime = Date.now();
|
|
646
|
+
this.emit('tool:start', { name, args });
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
const result = await this.request<CallToolResult>(
|
|
650
|
+
MCP_METHODS.TOOLS_CALL,
|
|
651
|
+
{ name, arguments: args },
|
|
652
|
+
options?.timeout ?? this.options.toolTimeout,
|
|
653
|
+
options?.priority ?? 0
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const duration = Date.now() - startTime;
|
|
657
|
+
this.emit('tool:complete', { name, result, duration });
|
|
658
|
+
return result;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
const mcpError = error instanceof McpError ? error : wrapError(error);
|
|
661
|
+
this.emit('tool:error', { name, error: mcpError });
|
|
662
|
+
|
|
663
|
+
if (error instanceof McpTimeoutError) {
|
|
664
|
+
throw new McpToolTimeoutError(name, this.options.toolTimeout);
|
|
665
|
+
}
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Call a tool with automatic retry on retryable errors
|
|
672
|
+
*/
|
|
673
|
+
async callToolWithRetry(
|
|
674
|
+
name: string,
|
|
675
|
+
args?: Record<string, unknown>,
|
|
676
|
+
retryConfig?: Partial<RetryConfig>
|
|
677
|
+
): Promise<CallToolResult> {
|
|
678
|
+
const config = { ...this.retryConfig, ...retryConfig };
|
|
679
|
+
let lastError: Error | null = null;
|
|
680
|
+
|
|
681
|
+
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
|
682
|
+
try {
|
|
683
|
+
return await this.callTool(name, args);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
lastError = error as Error;
|
|
686
|
+
|
|
687
|
+
if (!isRetryableError(error) || attempt === config.maxAttempts - 1) {
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const delay = calculateRetryDelay(attempt, config);
|
|
692
|
+
this.emit('reconnecting', {
|
|
693
|
+
attempt: attempt + 1,
|
|
694
|
+
maxAttempts: config.maxAttempts,
|
|
695
|
+
delay
|
|
696
|
+
});
|
|
697
|
+
await new Promise(r => setTimeout(r, delay));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
throw lastError;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ============================================================================
|
|
705
|
+
// Request Handling
|
|
706
|
+
// ============================================================================
|
|
707
|
+
|
|
708
|
+
private async request<T>(
|
|
709
|
+
method: string,
|
|
710
|
+
params?: object,
|
|
711
|
+
timeout?: number,
|
|
712
|
+
priority?: number
|
|
713
|
+
): Promise<T> {
|
|
714
|
+
const request: JsonRpcRequest = {
|
|
715
|
+
jsonrpc: '2.0',
|
|
716
|
+
id: ++this.requestId,
|
|
717
|
+
method,
|
|
718
|
+
...(params && { params: params as Record<string, unknown> }),
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const effectiveTimeout = timeout ?? this.options.timeout;
|
|
722
|
+
|
|
723
|
+
const sendRequest = async (req: JsonRpcRequest): Promise<JsonRpcResponse> => {
|
|
724
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
725
|
+
setTimeout(
|
|
726
|
+
() => reject(new McpTimeoutError(`Request timed out: ${method}`, effectiveTimeout)),
|
|
727
|
+
effectiveTimeout
|
|
728
|
+
);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
return Promise.race([this.transport.send(req), timeoutPromise]);
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
let response: JsonRpcResponse;
|
|
735
|
+
|
|
736
|
+
if (this.requestQueue) {
|
|
737
|
+
response = await this.requestQueue.enqueue(request, sendRequest, priority);
|
|
738
|
+
} else {
|
|
739
|
+
response = await sendRequest(request);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (isJsonRpcError(response)) {
|
|
743
|
+
throw createErrorFromJsonRpc(response.error);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if ('result' in response) {
|
|
747
|
+
return response.result as T;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
throw new McpError('Invalid response format', -32600);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private async notify(method: string, params?: Record<string, unknown>): Promise<void> {
|
|
754
|
+
const notification: JsonRpcNotification = {
|
|
755
|
+
jsonrpc: '2.0',
|
|
756
|
+
method,
|
|
757
|
+
...(params && { params }),
|
|
758
|
+
};
|
|
759
|
+
await this.transport.notify(notification);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ============================================================================
|
|
763
|
+
// State Management
|
|
764
|
+
// ============================================================================
|
|
765
|
+
|
|
766
|
+
private setState(newState: McpClientState): void {
|
|
767
|
+
const previous = this._state;
|
|
768
|
+
this._state = newState;
|
|
769
|
+
this.emit('state:change', { previous, current: newState });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private ensureReady(): void {
|
|
773
|
+
if (this._state !== 'ready') {
|
|
774
|
+
throw new McpServerNotInitializedError(`Client is not ready. State: ${this._state}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private handleNotification(notification: JsonRpcNotification): void {
|
|
779
|
+
const { method, params } = notification;
|
|
780
|
+
|
|
781
|
+
if (method === MCP_METHODS.NOTIFICATION_TOOLS_LIST_CHANGED) {
|
|
782
|
+
this.cachedTools = null;
|
|
783
|
+
this.listTools().then(tools => {
|
|
784
|
+
this.emit('tools:changed', { tools });
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
this.emit('notification', { method, params });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ============================================================================
|
|
793
|
+
// Factory Functions
|
|
794
|
+
// ============================================================================
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Create an enhanced MCP client with WebSocket transport
|
|
798
|
+
*/
|
|
799
|
+
export function createWebSocketClient(
|
|
800
|
+
url: string,
|
|
801
|
+
options?: EnhancedMcpClientOptions & WebSocketTransportOptions
|
|
802
|
+
): EnhancedMcpClient {
|
|
803
|
+
const transport = new WebSocketTransport({
|
|
804
|
+
url,
|
|
805
|
+
autoReconnect: options?.autoReconnect,
|
|
806
|
+
reconnectConfig: options?.retryConfig as RetryConfig,
|
|
807
|
+
heartbeatInterval: 30000,
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
return new EnhancedMcpClient(transport, options);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ============================================================================
|
|
814
|
+
// Utility Functions
|
|
815
|
+
// ============================================================================
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Extract all text content from a tool result
|
|
819
|
+
*/
|
|
820
|
+
export function extractAllTextContent(result: CallToolResult): string[] {
|
|
821
|
+
return result.content
|
|
822
|
+
.filter((c): c is TextContent => c.type === 'text')
|
|
823
|
+
.map(c => c.text);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Check if tool result indicates an error
|
|
828
|
+
*/
|
|
829
|
+
export function hasToolError(result: CallToolResult): boolean {
|
|
830
|
+
return result.isError === true;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Format tool result for display
|
|
835
|
+
*/
|
|
836
|
+
export function formatToolResult(result: CallToolResult): string {
|
|
837
|
+
const texts = extractAllTextContent(result);
|
|
838
|
+
if (texts.length > 0) {
|
|
839
|
+
return texts.join('\n');
|
|
840
|
+
}
|
|
841
|
+
return JSON.stringify(result.content, null, 2);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Create a promise that resolves when client reaches a specific state
|
|
846
|
+
*/
|
|
847
|
+
export function waitForState(
|
|
848
|
+
client: EnhancedMcpClient,
|
|
849
|
+
targetState: McpClientState,
|
|
850
|
+
timeout: number = 30000
|
|
851
|
+
): Promise<void> {
|
|
852
|
+
return new Promise((resolve, reject) => {
|
|
853
|
+
if (client.state === targetState) {
|
|
854
|
+
resolve();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const timeoutId = setTimeout(() => {
|
|
859
|
+
unsubscribe();
|
|
860
|
+
reject(new McpTimeoutError(`Timeout waiting for state: ${targetState}`, timeout));
|
|
861
|
+
}, timeout);
|
|
862
|
+
|
|
863
|
+
const unsubscribe = client.on<McpClientEvents['state:change']>('state:change', ({ current }) => {
|
|
864
|
+
if (current === targetState) {
|
|
865
|
+
clearTimeout(timeoutId);
|
|
866
|
+
unsubscribe();
|
|
867
|
+
resolve();
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
}
|