This is page 3 of 10. Use http://codebase.md/jakedismo/master-mcp-server?lines=true&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/modules/stdio-capability-discovery.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Logger } from '../utils/logger.js'
2 | import type { ServerCapabilities } from '../types/server.js'
3 | import { StdioManager } from './stdio-manager.js'
4 |
5 | export class StdioCapabilityDiscovery {
6 | constructor(private stdioManager: StdioManager = new StdioManager()) {}
7 |
8 | async discoverCapabilities(serverId: string, filePath: string): Promise<ServerCapabilities> {
9 | Logger.info('Discovering capabilities for STDIO server', { serverId, filePath })
10 |
11 | try {
12 | // Start the STDIO server process
13 | await this.stdioManager.startServer(serverId, filePath)
14 |
15 | // Send initialize request
16 | const initializeRequestId = Date.now()
17 | const initializeRequest = {
18 | jsonrpc: "2.0",
19 | id: initializeRequestId,
20 | method: "initialize",
21 | params: {
22 | protocolVersion: "2025-06-18",
23 | capabilities: {},
24 | clientInfo: {
25 | name: "master-mcp-server",
26 | version: "1.0.0"
27 | }
28 | }
29 | }
30 |
31 | await this.stdioManager.sendMessage(serverId, initializeRequest)
32 | const initializeResponse = await this.stdioManager.waitForResponse(serverId, initializeRequestId)
33 |
34 | if (initializeResponse.error) {
35 | throw new Error(`Failed to initialize STDIO server: ${initializeResponse.error.message}`)
36 | }
37 |
38 | // Send tools/list request
39 | const toolsRequestId = Date.now() + 1
40 | const toolsRequest = {
41 | jsonrpc: "2.0",
42 | id: toolsRequestId,
43 | method: "tools/list",
44 | params: {}
45 | }
46 |
47 | await this.stdioManager.sendMessage(serverId, toolsRequest)
48 | const toolsResponse = await this.stdioManager.waitForResponse(serverId, toolsRequestId)
49 |
50 | if (toolsResponse.error) {
51 | throw new Error(`Failed to list tools from STDIO server: ${toolsResponse.error.message}`)
52 | }
53 |
54 | const tools = toolsResponse.result?.tools || []
55 |
56 | // Send resources/list request
57 | const resourcesRequestId = Date.now() + 2
58 | const resourcesRequest = {
59 | jsonrpc: "2.0",
60 | id: resourcesRequestId,
61 | method: "resources/list",
62 | params: {}
63 | }
64 |
65 | await this.stdioManager.sendMessage(serverId, resourcesRequest)
66 | const resourcesResponse = await this.stdioManager.waitForResponse(serverId, resourcesRequestId)
67 |
68 | if (resourcesResponse.error) {
69 | throw new Error(`Failed to list resources from STDIO server: ${resourcesResponse.error.message}`)
70 | }
71 |
72 | const resources = resourcesResponse.result?.resources || []
73 |
74 | Logger.info('Discovered STDIO server capabilities', { serverId, tools: tools.length, resources: resources.length })
75 |
76 | return {
77 | tools,
78 | resources
79 | }
80 | } catch (error) {
81 | Logger.error('Failed to discover STDIO server capabilities', { serverId, error })
82 | throw error
83 | }
84 | }
85 |
86 | async callTool(serverId: string, toolName: string, args: any): Promise<any> {
87 | try {
88 | const toolRequestId = Date.now()
89 | const toolRequest = {
90 | jsonrpc: "2.0",
91 | id: toolRequestId,
92 | method: "tools/call",
93 | params: {
94 | name: toolName,
95 | arguments: args
96 | }
97 | }
98 |
99 | await this.stdioManager.sendMessage(serverId, toolRequest)
100 | const toolResponse = await this.stdioManager.waitForResponse(serverId, toolRequestId)
101 |
102 | if (toolResponse.error) {
103 | throw new Error(`Failed to call tool ${toolName} on STDIO server: ${toolResponse.error.message}`)
104 | }
105 |
106 | return toolResponse
107 | } catch (error) {
108 | Logger.error('STDIO tool call failed', { serverId, toolName, error })
109 | throw error
110 | }
111 | }
112 |
113 | async readResource(serverId: string, uri: string): Promise<any> {
114 | try {
115 | const resourceRequestId = Date.now()
116 | const resourceRequest = {
117 | jsonrpc: "2.0",
118 | id: resourceRequestId,
119 | method: "resources/read",
120 | params: {
121 | uri
122 | }
123 | }
124 |
125 | await this.stdioManager.sendMessage(serverId, resourceRequest)
126 | const resourceResponse = await this.stdioManager.waitForResponse(serverId, resourceRequestId)
127 |
128 | if (resourceResponse.error) {
129 | throw new Error(`Failed to read resource ${uri} from STDIO server: ${resourceResponse.error.message}`)
130 | }
131 |
132 | return resourceResponse
133 | } catch (error) {
134 | Logger.error('STDIO resource read failed', { serverId, uri, error })
135 | throw error
136 | }
137 | }
138 | }
```
--------------------------------------------------------------------------------
/docs/.vitepress/theme/components/ConfigGenerator.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="mcp-grid" style="margin: 8px 0 16px; align-items: start;">
3 | <div class="mcp-col-6">
4 | <h3 style="margin:6px 0">Master Settings</h3>
5 | <div class="mcp-callout">
6 | <label>Port
7 | <input v-model.number="state.port" type="number" min="1" max="65535" style="width:120px;margin-left:8px" />
8 | </label>
9 | <br />
10 | <label style="margin-top:8px;display:block">Base URL
11 | <input v-model="state.baseUrl" type="text" placeholder="https://your.domain" style="width:100%;margin-top:4px" />
12 | </label>
13 | <label style="margin-top:8px;display:block">Client Token (optional)
14 | <input v-model="state.clientToken" type="text" placeholder="Bearer token for testing" style="width:100%;margin-top:4px" />
15 | </label>
16 | </div>
17 |
18 | <h3 style="margin:12px 0 6px">Backends</h3>
19 | <div v-for="(s, i) in state.servers" :key="i" class="mcp-callout">
20 | <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
21 | <label>Id <input v-model="s.id" placeholder="search" /></label>
22 | <label>Type
23 | <select v-model="s.type">
24 | <option>local</option>
25 | <option>git</option>
26 | <option>npm</option>
27 | <option>docker</option>
28 | <option>pypi</option>
29 | </select>
30 | </label>
31 | <label v-if="s.type==='local'">Port <input v-model.number="s.config.port" type="number" min="1" max="65535" style="width:100px" /></label>
32 | <label v-else>URL <input v-model="s.config.url" placeholder="http://host:port" /></label>
33 | <label>Auth
34 | <select v-model="s.auth_strategy">
35 | <option>master_oauth</option>
36 | <option>delegate_oauth</option>
37 | <option>proxy_oauth</option>
38 | <option>bypass_auth</option>
39 | </select>
40 | </label>
41 | <button class="mcp-cta" style="margin-left:auto" @click="remove(i)">Remove</button>
42 | </div>
43 | </div>
44 | <button class="mcp-cta" @click="add">Add Backend</button>
45 | </div>
46 |
47 | <div class="mcp-col-6">
48 | <h3 style="margin:6px 0">Generated config.yaml</h3>
49 | <div style="position:relative">
50 | <button class="mcp-cta" style="position:absolute;right:8px;top:8px" @click="copyText(yaml)">Copy</button>
51 | <pre><code class="language-yaml">{{ yaml }}</code></pre>
52 | </div>
53 |
54 | <h3 style="margin:12px 0 6px">config.json</h3>
55 | <div style="position:relative">
56 | <button class="mcp-cta" style="position:absolute;right:8px;top:8px" @click="copyText(json)">Copy</button>
57 | <pre><code class="language-json">{{ json }}</code></pre>
58 | </div>
59 | </div>
60 | </div>
61 | </template>
62 |
63 | <script setup lang="ts">
64 | import { computed, reactive } from 'vue'
65 |
66 | type Server = {
67 | id: string
68 | type: 'local' | 'git' | 'npm' | 'docker' | 'pypi'
69 | auth_strategy: 'master_oauth' | 'delegate_oauth' | 'proxy_oauth' | 'bypass_auth'
70 | config: { port?: number; url?: string }
71 | }
72 |
73 | const state = reactive({
74 | port: 3000,
75 | baseUrl: '',
76 | clientToken: '',
77 | servers: [
78 | { id: 'search', type: 'local', auth_strategy: 'master_oauth', config: { port: 4100 } } as Server,
79 | ],
80 | })
81 |
82 | function add() {
83 | state.servers.push({ id: '', type: 'local', auth_strategy: 'master_oauth', config: {} })
84 | }
85 | function remove(i: number) {
86 | state.servers.splice(i, 1)
87 | }
88 |
89 | const jsonObj = computed(() => ({
90 | hosting: {
91 | port: state.port,
92 | base_url: state.baseUrl || undefined,
93 | },
94 | servers: state.servers.map(s => ({
95 | id: s.id, type: s.type, auth_strategy: s.auth_strategy, config: s.config,
96 | })),
97 | }))
98 |
99 | const json = computed(() => JSON.stringify(jsonObj.value, null, 2))
100 |
101 | function toYaml(obj: any, indent = 0): string {
102 | const pad = ' '.repeat(indent)
103 | if (obj === null || obj === undefined) return ''
104 | if (typeof obj !== 'object') return String(obj)
105 | if (Array.isArray(obj)) {
106 | return obj.map(v => `${pad}- ${typeof v === 'object' ? `\n${toYaml(v, indent + 1)}` : toYaml(v, indent)}`).join('\n')
107 | }
108 | return Object.entries(obj)
109 | .filter(([, v]) => v !== undefined)
110 | .map(([k, v]) => {
111 | if (v && typeof v === 'object') {
112 | const nested = toYaml(v, indent + 1)
113 | return `${pad}${k}:\n${nested}`
114 | }
115 | return `${pad}${k}: ${toYaml(v, 0)}`
116 | })
117 | .join('\n')
118 | }
119 |
120 | const yaml = computed(() => toYaml({
121 | hosting: { port: state.port, base_url: state.baseUrl || undefined },
122 | servers: jsonObj.value.servers,
123 | }))
124 |
125 | async function copyText(text: string) {
126 | try {
127 | await navigator.clipboard.writeText(text)
128 | } catch (e) {
129 | console.warn('Copy failed', e)
130 | }
131 | }
132 | </script>
133 |
134 |
```
--------------------------------------------------------------------------------
/src/config/secret-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { CryptoUtils } from '../utils/crypto.js'
2 | import { Logger } from '../utils/logger.js'
3 |
4 | export interface SecretManagerOptions {
5 | // Name of env var that holds the encryption key used for configuration secrets
6 | keyEnvVar?: string
7 | // Optional explicit key value (discouraged in production)
8 | key?: string
9 | }
10 |
11 | export class SecretManager {
12 | private key: string
13 |
14 | constructor(options?: SecretManagerOptions) {
15 | const env = (globalThis as any)?.process?.env ?? {}
16 | const provided = options?.key || env[options?.keyEnvVar || 'MASTER_CONFIG_KEY'] || env.MASTER_SECRET_KEY
17 | const isProd = (env.NODE_ENV || env.MASTER_ENV) === 'production'
18 | if (!provided) {
19 | if (isProd) throw new Error('Missing MASTER_CONFIG_KEY for decrypting secrets in production')
20 | Logger.warn('MASTER_CONFIG_KEY missing; using ephemeral key (dev only)')
21 | this.key = CryptoUtils.generateSecureRandom(32)
22 | } else {
23 | this.key = String(provided)
24 | }
25 | }
26 |
27 | getKey(): string {
28 | return this.key
29 | }
30 |
31 | encrypt(value: string): string {
32 | return `enc:gcm:${CryptoUtils.encrypt(value, this.key)}`
33 | }
34 |
35 | decrypt(value: string): string {
36 | if (value.startsWith('enc:gcm:')) {
37 | const raw = value.slice('enc:gcm:'.length)
38 | return CryptoUtils.decrypt(raw, this.key)
39 | }
40 | return value
41 | }
42 |
43 | isEncrypted(value: string): boolean {
44 | return typeof value === 'string' && value.indexOf('enc:gcm:') === 0
45 | }
46 |
47 | // Resolve secret placeholders within a config object
48 | // - enc:gcm:<base64> → decrypted
49 | // - env:VARNAME → process.env[VARNAME]
50 | resolveSecrets<T>(obj: T): T {
51 | const env = (globalThis as any)?.process?.env ?? {}
52 | const visit = (v: any): any => {
53 | if (typeof v === 'string') {
54 | const vs: string = String(v)
55 | if (this.isEncrypted(vs)) return this.decrypt(vs)
56 | if (vs.slice(0, 4) === 'env:') return String(env[vs.slice(4)] ?? '')
57 | return v
58 | }
59 | if (Array.isArray(v)) return v.map((x) => visit(x))
60 | if (v && typeof v === 'object') {
61 | const out: Record<string, unknown> = {}
62 | for (const [k, vv] of Object.entries(v)) out[k] = visit(vv)
63 | return out
64 | }
65 | return v
66 | }
67 | return visit(obj)
68 | }
69 |
70 | redact<T>(obj: T): T {
71 | const secretKeyMatcher = /secret|token|password|key/i
72 | const visit = (v: any, keyHint?: string): any => {
73 | if (typeof v === 'string') {
74 | const vs: string = String(v)
75 | if (this.isEncrypted(vs)) return '***'
76 | if (keyHint && secretKeyMatcher.test(keyHint)) return '***'
77 | if (vs.slice(0, 4) === 'env:' && secretKeyMatcher.test(keyHint || '')) return '***'
78 | return v
79 | }
80 | if (Array.isArray(v)) return v.map((x) => visit(x, keyHint))
81 | if (v && typeof v === 'object') {
82 | const out: Record<string, unknown> = {}
83 | for (const [k, vv] of Object.entries(v)) out[k] = visit(vv, k)
84 | return out
85 | }
86 | return v
87 | }
88 | return visit(obj)
89 | }
90 |
91 | rotate<T extends Record<string, unknown>>(obj: T, newKey: string, secretPaths?: string[]): T {
92 | // Re-encrypt values under known secret paths
93 | const prevKey = this.key
94 | this.key = newKey
95 | const result = structuredClone(obj)
96 | const paths = secretPaths ?? inferSecretPaths(obj)
97 | for (const p of paths) {
98 | try {
99 | const cur = getByPath(result, p)
100 | if (typeof cur === 'string') {
101 | const plain = this.isEncrypted(cur) ? CryptoUtils.decrypt(cur.slice('enc:gcm:'.length), prevKey) : cur
102 | setByPath(result, p, this.encrypt(plain))
103 | }
104 | } catch (err) {
105 | Logger.warn(`Failed to rotate secret at ${p}`, String(err))
106 | }
107 | }
108 | return result
109 | }
110 | }
111 |
112 | function getByPath(obj: any, path: string): unknown {
113 | const parts = path.split('.')
114 | let cur = obj
115 | for (const p of parts) {
116 | if (!cur || typeof cur !== 'object') return undefined
117 | cur = cur[p]
118 | }
119 | return cur
120 | }
121 |
122 | function setByPath(obj: any, path: string, value: unknown): void {
123 | const parts = path.split('.')
124 | let cur = obj
125 | for (let i = 0; i < parts.length - 1; i++) {
126 | const p = parts[i]
127 | if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {}
128 | cur = cur[p]
129 | }
130 | cur[parts[parts.length - 1]] = value
131 | }
132 |
133 | function inferSecretPaths(obj: Record<string, unknown>, base = ''): string[] {
134 | const out: string[] = []
135 | for (const [k, v] of Object.entries(obj)) {
136 | const p = base ? `${base}.${k}` : k
137 | if (typeof v === 'string') {
138 | if (/secret|token|password|key/i.test(k)) out.push(p)
139 | else if (v.startsWith('enc:gcm:') || v.startsWith('env:')) out.push(p)
140 | } else if (v && typeof v === 'object') {
141 | out.push(...inferSecretPaths(v as Record<string, unknown>, p))
142 | }
143 | }
144 | return out
145 | }
146 |
```
--------------------------------------------------------------------------------
/src/mcp-server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
3 | import type { Request, Response } from 'express'
4 | import { Logger } from './utils/logger.js'
5 | import { DependencyContainer } from './server/dependency-container.js'
6 | import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
7 |
8 | // Create an MCP server with the official SDK
9 | export async function createMcpServer(container: DependencyContainer): Promise<{
10 | mcpServer: McpServer,
11 | transport: StreamableHTTPServerTransport,
12 | handleRequest: (req: Request, res: Response) => Promise<void>
13 | }> {
14 | // Create the MCP server with server info
15 | const mcpServer = new McpServer({
16 | name: 'master-mcp-server',
17 | version: '0.1.0'
18 | }, {
19 | capabilities: {
20 | tools: { listChanged: true },
21 | resources: { listChanged: true },
22 | prompts: { listChanged: true }
23 | }
24 | })
25 |
26 | // Register tools from the aggregated servers BEFORE connecting to transport
27 | const aggregatedTools = container.master.getAggregatedTools()
28 | Logger.info('Aggregated tools', { count: aggregatedTools.length, tools: aggregatedTools.map(t => t.name) })
29 | for (const tool of aggregatedTools) {
30 | // Skip tools with names that might cause conflicts
31 | if (tool.name.includes('..')) continue;
32 |
33 | Logger.info('Registering tool', { name: tool.name, description: tool.description })
34 | // Register the tool with the MCP server
35 | mcpServer.tool(tool.name, tool.description ?? '', async (args) => {
36 | try {
37 | // Route the tool call to the appropriate backend server
38 | const result = await container.master.handler.handleCallTool({
39 | name: tool.name,
40 | arguments: args
41 | })
42 | return result as CallToolResult
43 | } catch (error) {
44 | Logger.error('Tool execution failed', { tool: tool.name, error })
45 | return {
46 | content: [{
47 | type: 'text',
48 | text: `Error executing tool ${tool.name}: ${error instanceof Error ? error.message : String(error)}`
49 | }],
50 | isError: true
51 | }
52 | }
53 | })
54 | }
55 |
56 | // Register resources from the aggregated servers BEFORE connecting to transport
57 | const aggregatedResources = container.master.getAggregatedResources()
58 | Logger.info('Aggregated resources', { count: aggregatedResources.length, resources: aggregatedResources.map(r => r.uri) })
59 | for (const resource of aggregatedResources) {
60 | // Skip resources with URIs that might cause conflicts
61 | if (resource.uri.includes('..')) continue;
62 |
63 | Logger.info('Registering resource', { name: resource.name, uri: resource.uri, description: resource.description })
64 | mcpServer.resource(
65 | resource.name ?? resource.uri,
66 | resource.uri,
67 | {
68 | description: resource.description,
69 | mimeType: resource.mimeType
70 | },
71 | async () => {
72 | try {
73 | // Route the resource read to the appropriate backend server
74 | const result = await container.master.handler.handleReadResource({
75 | uri: resource.uri
76 | })
77 |
78 | // Convert the result to the format expected by the MCP server
79 | if (typeof result.contents === 'string') {
80 | return {
81 | contents: [{
82 | uri: resource.uri,
83 | text: result.contents,
84 | mimeType: result.mimeType
85 | }]
86 | }
87 | } else {
88 | return {
89 | contents: [{
90 | uri: resource.uri,
91 | blob: Buffer.from(result.contents).toString('base64'),
92 | mimeType: result.mimeType
93 | }]
94 | }
95 | }
96 | } catch (error) {
97 | Logger.error('Resource read failed', { resource: resource.uri, error })
98 | throw new Error(`Error reading resource ${resource.uri}: ${error instanceof Error ? error.message : String(error)}`)
99 | }
100 | }
101 | )
102 | }
103 |
104 | // Create the HTTP streaming transport in stateless mode
105 | const transport = new StreamableHTTPServerTransport({
106 | sessionIdGenerator: undefined, // Stateless mode
107 | enableJsonResponse: false, // Use SSE by default
108 | enableDnsRebindingProtection: false
109 | })
110 |
111 | // Connect the server to the transport AFTER registering tools and resources
112 | await mcpServer.connect(transport)
113 |
114 | // Create a handler function for Express
115 | const handleRequest = async (req: Request, res: Response) => {
116 | try {
117 | await transport.handleRequest(req, res, req.body)
118 | } catch (error) {
119 | Logger.error('MCP request handling failed', { error })
120 | res.status(500).json({
121 | error: 'Internal server error'
122 | })
123 | }
124 | }
125 |
126 | return { mcpServer, transport, handleRequest }
127 | }
```
--------------------------------------------------------------------------------
/src/auth/token-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { OAuthToken } from '../types/auth.js'
2 | import { CryptoUtils } from '../utils/crypto.js'
3 | import { Logger } from '../utils/logger.js'
4 |
5 | export interface TokenStorage {
6 | set(key: string, value: string): Promise<void> | void
7 | get(key: string): Promise<string | undefined> | string | undefined
8 | delete(key: string): Promise<void> | void
9 | entries(): AsyncIterable<[string, string]> | Iterable<[string, string]>
10 | }
11 |
12 | class InMemoryTokenStorage implements TokenStorage {
13 | private map = new Map<string, string>()
14 |
15 | set(key: string, value: string): void {
16 | this.map.set(key, value)
17 | }
18 | get(key: string): string | undefined {
19 | return this.map.get(key)
20 | }
21 | delete(key: string): void {
22 | this.map.delete(key)
23 | }
24 | *entries(): Iterable<[string, string]> {
25 | yield* this.map.entries()
26 | }
27 | }
28 |
29 | export class TokenManager {
30 | private readonly storage: TokenStorage
31 | private readonly encKey: string
32 |
33 | constructor(options?: { storage?: TokenStorage; secret?: string }) {
34 | this.storage = options?.storage ?? autoDetectStorage()
35 | const g: any = globalThis as any
36 | const env = (g?.process?.env ?? g?.__WORKER_ENV ?? {}) as Record<string, string>
37 | const provided = options?.secret ?? (env as any).TOKEN_ENC_KEY
38 |
39 | if (!provided) {
40 | const envName = ((g?.process?.env ?? (g?.__WORKER_ENV ?? {})) as any).NODE_ENV ?? 'development'
41 | if (envName === 'production') {
42 | throw new Error('TOKEN_ENC_KEY is required in production for secure token storage')
43 | }
44 | Logger.warn('TOKEN_ENC_KEY missing; generating ephemeral dev key (tokens won\'t persist across restarts)')
45 | this.encKey = CryptoUtils.generateSecureRandom(32)
46 | } else {
47 | this.encKey = provided
48 | }
49 | }
50 |
51 | async storeToken(key: string, token: OAuthToken): Promise<void> {
52 | const serialized = JSON.stringify(token)
53 | const encrypted = CryptoUtils.encrypt(serialized, this.encKey)
54 | await this.storage.set(key, encrypted)
55 | }
56 |
57 | async getToken(key: string): Promise<OAuthToken | null> {
58 | const encrypted = await this.storage.get(key)
59 | if (!encrypted) return null
60 | try {
61 | const decrypted = CryptoUtils.decrypt(encrypted, this.encKey)
62 | return JSON.parse(decrypted) as OAuthToken
63 | } catch (err) {
64 | Logger.error('Failed to decrypt token; deleting corrupted entry', { key, err: String(err) })
65 | await this.storage.delete(key)
66 | return null
67 | }
68 | }
69 |
70 | async cleanupExpiredTokens(): Promise<void> {
71 | const now = Date.now()
72 | for await (const [k, v] of this.storage.entries() as AsyncIterable<[string, string]>) {
73 | try {
74 | const tok = JSON.parse(CryptoUtils.decrypt(v, this.encKey)) as OAuthToken
75 | if (typeof tok.expires_at === 'number' && tok.expires_at <= now) {
76 | await this.storage.delete(k)
77 | }
78 | } catch {
79 | await this.storage.delete(k)
80 | }
81 | }
82 | }
83 |
84 | generateState(data: unknown): string {
85 | const payload = JSON.stringify({ d: data, t: Date.now() })
86 | return CryptoUtils.encrypt(payload, this.encKey)
87 | }
88 |
89 | validateState(state: string, expectedData: unknown): boolean {
90 | try {
91 | const payload = JSON.parse(CryptoUtils.decrypt(state, this.encKey)) as { d: unknown }
92 | return JSON.stringify(payload.d) === JSON.stringify(expectedData)
93 | } catch {
94 | return false
95 | }
96 | }
97 | }
98 |
99 | export { InMemoryTokenStorage }
100 |
101 | /**
102 | * Auto-detects the best available storage backend.
103 | * - Cloudflare Workers: KV namespace bound as `TOKENS`
104 | * - Fallback: in-memory (non-persistent)
105 | */
106 | function autoDetectStorage(): TokenStorage {
107 | const g: any = globalThis as any
108 | const env = g.__WORKER_ENV || {}
109 | const kv = env.TOKENS || g.TOKENS || g.TOKENS_KV
110 | if (kv && typeof kv.get === 'function' && typeof kv.put === 'function' && typeof kv.delete === 'function') {
111 | return new KVTokenStorage(kv)
112 | }
113 | return new InMemoryTokenStorage()
114 | }
115 |
116 | class KVTokenStorage implements TokenStorage {
117 | constructor(private readonly kv: { get: (k: string) => Promise<string | null>; put: (k: string, v: string, opts?: any) => Promise<void>; delete: (k: string) => Promise<void>; list?: (opts?: any) => Promise<{ keys: { name: string }[] }> }) {}
118 | async set(key: string, value: string): Promise<void> {
119 | await this.kv.put(key, value)
120 | }
121 | async get(key: string): Promise<string | undefined> {
122 | const v = await this.kv.get(key)
123 | return v === null ? undefined : v
124 | }
125 | async delete(key: string): Promise<void> {
126 | await this.kv.delete(key)
127 | }
128 | async *entries(): AsyncIterable<[string, string]> {
129 | if (typeof this.kv.list === 'function') {
130 | const { keys } = await this.kv.list()
131 | for (const k of keys) {
132 | const v = await this.kv.get(k.name)
133 | if (v !== null) yield [k.name, v]
134 | }
135 | } else {
136 | // KV without list support: nothing to iterate
137 | return
138 | }
139 | }
140 | }
141 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import 'dotenv/config'
2 | import express from 'express'
3 | import type { Request, Response } from 'express'
4 | import { DependencyContainer } from './server/dependency-container.js'
5 | import { collectSystemMetrics } from './utils/monitoring.js'
6 | import { CapabilityAggregator } from './modules/capability-aggregator.js'
7 | import { createMcpServer } from './mcp-server.js'
8 |
9 | export interface RunningServer {
10 | name: string
11 | version: string
12 | container: DependencyContainer
13 | stop: () => Promise<void>
14 | }
15 |
16 | function isNode(): boolean {
17 | return Boolean((globalThis as any)?.process?.versions?.node)
18 | }
19 |
20 | export async function createServer(startHttp = true): Promise<RunningServer> {
21 | const version = (globalThis as any)?.process?.env?.APP_VERSION ?? '0.1.0'
22 | const container = new DependencyContainer()
23 | await container.initialize()
24 |
25 | const server: RunningServer = {
26 | name: 'master-mcp-server',
27 | version,
28 | container,
29 | stop: async () => {
30 | try {
31 | container.configManager.stop()
32 | await container.master.unloadAll()
33 | } catch {
34 | // ignore
35 | }
36 | },
37 | }
38 |
39 | if (isNode() && startHttp) {
40 | await startNodeHttp(container)
41 | }
42 |
43 | // Graceful shutdown (Node only)
44 | if (isNode()) {
45 | const onSig = async () => {
46 | await server.stop()
47 | ;(process as any).exit?.(0)
48 | }
49 | process.on('SIGINT', onSig)
50 | process.on('SIGTERM', onSig)
51 | }
52 |
53 | return server
54 | }
55 |
56 | async function startNodeHttp(container: DependencyContainer): Promise<void> {
57 | const app = express()
58 | app.use(express.json())
59 | // Serve static assets for OAuth pages
60 | // eslint-disable-next-line @typescript-eslint/no-var-requires
61 | const expressStatic = (express as any).static
62 | if (expressStatic) app.use('/static', expressStatic('static'))
63 |
64 | const getToken = (req: Request): string | undefined => {
65 | const h = req.headers['authorization'] || req.headers['Authorization']
66 | if (typeof h === 'string' && h.toLowerCase().startsWith('bearer ')) return h.slice(7)
67 | return undefined
68 | }
69 |
70 | app.get('/health', (_req, res) => res.json({ ok: true }))
71 | app.get('/metrics', (_req, res) => {
72 | try {
73 | res.json({ ok: true, system: collectSystemMetrics() })
74 | } catch {
75 | res.json({ ok: true })
76 | }
77 | })
78 | // Mount OAuth endpoints using the master server's controller
79 | try {
80 | container.master.getOAuthFlowController().registerExpress(app)
81 | } catch {
82 | // If not available yet, ignore; will be mounted on demand if needed
83 | }
84 | app.get('/capabilities', (_req, res) => {
85 | const agg = new CapabilityAggregator()
86 | const caps = agg.aggregate(Array.from(container.master.getRouter().getServers().values()))
87 | res.json(caps)
88 | })
89 |
90 | // Create the MCP server with HTTP streaming transport
91 | const { handleRequest } = await createMcpServer(container)
92 |
93 | // Register MCP endpoints
94 | app.post('/mcp', handleRequest)
95 | app.get('/mcp', handleRequest)
96 | app.delete('/mcp', handleRequest)
97 |
98 | // Keep the existing endpoints for backward compatibility
99 | app.post('/mcp/tools/list', async (_req: Request, res: Response) => {
100 | const handler = container.master.handler
101 | const result = await handler.handleListTools({ type: 'list_tools' })
102 | res.json(result)
103 | })
104 |
105 | app.post('/mcp/tools/call', async (req: Request, res: Response) => {
106 | const token = getToken(req)
107 | const handler = new (container.master.handler.constructor as any)({
108 | aggregator: container.aggregator,
109 | router: container.master.getRouter(),
110 | getClientToken: () => token,
111 | }) as typeof container.master.handler
112 | const result = await handler.handleCallTool({ name: req.body?.name, arguments: req.body?.arguments ?? {} })
113 | res.json(result)
114 | })
115 |
116 | app.post('/mcp/resources/list', async (_req: Request, res: Response) => {
117 | const handler = container.master.handler
118 | const result = await handler.handleListResources({ type: 'list_resources' })
119 | res.json(result)
120 | })
121 |
122 | app.post('/mcp/resources/read', async (req: Request, res: Response) => {
123 | const token = getToken(req)
124 | const handler = new (container.master.handler.constructor as any)({
125 | aggregator: container.aggregator,
126 | router: container.master.getRouter(),
127 | getClientToken: () => token,
128 | }) as typeof container.master.handler
129 | const result = await handler.handleReadResource({ uri: req.body?.uri })
130 | res.json(result)
131 | })
132 |
133 | const port = container.getConfig().hosting.port ?? 3000
134 | await new Promise<void>((resolve) => {
135 | app.listen(port, () => {
136 | // eslint-disable-next-line no-console
137 | console.log(`Master MCP listening on http://localhost:${port}`)
138 | resolve()
139 | })
140 | })
141 | }
142 |
143 | export default createServer
144 |
145 | // If this file is being run directly (not imported), start the server
146 | if (import.meta.url === `file://${process.argv[1]}`) {
147 | createServer().catch((err) => {
148 | console.error('Failed to start server:', err)
149 | process.exit(1)
150 | })
151 | }
152 |
```
--------------------------------------------------------------------------------
/src/utils/monitoring.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Lightweight metrics, health checks, and profiling utilities.
3 | */
4 |
5 | export type Labels = Record<string, string>
6 |
7 | export class Counter {
8 | private value = 0
9 | inc(delta = 1): void {
10 | this.value += delta
11 | }
12 | get(): number {
13 | return this.value
14 | }
15 | }
16 |
17 | export class Gauge {
18 | private value = 0
19 | set(v: number): void {
20 | this.value = v
21 | }
22 | add(delta: number): void {
23 | this.value += delta
24 | }
25 | get(): number {
26 | return this.value
27 | }
28 | }
29 |
30 | export class Histogram {
31 | private readonly buckets: number[]
32 | private counts: number[]
33 | private sum = 0
34 | constructor(buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]) {
35 | this.buckets = [...buckets].sort((a, b) => a - b)
36 | this.counts = Array(this.buckets.length + 1).fill(0)
37 | }
38 | observe(value: number): void {
39 | this.sum += value
40 | for (let i = 0; i < this.buckets.length; i++) {
41 | if (value <= this.buckets[i]) {
42 | this.counts[i]++
43 | return
44 | }
45 | }
46 | this.counts[this.counts.length - 1]++
47 | }
48 | snapshot(): { buckets: number[]; counts: number[]; sum: number } {
49 | return { buckets: [...this.buckets], counts: [...this.counts], sum: this.sum }
50 | }
51 | }
52 |
53 | export class MetricRegistry {
54 | private counters = new Map<string, Counter>()
55 | private gauges = new Map<string, Gauge>()
56 | private histograms = new Map<string, Histogram>()
57 |
58 | counter(name: string): Counter {
59 | let c = this.counters.get(name)
60 | if (!c) {
61 | c = new Counter()
62 | this.counters.set(name, c)
63 | }
64 | return c
65 | }
66 |
67 | gauge(name: string): Gauge {
68 | let g = this.gauges.get(name)
69 | if (!g) {
70 | g = new Gauge()
71 | this.gauges.set(name, g)
72 | }
73 | return g
74 | }
75 |
76 | histogram(name: string, buckets?: number[]): Histogram {
77 | let h = this.histograms.get(name)
78 | if (!h) {
79 | h = new Histogram(buckets)
80 | this.histograms.set(name, h)
81 | }
82 | return h
83 | }
84 |
85 | list(): { counters: Record<string, number>; gauges: Record<string, number>; histograms: Record<string, ReturnType<Histogram['snapshot']>> } {
86 | const counters: Record<string, number> = {}
87 | const gauges: Record<string, number> = {}
88 | const histograms: Record<string, ReturnType<Histogram['snapshot']>> = {}
89 | for (const [k, v] of this.counters.entries()) counters[k] = v.get()
90 | for (const [k, v] of this.gauges.entries()) gauges[k] = v.get()
91 | for (const [k, v] of this.histograms.entries()) histograms[k] = v.snapshot()
92 | return { counters, gauges, histograms }
93 | }
94 | }
95 |
96 | export class HealthCheckRegistry {
97 | private checks = new Map<string, () => Promise<{ ok: boolean; info?: unknown }>>()
98 | register(name: string, fn: () => Promise<{ ok: boolean; info?: unknown }>): void {
99 | this.checks.set(name, fn)
100 | }
101 | unregister(name: string): void {
102 | this.checks.delete(name)
103 | }
104 | async run(): Promise<{ status: 'ok' | 'degraded' | 'fail'; results: Record<string, { ok: boolean; info?: unknown }> }> {
105 | const entries: [string, { ok: boolean; info?: unknown }][] = []
106 | for (const [name, fn] of this.checks) {
107 | try {
108 | const res = await fn()
109 | entries.push([name, res])
110 | } catch (e) {
111 | entries.push([name, { ok: false, info: e instanceof Error ? e.message : String(e) }])
112 | }
113 | }
114 | const results = Object.fromEntries(entries)
115 | const oks = entries.filter(([, r]) => r.ok).length
116 | const status: 'ok' | 'degraded' | 'fail' = oks === entries.length ? 'ok' : oks > 0 ? 'degraded' : 'fail'
117 | return { status, results }
118 | }
119 | }
120 |
121 | /** Monitors event loop delay by scheduling microtasks. Returns a stop function. */
122 | export function monitorEventLoopLag(callback: (lagMs: number) => void, intervalMs = 500): () => void {
123 | let timer: any
124 | let stopped = false
125 | const tick = () => {
126 | if (stopped) return
127 | const start = now()
128 | timer = setTimeout(() => {
129 | const lag = now() - start - intervalMs
130 | callback(Math.max(0, lag))
131 | tick()
132 | }, intervalMs)
133 | }
134 | tick()
135 | return () => {
136 | stopped = true
137 | if (typeof clearTimeout === 'function' && timer) clearTimeout(timer)
138 | }
139 | }
140 |
141 | export function now(): number {
142 | if (typeof performance !== 'undefined' && typeof performance.now === 'function') return performance.now()
143 | return Date.now()
144 | }
145 |
146 | export function collectSystemMetrics(): Record<string, unknown> {
147 | const g: any = globalThis as any
148 | const out: Record<string, unknown> = { timestamp: new Date().toISOString() }
149 | try {
150 | if (g.process?.memoryUsage) {
151 | const m = g.process.memoryUsage()
152 | out.memory = {
153 | rss: m.rss,
154 | heapTotal: m.heapTotal,
155 | heapUsed: m.heapUsed,
156 | external: m.external,
157 | }
158 | }
159 | if (g.os?.loadavg) {
160 | const l = g.os.loadavg()
161 | out.loadavg = { '1m': l[0], '5m': l[1], '15m': l[2] }
162 | }
163 | } catch {
164 | // ignore
165 | }
166 | // Workers: best-effort
167 | if (!out.memory && (performance as any)?.memory) {
168 | out.memory = (performance as any).memory
169 | }
170 | return out
171 | }
172 |
173 |
```
--------------------------------------------------------------------------------
/src/routing/circuit-breaker.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Logger } from '../utils/logger.js'
2 |
3 | export type CircuitState = 'closed' | 'open' | 'half_open'
4 |
5 | export interface CircuitRecord {
6 | state: CircuitState
7 | failures: number
8 | successes: number
9 | nextTryAt: number // epoch ms when half-open trial is permitted
10 | openedAt?: number
11 | halfOpenInProgress?: boolean
12 | }
13 |
14 | export interface CircuitBreakerOptions {
15 | failureThreshold: number // failures before opening circuit
16 | successThreshold: number // successes in half-open before closing
17 | recoveryTimeoutMs: number // time to wait before permitting a half-open trial
18 | name?: string
19 | }
20 |
21 | export interface CircuitStorage {
22 | get(key: string): CircuitRecord | undefined
23 | set(key: string, value: CircuitRecord): void
24 | delete?(key: string): void
25 | }
26 |
27 | export class InMemoryCircuitStorage implements CircuitStorage {
28 | private readonly map = new Map<string, CircuitRecord>()
29 | get(key: string): CircuitRecord | undefined {
30 | return this.map.get(key)
31 | }
32 | set(key: string, value: CircuitRecord): void {
33 | this.map.set(key, value)
34 | }
35 | delete(key: string): void {
36 | this.map.delete(key)
37 | }
38 | }
39 |
40 | export class CircuitOpenError extends Error {
41 | readonly retryAfterMs?: number
42 | constructor(message: string, retryAfterMs?: number) {
43 | super(message)
44 | this.name = 'CircuitOpenError'
45 | this.retryAfterMs = retryAfterMs
46 | }
47 | }
48 |
49 | export class CircuitBreaker {
50 | private readonly storage: CircuitStorage
51 | private readonly opts: Required<CircuitBreakerOptions>
52 |
53 | constructor(options?: Partial<CircuitBreakerOptions>, storage?: CircuitStorage) {
54 | this.opts = {
55 | failureThreshold: options?.failureThreshold ?? 5,
56 | successThreshold: options?.successThreshold ?? 2,
57 | recoveryTimeoutMs: options?.recoveryTimeoutMs ?? 30_000,
58 | name: options?.name ?? 'default',
59 | }
60 | this.storage = storage ?? new InMemoryCircuitStorage()
61 | }
62 |
63 | private now(): number {
64 | return Date.now()
65 | }
66 |
67 | private initial(): CircuitRecord {
68 | return { state: 'closed', failures: 0, successes: 0, nextTryAt: 0 }
69 | }
70 |
71 | private getRecord(key: string): CircuitRecord {
72 | return this.storage.get(key) ?? this.initial()
73 | }
74 |
75 | canExecute(key: string): { allowed: boolean; state: CircuitState; retryAfterMs?: number } {
76 | const rec = this.getRecord(key)
77 | const now = this.now()
78 | if (rec.state === 'open') {
79 | if (now >= rec.nextTryAt && !rec.halfOpenInProgress) {
80 | rec.state = 'half_open'
81 | rec.halfOpenInProgress = true
82 | rec.successes = 0
83 | this.storage.set(key, rec)
84 | return { allowed: true, state: rec.state }
85 | }
86 | return { allowed: false, state: 'open', retryAfterMs: Math.max(0, rec.nextTryAt - now) }
87 | }
88 | if (rec.state === 'half_open') {
89 | // Only permit one in-flight trial at a time
90 | if (rec.halfOpenInProgress) return { allowed: false, state: 'half_open', retryAfterMs: this.opts.recoveryTimeoutMs }
91 | rec.halfOpenInProgress = true
92 | this.storage.set(key, rec)
93 | return { allowed: true, state: rec.state }
94 | }
95 | return { allowed: true, state: rec.state }
96 | }
97 |
98 | onSuccess(key: string): void {
99 | const rec = this.getRecord(key)
100 | if (rec.state === 'half_open') {
101 | rec.successes += 1
102 | rec.halfOpenInProgress = false
103 | if (rec.successes >= this.opts.successThreshold) {
104 | // Close the circuit after consecutive successes
105 | this.storage.set(key, this.initial())
106 | Logger.debug(`[Circuit] CLOSED after half-open successes`, { key, name: this.opts.name })
107 | return
108 | }
109 | this.storage.set(key, rec)
110 | return
111 | }
112 | // Closed state: reset failures on success
113 | rec.failures = 0
114 | this.storage.set(key, rec)
115 | }
116 |
117 | onFailure(key: string, _error?: unknown): void {
118 | const rec = this.getRecord(key)
119 | const now = this.now()
120 | if (rec.state === 'half_open') {
121 | // Failure during half-open => immediately open again
122 | rec.state = 'open'
123 | rec.failures = this.opts.failureThreshold
124 | rec.successes = 0
125 | rec.openedAt = now
126 | rec.nextTryAt = now + this.opts.recoveryTimeoutMs
127 | rec.halfOpenInProgress = false
128 | this.storage.set(key, rec)
129 | Logger.debug(`[Circuit] RE-OPEN after half-open failure`, { key, name: this.opts.name })
130 | return
131 | }
132 |
133 | rec.failures += 1
134 | if (rec.failures >= this.opts.failureThreshold) {
135 | rec.state = 'open'
136 | rec.openedAt = now
137 | rec.nextTryAt = now + this.opts.recoveryTimeoutMs
138 | this.storage.set(key, rec)
139 | Logger.debug(`[Circuit] OPEN due to failures`, { key, name: this.opts.name, failures: rec.failures })
140 | } else {
141 | this.storage.set(key, rec)
142 | }
143 | }
144 |
145 | async execute<T>(key: string, fn: () => Promise<T>): Promise<T> {
146 | const gate = this.canExecute(key)
147 | if (!gate.allowed) throw new CircuitOpenError('Circuit open', gate.retryAfterMs)
148 | try {
149 | const result = await fn()
150 | this.onSuccess(key)
151 | return result
152 | } catch (err) {
153 | this.onFailure(key, err)
154 | throw err
155 | }
156 | }
157 | }
158 |
159 |
```
--------------------------------------------------------------------------------
/tests/servers/test-streaming-both-simple.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import { spawn } from 'node:child_process'
4 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'
5 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
6 |
7 | async function startHttpServer() {
8 | console.log('Starting HTTP test server...')
9 |
10 | // Start the HTTP server as a background process
11 | const httpServer = spawn('node', ['examples/test-mcp-server.js'], {
12 | stdio: ['ignore', 'pipe', 'pipe'],
13 | env: { ...process.env, PORT: '3006' }
14 | })
15 |
16 | // Capture stdout and stderr
17 | httpServer.stdout.on('data', (data) => {
18 | console.log(`[HTTP Server] ${data.toString().trim()}`)
19 | })
20 |
21 | httpServer.stderr.on('data', (data) => {
22 | console.error(`[HTTP Server ERROR] ${data.toString().trim()}`)
23 | })
24 |
25 | // Wait for the server to start
26 | await new Promise((resolve, reject) => {
27 | let timeout = setTimeout(() => {
28 | reject(new Error('HTTP server startup timeout'))
29 | }, 5000)
30 |
31 | httpServer.stdout.on('data', (data) => {
32 | if (data.toString().includes('Test MCP server listening')) {
33 | clearTimeout(timeout)
34 | resolve()
35 | }
36 | })
37 | })
38 |
39 | return httpServer
40 | }
41 |
42 | async function runStreamingTest() {
43 | try {
44 | console.log('Testing Master MCP Server with HTTP Streaming...')
45 |
46 | // Create a streamable HTTP transport to connect to our MCP server
47 | const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
48 |
49 | // Create the MCP client
50 | const client = new Client({
51 | name: 'master-mcp-streaming-test-client',
52 | version: '1.0.0'
53 | })
54 |
55 | // Initialize the client
56 | await client.connect(transport)
57 | console.log('✅ Server initialized with streaming transport')
58 | console.log('Server info:', client.getServerVersion())
59 | console.log('Server capabilities:', client.getServerCapabilities())
60 |
61 | // List tools using streaming
62 | console.log('\n--- Testing tools/list with streaming ---')
63 | const toolsResult = await client.listTools({})
64 | console.log('✅ tools/list successful with streaming')
65 | console.log('Number of tools:', toolsResult.tools.length)
66 | console.log('Tools:', toolsResult.tools.map(t => t.name))
67 |
68 | // Verify both servers are present
69 | const hasHttpTool = toolsResult.tools.some(t => t.name === 'test-server.echo')
70 | const hasStdioTool = toolsResult.tools.some(t => t.name === 'stdio-server.stdio-echo')
71 |
72 | if (hasHttpTool) {
73 | console.log('✅ HTTP server tool found')
74 | } else {
75 | console.log('❌ HTTP server tool not found')
76 | }
77 |
78 | if (hasStdioTool) {
79 | console.log('✅ STDIO server tool found')
80 | } else {
81 | console.log('❌ STDIO server tool not found')
82 | }
83 |
84 | // List resources using streaming
85 | console.log('\n--- Testing resources/list with streaming ---')
86 | const resourcesResult = await client.listResources({})
87 | console.log('✅ resources/list successful with streaming')
88 | console.log('Number of resources:', resourcesResult.resources.length)
89 | console.log('Resources:', resourcesResult.resources.map(r => r.uri))
90 |
91 | // Verify both servers are present
92 | const hasHttpResource = resourcesResult.resources.some(r => r.uri === 'test-server.test://example')
93 | const hasStdioResource = resourcesResult.resources.some(r => r.uri === 'stdio-server.stdio://example/resource')
94 |
95 | if (hasHttpResource) {
96 | console.log('✅ HTTP server resource found')
97 | } else {
98 | console.log('❌ HTTP server resource not found')
99 | }
100 |
101 | if (hasStdioResource) {
102 | console.log('✅ STDIO server resource found')
103 | } else {
104 | console.log('❌ STDIO server resource not found')
105 | }
106 |
107 | // Test ping
108 | console.log('\n--- Testing ping with streaming ---')
109 | const pingResult = await client.ping()
110 | console.log('✅ ping successful with streaming')
111 | console.log('Ping result:', pingResult)
112 |
113 | // Summary
114 | console.log('\n--- Test Summary ---')
115 | if (hasHttpTool && hasStdioTool && hasHttpResource && hasStdioResource) {
116 | console.log('🎉 All tests passed! Both HTTP and STDIO servers are working correctly.')
117 | } else {
118 | console.log('⚠️ Some tests failed. Check the output above for details.')
119 | }
120 |
121 | // Close the connection
122 | await client.close()
123 | console.log('\n✅ Disconnected from MCP server')
124 |
125 | } catch (error) {
126 | console.error('❌ Streaming test failed:', error)
127 | console.error('Error stack:', error.stack)
128 | }
129 | }
130 |
131 | async function main() {
132 | let httpServer
133 |
134 | try {
135 | // Start the HTTP server
136 | httpServer = await startHttpServer()
137 |
138 | // Wait a bit for the master server to discover the HTTP server
139 | console.log('Waiting for server discovery...')
140 | await new Promise(resolve => setTimeout(resolve, 3000))
141 |
142 | // Run the streaming test
143 | await runStreamingTest()
144 | } catch (error) {
145 | console.error('Test failed:', error)
146 | } finally {
147 | // Clean up: kill the HTTP server
148 | if (httpServer) {
149 | console.log('Stopping HTTP server...')
150 | httpServer.kill()
151 | }
152 | }
153 | }
154 |
155 | // Run the test
156 | main()
```
--------------------------------------------------------------------------------
/docs/.vitepress/theme/components/AuthFlowDemo.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="mcp-callout">
3 | <label>Strategy
4 | <select v-model="strategy" style="margin-left:8px">
5 | <option value="master_oauth">master_oauth</option>
6 | <option value="delegate_oauth">delegate_oauth</option>
7 | <option value="proxy_oauth">proxy_oauth</option>
8 | <option value="bypass_auth">bypass_auth</option>
9 | </select>
10 | </label>
11 | </div>
12 |
13 | <div class="mcp-grid" style="align-items:start">
14 | <div class="mcp-col-6">
15 | <h4 style="margin:8px 0">Flow</h4>
16 | <ul>
17 | <li v-for="(s, i) in flow.steps" :key="i">{{ s }}</li>
18 | </ul>
19 | <div class="mcp-callout" v-if="flow.note">{{ flow.note }}</div>
20 | </div>
21 | <div class="mcp-col-6">
22 | <h4 style="margin:8px 0">Diagram</h4>
23 | <div class="mcp-diagram">
24 | <svg viewBox="0 0 600 240" width="100%" height="180">
25 | <defs>
26 | <marker id="arrow" markerWidth="10" markerHeight="10" refX="6" refY="3" orient="auto">
27 | <path d="M0,0 L0,6 L6,3 z" fill="currentColor" />
28 | </marker>
29 | </defs>
30 | <!-- Nodes -->
31 | <rect x="30" y="30" width="140" height="40" rx="8" fill="none" stroke="currentColor" />
32 | <text x="100" y="55" text-anchor="middle">Client</text>
33 | <rect x="230" y="30" width="140" height="40" rx="8" fill="none" stroke="currentColor" />
34 | <text x="300" y="55" text-anchor="middle">Master</text>
35 | <rect x="430" y="30" width="140" height="40" rx="8" fill="none" stroke="currentColor" />
36 | <text x="500" y="55" text-anchor="middle">Backend</text>
37 |
38 | <!-- Arrows vary by strategy -->
39 | <g v-if="strategy==='master_oauth'">
40 | <path d="M170,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
41 | <text x="200" y="40" text-anchor="middle">Bearer client_token</text>
42 | <path d="M370,50 L430,50" stroke="currentColor" marker-end="url(#arrow)" />
43 | <text x="400" y="40" text-anchor="middle">Bearer client_token</text>
44 | </g>
45 | <g v-else-if="strategy==='delegate_oauth'">
46 | <path d="M170,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
47 | <text x="200" y="40" text-anchor="middle">call tool</text>
48 | <path d="M230,90 L120,160" stroke="currentColor" marker-end="url(#arrow)" />
49 | <text x="170" y="130" text-anchor="middle">302 authorize</text>
50 | <path d="M120,160 L430,50" stroke="currentColor" marker-end="url(#arrow)" />
51 | <text x="275" y="120" text-anchor="middle">code + PKCE</text>
52 | <path d="M430,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
53 | <text x="330" y="40" text-anchor="middle">token stored</text>
54 | </g>
55 | <g v-else-if="strategy==='proxy_oauth'">
56 | <path d="M170,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
57 | <text x="200" y="40" text-anchor="middle">call tool</text>
58 | <path d="M370,50 L430,50" stroke="currentColor" marker-end="url(#arrow)" />
59 | <text x="400" y="40" text-anchor="middle">Bearer backend_token</text>
60 | <path d="M300,70 L300,120 L430,120" stroke="currentColor" marker-end="url(#arrow)" />
61 | <text x="360" y="110" text-anchor="middle">refresh if needed</text>
62 | </g>
63 | <g v-else>
64 | <path d="M170,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
65 | <path d="M370,50 L430,50" stroke="currentColor" marker-end="url(#arrow)" />
66 | <text x="400" y="40" text-anchor="middle">no auth header</text>
67 | </g>
68 | </svg>
69 | </div>
70 | </div>
71 | </div>
72 | </template>
73 |
74 | <script setup lang="ts">
75 | import { computed, ref } from 'vue'
76 |
77 | const strategy = ref<'master_oauth'|'delegate_oauth'|'proxy_oauth'|'bypass_auth'>('master_oauth')
78 |
79 | const flow = computed(() => {
80 | switch (strategy.value) {
81 | case 'master_oauth':
82 | return {
83 | steps: [
84 | 'Client calls master with Authorization: Bearer <client_token>',
85 | 'Master forwards same token to backend',
86 | 'Backend validates token and serves the request'
87 | ],
88 | note: 'Simple and effective when backends trust the same issuer/audience as the client.'
89 | }
90 | case 'delegate_oauth':
91 | return {
92 | steps: [
93 | 'Client calls master; master requires backend auth',
94 | 'Master responds with OAuth delegation metadata',
95 | 'Client completes provider auth via /oauth/authorize and callback',
96 | 'Master stores backend token and retries the call'
97 | ],
98 | note: 'Use PKCE + state. Configure provider endpoints and client credentials.'
99 | }
100 | case 'proxy_oauth':
101 | return {
102 | steps: [
103 | 'Master manages backend tokens and refresh cycles',
104 | 'Requests include backend token; refresh on expiry',
105 | 'Fallback to delegation or pass-through as configured'
106 | ],
107 | note: 'Centralizes token lifecycle with fewer client prompts.'
108 | }
109 | default:
110 | return {
111 | steps: [
112 | 'No authentication is added by master',
113 | 'Backend must not require Authorization for selected endpoints'
114 | ],
115 | note: 'Use only for public or development backends.'
116 | }
117 | }
118 | })
119 | </script>
120 |
121 |
```
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineConfig } from 'vitepress'
2 |
3 | export default defineConfig({
4 | title: 'Master MCP Server',
5 | description: 'Aggregate and orchestrate multiple MCP servers behind one endpoint',
6 | lastUpdated: true,
7 | cleanUrls: true,
8 | head: [
9 | ['meta', { name: 'theme-color', content: '#0ea5e9' }],
10 | ['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
11 | ],
12 | themeConfig: {
13 | logo: '/logo.svg',
14 | siteTitle: 'Master MCP Server',
15 | search: {
16 | provider: 'local'
17 | },
18 | socialLinks: [
19 | { icon: 'github', link: 'https://github.com/your-org/master-mcp-server' }
20 | ],
21 | outline: [2, 6],
22 | nav: [
23 | { text: 'Getting Started', link: '/getting-started/overview' },
24 | { text: 'Guides', link: '/guides/index' },
25 | { text: 'API', link: '/api/index' },
26 | { text: 'Configuration', link: '/configuration/overview' },
27 | { text: 'Deployment', link: '/deployment/index' },
28 | { text: 'Examples', link: '/examples/index' },
29 | { text: 'Advanced', link: '/advanced/index' },
30 | { text: 'Troubleshooting', link: '/troubleshooting/index' },
31 | { text: 'Contributing', link: '/contributing/index' }
32 | ],
33 | sidebar: {
34 | '/getting-started/': [
35 | {
36 | text: 'Getting Started',
37 | items: [
38 | { text: 'Overview', link: '/getting-started/overview' },
39 | { text: 'Installation', link: '/getting-started/installation' },
40 | { text: 'Quick Start', link: '/getting-started/quick-start' },
41 | { text: 'Quickstart (Node)', link: '/getting-started/quickstart-node' },
42 | { text: 'Quickstart (Workers)', link: '/getting-started/quickstart-workers' },
43 | { text: 'Core Concepts', link: '/getting-started/concepts' }
44 | ]
45 | }
46 | ],
47 | '/guides/': [
48 | {
49 | text: 'User Guides',
50 | items: [
51 | { text: 'Authentication', link: '/guides/authentication' },
52 | { text: 'OAuth Delegation', link: '/guides/oauth-delegation' },
53 | { text: 'Client Integration', link: '/guides/client-integration' },
54 | { text: 'Server Sharing', link: '/guides/server-sharing' },
55 | { text: 'Module Loading', link: '/guides/module-loading' },
56 | { text: 'Request Routing', link: '/guides/request-routing' },
57 | { text: 'Configuration', link: '/guides/configuration' },
58 | { text: 'Testing Strategy', link: '/guides/testing' }
59 | ]
60 | }
61 | ],
62 | '/api/': [
63 | {
64 | text: 'API Reference',
65 | items: [
66 | { text: 'Overview', link: '/api/index' },
67 | { text: 'Types', link: '/api/reference/modules' }
68 | ]
69 | }
70 | ],
71 | '/configuration/': [
72 | {
73 | text: 'Configuration',
74 | items: [
75 | { text: 'Overview', link: '/configuration/overview' },
76 | { text: 'Reference', link: '/configuration/reference' },
77 | { text: 'Examples', link: '/configuration/examples' },
78 | { text: 'Environment Variables', link: '/configuration/environment' }
79 | ]
80 | }
81 | ],
82 | '/deployment/': [
83 | {
84 | text: 'Deployment',
85 | items: [
86 | { text: 'Overview', link: '/deployment/index' },
87 | { text: 'Docker', link: '/deployment/docker' },
88 | { text: 'Cloudflare Workers', link: '/deployment/cloudflare-workers' },
89 | { text: 'Koyeb', link: '/deployment/koyeb' },
90 | { text: 'Docs Site', link: '/deployment/docs-site' }
91 | ]
92 | }
93 | ],
94 | '/examples/': [
95 | {
96 | text: 'Examples',
97 | items: [
98 | { text: 'Index', link: '/examples/index' },
99 | { text: 'Basic Node Aggregator', link: '/examples/basic-node' },
100 | { text: 'Cloudflare Worker', link: '/examples/cloudflare-worker' },
101 | { text: 'Advanced Routing', link: '/examples/advanced-routing' },
102 | { text: 'OAuth Delegation', link: '/examples/oauth-delegation' },
103 | { text: 'Testing Patterns', link: '/examples/testing' }
104 | ]
105 | }
106 | ],
107 | '/advanced/': [
108 | {
109 | text: 'Advanced Topics',
110 | items: [
111 | { text: 'Security Hardening', link: '/advanced/security' },
112 | { text: 'Performance & Scalability', link: '/advanced/performance' },
113 | { text: 'Monitoring & Logging', link: '/advanced/monitoring' },
114 | { text: 'Extensibility & Plugins', link: '/advanced/extensibility' }
115 | ]
116 | }
117 | ],
118 | '/troubleshooting/': [
119 | {
120 | text: 'Troubleshooting',
121 | items: [
122 | { text: 'Common Issues', link: '/troubleshooting/index' },
123 | { text: 'OAuth & Tokens', link: '/troubleshooting/oauth' },
124 | { text: 'Routing & Modules', link: '/troubleshooting/routing' },
125 | { text: 'Deployment', link: '/troubleshooting/deployment' }
126 | ]
127 | }
128 | ],
129 | '/contributing/': [
130 | {
131 | text: 'Contributing',
132 | items: [
133 | { text: 'Overview', link: '/contributing/index' },
134 | { text: 'Development Setup', link: '/contributing/dev-setup' },
135 | { text: 'Coding & Docs Guidelines', link: '/contributing/guidelines' }
136 | ]
137 | }
138 | ]
139 | }
140 | }
141 | })
142 |
```
--------------------------------------------------------------------------------
/src/server/config-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { MasterConfig, RoutingConfig, ServerConfig } from '../types/config.js'
2 | import { ConfigLoader } from '../config/config-loader.js'
3 | import { Logger } from '../utils/logger.js'
4 | import { EnvironmentManager } from '../config/environment-manager.js'
5 | import { SecretManager } from '../config/secret-manager.js'
6 |
7 | export interface ConfigManagerOptions {
8 | // If provided, watch the file for changes (Node only)
9 | watch?: boolean
10 | }
11 |
12 | type Listener = (config: MasterConfig) => void
13 |
14 | export class ConfigManager {
15 | private config: MasterConfig | null = null
16 | private readonly listeners: Set<Listener> = new Set()
17 | private stopWatcher?: () => void
18 | private readonly secrets = new SecretManager()
19 | private watchPaths: string[] = []
20 |
21 | constructor(private readonly options?: ConfigManagerOptions) {}
22 |
23 | async load(): Promise<MasterConfig> {
24 | const explicit = EnvironmentManager.getExplicitConfigPath()
25 | let loaded: MasterConfig
26 | try {
27 | loaded = await ConfigLoader.load({ path: explicit })
28 | } catch (err) {
29 | Logger.warn('Primary config load failed; attempting env-only load', String(err))
30 | loaded = await ConfigLoader.loadFromEnv()
31 | }
32 | const normalized = this.applyDefaults(loaded)
33 | this.config = normalized
34 | const redacted = this.secrets.redact(normalized)
35 | Logger.info('Configuration loaded', {
36 | servers: normalized.servers.length,
37 | hosting: normalized.hosting.platform,
38 | redacted,
39 | })
40 | if (this.options?.watch) this.prepareWatcher(explicit)
41 | return normalized
42 | }
43 |
44 | getConfig(): MasterConfig {
45 | if (!this.config) throw new Error('Config not loaded')
46 | return this.config
47 | }
48 |
49 | getRouting(): RoutingConfig | undefined {
50 | return this.config?.routing
51 | }
52 |
53 | onChange(listener: Listener): () => void {
54 | this.listeners.add(listener)
55 | return () => this.listeners.delete(listener)
56 | }
57 |
58 | async reload(): Promise<void> {
59 | await this.load()
60 | if (this.config) this.emit(this.config)
61 | }
62 |
63 | stop(): void {
64 | try {
65 | this.stopWatcher?.()
66 | } catch {
67 | // ignore
68 | }
69 | }
70 |
71 | private emit(config: MasterConfig): void {
72 | for (const l of this.listeners) {
73 | try {
74 | l(config)
75 | } catch (err) {
76 | Logger.warn('Config listener threw', err)
77 | }
78 | }
79 | }
80 |
81 | private applyDefaults(cfg: MasterConfig): MasterConfig {
82 | // Shallow copy to avoid mutation
83 | const copy: MasterConfig = {
84 | ...cfg,
85 | hosting: {
86 | platform: cfg.hosting.platform ?? 'node',
87 | port: cfg.hosting.port ?? 3000,
88 | base_url: cfg.hosting.base_url,
89 | },
90 | routing: cfg.routing ? { ...cfg.routing } : {},
91 | master_oauth: { ...cfg.master_oauth },
92 | servers: cfg.servers.map((s) => this.normalizeServer(s)),
93 | }
94 | return copy
95 | }
96 |
97 | private normalizeServer(s: ServerConfig): ServerConfig {
98 | const port = s.config?.port
99 | const normalized: ServerConfig = {
100 | ...s,
101 | config: {
102 | environment: s.config?.environment ?? {},
103 | args: s.config?.args ?? [],
104 | ...(port ? { port } : {}),
105 | },
106 | }
107 | return normalized
108 | }
109 |
110 | private prepareWatcher(explicitPath?: string): void {
111 | const isNode = Boolean((globalThis as any)?.process?.versions?.node)
112 | if (!isNode) return
113 | const { base, env } = EnvironmentManager.getConfigPaths('config')
114 | this.watchPaths = []
115 | if (explicitPath) this.watchPaths.push(explicitPath)
116 | if (base) this.watchPaths.push(base)
117 | if (env) this.watchPaths.push(env)
118 | this.startWatcher()
119 | }
120 |
121 | private startWatcher(): void {
122 | const isNode = Boolean((globalThis as any)?.process?.versions?.node)
123 | if (!isNode || this.watchPaths.length === 0) return
124 | import('node:fs').then((fs) => {
125 | const watchers: any[] = []
126 | const onChange = async () => {
127 | try {
128 | Logger.info('Config change detected; validating and reloading...')
129 | const prev = this.config
130 | const newCfg = await ConfigLoader.load({ path: EnvironmentManager.getExplicitConfigPath() })
131 | const applied = this.applyDefaults(newCfg)
132 | if (prev) this.auditDiff(prev, applied)
133 | this.config = applied
134 | this.emit(this.config)
135 | } catch (err) {
136 | Logger.warn('Hot-reload failed to apply new config', String(err))
137 | }
138 | }
139 | for (const p of this.watchPaths) {
140 | try {
141 | watchers.push((fs as any).watch(p, { persistent: false }, onChange))
142 | } catch (err) {
143 | Logger.warn(`Failed to watch ${p}`, String(err))
144 | }
145 | }
146 | this.stopWatcher = () => {
147 | for (const w of watchers) {
148 | try {
149 | w?.close?.()
150 | } catch {
151 | // ignore
152 | }
153 | }
154 | }
155 | }).catch((err) => Logger.warn('Failed to start config file watcher', String(err)))
156 | }
157 |
158 | private auditDiff(oldCfg: MasterConfig, newCfg: MasterConfig): void {
159 | const diff: Record<string, { from: unknown; to: unknown }> = {}
160 | const keys = new Set([...Object.keys(oldCfg), ...Object.keys(newCfg)])
161 | for (const k of keys) {
162 | const a: any = (oldCfg as any)[k]
163 | const b: any = (newCfg as any)[k]
164 | if (JSON.stringify(a) !== JSON.stringify(b)) diff[k] = { from: a, to: b }
165 | }
166 | const redacted = this.secrets.redact(diff)
167 | // Highlight non-hot-reloadable settings
168 | if (oldCfg.hosting?.port !== newCfg.hosting?.port) {
169 | Logger.warn('Hosting port changed; restart required to apply')
170 | }
171 | Logger.info('Config change audit', redacted)
172 | }
173 | }
174 |
```
--------------------------------------------------------------------------------
/docs/configuration/reference.md:
--------------------------------------------------------------------------------
```markdown
1 | # Configuration Reference
2 |
3 | Configuration can be provided as JSON or YAML. The loader merges:
4 |
5 | 1) `config/default.json`
6 | 2) `config/<env>.json` (`MASTER_ENV` or `NODE_ENV`)
7 | 3) Environment overrides
8 | 4) CLI overrides
9 | 5) Explicit file from `MASTER_CONFIG_PATH`
10 |
11 | Validated against `config/schema.json` (or embedded fallback).
12 |
13 | ## Top-Level Fields
14 |
15 | - `master_oauth` (required): OAuth settings for the master/client tokens
16 | - `authorization_endpoint` (url)
17 | - `token_endpoint` (url)
18 | - `client_id` (string)
19 | - `client_secret` (string | `env:VAR` | `enc:gcm:...`)
20 | - `redirect_uri` (string)
21 | - `scopes` (string[])
22 | - `issuer?` (string)
23 | - `jwks_uri?` (string)
24 | - `audience?` (string)
25 | - `hosting` (required)
26 | - `platform`: `node` | `cloudflare-workers` | `koyeb` | `docker` | `unknown`
27 | - `port?`: integer (Node only)
28 | - `base_url?`: used for OAuth redirect URL construction
29 | - `storage_backend?`: hints (e.g., `kv`, `durable_object`, `fs`)
30 | - `logging`
31 | - `level`: `debug` | `info` | `warn` | `error`
32 | - `routing`
33 | - `loadBalancer.strategy`: `round_robin` | `weighted` | `health`
34 | - `circuitBreaker`: `failureThreshold`, `successThreshold`, `recoveryTimeoutMs`
35 | - `retry`: `maxRetries`, `baseDelayMs`, `maxDelayMs`, `backoffFactor`, `jitter`, `retryOn.*`
36 | - `security`
37 | - `config_key_env?`: env var name containing config secret key (defaults to `MASTER_CONFIG_KEY`)
38 | - `audit?`: enable config change audit logs
39 | - `rotation_days?`: secret rotation policy hint
40 | - `servers` (required) — array of:
41 | - `id` (string)
42 | - `type`: `git` | `npm` | `pypi` | `docker` | `local`
43 | - `auth_strategy`: `master_oauth` | `delegate_oauth` | `bypass_auth` | `proxy_oauth`
44 | - `auth_config?`: provider-specific details
45 | - `config`:
46 | - `port?` (integer)
47 | - `environment?` (map)
48 | - `args?` (string[])
49 |
50 | ## YAML Example
51 |
52 | ```yaml
53 | hosting:
54 | platform: node
55 | port: 3000
56 |
57 | logging:
58 | level: info
59 |
60 | routing:
61 | loadBalancer: { strategy: round_robin }
62 | circuitBreaker: { failureThreshold: 5, successThreshold: 2, recoveryTimeoutMs: 30000 }
63 | retry: { maxRetries: 2, baseDelayMs: 250, maxDelayMs: 4000, backoffFactor: 2, jitter: full }
64 |
65 | master_oauth:
66 | authorization_endpoint: https://example.com/oauth/authorize
67 | token_endpoint: https://example.com/oauth/token
68 | client_id: master-mcp
69 | client_secret: env:MASTER_OAUTH_CLIENT_SECRET
70 | redirect_uri: http://localhost:3000/oauth/callback
71 | scopes: [openid]
72 |
73 | servers:
74 | - id: tools
75 | type: local
76 | auth_strategy: bypass_auth
77 | config:
78 | port: 3333
79 | ```
80 |
81 | <!-- GENERATED:BEGIN -->
82 |
83 | # Configuration Reference
84 |
85 | This reference is generated from the built-in JSON Schema used by the server to validate configuration.
86 |
87 | ## Top-Level Fields
88 |
89 | - `master_oauth` (required) — type: object
90 | - `master_oauth.issuer` — type: string
91 | - `master_oauth.authorization_endpoint` (required) — type: string, format: url
92 | - `master_oauth.token_endpoint` (required) — type: string, format: url
93 | - `master_oauth.jwks_uri` — type: string
94 | - `master_oauth.client_id` (required) — type: string
95 | - `master_oauth.client_secret` — type: string
96 | - `master_oauth.redirect_uri` (required) — type: string
97 | - `master_oauth.scopes` (required) — type: array
98 | - items: — type: string
99 | - `master_oauth.audience` — type: string
100 | - `hosting` (required) — type: object
101 | - `hosting.platform` (required) — type: string, enum: node, cloudflare-workers, koyeb, docker, unknown
102 | - `hosting.port` — type: number, format: integer
103 | - `hosting.base_url` — type: string
104 | - `logging` — type: object
105 | - `logging.level` — type: string, enum: debug, info, warn, error
106 | - `routing` — type: object
107 | - `routing.loadBalancer` — type: object
108 | - `routing.loadBalancer.strategy` — type: string
109 | - `routing.circuitBreaker` — type: object
110 | - `routing.retry` — type: object
111 | - `servers` (required) — type: array
112 | - items: — type: object
113 | - `servers[].id` (required) — type: string
114 | - `servers[].type` (required) — type: string, enum: git, npm, pypi, docker, local
115 | - `servers[].url` — type: string
116 | - `servers[].package` — type: string
117 | - `servers[].version` — type: string
118 | - `servers[].branch` — type: string
119 | - `servers[].auth_strategy` (required) — type: string, enum: master_oauth, delegate_oauth, bypass_auth, proxy_oauth
120 | - `servers[].auth_config` — type: object
121 | - `servers[].config` (required) — type: object
122 | - `servers[].config.environment` — type: object
123 | - `servers[].config.args` — type: array
124 | - items: — type: string
125 | - `servers[].config.port` — type: number, format: integer
126 |
127 |
128 | ## Examples
129 |
130 | ## Example: basic.yaml
131 |
132 | ```yaml
133 | hosting:
134 | platform: node
135 | port: 3000
136 |
137 | master_oauth:
138 | authorization_endpoint: https://example.com/auth
139 | token_endpoint: https://example.com/token
140 | client_id: demo-client
141 | redirect_uri: http://localhost:3000/callback
142 | scopes:
143 | - openid
144 |
145 | servers:
146 | - id: example
147 | type: local
148 | auth_strategy: master_oauth
149 | config:
150 | environment: {}
151 | args: []
152 | port: 3333
153 |
154 |
155 | ```
156 |
157 | ## Example: simple-setup.yaml
158 |
159 | ```yaml
160 | hosting:
161 | platform: node
162 | port: 3000
163 |
164 | master_oauth:
165 | authorization_endpoint: https://auth.example.com/authorize
166 | token_endpoint: https://auth.example.com/token
167 | client_id: master-mcp
168 | redirect_uri: http://localhost:3000/oauth/callback
169 | scopes: [openid, profile]
170 |
171 | servers:
172 | - id: local-simple
173 | type: local
174 | auth_strategy: bypass_auth
175 | config:
176 | port: 4001
177 | environment: {}
178 |
179 |
180 | ```
181 |
182 |
183 | <!-- GENERATED:END -->
```
--------------------------------------------------------------------------------
/tests/servers/test-streaming-both.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import { spawn } from 'node:child_process'
4 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'
5 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
6 |
7 | async function startHttpServer() {
8 | console.log('Starting HTTP test server...')
9 |
10 | // Start the HTTP server as a background process
11 | const httpServer = spawn('node', ['examples/test-mcp-server.js'], {
12 | stdio: ['ignore', 'pipe', 'pipe'],
13 | env: { ...process.env, PORT: '3006' }
14 | })
15 |
16 | // Capture stdout and stderr
17 | httpServer.stdout.on('data', (data) => {
18 | console.log(`[HTTP Server] ${data.toString().trim()}`)
19 | })
20 |
21 | httpServer.stderr.on('data', (data) => {
22 | console.error(`[HTTP Server ERROR] ${data.toString().trim()}`)
23 | })
24 |
25 | // Wait a moment for the server to start
26 | await new Promise(resolve => setTimeout(resolve, 2000))
27 |
28 | return httpServer
29 | }
30 |
31 | async function runStreamingTest() {
32 | try {
33 | console.log('Testing Master MCP Server with HTTP Streaming...')
34 |
35 | // Create a streamable HTTP transport to connect to our MCP server
36 | const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
37 |
38 | // Create the MCP client
39 | const client = new Client({
40 | name: 'master-mcp-streaming-test-client',
41 | version: '1.0.0'
42 | })
43 |
44 | // Initialize the client
45 | await client.connect(transport)
46 | console.log('✅ Server initialized with streaming transport')
47 | console.log('Server info:', client.getServerVersion())
48 | console.log('Server capabilities:', client.getServerCapabilities())
49 |
50 | // List tools using streaming
51 | console.log('\n--- Testing tools/list with streaming ---')
52 | const toolsResult = await client.listTools({})
53 | console.log('✅ tools/list successful with streaming')
54 | console.log('Number of tools:', toolsResult.tools.length)
55 | console.log('Tools:', toolsResult.tools.map(t => t.name))
56 |
57 | // List resources using streaming
58 | console.log('\n--- Testing resources/list with streaming ---')
59 | const resourcesResult = await client.listResources({})
60 | console.log('✅ resources/list successful with streaming')
61 | console.log('Number of resources:', resourcesResult.resources.length)
62 | console.log('Resources:', resourcesResult.resources.map(r => r.uri))
63 |
64 | // Test ping
65 | console.log('\n--- Testing ping with streaming ---')
66 | const pingResult = await client.ping()
67 | console.log('✅ ping successful with streaming')
68 | console.log('Ping result:', pingResult)
69 |
70 | // Try calling a tool from the HTTP server
71 | console.log('\n--- Testing tool call to HTTP server ---')
72 | try {
73 | const httpToolCallResult = await client.callTool({
74 | name: 'test-server.echo', // Prefixed with server ID
75 | arguments: { message: 'Hello from HTTP server!' }
76 | })
77 | console.log('✅ HTTP tool call successful')
78 | console.log('HTTP tool result:', JSON.stringify(httpToolCallResult, null, 2))
79 | } catch (error) {
80 | console.log('⚠️ HTTP tool call failed (might not be available):', error.message)
81 | }
82 |
83 | // Try calling a tool from the STDIO server
84 | console.log('\n--- Testing tool call to STDIO server ---')
85 | try {
86 | const stdioToolCallResult = await client.callTool({
87 | name: 'stdio-server.stdio-echo', // Prefixed with server ID
88 | arguments: { message: 'Hello from STDIO server!' }
89 | })
90 | console.log('✅ STDIO tool call successful')
91 | console.log('STDIO tool result:', JSON.stringify(stdioToolCallResult, null, 2))
92 | } catch (error) {
93 | console.log('⚠️ STDIO tool call failed (might not be available):', error.message)
94 | }
95 |
96 | // Try reading a resource from the HTTP server
97 | console.log('\n--- Testing resource read from HTTP server ---')
98 | try {
99 | const httpResourceResult = await client.readResource({
100 | uri: 'test://example' // This should be prefixed with server ID if needed
101 | })
102 | console.log('✅ HTTP resource read successful')
103 | console.log('HTTP resource result:', JSON.stringify(httpResourceResult, null, 2))
104 | } catch (error) {
105 | console.log('⚠️ HTTP resource read failed (might not be available):', error.message)
106 | }
107 |
108 | // Try reading a resource from the STDIO server
109 | console.log('\n--- Testing resource read from STDIO server ---')
110 | try {
111 | const stdioResourceResult = await client.readResource({
112 | uri: 'stdio-server.stdio://example/resource' // Prefixed with server ID
113 | })
114 | console.log('✅ STDIO resource read successful')
115 | console.log('STDIO resource result:', JSON.stringify(stdioResourceResult, null, 2))
116 | } catch (error) {
117 | console.log('⚠️ STDIO resource read failed (might not be available):', error.message)
118 | }
119 |
120 | // Close the connection
121 | await client.close()
122 | console.log('\n✅ Disconnected from MCP server')
123 | console.log('\n🎉 All streaming tests completed successfully!')
124 |
125 | } catch (error) {
126 | console.error('❌ Streaming test failed:', error)
127 | console.error('Error stack:', error.stack)
128 | }
129 | }
130 |
131 | async function main() {
132 | let httpServer
133 |
134 | try {
135 | // Start the HTTP server
136 | httpServer = await startHttpServer()
137 |
138 | // Run the streaming test
139 | await runStreamingTest()
140 | } catch (error) {
141 | console.error('Test failed:', error)
142 | } finally {
143 | // Clean up: kill the HTTP server
144 | if (httpServer) {
145 | console.log('Stopping HTTP server...')
146 | httpServer.kill()
147 | }
148 | }
149 | }
150 |
151 | // Run the test
152 | main()
```
--------------------------------------------------------------------------------
/src/config/environment-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { HostingConfig, MasterConfig } from '../types/config.js'
2 | import { Logger } from '../utils/logger.js'
3 |
4 | export type EnvironmentName = 'development' | 'staging' | 'production' | 'test'
5 |
6 | function isNode(): boolean {
7 | return Boolean((globalThis as any)?.process?.versions?.node)
8 | }
9 |
10 | export class EnvironmentManager {
11 | static detectEnvironment(): EnvironmentName {
12 | const env = ((globalThis as any)?.process?.env?.MASTER_ENV ||
13 | (globalThis as any)?.process?.env?.NODE_ENV ||
14 | 'development') as string
15 | const normalized = env.toLowerCase()
16 | if (normalized === 'prod') return 'production'
17 | if (normalized === 'stage' || normalized === 'staging') return 'staging'
18 | if (normalized === 'test') return 'test'
19 | return 'development'
20 | }
21 |
22 | static detectPlatform(): HostingConfig['platform'] {
23 | if (isNode()) return 'node'
24 | // Heuristic: in CF workers, 'WebSocketPair' and 'navigator' often exist
25 | // We default to workers if Node.js globals are absent
26 | return 'cloudflare-workers'
27 | }
28 |
29 | static getConfigPaths(baseDir = 'config'): { base?: string; env?: string; schema?: string } {
30 | const env = this.detectEnvironment()
31 | return {
32 | base: `${baseDir}/default.json`,
33 | env: `${baseDir}/${env}.json`,
34 | schema: `${baseDir}/schema.json`,
35 | }
36 | }
37 |
38 | static getExplicitConfigPath(): string | undefined {
39 | const fromEnv = (globalThis as any)?.process?.env?.MASTER_CONFIG_PATH
40 | const fromArg = isNode() ? EnvironmentManager.parseCliArgs().configPath : undefined
41 | return (fromArg as string | undefined) || (fromEnv as string | undefined)
42 | }
43 |
44 | static parseCliArgs(): { [k: string]: unknown; configPath?: string } {
45 | if (!isNode()) return {}
46 | const args = (process.argv || []).slice(2)
47 | const result: Record<string, unknown> = {}
48 | for (const a of args) {
49 | if (!a.startsWith('--')) continue
50 | const eq = a.indexOf('=')
51 | let key = a
52 | let val: unknown = true
53 | if (eq > -1) {
54 | key = a.slice(0, eq)
55 | const raw = a.slice(eq + 1)
56 | try {
57 | val = JSON.parse(raw)
58 | } catch {
59 | val = raw
60 | }
61 | }
62 | key = key.replace(/^--/, '')
63 | if (key === 'config' || key === 'config-path') {
64 | ;(result as any).configPath = String(val)
65 | continue
66 | }
67 | // Support dotted keys: --hosting.port=4000
68 | setByPath(result, key, val)
69 | }
70 | return result
71 | }
72 |
73 | static loadEnvOverrides(): Partial<MasterConfig> {
74 | // Map env vars to config fields. All are optional overrides.
75 | const env = (globalThis as any)?.process?.env ?? {}
76 | const hosting: Partial<HostingConfig> = {}
77 | if (env.MASTER_HOSTING_PLATFORM) hosting.platform = env.MASTER_HOSTING_PLATFORM as HostingConfig['platform']
78 | if (env.MASTER_HOSTING_PORT) hosting.port = Number(env.MASTER_HOSTING_PORT)
79 | if (env.MASTER_BASE_URL) hosting.base_url = String(env.MASTER_BASE_URL)
80 |
81 | const logging: Partial<NonNullable<MasterConfig['logging']>> = {}
82 | if (env.MASTER_LOG_LEVEL) logging.level = env.MASTER_LOG_LEVEL as any
83 |
84 | const master_oauth: Partial<MasterConfig['master_oauth']> = {}
85 | if (env.MASTER_OAUTH_ISSUER) master_oauth.issuer = String(env.MASTER_OAUTH_ISSUER)
86 | if (env.MASTER_OAUTH_AUTHORIZATION_ENDPOINT)
87 | master_oauth.authorization_endpoint = String(env.MASTER_OAUTH_AUTHORIZATION_ENDPOINT)
88 | if (env.MASTER_OAUTH_TOKEN_ENDPOINT) master_oauth.token_endpoint = String(env.MASTER_OAUTH_TOKEN_ENDPOINT)
89 | if (env.MASTER_OAUTH_JWKS_URI) master_oauth.jwks_uri = String(env.MASTER_OAUTH_JWKS_URI)
90 | if (env.MASTER_OAUTH_CLIENT_ID) master_oauth.client_id = String(env.MASTER_OAUTH_CLIENT_ID)
91 | if (env.MASTER_OAUTH_CLIENT_SECRET) master_oauth.client_secret = `env:MASTER_OAUTH_CLIENT_SECRET`
92 | if (env.MASTER_OAUTH_REDIRECT_URI) master_oauth.redirect_uri = String(env.MASTER_OAUTH_REDIRECT_URI)
93 | if (env.MASTER_OAUTH_SCOPES) master_oauth.scopes = String(env.MASTER_OAUTH_SCOPES)
94 | .split(',')
95 | .map((s) => s.trim())
96 | .filter(Boolean)
97 | if (env.MASTER_OAUTH_AUDIENCE) master_oauth.audience = String(env.MASTER_OAUTH_AUDIENCE)
98 |
99 | // Servers can be provided as JSON in MASTER_SERVERS or YAML in MASTER_SERVERS_YAML
100 | let servers: MasterConfig['servers'] | undefined
101 | try {
102 | if (env.MASTER_SERVERS) servers = JSON.parse(String(env.MASTER_SERVERS))
103 | } catch (err) {
104 | Logger.warn('Failed to parse MASTER_SERVERS JSON; ignoring', String(err))
105 | }
106 | if (!servers && env.MASTER_SERVERS_YAML) {
107 | try {
108 | // External import avoided in workers; only parse if Node
109 | const YAML = isNode() ? (require('yaml') as typeof import('yaml')) : undefined
110 | if (YAML) servers = YAML.parse(String(env.MASTER_SERVERS_YAML))
111 | } catch (err) {
112 | Logger.warn('Failed to parse MASTER_SERVERS_YAML; ignoring', String(err))
113 | }
114 | }
115 |
116 | const override: Partial<MasterConfig> = {}
117 | if (Object.keys(hosting).length) (override as any).hosting = hosting
118 | if (Object.keys(logging).length) (override as any).logging = logging
119 | if (Object.keys(master_oauth).length) (override as any).master_oauth = master_oauth
120 | if (servers) (override as any).servers = servers
121 | return override
122 | }
123 | }
124 |
125 | function setByPath(target: Record<string, unknown>, dottedKey: string, value: unknown): void {
126 | const parts = dottedKey.split('.')
127 | let cur: any = target
128 | for (let i = 0; i < parts.length - 1; i++) {
129 | const p = parts[i]
130 | if (typeof cur[p] !== 'object' || cur[p] === null) cur[p] = {}
131 | cur = cur[p]
132 | }
133 | cur[parts[parts.length - 1]] = value
134 | }
135 |
136 |
```
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vue.js:
--------------------------------------------------------------------------------
```javascript
1 | import {
2 | BaseTransition,
3 | BaseTransitionPropsValidators,
4 | Comment,
5 | DeprecationTypes,
6 | EffectScope,
7 | ErrorCodes,
8 | ErrorTypeStrings,
9 | Fragment,
10 | KeepAlive,
11 | ReactiveEffect,
12 | Static,
13 | Suspense,
14 | Teleport,
15 | Text,
16 | TrackOpTypes,
17 | Transition,
18 | TransitionGroup,
19 | TriggerOpTypes,
20 | VueElement,
21 | assertNumber,
22 | callWithAsyncErrorHandling,
23 | callWithErrorHandling,
24 | camelize,
25 | capitalize,
26 | cloneVNode,
27 | compatUtils,
28 | compile,
29 | computed,
30 | createApp,
31 | createBaseVNode,
32 | createBlock,
33 | createCommentVNode,
34 | createElementBlock,
35 | createHydrationRenderer,
36 | createPropsRestProxy,
37 | createRenderer,
38 | createSSRApp,
39 | createSlots,
40 | createStaticVNode,
41 | createTextVNode,
42 | createVNode,
43 | customRef,
44 | defineAsyncComponent,
45 | defineComponent,
46 | defineCustomElement,
47 | defineEmits,
48 | defineExpose,
49 | defineModel,
50 | defineOptions,
51 | defineProps,
52 | defineSSRCustomElement,
53 | defineSlots,
54 | devtools,
55 | effect,
56 | effectScope,
57 | getCurrentInstance,
58 | getCurrentScope,
59 | getCurrentWatcher,
60 | getTransitionRawChildren,
61 | guardReactiveProps,
62 | h,
63 | handleError,
64 | hasInjectionContext,
65 | hydrate,
66 | hydrateOnIdle,
67 | hydrateOnInteraction,
68 | hydrateOnMediaQuery,
69 | hydrateOnVisible,
70 | initCustomFormatter,
71 | initDirectivesForSSR,
72 | inject,
73 | isMemoSame,
74 | isProxy,
75 | isReactive,
76 | isReadonly,
77 | isRef,
78 | isRuntimeOnly,
79 | isShallow,
80 | isVNode,
81 | markRaw,
82 | mergeDefaults,
83 | mergeModels,
84 | mergeProps,
85 | nextTick,
86 | normalizeClass,
87 | normalizeProps,
88 | normalizeStyle,
89 | onActivated,
90 | onBeforeMount,
91 | onBeforeUnmount,
92 | onBeforeUpdate,
93 | onDeactivated,
94 | onErrorCaptured,
95 | onMounted,
96 | onRenderTracked,
97 | onRenderTriggered,
98 | onScopeDispose,
99 | onServerPrefetch,
100 | onUnmounted,
101 | onUpdated,
102 | onWatcherCleanup,
103 | openBlock,
104 | popScopeId,
105 | provide,
106 | proxyRefs,
107 | pushScopeId,
108 | queuePostFlushCb,
109 | reactive,
110 | readonly,
111 | ref,
112 | registerRuntimeCompiler,
113 | render,
114 | renderList,
115 | renderSlot,
116 | resolveComponent,
117 | resolveDirective,
118 | resolveDynamicComponent,
119 | resolveFilter,
120 | resolveTransitionHooks,
121 | setBlockTracking,
122 | setDevtoolsHook,
123 | setTransitionHooks,
124 | shallowReactive,
125 | shallowReadonly,
126 | shallowRef,
127 | ssrContextKey,
128 | ssrUtils,
129 | stop,
130 | toDisplayString,
131 | toHandlerKey,
132 | toHandlers,
133 | toRaw,
134 | toRef,
135 | toRefs,
136 | toValue,
137 | transformVNodeArgs,
138 | triggerRef,
139 | unref,
140 | useAttrs,
141 | useCssModule,
142 | useCssVars,
143 | useHost,
144 | useId,
145 | useModel,
146 | useSSRContext,
147 | useShadowRoot,
148 | useSlots,
149 | useTemplateRef,
150 | useTransitionState,
151 | vModelCheckbox,
152 | vModelDynamic,
153 | vModelRadio,
154 | vModelSelect,
155 | vModelText,
156 | vShow,
157 | version,
158 | warn,
159 | watch,
160 | watchEffect,
161 | watchPostEffect,
162 | watchSyncEffect,
163 | withAsyncContext,
164 | withCtx,
165 | withDefaults,
166 | withDirectives,
167 | withKeys,
168 | withMemo,
169 | withModifiers,
170 | withScopeId
171 | } from "./chunk-HVR2FF6M.js";
172 | export {
173 | BaseTransition,
174 | BaseTransitionPropsValidators,
175 | Comment,
176 | DeprecationTypes,
177 | EffectScope,
178 | ErrorCodes,
179 | ErrorTypeStrings,
180 | Fragment,
181 | KeepAlive,
182 | ReactiveEffect,
183 | Static,
184 | Suspense,
185 | Teleport,
186 | Text,
187 | TrackOpTypes,
188 | Transition,
189 | TransitionGroup,
190 | TriggerOpTypes,
191 | VueElement,
192 | assertNumber,
193 | callWithAsyncErrorHandling,
194 | callWithErrorHandling,
195 | camelize,
196 | capitalize,
197 | cloneVNode,
198 | compatUtils,
199 | compile,
200 | computed,
201 | createApp,
202 | createBlock,
203 | createCommentVNode,
204 | createElementBlock,
205 | createBaseVNode as createElementVNode,
206 | createHydrationRenderer,
207 | createPropsRestProxy,
208 | createRenderer,
209 | createSSRApp,
210 | createSlots,
211 | createStaticVNode,
212 | createTextVNode,
213 | createVNode,
214 | customRef,
215 | defineAsyncComponent,
216 | defineComponent,
217 | defineCustomElement,
218 | defineEmits,
219 | defineExpose,
220 | defineModel,
221 | defineOptions,
222 | defineProps,
223 | defineSSRCustomElement,
224 | defineSlots,
225 | devtools,
226 | effect,
227 | effectScope,
228 | getCurrentInstance,
229 | getCurrentScope,
230 | getCurrentWatcher,
231 | getTransitionRawChildren,
232 | guardReactiveProps,
233 | h,
234 | handleError,
235 | hasInjectionContext,
236 | hydrate,
237 | hydrateOnIdle,
238 | hydrateOnInteraction,
239 | hydrateOnMediaQuery,
240 | hydrateOnVisible,
241 | initCustomFormatter,
242 | initDirectivesForSSR,
243 | inject,
244 | isMemoSame,
245 | isProxy,
246 | isReactive,
247 | isReadonly,
248 | isRef,
249 | isRuntimeOnly,
250 | isShallow,
251 | isVNode,
252 | markRaw,
253 | mergeDefaults,
254 | mergeModels,
255 | mergeProps,
256 | nextTick,
257 | normalizeClass,
258 | normalizeProps,
259 | normalizeStyle,
260 | onActivated,
261 | onBeforeMount,
262 | onBeforeUnmount,
263 | onBeforeUpdate,
264 | onDeactivated,
265 | onErrorCaptured,
266 | onMounted,
267 | onRenderTracked,
268 | onRenderTriggered,
269 | onScopeDispose,
270 | onServerPrefetch,
271 | onUnmounted,
272 | onUpdated,
273 | onWatcherCleanup,
274 | openBlock,
275 | popScopeId,
276 | provide,
277 | proxyRefs,
278 | pushScopeId,
279 | queuePostFlushCb,
280 | reactive,
281 | readonly,
282 | ref,
283 | registerRuntimeCompiler,
284 | render,
285 | renderList,
286 | renderSlot,
287 | resolveComponent,
288 | resolveDirective,
289 | resolveDynamicComponent,
290 | resolveFilter,
291 | resolveTransitionHooks,
292 | setBlockTracking,
293 | setDevtoolsHook,
294 | setTransitionHooks,
295 | shallowReactive,
296 | shallowReadonly,
297 | shallowRef,
298 | ssrContextKey,
299 | ssrUtils,
300 | stop,
301 | toDisplayString,
302 | toHandlerKey,
303 | toHandlers,
304 | toRaw,
305 | toRef,
306 | toRefs,
307 | toValue,
308 | transformVNodeArgs,
309 | triggerRef,
310 | unref,
311 | useAttrs,
312 | useCssModule,
313 | useCssVars,
314 | useHost,
315 | useId,
316 | useModel,
317 | useSSRContext,
318 | useShadowRoot,
319 | useSlots,
320 | useTemplateRef,
321 | useTransitionState,
322 | vModelCheckbox,
323 | vModelDynamic,
324 | vModelRadio,
325 | vModelSelect,
326 | vModelText,
327 | vShow,
328 | version,
329 | warn,
330 | watch,
331 | watchEffect,
332 | watchPostEffect,
333 | watchSyncEffect,
334 | withAsyncContext,
335 | withCtx,
336 | withDefaults,
337 | withDirectives,
338 | withKeys,
339 | withMemo,
340 | withModifiers,
341 | withScopeId
342 | };
343 | //# sourceMappingURL=vue.js.map
344 |
```
--------------------------------------------------------------------------------
/src/config/config-loader.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { MasterConfig } from '../types/config.js'
2 | import { EnvironmentManager } from './environment-manager.js'
3 | import { SecretManager } from './secret-manager.js'
4 | import { SchemaValidator } from './schema-validator.js'
5 | import { Logger } from '../utils/logger.js'
6 |
7 | type LoadOptions = {
8 | // Explicit path to config file; when provided, overrides environment-based discovery
9 | path?: string
10 | // Optional base directory for default and env configs
11 | baseDir?: string
12 | // Provide a schema path override
13 | schemaPath?: string
14 | }
15 |
16 | function isNode(): boolean {
17 | return Boolean((globalThis as any)?.process?.versions?.node)
18 | }
19 |
20 | export class ConfigLoader {
21 |
22 | static async load(options?: LoadOptions): Promise<MasterConfig> {
23 | const envName = EnvironmentManager.detectEnvironment()
24 | const platform = EnvironmentManager.detectPlatform()
25 | const explicit = options?.path ?? EnvironmentManager.getExplicitConfigPath()
26 | const baseDir = options?.baseDir ?? 'config'
27 | const paths = EnvironmentManager.getConfigPaths(baseDir)
28 | const schemaPath = options?.schemaPath ?? paths.schema
29 |
30 | let fileConfig: Partial<MasterConfig> = {}
31 | const loadedFiles: string[] = []
32 |
33 | if (explicit && isNode()) {
34 | const cfg = await this.loadFromFile(explicit)
35 | fileConfig = deepMerge(fileConfig, cfg)
36 | loadedFiles.push(explicit)
37 | } else if (isNode()) {
38 | // Load default.json then <env>.json if present
39 | const fs = await import('node:fs/promises')
40 | const fsc = await import('node:fs')
41 | if (paths.base && fsc.existsSync(paths.base)) {
42 | fileConfig = deepMerge(fileConfig, await this.loadFromFile(paths.base))
43 | loadedFiles.push(paths.base)
44 | }
45 | if (paths.env && fsc.existsSync(paths.env)) {
46 | fileConfig = deepMerge(fileConfig, await this.loadFromFile(paths.env))
47 | loadedFiles.push(paths.env)
48 | }
49 | // If nothing loaded and config dir doesn't exist, try a default path
50 | ;(void fs)
51 | }
52 |
53 | Logger.info('File config loaded', { fileConfig, loadedFiles })
54 |
55 | // Environment variables
56 | const envOverrides = EnvironmentManager.loadEnvOverrides()
57 | Logger.info('Environment overrides', { envOverrides })
58 | fileConfig = deepMerge(fileConfig, envOverrides)
59 |
60 | // CLI args nested overrides
61 | const cli = EnvironmentManager.parseCliArgs()
62 | Logger.info('CLI args', { cli })
63 | fileConfig = deepMerge(fileConfig, cli as any)
64 |
65 | // Ensure hosting.platform and env awareness
66 | const normalized: Partial<MasterConfig> = {
67 | ...fileConfig,
68 | hosting: { ...fileConfig.hosting, platform },
69 | }
70 |
71 | Logger.info('Normalized config', { normalized })
72 |
73 | // Schema validation and secret resolution
74 | const schema = await SchemaValidator.loadSchema(schemaPath)
75 | const validated = SchemaValidator.assertValid<MasterConfig>(normalized, schema!)
76 | const secrets = new SecretManager()
77 | const resolved = secrets.resolveSecrets(validated)
78 |
79 | // Cache with key based on env and paths
80 | // In-memory caching can be added if needed; omitted to keep memory footprint small
81 |
82 | Logger.info('Configuration loaded', {
83 | files: loadedFiles,
84 | platform,
85 | env: envName,
86 | })
87 | return resolved
88 | }
89 |
90 | static async loadFromFile(filePath: string): Promise<Partial<MasterConfig>> {
91 | if (!isNode()) throw new Error('File loading is only supported in Node.js runtime')
92 | const fs = await import('node:fs/promises')
93 | const path = await import('node:path')
94 | const raw = await fs.readFile(filePath, 'utf8')
95 | Logger.info('Loading config from file', { filePath, raw })
96 | const ext = path.extname(filePath).toLowerCase()
97 | let parsed: any
98 | if (ext === '.json') parsed = JSON.parse(raw)
99 | else if (ext === '.yaml' || ext === '.yml') parsed = (await import('yaml')).parse(raw)
100 | else {
101 | // Fallback: try JSON then YAML
102 | try {
103 | parsed = JSON.parse(raw)
104 | } catch {
105 | parsed = (await import('yaml')).parse(raw)
106 | }
107 | }
108 | Logger.info('Parsed config from file', { filePath, parsed })
109 | return parsed as Partial<MasterConfig>
110 | }
111 |
112 | static async loadFromEnv(): Promise<MasterConfig> {
113 | // For compatibility with older phases
114 | const override = EnvironmentManager.loadEnvOverrides()
115 | const defaults: Partial<MasterConfig> = {
116 | hosting: {
117 | platform: EnvironmentManager.detectPlatform(),
118 | port: (globalThis as any)?.process?.env?.PORT ? Number((globalThis as any)?.process?.env?.PORT) : 3000,
119 | base_url: (globalThis as any)?.process?.env?.BASE_URL,
120 | },
121 | servers: [],
122 | master_oauth: {
123 | authorization_endpoint: 'https://example.com/auth',
124 | token_endpoint: 'https://example.com/token',
125 | client_id: 'placeholder',
126 | redirect_uri: 'http://localhost/callback',
127 | scopes: ['openid'],
128 | },
129 | }
130 | const merged = deepMerge(defaults, override) as MasterConfig
131 | const schema = await SchemaValidator.loadSchema()
132 | return SchemaValidator.assertValid(merged, schema!)
133 | }
134 | }
135 |
136 | function deepMerge<T>(base: T, override: Partial<T>): T {
137 | if (Array.isArray(base) && Array.isArray(override)) return override as unknown as T
138 | if (base && typeof base === 'object' && override && typeof override === 'object') {
139 | const out: any = { ...(base as any) }
140 | for (const [k, v] of Object.entries(override as any)) {
141 | if (v === undefined) continue
142 | if (Array.isArray(v)) out[k] = v
143 | else if (typeof v === 'object' && v !== null) out[k] = deepMerge((base as any)[k], v)
144 | else out[k] = v
145 | }
146 | return out
147 | }
148 | return (override as T) ?? base
149 | }
150 |
```
--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Validation and sanitization helpers with a small schema system.
3 | * No external dependencies; suitable for Node and Workers.
4 | */
5 |
6 | export function isNonEmptyString(value: unknown): value is string {
7 | return typeof value === 'string' && value.trim().length > 0
8 | }
9 |
10 | export function isRecord(value: unknown): value is Record<string, unknown> {
11 | return !!value && typeof value === 'object' && !Array.isArray(value)
12 | }
13 |
14 | export function sanitizeString(input: unknown, opts?: { maxLength?: number; trim?: boolean }): string {
15 | let s = typeof input === 'string' ? input : String(input ?? '')
16 | if (opts?.trim !== false) s = s.trim()
17 | // Remove control characters except tab, newline, carriage return
18 | s = s.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
19 | if (opts?.maxLength && s.length > opts.maxLength) s = s.slice(0, opts.maxLength)
20 | return s
21 | }
22 |
23 | export function sanitizeObject<T extends Record<string, unknown>>(obj: T): T {
24 | const dangerous = ['__proto__', 'constructor', 'prototype']
25 | for (const k of Object.keys(obj)) {
26 | if (dangerous.includes(k)) delete (obj as any)[k]
27 | }
28 | return obj
29 | }
30 |
31 | export function assert(condition: unknown, message = 'Assertion failed'): asserts condition {
32 | if (!condition) throw new Error(message)
33 | }
34 |
35 | export function assertString(value: unknown, message = 'Expected string'): asserts value is string {
36 | if (typeof value !== 'string') throw new Error(message)
37 | }
38 |
39 | export function assertNumber(value: unknown, message = 'Expected number'): asserts value is number {
40 | if (typeof value !== 'number' || Number.isNaN(value)) throw new Error(message)
41 | }
42 |
43 | export function assertBoolean(value: unknown, message = 'Expected boolean'): asserts value is boolean {
44 | if (typeof value !== 'boolean') throw new Error(message)
45 | }
46 |
47 | export type SafeParseResult<T> = { success: true; data: T } | { success: false; error: string }
48 |
49 | export interface Schema<T> {
50 | parse(input: unknown): T
51 | safeParse(input: unknown): SafeParseResult<T>
52 | }
53 |
54 | function makeSchema<T>(name: string, parse: (i: unknown) => T): Schema<T> {
55 | return {
56 | parse(input: unknown): T {
57 | try {
58 | return parse(input)
59 | } catch (e) {
60 | const msg = e instanceof Error ? e.message : String(e)
61 | throw new Error(`${name} validation failed: ${msg}`)
62 | }
63 | },
64 | safeParse(input: unknown): SafeParseResult<T> {
65 | try {
66 | return { success: true, data: parse(input) }
67 | } catch (e) {
68 | return { success: false, error: e instanceof Error ? e.message : String(e) }
69 | }
70 | },
71 | }
72 | }
73 |
74 | export const v = {
75 | string: (opts?: { min?: number; max?: number; pattern?: RegExp }) =>
76 | makeSchema<string>('string', (i) => {
77 | if (typeof i !== 'string') throw new Error('not a string')
78 | const s = i
79 | if (opts?.min !== undefined && s.length < opts.min) throw new Error(`min length ${opts.min}`)
80 | if (opts?.max !== undefined && s.length > opts.max) throw new Error(`max length ${opts.max}`)
81 | if (opts?.pattern && !opts.pattern.test(s)) throw new Error('pattern mismatch')
82 | return s
83 | }),
84 |
85 | number: (opts?: { min?: number; max?: number; int?: boolean }) =>
86 | makeSchema<number>('number', (i) => {
87 | if (typeof i !== 'number' || Number.isNaN(i)) throw new Error('not a number')
88 | const n = i
89 | if (opts?.int) {
90 | if (!Number.isInteger(n)) throw new Error('not an integer')
91 | }
92 | if (opts?.min !== undefined && n < opts.min) throw new Error(`min ${opts.min}`)
93 | if (opts?.max !== undefined && n > opts.max) throw new Error(`max ${opts.max}`)
94 | return n
95 | }),
96 |
97 | boolean: () => makeSchema<boolean>('boolean', (i) => {
98 | if (typeof i !== 'boolean') throw new Error('not a boolean')
99 | return i
100 | }),
101 |
102 | literal: <T extends string | number | boolean | null>(val: T) =>
103 | makeSchema<T>('literal', (i) => {
104 | if (i !== val) throw new Error(`expected ${String(val)}`)
105 | return i as T
106 | }),
107 |
108 | array: <T>(inner: Schema<T>, opts?: { min?: number; max?: number }) =>
109 | makeSchema<T[]>('array', (i) => {
110 | if (!Array.isArray(i)) throw new Error('not an array')
111 | if (opts?.min !== undefined && i.length < opts.min) throw new Error(`min length ${opts.min}`)
112 | if (opts?.max !== undefined && i.length > opts.max) throw new Error(`max length ${opts.max}`)
113 | return i.map((x) => inner.parse(x))
114 | }),
115 |
116 | object: <S extends Record<string, Schema<any>>>(shape: S) =>
117 | makeSchema<{ [K in keyof S]: S[K] extends Schema<infer U> ? U : never }>('object', (i) => {
118 | if (!isRecord(i)) throw new Error('not an object')
119 | const out: Record<string, unknown> = {}
120 | for (const [k, s] of Object.entries(shape)) {
121 | out[k] = (s as Schema<unknown>).parse((i as any)[k])
122 | }
123 | return out as any
124 | }),
125 |
126 | union: <A, B>(a: Schema<A>, b: Schema<B>) =>
127 | makeSchema<A | B>('union', (i) => {
128 | const ra = a.safeParse(i)
129 | if (ra.success) return ra.data
130 | const rb = b.safeParse(i)
131 | if (rb.success) return rb.data
132 | throw new Error(`no union match: ${ra.error}; ${rb.error}`)
133 | }),
134 |
135 | optional: <T>(inner: Schema<T>) =>
136 | makeSchema<T | undefined>('optional', (i) => {
137 | if (i === undefined) return undefined
138 | return inner.parse(i)
139 | }),
140 | }
141 |
142 | export function isEmail(input: string): boolean {
143 | // Simple and conservative
144 | return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)
145 | }
146 |
147 | export function isUrl(input: string): boolean {
148 | try {
149 | const u = new URL(input)
150 | return u.protocol === 'http:' || u.protocol === 'https:'
151 | } catch {
152 | return false
153 | }
154 | }
155 |
156 | export function isUUID(input: string): boolean {
157 | 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)
158 | }
159 |
160 | export function safeHeaderName(name: string): boolean {
161 | return /^[A-Za-z0-9-]+$/.test(name) && !/__/g.test(name)
162 | }
163 |
164 | export function safeHeaderValue(value: string): boolean {
165 | return !/[\r\n]/.test(value) && value.length < 8192
166 | }
167 |
168 | export function validateAgainstSchema<T>(schema: Schema<T>, input: unknown): T {
169 | return schema.parse(input)
170 | }
171 |
172 |
```
--------------------------------------------------------------------------------
/src/auth/multi-auth-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createRemoteJWKSet, jwtVerify } from 'jose'
2 | import type { AuthHeaders, OAuthDelegation, OAuthToken } from '../types/auth.js'
3 | import type { MasterAuthConfig, ServerAuthConfig } from '../types/config.js'
4 | import { AuthStrategy } from '../types/config.js'
5 | import { Logger } from '../utils/logger.js'
6 | import { getOAuthProvider } from './oauth-providers.js'
7 | import { TokenManager } from './token-manager.js'
8 |
9 | export class MultiAuthManager {
10 | private serverAuth: Map<string, { strategy: AuthStrategy; config?: ServerAuthConfig }> = new Map()
11 | private jwks?: ReturnType<typeof createRemoteJWKSet>
12 | private tokenManager = new TokenManager()
13 |
14 | constructor(private readonly config: MasterAuthConfig) {
15 | if (config.jwks_uri) {
16 | try {
17 | this.jwks = createRemoteJWKSet(new URL(config.jwks_uri))
18 | } catch (err) {
19 | Logger.warn('Failed to initialize JWKS for client token validation', err)
20 | }
21 | }
22 | }
23 |
24 | registerServerAuth(serverId: string, strategy: AuthStrategy, authConfig?: ServerAuthConfig): void {
25 | this.serverAuth.set(serverId, { strategy, config: authConfig })
26 | }
27 |
28 | private keyFor(clientToken: string, serverId: string): string {
29 | return `${serverId}::${clientToken.slice(0, 16)}`
30 | }
31 |
32 | async validateClientToken(token: string): Promise<boolean> {
33 | if (!token || typeof token !== 'string') return false
34 | if (!this.jwks) {
35 | // Best-effort: check structural validity and expiration if it is a JWT; otherwise accept as opaque bearer
36 | try {
37 | const { payload } = await jwtVerify(token, async () => {
38 | // No key ⇒ force failure to reach catch where we treat opaque tokens as valid
39 | throw new Error('no-jwks')
40 | })
41 | const now = Math.floor(Date.now() / 1000)
42 | return typeof payload.exp !== 'number' || payload.exp > now
43 | } catch {
44 | return true // Accept opaque tokens when no JWKS is configured
45 | }
46 | }
47 |
48 | try {
49 | await jwtVerify(token, this.jwks, {
50 | issuer: this.config.issuer,
51 | audience: this.config.audience ?? this.config.client_id,
52 | })
53 | return true
54 | } catch (err) {
55 | Logger.warn('Client token verification failed', String(err))
56 | return false
57 | }
58 | }
59 |
60 | async prepareAuthForBackend(serverId: string, clientToken: string): Promise<AuthHeaders | OAuthDelegation> {
61 | const isValid = await this.validateClientToken(clientToken)
62 | if (!isValid) throw new Error('Invalid client token')
63 |
64 | const entry = this.serverAuth.get(serverId)
65 | if (!entry) {
66 | // Default: pass-through
67 | return { Authorization: `Bearer ${clientToken}` }
68 | }
69 |
70 | const { strategy, config } = entry
71 | switch (strategy) {
72 | case AuthStrategy.MASTER_OAUTH:
73 | return this.handleMasterOAuth(serverId, clientToken)
74 | case AuthStrategy.DELEGATE_OAUTH:
75 | if (!config) throw new Error(`Missing auth config for server ${serverId}`)
76 | return this.handleDelegatedOAuth(serverId, clientToken, config)
77 | case AuthStrategy.BYPASS_AUTH:
78 | return {}
79 | case AuthStrategy.PROXY_OAUTH:
80 | if (!config) throw new Error(`Missing auth config for server ${serverId}`)
81 | return this.handleProxyOAuth(serverId, clientToken, config)
82 | default:
83 | return { Authorization: `Bearer ${clientToken}` }
84 | }
85 | }
86 |
87 | public async handleMasterOAuth(_serverId: string, clientToken: string): Promise<AuthHeaders> {
88 | // Pass-through the client's master token
89 | return { Authorization: `Bearer ${clientToken}` }
90 | }
91 |
92 | public async handleDelegatedOAuth(
93 | serverId: string,
94 | clientToken: string,
95 | serverAuthConfig: ServerAuthConfig
96 | ): Promise<OAuthDelegation> {
97 | // Return instructions for the client to complete OAuth against the provider
98 | const scopes = Array.isArray(serverAuthConfig.scopes) ? serverAuthConfig.scopes : ['openid']
99 | // Create state binding server + client
100 | const state = this.tokenManager.generateState({ serverId })
101 | // Store a minimal pending marker for later exchange if needed
102 | await this.tokenManager.storeToken(this.keyFor(clientToken, serverId), {
103 | access_token: '',
104 | expires_at: 0,
105 | scope: [],
106 | })
107 |
108 | return {
109 | type: 'oauth_delegation',
110 | auth_endpoint: serverAuthConfig.authorization_endpoint,
111 | token_endpoint: serverAuthConfig.token_endpoint,
112 | client_info: { client_id: serverAuthConfig.client_id, metadata: { state } },
113 | required_scopes: scopes,
114 | redirect_after_auth: true,
115 | }
116 | }
117 |
118 | public async handleProxyOAuth(
119 | serverId: string,
120 | clientToken: string,
121 | serverAuthConfig: ServerAuthConfig
122 | ): Promise<AuthHeaders> {
123 | const key = this.keyFor(clientToken, serverId)
124 | const existing = await this.tokenManager.getToken(key)
125 | const now = Date.now()
126 | if (existing && existing.access_token && existing.expires_at > now + 30_000) {
127 | return { Authorization: `Bearer ${existing.access_token}` }
128 | }
129 |
130 | if (existing?.refresh_token) {
131 | try {
132 | const provider = getOAuthProvider(serverAuthConfig as any)
133 | const refreshed = await provider.refreshToken(existing.refresh_token)
134 | await this.tokenManager.storeToken(key, refreshed)
135 | return { Authorization: `Bearer ${refreshed.access_token}` }
136 | } catch (err) {
137 | Logger.warn('Refresh token failed; falling back to pass-through', err)
138 | }
139 | }
140 |
141 | // Fallback: pass through the client token (may be accepted by backend if configured)
142 | return { Authorization: `Bearer ${clientToken}` }
143 | }
144 |
145 | async storeDelegatedToken(clientToken: string, serverId: string, serverToken: string | OAuthToken): Promise<void> {
146 | const key = this.keyFor(clientToken, serverId)
147 | const tokenObj: OAuthToken = typeof serverToken === 'string'
148 | ? { access_token: serverToken, expires_at: Date.now() + 3600_000, scope: [] }
149 | : serverToken
150 | await this.tokenManager.storeToken(key, tokenObj)
151 | }
152 |
153 | async getStoredServerToken(serverId: string, clientToken: string): Promise<string | undefined> {
154 | const tok = await this.tokenManager.getToken(this.keyFor(clientToken, serverId))
155 | return tok?.access_token
156 | }
157 | }
158 |
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { AuthInfo } from '../types/auth.js'
2 |
3 | export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
4 |
5 | export interface LogFields {
6 | [key: string]: unknown
7 | correlationId?: string
8 | }
9 |
10 | interface LoggerOptions {
11 | level?: LogLevel
12 | json?: boolean
13 | base?: LogFields
14 | }
15 |
16 | /**
17 | * Lightweight, structured, context-aware logger with JSON output support and
18 | * timing utilities. Designed to run on Node.js and Workers without deps.
19 | */
20 | export class Logger {
21 | private static level: LogLevel = ((): LogLevel => {
22 | const env = (globalThis as any)?.process?.env
23 | const raw = (env?.LOG_LEVEL || env?.NODE_LOG_LEVEL || 'info').toLowerCase()
24 | const allowed: LogLevel[] = ['fatal', 'error', 'warn', 'info', 'debug', 'trace']
25 | return (allowed.includes(raw as LogLevel) ? (raw as LogLevel) : 'info') as LogLevel
26 | })()
27 |
28 | private static json: boolean = ((): boolean => {
29 | const env = (globalThis as any)?.process?.env
30 | const raw = env?.LOG_FORMAT || env?.LOG_JSON
31 | if (!raw) return (env?.NODE_ENV === 'production') as boolean
32 | return String(raw).toLowerCase() === 'true' || String(raw).toLowerCase() === 'json'
33 | })()
34 |
35 | private static base: LogFields = {}
36 |
37 | static configure(opts: LoggerOptions): void {
38 | if (opts.level) this.level = opts.level
39 | if (typeof opts.json === 'boolean') this.json = opts.json
40 | if (opts.base) this.base = { ...this.base, ...sanitizeFields(opts.base) }
41 | }
42 |
43 | static with(fields: LogFields): typeof Logger {
44 | const merged = { ...this.base, ...sanitizeFields(fields) }
45 | const child = new Proxy(this, {
46 | get: (target, prop) => {
47 | if (prop === 'base') return merged
48 | return (target as any)[prop]
49 | },
50 | }) as typeof Logger
51 | return child
52 | }
53 |
54 | static setLevel(level: LogLevel): void {
55 | this.level = level
56 | }
57 |
58 | static enableJSON(enabled: boolean): void {
59 | this.json = enabled
60 | }
61 |
62 | static getLevel(): LogLevel {
63 | return this.level
64 | }
65 |
66 | static trace(message: string, fields?: LogFields | unknown): void {
67 | const f = fieldsToLogFields(fields)
68 | this._log('trace', message, f)
69 | }
70 | static debug(message: string, fields?: LogFields | unknown): void {
71 | const envDebug = (globalThis as any)?.process?.env?.DEBUG
72 | const f = fieldsToLogFields(fields)
73 | if (envDebug || this.levelAllowed('debug')) this._log('debug', message, f)
74 | }
75 | static info(message: string, fields?: LogFields | unknown): void {
76 | const f = fieldsToLogFields(fields)
77 | this._log('info', message, f)
78 | }
79 | static warn(message: string, fields?: LogFields | unknown): void {
80 | const f = fieldsToLogFields(fields)
81 | this._log('warn', message, f)
82 | }
83 | static error(message: string, fields?: LogFields | unknown): void {
84 | const f = fieldsToLogFields(fields)
85 | this._log('error', message, f)
86 | }
87 | static fatal(message: string, fields?: LogFields | unknown): void {
88 | const f = fieldsToLogFields(fields)
89 | this._log('fatal', message, f)
90 | }
91 |
92 | /**
93 | * Structured auth event helper for backward compatibility.
94 | */
95 | static logAuthEvent(event: string, context: AuthInfo): void {
96 | this.info('auth_event', { event, ...context })
97 | }
98 |
99 | /**
100 | * Structured server event helper for backward compatibility.
101 | */
102 | static logServerEvent(event: string, serverId: string, context?: unknown): void {
103 | const fields = fieldsToLogFields(context)
104 | this.info('server_event', { event, serverId, ...(fields ?? {}) })
105 | }
106 |
107 | /**
108 | * Starts a performance timer, returning a function to log completion.
109 | *
110 | * Usage:
111 | * const done = Logger.time('load_config', { id })
112 | * ...work...
113 | * done({ status: 'ok' })
114 | */
115 | static time(name: string, fields?: LogFields): (extra?: LogFields) => void {
116 | const start = now()
117 | const base = { name, ...(fields ? sanitizeFields(fields) : {}) }
118 | return (extra?: LogFields) => {
119 | const durationMs = Math.max(0, now() - start)
120 | this.info('perf', { ...base, ...(extra ? sanitizeFields(extra) : {}), durationMs })
121 | }
122 | }
123 |
124 | /**
125 | * Low-level log method honoring level and output format.
126 | */
127 | private static _log(level: LogLevel, message: string, fields?: LogFields): void {
128 | if (!this.levelAllowed(level)) return
129 | const ts = new Date().toISOString()
130 | const entry = {
131 | ts,
132 | level,
133 | msg: message,
134 | ...this.base,
135 | ...(fields ? sanitizeFields(fields) : {}),
136 | }
137 |
138 | // eslint-disable-next-line no-console
139 | if (this.json) console.log(JSON.stringify(entry))
140 | else console.log(formatHuman(entry))
141 | }
142 |
143 | private static levelAllowed(check: LogLevel): boolean {
144 | const order: LogLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']
145 | const curIdx = order.indexOf(this.level)
146 | const chkIdx = order.indexOf(check)
147 | return chkIdx >= curIdx
148 | }
149 | }
150 |
151 | function now(): number {
152 | if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
153 | return performance.now()
154 | }
155 | return Date.now()
156 | }
157 |
158 | function sanitizeFields(fields: LogFields): LogFields {
159 | const out: LogFields = {}
160 | for (const [k, v] of Object.entries(fields)) {
161 | if (v === undefined) continue
162 | if (v instanceof Error) {
163 | out[k] = {
164 | name: v.name,
165 | message: v.message,
166 | stack: v.stack,
167 | }
168 | } else if (typeof v === 'object' && v !== null) {
169 | try {
170 | // Avoid circular structures
171 | out[k] = JSON.parse(JSON.stringify(v))
172 | } catch {
173 | out[k] = String(v)
174 | }
175 | } else {
176 | out[k] = v as any
177 | }
178 | }
179 | return out
180 | }
181 |
182 | function formatHuman(entry: { [k: string]: unknown }): string {
183 | const { ts, level, msg, ...rest } = entry as any
184 | const head = `[${String(level).toUpperCase()}] ${ts} ${msg}`
185 | const restKeys = Object.keys(rest)
186 | if (restKeys.length === 0) return head
187 | return `${head} ${safeStringify(rest)}`
188 | }
189 |
190 | function safeStringify(obj: any): string {
191 | try {
192 | return JSON.stringify(obj)
193 | } catch {
194 | return '[object]'
195 | }
196 | }
197 |
198 | function fieldsToLogFields(f?: LogFields | unknown): LogFields | undefined {
199 | if (!f) return undefined
200 | if (typeof f === 'object' && !(f instanceof Error)) return f as LogFields
201 | return { detail: f }
202 | }
203 |
```
--------------------------------------------------------------------------------
/src/server/master-server.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Phase 1: avoid hard dependency on SDK types to ensure compilation
2 | import type { ServerCapabilities, LoadedServer } from '../types/server.js'
3 | import type { MasterConfig, RoutingConfig, ServerConfig } from '../types/config.js'
4 | import type { AuthHeaders, OAuthDelegation } from '../types/auth.js'
5 | import { ProtocolHandler } from './protocol-handler.js'
6 | import { DefaultModuleLoader } from '../modules/module-loader.js'
7 | import { CapabilityAggregator } from '../modules/capability-aggregator.js'
8 | import { RequestRouter } from '../modules/request-router.js'
9 | import { Logger } from '../utils/logger.js'
10 | import { MultiAuthManager } from '../auth/multi-auth-manager.js'
11 | import { OAuthFlowController } from '../oauth/flow-controller.js'
12 |
13 | export class MasterServer {
14 | readonly server: unknown
15 | readonly handler: ProtocolHandler
16 |
17 | private readonly loader = new DefaultModuleLoader()
18 | private readonly aggregator = new CapabilityAggregator()
19 | private readonly servers = new Map<string, LoadedServer>()
20 | private router!: RequestRouter
21 | private config?: MasterConfig
22 | private authManager?: MultiAuthManager
23 | private oauthController?: OAuthFlowController
24 | private getAuthHeaders: (
25 | serverId: string,
26 | clientToken?: string
27 | ) => Promise<AuthHeaders | OAuthDelegation | undefined>
28 |
29 | constructor(capabilities?: Partial<ServerCapabilities>, routing?: RoutingConfig) {
30 | const version = (globalThis as any)?.process?.env?.APP_VERSION ?? '0.1.0'
31 | this.server = { name: 'master-mcp-server', version }
32 | this.getAuthHeaders = async (_serverId: string, clientToken?: string) =>
33 | clientToken ? { Authorization: `Bearer ${clientToken}` } : undefined
34 | this.router = new RequestRouter(this.servers, this.aggregator, this.getAuthHeaders.bind(this), { routing })
35 | this.handler = new ProtocolHandler({ aggregator: this.aggregator, router: this.router })
36 | void capabilities
37 | }
38 |
39 | async startFromConfig(config: MasterConfig, clientToken?: string): Promise<void> {
40 | Logger.info('Starting MasterServer from config')
41 | this.config = config
42 | await this.loadServers(config.servers, clientToken)
43 | await this.discoverAllCapabilities(clientToken)
44 | }
45 |
46 | async loadServers(servers: ServerConfig[], clientToken?: string): Promise<void> {
47 | Logger.info('Loading servers', { servers })
48 | const loaded = await this.loader.loadServers(servers, clientToken)
49 | Logger.info('Loaded servers', { loaded: Array.from(loaded.entries()) })
50 | this.servers.clear()
51 | for (const [id, s] of loaded) this.servers.set(id, s)
52 | this.router = new RequestRouter(this.servers, this.aggregator, this.getAuthHeaders.bind(this), {
53 | routing: this.config?.routing,
54 | })
55 | ;(this as any).handler = new ProtocolHandler({ aggregator: this.aggregator, router: this.router })
56 | }
57 |
58 | async discoverAllCapabilities(clientToken?: string): Promise<void> {
59 | Logger.info('Discovering all capabilities', { servers: Array.from(this.servers.entries()) })
60 | const headersOnly = async (serverId: string, token?: string) => {
61 | const res = await this.getAuthHeaders(serverId, token)
62 | if (res && (res as OAuthDelegation).type === 'oauth_delegation') {
63 | return token ? { Authorization: `Bearer ${token}` } : undefined
64 | }
65 | return res as AuthHeaders | undefined
66 | }
67 | await this.aggregator.discoverCapabilities(this.servers, clientToken, headersOnly)
68 | Logger.info('Discovered all capabilities', { tools: this.aggregator.getAllTools(this.servers), resources: this.aggregator.getAllResources(this.servers) })
69 | }
70 |
71 | // Allow host app to inject an auth header strategy (e.g., MultiAuthManager)
72 | setAuthHeaderProvider(
73 | fn: (serverId: string, clientToken?: string) => Promise<AuthHeaders | OAuthDelegation | undefined>
74 | ): void {
75 | this.getAuthHeaders = fn
76 | this.router = new RequestRouter(this.servers, this.aggregator, this.getAuthHeaders.bind(this), {
77 | routing: this.config?.routing,
78 | })
79 | ;(this as any).handler = new ProtocolHandler({ aggregator: this.aggregator, router: this.router })
80 | }
81 |
82 | getRouter(): RequestRouter {
83 | return this.router
84 | }
85 |
86 | getAggregatedTools(): ServerCapabilities['tools'] {
87 | return this.aggregator.getAllTools(this.servers)
88 | }
89 |
90 | getAggregatedResources(): ServerCapabilities['resources'] {
91 | return this.aggregator.getAllResources(this.servers)
92 | }
93 |
94 | async performHealthChecks(clientToken?: string): Promise<Record<string, boolean>> {
95 | const results: Record<string, boolean> = {}
96 | for (const [id, s] of this.servers) {
97 | results[id] = await this.loader.performHealthCheck(s, clientToken)
98 | }
99 | return results
100 | }
101 |
102 | async restartServer(id: string): Promise<void> {
103 | await this.loader.restartServer(id)
104 | }
105 |
106 | async unloadAll(): Promise<void> {
107 | await Promise.all(Array.from(this.servers.keys()).map((id) => this.loader.unload(id)))
108 | this.servers.clear()
109 | }
110 |
111 | attachAuthManager(manager: MultiAuthManager): void {
112 | this.authManager = manager
113 | this.setAuthHeaderProvider((serverId: string, clientToken?: string) => {
114 | if (!clientToken) return Promise.resolve(undefined)
115 | return this.authManager!.prepareAuthForBackend(serverId, clientToken)
116 | })
117 | }
118 |
119 | // Provide an OAuthFlowController wired to the current config and auth manager.
120 | // Host runtimes (Node/Workers) can use this to mount HTTP endpoints without coupling MasterServer to a specific HTTP framework.
121 | getOAuthFlowController(): OAuthFlowController {
122 | if (!this.config) throw new Error('MasterServer config not initialized')
123 | if (!this.authManager) throw new Error('Auth manager not attached')
124 | if (!this.oauthController) {
125 | this.oauthController = new OAuthFlowController(
126 | {
127 | getConfig: () => this.config!,
128 | storeDelegatedToken: async (clientToken, serverId, token) => {
129 | await this.authManager!.storeDelegatedToken(clientToken, serverId, token)
130 | },
131 | },
132 | '/oauth'
133 | )
134 | }
135 | return this.oauthController
136 | }
137 |
138 | updateRouting(routing?: RoutingConfig): void {
139 | this.router = new RequestRouter(this.servers, this.aggregator, this.getAuthHeaders.bind(this), { routing })
140 | ;(this as any).handler = new ProtocolHandler({ aggregator: this.aggregator, router: this.router })
141 | }
142 | }
143 |
```
--------------------------------------------------------------------------------
/tests/servers/test-streaming-both-complete.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import { spawn } from 'node:child_process'
4 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'
5 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
6 |
7 | async function startHttpServer() {
8 | console.log('Starting HTTP test server...')
9 |
10 | // Start the HTTP server as a background process
11 | const httpServer = spawn('node', ['examples/test-mcp-server.js'], {
12 | stdio: ['ignore', 'pipe', 'pipe'],
13 | env: { ...process.env, PORT: '3006' }
14 | })
15 |
16 | // Capture stdout and stderr
17 | httpServer.stdout.on('data', (data) => {
18 | console.log(`[HTTP Server] ${data.toString().trim()}`)
19 | })
20 |
21 | httpServer.stderr.on('data', (data) => {
22 | console.error(`[HTTP Server ERROR] ${data.toString().trim()}`)
23 | })
24 |
25 | // Wait for the server to start
26 | await new Promise((resolve, reject) => {
27 | let timeout = setTimeout(() => {
28 | reject(new Error('HTTP server startup timeout'))
29 | }, 5000)
30 |
31 | httpServer.stdout.on('data', (data) => {
32 | if (data.toString().includes('Test MCP server listening')) {
33 | clearTimeout(timeout)
34 | resolve()
35 | }
36 | })
37 | })
38 |
39 | return httpServer
40 | }
41 |
42 | async function startMasterServer() {
43 | console.log('Starting Master MCP server...')
44 |
45 | // Start the master server as a background process
46 | const masterServer = spawn('npm', ['run', 'dev'], {
47 | stdio: ['ignore', 'pipe', 'pipe'],
48 | cwd: process.cwd()
49 | })
50 |
51 | // Capture stdout and stderr
52 | masterServer.stdout.on('data', (data) => {
53 | const output = data.toString().trim()
54 | // Only log important messages to avoid too much output
55 | if (output.includes('Master MCP listening') || output.includes('error') || output.includes('ERROR')) {
56 | console.log(`[Master Server] ${output}`)
57 | }
58 | })
59 |
60 | masterServer.stderr.on('data', (data) => {
61 | console.error(`[Master Server ERROR] ${data.toString().trim()}`)
62 | })
63 |
64 | // Wait for the server to start
65 | await new Promise((resolve, reject) => {
66 | let timeout = setTimeout(() => {
67 | reject(new Error('Master server startup timeout'))
68 | }, 15000)
69 |
70 | masterServer.stdout.on('data', (data) => {
71 | if (data.toString().includes('Master MCP listening')) {
72 | clearTimeout(timeout)
73 | console.log('[Master Server] Server is ready!')
74 | resolve()
75 | }
76 | })
77 | })
78 |
79 | return masterServer
80 | }
81 |
82 | async function runStreamingTest() {
83 | try {
84 | console.log('Testing Master MCP Server with HTTP Streaming...')
85 |
86 | // Create a streamable HTTP transport to connect to our MCP server
87 | const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
88 |
89 | // Create the MCP client
90 | const client = new Client({
91 | name: 'master-mcp-streaming-test-client',
92 | version: '1.0.0'
93 | })
94 |
95 | // Initialize the client
96 | await client.connect(transport)
97 | console.log('✅ Server initialized with streaming transport')
98 | console.log('Server info:', client.getServerVersion())
99 | console.log('Server capabilities:', client.getServerCapabilities())
100 |
101 | // List tools using streaming
102 | console.log('\n--- Testing tools/list with streaming ---')
103 | const toolsResult = await client.listTools({})
104 | console.log('✅ tools/list successful with streaming')
105 | console.log('Number of tools:', toolsResult.tools.length)
106 | console.log('Tools:', toolsResult.tools.map(t => t.name))
107 |
108 | // Verify both servers are present
109 | const hasHttpTool = toolsResult.tools.some(t => t.name === 'test-server.echo')
110 | const hasStdioTool = toolsResult.tools.some(t => t.name === 'stdio-server.stdio-echo')
111 |
112 | if (hasHttpTool) {
113 | console.log('✅ HTTP server tool found')
114 | } else {
115 | console.log('❌ HTTP server tool not found')
116 | }
117 |
118 | if (hasStdioTool) {
119 | console.log('✅ STDIO server tool found')
120 | } else {
121 | console.log('❌ STDIO server tool not found')
122 | }
123 |
124 | // List resources using streaming
125 | console.log('\n--- Testing resources/list with streaming ---')
126 | const resourcesResult = await client.listResources({})
127 | console.log('✅ resources/list successful with streaming')
128 | console.log('Number of resources:', resourcesResult.resources.length)
129 | console.log('Resources:', resourcesResult.resources.map(r => r.uri))
130 |
131 | // Verify both servers are present
132 | const hasHttpResource = resourcesResult.resources.some(r => r.uri === 'test-server.test://example')
133 | const hasStdioResource = resourcesResult.resources.some(r => r.uri === 'stdio-server.stdio://example/resource')
134 |
135 | if (hasHttpResource) {
136 | console.log('✅ HTTP server resource found')
137 | } else {
138 | console.log('❌ HTTP server resource not found')
139 | }
140 |
141 | if (hasStdioResource) {
142 | console.log('✅ STDIO server resource found')
143 | } else {
144 | console.log('❌ STDIO server resource not found')
145 | }
146 |
147 | // Test ping
148 | console.log('\n--- Testing ping with streaming ---')
149 | const pingResult = await client.ping()
150 | console.log('✅ ping successful with streaming')
151 | console.log('Ping result:', pingResult)
152 |
153 | // Summary
154 | console.log('\n--- Test Summary ---')
155 | if (hasHttpTool && hasStdioTool && hasHttpResource && hasStdioResource) {
156 | console.log('🎉 All tests passed! Both HTTP and STDIO servers are working correctly.')
157 | } else {
158 | console.log('⚠️ Some tests failed. Check the output above for details.')
159 | }
160 |
161 | // Close the connection
162 | await client.close()
163 | console.log('\n✅ Disconnected from MCP server')
164 |
165 | } catch (error) {
166 | console.error('❌ Streaming test failed:', error)
167 | console.error('Error stack:', error.stack)
168 | }
169 | }
170 |
171 | async function main() {
172 | let httpServer, masterServer
173 |
174 | try {
175 | // Start the HTTP server
176 | httpServer = await startHttpServer()
177 |
178 | // Start the master server
179 | masterServer = await startMasterServer()
180 |
181 | // Wait a bit for discovery to happen
182 | console.log('Waiting for server discovery...')
183 | await new Promise(resolve => setTimeout(resolve, 3000))
184 |
185 | // Run the streaming test
186 | await runStreamingTest()
187 | } catch (error) {
188 | console.error('Test failed:', error)
189 | } finally {
190 | // Clean up: kill the servers
191 | if (httpServer) {
192 | console.log('Stopping HTTP server...')
193 | httpServer.kill()
194 | }
195 | if (masterServer) {
196 | console.log('Stopping Master server...')
197 | masterServer.kill()
198 | }
199 | }
200 | }
201 |
202 | // Run the test
203 | main()
```
--------------------------------------------------------------------------------
/src/config/schema-validator.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { MasterConfig } from '../types/config.js'
2 | import { Logger } from '../utils/logger.js'
3 |
4 | type JSONSchema = {
5 | $id?: string
6 | type?: string | string[]
7 | properties?: Record<string, JSONSchema>
8 | required?: string[]
9 | additionalProperties?: boolean
10 | enum?: unknown[]
11 | items?: JSONSchema
12 | format?: 'url' | 'secret' | 'integer'
13 | anyOf?: JSONSchema[]
14 | allOf?: JSONSchema[]
15 | description?: string
16 | }
17 |
18 | export interface SchemaValidationError {
19 | path: string
20 | message: string
21 | }
22 |
23 | export class SchemaValidator {
24 | // Lightweight JSON Schema validator supporting core features used by our config schema
25 | static async loadSchema(schemaPath?: string): Promise<JSONSchema | undefined> {
26 | if (!schemaPath) return defaultSchema
27 | try {
28 | const isNode = Boolean((globalThis as any)?.process?.versions?.node)
29 | if (!isNode) return defaultSchema
30 | const fs = await import('node:fs/promises')
31 | const raw = await fs.readFile(schemaPath, 'utf8')
32 | return JSON.parse(raw) as JSONSchema
33 | } catch (err) {
34 | Logger.warn(`Failed to read schema at ${schemaPath}; using built-in`, String(err))
35 | return defaultSchema
36 | }
37 | }
38 |
39 | static validate(config: unknown, schema: JSONSchema): { valid: boolean; errors: SchemaValidationError[] } {
40 | const errors: SchemaValidationError[] = []
41 | validateAgainst(config, schema, '', errors)
42 | return { valid: errors.length === 0, errors }
43 | }
44 |
45 | static assertValid<T = MasterConfig>(config: unknown, schema: JSONSchema): T {
46 | const { valid, errors } = this.validate(config, schema)
47 | if (!valid) {
48 | const msg = errors.map((e) => `${e.path || '<root>'}: ${e.message}`).join('\n')
49 | throw new Error(`Configuration validation failed:\n${msg}`)
50 | }
51 | return config as T
52 | }
53 | }
54 |
55 | function typeOf(val: unknown): string {
56 | if (Array.isArray(val)) return 'array'
57 | return typeof val
58 | }
59 |
60 | function validateAgainst(value: unknown, schema: JSONSchema, path: string, errors: SchemaValidationError[]): void {
61 | if (!schema) return
62 | // Type check
63 | if (schema.type) {
64 | const allowed = Array.isArray(schema.type) ? schema.type : [schema.type]
65 | const actual = typeOf(value)
66 | if (!allowed.includes(actual)) {
67 | errors.push({ path, message: `expected type ${allowed.join('|')}, got ${actual}` })
68 | return
69 | }
70 | }
71 |
72 | if (schema.enum && !schema.enum.includes(value)) {
73 | errors.push({ path, message: `must be one of ${schema.enum.join(', ')}` })
74 | }
75 |
76 | if (schema.format) {
77 | if (schema.format === 'url' && typeof value === 'string') {
78 | try {
79 | // eslint-disable-next-line no-new
80 | new URL(value)
81 | } catch {
82 | errors.push({ path, message: 'must be a valid URL' })
83 | }
84 | }
85 | if (schema.format === 'integer' && typeof value === 'number') {
86 | if (!Number.isInteger(value)) errors.push({ path, message: 'must be an integer' })
87 | }
88 | }
89 |
90 | if (schema.properties && value && typeof value === 'object' && !Array.isArray(value)) {
91 | const v = value as Record<string, unknown>
92 | const required = schema.required || []
93 | for (const r of required) {
94 | if (!(r in v)) errors.push({ path: join(path, r), message: 'is required' })
95 | }
96 | for (const [k, subschema] of Object.entries(schema.properties)) {
97 | if (k in v) validateAgainst(v[k], subschema, join(path, k), errors)
98 | }
99 | if (schema.additionalProperties === false) {
100 | for (const k of Object.keys(v)) {
101 | if (!schema.properties[k]) errors.push({ path: join(path, k), message: 'is not allowed' })
102 | }
103 | }
104 | }
105 |
106 | if (schema.items && Array.isArray(value)) {
107 | value.forEach((item, idx) => validateAgainst(item, schema.items!, join(path, String(idx)), errors))
108 | }
109 |
110 | if (schema.allOf) {
111 | for (const s of schema.allOf) validateAgainst(value, s, path, errors)
112 | }
113 | if (schema.anyOf) {
114 | const ok = schema.anyOf.some((s) => {
115 | const temp: SchemaValidationError[] = []
116 | validateAgainst(value, s, path, temp)
117 | return temp.length === 0
118 | })
119 | if (!ok) errors.push({ path, message: 'does not match any allowed schema' })
120 | }
121 | }
122 |
123 | function join(base: string, key: string): string {
124 | return base ? `${base}.${key}` : key
125 | }
126 |
127 | // Built-in fallback schema captures core fields and constraints.
128 | const defaultSchema: JSONSchema = {
129 | type: 'object',
130 | required: ['master_oauth', 'hosting', 'servers'],
131 | properties: {
132 | master_oauth: {
133 | type: 'object',
134 | required: ['authorization_endpoint', 'token_endpoint', 'client_id', 'redirect_uri', 'scopes'],
135 | properties: {
136 | issuer: { type: 'string' },
137 | authorization_endpoint: { type: 'string', format: 'url' },
138 | token_endpoint: { type: 'string', format: 'url' },
139 | jwks_uri: { type: 'string' },
140 | client_id: { type: 'string' },
141 | client_secret: { type: 'string' },
142 | redirect_uri: { type: 'string' },
143 | scopes: { type: 'array', items: { type: 'string' } },
144 | audience: { type: 'string' },
145 | },
146 | additionalProperties: true,
147 | },
148 | hosting: {
149 | type: 'object',
150 | required: ['platform'],
151 | properties: {
152 | platform: { type: 'string', enum: ['node', 'cloudflare-workers', 'koyeb', 'docker', 'unknown'] },
153 | port: { type: 'number', format: 'integer' },
154 | base_url: { type: 'string' },
155 | },
156 | additionalProperties: true,
157 | },
158 | logging: {
159 | type: 'object',
160 | properties: { level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] } },
161 | },
162 | routing: {
163 | type: 'object',
164 | properties: {
165 | loadBalancer: { type: 'object', properties: { strategy: { type: 'string' } }, additionalProperties: true },
166 | circuitBreaker: { type: 'object', additionalProperties: true },
167 | retry: { type: 'object', additionalProperties: true },
168 | },
169 | additionalProperties: true,
170 | },
171 | servers: {
172 | type: 'array',
173 | items: {
174 | type: 'object',
175 | required: ['id', 'type', 'auth_strategy', 'config'],
176 | properties: {
177 | id: { type: 'string' },
178 | type: { type: 'string', enum: ['git', 'npm', 'pypi', 'docker', 'local'] },
179 | url: { type: 'string' },
180 | package: { type: 'string' },
181 | version: { type: 'string' },
182 | branch: { type: 'string' },
183 | auth_strategy: {
184 | type: 'string',
185 | enum: ['master_oauth', 'delegate_oauth', 'bypass_auth', 'proxy_oauth'],
186 | },
187 | auth_config: { type: 'object', additionalProperties: true },
188 | config: {
189 | type: 'object',
190 | properties: {
191 | environment: { type: 'object', additionalProperties: true },
192 | args: { type: 'array', items: { type: 'string' } },
193 | port: { type: 'number', format: 'integer' },
194 | },
195 | additionalProperties: true,
196 | },
197 | },
198 | additionalProperties: true,
199 | },
200 | },
201 | },
202 | additionalProperties: true,
203 | }
204 |
```