#
tokens: 49214/50000 48/252 files (page 2/10)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 10. Use http://codebase.md/jakedismo/master-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .eslintignore
├── .eslintrc.cjs
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .prettierrc.json
├── CHANGELOG.md
├── config
│   ├── default.json
│   ├── development.json
│   ├── production.json
│   └── schema.json
├── config.json
├── CONTRIBUTING.md
├── debug-stdio.cjs
├── debug-stdio.js
├── deploy
│   ├── cloudflare
│   │   ├── .gitkeep
│   │   ├── README.md
│   │   └── wrangler.toml
│   ├── docker
│   │   ├── .gitkeep
│   │   ├── docker-compose.yml
│   │   ├── Dockerfile
│   │   └── entrypoint.sh
│   ├── koyeb
│   │   ├── .gitkeep
│   │   └── koyeb.yaml
│   └── README.md
├── docker-compose.yml
├── Dockerfile
├── docs
│   ├── .DS_Store
│   ├── .vitepress
│   │   ├── cache
│   │   │   └── deps
│   │   │       ├── _metadata.json
│   │   │       ├── chunk-HVR2FF6M.js
│   │   │       ├── chunk-HVR2FF6M.js.map
│   │   │       ├── chunk-P2XGSYO7.js
│   │   │       ├── chunk-P2XGSYO7.js.map
│   │   │       ├── package.json
│   │   │       ├── vitepress___@vue_devtools-api.js
│   │   │       ├── vitepress___@vue_devtools-api.js.map
│   │   │       ├── vitepress___@vueuse_core.js
│   │   │       ├── vitepress___@vueuse_core.js.map
│   │   │       ├── vitepress___@vueuse_integrations_useFocusTrap.js
│   │   │       ├── vitepress___@vueuse_integrations_useFocusTrap.js.map
│   │   │       ├── vitepress___mark__js_src_vanilla__js.js
│   │   │       ├── vitepress___mark__js_src_vanilla__js.js.map
│   │   │       ├── vitepress___minisearch.js
│   │   │       ├── vitepress___minisearch.js.map
│   │   │       ├── vue.js
│   │   │       └── vue.js.map
│   │   ├── config.ts
│   │   ├── dist
│   │   │   ├── 404.html
│   │   │   ├── advanced
│   │   │   │   ├── extensibility.html
│   │   │   │   ├── index.html
│   │   │   │   ├── monitoring.html
│   │   │   │   ├── performance.html
│   │   │   │   └── security.html
│   │   │   ├── api
│   │   │   │   ├── index.html
│   │   │   │   └── README.html
│   │   │   ├── assets
│   │   │   │   ├── advanced_extensibility.md.TrXUn5w5.js
│   │   │   │   ├── advanced_extensibility.md.TrXUn5w5.lean.js
│   │   │   │   ├── advanced_index.md.CPcpUlw_.js
│   │   │   │   ├── advanced_index.md.CPcpUlw_.lean.js
│   │   │   │   ├── advanced_monitoring.md.DTybdNg-.js
│   │   │   │   ├── advanced_monitoring.md.DTybdNg-.lean.js
│   │   │   │   ├── advanced_performance.md.DKmzK0ia.js
│   │   │   │   ├── advanced_performance.md.DKmzK0ia.lean.js
│   │   │   │   ├── advanced_security.md.B-oBD7IB.js
│   │   │   │   ├── advanced_security.md.B-oBD7IB.lean.js
│   │   │   │   ├── api_index.md.Dl1JB08_.js
│   │   │   │   ├── api_index.md.Dl1JB08_.lean.js
│   │   │   │   ├── chunks
│   │   │   │   │   └── framework.CHl2ywxc.js
│   │   │   │   ├── configuration_environment-variables.md.Ddy3P_Wz.js
│   │   │   │   ├── configuration_environment-variables.md.Ddy3P_Wz.lean.js
│   │   │   │   ├── configuration_environment.md.DxcTQ623.js
│   │   │   │   ├── configuration_environment.md.DxcTQ623.lean.js
│   │   │   │   ├── configuration_overview.md.DIkVDv7V.js
│   │   │   │   ├── configuration_overview.md.DIkVDv7V.lean.js
│   │   │   │   ├── configuration_performance.md.DbJdmLrW.js
│   │   │   │   ├── configuration_performance.md.DbJdmLrW.lean.js
│   │   │   │   ├── configuration_reference.md.27IKWqtk.js
│   │   │   │   ├── configuration_reference.md.27IKWqtk.lean.js
│   │   │   │   ├── configuration_security.md.-OOlkzN4.js
│   │   │   │   ├── configuration_security.md.-OOlkzN4.lean.js
│   │   │   │   ├── contributing_dev-setup.md.Ceqh4w-R.js
│   │   │   │   ├── contributing_dev-setup.md.Ceqh4w-R.lean.js
│   │   │   │   ├── contributing_guidelines.md.ZEAX2yVh.js
│   │   │   │   ├── contributing_guidelines.md.ZEAX2yVh.lean.js
│   │   │   │   ├── contributing_index.md.DYq9R6wr.js
│   │   │   │   ├── contributing_index.md.DYq9R6wr.lean.js
│   │   │   │   ├── contributing_maintenance.md.k2bR0IaR.js
│   │   │   │   ├── contributing_maintenance.md.k2bR0IaR.lean.js
│   │   │   │   ├── deployment_cicd.md.Ci2T0UYC.js
│   │   │   │   ├── deployment_cicd.md.Ci2T0UYC.lean.js
│   │   │   │   ├── deployment_cloudflare-workers.md.D2WHsfep.js
│   │   │   │   ├── deployment_cloudflare-workers.md.D2WHsfep.lean.js
│   │   │   │   ├── deployment_docker.md.B8bQDQTo.js
│   │   │   │   ├── deployment_docker.md.B8bQDQTo.lean.js
│   │   │   │   ├── deployment_index.md.ClYeOkpy.js
│   │   │   │   ├── deployment_index.md.ClYeOkpy.lean.js
│   │   │   │   ├── deployment_koyeb.md.B_wJhvF7.js
│   │   │   │   ├── deployment_koyeb.md.B_wJhvF7.lean.js
│   │   │   │   ├── examples_advanced-routing.md.B3CqhLZ7.js
│   │   │   │   ├── examples_advanced-routing.md.B3CqhLZ7.lean.js
│   │   │   │   ├── examples_basic-node.md.CaDZzGlO.js
│   │   │   │   ├── examples_basic-node.md.CaDZzGlO.lean.js
│   │   │   │   ├── examples_cloudflare-worker.md.DwVSz-c7.js
│   │   │   │   ├── examples_cloudflare-worker.md.DwVSz-c7.lean.js
│   │   │   │   ├── examples_index.md.CBF_BLkl.js
│   │   │   │   ├── examples_index.md.CBF_BLkl.lean.js
│   │   │   │   ├── examples_oauth-delegation.md.1hZxoqDl.js
│   │   │   │   ├── examples_oauth-delegation.md.1hZxoqDl.lean.js
│   │   │   │   ├── examples_overview.md.CZN0JbZ7.js
│   │   │   │   ├── examples_overview.md.CZN0JbZ7.lean.js
│   │   │   │   ├── examples_testing.md.Dek4GpNs.js
│   │   │   │   ├── examples_testing.md.Dek4GpNs.lean.js
│   │   │   │   ├── getting-started_concepts.md.D7ON9iGB.js
│   │   │   │   ├── getting-started_concepts.md.D7ON9iGB.lean.js
│   │   │   │   ├── getting-started_installation.md.BKnVqAGg.js
│   │   │   │   ├── getting-started_installation.md.BKnVqAGg.lean.js
│   │   │   │   ├── getting-started_overview.md.DvJDFL2N.js
│   │   │   │   ├── getting-started_overview.md.DvJDFL2N.lean.js
│   │   │   │   ├── getting-started_quickstart-node.md.GOO4aGas.js
│   │   │   │   ├── getting-started_quickstart-node.md.GOO4aGas.lean.js
│   │   │   │   ├── getting-started_quickstart-workers.md.Cpofh8Mj.js
│   │   │   │   ├── getting-started_quickstart-workers.md.Cpofh8Mj.lean.js
│   │   │   │   ├── getting-started.md.DG9ndneo.js
│   │   │   │   ├── getting-started.md.DG9ndneo.lean.js
│   │   │   │   ├── guides_configuration-management.md.B-jwYMbA.js
│   │   │   │   ├── guides_configuration-management.md.B-jwYMbA.lean.js
│   │   │   │   ├── guides_configuration.md.Ci3zYDFA.js
│   │   │   │   ├── guides_configuration.md.Ci3zYDFA.lean.js
│   │   │   │   ├── guides_index.md.CIlq2fmx.js
│   │   │   │   ├── guides_index.md.CIlq2fmx.lean.js
│   │   │   │   ├── guides_module-loading.md.BkJvuRnQ.js
│   │   │   │   ├── guides_module-loading.md.BkJvuRnQ.lean.js
│   │   │   │   ├── guides_oauth-delegation.md.DEOZ-_G0.js
│   │   │   │   ├── guides_oauth-delegation.md.DEOZ-_G0.lean.js
│   │   │   │   ├── guides_request-routing.md.Bdzf0VLg.js
│   │   │   │   ├── guides_request-routing.md.Bdzf0VLg.lean.js
│   │   │   │   ├── guides_testing.md.kYfHqJLu.js
│   │   │   │   ├── guides_testing.md.kYfHqJLu.lean.js
│   │   │   │   ├── inter-italic-cyrillic-ext.r48I6akx.woff2
│   │   │   │   ├── inter-italic-cyrillic.By2_1cv3.woff2
│   │   │   │   ├── inter-italic-greek-ext.1u6EdAuj.woff2
│   │   │   │   ├── inter-italic-greek.DJ8dCoTZ.woff2
│   │   │   │   ├── inter-italic-latin-ext.CN1xVJS-.woff2
│   │   │   │   ├── inter-italic-latin.C2AdPX0b.woff2
│   │   │   │   ├── inter-italic-vietnamese.BSbpV94h.woff2
│   │   │   │   ├── inter-roman-cyrillic-ext.BBPuwvHQ.woff2
│   │   │   │   ├── inter-roman-cyrillic.C5lxZ8CY.woff2
│   │   │   │   ├── inter-roman-greek-ext.CqjqNYQ-.woff2
│   │   │   │   ├── inter-roman-greek.BBVDIX6e.woff2
│   │   │   │   ├── inter-roman-latin-ext.4ZJIpNVo.woff2
│   │   │   │   ├── inter-roman-latin.Di8DUHzh.woff2
│   │   │   │   ├── inter-roman-vietnamese.BjW4sHH5.woff2
│   │   │   │   ├── README.md.BO5r5M9u.js
│   │   │   │   ├── README.md.BO5r5M9u.lean.js
│   │   │   │   ├── style.BQrfSMzK.css
│   │   │   │   ├── troubleshooting_common-issues.md.CScvzWM1.js
│   │   │   │   ├── troubleshooting_common-issues.md.CScvzWM1.lean.js
│   │   │   │   ├── troubleshooting_deployment.md.DUhpqnLE.js
│   │   │   │   ├── troubleshooting_deployment.md.DUhpqnLE.lean.js
│   │   │   │   ├── troubleshooting_errors.md.BSCsEmGc.js
│   │   │   │   ├── troubleshooting_errors.md.BSCsEmGc.lean.js
│   │   │   │   ├── troubleshooting_oauth.md.Cw60Eka3.js
│   │   │   │   ├── troubleshooting_oauth.md.Cw60Eka3.lean.js
│   │   │   │   ├── troubleshooting_performance.md.DxY6LJcT.js
│   │   │   │   ├── troubleshooting_performance.md.DxY6LJcT.lean.js
│   │   │   │   ├── troubleshooting_routing.md.BHN-MDhs.js
│   │   │   │   ├── troubleshooting_routing.md.BHN-MDhs.lean.js
│   │   │   │   ├── troubleshooting_security-best-practices.md.Yiu8E-zt.js
│   │   │   │   ├── troubleshooting_security-best-practices.md.Yiu8E-zt.lean.js
│   │   │   │   ├── tutorials_beginner-getting-started.md.BXObgobW.js
│   │   │   │   ├── tutorials_beginner-getting-started.md.BXObgobW.lean.js
│   │   │   │   ├── tutorials_cloudflare-workers-tutorial.md.MPHsc0aT.js
│   │   │   │   ├── tutorials_cloudflare-workers-tutorial.md.MPHsc0aT.lean.js
│   │   │   │   ├── tutorials_load-balancing-and-resilience.md.Dv9r9jyW.js
│   │   │   │   ├── tutorials_load-balancing-and-resilience.md.Dv9r9jyW.lean.js
│   │   │   │   ├── tutorials_oauth-delegation-github.md.Nq4glqCe.js
│   │   │   │   └── tutorials_oauth-delegation-github.md.Nq4glqCe.lean.js
│   │   │   ├── configuration
│   │   │   │   ├── environment-variables.html
│   │   │   │   ├── environment.html
│   │   │   │   ├── examples.html
│   │   │   │   ├── overview.html
│   │   │   │   ├── performance.html
│   │   │   │   ├── reference.html
│   │   │   │   └── security.html
│   │   │   ├── contributing
│   │   │   │   ├── dev-setup.html
│   │   │   │   ├── guidelines.html
│   │   │   │   ├── index.html
│   │   │   │   └── maintenance.html
│   │   │   ├── deployment
│   │   │   │   ├── cicd.html
│   │   │   │   ├── cloudflare-workers.html
│   │   │   │   ├── docker.html
│   │   │   │   ├── index.html
│   │   │   │   └── koyeb.html
│   │   │   ├── diagrams
│   │   │   │   └── architecture.svg
│   │   │   ├── examples
│   │   │   │   ├── advanced-routing.html
│   │   │   │   ├── basic-node.html
│   │   │   │   ├── cloudflare-worker.html
│   │   │   │   ├── index.html
│   │   │   │   ├── oauth-delegation.html
│   │   │   │   ├── overview.html
│   │   │   │   └── testing.html
│   │   │   ├── getting-started
│   │   │   │   ├── concepts.html
│   │   │   │   ├── installation.html
│   │   │   │   ├── overview.html
│   │   │   │   ├── quick-start.html
│   │   │   │   ├── quickstart-node.html
│   │   │   │   └── quickstart-workers.html
│   │   │   ├── getting-started.html
│   │   │   ├── guides
│   │   │   │   ├── authentication.html
│   │   │   │   ├── client-integration.html
│   │   │   │   ├── configuration-management.html
│   │   │   │   ├── configuration.html
│   │   │   │   ├── index.html
│   │   │   │   ├── module-loading.html
│   │   │   │   ├── oauth-delegation.html
│   │   │   │   ├── request-routing.html
│   │   │   │   ├── server-management.html
│   │   │   │   ├── server-sharing.html
│   │   │   │   └── testing.html
│   │   │   ├── hashmap.json
│   │   │   ├── index.html
│   │   │   ├── logo.svg
│   │   │   ├── README.html
│   │   │   ├── reports
│   │   │   │   └── mcp-compliance-audit.html
│   │   │   ├── troubleshooting
│   │   │   │   ├── common-issues.html
│   │   │   │   ├── deployment.html
│   │   │   │   ├── errors.html
│   │   │   │   ├── index.html
│   │   │   │   ├── oauth.html
│   │   │   │   ├── performance.html
│   │   │   │   ├── routing.html
│   │   │   │   └── security-best-practices.html
│   │   │   ├── tutorials
│   │   │   │   ├── beginner-getting-started.html
│   │   │   │   ├── cloudflare-workers-tutorial.html
│   │   │   │   ├── load-balancing-and-resilience.html
│   │   │   │   └── oauth-delegation-github.html
│   │   │   └── vp-icons.css
│   │   └── theme
│   │       ├── components
│   │       │   ├── ApiPlayground.vue
│   │       │   ├── AuthFlowDemo.vue
│   │       │   ├── CodeTabs.vue
│   │       │   └── ConfigGenerator.vue
│   │       ├── index.ts
│   │       └── style.css
│   ├── advanced
│   │   ├── extensibility.md
│   │   ├── index.md
│   │   ├── monitoring.md
│   │   ├── performance.md
│   │   └── security.md
│   ├── api
│   │   ├── functions
│   │   │   └── createServer.md
│   │   ├── index.md
│   │   ├── interfaces
│   │   │   └── RunningServer.md
│   │   └── README.md
│   ├── architecture
│   │   └── images
│   │       └── mcp_master_architecture.svg
│   ├── configuration
│   │   ├── environment-variables.md
│   │   ├── environment.md
│   │   ├── examples.md
│   │   ├── overview.md
│   │   ├── performance.md
│   │   ├── reference.md
│   │   └── security.md
│   ├── contributing
│   │   ├── dev-setup.md
│   │   ├── guidelines.md
│   │   ├── index.md
│   │   └── maintenance.md
│   ├── deployment
│   │   ├── cicd.md
│   │   ├── cloudflare-workers.md
│   │   ├── docker.md
│   │   ├── docs-site.md
│   │   ├── index.md
│   │   └── koyeb.md
│   ├── examples
│   │   ├── advanced-routing.md
│   │   ├── basic-node.md
│   │   ├── cloudflare-worker.md
│   │   ├── index.md
│   │   ├── oauth-delegation.md
│   │   ├── overview.md
│   │   └── testing.md
│   ├── getting-started
│   │   ├── concepts.md
│   │   ├── installation.md
│   │   ├── overview.md
│   │   ├── quick-start.md
│   │   ├── quickstart-node.md
│   │   └── quickstart-workers.md
│   ├── getting-started.md
│   ├── guides
│   │   ├── authentication.md
│   │   ├── client-integration.md
│   │   ├── configuration-management.md
│   │   ├── configuration.md
│   │   ├── index.md
│   │   ├── module-loading.md
│   │   ├── oauth-delegation.md
│   │   ├── request-routing.md
│   │   ├── server-management.md
│   │   ├── server-sharing.md
│   │   └── testing.md
│   ├── index.html
│   ├── public
│   │   ├── diagrams
│   │   │   └── architecture.svg
│   │   ├── github-social.png
│   │   │   └── image.png
│   │   ├── logo.png
│   │   └── logo.svg
│   ├── README.md
│   ├── stdio-servers.md
│   ├── testing
│   │   └── phase-9-testing-architecture.md
│   ├── troubleshooting
│   │   ├── common-issues.md
│   │   ├── deployment.md
│   │   ├── errors.md
│   │   ├── index.md
│   │   ├── oauth.md
│   │   ├── performance.md
│   │   ├── routing.md
│   │   └── security-best-practices.md
│   └── tutorials
│       ├── beginner-getting-started.md
│       ├── cloudflare-workers-tutorial.md
│       ├── load-balancing-and-resilience.md
│       └── oauth-delegation-github.md
├── examples
│   ├── advanced-routing
│   │   ├── config.yaml
│   │   └── README.md
│   ├── basic-node
│   │   ├── config.yaml
│   │   ├── README.md
│   │   └── server.ts
│   ├── cloudflare-worker
│   │   ├── README.md
│   │   └── worker.ts
│   ├── custom-auth
│   │   ├── config.yaml
│   │   ├── index.ts
│   │   └── README.md
│   ├── multi-server
│   │   ├── config.yaml
│   │   └── README.md
│   ├── oauth-delegation
│   │   └── README.md
│   ├── oauth-node
│   │   ├── config.yaml
│   │   └── README.md
│   ├── performance
│   │   ├── config.yaml
│   │   └── README.md
│   ├── sample-configs
│   │   ├── basic.yaml
│   │   └── simple-setup.yaml
│   ├── security-hardening
│   │   └── README.md
│   ├── stdio-mcp-server.cjs
│   ├── test-mcp-server.js
│   └── test-stdio-server.js
├── LICENSE
├── master-mcp-definition.md
├── package-lock.json
├── package.json
├── README.md
├── reports
│   └── claude_report_20250815_222153.html
├── scripts
│   └── generate-config-docs.ts
├── src
│   ├── auth
│   │   ├── multi-auth-manager.ts
│   │   ├── oauth-providers.ts
│   │   └── token-manager.ts
│   ├── config
│   │   ├── config-loader.ts
│   │   ├── environment-manager.ts
│   │   ├── schema-validator.ts
│   │   └── secret-manager.ts
│   ├── index.ts
│   ├── mcp-server.ts
│   ├── modules
│   │   ├── capability-aggregator.ts
│   │   ├── module-loader.ts
│   │   ├── request-router.ts
│   │   ├── stdio-capability-discovery.ts
│   │   └── stdio-manager.ts
│   ├── oauth
│   │   ├── callback-handler.ts
│   │   ├── flow-controller.ts
│   │   ├── flow-validator.ts
│   │   ├── pkce-manager.ts
│   │   ├── state-manager.ts
│   │   └── web-interface.ts
│   ├── routing
│   │   ├── circuit-breaker.ts
│   │   ├── load-balancer.ts
│   │   ├── retry-handler.ts
│   │   └── route-registry.ts
│   ├── runtime
│   │   ├── node.ts
│   │   └── worker.ts
│   ├── server
│   │   ├── config-manager.ts
│   │   ├── dependency-container.ts
│   │   ├── master-server.ts
│   │   └── protocol-handler.ts
│   ├── types
│   │   ├── auth.ts
│   │   ├── config.ts
│   │   ├── jose-shim.d.ts
│   │   ├── mcp.ts
│   │   └── server.ts
│   └── utils
│       ├── cache.ts
│       ├── crypto.ts
│       ├── dev.ts
│       ├── errors.ts
│       ├── http.ts
│       ├── logger.ts
│       ├── monitoring.ts
│       ├── string.ts
│       ├── time.ts
│       ├── validation.ts
│       └── validators.ts
├── static
│   └── oauth
│       ├── consent.html
│       ├── error.html
│       ├── script.js
│       ├── style.css
│       └── success.html
├── tests
│   ├── _setup
│   │   ├── miniflare.setup.ts
│   │   └── vitest.setup.ts
│   ├── _utils
│   │   ├── log-capture.ts
│   │   ├── mock-fetch.ts
│   │   └── test-server.ts
│   ├── .gitkeep
│   ├── e2e
│   │   ├── flow-controller.express.test.ts
│   │   └── flow-controller.worker.test.ts
│   ├── factories
│   │   ├── configFactory.ts
│   │   ├── mcpFactory.ts
│   │   └── oauthFactory.ts
│   ├── fixtures
│   │   ├── capabilities.json
│   │   └── stdio-server.js
│   ├── integration
│   │   ├── modules.capability-aggregator.test.ts
│   │   ├── modules.module-loader-health.test.ts
│   │   ├── oauth.callback-handler.test.ts
│   │   └── request-router.test.ts
│   ├── mocks
│   │   ├── mcp
│   │   │   └── fake-backend.ts
│   │   └── oauth
│   │       └── mock-oidc-provider.ts
│   ├── perf
│   │   ├── artillery
│   │   │   └── auth-routing.yaml
│   │   └── perf.auth-and-routing.test.ts
│   ├── security
│   │   └── security.oauth-and-input.test.ts
│   ├── servers
│   │   ├── test-auth-simple.js
│   │   ├── test-debug.js
│   │   ├── test-master-mcp.js
│   │   ├── test-mcp-client.js
│   │   ├── test-streaming-both-complete.js
│   │   ├── test-streaming-both-full.js
│   │   ├── test-streaming-both-simple.js
│   │   ├── test-streaming-both.js
│   │   └── test-streaming.js
│   ├── setup
│   │   └── test-setup.ts
│   ├── unit
│   │   ├── auth.multi-auth-manager.test.ts
│   │   ├── auth.token-manager.test.ts
│   │   ├── config.environment-manager.test.ts
│   │   ├── config.schema-validator.test.ts
│   │   ├── config.secret-manager.test.ts
│   │   ├── modules
│   │   │   ├── stdio-capability-discovery.test.ts
│   │   │   └── stdio-manager.test.ts
│   │   ├── modules.route-registry.test.ts
│   │   ├── oauth.pkce-state.test.ts
│   │   ├── routing
│   │   │   └── circuit-breaker.test.ts
│   │   ├── routing.core.test.ts
│   │   ├── stdio-capability-discovery.test.ts
│   │   ├── utils.crypto.test.ts
│   │   ├── utils.logger.test.ts
│   │   └── utils.monitoring.test.ts
│   └── utils
│       ├── fake-express.ts
│       ├── mock-http.ts
│       ├── oauth-mocks.ts
│       └── token-storages.ts
├── tsconfig.base.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.worker.json
├── typedoc.json
├── vitest.config.ts
└── vitest.worker.config.ts
```

# Files

--------------------------------------------------------------------------------
/docs/guides/configuration-management.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Configuration Management
 2 | 
 3 | Master MCP Server supports JSON or YAML configuration files, environment variable overrides, CLI overrides, schema validation, and secret resolution.
 4 | 
 5 | ## Files
 6 | 
 7 | - Default paths: `config/default.json`, `config/<env>.json`
 8 | - Explicit path: set `MASTER_CONFIG_PATH=/path/to/config.yaml`
 9 | - Schema: `config/schema.json` (also embedded in code as a fallback)
10 | 
11 | ## Environment Overrides
12 | 
13 | Environment variables map to config fields. Key ones:
14 | 
15 | - `MASTER_HOSTING_PLATFORM`, `MASTER_HOSTING_PORT`, `MASTER_BASE_URL`
16 | - `MASTER_LOG_LEVEL`
17 | - `MASTER_OAUTH_*` (ISSUER, AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, SCOPES, AUDIENCE)
18 | - `MASTER_SERVERS` (JSON) or `MASTER_SERVERS_YAML` (YAML)
19 | 
20 | See `docs/configuration/environment-variables.md` and `.env.example`.
21 | 
22 | ## CLI Overrides
23 | 
24 | You can override nested fields with dotted keys:
25 | 
26 | ```
27 | node dist/node/index.js --hosting.port=4000 --routing.retry.maxRetries=3
28 | ```
29 | 
30 | ## Secrets
31 | 
32 | - `env:VARNAME` → replaced by `process.env.VARNAME` at load time
33 | - `enc:gcm:<base64>` → decrypted using `MASTER_CONFIG_KEY` (or `MASTER_SECRET_KEY`)
34 | 
35 | Use `SecretManager` to encrypt/decrypt/rotate secrets safely.
36 | 
37 | ## Hot Reload (Node)
38 | 
39 | When `ConfigManager` is created with `{ watch: true }`, changes to `config/default.json`, `config/<env>.json`, or an explicit path will be validated and emitted. Some changes (e.g., hosting.port) still require a restart.
40 | 
41 | ## Validation
42 | 
43 | Configs are validated using a lightweight schema validator (`SchemaValidator`) with support for types, enums, required fields, arrays, and formats (`url`, `integer`). On failure, the error lists the exact path and reason.
44 | 
45 | 
```

