This is page 3 of 8. Use http://codebase.md/jakedismo/master-mcp-server?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .env.example
├── .eslintignore
├── .eslintrc.cjs
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .prettierrc.json
├── CHANGELOG.md
├── config
│ ├── default.json
│ ├── development.json
│ ├── production.json
│ └── schema.json
├── config.json
├── CONTRIBUTING.md
├── debug-stdio.cjs
├── debug-stdio.js
├── deploy
│ ├── cloudflare
│ │ ├── .gitkeep
│ │ ├── README.md
│ │ └── wrangler.toml
│ ├── docker
│ │ ├── .gitkeep
│ │ ├── docker-compose.yml
│ │ ├── Dockerfile
│ │ └── entrypoint.sh
│ ├── koyeb
│ │ ├── .gitkeep
│ │ └── koyeb.yaml
│ └── README.md
├── docker-compose.yml
├── Dockerfile
├── docs
│ ├── .DS_Store
│ ├── .vitepress
│ │ ├── cache
│ │ │ └── deps
│ │ │ ├── _metadata.json
│ │ │ ├── chunk-HVR2FF6M.js
│ │ │ ├── chunk-HVR2FF6M.js.map
│ │ │ ├── chunk-P2XGSYO7.js
│ │ │ ├── chunk-P2XGSYO7.js.map
│ │ │ ├── package.json
│ │ │ ├── vitepress___@vue_devtools-api.js
│ │ │ ├── vitepress___@vue_devtools-api.js.map
│ │ │ ├── vitepress___@vueuse_core.js
│ │ │ ├── vitepress___@vueuse_core.js.map
│ │ │ ├── vitepress___@vueuse_integrations_useFocusTrap.js
│ │ │ ├── vitepress___@vueuse_integrations_useFocusTrap.js.map
│ │ │ ├── vitepress___mark__js_src_vanilla__js.js
│ │ │ ├── vitepress___mark__js_src_vanilla__js.js.map
│ │ │ ├── vitepress___minisearch.js
│ │ │ ├── vitepress___minisearch.js.map
│ │ │ ├── vue.js
│ │ │ └── vue.js.map
│ │ ├── config.ts
│ │ ├── dist
│ │ │ ├── 404.html
│ │ │ ├── advanced
│ │ │ │ ├── extensibility.html
│ │ │ │ ├── index.html
│ │ │ │ ├── monitoring.html
│ │ │ │ ├── performance.html
│ │ │ │ └── security.html
│ │ │ ├── api
│ │ │ │ ├── index.html
│ │ │ │ └── README.html
│ │ │ ├── assets
│ │ │ │ ├── advanced_extensibility.md.TrXUn5w5.js
│ │ │ │ ├── advanced_extensibility.md.TrXUn5w5.lean.js
│ │ │ │ ├── advanced_index.md.CPcpUlw_.js
│ │ │ │ ├── advanced_index.md.CPcpUlw_.lean.js
│ │ │ │ ├── advanced_monitoring.md.DTybdNg-.js
│ │ │ │ ├── advanced_monitoring.md.DTybdNg-.lean.js
│ │ │ │ ├── advanced_performance.md.DKmzK0ia.js
│ │ │ │ ├── advanced_performance.md.DKmzK0ia.lean.js
│ │ │ │ ├── advanced_security.md.B-oBD7IB.js
│ │ │ │ ├── advanced_security.md.B-oBD7IB.lean.js
│ │ │ │ ├── api_index.md.Dl1JB08_.js
│ │ │ │ ├── api_index.md.Dl1JB08_.lean.js
│ │ │ │ ├── chunks
│ │ │ │ │ └── framework.CHl2ywxc.js
│ │ │ │ ├── configuration_environment-variables.md.Ddy3P_Wz.js
│ │ │ │ ├── configuration_environment-variables.md.Ddy3P_Wz.lean.js
│ │ │ │ ├── configuration_environment.md.DxcTQ623.js
│ │ │ │ ├── configuration_environment.md.DxcTQ623.lean.js
│ │ │ │ ├── configuration_overview.md.DIkVDv7V.js
│ │ │ │ ├── configuration_overview.md.DIkVDv7V.lean.js
│ │ │ │ ├── configuration_performance.md.DbJdmLrW.js
│ │ │ │ ├── configuration_performance.md.DbJdmLrW.lean.js
│ │ │ │ ├── configuration_reference.md.27IKWqtk.js
│ │ │ │ ├── configuration_reference.md.27IKWqtk.lean.js
│ │ │ │ ├── configuration_security.md.-OOlkzN4.js
│ │ │ │ ├── configuration_security.md.-OOlkzN4.lean.js
│ │ │ │ ├── contributing_dev-setup.md.Ceqh4w-R.js
│ │ │ │ ├── contributing_dev-setup.md.Ceqh4w-R.lean.js
│ │ │ │ ├── contributing_guidelines.md.ZEAX2yVh.js
│ │ │ │ ├── contributing_guidelines.md.ZEAX2yVh.lean.js
│ │ │ │ ├── contributing_index.md.DYq9R6wr.js
│ │ │ │ ├── contributing_index.md.DYq9R6wr.lean.js
│ │ │ │ ├── contributing_maintenance.md.k2bR0IaR.js
│ │ │ │ ├── contributing_maintenance.md.k2bR0IaR.lean.js
│ │ │ │ ├── deployment_cicd.md.Ci2T0UYC.js
│ │ │ │ ├── deployment_cicd.md.Ci2T0UYC.lean.js
│ │ │ │ ├── deployment_cloudflare-workers.md.D2WHsfep.js
│ │ │ │ ├── deployment_cloudflare-workers.md.D2WHsfep.lean.js
│ │ │ │ ├── deployment_docker.md.B8bQDQTo.js
│ │ │ │ ├── deployment_docker.md.B8bQDQTo.lean.js
│ │ │ │ ├── deployment_index.md.ClYeOkpy.js
│ │ │ │ ├── deployment_index.md.ClYeOkpy.lean.js
│ │ │ │ ├── deployment_koyeb.md.B_wJhvF7.js
│ │ │ │ ├── deployment_koyeb.md.B_wJhvF7.lean.js
│ │ │ │ ├── examples_advanced-routing.md.B3CqhLZ7.js
│ │ │ │ ├── examples_advanced-routing.md.B3CqhLZ7.lean.js
│ │ │ │ ├── examples_basic-node.md.CaDZzGlO.js
│ │ │ │ ├── examples_basic-node.md.CaDZzGlO.lean.js
│ │ │ │ ├── examples_cloudflare-worker.md.DwVSz-c7.js
│ │ │ │ ├── examples_cloudflare-worker.md.DwVSz-c7.lean.js
│ │ │ │ ├── examples_index.md.CBF_BLkl.js
│ │ │ │ ├── examples_index.md.CBF_BLkl.lean.js
│ │ │ │ ├── examples_oauth-delegation.md.1hZxoqDl.js
│ │ │ │ ├── examples_oauth-delegation.md.1hZxoqDl.lean.js
│ │ │ │ ├── examples_overview.md.CZN0JbZ7.js
│ │ │ │ ├── examples_overview.md.CZN0JbZ7.lean.js
│ │ │ │ ├── examples_testing.md.Dek4GpNs.js
│ │ │ │ ├── examples_testing.md.Dek4GpNs.lean.js
│ │ │ │ ├── getting-started_concepts.md.D7ON9iGB.js
│ │ │ │ ├── getting-started_concepts.md.D7ON9iGB.lean.js
│ │ │ │ ├── getting-started_installation.md.BKnVqAGg.js
│ │ │ │ ├── getting-started_installation.md.BKnVqAGg.lean.js
│ │ │ │ ├── getting-started_overview.md.DvJDFL2N.js
│ │ │ │ ├── getting-started_overview.md.DvJDFL2N.lean.js
│ │ │ │ ├── getting-started_quickstart-node.md.GOO4aGas.js
│ │ │ │ ├── getting-started_quickstart-node.md.GOO4aGas.lean.js
│ │ │ │ ├── getting-started_quickstart-workers.md.Cpofh8Mj.js
│ │ │ │ ├── getting-started_quickstart-workers.md.Cpofh8Mj.lean.js
│ │ │ │ ├── getting-started.md.DG9ndneo.js
│ │ │ │ ├── getting-started.md.DG9ndneo.lean.js
│ │ │ │ ├── guides_configuration-management.md.B-jwYMbA.js
│ │ │ │ ├── guides_configuration-management.md.B-jwYMbA.lean.js
│ │ │ │ ├── guides_configuration.md.Ci3zYDFA.js
│ │ │ │ ├── guides_configuration.md.Ci3zYDFA.lean.js
│ │ │ │ ├── guides_index.md.CIlq2fmx.js
│ │ │ │ ├── guides_index.md.CIlq2fmx.lean.js
│ │ │ │ ├── guides_module-loading.md.BkJvuRnQ.js
│ │ │ │ ├── guides_module-loading.md.BkJvuRnQ.lean.js
│ │ │ │ ├── guides_oauth-delegation.md.DEOZ-_G0.js
│ │ │ │ ├── guides_oauth-delegation.md.DEOZ-_G0.lean.js
│ │ │ │ ├── guides_request-routing.md.Bdzf0VLg.js
│ │ │ │ ├── guides_request-routing.md.Bdzf0VLg.lean.js
│ │ │ │ ├── guides_testing.md.kYfHqJLu.js
│ │ │ │ ├── guides_testing.md.kYfHqJLu.lean.js
│ │ │ │ ├── inter-italic-cyrillic-ext.r48I6akx.woff2
│ │ │ │ ├── inter-italic-cyrillic.By2_1cv3.woff2
│ │ │ │ ├── inter-italic-greek-ext.1u6EdAuj.woff2
│ │ │ │ ├── inter-italic-greek.DJ8dCoTZ.woff2
│ │ │ │ ├── inter-italic-latin-ext.CN1xVJS-.woff2
│ │ │ │ ├── inter-italic-latin.C2AdPX0b.woff2
│ │ │ │ ├── inter-italic-vietnamese.BSbpV94h.woff2
│ │ │ │ ├── inter-roman-cyrillic-ext.BBPuwvHQ.woff2
│ │ │ │ ├── inter-roman-cyrillic.C5lxZ8CY.woff2
│ │ │ │ ├── inter-roman-greek-ext.CqjqNYQ-.woff2
│ │ │ │ ├── inter-roman-greek.BBVDIX6e.woff2
│ │ │ │ ├── inter-roman-latin-ext.4ZJIpNVo.woff2
│ │ │ │ ├── inter-roman-latin.Di8DUHzh.woff2
│ │ │ │ ├── inter-roman-vietnamese.BjW4sHH5.woff2
│ │ │ │ ├── README.md.BO5r5M9u.js
│ │ │ │ ├── README.md.BO5r5M9u.lean.js
│ │ │ │ ├── style.BQrfSMzK.css
│ │ │ │ ├── troubleshooting_common-issues.md.CScvzWM1.js
│ │ │ │ ├── troubleshooting_common-issues.md.CScvzWM1.lean.js
│ │ │ │ ├── troubleshooting_deployment.md.DUhpqnLE.js
│ │ │ │ ├── troubleshooting_deployment.md.DUhpqnLE.lean.js
│ │ │ │ ├── troubleshooting_errors.md.BSCsEmGc.js
│ │ │ │ ├── troubleshooting_errors.md.BSCsEmGc.lean.js
│ │ │ │ ├── troubleshooting_oauth.md.Cw60Eka3.js
│ │ │ │ ├── troubleshooting_oauth.md.Cw60Eka3.lean.js
│ │ │ │ ├── troubleshooting_performance.md.DxY6LJcT.js
│ │ │ │ ├── troubleshooting_performance.md.DxY6LJcT.lean.js
│ │ │ │ ├── troubleshooting_routing.md.BHN-MDhs.js
│ │ │ │ ├── troubleshooting_routing.md.BHN-MDhs.lean.js
│ │ │ │ ├── troubleshooting_security-best-practices.md.Yiu8E-zt.js
│ │ │ │ ├── troubleshooting_security-best-practices.md.Yiu8E-zt.lean.js
│ │ │ │ ├── tutorials_beginner-getting-started.md.BXObgobW.js
│ │ │ │ ├── tutorials_beginner-getting-started.md.BXObgobW.lean.js
│ │ │ │ ├── tutorials_cloudflare-workers-tutorial.md.MPHsc0aT.js
│ │ │ │ ├── tutorials_cloudflare-workers-tutorial.md.MPHsc0aT.lean.js
│ │ │ │ ├── tutorials_load-balancing-and-resilience.md.Dv9r9jyW.js
│ │ │ │ ├── tutorials_load-balancing-and-resilience.md.Dv9r9jyW.lean.js
│ │ │ │ ├── tutorials_oauth-delegation-github.md.Nq4glqCe.js
│ │ │ │ └── tutorials_oauth-delegation-github.md.Nq4glqCe.lean.js
│ │ │ ├── configuration
│ │ │ │ ├── environment-variables.html
│ │ │ │ ├── environment.html
│ │ │ │ ├── examples.html
│ │ │ │ ├── overview.html
│ │ │ │ ├── performance.html
│ │ │ │ ├── reference.html
│ │ │ │ └── security.html
│ │ │ ├── contributing
│ │ │ │ ├── dev-setup.html
│ │ │ │ ├── guidelines.html
│ │ │ │ ├── index.html
│ │ │ │ └── maintenance.html
│ │ │ ├── deployment
│ │ │ │ ├── cicd.html
│ │ │ │ ├── cloudflare-workers.html
│ │ │ │ ├── docker.html
│ │ │ │ ├── index.html
│ │ │ │ └── koyeb.html
│ │ │ ├── diagrams
│ │ │ │ └── architecture.svg
│ │ │ ├── examples
│ │ │ │ ├── advanced-routing.html
│ │ │ │ ├── basic-node.html
│ │ │ │ ├── cloudflare-worker.html
│ │ │ │ ├── index.html
│ │ │ │ ├── oauth-delegation.html
│ │ │ │ ├── overview.html
│ │ │ │ └── testing.html
│ │ │ ├── getting-started
│ │ │ │ ├── concepts.html
│ │ │ │ ├── installation.html
│ │ │ │ ├── overview.html
│ │ │ │ ├── quick-start.html
│ │ │ │ ├── quickstart-node.html
│ │ │ │ └── quickstart-workers.html
│ │ │ ├── getting-started.html
│ │ │ ├── guides
│ │ │ │ ├── authentication.html
│ │ │ │ ├── client-integration.html
│ │ │ │ ├── configuration-management.html
│ │ │ │ ├── configuration.html
│ │ │ │ ├── index.html
│ │ │ │ ├── module-loading.html
│ │ │ │ ├── oauth-delegation.html
│ │ │ │ ├── request-routing.html
│ │ │ │ ├── server-management.html
│ │ │ │ ├── server-sharing.html
│ │ │ │ └── testing.html
│ │ │ ├── hashmap.json
│ │ │ ├── index.html
│ │ │ ├── logo.svg
│ │ │ ├── README.html
│ │ │ ├── reports
│ │ │ │ └── mcp-compliance-audit.html
│ │ │ ├── troubleshooting
│ │ │ │ ├── common-issues.html
│ │ │ │ ├── deployment.html
│ │ │ │ ├── errors.html
│ │ │ │ ├── index.html
│ │ │ │ ├── oauth.html
│ │ │ │ ├── performance.html
│ │ │ │ ├── routing.html
│ │ │ │ └── security-best-practices.html
│ │ │ ├── tutorials
│ │ │ │ ├── beginner-getting-started.html
│ │ │ │ ├── cloudflare-workers-tutorial.html
│ │ │ │ ├── load-balancing-and-resilience.html
│ │ │ │ └── oauth-delegation-github.html
│ │ │ └── vp-icons.css
│ │ └── theme
│ │ ├── components
│ │ │ ├── ApiPlayground.vue
│ │ │ ├── AuthFlowDemo.vue
│ │ │ ├── CodeTabs.vue
│ │ │ └── ConfigGenerator.vue
│ │ ├── index.ts
│ │ └── style.css
│ ├── advanced
│ │ ├── extensibility.md
│ │ ├── index.md
│ │ ├── monitoring.md
│ │ ├── performance.md
│ │ └── security.md
│ ├── api
│ │ ├── functions
│ │ │ └── createServer.md
│ │ ├── index.md
│ │ ├── interfaces
│ │ │ └── RunningServer.md
│ │ └── README.md
│ ├── architecture
│ │ └── images
│ │ └── mcp_master_architecture.svg
│ ├── configuration
│ │ ├── environment-variables.md
│ │ ├── environment.md
│ │ ├── examples.md
│ │ ├── overview.md
│ │ ├── performance.md
│ │ ├── reference.md
│ │ └── security.md
│ ├── contributing
│ │ ├── dev-setup.md
│ │ ├── guidelines.md
│ │ ├── index.md
│ │ └── maintenance.md
│ ├── deployment
│ │ ├── cicd.md
│ │ ├── cloudflare-workers.md
│ │ ├── docker.md
│ │ ├── docs-site.md
│ │ ├── index.md
│ │ └── koyeb.md
│ ├── examples
│ │ ├── advanced-routing.md
│ │ ├── basic-node.md
│ │ ├── cloudflare-worker.md
│ │ ├── index.md
│ │ ├── oauth-delegation.md
│ │ ├── overview.md
│ │ └── testing.md
│ ├── getting-started
│ │ ├── concepts.md
│ │ ├── installation.md
│ │ ├── overview.md
│ │ ├── quick-start.md
│ │ ├── quickstart-node.md
│ │ └── quickstart-workers.md
│ ├── getting-started.md
│ ├── guides
│ │ ├── authentication.md
│ │ ├── client-integration.md
│ │ ├── configuration-management.md
│ │ ├── configuration.md
│ │ ├── index.md
│ │ ├── module-loading.md
│ │ ├── oauth-delegation.md
│ │ ├── request-routing.md
│ │ ├── server-management.md
│ │ ├── server-sharing.md
│ │ └── testing.md
│ ├── index.html
│ ├── public
│ │ ├── diagrams
│ │ │ └── architecture.svg
│ │ ├── github-social.png
│ │ │ └── image.png
│ │ ├── logo.png
│ │ └── logo.svg
│ ├── README.md
│ ├── stdio-servers.md
│ ├── testing
│ │ └── phase-9-testing-architecture.md
│ ├── troubleshooting
│ │ ├── common-issues.md
│ │ ├── deployment.md
│ │ ├── errors.md
│ │ ├── index.md
│ │ ├── oauth.md
│ │ ├── performance.md
│ │ ├── routing.md
│ │ └── security-best-practices.md
│ └── tutorials
│ ├── beginner-getting-started.md
│ ├── cloudflare-workers-tutorial.md
│ ├── load-balancing-and-resilience.md
│ └── oauth-delegation-github.md
├── examples
│ ├── advanced-routing
│ │ ├── config.yaml
│ │ └── README.md
│ ├── basic-node
│ │ ├── config.yaml
│ │ ├── README.md
│ │ └── server.ts
│ ├── cloudflare-worker
│ │ ├── README.md
│ │ └── worker.ts
│ ├── custom-auth
│ │ ├── config.yaml
│ │ ├── index.ts
│ │ └── README.md
│ ├── multi-server
│ │ ├── config.yaml
│ │ └── README.md
│ ├── oauth-delegation
│ │ └── README.md
│ ├── oauth-node
│ │ ├── config.yaml
│ │ └── README.md
│ ├── performance
│ │ ├── config.yaml
│ │ └── README.md
│ ├── sample-configs
│ │ ├── basic.yaml
│ │ └── simple-setup.yaml
│ ├── security-hardening
│ │ └── README.md
│ ├── stdio-mcp-server.cjs
│ ├── test-mcp-server.js
│ └── test-stdio-server.js
├── LICENSE
├── master-mcp-definition.md
├── package-lock.json
├── package.json
├── README.md
├── reports
│ └── claude_report_20250815_222153.html
├── scripts
│ └── generate-config-docs.ts
├── src
│ ├── auth
│ │ ├── multi-auth-manager.ts
│ │ ├── oauth-providers.ts
│ │ └── token-manager.ts
│ ├── config
│ │ ├── config-loader.ts
│ │ ├── environment-manager.ts
│ │ ├── schema-validator.ts
│ │ └── secret-manager.ts
│ ├── index.ts
│ ├── mcp-server.ts
│ ├── modules
│ │ ├── capability-aggregator.ts
│ │ ├── module-loader.ts
│ │ ├── request-router.ts
│ │ ├── stdio-capability-discovery.ts
│ │ └── stdio-manager.ts
│ ├── oauth
│ │ ├── callback-handler.ts
│ │ ├── flow-controller.ts
│ │ ├── flow-validator.ts
│ │ ├── pkce-manager.ts
│ │ ├── state-manager.ts
│ │ └── web-interface.ts
│ ├── routing
│ │ ├── circuit-breaker.ts
│ │ ├── load-balancer.ts
│ │ ├── retry-handler.ts
│ │ └── route-registry.ts
│ ├── runtime
│ │ ├── node.ts
│ │ └── worker.ts
│ ├── server
│ │ ├── config-manager.ts
│ │ ├── dependency-container.ts
│ │ ├── master-server.ts
│ │ └── protocol-handler.ts
│ ├── types
│ │ ├── auth.ts
│ │ ├── config.ts
│ │ ├── jose-shim.d.ts
│ │ ├── mcp.ts
│ │ └── server.ts
│ └── utils
│ ├── cache.ts
│ ├── crypto.ts
│ ├── dev.ts
│ ├── errors.ts
│ ├── http.ts
│ ├── logger.ts
│ ├── monitoring.ts
│ ├── string.ts
│ ├── time.ts
│ ├── validation.ts
│ └── validators.ts
├── static
│ └── oauth
│ ├── consent.html
│ ├── error.html
│ ├── script.js
│ ├── style.css
│ └── success.html
├── tests
│ ├── _setup
│ │ ├── miniflare.setup.ts
│ │ └── vitest.setup.ts
│ ├── _utils
│ │ ├── log-capture.ts
│ │ ├── mock-fetch.ts
│ │ └── test-server.ts
│ ├── .gitkeep
│ ├── e2e
│ │ ├── flow-controller.express.test.ts
│ │ └── flow-controller.worker.test.ts
│ ├── factories
│ │ ├── configFactory.ts
│ │ ├── mcpFactory.ts
│ │ └── oauthFactory.ts
│ ├── fixtures
│ │ ├── capabilities.json
│ │ └── stdio-server.js
│ ├── integration
│ │ ├── modules.capability-aggregator.test.ts
│ │ ├── modules.module-loader-health.test.ts
│ │ ├── oauth.callback-handler.test.ts
│ │ └── request-router.test.ts
│ ├── mocks
│ │ ├── mcp
│ │ │ └── fake-backend.ts
│ │ └── oauth
│ │ └── mock-oidc-provider.ts
│ ├── perf
│ │ ├── artillery
│ │ │ └── auth-routing.yaml
│ │ └── perf.auth-and-routing.test.ts
│ ├── security
│ │ └── security.oauth-and-input.test.ts
│ ├── servers
│ │ ├── test-auth-simple.js
│ │ ├── test-debug.js
│ │ ├── test-master-mcp.js
│ │ ├── test-mcp-client.js
│ │ ├── test-streaming-both-complete.js
│ │ ├── test-streaming-both-full.js
│ │ ├── test-streaming-both-simple.js
│ │ ├── test-streaming-both.js
│ │ └── test-streaming.js
│ ├── setup
│ │ └── test-setup.ts
│ ├── unit
│ │ ├── auth.multi-auth-manager.test.ts
│ │ ├── auth.token-manager.test.ts
│ │ ├── config.environment-manager.test.ts
│ │ ├── config.schema-validator.test.ts
│ │ ├── config.secret-manager.test.ts
│ │ ├── modules
│ │ │ ├── stdio-capability-discovery.test.ts
│ │ │ └── stdio-manager.test.ts
│ │ ├── modules.route-registry.test.ts
│ │ ├── oauth.pkce-state.test.ts
│ │ ├── routing
│ │ │ └── circuit-breaker.test.ts
│ │ ├── routing.core.test.ts
│ │ ├── stdio-capability-discovery.test.ts
│ │ ├── utils.crypto.test.ts
│ │ ├── utils.logger.test.ts
│ │ └── utils.monitoring.test.ts
│ └── utils
│ ├── fake-express.ts
│ ├── mock-http.ts
│ ├── oauth-mocks.ts
│ └── token-storages.ts
├── tsconfig.base.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.worker.json
├── typedoc.json
├── vitest.config.ts
└── vitest.worker.config.ts
```
# Files
--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Validation and sanitization helpers with a small schema system.
* No external dependencies; suitable for Node and Workers.
*/
export function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value)
}
export function sanitizeString(input: unknown, opts?: { maxLength?: number; trim?: boolean }): string {
let s = typeof input === 'string' ? input : String(input ?? '')
if (opts?.trim !== false) s = s.trim()
// Remove control characters except tab, newline, carriage return
s = s.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
if (opts?.maxLength && s.length > opts.maxLength) s = s.slice(0, opts.maxLength)
return s
}
export function sanitizeObject<T extends Record<string, unknown>>(obj: T): T {
const dangerous = ['__proto__', 'constructor', 'prototype']
for (const k of Object.keys(obj)) {
if (dangerous.includes(k)) delete (obj as any)[k]
}
return obj
}
export function assert(condition: unknown, message = 'Assertion failed'): asserts condition {
if (!condition) throw new Error(message)
}
export function assertString(value: unknown, message = 'Expected string'): asserts value is string {
if (typeof value !== 'string') throw new Error(message)
}
export function assertNumber(value: unknown, message = 'Expected number'): asserts value is number {
if (typeof value !== 'number' || Number.isNaN(value)) throw new Error(message)
}
export function assertBoolean(value: unknown, message = 'Expected boolean'): asserts value is boolean {
if (typeof value !== 'boolean') throw new Error(message)
}
export type SafeParseResult<T> = { success: true; data: T } | { success: false; error: string }
export interface Schema<T> {
parse(input: unknown): T
safeParse(input: unknown): SafeParseResult<T>
}
function makeSchema<T>(name: string, parse: (i: unknown) => T): Schema<T> {
return {
parse(input: unknown): T {
try {
return parse(input)
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
throw new Error(`${name} validation failed: ${msg}`)
}
},
safeParse(input: unknown): SafeParseResult<T> {
try {
return { success: true, data: parse(input) }
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : String(e) }
}
},
}
}
export const v = {
string: (opts?: { min?: number; max?: number; pattern?: RegExp }) =>
makeSchema<string>('string', (i) => {
if (typeof i !== 'string') throw new Error('not a string')
const s = i
if (opts?.min !== undefined && s.length < opts.min) throw new Error(`min length ${opts.min}`)
if (opts?.max !== undefined && s.length > opts.max) throw new Error(`max length ${opts.max}`)
if (opts?.pattern && !opts.pattern.test(s)) throw new Error('pattern mismatch')
return s
}),
number: (opts?: { min?: number; max?: number; int?: boolean }) =>
makeSchema<number>('number', (i) => {
if (typeof i !== 'number' || Number.isNaN(i)) throw new Error('not a number')
const n = i
if (opts?.int) {
if (!Number.isInteger(n)) throw new Error('not an integer')
}
if (opts?.min !== undefined && n < opts.min) throw new Error(`min ${opts.min}`)
if (opts?.max !== undefined && n > opts.max) throw new Error(`max ${opts.max}`)
return n
}),
boolean: () => makeSchema<boolean>('boolean', (i) => {
if (typeof i !== 'boolean') throw new Error('not a boolean')
return i
}),
literal: <T extends string | number | boolean | null>(val: T) =>
makeSchema<T>('literal', (i) => {
if (i !== val) throw new Error(`expected ${String(val)}`)
return i as T
}),
array: <T>(inner: Schema<T>, opts?: { min?: number; max?: number }) =>
makeSchema<T[]>('array', (i) => {
if (!Array.isArray(i)) throw new Error('not an array')
if (opts?.min !== undefined && i.length < opts.min) throw new Error(`min length ${opts.min}`)
if (opts?.max !== undefined && i.length > opts.max) throw new Error(`max length ${opts.max}`)
return i.map((x) => inner.parse(x))
}),
object: <S extends Record<string, Schema<any>>>(shape: S) =>
makeSchema<{ [K in keyof S]: S[K] extends Schema<infer U> ? U : never }>('object', (i) => {
if (!isRecord(i)) throw new Error('not an object')
const out: Record<string, unknown> = {}
for (const [k, s] of Object.entries(shape)) {
out[k] = (s as Schema<unknown>).parse((i as any)[k])
}
return out as any
}),
union: <A, B>(a: Schema<A>, b: Schema<B>) =>
makeSchema<A | B>('union', (i) => {
const ra = a.safeParse(i)
if (ra.success) return ra.data
const rb = b.safeParse(i)
if (rb.success) return rb.data
throw new Error(`no union match: ${ra.error}; ${rb.error}`)
}),
optional: <T>(inner: Schema<T>) =>
makeSchema<T | undefined>('optional', (i) => {
if (i === undefined) return undefined
return inner.parse(i)
}),
}
export function isEmail(input: string): boolean {
// Simple and conservative
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)
}
export function isUrl(input: string): boolean {
try {
const u = new URL(input)
return u.protocol === 'http:' || u.protocol === 'https:'
} catch {
return false
}
}
export function isUUID(input: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input)
}
export function safeHeaderName(name: string): boolean {
return /^[A-Za-z0-9-]+$/.test(name) && !/__/g.test(name)
}
export function safeHeaderValue(value: string): boolean {
return !/[\r\n]/.test(value) && value.length < 8192
}
export function validateAgainstSchema<T>(schema: Schema<T>, input: unknown): T {
return schema.parse(input)
}
```
--------------------------------------------------------------------------------
/src/auth/multi-auth-manager.ts:
--------------------------------------------------------------------------------
```typescript
import { createRemoteJWKSet, jwtVerify } from 'jose'
import type { AuthHeaders, OAuthDelegation, OAuthToken } from '../types/auth.js'
import type { MasterAuthConfig, ServerAuthConfig } from '../types/config.js'
import { AuthStrategy } from '../types/config.js'
import { Logger } from '../utils/logger.js'
import { getOAuthProvider } from './oauth-providers.js'
import { TokenManager } from './token-manager.js'
export class MultiAuthManager {
private serverAuth: Map<string, { strategy: AuthStrategy; config?: ServerAuthConfig }> = new Map()
private jwks?: ReturnType<typeof createRemoteJWKSet>
private tokenManager = new TokenManager()
constructor(private readonly config: MasterAuthConfig) {
if (config.jwks_uri) {
try {
this.jwks = createRemoteJWKSet(new URL(config.jwks_uri))
} catch (err) {
Logger.warn('Failed to initialize JWKS for client token validation', err)
}
}
}
registerServerAuth(serverId: string, strategy: AuthStrategy, authConfig?: ServerAuthConfig): void {
this.serverAuth.set(serverId, { strategy, config: authConfig })
}
private keyFor(clientToken: string, serverId: string): string {
return `${serverId}::${clientToken.slice(0, 16)}`
}
async validateClientToken(token: string): Promise<boolean> {
if (!token || typeof token !== 'string') return false
if (!this.jwks) {
// Best-effort: check structural validity and expiration if it is a JWT; otherwise accept as opaque bearer
try {
const { payload } = await jwtVerify(token, async () => {
// No key ⇒ force failure to reach catch where we treat opaque tokens as valid
throw new Error('no-jwks')
})
const now = Math.floor(Date.now() / 1000)
return typeof payload.exp !== 'number' || payload.exp > now
} catch {
return true // Accept opaque tokens when no JWKS is configured
}
}
try {
await jwtVerify(token, this.jwks, {
issuer: this.config.issuer,
audience: this.config.audience ?? this.config.client_id,
})
return true
} catch (err) {
Logger.warn('Client token verification failed', String(err))
return false
}
}
async prepareAuthForBackend(serverId: string, clientToken: string): Promise<AuthHeaders | OAuthDelegation> {
const isValid = await this.validateClientToken(clientToken)
if (!isValid) throw new Error('Invalid client token')
const entry = this.serverAuth.get(serverId)
if (!entry) {
// Default: pass-through
return { Authorization: `Bearer ${clientToken}` }
}
const { strategy, config } = entry
switch (strategy) {
case AuthStrategy.MASTER_OAUTH:
return this.handleMasterOAuth(serverId, clientToken)
case AuthStrategy.DELEGATE_OAUTH:
if (!config) throw new Error(`Missing auth config for server ${serverId}`)
return this.handleDelegatedOAuth(serverId, clientToken, config)
case AuthStrategy.BYPASS_AUTH:
return {}
case AuthStrategy.PROXY_OAUTH:
if (!config) throw new Error(`Missing auth config for server ${serverId}`)
return this.handleProxyOAuth(serverId, clientToken, config)
default:
return { Authorization: `Bearer ${clientToken}` }
}
}
public async handleMasterOAuth(_serverId: string, clientToken: string): Promise<AuthHeaders> {
// Pass-through the client's master token
return { Authorization: `Bearer ${clientToken}` }
}
public async handleDelegatedOAuth(
serverId: string,
clientToken: string,
serverAuthConfig: ServerAuthConfig
): Promise<OAuthDelegation> {
// Return instructions for the client to complete OAuth against the provider
const scopes = Array.isArray(serverAuthConfig.scopes) ? serverAuthConfig.scopes : ['openid']
// Create state binding server + client
const state = this.tokenManager.generateState({ serverId })
// Store a minimal pending marker for later exchange if needed
await this.tokenManager.storeToken(this.keyFor(clientToken, serverId), {
access_token: '',
expires_at: 0,
scope: [],
})
return {
type: 'oauth_delegation',
auth_endpoint: serverAuthConfig.authorization_endpoint,
token_endpoint: serverAuthConfig.token_endpoint,
client_info: { client_id: serverAuthConfig.client_id, metadata: { state } },
required_scopes: scopes,
redirect_after_auth: true,
}
}
public async handleProxyOAuth(
serverId: string,
clientToken: string,
serverAuthConfig: ServerAuthConfig
): Promise<AuthHeaders> {
const key = this.keyFor(clientToken, serverId)
const existing = await this.tokenManager.getToken(key)
const now = Date.now()
if (existing && existing.access_token && existing.expires_at > now + 30_000) {
return { Authorization: `Bearer ${existing.access_token}` }
}
if (existing?.refresh_token) {
try {
const provider = getOAuthProvider(serverAuthConfig as any)
const refreshed = await provider.refreshToken(existing.refresh_token)
await this.tokenManager.storeToken(key, refreshed)
return { Authorization: `Bearer ${refreshed.access_token}` }
} catch (err) {
Logger.warn('Refresh token failed; falling back to pass-through', err)
}
}
// Fallback: pass through the client token (may be accepted by backend if configured)
return { Authorization: `Bearer ${clientToken}` }
}
async storeDelegatedToken(clientToken: string, serverId: string, serverToken: string | OAuthToken): Promise<void> {
const key = this.keyFor(clientToken, serverId)
const tokenObj: OAuthToken = typeof serverToken === 'string'
? { access_token: serverToken, expires_at: Date.now() + 3600_000, scope: [] }
: serverToken
await this.tokenManager.storeToken(key, tokenObj)
}
async getStoredServerToken(serverId: string, clientToken: string): Promise<string | undefined> {
const tok = await this.tokenManager.getToken(this.keyFor(clientToken, serverId))
return tok?.access_token
}
}
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
import type { AuthInfo } from '../types/auth.js'
export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
export interface LogFields {
[key: string]: unknown
correlationId?: string
}
interface LoggerOptions {
level?: LogLevel
json?: boolean
base?: LogFields
}
/**
* Lightweight, structured, context-aware logger with JSON output support and
* timing utilities. Designed to run on Node.js and Workers without deps.
*/
export class Logger {
private static level: LogLevel = ((): LogLevel => {
const env = (globalThis as any)?.process?.env
const raw = (env?.LOG_LEVEL || env?.NODE_LOG_LEVEL || 'info').toLowerCase()
const allowed: LogLevel[] = ['fatal', 'error', 'warn', 'info', 'debug', 'trace']
return (allowed.includes(raw as LogLevel) ? (raw as LogLevel) : 'info') as LogLevel
})()
private static json: boolean = ((): boolean => {
const env = (globalThis as any)?.process?.env
const raw = env?.LOG_FORMAT || env?.LOG_JSON
if (!raw) return (env?.NODE_ENV === 'production') as boolean
return String(raw).toLowerCase() === 'true' || String(raw).toLowerCase() === 'json'
})()
private static base: LogFields = {}
static configure(opts: LoggerOptions): void {
if (opts.level) this.level = opts.level
if (typeof opts.json === 'boolean') this.json = opts.json
if (opts.base) this.base = { ...this.base, ...sanitizeFields(opts.base) }
}
static with(fields: LogFields): typeof Logger {
const merged = { ...this.base, ...sanitizeFields(fields) }
const child = new Proxy(this, {
get: (target, prop) => {
if (prop === 'base') return merged
return (target as any)[prop]
},
}) as typeof Logger
return child
}
static setLevel(level: LogLevel): void {
this.level = level
}
static enableJSON(enabled: boolean): void {
this.json = enabled
}
static getLevel(): LogLevel {
return this.level
}
static trace(message: string, fields?: LogFields | unknown): void {
const f = fieldsToLogFields(fields)
this._log('trace', message, f)
}
static debug(message: string, fields?: LogFields | unknown): void {
const envDebug = (globalThis as any)?.process?.env?.DEBUG
const f = fieldsToLogFields(fields)
if (envDebug || this.levelAllowed('debug')) this._log('debug', message, f)
}
static info(message: string, fields?: LogFields | unknown): void {
const f = fieldsToLogFields(fields)
this._log('info', message, f)
}
static warn(message: string, fields?: LogFields | unknown): void {
const f = fieldsToLogFields(fields)
this._log('warn', message, f)
}
static error(message: string, fields?: LogFields | unknown): void {
const f = fieldsToLogFields(fields)
this._log('error', message, f)
}
static fatal(message: string, fields?: LogFields | unknown): void {
const f = fieldsToLogFields(fields)
this._log('fatal', message, f)
}
/**
* Structured auth event helper for backward compatibility.
*/
static logAuthEvent(event: string, context: AuthInfo): void {
this.info('auth_event', { event, ...context })
}
/**
* Structured server event helper for backward compatibility.
*/
static logServerEvent(event: string, serverId: string, context?: unknown): void {
const fields = fieldsToLogFields(context)
this.info('server_event', { event, serverId, ...(fields ?? {}) })
}
/**
* Starts a performance timer, returning a function to log completion.
*
* Usage:
* const done = Logger.time('load_config', { id })
* ...work...
* done({ status: 'ok' })
*/
static time(name: string, fields?: LogFields): (extra?: LogFields) => void {
const start = now()
const base = { name, ...(fields ? sanitizeFields(fields) : {}) }
return (extra?: LogFields) => {
const durationMs = Math.max(0, now() - start)
this.info('perf', { ...base, ...(extra ? sanitizeFields(extra) : {}), durationMs })
}
}
/**
* Low-level log method honoring level and output format.
*/
private static _log(level: LogLevel, message: string, fields?: LogFields): void {
if (!this.levelAllowed(level)) return
const ts = new Date().toISOString()
const entry = {
ts,
level,
msg: message,
...this.base,
...(fields ? sanitizeFields(fields) : {}),
}
// eslint-disable-next-line no-console
if (this.json) console.log(JSON.stringify(entry))
else console.log(formatHuman(entry))
}
private static levelAllowed(check: LogLevel): boolean {
const order: LogLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']
const curIdx = order.indexOf(this.level)
const chkIdx = order.indexOf(check)
return chkIdx >= curIdx
}
}
function now(): number {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
return performance.now()
}
return Date.now()
}
function sanitizeFields(fields: LogFields): LogFields {
const out: LogFields = {}
for (const [k, v] of Object.entries(fields)) {
if (v === undefined) continue
if (v instanceof Error) {
out[k] = {
name: v.name,
message: v.message,
stack: v.stack,
}
} else if (typeof v === 'object' && v !== null) {
try {
// Avoid circular structures
out[k] = JSON.parse(JSON.stringify(v))
} catch {
out[k] = String(v)
}
} else {
out[k] = v as any
}
}
return out
}
function formatHuman(entry: { [k: string]: unknown }): string {
const { ts, level, msg, ...rest } = entry as any
const head = `[${String(level).toUpperCase()}] ${ts} ${msg}`
const restKeys = Object.keys(rest)
if (restKeys.length === 0) return head
return `${head} ${safeStringify(rest)}`
}
function safeStringify(obj: any): string {
try {
return JSON.stringify(obj)
} catch {
return '[object]'
}
}
function fieldsToLogFields(f?: LogFields | unknown): LogFields | undefined {
if (!f) return undefined
if (typeof f === 'object' && !(f instanceof Error)) return f as LogFields
return { detail: f }
}
```
--------------------------------------------------------------------------------
/src/server/master-server.ts:
--------------------------------------------------------------------------------
```typescript
// Phase 1: avoid hard dependency on SDK types to ensure compilation
import type { ServerCapabilities, LoadedServer } from '../types/server.js'
import type { MasterConfig, RoutingConfig, ServerConfig } from '../types/config.js'
import type { AuthHeaders, OAuthDelegation } from '../types/auth.js'
import { ProtocolHandler } from './protocol-handler.js'
import { DefaultModuleLoader } from '../modules/module-loader.js'
import { CapabilityAggregator } from '../modules/capability-aggregator.js'
import { RequestRouter } from '../modules/request-router.js'
import { Logger } from '../utils/logger.js'
import { MultiAuthManager } from '../auth/multi-auth-manager.js'
import { OAuthFlowController } from '../oauth/flow-controller.js'
export class MasterServer {
readonly server: unknown
readonly handler: ProtocolHandler
private readonly loader = new DefaultModuleLoader()
private readonly aggregator = new CapabilityAggregator()
private readonly servers = new Map<string, LoadedServer>()
private router!: RequestRouter
private config?: MasterConfig
private authManager?: MultiAuthManager
private oauthController?: OAuthFlowController
private getAuthHeaders: (
serverId: string,
clientToken?: string
) => Promise<AuthHeaders | OAuthDelegation | undefined>
constructor(capabilities?: Partial<ServerCapabilities>, routing?: RoutingConfig) {
const version = (globalThis as any)?.process?.env?.APP_VERSION ?? '0.1.0'
this.server = { name: 'master-mcp-server', version }
this.getAuthHeaders = async (_serverId: string, clientToken?: string) =>
clientToken ? { Authorization: `Bearer ${clientToken}` } : undefined
this.router = new RequestRouter(this.servers, this.aggregator, this.getAuthHeaders.bind(this), { routing })
this.handler = new ProtocolHandler({ aggregator: this.aggregator, router: this.router })
void capabilities
}
async startFromConfig(config: MasterConfig, clientToken?: string): Promise<void> {
Logger.info('Starting MasterServer from config')
this.config = config
await this.loadServers(config.servers, clientToken)
await this.discoverAllCapabilities(clientToken)
}
async loadServers(servers: ServerConfig[], clientToken?: string): Promise<void> {
Logger.info('Loading servers', { servers })
const loaded = await this.loader.loadServers(servers, clientToken)
Logger.info('Loaded servers', { loaded: Array.from(loaded.entries()) })
this.servers.clear()
for (const [id, s] of loaded) this.servers.set(id, s)
this.router = new RequestRouter(this.servers, this.aggregator, this.getAuthHeaders.bind(this), {
routing: this.config?.routing,
})
;(this as any).handler = new ProtocolHandler({ aggregator: this.aggregator, router: this.router })
}
async discoverAllCapabilities(clientToken?: string): Promise<void> {
Logger.info('Discovering all capabilities', { servers: Array.from(this.servers.entries()) })
const headersOnly = async (serverId: string, token?: string) => {
const res = await this.getAuthHeaders(serverId, token)
if (res && (res as OAuthDelegation).type === 'oauth_delegation') {
return token ? { Authorization: `Bearer ${token}` } : undefined
}
return res as AuthHeaders | undefined
}
await this.aggregator.discoverCapabilities(this.servers, clientToken, headersOnly)
Logger.info('Discovered all capabilities', { tools: this.aggregator.getAllTools(this.servers), resources: this.aggregator.getAllResources(this.servers) })
}
// Allow host app to inject an auth header strategy (e.g., MultiAuthManager)
setAuthHeaderProvider(
fn: (serverId: string, clientToken?: string) => Promise<AuthHeaders | OAuthDelegation | undefined>
): void {
this.getAuthHeaders = fn
this.router = new RequestRouter(this.servers, this.aggregator, this.getAuthHeaders.bind(this), {
routing: this.config?.routing,
})
;(this as any).handler = new ProtocolHandler({ aggregator: this.aggregator, router: this.router })
}
getRouter(): RequestRouter {
return this.router
}
getAggregatedTools(): ServerCapabilities['tools'] {
return this.aggregator.getAllTools(this.servers)
}
getAggregatedResources(): ServerCapabilities['resources'] {
return this.aggregator.getAllResources(this.servers)
}
async performHealthChecks(clientToken?: string): Promise<Record<string, boolean>> {
const results: Record<string, boolean> = {}
for (const [id, s] of this.servers) {
results[id] = await this.loader.performHealthCheck(s, clientToken)
}
return results
}
async restartServer(id: string): Promise<void> {
await this.loader.restartServer(id)
}
async unloadAll(): Promise<void> {
await Promise.all(Array.from(this.servers.keys()).map((id) => this.loader.unload(id)))
this.servers.clear()
}
attachAuthManager(manager: MultiAuthManager): void {
this.authManager = manager
this.setAuthHeaderProvider((serverId: string, clientToken?: string) => {
if (!clientToken) return Promise.resolve(undefined)
return this.authManager!.prepareAuthForBackend(serverId, clientToken)
})
}
// Provide an OAuthFlowController wired to the current config and auth manager.
// Host runtimes (Node/Workers) can use this to mount HTTP endpoints without coupling MasterServer to a specific HTTP framework.
getOAuthFlowController(): OAuthFlowController {
if (!this.config) throw new Error('MasterServer config not initialized')
if (!this.authManager) throw new Error('Auth manager not attached')
if (!this.oauthController) {
this.oauthController = new OAuthFlowController(
{
getConfig: () => this.config!,
storeDelegatedToken: async (clientToken, serverId, token) => {
await this.authManager!.storeDelegatedToken(clientToken, serverId, token)
},
},
'/oauth'
)
}
return this.oauthController
}
updateRouting(routing?: RoutingConfig): void {
this.router = new RequestRouter(this.servers, this.aggregator, this.getAuthHeaders.bind(this), { routing })
;(this as any).handler = new ProtocolHandler({ aggregator: this.aggregator, router: this.router })
}
}
```
--------------------------------------------------------------------------------
/tests/servers/test-streaming-both-complete.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import { spawn } from 'node:child_process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
async function startHttpServer() {
console.log('Starting HTTP test server...')
// Start the HTTP server as a background process
const httpServer = spawn('node', ['examples/test-mcp-server.js'], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, PORT: '3006' }
})
// Capture stdout and stderr
httpServer.stdout.on('data', (data) => {
console.log(`[HTTP Server] ${data.toString().trim()}`)
})
httpServer.stderr.on('data', (data) => {
console.error(`[HTTP Server ERROR] ${data.toString().trim()}`)
})
// Wait for the server to start
await new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
reject(new Error('HTTP server startup timeout'))
}, 5000)
httpServer.stdout.on('data', (data) => {
if (data.toString().includes('Test MCP server listening')) {
clearTimeout(timeout)
resolve()
}
})
})
return httpServer
}
async function startMasterServer() {
console.log('Starting Master MCP server...')
// Start the master server as a background process
const masterServer = spawn('npm', ['run', 'dev'], {
stdio: ['ignore', 'pipe', 'pipe'],
cwd: process.cwd()
})
// Capture stdout and stderr
masterServer.stdout.on('data', (data) => {
const output = data.toString().trim()
// Only log important messages to avoid too much output
if (output.includes('Master MCP listening') || output.includes('error') || output.includes('ERROR')) {
console.log(`[Master Server] ${output}`)
}
})
masterServer.stderr.on('data', (data) => {
console.error(`[Master Server ERROR] ${data.toString().trim()}`)
})
// Wait for the server to start
await new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
reject(new Error('Master server startup timeout'))
}, 15000)
masterServer.stdout.on('data', (data) => {
if (data.toString().includes('Master MCP listening')) {
clearTimeout(timeout)
console.log('[Master Server] Server is ready!')
resolve()
}
})
})
return masterServer
}
async function runStreamingTest() {
try {
console.log('Testing Master MCP Server with HTTP Streaming...')
// Create a streamable HTTP transport to connect to our MCP server
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
// Create the MCP client
const client = new Client({
name: 'master-mcp-streaming-test-client',
version: '1.0.0'
})
// Initialize the client
await client.connect(transport)
console.log('✅ Server initialized with streaming transport')
console.log('Server info:', client.getServerVersion())
console.log('Server capabilities:', client.getServerCapabilities())
// List tools using streaming
console.log('\n--- Testing tools/list with streaming ---')
const toolsResult = await client.listTools({})
console.log('✅ tools/list successful with streaming')
console.log('Number of tools:', toolsResult.tools.length)
console.log('Tools:', toolsResult.tools.map(t => t.name))
// Verify both servers are present
const hasHttpTool = toolsResult.tools.some(t => t.name === 'test-server.echo')
const hasStdioTool = toolsResult.tools.some(t => t.name === 'stdio-server.stdio-echo')
if (hasHttpTool) {
console.log('✅ HTTP server tool found')
} else {
console.log('❌ HTTP server tool not found')
}
if (hasStdioTool) {
console.log('✅ STDIO server tool found')
} else {
console.log('❌ STDIO server tool not found')
}
// List resources using streaming
console.log('\n--- Testing resources/list with streaming ---')
const resourcesResult = await client.listResources({})
console.log('✅ resources/list successful with streaming')
console.log('Number of resources:', resourcesResult.resources.length)
console.log('Resources:', resourcesResult.resources.map(r => r.uri))
// Verify both servers are present
const hasHttpResource = resourcesResult.resources.some(r => r.uri === 'test-server.test://example')
const hasStdioResource = resourcesResult.resources.some(r => r.uri === 'stdio-server.stdio://example/resource')
if (hasHttpResource) {
console.log('✅ HTTP server resource found')
} else {
console.log('❌ HTTP server resource not found')
}
if (hasStdioResource) {
console.log('✅ STDIO server resource found')
} else {
console.log('❌ STDIO server resource not found')
}
// Test ping
console.log('\n--- Testing ping with streaming ---')
const pingResult = await client.ping()
console.log('✅ ping successful with streaming')
console.log('Ping result:', pingResult)
// Summary
console.log('\n--- Test Summary ---')
if (hasHttpTool && hasStdioTool && hasHttpResource && hasStdioResource) {
console.log('🎉 All tests passed! Both HTTP and STDIO servers are working correctly.')
} else {
console.log('⚠️ Some tests failed. Check the output above for details.')
}
// Close the connection
await client.close()
console.log('\n✅ Disconnected from MCP server')
} catch (error) {
console.error('❌ Streaming test failed:', error)
console.error('Error stack:', error.stack)
}
}
async function main() {
let httpServer, masterServer
try {
// Start the HTTP server
httpServer = await startHttpServer()
// Start the master server
masterServer = await startMasterServer()
// Wait a bit for discovery to happen
console.log('Waiting for server discovery...')
await new Promise(resolve => setTimeout(resolve, 3000))
// Run the streaming test
await runStreamingTest()
} catch (error) {
console.error('Test failed:', error)
} finally {
// Clean up: kill the servers
if (httpServer) {
console.log('Stopping HTTP server...')
httpServer.kill()
}
if (masterServer) {
console.log('Stopping Master server...')
masterServer.kill()
}
}
}
// Run the test
main()
```
--------------------------------------------------------------------------------
/src/config/schema-validator.ts:
--------------------------------------------------------------------------------
```typescript
import type { MasterConfig } from '../types/config.js'
import { Logger } from '../utils/logger.js'
type JSONSchema = {
$id?: string
type?: string | string[]
properties?: Record<string, JSONSchema>
required?: string[]
additionalProperties?: boolean
enum?: unknown[]
items?: JSONSchema
format?: 'url' | 'secret' | 'integer'
anyOf?: JSONSchema[]
allOf?: JSONSchema[]
description?: string
}
export interface SchemaValidationError {
path: string
message: string
}
export class SchemaValidator {
// Lightweight JSON Schema validator supporting core features used by our config schema
static async loadSchema(schemaPath?: string): Promise<JSONSchema | undefined> {
if (!schemaPath) return defaultSchema
try {
const isNode = Boolean((globalThis as any)?.process?.versions?.node)
if (!isNode) return defaultSchema
const fs = await import('node:fs/promises')
const raw = await fs.readFile(schemaPath, 'utf8')
return JSON.parse(raw) as JSONSchema
} catch (err) {
Logger.warn(`Failed to read schema at ${schemaPath}; using built-in`, String(err))
return defaultSchema
}
}
static validate(config: unknown, schema: JSONSchema): { valid: boolean; errors: SchemaValidationError[] } {
const errors: SchemaValidationError[] = []
validateAgainst(config, schema, '', errors)
return { valid: errors.length === 0, errors }
}
static assertValid<T = MasterConfig>(config: unknown, schema: JSONSchema): T {
const { valid, errors } = this.validate(config, schema)
if (!valid) {
const msg = errors.map((e) => `${e.path || '<root>'}: ${e.message}`).join('\n')
throw new Error(`Configuration validation failed:\n${msg}`)
}
return config as T
}
}
function typeOf(val: unknown): string {
if (Array.isArray(val)) return 'array'
return typeof val
}
function validateAgainst(value: unknown, schema: JSONSchema, path: string, errors: SchemaValidationError[]): void {
if (!schema) return
// Type check
if (schema.type) {
const allowed = Array.isArray(schema.type) ? schema.type : [schema.type]
const actual = typeOf(value)
if (!allowed.includes(actual)) {
errors.push({ path, message: `expected type ${allowed.join('|')}, got ${actual}` })
return
}
}
if (schema.enum && !schema.enum.includes(value)) {
errors.push({ path, message: `must be one of ${schema.enum.join(', ')}` })
}
if (schema.format) {
if (schema.format === 'url' && typeof value === 'string') {
try {
// eslint-disable-next-line no-new
new URL(value)
} catch {
errors.push({ path, message: 'must be a valid URL' })
}
}
if (schema.format === 'integer' && typeof value === 'number') {
if (!Number.isInteger(value)) errors.push({ path, message: 'must be an integer' })
}
}
if (schema.properties && value && typeof value === 'object' && !Array.isArray(value)) {
const v = value as Record<string, unknown>
const required = schema.required || []
for (const r of required) {
if (!(r in v)) errors.push({ path: join(path, r), message: 'is required' })
}
for (const [k, subschema] of Object.entries(schema.properties)) {
if (k in v) validateAgainst(v[k], subschema, join(path, k), errors)
}
if (schema.additionalProperties === false) {
for (const k of Object.keys(v)) {
if (!schema.properties[k]) errors.push({ path: join(path, k), message: 'is not allowed' })
}
}
}
if (schema.items && Array.isArray(value)) {
value.forEach((item, idx) => validateAgainst(item, schema.items!, join(path, String(idx)), errors))
}
if (schema.allOf) {
for (const s of schema.allOf) validateAgainst(value, s, path, errors)
}
if (schema.anyOf) {
const ok = schema.anyOf.some((s) => {
const temp: SchemaValidationError[] = []
validateAgainst(value, s, path, temp)
return temp.length === 0
})
if (!ok) errors.push({ path, message: 'does not match any allowed schema' })
}
}
function join(base: string, key: string): string {
return base ? `${base}.${key}` : key
}
// Built-in fallback schema captures core fields and constraints.
const defaultSchema: JSONSchema = {
type: 'object',
required: ['master_oauth', 'hosting', 'servers'],
properties: {
master_oauth: {
type: 'object',
required: ['authorization_endpoint', 'token_endpoint', 'client_id', 'redirect_uri', 'scopes'],
properties: {
issuer: { type: 'string' },
authorization_endpoint: { type: 'string', format: 'url' },
token_endpoint: { type: 'string', format: 'url' },
jwks_uri: { type: 'string' },
client_id: { type: 'string' },
client_secret: { type: 'string' },
redirect_uri: { type: 'string' },
scopes: { type: 'array', items: { type: 'string' } },
audience: { type: 'string' },
},
additionalProperties: true,
},
hosting: {
type: 'object',
required: ['platform'],
properties: {
platform: { type: 'string', enum: ['node', 'cloudflare-workers', 'koyeb', 'docker', 'unknown'] },
port: { type: 'number', format: 'integer' },
base_url: { type: 'string' },
},
additionalProperties: true,
},
logging: {
type: 'object',
properties: { level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] } },
},
routing: {
type: 'object',
properties: {
loadBalancer: { type: 'object', properties: { strategy: { type: 'string' } }, additionalProperties: true },
circuitBreaker: { type: 'object', additionalProperties: true },
retry: { type: 'object', additionalProperties: true },
},
additionalProperties: true,
},
servers: {
type: 'array',
items: {
type: 'object',
required: ['id', 'type', 'auth_strategy', 'config'],
properties: {
id: { type: 'string' },
type: { type: 'string', enum: ['git', 'npm', 'pypi', 'docker', 'local'] },
url: { type: 'string' },
package: { type: 'string' },
version: { type: 'string' },
branch: { type: 'string' },
auth_strategy: {
type: 'string',
enum: ['master_oauth', 'delegate_oauth', 'bypass_auth', 'proxy_oauth'],
},
auth_config: { type: 'object', additionalProperties: true },
config: {
type: 'object',
properties: {
environment: { type: 'object', additionalProperties: true },
args: { type: 'array', items: { type: 'string' } },
port: { type: 'number', format: 'integer' },
},
additionalProperties: true,
},
},
additionalProperties: true,
},
},
},
additionalProperties: true,
}
```
--------------------------------------------------------------------------------
/tests/servers/test-streaming-both-full.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import { spawn } from 'node:child_process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
async function startHttpServer() {
console.log('Starting HTTP test server...')
// Start the HTTP server as a background process
const httpServer = spawn('node', ['examples/test-mcp-server.js'], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, PORT: '3006' }
})
// Capture stdout and stderr
httpServer.stdout.on('data', (data) => {
console.log(`[HTTP Server] ${data.toString().trim()}`)
})
httpServer.stderr.on('data', (data) => {
console.error(`[HTTP Server ERROR] ${data.toString().trim()}`)
})
// Wait for the server to start
await new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
reject(new Error('HTTP server startup timeout'))
}, 5000)
httpServer.stdout.on('data', (data) => {
if (data.toString().includes('Test MCP server listening')) {
clearTimeout(timeout)
resolve()
}
})
})
return httpServer
}
async function startMasterServer() {
console.log('Starting Master MCP server...')
// Start the master server as a background process
const masterServer = spawn('npm', ['run', 'dev'], {
stdio: ['ignore', 'pipe', 'pipe'],
cwd: process.cwd()
})
// Capture stdout and stderr
masterServer.stdout.on('data', (data) => {
const output = data.toString().trim()
console.log(`[Master Server] ${output}`)
// Don't log the full output as it's too verbose
})
masterServer.stderr.on('data', (data) => {
console.error(`[Master Server ERROR] ${data.toString().trim()}`)
})
// Wait for the server to start
await new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
reject(new Error('Master server startup timeout'))
}, 10000)
masterServer.stdout.on('data', (data) => {
if (data.toString().includes('Master MCP listening')) {
clearTimeout(timeout)
resolve()
}
})
})
return masterServer
}
async function runStreamingTest() {
try {
console.log('Testing Master MCP Server with HTTP Streaming...')
// Create a streamable HTTP transport to connect to our MCP server
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
// Create the MCP client
const client = new Client({
name: 'master-mcp-streaming-test-client',
version: '1.0.0'
})
// Initialize the client
await client.connect(transport)
console.log('✅ Server initialized with streaming transport')
console.log('Server info:', client.getServerVersion())
console.log('Server capabilities:', client.getServerCapabilities())
// List tools using streaming
console.log('\n--- Testing tools/list with streaming ---')
const toolsResult = await client.listTools({})
console.log('✅ tools/list successful with streaming')
console.log('Number of tools:', toolsResult.tools.length)
console.log('Tools:', toolsResult.tools.map(t => t.name))
// List resources using streaming
console.log('\n--- Testing resources/list with streaming ---')
const resourcesResult = await client.listResources({})
console.log('✅ resources/list successful with streaming')
console.log('Number of resources:', resourcesResult.resources.length)
console.log('Resources:', resourcesResult.resources.map(r => r.uri))
// Test ping
console.log('\n--- Testing ping with streaming ---')
const pingResult = await client.ping()
console.log('✅ ping successful with streaming')
console.log('Ping result:', pingResult)
// Try calling a tool from the HTTP server
console.log('\n--- Testing tool call to HTTP server ---')
try {
const httpToolCallResult = await client.callTool({
name: 'test-server.echo', // Prefixed with server ID
arguments: { message: 'Hello from HTTP server!' }
})
console.log('✅ HTTP tool call successful')
console.log('HTTP tool result:', JSON.stringify(httpToolCallResult, null, 2))
} catch (error) {
console.log('⚠️ HTTP tool call failed (might not be available):', error.message)
}
// Try calling a tool from the STDIO server
console.log('\n--- Testing tool call to STDIO server ---')
try {
const stdioToolCallResult = await client.callTool({
name: 'stdio-server.stdio-echo', // Prefixed with server ID
arguments: { message: 'Hello from STDIO server!' }
})
console.log('✅ STDIO tool call successful')
console.log('STDIO tool result:', JSON.stringify(stdioToolCallResult, null, 2))
} catch (error) {
console.log('⚠️ STDIO tool call failed (might not be available):', error.message)
}
// Try reading a resource from the HTTP server
console.log('\n--- Testing resource read from HTTP server ---')
try {
const httpResourceResult = await client.readResource({
uri: 'test-server.test://example' // Prefixed with server ID
})
console.log('✅ HTTP resource read successful')
console.log('HTTP resource result:', JSON.stringify(httpResourceResult, null, 2))
} catch (error) {
console.log('⚠️ HTTP resource read failed (might not be available):', error.message)
}
// Try reading a resource from the STDIO server
console.log('\n--- Testing resource read from STDIO server ---')
try {
const stdioResourceResult = await client.readResource({
uri: 'stdio-server.stdio://example/resource' // Prefixed with server ID
})
console.log('✅ STDIO resource read successful')
console.log('STDIO resource result:', JSON.stringify(stdioResourceResult, null, 2))
} catch (error) {
console.log('⚠️ STDIO resource read failed (might not be available):', error.message)
}
// Close the connection
await client.close()
console.log('\n✅ Disconnected from MCP server')
console.log('\n🎉 All streaming tests completed successfully!')
} catch (error) {
console.error('❌ Streaming test failed:', error)
console.error('Error stack:', error.stack)
}
}
async function main() {
let httpServer, masterServer
try {
// Start the HTTP server
httpServer = await startHttpServer()
// Start the master server
masterServer = await startMasterServer()
// Wait a bit for discovery to happen
console.log('Waiting for server discovery...')
await new Promise(resolve => setTimeout(resolve, 3000))
// Run the streaming test
await runStreamingTest()
} catch (error) {
console.error('Test failed:', error)
} finally {
// Clean up: kill the servers
if (httpServer) {
console.log('Stopping HTTP server...')
httpServer.kill()
}
if (masterServer) {
console.log('Stopping Master server...')
masterServer.kill()
}
}
}
// Run the test
main()
```
--------------------------------------------------------------------------------
/src/modules/stdio-manager.ts:
--------------------------------------------------------------------------------
```typescript
import { spawn, ChildProcess } from 'node:child_process'
import { Logger } from '../utils/logger.js'
import type { ServerProcess } from '../types/server.js'
export class StdioManager {
private processes = new Map<string, ChildProcess>()
private responseQueues = new Map<string, Array<{ resolve: (value: any) => void; reject: (reason: any) => void; id: number | string }>>()
private notificationCallbacks = new Map<string, (message: any) => void>()
private messageBuffers = new Map<string, string>()
async startServer(serverId: string, filePath: string, env?: Record<string, string>): Promise<ServerProcess> {
Logger.info('Starting STDIO server', { serverId, filePath })
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Timeout starting STDIO server ${serverId}`))
}, 10000) // 10 second timeout
try {
const proc = spawn('node', [filePath], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, ...env }
})
// Set up event handlers
proc.stdout?.on('data', (data) => {
this.handleStdoutData(serverId, data.toString())
})
proc.stderr?.on('data', (data) => {
Logger.warn('STDIO server stderr', { serverId, data: data.toString() })
})
proc.on('close', (code) => {
Logger.info('STDIO server process closed', { serverId, code })
this.cleanupProcess(serverId, new Error(`STDIO server ${serverId} process closed with code ${code}`))
})
proc.on('error', (err) => {
Logger.error('STDIO server process error', { serverId, error: err })
clearTimeout(timeout)
this.rejectPendingRequests(serverId, err)
reject(err)
})
// Check if process started successfully
proc.on('spawn', () => {
clearTimeout(timeout)
this.processes.set(serverId, proc)
this.responseQueues.set(serverId, [])
this.messageBuffers.set(serverId, '')
resolve({
pid: proc.pid,
stop: async () => {
return new Promise((resolve) => {
if (proc.connected) {
proc.kill()
setTimeout(() => resolve(), 1000) // Wait 1 second for graceful shutdown
} else {
resolve()
}
})
}
})
})
} catch (err) {
clearTimeout(timeout)
reject(err)
}
})
}
private handleStdoutData(serverId: string, data: string) {
const buffer = this.messageBuffers.get(serverId) || ''
const newBuffer = buffer + data
// Try to parse complete JSON messages
let remainingBuffer = newBuffer
while (remainingBuffer.trim().startsWith('{')) {
try {
// Try to parse as JSON
const trimmed = remainingBuffer.trim()
let endIndex = 1
let braceCount = 1
// Find the matching closing brace
for (let i = 1; i < trimmed.length; i++) {
if (trimmed[i] === '{') {
braceCount++
} else if (trimmed[i] === '}') {
braceCount--
if (braceCount === 0) {
endIndex = i + 1
break
}
}
}
if (braceCount === 0) {
// Found complete JSON object
const jsonString = trimmed.substring(0, endIndex)
const message = JSON.parse(jsonString)
// Process the message
this.processMessage(serverId, message)
// Update buffer to remaining data
remainingBuffer = trimmed.substring(endIndex)
// Skip any whitespace after the JSON object
remainingBuffer = remainingBuffer.replace(/^\\s+/, '')
} else {
// Incomplete JSON, wait for more data
break
}
} catch (err) {
// Incomplete JSON or parsing error, wait for more data
break
}
}
this.messageBuffers.set(serverId, remainingBuffer)
}
public onNotification(serverId: string, callback: (message: any) => void) {
this.notificationCallbacks.set(serverId, callback)
}
private processMessage(serverId: string, message: any) {
Logger.debug('Received message from STDIO server', { serverId, message })
// Check if this is a response to a pending request
if (message.id !== undefined) {
const queue = this.responseQueues.get(serverId)
if (queue) {
const index = queue.findIndex((item) => item.id === message.id)
if (index !== -1) {
const { resolve } = queue.splice(index, 1)[0]
resolve(message)
return
}
}
}
// Handle notifications (no id) or unmatched responses
const callback = this.notificationCallbacks.get(serverId)
if (callback) {
try {
callback(message)
} catch (err) {
Logger.error('Error in notification callback', { serverId, error: err })
}
} else {
Logger.debug('Received notification or unmatched response from STDIO server, but no callback registered', { serverId, message })
}
}
async sendMessage(serverId: string, message: any): Promise<void> {
const proc = this.processes.get(serverId)
if (!proc || !proc.stdin) {
throw new Error(`STDIO server ${serverId} not found or not connected`)
}
return new Promise((resolve, reject) => {
const messageStr = JSON.stringify(message) + '\n'
proc.stdin?.write(messageStr, (err) => {
if (err) reject(err)
else resolve()
})
})
}
async waitForResponse(serverId: string, messageId: number | string, timeoutMs = 30000): Promise<any> {
const proc = this.processes.get(serverId)
if (!proc) {
throw new Error(`STDIO server ${serverId} not found`)
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
// Remove the pending request from the queue
const queue = this.responseQueues.get(serverId) || []
const index = queue.findIndex(item => item.id === messageId)
if (index !== -1) {
queue.splice(index, 1)
}
reject(new Error(`Timeout waiting for response from STDIO server ${serverId} for message ${messageId}`))
}, timeoutMs)
// Add to response queue
const queue = this.responseQueues.get(serverId) || []
queue.push({
id: messageId,
resolve: (value: any) => {
clearTimeout(timeout)
resolve(value)
},
reject: (reason: any) => {
clearTimeout(timeout)
reject(reason)
}
})
this.responseQueues.set(serverId, queue)
})
}
private rejectPendingRequests(serverId: string, error: any) {
const queue = this.responseQueues.get(serverId)
if (queue) {
while (queue.length > 0) {
const { reject } = queue.shift()!
reject(error)
}
}
}
private cleanupProcess(serverId: string, error?: any) {
this.rejectPendingRequests(serverId, error || new Error(`STDIO server ${serverId} process closed`))
this.processes.delete(serverId)
this.responseQueues.delete(serverId)
this.messageBuffers.delete(serverId)
this.notificationCallbacks.delete(serverId)
}
}
```
--------------------------------------------------------------------------------
/src/utils/crypto.ts:
--------------------------------------------------------------------------------
```typescript
import {
createCipheriv,
createDecipheriv,
createHash,
createHmac,
randomBytes,
randomUUID as nodeRandomUUID,
timingSafeEqual,
pbkdf2Sync,
scryptSync,
hkdfSync,
} from 'node:crypto'
const IV_LENGTH = 12 // AES-GCM recommended 12 bytes
const AUTH_TAG_LENGTH = 16
function deriveKey(key: string | Buffer): Buffer {
return Buffer.isBuffer(key) ? createHash('sha256').update(key).digest() : createHash('sha256').update(Buffer.from(key)).digest()
}
function b64(input: ArrayBuffer | Uint8Array): string {
return Buffer.from(input as any).toString('base64')
}
function fromB64(input: string): Buffer {
return Buffer.from(input, 'base64')
}
/**
* Node-focused crypto utilities used by the Master MCP Server runtime.
* Worker builds exclude this file via tsconfig.worker.json.
*/
export class CryptoUtils {
/** Encrypts UTF-8 text using AES-256-GCM. Returns base64(iv||tag||ciphertext). */
static encrypt(data: string, key: string | Buffer): string {
const iv = randomBytes(IV_LENGTH)
const cipher = createCipheriv('aes-256-gcm', deriveKey(key), iv)
const ciphertext = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()])
const authTag = cipher.getAuthTag()
return Buffer.concat([iv, authTag, ciphertext]).toString('base64')
}
/** Decrypts base64(iv||tag||ciphertext) produced by encrypt(). */
static decrypt(encryptedData: string, key: string | Buffer): string {
const raw = Buffer.from(encryptedData, 'base64')
const iv = raw.subarray(0, IV_LENGTH)
const authTag = raw.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH)
const ciphertext = raw.subarray(IV_LENGTH + AUTH_TAG_LENGTH)
const decipher = createDecipheriv('aes-256-gcm', deriveKey(key), iv)
decipher.setAuthTag(authTag)
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()])
return plaintext.toString('utf8')
}
/** Secure random bytes as hex string. */
static generateSecureRandom(length: number): string {
return randomBytes(length).toString('hex')
}
/** Returns RFC4122 v4 UUID using crypto RNG. */
static uuid(): string {
return nodeRandomUUID()
}
/** SHA-256 digest as hex string. */
static hash(input: string | Buffer): string {
return createHash('sha256').update(input).digest('hex')
}
/** Constant-time equality check for hex strings produced by hash(). */
static verify(input: string | Buffer, hash: string): boolean {
const calculated = Buffer.from(this.hash(input), 'utf8')
const provided = Buffer.from(hash, 'utf8')
if (calculated.length !== provided.length) return false
return timingSafeEqual(calculated, provided)
}
/** Derives a key using PBKDF2-HMAC-SHA256. Returns base64 key bytes. */
static pbkdf2(
password: string | Buffer,
salt: string | Buffer,
iterations = 100_000,
keyLen = 32,
): string {
const dk = pbkdf2Sync(password, salt, iterations, keyLen, 'sha256')
return b64(dk)
}
/**
* Hashes password using PBKDF2. Format: pbkdf2$sha256$iter$saltB64$hashB64
*/
static pbkdf2Hash(password: string, iterations = 100_000, saltLen = 16): string {
const salt = randomBytes(saltLen)
const hash = pbkdf2Sync(password, salt, iterations, 32, 'sha256')
return `pbkdf2$sha256$${iterations}$${b64(salt)}$${b64(hash)}`
}
static pbkdf2Verify(password: string, encoded: string): boolean {
try {
const [algo, hashName, iterStr, saltB64, hashB64] = encoded.split('$')
if (algo !== 'pbkdf2' || hashName !== 'sha256') return false
const iterations = Number(iterStr)
const salt = fromB64(saltB64)
const expected = fromB64(hashB64)
const actual = pbkdf2Sync(password, salt, iterations, expected.length, 'sha256')
return timingSafeEqual(actual, expected)
} catch {
return false
}
}
/**
* Hashes password using scrypt with defaults N=16384, r=8, p=1.
* Format: scrypt$N$r$p$saltB64$hashB64
*/
static scryptHash(password: string, opts?: { N?: number; r?: number; p?: number; saltLen?: number; keyLen?: number }): string {
const N = opts?.N ?? 16384
const r = opts?.r ?? 8
const p = opts?.p ?? 1
const saltLen = opts?.saltLen ?? 16
const keyLen = opts?.keyLen ?? 32
const salt = randomBytes(saltLen)
const hash = scryptSync(password, salt, keyLen, { N, r, p })
return `scrypt$${N}$${r}$${p}$${b64(salt)}$${b64(hash)}`
}
static scryptVerify(password: string, encoded: string): boolean {
try {
const [algo, nStr, rStr, pStr, saltB64, hashB64] = encoded.split('$')
if (algo !== 'scrypt') return false
const N = Number(nStr)
const r = Number(rStr)
const p = Number(pStr)
const salt = fromB64(saltB64)
const expected = fromB64(hashB64)
const actual = scryptSync(password, salt, expected.length, { N, r, p })
return timingSafeEqual(actual, expected)
} catch {
return false
}
}
/**
* Attempts bcrypt via optional dependency. If unavailable, falls back to scrypt
* and encodes using the scrypt$... scheme. This ensures secure hashing without
* adding runtime deps.
*/
static async bcryptHash(password: string, rounds = 12): Promise<string> {
try {
// Attempt to use optional bcrypt packages if present via dynamic import
const mod = await dynamicImportAny(['bcrypt', 'bcryptjs'])
if (mod?.hash) return await mod.hash(password, rounds)
} catch {
// ignore and fallback
}
// Fallback to scrypt
return this.scryptHash(password)
}
static async bcryptVerify(password: string, encoded: string): Promise<boolean> {
// If it looks like a bcrypt hash, try optional bcrypt packages
if (encoded.startsWith('$2a$') || encoded.startsWith('$2b$') || encoded.startsWith('$2y$')) {
try {
const mod = await dynamicImportAny(['bcrypt', 'bcryptjs'])
if (mod?.compare) return await mod.compare(password, encoded)
} catch {
// ignore and fallback
}
return false
}
// Otherwise, support scrypt fallback
if (encoded.startsWith('scrypt$')) return this.scryptVerify(password, encoded)
if (encoded.startsWith('pbkdf2$')) return this.pbkdf2Verify(password, encoded)
return false
}
/** HKDF with SHA-256. Returns base64 key bytes. */
static hkdf(ikm: string | Buffer, salt: string | Buffer, info: string | Buffer, length = 32): string {
try {
const okm = hkdfSync('sha256', ikm, salt, info, length)
return b64(okm)
} catch {
// Fallback manual HKDF implementation (RFC 5869)
const prk = createHmac('sha256', salt as any).update(ikm as any).digest()
const n = Math.ceil(length / 32)
const t: any[] = []
let prev: any = Buffer.alloc(0)
for (let i = 0; i < n; i++) {
prev = createHmac('sha256', prk as any)
.update(Buffer.concat([prev, Buffer.from(info as any), Buffer.from([i + 1])]) as any)
.digest() as Buffer
t.push(prev)
}
return b64(Buffer.concat(t).subarray(0, length))
}
}
}
async function dynamicImportAny(modules: string[]): Promise<any | null> {
for (const m of modules) {
try {
// Avoid triggering TS module resolution by computing the specifier
const importer = new Function('m', 'return import(m)') as (m: string) => Promise<any>
const mod = await importer(m)
if (mod) return mod.default ?? mod
} catch {
// continue
}
}
return null
}
```
--------------------------------------------------------------------------------
/src/auth/oauth-providers.ts:
--------------------------------------------------------------------------------
```typescript
import fetch from 'node-fetch'
import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'
import type { OAuthToken, TokenValidationResult, UserInfo } from '../types/auth.js'
import type { ServerAuthConfig } from '../types/config.js'
import { Logger } from '../utils/logger.js'
export interface OAuthProvider {
validateToken(token: string): Promise<TokenValidationResult>
refreshToken(refreshToken: string): Promise<OAuthToken>
getUserInfo(token: string): Promise<UserInfo>
}
export class OAuthError extends Error {
constructor(message: string, public override cause?: unknown) {
super(message)
this.name = 'OAuthError'
}
}
async function postForm(url: string, body: Record<string, string>): Promise<any> {
const res = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
body: new URLSearchParams(body).toString(),
})
const text = await res.text()
if (!res.ok) {
throw new OAuthError(`Token endpoint error ${res.status}: ${text}`)
}
try {
return JSON.parse(text)
} catch {
// GitHub may return urlencoded; parse fallback
return Object.fromEntries(new URLSearchParams(text))
}
}
function toOAuthToken(json: any): OAuthToken {
const expiresIn = 'expires_in' in json ? Number(json.expires_in) : 3600
const scope = Array.isArray(json.scope)
? (json.scope as string[])
: typeof json.scope === 'string'
? (json.scope as string).split(/[ ,]+/).filter(Boolean)
: []
return {
access_token: String(json.access_token),
refresh_token: json.refresh_token ? String(json.refresh_token) : undefined,
expires_at: Date.now() + expiresIn * 1000,
scope,
}
}
export class GitHubOAuthProvider implements OAuthProvider {
constructor(private readonly config: ServerAuthConfig) {}
async validateToken(token: string): Promise<TokenValidationResult> {
try {
const res = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
})
if (!res.ok) {
const text = await res.text()
return { valid: false, error: `GitHub token invalid: ${res.status} ${text}` }
}
const scopesHeader = res.headers.get('x-oauth-scopes')
const scopes = scopesHeader ? scopesHeader.split(',').map((s) => s.trim()).filter(Boolean) : undefined
return { valid: true, scopes }
} catch (err) {
Logger.error('GitHub validateToken failed', err)
return { valid: false, error: String(err) }
}
}
async refreshToken(refreshToken: string): Promise<OAuthToken> {
const json = await postForm(this.config.token_endpoint, {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.config.client_id,
...(this.config.client_secret ? { client_secret: String(this.config.client_secret) } : {}),
})
return toOAuthToken(json)
}
async getUserInfo(token: string): Promise<UserInfo> {
const res = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
})
if (!res.ok) throw new OAuthError(`GitHub userinfo failed: ${res.status}`)
const json = (await res.json()) as any
return { id: String(json.id), name: json.name ?? undefined, email: json.email ?? undefined, avatarUrl: json.avatar_url ?? undefined }
}
}
export class GoogleOAuthProvider implements OAuthProvider {
private jwks = createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'))
constructor(private readonly config: ServerAuthConfig) {}
async validateToken(token: string): Promise<TokenValidationResult> {
// Try as JWT (id_token); fallback to userinfo call for access_token
try {
const { payload } = await jwtVerify(token, this.jwks, {
issuer: ['https://accounts.google.com', 'accounts.google.com'],
audience: this.config.client_id ? String(this.config.client_id) : undefined,
})
const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ') : undefined
const exp = typeof payload.exp === 'number' ? payload.exp * 1000 : undefined
return { valid: true, expiresAt: exp, scopes }
} catch (_e) {
// Not a valid id_token; try userinfo endpoint to validate access token
try {
const res = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) return { valid: false, error: `Google userinfo status ${res.status}` }
return { valid: true }
} catch (err) {
return { valid: false, error: String(err) }
}
}
}
async refreshToken(refreshToken: string): Promise<OAuthToken> {
const json = await postForm(this.config.token_endpoint, {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.config.client_id,
...(this.config.client_secret ? { client_secret: String(this.config.client_secret) } : {}),
})
return toOAuthToken(json)
}
async getUserInfo(token: string): Promise<UserInfo> {
const res = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new OAuthError(`Google userinfo failed: ${res.status}`)
const json = (await res.json()) as any
return { id: String(json.sub), name: json.name, email: json.email, avatarUrl: json.picture }
}
}
export class CustomOAuthProvider implements OAuthProvider {
private jwks?: ReturnType<typeof createRemoteJWKSet>
constructor(private readonly config: ServerAuthConfig & { jwks_uri?: string; issuer?: string; audience?: string }) {
if (this.config['jwks_uri']) {
this.jwks = createRemoteJWKSet(new URL(String(this.config['jwks_uri'])))
}
}
async validateToken(token: string): Promise<TokenValidationResult> {
// Prefer JWT validation if JWKS is provided, else try userinfo proxy via resource endpoint if configured
if (this.jwks) {
try {
const { payload } = await jwtVerify(token, this.jwks, {
issuer: this.config['issuer'] ? String(this.config['issuer']) : undefined,
audience: this.config['audience'] ? String(this.config['audience']) : undefined,
})
const exp = typeof payload.exp === 'number' ? payload.exp * 1000 : undefined
const scopes = typeof payload.scope === 'string' ? payload.scope.split(/[ ,]+/) : undefined
return { valid: true, expiresAt: exp, scopes }
} catch (err) {
return { valid: false, error: String(err) }
}
}
// As a generic fallback, we can't validate without provider-specific endpoint; treat as opaque Bearer
try {
decodeJwt(token) // will throw if not a JWT; but opaque tokens are allowed; just return valid unknown
return { valid: true }
} catch {
return { valid: true } // opaque non-JWT tokens assumed valid at this layer
}
}
async refreshToken(refreshToken: string): Promise<OAuthToken> {
const json = await postForm(this.config.token_endpoint, {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.config.client_id,
...(this.config.client_secret ? { client_secret: String(this.config.client_secret) } : {}),
})
return toOAuthToken(json)
}
async getUserInfo(token: string): Promise<UserInfo> {
// Generic OIDC userinfo often available at `${issuer}/userinfo`; but we only have authorization/token endpoints here.
const issuer = (this.config as any).issuer as string | undefined
if (!issuer) throw new OAuthError('userinfo endpoint unknown for custom provider (missing issuer)')
const url = new URL('/userinfo', issuer).toString()
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } })
if (!res.ok) throw new OAuthError(`Custom OIDC userinfo failed: ${res.status}`)
const json = (await res.json()) as any
return { id: String(json.sub ?? json.id ?? 'unknown'), ...json }
}
}
export function getOAuthProvider(config: ServerAuthConfig & { jwks_uri?: string; issuer?: string; audience?: string }): OAuthProvider {
switch (config.provider) {
case 'github':
return new GitHubOAuthProvider(config)
case 'google':
return new GoogleOAuthProvider(config)
default:
return new CustomOAuthProvider(config)
}
}
```
--------------------------------------------------------------------------------
/src/modules/capability-aggregator.ts:
--------------------------------------------------------------------------------
```typescript
import type { LoadedServer, ServerCapabilities } from '../types/server.js'
import type { ListResourcesResult, ListToolsResult, ToolDefinition, ResourceDefinition, PromptDefinition } from '../types/mcp.js'
import type { AuthHeaders } from '../types/auth.js'
import { Logger } from '../utils/logger.js'
export interface AggregatorOptions {
prefixStrategy?: 'serverId' | 'none'
// base path for discovery relative to server endpoint
capabilitiesEndpoint?: string // default '/capabilities'
toolsEndpoint?: string // default '/mcp/tools/list'
resourcesEndpoint?: string // default '/mcp/resources/list'
}
export interface CapabilityMapEntry {
serverId: string
originalName: string
}
export class CapabilityAggregator {
private readonly options: Required<AggregatorOptions>
private toolMap = new Map<string, CapabilityMapEntry>()
private resourceMap = new Map<string, CapabilityMapEntry>()
constructor(options?: AggregatorOptions) {
this.options = {
prefixStrategy: options?.prefixStrategy ?? 'serverId',
capabilitiesEndpoint: options?.capabilitiesEndpoint ?? '/capabilities',
toolsEndpoint: options?.toolsEndpoint ?? '/mcp/tools/list',
resourcesEndpoint: options?.resourcesEndpoint ?? '/mcp/resources/list',
}
}
reset(): void {
this.toolMap.clear()
this.resourceMap.clear()
}
getMappingForTool(aggregatedName: string): CapabilityMapEntry | undefined {
return this.toolMap.get(aggregatedName)
}
getMappingForResource(aggregatedUri: string): CapabilityMapEntry | undefined {
return this.resourceMap.get(aggregatedUri)
}
async discoverCapabilities(
servers: Map<string, LoadedServer>,
clientToken?: string,
getAuthHeaders?: (serverId: string, clientToken?: string) => Promise<AuthHeaders | undefined>
): Promise<void> {
Logger.info('Discovering capabilities', { servers: Array.from(servers.entries()) })
this.reset()
const fallbackHeaders: AuthHeaders = {}
if (clientToken) fallbackHeaders['Authorization'] = `Bearer ${clientToken}`
await Promise.all(
Array.from(servers.values()).map(async (server) => {
if (!server.endpoint || server.endpoint === 'unknown') {
// For STDIO servers, we need a different approach
if (server.type === 'stdio' && server.config.url?.startsWith('file://')) {
try {
Logger.info('Discovering capabilities for STDIO server', { serverId: server.id })
// Import the STDIO capability discovery module
const { StdioCapabilityDiscovery } = await import('./stdio-capability-discovery.js')
const stdioDiscovery = new StdioCapabilityDiscovery()
const filePath = server.config.url.replace('file://', '')
const caps = await stdioDiscovery.discoverCapabilities(server.id, filePath)
Logger.info('Fetched capabilities for STDIO server', { serverId: server.id, caps })
server.capabilities = caps
this.index(server.id, caps)
Logger.logServerEvent('capabilities_discovered', server.id, {
tools: caps.tools.length,
resources: caps.resources.length,
prompts: caps.prompts?.length ?? 0,
})
} catch (err) {
Logger.warn(`Failed capability discovery for STDIO server ${server.id}`, err)
}
} else {
Logger.warn(`Skipping server with unknown endpoint`, { serverId: server.id, type: server.type })
}
return
}
try {
Logger.info('Fetching capabilities for server', { serverId: server.id, endpoint: server.endpoint })
const headers = (await getAuthHeaders?.(server.id, clientToken)) ?? fallbackHeaders
const caps = await this.fetchCapabilities(server.endpoint, headers)
Logger.info('Fetched capabilities for server', { serverId: server.id, caps })
server.capabilities = caps
this.index(server.id, caps)
Logger.logServerEvent('capabilities_discovered', server.id, {
tools: caps.tools.length,
resources: caps.resources.length,
prompts: caps.prompts?.length ?? 0,
})
} catch (err) {
Logger.warn(`Failed capability discovery for ${server.id}`, err)
}
})
)
}
getAllTools(servers: Map<string, LoadedServer>): ToolDefinition[] {
const result: ToolDefinition[] = []
for (const server of servers.values()) {
const tools = server.capabilities?.tools ?? []
for (const t of tools) {
const name = this.aggregateName(server.id, t.name)
result.push({ ...t, name })
}
}
return result
}
getAllResources(servers: Map<string, LoadedServer>): ResourceDefinition[] {
const result: ResourceDefinition[] = []
for (const server of servers.values()) {
const resources = server.capabilities?.resources ?? []
for (const r of resources) {
const uri = this.aggregateName(server.id, r.uri)
result.push({ ...r, uri })
}
}
return result
}
aggregate(servers: LoadedServer[]): ServerCapabilities {
const tools = servers.flatMap((s) => (s.capabilities?.tools ?? []).map((t) => ({ ...t, name: this.aggregateName(s.id, t.name) })))
const resources = servers.flatMap((s) => (s.capabilities?.resources ?? []).map((r) => ({ ...r, uri: this.aggregateName(s.id, r.uri) })))
const prompts = servers.flatMap((s) => s.capabilities?.prompts ?? [])
return { tools, resources, prompts: prompts.length ? prompts : undefined }
}
// --- internals ---
private index(serverId: string, caps: ServerCapabilities): void {
for (const t of caps.tools) this.toolMap.set(this.aggregateName(serverId, t.name), { serverId, originalName: t.name })
for (const r of caps.resources) this.resourceMap.set(this.aggregateName(serverId, r.uri), { serverId, originalName: r.uri })
}
private aggregateName(serverId: string, name: string): string {
if (this.options.prefixStrategy === 'none') return name
return `${serverId}.${name}`
}
private ensureTrailingSlash(endpoint: string): string {
return endpoint.endsWith('/') ? endpoint : `${endpoint}/`
}
private async fetchCapabilities(endpoint: string, headers: AuthHeaders): Promise<ServerCapabilities> {
const urlCap = new URL(this.options.capabilitiesEndpoint, this.ensureTrailingSlash(endpoint)).toString()
Logger.info('Fetching capabilities from endpoint', { urlCap, headers })
try {
const res = await fetch(urlCap, { headers })
if (res.ok) {
const json = (await res.json()) as any
// Try to coerce shapes
const tools: ToolDefinition[] = Array.isArray(json.tools) ? json.tools : (json.capabilities?.tools ?? [])
const resources: ResourceDefinition[] = Array.isArray(json.resources) ? json.resources : (json.capabilities?.resources ?? [])
const prompts: PromptDefinition[] | undefined = Array.isArray(json.prompts) ? json.prompts : (json.capabilities?.prompts ?? undefined)
Logger.info('Fetched capabilities', { tools, resources, prompts })
return { tools, resources, prompts }
}
} catch (err) {
Logger.debug('Direct capabilities endpoint failed, trying fallbacks', err)
}
// Fallback: fetch tools and resources separately
const [tools, resources] = await Promise.all([this.fetchTools(endpoint, headers), this.fetchResources(endpoint, headers)])
Logger.info('Fetched capabilities using fallbacks', { tools, resources })
return { tools, resources }
}
private async fetchTools(endpoint: string, headers: AuthHeaders): Promise<ToolDefinition[]> {
const url = new URL(this.options.toolsEndpoint, this.ensureTrailingSlash(endpoint)).toString()
try {
const res = await fetch(url, { headers })
if (res.ok) {
const json = (await res.json()) as ListToolsResult
return json.tools ?? []
}
} catch (err) {
Logger.warn('fetchTools failed', err)
}
return []
}
private async fetchResources(endpoint: string, headers: AuthHeaders): Promise<ResourceDefinition[]> {
const url = new URL(this.options.resourcesEndpoint, this.ensureTrailingSlash(endpoint)).toString()
try {
const res = await fetch(url, { headers })
if (res.ok) {
const json = (await res.json()) as ListResourcesResult
return json.resources ?? []
}
} catch (err) {
Logger.warn('fetchResources failed', err)
}
return []
}
}
```
--------------------------------------------------------------------------------
/reports/claude_report_20250815_222153.html:
--------------------------------------------------------------------------------
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Usage Report - Jul 16 to Aug 15, 2025</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
border-radius: 10px;
margin-bottom: 30px;
}
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.header .subtitle { opacity: 0.9; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; }
.card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.card h2 {
font-size: 0.9em;
text-transform: uppercase;
color: #666;
margin-bottom: 10px;
}
.card .value {
font-size: 2em;
font-weight: bold;
color: #333;
}
.card .subtitle {
font-size: 0.9em;
color: #999;
margin-top: 5px;
}
.chart-container {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.chart-container h2 {
margin-bottom: 20px;
color: #333;
}
.insights {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.insights h2 { margin-bottom: 15px; }
.insights ul { list-style: none; }
.insights li {
padding: 10px;
margin-bottom: 10px;
border-left: 3px solid #667eea;
background: #f8f9fa;
}
.warning { border-left-color: #f59e0b !important; background: #fef3c7 !important; }
.opportunity { border-left-color: #10b981 !important; background: #d1fae5 !important; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e5e5;
}
th {
background: #f8f9fa;
font-weight: 600;
}
.footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: #666;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Claude Usage Report - Jul 16 to Aug 15, 2025</h1>
<div class="subtitle">
Jul 16, 2025 -
Aug 15, 2025
</div>
</div>
<div class="grid">
<div class="card">
<h2>Total Cost</h2>
<div class="value">$4175.02</div>
<div class="subtitle">Average $139.17/day</div>
</div>
<div class="card">
<h2>Total Tokens</h2>
<div class="value">2835.19M</div>
<div class="subtitle">28919 requests</div>
</div>
<div class="card">
<h2>Cache Efficiency</h2>
<div class="value">9982.6%</div>
<div class="subtitle">Saved $18780.13</div>
</div>
<div class="card">
<h2>Burn Rate</h2>
<div class="value">$5.81/hr</div>
<div class="subtitle">UNKNOWN</div>
</div>
</div>
<div class="chart-container">
<h2>Daily Usage Trend</h2>
<canvas id="dailyChart"></canvas>
</div>
<div class="chart-container">
<h2>Model Distribution</h2>
<canvas id="modelChart"></canvas>
</div>
<div class="insights">
<h2>Insights & Recommendations</h2>
<h3 style="color: #667eea; margin: 20px 0 10px;">💡 Recommendations</h3>
<ul>
<li>Consider using Claude Haiku for simpler tasks to reduce costs</li><li>Implement request batching to reduce API calls</li>
</ul>
<h3 style="color: #10b981; margin: 20px 0 10px;">✨ Opportunities</h3>
<ul>
<li class="opportunity">Excellent cache efficiency (9982.6%) - saving $18780.13</li>
</ul>
</div>
<div class="chart-container">
<h2>Model Usage</h2>
<table>
<thead>
<tr>
<th>Model</th>
<th>Requests</th>
<th>Total Tokens</th>
<th>Total Cost</th>
<th>Cache Hit Rate</th>
</tr>
</thead>
<tbody>
<tr>
<td>claude-opus-4-1-20250805</td>
<td>6,629</td>
<td>673.84M</td>
<td>$1870.10</td>
<td>95.0%</td>
</tr>
<tr>
<td>claude-opus-4-20250514</td>
<td>5,906</td>
<td>494.37M</td>
<td>$1464.34</td>
<td>94.4%</td>
</tr>
<tr>
<td>claude-sonnet-4-20250514</td>
<td>16,384</td>
<td>1666.97M</td>
<td>$840.58</td>
<td>94.6%</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">
Generated on Aug 15, 2025 22:21 •
Ouroboros Claude Monitoring System v1.0.0
</div>
</div>
<script>
// Daily usage chart
const dailyCtx = document.getElementById('dailyChart').getContext('2d');
new Chart(dailyCtx, {
type: 'line',
data: {
labels: ["Jul 16","Jul 17","Jul 18","Jul 19","Jul 21","Jul 23","Jul 24","Jul 27","Jul 28","Aug 3","Aug 4","Aug 5","Aug 6","Aug 7","Aug 8","Aug 9","Aug 10","Aug 11","Aug 12","Aug 13","Aug 14","Aug 15"],
datasets: [{
label: 'Daily Cost ($)',
data: ["13.96","174.73","116.18","3.61","166.24","81.62","95.45","3.53","78.99","108.26","544.52","10.80","193.35","549.84","300.48","583.22","348.56","28.68","233.65","455.15","6.09","78.10"],
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
}
}
});
// Model distribution chart
const modelCtx = document.getElementById('modelChart').getContext('2d');
new Chart(modelCtx, {
type: 'doughnut',
data: {
labels: ["opus-4","opus-4","sonnet-4"],
datasets: [{
data: ["1870.10","1464.34","840.58"],
backgroundColor: [
'#667eea',
'#764ba2',
'#f59e0b',
'#10b981',
'#ef4444'
]
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'right' }
}
}
});
</script>
</body>
</html>
```
--------------------------------------------------------------------------------
/src/modules/request-router.ts:
--------------------------------------------------------------------------------
```typescript
import type {
CallToolRequest,
CallToolResult,
ListResourcesRequest,
ListResourcesResult,
ListToolsRequest,
ListToolsResult,
ReadResourceRequest,
ReadResourceResult,
SubscribeRequest,
SubscribeResult,
} from '../types/mcp.js'
import type { LoadedServer } from '../types/server.js'
import type { AuthHeaders, OAuthDelegation } from '../types/auth.js'
import { CapabilityAggregator } from './capability-aggregator.js'
import { Logger } from '../utils/logger.js'
import { CircuitBreaker } from '../routing/circuit-breaker.js'
import { LoadBalancer } from '../routing/load-balancer.js'
import { RouteRegistry } from '../routing/route-registry.js'
import { RetryHandler } from '../routing/retry-handler.js'
import type { RoutingConfig } from '../types/config.js'
export interface RouterOptions {
callToolEndpoint?: string // default '/mcp/tools/call'
readResourceEndpoint?: string // default '/mcp/resources/read'
routing?: RoutingConfig
}
export interface RouterOptions {
callToolEndpoint?: string // default '/mcp/tools/call'
readResourceEndpoint?: string // default '/mcp/resources/read'
routing?: RoutingConfig
}
export class RequestRouter {
private readonly options: Required<Omit<RouterOptions, 'routing'>> & { routing: RoutingConfig }
private readonly circuit: CircuitBreaker
private readonly retry: RetryHandler
private readonly lb: LoadBalancer
private readonly registry: RouteRegistry
constructor(
private readonly servers: Map<string, LoadedServer>,
private readonly aggregator: CapabilityAggregator,
private readonly getAuthHeaders?: (
serverId: string,
clientToken?: string
) => Promise<AuthHeaders | OAuthDelegation | undefined>,
options?: RouterOptions
) {
this.options = {
callToolEndpoint: options?.callToolEndpoint ?? '/mcp/tools/call',
readResourceEndpoint: options?.readResourceEndpoint ?? '/mcp/resources/read',
routing: options?.routing ?? {},
}
this.circuit = new CircuitBreaker({
failureThreshold: this.options.routing.circuitBreaker?.failureThreshold ?? 5,
successThreshold: this.options.routing.circuitBreaker?.successThreshold ?? 2,
recoveryTimeoutMs: this.options.routing.circuitBreaker?.recoveryTimeoutMs ?? 30_000,
name: 'request-router',
})
this.retry = new RetryHandler({
maxRetries: this.options.routing.retry?.maxRetries ?? 2,
baseDelayMs: this.options.routing.retry?.baseDelayMs ?? 250,
maxDelayMs: this.options.routing.retry?.maxDelayMs ?? 4_000,
backoffFactor: this.options.routing.retry?.backoffFactor ?? 2,
jitter: this.options.routing.retry?.jitter ?? 'full',
retryOn: this.options.routing.retry?.retryOn ?? { networkErrors: true, httpStatusClasses: [5], httpStatuses: [408, 429] },
})
this.lb = new LoadBalancer({ strategy: this.options.routing.loadBalancer?.strategy ?? 'round_robin' })
this.registry = new RouteRegistry(this.servers, this.circuit, this.lb)
}
getServers(): Map<string, LoadedServer> {
return this.servers
}
async routeListTools(_req: ListToolsRequest): Promise<ListToolsResult> {
const tools = this.aggregator.getAllTools(this.servers)
return { tools }
}
async routeCallTool(req: CallToolRequest, clientToken?: string): Promise<CallToolResult> {
// Resolve mapping via aggregator if available
const map = this.aggregator.getMappingForTool(req.name)
const serverId = map?.serverId ?? req.name.split('.')[0]
const toolName = map?.originalName ?? (req.name.includes('.') ? req.name.split('.').slice(1).join('.') : req.name)
const server = this.servers.get(serverId)
if (!server) {
return { content: { error: `Server ${serverId} not found` }, isError: true }
}
// Handle STDIO servers differently
if (server.type === 'stdio') {
try {
Logger.info('Routing call to STDIO server', { serverId, toolName })
const { StdioCapabilityDiscovery } = await import('./stdio-capability-discovery.js')
const stdioDiscovery = new StdioCapabilityDiscovery()
const result = await stdioDiscovery.callTool(serverId, toolName, req.arguments ?? {})
return result.result || result
} catch (error) {
Logger.error('STDIO tool call failed', { serverId, toolName, error })
return { content: { error: `STDIO tool call failed: ${error}` }, isError: true }
}
}
const resolution = this.registry.resolve(serverId)
if (!resolution) {
return { content: { error: `Route not found for tool ${req.name}` }, isError: true }
}
const headers: AuthHeaders = { 'content-type': 'application/json' }
const auth = await this.getAuthHeaders?.(serverId, clientToken)
if (auth && (auth as OAuthDelegation).type === 'oauth_delegation') {
return { content: { error: 'OAuth delegation required', details: auth }, isError: true }
}
const extra = (auth as AuthHeaders) ?? (clientToken ? { Authorization: `Bearer ${clientToken}` } : {})
Object.assign(headers, extra)
const url = new URL(this.options.callToolEndpoint, this.ensureTrailingSlash(resolution.instance.url)).toString()
const key = `${serverId}::${resolution.instance.id}`
try {
const json = await this.circuit.execute(key, async () => {
const res = await this.fetchWithRetry(url, {
method: 'POST',
headers,
body: JSON.stringify({ name: toolName, arguments: req.arguments ?? {} }),
})
return (await res.json()) as CallToolResult
})
this.registry.markSuccess(serverId, resolution.instance.id)
return json
} catch (err) {
this.registry.markFailure(serverId, resolution.instance.id)
Logger.warn('routeCallTool failed', err)
return { content: { error: String(err) }, isError: true }
}
}
async routeListResources(_req: ListResourcesRequest): Promise<ListResourcesResult> {
const resources = this.aggregator.getAllResources(this.servers)
return { resources }
}
async routeReadResource(req: ReadResourceRequest, clientToken?: string): Promise<ReadResourceResult> {
const map = this.aggregator.getMappingForResource(req.uri)
const serverId = map?.serverId ?? req.uri.split('.')[0]
const resourceUri = map?.originalName ?? (req.uri.includes('.') ? req.uri.split('.').slice(1).join('.') : req.uri)
const server = this.servers.get(serverId)
if (!server) {
return { contents: `Server ${serverId} not found`, mimeType: 'text/plain' }
}
// Handle STDIO servers differently
if (server.type === 'stdio') {
try {
Logger.info('Routing read to STDIO server', { serverId, resourceUri })
const { StdioCapabilityDiscovery } = await import('./stdio-capability-discovery.js')
const stdioDiscovery = new StdioCapabilityDiscovery()
const result = await stdioDiscovery.readResource(serverId, resourceUri)
return result.result || result
} catch (error) {
Logger.error('STDIO resource read failed', { serverId, resourceUri, error })
return { contents: `STDIO resource read failed: ${error}`, mimeType: 'text/plain' }
}
}
const resolution = this.registry.resolve(serverId)
if (!resolution) {
return { contents: `Route not found for resource ${req.uri}`, mimeType: 'text/plain' }
}
const headers: AuthHeaders = { 'content-type': 'application/json' }
const auth = await this.getAuthHeaders?.(serverId, clientToken)
if (auth && (auth as OAuthDelegation).type === 'oauth_delegation') {
return { contents: JSON.stringify({ error: 'OAuth delegation required', details: auth }), mimeType: 'application/json' }
}
const extra = (auth as AuthHeaders) ?? (clientToken ? { Authorization: `Bearer ${clientToken}` } : {})
Object.assign(headers, extra)
const url = new URL(this.options.readResourceEndpoint, this.ensureTrailingSlash(resolution.instance.url)).toString()
const key = `${serverId}::${resolution.instance.id}`
try {
const json = await this.circuit.execute(key, async () => {
const res = await this.fetchWithRetry(url, { method: 'POST', headers, body: JSON.stringify({ uri: resourceUri }) })
return (await res.json()) as ReadResourceResult
})
this.registry.markSuccess(serverId, resolution.instance.id)
return json
} catch (err) {
this.registry.markFailure(serverId, resolution.instance.id)
Logger.warn('routeReadResource failed', err)
return { contents: String(err), mimeType: 'text/plain' }
}
}
async routeSubscribe(_req: SubscribeRequest): Promise<SubscribeResult> {
// Not implemented yet; aggregation events out of scope here
return { ok: true }
}
private ensureTrailingSlash(endpoint: string): string {
return endpoint.endsWith('/') ? endpoint : `${endpoint}/`
}
private async fetchWithRetry(input: string, init: RequestInit): Promise<Response> {
return this.retry.execute(async () => {
const res = await fetch(input, init)
if (!res.ok) {
// For retry logic, throw an error carrying status to trigger retry policy
const err = new Error(`HTTP ${res.status}`) as Error & { status?: number }
;(err as any).status = res.status
throw err
}
return res
}, (ctx) => {
Logger.debug('Retrying upstream request', ctx)
})
}
}
```
--------------------------------------------------------------------------------
/docs/testing/phase-9-testing-architecture.md:
--------------------------------------------------------------------------------
```markdown
# Phase 9 — Comprehensive Testing Architecture
This document specifies the end-to-end testing architecture for the Master MCP Server across Node.js and Cloudflare Workers. It is tailored to this codebase (TypeScript, ESM, strict mode) and builds on Phases 1–8 (auth, module loading, routing, config, OAuth, utils).
## Goals
- Cross-platform: Node 18+ and Cloudflare Workers.
- ESM + TypeScript strict compatibility.
- Leverage existing utilities (logging, validation, monitoring).
- Clear test layering: unit, integration, E2E, plus security and performance.
- Deterministic, isolated, and parallel-friendly test runs.
- CI/CD automation with quality gates and coverage thresholds.
---
## Framework Selection
- Unit/Integration (Node): Vitest
- Fast, ESM-native, TS-friendly. Built-in mock timers and coverage via V8.
- Supertest for Express-based HTTP integration of Node runtime (`src/index.ts`).
- Undici MockAgent (optional) for fetch interception on Node 18, when not spinning stub servers.
- Unit/Integration (Workers): Vitest + Miniflare 3
- `miniflare` test environment for `Request`/`Response` compatibility.
- Target `OAuthFlowController.handleRequest` and any worker entrypoints (`src/runtime/worker.ts`).
- E2E (HTTP-level): Vitest test suite using real HTTP listeners
- Node: start Express via `createServer(true)` and use Supertest/HTTP.
- Workers: run Miniflare instance or `wrangler dev` in CI as needed.
- Performance: Artillery
- Simple YAML scenarios to stress authentication and routing endpoints.
- Separate CI job; can be run locally against dev servers.
- Security: Vitest + fast-check (property-based) + static assertions
- Fuzz inputs for token parsing, state/PKCE validation, and router input validation.
- Optional OWASP ZAP baseline in CI (nightly) if desired.
---
## Directory Structure
```
tests/
unit/
routing/ # circuit-breaker, retry, load-balancer
modules/ # aggregator, router (with fetch mocked)
oauth/ # pkce/state/validator
utils/ # logger, validation helpers
integration/
node/ # express endpoints, config manager wiring
workers/ # flow-controller.handleRequest via Miniflare
oauth/ # callback flow with mock OIDC provider
e2e/
node/ # start full HTTP server and hit /mcp/*, /oauth/*
workers/ # worker entrypoint end-to-end
mocks/
oauth/ # mock OIDC provider (Node + Worker variants)
mcp/ # fake MCP backends (capabilities/tools/resources)
http/ # undici MockAgent helpers (Node)
factories/
configFactory.ts # MasterConfig, ServerConfig builders
oauthFactory.ts # tokens/states/JWKS
mcpFactory.ts # tool/resource definitions
fixtures/
capabilities.json
tools.json
resources.json
perf/
artillery/
auth-routing.yaml # load scenarios for auth + routing
security/
oauth.spec.ts # PKCE/state/nonce fuzz tests (fast-check)
_setup/
vitest.setup.ts # global hooks, silent logs, fake timers config
miniflare.setup.ts # worker env config for tests
_utils/
test-server.ts # ephemeral HTTP servers
mock-fetch.ts # Node fetch interception (Undici)
log-capture.ts # capture + assert logs
```
Notes:
- The unit layer uses pure module-level tests with fetch mocked or local HTTP stubs.
- Integration tests are black-box at module boundaries (e.g., ProtocolHandler + RequestRouter + mocks).
- E2E spins the real Node server and/or Miniflare worker with realistic mocks for upstream MCP servers and OIDC.
---
## Test Environment Management
- Node vs Workers selection
- Node-specific Vitest config: `vitest.config.ts` (environment: node)
- Workers-specific Vitest config: `vitest.worker.config.ts` (environment: miniflare)
- Test files can explicitly opt into Miniflare with `// @vitest-environment miniflare`.
- Isolation and determinism
- Use Vitest’s fake timers for retry/circuit tests.
- Ephemeral HTTP servers: `tests/_utils/test-server.ts` binds to port 0 and auto-closes in `afterEach`.
- Log capture: disable or capture logs via `Logger.configure({ json: true })` for stable assertions.
- Cross-platform resources
- OAuth flows invoke real HTTP endpoints; a mock OIDC provider runs as an in-process HTTP server in both Node and Miniflare scenarios (Miniflare tests remain in a Node host context, so spinning a Node HTTP stub is acceptable).
- Upstream MCP backends emulated by `fake-backend` servers exposing `/capabilities`, `/mcp/tools/*`, `/mcp/resources/*`.
---
## Mock and Stub Architecture
### MCP Protocol Backends
- Fake backend servers respond with deterministic JSON:
- `GET /capabilities`: lists tools/resources/prompts
- `POST /mcp/tools/list` and `/mcp/resources/list`: optional fallbacks
- `POST /mcp/tools/call`: echoes arguments or returns canned results
- `POST /mcp/resources/read`: returns fixture content
- Supports Bearer tokens to simulate auth propagation from Master.
### OAuth Provider (OIDC) Mock
- Node HTTP server (Express or http module) serving:
- `/.well-known/openid-configuration`
- `/authorize`: simulates auth code issuance by redirecting with `code` + `state`
- `/token`: returns JSON access token, optional refresh, scopes, `expires_in`
- `/jwks.json`: JWKS for completeness if JOSE validation is later added upstream
- Uses `jose` to generate ephemeral key material and produce signed tokens if needed.
- Configurable via factory helpers to tailor provider metadata per test.
### HTTP Mocking for Node (optional)
- `undici` MockAgent helper when spinning HTTP servers is overkill.
- Route by URL patterns and methods; fallback to network disallowed.
---
## Test Data Management
- Fixtures: JSON payloads for tools/resources/capabilities. Keep small and readable.
- Factories:
- `configFactory`: produce `MasterConfig` + `ServerConfig` with sensible defaults (ports, endpoints, auth strategies).
- `oauthFactory`: generate PKCE/state payloads and basic OAuth token shapes.
- `mcpFactory`: create tool/resource definitions and common requests.
- State/Token Stores:
- In-memory only; reset across tests.
- If a persistent DB is introduced later:
- Node: use SQLite in-memory or Testcontainers in CI; provide cleanup hooks.
- Workers: use Miniflare KV/D1 bindings with per-test namespaces.
---
## Performance Testing Strategy (Artillery)
- Scenarios:
- OAuth authorize/token (mock provider) happy-path latency and error rates.
- Routing: `POST /mcp/tools/call` with mixed success/failure from backends to exercise retry/circuit logic.
- Server lifecycle: parallel discovery calls to `/capabilities` against multiple backends.
- Metrics:
- p50/p90/p99 latency, RPS, error rates per route.
- Custom logs via `Logger.time` around critical paths; scrape from structured logs when running locally.
- CI:
- Run on a separate “performance” workflow or on nightly schedules to avoid slowing PRs.
---
## Security Testing Architecture
- Property-based testing with `fast-check` for:
- `FlowValidator.validateReturnTo`: ensure only safe origins/paths pass.
- `StateManager` and `PKCEManager`: state integrity, one-time consumption, and PKCE verifier binding.
- Input validation for router requests (e.g., tool/resource names) using `utils/validation` helpers.
- OAuth flow protections:
- Ensure `state` is required and consumed exactly once.
- Enforce PKCE method presence, verify rejection on mismatch.
- Token exchange failure handling and error surface is sanitized.
- Optional dynamic scans:
- OWASP ZAP baseline against local server in CI (nightly) to catch obvious misconfigurations.
---
## CI/CD Integration
### Jobs
- Lint + Typecheck: ESLint and `tsc -p tsconfig.node.json --noEmit`.
- Unit + Integration (Node): Vitest with coverage.
- Unit + Integration (Workers): Vitest (Miniflare) with coverage.
- E2E: start local server; run black-box tests.
- Security: property tests; optional ZAP baseline (nightly).
- Performance: Artillery (nightly or gated by label).
### Coverage and Quality Gates
- Coverage via Vitest v8 provider; thresholds:
- Global: `branches: 80%`, `functions: 85%`, `lines: 85%`, `statements: 85%`.
- Critical modules (routing, oauth): per-file `lines: 90%` target in follow-up.
- Fail PR job if thresholds not met.
- Upload `lcov` or `cobertura` to CI artifacts (or Codecov if desired).
---
## Test Utilities Integration (Phase 8)
- Logger: `Logger.configure({ json: true, level: 'error' })` in test setup; use `log-capture` to assert important events.
- Validation: assert guards (`assertString`, `sanitizeObject`) across boundary tests; fuzz via fast-check.
- Monitoring: wrap performance-critical test paths with `Logger.time` and assert upper bounds during perf runs.
---
## Local and CI Usage
- Local:
- `vitest -c vitest.config.ts` for Node suites.
- `vitest -c vitest.worker.config.ts` for Worker suites.
- `artillery run tests/perf/artillery/auth-routing.yaml` for load tests.
- CI:
- Run jobs in parallel; collect coverage and artifacts.
- Gate merges on lint, typecheck, unit/integration, and coverage.
---
## Next Steps (Implementation Guide)
1. Install dev deps: vitest, @vitest/coverage-v8, supertest, miniflare, artillery, fast-check, @types/supertest.
2. Add scripts: `test`, `test:node`, `test:workers`, `test:coverage`, `test:e2e`.
3. Fill factories and mocks with minimal working endpoints.
4. Seed initial critical tests:
- routing: circuit-breaker/retry/load-balancer
- oauth: state, pkce, callback error handling
- aggregator: discovery + mapping
- protocol: call tool/resource happy path and error path
```
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js:
--------------------------------------------------------------------------------
```javascript
import {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever
} from "./chunk-P2XGSYO7.js";
import "./chunk-HVR2FF6M.js";
export {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
computedAsync as asyncComputed,
refAutoReset as autoResetRef,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
computedWithControl as controlledComputed,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
reactify as createReactiveFn,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
refDebounced as debouncedRef,
watchDebounced as debouncedWatch,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
computedEager as eagerComputed,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
watchIgnorable as ignorableWatch,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
watchPausable as pausableWatch,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
refThrottled as throttledRef,
watchThrottled as throttledWatch,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
refDebounced as useDebounce,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
refThrottled as useThrottle,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever
};
//# sourceMappingURL=vitepress___@vueuse_core.js.map
```
--------------------------------------------------------------------------------
/src/modules/module-loader.ts:
--------------------------------------------------------------------------------
```typescript
import type { LoadedServer, ServerProcess, ServerType } from '../types/server.js'
import type { ServerConfig } from '../types/config.js'
import type { AuthHeaders } from '../types/auth.js'
import { Logger } from '../utils/logger.js'
export interface ModuleLoaderOptions {
healthEndpoint?: string // path appended to endpoint, defaults to '/health'
capabilitiesEndpoint?: string // path appended to endpoint, defaults to '/capabilities'
defaultHostname?: string // defaults to 'localhost'
}
export interface ModuleLoader {
loadServers(configs: ServerConfig[], clientToken?: string): Promise<Map<string, LoadedServer>>
load(config: ServerConfig, clientToken?: string): Promise<LoadedServer>
unload(id: string): Promise<void>
performHealthCheck(server: LoadedServer, clientToken?: string): Promise<boolean>
restartServer(serverId: string): Promise<void>
}
/**
* DefaultModuleLoader implements multi-source loading with cross-platform process placeholders.
* It avoids Node-specific APIs so it can compile for both Node and Workers builds.
* Actual spawning should be implemented by a platform-specific adapter in later phases.
*/
export class DefaultModuleLoader implements ModuleLoader {
private servers = new Map<string, LoadedServer>()
private options: Required<ModuleLoaderOptions>
constructor(options?: ModuleLoaderOptions) {
this.options = {
healthEndpoint: options?.healthEndpoint ?? '/health',
capabilitiesEndpoint: options?.capabilitiesEndpoint ?? '/capabilities',
defaultHostname: options?.defaultHostname ?? 'localhost',
}
}
async loadServers(configs: ServerConfig[], clientToken?: string): Promise<Map<string, LoadedServer>> {
const results = await Promise.all(
configs.map(async (cfg) => {
try {
const server = await this.load(cfg, clientToken)
return [cfg.id, server] as const
} catch (err) {
Logger.error(`Failed to load server ${cfg.id}`, err)
const server: LoadedServer = {
id: cfg.id,
type: 'unknown',
endpoint: 'unknown',
config: cfg,
status: 'error',
lastHealthCheck: Date.now(),
}
return [cfg.id, server] as const
}
})
)
for (const [id, s] of results) this.servers.set(id, s)
return new Map(this.servers)
}
async load(config: ServerConfig, clientToken?: string): Promise<LoadedServer> {
const type = this.detectServerType(config)
const base: LoadedServer = {
id: config.id,
type,
endpoint: this.deriveEndpoint(config),
config,
status: 'starting',
lastHealthCheck: 0,
}
let loaded: LoadedServer
switch (config.type) {
case 'git':
loaded = await this.loadFromGit(config, base)
break
case 'npm':
loaded = await this.loadFromNpm(config, base)
break
case 'pypi':
loaded = await this.loadFromPypi(config, base)
break
case 'docker':
loaded = await this.loadFromDocker(config, base)
break
case 'local':
loaded = await this.loadFromLocal(config, base)
break
default:
loaded = base
}
// Immediate health check to set running/error
try {
const ok = await this.performHealthCheck(loaded, clientToken)
loaded.status = ok ? 'running' : 'error'
} catch (err) {
Logger.warn(`Health check failed for ${config.id}`, err)
loaded.status = 'error'
}
this.servers.set(loaded.id, loaded)
return loaded
}
async unload(id: string): Promise<void> {
const server = this.servers.get(id)
if (!server) return
try {
await server.process?.stop()
} catch (err) {
Logger.warn(`Error stopping server ${id}`, err)
} finally {
this.servers.delete(id)
Logger.logServerEvent('unloaded', id)
}
}
async restartServer(serverId: string): Promise<void> {
const server = this.servers.get(serverId)
if (!server) throw new Error(`Server not found: ${serverId}`)
Logger.logServerEvent('restarting', serverId)
try {
await server.process?.stop()
} catch (err) {
Logger.warn(`Error stopping server ${serverId} during restart`, err)
}
// Re-load using the same config
const reloaded = await this.load(server.config)
this.servers.set(serverId, reloaded)
}
async performHealthCheck(server: LoadedServer, clientToken?: string): Promise<boolean> {
if (!server.endpoint || server.endpoint === 'unknown') {
server.lastHealthCheck = Date.now()
server.status = 'error'
return false
}
const url = new URL(this.options.healthEndpoint, this.ensureTrailingSlash(server.endpoint)).toString()
const headers: AuthHeaders = {}
// In Phase 3, auth integration is handled at higher layers; here we only accept a caller-provided token.
if (clientToken) headers['Authorization'] = `Bearer ${clientToken}`
try {
const res = await fetch(url, { headers })
server.lastHealthCheck = Date.now()
if (!res.ok) return false
const ct = res.headers.get('content-type') || ''
if (ct.includes('application/json')) {
const json = (await res.json()) as any
return Boolean(json?.ok ?? true)
}
return true
} catch (err) {
server.lastHealthCheck = Date.now()
Logger.warn(`Health check request failed for ${server.id}`, err)
return false
}
}
// --- Multi-source loading stubs (network/process performed outside this module in later phases) ---
private async loadFromGit(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
// In a full implementation, we'd clone and install. Here we assume it's pre-built and start it.
Logger.logServerEvent('loadFromGit', config.id, { url: config.url, branch: config.branch })
return this.startRuntime(config, base)
}
private async loadFromNpm(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
Logger.logServerEvent('loadFromNpm', config.id, { pkg: config.package, version: config.version })
return this.startRuntime(config, base)
}
private async loadFromPypi(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
Logger.logServerEvent('loadFromPypi', config.id, { pkg: config.package, version: config.version })
return this.startRuntime(config, base)
}
private async loadFromDocker(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
Logger.logServerEvent('loadFromDocker', config.id, { image: config.package, tag: config.version })
// Docker orchestration would run the container exposing a port; we only resolve endpoint here
return { ...base, status: 'starting' }
}
private async loadFromLocal(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
Logger.logServerEvent('loadFromLocal', config.id, { path: config.url })
Logger.info('Loading local server', { config })
const result = await this.startRuntime(config, base)
Logger.info('Loaded local server', { result })
return result
}
// --- Runtime orchestration and type detection ---
private detectServerType(config: ServerConfig): ServerType {
// Heuristics: look at package name/url/args for hints
const name = (config.package ?? config.url ?? '').toLowerCase()
Logger.info('Detecting server type', { name, config })
// For file URLs, we should detect as stdio
if (config.url && config.url.startsWith('file://')) {
Logger.info('Assuming stdio server for file URL', { url: config.url })
return 'stdio'
}
// For URLs, we can't determine the type from the URL itself
// Let's assume it's a node server for HTTP URLs
if (config.url && /^https?:\/\//i.test(config.url)) {
Logger.info('Assuming node server for HTTP URL', { url: config.url })
return 'node'
}
if (name.endsWith('.py') || /py|pypi|python/.test(name)) return 'python'
if (/ts|typescript/.test(name)) return 'typescript'
if (/node|js|npm/.test(name)) return 'node'
return 'unknown'
}
private deriveEndpoint(config: ServerConfig): string {
const port = config.config.port
if (port) return `http://${this.options.defaultHostname}:${port}`
// If URL looks like http(s):// use as-is
const url = config.url ?? ''
Logger.info('Deriving endpoint', { url, config })
if (/^https?:\/\//i.test(url)) return url
// For file URLs (STDIO servers), we can't derive an HTTP endpoint
if (url.startsWith('file://')) return 'unknown'
return 'unknown'
}
private async startRuntime(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
const type = this.detectServerType(config)
Logger.info('Detected server type', { type, config })
let proc: ServerProcess | undefined
try {
// For remote servers (HTTP URLs), we don't need to start a process
if (config.url && /^https?:\/\//i.test(config.url)) {
Logger.info('Remote server, no process to start', { url: config.url })
proc = undefined
} else if (type === 'stdio') {
// For STDIO servers, start as a child process
Logger.info('Starting STDIO server as child process', { url: config.url })
const { StdioManager } = await import('./stdio-manager.js')
const stdioManager = new StdioManager()
const filePath = config.url!.replace('file://', '')
proc = await stdioManager.startServer(config.id, filePath, config.config.environment)
} else if (type === 'python') {
proc = await this.startPythonServer(config)
} else if (type === 'typescript' || type === 'node') {
proc = await this.startTypeScriptServer(config)
} else {
// Unknown: assume externally managed endpoint
proc = undefined
}
} catch (err) {
Logger.error(`Failed to start runtime for ${config.id}`, err)
return { ...base, status: 'error' }
}
Logger.info('Started runtime', { proc, base })
return { ...base, process: proc, status: 'starting' }
}
// Cross-platform placeholders. Real implementation should manage child processes per-OS.
private async startPythonServer(_config: ServerConfig): Promise<ServerProcess> {
// Placeholder: assume an external process is started via orchestrator. Provide a no-op stop.
return { stop: async () => void 0 }
}
private async startTypeScriptServer(config: ServerConfig): Promise<ServerProcess> {
// Check if this is a file URL that should be started as a child process
if (config.url && config.url.startsWith('file://')) {
Logger.info('Starting TypeScript server as child process', { url: config.url })
// Import child_process dynamically
const { spawn } = await import('node:child_process')
// Convert file:// URL to path
const filePath = config.url.replace('file://', '')
// Spawn the process
const proc = spawn('node', [filePath], {
stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
env: { ...process.env, ...config.config.environment }
})
return {
pid: proc.pid,
stop: async () => {
return new Promise((resolve) => {
proc.kill()
setTimeout(() => resolve(), 1000) // Wait 1 second for graceful shutdown
})
}
}
}
// Placeholder: assume an external process is started via orchestrator. Provide a no-op stop.
return { stop: async () => void 0 }
}
private ensureTrailingSlash(endpoint: string): string {
if (!endpoint.endsWith('/')) return `${endpoint}/`
return endpoint
}
}
```
--------------------------------------------------------------------------------
/src/oauth/flow-controller.ts:
--------------------------------------------------------------------------------
```typescript
import type { MasterConfig } from '../types/config.js'
import type { OAuthToken } from '../types/auth.js'
import { PKCEManager } from './pkce-manager.js'
import { StateManager } from './state-manager.js'
import { FlowValidator } from './flow-validator.js'
import { CallbackHandler } from './callback-handler.js'
import { WebInterface } from './web-interface.js'
import { Logger } from '../utils/logger.js'
export interface FlowControllerDeps {
getConfig: () => MasterConfig
// Called to store a delegated token when server + client context is known
storeDelegatedToken?: (clientToken: string, serverId: string, token: OAuthToken) => Promise<void>
}
export class OAuthFlowController {
private readonly pkce = new PKCEManager()
private readonly state = new StateManager()
private readonly validator: FlowValidator
private readonly web = new WebInterface()
private readonly deps: FlowControllerDeps
private readonly basePath: string
constructor(deps: FlowControllerDeps, basePath = '/oauth') {
this.validator = new FlowValidator(deps.getConfig)
this.deps = deps
this.basePath = basePath
}
// Compute baseUrl from request context
private getBaseUrlFromExpress(req: any): string {
const cfg = this.deps.getConfig()
if (cfg.hosting?.base_url) return cfg.hosting.base_url
const proto = (req.protocol as string) || 'http'
const host = (req.get?.('host') as string) || req.headers?.host
return `${proto}://${host}`
}
private getBaseUrlFromRequest(req: Request): string {
const cfg = this.deps.getConfig()
if (cfg.hosting?.base_url) return cfg.hosting.base_url
try {
const u = new URL(req.url)
return `${u.protocol}//${u.host}`
} catch {
return 'http://localhost'
}
}
// Express registration (no direct dependency on express types)
registerExpress(app: any): void {
const base = this.basePath
// GET /oauth/authorize
app.get(`${base}/authorize`, async (req: any, res: any) => {
try {
const query = req.query || {}
const providerParam = typeof query.provider === 'string' ? query.provider : undefined
const serverId = typeof query.server_id === 'string' ? query.server_id : undefined
const scopesParam = typeof query.scope === 'string' ? query.scope : undefined
const returnTo = this.validator.validateReturnTo(
typeof query.return_to === 'string' ? query.return_to : undefined,
this.getBaseUrlFromExpress(req)
)
const { config, providerId } = this.validator.resolveProvider({ provider: providerParam, serverId })
const state = this.state.create({ provider: providerId, serverId, clientToken: this.getClientTokenFromExpress(req), returnTo })
const { challenge, method } = await this.pkce.generate(state)
const baseUrl = this.getBaseUrlFromExpress(req)
const redirectUri = new URL(`${this.basePath}/callback`, baseUrl).toString()
const authUrl = new URL(config.authorization_endpoint)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('client_id', config.client_id)
authUrl.searchParams.set('redirect_uri', redirectUri)
const scope = scopesParam ?? (config.scopes ? config.scopes.join(' ') : '')
if (scope) authUrl.searchParams.set('scope', scope)
authUrl.searchParams.set('state', state)
authUrl.searchParams.set('code_challenge', challenge)
authUrl.searchParams.set('code_challenge_method', method)
// Render a small redirect page to avoid exposing long URLs in Location header logs
res.set('content-type', 'text/html; charset=utf-8')
res.status(200).send(this.web.renderRedirectPage(providerId, authUrl.toString()))
} catch (err) {
Logger.warn('OAuth authorize failed', err)
res.redirect(`${this.basePath}/error`)
}
})
// GET /oauth/callback
app.get(`${base}/callback`, async (req: any, res: any) => {
try {
const params = new URLSearchParams(req.query as Record<string, string>)
const providerParam = typeof req.query?.provider === 'string' ? (req.query.provider as string) : undefined
const serverId = typeof req.query?.server_id === 'string' ? (req.query.server_id as string) : undefined
const { config } = this.validator.resolveProvider({ provider: providerParam, serverId })
const cb = new CallbackHandler({
config: this.deps.getConfig(),
pkceManager: this.pkce,
stateManager: this.state,
baseUrl: this.getBaseUrlFromExpress(req),
storeDelegatedToken: this.deps.storeDelegatedToken,
})
const result = await cb.handleCallback(params, config)
if (result.error) {
res.redirect(`${this.basePath}/error?msg=${encodeURIComponent(result.error)}`)
return
}
const returnTo = result.state?.returnTo
if (returnTo) {
res.redirect(returnTo)
} else {
res.set('content-type', 'text/html; charset=utf-8')
res.status(200).send(this.web.renderSuccessPage('Authorization complete. You may close this window.'))
}
} catch (err) {
Logger.warn('OAuth callback failed', err)
res.redirect(`${this.basePath}/error`)
}
})
// POST /oauth/token
app.post(`${base}/token`, async (req: any, res: any) => {
try {
const body = req.body || {}
const state = typeof body.state === 'string' ? body.state : undefined
const code = typeof body.code === 'string' ? body.code : undefined
const providerParam = typeof body.provider === 'string' ? body.provider : undefined
const serverId = typeof body.server_id === 'string' ? body.server_id : undefined
if (!state || !code) {
res.status(400).json({ error: 'Missing state or code' })
return
}
const { config } = this.validator.resolveProvider({ provider: providerParam, serverId })
const cb = new CallbackHandler({
config: this.deps.getConfig(),
pkceManager: this.pkce,
stateManager: this.state,
baseUrl: this.getBaseUrlFromExpress(req),
storeDelegatedToken: this.deps.storeDelegatedToken,
})
const result = await cb.handleCallback(new URLSearchParams({ state, code }), config)
if (result.error) {
res.status(400).json({ error: result.error })
return
}
// For security, do not return tokens to the browser; we store server-side
res.json({ ok: true })
} catch (err) {
Logger.warn('OAuth token exchange failed', err)
res.status(500).json({ error: 'Token exchange failed' })
}
})
// Success and error pages
app.get(`${base}/success`, (_req: any, res: any) => {
res.set('content-type', 'text/html; charset=utf-8')
res.status(200).send(this.web.renderSuccessPage())
})
app.get(`${base}/error`, (req: any, res: any) => {
const msg = typeof req.query?.msg === 'string' ? (req.query.msg as string) : undefined
res.set('content-type', 'text/html; charset=utf-8')
res.status(200).send(this.web.renderErrorPage(msg))
})
}
private getClientTokenFromExpress(req: any): string | undefined {
const h = (req.headers?.authorization as string) || (req.headers?.Authorization as string)
if (typeof h === 'string' && h.toLowerCase().startsWith('bearer ')) return h.slice(7)
return undefined
}
// Cross-platform Worker-style request handler
async handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url)
const path = url.pathname
if (req.method === 'GET' && path.endsWith(`${this.basePath}/authorize`)) {
try {
const providerParam = (url.searchParams.get('provider') || undefined) as string | undefined
const serverId = (url.searchParams.get('server_id') || undefined) as string | undefined
const scopesParam = (url.searchParams.get('scope') || undefined) as string | undefined
const returnTo = this.validator.validateReturnTo(url.searchParams.get('return_to'), this.getBaseUrlFromRequest(req))
const { config, providerId } = this.validator.resolveProvider({ provider: providerParam, serverId })
// Cannot reliably get Authorization header in some browser flows; ignore client token in Workers
const state = this.state.create({ provider: providerId, serverId, clientToken: undefined, returnTo })
const { challenge, method } = await this.pkce.generate(state)
const redirectUri = new URL(`${this.basePath}/callback`, this.getBaseUrlFromRequest(req)).toString()
const authUrl = new URL(config.authorization_endpoint)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('client_id', config.client_id)
authUrl.searchParams.set('redirect_uri', redirectUri)
const scope = scopesParam ?? (config.scopes ? config.scopes.join(' ') : '')
if (scope) authUrl.searchParams.set('scope', scope)
authUrl.searchParams.set('state', state)
authUrl.searchParams.set('code_challenge', challenge)
authUrl.searchParams.set('code_challenge_method', method)
return new Response(this.web.renderRedirectPage(providerId, authUrl.toString()), {
headers: { 'content-type': 'text/html; charset=utf-8' },
status: 200,
})
} catch (err) {
Logger.warn('OAuth authorize (worker) failed', err)
return new Response(this.web.renderErrorPage('Failed to start authorization'), {
headers: { 'content-type': 'text/html; charset=utf-8' },
status: 500,
})
}
}
if (req.method === 'GET' && path.endsWith(`${this.basePath}/callback`)) {
try {
const params = new URLSearchParams(url.search)
const providerParam = url.searchParams.get('provider')
const serverId = url.searchParams.get('server_id')
const { config } = this.validator.resolveProvider({ provider: providerParam, serverId })
const cb = new CallbackHandler({
config: this.deps.getConfig(),
pkceManager: this.pkce,
stateManager: this.state,
baseUrl: this.getBaseUrlFromRequest(req),
storeDelegatedToken: this.deps.storeDelegatedToken,
})
const result = await cb.handleCallback(params, config)
if (result.error) {
return new Response(this.web.renderErrorPage(result.error), {
headers: { 'content-type': 'text/html; charset=utf-8' },
status: 400,
})
}
const returnTo = result.state?.returnTo
if (returnTo) return Response.redirect(new URL(returnTo, this.getBaseUrlFromRequest(req)).toString(), 302)
return new Response(this.web.renderSuccessPage('Authorization complete. You may close this window.'), {
headers: { 'content-type': 'text/html; charset=utf-8' },
status: 200,
})
} catch (err) {
Logger.warn('OAuth callback (worker) failed', err)
return new Response(this.web.renderErrorPage('Callback handling failed'), {
headers: { 'content-type': 'text/html; charset=utf-8' },
status: 500,
})
}
}
if (req.method === 'POST' && path.endsWith(`${this.basePath}/token`)) {
try {
const ct = req.headers.get('content-type') || ''
let data: Record<string, string> = {}
if (ct.includes('application/json')) {
data = (await req.json()) as any
} else if (ct.includes('application/x-www-form-urlencoded')) {
data = Object.fromEntries(new URLSearchParams(await req.text())) as any
} else {
return new Response(JSON.stringify({ error: 'Unsupported content type' }), {
headers: { 'content-type': 'application/json' },
status: 415,
})
}
const state = typeof data.state === 'string' ? data.state : undefined
const code = typeof data.code === 'string' ? data.code : undefined
const providerParam = typeof data.provider === 'string' ? data.provider : undefined
const serverId = typeof data.server_id === 'string' ? data.server_id : undefined
if (!state || !code) return new Response(JSON.stringify({ error: 'Missing state or code' }), { headers: { 'content-type': 'application/json' }, status: 400 })
const { config } = this.validator.resolveProvider({ provider: providerParam, serverId })
const cb = new CallbackHandler({
config: this.deps.getConfig(),
pkceManager: this.pkce,
stateManager: this.state,
baseUrl: this.getBaseUrlFromRequest(req),
storeDelegatedToken: this.deps.storeDelegatedToken,
})
const result = await cb.handleCallback(new URLSearchParams({ state, code }), config)
if (result.error) return new Response(JSON.stringify({ error: result.error }), { headers: { 'content-type': 'application/json' }, status: 400 })
return new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } })
} catch (err) {
Logger.warn('OAuth token exchange (worker) failed', err)
return new Response(JSON.stringify({ error: 'Token exchange failed' }), { headers: { 'content-type': 'application/json' }, status: 500 })
}
}
if (req.method === 'GET' && path.endsWith(`${this.basePath}/success`)) {
return new Response(this.web.renderSuccessPage(), { headers: { 'content-type': 'text/html; charset=utf-8' } })
}
if (req.method === 'GET' && path.endsWith(`${this.basePath}/error`)) {
const msg = new URL(req.url).searchParams.get('msg') ?? undefined
return new Response(this.web.renderErrorPage(msg || undefined), { headers: { 'content-type': 'text/html; charset=utf-8' } })
}
return new Response('Not Found', { status: 404 })
}
}
```
--------------------------------------------------------------------------------
/docs/architecture/images/mcp_master_architecture.svg:
--------------------------------------------------------------------------------
```
<svg viewBox="0 0 1400 1000" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
</marker>
<pattern id="diagonalHatch" patternUnits="userSpaceOnUse" width="4" height="4">
<path d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2" style="stroke:#ccc, stroke-width:1"/>
</pattern>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="3" dy="3" stdDeviation="2" flood-color="#00000020"/>
</filter>
</defs>
<!-- Background -->
<rect width="1400" height="1000" fill="#f8fafc"/>
<!-- Title -->
<text x="700" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="bold" fill="#1e293b">
Master MCP Server Architecture
</text>
<text x="700" y="55" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#64748b">
Unified MCP Gateway with Shared OAuth & Multi-Repository Module Loading
</text>
<!-- Client Layer -->
<g id="client-layer">
<rect x="50" y="100" width="1300" height="80" fill="#e0f2fe" stroke="#0369a1" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="70" y="125" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#0369a1">MCP Clients</text>
<rect x="100" y="135" width="120" height="35" fill="#0284c7" rx="4"/>
<text x="160" y="157" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">Claude Desktop</text>
<rect x="240" y="135" width="120" height="35" fill="#0284c7" rx="4"/>
<text x="300" y="157" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">Cursor IDE</text>
<rect x="380" y="135" width="120" height="35" fill="#0284c7" rx="4"/>
<text x="440" y="157" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">Custom Apps</text>
<text x="600" y="152" font-family="Arial, sans-serif" font-size="12" fill="#64748b">Single MCP Connection</text>
</g>
<!-- Master MCP Server -->
<g id="master-server">
<rect x="50" y="220" width="1300" height="260" fill="#f1f5f9" stroke="#475569" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="70" y="245" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#1e293b">Master MCP Server Gateway</text>
<!-- OAuth Manager -->
<rect x="80" y="260" width="180" height="80" fill="#fef3c7" stroke="#f59e0b" stroke-width="2" rx="6"/>
<text x="170" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#92400e">OAuth Manager</text>
<text x="90" y="305" font-family="Arial, sans-serif" font-size="11" fill="#92400e">• Token validation</text>
<text x="90" y="320" font-family="Arial, sans-serif" font-size="11" fill="#92400e">• Shared auth state</text>
<text x="90" y="335" font-family="Arial, sans-serif" font-size="11" fill="#92400e">• Token forwarding</text>
<!-- Capability Aggregator -->
<rect x="280" y="260" width="180" height="80" fill="#ddd6fe" stroke="#8b5cf6" stroke-width="2" rx="6"/>
<text x="370" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#5b21b6">Capability Aggregator</text>
<text x="290" y="305" font-family="Arial, sans-serif" font-size="11" fill="#5b21b6">• Tool discovery</text>
<text x="290" y="320" font-family="Arial, sans-serif" font-size="11" fill="#5b21b6">• Schema merging</text>
<text x="290" y="335" font-family="Arial, sans-serif" font-size="11" fill="#5b21b6">• Resource listing</text>
<!-- Request Router -->
<rect x="480" y="260" width="180" height="80" fill="#dcfce7" stroke="#16a34a" stroke-width="2" rx="6"/>
<text x="570" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#166534">Request Router</text>
<text x="490" y="305" font-family="Arial, sans-serif" font-size="11" fill="#166534">• Tool call routing</text>
<text x="490" y="320" font-family="Arial, sans-serif" font-size="11" fill="#166534">• Load balancing</text>
<text x="490" y="335" font-family="Arial, sans-serif" font-size="11" fill="#166534">• Error handling</text>
<!-- Module Loader -->
<rect x="680" y="260" width="180" height="80" fill="#fee2e2" stroke="#dc2626" stroke-width="2" rx="6"/>
<text x="770" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#991b1b">Module Loader</text>
<text x="690" y="305" font-family="Arial, sans-serif" font-size="11" fill="#991b1b">• Git repo cloning</text>
<text x="690" y="320" font-family="Arial, sans-serif" font-size="11" fill="#991b1b">• Package installation</text>
<text x="690" y="335" font-family="Arial, sans-serif" font-size="11" fill="#991b1b">• Hot reloading</text>
<!-- Server Manager -->
<rect x="880" y="260" width="180" height="80" fill="#f3e8ff" stroke="#a855f7" stroke-width="2" rx="6"/>
<text x="970" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#7c2d12">Server Manager</text>
<text x="890" y="305" font-family="Arial, sans-serif" font-size="11" fill="#7c2d12">• Process lifecycle</text>
<text x="890" y="320" font-family="Arial, sans-serif" font-size="11" fill="#7c2d12">• Health monitoring</text>
<text x="890" y="335" font-family="Arial, sans-serif" font-size="11" fill="#7c2d12">• Auto-restart</text>
<!-- Config Manager -->
<rect x="1080" y="260" width="180" height="80" fill="#ecfdf5" stroke="#10b981" stroke-width="2" rx="6"/>
<text x="1170" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#065f46">Config Manager</text>
<text x="1090" y="305" font-family="Arial, sans-serif" font-size="11" fill="#065f46">• Server configs</text>
<text x="1090" y="320" font-family="Arial, sans-serif" font-size="11" fill="#065f46">• Environment vars</text>
<text x="1090" y="335" font-family="Arial, sans-serif" font-size="11" fill="#065f46">• Secret management</text>
<!-- MCP Protocol Handler -->
<rect x="280" y="360" width="700" height="60" fill="#e2e8f0" stroke="#64748b" stroke-width="2" rx="6"/>
<text x="630" y="385" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#334155">MCP Protocol Handler (Streamable HTTP)</text>
<text x="290" y="405" font-family="Arial, sans-serif" font-size="11" fill="#475569">Handles: list_tools, call_tool, list_resources, read_resource, subscribe, notifications</text>
</g>
<!-- Repository Sources -->
<g id="repositories">
<rect x="50" y="520" width="400" height="120" fill="#fffbeb" stroke="#f59e0b" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="70" y="545" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#92400e">Repository Sources</text>
<rect x="80" y="560" width="100" height="30" fill="#f59e0b" rx="4"/>
<text x="130" y="580" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">GitHub Repos</text>
<rect x="200" y="560" width="100" height="30" fill="#f59e0b" rx="4"/>
<text x="250" y="580" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">GitLab Repos</text>
<rect x="320" y="560" width="100" height="30" fill="#f59e0b" rx="4"/>
<text x="370" y="580" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Private Git</text>
<rect x="80" y="600" width="100" height="30" fill="#ea580c" rx="4"/>
<text x="130" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">NPM Registry</text>
<rect x="200" y="600" width="100" height="30" fill="#ea580c" rx="4"/>
<text x="250" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">PyPI Registry</text>
<rect x="320" y="600" width="100" height="30" fill="#ea580c" rx="4"/>
<text x="370" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Docker Hub</text>
</g>
<!-- Backend MCP Servers -->
<g id="backend-servers">
<rect x="500" y="520" width="850" height="180" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="520" y="545" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#166534">Backend MCP Servers (Auto-loaded)</text>
<!-- Python Servers -->
<g id="python-servers">
<text x="530" y="570" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#1e40af">Python Servers</text>
<rect x="530" y="580" width="140" height="50" fill="#dbeafe" stroke="#3b82f6" stroke-width="1" rx="4"/>
<text x="600" y="600" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#1e40af">File System MCP</text>
<text x="600" y="615" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#1e40af">Tools: read, write, list</text>
<rect x="690" y="580" width="140" height="50" fill="#dbeafe" stroke="#3b82f6" stroke-width="1" rx="4"/>
<text x="760" y="600" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#1e40af">Database MCP</text>
<text x="760" y="615" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#1e40af">Tools: query, insert, update</text>
<rect x="850" y="580" width="140" height="50" fill="#dbeafe" stroke="#3b82f6" stroke-width="1" rx="4"/>
<text x="920" y="600" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#1e40af">Email MCP</text>
<text x="920" y="615" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#1e40af">Tools: send, search, read</text>
</g>
<!-- TypeScript Servers -->
<g id="typescript-servers">
<text x="530" y="660" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#059669">TypeScript Servers</text>
<rect x="530" y="670" width="140" height="50" fill="#d1fae5" stroke="#10b981" stroke-width="1" rx="4"/>
<text x="600" y="690" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#059669">GitHub MCP</text>
<text x="600" y="705" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#059669">Tools: create_issue, pr</text>
<rect x="690" y="670" width="140" height="50" fill="#d1fae5" stroke="#10b981" stroke-width="1" rx="4"/>
<text x="760" y="690" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#059669">Slack MCP</text>
<text x="760" y="705" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#059669">Tools: post, search, dm</text>
<rect x="850" y="670" width="140" height="50" fill="#d1fae5" stroke="#10b981" stroke-width="1" rx="4"/>
<text x="920" y="690" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#059669">Calendar MCP</text>
<text x="920" y="705" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#059669">Tools: schedule, list</text>
<!-- More servers indicator -->
<rect x="1010" y="625" width="80" height="50" fill="#f3f4f6" stroke="#9ca3af" stroke-width="1" rx="4" stroke-dasharray="5,5"/>
<text x="1050" y="645" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#6b7280">More</text>
<text x="1050" y="660" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#6b7280">Servers...</text>
</g>
<!-- Server Runtime Info -->
<rect x="1120" y="580" width="200" height="110" fill="#fef7ff" stroke="#c084fc" stroke-width="1" rx="4"/>
<text x="1220" y="600" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#7c3aed">Runtime Environment</text>
<text x="1130" y="620" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Python 3.9+ processes</text>
<text x="1130" y="635" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Node.js 18+ processes</text>
<text x="1130" y="650" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Docker containers</text>
<text x="1130" y="665" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Health checks</text>
<text x="1130" y="680" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Auto-scaling</text>
</g>
<!-- Configuration -->
<g id="configuration">
<rect x="50" y="740" width="500" height="120" fill="#fef2f2" stroke="#ef4444" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="70" y="765" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#991b1b">Configuration (master-mcp-config.yaml)</text>
<rect x="80" y="780" width="440" height="70" fill="#fee2e2" rx="4"/>
<text x="90" y="800" font-family="Monaco, monospace" font-size="10" fill="#991b1b">servers:</text>
<text x="90" y="815" font-family="Monaco, monospace" font-size="10" fill="#991b1b"> - repo: github.com/user/filesystem-mcp</text>
<text x="90" y="830" font-family="Monaco, monospace" font-size="10" fill="#991b1b"> - repo: github.com/user/slack-mcp</text>
<text x="90" y="845" font-family="Monaco, monospace" font-size="10" fill="#991b1b"> - package: "@mcp/github-server@latest"</text>
</g>
<!-- Hosting Options -->
<g id="hosting">
<rect x="600" y="740" width="750" height="120" fill="#f0f9ff" stroke="#0ea5e9" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="620" y="765" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#0c4a6e">Hosting Options</text>
<rect x="640" y="780" width="120" height="30" fill="#0ea5e9" rx="4"/>
<text x="700" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Cloudflare Workers</text>
<rect x="780" y="780" width="80" height="30" fill="#0ea5e9" rx="4"/>
<text x="820" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Koyeb</text>
<rect x="880" y="780" width="80" height="30" fill="#0ea5e9" rx="4"/>
<text x="920" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Render</text>
<rect x="980" y="780" width="120" height="30" fill="#0ea5e9" rx="4"/>
<text x="1040" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Docker Compose</text>
<rect x="1120" y="780" width="120" height="30" fill="#0ea5e9" rx="4"/>
<text x="1180" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Kubernetes</text>
<text x="640" y="835" font-family="Arial, sans-serif" font-size="11" fill="#0c4a6e">Scale-to-zero, OAuth, global edge distribution</text>
</g>
<!-- Data Flow Arrows -->
<g id="arrows" marker-end="url(#arrowhead)">
<!-- Client to Master -->
<line x1="400" y1="180" x2="400" y2="220" stroke="#374151" stroke-width="2"/>
<text x="420" y="200" font-family="Arial, sans-serif" font-size="11" fill="#374151">MCP Requests</text>
<!-- OAuth Flow -->
<line x1="170" y1="180" x2="170" y2="260" stroke="#f59e0b" stroke-width="2"/>
<!-- Module Loading -->
<line x1="250" y1="520" x2="680" y2="340" stroke="#dc2626" stroke-width="2"/>
<!-- Server Management -->
<line x1="970" y1="340" x2="970" y2="520" stroke="#a855f7" stroke-width="2"/>
<!-- Capability Discovery -->
<line x1="370" y1="340" x2="700" y2="520" stroke="#8b5cf6" stroke-width="2"/>
<!-- Request Routing -->
<line x1="570" y1="340" x2="700" y2="520" stroke="#16a34a" stroke-width="2"/>
</g>
<!-- Legend -->
<g id="legend">
<rect x="50" y="890" width="1300" height="80" fill="#ffffff" stroke="#e2e8f0" stroke-width="1" rx="6" filter="url(#shadow)"/>
<text x="70" y="915" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#1e293b">Key Features</text>
<text x="80" y="935" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Zero-config server integration:</tspan> Existing MCP servers work without modification</text>
<text x="80" y="950" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Shared OAuth:</tspan> Single authentication for all backend servers</text>
<text x="80" y="965" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Dynamic capability discovery:</tspan> Tools and resources auto-aggregated from all servers</text>
<text x="600" y="935" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Multi-repository support:</tspan> Load from Git, NPM, PyPI, Docker</text>
<text x="600" y="950" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Hot reloading:</tspan> Update servers without client reconnection</text>
<text x="600" y="965" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Language agnostic:</tspan> Python, TypeScript, and future language support</text>
</g>
</svg>
```