#
tokens: 45393/50000 12/252 files (page 4/10)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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

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

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { spawn } from 'node:child_process'
  4 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'
  5 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
  6 | 
  7 | async function startHttpServer() {
  8 |   console.log('Starting HTTP test server...')
  9 |   
 10 |   // Start the HTTP server as a background process
 11 |   const httpServer = spawn('node', ['examples/test-mcp-server.js'], {
 12 |     stdio: ['ignore', 'pipe', 'pipe'],
 13 |     env: { ...process.env, PORT: '3006' }
 14 |   })
 15 |   
 16 |   // Capture stdout and stderr
 17 |   httpServer.stdout.on('data', (data) => {
 18 |     console.log(`[HTTP Server] ${data.toString().trim()}`)
 19 |   })
 20 |   
 21 |   httpServer.stderr.on('data', (data) => {
 22 |     console.error(`[HTTP Server ERROR] ${data.toString().trim()}`)
 23 |   })
 24 |   
 25 |   // Wait for the server to start
 26 |   await new Promise((resolve, reject) => {
 27 |     let timeout = setTimeout(() => {
 28 |       reject(new Error('HTTP server startup timeout'))
 29 |     }, 5000)
 30 |     
 31 |     httpServer.stdout.on('data', (data) => {
 32 |       if (data.toString().includes('Test MCP server listening')) {
 33 |         clearTimeout(timeout)
 34 |         resolve()
 35 |       }
 36 |     })
 37 |   })
 38 |   
 39 |   return httpServer
 40 | }
 41 | 
 42 | async function startMasterServer() {
 43 |   console.log('Starting Master MCP server...')
 44 |   
 45 |   // Start the master server as a background process
 46 |   const masterServer = spawn('npm', ['run', 'dev'], {
 47 |     stdio: ['ignore', 'pipe', 'pipe'],
 48 |     cwd: process.cwd()
 49 |   })
 50 |   
 51 |   // Capture stdout and stderr
 52 |   masterServer.stdout.on('data', (data) => {
 53 |     const output = data.toString().trim()
 54 |     console.log(`[Master Server] ${output}`)
 55 |     // Don't log the full output as it's too verbose
 56 |   })
 57 |   
 58 |   masterServer.stderr.on('data', (data) => {
 59 |     console.error(`[Master Server ERROR] ${data.toString().trim()}`)
 60 |   })
 61 |   
 62 |   // Wait for the server to start
 63 |   await new Promise((resolve, reject) => {
 64 |     let timeout = setTimeout(() => {
 65 |       reject(new Error('Master server startup timeout'))
 66 |     }, 10000)
 67 |     
 68 |     masterServer.stdout.on('data', (data) => {
 69 |       if (data.toString().includes('Master MCP listening')) {
 70 |         clearTimeout(timeout)
 71 |         resolve()
 72 |       }
 73 |     })
 74 |   })
 75 |   
 76 |   return masterServer
 77 | }
 78 | 
 79 | async function runStreamingTest() {
 80 |   try {
 81 |     console.log('Testing Master MCP Server with HTTP Streaming...')
 82 |     
 83 |     // Create a streamable HTTP transport to connect to our MCP server
 84 |     const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3005/mcp'))
 85 |     
 86 |     // Create the MCP client
 87 |     const client = new Client({
 88 |       name: 'master-mcp-streaming-test-client',
 89 |       version: '1.0.0'
 90 |     })
 91 |     
 92 |     // Initialize the client
 93 |     await client.connect(transport)
 94 |     console.log('✅ Server initialized with streaming transport')
 95 |     console.log('Server info:', client.getServerVersion())
 96 |     console.log('Server capabilities:', client.getServerCapabilities())
 97 |     
 98 |     // List tools using streaming
 99 |     console.log('\n--- Testing tools/list with streaming ---')
100 |     const toolsResult = await client.listTools({})
101 |     console.log('✅ tools/list successful with streaming')
102 |     console.log('Number of tools:', toolsResult.tools.length)
103 |     console.log('Tools:', toolsResult.tools.map(t => t.name))
104 |     
105 |     // List resources using streaming
106 |     console.log('\n--- Testing resources/list with streaming ---')
107 |     const resourcesResult = await client.listResources({})
108 |     console.log('✅ resources/list successful with streaming')
109 |     console.log('Number of resources:', resourcesResult.resources.length)
110 |     console.log('Resources:', resourcesResult.resources.map(r => r.uri))
111 |     
112 |     // Test ping
113 |     console.log('\n--- Testing ping with streaming ---')
114 |     const pingResult = await client.ping()
115 |     console.log('✅ ping successful with streaming')
116 |     console.log('Ping result:', pingResult)
117 |     
118 |     // Try calling a tool from the HTTP server
119 |     console.log('\n--- Testing tool call to HTTP server ---')
120 |     try {
121 |       const httpToolCallResult = await client.callTool({
122 |         name: 'test-server.echo',  // Prefixed with server ID
123 |         arguments: { message: 'Hello from HTTP server!' }
124 |       })
125 |       console.log('✅ HTTP tool call successful')
126 |       console.log('HTTP tool result:', JSON.stringify(httpToolCallResult, null, 2))
127 |     } catch (error) {
128 |       console.log('⚠️ HTTP tool call failed (might not be available):', error.message)
129 |     }
130 |     
131 |     // Try calling a tool from the STDIO server
132 |     console.log('\n--- Testing tool call to STDIO server ---')
133 |     try {
134 |       const stdioToolCallResult = await client.callTool({
135 |         name: 'stdio-server.stdio-echo',  // Prefixed with server ID
136 |         arguments: { message: 'Hello from STDIO server!' }
137 |       })
138 |       console.log('✅ STDIO tool call successful')
139 |       console.log('STDIO tool result:', JSON.stringify(stdioToolCallResult, null, 2))
140 |     } catch (error) {
141 |       console.log('⚠️ STDIO tool call failed (might not be available):', error.message)
142 |     }
143 |     
144 |     // Try reading a resource from the HTTP server
145 |     console.log('\n--- Testing resource read from HTTP server ---')
146 |     try {
147 |       const httpResourceResult = await client.readResource({
148 |         uri: 'test-server.test://example'  // Prefixed with server ID
149 |       })
150 |       console.log('✅ HTTP resource read successful')
151 |       console.log('HTTP resource result:', JSON.stringify(httpResourceResult, null, 2))
152 |     } catch (error) {
153 |       console.log('⚠️ HTTP resource read failed (might not be available):', error.message)
154 |     }
155 |     
156 |     // Try reading a resource from the STDIO server
157 |     console.log('\n--- Testing resource read from STDIO server ---')
158 |     try {
159 |       const stdioResourceResult = await client.readResource({
160 |         uri: 'stdio-server.stdio://example/resource'  // Prefixed with server ID
161 |       })
162 |       console.log('✅ STDIO resource read successful')
163 |       console.log('STDIO resource result:', JSON.stringify(stdioResourceResult, null, 2))
164 |     } catch (error) {
165 |       console.log('⚠️ STDIO resource read failed (might not be available):', error.message)
166 |     }
167 |     
168 |     // Close the connection
169 |     await client.close()
170 |     console.log('\n✅ Disconnected from MCP server')
171 |     console.log('\n🎉 All streaming tests completed successfully!')
172 |     
173 |   } catch (error) {
174 |     console.error('❌ Streaming test failed:', error)
175 |     console.error('Error stack:', error.stack)
176 |   }
177 | }
178 | 
179 | async function main() {
180 |   let httpServer, masterServer
181 |   
182 |   try {
183 |     // Start the HTTP server
184 |     httpServer = await startHttpServer()
185 |     
186 |     // Start the master server
187 |     masterServer = await startMasterServer()
188 |     
189 |     // Wait a bit for discovery to happen
190 |     console.log('Waiting for server discovery...')
191 |     await new Promise(resolve => setTimeout(resolve, 3000))
192 |     
193 |     // Run the streaming test
194 |     await runStreamingTest()
195 |   } catch (error) {
196 |     console.error('Test failed:', error)
197 |   } finally {
198 |     // Clean up: kill the servers
199 |     if (httpServer) {
200 |       console.log('Stopping HTTP server...')
201 |       httpServer.kill()
202 |     }
203 |     if (masterServer) {
204 |       console.log('Stopping Master server...')
205 |       masterServer.kill()
206 |     }
207 |   }
208 | }
209 | 
210 | // Run the test
211 | main()
```

--------------------------------------------------------------------------------
/src/modules/stdio-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { spawn, ChildProcess } from 'node:child_process'
  2 | import { Logger } from '../utils/logger.js'
  3 | import type { ServerProcess } from '../types/server.js'
  4 | 
  5 | export class StdioManager {
  6 |   private processes = new Map<string, ChildProcess>()
  7 |   private responseQueues = new Map<string, Array<{ resolve: (value: any) => void; reject: (reason: any) => void; id: number | string }>>()
  8 |   private notificationCallbacks = new Map<string, (message: any) => void>()
  9 |   private messageBuffers = new Map<string, string>()
 10 | 
 11 |   async startServer(serverId: string, filePath: string, env?: Record<string, string>): Promise<ServerProcess> {
 12 |     Logger.info('Starting STDIO server', { serverId, filePath })
 13 |     
 14 |     return new Promise((resolve, reject) => {
 15 |       const timeout = setTimeout(() => {
 16 |         reject(new Error(`Timeout starting STDIO server ${serverId}`))
 17 |       }, 10000) // 10 second timeout
 18 | 
 19 |       try {
 20 |         const proc = spawn('node', [filePath], {
 21 |           stdio: ['pipe', 'pipe', 'pipe'],
 22 |           env: { ...process.env, ...env }
 23 |         })
 24 | 
 25 |         // Set up event handlers
 26 |         proc.stdout?.on('data', (data) => {
 27 |           this.handleStdoutData(serverId, data.toString())
 28 |         })
 29 | 
 30 |         proc.stderr?.on('data', (data) => {
 31 |           Logger.warn('STDIO server stderr', { serverId, data: data.toString() })
 32 |         })
 33 | 
 34 |         proc.on('close', (code) => {
 35 |           Logger.info('STDIO server process closed', { serverId, code })
 36 |           this.cleanupProcess(serverId, new Error(`STDIO server ${serverId} process closed with code ${code}`))
 37 |         })
 38 | 
 39 |         proc.on('error', (err) => {
 40 |           Logger.error('STDIO server process error', { serverId, error: err })
 41 |           clearTimeout(timeout)
 42 |           this.rejectPendingRequests(serverId, err)
 43 |           reject(err)
 44 |         })
 45 | 
 46 |         // Check if process started successfully
 47 |         proc.on('spawn', () => {
 48 |           clearTimeout(timeout)
 49 |           this.processes.set(serverId, proc)
 50 |           this.responseQueues.set(serverId, [])
 51 |           this.messageBuffers.set(serverId, '')
 52 | 
 53 |           resolve({
 54 |             pid: proc.pid,
 55 |             stop: async () => {
 56 |               return new Promise((resolve) => {
 57 |                 if (proc.connected) {
 58 |                   proc.kill()
 59 |                   setTimeout(() => resolve(), 1000) // Wait 1 second for graceful shutdown
 60 |                 } else {
 61 |                   resolve()
 62 |                 }
 63 |               })
 64 |             }
 65 |           })
 66 |         })
 67 |       } catch (err) {
 68 |         clearTimeout(timeout)
 69 |         reject(err)
 70 |       }
 71 |     })
 72 |   }
 73 | 
 74 |   private handleStdoutData(serverId: string, data: string) {
 75 |     const buffer = this.messageBuffers.get(serverId) || ''
 76 |     const newBuffer = buffer + data
 77 |     
 78 |     // Try to parse complete JSON messages
 79 |     let remainingBuffer = newBuffer
 80 |     while (remainingBuffer.trim().startsWith('{')) {
 81 |       try {
 82 |         // Try to parse as JSON
 83 |         const trimmed = remainingBuffer.trim()
 84 |         let endIndex = 1
 85 |         let braceCount = 1
 86 |         
 87 |         // Find the matching closing brace
 88 |         for (let i = 1; i < trimmed.length; i++) {
 89 |           if (trimmed[i] === '{') {
 90 |             braceCount++
 91 |           } else if (trimmed[i] === '}') {
 92 |             braceCount--
 93 |             if (braceCount === 0) {
 94 |               endIndex = i + 1
 95 |               break
 96 |             }
 97 |           }
 98 |         }
 99 |         
100 |         if (braceCount === 0) {
101 |           // Found complete JSON object
102 |           const jsonString = trimmed.substring(0, endIndex)
103 |           const message = JSON.parse(jsonString)
104 |           
105 |           // Process the message
106 |           this.processMessage(serverId, message)
107 |           
108 |           // Update buffer to remaining data
109 |           remainingBuffer = trimmed.substring(endIndex)
110 |           // Skip any whitespace after the JSON object
111 |           remainingBuffer = remainingBuffer.replace(/^\\s+/, '')
112 |         } else {
113 |           // Incomplete JSON, wait for more data
114 |           break
115 |         }
116 |       } catch (err) {
117 |         // Incomplete JSON or parsing error, wait for more data
118 |         break
119 |       }
120 |     }
121 |     
122 |     this.messageBuffers.set(serverId, remainingBuffer)
123 |   }
124 | 
125 |   public onNotification(serverId: string, callback: (message: any) => void) {
126 |     this.notificationCallbacks.set(serverId, callback)
127 |   }
128 | 
129 |   private processMessage(serverId: string, message: any) {
130 |     Logger.debug('Received message from STDIO server', { serverId, message })
131 | 
132 |     // Check if this is a response to a pending request
133 |     if (message.id !== undefined) {
134 |       const queue = this.responseQueues.get(serverId)
135 |       if (queue) {
136 |         const index = queue.findIndex((item) => item.id === message.id)
137 |         if (index !== -1) {
138 |           const { resolve } = queue.splice(index, 1)[0]
139 |           resolve(message)
140 |           return
141 |         }
142 |       }
143 |     }
144 | 
145 |     // Handle notifications (no id) or unmatched responses
146 |     const callback = this.notificationCallbacks.get(serverId)
147 |     if (callback) {
148 |       try {
149 |         callback(message)
150 |       } catch (err) {
151 |         Logger.error('Error in notification callback', { serverId, error: err })
152 |       }
153 |     } else {
154 |       Logger.debug('Received notification or unmatched response from STDIO server, but no callback registered', { serverId, message })
155 |     }
156 |   }
157 | 
158 |   async sendMessage(serverId: string, message: any): Promise<void> {
159 |     const proc = this.processes.get(serverId)
160 |     if (!proc || !proc.stdin) {
161 |       throw new Error(`STDIO server ${serverId} not found or not connected`)
162 |     }
163 | 
164 |     return new Promise((resolve, reject) => {
165 |       const messageStr = JSON.stringify(message) + '\n'
166 |       proc.stdin?.write(messageStr, (err) => {
167 |         if (err) reject(err)
168 |         else resolve()
169 |       })
170 |     })
171 |   }
172 | 
173 |   async waitForResponse(serverId: string, messageId: number | string, timeoutMs = 30000): Promise<any> {
174 |     const proc = this.processes.get(serverId)
175 |     if (!proc) {
176 |       throw new Error(`STDIO server ${serverId} not found`)
177 |     }
178 | 
179 |     return new Promise((resolve, reject) => {
180 |       const timeout = setTimeout(() => {
181 |         // Remove the pending request from the queue
182 |         const queue = this.responseQueues.get(serverId) || []
183 |         const index = queue.findIndex(item => item.id === messageId)
184 |         if (index !== -1) {
185 |           queue.splice(index, 1)
186 |         }
187 |         reject(new Error(`Timeout waiting for response from STDIO server ${serverId} for message ${messageId}`))
188 |       }, timeoutMs)
189 | 
190 |       // Add to response queue
191 |       const queue = this.responseQueues.get(serverId) || []
192 |       queue.push({
193 |         id: messageId,
194 |         resolve: (value: any) => {
195 |           clearTimeout(timeout)
196 |           resolve(value)
197 |         },
198 |         reject: (reason: any) => {
199 |           clearTimeout(timeout)
200 |           reject(reason)
201 |         }
202 |       })
203 |       this.responseQueues.set(serverId, queue)
204 |     })
205 |   }
206 | 
207 |   private rejectPendingRequests(serverId: string, error: any) {
208 |     const queue = this.responseQueues.get(serverId)
209 |     if (queue) {
210 |       while (queue.length > 0) {
211 |         const { reject } = queue.shift()!
212 |         reject(error)
213 |       }
214 |     }
215 |   }
216 | 
217 |   private cleanupProcess(serverId: string, error?: any) {
218 |     this.rejectPendingRequests(serverId, error || new Error(`STDIO server ${serverId} process closed`))
219 |     this.processes.delete(serverId)
220 |     this.responseQueues.delete(serverId)
221 |     this.messageBuffers.delete(serverId)
222 |     this.notificationCallbacks.delete(serverId)
223 |   }
224 | }
```