--------------------------------------------------------------------------------
/src/utils/cache.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Simple in-memory TTL cache with optional memoization helpers.
 3 |  */
 4 | 
 5 | export interface CacheEntry<V> {
 6 |   value: V
 7 |   expiresAt: number
 8 | }
 9 | 
10 | export class TTLCache<K, V> {
11 |   private store = new Map<K, CacheEntry<V>>()
12 |   constructor(private defaultTtlMs = 60_000) {}
13 | 
14 |   set(key: K, value: V, ttlMs?: number): void {
15 |     const ttl = ttlMs ?? this.defaultTtlMs
16 |     const expiresAt = Date.now() + ttl
17 |     this.store.set(key, { value, expiresAt })
18 |   }
19 | 
20 |   get(key: K): V | undefined {
21 |     const hit = this.store.get(key)
22 |     if (!hit) return undefined
23 |     if (hit.expiresAt < Date.now()) {
24 |       this.store.delete(key)
25 |       return undefined
26 |     }
27 |     return hit.value
28 |   }
29 | 
30 |   has(key: K): boolean {
31 |     return this.get(key) !== undefined
32 |   }
33 | 
34 |   delete(key: K): void {
35 |     this.store.delete(key)
36 |   }
37 | 
38 |   clear(): void {
39 |     this.store.clear()
40 |   }
41 | 
42 |   size(): number {
43 |     return this.store.size
44 |   }
45 | 
46 |   sweep(): void {
47 |     const now = Date.now()
48 |     for (const [k, v] of this.store.entries()) {
49 |       if (v.expiresAt < now) this.store.delete(k)
50 |     }
51 |   }
52 | 
53 |   async getOrSet(key: K, loader: () => Promise<V>, ttlMs?: number): Promise<V> {
54 |     const existing = this.get(key)
55 |     if (existing !== undefined) return existing
56 |     const v = await loader()
57 |     this.set(key, v, ttlMs)
58 |     return v
59 |   }
60 | }
61 | 
62 | export function memoizeAsync<A extends unknown[], R>(fn: (...args: A) => Promise<R>, ttlMs = 60_000): (...args: A) => Promise<R> {
63 |   const cache = new TTLCache<string, R>(ttlMs)
64 |   return async (...args: A) => {
65 |     const key = JSON.stringify(args)
66 |     const hit = cache.get(key)
67 |     if (hit !== undefined) return hit
68 |     const res = await fn(...args)
69 |     cache.set(key, res, ttlMs)
70 |     return res
71 |   }
72 | }
73 | 
74 | 
```

--------------------------------------------------------------------------------
/docs/.vitepress/theme/style.css:
--------------------------------------------------------------------------------

```css
 1 | :root {
 2 |   --mcp-accent: #0ea5e9;
 3 |   --mcp-accent-600: #0284c7;
 4 |   --mcp-bg-soft: color-mix(in oklab, var(--vp-c-bg) 90%, var(--mcp-accent) 10%);
 5 | }
 6 | 
 7 | .mcp-kicker {
 8 |   font-size: .9rem;
 9 |   color: var(--vp-c-text-2);
10 |   text-transform: uppercase;
11 |   letter-spacing: .08em;
12 | }
13 | 
14 | /* Tabs */
15 | .mcp-tabs {
16 |   border: 1px solid var(--vp-c-divider);
17 |   border-radius: 10px;
18 |   overflow: hidden;
19 |   background: var(--vp-c-bg-soft);
20 | }
21 | .mcp-tabs__nav {
22 |   display: flex;
23 |   gap: 6px;
24 |   padding: 8px;
25 |   background: var(--vp-c-bg);
26 |   border-bottom: 1px solid var(--vp-c-divider);
27 | }
28 | .mcp-tabs__btn {
29 |   appearance: none;
30 |   border: 1px solid var(--vp-c-divider);
31 |   padding: 6px 12px;
32 |   border-radius: 8px;
33 |   background: var(--vp-c-bg-soft);
34 |   color: var(--vp-c-text-1);
35 |   cursor: pointer;
36 |   font-weight: 500;
37 | }
38 | .mcp-tabs__btn[aria-selected="true"] {
39 |   background: var(--mcp-accent);
40 |   border-color: var(--mcp-accent);
41 |   color: white;
42 | }
43 | .mcp-tabs__panel {
44 |   padding: 12px 14px;
45 | }
46 | 
47 | /* Utility blocks */
48 | .mcp-callout {
49 |   border-left: 3px solid var(--mcp-accent);
50 |   padding: 10px 14px;
51 |   margin: 10px 0;
52 |   background: var(--vp-c-bg-soft);
53 | }
54 | 
55 | .mcp-grid {
56 |   display: grid;
57 |   grid-template-columns: repeat(12, 1fr);
58 |   gap: 16px;
59 | }
60 | .mcp-col-6 { grid-column: span 6; }
61 | .mcp-col-12 { grid-column: span 12; }
62 | @media (max-width: 960px) {
63 |   .mcp-col-6 { grid-column: span 12; }
64 | }
65 | 
66 | .mcp-diagram {
67 |   width: 100%;
68 |   border: 1px dashed var(--vp-c-divider);
69 |   border-radius: 10px;
70 |   padding: 12px;
71 | }
72 | 
73 | .mcp-cta {
74 |   display: inline-flex;
75 |   align-items: center;
76 |   gap: 8px;
77 |   background: var(--mcp-accent);
78 |   color: white;
79 |   padding: 8px 12px;
80 |   border-radius: 8px;
81 |   text-decoration: none;
82 | }
83 | .mcp-cta:hover { background: var(--mcp-accent-600); }
84 | 
85 | 
```

--------------------------------------------------------------------------------
/tests/unit/routing.core.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import '../setup/test-setup.js'
 2 | import test from 'node:test'
 3 | import assert from 'node:assert/strict'
 4 | import { CircuitBreaker, CircuitOpenError } from '../../src/routing/circuit-breaker.js'
 5 | import { RetryHandler } from '../../src/routing/retry-handler.js'
 6 | import { LoadBalancer } from '../../src/routing/load-balancer.js'
 7 | 
 8 | test('CircuitBreaker opens and recovers', async () => {
 9 |   const cb = new CircuitBreaker({ failureThreshold: 2, successThreshold: 1, recoveryTimeoutMs: 10 })
10 |   const key = 'svc::inst'
11 |   await assert.rejects(cb.execute(key, async () => { throw new Error('fail') }))
12 |   await assert.rejects(cb.execute(key, async () => { throw new Error('fail') }))
13 |   // Now circuit open
14 |   await assert.rejects(cb.execute(key, async () => 'ok'), (e: any) => e instanceof CircuitOpenError)
15 |   // Wait for half-open
16 |   await new Promise((r) => setTimeout(r, 12))
17 |   const res = await cb.execute(key, async () => 'ok')
18 |   assert.equal(res, 'ok')
19 | })
20 | 
21 | test('RetryHandler retries on 5xx and succeeds', async () => {
22 |   const rh = new RetryHandler({ maxRetries: 2, baseDelayMs: 1, maxDelayMs: 2, jitter: 'none' })
23 |   let n = 0
24 |   const res = await rh.execute(async () => {
25 |     n++
26 |     if (n < 3) { const err: any = new Error('HTTP 500'); err.status = 500; throw err }
27 |     return 'ok'
28 |   })
29 |   assert.equal(res, 'ok')
30 |   assert.equal(n, 3)
31 | })
32 | 
33 | test('LoadBalancer round-robin selection', () => {
34 |   const lb = new LoadBalancer({ strategy: 'round_robin' })
35 |   const pool = [ { id: 'a' }, { id: 'b' }, { id: 'c' } ] as any
36 |   const chosen = [
37 |     lb.select('svc', pool)!.id,
38 |     lb.select('svc', pool)!.id,
39 |     lb.select('svc', pool)!.id,
40 |     lb.select('svc', pool)!.id,
41 |   ]
42 |   assert.deepEqual(chosen, ['a','b','c','a'])
43 | })
44 | 
45 | 
```

--------------------------------------------------------------------------------
/src/utils/time.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Date/time utilities, duration parsing and timezone helpers.
 3 |  */
 4 | 
 5 | export function now(): number {
 6 |   if (typeof performance !== 'undefined' && typeof performance.now === 'function') return performance.now()
 7 |   return Date.now()
 8 | }
 9 | 
10 | export function sleep(ms: number): Promise<void> {
11 |   return new Promise((resolve) => setTimeout(resolve, ms))
12 | }
13 | 
14 | /** Parses durations like "500ms", "2s", "5m", "1h", "1d". */
15 | export function parseDuration(input: string): number {
16 |   const m = String(input).trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i)
17 |   if (!m) throw new Error('Invalid duration')
18 |   const n = parseFloat(m[1])
19 |   const u = m[2].toLowerCase()
20 |   switch (u) {
21 |     case 'ms':
22 |       return n
23 |     case 's':
24 |       return n * 1000
25 |     case 'm':
26 |       return n * 60_000
27 |     case 'h':
28 |       return n * 3_600_000
29 |     case 'd':
30 |       return n * 86_400_000
31 |     default:
32 |       throw new Error('Invalid duration unit')
33 |   }
34 | }
35 | 
36 | export function formatDuration(ms: number): string {
37 |   if (ms < 1000) return `${ms}ms`
38 |   if (ms < 60_000) return `${(ms / 1000).toFixed(ms % 1000 === 0 ? 0 : 2)}s`
39 |   if (ms < 3_600_000) return `${(ms / 60_000).toFixed(ms % 60_000 === 0 ? 0 : 2)}m`
40 |   if (ms < 86_400_000) return `${(ms / 3_600_000).toFixed(ms % 3_600_000 === 0 ? 0 : 2)}h`
41 |   return `${(ms / 86_400_000).toFixed(ms % 86_400_000 === 0 ? 0 : 2)}d`
42 | }
43 | 
44 | export function toUTC(date: Date): string {
45 |   return date.toISOString()
46 | }
47 | 
48 | export function fromUnix(seconds: number): Date {
49 |   return new Date(seconds * 1000)
50 | }
51 | 
52 | export function formatInTimeZone(date: Date, timeZone: string, opts?: Intl.DateTimeFormatOptions): string {
53 |   const formatter = new Intl.DateTimeFormat('en-US', { timeZone, ...opts })
54 |   return formatter.format(date)
55 | }
56 | 
57 | 
```

--------------------------------------------------------------------------------
/tests/perf/perf.auth-and-routing.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import '../setup/test-setup.js'
 2 | import test from 'node:test'
 3 | import { performance } from 'node:perf_hooks'
 4 | import { MultiAuthManager } from '../../src/auth/multi-auth-manager.js'
 5 | import { AuthStrategy } from '../../src/types/config.js'
 6 | import { createMockServer } from '../utils/mock-http.js'
 7 | import { RequestRouter } from '../../src/modules/request-router.js'
 8 | import { CapabilityAggregator } from '../../src/modules/capability-aggregator.js'
 9 | 
10 | test('Perf: validateClientToken and routeCallTool throughput (smoke)', async (t) => {
11 |   const mam = new MultiAuthManager({ authorization_endpoint: 'http://a', token_endpoint: 'http://t', client_id: 'x', redirect_uri: 'http://l', scopes: ['openid'] } as any)
12 |   mam.registerServerAuth('s', AuthStrategy.BYPASS_AUTH)
13 |   const N = 1000
14 |   const t0 = performance.now()
15 |   for (let i = 0; i < N; i++) await mam.validateClientToken('opaque-token')
16 |   const dt = performance.now() - t0
17 |   t.diagnostic(`validateClientToken x${N}: ${Math.round(dt)}ms (${Math.round((N/dt)*1000)} ops/sec)`) // eslint-disable-line
18 | 
19 |   const upstream = await createMockServer([
20 |     { method: 'POST', path: '/mcp/tools/call', handler: () => ({ body: { content: { ok: true } } }) },
21 |   ])
22 |   const servers = new Map<string, any>([[ 's', { id: 's', type: 'node', endpoint: upstream.url, config: {} as any, status: 'running', lastHealthCheck: 0 } ]])
23 |   const rr = new RequestRouter(servers as any, new CapabilityAggregator())
24 |   const M = 200
25 |   const t1 = performance.now()
26 |   for (let i = 0; i < M; i++) await rr.routeCallTool({ name: 's.ping' })
27 |   const dt2 = performance.now() - t1
28 |   t.diagnostic(`routeCallTool x${M}: ${Math.round(dt2)}ms (${Math.round((M/dt2)*1000)} rps)`) // eslint-disable-line
29 |   await upstream.close()
30 | })
31 | 
32 | 
```

--------------------------------------------------------------------------------
/docs/public/logo.svg:
--------------------------------------------------------------------------------

```
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <svg
 3 |    viewBox="0 0 120 120"
 4 |    version="1.1"
 5 |    id="svg4"
 6 |    sodipodi:docname="logo.svg"
 7 |    inkscape:export-filename="logo.png"
 8 |    inkscape:export-xdpi="96"
 9 |    inkscape:export-ydpi="96"
10 |    inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
11 |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12 |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
13 |    xmlns="http://www.w3.org/2000/svg"
14 |    xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
15 |    id="namedview4"
16 |    pagecolor="#ffffff"
17 |    bordercolor="#000000"
18 |    borderopacity="0.25"
19 |    inkscape:showpageshadow="2"
20 |    inkscape:pageopacity="0.0"
21 |    inkscape:pagecheckerboard="0"
22 |    inkscape:deskcolor="#d1d1d1"
23 |    inkscape:zoom="2.9371694"
24 |    inkscape:cx="38.131951"
25 |    inkscape:cy="32.68453"
26 |    inkscape:window-width="1512"
27 |    inkscape:window-height="945"
28 |    inkscape:window-x="0"
29 |    inkscape:window-y="37"
30 |    inkscape:window-maximized="0"
31 |    inkscape:current-layer="svg4" />
32 | <defs
33 |    id="defs2">
34 |   <linearGradient
35 |      id="g"
36 |      x1="0"
37 |      y1="0"
38 |      x2="1"
39 |      y2="1">
40 |     <stop
41 |        offset="0%"
42 |        stop-color="#7C3AED"
43 |        id="stop1" />
44 |     <stop
45 |        offset="100%"
46 |        stop-color="#06B6D4"
47 |        id="stop2" />
48 |   </linearGradient>
49 | </defs>
50 | <circle
51 |    cx="60"
52 |    cy="60"
53 |    r="56"
54 |    fill="url(#g)"
55 |    id="circle2" />
56 | <g
57 |    fill="#fff"
58 |    id="g4">
59 |   <circle
60 |      cx="60"
61 |      cy="60"
62 |      r="10"
63 |      id="circle3" />
64 |   <path
65 |      d="M60 20 a40 40 0 0 1 0 80"
66 |      fill="none"
67 |      stroke="#fff"
68 |      stroke-width="6"
69 |      id="path3" />
70 |   <path
71 |      d="M20 60 a40 40 0 0 1 80 0"
72 |      fill="none"
73 |      stroke="#fff"
74 |      stroke-width="6"
75 |      id="path4" />
76 | </g>
77 | 
78 |   Sorry, your browser does not support inline SVG.
79 | </svg>
80 | 
```

--------------------------------------------------------------------------------
/docs/configuration/examples.md:
--------------------------------------------------------------------------------

