This is page 2 of 8. Use http://codebase.md/jakedismo/master-mcp-server?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
--------------------------------------------------------------------------------
/tests/integration/oauth.callback-handler.test.ts:
--------------------------------------------------------------------------------
```typescript
import '../setup/test-setup.js'
import test from 'node:test'
import assert from 'node:assert/strict'
// Test imports one by one to isolate the issue
console.log('Importing PKCEManager...')
import { PKCEManager } from '../../src/oauth/pkce-manager.js'
console.log('Importing StateManager...')
import { StateManager } from '../../src/oauth/state-manager.js'
console.log('Importing createMockServer...')
import { createMockServer } from '../utils/mock-http.js'
console.log('Importing CallbackHandler...')
import { CallbackHandler } from '../../src/oauth/callback-handler.js'
console.log('All imports successful')
test.skip('CallbackHandler exchanges code and stores token', async () => {
const tokenSrv = await createMockServer([
{ method: 'POST', path: '/token', handler: (_req, body) => {
if (body.code === 'good') return { body: { access_token: 'AT', expires_in: 60, scope: 'openid' } }
return { status: 400, body: { error: 'bad code' } }
} },
])
try {
try {
const pkce = new PKCEManager()
const stateMgr = new StateManager()
const state = stateMgr.create({ provider: 'prov', serverId: 'srv', clientToken: 'CT', returnTo: '/done' })
const { verifier } = await pkce.generate(state)
// pkce manager consumes verifier on getVerifier(), which CallbackHandler will do
const cfg: any = {
master_oauth: { authorization_endpoint: tokenSrv.url + '/auth', token_endpoint: tokenSrv.url + '/token', client_id: 'cid', redirect_uri: tokenSrv.url + '/cb', scopes: ['openid'] },
hosting: { platform: 'node' },
servers: [],
}
let stored: any
const cb = new CallbackHandler({ config: cfg, stateManager: stateMgr, pkceManager: pkce, baseUrl: tokenSrv.url, storeDelegatedToken: async (ct, sid, tok) => { stored = { ct, sid, tok } } })
const res = await cb.handleCallback(new URLSearchParams({ state, code: 'good' }), { provider: 'custom', authorization_endpoint: tokenSrv.url + '/auth', token_endpoint: tokenSrv.url + '/token', client_id: 'cid' })
assert.ok(res.token)
assert.equal(stored.ct, 'CT')
assert.equal(stored.sid, 'srv')
assert.equal(stored.tok.access_token, 'AT')
} catch (error) {
console.error('Test error:', error)
throw error
}
} finally {
await tokenSrv.close()
}
})
```
--------------------------------------------------------------------------------
/src/oauth/flow-validator.ts:
--------------------------------------------------------------------------------
```typescript
import type { MasterConfig, ServerAuthConfig } from '../types/config.js'
export interface ProviderResolution {
providerId: string
serverId?: string
config: ServerAuthConfig
}
export class FlowValidator {
constructor(private readonly getConfig: () => MasterConfig) {}
resolveProvider(params: { provider?: string | null; serverId?: string | null }): ProviderResolution {
const cfg = this.getConfig()
const provider = params.provider ?? undefined
const serverId = params.serverId ?? undefined
if (!provider && !serverId) {
return {
providerId: 'master',
config: {
provider: 'custom',
authorization_endpoint: cfg.master_oauth.authorization_endpoint,
token_endpoint: cfg.master_oauth.token_endpoint,
client_id: cfg.master_oauth.client_id,
client_secret: cfg.master_oauth.client_secret,
scopes: cfg.master_oauth.scopes,
},
}
}
if (provider === 'master') {
return {
providerId: 'master',
config: {
provider: 'custom',
authorization_endpoint: cfg.master_oauth.authorization_endpoint,
token_endpoint: cfg.master_oauth.token_endpoint,
client_id: cfg.master_oauth.client_id,
client_secret: cfg.master_oauth.client_secret,
scopes: cfg.master_oauth.scopes,
},
}
}
if (serverId) {
const server = cfg.servers.find((s) => s.id === serverId)
if (!server || !server.auth_config) throw new Error('Unknown server or missing auth configuration')
return { providerId: provider ?? serverId, serverId, config: server.auth_config }
}
const pre = cfg.oauth_delegation?.providers?.[String(provider)]
if (!pre) throw new Error('Unknown provider')
return { providerId: String(provider), config: pre }
}
validateReturnTo(returnTo: string | null | undefined, baseUrl?: string): string | undefined {
if (!returnTo) return undefined
try {
// Allow relative paths only, or same-origin absolute if matches baseUrl
if (returnTo.startsWith('/')) return returnTo
if (baseUrl) {
const origin = new URL(baseUrl).origin
const u = new URL(returnTo)
if (u.origin === origin) return u.pathname + u.search + u.hash
}
return undefined
} catch {
return undefined
}
}
}
```
--------------------------------------------------------------------------------
/docs/guides/client-integration.md:
--------------------------------------------------------------------------------
```markdown
---
title: Client Integration
---
# Client Integration
Connect your MCP clients to the Master MCP Server and verify end-to-end flows.
> Note: The Master MCP Server exposes HTTP endpoints for tools and resources (e.g., `/mcp/tools/call`). Custom clients can integrate directly over HTTP. For GUI clients like Claude Desktop, support for HTTP/remote servers may vary by version. If direct HTTP is unsupported, consider a small bridge (stdio → HTTP) or use the Node runtime directly inside your app.
## Custom Clients (HTTP)
Use any HTTP-capable client. Examples below:
```bash
curl -s -H 'content-type: application/json' \
-X POST http://localhost:3000/mcp/tools/list -d '{"type":"list_tools"}'
```
Node (fetch):
```ts
import fetch from 'node-fetch'
const res = await fetch('http://localhost:3000/mcp/tools/call', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: 'Bearer YOUR_CLIENT_TOKEN' },
body: JSON.stringify({ name: 'search.query', arguments: { q: 'hello' } })
})
console.log(await res.json())
```
See also: Getting Started → Quick Start and the <ApiPlayground /> on the landing page.
## Claude Desktop (Guidance)
Claude Desktop supports MCP servers via configuration. The exact configuration and supported transports can change; consult the latest Claude Desktop documentation.
Two approaches:
- If your Claude Desktop version supports remote/HTTP MCP servers, configure it to point at your master base URL (e.g., `http://localhost:3000`) and include a bearer token if required.
- Otherwise, run a small stdio bridge that speaks MCP to the client and forwards requests to the master HTTP endpoints. The bridge should:
- Respond to tool/resource listing using the master’s `/mcp/*/list` endpoints
- Forward tool calls and resource reads to `/mcp/tools/call` and `/mcp/resources/read`
- Map names like `serverId.toolName` consistently
> Tip: Keep your bridge stateless. Let the master handle routing, retries, and auth strategies.
## Testing Connections
- Health: `GET /health` → `{ ok: true }`
- Capabilities: `GET /capabilities` → aggregated tools/resources
- Tools/Resources: use the POST endpoints under `/mcp/*`
## Troubleshooting
- 401/403: ensure your Authorization header is present and matches backend expectations.
- Missing tools/resources: confirm the backend servers are healthy and listed in config.
- Delegated OAuth required: follow the flow at `/oauth/authorize?server_id=<id>`.
```
--------------------------------------------------------------------------------
/tests/mocks/oauth/mock-oidc-provider.ts:
--------------------------------------------------------------------------------
```typescript
import { createTestServer } from '../../_utils/test-server.js'
export interface MockOidcOptions {
issuer?: string
clientId?: string
clientSecret?: string
scopes?: string[]
}
export async function startMockOidcProvider(opts?: MockOidcOptions): Promise<{
issuer: string
authorization_endpoint: string
token_endpoint: string
jwks_uri: string
stop: () => Promise<void>
}> {
const srv = await createTestServer()
const issuer = opts?.issuer ?? `${srv.url}`
const clientId = opts?.clientId ?? 'test-client'
const scopes = opts?.scopes ?? ['openid', 'profile']
const codeStore = new Map<string, { scope: string[] }>()
// OIDC Discovery
srv.register('GET', '/.well-known/openid-configuration', () => ({
body: {
issuer,
authorization_endpoint: `${issuer}/authorize`,
token_endpoint: `${issuer}/token`,
jwks_uri: `${issuer}/jwks.json`,
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
},
}))
// Simplified authorize: immediately redirects back with code + state
srv.register('GET', '/authorize', (_req, _raw) => {
const url = new URL(_req.url || '/', issuer)
const redirectUri = url.searchParams.get('redirect_uri') || ''
const state = url.searchParams.get('state') || ''
const scopeStr = url.searchParams.get('scope') || scopes.join(' ')
const code = `code_${Math.random().toString(36).slice(2)}`
codeStore.set(code, { scope: scopeStr.split(/[ ,]+/).filter(Boolean) })
return {
status: 302,
headers: { location: `${redirectUri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}` },
}
})
// Token endpoint
srv.register('POST', '/token', (_req, raw) => {
const params = new URLSearchParams(raw || '')
const code = params.get('code') || ''
const rec = codeStore.get(code)
if (!rec) return { status: 400, body: { error: 'invalid_grant' } }
// Minimal token response
return {
body: {
access_token: `at_${code}`,
token_type: 'bearer',
scope: rec.scope.join(' '),
expires_in: 3600,
},
}
})
// Static JWKS (not strictly needed for current code paths)
srv.register('GET', '/jwks.json', () => ({ body: { keys: [] } }))
return {
issuer,
authorization_endpoint: `${issuer}/authorize`,
token_endpoint: `${issuer}/token`,
jwks_uri: `${issuer}/jwks.json`,
stop: srv.close,
}
}
```
--------------------------------------------------------------------------------
/examples/test-mcp-server.js:
--------------------------------------------------------------------------------
```javascript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import express from 'express'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
// Create a simple test MCP server
const server = new McpServer({
name: 'test-mcp-server',
version: '1.0.0'
}, {
capabilities: {
tools: { listChanged: true },
resources: { listChanged: true }
}
})
// Register a simple tool
server.tool('echo', 'Echoes back the input', { message: { type: 'string' } }, async (args) => {
return {
content: [{
type: 'text',
text: `Echo: ${args.message}`
}]
}
})
// Register a simple resource
server.resource('test-resource', 'test://example', { description: 'A test resource' }, async () => {
return {
contents: [{
uri: 'test://example',
text: 'This is a test resource'
}]
}
})
// Create an Express app
const app = express()
app.use(express.json())
// Create the HTTP streaming transport
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless mode
enableJsonResponse: false, // Use SSE by default
enableDnsRebindingProtection: false
})
// Connect the server to the transport
await server.connect(transport)
// Handle MCP requests
app.post('/mcp', async (req, res) => {
try {
await transport.handleRequest(req, res, req.body)
} catch (error) {
console.error('MCP request handling failed', { error })
res.status(500).json({
error: 'Internal server error'
})
}
})
app.get('/mcp', async (req, res) => {
try {
await transport.handleRequest(req, res)
} catch (error) {
console.error('MCP request handling failed', { error })
res.status(500).json({
error: 'Internal server error'
})
}
})
// Expose the capabilities endpoint
app.get('/capabilities', (req, res) => {
res.json({
tools: [
{
name: 'echo',
description: 'Echoes back the input',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string'
}
},
required: ['message']
}
}
],
resources: [
{
uri: 'test://example',
name: 'test-resource',
description: 'A test resource'
}
]
})
})
// Start the server
const port = process.env.PORT || 3006
app.listen(port, () => {
console.log(`Test MCP server listening on http://localhost:${port}`)
})
```
--------------------------------------------------------------------------------
/tests/servers/test-mcp-client.js:
--------------------------------------------------------------------------------
```javascript
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
async function runTest() {
try {
console.log('Testing Master MCP Server...')
// Create a streamable HTTP transport to connect to our MCP server
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
// Create the MCP client
const client = new Client({
name: 'test-client',
version: '1.0.0'
})
// Initialize the client
await client.connect(transport)
console.log('✅ Server initialized')
console.log('Server info:', client.getServerVersion())
console.log('Server capabilities:', client.getServerCapabilities())
// List tools
console.log('\n--- Testing tools/list ---')
const toolsResult = await client.listTools({})
console.log('✅ tools/list successful')
console.log('Number of tools:', toolsResult.tools.length)
console.log('Tools:', toolsResult.tools.map(t => t.name))
// List resources
console.log('\n--- Testing resources/list ---')
const resourcesResult = await client.listResources({})
console.log('✅ resources/list successful')
console.log('Number of resources:', resourcesResult.resources.length)
console.log('Resources:', resourcesResult.resources.map(r => r.uri))
// Test the health endpoint
console.log('\n--- Testing health endpoint ---')
try {
const response = await fetch('http://localhost:3005/health')
const health = await response.json()
console.log('✅ Health endpoint successful')
console.log('Health status:', health)
} catch (error) {
console.log('⚠️ Health endpoint test failed:', error.message)
}
// Test the metrics endpoint
console.log('\n--- Testing metrics endpoint ---')
try {
const response = await fetch('http://localhost:3005/metrics')
const metrics = await response.json()
console.log('✅ Metrics endpoint successful')
console.log('Metrics:', metrics)
} catch (error) {
console.log('⚠️ Metrics endpoint test failed:', error.message)
}
// Close the connection
await client.close()
console.log('\n✅ Disconnected from MCP server')
console.log('\n🎉 All tests completed successfully!')
} catch (error) {
console.error('❌ Test failed:', error)
console.error('Error stack:', error.stack)
}
}
// Run the test
runTest()
```
--------------------------------------------------------------------------------
/config/schema.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/master-mcp-config.schema.json",
"type": "object",
"required": ["master_oauth", "hosting", "servers"],
"properties": {
"master_oauth": {
"type": "object",
"required": ["authorization_endpoint", "token_endpoint", "client_id", "redirect_uri", "scopes"],
"properties": {
"issuer": { "type": "string" },
"authorization_endpoint": { "type": "string" },
"token_endpoint": { "type": "string" },
"jwks_uri": { "type": "string" },
"client_id": { "type": "string" },
"client_secret": { "type": "string" },
"redirect_uri": { "type": "string" },
"scopes": { "type": "array", "items": { "type": "string" } },
"audience": { "type": "string" }
},
"additionalProperties": true
},
"hosting": {
"type": "object",
"required": ["platform", "base_url"],
"properties": {
"platform": { "type": "string", "enum": ["node", "cloudflare-workers", "koyeb", "docker", "unknown"] },
"port": { "type": "number" },
"base_url": { "type": "string" },
"storage_backend": { "type": "string" }
},
"additionalProperties": true
},
"logging": { "type": "object", "properties": { "level": { "type": "string", "enum": ["debug", "info", "warn", "error"] } } },
"routing": { "type": "object", "additionalProperties": true },
"security": { "type": "object", "additionalProperties": true },
"servers": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type", "auth_strategy", "config"],
"properties": {
"id": { "type": "string" },
"type": { "type": "string", "enum": ["git", "npm", "pypi", "docker", "local"] },
"url": { "type": "string" },
"package": { "type": "string" },
"version": { "type": "string" },
"branch": { "type": "string" },
"auth_strategy": { "type": "string", "enum": ["master_oauth", "delegate_oauth", "bypass_auth", "proxy_oauth"] },
"auth_config": { "type": "object", "additionalProperties": true },
"config": {
"type": "object",
"properties": {
"environment": { "type": "object", "additionalProperties": true },
"args": { "type": "array", "items": { "type": "string" } },
"port": { "type": "integer" }
},
"additionalProperties": true
}
},
"additionalProperties": true
}
}
},
"additionalProperties": true
}
```
--------------------------------------------------------------------------------
/src/utils/string.ts:
--------------------------------------------------------------------------------
```typescript
/**
* String manipulation and parsing utilities.
*/
export function slugify(input: string): string {
return input
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)+/g, '')
}
export function toCamelCase(input: string): string {
return input
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
.replace(/^(.)/, (m) => m.toLowerCase())
}
export function toKebabCase(input: string): string {
return input
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.toLowerCase()
}
export function toSnakeCase(input: string): string {
return input
.replace(/([a-z])([A-Z])/g, '$1_$2')
.replace(/[\s-]+/g, '_')
.toLowerCase()
}
export function escapeHTML(input: string): string {
return input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
export function escapeRegExp(input: string): string {
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
export function joinUrlPaths(...parts: string[]): string {
const sanitized = parts.filter(Boolean).map((p) => p.replace(/(^\/+|\/+?$)/g, ''))
const joined = sanitized.join('/')
return `/${joined}`.replace(/\/+$/g, '') || '/'
}
export function trimSafe(input: unknown): string {
return String(input ?? '').trim()
}
export function truncateMiddle(input: string, maxLength: number): string {
if (input.length <= maxLength) return input
if (maxLength <= 3) return input.slice(0, maxLength)
const keep = Math.floor((maxLength - 3) / 2)
return `${input.slice(0, keep)}...${input.slice(-keep)}`
}
export function toBase64(input: string): string {
if (typeof btoa === 'function') return btoa(input)
return Buffer.from(input, 'utf8').toString('base64')
}
export function fromBase64(input: string): string {
if (typeof atob === 'function') return atob(input)
return Buffer.from(input, 'base64').toString('utf8')
}
export function stableJSONStringify(value: unknown): string {
return JSON.stringify(sortKeys(value))
}
function sortKeys(value: any): any {
if (Array.isArray(value)) return value.map(sortKeys)
if (value && typeof value === 'object') {
const out: Record<string, any> = {}
for (const key of Object.keys(value).sort()) out[key] = sortKeys(value[key])
return out
}
return value
}
export function parseBoolean(input: unknown, defaultValue = false): boolean {
const s = String(input ?? '').trim().toLowerCase()
if (s === 'true' || s === '1' || s === 'yes' || s === 'y') return true
if (s === 'false' || s === '0' || s === 'no' || s === 'n') return false
return defaultValue
}
export function normalizeUrl(input: string): string {
try {
const u = new URL(input)
u.hash = ''
return u.toString()
} catch {
return input
}
}
```
--------------------------------------------------------------------------------
/tests/integration/request-router.test.ts:
--------------------------------------------------------------------------------
```typescript
import '../setup/test-setup.js'
import test from 'node:test'
import assert from 'node:assert/strict'
import { RequestRouter } from '../../src/modules/request-router.js'
import { CapabilityAggregator } from '../../src/modules/capability-aggregator.js'
import { createMockServer } from '../utils/mock-http.js'
test('RequestRouter routes tool/resource with pass-through auth', async () => {
const sOk = await createMockServer([
{ method: 'POST', path: '/mcp/tools/call', handler: (_req, body) => ({ body: { content: { ok: true, args: body.arguments } } }) },
{ method: 'POST', path: '/mcp/resources/read', handler: (_req, body) => ({ body: { contents: `read:${body.uri}`, mimeType: 'text/plain' } }) },
])
try {
const servers = new Map<string, any>([[
's1', { id: 's1', type: 'node', endpoint: sOk.url, config: {} as any, status: 'running', lastHealthCheck: 0 }
]])
const rr = new RequestRouter(servers as any, new CapabilityAggregator(), async (_sid, token) => token ? ({ Authorization: `Bearer ${token}` }) : undefined)
const toolRes = await rr.routeCallTool({ name: 's1.echo', arguments: { a: 1 } }, 'CT')
assert.equal((toolRes as any).content.ok, true)
const readRes = await rr.routeReadResource({ uri: 's1.file' }, 'CT')
assert.equal((readRes as any).contents, 'read:file')
} finally {
await sOk.close()
}
})
test('RequestRouter returns delegation error when provider requires', async () => {
const s = await createMockServer([
{ method: 'POST', path: '/mcp/tools/call', handler: (_req, _body) => ({ body: { content: { ok: true } } }) },
])
try {
const servers = new Map<string, any>([[ 's1', { id: 's1', type: 'node', endpoint: s.url, config: {} as any, status: 'running', lastHealthCheck: 0 } ]])
const rr = new RequestRouter(servers as any, new CapabilityAggregator(), async () => ({ type: 'oauth_delegation', auth_endpoint: 'x', token_endpoint: 'y', client_info: {}, required_scopes: [], redirect_after_auth: true } as any))
const res = await rr.routeCallTool({ name: 's1.x' }, 'CT')
assert.equal((res as any).isError, true)
} finally {
await s.close()
}
})
test('RequestRouter retries on transient failure and eventually succeeds', async () => {
let n = 0
const s = await createMockServer([
{ method: 'POST', path: '/mcp/tools/call', handler: () => {
n++
if (n < 3) return { status: 500, body: { error: 'boom' } }
return { body: { content: { ok: true } } }
} },
])
try {
const servers = new Map<string, any>([[ 's1', { id: 's1', type: 'node', endpoint: s.url, config: {} as any, status: 'running', lastHealthCheck: 0 } ]])
const rr = new RequestRouter(servers as any, new CapabilityAggregator())
const res = await rr.routeCallTool({ name: 's1.task' })
// @ts-ignore
assert.equal(res.content.ok, true)
} finally {
await s.close()
}
})
```
--------------------------------------------------------------------------------
/docs/.vitepress/theme/components/ApiPlayground.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="mcp-callout" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<label>Endpoint
<select v-model="endpoint" style="margin-left:8px">
<option value="tools.list">POST /mcp/tools/list</option>
<option value="tools.call">POST /mcp/tools/call</option>
<option value="resources.list">POST /mcp/resources/list</option>
<option value="resources.read">POST /mcp/resources/read</option>
</select>
</label>
<label>Base URL <input v-model="baseUrl" placeholder="http://localhost:3000" /></label>
<label>Token <input v-model="token" placeholder="optional bearer" /></label>
</div>
<div class="mcp-grid">
<div class="mcp-col-6">
<h4>Request Body</h4>
<textarea v-model="body" rows="10" style="width:100%;font-family:var(--vp-font-family-mono)"></textarea>
</div>
<div class="mcp-col-6">
<h4>curl</h4>
<pre><code>{{ curl }}</code></pre>
<h4 style="margin-top:10px">Node (fetch)</h4>
<pre><code class="language-ts">{{ node }}</code></pre>
</div>
</div>
<p style="color:var(--vp-c-text-2);font-size:.9rem;margin-top:6px">Note: This playground does not perform live requests in the docs site; copy commands to run locally.</p>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
const endpoint = ref<'tools.list'|'tools.call'|'resources.list'|'resources.read'>('tools.list')
const baseUrl = ref('http://localhost:3000')
const token = ref('')
const body = ref('')
const defaultBodies: Record<string, string> = {
'tools.list': JSON.stringify({ type: 'list_tools' }, null, 2),
'tools.call': JSON.stringify({ name: 'serverId.toolName', arguments: { query: 'hello' } }, null, 2),
'resources.list': JSON.stringify({ type: 'list_resources' }, null, 2),
'resources.read': JSON.stringify({ uri: 'serverId:resourceId' }, null, 2)
}
watch(endpoint, (v) => { body.value = defaultBodies[v] })
body.value = defaultBodies[endpoint.value]
const path = computed(() => {
switch (endpoint.value) {
case 'tools.list': return '/mcp/tools/list'
case 'tools.call': return '/mcp/tools/call'
case 'resources.list': return '/mcp/resources/list'
case 'resources.read': return '/mcp/resources/read'
}
})
const curl = computed(() => {
const headers = ["-H 'content-type: application/json'"]
if (token.value) headers.push(`-H 'authorization: Bearer ${token.value}'`)
return `curl -s ${headers.join(' ')} -X POST ${baseUrl.value}${path.value} -d '${body.value.replace(/'/g, "'\\''")}'`
})
const node = computed(() => `import fetch from 'node-fetch'
const res = await fetch('${baseUrl.value}${path.value}', {
method: 'POST',
headers: {
'content-type': 'application/json'${token.value ? ",\n authorization: 'Bearer " + token.value + "'" : ''}
},
body: JSON.stringify(${body.value})
})
const data = await res.json()
console.log(data)
`)
</script>
```
--------------------------------------------------------------------------------
/src/oauth/pkce-manager.ts:
--------------------------------------------------------------------------------
```typescript
// PKCE (Proof Key for Code Exchange) manager with cross-platform support
// Generates code_verifier and S256 code_challenge and tracks them per state
export interface PkceRecord {
verifier: string
method: 'S256' | 'plain'
createdAt: number
expiresAt: number
}
export interface PkceManagerOptions {
ttlMs?: number
}
function getCrypto(): any {
const g: any = globalThis as any
if (g.crypto && g.crypto.subtle && g.crypto.getRandomValues) return g.crypto as any
// Node fallback
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodeCrypto = require('node:crypto')
return nodeCrypto.webcrypto as any
} catch {
throw new Error('Secure crypto not available in this environment')
}
}
function base64UrlEncode(bytes: Uint8Array): string {
let str = ''
for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i])
// btoa is available in browser/worker; Node 18 has global btoa via Buffer workaround
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const b64 = typeof btoa === 'function' ? btoa(str) : Buffer.from(bytes).toString('base64')
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
}
function randomString(length = 64): string {
const crypto = getCrypto()
const bytes = new Uint8Array(length)
crypto.getRandomValues(bytes)
// Allowed characters for verifier are [A-Z a-z 0-9 - . _ ~]
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'
let out = ''
for (let i = 0; i < length; i++) out += chars[bytes[i] % chars.length]
return out
}
export class PKCEManager {
private readonly store = new Map<string, PkceRecord>()
private readonly ttl: number
constructor(options?: PkceManagerOptions) {
this.ttl = options?.ttlMs ?? 10 * 60_000 // 10 minutes
}
async generate(state: string): Promise<{ challenge: string; method: 'S256'; verifier: string }> {
const verifier = randomString(64)
const challenge = await this.computeS256(verifier)
const now = Date.now()
this.store.set(state, {
verifier,
method: 'S256',
createdAt: now,
expiresAt: now + this.ttl,
})
return { challenge, method: 'S256', verifier }
}
getVerifier(state: string, consume = true): string | undefined {
const rec = this.store.get(state)
if (!rec) return undefined
if (rec.expiresAt <= Date.now()) {
this.store.delete(state)
return undefined
}
if (consume) this.store.delete(state)
return rec.verifier
}
cleanup(): void {
const now = Date.now()
for (const [k, v] of this.store) if (v.expiresAt <= now) this.store.delete(k)
}
private async computeS256(verifier: string): Promise<string> {
const crypto = getCrypto()
const enc = new TextEncoder().encode(verifier)
const digest = await crypto.subtle.digest('SHA-256', enc)
return base64UrlEncode(new Uint8Array(digest))
}
}
```
--------------------------------------------------------------------------------
/scripts/generate-config-docs.ts:
--------------------------------------------------------------------------------
```typescript
/*
* Generates docs/configuration/reference.md from the built-in JSON schema
* and enriches with examples from examples/sample-configs.
*/
import { SchemaValidator } from '../src/config/schema-validator.js'
import { promises as fs } from 'node:fs'
import path from 'node:path'
type JSONSchema = any
async function main() {
const schema: JSONSchema | undefined = await SchemaValidator.loadSchema()
if (!schema) throw new Error('Unable to load configuration schema')
const examplesDir = path.resolve('examples/sample-configs')
const exampleFiles = await fs.readdir(examplesDir)
const exampleSnippets: string[] = []
for (const f of exampleFiles) {
if (!f.endsWith('.yaml') && !f.endsWith('.yml')) continue
const content = await fs.readFile(path.join(examplesDir, f), 'utf8')
exampleSnippets.push(`## Example: ${f}\n\n\u0060\u0060\u0060yaml\n${content}\n\u0060\u0060\u0060\n`)
}
const lines: string[] = []
lines.push('# Configuration Reference')
lines.push('')
lines.push('This reference is generated from the built-in JSON Schema used by the server to validate configuration.')
lines.push('')
lines.push('## Top-Level Fields')
lines.push('')
lines.push(renderObject(schema))
lines.push('')
lines.push('## Examples')
lines.push('')
lines.push(exampleSnippets.join('\n'))
const target = path.resolve('docs/configuration/reference.md')
const contents = await fs.readFile(target, 'utf8').catch(() => '')
const start = '<!-- GENERATED:BEGIN -->'
const end = '<!-- GENERATED:END -->'
const prefix = contents.split(start)[0] ?? ''
const suffix = contents.split(end)[1] ?? ''
const generated = `${start}\n\n${lines.join('\n')}\n\n${end}`
const next = `${prefix}${generated}${suffix}`
await fs.writeFile(target, next, 'utf8')
}
function renderObject(schema: JSONSchema, indent = 0, name?: string): string {
const pad = ' '.repeat(indent)
if (!schema || typeof schema !== 'object') return ''
let s = ''
if (schema.type === 'object' || schema.properties) {
const required = new Set<string>((schema.required || []) as string[])
for (const [key, value] of Object.entries(schema.properties || {})) {
const req = required.has(key) ? ' (required)' : ''
s += `${pad}- \`${name ? name + '.' : ''}${key}\`${req}${renderType(value)}\n`
if ((value as any).properties || (value as any).items) {
s += renderObject(value, indent + 1, name ? `${name}.${key}` : key)
}
}
}
if (schema.items) {
s += `${pad} - items:${renderType(schema.items)}\n`
s += renderObject(schema.items, indent + 2, name ? `${name}[]` : '[]')
}
return s
}
function renderType(schema: JSONSchema): string {
const t = Array.isArray(schema.type) ? schema.type.join('|') : schema.type
const enumVals = schema.enum ? `, enum: ${schema.enum.join(', ')}` : ''
const fmt = schema.format ? `, format: ${schema.format}` : ''
return t ? ` — type: ${t}${enumVals}${fmt}` : `${enumVals}${fmt}`
}
main().catch((err) => {
console.error(err)
process.exitCode = 1
})
```
--------------------------------------------------------------------------------
/src/server/dependency-container.ts:
--------------------------------------------------------------------------------
```typescript
import { ConfigManager } from './config-manager.js'
import { DefaultModuleLoader } from '../modules/module-loader.js'
import { CapabilityAggregator } from '../modules/capability-aggregator.js'
import { RequestRouter } from '../modules/request-router.js'
import { ProtocolHandler } from './protocol-handler.js'
import { MasterServer } from './master-server.js'
import { MultiAuthManager } from '../auth/multi-auth-manager.js'
import type { MasterConfig, ServerConfig } from '../types/config.js'
import { Logger } from '../utils/logger.js'
export class DependencyContainer {
readonly configManager: ConfigManager
readonly loader: DefaultModuleLoader
readonly aggregator: CapabilityAggregator
readonly master: MasterServer
readonly authManager: MultiAuthManager
readonly router: RequestRouter
readonly handler: ProtocolHandler
private config!: MasterConfig
constructor() {
this.configManager = new ConfigManager({ watch: true })
this.loader = new DefaultModuleLoader()
this.aggregator = new CapabilityAggregator()
// Create a temporary router for early wiring; will be replaced after config load
this.router = new RequestRouter(new Map(), this.aggregator)
this.master = new MasterServer(undefined, undefined)
// Temporarily construct auth manager with placeholder; will be replaced after config
this.authManager = new MultiAuthManager({
authorization_endpoint: 'about:blank',
token_endpoint: 'about:blank',
client_id: 'placeholder',
redirect_uri: 'about:blank',
scopes: ['openid'],
})
this.handler = this.master.handler
}
async initialize(clientToken?: string): Promise<void> {
this.config = await this.configManager.load()
// Recreate auth manager with real config
const auth = new MultiAuthManager(this.config.master_oauth)
this.registerServerAuth(auth, this.config.servers)
this.master.attachAuthManager(auth)
;(this as any).authManager = auth
// Load servers and discover capabilities
await this.master.startFromConfig(this.config, clientToken)
this.master.updateRouting(this.config.routing)
// Recreate router/handler references for easy access
;(this as any).router = this.master.getRouter()
;(this as any).handler = this.master.handler
// Watch for config changes and hot-reload
this.configManager.onChange(async (cfg) => {
try {
Logger.info('Applying updated configuration to MasterServer')
this.config = cfg
this.registerServerAuth(this.authManager, cfg.servers)
await this.master.loadServers(cfg.servers)
await this.master.discoverAllCapabilities()
this.master.updateRouting(cfg.routing)
} catch (err) {
Logger.warn('Failed to apply updated config', err)
}
})
}
getConfig(): MasterConfig {
return this.config
}
private registerServerAuth(manager: MultiAuthManager, servers: ServerConfig[]): void {
for (const s of servers) {
try {
manager.registerServerAuth(s.id, s.auth_strategy, s.auth_config)
} catch (err) {
Logger.warn(`Failed to register auth for server ${s.id}`, err)
}
}
}
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "master-mcp-server",
"version": "0.1.0",
"private": true,
"description": "Master MCP Server that aggregates multiple MCP servers behind a single endpoint.",
"license": "UNLICENSED",
"type": "module",
"engines": {
"node": ">=18.17"
},
"scripts": {
"prepare": "husky install || true",
"clean": "rimraf dist .turbo tsconfig.tsbuildinfo",
"typecheck": "tsc -p tsconfig.node.json --noEmit",
"build": "npm run build:node && npm run build:worker",
"build:node": "tsc -p tsconfig.node.json",
"build:worker": "tsc -p tsconfig.worker.json",
"dev": "node --loader ts-node/esm src/index.ts",
"dev:watch": "nodemon --watch src --ext ts,tsx,json --exec 'node --loader ts-node/esm src/index.ts'",
"start": "node dist/node/index.js",
"start:prod": "NODE_ENV=production node dist/node/index.js",
"lint": "eslint --config .eslintrc.cjs . --ext .ts,.tsx",
"format": "prettier --write .",
"test": "NODE_V8_COVERAGE=.coverage node --loader ts-node/esm --test",
"test:unit": "node --loader ts-node/esm --test tests/unit/**/*.test.ts",
"test:integration": "node --loader ts-node/esm --test tests/integration/**/*.test.ts",
"test:e2e": "node --loader ts-node/esm --test tests/e2e/**/*.test.ts",
"test:perf": "node --loader ts-node/esm --test tests/perf/**/*.test.ts",
"test:security": "node --loader ts-node/esm --test tests/security/**/*.test.ts",
"test:watch": "node --loader ts-node/esm --test --watch",
"test:mcp": "node --loader ts-node/esm test-master-mcp.js",
"test:streaming": "node --loader ts-node/esm tests/servers/test-streaming.js",
"test:streaming-both": "node --loader ts-node/esm tests/servers/test-streaming-both.js",
"test:streaming-both-full": "node --loader ts-node/esm tests/servers/test-streaming-both-full.js",
"test:streaming-both-simple": "node --loader ts-node/esm tests/servers/test-streaming-both-simple.js",
"test:streaming-both-complete": "node --loader ts-node/esm tests/servers/test-streaming-both-complete.js",
"docs:api": "typedoc",
"docs:config": "node --loader ts-node/esm scripts/generate-config-docs.ts",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"docs:pdf": "md-to-pdf docs/index.md --basedir docs",
"docs:all": "npm run docs:api && npm run docs:config && npm run docs:build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.3",
"dotenv": "^17.2.1",
"express": "5.1.0",
"jose": "6.0.12",
"node-fetch": "^3.3.2",
"yaml": "^2.5.0"
},
"devDependencies": {
"@types/express": "5.0.3",
"@types/node": "24.3.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"eslint": "9.33.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "^2.29.1",
"husky": "^9.1.6",
"md-to-pdf": "^5.2.4",
"nodemon": "^3.1.4",
"prettier": "^3.3.2",
"rimraf": "6.0.1",
"ts-node": "^10.9.2",
"typedoc": "^0.28.10",
"typedoc-plugin-markdown": "^4.8.1",
"typescript": "^5.5.4",
"vitepress": "^1.6.4"
},
"main": "index.js",
"keywords": [],
"author": ""
}
```
--------------------------------------------------------------------------------
/src/routing/route-registry.ts:
--------------------------------------------------------------------------------
```typescript
import type { LoadedServer, ServerInstance } from '../types/server.js'
import { Logger } from '../utils/logger.js'
import { CircuitBreaker } from './circuit-breaker.js'
import { LoadBalancer } from './load-balancer.js'
export interface RouteRegistryOptions {
// Mapping lifetime for cached routes (ms)
cacheTtlMs?: number
}
export interface RouteResolution {
serverId: string
instance: ServerInstance
}
export class RouteRegistry {
private readonly servers: Map<string, LoadedServer>
private readonly circuit: CircuitBreaker
private readonly lb: LoadBalancer
private readonly cacheTtl: number
private cache = new Map<string, { value: RouteResolution; expiresAt: number }>()
constructor(
servers: Map<string, LoadedServer>,
circuit: CircuitBreaker,
lb: LoadBalancer,
options?: RouteRegistryOptions
) {
this.servers = servers
this.circuit = circuit
this.lb = lb
this.cacheTtl = options?.cacheTtlMs ?? 5_000
}
updateServers(servers: Map<string, LoadedServer>): void {
// Shallow replace reference
;(this as any).servers = servers
this.cache.clear()
}
getInstances(serverId: string): ServerInstance[] {
const server = this.servers.get(serverId)
if (!server) return []
const instances = server.instances && server.instances.length
? server.instances
: (server.endpoint && server.endpoint !== 'unknown'
? [{ id: `${serverId}-primary`, url: server.endpoint, weight: 1, healthScore: server.status === 'running' ? 100 : 0 }]
: [])
return instances
}
resolve(serverId: string): RouteResolution | undefined {
const cached = this.cache.get(serverId)
const now = Date.now()
if (cached && cached.expiresAt > now) return cached.value
const instances = this.getInstances(serverId)
if (!instances.length) return undefined
// Filter by circuit breaker allowance
const allowed = instances.filter((i) => this.circuit.canExecute(this.key(serverId, i.id)).allowed)
const pool = allowed.length ? allowed : instances
const chosen = this.lb.select(serverId, pool)
if (!chosen) return undefined
const resolution: RouteResolution = { serverId, instance: chosen }
this.cache.set(serverId, { value: resolution, expiresAt: now + this.cacheTtl })
return resolution
}
markSuccess(serverId: string, instanceId: string): void {
const key = this.key(serverId, instanceId)
this.circuit.onSuccess(key)
this.bumpHealth(serverId, instanceId, +5)
}
markFailure(serverId: string, instanceId: string): void {
const key = this.key(serverId, instanceId)
this.circuit.onFailure(key)
this.bumpHealth(serverId, instanceId, -20)
}
private key(serverId: string, instanceId: string): string {
return `${serverId}::${instanceId}`
}
private bumpHealth(serverId: string, instanceId: string, delta: number): void {
const s = this.servers.get(serverId)
if (!s) return
const arr = s.instances
if (!arr) return
const inst = arr.find((i) => i.id === instanceId)
if (!inst) return
const prev = inst.healthScore ?? 50
const next = Math.max(0, Math.min(100, prev + delta))
inst.healthScore = next
Logger.debug('Instance health updated', { serverId, instanceId, healthScore: next })
}
}
```
--------------------------------------------------------------------------------
/src/oauth/callback-handler.ts:
--------------------------------------------------------------------------------
```typescript
import type { MasterConfig, ServerAuthConfig } from '../types/config.js'
import type { OAuthToken } from '../types/auth.js'
import { StateManager, type OAuthStatePayload } from './state-manager.js'
import { PKCEManager } from './pkce-manager.js'
export interface CallbackContext {
config: MasterConfig
stateManager: StateManager
pkceManager: PKCEManager
baseUrl: string
// Store token callback: serverId and clientToken must identify the storage key
storeDelegatedToken?: (clientToken: string, serverId: string, token: OAuthToken) => Promise<void>
}
function toOAuthToken(json: any): OAuthToken {
const expiresIn = 'expires_in' in json ? Number(json.expires_in) : 3600
const scope = Array.isArray(json.scope)
? (json.scope as string[])
: typeof json.scope === 'string'
? (json.scope as string).split(/[ ,]+/).filter(Boolean)
: []
return {
access_token: String(json.access_token),
refresh_token: json.refresh_token ? String(json.refresh_token) : undefined,
expires_at: Date.now() + expiresIn * 1000,
scope,
}
}
async function exchangeAuthorizationCode(
code: string,
cfg: ServerAuthConfig,
redirectUri: string,
codeVerifier: string
): Promise<OAuthToken> {
const body = new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: cfg.client_id,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
})
if (cfg.client_secret) body.set('client_secret', String(cfg.client_secret))
const res = await fetch(cfg.token_endpoint, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
body,
})
const text = await res.text()
if (!res.ok) throw new Error(`Token endpoint error ${res.status}: ${text}`)
let json: any
try {
json = JSON.parse(text)
} catch {
json = Object.fromEntries(new URLSearchParams(text))
}
return toOAuthToken(json)
}
export class CallbackHandler {
constructor(private readonly ctx: CallbackContext) {}
async handleCallback(params: URLSearchParams, providerConfig: ServerAuthConfig): Promise<{ token?: OAuthToken; error?: string; state?: OAuthStatePayload }>
{
const error = params.get('error')
if (error) {
const desc = params.get('error_description') ?? 'OAuth authorization failed'
return { error: `${error}: ${desc}` }
}
const stateStr = params.get('state')
const code = params.get('code')
if (!stateStr || !code) return { error: 'Missing state or code' }
const state = this.ctx.stateManager.consume(stateStr)
if (!state) return { error: 'Invalid or expired state' }
const verifier = this.ctx.pkceManager.getVerifier(stateStr)
if (!verifier) return { error: 'PKCE verification failed' }
const redirectUri = new URL('/oauth/callback', this.ctx.baseUrl).toString()
try {
const token = await exchangeAuthorizationCode(code, providerConfig, redirectUri, verifier)
// Store if we can identify a client + server context
if (state.clientToken && state.serverId && this.ctx.storeDelegatedToken) {
await this.ctx.storeDelegatedToken(state.clientToken, state.serverId, token)
}
return { token, state }
} catch (err: any) {
return { error: err?.message ?? 'Token exchange failed' }
}
}
}
```
--------------------------------------------------------------------------------
/docs/stdio-servers.md:
--------------------------------------------------------------------------------
```markdown
# STDIO Server Support
The Master MCP Server now supports STDIO-based MCP servers in addition to HTTP-based servers. This allows you to aggregate both network-based and locally-running MCP servers through a single endpoint.
## Configuration
To configure a STDIO server, use a `file://` URL in your server configuration:
```json
{
"servers": [
{
"id": "stdio-server",
"type": "local",
"url": "file://./path/to/your/stdio-mcp-server.cjs",
"auth_strategy": "bypass_auth",
"config": {
"environment": {},
"args": []
}
}
]
}
```
The Master MCP Server will automatically detect `file://` URLs and treat them as STDIO servers, starting them as child processes and communicating with them through stdin/stdout using JSON-RPC.
## How It Works
1. **Server Detection**: The Master MCP Server detects `file://` URLs and identifies them as STDIO servers
2. **Process Management**: STDIO servers are started as child processes
3. **Communication**: The Master MCP Server communicates with STDIO servers using JSON-RPC over stdin/stdout
4. **Capability Discovery**: The Master MCP Server discovers tools and resources from STDIO servers
5. **Request Routing**: Tool calls and resource reads are routed to the appropriate STDIO servers
## Benefits
- **Unified Interface**: Access both HTTP and STDIO servers through a single MCP endpoint
- **Process Isolation**: Each STDIO server runs in its own process for better isolation
- **Automatic Management**: The Master MCP Server handles process lifecycle management
- **Seamless Integration**: STDIO servers appear as regular MCP servers to clients
## Requirements
- STDIO servers must implement the MCP protocol using JSON-RPC over stdin/stdout
- STDIO servers should follow the MCP specification for initialization and capability discovery
- STDIO servers must be executable Node.js scripts (`.js` or `.cjs` files)
## Example STDIO Server
Here's a simple example of a STDIO server:
```javascript
// Simple STDIO server that implements the MCP protocol
// Save this as stdio-mcp-server.cjs and make it executable with chmod +x
```
process.stdin.on('data', async (data) => {
try {
const lines = data.toString().split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
const request = JSON.parse(line);
// Handle initialize request
if (request.method === 'initialize') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
protocolVersion: '2025-06-18',
capabilities: {
tools: { listChanged: true },
resources: { listChanged: true }
},
serverInfo: {
name: 'example-stdio-server',
version: '1.0.0'
}
}
};
process.stdout.write(JSON.stringify(response) + '\n');
}
// Handle other requests...
}
} catch (err) {
const errorResponse = {
jsonrpc: '2.0',
id: null,
error: {
code: -32700,
message: 'Parse error',
data: err.message
}
};
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
});
```
Make sure to make your STDIO server executable:
```bash
chmod +x ./path/to/your/stdio-mcp-server.cjs
```
```
--------------------------------------------------------------------------------
/src/routing/retry-handler.ts:
--------------------------------------------------------------------------------
```typescript
import { Logger } from '../utils/logger.js'
export type JitterMode = 'none' | 'full'
export interface RetryPolicy {
maxRetries: number
baseDelayMs: number
maxDelayMs: number
backoffFactor: number // multiplier per attempt
jitter: JitterMode
timeoutMs?: number // overall timeout budget (optional)
retryOn?: {
networkErrors?: boolean
httpStatuses?: number[]
httpStatusClasses?: Array<4 | 5> // 4=4xx,5=5xx
}
}
export interface RetryContext {
attempt: number
lastError?: unknown
lastStatus?: number
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function withJitter(base: number, mode: JitterMode): number {
if (mode === 'none') return base
// Full jitter: random between 0 and base
return Math.floor(Math.random() * base)
}
function isRetryable(policy: RetryPolicy, _err: unknown, status?: number): boolean {
if (status !== undefined) {
if (policy.retryOn?.httpStatuses?.includes(status)) return true
const klass = Math.floor(status / 100)
if (klass === 5 && policy.retryOn?.httpStatusClasses?.includes(5)) return true
if (klass === 4 && policy.retryOn?.httpStatusClasses?.includes(4)) return status === 408 || status === 429
return false
}
// No status means a network error or thrown exception from fetch
return Boolean(policy.retryOn?.networkErrors ?? true)
}
export class RetryHandler {
private readonly policy: RetryPolicy
constructor(policy?: Partial<RetryPolicy>) {
this.policy = {
maxRetries: policy?.maxRetries ?? 3,
baseDelayMs: policy?.baseDelayMs ?? 200,
maxDelayMs: policy?.maxDelayMs ?? 5_000,
backoffFactor: policy?.backoffFactor ?? 2,
jitter: policy?.jitter ?? 'full',
timeoutMs: policy?.timeoutMs,
retryOn: policy?.retryOn ?? { networkErrors: true, httpStatusClasses: [5], httpStatuses: [408, 429] },
}
}
async execute<T>(op: () => Promise<T>, onRetry?: (ctx: RetryContext) => void): Promise<T> {
const start = Date.now()
let delay = this.policy.baseDelayMs
let lastError: unknown
for (let attempt = 0; attempt <= this.policy.maxRetries; attempt++) {
try {
const result = await op()
return result
} catch (err: any) {
// If this looks like a fetch Response-like error, extract status
let status: number | undefined
if (err && typeof err === 'object' && 'status' in err && typeof (err as any).status === 'number') {
status = (err as any).status
}
if (attempt >= this.policy.maxRetries || !isRetryable(this.policy, err, status)) {
throw err
}
lastError = err
const ctx: RetryContext = { attempt, lastError, lastStatus: status }
try {
onRetry?.(ctx)
} catch { /* ignore */ }
if (this.policy.timeoutMs && Date.now() - start + delay > this.policy.timeoutMs) {
Logger.warn('Retry timeout budget exceeded')
throw err
}
const wait = Math.min(this.policy.maxDelayMs, withJitter(delay, this.policy.jitter))
await sleep(wait)
delay = Math.min(this.policy.maxDelayMs, Math.floor(delay * this.policy.backoffFactor))
}
}
// Should be unreachable
throw lastError ?? new Error('RetryHandler failed without error')
}
}
```
--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------
```typescript
import type { OAuthDelegation } from './auth.js'
export interface MasterConfig {
master_oauth: MasterOAuthConfig
servers: ServerConfig[]
oauth_delegation?: OAuthDelegationConfig
hosting: HostingConfig
routing?: RoutingConfig
logging?: LoggingConfig
security?: SecurityConfig
}
export interface ServerConfig {
id: string
type: 'git' | 'npm' | 'pypi' | 'docker' | 'local'
url?: string
package?: string
version?: string
branch?: string
auth_strategy: AuthStrategy
auth_config?: ServerAuthConfig
config: {
environment?: Record<string, string>
args?: string[]
port?: number
}
}
export enum AuthStrategy {
MASTER_OAUTH = 'master_oauth',
DELEGATE_OAUTH = 'delegate_oauth',
BYPASS_AUTH = 'bypass_auth',
PROXY_OAUTH = 'proxy_oauth'
}
export interface MasterOAuthConfig {
issuer?: string
authorization_endpoint: string
token_endpoint: string
jwks_uri?: string
client_id: string
client_secret?: string
redirect_uri: string
scopes: string[]
audience?: string
}
// Alias used by MultiAuthManager constructor in later phases
export type MasterAuthConfig = MasterOAuthConfig
export interface OAuthDelegationConfig {
enabled: boolean
callback_base_url?: string
// Optional pre-configured providers by ID
providers?: Record<string, ServerAuthConfig>
}
export interface HostingConfig {
platform: 'node' | 'cloudflare-workers' | 'koyeb' | 'docker' | 'unknown'
port?: number
base_url?: string
// Optional platform-specific storage/backend hints
storage_backend?: 'memory' | 'fs' | 'durable_object' | 'kv' | 's3'
}
export interface LoggingConfig {
level?: 'debug' | 'info' | 'warn' | 'error'
}
export interface SecurityConfig {
// Env var name containing encryption key for config secrets
config_key_env?: string
// Enable audit logging for config changes
audit?: boolean
// Optional secret rotation policy in days
rotation_days?: number
}
export interface ServerAuthConfig {
provider: 'github' | 'google' | 'custom'
authorization_endpoint: string
token_endpoint: string
client_id: string
client_secret?: string
scopes?: string[]
// Additional provider-specific fields
[key: string]: unknown
}
// Re-export for convenience in consumers
export type { OAuthDelegation }
// ---- Routing configuration ----
export type LoadBalancingStrategy = 'round_robin' | 'weighted' | 'health'
export interface LoadBalancerConfig {
strategy?: LoadBalancingStrategy
}
export interface CircuitBreakerConfig {
failureThreshold?: number
successThreshold?: number
recoveryTimeoutMs?: number
}
export interface RetryPolicyConfig {
maxRetries?: number
baseDelayMs?: number
maxDelayMs?: number
backoffFactor?: number
jitter?: 'none' | 'full'
retryOn?: {
networkErrors?: boolean
httpStatuses?: number[]
httpStatusClasses?: Array<4 | 5>
}
}
export interface RoutingConfig {
loadBalancer?: LoadBalancerConfig
circuitBreaker?: CircuitBreakerConfig
retry?: RetryPolicyConfig
}
// Defaults for consumers that want a baseline configuration
export const DefaultRoutingConfig: RoutingConfig = {
loadBalancer: { strategy: 'round_robin' },
circuitBreaker: { failureThreshold: 5, successThreshold: 2, recoveryTimeoutMs: 30_000 },
retry: { maxRetries: 2, baseDelayMs: 250, maxDelayMs: 4_000, backoffFactor: 2, jitter: 'full' },
}
export const DefaultHostingConfig: HostingConfig = {
platform: 'node',
port: 3000,
}
```
--------------------------------------------------------------------------------
/docs/guides/authentication.md:
--------------------------------------------------------------------------------
```markdown
# Authentication Guide
Master MCP Server supports multiple authentication strategies between the client (master) and each backend server.
## Strategies
- master_oauth: Pass the client token from the master directly to the backend.
- delegate_oauth: Instruct the client to complete an OAuth flow against the backend provider, then store a backend token.
- proxy_oauth: Use the master to refresh and proxy backend tokens, falling back to pass-through.
- bypass_auth: No auth headers are sent to the backend.
Configure per-server via `servers[].auth_strategy` and optional `servers[].auth_config`.
<AuthFlowDemo />
<CodeTabs :options="[
{ label: 'master_oauth', value: 'master' },
{ label: 'delegate_oauth', value: 'delegate' },
{ label: 'proxy_oauth', value: 'proxy' },
{ label: 'bypass_auth', value: 'bypass' }
]">
<template #master>
```yaml
servers:
- id: search
type: local
auth_strategy: master_oauth
config: { port: 4100 }
```
</template>
<template #delegate>
```yaml
servers:
- id: github-tools
type: local
auth_strategy: delegate_oauth
auth_config:
provider: github
authorization_endpoint: https://github.com/login/oauth/authorize
token_endpoint: https://github.com/login/oauth/access_token
client_id: ${GITHUB_CLIENT_ID}
client_secret: env:GITHUB_CLIENT_SECRET
scopes: [repo, read:user]
config: { port: 4010 }
```
</template>
<template #proxy>
```yaml
servers:
- id: internal
type: local
auth_strategy: proxy_oauth
auth_config:
token_source: env:INTERNAL_BACKEND_TOKEN
config: { port: 4200 }
```
</template>
<template #bypass>
```yaml
servers:
- id: public
type: local
auth_strategy: bypass_auth
config: { port: 4300 }
```
</template>
</CodeTabs>
```yaml
servers:
- id: github-tools
type: local
auth_strategy: delegate_oauth
auth_config:
provider: github
authorization_endpoint: https://github.com/login/oauth/authorize
token_endpoint: https://github.com/login/oauth/access_token
client_id: ${GITHUB_CLIENT_ID}
client_secret: env:GITHUB_CLIENT_SECRET
scopes: [repo, read:user]
config:
port: 4010
```
## Flow Overview
1) Client calls a tool/resource via master with `Authorization: Bearer <client_token>`.
2) Master determines server strategy via `MultiAuthManager`.
3) If delegation is required, master responds with `{ type: 'oauth_delegation', ... }` metadata.
4) Client opens `GET /oauth/authorize?server_id=<id>` to initiate the auth code + PKCE flow.
5) Redirect back to `GET /oauth/callback` stores the backend token (associated with client token + server id).
6) Retries to the backend now include `Authorization: Bearer <server_token>` as needed.
## Endpoints
- `GET /oauth/authorize` → Starts flow; query: `server_id`, optional `provider` if preconfigured.
- `GET /oauth/callback` → Exchanges code for token and stores it.
- `GET /oauth/success` + `GET /oauth/error` → Result pages.
These are mounted automatically in the Node runtime (`src/index.ts`) and can be used in Workers via `OAuthFlowController.handleRequest()`.
## Customizing Auth
Attach a custom `MultiAuthManager` instance to the `MasterServer`:
```ts
import { MasterServer } from '../src/server/master-server'
import { MultiAuthManager } from '../src/auth/multi-auth-manager'
const master = new MasterServer()
const auth = new MultiAuthManager(config.master_oauth)
auth.registerServerAuth('github-tools', 'delegate_oauth', {/* provider config */})
master.attachAuthManager(auth)
```
See `examples/custom-auth` for a working example.
```
--------------------------------------------------------------------------------
/src/utils/errors.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Standardized error types and helpers.
*/
export type ErrorSeverity = 'fatal' | 'error' | 'warn'
export interface SerializedError {
name: string
message: string
code?: string
status?: number
severity?: ErrorSeverity
details?: unknown
stack?: string
cause?: SerializedError
}
export class AppError extends Error {
code?: string
status?: number
severity: ErrorSeverity
details?: unknown
override cause?: unknown
constructor(message: string, opts?: { code?: string; status?: number; severity?: ErrorSeverity; details?: unknown; cause?: unknown }) {
super(message)
this.name = this.constructor.name
this.code = opts?.code
this.status = opts?.status
this.severity = opts?.severity ?? 'error'
this.details = opts?.details
this.cause = opts?.cause
}
toJSON(): SerializedError {
return serializeError(this)
}
}
export class ValidationError extends AppError {
constructor(message: string, details?: unknown) {
super(message, { code: 'VALIDATION_ERROR', status: 400, details, severity: 'warn' })
}
}
export class AuthError extends AppError {
constructor(message: string, details?: unknown) {
super(message, { code: 'AUTH_ERROR', status: 401, details })
}
}
export class PermissionError extends AppError {
constructor(message: string, details?: unknown) {
super(message, { code: 'FORBIDDEN', status: 403, details })
}
}
export class NotFoundError extends AppError {
constructor(message: string, details?: unknown) {
super(message, { code: 'NOT_FOUND', status: 404, details, severity: 'warn' })
}
}
export class RateLimitError extends AppError {
constructor(message: string, details?: unknown) {
super(message, { code: 'RATE_LIMITED', status: 429, details, severity: 'warn' })
}
}
export class ExternalServiceError extends AppError {
constructor(message: string, details?: unknown) {
super(message, { code: 'EXTERNAL_SERVICE_ERROR', status: 502, details })
}
}
export class CircuitBreakerError extends AppError {
constructor(message: string, details?: unknown) {
super(message, { code: 'CIRCUIT_OPEN', status: 503, details })
}
}
export function serializeError(err: unknown): SerializedError {
if (err instanceof AppError) {
return {
name: err.name,
message: err.message,
code: err.code,
status: err.status,
severity: err.severity,
details: err.details,
stack: err.stack,
cause: err.cause ? serializeError(err.cause as any) : undefined,
}
}
if (err instanceof Error) {
return {
name: err.name,
message: err.message,
stack: err.stack,
}
}
return { name: 'Error', message: String(err) }
}
export function deserializeError(obj: SerializedError): AppError {
const err = new AppError(obj.message, {
code: obj.code,
status: obj.status,
severity: obj.severity,
details: obj.details,
cause: obj.cause ? deserializeError(obj.cause) : undefined,
})
err.name = obj.name
err.stack = obj.stack
return err
}
export function withErrorContext<T>(fn: () => Promise<T>, context: Record<string, unknown>): Promise<T> {
return fn().catch((e) => {
if (e instanceof AppError) throw new AppError(e.message, { ...e, details: { ...(e.details as any), ...context } })
const err = new AppError('Unhandled error', { code: 'UNHANDLED', details: { cause: serializeError(e), ...context } })
throw err
})
}
export function stackTrace(e?: Error): string {
const err = e ?? new Error('stack')
return err.stack || ''
}
export function isAppError(e: unknown): e is AppError {
return e instanceof AppError
}
```
--------------------------------------------------------------------------------
/examples/custom-auth/index.ts:
--------------------------------------------------------------------------------
```typescript
import express from 'express'
import type { Request, Response } from 'express'
import { ConfigLoader } from '../../src/config/config-loader.js'
import { MasterServer } from '../../src/server/master-server.js'
import { MultiAuthManager } from '../../src/auth/multi-auth-manager.js'
import { CapabilityAggregator } from '../../src/modules/capability-aggregator.js'
import { collectSystemMetrics } from '../../src/utils/monitoring.js'
// A custom auth manager that adds an extra header for a specific backend
class CustomAuthManager extends MultiAuthManager {
override async handleMasterOAuth(serverId: string, clientToken: string) {
const base = await super.handleMasterOAuth(serverId, clientToken)
// Example: add a hint header for a particular backend
if (serverId === 'custom-proxy') {
return { ...base, 'X-Custom-Auth': 'enabled' }
}
return base
}
}
async function main() {
const configPath = process.env.MASTER_CONFIG_PATH || 'examples/custom-auth/config.yaml'
const cfg = await ConfigLoader.load({ path: configPath })
const master = new MasterServer()
const customAuth = new CustomAuthManager(cfg.master_oauth)
// Register per-server strategies from config
for (const s of cfg.servers) customAuth.registerServerAuth(s.id, s.auth_strategy as any, s.auth_config as any)
master.attachAuthManager(customAuth)
await master.startFromConfig(cfg)
const app = express()
app.use(express.json())
app.get('/health', (_req, res) => res.json({ ok: true }))
app.get('/metrics', (_req, res) => res.json({ ok: true, system: collectSystemMetrics() }))
// OAuth endpoints
master.getOAuthFlowController().registerExpress(app)
// Capabilities
app.get('/capabilities', (_req: Request, res: Response) => {
const agg = new CapabilityAggregator()
const caps = agg.aggregate(Array.from(master.getRouter().getServers().values()))
res.json(caps)
})
const getToken = (req: Request) => {
const h = req.headers['authorization'] || req.headers['Authorization']
return typeof h === 'string' && h.toLowerCase().startsWith('bearer ') ? h.slice(7) : undefined
}
// MCP endpoints
app.post('/mcp/tools/list', async (_req: Request, res: Response) => {
const handler = master.handler
const result = await handler.handleListTools({ type: 'list_tools' } as any)
res.json(result)
})
app.post('/mcp/tools/call', async (req: Request, res: Response) => {
const token = getToken(req)
const handler = new (master.handler.constructor as any)({
aggregator: (master as any).aggregator,
router: master.getRouter(),
getClientToken: () => token,
}) as typeof master.handler
const result = await handler.handleCallTool({ name: req.body?.name, arguments: req.body?.arguments ?? {} } as any)
res.json(result)
})
app.post('/mcp/resources/list', async (_req: Request, res: Response) => {
const handler = master.handler
const result = await handler.handleListResources({ type: 'list_resources' } as any)
res.json(result)
})
app.post('/mcp/resources/read', async (req: Request, res: Response) => {
const token = getToken(req)
const handler = new (master.handler.constructor as any)({
aggregator: (master as any).aggregator,
router: master.getRouter(),
getClientToken: () => token,
}) as typeof master.handler
const result = await handler.handleReadResource({ uri: req.body?.uri } as any)
res.json(result)
})
const port = cfg.hosting.port || 3000
app.listen(port, () => console.log(`Custom auth example on :${port}`))
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
```
--------------------------------------------------------------------------------
/tests/unit/routing/circuit-breaker.test.ts:
--------------------------------------------------------------------------------
```typescript
import { test } from 'node:test'
import assert from 'node:assert'
import { CircuitBreaker, InMemoryCircuitStorage } from '../../../src/routing/circuit-breaker.js'
test('CircuitBreaker onSuccess in closed state should only reset failures', async () => {
const storage = new InMemoryCircuitStorage()
const breaker = new CircuitBreaker({
failureThreshold: 2,
successThreshold: 2,
recoveryTimeoutMs: 100,
}, storage)
const key = 'test-key'
// Simulate some failures, but not enough to open the circuit
breaker.onFailure(key)
let record = storage.get(key)
assert.strictEqual(record?.failures, 1, 'should have 1 failure')
assert.strictEqual(record?.state, 'closed', 'should be in closed state')
// Simulate a success
breaker.onSuccess(key)
record = storage.get(key)
assert.strictEqual(record?.failures, 0, 'failures should be reset to 0')
assert.strictEqual(record?.successes, 0, 'successes should remain 0') // This is the key check for the bug
assert.strictEqual(record?.state, 'closed', 'should remain in closed state')
})
test('CircuitBreaker should open after reaching failure threshold', async () => {
const storage = new InMemoryCircuitStorage()
const breaker = new CircuitBreaker({
failureThreshold: 2,
successThreshold: 2,
recoveryTimeoutMs: 100,
}, storage)
const key = 'test-key'
breaker.onFailure(key)
breaker.onFailure(key)
const record = storage.get(key)
assert.strictEqual(record?.state, 'open', 'should be in open state')
assert.strictEqual(record?.failures, 2, 'should have 2 failures')
})
test('CircuitBreaker should transition to half-open after recovery timeout', async () => {
const storage = new InMemoryCircuitStorage()
const breaker = new CircuitBreaker({
failureThreshold: 2,
successThreshold: 2,
recoveryTimeoutMs: 50,
}, storage)
const key = 'test-key'
// Open the circuit
breaker.onFailure(key)
breaker.onFailure(key)
// Wait for recovery timeout
await new Promise(resolve => setTimeout(resolve, 60))
// First call should be allowed and move to half-open
const gate = breaker.canExecute(key)
assert.strictEqual(gate.allowed, true, 'execution should be allowed')
const record = storage.get(key)
assert.strictEqual(record?.state, 'half_open', 'should be in half-open state')
})
test('CircuitBreaker should close after successes in half-open state', async () => {
const storage = new InMemoryCircuitStorage()
const breaker = new CircuitBreaker({
failureThreshold: 2,
successThreshold: 2,
recoveryTimeoutMs: 50,
}, storage)
const key = 'test-key'
// Open the circuit
breaker.onFailure(key)
breaker.onFailure(key)
// Wait for recovery timeout
await new Promise(resolve => setTimeout(resolve, 60))
// Transition to half-open
breaker.canExecute(key)
// Succeed twice
breaker.onSuccess(key)
breaker.onSuccess(key)
const record = storage.get(key)
assert.strictEqual(record?.state, 'closed', 'should be in closed state')
assert.strictEqual(record?.failures, 0, 'failures should be reset')
assert.strictEqual(record?.successes, 0, 'successes should be reset')
})
test('CircuitBreaker should re-open after failure in half-open state', async () => {
const storage = new InMemoryCircuitStorage()
const breaker = new CircuitBreaker({
failureThreshold: 2,
successThreshold: 2,
recoveryTimeoutMs: 50,
}, storage)
const key = 'test-key'
// Open the circuit
breaker.onFailure(key)
breaker.onFailure(key)
// Wait for recovery timeout
await new Promise(resolve => setTimeout(resolve, 60))
// Transition to half-open
breaker.canExecute(key)
// Fail once
breaker.onFailure(key)
const record = storage.get(key)
assert.strictEqual(record?.state, 'open', 'should be in open state again')
})
```
--------------------------------------------------------------------------------
/examples/test-stdio-server.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
// Simple test STDIO server for testing purposes
// Simple JSON-RPC server that echoes back requests
// Function to send a response
function sendResponse(response) {
process.stdout.write(JSON.stringify(response) + '\n');
}
// Function to send a notification
function sendNotification(notification) {
process.stdout.write(JSON.stringify(notification) + '\n');
}
// Handle incoming requests
process.stdin.on('data', (data) => {
try {
const lines = data.toString().split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.trim() === '') continue;
const request = JSON.parse(line);
// Handle initialize request
if (request.method === 'initialize') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
protocolVersion: '2025-06-18',
capabilities: {
tools: {
listChanged: true
},
resources: {
listChanged: true
}
},
serverInfo: {
name: 'test-stdio-server',
version: '1.0.0'
}
}
};
sendResponse(response);
}
// Handle tools/list request
else if (request.method === 'tools/list') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
tools: [
{
name: 'stdio-echo',
description: 'Echoes back the input',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string'
}
},
required: ['message']
}
}
]
}
};
sendResponse(response);
}
// Handle resources/list request
else if (request.method === 'resources/list') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
resources: [
{
uri: 'stdio://example/resource',
name: 'stdio-resource',
description: 'A test resource from STDIO server'
}
]
}
};
sendResponse(response);
}
// Handle tools/call request
else if (request.method === 'tools/call') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: `STDIO Echo: ${request.params.arguments?.message || 'No message'}`
}
]
}
};
sendResponse(response);
}
// Handle resources/read request
else if (request.method === 'resources/read') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
contents: [
{
uri: request.params.uri,
text: 'This is content from a STDIO server resource',
mimeType: 'text/plain'
}
]
}
};
sendResponse(response);
}
// Handle unknown methods
else {
const response = {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32601,
message: `Method not found: ${request.method}`
}
};
sendResponse(response);
}
}
} catch (err) {
// Send error response
const errorResponse = {
jsonrpc: '2.0',
id: null,
error: {
code: -32700,
message: 'Parse error',
data: err.message
}
};
sendResponse(errorResponse);
}
});
// Send a notification that the server is ready
sendNotification({
jsonrpc: '2.0',
method: 'notifications/initialized',
params: {}
});
console.error('Test STDIO server started');
```
--------------------------------------------------------------------------------
/src/utils/http.ts:
--------------------------------------------------------------------------------
```typescript
/**
* HTTP utilities for request/response handling, header manipulation, and content
* type parsing. Minimal and cross-platform (Node 18+ and Workers).
*/
export type HeadersLike = Headers | Record<string, string> | Array<[string, string]>
export function normalizeHeaders(h: HeadersLike | undefined | null): Record<string, string> {
const out: Record<string, string> = {}
if (!h) return out
if (typeof (h as any).forEach === 'function') {
;(h as Headers).forEach((v, k) => (out[k.toLowerCase()] = v))
return out
}
if (Array.isArray(h)) {
for (const [k, v] of h) out[k.toLowerCase()] = String(v)
return out
}
for (const [k, v] of Object.entries(h)) out[k.toLowerCase()] = String(v)
return out
}
export function getHeader(h: HeadersLike | undefined | null, name: string): string | undefined {
const map = normalizeHeaders(h)
return map[name.toLowerCase()]
}
export function setHeader(h: HeadersLike | undefined | null, name: string, value: string): HeadersLike {
if (!h) return { [name]: value }
if (typeof (h as any).set === 'function') {
const copy = new Headers(h as Headers)
copy.set(name, value)
return copy
}
const map = normalizeHeaders(h)
map[name.toLowerCase()] = value
return map
}
export function getContentType(h: HeadersLike | undefined | null): string | undefined {
return getHeader(h, 'content-type')
}
export function isJsonContentType(ct?: string): boolean {
if (!ct) return false
return /application\/json|\+json/i.test(ct)
}
export async function parseBody(req: Request, limitBytes = 1_000_000): Promise<any> {
const ct = getContentType(req.headers)
if (isJsonContentType(ct)) {
const text = await readTextLimited(req, limitBytes)
try {
return JSON.parse(text)
} catch (e) {
throw new Error('Invalid JSON body')
}
}
if (/text\//i.test(ct || '')) return readTextLimited(req, limitBytes)
// default to arrayBuffer
const buf = await req.arrayBuffer()
if (buf.byteLength > limitBytes) throw new Error('Body too large')
return buf
}
export async function readTextLimited(req: Request, limitBytes: number): Promise<string> {
const buf = await req.arrayBuffer()
if (buf.byteLength > limitBytes) throw new Error('Body too large')
return new TextDecoder().decode(buf)
}
export function jsonResponse(body: unknown, init?: ResponseInit): Response {
const headers = new Headers(init?.headers)
headers.set('content-type', 'application/json; charset=utf-8')
const payload = JSON.stringify(body)
return new Response(payload, { ...init, headers })
}
export function buildQuery(params: Record<string, string | number | boolean | null | undefined>): string {
const usp = new URLSearchParams()
for (const [k, v] of Object.entries(params)) {
if (v === undefined || v === null) continue
usp.append(k, String(v))
}
const s = usp.toString()
return s ? `?${s}` : ''
}
export function appendQuery(url: string, params: Record<string, string | number | boolean | null | undefined>): string {
const u = new URL(url, 'http://localhost')
for (const [k, v] of Object.entries(params)) {
if (v === undefined || v === null) continue
u.searchParams.set(k, String(v))
}
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
// Relative URL; return pathname+search only
return `${u.pathname}${u.search}`
}
return u.toString()
}
export function ensureCorrelationId(headers?: HeadersLike | null): string {
const existing = headers && getHeader(headers, 'x-correlation-id')
if (existing) return existing
return randomId()
}
export function randomId(): string {
const g: any = globalThis as any
try {
if (g.crypto?.randomUUID) return g.crypto.randomUUID()
} catch {
// ignore
}
try {
if (g.crypto?.getRandomValues) {
const bytes = new Uint8Array(16)
g.crypto.getRandomValues(bytes)
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
}
} catch {
// ignore
}
return Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2)
}
```
--------------------------------------------------------------------------------
/examples/stdio-mcp-server.cjs:
--------------------------------------------------------------------------------
```
#!/usr/bin/env node
// Simple test STDIO server for testing purposes
const fs = require('fs')
// Simple JSON-RPC server that echoes back requests
let messageId = 0
// Function to send a response
function sendResponse(response) {
process.stdout.write(JSON.stringify(response) + '\n')
}
// Function to send a notification
function sendNotification(notification) {
process.stdout.write(JSON.stringify(notification) + '\n')
}
// Handle incoming requests
process.stdin.on('data', (data) => {
try {
const lines = data.toString().split('\n').filter(line => line.trim() !== '')
for (const line of lines) {
if (line.trim() === '') continue
const request = JSON.parse(line)
// Handle initialize request
if (request.method === 'initialize') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
protocolVersion: '2025-06-18',
capabilities: {
tools: {
listChanged: true
},
resources: {
listChanged: true
}
},
serverInfo: {
name: 'test-stdio-server',
version: '1.0.0'
}
}
}
sendResponse(response)
}
// Handle tools/list request
else if (request.method === 'tools/list') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
tools: [
{
name: 'stdio-echo',
description: 'Echoes back the input',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string'
}
},
required: ['message']
}
}
]
}
}
sendResponse(response)
}
// Handle resources/list request
else if (request.method === 'resources/list') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
resources: [
{
uri: 'stdio://example/resource',
name: 'stdio-resource',
description: 'A test resource from STDIO server'
}
]
}
}
sendResponse(response)
}
// Handle tools/call request
else if (request.method === 'tools/call') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: `STDIO Echo: ${request.params.arguments?.message || 'No message'}`
}
]
}
}
sendResponse(response)
}
// Handle resources/read request
else if (request.method === 'resources/read') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
contents: [
{
uri: request.params.uri,
text: 'This is content from a STDIO server resource',
mimeType: 'text/plain'
}
]
}
}
sendResponse(response)
}
// Handle unknown methods
else {
const response = {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32601,
message: `Method not found: ${request.method}`
}
}
sendResponse(response)
}
}
} catch (err) {
// Send error response
const errorResponse = {
jsonrpc: '2.0',
id: null,
error: {
code: -32700,
message: 'Parse error',
data: err.message
}
}
sendResponse(errorResponse)
}
})
// Send a notification that the server is ready
sendNotification({
jsonrpc: '2.0',
method: 'notifications/initialized',
params: {}
})
console.error('Test STDIO server started')
```
--------------------------------------------------------------------------------
/src/modules/stdio-capability-discovery.ts:
--------------------------------------------------------------------------------
```typescript
import { Logger } from '../utils/logger.js'
import type { ServerCapabilities } from '../types/server.js'
import { StdioManager } from './stdio-manager.js'
export class StdioCapabilityDiscovery {
constructor(private stdioManager: StdioManager = new StdioManager()) {}
async discoverCapabilities(serverId: string, filePath: string): Promise<ServerCapabilities> {
Logger.info('Discovering capabilities for STDIO server', { serverId, filePath })
try {
// Start the STDIO server process
await this.stdioManager.startServer(serverId, filePath)
// Send initialize request
const initializeRequestId = Date.now()
const initializeRequest = {
jsonrpc: "2.0",
id: initializeRequestId,
method: "initialize",
params: {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: {
name: "master-mcp-server",
version: "1.0.0"
}
}
}
await this.stdioManager.sendMessage(serverId, initializeRequest)
const initializeResponse = await this.stdioManager.waitForResponse(serverId, initializeRequestId)
if (initializeResponse.error) {
throw new Error(`Failed to initialize STDIO server: ${initializeResponse.error.message}`)
}
// Send tools/list request
const toolsRequestId = Date.now() + 1
const toolsRequest = {
jsonrpc: "2.0",
id: toolsRequestId,
method: "tools/list",
params: {}
}
await this.stdioManager.sendMessage(serverId, toolsRequest)
const toolsResponse = await this.stdioManager.waitForResponse(serverId, toolsRequestId)
if (toolsResponse.error) {
throw new Error(`Failed to list tools from STDIO server: ${toolsResponse.error.message}`)
}
const tools = toolsResponse.result?.tools || []
// Send resources/list request
const resourcesRequestId = Date.now() + 2
const resourcesRequest = {
jsonrpc: "2.0",
id: resourcesRequestId,
method: "resources/list",
params: {}
}
await this.stdioManager.sendMessage(serverId, resourcesRequest)
const resourcesResponse = await this.stdioManager.waitForResponse(serverId, resourcesRequestId)
if (resourcesResponse.error) {
throw new Error(`Failed to list resources from STDIO server: ${resourcesResponse.error.message}`)
}
const resources = resourcesResponse.result?.resources || []
Logger.info('Discovered STDIO server capabilities', { serverId, tools: tools.length, resources: resources.length })
return {
tools,
resources
}
} catch (error) {
Logger.error('Failed to discover STDIO server capabilities', { serverId, error })
throw error
}
}
async callTool(serverId: string, toolName: string, args: any): Promise<any> {
try {
const toolRequestId = Date.now()
const toolRequest = {
jsonrpc: "2.0",
id: toolRequestId,
method: "tools/call",
params: {
name: toolName,
arguments: args
}
}
await this.stdioManager.sendMessage(serverId, toolRequest)
const toolResponse = await this.stdioManager.waitForResponse(serverId, toolRequestId)
if (toolResponse.error) {
throw new Error(`Failed to call tool ${toolName} on STDIO server: ${toolResponse.error.message}`)
}
return toolResponse
} catch (error) {
Logger.error('STDIO tool call failed', { serverId, toolName, error })
throw error
}
}
async readResource(serverId: string, uri: string): Promise<any> {
try {
const resourceRequestId = Date.now()
const resourceRequest = {
jsonrpc: "2.0",
id: resourceRequestId,
method: "resources/read",
params: {
uri
}
}
await this.stdioManager.sendMessage(serverId, resourceRequest)
const resourceResponse = await this.stdioManager.waitForResponse(serverId, resourceRequestId)
if (resourceResponse.error) {
throw new Error(`Failed to read resource ${uri} from STDIO server: ${resourceResponse.error.message}`)
}
return resourceResponse
} catch (error) {
Logger.error('STDIO resource read failed', { serverId, uri, error })
throw error
}
}
}
```
--------------------------------------------------------------------------------
/docs/.vitepress/theme/components/ConfigGenerator.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="mcp-grid" style="margin: 8px 0 16px; align-items: start;">
<div class="mcp-col-6">
<h3 style="margin:6px 0">Master Settings</h3>
<div class="mcp-callout">
<label>Port
<input v-model.number="state.port" type="number" min="1" max="65535" style="width:120px;margin-left:8px" />
</label>
<br />
<label style="margin-top:8px;display:block">Base URL
<input v-model="state.baseUrl" type="text" placeholder="https://your.domain" style="width:100%;margin-top:4px" />
</label>
<label style="margin-top:8px;display:block">Client Token (optional)
<input v-model="state.clientToken" type="text" placeholder="Bearer token for testing" style="width:100%;margin-top:4px" />
</label>
</div>
<h3 style="margin:12px 0 6px">Backends</h3>
<div v-for="(s, i) in state.servers" :key="i" class="mcp-callout">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<label>Id <input v-model="s.id" placeholder="search" /></label>
<label>Type
<select v-model="s.type">
<option>local</option>
<option>git</option>
<option>npm</option>
<option>docker</option>
<option>pypi</option>
</select>
</label>
<label v-if="s.type==='local'">Port <input v-model.number="s.config.port" type="number" min="1" max="65535" style="width:100px" /></label>
<label v-else>URL <input v-model="s.config.url" placeholder="http://host:port" /></label>
<label>Auth
<select v-model="s.auth_strategy">
<option>master_oauth</option>
<option>delegate_oauth</option>
<option>proxy_oauth</option>
<option>bypass_auth</option>
</select>
</label>
<button class="mcp-cta" style="margin-left:auto" @click="remove(i)">Remove</button>
</div>
</div>
<button class="mcp-cta" @click="add">Add Backend</button>
</div>
<div class="mcp-col-6">
<h3 style="margin:6px 0">Generated config.yaml</h3>
<div style="position:relative">
<button class="mcp-cta" style="position:absolute;right:8px;top:8px" @click="copyText(yaml)">Copy</button>
<pre><code class="language-yaml">{{ yaml }}</code></pre>
</div>
<h3 style="margin:12px 0 6px">config.json</h3>
<div style="position:relative">
<button class="mcp-cta" style="position:absolute;right:8px;top:8px" @click="copyText(json)">Copy</button>
<pre><code class="language-json">{{ json }}</code></pre>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive } from 'vue'
type Server = {
id: string
type: 'local' | 'git' | 'npm' | 'docker' | 'pypi'
auth_strategy: 'master_oauth' | 'delegate_oauth' | 'proxy_oauth' | 'bypass_auth'
config: { port?: number; url?: string }
}
const state = reactive({
port: 3000,
baseUrl: '',
clientToken: '',
servers: [
{ id: 'search', type: 'local', auth_strategy: 'master_oauth', config: { port: 4100 } } as Server,
],
})
function add() {
state.servers.push({ id: '', type: 'local', auth_strategy: 'master_oauth', config: {} })
}
function remove(i: number) {
state.servers.splice(i, 1)
}
const jsonObj = computed(() => ({
hosting: {
port: state.port,
base_url: state.baseUrl || undefined,
},
servers: state.servers.map(s => ({
id: s.id, type: s.type, auth_strategy: s.auth_strategy, config: s.config,
})),
}))
const json = computed(() => JSON.stringify(jsonObj.value, null, 2))
function toYaml(obj: any, indent = 0): string {
const pad = ' '.repeat(indent)
if (obj === null || obj === undefined) return ''
if (typeof obj !== 'object') return String(obj)
if (Array.isArray(obj)) {
return obj.map(v => `${pad}- ${typeof v === 'object' ? `\n${toYaml(v, indent + 1)}` : toYaml(v, indent)}`).join('\n')
}
return Object.entries(obj)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => {
if (v && typeof v === 'object') {
const nested = toYaml(v, indent + 1)
return `${pad}${k}:\n${nested}`
}
return `${pad}${k}: ${toYaml(v, 0)}`
})
.join('\n')
}
const yaml = computed(() => toYaml({
hosting: { port: state.port, base_url: state.baseUrl || undefined },
servers: jsonObj.value.servers,
}))
async function copyText(text: string) {
try {
await navigator.clipboard.writeText(text)
} catch (e) {
console.warn('Copy failed', e)
}
}
</script>
```
--------------------------------------------------------------------------------
/src/config/secret-manager.ts:
--------------------------------------------------------------------------------
```typescript
import { CryptoUtils } from '../utils/crypto.js'
import { Logger } from '../utils/logger.js'
export interface SecretManagerOptions {
// Name of env var that holds the encryption key used for configuration secrets
keyEnvVar?: string
// Optional explicit key value (discouraged in production)
key?: string
}
export class SecretManager {
private key: string
constructor(options?: SecretManagerOptions) {
const env = (globalThis as any)?.process?.env ?? {}
const provided = options?.key || env[options?.keyEnvVar || 'MASTER_CONFIG_KEY'] || env.MASTER_SECRET_KEY
const isProd = (env.NODE_ENV || env.MASTER_ENV) === 'production'
if (!provided) {
if (isProd) throw new Error('Missing MASTER_CONFIG_KEY for decrypting secrets in production')
Logger.warn('MASTER_CONFIG_KEY missing; using ephemeral key (dev only)')
this.key = CryptoUtils.generateSecureRandom(32)
} else {
this.key = String(provided)
}
}
getKey(): string {
return this.key
}
encrypt(value: string): string {
return `enc:gcm:${CryptoUtils.encrypt(value, this.key)}`
}
decrypt(value: string): string {
if (value.startsWith('enc:gcm:')) {
const raw = value.slice('enc:gcm:'.length)
return CryptoUtils.decrypt(raw, this.key)
}
return value
}
isEncrypted(value: string): boolean {
return typeof value === 'string' && value.indexOf('enc:gcm:') === 0
}
// Resolve secret placeholders within a config object
// - enc:gcm:<base64> → decrypted
// - env:VARNAME → process.env[VARNAME]
resolveSecrets<T>(obj: T): T {
const env = (globalThis as any)?.process?.env ?? {}
const visit = (v: any): any => {
if (typeof v === 'string') {
const vs: string = String(v)
if (this.isEncrypted(vs)) return this.decrypt(vs)
if (vs.slice(0, 4) === 'env:') return String(env[vs.slice(4)] ?? '')
return v
}
if (Array.isArray(v)) return v.map((x) => visit(x))
if (v && typeof v === 'object') {
const out: Record<string, unknown> = {}
for (const [k, vv] of Object.entries(v)) out[k] = visit(vv)
return out
}
return v
}
return visit(obj)
}
redact<T>(obj: T): T {
const secretKeyMatcher = /secret|token|password|key/i
const visit = (v: any, keyHint?: string): any => {
if (typeof v === 'string') {
const vs: string = String(v)
if (this.isEncrypted(vs)) return '***'
if (keyHint && secretKeyMatcher.test(keyHint)) return '***'
if (vs.slice(0, 4) === 'env:' && secretKeyMatcher.test(keyHint || '')) return '***'
return v
}
if (Array.isArray(v)) return v.map((x) => visit(x, keyHint))
if (v && typeof v === 'object') {
const out: Record<string, unknown> = {}
for (const [k, vv] of Object.entries(v)) out[k] = visit(vv, k)
return out
}
return v
}
return visit(obj)
}
rotate<T extends Record<string, unknown>>(obj: T, newKey: string, secretPaths?: string[]): T {
// Re-encrypt values under known secret paths
const prevKey = this.key
this.key = newKey
const result = structuredClone(obj)
const paths = secretPaths ?? inferSecretPaths(obj)
for (const p of paths) {
try {
const cur = getByPath(result, p)
if (typeof cur === 'string') {
const plain = this.isEncrypted(cur) ? CryptoUtils.decrypt(cur.slice('enc:gcm:'.length), prevKey) : cur
setByPath(result, p, this.encrypt(plain))
}
} catch (err) {
Logger.warn(`Failed to rotate secret at ${p}`, String(err))
}
}
return result
}
}
function getByPath(obj: any, path: string): unknown {
const parts = path.split('.')
let cur = obj
for (const p of parts) {
if (!cur || typeof cur !== 'object') return undefined
cur = cur[p]
}
return cur
}
function setByPath(obj: any, path: string, value: unknown): void {
const parts = path.split('.')
let cur = obj
for (let i = 0; i < parts.length - 1; i++) {
const p = parts[i]
if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {}
cur = cur[p]
}
cur[parts[parts.length - 1]] = value
}
function inferSecretPaths(obj: Record<string, unknown>, base = ''): string[] {
const out: string[] = []
for (const [k, v] of Object.entries(obj)) {
const p = base ? `${base}.${k}` : k
if (typeof v === 'string') {
if (/secret|token|password|key/i.test(k)) out.push(p)
else if (v.startsWith('enc:gcm:') || v.startsWith('env:')) out.push(p)
} else if (v && typeof v === 'object') {
out.push(...inferSecretPaths(v as Record<string, unknown>, p))
}
}
return out
}
```
--------------------------------------------------------------------------------
/src/mcp-server.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import type { Request, Response } from 'express'
import { Logger } from './utils/logger.js'
import { DependencyContainer } from './server/dependency-container.js'
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
// Create an MCP server with the official SDK
export async function createMcpServer(container: DependencyContainer): Promise<{
mcpServer: McpServer,
transport: StreamableHTTPServerTransport,
handleRequest: (req: Request, res: Response) => Promise<void>
}> {
// Create the MCP server with server info
const mcpServer = new McpServer({
name: 'master-mcp-server',
version: '0.1.0'
}, {
capabilities: {
tools: { listChanged: true },
resources: { listChanged: true },
prompts: { listChanged: true }
}
})
// Register tools from the aggregated servers BEFORE connecting to transport
const aggregatedTools = container.master.getAggregatedTools()
Logger.info('Aggregated tools', { count: aggregatedTools.length, tools: aggregatedTools.map(t => t.name) })
for (const tool of aggregatedTools) {
// Skip tools with names that might cause conflicts
if (tool.name.includes('..')) continue;
Logger.info('Registering tool', { name: tool.name, description: tool.description })
// Register the tool with the MCP server
mcpServer.tool(tool.name, tool.description ?? '', async (args) => {
try {
// Route the tool call to the appropriate backend server
const result = await container.master.handler.handleCallTool({
name: tool.name,
arguments: args
})
return result as CallToolResult
} catch (error) {
Logger.error('Tool execution failed', { tool: tool.name, error })
return {
content: [{
type: 'text',
text: `Error executing tool ${tool.name}: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
}
}
})
}
// Register resources from the aggregated servers BEFORE connecting to transport
const aggregatedResources = container.master.getAggregatedResources()
Logger.info('Aggregated resources', { count: aggregatedResources.length, resources: aggregatedResources.map(r => r.uri) })
for (const resource of aggregatedResources) {
// Skip resources with URIs that might cause conflicts
if (resource.uri.includes('..')) continue;
Logger.info('Registering resource', { name: resource.name, uri: resource.uri, description: resource.description })
mcpServer.resource(
resource.name ?? resource.uri,
resource.uri,
{
description: resource.description,
mimeType: resource.mimeType
},
async () => {
try {
// Route the resource read to the appropriate backend server
const result = await container.master.handler.handleReadResource({
uri: resource.uri
})
// Convert the result to the format expected by the MCP server
if (typeof result.contents === 'string') {
return {
contents: [{
uri: resource.uri,
text: result.contents,
mimeType: result.mimeType
}]
}
} else {
return {
contents: [{
uri: resource.uri,
blob: Buffer.from(result.contents).toString('base64'),
mimeType: result.mimeType
}]
}
}
} catch (error) {
Logger.error('Resource read failed', { resource: resource.uri, error })
throw new Error(`Error reading resource ${resource.uri}: ${error instanceof Error ? error.message : String(error)}`)
}
}
)
}
// Create the HTTP streaming transport in stateless mode
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless mode
enableJsonResponse: false, // Use SSE by default
enableDnsRebindingProtection: false
})
// Connect the server to the transport AFTER registering tools and resources
await mcpServer.connect(transport)
// Create a handler function for Express
const handleRequest = async (req: Request, res: Response) => {
try {
await transport.handleRequest(req, res, req.body)
} catch (error) {
Logger.error('MCP request handling failed', { error })
res.status(500).json({
error: 'Internal server error'
})
}
}
return { mcpServer, transport, handleRequest }
}
```
--------------------------------------------------------------------------------
/src/auth/token-manager.ts:
--------------------------------------------------------------------------------
```typescript
import type { OAuthToken } from '../types/auth.js'
import { CryptoUtils } from '../utils/crypto.js'
import { Logger } from '../utils/logger.js'
export interface TokenStorage {
set(key: string, value: string): Promise<void> | void
get(key: string): Promise<string | undefined> | string | undefined
delete(key: string): Promise<void> | void
entries(): AsyncIterable<[string, string]> | Iterable<[string, string]>
}
class InMemoryTokenStorage implements TokenStorage {
private map = new Map<string, string>()
set(key: string, value: string): void {
this.map.set(key, value)
}
get(key: string): string | undefined {
return this.map.get(key)
}
delete(key: string): void {
this.map.delete(key)
}
*entries(): Iterable<[string, string]> {
yield* this.map.entries()
}
}
export class TokenManager {
private readonly storage: TokenStorage
private readonly encKey: string
constructor(options?: { storage?: TokenStorage; secret?: string }) {
this.storage = options?.storage ?? autoDetectStorage()
const g: any = globalThis as any
const env = (g?.process?.env ?? g?.__WORKER_ENV ?? {}) as Record<string, string>
const provided = options?.secret ?? (env as any).TOKEN_ENC_KEY
if (!provided) {
const envName = ((g?.process?.env ?? (g?.__WORKER_ENV ?? {})) as any).NODE_ENV ?? 'development'
if (envName === 'production') {
throw new Error('TOKEN_ENC_KEY is required in production for secure token storage')
}
Logger.warn('TOKEN_ENC_KEY missing; generating ephemeral dev key (tokens won\'t persist across restarts)')
this.encKey = CryptoUtils.generateSecureRandom(32)
} else {
this.encKey = provided
}
}
async storeToken(key: string, token: OAuthToken): Promise<void> {
const serialized = JSON.stringify(token)
const encrypted = CryptoUtils.encrypt(serialized, this.encKey)
await this.storage.set(key, encrypted)
}
async getToken(key: string): Promise<OAuthToken | null> {
const encrypted = await this.storage.get(key)
if (!encrypted) return null
try {
const decrypted = CryptoUtils.decrypt(encrypted, this.encKey)
return JSON.parse(decrypted) as OAuthToken
} catch (err) {
Logger.error('Failed to decrypt token; deleting corrupted entry', { key, err: String(err) })
await this.storage.delete(key)
return null
}
}
async cleanupExpiredTokens(): Promise<void> {
const now = Date.now()
for await (const [k, v] of this.storage.entries() as AsyncIterable<[string, string]>) {
try {
const tok = JSON.parse(CryptoUtils.decrypt(v, this.encKey)) as OAuthToken
if (typeof tok.expires_at === 'number' && tok.expires_at <= now) {
await this.storage.delete(k)
}
} catch {
await this.storage.delete(k)
}
}
}
generateState(data: unknown): string {
const payload = JSON.stringify({ d: data, t: Date.now() })
return CryptoUtils.encrypt(payload, this.encKey)
}
validateState(state: string, expectedData: unknown): boolean {
try {
const payload = JSON.parse(CryptoUtils.decrypt(state, this.encKey)) as { d: unknown }
return JSON.stringify(payload.d) === JSON.stringify(expectedData)
} catch {
return false
}
}
}
export { InMemoryTokenStorage }
/**
* Auto-detects the best available storage backend.
* - Cloudflare Workers: KV namespace bound as `TOKENS`
* - Fallback: in-memory (non-persistent)
*/
function autoDetectStorage(): TokenStorage {
const g: any = globalThis as any
const env = g.__WORKER_ENV || {}
const kv = env.TOKENS || g.TOKENS || g.TOKENS_KV
if (kv && typeof kv.get === 'function' && typeof kv.put === 'function' && typeof kv.delete === 'function') {
return new KVTokenStorage(kv)
}
return new InMemoryTokenStorage()
}
class KVTokenStorage implements TokenStorage {
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 }[] }> }) {}
async set(key: string, value: string): Promise<void> {
await this.kv.put(key, value)
}
async get(key: string): Promise<string | undefined> {
const v = await this.kv.get(key)
return v === null ? undefined : v
}
async delete(key: string): Promise<void> {
await this.kv.delete(key)
}
async *entries(): AsyncIterable<[string, string]> {
if (typeof this.kv.list === 'function') {
const { keys } = await this.kv.list()
for (const k of keys) {
const v = await this.kv.get(k.name)
if (v !== null) yield [k.name, v]
}
} else {
// KV without list support: nothing to iterate
return
}
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import 'dotenv/config'
import express from 'express'
import type { Request, Response } from 'express'
import { DependencyContainer } from './server/dependency-container.js'
import { collectSystemMetrics } from './utils/monitoring.js'
import { CapabilityAggregator } from './modules/capability-aggregator.js'
import { createMcpServer } from './mcp-server.js'
export interface RunningServer {
name: string
version: string
container: DependencyContainer
stop: () => Promise<void>
}
function isNode(): boolean {
return Boolean((globalThis as any)?.process?.versions?.node)
}
export async function createServer(startHttp = true): Promise<RunningServer> {
const version = (globalThis as any)?.process?.env?.APP_VERSION ?? '0.1.0'
const container = new DependencyContainer()
await container.initialize()
const server: RunningServer = {
name: 'master-mcp-server',
version,
container,
stop: async () => {
try {
container.configManager.stop()
await container.master.unloadAll()
} catch {
// ignore
}
},
}
if (isNode() && startHttp) {
await startNodeHttp(container)
}
// Graceful shutdown (Node only)
if (isNode()) {
const onSig = async () => {
await server.stop()
;(process as any).exit?.(0)
}
process.on('SIGINT', onSig)
process.on('SIGTERM', onSig)
}
return server
}
async function startNodeHttp(container: DependencyContainer): Promise<void> {
const app = express()
app.use(express.json())
// Serve static assets for OAuth pages
// eslint-disable-next-line @typescript-eslint/no-var-requires
const expressStatic = (express as any).static
if (expressStatic) app.use('/static', expressStatic('static'))
const getToken = (req: Request): string | undefined => {
const h = req.headers['authorization'] || req.headers['Authorization']
if (typeof h === 'string' && h.toLowerCase().startsWith('bearer ')) return h.slice(7)
return undefined
}
app.get('/health', (_req, res) => res.json({ ok: true }))
app.get('/metrics', (_req, res) => {
try {
res.json({ ok: true, system: collectSystemMetrics() })
} catch {
res.json({ ok: true })
}
})
// Mount OAuth endpoints using the master server's controller
try {
container.master.getOAuthFlowController().registerExpress(app)
} catch {
// If not available yet, ignore; will be mounted on demand if needed
}
app.get('/capabilities', (_req, res) => {
const agg = new CapabilityAggregator()
const caps = agg.aggregate(Array.from(container.master.getRouter().getServers().values()))
res.json(caps)
})
// Create the MCP server with HTTP streaming transport
const { handleRequest } = await createMcpServer(container)
// Register MCP endpoints
app.post('/mcp', handleRequest)
app.get('/mcp', handleRequest)
app.delete('/mcp', handleRequest)
// Keep the existing endpoints for backward compatibility
app.post('/mcp/tools/list', async (_req: Request, res: Response) => {
const handler = container.master.handler
const result = await handler.handleListTools({ type: 'list_tools' })
res.json(result)
})
app.post('/mcp/tools/call', async (req: Request, res: Response) => {
const token = getToken(req)
const handler = new (container.master.handler.constructor as any)({
aggregator: container.aggregator,
router: container.master.getRouter(),
getClientToken: () => token,
}) as typeof container.master.handler
const result = await handler.handleCallTool({ name: req.body?.name, arguments: req.body?.arguments ?? {} })
res.json(result)
})
app.post('/mcp/resources/list', async (_req: Request, res: Response) => {
const handler = container.master.handler
const result = await handler.handleListResources({ type: 'list_resources' })
res.json(result)
})
app.post('/mcp/resources/read', async (req: Request, res: Response) => {
const token = getToken(req)
const handler = new (container.master.handler.constructor as any)({
aggregator: container.aggregator,
router: container.master.getRouter(),
getClientToken: () => token,
}) as typeof container.master.handler
const result = await handler.handleReadResource({ uri: req.body?.uri })
res.json(result)
})
const port = container.getConfig().hosting.port ?? 3000
await new Promise<void>((resolve) => {
app.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`Master MCP listening on http://localhost:${port}`)
resolve()
})
})
}
export default createServer
// If this file is being run directly (not imported), start the server
if (import.meta.url === `file://${process.argv[1]}`) {
createServer().catch((err) => {
console.error('Failed to start server:', err)
process.exit(1)
})
}
```
--------------------------------------------------------------------------------
/src/utils/monitoring.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Lightweight metrics, health checks, and profiling utilities.
*/
export type Labels = Record<string, string>
export class Counter {
private value = 0
inc(delta = 1): void {
this.value += delta
}
get(): number {
return this.value
}
}
export class Gauge {
private value = 0
set(v: number): void {
this.value = v
}
add(delta: number): void {
this.value += delta
}
get(): number {
return this.value
}
}
export class Histogram {
private readonly buckets: number[]
private counts: number[]
private sum = 0
constructor(buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]) {
this.buckets = [...buckets].sort((a, b) => a - b)
this.counts = Array(this.buckets.length + 1).fill(0)
}
observe(value: number): void {
this.sum += value
for (let i = 0; i < this.buckets.length; i++) {
if (value <= this.buckets[i]) {
this.counts[i]++
return
}
}
this.counts[this.counts.length - 1]++
}
snapshot(): { buckets: number[]; counts: number[]; sum: number } {
return { buckets: [...this.buckets], counts: [...this.counts], sum: this.sum }
}
}
export class MetricRegistry {
private counters = new Map<string, Counter>()
private gauges = new Map<string, Gauge>()
private histograms = new Map<string, Histogram>()
counter(name: string): Counter {
let c = this.counters.get(name)
if (!c) {
c = new Counter()
this.counters.set(name, c)
}
return c
}
gauge(name: string): Gauge {
let g = this.gauges.get(name)
if (!g) {
g = new Gauge()
this.gauges.set(name, g)
}
return g
}
histogram(name: string, buckets?: number[]): Histogram {
let h = this.histograms.get(name)
if (!h) {
h = new Histogram(buckets)
this.histograms.set(name, h)
}
return h
}
list(): { counters: Record<string, number>; gauges: Record<string, number>; histograms: Record<string, ReturnType<Histogram['snapshot']>> } {
const counters: Record<string, number> = {}
const gauges: Record<string, number> = {}
const histograms: Record<string, ReturnType<Histogram['snapshot']>> = {}
for (const [k, v] of this.counters.entries()) counters[k] = v.get()
for (const [k, v] of this.gauges.entries()) gauges[k] = v.get()
for (const [k, v] of this.histograms.entries()) histograms[k] = v.snapshot()
return { counters, gauges, histograms }
}
}
export class HealthCheckRegistry {
private checks = new Map<string, () => Promise<{ ok: boolean; info?: unknown }>>()
register(name: string, fn: () => Promise<{ ok: boolean; info?: unknown }>): void {
this.checks.set(name, fn)
}
unregister(name: string): void {
this.checks.delete(name)
}
async run(): Promise<{ status: 'ok' | 'degraded' | 'fail'; results: Record<string, { ok: boolean; info?: unknown }> }> {
const entries: [string, { ok: boolean; info?: unknown }][] = []
for (const [name, fn] of this.checks) {
try {
const res = await fn()
entries.push([name, res])
} catch (e) {
entries.push([name, { ok: false, info: e instanceof Error ? e.message : String(e) }])
}
}
const results = Object.fromEntries(entries)
const oks = entries.filter(([, r]) => r.ok).length
const status: 'ok' | 'degraded' | 'fail' = oks === entries.length ? 'ok' : oks > 0 ? 'degraded' : 'fail'
return { status, results }
}
}
/** Monitors event loop delay by scheduling microtasks. Returns a stop function. */
export function monitorEventLoopLag(callback: (lagMs: number) => void, intervalMs = 500): () => void {
let timer: any
let stopped = false
const tick = () => {
if (stopped) return
const start = now()
timer = setTimeout(() => {
const lag = now() - start - intervalMs
callback(Math.max(0, lag))
tick()
}, intervalMs)
}
tick()
return () => {
stopped = true
if (typeof clearTimeout === 'function' && timer) clearTimeout(timer)
}
}
export function now(): number {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') return performance.now()
return Date.now()
}
export function collectSystemMetrics(): Record<string, unknown> {
const g: any = globalThis as any
const out: Record<string, unknown> = { timestamp: new Date().toISOString() }
try {
if (g.process?.memoryUsage) {
const m = g.process.memoryUsage()
out.memory = {
rss: m.rss,
heapTotal: m.heapTotal,
heapUsed: m.heapUsed,
external: m.external,
}
}
if (g.os?.loadavg) {
const l = g.os.loadavg()
out.loadavg = { '1m': l[0], '5m': l[1], '15m': l[2] }
}
} catch {
// ignore
}
// Workers: best-effort
if (!out.memory && (performance as any)?.memory) {
out.memory = (performance as any).memory
}
return out
}
```
--------------------------------------------------------------------------------
/src/routing/circuit-breaker.ts:
--------------------------------------------------------------------------------
```typescript
import { Logger } from '../utils/logger.js'
export type CircuitState = 'closed' | 'open' | 'half_open'
export interface CircuitRecord {
state: CircuitState
failures: number
successes: number
nextTryAt: number // epoch ms when half-open trial is permitted
openedAt?: number
halfOpenInProgress?: boolean
}
export interface CircuitBreakerOptions {
failureThreshold: number // failures before opening circuit
successThreshold: number // successes in half-open before closing
recoveryTimeoutMs: number // time to wait before permitting a half-open trial
name?: string
}
export interface CircuitStorage {
get(key: string): CircuitRecord | undefined
set(key: string, value: CircuitRecord): void
delete?(key: string): void
}
export class InMemoryCircuitStorage implements CircuitStorage {
private readonly map = new Map<string, CircuitRecord>()
get(key: string): CircuitRecord | undefined {
return this.map.get(key)
}
set(key: string, value: CircuitRecord): void {
this.map.set(key, value)
}
delete(key: string): void {
this.map.delete(key)
}
}
export class CircuitOpenError extends Error {
readonly retryAfterMs?: number
constructor(message: string, retryAfterMs?: number) {
super(message)
this.name = 'CircuitOpenError'
this.retryAfterMs = retryAfterMs
}
}
export class CircuitBreaker {
private readonly storage: CircuitStorage
private readonly opts: Required<CircuitBreakerOptions>
constructor(options?: Partial<CircuitBreakerOptions>, storage?: CircuitStorage) {
this.opts = {
failureThreshold: options?.failureThreshold ?? 5,
successThreshold: options?.successThreshold ?? 2,
recoveryTimeoutMs: options?.recoveryTimeoutMs ?? 30_000,
name: options?.name ?? 'default',
}
this.storage = storage ?? new InMemoryCircuitStorage()
}
private now(): number {
return Date.now()
}
private initial(): CircuitRecord {
return { state: 'closed', failures: 0, successes: 0, nextTryAt: 0 }
}
private getRecord(key: string): CircuitRecord {
return this.storage.get(key) ?? this.initial()
}
canExecute(key: string): { allowed: boolean; state: CircuitState; retryAfterMs?: number } {
const rec = this.getRecord(key)
const now = this.now()
if (rec.state === 'open') {
if (now >= rec.nextTryAt && !rec.halfOpenInProgress) {
rec.state = 'half_open'
rec.halfOpenInProgress = true
rec.successes = 0
this.storage.set(key, rec)
return { allowed: true, state: rec.state }
}
return { allowed: false, state: 'open', retryAfterMs: Math.max(0, rec.nextTryAt - now) }
}
if (rec.state === 'half_open') {
// Only permit one in-flight trial at a time
if (rec.halfOpenInProgress) return { allowed: false, state: 'half_open', retryAfterMs: this.opts.recoveryTimeoutMs }
rec.halfOpenInProgress = true
this.storage.set(key, rec)
return { allowed: true, state: rec.state }
}
return { allowed: true, state: rec.state }
}
onSuccess(key: string): void {
const rec = this.getRecord(key)
if (rec.state === 'half_open') {
rec.successes += 1
rec.halfOpenInProgress = false
if (rec.successes >= this.opts.successThreshold) {
// Close the circuit after consecutive successes
this.storage.set(key, this.initial())
Logger.debug(`[Circuit] CLOSED after half-open successes`, { key, name: this.opts.name })
return
}
this.storage.set(key, rec)
return
}
// Closed state: reset failures on success
rec.failures = 0
this.storage.set(key, rec)
}
onFailure(key: string, _error?: unknown): void {
const rec = this.getRecord(key)
const now = this.now()
if (rec.state === 'half_open') {
// Failure during half-open => immediately open again
rec.state = 'open'
rec.failures = this.opts.failureThreshold
rec.successes = 0
rec.openedAt = now
rec.nextTryAt = now + this.opts.recoveryTimeoutMs
rec.halfOpenInProgress = false
this.storage.set(key, rec)
Logger.debug(`[Circuit] RE-OPEN after half-open failure`, { key, name: this.opts.name })
return
}
rec.failures += 1
if (rec.failures >= this.opts.failureThreshold) {
rec.state = 'open'
rec.openedAt = now
rec.nextTryAt = now + this.opts.recoveryTimeoutMs
this.storage.set(key, rec)
Logger.debug(`[Circuit] OPEN due to failures`, { key, name: this.opts.name, failures: rec.failures })
} else {
this.storage.set(key, rec)
}
}
async execute<T>(key: string, fn: () => Promise<T>): Promise<T> {
const gate = this.canExecute(key)
if (!gate.allowed) throw new CircuitOpenError('Circuit open', gate.retryAfterMs)
try {
const result = await fn()
this.onSuccess(key)
return result
} catch (err) {
this.onFailure(key, err)
throw err
}
}
}
```
--------------------------------------------------------------------------------
/tests/servers/test-streaming-both-simple.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import { spawn } from 'node:child_process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
async function startHttpServer() {
console.log('Starting HTTP test server...')
// Start the HTTP server as a background process
const httpServer = spawn('node', ['examples/test-mcp-server.js'], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, PORT: '3006' }
})
// Capture stdout and stderr
httpServer.stdout.on('data', (data) => {
console.log(`[HTTP Server] ${data.toString().trim()}`)
})
httpServer.stderr.on('data', (data) => {
console.error(`[HTTP Server ERROR] ${data.toString().trim()}`)
})
// Wait for the server to start
await new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
reject(new Error('HTTP server startup timeout'))
}, 5000)
httpServer.stdout.on('data', (data) => {
if (data.toString().includes('Test MCP server listening')) {
clearTimeout(timeout)
resolve()
}
})
})
return httpServer
}
async function runStreamingTest() {
try {
console.log('Testing Master MCP Server with HTTP Streaming...')
// Create a streamable HTTP transport to connect to our MCP server
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
// Create the MCP client
const client = new Client({
name: 'master-mcp-streaming-test-client',
version: '1.0.0'
})
// Initialize the client
await client.connect(transport)
console.log('✅ Server initialized with streaming transport')
console.log('Server info:', client.getServerVersion())
console.log('Server capabilities:', client.getServerCapabilities())
// List tools using streaming
console.log('\n--- Testing tools/list with streaming ---')
const toolsResult = await client.listTools({})
console.log('✅ tools/list successful with streaming')
console.log('Number of tools:', toolsResult.tools.length)
console.log('Tools:', toolsResult.tools.map(t => t.name))
// Verify both servers are present
const hasHttpTool = toolsResult.tools.some(t => t.name === 'test-server.echo')
const hasStdioTool = toolsResult.tools.some(t => t.name === 'stdio-server.stdio-echo')
if (hasHttpTool) {
console.log('✅ HTTP server tool found')
} else {
console.log('❌ HTTP server tool not found')
}
if (hasStdioTool) {
console.log('✅ STDIO server tool found')
} else {
console.log('❌ STDIO server tool not found')
}
// List resources using streaming
console.log('\n--- Testing resources/list with streaming ---')
const resourcesResult = await client.listResources({})
console.log('✅ resources/list successful with streaming')
console.log('Number of resources:', resourcesResult.resources.length)
console.log('Resources:', resourcesResult.resources.map(r => r.uri))
// Verify both servers are present
const hasHttpResource = resourcesResult.resources.some(r => r.uri === 'test-server.test://example')
const hasStdioResource = resourcesResult.resources.some(r => r.uri === 'stdio-server.stdio://example/resource')
if (hasHttpResource) {
console.log('✅ HTTP server resource found')
} else {
console.log('❌ HTTP server resource not found')
}
if (hasStdioResource) {
console.log('✅ STDIO server resource found')
} else {
console.log('❌ STDIO server resource not found')
}
// Test ping
console.log('\n--- Testing ping with streaming ---')
const pingResult = await client.ping()
console.log('✅ ping successful with streaming')
console.log('Ping result:', pingResult)
// Summary
console.log('\n--- Test Summary ---')
if (hasHttpTool && hasStdioTool && hasHttpResource && hasStdioResource) {
console.log('🎉 All tests passed! Both HTTP and STDIO servers are working correctly.')
} else {
console.log('⚠️ Some tests failed. Check the output above for details.')
}
// Close the connection
await client.close()
console.log('\n✅ Disconnected from MCP server')
} catch (error) {
console.error('❌ Streaming test failed:', error)
console.error('Error stack:', error.stack)
}
}
async function main() {
let httpServer
try {
// Start the HTTP server
httpServer = await startHttpServer()
// Wait a bit for the master server to discover the HTTP server
console.log('Waiting for server discovery...')
await new Promise(resolve => setTimeout(resolve, 3000))
// Run the streaming test
await runStreamingTest()
} catch (error) {
console.error('Test failed:', error)
} finally {
// Clean up: kill the HTTP server
if (httpServer) {
console.log('Stopping HTTP server...')
httpServer.kill()
}
}
}
// Run the test
main()
```
--------------------------------------------------------------------------------
/docs/.vitepress/theme/components/AuthFlowDemo.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="mcp-callout">
<label>Strategy
<select v-model="strategy" style="margin-left:8px">
<option value="master_oauth">master_oauth</option>
<option value="delegate_oauth">delegate_oauth</option>
<option value="proxy_oauth">proxy_oauth</option>
<option value="bypass_auth">bypass_auth</option>
</select>
</label>
</div>
<div class="mcp-grid" style="align-items:start">
<div class="mcp-col-6">
<h4 style="margin:8px 0">Flow</h4>
<ul>
<li v-for="(s, i) in flow.steps" :key="i">{{ s }}</li>
</ul>
<div class="mcp-callout" v-if="flow.note">{{ flow.note }}</div>
</div>
<div class="mcp-col-6">
<h4 style="margin:8px 0">Diagram</h4>
<div class="mcp-diagram">
<svg viewBox="0 0 600 240" width="100%" height="180">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="6" refY="3" orient="auto">
<path d="M0,0 L0,6 L6,3 z" fill="currentColor" />
</marker>
</defs>
<!-- Nodes -->
<rect x="30" y="30" width="140" height="40" rx="8" fill="none" stroke="currentColor" />
<text x="100" y="55" text-anchor="middle">Client</text>
<rect x="230" y="30" width="140" height="40" rx="8" fill="none" stroke="currentColor" />
<text x="300" y="55" text-anchor="middle">Master</text>
<rect x="430" y="30" width="140" height="40" rx="8" fill="none" stroke="currentColor" />
<text x="500" y="55" text-anchor="middle">Backend</text>
<!-- Arrows vary by strategy -->
<g v-if="strategy==='master_oauth'">
<path d="M170,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
<text x="200" y="40" text-anchor="middle">Bearer client_token</text>
<path d="M370,50 L430,50" stroke="currentColor" marker-end="url(#arrow)" />
<text x="400" y="40" text-anchor="middle">Bearer client_token</text>
</g>
<g v-else-if="strategy==='delegate_oauth'">
<path d="M170,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
<text x="200" y="40" text-anchor="middle">call tool</text>
<path d="M230,90 L120,160" stroke="currentColor" marker-end="url(#arrow)" />
<text x="170" y="130" text-anchor="middle">302 authorize</text>
<path d="M120,160 L430,50" stroke="currentColor" marker-end="url(#arrow)" />
<text x="275" y="120" text-anchor="middle">code + PKCE</text>
<path d="M430,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
<text x="330" y="40" text-anchor="middle">token stored</text>
</g>
<g v-else-if="strategy==='proxy_oauth'">
<path d="M170,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
<text x="200" y="40" text-anchor="middle">call tool</text>
<path d="M370,50 L430,50" stroke="currentColor" marker-end="url(#arrow)" />
<text x="400" y="40" text-anchor="middle">Bearer backend_token</text>
<path d="M300,70 L300,120 L430,120" stroke="currentColor" marker-end="url(#arrow)" />
<text x="360" y="110" text-anchor="middle">refresh if needed</text>
</g>
<g v-else>
<path d="M170,50 L230,50" stroke="currentColor" marker-end="url(#arrow)" />
<path d="M370,50 L430,50" stroke="currentColor" marker-end="url(#arrow)" />
<text x="400" y="40" text-anchor="middle">no auth header</text>
</g>
</svg>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
const strategy = ref<'master_oauth'|'delegate_oauth'|'proxy_oauth'|'bypass_auth'>('master_oauth')
const flow = computed(() => {
switch (strategy.value) {
case 'master_oauth':
return {
steps: [
'Client calls master with Authorization: Bearer <client_token>',
'Master forwards same token to backend',
'Backend validates token and serves the request'
],
note: 'Simple and effective when backends trust the same issuer/audience as the client.'
}
case 'delegate_oauth':
return {
steps: [
'Client calls master; master requires backend auth',
'Master responds with OAuth delegation metadata',
'Client completes provider auth via /oauth/authorize and callback',
'Master stores backend token and retries the call'
],
note: 'Use PKCE + state. Configure provider endpoints and client credentials.'
}
case 'proxy_oauth':
return {
steps: [
'Master manages backend tokens and refresh cycles',
'Requests include backend token; refresh on expiry',
'Fallback to delegation or pass-through as configured'
],
note: 'Centralizes token lifecycle with fewer client prompts.'
}
default:
return {
steps: [
'No authentication is added by master',
'Backend must not require Authorization for selected endpoints'
],
note: 'Use only for public or development backends.'
}
}
})
</script>
```
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from 'vitepress'
export default defineConfig({
title: 'Master MCP Server',
description: 'Aggregate and orchestrate multiple MCP servers behind one endpoint',
lastUpdated: true,
cleanUrls: true,
head: [
['meta', { name: 'theme-color', content: '#0ea5e9' }],
['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
],
themeConfig: {
logo: '/logo.svg',
siteTitle: 'Master MCP Server',
search: {
provider: 'local'
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/your-org/master-mcp-server' }
],
outline: [2, 6],
nav: [
{ text: 'Getting Started', link: '/getting-started/overview' },
{ text: 'Guides', link: '/guides/index' },
{ text: 'API', link: '/api/index' },
{ text: 'Configuration', link: '/configuration/overview' },
{ text: 'Deployment', link: '/deployment/index' },
{ text: 'Examples', link: '/examples/index' },
{ text: 'Advanced', link: '/advanced/index' },
{ text: 'Troubleshooting', link: '/troubleshooting/index' },
{ text: 'Contributing', link: '/contributing/index' }
],
sidebar: {
'/getting-started/': [
{
text: 'Getting Started',
items: [
{ text: 'Overview', link: '/getting-started/overview' },
{ text: 'Installation', link: '/getting-started/installation' },
{ text: 'Quick Start', link: '/getting-started/quick-start' },
{ text: 'Quickstart (Node)', link: '/getting-started/quickstart-node' },
{ text: 'Quickstart (Workers)', link: '/getting-started/quickstart-workers' },
{ text: 'Core Concepts', link: '/getting-started/concepts' }
]
}
],
'/guides/': [
{
text: 'User Guides',
items: [
{ text: 'Authentication', link: '/guides/authentication' },
{ text: 'OAuth Delegation', link: '/guides/oauth-delegation' },
{ text: 'Client Integration', link: '/guides/client-integration' },
{ text: 'Server Sharing', link: '/guides/server-sharing' },
{ text: 'Module Loading', link: '/guides/module-loading' },
{ text: 'Request Routing', link: '/guides/request-routing' },
{ text: 'Configuration', link: '/guides/configuration' },
{ text: 'Testing Strategy', link: '/guides/testing' }
]
}
],
'/api/': [
{
text: 'API Reference',
items: [
{ text: 'Overview', link: '/api/index' },
{ text: 'Types', link: '/api/reference/modules' }
]
}
],
'/configuration/': [
{
text: 'Configuration',
items: [
{ text: 'Overview', link: '/configuration/overview' },
{ text: 'Reference', link: '/configuration/reference' },
{ text: 'Examples', link: '/configuration/examples' },
{ text: 'Environment Variables', link: '/configuration/environment' }
]
}
],
'/deployment/': [
{
text: 'Deployment',
items: [
{ text: 'Overview', link: '/deployment/index' },
{ text: 'Docker', link: '/deployment/docker' },
{ text: 'Cloudflare Workers', link: '/deployment/cloudflare-workers' },
{ text: 'Koyeb', link: '/deployment/koyeb' },
{ text: 'Docs Site', link: '/deployment/docs-site' }
]
}
],
'/examples/': [
{
text: 'Examples',
items: [
{ text: 'Index', link: '/examples/index' },
{ text: 'Basic Node Aggregator', link: '/examples/basic-node' },
{ text: 'Cloudflare Worker', link: '/examples/cloudflare-worker' },
{ text: 'Advanced Routing', link: '/examples/advanced-routing' },
{ text: 'OAuth Delegation', link: '/examples/oauth-delegation' },
{ text: 'Testing Patterns', link: '/examples/testing' }
]
}
],
'/advanced/': [
{
text: 'Advanced Topics',
items: [
{ text: 'Security Hardening', link: '/advanced/security' },
{ text: 'Performance & Scalability', link: '/advanced/performance' },
{ text: 'Monitoring & Logging', link: '/advanced/monitoring' },
{ text: 'Extensibility & Plugins', link: '/advanced/extensibility' }
]
}
],
'/troubleshooting/': [
{
text: 'Troubleshooting',
items: [
{ text: 'Common Issues', link: '/troubleshooting/index' },
{ text: 'OAuth & Tokens', link: '/troubleshooting/oauth' },
{ text: 'Routing & Modules', link: '/troubleshooting/routing' },
{ text: 'Deployment', link: '/troubleshooting/deployment' }
]
}
],
'/contributing/': [
{
text: 'Contributing',
items: [
{ text: 'Overview', link: '/contributing/index' },
{ text: 'Development Setup', link: '/contributing/dev-setup' },
{ text: 'Coding & Docs Guidelines', link: '/contributing/guidelines' }
]
}
]
}
}
})
```
--------------------------------------------------------------------------------
/src/server/config-manager.ts:
--------------------------------------------------------------------------------
```typescript
import type { MasterConfig, RoutingConfig, ServerConfig } from '../types/config.js'
import { ConfigLoader } from '../config/config-loader.js'
import { Logger } from '../utils/logger.js'
import { EnvironmentManager } from '../config/environment-manager.js'
import { SecretManager } from '../config/secret-manager.js'
export interface ConfigManagerOptions {
// If provided, watch the file for changes (Node only)
watch?: boolean
}
type Listener = (config: MasterConfig) => void
export class ConfigManager {
private config: MasterConfig | null = null
private readonly listeners: Set<Listener> = new Set()
private stopWatcher?: () => void
private readonly secrets = new SecretManager()
private watchPaths: string[] = []
constructor(private readonly options?: ConfigManagerOptions) {}
async load(): Promise<MasterConfig> {
const explicit = EnvironmentManager.getExplicitConfigPath()
let loaded: MasterConfig
try {
loaded = await ConfigLoader.load({ path: explicit })
} catch (err) {
Logger.warn('Primary config load failed; attempting env-only load', String(err))
loaded = await ConfigLoader.loadFromEnv()
}
const normalized = this.applyDefaults(loaded)
this.config = normalized
const redacted = this.secrets.redact(normalized)
Logger.info('Configuration loaded', {
servers: normalized.servers.length,
hosting: normalized.hosting.platform,
redacted,
})
if (this.options?.watch) this.prepareWatcher(explicit)
return normalized
}
getConfig(): MasterConfig {
if (!this.config) throw new Error('Config not loaded')
return this.config
}
getRouting(): RoutingConfig | undefined {
return this.config?.routing
}
onChange(listener: Listener): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
async reload(): Promise<void> {
await this.load()
if (this.config) this.emit(this.config)
}
stop(): void {
try {
this.stopWatcher?.()
} catch {
// ignore
}
}
private emit(config: MasterConfig): void {
for (const l of this.listeners) {
try {
l(config)
} catch (err) {
Logger.warn('Config listener threw', err)
}
}
}
private applyDefaults(cfg: MasterConfig): MasterConfig {
// Shallow copy to avoid mutation
const copy: MasterConfig = {
...cfg,
hosting: {
platform: cfg.hosting.platform ?? 'node',
port: cfg.hosting.port ?? 3000,
base_url: cfg.hosting.base_url,
},
routing: cfg.routing ? { ...cfg.routing } : {},
master_oauth: { ...cfg.master_oauth },
servers: cfg.servers.map((s) => this.normalizeServer(s)),
}
return copy
}
private normalizeServer(s: ServerConfig): ServerConfig {
const port = s.config?.port
const normalized: ServerConfig = {
...s,
config: {
environment: s.config?.environment ?? {},
args: s.config?.args ?? [],
...(port ? { port } : {}),
},
}
return normalized
}
private prepareWatcher(explicitPath?: string): void {
const isNode = Boolean((globalThis as any)?.process?.versions?.node)
if (!isNode) return
const { base, env } = EnvironmentManager.getConfigPaths('config')
this.watchPaths = []
if (explicitPath) this.watchPaths.push(explicitPath)
if (base) this.watchPaths.push(base)
if (env) this.watchPaths.push(env)
this.startWatcher()
}
private startWatcher(): void {
const isNode = Boolean((globalThis as any)?.process?.versions?.node)
if (!isNode || this.watchPaths.length === 0) return
import('node:fs').then((fs) => {
const watchers: any[] = []
const onChange = async () => {
try {
Logger.info('Config change detected; validating and reloading...')
const prev = this.config
const newCfg = await ConfigLoader.load({ path: EnvironmentManager.getExplicitConfigPath() })
const applied = this.applyDefaults(newCfg)
if (prev) this.auditDiff(prev, applied)
this.config = applied
this.emit(this.config)
} catch (err) {
Logger.warn('Hot-reload failed to apply new config', String(err))
}
}
for (const p of this.watchPaths) {
try {
watchers.push((fs as any).watch(p, { persistent: false }, onChange))
} catch (err) {
Logger.warn(`Failed to watch ${p}`, String(err))
}
}
this.stopWatcher = () => {
for (const w of watchers) {
try {
w?.close?.()
} catch {
// ignore
}
}
}
}).catch((err) => Logger.warn('Failed to start config file watcher', String(err)))
}
private auditDiff(oldCfg: MasterConfig, newCfg: MasterConfig): void {
const diff: Record<string, { from: unknown; to: unknown }> = {}
const keys = new Set([...Object.keys(oldCfg), ...Object.keys(newCfg)])
for (const k of keys) {
const a: any = (oldCfg as any)[k]
const b: any = (newCfg as any)[k]
if (JSON.stringify(a) !== JSON.stringify(b)) diff[k] = { from: a, to: b }
}
const redacted = this.secrets.redact(diff)
// Highlight non-hot-reloadable settings
if (oldCfg.hosting?.port !== newCfg.hosting?.port) {
Logger.warn('Hosting port changed; restart required to apply')
}
Logger.info('Config change audit', redacted)
}
}
```
--------------------------------------------------------------------------------
/docs/configuration/reference.md:
--------------------------------------------------------------------------------
```markdown
# Configuration Reference
Configuration can be provided as JSON or YAML. The loader merges:
1) `config/default.json`
2) `config/<env>.json` (`MASTER_ENV` or `NODE_ENV`)
3) Environment overrides
4) CLI overrides
5) Explicit file from `MASTER_CONFIG_PATH`
Validated against `config/schema.json` (or embedded fallback).
## Top-Level Fields
- `master_oauth` (required): OAuth settings for the master/client tokens
- `authorization_endpoint` (url)
- `token_endpoint` (url)
- `client_id` (string)
- `client_secret` (string | `env:VAR` | `enc:gcm:...`)
- `redirect_uri` (string)
- `scopes` (string[])
- `issuer?` (string)
- `jwks_uri?` (string)
- `audience?` (string)
- `hosting` (required)
- `platform`: `node` | `cloudflare-workers` | `koyeb` | `docker` | `unknown`
- `port?`: integer (Node only)
- `base_url?`: used for OAuth redirect URL construction
- `storage_backend?`: hints (e.g., `kv`, `durable_object`, `fs`)
- `logging`
- `level`: `debug` | `info` | `warn` | `error`
- `routing`
- `loadBalancer.strategy`: `round_robin` | `weighted` | `health`
- `circuitBreaker`: `failureThreshold`, `successThreshold`, `recoveryTimeoutMs`
- `retry`: `maxRetries`, `baseDelayMs`, `maxDelayMs`, `backoffFactor`, `jitter`, `retryOn.*`
- `security`
- `config_key_env?`: env var name containing config secret key (defaults to `MASTER_CONFIG_KEY`)
- `audit?`: enable config change audit logs
- `rotation_days?`: secret rotation policy hint
- `servers` (required) — array of:
- `id` (string)
- `type`: `git` | `npm` | `pypi` | `docker` | `local`
- `auth_strategy`: `master_oauth` | `delegate_oauth` | `bypass_auth` | `proxy_oauth`
- `auth_config?`: provider-specific details
- `config`:
- `port?` (integer)
- `environment?` (map)
- `args?` (string[])
## YAML Example
```yaml
hosting:
platform: node
port: 3000
logging:
level: info
routing:
loadBalancer: { strategy: round_robin }
circuitBreaker: { failureThreshold: 5, successThreshold: 2, recoveryTimeoutMs: 30000 }
retry: { maxRetries: 2, baseDelayMs: 250, maxDelayMs: 4000, backoffFactor: 2, jitter: full }
master_oauth:
authorization_endpoint: https://example.com/oauth/authorize
token_endpoint: https://example.com/oauth/token
client_id: master-mcp
client_secret: env:MASTER_OAUTH_CLIENT_SECRET
redirect_uri: http://localhost:3000/oauth/callback
scopes: [openid]
servers:
- id: tools
type: local
auth_strategy: bypass_auth
config:
port: 3333
```
<!-- GENERATED:BEGIN -->
# Configuration Reference
This reference is generated from the built-in JSON Schema used by the server to validate configuration.
## Top-Level Fields
- `master_oauth` (required) — type: object
- `master_oauth.issuer` — type: string
- `master_oauth.authorization_endpoint` (required) — type: string, format: url
- `master_oauth.token_endpoint` (required) — type: string, format: url
- `master_oauth.jwks_uri` — type: string
- `master_oauth.client_id` (required) — type: string
- `master_oauth.client_secret` — type: string
- `master_oauth.redirect_uri` (required) — type: string
- `master_oauth.scopes` (required) — type: array
- items: — type: string
- `master_oauth.audience` — type: string
- `hosting` (required) — type: object
- `hosting.platform` (required) — type: string, enum: node, cloudflare-workers, koyeb, docker, unknown
- `hosting.port` — type: number, format: integer
- `hosting.base_url` — type: string
- `logging` — type: object
- `logging.level` — type: string, enum: debug, info, warn, error
- `routing` — type: object
- `routing.loadBalancer` — type: object
- `routing.loadBalancer.strategy` — type: string
- `routing.circuitBreaker` — type: object
- `routing.retry` — type: object
- `servers` (required) — type: array
- items: — type: object
- `servers[].id` (required) — type: string
- `servers[].type` (required) — type: string, enum: git, npm, pypi, docker, local
- `servers[].url` — type: string
- `servers[].package` — type: string
- `servers[].version` — type: string
- `servers[].branch` — type: string
- `servers[].auth_strategy` (required) — type: string, enum: master_oauth, delegate_oauth, bypass_auth, proxy_oauth
- `servers[].auth_config` — type: object
- `servers[].config` (required) — type: object
- `servers[].config.environment` — type: object
- `servers[].config.args` — type: array
- items: — type: string
- `servers[].config.port` — type: number, format: integer
## Examples
## Example: basic.yaml
```yaml
hosting:
platform: node
port: 3000
master_oauth:
authorization_endpoint: https://example.com/auth
token_endpoint: https://example.com/token
client_id: demo-client
redirect_uri: http://localhost:3000/callback
scopes:
- openid
servers:
- id: example
type: local
auth_strategy: master_oauth
config:
environment: {}
args: []
port: 3333
```
## Example: simple-setup.yaml
```yaml
hosting:
platform: node
port: 3000
master_oauth:
authorization_endpoint: https://auth.example.com/authorize
token_endpoint: https://auth.example.com/token
client_id: master-mcp
redirect_uri: http://localhost:3000/oauth/callback
scopes: [openid, profile]
servers:
- id: local-simple
type: local
auth_strategy: bypass_auth
config:
port: 4001
environment: {}
```
<!-- GENERATED:END -->
```
--------------------------------------------------------------------------------
/tests/servers/test-streaming-both.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import { spawn } from 'node:child_process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
async function startHttpServer() {
console.log('Starting HTTP test server...')
// Start the HTTP server as a background process
const httpServer = spawn('node', ['examples/test-mcp-server.js'], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, PORT: '3006' }
})
// Capture stdout and stderr
httpServer.stdout.on('data', (data) => {
console.log(`[HTTP Server] ${data.toString().trim()}`)
})
httpServer.stderr.on('data', (data) => {
console.error(`[HTTP Server ERROR] ${data.toString().trim()}`)
})
// Wait a moment for the server to start
await new Promise(resolve => setTimeout(resolve, 2000))
return httpServer
}
async function runStreamingTest() {
try {
console.log('Testing Master MCP Server with HTTP Streaming...')
// Create a streamable HTTP transport to connect to our MCP server
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
// Create the MCP client
const client = new Client({
name: 'master-mcp-streaming-test-client',
version: '1.0.0'
})
// Initialize the client
await client.connect(transport)
console.log('✅ Server initialized with streaming transport')
console.log('Server info:', client.getServerVersion())
console.log('Server capabilities:', client.getServerCapabilities())
// List tools using streaming
console.log('\n--- Testing tools/list with streaming ---')
const toolsResult = await client.listTools({})
console.log('✅ tools/list successful with streaming')
console.log('Number of tools:', toolsResult.tools.length)
console.log('Tools:', toolsResult.tools.map(t => t.name))
// List resources using streaming
console.log('\n--- Testing resources/list with streaming ---')
const resourcesResult = await client.listResources({})
console.log('✅ resources/list successful with streaming')
console.log('Number of resources:', resourcesResult.resources.length)
console.log('Resources:', resourcesResult.resources.map(r => r.uri))
// Test ping
console.log('\n--- Testing ping with streaming ---')
const pingResult = await client.ping()
console.log('✅ ping successful with streaming')
console.log('Ping result:', pingResult)
// Try calling a tool from the HTTP server
console.log('\n--- Testing tool call to HTTP server ---')
try {
const httpToolCallResult = await client.callTool({
name: 'test-server.echo', // Prefixed with server ID
arguments: { message: 'Hello from HTTP server!' }
})
console.log('✅ HTTP tool call successful')
console.log('HTTP tool result:', JSON.stringify(httpToolCallResult, null, 2))
} catch (error) {
console.log('⚠️ HTTP tool call failed (might not be available):', error.message)
}
// Try calling a tool from the STDIO server
console.log('\n--- Testing tool call to STDIO server ---')
try {
const stdioToolCallResult = await client.callTool({
name: 'stdio-server.stdio-echo', // Prefixed with server ID
arguments: { message: 'Hello from STDIO server!' }
})
console.log('✅ STDIO tool call successful')
console.log('STDIO tool result:', JSON.stringify(stdioToolCallResult, null, 2))
} catch (error) {
console.log('⚠️ STDIO tool call failed (might not be available):', error.message)
}
// Try reading a resource from the HTTP server
console.log('\n--- Testing resource read from HTTP server ---')
try {
const httpResourceResult = await client.readResource({
uri: 'test://example' // This should be prefixed with server ID if needed
})
console.log('✅ HTTP resource read successful')
console.log('HTTP resource result:', JSON.stringify(httpResourceResult, null, 2))
} catch (error) {
console.log('⚠️ HTTP resource read failed (might not be available):', error.message)
}
// Try reading a resource from the STDIO server
console.log('\n--- Testing resource read from STDIO server ---')
try {
const stdioResourceResult = await client.readResource({
uri: 'stdio-server.stdio://example/resource' // Prefixed with server ID
})
console.log('✅ STDIO resource read successful')
console.log('STDIO resource result:', JSON.stringify(stdioResourceResult, null, 2))
} catch (error) {
console.log('⚠️ STDIO resource read failed (might not be available):', error.message)
}
// Close the connection
await client.close()
console.log('\n✅ Disconnected from MCP server')
console.log('\n🎉 All streaming tests completed successfully!')
} catch (error) {
console.error('❌ Streaming test failed:', error)
console.error('Error stack:', error.stack)
}
}
async function main() {
let httpServer
try {
// Start the HTTP server
httpServer = await startHttpServer()
// Run the streaming test
await runStreamingTest()
} catch (error) {
console.error('Test failed:', error)
} finally {
// Clean up: kill the HTTP server
if (httpServer) {
console.log('Stopping HTTP server...')
httpServer.kill()
}
}
}
// Run the test
main()
```
--------------------------------------------------------------------------------
/src/config/environment-manager.ts:
--------------------------------------------------------------------------------
```typescript
import type { HostingConfig, MasterConfig } from '../types/config.js'
import { Logger } from '../utils/logger.js'
export type EnvironmentName = 'development' | 'staging' | 'production' | 'test'
function isNode(): boolean {
return Boolean((globalThis as any)?.process?.versions?.node)
}
export class EnvironmentManager {
static detectEnvironment(): EnvironmentName {
const env = ((globalThis as any)?.process?.env?.MASTER_ENV ||
(globalThis as any)?.process?.env?.NODE_ENV ||
'development') as string
const normalized = env.toLowerCase()
if (normalized === 'prod') return 'production'
if (normalized === 'stage' || normalized === 'staging') return 'staging'
if (normalized === 'test') return 'test'
return 'development'
}
static detectPlatform(): HostingConfig['platform'] {
if (isNode()) return 'node'
// Heuristic: in CF workers, 'WebSocketPair' and 'navigator' often exist
// We default to workers if Node.js globals are absent
return 'cloudflare-workers'
}
static getConfigPaths(baseDir = 'config'): { base?: string; env?: string; schema?: string } {
const env = this.detectEnvironment()
return {
base: `${baseDir}/default.json`,
env: `${baseDir}/${env}.json`,
schema: `${baseDir}/schema.json`,
}
}
static getExplicitConfigPath(): string | undefined {
const fromEnv = (globalThis as any)?.process?.env?.MASTER_CONFIG_PATH
const fromArg = isNode() ? EnvironmentManager.parseCliArgs().configPath : undefined
return (fromArg as string | undefined) || (fromEnv as string | undefined)
}
static parseCliArgs(): { [k: string]: unknown; configPath?: string } {
if (!isNode()) return {}
const args = (process.argv || []).slice(2)
const result: Record<string, unknown> = {}
for (const a of args) {
if (!a.startsWith('--')) continue
const eq = a.indexOf('=')
let key = a
let val: unknown = true
if (eq > -1) {
key = a.slice(0, eq)
const raw = a.slice(eq + 1)
try {
val = JSON.parse(raw)
} catch {
val = raw
}
}
key = key.replace(/^--/, '')
if (key === 'config' || key === 'config-path') {
;(result as any).configPath = String(val)
continue
}
// Support dotted keys: --hosting.port=4000
setByPath(result, key, val)
}
return result
}
static loadEnvOverrides(): Partial<MasterConfig> {
// Map env vars to config fields. All are optional overrides.
const env = (globalThis as any)?.process?.env ?? {}
const hosting: Partial<HostingConfig> = {}
if (env.MASTER_HOSTING_PLATFORM) hosting.platform = env.MASTER_HOSTING_PLATFORM as HostingConfig['platform']
if (env.MASTER_HOSTING_PORT) hosting.port = Number(env.MASTER_HOSTING_PORT)
if (env.MASTER_BASE_URL) hosting.base_url = String(env.MASTER_BASE_URL)
const logging: Partial<NonNullable<MasterConfig['logging']>> = {}
if (env.MASTER_LOG_LEVEL) logging.level = env.MASTER_LOG_LEVEL as any
const master_oauth: Partial<MasterConfig['master_oauth']> = {}
if (env.MASTER_OAUTH_ISSUER) master_oauth.issuer = String(env.MASTER_OAUTH_ISSUER)
if (env.MASTER_OAUTH_AUTHORIZATION_ENDPOINT)
master_oauth.authorization_endpoint = String(env.MASTER_OAUTH_AUTHORIZATION_ENDPOINT)
if (env.MASTER_OAUTH_TOKEN_ENDPOINT) master_oauth.token_endpoint = String(env.MASTER_OAUTH_TOKEN_ENDPOINT)
if (env.MASTER_OAUTH_JWKS_URI) master_oauth.jwks_uri = String(env.MASTER_OAUTH_JWKS_URI)
if (env.MASTER_OAUTH_CLIENT_ID) master_oauth.client_id = String(env.MASTER_OAUTH_CLIENT_ID)
if (env.MASTER_OAUTH_CLIENT_SECRET) master_oauth.client_secret = `env:MASTER_OAUTH_CLIENT_SECRET`
if (env.MASTER_OAUTH_REDIRECT_URI) master_oauth.redirect_uri = String(env.MASTER_OAUTH_REDIRECT_URI)
if (env.MASTER_OAUTH_SCOPES) master_oauth.scopes = String(env.MASTER_OAUTH_SCOPES)
.split(',')
.map((s) => s.trim())
.filter(Boolean)
if (env.MASTER_OAUTH_AUDIENCE) master_oauth.audience = String(env.MASTER_OAUTH_AUDIENCE)
// Servers can be provided as JSON in MASTER_SERVERS or YAML in MASTER_SERVERS_YAML
let servers: MasterConfig['servers'] | undefined
try {
if (env.MASTER_SERVERS) servers = JSON.parse(String(env.MASTER_SERVERS))
} catch (err) {
Logger.warn('Failed to parse MASTER_SERVERS JSON; ignoring', String(err))
}
if (!servers && env.MASTER_SERVERS_YAML) {
try {
// External import avoided in workers; only parse if Node
const YAML = isNode() ? (require('yaml') as typeof import('yaml')) : undefined
if (YAML) servers = YAML.parse(String(env.MASTER_SERVERS_YAML))
} catch (err) {
Logger.warn('Failed to parse MASTER_SERVERS_YAML; ignoring', String(err))
}
}
const override: Partial<MasterConfig> = {}
if (Object.keys(hosting).length) (override as any).hosting = hosting
if (Object.keys(logging).length) (override as any).logging = logging
if (Object.keys(master_oauth).length) (override as any).master_oauth = master_oauth
if (servers) (override as any).servers = servers
return override
}
}
function setByPath(target: Record<string, unknown>, dottedKey: string, value: unknown): void {
const parts = dottedKey.split('.')
let cur: any = target
for (let i = 0; i < parts.length - 1; i++) {
const p = parts[i]
if (typeof cur[p] !== 'object' || cur[p] === null) cur[p] = {}
cur = cur[p]
}
cur[parts[parts.length - 1]] = value
}
```
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vue.js:
--------------------------------------------------------------------------------
```javascript
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-HVR2FF6M.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};
//# sourceMappingURL=vue.js.map
```
--------------------------------------------------------------------------------
/src/config/config-loader.ts:
--------------------------------------------------------------------------------
```typescript
import type { MasterConfig } from '../types/config.js'
import { EnvironmentManager } from './environment-manager.js'
import { SecretManager } from './secret-manager.js'
import { SchemaValidator } from './schema-validator.js'
import { Logger } from '../utils/logger.js'
type LoadOptions = {
// Explicit path to config file; when provided, overrides environment-based discovery
path?: string
// Optional base directory for default and env configs
baseDir?: string
// Provide a schema path override
schemaPath?: string
}
function isNode(): boolean {
return Boolean((globalThis as any)?.process?.versions?.node)
}
export class ConfigLoader {
static async load(options?: LoadOptions): Promise<MasterConfig> {
const envName = EnvironmentManager.detectEnvironment()
const platform = EnvironmentManager.detectPlatform()
const explicit = options?.path ?? EnvironmentManager.getExplicitConfigPath()
const baseDir = options?.baseDir ?? 'config'
const paths = EnvironmentManager.getConfigPaths(baseDir)
const schemaPath = options?.schemaPath ?? paths.schema
let fileConfig: Partial<MasterConfig> = {}
const loadedFiles: string[] = []
if (explicit && isNode()) {
const cfg = await this.loadFromFile(explicit)
fileConfig = deepMerge(fileConfig, cfg)
loadedFiles.push(explicit)
} else if (isNode()) {
// Load default.json then <env>.json if present
const fs = await import('node:fs/promises')
const fsc = await import('node:fs')
if (paths.base && fsc.existsSync(paths.base)) {
fileConfig = deepMerge(fileConfig, await this.loadFromFile(paths.base))
loadedFiles.push(paths.base)
}
if (paths.env && fsc.existsSync(paths.env)) {
fileConfig = deepMerge(fileConfig, await this.loadFromFile(paths.env))
loadedFiles.push(paths.env)
}
// If nothing loaded and config dir doesn't exist, try a default path
;(void fs)
}
Logger.info('File config loaded', { fileConfig, loadedFiles })
// Environment variables
const envOverrides = EnvironmentManager.loadEnvOverrides()
Logger.info('Environment overrides', { envOverrides })
fileConfig = deepMerge(fileConfig, envOverrides)
// CLI args nested overrides
const cli = EnvironmentManager.parseCliArgs()
Logger.info('CLI args', { cli })
fileConfig = deepMerge(fileConfig, cli as any)
// Ensure hosting.platform and env awareness
const normalized: Partial<MasterConfig> = {
...fileConfig,
hosting: { ...fileConfig.hosting, platform },
}
Logger.info('Normalized config', { normalized })
// Schema validation and secret resolution
const schema = await SchemaValidator.loadSchema(schemaPath)
const validated = SchemaValidator.assertValid<MasterConfig>(normalized, schema!)
const secrets = new SecretManager()
const resolved = secrets.resolveSecrets(validated)
// Cache with key based on env and paths
// In-memory caching can be added if needed; omitted to keep memory footprint small
Logger.info('Configuration loaded', {
files: loadedFiles,
platform,
env: envName,
})
return resolved
}
static async loadFromFile(filePath: string): Promise<Partial<MasterConfig>> {
if (!isNode()) throw new Error('File loading is only supported in Node.js runtime')
const fs = await import('node:fs/promises')
const path = await import('node:path')
const raw = await fs.readFile(filePath, 'utf8')
Logger.info('Loading config from file', { filePath, raw })
const ext = path.extname(filePath).toLowerCase()
let parsed: any
if (ext === '.json') parsed = JSON.parse(raw)
else if (ext === '.yaml' || ext === '.yml') parsed = (await import('yaml')).parse(raw)
else {
// Fallback: try JSON then YAML
try {
parsed = JSON.parse(raw)
} catch {
parsed = (await import('yaml')).parse(raw)
}
}
Logger.info('Parsed config from file', { filePath, parsed })
return parsed as Partial<MasterConfig>
}
static async loadFromEnv(): Promise<MasterConfig> {
// For compatibility with older phases
const override = EnvironmentManager.loadEnvOverrides()
const defaults: Partial<MasterConfig> = {
hosting: {
platform: EnvironmentManager.detectPlatform(),
port: (globalThis as any)?.process?.env?.PORT ? Number((globalThis as any)?.process?.env?.PORT) : 3000,
base_url: (globalThis as any)?.process?.env?.BASE_URL,
},
servers: [],
master_oauth: {
authorization_endpoint: 'https://example.com/auth',
token_endpoint: 'https://example.com/token',
client_id: 'placeholder',
redirect_uri: 'http://localhost/callback',
scopes: ['openid'],
},
}
const merged = deepMerge(defaults, override) as MasterConfig
const schema = await SchemaValidator.loadSchema()
return SchemaValidator.assertValid(merged, schema!)
}
}
function deepMerge<T>(base: T, override: Partial<T>): T {
if (Array.isArray(base) && Array.isArray(override)) return override as unknown as T
if (base && typeof base === 'object' && override && typeof override === 'object') {
const out: any = { ...(base as any) }
for (const [k, v] of Object.entries(override as any)) {
if (v === undefined) continue
if (Array.isArray(v)) out[k] = v
else if (typeof v === 'object' && v !== null) out[k] = deepMerge((base as any)[k], v)
else out[k] = v
}
return out
}
return (override as T) ?? base
}
```