--------------------------------------------------------------------------------
/src/utils/crypto.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   createCipheriv,
  3 |   createDecipheriv,
  4 |   createHash,
  5 |   createHmac,
  6 |   randomBytes,
  7 |   randomUUID as nodeRandomUUID,
  8 |   timingSafeEqual,
  9 |   pbkdf2Sync,
 10 |   scryptSync,
 11 |   hkdfSync,
 12 | } from 'node:crypto'
 13 | 
 14 | const IV_LENGTH = 12 // AES-GCM recommended 12 bytes
 15 | const AUTH_TAG_LENGTH = 16
 16 | 
 17 | function deriveKey(key: string | Buffer): Buffer {
 18 |   return Buffer.isBuffer(key) ? createHash('sha256').update(key).digest() : createHash('sha256').update(Buffer.from(key)).digest()
 19 | }
 20 | 
 21 | function b64(input: ArrayBuffer | Uint8Array): string {
 22 |   return Buffer.from(input as any).toString('base64')
 23 | }
 24 | 
 25 | function fromB64(input: string): Buffer {
 26 |   return Buffer.from(input, 'base64')
 27 | }
 28 | 
 29 | /**
 30 |  * Node-focused crypto utilities used by the Master MCP Server runtime.
 31 |  * Worker builds exclude this file via tsconfig.worker.json.
 32 |  */
 33 | export class CryptoUtils {
 34 |   /** Encrypts UTF-8 text using AES-256-GCM. Returns base64(iv||tag||ciphertext). */
 35 |   static encrypt(data: string, key: string | Buffer): string {
 36 |     const iv = randomBytes(IV_LENGTH)
 37 |     const cipher = createCipheriv('aes-256-gcm', deriveKey(key), iv)
 38 |     const ciphertext = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()])
 39 |     const authTag = cipher.getAuthTag()
 40 |     return Buffer.concat([iv, authTag, ciphertext]).toString('base64')
 41 |   }
 42 | 
 43 |   /** Decrypts base64(iv||tag||ciphertext) produced by encrypt(). */
 44 |   static decrypt(encryptedData: string, key: string | Buffer): string {
 45 |     const raw = Buffer.from(encryptedData, 'base64')
 46 |     const iv = raw.subarray(0, IV_LENGTH)
 47 |     const authTag = raw.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH)
 48 |     const ciphertext = raw.subarray(IV_LENGTH + AUTH_TAG_LENGTH)
 49 |     const decipher = createDecipheriv('aes-256-gcm', deriveKey(key), iv)
 50 |     decipher.setAuthTag(authTag)
 51 |     const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()])
 52 |     return plaintext.toString('utf8')
 53 |   }
 54 | 
 55 |   /** Secure random bytes as hex string. */
 56 |   static generateSecureRandom(length: number): string {
 57 |     return randomBytes(length).toString('hex')
 58 |   }
 59 | 
 60 |   /** Returns RFC4122 v4 UUID using crypto RNG. */
 61 |   static uuid(): string {
 62 |     return nodeRandomUUID()
 63 |   }
 64 | 
 65 |   /** SHA-256 digest as hex string. */
 66 |   static hash(input: string | Buffer): string {
 67 |     return createHash('sha256').update(input).digest('hex')
 68 |   }
 69 | 
 70 |   /** Constant-time equality check for hex strings produced by hash(). */
 71 |   static verify(input: string | Buffer, hash: string): boolean {
 72 |     const calculated = Buffer.from(this.hash(input), 'utf8')
 73 |     const provided = Buffer.from(hash, 'utf8')
 74 |     if (calculated.length !== provided.length) return false
 75 |     return timingSafeEqual(calculated, provided)
 76 |   }
 77 | 
 78 |   /** Derives a key using PBKDF2-HMAC-SHA256. Returns base64 key bytes. */
 79 |   static pbkdf2(
 80 |     password: string | Buffer,
 81 |     salt: string | Buffer,
 82 |     iterations = 100_000,
 83 |     keyLen = 32,
 84 |   ): string {
 85 |     const dk = pbkdf2Sync(password, salt, iterations, keyLen, 'sha256')
 86 |     return b64(dk)
 87 |   }
 88 | 
 89 |   /**
 90 |    * Hashes password using PBKDF2. Format: pbkdf2$sha256$iter$saltB64$hashB64
 91 |    */
 92 |   static pbkdf2Hash(password: string, iterations = 100_000, saltLen = 16): string {
 93 |     const salt = randomBytes(saltLen)
 94 |     const hash = pbkdf2Sync(password, salt, iterations, 32, 'sha256')
 95 |     return `pbkdf2$sha256$${iterations}$${b64(salt)}$${b64(hash)}`
 96 |   }
 97 | 
 98 |   static pbkdf2Verify(password: string, encoded: string): boolean {
 99 |     try {
100 |       const [algo, hashName, iterStr, saltB64, hashB64] = encoded.split('$')
101 |       if (algo !== 'pbkdf2' || hashName !== 'sha256') return false
102 |       const iterations = Number(iterStr)
103 |       const salt = fromB64(saltB64)
104 |       const expected = fromB64(hashB64)
105 |       const actual = pbkdf2Sync(password, salt, iterations, expected.length, 'sha256')
106 |       return timingSafeEqual(actual, expected)
107 |     } catch {
108 |       return false
109 |     }
110 |   }
111 | 
112 |   /**
113 |    * Hashes password using scrypt with defaults N=16384, r=8, p=1.
114 |    * Format: scrypt$N$r$p$saltB64$hashB64
115 |    */
116 |   static scryptHash(password: string, opts?: { N?: number; r?: number; p?: number; saltLen?: number; keyLen?: number }): string {
117 |     const N = opts?.N ?? 16384
118 |     const r = opts?.r ?? 8
119 |     const p = opts?.p ?? 1
120 |     const saltLen = opts?.saltLen ?? 16
121 |     const keyLen = opts?.keyLen ?? 32
122 |     const salt = randomBytes(saltLen)
123 |     const hash = scryptSync(password, salt, keyLen, { N, r, p })
124 |     return `scrypt$${N}$${r}$${p}$${b64(salt)}$${b64(hash)}`
125 |   }
126 | 
127 |   static scryptVerify(password: string, encoded: string): boolean {
128 |     try {
129 |       const [algo, nStr, rStr, pStr, saltB64, hashB64] = encoded.split('$')
130 |       if (algo !== 'scrypt') return false
131 |       const N = Number(nStr)
132 |       const r = Number(rStr)
133 |       const p = Number(pStr)
134 |       const salt = fromB64(saltB64)
135 |       const expected = fromB64(hashB64)
136 |       const actual = scryptSync(password, salt, expected.length, { N, r, p })
137 |       return timingSafeEqual(actual, expected)
138 |     } catch {
139 |       return false
140 |     }
141 |   }
142 | 
143 |   /**
144 |    * Attempts bcrypt via optional dependency. If unavailable, falls back to scrypt
145 |    * and encodes using the scrypt$... scheme. This ensures secure hashing without
146 |    * adding runtime deps.
147 |    */
148 |   static async bcryptHash(password: string, rounds = 12): Promise<string> {
149 |     try {
150 |       // Attempt to use optional bcrypt packages if present via dynamic import
151 |       const mod = await dynamicImportAny(['bcrypt', 'bcryptjs'])
152 |       if (mod?.hash) return await mod.hash(password, rounds)
153 |     } catch {
154 |       // ignore and fallback
155 |     }
156 |     // Fallback to scrypt
157 |     return this.scryptHash(password)
158 |   }
159 | 
160 |   static async bcryptVerify(password: string, encoded: string): Promise<boolean> {
161 |     // If it looks like a bcrypt hash, try optional bcrypt packages
162 |     if (encoded.startsWith('$2a$') || encoded.startsWith('$2b$') || encoded.startsWith('$2y$')) {
163 |       try {
164 |         const mod = await dynamicImportAny(['bcrypt', 'bcryptjs'])
165 |         if (mod?.compare) return await mod.compare(password, encoded)
166 |       } catch {
167 |         // ignore and fallback
168 |       }
169 |       return false
170 |     }
171 |     // Otherwise, support scrypt fallback
172 |     if (encoded.startsWith('scrypt$')) return this.scryptVerify(password, encoded)
173 |     if (encoded.startsWith('pbkdf2$')) return this.pbkdf2Verify(password, encoded)
174 |     return false
175 |   }
176 | 
177 |   /** HKDF with SHA-256. Returns base64 key bytes. */
178 |   static hkdf(ikm: string | Buffer, salt: string | Buffer, info: string | Buffer, length = 32): string {
179 |     try {
180 |       const okm = hkdfSync('sha256', ikm, salt, info, length)
181 |       return b64(okm)
182 |     } catch {
183 |       // Fallback manual HKDF implementation (RFC 5869)
184 |       const prk = createHmac('sha256', salt as any).update(ikm as any).digest()
185 |       const n = Math.ceil(length / 32)
186 |       const t: any[] = []
187 |       let prev: any = Buffer.alloc(0)
188 |       for (let i = 0; i < n; i++) {
189 |         prev = createHmac('sha256', prk as any)
190 |           .update(Buffer.concat([prev, Buffer.from(info as any), Buffer.from([i + 1])]) as any)
191 |           .digest() as Buffer
192 |         t.push(prev)
193 |       }
194 |       return b64(Buffer.concat(t).subarray(0, length))
195 |     }
196 |   }
197 | }
198 | 
199 | async function dynamicImportAny(modules: string[]): Promise<any | null> {
200 |   for (const m of modules) {
201 |     try {
202 |       // Avoid triggering TS module resolution by computing the specifier
203 |       const importer = new Function('m', 'return import(m)') as (m: string) => Promise<any>
204 |       const mod = await importer(m)
205 |       if (mod) return mod.default ?? mod
206 |     } catch {
207 |       // continue
208 |     }
209 |   }
210 |   return null
211 | }
212 | 
```

--------------------------------------------------------------------------------
/src/auth/oauth-providers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fetch from 'node-fetch'
  2 | import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'
  3 | import type { OAuthToken, TokenValidationResult, UserInfo } from '../types/auth.js'
  4 | import type { ServerAuthConfig } from '../types/config.js'
  5 | import { Logger } from '../utils/logger.js'
  6 | 
  7 | export interface OAuthProvider {
  8 |   validateToken(token: string): Promise<TokenValidationResult>
  9 |   refreshToken(refreshToken: string): Promise<OAuthToken>
 10 |   getUserInfo(token: string): Promise<UserInfo>
 11 | }
 12 | 
 13 | export class OAuthError extends Error {
 14 |   constructor(message: string, public override cause?: unknown) {
 15 |     super(message)
 16 |     this.name = 'OAuthError'
 17 |   }
 18 | }
 19 | 
 20 | async function postForm(url: string, body: Record<string, string>): Promise<any> {
 21 |   const res = await fetch(url, {
 22 |     method: 'POST',
 23 |     headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
 24 |     body: new URLSearchParams(body).toString(),
 25 |   })
 26 |   const text = await res.text()
 27 |   if (!res.ok) {
 28 |     throw new OAuthError(`Token endpoint error ${res.status}: ${text}`)
 29 |   }
 30 |   try {
 31 |     return JSON.parse(text)
 32 |   } catch {
 33 |     // GitHub may return urlencoded; parse fallback
 34 |     return Object.fromEntries(new URLSearchParams(text))
 35 |   }
 36 | }
 37 | 
 38 | function toOAuthToken(json: any): OAuthToken {
 39 |   const expiresIn = 'expires_in' in json ? Number(json.expires_in) : 3600
 40 |   const scope = Array.isArray(json.scope)
 41 |     ? (json.scope as string[])
 42 |     : typeof json.scope === 'string'
 43 |       ? (json.scope as string).split(/[ ,]+/).filter(Boolean)
 44 |       : []
 45 |   return {
 46 |     access_token: String(json.access_token),
 47 |     refresh_token: json.refresh_token ? String(json.refresh_token) : undefined,
 48 |     expires_at: Date.now() + expiresIn * 1000,
 49 |     scope,
 50 |   }
 51 | }
 52 | 
 53 | export class GitHubOAuthProvider implements OAuthProvider {
 54 |   constructor(private readonly config: ServerAuthConfig) {}
 55 | 
 56 |   async validateToken(token: string): Promise<TokenValidationResult> {
 57 |     try {
 58 |       const res = await fetch('https://api.github.com/user', {
 59 |         headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
 60 |       })
 61 |       if (!res.ok) {
 62 |         const text = await res.text()
 63 |         return { valid: false, error: `GitHub token invalid: ${res.status} ${text}` }
 64 |       }
 65 |       const scopesHeader = res.headers.get('x-oauth-scopes')
 66 |       const scopes = scopesHeader ? scopesHeader.split(',').map((s) => s.trim()).filter(Boolean) : undefined
 67 |       return { valid: true, scopes }
 68 |     } catch (err) {
 69 |       Logger.error('GitHub validateToken failed', err)
 70 |       return { valid: false, error: String(err) }
 71 |     }
 72 |   }
 73 | 
 74 |   async refreshToken(refreshToken: string): Promise<OAuthToken> {
 75 |     const json = await postForm(this.config.token_endpoint, {
 76 |       grant_type: 'refresh_token',
 77 |       refresh_token: refreshToken,
 78 |       client_id: this.config.client_id,
 79 |       ...(this.config.client_secret ? { client_secret: String(this.config.client_secret) } : {}),
 80 |     })
 81 |     return toOAuthToken(json)
 82 |   }
 83 | 
 84 |   async getUserInfo(token: string): Promise<UserInfo> {
 85 |     const res = await fetch('https://api.github.com/user', {
 86 |       headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
 87 |     })
 88 |     if (!res.ok) throw new OAuthError(`GitHub userinfo failed: ${res.status}`)
 89 |     const json = (await res.json()) as any
 90 |     return { id: String(json.id), name: json.name ?? undefined, email: json.email ?? undefined, avatarUrl: json.avatar_url ?? undefined }
 91 |   }
 92 | }
 93 | 
 94 | export class GoogleOAuthProvider implements OAuthProvider {
 95 |   private jwks = createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'))
 96 | 
 97 |   constructor(private readonly config: ServerAuthConfig) {}
 98 | 
 99 |   async validateToken(token: string): Promise<TokenValidationResult> {
100 |     // Try as JWT (id_token); fallback to userinfo call for access_token
101 |     try {
102 |       const { payload } = await jwtVerify(token, this.jwks, {
103 |         issuer: ['https://accounts.google.com', 'accounts.google.com'],
104 |         audience: this.config.client_id ? String(this.config.client_id) : undefined,
105 |       })
106 |       const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ') : undefined
107 |       const exp = typeof payload.exp === 'number' ? payload.exp * 1000 : undefined
108 |       return { valid: true, expiresAt: exp, scopes }
109 |     } catch (_e) {
110 |       // Not a valid id_token; try userinfo endpoint to validate access token
111 |       try {
112 |         const res = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
113 |           headers: { Authorization: `Bearer ${token}` },
114 |         })
115 |         if (!res.ok) return { valid: false, error: `Google userinfo status ${res.status}` }
116 |         return { valid: true }
117 |       } catch (err) {
118 |         return { valid: false, error: String(err) }
119 |       }
120 |     }
121 |   }
122 | 
123 |   async refreshToken(refreshToken: string): Promise<OAuthToken> {
124 |     const json = await postForm(this.config.token_endpoint, {
125 |       grant_type: 'refresh_token',
126 |       refresh_token: refreshToken,
127 |       client_id: this.config.client_id,
128 |       ...(this.config.client_secret ? { client_secret: String(this.config.client_secret) } : {}),
129 |     })
130 |     return toOAuthToken(json)
131 |   }
132 | 
133 |   async getUserInfo(token: string): Promise<UserInfo> {
134 |     const res = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
135 |       headers: { Authorization: `Bearer ${token}` },
136 |     })
137 |     if (!res.ok) throw new OAuthError(`Google userinfo failed: ${res.status}`)
138 |     const json = (await res.json()) as any
139 |     return { id: String(json.sub), name: json.name, email: json.email, avatarUrl: json.picture }
140 |   }
141 | }
142 | 
143 | export class CustomOAuthProvider implements OAuthProvider {
144 |   private jwks?: ReturnType<typeof createRemoteJWKSet>
145 |   constructor(private readonly config: ServerAuthConfig & { jwks_uri?: string; issuer?: string; audience?: string }) {
146 |     if (this.config['jwks_uri']) {
147 |       this.jwks = createRemoteJWKSet(new URL(String(this.config['jwks_uri'])))
148 |     }
149 |   }
150 | 
151 |   async validateToken(token: string): Promise<TokenValidationResult> {
152 |     // Prefer JWT validation if JWKS is provided, else try userinfo proxy via resource endpoint if configured
153 |     if (this.jwks) {
154 |       try {
155 |         const { payload } = await jwtVerify(token, this.jwks, {
156 |           issuer: this.config['issuer'] ? String(this.config['issuer']) : undefined,
157 |           audience: this.config['audience'] ? String(this.config['audience']) : undefined,
158 |         })
159 |         const exp = typeof payload.exp === 'number' ? payload.exp * 1000 : undefined
160 |         const scopes = typeof payload.scope === 'string' ? payload.scope.split(/[ ,]+/) : undefined
161 |         return { valid: true, expiresAt: exp, scopes }
162 |       } catch (err) {
163 |         return { valid: false, error: String(err) }
164 |       }
165 |     }
166 |     // As a generic fallback, we can't validate without provider-specific endpoint; treat as opaque Bearer
167 |     try {
168 |       decodeJwt(token) // will throw if not a JWT; but opaque tokens are allowed; just return valid unknown
169 |       return { valid: true }
170 |     } catch {
171 |       return { valid: true } // opaque non-JWT tokens assumed valid at this layer
172 |     }
173 |   }
174 | 
175 |   async refreshToken(refreshToken: string): Promise<OAuthToken> {
176 |     const json = await postForm(this.config.token_endpoint, {
177 |       grant_type: 'refresh_token',
178 |       refresh_token: refreshToken,
179 |       client_id: this.config.client_id,
180 |       ...(this.config.client_secret ? { client_secret: String(this.config.client_secret) } : {}),
181 |     })
182 |     return toOAuthToken(json)
183 |   }
184 | 
185 |   async getUserInfo(token: string): Promise<UserInfo> {
186 |     // Generic OIDC userinfo often available at `${issuer}/userinfo`; but we only have authorization/token endpoints here.
187 |     const issuer = (this.config as any).issuer as string | undefined
188 |     if (!issuer) throw new OAuthError('userinfo endpoint unknown for custom provider (missing issuer)')
189 |     const url = new URL('/userinfo', issuer).toString()
190 |     const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } })
191 |     if (!res.ok) throw new OAuthError(`Custom OIDC userinfo failed: ${res.status}`)
192 |     const json = (await res.json()) as any
193 |     return { id: String(json.sub ?? json.id ?? 'unknown'), ...json }
194 |   }
195 | }
196 | 
197 | export function getOAuthProvider(config: ServerAuthConfig & { jwks_uri?: string; issuer?: string; audience?: string }): OAuthProvider {
198 |   switch (config.provider) {
199 |     case 'github':
200 |       return new GitHubOAuthProvider(config)
201 |     case 'google':
202 |       return new GoogleOAuthProvider(config)
203 |     default:
204 |       return new CustomOAuthProvider(config)
205 |   }
206 | }
207 | 
```