```markdown
 1 | ---
 2 | title: Configuration Examples
 3 | ---
 4 | 
 5 | # Configuration Examples
 6 | 
 7 | Real-world scenarios to use as starting points.
 8 | 
 9 | ## Minimal Local Aggregation
10 | 
11 | ```yaml
12 | hosting:
13 |   port: 3000
14 | servers:
15 |   - id: search
16 |     type: local
17 |     auth_strategy: master_oauth
18 |     config: { port: 4100 }
19 | ```
20 | 
21 | ## Mixed Auth Strategies (GitHub Delegation)
22 | 
23 | ```yaml
24 | hosting:
25 |   port: 3000
26 |   base_url: https://your.domain
27 | servers:
28 |   - id: search
29 |     type: local
30 |     auth_strategy: master_oauth
31 |     config: { port: 4100 }
32 |   - id: github-tools
33 |     type: local
34 |     auth_strategy: delegate_oauth
35 |     auth_config:
36 |       provider: github
37 |       authorization_endpoint: https://github.com/login/oauth/authorize
38 |       token_endpoint: https://github.com/login/oauth/access_token
39 |       client_id: ${GITHUB_CLIENT_ID}
40 |       client_secret: env:GITHUB_CLIENT_SECRET
41 |       scopes: [repo, read:user]
42 |     config: { port: 4010 }
43 | routing:
44 |   retry:
45 |     maxRetries: 2
46 |     baseDelayMs: 200
47 |   circuitBreaker:
48 |     failureThreshold: 5
49 |     successThreshold: 2
50 |     recoveryTimeoutMs: 10000
51 | ```
52 | 
53 | ## Dockerized Production
54 | 
55 | ```yaml
56 | hosting:
57 |   port: 3000
58 | servers:
59 |   - id: search
60 |     type: local
61 |     auth_strategy: bypass_auth
62 |     config: { url: http://search:4100 }
63 | ```
64 | 
65 | Run with env:
66 | 
67 | ```bash
68 | TOKEN_ENC_KEY=... MASTER_BASE_URL=https://master.example.com docker compose up -d
69 | ```
70 | 
71 | ## Multi-tenant (Advanced)
72 | 
73 | In multi-tenant deployments, use separate configs per tenant and map them under different base URLs or headers. Keep secrets isolated and rotate regularly.
74 | 
75 | ```yaml
76 | # tenant-a.yaml
77 | hosting: { port: 3001 }
78 | servers: [ { id: search, type: local, auth_strategy: master_oauth, config: { port: 4110 } } ]
79 | 
80 | # tenant-b.yaml
81 | hosting: { port: 3002 }
82 | servers: [ { id: search, type: local, auth_strategy: master_oauth, config: { port: 4120 } } ]
83 | ```
84 | 
85 | 
```

--------------------------------------------------------------------------------
/tests/servers/test-auth-simple.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { MultiAuthManager } from '../../src/auth/multi-auth-manager.js'
 2 | import { AuthStrategy } from '../../src/types/config.js'
 3 | import '../setup/test-setup.js'
 4 | 
 5 | const masterCfg = {
 6 |   authorization_endpoint: 'http://localhost/auth',
 7 |   token_endpoint: 'http://localhost/token',
 8 |   client_id: 'master',
 9 |   redirect_uri: 'http://localhost/cb',
10 |   scopes: ['openid'],
11 | }
12 | 
13 | async function testAuth() {
14 |   console.log('Testing MultiAuthManager...')
15 |   
16 |   try {
17 |     const mam = new MultiAuthManager(masterCfg)
18 |     mam.registerServerAuth('srv1', AuthStrategy.MASTER_OAUTH)
19 |     const h = await mam.prepareAuthForBackend('srv1', 'CLIENT')
20 |     
21 |     if (h.Authorization === 'Bearer CLIENT') {
22 |       console.log('✅ Test 1 passed: Master OAuth pass-through')
23 |     } else {
24 |       console.log('❌ Test 1 failed:', h)
25 |     }
26 | 
27 |     // Test delegation
28 |     mam.registerServerAuth('srv2', AuthStrategy.DELEGATE_OAUTH, {
29 |       provider: 'custom', 
30 |       authorization_endpoint: 'http://p/auth', 
31 |       token_endpoint: 'http://p/token', 
32 |       client_id: 'c'
33 |     })
34 |     const d = await mam.prepareAuthForBackend('srv2', 'CLIENT')
35 |     
36 |     if (d.type === 'oauth_delegation') {
37 |       console.log('✅ Test 2 passed: OAuth delegation')
38 |     } else {
39 |       console.log('❌ Test 2 failed:', d)
40 |     }
41 | 
42 |     // Test storage
43 |     await mam.storeDelegatedToken('CLIENT', 'srv', { access_token: 'S', expires_at: Date.now() + 1000, scope: [] })
44 |     const tok = await mam.getStoredServerToken('srv', 'CLIENT')
45 |     
46 |     if (tok === 'S') {
47 |       console.log('✅ Test 3 passed: Token storage')
48 |     } else {
49 |       console.log('❌ Test 3 failed:', tok)
50 |     }
51 |     
52 |     console.log('All tests completed successfully!')
53 |     
54 |   } catch (error) {
55 |     console.error('Test failed:', error)
56 |     console.error('Stack:', error.stack)
57 |   }
58 | }
59 | 
60 | testAuth()
```

--------------------------------------------------------------------------------
/tests/e2e/flow-controller.express.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import '../setup/test-setup.js'
 2 | import test from 'node:test'
 3 | import assert from 'node:assert/strict'
 4 | import { OAuthFlowController } from '../../src/oauth/flow-controller.js'
 5 | import { FakeExpressApp } from '../utils/fake-express.js'
 6 | import { createMockServer } from '../utils/mock-http.js'
 7 | 
 8 | test('OAuthFlowController Express flow: authorize -> token -> callback', async () => {
 9 |   const tokenSrv = await createMockServer([
10 |     { method: 'POST', path: '/token', handler: (_req, _body) => ({ body: { access_token: 'AT', expires_in: 60, scope: 'openid' } }) },
11 |   ])
12 |   try {
13 |     const cfg = {
14 |       master_oauth: {
15 |         authorization_endpoint: tokenSrv.url + '/authorize',
16 |         token_endpoint: tokenSrv.url + '/token',
17 |         client_id: 'cid',
18 |         redirect_uri: 'http://localhost/oauth/callback',
19 |         scopes: ['openid'],
20 |       },
21 |       hosting: { platform: 'node', base_url: 'http://localhost' },
22 |       servers: [],
23 |     }
24 |     const ctrl = new OAuthFlowController({ getConfig: () => cfg as any })
25 |     const app = new FakeExpressApp()
26 |     ctrl.registerExpress(app as any)
27 | 
28 |     const auth = await app.invoke('GET', '/oauth/authorize', { query: { provider: 'master' } })
29 |     assert.equal(auth.status, 200)
30 |     assert.match(String(auth.body), /Redirecting/)
31 |     const m = String(auth.body).match(/url=([^"\s]+)/)
32 |     assert.ok(m && m[1])
33 |     const urlStr = m[1].replace(/&amp;/g, '&') // Decode HTML entities
34 |     const url = new URL(urlStr)
35 |     const state = url.searchParams.get('state')!
36 |     assert.ok(state)
37 | 
38 |     const cb = await app.invoke('GET', '/oauth/callback', { query: { state, code: 'good', provider: 'master' } })
39 |     assert.equal(cb.status, 200)
40 |     assert.match(String(cb.body), /Authorization complete|You may close this window/)
41 |   } finally {
42 |     await tokenSrv.close()
43 |   }
44 | })
45 | 
46 | 
```

--------------------------------------------------------------------------------
/deploy/docker/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # syntax=docker/dockerfile:1.7
 2 | 
 3 | ARG NODE_VERSION=20
 4 | FROM node:${NODE_VERSION}-slim AS base
 5 | ENV PNPM_HOME=/usr/local/share/pnpm \
 6 |     NODE_ENV=production \
 7 |     APP_HOME=/app
 8 | WORKDIR ${APP_HOME}
 9 | 
10 | # ---------- Builder ----------
11 | FROM base AS builder
12 | ENV NODE_ENV=development
13 | SHELL ["/bin/sh", "-lc"]
14 | RUN --mount=type=cache,target=/var/cache/apt \
15 |     --mount=type=cache,target=/var/lib/apt/lists \
16 |     apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
17 | COPY package.json package-lock.json ./
18 | RUN --mount=type=cache,target=/root/.npm npm ci
19 | COPY tsconfig*.json ./
20 | COPY src ./src
21 | COPY config ./config
22 | COPY static ./static
23 | RUN npm run build:node
24 | 
25 | # ---------- Runtime ----------
26 | FROM base AS runtime
27 | SHELL ["/bin/sh", "-lc"]
28 | # Create non-root user
29 | RUN useradd -r -u 10001 -g root nodejs && mkdir -p /app && chown -R nodejs:root /app
30 | 
31 | # Only production deps
32 | COPY package.json package-lock.json ./
33 | RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --ignore-scripts && npm cache clean --force
34 | 
35 | # Copy build artifacts and minimal runtime assets
36 | COPY --from=builder /app/dist/node ./dist/node
37 | COPY --from=builder /app/config ./config
38 | COPY --from=builder /app/static ./static
39 | COPY deploy/docker/entrypoint.sh /entrypoint.sh
40 | RUN chmod +x /entrypoint.sh
41 | 
42 | ENV NODE_ENV=production \
43 |     LOG_FORMAT=json \
44 |     PORT=3000 \
45 |     MASTER_HOSTING_PLATFORM=node
46 | 
47 | EXPOSE 3000
48 | USER nodejs
49 | ENTRYPOINT ["/entrypoint.sh"]
50 | CMD ["node", "dist/node/index.js"]
51 | 
52 | # Healthcheck without adding curl/wget: use Node's http module
53 | HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \
54 |   CMD node -e "const http=require('http');const p=process.env.PORT||3000;http.get({host:'127.0.0.1',port:p,path:'/health'},r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"
55 | 
56 | 
```

--------------------------------------------------------------------------------
/src/utils/dev.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Development and debugging helpers.
 3 |  */
 4 | 
 5 | export function isDev(): boolean {
 6 |   const env = (globalThis as any)?.process?.env
 7 |   return env?.NODE_ENV !== 'production'
 8 | }
 9 | 
10 | export function debugLog(...args: unknown[]): void {
11 |   if (!isDev()) return
12 |   // eslint-disable-next-line no-console
13 |   console.debug('[DEV]', ...args)
14 | }
15 | 
16 | export function invariant(condition: unknown, message = 'Invariant failed'): asserts condition {
17 |   if (!condition) throw new Error(message)
18 | }
19 | 
20 | export function assertNever(x: never, message = 'Unexpected object'): never {
21 |   throw new Error(`${message}: ${String(x)}`)
22 | }
23 | 
24 | export function pretty(value: unknown): string {
25 |   try {
26 |     const util = (globalThis as any).require ? (globalThis as any).require('node:util') : undefined
27 |     if (util?.inspect) return util.inspect(value, { depth: 4, colors: true })
28 |   } catch {
29 |     // ignore
30 |   }
31 |   try {
32 |     return JSON.stringify(value, null, 2)
33 |   } catch {
34 |     return String(value)
35 |   }
36 | }
37 | 
38 | export function deprecate(fn: (...args: any[]) => any, message: string): (...args: any[]) => any {
39 |   let warned = false
40 |   return (...args: any[]) => {
41 |     if (!warned) {
42 |       warned = true
43 |       // eslint-disable-next-line no-console
44 |       console.warn(`[DEPRECATED] ${message}`)
45 |     }
46 |     return fn(...args)
47 |   }
48 | }
49 | 
50 | export function withTiming<T>(name: string, fn: () => T): { result: T; durationMs: number; name: string } {
51 |   const start = typeof performance !== 'undefined' ? performance.now() : Date.now()
52 |   const result = fn()
53 |   const durationMs = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - start
54 |   return { result, durationMs, name }
55 | }
56 | 
57 | export function sleep(ms: number): Promise<void> {
58 |   return new Promise((res) => setTimeout(res, ms))
59 | }
60 | 
61 | export function mockRandom(fn: () => number): () => void {
62 |   const original = Math.random
63 |   ;(Math as any).random = fn
64 |   return () => {
65 |     ;(Math as any).random = original
66 |   }
67 | }
68 | 
```

--------------------------------------------------------------------------------
/tests/servers/test-master-mcp.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'
 2 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
 3 | 
 4 | async function runTest() {
 5 |   try {
 6 |     console.log('Testing Master MCP Server...')
 7 |     
 8 |     // Create a streamable HTTP transport to connect to our MCP server
 9 |     const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
10 |     
11 |     // Create the MCP client
12 |     const client = new Client({
13 |       name: 'master-mcp-test-client',
14 |       version: '1.0.0'
15 |     })
16 |     
17 |     // Initialize the client
18 |     await client.connect(transport)
19 |     console.log('✅ Server initialized')
20 |     console.log('Server info:', client.getServerVersion())
21 |     console.log('Protocol version:', client.getServerCapabilities())
22 |     
23 |     // List tools
24 |     console.log('\n--- Testing tools/list ---')
25 |     const toolsResult = await client.listTools({})
26 |     console.log('✅ tools/list successful')
27 |     console.log('Number of tools:', toolsResult.tools.length)
28 |     console.log('Tools:', toolsResult.tools.map(t => t.name))
29 |     
30 |     // List resources
31 |     console.log('\n--- Testing resources/list ---')
32 |     const resourcesResult = await client.listResources({})
33 |     console.log('✅ resources/list successful')
34 |     console.log('Number of resources:', resourcesResult.resources.length)
35 |     console.log('Resources:', resourcesResult.resources.map(r => r.uri))
36 |     
37 |     // Test ping
38 |     console.log('\n--- Testing ping ---')
39 |     const pingResult = await client.ping()
40 |     console.log('✅ ping successful')
41 |     console.log('Ping result:', pingResult)
42 |     
43 |     // Close the connection
44 |     await client.close()
45 |     console.log('\n✅ Disconnected from MCP server')
46 |     console.log('\n🎉 All tests completed successfully!')
47 |     
48 |   } catch (error) {
49 |     console.error('❌ Test failed:', error)
50 |     console.error('Error stack:', error.stack)
51 |   }
52 | }
53 | 
54 | // Run the test
55 | runTest()
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # syntax=docker/dockerfile:1.7
 2 | 
 3 | # Multi-stage build for production and dev with multi-arch support.
 4 | # Targets:
 5 | # - base: common base image
 6 | # - deps: install all dependencies (including dev)
 7 | # - build: compile TypeScript to dist
 8 | # - prod-deps: install only production deps
 9 | # - runner: minimal runtime image
10 | # - dev: development image with hot-reload support
11 | 
12 | ARG NODE_VERSION=20.14.0
13 | ARG ALPINE_VERSION=3.19
14 | 
15 | FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS base
16 | ENV NODE_ENV=production \
17 |     APP_HOME=/app \
18 |     PNPM_HOME=/pnpm
19 | WORKDIR ${APP_HOME}
20 | RUN addgroup -g 1001 -S nodejs && adduser -S node -u 1001 -G nodejs
21 | 
22 | FROM base AS deps
23 | ENV NODE_ENV=development
24 | COPY package*.json ./
25 | # Prefer npm ci for reproducible installs
26 | RUN --mount=type=cache,target=/root/.npm \
27 |     npm ci
28 | 
29 | FROM deps AS build
30 | COPY tsconfig*.json ./
31 | COPY src ./src
32 | COPY config ./config
33 | COPY static ./static
34 | RUN npm run build
35 | 
36 | FROM deps AS prod-deps
37 | ENV NODE_ENV=production
38 | RUN --mount=type=cache,target=/root/.npm \
39 |     npm prune --omit=dev
40 | 
41 | FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS runner
42 | ENV NODE_ENV=production \
43 |     APP_HOME=/app \
44 |     PORT=3000
45 | WORKDIR ${APP_HOME}
46 | # Busybox wget is enough for healthcheck; curl can be used if preferred
47 | RUN apk add --no-cache wget
48 | 
49 | # Copy built app and production deps
50 | COPY --from=prod-deps ${APP_HOME}/node_modules ./node_modules
51 | COPY --from=build ${APP_HOME}/dist ./dist
52 | COPY package*.json ./
53 | COPY config ./config
54 | COPY static ./static
55 | 
56 | # Use non-root user
57 | USER 1001
58 | 
59 | EXPOSE 3000
60 | HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
61 |   CMD wget -qO- http://127.0.0.1:${PORT}/health || exit 1
62 | 
63 | # Default start command
64 | CMD ["node", "dist/node/index.js"]
65 | 
66 | # Development image with hot reloading (nodemon)
67 | FROM deps AS dev
68 | ENV NODE_ENV=development \
69 |     PORT=3000
70 | RUN npm pkg set scripts.dev:watch="nodemon --watch src --ext ts,tsx,json --exec 'node --loader ts-node/esm src/index.ts'" && \
71 |     npm i -D nodemon@^3
72 | CMD ["npm", "run", "dev:watch"]
73 | 
74 | 
```

--------------------------------------------------------------------------------
/src/oauth/web-interface.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export class WebInterface {
 2 |   // Minimal, accessible pages for success and error. CSS served from /static/oauth/style.css
 3 |   renderRedirectPage(providerName: string, redirectUrl: string): string {
 4 |     const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
 5 |     return `<!doctype html>
 6 | <html lang="en">
 7 | <head>
 8 |   <meta charset="utf-8" />
 9 |   <meta name="viewport" content="width=device-width, initial-scale=1" />
10 |   <title>Continue to ${esc(providerName)}</title>
11 |   <link rel="stylesheet" href="/static/oauth/style.css" />
12 |   <meta http-equiv="refresh" content="0;url=${esc(redirectUrl)}" />
13 |   <script>location.replace(${JSON.stringify(redirectUrl)})</script>
14 |   </head>
15 | <body>
16 |   <main class="container">
17 |     <h1>Redirecting…</h1>
18 |     <p>Taking you to ${esc(providerName)} to sign in.</p>
19 |     <p><a class="button" href="${esc(redirectUrl)}">Continue</a></p>
20 |   </main>
21 | </body>
22 | </html>`
23 |   }
24 | 
25 |   renderSuccessPage(message = 'Authorization completed successfully.'): string {
26 |     const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
27 |     return `<!doctype html>
28 | <html lang="en">
29 | <head>
30 |   <meta charset="utf-8" />
31 |   <meta name="viewport" content="width=device-width, initial-scale=1" />
32 |   <title>OAuth Success</title>
33 |   <link rel="stylesheet" href="/static/oauth/style.css" />
34 | </head>
35 | <body>
36 |   <main class="container">
37 |     <h1>Success</h1>
38 |     <p>${esc(message)}</p>
39 |   </main>
40 | </body>
41 | </html>`
42 |   }
43 | 
44 |   renderErrorPage(error = 'Authorization failed.'): string {
45 |     const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
46 |     return `<!doctype html>
47 | <html lang="en">
48 | <head>
49 |   <meta charset="utf-8" />
50 |   <meta name="viewport" content="width=device-width, initial-scale=1" />
51 |   <title>OAuth Error</title>
52 |   <link rel="stylesheet" href="/static/oauth/style.css" />
53 | </head>
54 | <body>
55 |   <main class="container error">
56 |     <h1>Authorization Error</h1>
57 |     <p>${esc(error)}</p>
58 |   </main>
59 | </body>
60 | </html>`
61 |   }
62 | }
63 | 
64 | 
```

--------------------------------------------------------------------------------
/tests/servers/test-streaming.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'
 2 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
 3 | 
 4 | async function runStreamingTest() {
 5 |   try {
 6 |     console.log('Testing Master MCP Server with HTTP Streaming...')
 7 |     
 8 |     // Create a streamable HTTP transport to connect to our MCP server
 9 |     const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
10 |     
11 |     // Create the MCP client
12 |     const client = new Client({
13 |       name: 'master-mcp-streaming-test-client',
14 |       version: '1.0.0'
15 |     })
16 |     
17 |     // Initialize the client
18 |     await client.connect(transport)
19 |     console.log('✅ Server initialized with streaming transport')
20 |     console.log('Server info:', client.getServerVersion())
21 |     console.log('Server capabilities:', client.getServerCapabilities())
22 |     
23 |     // List tools using streaming
24 |     console.log('\n--- Testing tools/list with streaming ---')
25 |     const toolsResult = await client.listTools({})
26 |     console.log('✅ tools/list successful with streaming')
27 |     console.log('Number of tools:', toolsResult.tools.length)
28 |     
29 |     // List resources using streaming
30 |     console.log('\n--- Testing resources/list with streaming ---')
31 |     const resourcesResult = await client.listResources({})
32 |     console.log('✅ resources/list successful with streaming')
33 |     console.log('Number of resources:', resourcesResult.resources.length)
34 |     
35 |     // Test ping
36 |     console.log('\n--- Testing ping with streaming ---')
37 |     const pingResult = await client.ping()
38 |     console.log('✅ ping successful with streaming')
39 |     console.log('Ping result:', pingResult)
40 |     
41 |     // Close the connection
42 |     await client.close()
43 |     console.log('\n✅ Disconnected from MCP server')
44 |     console.log('\n🎉 All streaming tests completed successfully!')
45 |     
46 |   } catch (error) {
47 |     console.error('❌ Streaming test failed:', error)
48 |     console.error('Error stack:', error.stack)
49 |   }
50 | }
51 | 
52 | // Run the streaming test
53 | runStreamingTest()
```

--------------------------------------------------------------------------------
/docs/guides/server-management.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Server Management
 2 | 
 3 | The `MasterServer` orchestrates backend servers and exposes convenience APIs.
 4 | 
 5 | ## Key APIs
 6 | 
 7 | - `startFromConfig(config, clientToken?)`: Load and health-check backends, discover capabilities
 8 | - `performHealthChecks(clientToken?)`: Returns `{ [serverId]: boolean }`
 9 | - `restartServer(id)`: Restarts a backend (when supported)
10 | - `unloadAll()`: Stops and clears all backends
11 | - `getRouter()`: Access to `RequestRouter`
12 | - `getAggregatedTools()/getAggregatedResources()`: Current aggregated definitions
13 | - `attachAuthManager(multiAuth)`: Injects a `MultiAuthManager`
14 | - `getOAuthFlowController()`: Provides an OAuth controller to mount in your runtime
15 | 
16 | ## Node Runtime
17 | 
18 | `src/index.ts` creates an Express app exposing health, metrics, OAuth endpoints, and MCP HTTP endpoints. Use `npm run dev` during development.
19 | 
20 | ## Workers Runtime
21 | 
22 | `src/runtime/worker.ts` exports a `fetch` handler integrating the protocol and OAuth flows. Configure via `deploy/cloudflare/wrangler.toml`.
23 | 
24 | ## Adding Backends by Source
25 | 
26 | > Note: Some origin types (git/npm/pypi/docker) are treated as config-driven endpoints in the current loader. Provide an explicit `url` or `port` for the running backend.
27 | 
28 | <CodeTabs :options="[
29 |   { label: 'Local', value: 'local' },
30 |   { label: 'Git', value: 'git' },
31 |   { label: 'NPM', value: 'npm' },
32 |   { label: 'Docker', value: 'docker' }
33 | ]">
34 |   <template #local>
35 | 
36 | ```yaml
37 | servers:
38 |   - id: search
39 |     type: local
40 |     auth_strategy: master_oauth
41 |     config: { port: 4100 }
42 | ```
43 | 
44 |   </template>
45 |   <template #git>
46 | 
47 | ```yaml
48 | servers:
49 |   - id: tools-from-git
50 |     type: git
51 |     auth_strategy: bypass_auth
52 |     config:
53 |       url: http://git-tools.internal:4010
54 | ```
55 | 
56 |   </template>
57 |   <template #npm>
58 | 
59 | ```yaml
60 | servers:
61 |   - id: npm-tools
62 |     type: npm
63 |     auth_strategy: proxy_oauth
64 |     config:
65 |       url: http://npm-tools:4020
66 | ```
67 | 
68 |   </template>
69 |   <template #docker>
70 | 
71 | ```yaml
72 | servers:
73 |   - id: containerized
74 |     type: docker
75 |     auth_strategy: master_oauth
76 |     config:
77 |       url: http://containerized:4030
78 | ```
79 | 
80 |   </template>
81 | </CodeTabs>
82 | 
83 | 
```

--------------------------------------------------------------------------------
/src/oauth/state-manager.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // State Manager for OAuth CSRF protection
 2 | // Generates random opaque state tokens and tracks associated payload with TTL
 3 | 
 4 | export interface OAuthStatePayload {
 5 |   provider?: string
 6 |   serverId?: string
 7 |   clientToken?: string
 8 |   returnTo?: string
 9 |   issuedAt: number
10 | }
11 | 
12 | export interface StateRecord {
13 |   payload: OAuthStatePayload
14 |   expiresAt: number
15 | }
16 | 
17 | export interface StateManagerOptions {
18 |   ttlMs?: number
19 | }
20 | 
21 | function getCrypto(): any {
22 |   const g: any = globalThis as any
23 |   if (g.crypto && g.crypto.subtle && g.crypto.getRandomValues) return g.crypto as any
24 |   try {
25 |     // eslint-disable-next-line @typescript-eslint/no-var-requires
26 |     const nodeCrypto = require('node:crypto')
27 |     return nodeCrypto.webcrypto as any
28 |   } catch {
29 |     throw new Error('Secure crypto not available in this environment')
30 |   }
31 | }
32 | 
33 | function randomId(bytes = 32): string {
34 |   const crypto = getCrypto()
35 |   const arr = new Uint8Array(bytes)
36 |   crypto.getRandomValues(arr)
37 |   let str = ''
38 |   for (let i = 0; i < arr.length; i++) str += arr[i].toString(16).padStart(2, '0')
39 |   return str
40 | }
41 | 
42 | export class StateManager {
43 |   private readonly store = new Map<string, StateRecord>()
44 |   private readonly ttl: number
45 | 
46 |   constructor(options?: StateManagerOptions) {
47 |     this.ttl = options?.ttlMs ?? 10 * 60_000
48 |   }
49 | 
50 |   create(payload: Omit<OAuthStatePayload, 'issuedAt'>): string {
51 |     const state = randomId(32)
52 |     const now = Date.now()
53 |     this.store.set(state, { payload: { ...payload, issuedAt: now }, expiresAt: now + this.ttl })
54 |     return state
55 |   }
56 | 
57 |   consume(state: string): OAuthStatePayload | null {
58 |     const rec = this.store.get(state)
59 |     if (!rec) return null
60 |     this.store.delete(state)
61 |     if (rec.expiresAt <= Date.now()) return null
62 |     return rec.payload
63 |   }
64 | 
65 |   peek(state: string): OAuthStatePayload | null {
66 |     const rec = this.store.get(state)
67 |     if (!rec || rec.expiresAt <= Date.now()) return null
68 |     return rec.payload
69 |   }
70 | 
71 |   cleanup(): void {
72 |     const now = Date.now()
73 |     for (const [k, v] of this.store) if (v.expiresAt <= now) this.store.delete(k)
74 |   }
75 | }
76 | 
```

--------------------------------------------------------------------------------
/tests/utils/mock-http.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import http from 'node:http'
 2 | 
 3 | export type Handler = (req: http.IncomingMessage, body: any) => { status?: number; headers?: Record<string, string>; body?: any }
 4 | 
 5 | export interface Route {
 6 |   method: string
 7 |   path: string | RegExp
 8 |   handler: Handler
 9 | }
10 | 
11 | export interface MockServer {
12 |   url: string
13 |   port: number
14 |   close: () => Promise<void>
15 | }
16 | 
17 | export function createMockServer(routes: Route[], opts?: { port?: number }): Promise<MockServer> {
18 |   const server = http.createServer(async (req, res) => {
19 |     const url = new URL(req.url || '/', `http://${req.headers.host}`)
20 |     const chunks: Buffer[] = []
21 |     for await (const c of req) chunks.push(c as Buffer)
22 |     let body: any = Buffer.concat(chunks).toString('utf8')
23 |     const ct = (req.headers['content-type'] || '').toString()
24 |     try {
25 |       if (ct.includes('application/json') && body) body = JSON.parse(body)
26 |       else if (ct.includes('application/x-www-form-urlencoded') && body) body = Object.fromEntries(new URLSearchParams(body))
27 |     } catch {
28 |       // leave as raw string
29 |     }
30 | 
31 |     const route = routes.find((r) => r.method.toUpperCase() === (req.method || '').toUpperCase() &&
32 |       (typeof r.path === 'string' ? r.path === url.pathname : r.path.test(url.pathname)))
33 | 
34 |     const result = route ? route.handler(req, body) : { status: 404, body: { error: 'not found' } }
35 |     const status = result.status ?? 200
36 |     const headers = result.headers ?? { 'content-type': 'application/json' }
37 |     const payload = result.body ?? { ok: true }
38 |     res.statusCode = status
39 |     for (const [k, v] of Object.entries(headers)) res.setHeader(k, v)
40 |     res.end(typeof payload === 'string' || Buffer.isBuffer(payload) ? payload : JSON.stringify(payload))
41 |   })
42 | 
43 |   return new Promise((resolve) => {
44 |     server.listen(opts?.port ?? 0, () => {
45 |       const addr = server.address()
46 |       const port = typeof addr === 'object' && addr ? addr.port : (opts?.port ?? 0)
47 |       resolve({
48 |         url: `http://localhost:${port}`,
49 |         port,
50 |         close: () => new Promise((r) => server.close(() => r())),
51 |       })
52 |     })
53 |   })
54 | }
55 | 
56 | 
```

--------------------------------------------------------------------------------
/src/routing/load-balancer.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export type LoadBalancingStrategy = 'round_robin' | 'weighted' | 'health'
 2 | 
 3 | export interface LoadBalancingInstance {
 4 |   id: string
 5 |   weight?: number
 6 |   healthScore?: number // 0..100; higher is better
 7 | }
 8 | 
 9 | export interface LoadBalancerOptions {
10 |   strategy: LoadBalancingStrategy
11 | }
12 | 
13 | export class LoadBalancer {
14 |   private readonly opts: Required<LoadBalancerOptions>
15 |   private rrIndex: Map<string, number> = new Map()
16 | 
17 |   constructor(options?: Partial<LoadBalancerOptions>) {
18 |     this.opts = { strategy: options?.strategy ?? 'round_robin' }
19 |   }
20 | 
21 |   select<T extends LoadBalancingInstance>(key: string, instances: T[]): T | undefined {
22 |     if (!instances.length) return undefined
23 |     switch (this.opts.strategy) {
24 |       case 'weighted':
25 |         return this.selectWeighted(instances)
26 |       case 'health':
27 |         return this.selectHealth(instances, key)
28 |       case 'round_robin':
29 |       default:
30 |         return this.selectRoundRobin(key, instances)
31 |     }
32 |   }
33 | 
34 |   private selectRoundRobin<T extends LoadBalancingInstance>(key: string, instances: T[]): T {
35 |     const idx = this.rrIndex.get(key) ?? 0
36 |     const chosen = instances[idx % instances.length]
37 |     this.rrIndex.set(key, (idx + 1) % instances.length)
38 |     return chosen
39 |   }
40 | 
41 |   private selectWeighted<T extends LoadBalancingInstance>(instances: T[]): T {
42 |     const weights = instances.map((i) => Math.max(1, Math.floor(i.weight ?? 1)))
43 |     const total = weights.reduce((a, b) => a + b, 0)
44 |     let r = Math.random() * total
45 |     for (let i = 0; i < instances.length; i++) {
46 |       if (r < weights[i]) return instances[i]
47 |       r -= weights[i]
48 |     }
49 |     return instances[0]
50 |   }
51 | 
52 |   private selectHealth<T extends LoadBalancingInstance>(instances: T[], key: string): T {
53 |     // Choose highest health; tie-break with RR for stability
54 |     const sorted = [...instances].sort((a, b) => (b.healthScore ?? 0) - (a.healthScore ?? 0))
55 |     const topScore = sorted[0].healthScore ?? 0
56 |     const top = sorted.filter((i) => (i.healthScore ?? 0) === topScore)
57 |     if (top.length === 1) return top[0]
58 |     return this.selectRoundRobin(key + '::health', top)
59 |   }
60 | }
61 | 
```

