@hcu-lab.me/mcp-device 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +21 -0
- package/README.md +57 -2
- package/SECURITY.md +67 -0
- package/config/revoked-approvals.json +1 -0
- package/control-plane/config/mcp-servers.example.toml +52 -0
- package/control-plane/config/mcp-servers.toml +52 -0
- package/control-plane/contracts/device-public-contract-v1.json +733 -0
- package/control-plane/scripts/GATEWAY-LICENSE.txt +7 -0
- package/control-plane/scripts/account-http.mjs +220 -0
- package/control-plane/scripts/account-store.mjs +368 -0
- package/control-plane/scripts/admin.mjs +68 -0
- package/control-plane/scripts/auth-session.mjs +411 -0
- package/control-plane/scripts/authenticated-mcp-wrapper.mjs +1281 -0
- package/control-plane/scripts/benchmark-command.mjs +9 -0
- package/control-plane/scripts/benchmark-mcp-catalog.mjs +174 -0
- package/control-plane/scripts/catalog-budget.mjs +87 -0
- package/control-plane/scripts/check-device-public-contract.mjs +20 -0
- package/control-plane/scripts/custom-tools/index.mjs +221 -0
- package/control-plane/scripts/custom-tools/path-utils.mjs +117 -0
- package/control-plane/scripts/custom-tools/response-utils.mjs +34 -0
- package/control-plane/scripts/dashboard-http.mjs +598 -0
- package/control-plane/scripts/device-access-policy.mjs +205 -0
- package/control-plane/scripts/device-admin.mjs +43 -0
- package/control-plane/scripts/device-audit.mjs +63 -0
- package/control-plane/scripts/device-broker.mjs +1461 -0
- package/control-plane/scripts/device-execution-tool-definitions.mjs +217 -0
- package/control-plane/scripts/device-inventory.mjs +128 -0
- package/control-plane/scripts/device-pairing-http.mjs +109 -0
- package/control-plane/scripts/device-pairing-store.mjs +245 -0
- package/control-plane/scripts/device-public-contract.mjs +165 -0
- package/control-plane/scripts/device-release.mjs +95 -0
- package/control-plane/scripts/device-secure-transport.mjs +128 -0
- package/control-plane/scripts/device-store.mjs +283 -0
- package/control-plane/scripts/device-usage.mjs +657 -0
- package/control-plane/scripts/embedded-device.mjs +149 -0
- package/control-plane/scripts/enduser-ui.mjs +45 -0
- package/control-plane/scripts/external-tool-broker.mjs +179 -0
- package/control-plane/scripts/oauth-authorization-server.mjs +589 -0
- package/control-plane/scripts/operator-dashboard-http.mjs +81 -0
- package/control-plane/scripts/project-inspection.mjs +227 -0
- package/control-plane/scripts/projects/project-id.mjs +69 -0
- package/control-plane/scripts/projects/trusted-roots-projects.mjs +375 -0
- package/control-plane/scripts/prompts/index.mjs +60 -0
- package/control-plane/scripts/remote-process-sessions.mjs +78 -0
- package/control-plane/scripts/remote-tool-result.mjs +9 -0
- package/control-plane/scripts/resources/index.mjs +166 -0
- package/control-plane/scripts/runtime-profile.mjs +54 -0
- package/control-plane/scripts/shell-tool-descriptor.mjs +17 -0
- package/control-plane/scripts/skill-source-safety.mjs +46 -0
- package/control-plane/scripts/skills/README.md +70 -0
- package/control-plane/scripts/skills/_upstream_licenses/anthropic-THIRD_PARTY_NOTICES.md +405 -0
- package/control-plane/scripts/skills/_upstream_licenses/hallmark-LICENSE.txt +21 -0
- package/control-plane/scripts/skills/_upstream_licenses/ponytail-LICENSE.txt +21 -0
- package/control-plane/scripts/skills/_upstream_licenses/stitch-skills-LICENSE.txt +201 -0
- package/control-plane/scripts/skills/_upstream_licenses/superpowers-LICENSE.txt +21 -0
- package/control-plane/scripts/skills/algorithmic-art/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/algorithmic-art/SKILL.md +405 -0
- package/control-plane/scripts/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/control-plane/scripts/skills/algorithmic-art/templates/viewer.html +599 -0
- package/control-plane/scripts/skills/brainstorming/SKILL.md +285 -0
- package/control-plane/scripts/skills/brainstorming/scripts/frame-template.html +213 -0
- package/control-plane/scripts/skills/brainstorming/scripts/helper.js +167 -0
- package/control-plane/scripts/skills/brainstorming/scripts/server.cjs +723 -0
- package/control-plane/scripts/skills/brainstorming/scripts/start-server.sh +209 -0
- package/control-plane/scripts/skills/brainstorming/scripts/stop-server.sh +120 -0
- package/control-plane/scripts/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/control-plane/scripts/skills/brainstorming/visual-companion.md +299 -0
- package/control-plane/scripts/skills/brand-guidelines/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/brand-guidelines/SKILL.md +73 -0
- package/control-plane/scripts/skills/claude-api/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/claude-api/SKILL.md +570 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/README.md +362 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/batches.md +14 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/files-api.md +25 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/streaming.md +28 -0
- package/control-plane/scripts/skills/claude-api/csharp/claude-api/tool-use.md +164 -0
- package/control-plane/scripts/skills/claude-api/curl/examples.md +256 -0
- package/control-plane/scripts/skills/claude-api/curl/managed-agents.md +371 -0
- package/control-plane/scripts/skills/claude-api/go/claude-api/README.md +188 -0
- package/control-plane/scripts/skills/claude-api/go/claude-api/files-api.md +23 -0
- package/control-plane/scripts/skills/claude-api/go/claude-api/streaming.md +43 -0
- package/control-plane/scripts/skills/claude-api/go/claude-api/tool-use.md +220 -0
- package/control-plane/scripts/skills/claude-api/go/managed-agents/README.md +564 -0
- package/control-plane/scripts/skills/claude-api/java/claude-api/README.md +238 -0
- package/control-plane/scripts/skills/claude-api/java/claude-api/files-api.md +27 -0
- package/control-plane/scripts/skills/claude-api/java/claude-api/streaming.md +24 -0
- package/control-plane/scripts/skills/claude-api/java/claude-api/tool-use.md +228 -0
- package/control-plane/scripts/skills/claude-api/java/managed-agents/README.md +443 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/README.md +174 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/batches.md +15 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/files-api.md +13 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/streaming.md +27 -0
- package/control-plane/scripts/skills/claude-api/php/claude-api/tool-use.md +253 -0
- package/control-plane/scripts/skills/claude-api/php/managed-agents/README.md +443 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/README.md +565 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/batches.md +198 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/files-api.md +170 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/sdk-upgrade.md +286 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/streaming.md +179 -0
- package/control-plane/scripts/skills/claude-api/python/claude-api/tool-use.md +627 -0
- package/control-plane/scripts/skills/claude-api/python/managed-agents/README.md +335 -0
- package/control-plane/scripts/skills/claude-api/ruby/claude-api/README.md +134 -0
- package/control-plane/scripts/skills/claude-api/ruby/claude-api/streaming.md +16 -0
- package/control-plane/scripts/skills/claude-api/ruby/claude-api/tool-use.md +41 -0
- package/control-plane/scripts/skills/claude-api/ruby/managed-agents/README.md +394 -0
- package/control-plane/scripts/skills/claude-api/shared/admin-api.md +179 -0
- package/control-plane/scripts/skills/claude-api/shared/agent-design.md +101 -0
- package/control-plane/scripts/skills/claude-api/shared/anthropic-cli.md +269 -0
- package/control-plane/scripts/skills/claude-api/shared/claude-platform-on-aws.md +59 -0
- package/control-plane/scripts/skills/claude-api/shared/cost-optimization.md +233 -0
- package/control-plane/scripts/skills/claude-api/shared/error-codes.md +262 -0
- package/control-plane/scripts/skills/claude-api/shared/live-sources.md +166 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-api-reference.md +455 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-client-patterns.md +220 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-core.md +359 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-environments.md +226 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-events.md +290 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-memory.md +201 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-multiagent.md +260 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-onboarding.md +82 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-outcomes.md +108 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-overview.md +77 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-scheduled-deployments.md +156 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +282 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-tools.md +504 -0
- package/control-plane/scripts/skills/claude-api/shared/managed-agents-webhooks.md +143 -0
- package/control-plane/scripts/skills/claude-api/shared/model-migration.md +1872 -0
- package/control-plane/scripts/skills/claude-api/shared/models.md +142 -0
- package/control-plane/scripts/skills/claude-api/shared/platform-availability.md +52 -0
- package/control-plane/scripts/skills/claude-api/shared/prompt-audit.md +223 -0
- package/control-plane/scripts/skills/claude-api/shared/prompt-caching.md +292 -0
- package/control-plane/scripts/skills/claude-api/shared/token-counting.md +56 -0
- package/control-plane/scripts/skills/claude-api/shared/tool-use-concepts.md +526 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/README.md +405 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/batches.md +106 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/files-api.md +98 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/streaming.md +178 -0
- package/control-plane/scripts/skills/claude-api/typescript/claude-api/tool-use.md +586 -0
- package/control-plane/scripts/skills/claude-api/typescript/managed-agents/README.md +359 -0
- package/control-plane/scripts/skills/dispatching-parallel-agents/SKILL.md +167 -0
- package/control-plane/scripts/skills/enhance-prompt/README.md +34 -0
- package/control-plane/scripts/skills/enhance-prompt/SKILL.md +204 -0
- package/control-plane/scripts/skills/enhance-prompt/references/KEYWORDS.md +114 -0
- package/control-plane/scripts/skills/executing-plans/SKILL.md +371 -0
- package/control-plane/scripts/skills/executing-plans/scripts/task-done +52 -0
- package/control-plane/scripts/skills/executing-plans/scripts/task-start +28 -0
- package/control-plane/scripts/skills/extract-design-md/SKILL.md +358 -0
- package/control-plane/scripts/skills/extract-design-md/examples/DESIGN.md +210 -0
- package/control-plane/scripts/skills/extract-design-md/references/angular.md +122 -0
- package/control-plane/scripts/skills/extract-design-md/references/plain-css.md +161 -0
- package/control-plane/scripts/skills/extract-design-md/references/react-tailwind.md +157 -0
- package/control-plane/scripts/skills/extract-design-md/references/svelte.md +101 -0
- package/control-plane/scripts/skills/extract-design-md/references/vue.md +107 -0
- package/control-plane/scripts/skills/finishing-a-development-branch/SKILL.md +225 -0
- package/control-plane/scripts/skills/frontend-design/LICENSE.txt +177 -0
- package/control-plane/scripts/skills/frontend-design/SKILL.md +71 -0
- package/control-plane/scripts/skills/hallmark/SKILL.md +558 -0
- package/control-plane/scripts/skills/hallmark/references/anti-patterns.md +418 -0
- package/control-plane/scripts/skills/hallmark/references/assets.md +406 -0
- package/control-plane/scripts/skills/hallmark/references/color.md +95 -0
- package/control-plane/scripts/skills/hallmark/references/component-cookbook.md +263 -0
- package/control-plane/scripts/skills/hallmark/references/components/c1-outlined-chip.md +12 -0
- package/control-plane/scripts/skills/hallmark/references/components/c2-inline-form-as-cta.md +16 -0
- package/control-plane/scripts/skills/hallmark/references/components/c3-typographic-link.md +8 -0
- package/control-plane/scripts/skills/hallmark/references/components/c4-sticky-bottom-bar.md +16 -0
- package/control-plane/scripts/skills/hallmark/references/components/f1-bento-grid.md +20 -0
- package/control-plane/scripts/skills/hallmark/references/components/f2-sticky-scroll-stack.md +20 -0
- package/control-plane/scripts/skills/hallmark/references/components/f3-tabular-spec-sheet.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/f4-step-sequence.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/f5-annotated-screenshot.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/f6-product-card-grid.md +41 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft1-mast-headed.md +13 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft2-inline-rule-single-line.md +10 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft3-index-style-category-list.md +12 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft4-dense-typographic.md +10 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft5-statement.md +21 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft6-letter-close.md +19 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft7-newsletter-first.md +27 -0
- package/control-plane/scripts/skills/hallmark/references/components/ft8-marquee-scroll.md +25 -0
- package/control-plane/scripts/skills/hallmark/references/components/h1-marquee.md +15 -0
- package/control-plane/scripts/skills/hallmark/references/components/h2-split-diptych.md +15 -0
- package/control-plane/scripts/skills/hallmark/references/components/h3-quote-led.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/h4-stat-led.md +14 -0
- package/control-plane/scripts/skills/hallmark/references/components/h5-letter-hero.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/h6-photographic-fold.md +16 -0
- package/control-plane/scripts/skills/hallmark/references/components/h7-demo-video-clipped-by-viewport-edge.md +27 -0
- package/control-plane/scripts/skills/hallmark/references/components/h8-mockup-split-browser-framed.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/components/h9-custom-illustration-centerpiece.md +27 -0
- package/control-plane/scripts/skills/hallmark/references/components/n1-wordmark-2-links.md +12 -0
- package/control-plane/scripts/skills/hallmark/references/components/n10-floating-on-scroll-morph.md +19 -0
- package/control-plane/scripts/skills/hallmark/references/components/n11-mega-menu.md +40 -0
- package/control-plane/scripts/skills/hallmark/references/components/n12-banner-retract.md +34 -0
- package/control-plane/scripts/skills/hallmark/references/components/n13-inline-cmdk-pill.md +39 -0
- package/control-plane/scripts/skills/hallmark/references/components/n1b-saas-three-section.md +35 -0
- package/control-plane/scripts/skills/hallmark/references/components/n2-floating-chip.md +14 -0
- package/control-plane/scripts/skills/hallmark/references/components/n3-side-rail.md +14 -0
- package/control-plane/scripts/skills/hallmark/references/components/n4-hidden-behind-k.md +9 -0
- package/control-plane/scripts/skills/hallmark/references/components/n5-floating-pill.md +28 -0
- package/control-plane/scripts/skills/hallmark/references/components/n6-newspaper-masthead.md +24 -0
- package/control-plane/scripts/skills/hallmark/references/components/n7-brutal-slab.md +22 -0
- package/control-plane/scripts/skills/hallmark/references/components/n8-terminal-command.md +21 -0
- package/control-plane/scripts/skills/hallmark/references/components/n9-edge-aligned-minimal.md +17 -0
- package/control-plane/scripts/skills/hallmark/references/components/s1-left-margin-numbered.md +15 -0
- package/control-plane/scripts/skills/hallmark/references/components/s2-hanging.md +13 -0
- package/control-plane/scripts/skills/hallmark/references/components/s3-sticky-pinned.md +19 -0
- package/control-plane/scripts/skills/hallmark/references/components/s4-inline-no-break.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/s5-bottom-anchored.md +13 -0
- package/control-plane/scripts/skills/hallmark/references/components/t1-pull-quote-with-marginalia.md +12 -0
- package/control-plane/scripts/skills/hallmark/references/components/t2-logo-wall-hairline.md +19 -0
- package/control-plane/scripts/skills/hallmark/references/components/t3-single-huge-quote.md +11 -0
- package/control-plane/scripts/skills/hallmark/references/components/t4-numbered-stat-strip.md +14 -0
- package/control-plane/scripts/skills/hallmark/references/contract.md +25 -0
- package/control-plane/scripts/skills/hallmark/references/copy.md +182 -0
- package/control-plane/scripts/skills/hallmark/references/custom-craft.md +626 -0
- package/control-plane/scripts/skills/hallmark/references/custom-theme.md +367 -0
- package/control-plane/scripts/skills/hallmark/references/design-md.md +116 -0
- package/control-plane/scripts/skills/hallmark/references/export-formats.md +329 -0
- package/control-plane/scripts/skills/hallmark/references/floating-nav.md +89 -0
- package/control-plane/scripts/skills/hallmark/references/genres/atmospheric.md +69 -0
- package/control-plane/scripts/skills/hallmark/references/genres/editorial.md +72 -0
- package/control-plane/scripts/skills/hallmark/references/genres/modern-minimal.md +71 -0
- package/control-plane/scripts/skills/hallmark/references/genres/playful.md +70 -0
- package/control-plane/scripts/skills/hallmark/references/hero-enrichment.md +475 -0
- package/control-plane/scripts/skills/hallmark/references/imagery-kit.md +170 -0
- package/control-plane/scripts/skills/hallmark/references/interaction-and-states.md +208 -0
- package/control-plane/scripts/skills/hallmark/references/layout-and-space.md +112 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/01-bento-grid.md +35 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/02-long-document.md +34 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/03-marquee-hero.md +31 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/04-stat-led.md +34 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/05-workbench.md +32 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/06-conversational-faq.md +33 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/07-manifesto.md +32 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/08-photographic.md +34 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/09-quote-led.md +32 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/10-specimen.md +32 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/11-catalogue.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/12-letter.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/13-index-first.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/14-narrative-workflow.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/15-split-studio.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/17-type-specimen.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/18-portfolio-grid.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/19-map-diagram.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/20-ecosystem-index.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures/21-component-playground.md +23 -0
- package/control-plane/scripts/skills/hallmark/references/macrostructures.md +89 -0
- package/control-plane/scripts/skills/hallmark/references/microinteractions.md +258 -0
- package/control-plane/scripts/skills/hallmark/references/motion.md +109 -0
- package/control-plane/scripts/skills/hallmark/references/preview-examples.md +49 -0
- package/control-plane/scripts/skills/hallmark/references/responsive.md +138 -0
- package/control-plane/scripts/skills/hallmark/references/slop-test.md +192 -0
- package/control-plane/scripts/skills/hallmark/references/structure.md +160 -0
- package/control-plane/scripts/skills/hallmark/references/study.md +509 -0
- package/control-plane/scripts/skills/hallmark/references/themes/carnival.md +301 -0
- package/control-plane/scripts/skills/hallmark/references/themes/cobalt.md +146 -0
- package/control-plane/scripts/skills/hallmark/references/themes/grid.md +161 -0
- package/control-plane/scripts/skills/hallmark/references/themes/hum.md +403 -0
- package/control-plane/scripts/skills/hallmark/references/themes/lumen.md +478 -0
- package/control-plane/scripts/skills/hallmark/references/typography.md +243 -0
- package/control-plane/scripts/skills/hallmark/references/upstream/recipes.md +186 -0
- package/control-plane/scripts/skills/hallmark/references/upstream/study-examples.md +176 -0
- package/control-plane/scripts/skills/hallmark/references/upstream/tokens.css +1221 -0
- package/control-plane/scripts/skills/hallmark/references/verbs/audit.md +25 -0
- package/control-plane/scripts/skills/hallmark/references/verbs/redesign.md +269 -0
- package/control-plane/scripts/skills/human-comms/SKILL.md +29 -0
- package/control-plane/scripts/skills/index.mjs +432 -0
- package/control-plane/scripts/skills/internal-comms/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/internal-comms/SKILL.md +32 -0
- package/control-plane/scripts/skills/internal-comms/examples/3p-updates.md +47 -0
- package/control-plane/scripts/skills/internal-comms/examples/company-newsletter.md +65 -0
- package/control-plane/scripts/skills/internal-comms/examples/faq-answers.md +30 -0
- package/control-plane/scripts/skills/internal-comms/examples/general-comms.md +16 -0
- package/control-plane/scripts/skills/mcp-adapter.mjs +94 -0
- package/control-plane/scripts/skills/mcp-builder/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/mcp-builder/SKILL.md +236 -0
- package/control-plane/scripts/skills/mcp-builder/reference/evaluation.md +602 -0
- package/control-plane/scripts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/control-plane/scripts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/control-plane/scripts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/control-plane/scripts/skills/mcp-builder/scripts/connections.py +151 -0
- package/control-plane/scripts/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/control-plane/scripts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/control-plane/scripts/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/control-plane/scripts/skills/ponytail/SKILL.md +120 -0
- package/control-plane/scripts/skills/ponytail-audit/SKILL.md +41 -0
- package/control-plane/scripts/skills/ponytail-debt/SKILL.md +44 -0
- package/control-plane/scripts/skills/ponytail-gain/SKILL.md +50 -0
- package/control-plane/scripts/skills/ponytail-help/SKILL.md +71 -0
- package/control-plane/scripts/skills/ponytail-review/SKILL.md +57 -0
- package/control-plane/scripts/skills/receiving-code-review/SKILL.md +205 -0
- package/control-plane/scripts/skills/requesting-code-review/SKILL.md +95 -0
- package/control-plane/scripts/skills/requesting-code-review/code-reviewer.md +198 -0
- package/control-plane/scripts/skills/skill-creator/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/skill-creator/SKILL.md +485 -0
- package/control-plane/scripts/skills/skill-creator/agents/analyzer.md +274 -0
- package/control-plane/scripts/skills/skill-creator/agents/comparator.md +202 -0
- package/control-plane/scripts/skills/skill-creator/agents/grader.md +223 -0
- package/control-plane/scripts/skills/skill-creator/assets/eval_review.html +146 -0
- package/control-plane/scripts/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/control-plane/scripts/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/control-plane/scripts/skills/skill-creator/references/schemas.md +430 -0
- package/control-plane/scripts/skills/skill-creator/scripts/__init__.py +0 -0
- package/control-plane/scripts/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/control-plane/scripts/skills/skill-creator/scripts/generate_report.py +326 -0
- package/control-plane/scripts/skills/skill-creator/scripts/improve_description.py +247 -0
- package/control-plane/scripts/skills/skill-creator/scripts/package_skill.py +136 -0
- package/control-plane/scripts/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/control-plane/scripts/skills/skill-creator/scripts/run_eval.py +310 -0
- package/control-plane/scripts/skills/skill-creator/scripts/run_loop.py +328 -0
- package/control-plane/scripts/skills/skill-creator/scripts/utils.py +47 -0
- package/control-plane/scripts/skills/slack-gif-creator/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/slack-gif-creator/SKILL.md +254 -0
- package/control-plane/scripts/skills/slack-gif-creator/core/easing.py +234 -0
- package/control-plane/scripts/skills/slack-gif-creator/core/frame_composer.py +176 -0
- package/control-plane/scripts/skills/slack-gif-creator/core/gif_builder.py +269 -0
- package/control-plane/scripts/skills/slack-gif-creator/core/validators.py +136 -0
- package/control-plane/scripts/skills/slack-gif-creator/requirements.txt +4 -0
- package/control-plane/scripts/skills/sources.json +401 -0
- package/control-plane/scripts/skills/sources.lock.json +416 -0
- package/control-plane/scripts/skills/subagent-driven-development/SKILL.md +570 -0
- package/control-plane/scripts/skills/subagent-driven-development/implementer-prompt.md +154 -0
- package/control-plane/scripts/skills/subagent-driven-development/re-review-prompt.md +115 -0
- package/control-plane/scripts/skills/subagent-driven-development/scripts/review-package +53 -0
- package/control-plane/scripts/skills/subagent-driven-development/scripts/sdd-workspace +82 -0
- package/control-plane/scripts/skills/subagent-driven-development/scripts/task-brief +43 -0
- package/control-plane/scripts/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
- package/control-plane/scripts/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/control-plane/scripts/skills/systematic-debugging/SKILL.md +283 -0
- package/control-plane/scripts/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/control-plane/scripts/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/control-plane/scripts/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/control-plane/scripts/skills/systematic-debugging/find-polluter.sh +72 -0
- package/control-plane/scripts/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/control-plane/scripts/skills/systematic-debugging/test-academic.md +14 -0
- package/control-plane/scripts/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/control-plane/scripts/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/control-plane/scripts/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/control-plane/scripts/skills/test-driven-development/SKILL.md +330 -0
- package/control-plane/scripts/skills/test-driven-development/writing-good-tests.md +198 -0
- package/control-plane/scripts/skills/theme-factory/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/theme-factory/SKILL.md +59 -0
- package/control-plane/scripts/skills/theme-factory/theme-showcase.pdf +0 -0
- package/control-plane/scripts/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/desert-rose.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/golden-hour.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/control-plane/scripts/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/control-plane/scripts/skills/using-git-worktrees/SKILL.md +167 -0
- package/control-plane/scripts/skills/verification-before-completion/SKILL.md +120 -0
- package/control-plane/scripts/skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/web-artifacts-builder/SKILL.md +74 -0
- package/control-plane/scripts/skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/control-plane/scripts/skills/web-artifacts-builder/scripts/init-artifact.sh +322 -0
- package/control-plane/scripts/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/control-plane/scripts/skills/webapp-testing/LICENSE.txt +202 -0
- package/control-plane/scripts/skills/webapp-testing/SKILL.md +96 -0
- package/control-plane/scripts/skills/webapp-testing/examples/console_logging.py +35 -0
- package/control-plane/scripts/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/control-plane/scripts/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/control-plane/scripts/skills/webapp-testing/scripts/with_server.py +106 -0
- package/control-plane/scripts/skills/writing-plans/SKILL.md +192 -0
- package/control-plane/scripts/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/control-plane/scripts/skills/writing-skills/SKILL.md +681 -0
- package/control-plane/scripts/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/control-plane/scripts/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/control-plane/scripts/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/control-plane/scripts/skills/writing-skills/persuasion-principles.md +187 -0
- package/control-plane/scripts/skills/writing-skills/render-graphs.js +169 -0
- package/control-plane/scripts/skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/control-plane/scripts/smoke-mcp-schemas.mjs +30 -0
- package/control-plane/scripts/surface-config.mjs +45 -0
- package/control-plane/scripts/sync-skills.mjs +370 -0
- package/control-plane/scripts/tool-metrics.mjs +74 -0
- package/control-plane/scripts/tool-risk.mjs +156 -0
- package/control-plane/scripts/tool-surface-stability.mjs +38 -0
- package/control-plane/scripts/upstreams/catalog-cache.mjs +28 -0
- package/control-plane/scripts/upstreams/config.mjs +158 -0
- package/control-plane/scripts/upstreams/diagnostics.mjs +21 -0
- package/control-plane/scripts/upstreams/http-client.mjs +67 -0
- package/control-plane/scripts/upstreams/manager.mjs +535 -0
- package/control-plane/scripts/upstreams/names.mjs +55 -0
- package/control-plane/scripts/upstreams/presets.mjs +60 -0
- package/control-plane/scripts/upstreams/resource-uri.mjs +29 -0
- package/control-plane/scripts/upstreams/stdio-client.mjs +73 -0
- package/control-plane/scripts/workspace-registry.mjs +147 -0
- package/dist/context/authorization.d.ts +5 -0
- package/dist/context/authorization.js +70 -0
- package/dist/context/capture.d.ts +46 -0
- package/dist/context/capture.js +187 -0
- package/dist/context/cli.d.ts +47 -0
- package/dist/context/cli.js +287 -0
- package/dist/context/device-adapter.d.ts +102 -0
- package/dist/context/device-adapter.js +79 -0
- package/dist/context/gateway.d.ts +354 -0
- package/dist/context/gateway.js +50 -0
- package/dist/context/graph.d.ts +63 -0
- package/dist/context/graph.js +214 -0
- package/dist/context/importer.d.ts +8 -0
- package/dist/context/importer.js +69 -0
- package/dist/context/indexer.d.ts +14 -0
- package/dist/context/indexer.js +57 -0
- package/dist/context/migrations/index.d.ts +4 -0
- package/dist/context/migrations/index.js +87 -0
- package/dist/context/redact.d.ts +15 -0
- package/dist/context/redact.js +43 -0
- package/dist/context/repositories.d.ts +9 -0
- package/dist/context/repositories.js +45 -0
- package/dist/context/search.d.ts +63 -0
- package/dist/context/search.js +207 -0
- package/dist/context/service.d.ts +278 -0
- package/dist/context/service.js +160 -0
- package/dist/context/store.d.ts +134 -0
- package/dist/context/store.js +722 -0
- package/dist/context/text.d.ts +4 -0
- package/dist/context/text.js +14 -0
- package/dist/context/tool-contract.d.ts +534 -0
- package/dist/context/tool-contract.js +45 -0
- package/dist/device/core.d.ts +29 -0
- package/dist/device/core.js +66 -0
- package/dist/device/device.d.ts +5 -1
- package/dist/device/device.js +87 -39
- package/dist/device/gateway-channel.d.ts +8 -1
- package/dist/device/gateway-channel.js +51 -3
- package/dist/device/gateway-tool-adapter.d.ts +9 -0
- package/dist/device/gateway-tool-adapter.js +20 -1
- package/dist/device/official-trust.d.ts +12 -0
- package/dist/device/official-trust.js +25 -1
- package/dist/device/runtime-owner.js +9 -1
- package/dist/distribution.d.ts +4 -0
- package/dist/distribution.js +8 -0
- package/dist/log-palace/cli.d.ts +7 -0
- package/dist/log-palace/cli.js +89 -0
- package/dist/log-palace/formats.d.ts +26 -0
- package/dist/log-palace/formats.js +126 -0
- package/dist/log-palace/store.d.ts +129 -0
- package/dist/log-palace/store.js +856 -0
- package/dist/mcp-device.js +27 -5
- package/dist/npm-scripts/remote.js +1 -0
- package/dist/platform/activity-journal.d.ts +16 -0
- package/dist/platform/activity-journal.js +68 -0
- package/dist/platform/cli.d.ts +1 -0
- package/dist/platform/cli.js +300 -0
- package/dist/platform/coordination-lock.d.ts +45 -0
- package/dist/platform/coordination-lock.js +233 -0
- package/dist/platform/device-runtime.d.ts +16 -0
- package/dist/platform/device-runtime.js +83 -0
- package/dist/platform/knowledge-contract.d.ts +46 -0
- package/dist/platform/knowledge-contract.js +1 -0
- package/dist/platform/knowledge-mcp.d.ts +29 -0
- package/dist/platform/knowledge-mcp.js +95 -0
- package/dist/platform/knowledge-router.d.ts +64 -0
- package/dist/platform/knowledge-router.js +320 -0
- package/dist/platform/plugin-child.d.ts +1 -0
- package/dist/platform/plugin-child.js +115 -0
- package/dist/platform/plugin-contract.d.ts +76 -0
- package/dist/platform/plugin-contract.js +53 -0
- package/dist/platform/plugin-driver.d.ts +8 -0
- package/dist/platform/plugin-driver.js +443 -0
- package/dist/platform/plugin-host.d.ts +88 -0
- package/dist/platform/plugin-host.js +817 -0
- package/dist/platform/plugin-policy.d.ts +3 -0
- package/dist/platform/plugin-policy.js +29 -0
- package/dist/platform/plugin-services.d.ts +14 -0
- package/dist/platform/plugin-services.js +32 -0
- package/dist/platform/process-identity.d.ts +9 -0
- package/dist/platform/process-identity.js +68 -0
- package/dist/platform/runtime-lease.d.ts +4 -0
- package/dist/platform/runtime-lease.js +58 -0
- package/dist/platform/runtime-root.d.ts +9 -0
- package/dist/platform/runtime-root.js +61 -0
- package/dist/platform/state-backup.d.ts +92 -0
- package/dist/platform/state-backup.js +399 -0
- package/dist/plugins/reference/plugin.d.ts +3 -0
- package/dist/plugins/reference/plugin.js +13 -0
- package/dist/server.js +5 -1
- package/dist/terminal-manager.d.ts +7 -0
- package/dist/terminal-manager.js +10 -0
- package/dist/tool-dispatcher.js +5 -1
- package/dist/tools/improved-process-tools.js +4 -0
- package/dist/types.d.ts +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +29 -6
- package/skills/mcp-device-context/SKILL.md +40 -0
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv';
|
|
8
|
+
import express from 'express';
|
|
9
|
+
import { requireBearerAuth } from '@modelcontextprotocol/express';
|
|
10
|
+
import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest } from '@modelcontextprotocol/node';
|
|
11
|
+
import { createMcpHandler, getOAuthProtectedResourceMetadataUrl, isInitializeRequest, isLegacyRequest, Server } from '@modelcontextprotocol/server';
|
|
12
|
+
import { getRuntimeProfile } from './runtime-profile.mjs';
|
|
13
|
+
import { applyToolRisk, assertToolAllowedForProfile, shouldExposeToolForProfile } from './tool-risk.mjs';
|
|
14
|
+
import { listRepoResources, listRepoResourceTemplates, readRepoResource } from './resources/index.mjs';
|
|
15
|
+
import { getRepoPrompt, listRepoPrompts } from './prompts/index.mjs';
|
|
16
|
+
import { loadSurfaceConfig } from './surface-config.mjs';
|
|
17
|
+
import { LOCAL_COLLISION_TOOL_NAMES, workspaceCatalogChanges } from './tool-surface-stability.mjs';
|
|
18
|
+
import { createSkillRegistry } from './skills/index.mjs';
|
|
19
|
+
import { registerSkillsExtension } from './skills/mcp-adapter.mjs';
|
|
20
|
+
import { createExternalToolBroker } from './external-tool-broker.mjs';
|
|
21
|
+
import { createExternalMcpManager } from './upstreams/manager.mjs';
|
|
22
|
+
import { normalizeExternalMcpConfig } from './upstreams/config.mjs';
|
|
23
|
+
import { isExternalResourceUri } from './upstreams/resource-uri.mjs';
|
|
24
|
+
import { FILESYSTEM_TOOL_DEFINITIONS, filesystemToolMeta, listProcessTools, shellExecuteToolMeta } from './device-execution-tool-definitions.mjs';
|
|
25
|
+
import { callCustomTool, isLocalCustomTool, listCustomTools } from './custom-tools/index.mjs';
|
|
26
|
+
import {
|
|
27
|
+
AUTH_SUPPORTED_SCOPES,
|
|
28
|
+
AccountAuthProvider,
|
|
29
|
+
SQLiteAuthState,
|
|
30
|
+
installAuthorizationGateRoutes,
|
|
31
|
+
isStaticBearerAuthorization,
|
|
32
|
+
shouldCreateTransportForRequest,
|
|
33
|
+
shouldUseStatefulSessionTransport
|
|
34
|
+
} from './auth-session.mjs';
|
|
35
|
+
import { createOAuthAuthorizationServerRouter } from './oauth-authorization-server.mjs';
|
|
36
|
+
import { buildToolMetric, createToolMetricsRecorder } from './tool-metrics.mjs';
|
|
37
|
+
import { createRemoteProcessSessionRegistry } from './remote-process-sessions.mjs';
|
|
38
|
+
import { normalizeRemoteFilesystemResult } from './remote-tool-result.mjs';
|
|
39
|
+
import { createDeviceBroker } from './device-broker.mjs';
|
|
40
|
+
import { createDeviceReleaseProvider } from './device-release.mjs';
|
|
41
|
+
import { loadDeviceInnerTlsConfig } from './device-secure-transport.mjs';
|
|
42
|
+
import { listDevicesToolDefinition, paginateDeviceInventory } from './device-inventory.mjs';
|
|
43
|
+
import { installDevicePairingRoutes } from './device-pairing-http.mjs';
|
|
44
|
+
import { createDevicePairingStore } from './device-pairing-store.mjs';
|
|
45
|
+
import { createDeviceStore } from './device-store.mjs';
|
|
46
|
+
import { createDeviceUsageStore } from './device-usage.mjs';
|
|
47
|
+
import { callerAuditId, createDeviceAccessPolicy } from './device-access-policy.mjs';
|
|
48
|
+
import { createDeviceAuditRecorder } from './device-audit.mjs';
|
|
49
|
+
import { createAccountStore } from './account-store.mjs';
|
|
50
|
+
import { installAccountRoutes } from './account-http.mjs';
|
|
51
|
+
import { installDashboardRoutes } from './dashboard-http.mjs';
|
|
52
|
+
import { installOperatorDashboardRoutes } from './operator-dashboard-http.mjs';
|
|
53
|
+
import { createEmbeddedDeviceController } from './embedded-device.mjs';
|
|
54
|
+
import { findUnifiedMcpConfigPath } from './projects/trusted-roots-projects.mjs';
|
|
55
|
+
import {
|
|
56
|
+
classifyWorkspaceChange,
|
|
57
|
+
createWorkspaceRegistry
|
|
58
|
+
} from './workspace-registry.mjs';
|
|
59
|
+
|
|
60
|
+
export function resolveServerConfigPath({ configPath, assetRoot, packageRoot, env = {} }) {
|
|
61
|
+
return path.resolve(configPath || findUnifiedMcpConfigPath(env, packageRoot) || path.join(assetRoot, 'config', 'mcp-servers.toml'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One account/control plane plus one mandatory shared Device Core.
|
|
66
|
+
* All operational paths and environment are explicit; importing this module has no startup side effects.
|
|
67
|
+
*/
|
|
68
|
+
export async function startServerRuntime(options = {}) {
|
|
69
|
+
const env = { ...(options.env || {}) };
|
|
70
|
+
const core = options.core;
|
|
71
|
+
const identity = options.identity;
|
|
72
|
+
if (!core || typeof core.invoke !== 'function' || !Array.isArray(core.capabilities)) {
|
|
73
|
+
throw new Error('Server mode requires an embedded shared Device Core.');
|
|
74
|
+
}
|
|
75
|
+
if (!identity || typeof identity.loadOrCreate !== 'function' || typeof identity.signChallenge !== 'function') {
|
|
76
|
+
throw new Error('Server mode requires the normal Device identity and proof signer.');
|
|
77
|
+
}
|
|
78
|
+
if (!options.stateRoot) throw new Error('Server stateRoot must be explicit.');
|
|
79
|
+
const startupCleanups = [];
|
|
80
|
+
let resourcesClosed = false;
|
|
81
|
+
let coreClosed = false;
|
|
82
|
+
async function closeOwnedCore() {
|
|
83
|
+
if (options.closeCore === false || coreClosed) return;
|
|
84
|
+
await core.close();
|
|
85
|
+
coreClosed = true;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const assetRoot = path.resolve(options.assetRoot || path.join(path.dirname(fileURLToPath(import.meta.url)), '..'));
|
|
89
|
+
const packageRoot = path.resolve(options.packageRoot || path.join(assetRoot, '..'));
|
|
90
|
+
const validatorProvider = new AjvJsonSchemaValidator();
|
|
91
|
+
const contextDefinitions = options.contextTools ?? (await import(pathToFileURL(path.join(packageRoot, 'dist', 'context', 'tool-contract.js')).href)).buildContextCatalog();
|
|
92
|
+
const contextTools = new Map(contextDefinitions.map(definition => {
|
|
93
|
+
const inputSchema = definition.name === 'local_wiki'
|
|
94
|
+
? { ...definition.inputSchema, properties: { ...definition.inputSchema.properties, device_id: { type: 'string', minLength: 1 } }, required: [...(definition.inputSchema.required || []), 'device_id'] }
|
|
95
|
+
: definition.inputSchema;
|
|
96
|
+
const tool = { name: definition.name, description: definition.description, inputSchema, annotations: definition.annotations };
|
|
97
|
+
return [tool.name, { tool, validate: validatorProvider.getValidator(inputSchema) }];
|
|
98
|
+
}));
|
|
99
|
+
const extraTools = new Map();
|
|
100
|
+
for (const extension of options.extraTools || []) {
|
|
101
|
+
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(String(extension.name || '')) || typeof extension.handler !== 'function') {
|
|
102
|
+
throw new Error('Extension tools require a valid name, schema and handler.');
|
|
103
|
+
}
|
|
104
|
+
if (LOCAL_COLLISION_TOOL_NAMES.has(extension.name) || contextTools.has(extension.name) || extraTools.has(extension.name)) {
|
|
105
|
+
throw new Error('Duplicate platform tool: ' + extension.name);
|
|
106
|
+
}
|
|
107
|
+
const { handler, ...tool } = extension;
|
|
108
|
+
extraTools.set(tool.name, { tool, handler, validate: validatorProvider.getValidator(tool.inputSchema || { type: 'object' }) });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const gatewayPackageVersion = (() => {
|
|
112
|
+
try { return String(JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))?.version || 'unknown'); }
|
|
113
|
+
catch { return 'unknown'; }
|
|
114
|
+
})();
|
|
115
|
+
const gatewayBuildSha = String(env.MCP_GATEWAY_BUILD_SHA || '').trim() || (() => {
|
|
116
|
+
try { return String(execFileSync('git', ['rev-parse', '--short=7', 'HEAD'], { cwd: packageRoot, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] })).trim(); }
|
|
117
|
+
catch { return ''; }
|
|
118
|
+
})();
|
|
119
|
+
const gatewayServerVersion = String(env.MCP_GATEWAY_SERVER_VERSION || '').trim() || [gatewayPackageVersion, gatewayBuildSha].filter(Boolean).join(' · ');
|
|
120
|
+
const releaseLookupEnabled = !['0', 'false', 'no', 'off'].includes(String(env.MCP_DEVICE_RELEASE_LOOKUP ?? 'true').trim().toLowerCase());
|
|
121
|
+
const deviceReleaseProvider = options.deviceReleaseProvider || createDeviceReleaseProvider({
|
|
122
|
+
fallbackVersion: env.MCP_DEVICE_LATEST_VERSION || null,
|
|
123
|
+
enabled: releaseLookupEnabled
|
|
124
|
+
});
|
|
125
|
+
const runtimeDirectory = path.resolve(options.stateRoot);
|
|
126
|
+
const repoRoot = options.repoRoot || packageRoot;
|
|
127
|
+
const gatewayPort = Number(options.port ?? 8104);
|
|
128
|
+
if (!Number.isInteger(gatewayPort) || gatewayPort < 1 || gatewayPort > 65535) throw new Error('Invalid server port.');
|
|
129
|
+
const loopbackPort = options.loopbackPort == null ? null : Number(options.loopbackPort);
|
|
130
|
+
if (loopbackPort !== null && (!Number.isInteger(loopbackPort) || loopbackPort < 1 || loopbackPort > 65535 || loopbackPort === gatewayPort))
|
|
131
|
+
throw new Error('Invalid loopback MCP port.');
|
|
132
|
+
const gatewayHost = String(options.host || '127.0.0.1').trim();
|
|
133
|
+
const advertisedHost = String(env.MCP_ADVERTISE_HOST || '').trim() || (gatewayHost === '0.0.0.0' ? '127.0.0.1' : gatewayHost);
|
|
134
|
+
const advertisedUrl = String(options.publicUrl || env.MCP_ADVERTISE_URL || '').trim();
|
|
135
|
+
const allowHttpLan = options.allowHttpLan === true;
|
|
136
|
+
if (allowHttpLan) {
|
|
137
|
+
const explicitUrl = normalizeBaseUrl(advertisedUrl);
|
|
138
|
+
if (!explicitUrl || new URL(explicitUrl).protocol !== 'http:') {
|
|
139
|
+
throw new Error('allowHttpLan requires an explicit http:// publicUrl.');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const fallbackBaseUrl = `http://${advertisedHost}:${gatewayPort}`;
|
|
143
|
+
// The operator dashboard is mounted on this same authenticated application.
|
|
144
|
+
const staticBearerToken = env.MCP_BEARER_TOKEN;
|
|
145
|
+
const runtimeProfile = getRuntimeProfile(env);
|
|
146
|
+
const useStatefulMcpSessions = shouldUseStatefulSessionTransport(env.MCP_STATEFUL_SESSIONS);
|
|
147
|
+
const enableFilesystem = String(env.ENABLE_FILESYSTEM || 'true').toLowerCase() === 'true';
|
|
148
|
+
const enableShell = String(env.ENABLE_SHELL || 'true').toLowerCase() === 'true';
|
|
149
|
+
const debugAuth = envFlag(env.MCP_DEBUG_AUTH, false);
|
|
150
|
+
const slowToolThresholdMs = normalizeDurationMs(env.MCP_SLOW_TOOL_MS, 5000);
|
|
151
|
+
const toolMetrics = createToolMetricsRecorder({
|
|
152
|
+
metricsPath: path.resolve(env.MCP_METRICS_PATH || path.join(runtimeDirectory, 'mcp-calls.ndjson')),
|
|
153
|
+
enabled: envFlag(env.MCP_METRICS_ENABLED, false)
|
|
154
|
+
});
|
|
155
|
+
startupCleanups.push(() => toolMetrics.close());
|
|
156
|
+
const FILESYSTEM_TOOL_NAMES = new Set(['read_text_file', 'write_file', 'edit_file']);
|
|
157
|
+
const PROCESS_TOOL_NAMES = new Set(['start_process', 'read_process_output', 'interact_with_process', 'terminate_process']);
|
|
158
|
+
const activeProxyServers = new Set();
|
|
159
|
+
|
|
160
|
+
if (!repoRoot) throw new Error('REPO_ROOT is required');
|
|
161
|
+
if (!enableFilesystem && !enableShell) throw new Error('At least one execution tool family must be enabled');
|
|
162
|
+
|
|
163
|
+
function envFlag(value, defaultValue = false) {
|
|
164
|
+
const text = String(value ?? '').trim().toLowerCase();
|
|
165
|
+
if (!text) return defaultValue;
|
|
166
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(text)) return true;
|
|
167
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(text)) return false;
|
|
168
|
+
return defaultValue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeDurationMs(value, defaultValue) {
|
|
172
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
173
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : defaultValue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const configPath = resolveServerConfigPath({ configPath: options.configPath, assetRoot, packageRoot, env });
|
|
177
|
+
if (!configPath) throw new Error('config/mcp-servers.toml is required');
|
|
178
|
+
const workspaceRegistry = createWorkspaceRegistry({
|
|
179
|
+
configPath,
|
|
180
|
+
repoRoot: packageRoot,
|
|
181
|
+
env: env
|
|
182
|
+
});
|
|
183
|
+
startupCleanups.push(() => workspaceRegistry.close());
|
|
184
|
+
|
|
185
|
+
function workspaceSnapshot() {
|
|
186
|
+
return workspaceRegistry.snapshot();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function currentSurfaceConfig(snapshot = workspaceSnapshot()) {
|
|
190
|
+
return loadSurfaceConfig(snapshot.rawConfig, env);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const remoteProcessSessions = createRemoteProcessSessionRegistry();
|
|
194
|
+
const gatewayDbPath = path.join(runtimeDirectory, 'gateway.sqlite');
|
|
195
|
+
const accountStore = createAccountStore({ dbPath: gatewayDbPath });
|
|
196
|
+
startupCleanups.push(() => accountStore.close());
|
|
197
|
+
const deviceStore = createDeviceStore({ dbPath: gatewayDbPath });
|
|
198
|
+
startupCleanups.push(() => deviceStore.close());
|
|
199
|
+
const devicePairingStore = createDevicePairingStore({ dbPath: gatewayDbPath });
|
|
200
|
+
startupCleanups.push(() => devicePairingStore.close());
|
|
201
|
+
const deviceUsageStore = createDeviceUsageStore({ dbPath: gatewayDbPath });
|
|
202
|
+
startupCleanups.push(() => deviceUsageStore.close());
|
|
203
|
+
const deviceAudit = createDeviceAuditRecorder({
|
|
204
|
+
auditPath: path.join(runtimeDirectory, 'device-audit.jsonl'),
|
|
205
|
+
enabled: envFlag(env.MCP_DEVICE_AUDIT_ENABLED, false)
|
|
206
|
+
});
|
|
207
|
+
startupCleanups.push(() => deviceAudit.close());
|
|
208
|
+
const deviceBroker = createDeviceBroker({
|
|
209
|
+
enrollmentToken: env.MCP_DEVICE_ENROLLMENT_TOKEN,
|
|
210
|
+
deviceStore,
|
|
211
|
+
pairingStore: devicePairingStore,
|
|
212
|
+
usageStore: deviceUsageStore,
|
|
213
|
+
auditRecorder: deviceAudit,
|
|
214
|
+
onDeviceForgotten: ({ deviceId }) => remoteProcessSessions.removeByDevice(deviceId),
|
|
215
|
+
innerTls: options.innerTls || loadDeviceInnerTlsConfig(env)
|
|
216
|
+
});
|
|
217
|
+
startupCleanups.push(() => deviceBroker.shutdown());
|
|
218
|
+
const deviceAccessPolicy = createDeviceAccessPolicy({
|
|
219
|
+
raw: env.MCP_DEVICE_ACCESS_POLICY || '',
|
|
220
|
+
profile: runtimeProfile.name
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const skillRegistry = createSkillRegistry({ directory: options.skillRoot || path.join(assetRoot, 'scripts', 'skills') });
|
|
224
|
+
|
|
225
|
+
async function broadcastCatalogChanges(changes = {}) {
|
|
226
|
+
const tasks = [];
|
|
227
|
+
for (const server of activeProxyServers) {
|
|
228
|
+
if (changes.toolsChanged) tasks.push(server.sendToolListChanged().catch(() => {}));
|
|
229
|
+
if (changes.resourcesChanged) tasks.push(server.sendResourceListChanged().catch(() => {}));
|
|
230
|
+
if (changes.promptsChanged) tasks.push(server.sendPromptListChanged().catch(() => {}));
|
|
231
|
+
}
|
|
232
|
+
await Promise.all(tasks);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function localToolNamesForCollisionCheck() {
|
|
236
|
+
return [...LOCAL_COLLISION_TOOL_NAMES, ...contextTools.keys(), ...extraTools.keys()];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const externalMcpManager = await createExternalMcpManager({
|
|
240
|
+
// Startup and hot reload must read the same explicit unified configuration.
|
|
241
|
+
env: { ...env, MCP_UPSTREAM_CONFIG: configPath },
|
|
242
|
+
repoRoot: packageRoot,
|
|
243
|
+
localToolNames: localToolNamesForCollisionCheck(),
|
|
244
|
+
localPromptNames: listRepoPrompts({ runtimeProfile }).map(prompt => prompt.name),
|
|
245
|
+
onCatalogChanged: async changes => {
|
|
246
|
+
await broadcastCatalogChanges(changes);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
startupCleanups.push(() => externalMcpManager.shutdown());
|
|
250
|
+
const externalToolBroker = createExternalToolBroker({
|
|
251
|
+
getTools: () => externalMcpManager.getCachedTools(),
|
|
252
|
+
invokeTool: (name, args) => externalMcpManager.callTool(name, args)
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
workspaceRegistry.subscribe(async (next, previous) => {
|
|
256
|
+
const { rootsChanged, upstreamChanged } = classifyWorkspaceChange(next, previous);
|
|
257
|
+
if (rootsChanged) {
|
|
258
|
+
await broadcastCatalogChanges(workspaceCatalogChanges({ rootsChanged }, currentSurfaceConfig(next)));
|
|
259
|
+
}
|
|
260
|
+
if (upstreamChanged) {
|
|
261
|
+
const nextExternalConfig = normalizeExternalMcpConfig(next.rawConfig, {
|
|
262
|
+
configPath,
|
|
263
|
+
repoRoot: packageRoot,
|
|
264
|
+
env: env
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
await externalMcpManager.reconcile(nextExternalConfig);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(`[external-mcp] reconcile failed; keeping committed topology: ${error.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
function customToolContext(callerContext = {}) {
|
|
275
|
+
const snapshot = workspaceSnapshot();
|
|
276
|
+
return {
|
|
277
|
+
resolvedRepoRoots: snapshot.roots,
|
|
278
|
+
resolvedRepoRoot: snapshot.roots[0],
|
|
279
|
+
projectRegistry: snapshot.projectRegistry,
|
|
280
|
+
accountId: callerContext.accountId || null,
|
|
281
|
+
listVisibleDevices: () => deviceAccessPolicy.filterDevices(
|
|
282
|
+
deviceBroker.listDevices({ accountId: callerContext.accountId || null }),
|
|
283
|
+
{
|
|
284
|
+
callerSubject: callerContext.callerSubject || callerContext.callerCategory || 'anonymous',
|
|
285
|
+
callerCategory: callerContext.callerCategory || 'anonymous'
|
|
286
|
+
}
|
|
287
|
+
),
|
|
288
|
+
callDeviceTool: async (tool, args = {}) => {
|
|
289
|
+
const { deviceId, toolArguments } = requireDeviceId(args);
|
|
290
|
+
return await callRemoteDevice({ context: callerContext, deviceId, tool, arguments: toolArguments });
|
|
291
|
+
},
|
|
292
|
+
externalToolBroker,
|
|
293
|
+
skillRegistry,
|
|
294
|
+
runtimeProfile,
|
|
295
|
+
packageRoot,
|
|
296
|
+
env: env
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function listMergedTools() {
|
|
301
|
+
const tools = [];
|
|
302
|
+
if (enableFilesystem) {
|
|
303
|
+
tools.push(...FILESYSTEM_TOOL_DEFINITIONS.map(filesystemToolMeta));
|
|
304
|
+
}
|
|
305
|
+
tools.push(...listCustomTools(customToolContext()));
|
|
306
|
+
tools.push(applyToolRisk(listDevicesToolDefinition()));
|
|
307
|
+
if (enableShell) {
|
|
308
|
+
tools.push(shellExecuteToolMeta());
|
|
309
|
+
tools.push(...listProcessTools());
|
|
310
|
+
}
|
|
311
|
+
tools.push(...[...contextTools.values()].map(entry => entry.tool));
|
|
312
|
+
tools.push(...[...extraTools.values()].map(entry => entry.tool));
|
|
313
|
+
tools.push(...await externalMcpManager.listAllToolsUnfiltered());
|
|
314
|
+
return tools.filter(tool => shouldExposeToolForProfile(tool, runtimeProfile));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function persistSchemaSnapshot(tools) {
|
|
318
|
+
const toolSchemaBytes = Buffer.byteLength(JSON.stringify({ tools }), 'utf8');
|
|
319
|
+
const estimatedTokens = Math.ceil(toolSchemaBytes / 4);
|
|
320
|
+
const estimationMethod = 'utf8_bytes_div_4_estimate';
|
|
321
|
+
deviceUsageStore.setSchemaSnapshot({
|
|
322
|
+
toolCount: tools.length,
|
|
323
|
+
schemaBytes: toolSchemaBytes,
|
|
324
|
+
estimatedTokens,
|
|
325
|
+
estimationMethod
|
|
326
|
+
});
|
|
327
|
+
deviceBroker.setSchemaSnapshot({
|
|
328
|
+
toolCount: tools.length,
|
|
329
|
+
toolSchemaBytes,
|
|
330
|
+
toolSchemaTokenEstimate: estimatedTokens,
|
|
331
|
+
tokenEstimateMethod: estimationMethod
|
|
332
|
+
});
|
|
333
|
+
return { toolSchemaBytes, estimatedTokens, estimationMethod };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function refreshDeviceSchemaSnapshot() {
|
|
337
|
+
const tools = await listMergedTools();
|
|
338
|
+
return persistSchemaSnapshot(tools);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function structuredToolText(value, { includeStructured = false } = {}) {
|
|
342
|
+
const result = { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
|
|
343
|
+
return includeStructured ? { ...result, structuredContent: value } : result;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const SHELL_RESPONSE_BUDGET_BYTES = 128 * 1024;
|
|
347
|
+
const SHELL_TOOL_RESULT_BUDGET_BYTES = SHELL_RESPONSE_BUDGET_BYTES - (8 * 1024);
|
|
348
|
+
|
|
349
|
+
function boundedShellToolText(value) {
|
|
350
|
+
const response = structuredToolText(value, { includeStructured: true });
|
|
351
|
+
if (Buffer.byteLength(JSON.stringify(response)) <= SHELL_TOOL_RESULT_BUDGET_BYTES) return response;
|
|
352
|
+
throw new Error(`shell_execute response exceeds ${SHELL_RESPONSE_BUDGET_BYTES} byte budget`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function splitDeviceArguments(args = {}) {
|
|
356
|
+
const { device_id: deviceId, ...toolArguments } = args;
|
|
357
|
+
return { deviceId: String(deviceId || '').trim(), toolArguments };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function requireDeviceId(args = {}) {
|
|
361
|
+
const routed = splitDeviceArguments(args);
|
|
362
|
+
if (routed.deviceId) return routed;
|
|
363
|
+
const error = new Error('DEVICE_ID_REQUIRED: device_id is required for execution tools.');
|
|
364
|
+
error.code = 'DEVICE_ID_REQUIRED';
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function metricDeviceId(toolName, args = {}, context = {}) {
|
|
369
|
+
const explicit = String(args.device_id || '').trim();
|
|
370
|
+
if (explicit) return explicit;
|
|
371
|
+
if (!PROCESS_TOOL_NAMES.has(toolName) || toolName === 'start_process' || !args.session_id) return null;
|
|
372
|
+
try {
|
|
373
|
+
return remoteProcessSessions.resolve({ sessionId: args.session_id, ownerKey: context.callerKey || 'anonymous' }).deviceId;
|
|
374
|
+
} catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function metricSkillName(toolName, args = {}) {
|
|
380
|
+
if (toolName !== 'load_skill') return null;
|
|
381
|
+
return String(args.name || '').trim() || null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function callRemoteDevice({
|
|
385
|
+
context = {},
|
|
386
|
+
deviceId,
|
|
387
|
+
tool,
|
|
388
|
+
arguments: args = {},
|
|
389
|
+
timeoutMs,
|
|
390
|
+
expectedConnectionEpoch,
|
|
391
|
+
expectedExecutionRuntimeGeneration,
|
|
392
|
+
includeDispatchContext = false
|
|
393
|
+
}) {
|
|
394
|
+
const callerSubject = context.callerSubject || context.callerCategory || 'anonymous';
|
|
395
|
+
const callerCategory = context.callerCategory || 'anonymous';
|
|
396
|
+
const requestId = randomUUID();
|
|
397
|
+
const startedAt = Date.now();
|
|
398
|
+
let grant = null;
|
|
399
|
+
try {
|
|
400
|
+
grant = deviceAccessPolicy.authorize({
|
|
401
|
+
callerSubject,
|
|
402
|
+
callerCategory,
|
|
403
|
+
deviceId,
|
|
404
|
+
tool,
|
|
405
|
+
arguments: args
|
|
406
|
+
});
|
|
407
|
+
const brokerResult = await deviceBroker.callDevice({
|
|
408
|
+
requestId,
|
|
409
|
+
accountId: context.accountId || null,
|
|
410
|
+
deviceId,
|
|
411
|
+
tool,
|
|
412
|
+
arguments: args,
|
|
413
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
414
|
+
...(expectedConnectionEpoch === undefined ? {} : { expectedConnectionEpoch }),
|
|
415
|
+
...(expectedExecutionRuntimeGeneration === undefined ? {} : { expectedExecutionRuntimeGeneration }),
|
|
416
|
+
...(includeDispatchContext ? { includeDispatchContext: true } : {})
|
|
417
|
+
});
|
|
418
|
+
const result = includeDispatchContext ? brokerResult?.result : brokerResult;
|
|
419
|
+
const outputBytes = deviceAccessPolicy.assertOutput(grant, result);
|
|
420
|
+
deviceAudit.record({
|
|
421
|
+
requestId,
|
|
422
|
+
callerId: callerAuditId(callerSubject),
|
|
423
|
+
callerCategory,
|
|
424
|
+
deviceId,
|
|
425
|
+
tool,
|
|
426
|
+
outcome: 'success',
|
|
427
|
+
durationMs: Date.now() - startedAt,
|
|
428
|
+
inputBytes: grant.inputBytes,
|
|
429
|
+
outputBytes
|
|
430
|
+
});
|
|
431
|
+
return includeDispatchContext
|
|
432
|
+
? { result, dispatchContext: brokerResult.dispatchContext }
|
|
433
|
+
: result;
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const inputBytes = grant?.inputBytes ?? Buffer.byteLength(JSON.stringify(args ?? {}), 'utf8');
|
|
436
|
+
deviceAudit.record({
|
|
437
|
+
requestId,
|
|
438
|
+
callerId: callerAuditId(callerSubject),
|
|
439
|
+
callerCategory,
|
|
440
|
+
deviceId,
|
|
441
|
+
tool,
|
|
442
|
+
outcome: 'error',
|
|
443
|
+
durationMs: Date.now() - startedAt,
|
|
444
|
+
inputBytes,
|
|
445
|
+
errorCode: String(error?.code || 'REMOTE_DEVICE_ERROR').slice(0, 64)
|
|
446
|
+
});
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function routeToolCall(request, context = {}) {
|
|
452
|
+
const { callerKey } = context;
|
|
453
|
+
const toolName = request.params.name;
|
|
454
|
+
assertToolAllowedForProfile(toolName, runtimeProfile);
|
|
455
|
+
const extension = extraTools.get(toolName);
|
|
456
|
+
if (extension) {
|
|
457
|
+
if (!context.accountId) throw new Error('ACCOUNT_ID_REQUIRED: an authenticated account is required.');
|
|
458
|
+
const args = request.params.arguments || {};
|
|
459
|
+
const validation = extension.validate(args);
|
|
460
|
+
if (!validation.valid) throw new Error('Invalid arguments for ' + toolName + ': ' + validation.errorMessage);
|
|
461
|
+
const trustedContext = Object.freeze({
|
|
462
|
+
...context,
|
|
463
|
+
sessionId: context.activitySessionId || context.callerKey,
|
|
464
|
+
listDevices: () => deviceBroker.listDevices({ accountId: context.accountId }),
|
|
465
|
+
callDevice: ({ deviceId, tool, arguments: toolArguments = {}, timeoutMs }) =>
|
|
466
|
+
callRemoteDevice({ context, deviceId, tool, arguments: toolArguments, timeoutMs })
|
|
467
|
+
});
|
|
468
|
+
const result = await extension.handler(args, trustedContext);
|
|
469
|
+
return result && Array.isArray(result.content) ? result : structuredToolText(result, { includeStructured: true });
|
|
470
|
+
}
|
|
471
|
+
const contextEntry = contextTools.get(toolName);
|
|
472
|
+
if (contextEntry) {
|
|
473
|
+
const args = request.params.arguments || {};
|
|
474
|
+
const validation = contextEntry.validate(args);
|
|
475
|
+
if (!validation.valid) throw new Error('Invalid arguments for ' + toolName + ': ' + validation.errorMessage);
|
|
476
|
+
const { deviceId, toolArguments } = requireDeviceId(args);
|
|
477
|
+
// Context's published schema owns device_id; preserve it for every scoped action.
|
|
478
|
+
const forwarded = toolName === 'local_wiki' ? toolArguments : { ...toolArguments, device_id: deviceId };
|
|
479
|
+
const result = await callRemoteDevice({ context, deviceId, tool: toolName, arguments: forwarded });
|
|
480
|
+
return result && Array.isArray(result.content) ? result : structuredToolText(result, { includeStructured: true });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (toolName === 'shell_execute' && enableShell) {
|
|
484
|
+
const { deviceId, toolArguments } = requireDeviceId(request.params.arguments || {});
|
|
485
|
+
const remoteTimeoutMs = Number(toolArguments.timeout_ms || 28000);
|
|
486
|
+
if (!Number.isInteger(remoteTimeoutMs) || remoteTimeoutMs < 1 || remoteTimeoutMs > 28000) {
|
|
487
|
+
throw new Error('Remote shell_execute timeout_ms must be between 1 and 28000; use start_process for longer commands.');
|
|
488
|
+
}
|
|
489
|
+
const result = await callRemoteDevice({
|
|
490
|
+
context,
|
|
491
|
+
deviceId,
|
|
492
|
+
tool: 'shell_execute',
|
|
493
|
+
arguments: { ...toolArguments, timeout_ms: remoteTimeoutMs },
|
|
494
|
+
timeoutMs: remoteTimeoutMs + 2000
|
|
495
|
+
});
|
|
496
|
+
return boundedShellToolText(result);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (toolName === 'list_devices') {
|
|
500
|
+
const devices = deviceAccessPolicy.filterDevices(deviceBroker.listDevices({ accountId: context.accountId || null }), {
|
|
501
|
+
callerSubject: context.callerSubject || context.callerCategory || 'anonymous',
|
|
502
|
+
callerCategory: context.callerCategory || 'anonymous'
|
|
503
|
+
});
|
|
504
|
+
return structuredToolText({ ok: true, ...paginateDeviceInventory(devices, request.params.arguments || {}) }, { includeStructured: true });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (PROCESS_TOOL_NAMES.has(toolName) && enableShell) {
|
|
508
|
+
const args = request.params.arguments || {};
|
|
509
|
+
const ownerKey = callerKey || 'anonymous';
|
|
510
|
+
if (toolName === 'start_process') {
|
|
511
|
+
const { deviceId, toolArguments } = requireDeviceId(args);
|
|
512
|
+
const dispatch = await callRemoteDevice({
|
|
513
|
+
context,
|
|
514
|
+
deviceId,
|
|
515
|
+
tool: 'start_process',
|
|
516
|
+
arguments: toolArguments,
|
|
517
|
+
timeoutMs: Math.min(Number(toolArguments.timeout_ms || 10000) + 2000, 30000),
|
|
518
|
+
includeDispatchContext: true
|
|
519
|
+
});
|
|
520
|
+
const remote = dispatch.result;
|
|
521
|
+
const remoteSessionId = remote?.sessionId ?? remote?.session_id;
|
|
522
|
+
if (!remoteSessionId) throw new Error('Remote start_process did not return a session identifier.');
|
|
523
|
+
const sessionId = remoteProcessSessions.register({
|
|
524
|
+
ownerKey,
|
|
525
|
+
deviceId: dispatch.dispatchContext.deviceId,
|
|
526
|
+
connectionEpoch: dispatch.dispatchContext.connectionEpoch,
|
|
527
|
+
executionRuntimeGeneration: dispatch.dispatchContext.executionRuntimeGeneration,
|
|
528
|
+
remoteSessionId
|
|
529
|
+
});
|
|
530
|
+
return structuredToolText({ ...remote, sessionId, session_id: sessionId }, { includeStructured: true });
|
|
531
|
+
}
|
|
532
|
+
const remoteSession = remoteProcessSessions.resolve({ sessionId: args.session_id, ownerKey });
|
|
533
|
+
const remoteArgs = { ...args, session_id: remoteSession.remoteSessionId };
|
|
534
|
+
const remoteTimeoutMs = toolName === 'read_process_output'
|
|
535
|
+
? 12000
|
|
536
|
+
: Math.min(Number(args.timeout_ms || 10000) + 2000, 30000);
|
|
537
|
+
const remote = await callRemoteDevice({
|
|
538
|
+
context,
|
|
539
|
+
deviceId: remoteSession.deviceId,
|
|
540
|
+
tool: toolName,
|
|
541
|
+
arguments: remoteArgs,
|
|
542
|
+
timeoutMs: remoteTimeoutMs,
|
|
543
|
+
expectedConnectionEpoch: remoteSession.connectionEpoch,
|
|
544
|
+
expectedExecutionRuntimeGeneration: remoteSession.executionRuntimeGeneration
|
|
545
|
+
});
|
|
546
|
+
if (toolName === 'terminate_process') {
|
|
547
|
+
remoteProcessSessions.remove({ sessionId: args.session_id, ownerKey });
|
|
548
|
+
} else {
|
|
549
|
+
remoteProcessSessions.touch({ sessionId: args.session_id, ownerKey });
|
|
550
|
+
}
|
|
551
|
+
return structuredToolText({ ...remote, sessionId: args.session_id, session_id: args.session_id }, { includeStructured: true });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (FILESYSTEM_TOOL_NAMES.has(toolName)) {
|
|
555
|
+
const { deviceId, toolArguments } = requireDeviceId(request.params.arguments || {});
|
|
556
|
+
const result = await callRemoteDevice({ context, deviceId, tool: toolName, arguments: toolArguments });
|
|
557
|
+
return result && Array.isArray(result.content)
|
|
558
|
+
? normalizeRemoteFilesystemResult(result)
|
|
559
|
+
: structuredToolText(result, { includeStructured: true });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (isLocalCustomTool(toolName)) {
|
|
563
|
+
return await callCustomTool(toolName, request.params.arguments || {}, customToolContext(context));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (externalMcpManager.isEagerToolName(toolName)) {
|
|
567
|
+
return await externalMcpManager.callTool(toolName, request.params.arguments || {}, runtimeProfile);
|
|
568
|
+
}
|
|
569
|
+
if (externalMcpManager.isExternalToolName(toolName)) {
|
|
570
|
+
throw new Error(`External MCP tool ${toolName} is deferred; use external_tool_search and the appropriate external_tool_call_* lane.`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
throw new Error(`Unknown or disabled tool: ${toolName}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function metricBelongsToCurrentDeviceEnrollment(metric, startedAt) {
|
|
577
|
+
const deviceId = String(metric?.deviceId || '').trim();
|
|
578
|
+
return !deviceId || deviceStore.containsEnrollmentAt(deviceId, startedAt);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function recordToolUsage(metric, startedAt) {
|
|
582
|
+
try {
|
|
583
|
+
if (metricBelongsToCurrentDeviceEnrollment(metric, startedAt)) {
|
|
584
|
+
deviceUsageStore.recordToolCall(metric);
|
|
585
|
+
}
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.error(`[usage-store] tool event persist failed: ${error.message}`);
|
|
588
|
+
}
|
|
589
|
+
toolMetrics.record(metric);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function routeObservedToolCall(request, context) {
|
|
593
|
+
const toolName = request.params?.name || 'unknown';
|
|
594
|
+
const args = request.params?.arguments || {};
|
|
595
|
+
const deviceId = metricDeviceId(toolName, args, context);
|
|
596
|
+
const skillName = metricSkillName(toolName, args);
|
|
597
|
+
const startedAt = Date.now();
|
|
598
|
+
console.log(`[tool-call:start] ${toolName}`);
|
|
599
|
+
try {
|
|
600
|
+
const result = await routeToolCall(request, context);
|
|
601
|
+
const durationMs = Date.now() - startedAt;
|
|
602
|
+
console.log(`[tool-call:finish] ${toolName} durationMs=${durationMs}`);
|
|
603
|
+
if (durationMs > slowToolThresholdMs) console.log(`[tool-call:slow] ${toolName} durationMs=${durationMs}`);
|
|
604
|
+
const metric = buildToolMetric({
|
|
605
|
+
toolName,
|
|
606
|
+
args,
|
|
607
|
+
result,
|
|
608
|
+
durationMs,
|
|
609
|
+
callerCategory: context?.callerCategory,
|
|
610
|
+
accountId: context?.accountId,
|
|
611
|
+
activitySessionId: context?.activitySessionId,
|
|
612
|
+
deviceId,
|
|
613
|
+
skillName,
|
|
614
|
+
upstream: externalMcpManager.isExternalToolName(toolName) ? 'external-mcp' : null
|
|
615
|
+
});
|
|
616
|
+
recordToolUsage(metric, startedAt);
|
|
617
|
+
return result;
|
|
618
|
+
} catch (error) {
|
|
619
|
+
const durationMs = Date.now() - startedAt;
|
|
620
|
+
console.log(`[tool-call:error] ${toolName} durationMs=${durationMs}`);
|
|
621
|
+
const metric = buildToolMetric({
|
|
622
|
+
toolName,
|
|
623
|
+
args,
|
|
624
|
+
durationMs,
|
|
625
|
+
callerCategory: context?.callerCategory,
|
|
626
|
+
accountId: context?.accountId,
|
|
627
|
+
activitySessionId: context?.activitySessionId,
|
|
628
|
+
deviceId,
|
|
629
|
+
skillName,
|
|
630
|
+
upstream: externalMcpManager.isExternalToolName(toolName) ? 'external-mcp' : null,
|
|
631
|
+
error
|
|
632
|
+
});
|
|
633
|
+
recordToolUsage(metric, startedAt);
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function currentResourceContext(callerContext = {}) {
|
|
639
|
+
const snapshot = workspaceSnapshot();
|
|
640
|
+
return {
|
|
641
|
+
resolvedRepoRoots: snapshot.roots,
|
|
642
|
+
resolvedRepoRoot: snapshot.roots[0],
|
|
643
|
+
projectRegistry: snapshot.projectRegistry,
|
|
644
|
+
surfaceConfig: currentSurfaceConfig(snapshot),
|
|
645
|
+
accountId: callerContext.accountId || null,
|
|
646
|
+
listVisibleDevices: () => deviceAccessPolicy.filterDevices(
|
|
647
|
+
deviceBroker.listDevices({ accountId: callerContext.accountId || null }),
|
|
648
|
+
{
|
|
649
|
+
callerSubject: callerContext.callerSubject || callerContext.callerCategory || 'anonymous',
|
|
650
|
+
callerCategory: callerContext.callerCategory || 'anonymous'
|
|
651
|
+
}
|
|
652
|
+
),
|
|
653
|
+
callDeviceTool: async (tool, args = {}) => {
|
|
654
|
+
const { deviceId, toolArguments } = requireDeviceId(args);
|
|
655
|
+
return await callRemoteDevice({ context: callerContext, deviceId, tool, arguments: toolArguments });
|
|
656
|
+
},
|
|
657
|
+
packageRoot,
|
|
658
|
+
env: env,
|
|
659
|
+
skillRegistry,
|
|
660
|
+
listTools: listMergedTools
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function createProxyServer({ era = 'legacy', accountId, activitySessionId, callerKey, callerCategory, callerSubject }) {
|
|
665
|
+
const metadata = workspaceSnapshot().server;
|
|
666
|
+
const server = new Server(
|
|
667
|
+
{
|
|
668
|
+
name: metadata.name,
|
|
669
|
+
title: metadata.title,
|
|
670
|
+
version: gatewayPackageVersion,
|
|
671
|
+
description: metadata.description
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
instructions: [metadata.instructions, 'Native Skills clients use the MCP Skills extension. Other clients may discover and load reusable skills with skill_catalog and load_skill.'].filter(Boolean).join(' '),
|
|
675
|
+
capabilities: {
|
|
676
|
+
tools: { listChanged: true },
|
|
677
|
+
resources: { subscribe: false, listChanged: true },
|
|
678
|
+
prompts: { listChanged: true }
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
server.setRequestHandler('tools/list', async () => {
|
|
684
|
+
const tools = await listMergedTools();
|
|
685
|
+
const { toolSchemaBytes, estimatedTokens, estimationMethod } = persistSchemaSnapshot(tools);
|
|
686
|
+
deviceUsageStore.recordCatalogList({
|
|
687
|
+
accountId,
|
|
688
|
+
activitySessionId,
|
|
689
|
+
toolCount: tools.length,
|
|
690
|
+
schemaBytes: toolSchemaBytes,
|
|
691
|
+
estimatedTokens,
|
|
692
|
+
estimationMethod
|
|
693
|
+
});
|
|
694
|
+
return { tools };
|
|
695
|
+
});
|
|
696
|
+
server.setRequestHandler('tools/call', request => routeObservedToolCall(request, { accountId, activitySessionId, callerKey, callerCategory, callerSubject }));
|
|
697
|
+
const callerContext = { accountId, activitySessionId, callerKey, callerCategory, callerSubject };
|
|
698
|
+
server.setRequestHandler('resources/list', async () => {
|
|
699
|
+
const context = currentResourceContext(callerContext);
|
|
700
|
+
return { resources: [...listRepoResources(context), ...await externalMcpManager.listResources()] };
|
|
701
|
+
});
|
|
702
|
+
server.setRequestHandler('resources/templates/list', async () => {
|
|
703
|
+
const context = currentResourceContext(callerContext);
|
|
704
|
+
return { resourceTemplates: [...listRepoResourceTemplates(context), ...await externalMcpManager.listResourceTemplates()] };
|
|
705
|
+
});
|
|
706
|
+
server.setRequestHandler('resources/read', async request => {
|
|
707
|
+
if (isExternalResourceUri(request.params.uri)) return await externalMcpManager.readResource(request.params.uri);
|
|
708
|
+
return await readRepoResource(request.params.uri, currentResourceContext(callerContext));
|
|
709
|
+
});
|
|
710
|
+
server.setRequestHandler('prompts/list', async () => ({
|
|
711
|
+
prompts: [...listRepoPrompts({ runtimeProfile }), ...await externalMcpManager.listPrompts()]
|
|
712
|
+
}));
|
|
713
|
+
server.setRequestHandler('prompts/get', async request => {
|
|
714
|
+
if (externalMcpManager.isExternalPromptName(request.params.name)) {
|
|
715
|
+
return await externalMcpManager.getPrompt(request.params.name, request.params.arguments || {});
|
|
716
|
+
}
|
|
717
|
+
return getRepoPrompt(request.params.name, request.params.arguments || {}, { runtimeProfile });
|
|
718
|
+
});
|
|
719
|
+
if (era === 'legacy') activeProxyServers.add(server);
|
|
720
|
+
if (era === 'modern') registerSkillsExtension(server, skillRegistry);
|
|
721
|
+
return server;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function summarizeRequestBody(body) {
|
|
725
|
+
if (!body || typeof body !== 'object') {
|
|
726
|
+
return 'no-body';
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const method = typeof body.method === 'string' ? body.method : 'unknown';
|
|
730
|
+
const toolName = body.params && typeof body.params.name === 'string' ? body.params.name : '';
|
|
731
|
+
return toolName ? `${method}:${toolName}` : method;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function setIncomingHeader(req, name, value) {
|
|
735
|
+
req.headers[name.toLowerCase()] = value;
|
|
736
|
+
if (!Array.isArray(req.rawHeaders)) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const index = req.rawHeaders.findIndex(header => header.toLowerCase() === name.toLowerCase());
|
|
741
|
+
if (index >= 0) {
|
|
742
|
+
req.rawHeaders[index + 1] = value;
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
req.rawHeaders.push(name, value);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function fingerprint(value) {
|
|
750
|
+
const text = String(value || '');
|
|
751
|
+
return `${text.length}:${createHash('sha256').update(text).digest('hex').slice(0, 12)}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function callerKeyFromParts({ accountId = '', oauthClientId = '', staticBearer = false, sessionId = '' } = {}) {
|
|
755
|
+
const identity = accountId ? `account:${accountId}` : oauthClientId ? `oauth:${oauthClientId}` : staticBearer ? 'static-bearer' : 'anonymous';
|
|
756
|
+
const digest = createHash('sha256')
|
|
757
|
+
.update(sessionId ? `${identity}\nsession:${sessionId}` : identity)
|
|
758
|
+
.digest('hex')
|
|
759
|
+
.slice(0, 24);
|
|
760
|
+
return `caller:${digest}`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function callerKeyFromRequest(req) {
|
|
764
|
+
return callerKeyFromParts({
|
|
765
|
+
accountId: req.auth?.accountId || '',
|
|
766
|
+
oauthClientId: req.auth?.clientId || '',
|
|
767
|
+
staticBearer: isStaticBearerAuthorization(req.headers.authorization, staticBearerToken),
|
|
768
|
+
sessionId: useStatefulMcpSessions ? req.headers['mcp-session-id'] || '' : ''
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function callerCategoryFromRequest(req) {
|
|
773
|
+
if (isStaticBearerAuthorization(req.headers.authorization, staticBearerToken)) return 'static-bearer';
|
|
774
|
+
if (req.auth?.clientId) return 'oauth';
|
|
775
|
+
return 'anonymous';
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function callerSubjectFromRequest(req) {
|
|
779
|
+
if (isStaticBearerAuthorization(req.headers.authorization, staticBearerToken)) return 'static-bearer';
|
|
780
|
+
if (req.auth?.accountId) return `account:${req.auth.accountId}`;
|
|
781
|
+
return 'anonymous';
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function getProvidedAuthorizationToken(authorizationHeader) {
|
|
785
|
+
if (typeof authorizationHeader !== 'string') {
|
|
786
|
+
return '';
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const header = authorizationHeader.trim();
|
|
790
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
791
|
+
return (match ? match[1] : header).trim();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
await refreshDeviceSchemaSnapshot();
|
|
795
|
+
const app = express();
|
|
796
|
+
app.set('trust proxy', 1);
|
|
797
|
+
app.use(express.json({ limit: '2mb' }));
|
|
798
|
+
app.use(express.urlencoded({ extended: false }));
|
|
799
|
+
const accountHttp = installAccountRoutes(app, {
|
|
800
|
+
accountStore,
|
|
801
|
+
allowAdminLogin: true,
|
|
802
|
+
needInvite: () => workspaceSnapshot().rawConfig?.auth?.need_invite !== false
|
|
803
|
+
});
|
|
804
|
+
installOperatorDashboardRoutes(app, {
|
|
805
|
+
accountStore, usageStore: deviceUsageStore, deviceBroker,
|
|
806
|
+
accountFromRequest: accountHttp.sessionAccountFromRequest
|
|
807
|
+
});
|
|
808
|
+
const oauthStateStore = new SQLiteAuthState(gatewayDbPath);
|
|
809
|
+
startupCleanups.push(() => oauthStateStore.close());
|
|
810
|
+
installDashboardRoutes(app, {
|
|
811
|
+
accountFromRequest: accountHttp.accountFromRequest,
|
|
812
|
+
usageStore: deviceUsageStore,
|
|
813
|
+
deviceBroker,
|
|
814
|
+
baseUrlFromRequest: requestBaseUrl,
|
|
815
|
+
oauthClientLookup: clientId => oauthStateStore.getClient(clientId),
|
|
816
|
+
listTools: listMergedTools,
|
|
817
|
+
serverVersion: gatewayServerVersion,
|
|
818
|
+
deviceReleaseProvider
|
|
819
|
+
});
|
|
820
|
+
const provider = new AccountAuthProvider({
|
|
821
|
+
stateStore: oauthStateStore,
|
|
822
|
+
accountStore,
|
|
823
|
+
accountFromRequest: accountHttp.accountFromRequest,
|
|
824
|
+
sessionBindingFromRequest: accountHttp.sessionBindingFromRequest,
|
|
825
|
+
activityStore: deviceUsageStore
|
|
826
|
+
});
|
|
827
|
+
installAuthorizationGateRoutes(app, {
|
|
828
|
+
provider,
|
|
829
|
+
clearSession: accountHttp.clearSession,
|
|
830
|
+
loginLocation: accountHttp.loginLocation
|
|
831
|
+
});
|
|
832
|
+
app.use((req, res, next) => {
|
|
833
|
+
if (req.path !== '/mcp') {
|
|
834
|
+
next();
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const origin = req.headers.origin;
|
|
839
|
+
if (origin) {
|
|
840
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
841
|
+
res.setHeader('Vary', 'Origin');
|
|
842
|
+
}
|
|
843
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
844
|
+
res.setHeader(
|
|
845
|
+
'Access-Control-Allow-Headers',
|
|
846
|
+
'authorization, content-type, accept, mcp-session-id, mcp-protocol-version'
|
|
847
|
+
);
|
|
848
|
+
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
|
|
849
|
+
|
|
850
|
+
if (req.method === 'OPTIONS') {
|
|
851
|
+
res.status(204).end();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
next();
|
|
856
|
+
});
|
|
857
|
+
app.use((req, _res, next) => {
|
|
858
|
+
if (req.path === '/mcp') {
|
|
859
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
860
|
+
const authHeader = req.headers.authorization ? 'yes' : 'no';
|
|
861
|
+
console.log(`[mcp-http] ${req.method} /mcp auth=${authHeader} session=${sessionId || 'none'}`);
|
|
862
|
+
}
|
|
863
|
+
next();
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
function normalizeBaseUrl(value) {
|
|
867
|
+
return String(value || '').trim().replace(/\/+$/, '');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function requestBaseUrl(req) {
|
|
871
|
+
const explicitBaseUrl = normalizeBaseUrl(advertisedUrl);
|
|
872
|
+
if (explicitBaseUrl) {
|
|
873
|
+
return explicitBaseUrl;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const forwardedProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim();
|
|
877
|
+
const forwardedHost = String(req.headers['x-forwarded-host'] || '').split(',')[0].trim();
|
|
878
|
+
const host = forwardedHost || req.headers.host;
|
|
879
|
+
if (!host) {
|
|
880
|
+
return fallbackBaseUrl;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
let hostname = String(host).toLowerCase();
|
|
884
|
+
try {
|
|
885
|
+
hostname = new URL(`http://${host}`).hostname.toLowerCase();
|
|
886
|
+
} catch {
|
|
887
|
+
hostname = hostname.split(':')[0];
|
|
888
|
+
}
|
|
889
|
+
const isLocalHost = ['localhost', '127.0.0.1', '::1'].includes(hostname);
|
|
890
|
+
const proto = forwardedProto || (isLocalHost ? req.protocol || (req.secure ? 'https' : 'http') : 'https');
|
|
891
|
+
return normalizeBaseUrl(`${proto}://${host}`);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
installDevicePairingRoutes(app, {
|
|
895
|
+
pairingStore: devicePairingStore,
|
|
896
|
+
accountFromRequest: accountHttp.accountFromRequest,
|
|
897
|
+
baseUrlFromRequest: requestBaseUrl
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
function buildAuthUrls(baseUrl) {
|
|
901
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl) || fallbackBaseUrl;
|
|
902
|
+
const issuerUrl = new URL(normalizedBaseUrl);
|
|
903
|
+
const resourceServerUrl = new URL('/mcp', `${normalizedBaseUrl}/`);
|
|
904
|
+
return { issuerUrl, resourceServerUrl };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const authRouters = new Map();
|
|
908
|
+
|
|
909
|
+
function getAuthRouterForBaseUrl(baseUrl) {
|
|
910
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl) || fallbackBaseUrl;
|
|
911
|
+
const cached = authRouters.get(normalizedBaseUrl);
|
|
912
|
+
if (cached) {
|
|
913
|
+
return cached;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const { issuerUrl, resourceServerUrl } = buildAuthUrls(normalizedBaseUrl);
|
|
917
|
+
const router = createOAuthAuthorizationServerRouter({
|
|
918
|
+
provider,
|
|
919
|
+
issuerUrl,
|
|
920
|
+
resourceServerUrl,
|
|
921
|
+
dangerouslyAllowInsecureIssuerUrl: allowHttpLan,
|
|
922
|
+
authorizationOptions: {
|
|
923
|
+
rateLimit: {
|
|
924
|
+
validate: { creationStack: false }
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
tokenOptions: {
|
|
928
|
+
rateLimit: {
|
|
929
|
+
validate: { creationStack: false }
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
clientRegistrationOptions: {
|
|
933
|
+
rateLimit: {
|
|
934
|
+
validate: { creationStack: false }
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
revocationOptions: {
|
|
938
|
+
rateLimit: {
|
|
939
|
+
validate: { creationStack: false }
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
scopesSupported: AUTH_SUPPORTED_SCOPES,
|
|
943
|
+
resourceName: 'Local Dev MCP'
|
|
944
|
+
});
|
|
945
|
+
authRouters.set(normalizedBaseUrl, router);
|
|
946
|
+
return router;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
app.use((req, res, next) => {
|
|
950
|
+
getAuthRouterForBaseUrl(requestBaseUrl(req))(req, res, next);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
const oauthAuthMiddlewares = new Map();
|
|
954
|
+
|
|
955
|
+
function getOAuthAuthMiddlewareForBaseUrl(baseUrl) {
|
|
956
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl) || fallbackBaseUrl;
|
|
957
|
+
const cached = oauthAuthMiddlewares.get(normalizedBaseUrl);
|
|
958
|
+
if (cached) {
|
|
959
|
+
return cached;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const { resourceServerUrl } = buildAuthUrls(normalizedBaseUrl);
|
|
963
|
+
const middleware = requireBearerAuth({
|
|
964
|
+
verifier: provider,
|
|
965
|
+
requiredScopes: [],
|
|
966
|
+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(resourceServerUrl)
|
|
967
|
+
});
|
|
968
|
+
oauthAuthMiddlewares.set(normalizedBaseUrl, middleware);
|
|
969
|
+
return middleware;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function mcpAuthMiddleware(req, res, next) {
|
|
973
|
+
if (isStaticBearerAuthorization(req.headers.authorization, staticBearerToken)) {
|
|
974
|
+
console.log('[auth] static-bearer accepted');
|
|
975
|
+
setIncomingHeader(req, 'accept', 'application/json, text/event-stream');
|
|
976
|
+
next();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (debugAuth && req.headers.authorization && staticBearerToken) {
|
|
981
|
+
const providedToken = getProvidedAuthorizationToken(req.headers.authorization);
|
|
982
|
+
console.log(
|
|
983
|
+
`[auth] static-bearer not matched; trying OAuth provided=${fingerprint(providedToken)} expected=${fingerprint(staticBearerToken)}`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
getOAuthAuthMiddlewareForBaseUrl(requestBaseUrl(req))(req, res, next);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function modernCallerContext(ctx = {}) {
|
|
991
|
+
const authorization = ctx.requestInfo?.headers?.get?.('authorization') || '';
|
|
992
|
+
const staticBearer = isStaticBearerAuthorization(authorization, staticBearerToken);
|
|
993
|
+
const accountId = ctx.authInfo?.accountId || null;
|
|
994
|
+
const oauthClientId = ctx.authInfo?.clientId || '';
|
|
995
|
+
const callerCategory = staticBearer ? 'static-bearer' : oauthClientId ? 'oauth' : 'anonymous';
|
|
996
|
+
const callerSubject = staticBearer ? 'static-bearer' : accountId ? `account:${accountId}` : 'anonymous';
|
|
997
|
+
return {
|
|
998
|
+
era: ctx.era,
|
|
999
|
+
accountId,
|
|
1000
|
+
activitySessionId: ctx.authInfo?.activitySessionId || null,
|
|
1001
|
+
callerKey: callerKeyFromParts({ accountId: accountId || '', oauthClientId, staticBearer }),
|
|
1002
|
+
callerCategory,
|
|
1003
|
+
callerSubject
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const modernMcpHandler = createMcpHandler(
|
|
1008
|
+
ctx => createProxyServer(modernCallerContext(ctx)),
|
|
1009
|
+
{
|
|
1010
|
+
legacy: useStatefulMcpSessions ? 'reject' : 'stateless',
|
|
1011
|
+
responseMode: 'auto',
|
|
1012
|
+
onerror: error => console.error('[mcp-v2]', error)
|
|
1013
|
+
}
|
|
1014
|
+
);
|
|
1015
|
+
const modernNodeHandler = toNodeHandler(modernMcpHandler, {
|
|
1016
|
+
onerror: error => console.error('[mcp-v2-node]', error)
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
const transports = {};
|
|
1020
|
+
|
|
1021
|
+
async function createTransport(req) {
|
|
1022
|
+
let transport;
|
|
1023
|
+
const server = createProxyServer({
|
|
1024
|
+
accountId: req.auth?.accountId || null,
|
|
1025
|
+
activitySessionId: req.auth?.activitySessionId || null,
|
|
1026
|
+
callerKey: callerKeyFromRequest(req),
|
|
1027
|
+
callerCategory: callerCategoryFromRequest(req),
|
|
1028
|
+
callerSubject: callerSubjectFromRequest(req)
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
if (!useStatefulMcpSessions) {
|
|
1032
|
+
transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
1033
|
+
transport.onclose = () => activeProxyServers.delete(server);
|
|
1034
|
+
await server.connect(transport);
|
|
1035
|
+
return transport;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
transport = new NodeStreamableHTTPServerTransport({
|
|
1039
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1040
|
+
onsessioninitialized: id => {
|
|
1041
|
+
transports[id] = transport;
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
transport.onclose = () => {
|
|
1046
|
+
activeProxyServers.delete(server);
|
|
1047
|
+
const sid = transport.sessionId;
|
|
1048
|
+
if (sid && transports[sid]) delete transports[sid];
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
await server.connect(transport);
|
|
1052
|
+
return transport;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const mcpPostHandler = async (req, res) => {
|
|
1056
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
1057
|
+
const requestSummary = summarizeRequestBody(req.body);
|
|
1058
|
+
console.log(`[mcp-post:start] ${requestSummary} session=${sessionId || 'none'}`);
|
|
1059
|
+
|
|
1060
|
+
try {
|
|
1061
|
+
let transport;
|
|
1062
|
+
if (useStatefulMcpSessions && sessionId && transports[sessionId]) {
|
|
1063
|
+
transport = transports[sessionId];
|
|
1064
|
+
} else if (
|
|
1065
|
+
!useStatefulMcpSessions ||
|
|
1066
|
+
shouldCreateTransportForRequest(sessionId, req.body, transports) ||
|
|
1067
|
+
(!sessionId && isInitializeRequest(req.body))
|
|
1068
|
+
) {
|
|
1069
|
+
transport = await createTransport(req);
|
|
1070
|
+
await transport.handleRequest(req, res, req.body);
|
|
1071
|
+
return;
|
|
1072
|
+
} else {
|
|
1073
|
+
console.log(`[mcp-post:reject] ${requestSummary} reason=invalid-session session=${sessionId || 'none'}`);
|
|
1074
|
+
res.status(400).json({
|
|
1075
|
+
jsonrpc: '2.0',
|
|
1076
|
+
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
|
|
1077
|
+
id: null
|
|
1078
|
+
});
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
await transport.handleRequest(req, res, req.body);
|
|
1083
|
+
console.log(`[mcp-post:finish] ${requestSummary} session=${sessionId || 'none'}`);
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
console.error('Error handling MCP POST request:', error);
|
|
1086
|
+
if (!res.headersSent) {
|
|
1087
|
+
res.status(500).json({
|
|
1088
|
+
jsonrpc: '2.0',
|
|
1089
|
+
error: { code: -32603, message: 'Internal server error' },
|
|
1090
|
+
id: null
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
const mcpGetHandler = async (req, res) => {
|
|
1097
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
1098
|
+
console.log(`[mcp-get:start] session=${sessionId || 'none'}`);
|
|
1099
|
+
if (!sessionId || !transports[sessionId]) {
|
|
1100
|
+
console.log(`[mcp-get:reject] reason=invalid-session session=${sessionId || 'none'}`);
|
|
1101
|
+
res.status(400).send('Invalid or missing session ID');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
await transports[sessionId].handleRequest(req, res);
|
|
1106
|
+
console.log(`[mcp-get:finish] session=${sessionId || 'none'}`);
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
const mcpDeleteHandler = async (req, res) => {
|
|
1110
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
1111
|
+
console.log(`[mcp-delete:start] session=${sessionId || 'none'}`);
|
|
1112
|
+
if (!sessionId || !transports[sessionId]) {
|
|
1113
|
+
console.log(`[mcp-delete:reject] reason=invalid-session session=${sessionId || 'none'}`);
|
|
1114
|
+
res.status(400).send('Invalid or missing session ID');
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
await transports[sessionId].handleRequest(req, res);
|
|
1119
|
+
console.log(`[mcp-delete:finish] session=${sessionId || 'none'}`);
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
app.get('/healthz', (_req, res) => {
|
|
1123
|
+
const skills = skillRegistry.health();
|
|
1124
|
+
res.json({
|
|
1125
|
+
ok: skills.status === 'healthy',
|
|
1126
|
+
enableFilesystem,
|
|
1127
|
+
enableShell,
|
|
1128
|
+
skills
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
async function unifiedMcpHandler(req, res) {
|
|
1133
|
+
if (!useStatefulMcpSessions) {
|
|
1134
|
+
await modernNodeHandler(req, res, req.body);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const probe = await toWebRequest(req, req.body);
|
|
1139
|
+
if (!(await isLegacyRequest(probe))) {
|
|
1140
|
+
await modernNodeHandler(req, res, req.body);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (req.method === 'POST') {
|
|
1145
|
+
await mcpPostHandler(req, res);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (req.method === 'GET') {
|
|
1149
|
+
await mcpGetHandler(req, res);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (req.method === 'DELETE') {
|
|
1153
|
+
await mcpDeleteHandler(req, res);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
res.status(405).end();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
app.all('/mcp', mcpAuthMiddleware, unifiedMcpHandler);
|
|
1160
|
+
|
|
1161
|
+
let localDevice = null;
|
|
1162
|
+
let shutdownPromise = null;
|
|
1163
|
+
const serverInstance = app.listen(gatewayPort, gatewayHost);
|
|
1164
|
+
let loopbackInstance = null;
|
|
1165
|
+
|
|
1166
|
+
function shutdown() {
|
|
1167
|
+
// Keep the same failed result for every caller. A previous failed cleanup must
|
|
1168
|
+
// never look like a stopped runtime and allow its state lease to be released.
|
|
1169
|
+
shutdownPromise ??= (async () => {
|
|
1170
|
+
const failures = [];
|
|
1171
|
+
async function attempt(resource, cleanup) {
|
|
1172
|
+
try { await cleanup(); }
|
|
1173
|
+
catch (error) { failures.push({ resource, error }); }
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
await attempt('embedded-device', () => localDevice?.close());
|
|
1177
|
+
const listeners = [serverInstance, loopbackInstance].filter(Boolean);
|
|
1178
|
+
const serverClosed = Promise.all(listeners.map(listener => new Promise(resolve => {
|
|
1179
|
+
try {
|
|
1180
|
+
listener.close(error => {
|
|
1181
|
+
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') failures.push({ resource: 'http-server', error });
|
|
1182
|
+
resolve();
|
|
1183
|
+
});
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
failures.push({ resource: 'http-server', error });
|
|
1186
|
+
resolve();
|
|
1187
|
+
}
|
|
1188
|
+
})));
|
|
1189
|
+
for (const listener of listeners) await attempt('http-idle-connections', () => listener.closeIdleConnections?.());
|
|
1190
|
+
await attempt('workspace-registry', () => workspaceRegistry.close());
|
|
1191
|
+
await attempt('tool-metrics', () => toolMetrics.close());
|
|
1192
|
+
await attempt('device-audit', () => deviceAudit.close());
|
|
1193
|
+
await attempt('device-broker', () => deviceBroker.shutdown());
|
|
1194
|
+
for (const transport of Object.values(transports)) {
|
|
1195
|
+
await attempt('mcp-transport', () => transport.close());
|
|
1196
|
+
}
|
|
1197
|
+
for (const proxy of activeProxyServers) {
|
|
1198
|
+
await attempt('mcp-proxy', async () => {
|
|
1199
|
+
await proxy.close();
|
|
1200
|
+
activeProxyServers.delete(proxy);
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
for (const listener of listeners) await attempt('http-connections', () => listener.closeAllConnections?.());
|
|
1204
|
+
await serverClosed;
|
|
1205
|
+
await attempt('oauth-state', () => oauthStateStore.close());
|
|
1206
|
+
await attempt('accounts', () => accountStore.close());
|
|
1207
|
+
await attempt('device-usage', () => deviceUsageStore.close());
|
|
1208
|
+
await attempt('device-pairings', () => devicePairingStore.close());
|
|
1209
|
+
await attempt('device-store', () => deviceStore.close());
|
|
1210
|
+
await attempt('upstreams', () => externalMcpManager.shutdown());
|
|
1211
|
+
await attempt('device-core', () => closeOwnedCore());
|
|
1212
|
+
resourcesClosed = failures.length === 0;
|
|
1213
|
+
if (failures.length === 1) throw failures[0].error;
|
|
1214
|
+
if (failures.length > 1) {
|
|
1215
|
+
const error = new AggregateError(failures.map(failure => failure.error),
|
|
1216
|
+
'Server cleanup failed for: ' + failures.map(failure => failure.resource).join(', '));
|
|
1217
|
+
error.code = 'SERVER_CLEANUP_FAILED';
|
|
1218
|
+
throw error;
|
|
1219
|
+
}
|
|
1220
|
+
})();
|
|
1221
|
+
return shutdownPromise;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
try {
|
|
1225
|
+
await new Promise((resolve, reject) => {
|
|
1226
|
+
serverInstance.once('listening', resolve);
|
|
1227
|
+
serverInstance.once('error', reject);
|
|
1228
|
+
});
|
|
1229
|
+
deviceBroker.attach(serverInstance);
|
|
1230
|
+
if (loopbackPort !== null) {
|
|
1231
|
+
// The same authenticated app and database serve both ports. Never bypass account authorization.
|
|
1232
|
+
// Only bind loopback: a secondary port must not become an additional public listener.
|
|
1233
|
+
// The old 8101 Cloudflare tunnel can still reach the local TCP listener.
|
|
1234
|
+
// Reject routed/proxied requests: only direct same-machine HTTP clients may use this port.
|
|
1235
|
+
loopbackInstance = http.createServer((req, res) => {
|
|
1236
|
+
const hostname = String(req.headers.host || '').split(':')[0].toLowerCase();
|
|
1237
|
+
const loopbackHost = hostname === '127.0.0.1' || hostname === 'localhost';
|
|
1238
|
+
const proxied = Boolean(req.headers['cf-connecting-ip'] || req.headers['cf-ray'] || req.headers['x-forwarded-for'] || req.headers['x-forwarded-host']);
|
|
1239
|
+
if (!loopbackHost || proxied) {
|
|
1240
|
+
res.writeHead(421, { 'content-type': 'text/plain; charset=utf-8' });
|
|
1241
|
+
res.end('Local MCP listener requires direct loopback access');
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
app(req, res);
|
|
1245
|
+
}).listen(loopbackPort, '127.0.0.1');
|
|
1246
|
+
await new Promise((resolve, reject) => {
|
|
1247
|
+
loopbackInstance.once('listening', resolve);
|
|
1248
|
+
loopbackInstance.once('error', reject);
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
localDevice = await createEmbeddedDeviceController({
|
|
1252
|
+
broker: deviceBroker, deviceStore, pairingStore: devicePairingStore,
|
|
1253
|
+
identity, core, deviceName: options.deviceName,
|
|
1254
|
+
baseUrl: normalizeBaseUrl(advertisedUrl) || fallbackBaseUrl,
|
|
1255
|
+
onPairing: options.onPairing,
|
|
1256
|
+
pollIntervalMs: options.pairingPollIntervalMs
|
|
1257
|
+
});
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
try { await shutdown(); } catch { /* preserve the startup error */ }
|
|
1260
|
+
throw error;
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
app, server: serverInstance,
|
|
1264
|
+
baseUrl: normalizeBaseUrl(advertisedUrl) || fallbackBaseUrl,
|
|
1265
|
+
loopbackUrl: loopbackPort === null ? null : `http://127.0.0.1:${loopbackPort}/mcp`,
|
|
1266
|
+
broker: deviceBroker, accounts: accountStore, pairings: devicePairingStore,
|
|
1267
|
+
localDevice, dbPath: gatewayDbPath,
|
|
1268
|
+
refreshCapabilities: () => localDevice.refreshCapabilities(),
|
|
1269
|
+
listTools: listMergedTools,
|
|
1270
|
+
close: shutdown
|
|
1271
|
+
};
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
if (!resourcesClosed) {
|
|
1274
|
+
for (const cleanup of startupCleanups.reverse()) {
|
|
1275
|
+
try { await cleanup(); } catch { /* preserve the startup error */ }
|
|
1276
|
+
}
|
|
1277
|
+
try { await closeOwnedCore(); } catch { /* preserve the startup error */ }
|
|
1278
|
+
}
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1281
|
+
}
|