--------------------------------------------------------------------------------
/src/modules/capability-aggregator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { LoadedServer, ServerCapabilities } from '../types/server.js'
  2 | import type { ListResourcesResult, ListToolsResult, ToolDefinition, ResourceDefinition, PromptDefinition } from '../types/mcp.js'
  3 | import type { AuthHeaders } from '../types/auth.js'
  4 | import { Logger } from '../utils/logger.js'
  5 | 
  6 | export interface AggregatorOptions {
  7 |   prefixStrategy?: 'serverId' | 'none'
  8 |   // base path for discovery relative to server endpoint
  9 |   capabilitiesEndpoint?: string // default '/capabilities'
 10 |   toolsEndpoint?: string // default '/mcp/tools/list'
 11 |   resourcesEndpoint?: string // default '/mcp/resources/list'
 12 | }
 13 | 
 14 | export interface CapabilityMapEntry {
 15 |   serverId: string
 16 |   originalName: string
 17 | }
 18 | 
 19 | export class CapabilityAggregator {
 20 |   private readonly options: Required<AggregatorOptions>
 21 |   private toolMap = new Map<string, CapabilityMapEntry>()
 22 |   private resourceMap = new Map<string, CapabilityMapEntry>()
 23 | 
 24 |   constructor(options?: AggregatorOptions) {
 25 |     this.options = {
 26 |       prefixStrategy: options?.prefixStrategy ?? 'serverId',
 27 |       capabilitiesEndpoint: options?.capabilitiesEndpoint ?? '/capabilities',
 28 |       toolsEndpoint: options?.toolsEndpoint ?? '/mcp/tools/list',
 29 |       resourcesEndpoint: options?.resourcesEndpoint ?? '/mcp/resources/list',
 30 |     }
 31 |   }
 32 | 
 33 |   reset(): void {
 34 |     this.toolMap.clear()
 35 |     this.resourceMap.clear()
 36 |   }
 37 | 
 38 |   getMappingForTool(aggregatedName: string): CapabilityMapEntry | undefined {
 39 |     return this.toolMap.get(aggregatedName)
 40 |   }
 41 | 
 42 |   getMappingForResource(aggregatedUri: string): CapabilityMapEntry | undefined {
 43 |     return this.resourceMap.get(aggregatedUri)
 44 |   }
 45 | 
 46 |   async discoverCapabilities(
 47 |     servers: Map<string, LoadedServer>,
 48 |     clientToken?: string,
 49 |     getAuthHeaders?: (serverId: string, clientToken?: string) => Promise<AuthHeaders | undefined>
 50 |   ): Promise<void> {
 51 |     Logger.info('Discovering capabilities', { servers: Array.from(servers.entries()) })
 52 |     this.reset()
 53 |     const fallbackHeaders: AuthHeaders = {}
 54 |     if (clientToken) fallbackHeaders['Authorization'] = `Bearer ${clientToken}`
 55 | 
 56 |     await Promise.all(
 57 |       Array.from(servers.values()).map(async (server) => {
 58 |         if (!server.endpoint || server.endpoint === 'unknown') {
 59 |           // For STDIO servers, we need a different approach
 60 |           if (server.type === 'stdio' && server.config.url?.startsWith('file://')) {
 61 |             try {
 62 |               Logger.info('Discovering capabilities for STDIO server', { serverId: server.id })
 63 |               // Import the STDIO capability discovery module
 64 |               const { StdioCapabilityDiscovery } = await import('./stdio-capability-discovery.js')
 65 |               const stdioDiscovery = new StdioCapabilityDiscovery()
 66 |               
 67 |               const filePath = server.config.url.replace('file://', '')
 68 |               const caps = await stdioDiscovery.discoverCapabilities(server.id, filePath)
 69 |               Logger.info('Fetched capabilities for STDIO server', { serverId: server.id, caps })
 70 |               server.capabilities = caps
 71 |               this.index(server.id, caps)
 72 |               Logger.logServerEvent('capabilities_discovered', server.id, {
 73 |                 tools: caps.tools.length,
 74 |                 resources: caps.resources.length,
 75 |                 prompts: caps.prompts?.length ?? 0,
 76 |               })
 77 |             } catch (err) {
 78 |               Logger.warn(`Failed capability discovery for STDIO server ${server.id}`, err)
 79 |             }
 80 |           } else {
 81 |             Logger.warn(`Skipping server with unknown endpoint`, { serverId: server.id, type: server.type })
 82 |           }
 83 |           return
 84 |         }
 85 |         try {
 86 |           Logger.info('Fetching capabilities for server', { serverId: server.id, endpoint: server.endpoint })
 87 |           const headers = (await getAuthHeaders?.(server.id, clientToken)) ?? fallbackHeaders
 88 |           const caps = await this.fetchCapabilities(server.endpoint, headers)
 89 |           Logger.info('Fetched capabilities for server', { serverId: server.id, caps })
 90 |           server.capabilities = caps
 91 |           this.index(server.id, caps)
 92 |           Logger.logServerEvent('capabilities_discovered', server.id, {
 93 |             tools: caps.tools.length,
 94 |             resources: caps.resources.length,
 95 |             prompts: caps.prompts?.length ?? 0,
 96 |           })
 97 |         } catch (err) {
 98 |           Logger.warn(`Failed capability discovery for ${server.id}`, err)
 99 |         }
100 |       })
101 |     )
102 |   }
103 | 
104 |   getAllTools(servers: Map<string, LoadedServer>): ToolDefinition[] {
105 |     const result: ToolDefinition[] = []
106 |     for (const server of servers.values()) {
107 |       const tools = server.capabilities?.tools ?? []
108 |       for (const t of tools) {
109 |         const name = this.aggregateName(server.id, t.name)
110 |         result.push({ ...t, name })
111 |       }
112 |     }
113 |     return result
114 |   }
115 | 
116 |   getAllResources(servers: Map<string, LoadedServer>): ResourceDefinition[] {
117 |     const result: ResourceDefinition[] = []
118 |     for (const server of servers.values()) {
119 |       const resources = server.capabilities?.resources ?? []
120 |       for (const r of resources) {
121 |         const uri = this.aggregateName(server.id, r.uri)
122 |         result.push({ ...r, uri })
123 |       }
124 |     }
125 |     return result
126 |   }
127 | 
128 |   aggregate(servers: LoadedServer[]): ServerCapabilities {
129 |     const tools = servers.flatMap((s) => (s.capabilities?.tools ?? []).map((t) => ({ ...t, name: this.aggregateName(s.id, t.name) })))
130 |     const resources = servers.flatMap((s) => (s.capabilities?.resources ?? []).map((r) => ({ ...r, uri: this.aggregateName(s.id, r.uri) })))
131 |     const prompts = servers.flatMap((s) => s.capabilities?.prompts ?? [])
132 |     return { tools, resources, prompts: prompts.length ? prompts : undefined }
133 |   }
134 | 
135 |   // --- internals ---
136 |   private index(serverId: string, caps: ServerCapabilities): void {
137 |     for (const t of caps.tools) this.toolMap.set(this.aggregateName(serverId, t.name), { serverId, originalName: t.name })
138 |     for (const r of caps.resources) this.resourceMap.set(this.aggregateName(serverId, r.uri), { serverId, originalName: r.uri })
139 |   }
140 | 
141 |   private aggregateName(serverId: string, name: string): string {
142 |     if (this.options.prefixStrategy === 'none') return name
143 |     return `${serverId}.${name}`
144 |   }
145 | 
146 |   private ensureTrailingSlash(endpoint: string): string {
147 |     return endpoint.endsWith('/') ? endpoint : `${endpoint}/`
148 |   }
149 | 
150 |   private async fetchCapabilities(endpoint: string, headers: AuthHeaders): Promise<ServerCapabilities> {
151 |     const urlCap = new URL(this.options.capabilitiesEndpoint, this.ensureTrailingSlash(endpoint)).toString()
152 |     Logger.info('Fetching capabilities from endpoint', { urlCap, headers })
153 |     try {
154 |       const res = await fetch(urlCap, { headers })
155 |       if (res.ok) {
156 |         const json = (await res.json()) as any
157 |         // Try to coerce shapes
158 |         const tools: ToolDefinition[] = Array.isArray(json.tools) ? json.tools : (json.capabilities?.tools ?? [])
159 |         const resources: ResourceDefinition[] = Array.isArray(json.resources) ? json.resources : (json.capabilities?.resources ?? [])
160 |         const prompts: PromptDefinition[] | undefined = Array.isArray(json.prompts) ? json.prompts : (json.capabilities?.prompts ?? undefined)
161 |         Logger.info('Fetched capabilities', { tools, resources, prompts })
162 |         return { tools, resources, prompts }
163 |       }
164 |     } catch (err) {
165 |       Logger.debug('Direct capabilities endpoint failed, trying fallbacks', err)
166 |     }
167 | 
168 |     // Fallback: fetch tools and resources separately
169 |     const [tools, resources] = await Promise.all([this.fetchTools(endpoint, headers), this.fetchResources(endpoint, headers)])
170 |     Logger.info('Fetched capabilities using fallbacks', { tools, resources })
171 |     return { tools, resources }
172 |   }
173 | 
174 |   private async fetchTools(endpoint: string, headers: AuthHeaders): Promise<ToolDefinition[]> {
175 |     const url = new URL(this.options.toolsEndpoint, this.ensureTrailingSlash(endpoint)).toString()
176 |     try {
177 |       const res = await fetch(url, { headers })
178 |       if (res.ok) {
179 |         const json = (await res.json()) as ListToolsResult
180 |         return json.tools ?? []
181 |       }
182 |     } catch (err) {
183 |       Logger.warn('fetchTools failed', err)
184 |     }
185 |     return []
186 |   }
187 | 
188 |   private async fetchResources(endpoint: string, headers: AuthHeaders): Promise<ResourceDefinition[]> {
189 |     const url = new URL(this.options.resourcesEndpoint, this.ensureTrailingSlash(endpoint)).toString()
190 |     try {
191 |       const res = await fetch(url, { headers })
192 |       if (res.ok) {
193 |         const json = (await res.json()) as ListResourcesResult
194 |         return json.resources ?? []
195 |       }
196 |     } catch (err) {
197 |       Logger.warn('fetchResources failed', err)
198 |     }
199 |     return []
200 |   }
201 | }
202 | 
```

--------------------------------------------------------------------------------
/reports/claude_report_20250815_222153.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 | <head>
  4 |     <meta charset="UTF-8">
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6 |     <title>Claude Usage Report - Jul 16 to Aug 15, 2025</title>
  7 |     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  8 |     <style>
  9 |         * { margin: 0; padding: 0; box-sizing: border-box; }
 10 |         body { 
 11 |             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 12 |             line-height: 1.6;
 13 |             color: #333;
 14 |             background: #f5f5f5;
 15 |         }
 16 |         .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
 17 |         .header {
 18 |             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 19 |             color: white;
 20 |             padding: 40px;
 21 |             border-radius: 10px;
 22 |             margin-bottom: 30px;
 23 |         }
 24 |         .header h1 { font-size: 2.5em; margin-bottom: 10px; }
 25 |         .header .subtitle { opacity: 0.9; }
 26 |         .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; }
 27 |         .card {
 28 |             background: white;
 29 |             border-radius: 10px;
 30 |             padding: 20px;
 31 |             box-shadow: 0 2px 10px rgba(0,0,0,0.1);
 32 |         }
 33 |         .card h2 { 
 34 |             font-size: 0.9em;
 35 |             text-transform: uppercase;
 36 |             color: #666;
 37 |             margin-bottom: 10px;
 38 |         }
 39 |         .card .value { 
 40 |             font-size: 2em;
 41 |             font-weight: bold;
 42 |             color: #333;
 43 |         }
 44 |         .card .subtitle {
 45 |             font-size: 0.9em;
 46 |             color: #999;
 47 |             margin-top: 5px;
 48 |         }
 49 |         .chart-container {
 50 |             background: white;
 51 |             border-radius: 10px;
 52 |             padding: 20px;
 53 |             margin-bottom: 20px;
 54 |             box-shadow: 0 2px 10px rgba(0,0,0,0.1);
 55 |         }
 56 |         .chart-container h2 {
 57 |             margin-bottom: 20px;
 58 |             color: #333;
 59 |         }
 60 |         .insights {
 61 |             background: white;
 62 |             border-radius: 10px;
 63 |             padding: 20px;
 64 |             margin-bottom: 20px;
 65 |             box-shadow: 0 2px 10px rgba(0,0,0,0.1);
 66 |         }
 67 |         .insights h2 { margin-bottom: 15px; }
 68 |         .insights ul { list-style: none; }
 69 |         .insights li {
 70 |             padding: 10px;
 71 |             margin-bottom: 10px;
 72 |             border-left: 3px solid #667eea;
 73 |             background: #f8f9fa;
 74 |         }
 75 |         .warning { border-left-color: #f59e0b !important; background: #fef3c7 !important; }
 76 |         .opportunity { border-left-color: #10b981 !important; background: #d1fae5 !important; }
 77 |         table {
 78 |             width: 100%;
 79 |             border-collapse: collapse;
 80 |             margin-top: 20px;
 81 |         }
 82 |         th, td {
 83 |             padding: 12px;
 84 |             text-align: left;
 85 |             border-bottom: 1px solid #e5e5e5;
 86 |         }
 87 |         th {
 88 |             background: #f8f9fa;
 89 |             font-weight: 600;
 90 |         }
 91 |         .footer {
 92 |             text-align: center;
 93 |             margin-top: 40px;
 94 |             padding: 20px;
 95 |             color: #666;
 96 |             font-size: 0.9em;
 97 |         }
 98 |     </style>
 99 | </head>
100 | <body>
101 |     <div class="container">
102 |         <div class="header">
103 |             <h1>Claude Usage Report - Jul 16 to Aug 15, 2025</h1>
104 |             <div class="subtitle">
105 |                 Jul 16, 2025 - 
106 |                 Aug 15, 2025
107 |             </div>
108 |         </div>
109 | 
110 |         <div class="grid">
111 |             <div class="card">
112 |                 <h2>Total Cost</h2>
113 |                 <div class="value">$4175.02</div>
114 |                 <div class="subtitle">Average $139.17/day</div>
115 |             </div>
116 |             <div class="card">
117 |                 <h2>Total Tokens</h2>
118 |                 <div class="value">2835.19M</div>
119 |                 <div class="subtitle">28919 requests</div>
120 |             </div>
121 |             <div class="card">
122 |                 <h2>Cache Efficiency</h2>
123 |                 <div class="value">9982.6%</div>
124 |                 <div class="subtitle">Saved $18780.13</div>
125 |             </div>
126 |             <div class="card">
127 |                 <h2>Burn Rate</h2>
128 |                 <div class="value">$5.81/hr</div>
129 |                 <div class="subtitle">UNKNOWN</div>
130 |             </div>
131 |         </div>
132 | 
133 |         
134 |         <div class="chart-container">
135 |             <h2>Daily Usage Trend</h2>
136 |             <canvas id="dailyChart"></canvas>
137 |         </div>
138 | 
139 |         <div class="chart-container">
140 |             <h2>Model Distribution</h2>
141 |             <canvas id="modelChart"></canvas>
142 |         </div>
143 |     
144 | 
145 |         <div class="insights">
146 |             <h2>Insights & Recommendations</h2>
147 |             
148 |             
149 |                 <h3 style="color: #667eea; margin: 20px 0 10px;">💡 Recommendations</h3>
150 |                 <ul>
151 |                     <li>Consider using Claude Haiku for simpler tasks to reduce costs</li><li>Implement request batching to reduce API calls</li>
152 |                 </ul>
153 |             
154 |             
155 |                 <h3 style="color: #10b981; margin: 20px 0 10px;">✨ Opportunities</h3>
156 |                 <ul>
157 |                     <li class="opportunity">Excellent cache efficiency (9982.6%) - saving $18780.13</li>
158 |                 </ul>
159 |             
160 |         </div>
161 | 
162 |         <div class="chart-container">
163 |             <h2>Model Usage</h2>
164 |             <table>
165 |                 <thead>
166 |                     <tr>
167 |                         <th>Model</th>
168 |                         <th>Requests</th>
169 |                         <th>Total Tokens</th>
170 |                         <th>Total Cost</th>
171 |                         <th>Cache Hit Rate</th>
172 |                     </tr>
173 |                 </thead>
174 |                 <tbody>
175 |                     
176 |                         <tr>
177 |                             <td>claude-opus-4-1-20250805</td>
178 |                             <td>6,629</td>
179 |                             <td>673.84M</td>
180 |                             <td>$1870.10</td>
181 |                             <td>95.0%</td>
182 |                         </tr>
183 |                     
184 |                         <tr>
185 |                             <td>claude-opus-4-20250514</td>
186 |                             <td>5,906</td>
187 |                             <td>494.37M</td>
188 |                             <td>$1464.34</td>
189 |                             <td>94.4%</td>
190 |                         </tr>
191 |                     
192 |                         <tr>
193 |                             <td>claude-sonnet-4-20250514</td>
194 |                             <td>16,384</td>
195 |                             <td>1666.97M</td>
196 |                             <td>$840.58</td>
197 |                             <td>94.6%</td>
198 |                         </tr>
199 |                     
200 |                 </tbody>
201 |             </table>
202 |         </div>
203 | 
204 |         <div class="footer">
205 |             Generated on Aug 15, 2025 22:21 • 
206 |             Ouroboros Claude Monitoring System v1.0.0
207 |         </div>
208 |     </div>
209 | 
210 |     
211 |     <script>
212 |         // Daily usage chart
213 |         const dailyCtx = document.getElementById('dailyChart').getContext('2d');
214 |         new Chart(dailyCtx, {
215 |             type: 'line',
216 |             data: {
217 |                 labels: ["Jul 16","Jul 17","Jul 18","Jul 19","Jul 21","Jul 23","Jul 24","Jul 27","Jul 28","Aug 3","Aug 4","Aug 5","Aug 6","Aug 7","Aug 8","Aug 9","Aug 10","Aug 11","Aug 12","Aug 13","Aug 14","Aug 15"],
218 |                 datasets: [{
219 |                     label: 'Daily Cost ($)',
220 |                     data: ["13.96","174.73","116.18","3.61","166.24","81.62","95.45","3.53","78.99","108.26","544.52","10.80","193.35","549.84","300.48","583.22","348.56","28.68","233.65","455.15","6.09","78.10"],
221 |                     borderColor: '#667eea',
222 |                     backgroundColor: 'rgba(102, 126, 234, 0.1)',
223 |                     tension: 0.4
224 |                 }]
225 |             },
226 |             options: {
227 |                 responsive: true,
228 |                 plugins: {
229 |                     legend: { display: false }
230 |                 }
231 |             }
232 |         });
233 | 
234 |         // Model distribution chart
235 |         const modelCtx = document.getElementById('modelChart').getContext('2d');
236 |         new Chart(modelCtx, {
237 |             type: 'doughnut',
238 |             data: {
239 |                 labels: ["opus-4","opus-4","sonnet-4"],
240 |                 datasets: [{
241 |                     data: ["1870.10","1464.34","840.58"],
242 |                     backgroundColor: [
243 |                         '#667eea',
244 |                         '#764ba2',
245 |                         '#f59e0b',
246 |                         '#10b981',
247 |                         '#ef4444'
248 |                     ]
249 |                 }]
250 |             },
251 |             options: {
252 |                 responsive: true,
253 |                 plugins: {
254 |                     legend: { position: 'right' }
255 |                 }
256 |             }
257 |         });
258 |     </script>
259 |     
260 | </body>
261 | </html>
```