--------------------------------------------------------------------------------
/tests/_utils/test-server.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import http from 'node:http'
 2 | 
 3 | export interface RouteHandler {
 4 |   (req: http.IncomingMessage, body: string | undefined): { status?: number; headers?: Record<string, string>; body?: any }
 5 | }
 6 | 
 7 | export interface TestServer {
 8 |   url: string
 9 |   port: number
10 |   close: () => Promise<void>
11 |   register: (method: string, path: string, handler: RouteHandler) => void
12 | }
13 | 
14 | export function createTestServer(): Promise<TestServer> {
15 |   const routes = new Map<string, RouteHandler>()
16 |   const server = http.createServer(async (req, res) => {
17 |     try {
18 |       const url = new URL(req.url || '/', 'http://localhost')
19 |       const key = `${(req.method || 'GET').toUpperCase()} ${url.pathname}`
20 |       let body: string | undefined
21 |       if (req.method && ['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) {
22 |         body = await new Promise<string>((resolve) => {
23 |           const chunks: Buffer[] = []
24 |           req.on('data', (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(String(c))))
25 |           req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
26 |         })
27 |       }
28 |       const handler = routes.get(key)
29 |       if (!handler) {
30 |         res.statusCode = 404
31 |         res.end('not found')
32 |         return
33 |       }
34 |       const result = handler(req, body)
35 |       const status = result.status ?? 200
36 |       const headers = { 'content-type': 'application/json', ...(result.headers ?? {}) }
37 |       const payload = typeof result.body === 'string' ? result.body : JSON.stringify(result.body ?? { ok: true })
38 |       res.writeHead(status, headers)
39 |       res.end(payload)
40 |     } catch (err: any) {
41 |       res.writeHead(500, { 'content-type': 'application/json' })
42 |       res.end(JSON.stringify({ error: err?.message ?? 'internal error' }))
43 |     }
44 |   })
45 | 
46 |   return new Promise((resolve) => {
47 |     server.listen(0, '127.0.0.1', () => {
48 |       const addr = server.address()
49 |       const port = typeof addr === 'object' && addr ? addr.port : 0
50 |       resolve({
51 |         port,
52 |         url: `http://127.0.0.1:${port}`,
53 |         close: () => new Promise<void>((r) => server.close(() => r())),
54 |         register: (method: string, path: string, handler: RouteHandler) => {
55 |           routes.set(`${method.toUpperCase()} ${path}`, handler)
56 |         },
57 |       })
58 |     })
59 |   })
60 | }
61 | 
62 | 
```

--------------------------------------------------------------------------------
/src/server/protocol-handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type {
 2 |   CallToolRequest,
 3 |   CallToolResult,
 4 |   ListResourcesRequest,
 5 |   ListResourcesResult,
 6 |   ListToolsRequest,
 7 |   ListToolsResult,
 8 |   ReadResourceRequest,
 9 |   ReadResourceResult,
10 |   SubscribeRequest,
11 |   SubscribeResult,
12 | } from '../types/mcp.js'
13 | import type { CapabilityAggregator } from '../modules/capability-aggregator.js'
14 | import type { RequestRouter } from '../modules/request-router.js'
15 | import { Logger } from '../utils/logger.js'
16 | 
17 | export interface ProtocolContext {
18 |   aggregator: CapabilityAggregator
19 |   router: RequestRouter
20 |   // Optional client bearer token provided by gateway
21 |   getClientToken?: () => string | undefined
22 | }
23 | 
24 | export class ProtocolHandler {
25 |   constructor(private readonly ctx: ProtocolContext) {}
26 | 
27 |   async handleListTools(_req: ListToolsRequest): Promise<ListToolsResult> {
28 |     try {
29 |       const tools = this.ctx.aggregator.getAllTools(this.ctx.router.getServers())
30 |       return { tools }
31 |     } catch (err) {
32 |       Logger.error('handleListTools failed', err)
33 |       return { tools: [] }
34 |     }
35 |   }
36 | 
37 |   async handleCallTool(req: CallToolRequest): Promise<CallToolResult> {
38 |     try {
39 |       const token = this.ctx.getClientToken?.()
40 |       const res = await this.ctx.router.routeCallTool(req, token)
41 |       return res
42 |     } catch (err) {
43 |       Logger.warn('handleCallTool error', err)
44 |       return { content: { error: String(err) }, isError: true }
45 |     }
46 |   }
47 | 
48 |   async handleListResources(_req: ListResourcesRequest): Promise<ListResourcesResult> {
49 |     try {
50 |       const resources = this.ctx.aggregator.getAllResources(this.ctx.router.getServers())
51 |       return { resources }
52 |     } catch (err) {
53 |       Logger.error('handleListResources failed', err)
54 |       return { resources: [] }
55 |     }
56 |   }
57 | 
58 |   async handleReadResource(req: ReadResourceRequest): Promise<ReadResourceResult> {
59 |     try {
60 |       const token = this.ctx.getClientToken?.()
61 |       const res = await this.ctx.router.routeReadResource(req, token)
62 |       return res
63 |     } catch (err) {
64 |       Logger.warn('handleReadResource error', err)
65 |       return { contents: String(err), mimeType: 'text/plain' }
66 |     }
67 |   }
68 | 
69 |   async handleSubscribe(_req: SubscribeRequest): Promise<SubscribeResult> {
70 |     // Event subscriptions not yet surfaced; return OK for MCP compatibility
71 |     return { ok: true }
72 |   }
73 | }
74 | 
```

--------------------------------------------------------------------------------
/tests/integration/oauth.callback-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import '../setup/test-setup.js'
 2 | import test from 'node:test'
 3 | import assert from 'node:assert/strict'
 4 | 
 5 | // Test imports one by one to isolate the issue
 6 | console.log('Importing PKCEManager...')
 7 | import { PKCEManager } from '../../src/oauth/pkce-manager.js'
 8 | console.log('Importing StateManager...')
 9 | import { StateManager } from '../../src/oauth/state-manager.js'
10 | console.log('Importing createMockServer...')
11 | import { createMockServer } from '../utils/mock-http.js'
12 | console.log('Importing CallbackHandler...')
13 | import { CallbackHandler } from '../../src/oauth/callback-handler.js'
14 | console.log('All imports successful')
15 | 
16 | test.skip('CallbackHandler exchanges code and stores token', async () => {
17 |   const tokenSrv = await createMockServer([
18 |     { method: 'POST', path: '/token', handler: (_req, body) => {
19 |       if (body.code === 'good') return { body: { access_token: 'AT', expires_in: 60, scope: 'openid' } }
20 |       return { status: 400, body: { error: 'bad code' } }
21 |     } },
22 |   ])
23 |   try {
24 |     try {
25 |     const pkce = new PKCEManager()
26 |     const stateMgr = new StateManager()
27 |     const state = stateMgr.create({ provider: 'prov', serverId: 'srv', clientToken: 'CT', returnTo: '/done' })
28 |     const { verifier } = await pkce.generate(state)
29 |     // pkce manager consumes verifier on getVerifier(), which CallbackHandler will do
30 |     const cfg: any = {
31 |       master_oauth: { authorization_endpoint: tokenSrv.url + '/auth', token_endpoint: tokenSrv.url + '/token', client_id: 'cid', redirect_uri: tokenSrv.url + '/cb', scopes: ['openid'] },
32 |       hosting: { platform: 'node' },
33 |       servers: [],
34 |     }
35 |     let stored: any
36 |     const cb = new CallbackHandler({ config: cfg, stateManager: stateMgr, pkceManager: pkce, baseUrl: tokenSrv.url, storeDelegatedToken: async (ct, sid, tok) => { stored = { ct, sid, tok } } })
37 |     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' })
38 |     assert.ok(res.token)
39 |     assert.equal(stored.ct, 'CT')
40 |     assert.equal(stored.sid, 'srv')
41 |     assert.equal(stored.tok.access_token, 'AT')
42 |     } catch (error) {
43 |       console.error('Test error:', error)
44 |       throw error
45 |     }
46 |   } finally {
47 |     await tokenSrv.close()
48 |   }
49 | })
50 | 
51 | 
```

--------------------------------------------------------------------------------
/src/oauth/flow-validator.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type { MasterConfig, ServerAuthConfig } from '../types/config.js'
 2 | 
 3 | export interface ProviderResolution {
 4 |   providerId: string
 5 |   serverId?: string
 6 |   config: ServerAuthConfig
 7 | }
 8 | 
 9 | export class FlowValidator {
10 |   constructor(private readonly getConfig: () => MasterConfig) {}
11 | 
12 |   resolveProvider(params: { provider?: string | null; serverId?: string | null }): ProviderResolution {
13 |     const cfg = this.getConfig()
14 |     const provider = params.provider ?? undefined
15 |     const serverId = params.serverId ?? undefined
16 | 
17 |     if (!provider && !serverId) {
18 |       return {
19 |         providerId: 'master',
20 |         config: {
21 |           provider: 'custom',
22 |           authorization_endpoint: cfg.master_oauth.authorization_endpoint,
23 |           token_endpoint: cfg.master_oauth.token_endpoint,
24 |           client_id: cfg.master_oauth.client_id,
25 |           client_secret: cfg.master_oauth.client_secret,
26 |           scopes: cfg.master_oauth.scopes,
27 |         },
28 |       }
29 |     }
30 | 
31 |     if (provider === 'master') {
32 |       return {
33 |         providerId: 'master',
34 |         config: {
35 |           provider: 'custom',
36 |           authorization_endpoint: cfg.master_oauth.authorization_endpoint,
37 |           token_endpoint: cfg.master_oauth.token_endpoint,
38 |           client_id: cfg.master_oauth.client_id,
39 |           client_secret: cfg.master_oauth.client_secret,
40 |           scopes: cfg.master_oauth.scopes,
41 |         },
42 |       }
43 |     }
44 | 
45 |     if (serverId) {
46 |       const server = cfg.servers.find((s) => s.id === serverId)
47 |       if (!server || !server.auth_config) throw new Error('Unknown server or missing auth configuration')
48 |       return { providerId: provider ?? serverId, serverId, config: server.auth_config }
49 |     }
50 | 
51 |     const pre = cfg.oauth_delegation?.providers?.[String(provider)]
52 |     if (!pre) throw new Error('Unknown provider')
53 |     return { providerId: String(provider), config: pre }
54 |   }
55 | 
56 |   validateReturnTo(returnTo: string | null | undefined, baseUrl?: string): string | undefined {
57 |     if (!returnTo) return undefined
58 |     try {
59 |       // Allow relative paths only, or same-origin absolute if matches baseUrl
60 |       if (returnTo.startsWith('/')) return returnTo
61 |       if (baseUrl) {
62 |         const origin = new URL(baseUrl).origin
63 |         const u = new URL(returnTo)
64 |         if (u.origin === origin) return u.pathname + u.search + u.hash
65 |       }
66 |       return undefined
67 |     } catch {
68 |       return undefined
69 |     }
70 |   }
71 | }
72 | 
73 | 
```

--------------------------------------------------------------------------------
/docs/guides/client-integration.md:
--------------------------------------------------------------------------------

```markdown
 1 | ---
 2 | title: Client Integration
 3 | ---
 4 | 
 5 | # Client Integration
 6 | 
 7 | Connect your MCP clients to the Master MCP Server and verify end-to-end flows.
 8 | 
 9 | > 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.
10 | 
11 | ## Custom Clients (HTTP)
12 | 
13 | Use any HTTP-capable client. Examples below:
14 | 
15 | ```bash
16 | curl -s -H 'content-type: application/json' \
17 |   -X POST http://localhost:3000/mcp/tools/list -d '{"type":"list_tools"}'
18 | ```
19 | 
20 | Node (fetch):
21 | 
22 | ```ts
23 | import fetch from 'node-fetch'
24 | const res = await fetch('http://localhost:3000/mcp/tools/call', {
25 |   method: 'POST',
26 |   headers: { 'content-type': 'application/json', authorization: 'Bearer YOUR_CLIENT_TOKEN' },
27 |   body: JSON.stringify({ name: 'search.query', arguments: { q: 'hello' } })
28 | })
29 | console.log(await res.json())
30 | ```
31 | 
32 | See also: Getting Started → Quick Start and the <ApiPlayground /> on the landing page.
33 | 
34 | ## Claude Desktop (Guidance)
35 | 
36 | Claude Desktop supports MCP servers via configuration. The exact configuration and supported transports can change; consult the latest Claude Desktop documentation.
37 | 
38 | Two approaches:
39 | 
40 | - 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.
41 | - Otherwise, run a small stdio bridge that speaks MCP to the client and forwards requests to the master HTTP endpoints. The bridge should:
42 |   - Respond to tool/resource listing using the master’s `/mcp/*/list` endpoints
43 |   - Forward tool calls and resource reads to `/mcp/tools/call` and `/mcp/resources/read`
44 |   - Map names like `serverId.toolName` consistently
45 | 
46 | > Tip: Keep your bridge stateless. Let the master handle routing, retries, and auth strategies.
47 | 
48 | ## Testing Connections
49 | 
50 | - Health: `GET /health` → `{ ok: true }`
51 | - Capabilities: `GET /capabilities` → aggregated tools/resources
52 | - Tools/Resources: use the POST endpoints under `/mcp/*`
53 | 
54 | ## Troubleshooting
55 | 
56 | - 401/403: ensure your Authorization header is present and matches backend expectations.
57 | - Missing tools/resources: confirm the backend servers are healthy and listed in config.
58 | - Delegated OAuth required: follow the flow at `/oauth/authorize?server_id=<id>`.
59 | 
60 | 
```

--------------------------------------------------------------------------------
/tests/mocks/oauth/mock-oidc-provider.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { createTestServer } from '../../_utils/test-server.js'
 2 | 
 3 | export interface MockOidcOptions {
 4 |   issuer?: string
 5 |   clientId?: string
 6 |   clientSecret?: string
 7 |   scopes?: string[]
 8 | }
 9 | 
10 | export async function startMockOidcProvider(opts?: MockOidcOptions): Promise<{
11 |   issuer: string
12 |   authorization_endpoint: string
13 |   token_endpoint: string
14 |   jwks_uri: string
15 |   stop: () => Promise<void>
16 | }> {
17 |   const srv = await createTestServer()
18 |   const issuer = opts?.issuer ?? `${srv.url}`
19 |   const clientId = opts?.clientId ?? 'test-client'
20 |   const scopes = opts?.scopes ?? ['openid', 'profile']
21 | 
22 |   const codeStore = new Map<string, { scope: string[] }>()
23 | 
24 |   // OIDC Discovery
25 |   srv.register('GET', '/.well-known/openid-configuration', () => ({
26 |     body: {
27 |       issuer,
28 |       authorization_endpoint: `${issuer}/authorize`,
29 |       token_endpoint: `${issuer}/token`,
30 |       jwks_uri: `${issuer}/jwks.json`,
31 |       response_types_supported: ['code'],
32 |       grant_types_supported: ['authorization_code', 'refresh_token'],
33 |     },
34 |   }))
35 | 
36 |   // Simplified authorize: immediately redirects back with code + state
37 |   srv.register('GET', '/authorize', (_req, _raw) => {
38 |     const url = new URL(_req.url || '/', issuer)
39 |     const redirectUri = url.searchParams.get('redirect_uri') || ''
40 |     const state = url.searchParams.get('state') || ''
41 |     const scopeStr = url.searchParams.get('scope') || scopes.join(' ')
42 |     const code = `code_${Math.random().toString(36).slice(2)}`
43 |     codeStore.set(code, { scope: scopeStr.split(/[ ,]+/).filter(Boolean) })
44 |     return {
45 |       status: 302,
46 |       headers: { location: `${redirectUri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}` },
47 |     }
48 |   })
49 | 
50 |   // Token endpoint
51 |   srv.register('POST', '/token', (_req, raw) => {
52 |     const params = new URLSearchParams(raw || '')
53 |     const code = params.get('code') || ''
54 |     const rec = codeStore.get(code)
55 |     if (!rec) return { status: 400, body: { error: 'invalid_grant' } }
56 |     // Minimal token response
57 |     return {
58 |       body: {
59 |         access_token: `at_${code}`,
60 |         token_type: 'bearer',
61 |         scope: rec.scope.join(' '),
62 |         expires_in: 3600,
63 |       },
64 |     }
65 |   })
66 | 
67 |   // Static JWKS (not strictly needed for current code paths)
68 |   srv.register('GET', '/jwks.json', () => ({ body: { keys: [] } }))
69 | 
70 |   return {
71 |     issuer,
72 |     authorization_endpoint: `${issuer}/authorize`,
73 |     token_endpoint: `${issuer}/token`,
74 |     jwks_uri: `${issuer}/jwks.json`,
75 |     stop: srv.close,
76 |   }
77 | }
78 | 
79 | 
```

--------------------------------------------------------------------------------
/examples/test-mcp-server.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
  2 | import express from 'express'
  3 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
  4 | 
  5 | // Create a simple test MCP server
  6 | const server = new McpServer({
  7 |   name: 'test-mcp-server',
  8 |   version: '1.0.0'
  9 | }, {
 10 |   capabilities: {
 11 |     tools: { listChanged: true },
 12 |     resources: { listChanged: true }
 13 |   }
 14 | })
 15 | 
 16 | // Register a simple tool
 17 | server.tool('echo', 'Echoes back the input', { message: { type: 'string' } }, async (args) => {
 18 |   return {
 19 |     content: [{
 20 |       type: 'text',
 21 |       text: `Echo: ${args.message}`
 22 |     }]
 23 |   }
 24 | })
 25 | 
 26 | // Register a simple resource
 27 | server.resource('test-resource', 'test://example', { description: 'A test resource' }, async () => {
 28 |   return {
 29 |     contents: [{
 30 |       uri: 'test://example',
 31 |       text: 'This is a test resource'
 32 |     }]
 33 |   }
 34 | })
 35 | 
 36 | // Create an Express app
 37 | const app = express()
 38 | app.use(express.json())
 39 | 
 40 | // Create the HTTP streaming transport
 41 | const transport = new StreamableHTTPServerTransport({
 42 |   sessionIdGenerator: undefined, // Stateless mode
 43 |   enableJsonResponse: false, // Use SSE by default
 44 |   enableDnsRebindingProtection: false
 45 | })
 46 | 
 47 | // Connect the server to the transport
 48 | await server.connect(transport)
 49 | 
 50 | // Handle MCP requests
 51 | app.post('/mcp', async (req, res) => {
 52 |   try {
 53 |     await transport.handleRequest(req, res, req.body)
 54 |   } catch (error) {
 55 |     console.error('MCP request handling failed', { error })
 56 |     res.status(500).json({
 57 |       error: 'Internal server error'
 58 |     })
 59 |   }
 60 | })
 61 | 
 62 | app.get('/mcp', async (req, res) => {
 63 |   try {
 64 |     await transport.handleRequest(req, res)
 65 |   } catch (error) {
 66 |     console.error('MCP request handling failed', { error })
 67 |     res.status(500).json({
 68 |       error: 'Internal server error'
 69 |     })
 70 |   }
 71 | })
 72 | 
 73 | // Expose the capabilities endpoint
 74 | app.get('/capabilities', (req, res) => {
 75 |   res.json({
 76 |     tools: [
 77 |       {
 78 |         name: 'echo',
 79 |         description: 'Echoes back the input',
 80 |         inputSchema: {
 81 |           type: 'object',
 82 |           properties: {
 83 |             message: {
 84 |               type: 'string'
 85 |             }
 86 |           },
 87 |           required: ['message']
 88 |         }
 89 |       }
 90 |     ],
 91 |     resources: [
 92 |       {
 93 |         uri: 'test://example',
 94 |         name: 'test-resource',
 95 |         description: 'A test resource'
 96 |       }
 97 |     ]
 98 |   })
 99 | })
100 | 
101 | // Start the server
102 | const port = process.env.PORT || 3006
103 | app.listen(port, () => {
104 |   console.log(`Test MCP server listening on http://localhost:${port}`)
105 | })
```

--------------------------------------------------------------------------------
/tests/servers/test-mcp-client.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'
 2 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
 3 | 
 4 | async function runTest() {
 5 |   try {
 6 |     console.log('Testing Master MCP Server...')
 7 |     
 8 |     // Create a streamable HTTP transport to connect to our MCP server
 9 |     const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
10 |     
11 |     // Create the MCP client
12 |     const client = new Client({
13 |       name: 'test-client',
14 |       version: '1.0.0'
15 |     })
16 |     
17 |     // Initialize the client
18 |     await client.connect(transport)
19 |     console.log('✅ Server initialized')
20 |     console.log('Server info:', client.getServerVersion())
21 |     console.log('Server capabilities:', client.getServerCapabilities())
22 |     
23 |     // List tools
24 |     console.log('\n--- Testing tools/list ---')
25 |     const toolsResult = await client.listTools({})
26 |     console.log('✅ tools/list successful')
27 |     console.log('Number of tools:', toolsResult.tools.length)
28 |     console.log('Tools:', toolsResult.tools.map(t => t.name))
29 |     
30 |     // List resources
31 |     console.log('\n--- Testing resources/list ---')
32 |     const resourcesResult = await client.listResources({})
33 |     console.log('✅ resources/list successful')
34 |     console.log('Number of resources:', resourcesResult.resources.length)
35 |     console.log('Resources:', resourcesResult.resources.map(r => r.uri))
36 |     
37 |     // Test the health endpoint
38 |     console.log('\n--- Testing health endpoint ---')
39 |     try {
40 |       const response = await fetch('http://localhost:3005/health')
41 |       const health = await response.json()
42 |       console.log('✅ Health endpoint successful')
43 |       console.log('Health status:', health)
44 |     } catch (error) {
45 |       console.log('⚠️ Health endpoint test failed:', error.message)
46 |     }
47 |     
48 |     // Test the metrics endpoint
49 |     console.log('\n--- Testing metrics endpoint ---')
50 |     try {
51 |       const response = await fetch('http://localhost:3005/metrics')
52 |       const metrics = await response.json()
53 |       console.log('✅ Metrics endpoint successful')
54 |       console.log('Metrics:', metrics)
55 |     } catch (error) {
56 |       console.log('⚠️ Metrics endpoint test failed:', error.message)
57 |     }
58 |     
59 |     // Close the connection
60 |     await client.close()
61 |     console.log('\n✅ Disconnected from MCP server')
62 |     console.log('\n🎉 All tests completed successfully!')
63 |     
64 |   } catch (error) {
65 |     console.error('❌ Test failed:', error)
66 |     console.error('Error stack:', error.stack)
67 |   }
68 | }
69 | 
70 | // Run the test
71 | runTest()
```

--------------------------------------------------------------------------------
/config/schema.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://json-schema.org/draft/2020-12/schema",
 3 |   "$id": "https://example.com/master-mcp-config.schema.json",
 4 |   "type": "object",
 5 |   "required": ["master_oauth", "hosting", "servers"],
 6 |   "properties": {
 7 |     "master_oauth": {
 8 |       "type": "object",
 9 |       "required": ["authorization_endpoint", "token_endpoint", "client_id", "redirect_uri", "scopes"],
10 |       "properties": {
11 |         "issuer": { "type": "string" },
12 |         "authorization_endpoint": { "type": "string" },
13 |         "token_endpoint": { "type": "string" },
14 |         "jwks_uri": { "type": "string" },
15 |         "client_id": { "type": "string" },
16 |         "client_secret": { "type": "string" },
17 |         "redirect_uri": { "type": "string" },
18 |         "scopes": { "type": "array", "items": { "type": "string" } },
19 |         "audience": { "type": "string" }
20 |       },
21 |       "additionalProperties": true
22 |     },
23 |     "hosting": {
24 |       "type": "object",
25 |       "required": ["platform", "base_url"],
26 |       "properties": {
27 |         "platform": { "type": "string", "enum": ["node", "cloudflare-workers", "koyeb", "docker", "unknown"] },
28 |         "port": { "type": "number" },
29 |         "base_url": { "type": "string" },
30 |         "storage_backend": { "type": "string" }
31 |       },
32 |       "additionalProperties": true
33 |     },
34 |     "logging": { "type": "object", "properties": { "level": { "type": "string", "enum": ["debug", "info", "warn", "error"] } } },
35 |     "routing": { "type": "object", "additionalProperties": true },
36 |     "security": { "type": "object", "additionalProperties": true },
37 |     "servers": {
38 |       "type": "array",
39 |       "items": {
40 |         "type": "object",
41 |         "required": ["id", "type", "auth_strategy", "config"],
42 |         "properties": {
43 |           "id": { "type": "string" },
44 |           "type": { "type": "string", "enum": ["git", "npm", "pypi", "docker", "local"] },
45 |           "url": { "type": "string" },
46 |           "package": { "type": "string" },
47 |           "version": { "type": "string" },
48 |           "branch": { "type": "string" },
49 |           "auth_strategy": { "type": "string", "enum": ["master_oauth", "delegate_oauth", "bypass_auth", "proxy_oauth"] },
50 |           "auth_config": { "type": "object", "additionalProperties": true },
51 |           "config": {
52 |             "type": "object",
53 |             "properties": {
54 |               "environment": { "type": "object", "additionalProperties": true },
55 |               "args": { "type": "array", "items": { "type": "string" } },
56 |               "port": { "type": "integer" }
57 |             },
58 |             "additionalProperties": true
59 |           }
60 |         },
61 |         "additionalProperties": true
62 |       }
63 |     }
64 |   },
65 |   "additionalProperties": true
66 | }
67 | 
68 | 
```

--------------------------------------------------------------------------------
/src/utils/string.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * String manipulation and parsing utilities.
  3 |  */
  4 | 
  5 | export function slugify(input: string): string {
  6 |   return input
  7 |     .normalize('NFKD')
  8 |     .replace(/[\u0300-\u036f]/g, '')
  9 |     .toLowerCase()
 10 |     .replace(/[^a-z0-9]+/g, '-')
 11 |     .replace(/(^-|-$)+/g, '')
 12 | }
 13 | 
 14 | export function toCamelCase(input: string): string {
 15 |   return input
 16 |     .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
 17 |     .replace(/^(.)/, (m) => m.toLowerCase())
 18 | }
 19 | 
 20 | export function toKebabCase(input: string): string {
 21 |   return input
 22 |     .replace(/([a-z])([A-Z])/g, '$1-$2')
 23 |     .replace(/[\s_]+/g, '-')
 24 |     .toLowerCase()
 25 | }
 26 | 
 27 | export function toSnakeCase(input: string): string {
 28 |   return input
 29 |     .replace(/([a-z])([A-Z])/g, '$1_$2')
 30 |     .replace(/[\s-]+/g, '_')
 31 |     .toLowerCase()
 32 | }
 33 | 
 34 | export function escapeHTML(input: string): string {
 35 |   return input
 36 |     .replace(/&/g, '&amp;')
 37 |     .replace(/</g, '&lt;')
 38 |     .replace(/>/g, '&gt;')
 39 |     .replace(/"/g, '&quot;')
 40 |     .replace(/'/g, '&#39;')
 41 | }
 42 | 
 43 | export function escapeRegExp(input: string): string {
 44 |   return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
 45 | }
 46 | 
 47 | export function joinUrlPaths(...parts: string[]): string {
 48 |   const sanitized = parts.filter(Boolean).map((p) => p.replace(/(^\/+|\/+?$)/g, ''))
 49 |   const joined = sanitized.join('/')
 50 |   return `/${joined}`.replace(/\/+$/g, '') || '/'
 51 | }
 52 | 
 53 | export function trimSafe(input: unknown): string {
 54 |   return String(input ?? '').trim()
 55 | }
 56 | 
 57 | export function truncateMiddle(input: string, maxLength: number): string {
 58 |   if (input.length <= maxLength) return input
 59 |   if (maxLength <= 3) return input.slice(0, maxLength)
 60 |   const keep = Math.floor((maxLength - 3) / 2)
 61 |   return `${input.slice(0, keep)}...${input.slice(-keep)}`
 62 | }
 63 | 
 64 | export function toBase64(input: string): string {
 65 |   if (typeof btoa === 'function') return btoa(input)
 66 |   return Buffer.from(input, 'utf8').toString('base64')
 67 | }
 68 | 
 69 | export function fromBase64(input: string): string {
 70 |   if (typeof atob === 'function') return atob(input)
 71 |   return Buffer.from(input, 'base64').toString('utf8')
 72 | }
 73 | 
 74 | export function stableJSONStringify(value: unknown): string {
 75 |   return JSON.stringify(sortKeys(value))
 76 | }
 77 | 
 78 | function sortKeys(value: any): any {
 79 |   if (Array.isArray(value)) return value.map(sortKeys)
 80 |   if (value && typeof value === 'object') {
 81 |     const out: Record<string, any> = {}
 82 |     for (const key of Object.keys(value).sort()) out[key] = sortKeys(value[key])
 83 |     return out
 84 |   }
 85 |   return value
 86 | }
 87 | 
 88 | export function parseBoolean(input: unknown, defaultValue = false): boolean {
 89 |   const s = String(input ?? '').trim().toLowerCase()
 90 |   if (s === 'true' || s === '1' || s === 'yes' || s === 'y') return true
 91 |   if (s === 'false' || s === '0' || s === 'no' || s === 'n') return false
 92 |   return defaultValue
 93 | }
 94 | 
 95 | export function normalizeUrl(input: string): string {
 96 |   try {
 97 |     const u = new URL(input)
 98 |     u.hash = ''
 99 |     return u.toString()
100 |   } catch {
101 |     return input
102 |   }
103 | }
104 | 
105 | 
```

--------------------------------------------------------------------------------
/tests/integration/request-router.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import '../setup/test-setup.js'
 2 | import test from 'node:test'
 3 | import assert from 'node:assert/strict'
 4 | import { RequestRouter } from '../../src/modules/request-router.js'
 5 | import { CapabilityAggregator } from '../../src/modules/capability-aggregator.js'
 6 | import { createMockServer } from '../utils/mock-http.js'
 7 | 
 8 | test('RequestRouter routes tool/resource with pass-through auth', async () => {
 9 |   const sOk = await createMockServer([
10 |     { method: 'POST', path: '/mcp/tools/call', handler: (_req, body) => ({ body: { content: { ok: true, args: body.arguments } } }) },
11 |     { method: 'POST', path: '/mcp/resources/read', handler: (_req, body) => ({ body: { contents: `read:${body.uri}`, mimeType: 'text/plain' } }) },
12 |   ])
13 |   try {
14 |     const servers = new Map<string, any>([[
15 |       's1', { id: 's1', type: 'node', endpoint: sOk.url, config: {} as any, status: 'running', lastHealthCheck: 0 }
16 |     ]])
17 |     const rr = new RequestRouter(servers as any, new CapabilityAggregator(), async (_sid, token) => token ? ({ Authorization: `Bearer ${token}` }) : undefined)
18 |     const toolRes = await rr.routeCallTool({ name: 's1.echo', arguments: { a: 1 } }, 'CT')
19 |     assert.equal((toolRes as any).content.ok, true)
20 |     const readRes = await rr.routeReadResource({ uri: 's1.file' }, 'CT')
21 |     assert.equal((readRes as any).contents, 'read:file')
22 |   } finally {
23 |     await sOk.close()
24 |   }
25 | })
26 | 
27 | test('RequestRouter returns delegation error when provider requires', async () => {
28 |   const s = await createMockServer([
29 |     { method: 'POST', path: '/mcp/tools/call', handler: (_req, _body) => ({ body: { content: { ok: true } } }) },
30 |   ])
31 |   try {
32 |     const servers = new Map<string, any>([[ 's1', { id: 's1', type: 'node', endpoint: s.url, config: {} as any, status: 'running', lastHealthCheck: 0 } ]])
33 |     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))
34 |     const res = await rr.routeCallTool({ name: 's1.x' }, 'CT')
35 |     assert.equal((res as any).isError, true)
36 |   } finally {
37 |     await s.close()
38 |   }
39 | })
40 | 
41 | test('RequestRouter retries on transient failure and eventually succeeds', async () => {
42 |   let n = 0
43 |   const s = await createMockServer([
44 |     { method: 'POST', path: '/mcp/tools/call', handler: () => {
45 |       n++
46 |       if (n < 3) return { status: 500, body: { error: 'boom' } }
47 |       return { body: { content: { ok: true } } }
48 |     } },
49 |   ])
50 |   try {
51 |     const servers = new Map<string, any>([[ 's1', { id: 's1', type: 'node', endpoint: s.url, config: {} as any, status: 'running', lastHealthCheck: 0 } ]])
52 |     const rr = new RequestRouter(servers as any, new CapabilityAggregator())
53 |     const res = await rr.routeCallTool({ name: 's1.task' })
54 |     // @ts-ignore
55 |     assert.equal(res.content.ok, true)
56 |   } finally {
57 |     await s.close()
58 |   }
59 | })
60 | 
```

--------------------------------------------------------------------------------
/docs/.vitepress/theme/components/ApiPlayground.vue:
--------------------------------------------------------------------------------

```vue
 1 | <template>
 2 |   <div class="mcp-callout" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
 3 |     <label>Endpoint
 4 |       <select v-model="endpoint" style="margin-left:8px">
 5 |         <option value="tools.list">POST /mcp/tools/list</option>
 6 |         <option value="tools.call">POST /mcp/tools/call</option>
 7 |         <option value="resources.list">POST /mcp/resources/list</option>
 8 |         <option value="resources.read">POST /mcp/resources/read</option>
 9 |       </select>
10 |     </label>
11 |     <label>Base URL <input v-model="baseUrl" placeholder="http://localhost:3000" /></label>
12 |     <label>Token <input v-model="token" placeholder="optional bearer" /></label>
13 |   </div>
14 | 
15 |   <div class="mcp-grid">
16 |     <div class="mcp-col-6">
17 |       <h4>Request Body</h4>
18 |       <textarea v-model="body" rows="10" style="width:100%;font-family:var(--vp-font-family-mono)"></textarea>
19 |     </div>
20 |     <div class="mcp-col-6">
21 |       <h4>curl</h4>
22 |       <pre><code>{{ curl }}</code></pre>
23 |       <h4 style="margin-top:10px">Node (fetch)</h4>
24 |       <pre><code class="language-ts">{{ node }}</code></pre>
25 |     </div>
26 |   </div>
27 |   <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>
28 | </template>
29 | 
30 | <script setup lang="ts">
31 | import { computed, ref, watch } from 'vue'
32 | 
33 | const endpoint = ref<'tools.list'|'tools.call'|'resources.list'|'resources.read'>('tools.list')
34 | const baseUrl = ref('http://localhost:3000')
35 | const token = ref('')
36 | const body = ref('')
37 | 
38 | const defaultBodies: Record<string, string> = {
39 |   'tools.list': JSON.stringify({ type: 'list_tools' }, null, 2),
40 |   'tools.call': JSON.stringify({ name: 'serverId.toolName', arguments: { query: 'hello' } }, null, 2),
41 |   'resources.list': JSON.stringify({ type: 'list_resources' }, null, 2),
42 |   'resources.read': JSON.stringify({ uri: 'serverId:resourceId' }, null, 2)
43 | }
44 | 
45 | watch(endpoint, (v) => { body.value = defaultBodies[v] })
46 | body.value = defaultBodies[endpoint.value]
47 | 
48 | const path = computed(() => {
49 |   switch (endpoint.value) {
50 |     case 'tools.list': return '/mcp/tools/list'
51 |     case 'tools.call': return '/mcp/tools/call'
52 |     case 'resources.list': return '/mcp/resources/list'
53 |     case 'resources.read': return '/mcp/resources/read'
54 |   }
55 | })
56 | 
57 | const curl = computed(() => {
58 |   const headers = ["-H 'content-type: application/json'"]
59 |   if (token.value) headers.push(`-H 'authorization: Bearer ${token.value}'`)
60 |   return `curl -s ${headers.join(' ')} -X POST ${baseUrl.value}${path.value} -d '${body.value.replace(/'/g, "'\\''")}'`
61 | })
62 | 
63 | const node = computed(() => `import fetch from 'node-fetch'
64 | 
65 | const res = await fetch('${baseUrl.value}${path.value}', {
66 |   method: 'POST',
67 |   headers: {
68 |     'content-type': 'application/json'${token.value ? ",\n    authorization: 'Bearer " + token.value + "'" : ''}
69 |   },
70 |   body: JSON.stringify(${body.value})
71 | })
72 | const data = await res.json()
73 | console.log(data)
74 | `)
75 | </script>
76 | 
77 | 
```

--------------------------------------------------------------------------------
/src/oauth/pkce-manager.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // PKCE (Proof Key for Code Exchange) manager with cross-platform support
 2 | // Generates code_verifier and S256 code_challenge and tracks them per state
 3 | 
 4 | export interface PkceRecord {
 5 |   verifier: string
 6 |   method: 'S256' | 'plain'
 7 |   createdAt: number
 8 |   expiresAt: number
 9 | }
10 | 
11 | export interface PkceManagerOptions {
12 |   ttlMs?: number
13 | }
14 | 
15 | function getCrypto(): any {
16 |   const g: any = globalThis as any
17 |   if (g.crypto && g.crypto.subtle && g.crypto.getRandomValues) return g.crypto as any
18 |   // Node fallback
19 |   try {
20 |     // eslint-disable-next-line @typescript-eslint/no-var-requires
21 |     const nodeCrypto = require('node:crypto')
22 |     return nodeCrypto.webcrypto as any
23 |   } catch {
24 |     throw new Error('Secure crypto not available in this environment')
25 |   }
26 | }
27 | 
28 | function base64UrlEncode(bytes: Uint8Array): string {
29 |   let str = ''
30 |   for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i])
31 |   // btoa is available in browser/worker; Node 18 has global btoa via Buffer workaround
32 |   // eslint-disable-next-line @typescript-eslint/ban-ts-comment
33 |   // @ts-ignore
34 |   const b64 = typeof btoa === 'function' ? btoa(str) : Buffer.from(bytes).toString('base64')
35 |   return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
36 | }
37 | 
38 | function randomString(length = 64): string {
39 |   const crypto = getCrypto()
40 |   const bytes = new Uint8Array(length)
41 |   crypto.getRandomValues(bytes)
42 |   // Allowed characters for verifier are [A-Z a-z 0-9 - . _ ~]
43 |   const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'
44 |   let out = ''
45 |   for (let i = 0; i < length; i++) out += chars[bytes[i] % chars.length]
46 |   return out
47 | }
48 | 
49 | export class PKCEManager {
50 |   private readonly store = new Map<string, PkceRecord>()
51 |   private readonly ttl: number
52 | 
53 |   constructor(options?: PkceManagerOptions) {
54 |     this.ttl = options?.ttlMs ?? 10 * 60_000 // 10 minutes
55 |   }
56 | 
57 |   async generate(state: string): Promise<{ challenge: string; method: 'S256'; verifier: string }> {
58 |     const verifier = randomString(64)
59 |     const challenge = await this.computeS256(verifier)
60 |     const now = Date.now()
61 |     this.store.set(state, {
62 |       verifier,
63 |       method: 'S256',
64 |       createdAt: now,
65 |       expiresAt: now + this.ttl,
66 |     })
67 |     return { challenge, method: 'S256', verifier }
68 |   }
69 | 
70 |   getVerifier(state: string, consume = true): string | undefined {
71 |     const rec = this.store.get(state)
72 |     if (!rec) return undefined
73 |     if (rec.expiresAt <= Date.now()) {
74 |       this.store.delete(state)
75 |       return undefined
76 |     }
77 |     if (consume) this.store.delete(state)
78 |     return rec.verifier
79 |   }
80 | 
81 |   cleanup(): void {
82 |     const now = Date.now()
83 |     for (const [k, v] of this.store) if (v.expiresAt <= now) this.store.delete(k)
84 |   }
85 | 
86 |   private async computeS256(verifier: string): Promise<string> {
87 |     const crypto = getCrypto()
88 |     const enc = new TextEncoder().encode(verifier)
89 |     const digest = await crypto.subtle.digest('SHA-256', enc)
90 |     return base64UrlEncode(new Uint8Array(digest))
91 |   }
92 | }
93 | 
```