--------------------------------------------------------------------------------
/src/modules/request-router.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 { LoadedServer } from '../types/server.js'
 14 | import type { AuthHeaders, OAuthDelegation } from '../types/auth.js'
 15 | import { CapabilityAggregator } from './capability-aggregator.js'
 16 | import { Logger } from '../utils/logger.js'
 17 | import { CircuitBreaker } from '../routing/circuit-breaker.js'
 18 | import { LoadBalancer } from '../routing/load-balancer.js'
 19 | import { RouteRegistry } from '../routing/route-registry.js'
 20 | import { RetryHandler } from '../routing/retry-handler.js'
 21 | import type { RoutingConfig } from '../types/config.js'
 22 | 
 23 | export interface RouterOptions {
 24 |   callToolEndpoint?: string // default '/mcp/tools/call'
 25 |   readResourceEndpoint?: string // default '/mcp/resources/read'
 26 |   routing?: RoutingConfig
 27 | }
 28 | 
 29 | export interface RouterOptions {
 30 |   callToolEndpoint?: string // default '/mcp/tools/call'
 31 |   readResourceEndpoint?: string // default '/mcp/resources/read'
 32 |   routing?: RoutingConfig
 33 | }
 34 | 
 35 | export class RequestRouter {
 36 |   private readonly options: Required<Omit<RouterOptions, 'routing'>> & { routing: RoutingConfig }
 37 |   private readonly circuit: CircuitBreaker
 38 |   private readonly retry: RetryHandler
 39 |   private readonly lb: LoadBalancer
 40 |   private readonly registry: RouteRegistry
 41 | 
 42 |   constructor(
 43 |     private readonly servers: Map<string, LoadedServer>,
 44 |     private readonly aggregator: CapabilityAggregator,
 45 |     private readonly getAuthHeaders?: (
 46 |       serverId: string,
 47 |       clientToken?: string
 48 |     ) => Promise<AuthHeaders | OAuthDelegation | undefined>,
 49 |     options?: RouterOptions
 50 |   ) {
 51 |     this.options = {
 52 |       callToolEndpoint: options?.callToolEndpoint ?? '/mcp/tools/call',
 53 |       readResourceEndpoint: options?.readResourceEndpoint ?? '/mcp/resources/read',
 54 |       routing: options?.routing ?? {},
 55 |     }
 56 |     this.circuit = new CircuitBreaker({
 57 |       failureThreshold: this.options.routing.circuitBreaker?.failureThreshold ?? 5,
 58 |       successThreshold: this.options.routing.circuitBreaker?.successThreshold ?? 2,
 59 |       recoveryTimeoutMs: this.options.routing.circuitBreaker?.recoveryTimeoutMs ?? 30_000,
 60 |       name: 'request-router',
 61 |     })
 62 |     this.retry = new RetryHandler({
 63 |       maxRetries: this.options.routing.retry?.maxRetries ?? 2,
 64 |       baseDelayMs: this.options.routing.retry?.baseDelayMs ?? 250,
 65 |       maxDelayMs: this.options.routing.retry?.maxDelayMs ?? 4_000,
 66 |       backoffFactor: this.options.routing.retry?.backoffFactor ?? 2,
 67 |       jitter: this.options.routing.retry?.jitter ?? 'full',
 68 |       retryOn: this.options.routing.retry?.retryOn ?? { networkErrors: true, httpStatusClasses: [5], httpStatuses: [408, 429] },
 69 |     })
 70 |     this.lb = new LoadBalancer({ strategy: this.options.routing.loadBalancer?.strategy ?? 'round_robin' })
 71 |     this.registry = new RouteRegistry(this.servers, this.circuit, this.lb)
 72 |   }
 73 | 
 74 |   getServers(): Map<string, LoadedServer> {
 75 |     return this.servers
 76 |   }
 77 | 
 78 |   async routeListTools(_req: ListToolsRequest): Promise<ListToolsResult> {
 79 |     const tools = this.aggregator.getAllTools(this.servers)
 80 |     return { tools }
 81 |   }
 82 | 
 83 |   async routeCallTool(req: CallToolRequest, clientToken?: string): Promise<CallToolResult> {
 84 |     // Resolve mapping via aggregator if available
 85 |     const map = this.aggregator.getMappingForTool(req.name)
 86 |     const serverId = map?.serverId ?? req.name.split('.')[0]
 87 |     const toolName = map?.originalName ?? (req.name.includes('.') ? req.name.split('.').slice(1).join('.') : req.name)
 88 | 
 89 |     const server = this.servers.get(serverId)
 90 |     if (!server) {
 91 |       return { content: { error: `Server ${serverId} not found` }, isError: true }
 92 |     }
 93 | 
 94 |     // Handle STDIO servers differently
 95 |     if (server.type === 'stdio') {
 96 |       try {
 97 |         Logger.info('Routing call to STDIO server', { serverId, toolName })
 98 |         const { StdioCapabilityDiscovery } = await import('./stdio-capability-discovery.js')
 99 |         const stdioDiscovery = new StdioCapabilityDiscovery()
100 |         const result = await stdioDiscovery.callTool(serverId, toolName, req.arguments ?? {})
101 |         return result.result || result
102 |       } catch (error) {
103 |         Logger.error('STDIO tool call failed', { serverId, toolName, error })
104 |         return { content: { error: `STDIO tool call failed: ${error}` }, isError: true }
105 |       }
106 |     }
107 | 
108 |     const resolution = this.registry.resolve(serverId)
109 |     if (!resolution) {
110 |       return { content: { error: `Route not found for tool ${req.name}` }, isError: true }
111 |     }
112 | 
113 |     const headers: AuthHeaders = { 'content-type': 'application/json' }
114 |     const auth = await this.getAuthHeaders?.(serverId, clientToken)
115 |     if (auth && (auth as OAuthDelegation).type === 'oauth_delegation') {
116 |       return { content: { error: 'OAuth delegation required', details: auth }, isError: true }
117 |     }
118 |     const extra = (auth as AuthHeaders) ?? (clientToken ? { Authorization: `Bearer ${clientToken}` } : {})
119 |     Object.assign(headers, extra)
120 | 
121 |     const url = new URL(this.options.callToolEndpoint, this.ensureTrailingSlash(resolution.instance.url)).toString()
122 |     const key = `${serverId}::${resolution.instance.id}`
123 | 
124 |     try {
125 |       const json = await this.circuit.execute(key, async () => {
126 |         const res = await this.fetchWithRetry(url, {
127 |           method: 'POST',
128 |           headers,
129 |           body: JSON.stringify({ name: toolName, arguments: req.arguments ?? {} }),
130 |         })
131 |         return (await res.json()) as CallToolResult
132 |       })
133 |       this.registry.markSuccess(serverId, resolution.instance.id)
134 |       return json
135 |     } catch (err) {
136 |       this.registry.markFailure(serverId, resolution.instance.id)
137 |       Logger.warn('routeCallTool failed', err)
138 |       return { content: { error: String(err) }, isError: true }
139 |     }
140 |   }
141 | 
142 |   async routeListResources(_req: ListResourcesRequest): Promise<ListResourcesResult> {
143 |     const resources = this.aggregator.getAllResources(this.servers)
144 |     return { resources }
145 |   }
146 | 
147 |   async routeReadResource(req: ReadResourceRequest, clientToken?: string): Promise<ReadResourceResult> {
148 |     const map = this.aggregator.getMappingForResource(req.uri)
149 |     const serverId = map?.serverId ?? req.uri.split('.')[0]
150 |     const resourceUri = map?.originalName ?? (req.uri.includes('.') ? req.uri.split('.').slice(1).join('.') : req.uri)
151 | 
152 |     const server = this.servers.get(serverId)
153 |     if (!server) {
154 |       return { contents: `Server ${serverId} not found`, mimeType: 'text/plain' }
155 |     }
156 | 
157 |     // Handle STDIO servers differently
158 |     if (server.type === 'stdio') {
159 |       try {
160 |         Logger.info('Routing read to STDIO server', { serverId, resourceUri })
161 |         const { StdioCapabilityDiscovery } = await import('./stdio-capability-discovery.js')
162 |         const stdioDiscovery = new StdioCapabilityDiscovery()
163 |         const result = await stdioDiscovery.readResource(serverId, resourceUri)
164 |         return result.result || result
165 |       } catch (error) {
166 |         Logger.error('STDIO resource read failed', { serverId, resourceUri, error })
167 |         return { contents: `STDIO resource read failed: ${error}`, mimeType: 'text/plain' }
168 |       }
169 |     }
170 | 
171 |     const resolution = this.registry.resolve(serverId)
172 |     if (!resolution) {
173 |       return { contents: `Route not found for resource ${req.uri}`, mimeType: 'text/plain' }
174 |     }
175 | 
176 |     const headers: AuthHeaders = { 'content-type': 'application/json' }
177 |     const auth = await this.getAuthHeaders?.(serverId, clientToken)
178 |     if (auth && (auth as OAuthDelegation).type === 'oauth_delegation') {
179 |       return { contents: JSON.stringify({ error: 'OAuth delegation required', details: auth }), mimeType: 'application/json' }
180 |     }
181 |     const extra = (auth as AuthHeaders) ?? (clientToken ? { Authorization: `Bearer ${clientToken}` } : {})
182 |     Object.assign(headers, extra)
183 | 
184 |     const url = new URL(this.options.readResourceEndpoint, this.ensureTrailingSlash(resolution.instance.url)).toString()
185 |     const key = `${serverId}::${resolution.instance.id}`
186 |     try {
187 |       const json = await this.circuit.execute(key, async () => {
188 |         const res = await this.fetchWithRetry(url, { method: 'POST', headers, body: JSON.stringify({ uri: resourceUri }) })
189 |         return (await res.json()) as ReadResourceResult
190 |       })
191 |       this.registry.markSuccess(serverId, resolution.instance.id)
192 |       return json
193 |     } catch (err) {
194 |       this.registry.markFailure(serverId, resolution.instance.id)
195 |       Logger.warn('routeReadResource failed', err)
196 |       return { contents: String(err), mimeType: 'text/plain' }
197 |     }
198 |   }
199 | 
200 |   async routeSubscribe(_req: SubscribeRequest): Promise<SubscribeResult> {
201 |     // Not implemented yet; aggregation events out of scope here
202 |     return { ok: true }
203 |   }
204 | 
205 |   private ensureTrailingSlash(endpoint: string): string {
206 |     return endpoint.endsWith('/') ? endpoint : `${endpoint}/`
207 |   }
208 | 
209 |   private async fetchWithRetry(input: string, init: RequestInit): Promise<Response> {
210 |     return this.retry.execute(async () => {
211 |       const res = await fetch(input, init)
212 |       if (!res.ok) {
213 |         // For retry logic, throw an error carrying status to trigger retry policy
214 |         const err = new Error(`HTTP ${res.status}`) as Error & { status?: number }
215 |         ;(err as any).status = res.status
216 |         throw err
217 |       }
218 |       return res
219 |     }, (ctx) => {
220 |       Logger.debug('Retrying upstream request', ctx)
221 |     })
222 |   }
223 | }
224 | 
```

--------------------------------------------------------------------------------
/docs/testing/phase-9-testing-architecture.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Phase 9 — Comprehensive Testing Architecture
  2 | 
  3 | This document specifies the end-to-end testing architecture for the Master MCP Server across Node.js and Cloudflare Workers. It is tailored to this codebase (TypeScript, ESM, strict mode) and builds on Phases 1–8 (auth, module loading, routing, config, OAuth, utils).
  4 | 
  5 | ## Goals
  6 | - Cross-platform: Node 18+ and Cloudflare Workers.
  7 | - ESM + TypeScript strict compatibility.
  8 | - Leverage existing utilities (logging, validation, monitoring).
  9 | - Clear test layering: unit, integration, E2E, plus security and performance.
 10 | - Deterministic, isolated, and parallel-friendly test runs.
 11 | - CI/CD automation with quality gates and coverage thresholds.
 12 | 
 13 | ---
 14 | 
 15 | ## Framework Selection
 16 | 
 17 | - Unit/Integration (Node): Vitest
 18 |   - Fast, ESM-native, TS-friendly. Built-in mock timers and coverage via V8.
 19 |   - Supertest for Express-based HTTP integration of Node runtime (`src/index.ts`).
 20 |   - Undici MockAgent (optional) for fetch interception on Node 18, when not spinning stub servers.
 21 | 
 22 | - Unit/Integration (Workers): Vitest + Miniflare 3
 23 |   - `miniflare` test environment for `Request`/`Response` compatibility.
 24 |   - Target `OAuthFlowController.handleRequest` and any worker entrypoints (`src/runtime/worker.ts`).
 25 | 
 26 | - E2E (HTTP-level): Vitest test suite using real HTTP listeners
 27 |   - Node: start Express via `createServer(true)` and use Supertest/HTTP.
 28 |   - Workers: run Miniflare instance or `wrangler dev` in CI as needed.
 29 | 
 30 | - Performance: Artillery
 31 |   - Simple YAML scenarios to stress authentication and routing endpoints.
 32 |   - Separate CI job; can be run locally against dev servers.
 33 | 
 34 | - Security: Vitest + fast-check (property-based) + static assertions
 35 |   - Fuzz inputs for token parsing, state/PKCE validation, and router input validation.
 36 |   - Optional OWASP ZAP baseline in CI (nightly) if desired.
 37 | 
 38 | ---
 39 | 
 40 | ## Directory Structure
 41 | 
 42 | ```
 43 | tests/
 44 |   unit/
 45 |     routing/              # circuit-breaker, retry, load-balancer
 46 |     modules/              # aggregator, router (with fetch mocked)
 47 |     oauth/                # pkce/state/validator
 48 |     utils/                # logger, validation helpers
 49 |   integration/
 50 |     node/                 # express endpoints, config manager wiring
 51 |     workers/              # flow-controller.handleRequest via Miniflare
 52 |     oauth/                # callback flow with mock OIDC provider
 53 |   e2e/
 54 |     node/                 # start full HTTP server and hit /mcp/*, /oauth/*
 55 |     workers/              # worker entrypoint end-to-end
 56 |   mocks/
 57 |     oauth/                # mock OIDC provider (Node + Worker variants)
 58 |     mcp/                  # fake MCP backends (capabilities/tools/resources)
 59 |     http/                 # undici MockAgent helpers (Node)
 60 |   factories/
 61 |     configFactory.ts      # MasterConfig, ServerConfig builders
 62 |     oauthFactory.ts       # tokens/states/JWKS
 63 |     mcpFactory.ts         # tool/resource definitions
 64 |   fixtures/
 65 |     capabilities.json
 66 |     tools.json
 67 |     resources.json
 68 |   perf/
 69 |     artillery/
 70 |       auth-routing.yaml   # load scenarios for auth + routing
 71 |   security/
 72 |     oauth.spec.ts         # PKCE/state/nonce fuzz tests (fast-check)
 73 |   _setup/
 74 |     vitest.setup.ts       # global hooks, silent logs, fake timers config
 75 |     miniflare.setup.ts    # worker env config for tests
 76 |   _utils/
 77 |     test-server.ts        # ephemeral HTTP servers
 78 |     mock-fetch.ts         # Node fetch interception (Undici)
 79 |     log-capture.ts        # capture + assert logs
 80 | ```
 81 | 
 82 | Notes:
 83 | - The unit layer uses pure module-level tests with fetch mocked or local HTTP stubs.
 84 | - Integration tests are black-box at module boundaries (e.g., ProtocolHandler + RequestRouter + mocks).
 85 | - E2E spins the real Node server and/or Miniflare worker with realistic mocks for upstream MCP servers and OIDC.
 86 | 
 87 | ---
 88 | 
 89 | ## Test Environment Management
 90 | 
 91 | - Node vs Workers selection
 92 |   - Node-specific Vitest config: `vitest.config.ts` (environment: node)
 93 |   - Workers-specific Vitest config: `vitest.worker.config.ts` (environment: miniflare)
 94 |   - Test files can explicitly opt into Miniflare with `// @vitest-environment miniflare`.
 95 | 
 96 | - Isolation and determinism
 97 |   - Use Vitest’s fake timers for retry/circuit tests.
 98 |   - Ephemeral HTTP servers: `tests/_utils/test-server.ts` binds to port 0 and auto-closes in `afterEach`.
 99 |   - Log capture: disable or capture logs via `Logger.configure({ json: true })` for stable assertions.