--------------------------------------------------------------------------------
/scripts/generate-config-docs.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /*
 2 |  * Generates docs/configuration/reference.md from the built-in JSON schema
 3 |  * and enriches with examples from examples/sample-configs.
 4 |  */
 5 | import { SchemaValidator } from '../src/config/schema-validator.js'
 6 | import { promises as fs } from 'node:fs'
 7 | import path from 'node:path'
 8 | 
 9 | type JSONSchema = any
10 | 
11 | async function main() {
12 |   const schema: JSONSchema | undefined = await SchemaValidator.loadSchema()
13 |   if (!schema) throw new Error('Unable to load configuration schema')
14 | 
15 |   const examplesDir = path.resolve('examples/sample-configs')
16 |   const exampleFiles = await fs.readdir(examplesDir)
17 |   const exampleSnippets: string[] = []
18 |   for (const f of exampleFiles) {
19 |     if (!f.endsWith('.yaml') && !f.endsWith('.yml')) continue
20 |     const content = await fs.readFile(path.join(examplesDir, f), 'utf8')
21 |     exampleSnippets.push(`## Example: ${f}\n\n\u0060\u0060\u0060yaml\n${content}\n\u0060\u0060\u0060\n`)
22 |   }
23 | 
24 |   const lines: string[] = []
25 |   lines.push('# Configuration Reference')
26 |   lines.push('')
27 |   lines.push('This reference is generated from the built-in JSON Schema used by the server to validate configuration.')
28 |   lines.push('')
29 |   lines.push('## Top-Level Fields')
30 |   lines.push('')
31 |   lines.push(renderObject(schema))
32 |   lines.push('')
33 |   lines.push('## Examples')
34 |   lines.push('')
35 |   lines.push(exampleSnippets.join('\n'))
36 | 
37 |   const target = path.resolve('docs/configuration/reference.md')
38 |   const contents = await fs.readFile(target, 'utf8').catch(() => '')
39 |   const start = '<!-- GENERATED:BEGIN -->'
40 |   const end = '<!-- GENERATED:END -->'
41 |   const prefix = contents.split(start)[0] ?? ''
42 |   const suffix = contents.split(end)[1] ?? ''
43 |   const generated = `${start}\n\n${lines.join('\n')}\n\n${end}`
44 |   const next = `${prefix}${generated}${suffix}`
45 |   await fs.writeFile(target, next, 'utf8')
46 | }
47 | 
48 | function renderObject(schema: JSONSchema, indent = 0, name?: string): string {
49 |   const pad = '  '.repeat(indent)
50 |   if (!schema || typeof schema !== 'object') return ''
51 |   let s = ''
52 |   if (schema.type === 'object' || schema.properties) {
53 |     const required = new Set<string>((schema.required || []) as string[])
54 |     for (const [key, value] of Object.entries(schema.properties || {})) {
55 |       const req = required.has(key) ? ' (required)' : ''
56 |       s += `${pad}- \`${name ? name + '.' : ''}${key}\`${req}${renderType(value)}\n`
57 |       if ((value as any).properties || (value as any).items) {
58 |         s += renderObject(value, indent + 1, name ? `${name}.${key}` : key)
59 |       }
60 |     }
61 |   }
62 |   if (schema.items) {
63 |     s += `${pad}  - items:${renderType(schema.items)}\n`
64 |     s += renderObject(schema.items, indent + 2, name ? `${name}[]` : '[]')
65 |   }
66 |   return s
67 | }
68 | 
69 | function renderType(schema: JSONSchema): string {
70 |   const t = Array.isArray(schema.type) ? schema.type.join('|') : schema.type
71 |   const enumVals = schema.enum ? `, enum: ${schema.enum.join(', ')}` : ''
72 |   const fmt = schema.format ? `, format: ${schema.format}` : ''
73 |   return t ? ` — type: ${t}${enumVals}${fmt}` : `${enumVals}${fmt}`
74 | }
75 | 
76 | main().catch((err) => {
77 |   console.error(err)
78 |   process.exitCode = 1
79 | })
80 | 
81 | 
```

--------------------------------------------------------------------------------
/src/server/dependency-container.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ConfigManager } from './config-manager.js'
 2 | import { DefaultModuleLoader } from '../modules/module-loader.js'
 3 | import { CapabilityAggregator } from '../modules/capability-aggregator.js'
 4 | import { RequestRouter } from '../modules/request-router.js'
 5 | import { ProtocolHandler } from './protocol-handler.js'
 6 | import { MasterServer } from './master-server.js'
 7 | import { MultiAuthManager } from '../auth/multi-auth-manager.js'
 8 | import type { MasterConfig, ServerConfig } from '../types/config.js'
 9 | import { Logger } from '../utils/logger.js'
10 | 
11 | export class DependencyContainer {
12 |   readonly configManager: ConfigManager
13 |   readonly loader: DefaultModuleLoader
14 |   readonly aggregator: CapabilityAggregator
15 |   readonly master: MasterServer
16 |   readonly authManager: MultiAuthManager
17 |   readonly router: RequestRouter
18 |   readonly handler: ProtocolHandler
19 | 
20 |   private config!: MasterConfig
21 | 
22 |   constructor() {
23 |     this.configManager = new ConfigManager({ watch: true })
24 |     this.loader = new DefaultModuleLoader()
25 |     this.aggregator = new CapabilityAggregator()
26 |     // Create a temporary router for early wiring; will be replaced after config load
27 |     this.router = new RequestRouter(new Map(), this.aggregator)
28 |     this.master = new MasterServer(undefined, undefined)
29 |     // Temporarily construct auth manager with placeholder; will be replaced after config
30 |     this.authManager = new MultiAuthManager({
31 |       authorization_endpoint: 'about:blank',
32 |       token_endpoint: 'about:blank',
33 |       client_id: 'placeholder',
34 |       redirect_uri: 'about:blank',
35 |       scopes: ['openid'],
36 |     })
37 |     this.handler = this.master.handler
38 |   }
39 | 
40 |   async initialize(clientToken?: string): Promise<void> {
41 |     this.config = await this.configManager.load()
42 |     // Recreate auth manager with real config
43 |     const auth = new MultiAuthManager(this.config.master_oauth)
44 |     this.registerServerAuth(auth, this.config.servers)
45 |     this.master.attachAuthManager(auth)
46 |     ;(this as any).authManager = auth
47 | 
48 |     // Load servers and discover capabilities
49 |     await this.master.startFromConfig(this.config, clientToken)
50 |     this.master.updateRouting(this.config.routing)
51 | 
52 |     // Recreate router/handler references for easy access
53 |     ;(this as any).router = this.master.getRouter()
54 |     ;(this as any).handler = this.master.handler
55 | 
56 |     // Watch for config changes and hot-reload
57 |     this.configManager.onChange(async (cfg) => {
58 |       try {
59 |         Logger.info('Applying updated configuration to MasterServer')
60 |         this.config = cfg
61 |         this.registerServerAuth(this.authManager, cfg.servers)
62 |         await this.master.loadServers(cfg.servers)
63 |         await this.master.discoverAllCapabilities()
64 |         this.master.updateRouting(cfg.routing)
65 |       } catch (err) {
66 |         Logger.warn('Failed to apply updated config', err)
67 |       }
68 |     })
69 |   }
70 | 
71 |   getConfig(): MasterConfig {
72 |     return this.config
73 |   }
74 | 
75 |   private registerServerAuth(manager: MultiAuthManager, servers: ServerConfig[]): void {
76 |     for (const s of servers) {
77 |       try {
78 |         manager.registerServerAuth(s.id, s.auth_strategy, s.auth_config)
79 |       } catch (err) {
80 |         Logger.warn(`Failed to register auth for server ${s.id}`, err)
81 |       }
82 |     }
83 |   }
84 | }
85 | 
86 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "master-mcp-server",
 3 |   "version": "0.1.0",
 4 |   "private": true,
 5 |   "description": "Master MCP Server that aggregates multiple MCP servers behind a single endpoint.",
 6 |   "license": "UNLICENSED",
 7 |   "type": "module",
 8 |   "engines": {
 9 |     "node": ">=18.17"
10 |   },
11 |   "scripts": {
12 |     "prepare": "husky install || true",
13 |     "clean": "rimraf dist .turbo tsconfig.tsbuildinfo",
14 |     "typecheck": "tsc -p tsconfig.node.json --noEmit",
15 |     "build": "npm run build:node && npm run build:worker",
16 |     "build:node": "tsc -p tsconfig.node.json",
17 |     "build:worker": "tsc -p tsconfig.worker.json",
18 |     "dev": "node --loader ts-node/esm src/index.ts",
19 |     "dev:watch": "nodemon --watch src --ext ts,tsx,json --exec 'node --loader ts-node/esm src/index.ts'",
20 |     "start": "node dist/node/index.js",
21 |     "start:prod": "NODE_ENV=production node dist/node/index.js",
22 |     "lint": "eslint --config .eslintrc.cjs . --ext .ts,.tsx",
23 |     "format": "prettier --write .",
24 |     "test": "NODE_V8_COVERAGE=.coverage node --loader ts-node/esm --test",
25 |     "test:unit": "node --loader ts-node/esm --test tests/unit/**/*.test.ts",
26 |     "test:integration": "node --loader ts-node/esm --test tests/integration/**/*.test.ts",
27 |     "test:e2e": "node --loader ts-node/esm --test tests/e2e/**/*.test.ts",
28 |     "test:perf": "node --loader ts-node/esm --test tests/perf/**/*.test.ts",
29 |     "test:security": "node --loader ts-node/esm --test tests/security/**/*.test.ts",
30 |     "test:watch": "node --loader ts-node/esm --test --watch",
31 |     "test:mcp": "node --loader ts-node/esm test-master-mcp.js",
32 |     "test:streaming": "node --loader ts-node/esm tests/servers/test-streaming.js",
33 |     "test:streaming-both": "node --loader ts-node/esm tests/servers/test-streaming-both.js",
34 |     "test:streaming-both-full": "node --loader ts-node/esm tests/servers/test-streaming-both-full.js",
35 |     "test:streaming-both-simple": "node --loader ts-node/esm tests/servers/test-streaming-both-simple.js",
36 |     "test:streaming-both-complete": "node --loader ts-node/esm tests/servers/test-streaming-both-complete.js",
37 |     "docs:api": "typedoc",
38 |     "docs:config": "node --loader ts-node/esm scripts/generate-config-docs.ts",
39 |     "docs:dev": "vitepress dev docs",
40 |     "docs:build": "vitepress build docs",
41 |     "docs:preview": "vitepress preview docs",
42 |     "docs:pdf": "md-to-pdf docs/index.md --basedir docs",
43 |     "docs:all": "npm run docs:api && npm run docs:config && npm run docs:build"
44 |   },
45 |   "dependencies": {
46 |     "@modelcontextprotocol/sdk": "^1.17.3",
47 |     "dotenv": "^17.2.1",
48 |     "express": "5.1.0",
49 |     "jose": "6.0.12",
50 |     "node-fetch": "^3.3.2",
51 |     "yaml": "^2.5.0"
52 |   },
53 |   "devDependencies": {
54 |     "@types/express": "5.0.3",
55 |     "@types/node": "24.3.0",
56 |     "@typescript-eslint/eslint-plugin": "8.40.0",
57 |     "@typescript-eslint/parser": "8.40.0",
58 |     "eslint": "9.33.0",
59 |     "eslint-config-prettier": "10.1.8",
60 |     "eslint-plugin-import": "^2.29.1",
61 |     "husky": "^9.1.6",
62 |     "md-to-pdf": "^5.2.4",
63 |     "nodemon": "^3.1.4",
64 |     "prettier": "^3.3.2",
65 |     "rimraf": "6.0.1",
66 |     "ts-node": "^10.9.2",
67 |     "typedoc": "^0.28.10",
68 |     "typedoc-plugin-markdown": "^4.8.1",
69 |     "typescript": "^5.5.4",
70 |     "vitepress": "^1.6.4"
71 |   },
72 |   "main": "index.js",
73 |   "keywords": [],
74 |   "author": ""
75 | }
76 | 
```