100 | 
101 | - Cross-platform resources
102 |   - OAuth flows invoke real HTTP endpoints; a mock OIDC provider runs as an in-process HTTP server in both Node and Miniflare scenarios (Miniflare tests remain in a Node host context, so spinning a Node HTTP stub is acceptable).
103 |   - Upstream MCP backends emulated by `fake-backend` servers exposing `/capabilities`, `/mcp/tools/*`, `/mcp/resources/*`.
104 | 
105 | ---
106 | 
107 | ## Mock and Stub Architecture
108 | 
109 | ### MCP Protocol Backends
110 | - Fake backend servers respond with deterministic JSON:
111 |   - `GET /capabilities`: lists tools/resources/prompts
112 |   - `POST /mcp/tools/list` and `/mcp/resources/list`: optional fallbacks
113 |   - `POST /mcp/tools/call`: echoes arguments or returns canned results
114 |   - `POST /mcp/resources/read`: returns fixture content
115 | - Supports Bearer tokens to simulate auth propagation from Master.
116 | 
117 | ### OAuth Provider (OIDC) Mock
118 | - Node HTTP server (Express or http module) serving:
119 |   - `/.well-known/openid-configuration`
120 |   - `/authorize`: simulates auth code issuance by redirecting with `code` + `state`
121 |   - `/token`: returns JSON access token, optional refresh, scopes, `expires_in`
122 |   - `/jwks.json`: JWKS for completeness if JOSE validation is later added upstream
123 | - Uses `jose` to generate ephemeral key material and produce signed tokens if needed.
124 | - Configurable via factory helpers to tailor provider metadata per test.
125 | 
126 | ### HTTP Mocking for Node (optional)
127 | - `undici` MockAgent helper when spinning HTTP servers is overkill.
128 | - Route by URL patterns and methods; fallback to network disallowed.
129 | 
130 | ---
131 | 
132 | ## Test Data Management
133 | 
134 | - Fixtures: JSON payloads for tools/resources/capabilities. Keep small and readable.
135 | - Factories:
136 |   - `configFactory`: produce `MasterConfig` + `ServerConfig` with sensible defaults (ports, endpoints, auth strategies).
137 |   - `oauthFactory`: generate PKCE/state payloads and basic OAuth token shapes.
138 |   - `mcpFactory`: create tool/resource definitions and common requests.
139 | - State/Token Stores:
140 |   - In-memory only; reset across tests.
141 |   - If a persistent DB is introduced later:
142 |     - Node: use SQLite in-memory or Testcontainers in CI; provide cleanup hooks.
143 |     - Workers: use Miniflare KV/D1 bindings with per-test namespaces.
144 | 
145 | ---
146 | 
147 | ## Performance Testing Strategy (Artillery)
148 | 
149 | - Scenarios:
150 |   - OAuth authorize/token (mock provider) happy-path latency and error rates.
151 |   - Routing: `POST /mcp/tools/call` with mixed success/failure from backends to exercise retry/circuit logic.
152 |   - Server lifecycle: parallel discovery calls to `/capabilities` against multiple backends.
153 | - Metrics:
154 |   - p50/p90/p99 latency, RPS, error rates per route.
155 |   - Custom logs via `Logger.time` around critical paths; scrape from structured logs when running locally.
156 | - CI:
157 |   - Run on a separate “performance” workflow or on nightly schedules to avoid slowing PRs.
158 | 
159 | ---
160 | 
161 | ## Security Testing Architecture
162 | 
163 | - Property-based testing with `fast-check` for:
164 |   - `FlowValidator.validateReturnTo`: ensure only safe origins/paths pass.
165 |   - `StateManager` and `PKCEManager`: state integrity, one-time consumption, and PKCE verifier binding.
166 |   - Input validation for router requests (e.g., tool/resource names) using `utils/validation` helpers.
167 | - OAuth flow protections:
168 |   - Ensure `state` is required and consumed exactly once.
169 |   - Enforce PKCE method presence, verify rejection on mismatch.
170 |   - Token exchange failure handling and error surface is sanitized.
171 | - Optional dynamic scans:
172 |   - OWASP ZAP baseline against local server in CI (nightly) to catch obvious misconfigurations.
173 | 
174 | ---
175 | 
176 | ## CI/CD Integration
177 | 
178 | ### Jobs
179 | - Lint + Typecheck: ESLint and `tsc -p tsconfig.node.json --noEmit`.
180 | - Unit + Integration (Node): Vitest with coverage.
181 | - Unit + Integration (Workers): Vitest (Miniflare) with coverage.
182 | - E2E: start local server; run black-box tests.
183 | - Security: property tests; optional ZAP baseline (nightly).
184 | - Performance: Artillery (nightly or gated by label).
185 | 
186 | ### Coverage and Quality Gates
187 | - Coverage via Vitest v8 provider; thresholds:
188 |   - Global: `branches: 80%`, `functions: 85%`, `lines: 85%`, `statements: 85%`.
189 |   - Critical modules (routing, oauth): per-file `lines: 90%` target in follow-up.
190 | - Fail PR job if thresholds not met.
191 | - Upload `lcov` or `cobertura` to CI artifacts (or Codecov if desired).
192 | 
193 | ---
194 | 
195 | ## Test Utilities Integration (Phase 8)
196 | 
197 | - Logger: `Logger.configure({ json: true, level: 'error' })` in test setup; use `log-capture` to assert important events.
198 | - Validation: assert guards (`assertString`, `sanitizeObject`) across boundary tests; fuzz via fast-check.
199 | - Monitoring: wrap performance-critical test paths with `Logger.time` and assert upper bounds during perf runs.
200 | 
201 | ---
202 | 
203 | ## Local and CI Usage
204 | 
205 | - Local:
206 |   - `vitest -c vitest.config.ts` for Node suites.
207 |   - `vitest -c vitest.worker.config.ts` for Worker suites.
208 |   - `artillery run tests/perf/artillery/auth-routing.yaml` for load tests.
209 | 
210 | - CI:
211 |   - Run jobs in parallel; collect coverage and artifacts.
212 |   - Gate merges on lint, typecheck, unit/integration, and coverage.
213 | 
214 | ---
215 | 
216 | ## Next Steps (Implementation Guide)
217 | 1. Install dev deps: vitest, @vitest/coverage-v8, supertest, miniflare, artillery, fast-check, @types/supertest.
218 | 2. Add scripts: `test`, `test:node`, `test:workers`, `test:coverage`, `test:e2e`.
219 | 3. Fill factories and mocks with minimal working endpoints.
220 | 4. Seed initial critical tests:
221 |    - routing: circuit-breaker/retry/load-balancer
222 |    - oauth: state, pkce, callback error handling
223 |    - aggregator: discovery + mapping
224 |    - protocol: call tool/resource happy path and error path
225 | 
226 | 
```

--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js:
--------------------------------------------------------------------------------

```javascript
  1 | import {
  2 |   DefaultMagicKeysAliasMap,
  3 |   StorageSerializers,
  4 |   TransitionPresets,
  5 |   assert,
  6 |   breakpointsAntDesign,
  7 |   breakpointsBootstrapV5,
  8 |   breakpointsElement,
  9 |   breakpointsMasterCss,
 10 |   breakpointsPrimeFlex,
 11 |   breakpointsQuasar,
 12 |   breakpointsSematic,
 13 |   breakpointsTailwind,
 14 |   breakpointsVuetify,
 15 |   breakpointsVuetifyV2,
 16 |   breakpointsVuetifyV3,
 17 |   bypassFilter,
 18 |   camelize,
 19 |   clamp,
 20 |   cloneFnJSON,
 21 |   computedAsync,
 22 |   computedEager,
 23 |   computedInject,
 24 |   computedWithControl,
 25 |   containsProp,
 26 |   controlledRef,
 27 |   createEventHook,
 28 |   createFetch,
 29 |   createFilterWrapper,
 30 |   createGlobalState,
 31 |   createInjectionState,
 32 |   createRef,
 33 |   createReusableTemplate,
 34 |   createSharedComposable,
 35 |   createSingletonPromise,
 36 |   createTemplatePromise,
 37 |   createUnrefFn,
 38 |   customStorageEventName,
 39 |   debounceFilter,
 40 |   defaultDocument,
 41 |   defaultLocation,
 42 |   defaultNavigator,
 43 |   defaultWindow,
 44 |   executeTransition,
 45 |   extendRef,
 46 |   formatDate,
 47 |   formatTimeAgo,
 48 |   get,
 49 |   getLifeCycleTarget,
 50 |   getSSRHandler,
 51 |   hasOwn,
 52 |   hyphenate,
 53 |   identity,
 54 |   increaseWithUnit,
 55 |   injectLocal,
 56 |   invoke,
 57 |   isClient,
 58 |   isDef,
 59 |   isDefined,
 60 |   isIOS,
 61 |   isObject,
 62 |   isWorker,
 63 |   makeDestructurable,
 64 |   mapGamepadToXbox360Controller,
 65 |   noop,
 66 |   normalizeDate,
 67 |   notNullish,
 68 |   now,
 69 |   objectEntries,
 70 |   objectOmit,
 71 |   objectPick,
 72 |   onClickOutside,
 73 |   onElementRemoval,
 74 |   onKeyDown,
 75 |   onKeyPressed,
 76 |   onKeyStroke,
 77 |   onKeyUp,
 78 |   onLongPress,
 79 |   onStartTyping,
 80 |   pausableFilter,
 81 |   promiseTimeout,
 82 |   provideLocal,
 83 |   provideSSRWidth,
 84 |   pxValue,
 85 |   rand,
 86 |   reactify,
 87 |   reactifyObject,
 88 |   reactiveComputed,
 89 |   reactiveOmit,
 90 |   reactivePick,
 91 |   refAutoReset,
 92 |   refDebounced,
 93 |   refDefault,
 94 |   refThrottled,
 95 |   refWithControl,
 96 |   resolveRef,
 97 |   resolveUnref,
 98 |   set,
 99 |   setSSRHandler,
100 |   syncRef,
101 |   syncRefs,
102 |   templateRef,
103 |   throttleFilter,
104 |   timestamp,
105 |   toArray,
106 |   toReactive,
107 |   toRef,
108 |   toRefs,
109 |   toValue,
110 |   tryOnBeforeMount,
111 |   tryOnBeforeUnmount,
112 |   tryOnMounted,
113 |   tryOnScopeDispose,
114 |   tryOnUnmounted,
115 |   unrefElement,
116 |   until,
117 |   useActiveElement,
118 |   useAnimate,
119 |   useArrayDifference,
120 |   useArrayEvery,
121 |   useArrayFilter,
122 |   useArrayFind,
123 |   useArrayFindIndex,
124 |   useArrayFindLast,
125 |   useArrayIncludes,
126 |   useArrayJoin,
127 |   useArrayMap,
128 |   useArrayReduce,
129 |   useArraySome,
130 |   useArrayUnique,
131 |   useAsyncQueue,
132 |   useAsyncState,
133 |   useBase64,
134 |   useBattery,
135 |   useBluetooth,
136 |   useBreakpoints,
137 |   useBroadcastChannel,
138 |   useBrowserLocation,
139 |   useCached,
140 |   useClipboard,
141 |   useClipboardItems,
142 |   useCloned,
143 |   useColorMode,
144 |   useConfirmDialog,
145 |   useCountdown,
146 |   useCounter,
147 |   useCssVar,
148 |   useCurrentElement,
149 |   useCycleList,
150 |   useDark,
151 |   useDateFormat,
152 |   useDebounceFn,
153 |   useDebouncedRefHistory,
154 |   useDeviceMotion,
155 |   useDeviceOrientation,
156 |   useDevicePixelRatio,
157 |   useDevicesList,
158 |   useDisplayMedia,
159 |   useDocumentVisibility,
160 |   useDraggable,
161 |   useDropZone,
162 |   useElementBounding,
163 |   useElementByPoint,
164 |   useElementHover,
165 |   useElementSize,
166 |   useElementVisibility,
167 |   useEventBus,
168 |   useEventListener,
169 |   useEventSource,
170 |   useEyeDropper,
171 |   useFavicon,
172 |   useFetch,
173 |   useFileDialog,
174 |   useFileSystemAccess,
175 |   useFocus,
176 |   useFocusWithin,
177 |   useFps,
178 |   useFullscreen,
179 |   useGamepad,
180 |   useGeolocation,
181 |   useIdle,
182 |   useImage,
183 |   useInfiniteScroll,
184 |   useIntersectionObserver,
185 |   useInterval,
186 |   useIntervalFn,
187 |   useKeyModifier,
188 |   useLastChanged,
189 |   useLocalStorage,
190 |   useMagicKeys,
191 |   useManualRefHistory,
192 |   useMediaControls,
193 |   useMediaQuery,
194 |   useMemoize,
195 |   useMemory,
196 |   useMounted,
197 |   useMouse,
198 |   useMouseInElement,
199 |   useMousePressed,
200 |   useMutationObserver,
201 |   useNavigatorLanguage,
202 |   useNetwork,
203 |   useNow,
204 |   useObjectUrl,
205 |   useOffsetPagination,
206 |   useOnline,
207 |   usePageLeave,
208 |   useParallax,
209 |   useParentElement,
210 |   usePerformanceObserver,
211 |   usePermission,
212 |   usePointer,
213 |   usePointerLock,
214 |   usePointerSwipe,
215 |   usePreferredColorScheme,
216 |   usePreferredContrast,
217 |   usePreferredDark,
218 |   usePreferredLanguages,
219 |   usePreferredReducedMotion,
220 |   usePreferredReducedTransparency,
221 |   usePrevious,
222 |   useRafFn,
223 |   useRefHistory,
224 |   useResizeObserver,
225 |   useSSRWidth,
226 |   useScreenOrientation,
227 |   useScreenSafeArea,
228 |   useScriptTag,
229 |   useScroll,
230 |   useScrollLock,
231 |   useSessionStorage,
232 |   useShare,
233 |   useSorted,
234 |   useSpeechRecognition,
235 |   useSpeechSynthesis,
236 |   useStepper,
237 |   useStorage,
238 |   useStorageAsync,
239 |   useStyleTag,
240 |   useSupported,
241 |   useSwipe,
242 |   useTemplateRefsList,
243 |   useTextDirection,
244 |   useTextSelection,
245 |   useTextareaAutosize,
246 |   useThrottleFn,
247 |   useThrottledRefHistory,
248 |   useTimeAgo,
249 |   useTimeout,
250 |   useTimeoutFn,
251 |   useTimeoutPoll,
252 |   useTimestamp,
253 |   useTitle,
254 |   useToNumber,
255 |   useToString,
256 |   useToggle,
257 |   useTransition,
258 |   useUrlSearchParams,
259 |   useUserMedia,
260 |   useVModel,
261 |   useVModels,
262 |   useVibrate,
263 |   useVirtualList,
264 |   useWakeLock,
265 |   useWebNotification,
266 |   useWebSocket,
267 |   useWebWorker,
268 |   useWebWorkerFn,
269 |   useWindowFocus,
270 |   useWindowScroll,
271 |   useWindowSize,
272 |   watchArray,
273 |   watchAtMost,
274 |   watchDebounced,
275 |   watchDeep,
276 |   watchIgnorable,
277 |   watchImmediate,
278 |   watchOnce,
279 |   watchPausable,
280 |   watchThrottled,
281 |   watchTriggerable,
282 |   watchWithFilter,
283 |   whenever
284 | } from "./chunk-P2XGSYO7.js";
285 | import "./chunk-HVR2FF6M.js";
286 | export {
287 |   DefaultMagicKeysAliasMap,
288 |   StorageSerializers,
289 |   TransitionPresets,
290 |   assert,
291 |   computedAsync as asyncComputed,
292 |   refAutoReset as autoResetRef,
293 |   breakpointsAntDesign,
294 |   breakpointsBootstrapV5,
295 |   breakpointsElement,
296 |   breakpointsMasterCss,
297 |   breakpointsPrimeFlex,
298 |   breakpointsQuasar,
299 |   breakpointsSematic,
300 |   breakpointsTailwind,
301 |   breakpointsVuetify,
302 |   breakpointsVuetifyV2,
303 |   breakpointsVuetifyV3,
304 |   bypassFilter,
305 |   camelize,
306 |   clamp,
307 |   cloneFnJSON,
308 |   computedAsync,
309 |   computedEager,
310 |   computedInject,
311 |   computedWithControl,
312 |   containsProp,
313 |   computedWithControl as controlledComputed,
314 |   controlledRef,
315 |   createEventHook,
316 |   createFetch,
317 |   createFilterWrapper,
318 |   createGlobalState,
319 |   createInjectionState,
320 |   reactify as createReactiveFn,
321 |   createRef,
322 |   createReusableTemplate,
323 |   createSharedComposable,
324 |   createSingletonPromise,
325 |   createTemplatePromise,
326 |   createUnrefFn,
327 |   customStorageEventName,
328 |   debounceFilter,
329 |   refDebounced as debouncedRef,
330 |   watchDebounced as debouncedWatch,
331 |   defaultDocument,
332 |   defaultLocation,
333 |   defaultNavigator,
334 |   defaultWindow,
335 |   computedEager as eagerComputed,
336 |   executeTransition,
337 |   extendRef,
338 |   formatDate,
339 |   formatTimeAgo,
340 |   get,
341 |   getLifeCycleTarget,
342 |   getSSRHandler,
343 |   hasOwn,
344 |   hyphenate,
345 |   identity,
346 |   watchIgnorable as ignorableWatch,
347 |   increaseWithUnit,
348 |   injectLocal,
349 |   invoke,
350 |   isClient,
351 |   isDef,
352 |   isDefined,
353 |   isIOS,
354 |   isObject,
355 |   isWorker,
356 |   makeDestructurable,
357 |   mapGamepadToXbox360Controller,
358 |   noop,
359 |   normalizeDate,
360 |   notNullish,
361 |   now,
362 |   objectEntries,
363 |   objectOmit,
364 |   objectPick,
365 |   onClickOutside,
366 |   onElementRemoval,
367 |   onKeyDown,
368 |   onKeyPressed,
369 |   onKeyStroke,
370 |   onKeyUp,
371 |   onLongPress,
372 |   onStartTyping,
373 |   pausableFilter,
374 |   watchPausable as pausableWatch,
375 |   promiseTimeout,
376 |   provideLocal,
377 |   provideSSRWidth,
378 |   pxValue,
379 |   rand,
380 |   reactify,
381 |   reactifyObject,
382 |   reactiveComputed,
383 |   reactiveOmit,
384 |   reactivePick,
385 |   refAutoReset,
386 |   refDebounced,
387 |   refDefault,
388 |   refThrottled,
389 |   refWithControl,
390 |   resolveRef,
391 |   resolveUnref,
392 |   set,
393 |   setSSRHandler,
394 |   syncRef,
395 |   syncRefs,
396 |   templateRef,
397 |   throttleFilter,
398 |   refThrottled as throttledRef,
399 |   watchThrottled as throttledWatch,
400 |   timestamp,
401 |   toArray,
402 |   toReactive,
403 |   toRef,
404 |   toRefs,
405 |   toValue,
406 |   tryOnBeforeMount,
407 |   tryOnBeforeUnmount,
408 |   tryOnMounted,
409 |   tryOnScopeDispose,
410 |   tryOnUnmounted,
411 |   unrefElement,
412 |   until,
413 |   useActiveElement,
414 |   useAnimate,
415 |   useArrayDifference,
416 |   useArrayEvery,
417 |   useArrayFilter,
418 |   useArrayFind,
419 |   useArrayFindIndex,
420 |   useArrayFindLast,
421 |   useArrayIncludes,
422 |   useArrayJoin,
423 |   useArrayMap,
424 |   useArrayReduce,
425 |   useArraySome,
426 |   useArrayUnique,
427 |   useAsyncQueue,
428 |   useAsyncState,
429 |   useBase64,
430 |   useBattery,
431 |   useBluetooth,
432 |   useBreakpoints,
433 |   useBroadcastChannel,
434 |   useBrowserLocation,
435 |   useCached,
436 |   useClipboard,
437 |   useClipboardItems,
438 |   useCloned,
439 |   useColorMode,
440 |   useConfirmDialog,
441 |   useCountdown,
442 |   useCounter,
443 |   useCssVar,
444 |   useCurrentElement,
445 |   useCycleList,
446 |   useDark,
447 |   useDateFormat,
448 |   refDebounced as useDebounce,
449 |   useDebounceFn,
450 |   useDebouncedRefHistory,
451 |   useDeviceMotion,
452 |   useDeviceOrientation,
453 |   useDevicePixelRatio,
454 |   useDevicesList,
455 |   useDisplayMedia,
456 |   useDocumentVisibility,
457 |   useDraggable,
458 |   useDropZone,
459 |   useElementBounding,
460 |   useElementByPoint,
461 |   useElementHover,
462 |   useElementSize,
463 |   useElementVisibility,
464 |   useEventBus,
465 |   useEventListener,
466 |   useEventSource,
467 |   useEyeDropper,
468 |   useFavicon,
469 |   useFetch,
470 |   useFileDialog,
471 |   useFileSystemAccess,
472 |   useFocus,
473 |   useFocusWithin,
474 |   useFps,
475 |   useFullscreen,
476 |   useGamepad,
477 |   useGeolocation,
478 |   useIdle,
479 |   useImage,
480 |   useInfiniteScroll,
481 |   useIntersectionObserver,
482 |   useInterval,
483 |   useIntervalFn,
484 |   useKeyModifier,
485 |   useLastChanged,
486 |   useLocalStorage,
487 |   useMagicKeys,
488 |   useManualRefHistory,
489 |   useMediaControls,
490 |   useMediaQuery,
491 |   useMemoize,
492 |   useMemory,
493 |   useMounted,
494 |   useMouse,
495 |   useMouseInElement,
496 |   useMousePressed,
497 |   useMutationObserver,
498 |   useNavigatorLanguage,
499 |   useNetwork,
500 |   useNow,
501 |   useObjectUrl,
502 |   useOffsetPagination,
503 |   useOnline,
504 |   usePageLeave,
505 |   useParallax,
506 |   useParentElement,
507 |   usePerformanceObserver,
508 |   usePermission,
509 |   usePointer,
510 |   usePointerLock,
511 |   usePointerSwipe,
512 |   usePreferredColorScheme,
513 |   usePreferredContrast,
514 |   usePreferredDark,
515 |   usePreferredLanguages,
516 |   usePreferredReducedMotion,
517 |   usePreferredReducedTransparency,
518 |   usePrevious,
519 |   useRafFn,
520 |   useRefHistory,
521 |   useResizeObserver,
522 |   useSSRWidth,
523 |   useScreenOrientation,
524 |   useScreenSafeArea,
525 |   useScriptTag,
526 |   useScroll,
527 |   useScrollLock,
528 |   useSessionStorage,
529 |   useShare,
530 |   useSorted,
531 |   useSpeechRecognition,
532 |   useSpeechSynthesis,
533 |   useStepper,
534 |   useStorage,
535 |   useStorageAsync,
536 |   useStyleTag,
537 |   useSupported,
538 |   useSwipe,
539 |   useTemplateRefsList,
540 |   useTextDirection,
541 |   useTextSelection,
542 |   useTextareaAutosize,
543 |   refThrottled as useThrottle,
544 |   useThrottleFn,
545 |   useThrottledRefHistory,
546 |   useTimeAgo,
547 |   useTimeout,
548 |   useTimeoutFn,
549 |   useTimeoutPoll,
550 |   useTimestamp,
551 |   useTitle,
552 |   useToNumber,
553 |   useToString,
554 |   useToggle,
555 |   useTransition,
556 |   useUrlSearchParams,
557 |   useUserMedia,
558 |   useVModel,
559 |   useVModels,
560 |   useVibrate,
561 |   useVirtualList,
562 |   useWakeLock,
563 |   useWebNotification,
564 |   useWebSocket,
565 |   useWebWorker,
566 |   useWebWorkerFn,
567 |   useWindowFocus,
568 |   useWindowScroll,
569 |   useWindowSize,
570 |   watchArray,
571 |   watchAtMost,
572 |   watchDebounced,
573 |   watchDeep,
574 |   watchIgnorable,
575 |   watchImmediate,
576 |   watchOnce,
577 |   watchPausable,
578 |   watchThrottled,
579 |   watchTriggerable,
580 |   watchWithFilter,
581 |   whenever
582 | };
583 | //# sourceMappingURL=vitepress___@vueuse_core.js.map
584 | 
```

--------------------------------------------------------------------------------
/src/modules/module-loader.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { LoadedServer, ServerProcess, ServerType } from '../types/server.js'
  2 | import type { ServerConfig } from '../types/config.js'
  3 | import type { AuthHeaders } from '../types/auth.js'
  4 | import { Logger } from '../utils/logger.js'
  5 | 
  6 | export interface ModuleLoaderOptions {
  7 |   healthEndpoint?: string // path appended to endpoint, defaults to '/health'
  8 |   capabilitiesEndpoint?: string // path appended to endpoint, defaults to '/capabilities'
  9 |   defaultHostname?: string // defaults to 'localhost'
 10 | }
 11 | 
 12 | export interface ModuleLoader {
 13 |   loadServers(configs: ServerConfig[], clientToken?: string): Promise<Map<string, LoadedServer>>
 14 |   load(config: ServerConfig, clientToken?: string): Promise<LoadedServer>
 15 |   unload(id: string): Promise<void>
 16 |   performHealthCheck(server: LoadedServer, clientToken?: string): Promise<boolean>
 17 |   restartServer(serverId: string): Promise<void>
 18 | }
 19 | 
 20 | /**
 21 |  * DefaultModuleLoader implements multi-source loading with cross-platform process placeholders.
 22 |  * It avoids Node-specific APIs so it can compile for both Node and Workers builds.
 23 |  * Actual spawning should be implemented by a platform-specific adapter in later phases.
 24 |  */
 25 | export class DefaultModuleLoader implements ModuleLoader {
 26 |   private servers = new Map<string, LoadedServer>()
 27 |   private options: Required<ModuleLoaderOptions>
 28 | 
 29 |   constructor(options?: ModuleLoaderOptions) {
 30 |     this.options = {
 31 |       healthEndpoint: options?.healthEndpoint ?? '/health',
 32 |       capabilitiesEndpoint: options?.capabilitiesEndpoint ?? '/capabilities',
 33 |       defaultHostname: options?.defaultHostname ?? 'localhost',
 34 |     }
 35 |   }
 36 | 
 37 |   async loadServers(configs: ServerConfig[], clientToken?: string): Promise<Map<string, LoadedServer>> {
 38 |     const results = await Promise.all(
 39 |       configs.map(async (cfg) => {
 40 |         try {
 41 |           const server = await this.load(cfg, clientToken)
 42 |           return [cfg.id, server] as const
 43 |         } catch (err) {
 44 |           Logger.error(`Failed to load server ${cfg.id}`, err)
 45 |           const server: LoadedServer = {
 46 |             id: cfg.id,
 47 |             type: 'unknown',
 48 |             endpoint: 'unknown',
 49 |             config: cfg,
 50 |             status: 'error',
 51 |             lastHealthCheck: Date.now(),
 52 |           }
 53 |           return [cfg.id, server] as const
 54 |         }
 55 |       })
 56 |     )
 57 |     for (const [id, s] of results) this.servers.set(id, s)
 58 |     return new Map(this.servers)
 59 |   }
 60 | 
 61 |   async load(config: ServerConfig, clientToken?: string): Promise<LoadedServer> {
 62 |     const type = this.detectServerType(config)
 63 |     const base: LoadedServer = {
 64 |       id: config.id,
 65 |       type,
 66 |       endpoint: this.deriveEndpoint(config),
 67 |       config,
 68 |       status: 'starting',
 69 |       lastHealthCheck: 0,
 70 |     }
 71 | 
 72 |     let loaded: LoadedServer
 73 |     switch (config.type) {
 74 |       case 'git':
 75 |         loaded = await this.loadFromGit(config, base)
 76 |         break
 77 |       case 'npm':
 78 |         loaded = await this.loadFromNpm(config, base)
 79 |         break
 80 |       case 'pypi':
 81 |         loaded = await this.loadFromPypi(config, base)
 82 |         break
 83 |       case 'docker':
 84 |         loaded = await this.loadFromDocker(config, base)
 85 |         break
 86 |       case 'local':
 87 |         loaded = await this.loadFromLocal(config, base)
 88 |         break
 89 |       default:
 90 |         loaded = base
 91 |     }
 92 | 
 93 |     // Immediate health check to set running/error
 94 |     try {
 95 |       const ok = await this.performHealthCheck(loaded, clientToken)
 96 |       loaded.status = ok ? 'running' : 'error'
 97 |     } catch (err) {
 98 |       Logger.warn(`Health check failed for ${config.id}`, err)
 99 |       loaded.status = 'error'
100 |     }
101 | 
102 |     this.servers.set(loaded.id, loaded)
103 |     return loaded
104 |   }
105 | 
106 |   async unload(id: string): Promise<void> {
107 |     const server = this.servers.get(id)
108 |     if (!server) return
109 |     try {
110 |       await server.process?.stop()
111 |     } catch (err) {
112 |       Logger.warn(`Error stopping server ${id}`, err)
113 |     } finally {
114 |       this.servers.delete(id)
115 |       Logger.logServerEvent('unloaded', id)
116 |     }
117 |   }
118 | 
119 |   async restartServer(serverId: string): Promise<void> {
120 |     const server = this.servers.get(serverId)
121 |     if (!server) throw new Error(`Server not found: ${serverId}`)
122 |     Logger.logServerEvent('restarting', serverId)
123 |     try {
124 |       await server.process?.stop()
125 |     } catch (err) {
126 |       Logger.warn(`Error stopping server ${serverId} during restart`, err)
127 |     }
128 |     // Re-load using the same config
129 |     const reloaded = await this.load(server.config)
130 |     this.servers.set(serverId, reloaded)
131 |   }
132 | 
133 |   async performHealthCheck(server: LoadedServer, clientToken?: string): Promise<boolean> {
134 |     if (!server.endpoint || server.endpoint === 'unknown') {
135 |       server.lastHealthCheck = Date.now()
136 |       server.status = 'error'
137 |       return false
138 |     }
139 |     const url = new URL(this.options.healthEndpoint, this.ensureTrailingSlash(server.endpoint)).toString()
140 |     const headers: AuthHeaders = {}
141 |     // In Phase 3, auth integration is handled at higher layers; here we only accept a caller-provided token.
142 |     if (clientToken) headers['Authorization'] = `Bearer ${clientToken}`
143 |     try {
144 |       const res = await fetch(url, { headers })
145 |       server.lastHealthCheck = Date.now()
146 |       if (!res.ok) return false
147 |       const ct = res.headers.get('content-type') || ''
148 |       if (ct.includes('application/json')) {
149 |         const json = (await res.json()) as any
150 |         return Boolean(json?.ok ?? true)
151 |       }
152 |       return true
153 |     } catch (err) {
154 |       server.lastHealthCheck = Date.now()
155 |       Logger.warn(`Health check request failed for ${server.id}`, err)
156 |       return false
157 |     }
158 |   }
159 | 
160 |   // --- Multi-source loading stubs (network/process performed outside this module in later phases) ---
161 |   private async loadFromGit(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
162 |     // In a full implementation, we'd clone and install. Here we assume it's pre-built and start it.
163 |     Logger.logServerEvent('loadFromGit', config.id, { url: config.url, branch: config.branch })
164 |     return this.startRuntime(config, base)
165 |   }
166 | 
167 |   private async loadFromNpm(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
168 |     Logger.logServerEvent('loadFromNpm', config.id, { pkg: config.package, version: config.version })
169 |     return this.startRuntime(config, base)
170 |   }
171 | 
172 |   private async loadFromPypi(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
173 |     Logger.logServerEvent('loadFromPypi', config.id, { pkg: config.package, version: config.version })
174 |     return this.startRuntime(config, base)
175 |   }
176 | 
177 |   private async loadFromDocker(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
178 |     Logger.logServerEvent('loadFromDocker', config.id, { image: config.package, tag: config.version })
179 |     // Docker orchestration would run the container exposing a port; we only resolve endpoint here
180 |     return { ...base, status: 'starting' }
181 |   }
182 | 
183 |   private async loadFromLocal(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
184 |     Logger.logServerEvent('loadFromLocal', config.id, { path: config.url })
185 |     Logger.info('Loading local server', { config })
186 |     const result = await this.startRuntime(config, base)
187 |     Logger.info('Loaded local server', { result })
188 |     return result
189 |   }
190 | 
191 |   // --- Runtime orchestration and type detection ---
192 |   private detectServerType(config: ServerConfig): ServerType {
193 |     // Heuristics: look at package name/url/args for hints
194 |     const name = (config.package ?? config.url ?? '').toLowerCase()
195 |     Logger.info('Detecting server type', { name, config })
196 |     
197 |     // For file URLs, we should detect as stdio
198 |     if (config.url && config.url.startsWith('file://')) {
199 |       Logger.info('Assuming stdio server for file URL', { url: config.url })
200 |       return 'stdio'
201 |     }
202 |     
203 |     // For URLs, we can't determine the type from the URL itself
204 |     // Let's assume it's a node server for HTTP URLs
205 |     if (config.url && /^https?:\/\//i.test(config.url)) {
206 |       Logger.info('Assuming node server for HTTP URL', { url: config.url })
207 |       return 'node'
208 |     }
209 |     
210 |     if (name.endsWith('.py') || /py|pypi|python/.test(name)) return 'python'
211 |     if (/ts|typescript/.test(name)) return 'typescript'
212 |     if (/node|js|npm/.test(name)) return 'node'
213 |     return 'unknown'
214 |   }
215 | 
216 |   private deriveEndpoint(config: ServerConfig): string {
217 |     const port = config.config.port
218 |     if (port) return `http://${this.options.defaultHostname}:${port}`
219 |     // If URL looks like http(s):// use as-is
220 |     const url = config.url ?? ''
221 |     Logger.info('Deriving endpoint', { url, config })
222 |     if (/^https?:\/\//i.test(url)) return url
223 |     // For file URLs (STDIO servers), we can't derive an HTTP endpoint
224 |     if (url.startsWith('file://')) return 'unknown'
225 |     return 'unknown'
226 |   }
227 | 
228 |   private async startRuntime(config: ServerConfig, base: LoadedServer): Promise<LoadedServer> {
229 |     const type = this.detectServerType(config)
230 |     Logger.info('Detected server type', { type, config })
231 |     let proc: ServerProcess | undefined
232 |     try {
233 |       // For remote servers (HTTP URLs), we don't need to start a process
234 |       if (config.url && /^https?:\/\//i.test(config.url)) {
235 |         Logger.info('Remote server, no process to start', { url: config.url })
236 |         proc = undefined
237 |       } else if (type === 'stdio') {
238 |         // For STDIO servers, start as a child process
239 |         Logger.info('Starting STDIO server as child process', { url: config.url })
240 |         const { StdioManager } = await import('./stdio-manager.js')
241 |         const stdioManager = new StdioManager()
242 |         const filePath = config.url!.replace('file://', '')
243 |         proc = await stdioManager.startServer(config.id, filePath, config.config.environment)
244 |       } else if (type === 'python') {
245 |         proc = await this.startPythonServer(config)
246 |       } else if (type === 'typescript' || type === 'node') {
247 |         proc = await this.startTypeScriptServer(config)
248 |       } else {
249 |         // Unknown: assume externally managed endpoint
250 |         proc = undefined
251 |       }
252 |     } catch (err) {
253 |       Logger.error(`Failed to start runtime for ${config.id}`, err)
254 |       return { ...base, status: 'error' }
255 |     }
256 |     Logger.info('Started runtime', { proc, base })
257 |     return { ...base, process: proc, status: 'starting' }
258 |   }
259 | 
260 |   // Cross-platform placeholders. Real implementation should manage child processes per-OS.
261 |   private async startPythonServer(_config: ServerConfig): Promise<ServerProcess> {
262 |     // Placeholder: assume an external process is started via orchestrator. Provide a no-op stop.
263 |     return { stop: async () => void 0 }
264 |   }
265 | 
266 |   private async startTypeScriptServer(config: ServerConfig): Promise<ServerProcess> {
267 |     // Check if this is a file URL that should be started as a child process
268 |     if (config.url && config.url.startsWith('file://')) {
269 |       Logger.info('Starting TypeScript server as child process', { url: config.url })
270 |       
271 |       // Import child_process dynamically
272 |       const { spawn } = await import('node:child_process')
273 |       
274 |       // Convert file:// URL to path
275 |       const filePath = config.url.replace('file://', '')
276 |       
277 |       // Spawn the process
278 |       const proc = spawn('node', [filePath], {
279 |         stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
280 |         env: { ...process.env, ...config.config.environment }
281 |       })
282 |       
283 |       return {
284 |         pid: proc.pid,
285 |         stop: async () => {
286 |           return new Promise((resolve) => {
287 |             proc.kill()
288 |             setTimeout(() => resolve(), 1000) // Wait 1 second for graceful shutdown
289 |           })
290 |         }
291 |       }
292 |     }
293 |     
294 |     // Placeholder: assume an external process is started via orchestrator. Provide a no-op stop.
295 |     return { stop: async () => void 0 }
296 |   }
297 | 
298 |   private ensureTrailingSlash(endpoint: string): string {
299 |     if (!endpoint.endsWith('/')) return `${endpoint}/`
300 |     return endpoint
301 |   }
302 | }
303 | 
```

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

```typescript
  1 | import type { MasterConfig } from '../types/config.js'
  2 | import type { OAuthToken } from '../types/auth.js'
  3 | import { PKCEManager } from './pkce-manager.js'
  4 | import { StateManager } from './state-manager.js'
  5 | import { FlowValidator } from './flow-validator.js'
  6 | import { CallbackHandler } from './callback-handler.js'
  7 | import { WebInterface } from './web-interface.js'
  8 | import { Logger } from '../utils/logger.js'
  9 | 
 10 | export interface FlowControllerDeps {
 11 |   getConfig: () => MasterConfig
 12 |   // Called to store a delegated token when server + client context is known
 13 |   storeDelegatedToken?: (clientToken: string, serverId: string, token: OAuthToken) => Promise<void>
 14 | }
 15 | 
 16 | export class OAuthFlowController {
 17 |   private readonly pkce = new PKCEManager()
 18 |   private readonly state = new StateManager()
 19 |   private readonly validator: FlowValidator
 20 |   private readonly web = new WebInterface()
 21 |   private readonly deps: FlowControllerDeps
 22 |   private readonly basePath: string
 23 | 
 24 |   constructor(deps: FlowControllerDeps, basePath = '/oauth') {
 25 |     this.validator = new FlowValidator(deps.getConfig)
 26 |     this.deps = deps
 27 |     this.basePath = basePath
 28 |   }
 29 | 
 30 |   // Compute baseUrl from request context
 31 |   private getBaseUrlFromExpress(req: any): string {
 32 |     const cfg = this.deps.getConfig()
 33 |     if (cfg.hosting?.base_url) return cfg.hosting.base_url
 34 |     const proto = (req.protocol as string) || 'http'
 35 |     const host = (req.get?.('host') as string) || req.headers?.host
 36 |     return `${proto}://${host}`
 37 |   }
 38 | 
 39 |   private getBaseUrlFromRequest(req: Request): string {
 40 |     const cfg = this.deps.getConfig()
 41 |     if (cfg.hosting?.base_url) return cfg.hosting.base_url
 42 |     try {
 43 |       const u = new URL(req.url)
 44 |       return `${u.protocol}//${u.host}`
 45 |     } catch {
 46 |       return 'http://localhost'
 47 |     }
 48 |   }
 49 | 
 50 |   // Express registration (no direct dependency on express types)
 51 |   registerExpress(app: any): void {
 52 |     const base = this.basePath
 53 |     // GET /oauth/authorize
 54 |     app.get(`${base}/authorize`, async (req: any, res: any) => {
 55 |       try {
 56 |         const query = req.query || {}
 57 |         const providerParam = typeof query.provider === 'string' ? query.provider : undefined
 58 |         const serverId = typeof query.server_id === 'string' ? query.server_id : undefined
 59 |         const scopesParam = typeof query.scope === 'string' ? query.scope : undefined
 60 |         const returnTo = this.validator.validateReturnTo(
 61 |           typeof query.return_to === 'string' ? query.return_to : undefined,
 62 |           this.getBaseUrlFromExpress(req)
 63 |         )
 64 |         const { config, providerId } = this.validator.resolveProvider({ provider: providerParam, serverId })
 65 |         const state = this.state.create({ provider: providerId, serverId, clientToken: this.getClientTokenFromExpress(req), returnTo })
 66 |         const { challenge, method } = await this.pkce.generate(state)
 67 |         const baseUrl = this.getBaseUrlFromExpress(req)
 68 |         const redirectUri = new URL(`${this.basePath}/callback`, baseUrl).toString()
 69 |         const authUrl = new URL(config.authorization_endpoint)
 70 |         authUrl.searchParams.set('response_type', 'code')
 71 |         authUrl.searchParams.set('client_id', config.client_id)
 72 |         authUrl.searchParams.set('redirect_uri', redirectUri)
 73 |         const scope = scopesParam ?? (config.scopes ? config.scopes.join(' ') : '')
 74 |         if (scope) authUrl.searchParams.set('scope', scope)
 75 |         authUrl.searchParams.set('state', state)
 76 |         authUrl.searchParams.set('code_challenge', challenge)
 77 |         authUrl.searchParams.set('code_challenge_method', method)
 78 | 
 79 |         // Render a small redirect page to avoid exposing long URLs in Location header logs
 80 |         res.set('content-type', 'text/html; charset=utf-8')
 81 |         res.status(200).send(this.web.renderRedirectPage(providerId, authUrl.toString()))
 82 |       } catch (err) {
 83 |         Logger.warn('OAuth authorize failed', err)
 84 |         res.redirect(`${this.basePath}/error`)
 85 |       }
 86 |     })
 87 | 
 88 |     // GET /oauth/callback
 89 |     app.get(`${base}/callback`, async (req: any, res: any) => {
 90 |       try {
 91 |         const params = new URLSearchParams(req.query as Record<string, string>)
 92 |         const providerParam = typeof req.query?.provider === 'string' ? (req.query.provider as string) : undefined
 93 |         const serverId = typeof req.query?.server_id === 'string' ? (req.query.server_id as string) : undefined
 94 |         const { config } = this.validator.resolveProvider({ provider: providerParam, serverId })
 95 |         const cb = new CallbackHandler({
 96 |           config: this.deps.getConfig(),
 97 |           pkceManager: this.pkce,
 98 |           stateManager: this.state,
 99 |           baseUrl: this.getBaseUrlFromExpress(req),
100 |           storeDelegatedToken: this.deps.storeDelegatedToken,
101 |         })
102 |         const result = await cb.handleCallback(params, config)
103 |         if (result.error) {
104 |           res.redirect(`${this.basePath}/error?msg=${encodeURIComponent(result.error)}`)
105 |           return
106 |         }
107 |         const returnTo = result.state?.returnTo
108 |         if (returnTo) {
109 |           res.redirect(returnTo)
110 |         } else {
111 |           res.set('content-type', 'text/html; charset=utf-8')
112 |           res.status(200).send(this.web.renderSuccessPage('Authorization complete. You may close this window.'))
113 |         }
114 |       } catch (err) {
115 |         Logger.warn('OAuth callback failed', err)
116 |         res.redirect(`${this.basePath}/error`)
117 |       }
118 |     })
119 | 
120 |     // POST /oauth/token
121 |     app.post(`${base}/token`, async (req: any, res: any) => {
122 |       try {
123 |         const body = req.body || {}
124 |         const state = typeof body.state === 'string' ? body.state : undefined
125 |         const code = typeof body.code === 'string' ? body.code : undefined
126 |         const providerParam = typeof body.provider === 'string' ? body.provider : undefined
127 |         const serverId = typeof body.server_id === 'string' ? body.server_id : undefined
128 |         if (!state || !code) {
129 |           res.status(400).json({ error: 'Missing state or code' })
130 |           return
131 |         }
132 |         const { config } = this.validator.resolveProvider({ provider: providerParam, serverId })
133 |         const cb = new CallbackHandler({
134 |           config: this.deps.getConfig(),
135 |           pkceManager: this.pkce,
136 |           stateManager: this.state,
137 |           baseUrl: this.getBaseUrlFromExpress(req),
138 |           storeDelegatedToken: this.deps.storeDelegatedToken,
139 |         })
140 |         const result = await cb.handleCallback(new URLSearchParams({ state, code }), config)
141 |         if (result.error) {
142 |           res.status(400).json({ error: result.error })
143 |           return
144 |         }
145 |         // For security, do not return tokens to the browser; we store server-side
146 |         res.json({ ok: true })
147 |       } catch (err) {
148 |         Logger.warn('OAuth token exchange failed', err)
149 |         res.status(500).json({ error: 'Token exchange failed' })
150 |       }
151 |     })
152 | 
153 |     // Success and error pages
154 |     app.get(`${base}/success`, (_req: any, res: any) => {
155 |       res.set('content-type', 'text/html; charset=utf-8')
156 |       res.status(200).send(this.web.renderSuccessPage())
157 |     })
158 | 
159 |     app.get(`${base}/error`, (req: any, res: any) => {
160 |       const msg = typeof req.query?.msg === 'string' ? (req.query.msg as string) : undefined
161 |       res.set('content-type', 'text/html; charset=utf-8')
162 |       res.status(200).send(this.web.renderErrorPage(msg))
163 |     })
164 |   }
165 | 
166 |   private getClientTokenFromExpress(req: any): string | undefined {
167 |     const h = (req.headers?.authorization as string) || (req.headers?.Authorization as string)
168 |     if (typeof h === 'string' && h.toLowerCase().startsWith('bearer ')) return h.slice(7)
169 |     return undefined
170 |   }
171 | 
172 |   // Cross-platform Worker-style request handler
173 |   async handleRequest(req: Request): Promise<Response> {
174 |     const url = new URL(req.url)
175 |     const path = url.pathname
176 |     if (req.method === 'GET' && path.endsWith(`${this.basePath}/authorize`)) {
177 |       try {
178 |         const providerParam = (url.searchParams.get('provider') || undefined) as string | undefined
179 |         const serverId = (url.searchParams.get('server_id') || undefined) as string | undefined
180 |         const scopesParam = (url.searchParams.get('scope') || undefined) as string | undefined
181 |         const returnTo = this.validator.validateReturnTo(url.searchParams.get('return_to'), this.getBaseUrlFromRequest(req))
182 |         const { config, providerId } = this.validator.resolveProvider({ provider: providerParam, serverId })
183 |         // Cannot reliably get Authorization header in some browser flows; ignore client token in Workers
184 |         const state = this.state.create({ provider: providerId, serverId, clientToken: undefined, returnTo })
185 |         const { challenge, method } = await this.pkce.generate(state)
186 |         const redirectUri = new URL(`${this.basePath}/callback`, this.getBaseUrlFromRequest(req)).toString()
187 |         const authUrl = new URL(config.authorization_endpoint)
188 |         authUrl.searchParams.set('response_type', 'code')
189 |         authUrl.searchParams.set('client_id', config.client_id)
190 |         authUrl.searchParams.set('redirect_uri', redirectUri)
191 |         const scope = scopesParam ?? (config.scopes ? config.scopes.join(' ') : '')
192 |         if (scope) authUrl.searchParams.set('scope', scope)
193 |         authUrl.searchParams.set('state', state)
194 |         authUrl.searchParams.set('code_challenge', challenge)
195 |         authUrl.searchParams.set('code_challenge_method', method)
196 |         return new Response(this.web.renderRedirectPage(providerId, authUrl.toString()), {
197 |           headers: { 'content-type': 'text/html; charset=utf-8' },
198 |           status: 200,
199 |         })
200 |       } catch (err) {
201 |         Logger.warn('OAuth authorize (worker) failed', err)
202 |         return new Response(this.web.renderErrorPage('Failed to start authorization'), {
203 |           headers: { 'content-type': 'text/html; charset=utf-8' },
204 |           status: 500,
205 |         })
206 |       }
207 |     }
208 | 
209 |     if (req.method === 'GET' && path.endsWith(`${this.basePath}/callback`)) {
210 |       try {
211 |         const params = new URLSearchParams(url.search)
212 |         const providerParam = url.searchParams.get('provider')
213 |         const serverId = url.searchParams.get('server_id')
214 |         const { config } = this.validator.resolveProvider({ provider: providerParam, serverId })
215 |         const cb = new CallbackHandler({
216 |           config: this.deps.getConfig(),
217 |           pkceManager: this.pkce,
218 |           stateManager: this.state,
219 |           baseUrl: this.getBaseUrlFromRequest(req),
220 |           storeDelegatedToken: this.deps.storeDelegatedToken,
221 |         })
222 |         const result = await cb.handleCallback(params, config)
223 |         if (result.error) {
224 |           return new Response(this.web.renderErrorPage(result.error), {
225 |             headers: { 'content-type': 'text/html; charset=utf-8' },
226 |             status: 400,
227 |           })
228 |         }
229 |         const returnTo = result.state?.returnTo
230 |         if (returnTo) return Response.redirect(new URL(returnTo, this.getBaseUrlFromRequest(req)).toString(), 302)
231 |         return new Response(this.web.renderSuccessPage('Authorization complete. You may close this window.'), {
232 |           headers: { 'content-type': 'text/html; charset=utf-8' },
233 |           status: 200,
234 |         })
235 |       } catch (err) {
236 |         Logger.warn('OAuth callback (worker) failed', err)
237 |         return new Response(this.web.renderErrorPage('Callback handling failed'), {
238 |           headers: { 'content-type': 'text/html; charset=utf-8' },
239 |           status: 500,
240 |         })
241 |       }
242 |     }
243 | 
244 |     if (req.method === 'POST' && path.endsWith(`${this.basePath}/token`)) {
245 |       try {
246 |         const ct = req.headers.get('content-type') || ''
247 |         let data: Record<string, string> = {}
248 |         if (ct.includes('application/json')) {
249 |           data = (await req.json()) as any
250 |         } else if (ct.includes('application/x-www-form-urlencoded')) {
251 |           data = Object.fromEntries(new URLSearchParams(await req.text())) as any
252 |         } else {
253 |           return new Response(JSON.stringify({ error: 'Unsupported content type' }), {
254 |             headers: { 'content-type': 'application/json' },
255 |             status: 415,
256 |           })
257 |         }
258 |         const state = typeof data.state === 'string' ? data.state : undefined
259 |         const code = typeof data.code === 'string' ? data.code : undefined
260 |         const providerParam = typeof data.provider === 'string' ? data.provider : undefined
261 |         const serverId = typeof data.server_id === 'string' ? data.server_id : undefined
262 |         if (!state || !code) return new Response(JSON.stringify({ error: 'Missing state or code' }), { headers: { 'content-type': 'application/json' }, status: 400 })
263 |         const { config } = this.validator.resolveProvider({ provider: providerParam, serverId })
264 |         const cb = new CallbackHandler({
265 |           config: this.deps.getConfig(),
266 |           pkceManager: this.pkce,
267 |           stateManager: this.state,
268 |           baseUrl: this.getBaseUrlFromRequest(req),
269 |           storeDelegatedToken: this.deps.storeDelegatedToken,
270 |         })
271 |         const result = await cb.handleCallback(new URLSearchParams({ state, code }), config)
272 |         if (result.error) return new Response(JSON.stringify({ error: result.error }), { headers: { 'content-type': 'application/json' }, status: 400 })
273 |         return new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } })
274 |       } catch (err) {
275 |         Logger.warn('OAuth token exchange (worker) failed', err)
276 |         return new Response(JSON.stringify({ error: 'Token exchange failed' }), { headers: { 'content-type': 'application/json' }, status: 500 })
277 |       }
278 |     }
279 | 
280 |     if (req.method === 'GET' && path.endsWith(`${this.basePath}/success`)) {
281 |       return new Response(this.web.renderSuccessPage(), { headers: { 'content-type': 'text/html; charset=utf-8' } })
282 |     }
283 |     if (req.method === 'GET' && path.endsWith(`${this.basePath}/error`)) {
284 |       const msg = new URL(req.url).searchParams.get('msg') ?? undefined
285 |       return new Response(this.web.renderErrorPage(msg || undefined), { headers: { 'content-type': 'text/html; charset=utf-8' } })
286 |     }
287 | 
288 |     return new Response('Not Found', { status: 404 })
289 |   }
290 | }
291 | 
```

--------------------------------------------------------------------------------
/docs/architecture/images/mcp_master_architecture.svg:
--------------------------------------------------------------------------------

```
  1 | <svg viewBox="0 0 1400 1000" xmlns="http://www.w3.org/2000/svg">
  2 |   <defs>
  3 |     <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
  4 |       <polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
  5 |     </marker>
  6 |     <pattern id="diagonalHatch" patternUnits="userSpaceOnUse" width="4" height="4">
  7 |       <path d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2" style="stroke:#ccc, stroke-width:1"/>
  8 |     </pattern>
  9 |     <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
 10 |       <feDropShadow dx="3" dy="3" stdDeviation="2" flood-color="#00000020"/>
 11 |     </filter>
 12 |   </defs>
 13 | 
 14 |   <!-- Background -->
 15 |   <rect width="1400" height="1000" fill="#f8fafc"/>
 16 |   
 17 |   <!-- Title -->
 18 |   <text x="700" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="bold" fill="#1e293b">
 19 |     Master MCP Server Architecture
 20 |   </text>
 21 |   <text x="700" y="55" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#64748b">
 22 |     Unified MCP Gateway with Shared OAuth &amp; Multi-Repository Module Loading
 23 |   </text>
 24 | 
 25 |   <!-- Client Layer -->
 26 |   <g id="client-layer">
 27 |     <rect x="50" y="100" width="1300" height="80" fill="#e0f2fe" stroke="#0369a1" stroke-width="2" rx="8" filter="url(#shadow)"/>
 28 |     <text x="70" y="125" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#0369a1">MCP Clients</text>
 29 |     
 30 |     <rect x="100" y="135" width="120" height="35" fill="#0284c7" rx="4"/>
 31 |     <text x="160" y="157" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">Claude Desktop</text>
 32 |     
 33 |     <rect x="240" y="135" width="120" height="35" fill="#0284c7" rx="4"/>
 34 |     <text x="300" y="157" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">Cursor IDE</text>
 35 |     
 36 |     <rect x="380" y="135" width="120" height="35" fill="#0284c7" rx="4"/>
 37 |     <text x="440" y="157" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">Custom Apps</text>
 38 |     
 39 |     <text x="600" y="152" font-family="Arial, sans-serif" font-size="12" fill="#64748b">Single MCP Connection</text>
 40 |   </g>
 41 | 
 42 |   <!-- Master MCP Server -->
 43 |   <g id="master-server">
 44 |     <rect x="50" y="220" width="1300" height="260" fill="#f1f5f9" stroke="#475569" stroke-width="2" rx="8" filter="url(#shadow)"/>
 45 |     <text x="70" y="245" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#1e293b">Master MCP Server Gateway</text>
 46 | 
 47 |     <!-- OAuth Manager -->
 48 |     <rect x="80" y="260" width="180" height="80" fill="#fef3c7" stroke="#f59e0b" stroke-width="2" rx="6"/>
 49 |     <text x="170" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#92400e">OAuth Manager</text>
 50 |     <text x="90" y="305" font-family="Arial, sans-serif" font-size="11" fill="#92400e">• Token validation</text>
 51 |     <text x="90" y="320" font-family="Arial, sans-serif" font-size="11" fill="#92400e">• Shared auth state</text>
 52 |     <text x="90" y="335" font-family="Arial, sans-serif" font-size="11" fill="#92400e">• Token forwarding</text>
 53 | 
 54 |     <!-- Capability Aggregator -->
 55 |     <rect x="280" y="260" width="180" height="80" fill="#ddd6fe" stroke="#8b5cf6" stroke-width="2" rx="6"/>
 56 |     <text x="370" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#5b21b6">Capability Aggregator</text>
 57 |     <text x="290" y="305" font-family="Arial, sans-serif" font-size="11" fill="#5b21b6">• Tool discovery</text>
 58 |     <text x="290" y="320" font-family="Arial, sans-serif" font-size="11" fill="#5b21b6">• Schema merging</text>
 59 |     <text x="290" y="335" font-family="Arial, sans-serif" font-size="11" fill="#5b21b6">• Resource listing</text>
 60 | 
 61 |     <!-- Request Router -->
 62 |     <rect x="480" y="260" width="180" height="80" fill="#dcfce7" stroke="#16a34a" stroke-width="2" rx="6"/>
 63 |     <text x="570" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#166534">Request Router</text>
 64 |     <text x="490" y="305" font-family="Arial, sans-serif" font-size="11" fill="#166534">• Tool call routing</text>
 65 |     <text x="490" y="320" font-family="Arial, sans-serif" font-size="11" fill="#166534">• Load balancing</text>
 66 |     <text x="490" y="335" font-family="Arial, sans-serif" font-size="11" fill="#166534">• Error handling</text>
 67 | 
 68 |     <!-- Module Loader -->
 69 |     <rect x="680" y="260" width="180" height="80" fill="#fee2e2" stroke="#dc2626" stroke-width="2" rx="6"/>
 70 |     <text x="770" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#991b1b">Module Loader</text>
 71 |     <text x="690" y="305" font-family="Arial, sans-serif" font-size="11" fill="#991b1b">• Git repo cloning</text>
 72 |     <text x="690" y="320" font-family="Arial, sans-serif" font-size="11" fill="#991b1b">• Package installation</text>
 73 |     <text x="690" y="335" font-family="Arial, sans-serif" font-size="11" fill="#991b1b">• Hot reloading</text>
 74 | 
 75 |     <!-- Server Manager -->
 76 |     <rect x="880" y="260" width="180" height="80" fill="#f3e8ff" stroke="#a855f7" stroke-width="2" rx="6"/>
 77 |     <text x="970" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#7c2d12">Server Manager</text>
 78 |     <text x="890" y="305" font-family="Arial, sans-serif" font-size="11" fill="#7c2d12">• Process lifecycle</text>
 79 |     <text x="890" y="320" font-family="Arial, sans-serif" font-size="11" fill="#7c2d12">• Health monitoring</text>
 80 |     <text x="890" y="335" font-family="Arial, sans-serif" font-size="11" fill="#7c2d12">• Auto-restart</text>
 81 | 
 82 |     <!-- Config Manager -->
 83 |     <rect x="1080" y="260" width="180" height="80" fill="#ecfdf5" stroke="#10b981" stroke-width="2" rx="6"/>
 84 |     <text x="1170" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#065f46">Config Manager</text>
 85 |     <text x="1090" y="305" font-family="Arial, sans-serif" font-size="11" fill="#065f46">• Server configs</text>
 86 |     <text x="1090" y="320" font-family="Arial, sans-serif" font-size="11" fill="#065f46">• Environment vars</text>
 87 |     <text x="1090" y="335" font-family="Arial, sans-serif" font-size="11" fill="#065f46">• Secret management</text>
 88 | 
 89 |     <!-- MCP Protocol Handler -->
 90 |     <rect x="280" y="360" width="700" height="60" fill="#e2e8f0" stroke="#64748b" stroke-width="2" rx="6"/>
 91 |     <text x="630" y="385" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#334155">MCP Protocol Handler (Streamable HTTP)</text>
 92 |     <text x="290" y="405" font-family="Arial, sans-serif" font-size="11" fill="#475569">Handles: list_tools, call_tool, list_resources, read_resource, subscribe, notifications</text>
 93 |   </g>
 94 | 
 95 |   <!-- Repository Sources -->
 96 |   <g id="repositories">
 97 |     <rect x="50" y="520" width="400" height="120" fill="#fffbeb" stroke="#f59e0b" stroke-width="2" rx="8" filter="url(#shadow)"/>
 98 |     <text x="70" y="545" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#92400e">Repository Sources</text>
 99 |     