--------------------------------------------------------------------------------
/src/routing/route-registry.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { LoadedServer, ServerInstance } from '../types/server.js'
  2 | import { Logger } from '../utils/logger.js'
  3 | import { CircuitBreaker } from './circuit-breaker.js'
  4 | import { LoadBalancer } from './load-balancer.js'
  5 | 
  6 | export interface RouteRegistryOptions {
  7 |   // Mapping lifetime for cached routes (ms)
  8 |   cacheTtlMs?: number
  9 | }
 10 | 
 11 | export interface RouteResolution {
 12 |   serverId: string
 13 |   instance: ServerInstance
 14 | }
 15 | 
 16 | export class RouteRegistry {
 17 |   private readonly servers: Map<string, LoadedServer>
 18 |   private readonly circuit: CircuitBreaker
 19 |   private readonly lb: LoadBalancer
 20 |   private readonly cacheTtl: number
 21 |   private cache = new Map<string, { value: RouteResolution; expiresAt: number }>()
 22 | 
 23 |   constructor(
 24 |     servers: Map<string, LoadedServer>,
 25 |     circuit: CircuitBreaker,
 26 |     lb: LoadBalancer,
 27 |     options?: RouteRegistryOptions
 28 |   ) {
 29 |     this.servers = servers
 30 |     this.circuit = circuit
 31 |     this.lb = lb
 32 |     this.cacheTtl = options?.cacheTtlMs ?? 5_000
 33 |   }
 34 | 
 35 |   updateServers(servers: Map<string, LoadedServer>): void {
 36 |     // Shallow replace reference
 37 |     ;(this as any).servers = servers
 38 |     this.cache.clear()
 39 |   }
 40 | 
 41 |   getInstances(serverId: string): ServerInstance[] {
 42 |     const server = this.servers.get(serverId)
 43 |     if (!server) return []
 44 |     const instances = server.instances && server.instances.length
 45 |       ? server.instances
 46 |       : (server.endpoint && server.endpoint !== 'unknown'
 47 |         ? [{ id: `${serverId}-primary`, url: server.endpoint, weight: 1, healthScore: server.status === 'running' ? 100 : 0 }]
 48 |         : [])
 49 |     return instances
 50 |   }
 51 | 
 52 |   resolve(serverId: string): RouteResolution | undefined {
 53 |     const cached = this.cache.get(serverId)
 54 |     const now = Date.now()
 55 |     if (cached && cached.expiresAt > now) return cached.value
 56 | 
 57 |     const instances = this.getInstances(serverId)
 58 |     if (!instances.length) return undefined
 59 | 
 60 |     // Filter by circuit breaker allowance
 61 |     const allowed = instances.filter((i) => this.circuit.canExecute(this.key(serverId, i.id)).allowed)
 62 |     const pool = allowed.length ? allowed : instances
 63 |     const chosen = this.lb.select(serverId, pool)
 64 |     if (!chosen) return undefined
 65 | 
 66 |     const resolution: RouteResolution = { serverId, instance: chosen }
 67 |     this.cache.set(serverId, { value: resolution, expiresAt: now + this.cacheTtl })
 68 |     return resolution
 69 |   }
 70 | 
 71 |   markSuccess(serverId: string, instanceId: string): void {
 72 |     const key = this.key(serverId, instanceId)
 73 |     this.circuit.onSuccess(key)
 74 |     this.bumpHealth(serverId, instanceId, +5)
 75 |   }
 76 | 
 77 |   markFailure(serverId: string, instanceId: string): void {
 78 |     const key = this.key(serverId, instanceId)
 79 |     this.circuit.onFailure(key)
 80 |     this.bumpHealth(serverId, instanceId, -20)
 81 |   }
 82 | 
 83 |   private key(serverId: string, instanceId: string): string {
 84 |     return `${serverId}::${instanceId}`
 85 |   }
 86 | 
 87 |   private bumpHealth(serverId: string, instanceId: string, delta: number): void {
 88 |     const s = this.servers.get(serverId)
 89 |     if (!s) return
 90 |     const arr = s.instances
 91 |     if (!arr) return
 92 |     const inst = arr.find((i) => i.id === instanceId)
 93 |     if (!inst) return
 94 |     const prev = inst.healthScore ?? 50
 95 |     const next = Math.max(0, Math.min(100, prev + delta))
 96 |     inst.healthScore = next
 97 |     Logger.debug('Instance health updated', { serverId, instanceId, healthScore: next })
 98 |   }
 99 | }
100 | 
101 | 
```

--------------------------------------------------------------------------------
/src/oauth/callback-handler.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type { MasterConfig, ServerAuthConfig } from '../types/config.js'
 2 | import type { OAuthToken } from '../types/auth.js'
 3 | import { StateManager, type OAuthStatePayload } from './state-manager.js'
 4 | import { PKCEManager } from './pkce-manager.js'
 5 | 
 6 | export interface CallbackContext {
 7 |   config: MasterConfig
 8 |   stateManager: StateManager
 9 |   pkceManager: PKCEManager
10 |   baseUrl: string
11 |   // Store token callback: serverId and clientToken must identify the storage key
12 |   storeDelegatedToken?: (clientToken: string, serverId: string, token: OAuthToken) => Promise<void>
13 | }
14 | 
15 | function toOAuthToken(json: any): OAuthToken {
16 |   const expiresIn = 'expires_in' in json ? Number(json.expires_in) : 3600
17 |   const scope = Array.isArray(json.scope)
18 |     ? (json.scope as string[])
19 |     : typeof json.scope === 'string'
20 |       ? (json.scope as string).split(/[ ,]+/).filter(Boolean)
21 |       : []
22 |   return {
23 |     access_token: String(json.access_token),
24 |     refresh_token: json.refresh_token ? String(json.refresh_token) : undefined,
25 |     expires_at: Date.now() + expiresIn * 1000,
26 |     scope,
27 |   }
28 | }
29 | 
30 | async function exchangeAuthorizationCode(
31 |   code: string,
32 |   cfg: ServerAuthConfig,
33 |   redirectUri: string,
34 |   codeVerifier: string
35 | ): Promise<OAuthToken> {
36 |   const body = new URLSearchParams({
37 |     grant_type: 'authorization_code',
38 |     code,
39 |     client_id: cfg.client_id,
40 |     redirect_uri: redirectUri,
41 |     code_verifier: codeVerifier,
42 |   })
43 |   if (cfg.client_secret) body.set('client_secret', String(cfg.client_secret))
44 |   const res = await fetch(cfg.token_endpoint, {
45 |     method: 'POST',
46 |     headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
47 |     body,
48 |   })
49 |   const text = await res.text()
50 |   if (!res.ok) throw new Error(`Token endpoint error ${res.status}: ${text}`)
51 |   let json: any
52 |   try {
53 |     json = JSON.parse(text)
54 |   } catch {
55 |     json = Object.fromEntries(new URLSearchParams(text))
56 |   }
57 |   return toOAuthToken(json)
58 | }
59 | 
60 | export class CallbackHandler {
61 |   constructor(private readonly ctx: CallbackContext) {}
62 | 
63 |   async handleCallback(params: URLSearchParams, providerConfig: ServerAuthConfig): Promise<{ token?: OAuthToken; error?: string; state?: OAuthStatePayload }>
64 |   {
65 |     const error = params.get('error')
66 |     if (error) {
67 |       const desc = params.get('error_description') ?? 'OAuth authorization failed'
68 |       return { error: `${error}: ${desc}` }
69 |     }
70 |     const stateStr = params.get('state')
71 |     const code = params.get('code')
72 |     if (!stateStr || !code) return { error: 'Missing state or code' }
73 | 
74 |     const state = this.ctx.stateManager.consume(stateStr)
75 |     if (!state) return { error: 'Invalid or expired state' }
76 | 
77 |     const verifier = this.ctx.pkceManager.getVerifier(stateStr)
78 |     if (!verifier) return { error: 'PKCE verification failed' }
79 | 
80 |     const redirectUri = new URL('/oauth/callback', this.ctx.baseUrl).toString()
81 |     try {
82 |       const token = await exchangeAuthorizationCode(code, providerConfig, redirectUri, verifier)
83 |       // Store if we can identify a client + server context
84 |       if (state.clientToken && state.serverId && this.ctx.storeDelegatedToken) {
85 |         await this.ctx.storeDelegatedToken(state.clientToken, state.serverId, token)
86 |       }
87 |       return { token, state }
88 |     } catch (err: any) {
89 |       return { error: err?.message ?? 'Token exchange failed' }
90 |     }
91 |   }
92 | }
93 | 
94 | 
```

--------------------------------------------------------------------------------
/docs/stdio-servers.md:
--------------------------------------------------------------------------------