100 |     <rect x="80" y="560" width="100" height="30" fill="#f59e0b" rx="4"/>
101 |     <text x="130" y="580" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">GitHub Repos</text>
102 |     
103 |     <rect x="200" y="560" width="100" height="30" fill="#f59e0b" rx="4"/>
104 |     <text x="250" y="580" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">GitLab Repos</text>
105 |     
106 |     <rect x="320" y="560" width="100" height="30" fill="#f59e0b" rx="4"/>
107 |     <text x="370" y="580" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Private Git</text>
108 | 
109 |     <rect x="80" y="600" width="100" height="30" fill="#ea580c" rx="4"/>
110 |     <text x="130" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">NPM Registry</text>
111 |     
112 |     <rect x="200" y="600" width="100" height="30" fill="#ea580c" rx="4"/>
113 |     <text x="250" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">PyPI Registry</text>
114 |     
115 |     <rect x="320" y="600" width="100" height="30" fill="#ea580c" rx="4"/>
116 |     <text x="370" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Docker Hub</text>
117 |   </g>
118 | 
119 |   <!-- Backend MCP Servers -->
120 |   <g id="backend-servers">
121 |     <rect x="500" y="520" width="850" height="180" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" rx="8" filter="url(#shadow)"/>
122 |     <text x="520" y="545" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#166534">Backend MCP Servers (Auto-loaded)</text>
123 | 
124 |     <!-- Python Servers -->
125 |     <g id="python-servers">
126 |       <text x="530" y="570" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#1e40af">Python Servers</text>
127 |       
128 |       <rect x="530" y="580" width="140" height="50" fill="#dbeafe" stroke="#3b82f6" stroke-width="1" rx="4"/>
129 |       <text x="600" y="600" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#1e40af">File System MCP</text>
130 |       <text x="600" y="615" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#1e40af">Tools: read, write, list</text>
131 | 
132 |       <rect x="690" y="580" width="140" height="50" fill="#dbeafe" stroke="#3b82f6" stroke-width="1" rx="4"/>
133 |       <text x="760" y="600" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#1e40af">Database MCP</text>
134 |       <text x="760" y="615" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#1e40af">Tools: query, insert, update</text>
135 | 
136 |       <rect x="850" y="580" width="140" height="50" fill="#dbeafe" stroke="#3b82f6" stroke-width="1" rx="4"/>
137 |       <text x="920" y="600" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#1e40af">Email MCP</text>
138 |       <text x="920" y="615" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#1e40af">Tools: send, search, read</text>
139 |     </g>
140 | 
141 |     <!-- TypeScript Servers -->
142 |     <g id="typescript-servers">
143 |       <text x="530" y="660" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#059669">TypeScript Servers</text>
144 |       
145 |       <rect x="530" y="670" width="140" height="50" fill="#d1fae5" stroke="#10b981" stroke-width="1" rx="4"/>
146 |       <text x="600" y="690" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#059669">GitHub MCP</text>
147 |       <text x="600" y="705" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#059669">Tools: create_issue, pr</text>
148 | 
149 |       <rect x="690" y="670" width="140" height="50" fill="#d1fae5" stroke="#10b981" stroke-width="1" rx="4"/>
150 |       <text x="760" y="690" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#059669">Slack MCP</text>
151 |       <text x="760" y="705" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#059669">Tools: post, search, dm</text>
152 | 
153 |       <rect x="850" y="670" width="140" height="50" fill="#d1fae5" stroke="#10b981" stroke-width="1" rx="4"/>
154 |       <text x="920" y="690" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#059669">Calendar MCP</text>
155 |       <text x="920" y="705" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#059669">Tools: schedule, list</text>
156 | 
157 |       <!-- More servers indicator -->
158 |       <rect x="1010" y="625" width="80" height="50" fill="#f3f4f6" stroke="#9ca3af" stroke-width="1" rx="4" stroke-dasharray="5,5"/>
159 |       <text x="1050" y="645" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#6b7280">More</text>
160 |       <text x="1050" y="660" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#6b7280">Servers...</text>
161 |     </g>
162 | 
163 |     <!-- Server Runtime Info -->
164 |     <rect x="1120" y="580" width="200" height="110" fill="#fef7ff" stroke="#c084fc" stroke-width="1" rx="4"/>
165 |     <text x="1220" y="600" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#7c3aed">Runtime Environment</text>
166 |     <text x="1130" y="620" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Python 3.9+ processes</text>
167 |     <text x="1130" y="635" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Node.js 18+ processes</text>
168 |     <text x="1130" y="650" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Docker containers</text>
169 |     <text x="1130" y="665" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Health checks</text>
170 |     <text x="1130" y="680" font-family="Arial, sans-serif" font-size="10" fill="#7c3aed">• Auto-scaling</text>
171 |   </g>
172 | 
173 |   <!-- Configuration -->
174 |   <g id="configuration">
175 |     <rect x="50" y="740" width="500" height="120" fill="#fef2f2" stroke="#ef4444" stroke-width="2" rx="8" filter="url(#shadow)"/>
176 |     <text x="70" y="765" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#991b1b">Configuration (master-mcp-config.yaml)</text>
177 |     
178 |     <rect x="80" y="780" width="440" height="70" fill="#fee2e2" rx="4"/>
179 |     <text x="90" y="800" font-family="Monaco, monospace" font-size="10" fill="#991b1b">servers:</text>
180 |     <text x="90" y="815" font-family="Monaco, monospace" font-size="10" fill="#991b1b">  - repo: github.com/user/filesystem-mcp</text>
181 |     <text x="90" y="830" font-family="Monaco, monospace" font-size="10" fill="#991b1b">  - repo: github.com/user/slack-mcp</text>
182 |     <text x="90" y="845" font-family="Monaco, monospace" font-size="10" fill="#991b1b">  - package: "@mcp/github-server@latest"</text>
183 |   </g>
184 | 
185 |   <!-- Hosting Options -->
186 |   <g id="hosting">
187 |     <rect x="600" y="740" width="750" height="120" fill="#f0f9ff" stroke="#0ea5e9" stroke-width="2" rx="8" filter="url(#shadow)"/>
188 |     <text x="620" y="765" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#0c4a6e">Hosting Options</text>
189 |     
190 |     <rect x="640" y="780" width="120" height="30" fill="#0ea5e9" rx="4"/>
191 |     <text x="700" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Cloudflare Workers</text>
192 |     
193 |     <rect x="780" y="780" width="80" height="30" fill="#0ea5e9" rx="4"/>
194 |     <text x="820" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Koyeb</text>
195 |     
196 |     <rect x="880" y="780" width="80" height="30" fill="#0ea5e9" rx="4"/>
197 |     <text x="920" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Render</text>
198 |     
199 |     <rect x="980" y="780" width="120" height="30" fill="#0ea5e9" rx="4"/>
200 |     <text x="1040" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Docker Compose</text>
201 |     
202 |     <rect x="1120" y="780" width="120" height="30" fill="#0ea5e9" rx="4"/>
203 |     <text x="1180" y="800" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">Kubernetes</text>
204 | 
205 |     <text x="640" y="835" font-family="Arial, sans-serif" font-size="11" fill="#0c4a6e">Scale-to-zero, OAuth, global edge distribution</text>
206 |   </g>
207 | 
208 |   <!-- Data Flow Arrows -->
209 |   <g id="arrows" marker-end="url(#arrowhead)">
210 |     <!-- Client to Master -->
211 |     <line x1="400" y1="180" x2="400" y2="220" stroke="#374151" stroke-width="2"/>
212 |     <text x="420" y="200" font-family="Arial, sans-serif" font-size="11" fill="#374151">MCP Requests</text>
213 | 
214 |     <!-- OAuth Flow -->
215 |     <line x1="170" y1="180" x2="170" y2="260" stroke="#f59e0b" stroke-width="2"/>
216 |     
217 |     <!-- Module Loading -->
218 |     <line x1="250" y1="520" x2="680" y2="340" stroke="#dc2626" stroke-width="2"/>
219 |     
220 |     <!-- Server Management -->
221 |     <line x1="970" y1="340" x2="970" y2="520" stroke="#a855f7" stroke-width="2"/>
222 |     
223 |     <!-- Capability Discovery -->
224 |     <line x1="370" y1="340" x2="700" y2="520" stroke="#8b5cf6" stroke-width="2"/>
225 |     
226 |     <!-- Request Routing -->
227 |     <line x1="570" y1="340" x2="700" y2="520" stroke="#16a34a" stroke-width="2"/>
228 |   </g>
229 | 
230 |   <!-- Legend -->
231 |   <g id="legend">
232 |     <rect x="50" y="890" width="1300" height="80" fill="#ffffff" stroke="#e2e8f0" stroke-width="1" rx="6" filter="url(#shadow)"/>
233 |     <text x="70" y="915" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#1e293b">Key Features</text>
234 |     
235 |     <text x="80" y="935" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Zero-config server integration:</tspan> Existing MCP servers work without modification</text>
236 |     <text x="80" y="950" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Shared OAuth:</tspan> Single authentication for all backend servers</text>
237 |     <text x="80" y="965" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Dynamic capability discovery:</tspan> Tools and resources auto-aggregated from all servers</text>
238 |     
239 |     <text x="600" y="935" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Multi-repository support:</tspan> Load from Git, NPM, PyPI, Docker</text>
240 |     <text x="600" y="950" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Hot reloading:</tspan> Update servers without client reconnection</text>
241 |     <text x="600" y="965" font-family="Arial, sans-serif" font-size="12" fill="#475569">• <tspan font-weight="bold">Language agnostic:</tspan> Python, TypeScript, and future language support</text>
242 |   </g>
243 | </svg>
```
Page 4/10FirstPrevNextLast