```markdown
  1 | # STDIO Server Support
  2 | 
  3 | 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.
  4 | 
  5 | ## Configuration
  6 | 
  7 | To configure a STDIO server, use a `file://` URL in your server configuration:
  8 | 
  9 | ```json
 10 | {
 11 |   "servers": [
 12 |     {
 13 |       "id": "stdio-server",
 14 |       "type": "local",
 15 |       "url": "file://./path/to/your/stdio-mcp-server.cjs",
 16 |       "auth_strategy": "bypass_auth",
 17 |       "config": {
 18 |         "environment": {},
 19 |         "args": []
 20 |       }
 21 |     }
 22 |   ]
 23 | }
 24 | ```
 25 | 
 26 | 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.
 27 | 
 28 | ## How It Works
 29 | 
 30 | 1. **Server Detection**: The Master MCP Server detects `file://` URLs and identifies them as STDIO servers
 31 | 2. **Process Management**: STDIO servers are started as child processes
 32 | 3. **Communication**: The Master MCP Server communicates with STDIO servers using JSON-RPC over stdin/stdout
 33 | 4. **Capability Discovery**: The Master MCP Server discovers tools and resources from STDIO servers
 34 | 5. **Request Routing**: Tool calls and resource reads are routed to the appropriate STDIO servers
 35 | 
 36 | ## Benefits
 37 | 
 38 | - **Unified Interface**: Access both HTTP and STDIO servers through a single MCP endpoint
 39 | - **Process Isolation**: Each STDIO server runs in its own process for better isolation
 40 | - **Automatic Management**: The Master MCP Server handles process lifecycle management
 41 | - **Seamless Integration**: STDIO servers appear as regular MCP servers to clients
 42 | 
 43 | ## Requirements
 44 | 
 45 | - STDIO servers must implement the MCP protocol using JSON-RPC over stdin/stdout
 46 | - STDIO servers should follow the MCP specification for initialization and capability discovery
 47 | - STDIO servers must be executable Node.js scripts (`.js` or `.cjs` files)
 48 | 
 49 | ## Example STDIO Server
 50 | 
 51 | Here's a simple example of a STDIO server:
 52 | 
 53 | ```javascript
 54 | // Simple STDIO server that implements the MCP protocol
 55 | // Save this as stdio-mcp-server.cjs and make it executable with chmod +x
 56 | ```
 57 | process.stdin.on('data', async (data) => {
 58 |   try {
 59 |     const lines = data.toString().split('\n').filter(line => line.trim() !== '');
 60 |     for (const line of lines) {
 61 |       const request = JSON.parse(line);
 62 |       
 63 |       // Handle initialize request
 64 |       if (request.method === 'initialize') {
 65 |         const response = {
 66 |           jsonrpc: '2.0',
 67 |           id: request.id,
 68 |           result: {
 69 |             protocolVersion: '2025-06-18',
 70 |             capabilities: {
 71 |               tools: { listChanged: true },
 72 |               resources: { listChanged: true }
 73 |             },
 74 |             serverInfo: {
 75 |               name: 'example-stdio-server',
 76 |               version: '1.0.0'
 77 |             }
 78 |           }
 79 |         };
 80 |         process.stdout.write(JSON.stringify(response) + '\n');
 81 |       }
 82 |       // Handle other requests...
 83 |     }
 84 |   } catch (err) {
 85 |     const errorResponse = {
 86 |       jsonrpc: '2.0',
 87 |       id: null,
 88 |       error: {
 89 |         code: -32700,
 90 |         message: 'Parse error',
 91 |         data: err.message
 92 |       }
 93 |     };
 94 |     process.stdout.write(JSON.stringify(errorResponse) + '\n');
 95 |   }
 96 | });
 97 | ```
 98 | 
 99 | Make sure to make your STDIO server executable:
100 | 
101 | ```bash
102 | chmod +x ./path/to/your/stdio-mcp-server.cjs
103 | ```
```

--------------------------------------------------------------------------------
/src/routing/retry-handler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Logger } from '../utils/logger.js'
  2 | 
  3 | export type JitterMode = 'none' | 'full'
  4 | 
  5 | export interface RetryPolicy {
  6 |   maxRetries: number
  7 |   baseDelayMs: number
  8 |   maxDelayMs: number
  9 |   backoffFactor: number // multiplier per attempt
 10 |   jitter: JitterMode
 11 |   timeoutMs?: number // overall timeout budget (optional)
 12 |   retryOn?: {
 13 |     networkErrors?: boolean
 14 |     httpStatuses?: number[]
 15 |     httpStatusClasses?: Array<4 | 5> // 4=4xx,5=5xx
 16 |   }
 17 | }
 18 | 
 19 | export interface RetryContext {
 20 |   attempt: number
 21 |   lastError?: unknown
 22 |   lastStatus?: number
 23 | }
 24 | 
 25 | function sleep(ms: number): Promise<void> {
 26 |   return new Promise((resolve) => setTimeout(resolve, ms))
 27 | }
 28 | 
 29 | function withJitter(base: number, mode: JitterMode): number {
 30 |   if (mode === 'none') return base
 31 |   // Full jitter: random between 0 and base
 32 |   return Math.floor(Math.random() * base)
 33 | }
 34 | 
 35 | function isRetryable(policy: RetryPolicy, _err: unknown, status?: number): boolean {
 36 |   if (status !== undefined) {
 37 |     if (policy.retryOn?.httpStatuses?.includes(status)) return true
 38 |     const klass = Math.floor(status / 100)
 39 |     if (klass === 5 && policy.retryOn?.httpStatusClasses?.includes(5)) return true
 40 |     if (klass === 4 && policy.retryOn?.httpStatusClasses?.includes(4)) return status === 408 || status === 429
 41 |     return false
 42 |   }
 43 |   // No status means a network error or thrown exception from fetch
 44 |   return Boolean(policy.retryOn?.networkErrors ?? true)
 45 | }
 46 | 
 47 | export class RetryHandler {
 48 |   private readonly policy: RetryPolicy
 49 | 
 50 |   constructor(policy?: Partial<RetryPolicy>) {
 51 |     this.policy = {
 52 |       maxRetries: policy?.maxRetries ?? 3,
 53 |       baseDelayMs: policy?.baseDelayMs ?? 200,
 54 |       maxDelayMs: policy?.maxDelayMs ?? 5_000,
 55 |       backoffFactor: policy?.backoffFactor ?? 2,
 56 |       jitter: policy?.jitter ?? 'full',
 57 |       timeoutMs: policy?.timeoutMs,
 58 |       retryOn: policy?.retryOn ?? { networkErrors: true, httpStatusClasses: [5], httpStatuses: [408, 429] },
 59 |     }
 60 |   }
 61 | 
 62 |   async execute<T>(op: () => Promise<T>, onRetry?: (ctx: RetryContext) => void): Promise<T> {
 63 |     const start = Date.now()
 64 |     let delay = this.policy.baseDelayMs
 65 |     let lastError: unknown
 66 |     for (let attempt = 0; attempt <= this.policy.maxRetries; attempt++) {
 67 |       try {
 68 |         const result = await op()
 69 |         return result
 70 |       } catch (err: any) {
 71 |         // If this looks like a fetch Response-like error, extract status
 72 |         let status: number | undefined
 73 |         if (err && typeof err === 'object' && 'status' in err && typeof (err as any).status === 'number') {
 74 |           status = (err as any).status
 75 |         }
 76 | 
 77 |         if (attempt >= this.policy.maxRetries || !isRetryable(this.policy, err, status)) {
 78 |           throw err
 79 |         }
 80 | 
 81 |         lastError = err
 82 |         const ctx: RetryContext = { attempt, lastError, lastStatus: status }
 83 |         try {
 84 |           onRetry?.(ctx)
 85 |         } catch { /* ignore */ }
 86 | 
 87 |         if (this.policy.timeoutMs && Date.now() - start + delay > this.policy.timeoutMs) {
 88 |           Logger.warn('Retry timeout budget exceeded')
 89 |           throw err
 90 |         }
 91 | 
 92 |         const wait = Math.min(this.policy.maxDelayMs, withJitter(delay, this.policy.jitter))
 93 |         await sleep(wait)
 94 |         delay = Math.min(this.policy.maxDelayMs, Math.floor(delay * this.policy.backoffFactor))
 95 |       }
 96 |     }
 97 |     // Should be unreachable
 98 |     throw lastError ?? new Error('RetryHandler failed without error')
 99 |   }
100 | }
101 | 
```

--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { OAuthDelegation } from './auth.js'
  2 | 
  3 | export interface MasterConfig {
  4 |   master_oauth: MasterOAuthConfig
  5 |   servers: ServerConfig[]
  6 |   oauth_delegation?: OAuthDelegationConfig
  7 |   hosting: HostingConfig
  8 |   routing?: RoutingConfig
  9 |   logging?: LoggingConfig
 10 |   security?: SecurityConfig
 11 | }
 12 | 
 13 | export interface ServerConfig {
 14 |   id: string
 15 |   type: 'git' | 'npm' | 'pypi' | 'docker' | 'local'
 16 |   url?: string
 17 |   package?: string
 18 |   version?: string
 19 |   branch?: string
 20 |   auth_strategy: AuthStrategy
 21 |   auth_config?: ServerAuthConfig
 22 |   config: {
 23 |     environment?: Record<string, string>
 24 |     args?: string[]
 25 |     port?: number
 26 |   }
 27 | }
 28 | 
 29 | export enum AuthStrategy {
 30 |   MASTER_OAUTH = 'master_oauth',
 31 |   DELEGATE_OAUTH = 'delegate_oauth',
 32 |   BYPASS_AUTH = 'bypass_auth',
 33 |   PROXY_OAUTH = 'proxy_oauth'
 34 | }
 35 | 
 36 | export interface MasterOAuthConfig {
 37 |   issuer?: string
 38 |   authorization_endpoint: string
 39 |   token_endpoint: string
 40 |   jwks_uri?: string
 41 |   client_id: string
 42 |   client_secret?: string
 43 |   redirect_uri: string
 44 |   scopes: string[]
 45 |   audience?: string
 46 | }
 47 | 
 48 | // Alias used by MultiAuthManager constructor in later phases
 49 | export type MasterAuthConfig = MasterOAuthConfig
 50 | 
 51 | export interface OAuthDelegationConfig {
 52 |   enabled: boolean
 53 |   callback_base_url?: string
 54 |   // Optional pre-configured providers by ID
 55 |   providers?: Record<string, ServerAuthConfig>
 56 | }
 57 | 
 58 | export interface HostingConfig {
 59 |   platform: 'node' | 'cloudflare-workers' | 'koyeb' | 'docker' | 'unknown'
 60 |   port?: number
 61 |   base_url?: string
 62 |   // Optional platform-specific storage/backend hints
 63 |   storage_backend?: 'memory' | 'fs' | 'durable_object' | 'kv' | 's3'
 64 | }
 65 | 
 66 | export interface LoggingConfig {
 67 |   level?: 'debug' | 'info' | 'warn' | 'error'
 68 | }
 69 | 
 70 | export interface SecurityConfig {
 71 |   // Env var name containing encryption key for config secrets
 72 |   config_key_env?: string
 73 |   // Enable audit logging for config changes
 74 |   audit?: boolean
 75 |   // Optional secret rotation policy in days
 76 |   rotation_days?: number
 77 | }
 78 | 
 79 | export interface ServerAuthConfig {
 80 |   provider: 'github' | 'google' | 'custom'
 81 |   authorization_endpoint: string
 82 |   token_endpoint: string
 83 |   client_id: string
 84 |   client_secret?: string
 85 |   scopes?: string[]
 86 |   // Additional provider-specific fields
 87 |   [key: string]: unknown
 88 | }
 89 | 
 90 | // Re-export for convenience in consumers
 91 | export type { OAuthDelegation }
 92 | 
 93 | // ---- Routing configuration ----
 94 | export type LoadBalancingStrategy = 'round_robin' | 'weighted' | 'health'
 95 | 
 96 | export interface LoadBalancerConfig {
 97 |   strategy?: LoadBalancingStrategy
 98 | }
 99 | 
100 | export interface CircuitBreakerConfig {
101 |   failureThreshold?: number
102 |   successThreshold?: number
103 |   recoveryTimeoutMs?: number
104 | }
105 | 
106 | export interface RetryPolicyConfig {
107 |   maxRetries?: number
108 |   baseDelayMs?: number
109 |   maxDelayMs?: number
110 |   backoffFactor?: number
111 |   jitter?: 'none' | 'full'
112 |   retryOn?: {
113 |     networkErrors?: boolean
114 |     httpStatuses?: number[]
115 |     httpStatusClasses?: Array<4 | 5>
116 |   }
117 | }
118 | 
119 | export interface RoutingConfig {
120 |   loadBalancer?: LoadBalancerConfig
121 |   circuitBreaker?: CircuitBreakerConfig
122 |   retry?: RetryPolicyConfig
123 | }
124 | 
125 | // Defaults for consumers that want a baseline configuration
126 | export const DefaultRoutingConfig: RoutingConfig = {
127 |   loadBalancer: { strategy: 'round_robin' },
128 |   circuitBreaker: { failureThreshold: 5, successThreshold: 2, recoveryTimeoutMs: 30_000 },
129 |   retry: { maxRetries: 2, baseDelayMs: 250, maxDelayMs: 4_000, backoffFactor: 2, jitter: 'full' },
130 | }
131 | 
132 | export const DefaultHostingConfig: HostingConfig = {
133 |   platform: 'node',
134 |   port: 3000,
135 | }
136 | 
```

--------------------------------------------------------------------------------
/docs/guides/authentication.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Authentication Guide
  2 | 
  3 | Master MCP Server supports multiple authentication strategies between the client (master) and each backend server.
  4 | 
  5 | ## Strategies
  6 | 
  7 | - master_oauth: Pass the client token from the master directly to the backend.
  8 | - delegate_oauth: Instruct the client to complete an OAuth flow against the backend provider, then store a backend token.
  9 | - proxy_oauth: Use the master to refresh and proxy backend tokens, falling back to pass-through.
 10 | - bypass_auth: No auth headers are sent to the backend.
 11 | 
 12 | Configure per-server via `servers[].auth_strategy` and optional `servers[].auth_config`.
 13 | 
 14 | <AuthFlowDemo />
 15 | 
 16 | <CodeTabs :options="[
 17 |   { label: 'master_oauth', value: 'master' },
 18 |   { label: 'delegate_oauth', value: 'delegate' },
 19 |   { label: 'proxy_oauth', value: 'proxy' },
 20 |   { label: 'bypass_auth', value: 'bypass' }
 21 | ]">
 22 |   <template #master>
 23 | 
 24 | ```yaml
 25 | servers:
 26 |   - id: search
 27 |     type: local
 28 |     auth_strategy: master_oauth
 29 |     config: { port: 4100 }
 30 | ```
 31 | 
 32 |   </template>
 33 |   <template #delegate>
 34 | 
 35 | ```yaml
 36 | servers:
 37 |   - id: github-tools
 38 |     type: local
 39 |     auth_strategy: delegate_oauth
 40 |     auth_config:
 41 |       provider: github
 42 |       authorization_endpoint: https://github.com/login/oauth/authorize
 43 |       token_endpoint: https://github.com/login/oauth/access_token
 44 |       client_id: ${GITHUB_CLIENT_ID}
 45 |       client_secret: env:GITHUB_CLIENT_SECRET
 46 |       scopes: [repo, read:user]
 47 |     config: { port: 4010 }
 48 | ```
 49 | 
 50 |   </template>
 51 |   <template #proxy>
 52 | 
 53 | ```yaml
 54 | servers:
 55 |   - id: internal
 56 |     type: local
 57 |     auth_strategy: proxy_oauth
 58 |     auth_config:
 59 |       token_source: env:INTERNAL_BACKEND_TOKEN
 60 |     config: { port: 4200 }
 61 | ```
 62 | 
 63 |   </template>
 64 |   <template #bypass>
 65 | 
 66 | ```yaml
 67 | servers:
 68 |   - id: public
 69 |     type: local
 70 |     auth_strategy: bypass_auth
 71 |     config: { port: 4300 }
 72 | ```
 73 | 
 74 |   </template>
 75 | </CodeTabs>
 76 | 
 77 | ```yaml
 78 | servers:
 79 |   - id: github-tools
 80 |     type: local
 81 |     auth_strategy: delegate_oauth
 82 |     auth_config:
 83 |       provider: github
 84 |       authorization_endpoint: https://github.com/login/oauth/authorize
 85 |       token_endpoint: https://github.com/login/oauth/access_token
 86 |       client_id: ${GITHUB_CLIENT_ID}
 87 |       client_secret: env:GITHUB_CLIENT_SECRET
 88 |       scopes: [repo, read:user]
 89 |     config:
 90 |       port: 4010
 91 | ```
 92 | 
 93 | ## Flow Overview
 94 | 
 95 | 1) Client calls a tool/resource via master with `Authorization: Bearer <client_token>`.
 96 | 2) Master determines server strategy via `MultiAuthManager`.
 97 | 3) If delegation is required, master responds with `{ type: 'oauth_delegation', ... }` metadata.
 98 | 4) Client opens `GET /oauth/authorize?server_id=<id>` to initiate the auth code + PKCE flow.
 99 | 5) Redirect back to `GET /oauth/callback` stores the backend token (associated with client token + server id).
100 | 6) Retries to the backend now include `Authorization: Bearer <server_token>` as needed.
101 | 
102 | ## Endpoints
103 | 
104 | - `GET /oauth/authorize` → Starts flow; query: `server_id`, optional `provider` if preconfigured.
105 | - `GET /oauth/callback` → Exchanges code for token and stores it.
106 | - `GET /oauth/success` + `GET /oauth/error` → Result pages.
107 | 
108 | These are mounted automatically in the Node runtime (`src/index.ts`) and can be used in Workers via `OAuthFlowController.handleRequest()`.
109 | 
110 | ## Customizing Auth
111 | 
112 | Attach a custom `MultiAuthManager` instance to the `MasterServer`:
113 | 
114 | ```ts
115 | import { MasterServer } from '../src/server/master-server'
116 | import { MultiAuthManager } from '../src/auth/multi-auth-manager'
117 | 
118 | const master = new MasterServer()
119 | const auth = new MultiAuthManager(config.master_oauth)
120 | auth.registerServerAuth('github-tools', 'delegate_oauth', {/* provider config */})
121 | master.attachAuthManager(auth)
122 | ```
123 | 
124 | See `examples/custom-auth` for a working example.
125 | 
```

--------------------------------------------------------------------------------
/src/utils/errors.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Standardized error types and helpers.
  3 |  */
  4 | 
  5 | export type ErrorSeverity = 'fatal' | 'error' | 'warn'
  6 | 
  7 | export interface SerializedError {
  8 |   name: string
  9 |   message: string
 10 |   code?: string
 11 |   status?: number
 12 |   severity?: ErrorSeverity
 13 |   details?: unknown
 14 |   stack?: string
 15 |   cause?: SerializedError
 16 | }
 17 | 
 18 | export class AppError extends Error {
 19 |   code?: string
 20 |   status?: number
 21 |   severity: ErrorSeverity
 22 |   details?: unknown
 23 |   override cause?: unknown
 24 | 
 25 |   constructor(message: string, opts?: { code?: string; status?: number; severity?: ErrorSeverity; details?: unknown; cause?: unknown }) {
 26 |     super(message)
 27 |     this.name = this.constructor.name
 28 |     this.code = opts?.code
 29 |     this.status = opts?.status
 30 |     this.severity = opts?.severity ?? 'error'
 31 |     this.details = opts?.details
 32 |     this.cause = opts?.cause
 33 |   }
 34 | 
 35 |   toJSON(): SerializedError {
 36 |     return serializeError(this)
 37 |   }
 38 | }
 39 | 
 40 | export class ValidationError extends AppError {
 41 |   constructor(message: string, details?: unknown) {
 42 |     super(message, { code: 'VALIDATION_ERROR', status: 400, details, severity: 'warn' })
 43 |   }
 44 | }
 45 | 
 46 | export class AuthError extends AppError {
 47 |   constructor(message: string, details?: unknown) {
 48 |     super(message, { code: 'AUTH_ERROR', status: 401, details })
 49 |   }
 50 | }
 51 | 
 52 | export class PermissionError extends AppError {
 53 |   constructor(message: string, details?: unknown) {
 54 |     super(message, { code: 'FORBIDDEN', status: 403, details })
 55 |   }
 56 | }
 57 | 
 58 | export class NotFoundError extends AppError {
 59 |   constructor(message: string, details?: unknown) {
 60 |     super(message, { code: 'NOT_FOUND', status: 404, details, severity: 'warn' })
 61 |   }
 62 | }
 63 | 
 64 | export class RateLimitError extends AppError {
 65 |   constructor(message: string, details?: unknown) {
 66 |     super(message, { code: 'RATE_LIMITED', status: 429, details, severity: 'warn' })
 67 |   }
 68 | }
 69 | 
 70 | export class ExternalServiceError extends AppError {
 71 |   constructor(message: string, details?: unknown) {
 72 |     super(message, { code: 'EXTERNAL_SERVICE_ERROR', status: 502, details })
 73 |   }
 74 | }
 75 | 
 76 | export class CircuitBreakerError extends AppError {
 77 |   constructor(message: string, details?: unknown) {
 78 |     super(message, { code: 'CIRCUIT_OPEN', status: 503, details })
 79 |   }
 80 | }
 81 | 
 82 | export function serializeError(err: unknown): SerializedError {
 83 |   if (err instanceof AppError) {
 84 |     return {
 85 |       name: err.name,
 86 |       message: err.message,
 87 |       code: err.code,
 88 |       status: err.status,
 89 |       severity: err.severity,
 90 |       details: err.details,
 91 |       stack: err.stack,
 92 |       cause: err.cause ? serializeError(err.cause as any) : undefined,
 93 |     }
 94 |   }
 95 |   if (err instanceof Error) {
 96 |     return {
 97 |       name: err.name,
 98 |       message: err.message,
 99 |       stack: err.stack,
100 |     }
101 |   }
102 |   return { name: 'Error', message: String(err) }
103 | }
104 | 
105 | export function deserializeError(obj: SerializedError): AppError {
106 |   const err = new AppError(obj.message, {
107 |     code: obj.code,
108 |     status: obj.status,
109 |     severity: obj.severity,
110 |     details: obj.details,
111 |     cause: obj.cause ? deserializeError(obj.cause) : undefined,
112 |   })
113 |   err.name = obj.name
114 |   err.stack = obj.stack
115 |   return err
116 | }
117 | 
118 | export function withErrorContext<T>(fn: () => Promise<T>, context: Record<string, unknown>): Promise<T> {
119 |   return fn().catch((e) => {
120 |     if (e instanceof AppError) throw new AppError(e.message, { ...e, details: { ...(e.details as any), ...context } })
121 |     const err = new AppError('Unhandled error', { code: 'UNHANDLED', details: { cause: serializeError(e), ...context } })
122 |     throw err
123 |   })
124 | }
125 | 
126 | export function stackTrace(e?: Error): string {
127 |   const err = e ?? new Error('stack')
128 |   return err.stack || ''
129 | }
130 | 
131 | export function isAppError(e: unknown): e is AppError {
132 |   return e instanceof AppError
133 | }
134 | 
```

--------------------------------------------------------------------------------
/examples/custom-auth/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import express from 'express'
 2 | import type { Request, Response } from 'express'
 3 | import { ConfigLoader } from '../../src/config/config-loader.js'
 4 | import { MasterServer } from '../../src/server/master-server.js'
 5 | import { MultiAuthManager } from '../../src/auth/multi-auth-manager.js'
 6 | import { CapabilityAggregator } from '../../src/modules/capability-aggregator.js'
 7 | import { collectSystemMetrics } from '../../src/utils/monitoring.js'
 8 | 
 9 | // A custom auth manager that adds an extra header for a specific backend
10 | class CustomAuthManager extends MultiAuthManager {
11 |   override async handleMasterOAuth(serverId: string, clientToken: string) {
12 |     const base = await super.handleMasterOAuth(serverId, clientToken)
13 |     // Example: add a hint header for a particular backend
14 |     if (serverId === 'custom-proxy') {
15 |       return { ...base, 'X-Custom-Auth': 'enabled' }
16 |     }
17 |     return base
18 |   }
19 | }
20 | 
21 | async function main() {
22 |   const configPath = process.env.MASTER_CONFIG_PATH || 'examples/custom-auth/config.yaml'
23 |   const cfg = await ConfigLoader.load({ path: configPath })
24 | 
25 |   const master = new MasterServer()
26 |   const customAuth = new CustomAuthManager(cfg.master_oauth)
27 |   // Register per-server strategies from config
28 |   for (const s of cfg.servers) customAuth.registerServerAuth(s.id, s.auth_strategy as any, s.auth_config as any)
29 |   master.attachAuthManager(customAuth)
30 | 
31 |   await master.startFromConfig(cfg)
32 | 
33 |   const app = express()
34 |   app.use(express.json())
35 | 
36 |   app.get('/health', (_req, res) => res.json({ ok: true }))
37 |   app.get('/metrics', (_req, res) => res.json({ ok: true, system: collectSystemMetrics() }))
38 | 
39 |   // OAuth endpoints
40 |   master.getOAuthFlowController().registerExpress(app)
41 | 
42 |   // Capabilities
43 |   app.get('/capabilities', (_req: Request, res: Response) => {
44 |     const agg = new CapabilityAggregator()
45 |     const caps = agg.aggregate(Array.from(master.getRouter().getServers().values()))
46 |     res.json(caps)
47 |   })
48 | 
49 |   const getToken = (req: Request) => {
50 |     const h = req.headers['authorization'] || req.headers['Authorization']
51 |     return typeof h === 'string' && h.toLowerCase().startsWith('bearer ') ? h.slice(7) : undefined
52 |   }
53 | 
54 |   // MCP endpoints
55 |   app.post('/mcp/tools/list', async (_req: Request, res: Response) => {
56 |     const handler = master.handler
57 |     const result = await handler.handleListTools({ type: 'list_tools' } as any)
58 |     res.json(result)
59 |   })
60 | 
61 |   app.post('/mcp/tools/call', async (req: Request, res: Response) => {
62 |     const token = getToken(req)
63 |     const handler = new (master.handler.constructor as any)({
64 |       aggregator: (master as any).aggregator,
65 |       router: master.getRouter(),
66 |       getClientToken: () => token,
67 |     }) as typeof master.handler
68 |     const result = await handler.handleCallTool({ name: req.body?.name, arguments: req.body?.arguments ?? {} } as any)
69 |     res.json(result)
70 |   })
71 | 
72 |   app.post('/mcp/resources/list', async (_req: Request, res: Response) => {
73 |     const handler = master.handler
74 |     const result = await handler.handleListResources({ type: 'list_resources' } as any)
75 |     res.json(result)
76 |   })
77 | 
78 |   app.post('/mcp/resources/read', async (req: Request, res: Response) => {
79 |     const token = getToken(req)
80 |     const handler = new (master.handler.constructor as any)({
81 |       aggregator: (master as any).aggregator,
82 |       router: master.getRouter(),
83 |       getClientToken: () => token,
84 |     }) as typeof master.handler
85 |     const result = await handler.handleReadResource({ uri: req.body?.uri } as any)
86 |     res.json(result)
87 |   })
88 | 
89 |   const port = cfg.hosting.port || 3000
90 |   app.listen(port, () => console.log(`Custom auth example on :${port}`))
91 | }
92 | 
93 | main().catch((err) => {
94 |   console.error(err)
95 |   process.exit(1)
96 | })
97 | 
98 | 
```

--------------------------------------------------------------------------------
/tests/unit/routing/circuit-breaker.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { test } from 'node:test'
  2 | import assert from 'node:assert'
  3 | import { CircuitBreaker, InMemoryCircuitStorage } from '../../../src/routing/circuit-breaker.js'
  4 | 
  5 | test('CircuitBreaker onSuccess in closed state should only reset failures', async () => {
  6 |   const storage = new InMemoryCircuitStorage()
  7 |   const breaker = new CircuitBreaker({
  8 |     failureThreshold: 2,
  9 |     successThreshold: 2,
 10 |     recoveryTimeoutMs: 100,
 11 |   }, storage)
 12 |   const key = 'test-key'
 13 | 
 14 |   // Simulate some failures, but not enough to open the circuit
 15 |   breaker.onFailure(key)
 16 |   let record = storage.get(key)
 17 |   assert.strictEqual(record?.failures, 1, 'should have 1 failure')
 18 |   assert.strictEqual(record?.state, 'closed', 'should be in closed state')
 19 | 
 20 |   // Simulate a success
 21 |   breaker.onSuccess(key)
 22 |   record = storage.get(key)
 23 |   assert.strictEqual(record?.failures, 0, 'failures should be reset to 0')
 24 |   assert.strictEqual(record?.successes, 0, 'successes should remain 0') // This is the key check for the bug
 25 |   assert.strictEqual(record?.state, 'closed', 'should remain in closed state')
 26 | })
 27 | 
 28 | test('CircuitBreaker should open after reaching failure threshold', async () => {
 29 |     const storage = new InMemoryCircuitStorage()
 30 |     const breaker = new CircuitBreaker({
 31 |       failureThreshold: 2,
 32 |       successThreshold: 2,
 33 |       recoveryTimeoutMs: 100,
 34 |     }, storage)
 35 |     const key = 'test-key'
 36 | 
 37 |     breaker.onFailure(key)
 38 |     breaker.onFailure(key)
 39 | 
 40 |     const record = storage.get(key)
 41 |     assert.strictEqual(record?.state, 'open', 'should be in open state')
 42 |     assert.strictEqual(record?.failures, 2, 'should have 2 failures')
 43 | })
 44 | 
 45 | test('CircuitBreaker should transition to half-open after recovery timeout', async () => {
 46 |     const storage = new InMemoryCircuitStorage()
 47 |     const breaker = new CircuitBreaker({
 48 |       failureThreshold: 2,
 49 |       successThreshold: 2,
 50 |       recoveryTimeoutMs: 50,
 51 |     }, storage)
 52 |     const key = 'test-key'
 53 | 
 54 |     // Open the circuit
 55 |     breaker.onFailure(key)
 56 |     breaker.onFailure(key)
 57 | 
 58 |     // Wait for recovery timeout
 59 |     await new Promise(resolve => setTimeout(resolve, 60))
 60 | 
 61 |     // First call should be allowed and move to half-open
 62 |     const gate = breaker.canExecute(key)
 63 |     assert.strictEqual(gate.allowed, true, 'execution should be allowed')
 64 | 
 65 |     const record = storage.get(key)
 66 |     assert.strictEqual(record?.state, 'half_open', 'should be in half-open state')
 67 | })
 68 | 
 69 | test('CircuitBreaker should close after successes in half-open state', async () => {
 70 |     const storage = new InMemoryCircuitStorage()
 71 |     const breaker = new CircuitBreaker({
 72 |       failureThreshold: 2,
 73 |       successThreshold: 2,
 74 |       recoveryTimeoutMs: 50,
 75 |     }, storage)
 76 |     const key = 'test-key'
 77 | 
 78 |     // Open the circuit
 79 |     breaker.onFailure(key)
 80 |     breaker.onFailure(key)
 81 | 
 82 |     // Wait for recovery timeout
 83 |     await new Promise(resolve => setTimeout(resolve, 60))
 84 | 
 85 |     // Transition to half-open
 86 |     breaker.canExecute(key)
 87 | 
 88 |     // Succeed twice
 89 |     breaker.onSuccess(key)
 90 |     breaker.onSuccess(key)
 91 | 
 92 |     const record = storage.get(key)
 93 |     assert.strictEqual(record?.state, 'closed', 'should be in closed state')
 94 |     assert.strictEqual(record?.failures, 0, 'failures should be reset')
 95 |     assert.strictEqual(record?.successes, 0, 'successes should be reset')
 96 | })
 97 | 
 98 | test('CircuitBreaker should re-open after failure in half-open state', async () => {
 99 |     const storage = new InMemoryCircuitStorage()
100 |     const breaker = new CircuitBreaker({
101 |       failureThreshold: 2,
102 |       successThreshold: 2,
103 |       recoveryTimeoutMs: 50,
104 |     }, storage)
105 |     const key = 'test-key'
106 | 
107 |     // Open the circuit
108 |     breaker.onFailure(key)
109 |     breaker.onFailure(key)
110 | 
111 |     // Wait for recovery timeout
112 |     await new Promise(resolve => setTimeout(resolve, 60))
113 | 
114 |     // Transition to half-open
115 |     breaker.canExecute(key)
116 | 
117 |     // Fail once
118 |     breaker.onFailure(key)
119 | 
120 |     const record = storage.get(key)
121 |     assert.strictEqual(record?.state, 'open', 'should be in open state again')
122 | })
```

--------------------------------------------------------------------------------
/examples/test-stdio-server.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | // Simple test STDIO server for testing purposes
  4 | 
  5 | // Simple JSON-RPC server that echoes back requests
  6 | 
  7 | // Function to send a response
  8 | function sendResponse(response) {
  9 |   process.stdout.write(JSON.stringify(response) + '\n');
 10 | }
 11 | 
 12 | // Function to send a notification
 13 | function sendNotification(notification) {
 14 |   process.stdout.write(JSON.stringify(notification) + '\n');
 15 | }
 16 | 
 17 | // Handle incoming requests
 18 | process.stdin.on('data', (data) => {
 19 |   try {
 20 |     const lines = data.toString().split('\n').filter(line => line.trim() !== '');
 21 |     for (const line of lines) {
 22 |       if (line.trim() === '') continue;
 23 |       
 24 |       const request = JSON.parse(line);
 25 |       
 26 |       // Handle initialize request
 27 |       if (request.method === 'initialize') {
 28 |         const response = {
 29 |           jsonrpc: '2.0',
 30 |           id: request.id,
 31 |           result: {
 32 |             protocolVersion: '2025-06-18',
 33 |             capabilities: {
 34 |               tools: {
 35 |                 listChanged: true
 36 |               },
 37 |               resources: {
 38 |                 listChanged: true
 39 |               }
 40 |             },
 41 |             serverInfo: {
 42 |               name: 'test-stdio-server',
 43 |               version: '1.0.0'
 44 |             }
 45 |           }
 46 |         };
 47 |         sendResponse(response);
 48 |       }
 49 |       // Handle tools/list request
 50 |       else if (request.method === 'tools/list') {
 51 |         const response = {
 52 |           jsonrpc: '2.0',
 53 |           id: request.id,
 54 |           result: {
 55 |             tools: [
 56 |               {
 57 |                 name: 'stdio-echo',
 58 |                 description: 'Echoes back the input',
 59 |                 inputSchema: {
 60 |                   type: 'object',
 61 |                   properties: {
 62 |                     message: {
 63 |                       type: 'string'
 64 |                     }
 65 |                   },
 66 |                   required: ['message']
 67 |                 }
 68 |               }
 69 |             ]
 70 |           }
 71 |         };
 72 |         sendResponse(response);
 73 |       }
 74 |       // Handle resources/list request
 75 |       else if (request.method === 'resources/list') {
 76 |         const response = {
 77 |           jsonrpc: '2.0',
 78 |           id: request.id,
 79 |           result: {
 80 |             resources: [
 81 |               {
 82 |                 uri: 'stdio://example/resource',
 83 |                 name: 'stdio-resource',
 84 |                 description: 'A test resource from STDIO server'
 85 |               }
 86 |             ]
 87 |           }
 88 |         };
 89 |         sendResponse(response);
 90 |       }
 91 |       // Handle tools/call request
 92 |       else if (request.method === 'tools/call') {
 93 |         const response = {
 94 |           jsonrpc: '2.0',
 95 |           id: request.id,
 96 |           result: {
 97 |             content: [
 98 |               {
 99 |                 type: 'text',
100 |                 text: `STDIO Echo: ${request.params.arguments?.message || 'No message'}`
101 |               }
102 |             ]
103 |           }
104 |         };
105 |         sendResponse(response);
106 |       }
107 |       // Handle resources/read request
108 |       else if (request.method === 'resources/read') {
109 |         const response = {
110 |           jsonrpc: '2.0',
111 |           id: request.id,
112 |           result: {
113 |             contents: [
114 |               {
115 |                 uri: request.params.uri,
116 |                 text: 'This is content from a STDIO server resource',
117 |                 mimeType: 'text/plain'
118 |               }
119 |             ]
120 |           }
121 |         };
122 |         sendResponse(response);
123 |       }
124 |       // Handle unknown methods
125 |       else {
126 |         const response = {
127 |           jsonrpc: '2.0',
128 |           id: request.id,
129 |           error: {
130 |             code: -32601,
131 |             message: `Method not found: ${request.method}`
132 |           }
133 |         };
134 |         sendResponse(response);
135 |       }
136 |     }
137 |   } catch (err) {
138 |     // Send error response
139 |     const errorResponse = {
140 |       jsonrpc: '2.0',
141 |       id: null,
142 |       error: {
143 |         code: -32700,
144 |         message: 'Parse error',
145 |         data: err.message
146 |       }
147 |     };
148 |     sendResponse(errorResponse);
149 |   }
150 | });
151 | 
152 | // Send a notification that the server is ready
153 | sendNotification({
154 |   jsonrpc: '2.0',
155 |   method: 'notifications/initialized',
156 |   params: {}
157 | });
158 | 
159 | console.error('Test STDIO server started');
```

--------------------------------------------------------------------------------
/src/utils/http.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * HTTP utilities for request/response handling, header manipulation, and content
  3 |  * type parsing. Minimal and cross-platform (Node 18+ and Workers).
  4 |  */
  5 | 
  6 | export type HeadersLike = Headers | Record<string, string> | Array<[string, string]>
  7 | 
  8 | export function normalizeHeaders(h: HeadersLike | undefined | null): Record<string, string> {
  9 |   const out: Record<string, string> = {}
 10 |   if (!h) return out
 11 |   if (typeof (h as any).forEach === 'function') {
 12 |     ;(h as Headers).forEach((v, k) => (out[k.toLowerCase()] = v))
 13 |     return out
 14 |   }
 15 |   if (Array.isArray(h)) {
 16 |     for (const [k, v] of h) out[k.toLowerCase()] = String(v)
 17 |     return out
 18 |   }
 19 |   for (const [k, v] of Object.entries(h)) out[k.toLowerCase()] = String(v)
 20 |   return out
 21 | }
 22 | 
 23 | export function getHeader(h: HeadersLike | undefined | null, name: string): string | undefined {
 24 |   const map = normalizeHeaders(h)
 25 |   return map[name.toLowerCase()]
 26 | }
 27 | 
 28 | export function setHeader(h: HeadersLike | undefined | null, name: string, value: string): HeadersLike {
 29 |   if (!h) return { [name]: value }
 30 |   if (typeof (h as any).set === 'function') {
 31 |     const copy = new Headers(h as Headers)
 32 |     copy.set(name, value)
 33 |     return copy
 34 |   }
 35 |   const map = normalizeHeaders(h)
 36 |   map[name.toLowerCase()] = value
 37 |   return map
 38 | }
 39 | 
 40 | export function getContentType(h: HeadersLike | undefined | null): string | undefined {
 41 |   return getHeader(h, 'content-type')
 42 | }
 43 | 
 44 | export function isJsonContentType(ct?: string): boolean {
 45 |   if (!ct) return false
 46 |   return /application\/json|\+json/i.test(ct)
 47 | }
 48 | 
 49 | export async function parseBody(req: Request, limitBytes = 1_000_000): Promise<any> {
 50 |   const ct = getContentType(req.headers)
 51 |   if (isJsonContentType(ct)) {
 52 |     const text = await readTextLimited(req, limitBytes)
 53 |     try {
 54 |       return JSON.parse(text)
 55 |     } catch (e) {
 56 |       throw new Error('Invalid JSON body')
 57 |     }
 58 |   }
 59 |   if (/text\//i.test(ct || '')) return readTextLimited(req, limitBytes)
 60 |   // default to arrayBuffer
 61 |   const buf = await req.arrayBuffer()
 62 |   if (buf.byteLength > limitBytes) throw new Error('Body too large')
 63 |   return buf
 64 | }
 65 | 
 66 | export async function readTextLimited(req: Request, limitBytes: number): Promise<string> {
 67 |   const buf = await req.arrayBuffer()
 68 |   if (buf.byteLength > limitBytes) throw new Error('Body too large')
 69 |   return new TextDecoder().decode(buf)
 70 | }
 71 | 
 72 | export function jsonResponse(body: unknown, init?: ResponseInit): Response {
 73 |   const headers = new Headers(init?.headers)
 74 |   headers.set('content-type', 'application/json; charset=utf-8')
 75 |   const payload = JSON.stringify(body)
 76 |   return new Response(payload, { ...init, headers })
 77 | }
 78 | 
 79 | export function buildQuery(params: Record<string, string | number | boolean | null | undefined>): string {
 80 |   const usp = new URLSearchParams()
 81 |   for (const [k, v] of Object.entries(params)) {
 82 |     if (v === undefined || v === null) continue
 83 |     usp.append(k, String(v))
 84 |   }
 85 |   const s = usp.toString()
 86 |   return s ? `?${s}` : ''
 87 | }
 88 | 
 89 | export function appendQuery(url: string, params: Record<string, string | number | boolean | null | undefined>): string {
 90 |   const u = new URL(url, 'http://localhost')
 91 |   for (const [k, v] of Object.entries(params)) {
 92 |     if (v === undefined || v === null) continue
 93 |     u.searchParams.set(k, String(v))
 94 |   }
 95 |   if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
 96 |     // Relative URL; return pathname+search only
 97 |     return `${u.pathname}${u.search}`
 98 |   }
 99 |   return u.toString()
100 | }
101 | 
102 | export function ensureCorrelationId(headers?: HeadersLike | null): string {
103 |   const existing = headers && getHeader(headers, 'x-correlation-id')
104 |   if (existing) return existing
105 |   return randomId()
106 | }
107 | 
108 | export function randomId(): string {
109 |   const g: any = globalThis as any
110 |   try {
111 |     if (g.crypto?.randomUUID) return g.crypto.randomUUID()
112 |   } catch {
113 |     // ignore
114 |   }
115 |   try {
116 |     if (g.crypto?.getRandomValues) {
117 |       const bytes = new Uint8Array(16)
118 |       g.crypto.getRandomValues(bytes)
119 |       return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
120 |     }
121 |   } catch {
122 |     // ignore
123 |   }
124 |   return Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2)
125 | }
126 | 
127 | 
```

--------------------------------------------------------------------------------
/examples/stdio-mcp-server.cjs:
--------------------------------------------------------------------------------

```
  1 | #!/usr/bin/env node
  2 | 
  3 | // Simple test STDIO server for testing purposes
  4 | const fs = require('fs')
  5 | 
  6 | // Simple JSON-RPC server that echoes back requests
  7 | let messageId = 0
  8 | 
  9 | // Function to send a response
 10 | function sendResponse(response) {
 11 |   process.stdout.write(JSON.stringify(response) + '\n')
 12 | }
 13 | 
 14 | // Function to send a notification
 15 | function sendNotification(notification) {
 16 |   process.stdout.write(JSON.stringify(notification) + '\n')
 17 | }
 18 | 
 19 | // Handle incoming requests
 20 | process.stdin.on('data', (data) => {
 21 |   try {
 22 |     const lines = data.toString().split('\n').filter(line => line.trim() !== '')
 23 |     for (const line of lines) {
 24 |       if (line.trim() === '') continue
 25 |       
 26 |       const request = JSON.parse(line)
 27 |       
 28 |       // Handle initialize request
 29 |       if (request.method === 'initialize') {
 30 |         const response = {
 31 |           jsonrpc: '2.0',
 32 |           id: request.id,
 33 |           result: {
 34 |             protocolVersion: '2025-06-18',
 35 |             capabilities: {
 36 |               tools: {
 37 |                 listChanged: true
 38 |               },
 39 |               resources: {
 40 |                 listChanged: true
 41 |               }
 42 |             },
 43 |             serverInfo: {
 44 |               name: 'test-stdio-server',
 45 |               version: '1.0.0'
 46 |             }
 47 |           }
 48 |         }
 49 |         sendResponse(response)
 50 |       }
 51 |       // Handle tools/list request
 52 |       else if (request.method === 'tools/list') {
 53 |         const response = {
 54 |           jsonrpc: '2.0',
 55 |           id: request.id,
 56 |           result: {
 57 |             tools: [
 58 |               {
 59 |                 name: 'stdio-echo',
 60 |                 description: 'Echoes back the input',
 61 |                 inputSchema: {
 62 |                   type: 'object',
 63 |                   properties: {
 64 |                     message: {
 65 |                       type: 'string'
 66 |                     }
 67 |                   },
 68 |                   required: ['message']
 69 |                 }
 70 |               }
 71 |             ]
 72 |           }
 73 |         }
 74 |         sendResponse(response)
 75 |       }
 76 |       // Handle resources/list request
 77 |       else if (request.method === 'resources/list') {
 78 |         const response = {
 79 |           jsonrpc: '2.0',
 80 |           id: request.id,
 81 |           result: {
 82 |             resources: [
 83 |               {
 84 |                 uri: 'stdio://example/resource',
 85 |                 name: 'stdio-resource',
 86 |                 description: 'A test resource from STDIO server'
 87 |               }
 88 |             ]
 89 |           }
 90 |         }
 91 |         sendResponse(response)
 92 |       }
 93 |       // Handle tools/call request
 94 |       else if (request.method === 'tools/call') {
 95 |         const response = {
 96 |           jsonrpc: '2.0',
 97 |           id: request.id,
 98 |           result: {
 99 |             content: [
100 |               {
101 |                 type: 'text',
102 |                 text: `STDIO Echo: ${request.params.arguments?.message || 'No message'}`
103 |               }
104 |             ]
105 |           }
106 |         }
107 |         sendResponse(response)
108 |       }
109 |       // Handle resources/read request
110 |       else if (request.method === 'resources/read') {
111 |         const response = {
112 |           jsonrpc: '2.0',
113 |           id: request.id,
114 |           result: {
115 |             contents: [
116 |               {
117 |                 uri: request.params.uri,
118 |                 text: 'This is content from a STDIO server resource',
119 |                 mimeType: 'text/plain'
120 |               }
121 |             ]
122 |           }
123 |         }
124 |         sendResponse(response)
125 |       }
126 |       // Handle unknown methods
127 |       else {
128 |         const response = {
129 |           jsonrpc: '2.0',
130 |           id: request.id,
131 |           error: {
132 |             code: -32601,
133 |             message: `Method not found: ${request.method}`
134 |           }
135 |         }
136 |         sendResponse(response)
137 |       }
138 |     }
139 |   } catch (err) {
140 |     // Send error response
141 |     const errorResponse = {
142 |       jsonrpc: '2.0',
143 |       id: null,
144 |       error: {
145 |         code: -32700,
146 |         message: 'Parse error',
147 |         data: err.message
148 |       }
149 |     }
150 |     sendResponse(errorResponse)
151 |   }
152 | })
153 | 
154 | // Send a notification that the server is ready
155 | sendNotification({
156 |   jsonrpc: '2.0',
157 |   method: 'notifications/initialized',
158 |   params: {}
159 | })
160 | 
161 | console.error('Test STDIO server started')
```
Page 2/10FirstPrevNextLast