@obra-studio/figma-console-mcp 1.32.0
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/LICENSE +21 -0
- package/README.md +879 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
- package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
- package/dist/apps/design-system-dashboard/server.d.ts +24 -0
- package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/server.js +160 -0
- package/dist/apps/design-system-dashboard/server.js.map +1 -0
- package/dist/apps/token-browser/server.d.ts +26 -0
- package/dist/apps/token-browser/server.d.ts.map +1 -0
- package/dist/apps/token-browser/server.js +137 -0
- package/dist/apps/token-browser/server.js.map +1 -0
- package/dist/browser/base.d.ts +58 -0
- package/dist/browser/base.d.ts.map +1 -0
- package/dist/browser/base.js +6 -0
- package/dist/browser/base.js.map +1 -0
- package/dist/browser/local.d.ts +87 -0
- package/dist/browser/local.d.ts.map +1 -0
- package/dist/browser/local.js +318 -0
- package/dist/browser/local.js.map +1 -0
- package/dist/core/accessibility-tools.d.ts +21 -0
- package/dist/core/accessibility-tools.d.ts.map +1 -0
- package/dist/core/accessibility-tools.js +307 -0
- package/dist/core/accessibility-tools.js.map +1 -0
- package/dist/core/annotation-tools.d.ts +14 -0
- package/dist/core/annotation-tools.d.ts.map +1 -0
- package/dist/core/annotation-tools.js +231 -0
- package/dist/core/annotation-tools.js.map +1 -0
- package/dist/core/autodocs-tools.d.ts +7 -0
- package/dist/core/autodocs-tools.d.ts.map +1 -0
- package/dist/core/autodocs-tools.js +195 -0
- package/dist/core/autodocs-tools.js.map +1 -0
- package/dist/core/comment-tools.d.ts +11 -0
- package/dist/core/comment-tools.d.ts.map +1 -0
- package/dist/core/comment-tools.js +293 -0
- package/dist/core/comment-tools.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +154 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/console-monitor.d.ts +82 -0
- package/dist/core/console-monitor.d.ts.map +1 -0
- package/dist/core/console-monitor.js +428 -0
- package/dist/core/console-monitor.js.map +1 -0
- package/dist/core/deep-component-tools.d.ts +14 -0
- package/dist/core/deep-component-tools.d.ts.map +1 -0
- package/dist/core/deep-component-tools.js +129 -0
- package/dist/core/deep-component-tools.js.map +1 -0
- package/dist/core/design-code-tools.d.ts +116 -0
- package/dist/core/design-code-tools.d.ts.map +1 -0
- package/dist/core/design-code-tools.js +2751 -0
- package/dist/core/design-code-tools.js.map +1 -0
- package/dist/core/design-system-manifest.d.ts +272 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -0
- package/dist/core/design-system-manifest.js +261 -0
- package/dist/core/design-system-manifest.js.map +1 -0
- package/dist/core/design-system-tools.d.ts +67 -0
- package/dist/core/design-system-tools.d.ts.map +1 -0
- package/dist/core/design-system-tools.js +874 -0
- package/dist/core/design-system-tools.js.map +1 -0
- package/dist/core/diagnose-tool.d.ts +33 -0
- package/dist/core/diagnose-tool.d.ts.map +1 -0
- package/dist/core/diagnose-tool.js +97 -0
- package/dist/core/diagnose-tool.js.map +1 -0
- package/dist/core/diff/changelog-formatter.d.ts +35 -0
- package/dist/core/diff/changelog-formatter.d.ts.map +1 -0
- package/dist/core/diff/changelog-formatter.js +276 -0
- package/dist/core/diff/changelog-formatter.js.map +1 -0
- package/dist/core/diff/diff-engine.d.ts +127 -0
- package/dist/core/diff/diff-engine.d.ts.map +1 -0
- package/dist/core/diff/diff-engine.js +335 -0
- package/dist/core/diff/diff-engine.js.map +1 -0
- package/dist/core/diff/property-compare.d.ts +19 -0
- package/dist/core/diff/property-compare.d.ts.map +1 -0
- package/dist/core/diff/property-compare.js +37 -0
- package/dist/core/diff/property-compare.js.map +1 -0
- package/dist/core/diff/version-cache.d.ts +40 -0
- package/dist/core/diff/version-cache.d.ts.map +1 -0
- package/dist/core/diff/version-cache.js +75 -0
- package/dist/core/diff/version-cache.js.map +1 -0
- package/dist/core/enrichment/enrichment-service.d.ts +52 -0
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
- package/dist/core/enrichment/enrichment-service.js +369 -0
- package/dist/core/enrichment/enrichment-service.js.map +1 -0
- package/dist/core/enrichment/index.d.ts +8 -0
- package/dist/core/enrichment/index.d.ts.map +1 -0
- package/dist/core/enrichment/index.js +8 -0
- package/dist/core/enrichment/index.js.map +1 -0
- package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
- package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
- package/dist/core/enrichment/relationship-mapper.js +352 -0
- package/dist/core/enrichment/relationship-mapper.js.map +1 -0
- package/dist/core/enrichment/style-resolver.d.ts +80 -0
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
- package/dist/core/enrichment/style-resolver.js +327 -0
- package/dist/core/enrichment/style-resolver.js.map +1 -0
- package/dist/core/figjam-tools.d.ts +8 -0
- package/dist/core/figjam-tools.d.ts.map +1 -0
- package/dist/core/figjam-tools.js +548 -0
- package/dist/core/figjam-tools.js.map +1 -0
- package/dist/core/figma-api.d.ts +245 -0
- package/dist/core/figma-api.d.ts.map +1 -0
- package/dist/core/figma-api.js +446 -0
- package/dist/core/figma-api.js.map +1 -0
- package/dist/core/figma-connector.d.ts +180 -0
- package/dist/core/figma-connector.d.ts.map +1 -0
- package/dist/core/figma-connector.js +8 -0
- package/dist/core/figma-connector.js.map +1 -0
- package/dist/core/figma-desktop-connector.d.ts +312 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -0
- package/dist/core/figma-desktop-connector.js +1298 -0
- package/dist/core/figma-desktop-connector.js.map +1 -0
- package/dist/core/figma-reconstruction-spec.d.ts +166 -0
- package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
- package/dist/core/figma-reconstruction-spec.js +403 -0
- package/dist/core/figma-reconstruction-spec.js.map +1 -0
- package/dist/core/figma-style-extractor.d.ts +76 -0
- package/dist/core/figma-style-extractor.d.ts.map +1 -0
- package/dist/core/figma-style-extractor.js +312 -0
- package/dist/core/figma-style-extractor.js.map +1 -0
- package/dist/core/figma-tools.d.ts +22 -0
- package/dist/core/figma-tools.d.ts.map +1 -0
- package/dist/core/figma-tools.js +3187 -0
- package/dist/core/figma-tools.js.map +1 -0
- package/dist/core/identity.d.ts +41 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +97 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/library-tools.d.ts +17 -0
- package/dist/core/library-tools.d.ts.map +1 -0
- package/dist/core/library-tools.js +581 -0
- package/dist/core/library-tools.js.map +1 -0
- package/dist/core/logger.d.ts +22 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/port-discovery.d.ts +171 -0
- package/dist/core/port-discovery.d.ts.map +1 -0
- package/dist/core/port-discovery.js +563 -0
- package/dist/core/port-discovery.js.map +1 -0
- package/dist/core/resolve-package-root.d.ts +2 -0
- package/dist/core/resolve-package-root.d.ts.map +1 -0
- package/dist/core/resolve-package-root.js +12 -0
- package/dist/core/resolve-package-root.js.map +1 -0
- package/dist/core/slides-tools.d.ts +8 -0
- package/dist/core/slides-tools.d.ts.map +1 -0
- package/dist/core/slides-tools.js +715 -0
- package/dist/core/slides-tools.js.map +1 -0
- package/dist/core/snippet-injector.d.ts +24 -0
- package/dist/core/snippet-injector.d.ts.map +1 -0
- package/dist/core/snippet-injector.js +97 -0
- package/dist/core/snippet-injector.js.map +1 -0
- package/dist/core/tokens/alias-resolver.d.ts +55 -0
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
- package/dist/core/tokens/alias-resolver.js +136 -0
- package/dist/core/tokens/alias-resolver.js.map +1 -0
- package/dist/core/tokens/config.d.ts +87 -0
- package/dist/core/tokens/config.d.ts.map +1 -0
- package/dist/core/tokens/config.js +285 -0
- package/dist/core/tokens/config.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +81 -0
- package/dist/core/tokens/figma-converter.d.ts.map +1 -0
- package/dist/core/tokens/figma-converter.js +196 -0
- package/dist/core/tokens/figma-converter.js.map +1 -0
- package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
- package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
- package/dist/core/tokens/formatters/css-vars.js +330 -0
- package/dist/core/tokens/formatters/css-vars.js.map +1 -0
- package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
- package/dist/core/tokens/formatters/dtcg.js +301 -0
- package/dist/core/tokens/formatters/dtcg.js.map +1 -0
- package/dist/core/tokens/formatters/index.d.ts +30 -0
- package/dist/core/tokens/formatters/index.d.ts.map +1 -0
- package/dist/core/tokens/formatters/index.js +46 -0
- package/dist/core/tokens/formatters/index.js.map +1 -0
- package/dist/core/tokens/formatters/json.d.ts +37 -0
- package/dist/core/tokens/formatters/json.d.ts.map +1 -0
- package/dist/core/tokens/formatters/json.js +188 -0
- package/dist/core/tokens/formatters/json.js.map +1 -0
- package/dist/core/tokens/formatters/less.d.ts +4 -0
- package/dist/core/tokens/formatters/less.d.ts.map +1 -0
- package/dist/core/tokens/formatters/less.js +5 -0
- package/dist/core/tokens/formatters/less.js.map +1 -0
- package/dist/core/tokens/formatters/scss.d.ts +26 -0
- package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
- package/dist/core/tokens/formatters/scss.js +253 -0
- package/dist/core/tokens/formatters/scss.js.map +1 -0
- package/dist/core/tokens/formatters/stubs.d.ts +9 -0
- package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
- package/dist/core/tokens/formatters/stubs.js +14 -0
- package/dist/core/tokens/formatters/stubs.js.map +1 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.js +208 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
- package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
- package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
- package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v4.js +331 -0
- package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
- package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
- package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tokens-studio.js +251 -0
- package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
- package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
- package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
- package/dist/core/tokens/formatters/ts-module.js +199 -0
- package/dist/core/tokens/formatters/ts-module.js.map +1 -0
- package/dist/core/tokens/index.d.ts +17 -0
- package/dist/core/tokens/index.d.ts.map +1 -0
- package/dist/core/tokens/index.js +16 -0
- package/dist/core/tokens/index.js.map +1 -0
- package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
- package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
- package/dist/core/tokens/parsers/css-vars.js +5 -0
- package/dist/core/tokens/parsers/css-vars.js.map +1 -0
- package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
- package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
- package/dist/core/tokens/parsers/dtcg.js +254 -0
- package/dist/core/tokens/parsers/dtcg.js.map +1 -0
- package/dist/core/tokens/parsers/index.d.ts +37 -0
- package/dist/core/tokens/parsers/index.d.ts.map +1 -0
- package/dist/core/tokens/parsers/index.js +139 -0
- package/dist/core/tokens/parsers/index.js.map +1 -0
- package/dist/core/tokens/parsers/json.d.ts +4 -0
- package/dist/core/tokens/parsers/json.d.ts.map +1 -0
- package/dist/core/tokens/parsers/json.js +8 -0
- package/dist/core/tokens/parsers/json.js.map +1 -0
- package/dist/core/tokens/parsers/scss.d.ts +3 -0
- package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
- package/dist/core/tokens/parsers/scss.js +5 -0
- package/dist/core/tokens/parsers/scss.js.map +1 -0
- package/dist/core/tokens/parsers/stubs.d.ts +15 -0
- package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
- package/dist/core/tokens/parsers/stubs.js +21 -0
- package/dist/core/tokens/parsers/stubs.js.map +1 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
- package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
- package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
- package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
- package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
- package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
- package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tokens-studio.js +5 -0
- package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
- package/dist/core/tokens/schemas.d.ts +31 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -0
- package/dist/core/tokens/schemas.js +149 -0
- package/dist/core/tokens/schemas.js.map +1 -0
- package/dist/core/tokens/transforms/color.d.ts +9 -0
- package/dist/core/tokens/transforms/color.d.ts.map +1 -0
- package/dist/core/tokens/transforms/color.js +13 -0
- package/dist/core/tokens/transforms/color.js.map +1 -0
- package/dist/core/tokens/transforms/index.d.ts +36 -0
- package/dist/core/tokens/transforms/index.d.ts.map +1 -0
- package/dist/core/tokens/transforms/index.js +30 -0
- package/dist/core/tokens/transforms/index.js.map +1 -0
- package/dist/core/tokens/transforms/size.d.ts +7 -0
- package/dist/core/tokens/transforms/size.d.ts.map +1 -0
- package/dist/core/tokens/transforms/size.js +8 -0
- package/dist/core/tokens/transforms/size.js.map +1 -0
- package/dist/core/tokens/types.d.ts +228 -0
- package/dist/core/tokens/types.d.ts.map +1 -0
- package/dist/core/tokens/types.js +19 -0
- package/dist/core/tokens/types.js.map +1 -0
- package/dist/core/tokens-tools.d.ts +42 -0
- package/dist/core/tokens-tools.d.ts.map +1 -0
- package/dist/core/tokens-tools.js +860 -0
- package/dist/core/tokens-tools.js.map +1 -0
- package/dist/core/types/design-code.d.ts +271 -0
- package/dist/core/types/design-code.d.ts.map +1 -0
- package/dist/core/types/design-code.js +5 -0
- package/dist/core/types/design-code.js.map +1 -0
- package/dist/core/types/enriched.d.ts +213 -0
- package/dist/core/types/enriched.d.ts.map +1 -0
- package/dist/core/types/enriched.js +6 -0
- package/dist/core/types/enriched.js.map +1 -0
- package/dist/core/types/index.d.ts +104 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +5 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/core/variable-resolver.d.ts +45 -0
- package/dist/core/variable-resolver.d.ts.map +1 -0
- package/dist/core/variable-resolver.js +86 -0
- package/dist/core/variable-resolver.js.map +1 -0
- package/dist/core/version-tools.d.ts +59 -0
- package/dist/core/version-tools.d.ts.map +1 -0
- package/dist/core/version-tools.js +1159 -0
- package/dist/core/version-tools.js.map +1 -0
- package/dist/core/websocket-connector.d.ts +187 -0
- package/dist/core/websocket-connector.d.ts.map +1 -0
- package/dist/core/websocket-connector.js +378 -0
- package/dist/core/websocket-connector.js.map +1 -0
- package/dist/core/websocket-server.js +866 -0
- package/dist/core/websocket-server.js.map +1 -0
- package/dist/core/write-tools.d.ts +7 -0
- package/dist/core/write-tools.d.ts.map +1 -0
- package/dist/core/write-tools.js +2172 -0
- package/dist/core/write-tools.js.map +1 -0
- package/dist/local.d.ts +95 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +3036 -0
- package/dist/local.js.map +1 -0
- package/dist/vendor/obra-autodocs/autodocs-body.generated.d.ts +2 -0
- package/dist/vendor/obra-autodocs/autodocs-body.generated.d.ts.map +1 -0
- package/dist/vendor/obra-autodocs/autodocs-body.generated.js +6 -0
- package/dist/vendor/obra-autodocs/autodocs-body.generated.js.map +1 -0
- package/figma-desktop-bridge/README.md +365 -0
- package/figma-desktop-bridge/code.js +6504 -0
- package/figma-desktop-bridge/icon.png +0 -0
- package/figma-desktop-bridge/manifest.json +67 -0
- package/figma-desktop-bridge/ui.html +2441 -0
- package/package.json +98 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port Discovery Module
|
|
3
|
+
*
|
|
4
|
+
* Handles dynamic WebSocket port assignment with range-based fallback.
|
|
5
|
+
* When the preferred port (default 9223) is taken by another MCP server instance
|
|
6
|
+
* (e.g., Claude Desktop Chat tab vs Code tab), the server automatically tries
|
|
7
|
+
* the next port in a fixed range (9223-9232).
|
|
8
|
+
*
|
|
9
|
+
* Port advertisement files are written to /tmp so the Figma plugin can discover
|
|
10
|
+
* which port to connect to. Each instance writes its own file with PID for
|
|
11
|
+
* stale-file detection.
|
|
12
|
+
*
|
|
13
|
+
* Zombie process detection:
|
|
14
|
+
* Active servers refresh their port file every 30s (heartbeat).
|
|
15
|
+
* On startup, cleanupStalePortFiles() detects zombies via:
|
|
16
|
+
* 1. Dead PID — process no longer exists (existing behavior)
|
|
17
|
+
* 2. Stale heartbeat — lastSeen older than 5 minutes (process frozen/hung)
|
|
18
|
+
* 3. Age ceiling — startedAt older than 4 hours with no heartbeat (pre-v1.12 compat)
|
|
19
|
+
* Zombie processes are terminated with SIGTERM to free their ports.
|
|
20
|
+
*
|
|
21
|
+
* Data flow:
|
|
22
|
+
* Server binds port → writes /tmp/figma-console-mcp-{port}.json
|
|
23
|
+
* Server heartbeat → refreshes lastSeen every 30s
|
|
24
|
+
* Plugin scans ports 9223-9232 → connects to first responding server
|
|
25
|
+
* External tools read port files for discovery
|
|
26
|
+
*/
|
|
27
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync, readdirSync } from 'fs';
|
|
28
|
+
import { join } from 'path';
|
|
29
|
+
import { tmpdir } from 'os';
|
|
30
|
+
import { createChildLogger } from './logger.js';
|
|
31
|
+
const logger = createChildLogger({ component: 'port-discovery' });
|
|
32
|
+
/** Default preferred WebSocket port */
|
|
33
|
+
export const DEFAULT_WS_PORT = 9223;
|
|
34
|
+
/** Number of ports in the fallback range (9223-9232 = 10 ports) */
|
|
35
|
+
export const PORT_RANGE_SIZE = 10;
|
|
36
|
+
/** Prefix for port advertisement files in /tmp */
|
|
37
|
+
const PORT_FILE_PREFIX = 'figma-console-mcp-';
|
|
38
|
+
/** Directory for port advertisement files */
|
|
39
|
+
const PORT_FILE_DIR = tmpdir();
|
|
40
|
+
/** Maximum age before a port file without heartbeat is considered stale (4 hours) */
|
|
41
|
+
export const MAX_PORT_FILE_AGE_MS = 4 * 60 * 60 * 1000;
|
|
42
|
+
/** Maximum time since last heartbeat before a process is considered stale (5 minutes) */
|
|
43
|
+
export const HEARTBEAT_STALE_MS = 5 * 60 * 1000;
|
|
44
|
+
/** Grace period after SIGTERM before escalating to SIGKILL (ms) */
|
|
45
|
+
export const TERMINATE_GRACE_MS = 400;
|
|
46
|
+
/**
|
|
47
|
+
* Minimum process age before an orphan may be reaped (ms). Protects a sibling
|
|
48
|
+
* server that is mid-startup — it has bound a port but not yet written its
|
|
49
|
+
* advertisement file, so it would otherwise look like an orphan. By the time a
|
|
50
|
+
* real server is this old it has advertised and is in the known-PID set.
|
|
51
|
+
*/
|
|
52
|
+
export const ORPHAN_MIN_AGE_MS = 60 * 1000;
|
|
53
|
+
/** Interval for the periodic background reaper (ms) */
|
|
54
|
+
export const REAP_INTERVAL_MS = 5 * 60 * 1000;
|
|
55
|
+
/** Minimum age before an instance can be evicted as last resort (2 minutes) */
|
|
56
|
+
export const EVICTION_MIN_AGE_MS = 2 * 60 * 1000;
|
|
57
|
+
/** Interval between heartbeat refreshes (30 seconds) */
|
|
58
|
+
export const HEARTBEAT_INTERVAL_MS = 30 * 1000;
|
|
59
|
+
/**
|
|
60
|
+
* Try to bind a WebSocket server to ports in a range, starting from the preferred port.
|
|
61
|
+
* Returns the first port that binds successfully.
|
|
62
|
+
*
|
|
63
|
+
* @param preferredPort - The port to try first (default 9223)
|
|
64
|
+
* @param host - The host to bind to (default 'localhost')
|
|
65
|
+
* @returns The actual port that was bound
|
|
66
|
+
* @throws If all ports in the range are exhausted
|
|
67
|
+
*/
|
|
68
|
+
export function getPortRange(preferredPort = DEFAULT_WS_PORT) {
|
|
69
|
+
const ports = [];
|
|
70
|
+
for (let i = 0; i < PORT_RANGE_SIZE; i++) {
|
|
71
|
+
ports.push(preferredPort + i);
|
|
72
|
+
}
|
|
73
|
+
return ports;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get the file path for a port advertisement file.
|
|
77
|
+
*/
|
|
78
|
+
export function getPortFilePath(port) {
|
|
79
|
+
return join(PORT_FILE_DIR, `${PORT_FILE_PREFIX}${port}.json`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Write a port advertisement file so clients can discover this server instance.
|
|
83
|
+
* Includes PID for stale-file detection and lastSeen for heartbeat tracking.
|
|
84
|
+
*/
|
|
85
|
+
export function advertisePort(port, host = 'localhost') {
|
|
86
|
+
const now = new Date().toISOString();
|
|
87
|
+
const data = {
|
|
88
|
+
port,
|
|
89
|
+
pid: process.pid,
|
|
90
|
+
host,
|
|
91
|
+
startedAt: now,
|
|
92
|
+
lastSeen: now,
|
|
93
|
+
};
|
|
94
|
+
const filePath = getPortFilePath(port);
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
97
|
+
logger.info({ port, filePath }, 'Port advertised');
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.warn({ port, filePath, error }, 'Failed to write port advertisement file');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Refresh the lastSeen timestamp in a port advertisement file.
|
|
105
|
+
* Called periodically as a heartbeat to prove this server is still active.
|
|
106
|
+
* Non-fatal — heartbeat failures are silently ignored.
|
|
107
|
+
*/
|
|
108
|
+
export function refreshPortAdvertisement(port) {
|
|
109
|
+
const filePath = getPortFilePath(port);
|
|
110
|
+
try {
|
|
111
|
+
if (!existsSync(filePath))
|
|
112
|
+
return;
|
|
113
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
114
|
+
const data = JSON.parse(raw);
|
|
115
|
+
// Only refresh our own port file
|
|
116
|
+
if (data.pid !== process.pid)
|
|
117
|
+
return;
|
|
118
|
+
data.lastSeen = new Date().toISOString();
|
|
119
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Best-effort — heartbeat failures are non-fatal
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Remove the port advertisement file for this instance.
|
|
127
|
+
* Call on clean shutdown.
|
|
128
|
+
*/
|
|
129
|
+
export function unadvertisePort(port) {
|
|
130
|
+
const filePath = getPortFilePath(port);
|
|
131
|
+
try {
|
|
132
|
+
if (existsSync(filePath)) {
|
|
133
|
+
unlinkSync(filePath);
|
|
134
|
+
logger.debug({ port, filePath }, 'Port advertisement removed');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Best-effort cleanup — file may already be gone
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Check if a PID is still alive.
|
|
143
|
+
*/
|
|
144
|
+
function isProcessAlive(pid) {
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 0); // Signal 0 = existence check, doesn't actually kill
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Determine if a port file represents a zombie/stale MCP instance.
|
|
155
|
+
*
|
|
156
|
+
* Detection layers:
|
|
157
|
+
* 1. If lastSeen exists (v1.12+): stale if older than HEARTBEAT_STALE_MS (5 min)
|
|
158
|
+
* 2. If lastSeen is missing (pre-v1.12): stale if startedAt older than MAX_PORT_FILE_AGE_MS (4h)
|
|
159
|
+
*
|
|
160
|
+
* Assumes the owning process IS alive (PID check should happen before calling this).
|
|
161
|
+
*/
|
|
162
|
+
export function isStaleInstance(data) {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
// If heartbeat exists, use it — active servers refresh every 30s
|
|
165
|
+
if (data.lastSeen) {
|
|
166
|
+
const lastSeenAge = now - new Date(data.lastSeen).getTime();
|
|
167
|
+
return lastSeenAge > HEARTBEAT_STALE_MS;
|
|
168
|
+
}
|
|
169
|
+
// No heartbeat (pre-v1.12 instance) — fall back to startup age
|
|
170
|
+
const startedAge = now - new Date(data.startedAt).getTime();
|
|
171
|
+
return startedAge > MAX_PORT_FILE_AGE_MS;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Block the current thread for `ms` milliseconds (synchronous).
|
|
175
|
+
* Used between SIGTERM and SIGKILL so terminateProcess can stay synchronous
|
|
176
|
+
* (its callers — cleanup functions — are synchronous and use execSync).
|
|
177
|
+
*/
|
|
178
|
+
function sleepSyncMs(ms) {
|
|
179
|
+
if (ms <= 0)
|
|
180
|
+
return;
|
|
181
|
+
try {
|
|
182
|
+
// Cross-platform, no child process. SharedArrayBuffer/Atomics are standard in Node 18+.
|
|
183
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
const end = Date.now() + ms;
|
|
187
|
+
while (Date.now() < end) { /* best-effort spin fallback */ }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Elapsed time since a process started, in milliseconds. Returns null if it
|
|
192
|
+
* cannot be determined (process gone, or `ps` unavailable/unparseable).
|
|
193
|
+
*/
|
|
194
|
+
function getProcessAgeMs(pid) {
|
|
195
|
+
if (process.platform === 'win32')
|
|
196
|
+
return null;
|
|
197
|
+
try {
|
|
198
|
+
const { execSync } = require('child_process');
|
|
199
|
+
// `etime` (formatted elapsed time) is portable across macOS and Linux;
|
|
200
|
+
// `etimes` (seconds) is Linux-only — macOS ps rejects it. Format is
|
|
201
|
+
// [[DD-]HH:]MM:SS, e.g. "05:51", "55:27", "01:13:31", "09-14:15:41".
|
|
202
|
+
const out = execSync(`ps -p ${pid} -o etime= 2>/dev/null`, {
|
|
203
|
+
encoding: 'utf-8',
|
|
204
|
+
timeout: 2000,
|
|
205
|
+
}).trim();
|
|
206
|
+
const m = out.match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
|
|
207
|
+
if (!m)
|
|
208
|
+
return null;
|
|
209
|
+
const days = parseInt(m[1] || '0', 10);
|
|
210
|
+
const hours = parseInt(m[2] || '0', 10);
|
|
211
|
+
const mins = parseInt(m[3], 10);
|
|
212
|
+
const secs = parseInt(m[4], 10);
|
|
213
|
+
return ((((days * 24 + hours) * 60 + mins) * 60) + secs) * 1000;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Terminate a process by PID, escalating to SIGKILL if it ignores SIGTERM.
|
|
221
|
+
*
|
|
222
|
+
* A hung MCP server can catch SIGTERM (its shutdown handler runs) yet never
|
|
223
|
+
* reach process.exit() — e.g. when graceful WebSocket/HTTP close blocks on a
|
|
224
|
+
* lingering connection. SIGTERM alone then leaves a zombie holding its port
|
|
225
|
+
* (often with its advertisement file already removed by the handler). We send
|
|
226
|
+
* SIGTERM, wait briefly, and force-kill with SIGKILL if it is still alive.
|
|
227
|
+
*
|
|
228
|
+
* @returns true if the process is confirmed gone afterwards, false if it survived.
|
|
229
|
+
*/
|
|
230
|
+
function terminateProcess(pid, graceMs = TERMINATE_GRACE_MS) {
|
|
231
|
+
// SIGTERM first — give the process a chance to shut down gracefully.
|
|
232
|
+
try {
|
|
233
|
+
process.kill(pid, 'SIGTERM');
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return true; // already gone — nothing to terminate
|
|
237
|
+
}
|
|
238
|
+
// Windows: SIGTERM maps to TerminateProcess (immediate, uncatchable).
|
|
239
|
+
if (process.platform === 'win32')
|
|
240
|
+
return !isProcessAlive(pid);
|
|
241
|
+
// POSIX: let the graceful handler run, then force-kill if it ignored SIGTERM.
|
|
242
|
+
sleepSyncMs(graceMs);
|
|
243
|
+
if (!isProcessAlive(pid))
|
|
244
|
+
return true;
|
|
245
|
+
try {
|
|
246
|
+
process.kill(pid, 'SIGKILL');
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return true; // exited between the check and the kill
|
|
250
|
+
}
|
|
251
|
+
sleepSyncMs(Math.min(graceMs, 200));
|
|
252
|
+
return !isProcessAlive(pid);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Read and validate a port advertisement file.
|
|
256
|
+
* Returns null if the file doesn't exist, is invalid, or the owning process is dead.
|
|
257
|
+
*/
|
|
258
|
+
export function readPortFile(port) {
|
|
259
|
+
const filePath = getPortFilePath(port);
|
|
260
|
+
if (!existsSync(filePath))
|
|
261
|
+
return null;
|
|
262
|
+
try {
|
|
263
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
264
|
+
const data = JSON.parse(raw);
|
|
265
|
+
// Validate the owning process is still alive
|
|
266
|
+
if (!isProcessAlive(data.pid)) {
|
|
267
|
+
logger.debug({ port, pid: data.pid }, 'Stale port file detected (process dead), cleaning up');
|
|
268
|
+
try {
|
|
269
|
+
unlinkSync(filePath);
|
|
270
|
+
}
|
|
271
|
+
catch { /* best-effort */ }
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return data;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Discover all active Figma Console MCP server instances by scanning port files.
|
|
282
|
+
* Validates each file's PID to filter out stale entries.
|
|
283
|
+
*/
|
|
284
|
+
export function discoverActiveInstances(preferredPort = DEFAULT_WS_PORT) {
|
|
285
|
+
const instances = [];
|
|
286
|
+
for (const port of getPortRange(preferredPort)) {
|
|
287
|
+
const data = readPortFile(port);
|
|
288
|
+
if (data) {
|
|
289
|
+
instances.push(data);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return instances;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Clean up stale port files and terminate zombie MCP processes.
|
|
296
|
+
*
|
|
297
|
+
* Runs at startup before port binding. Detects stale instances via:
|
|
298
|
+
* 1. Dead PID — process no longer exists → delete file
|
|
299
|
+
* 2. Zombie process — alive but stale (no heartbeat or expired heartbeat)
|
|
300
|
+
* → send SIGTERM to free the port, then delete file
|
|
301
|
+
* 3. Corrupt file — invalid JSON → delete file
|
|
302
|
+
*/
|
|
303
|
+
export function cleanupStalePortFiles() {
|
|
304
|
+
let cleaned = 0;
|
|
305
|
+
try {
|
|
306
|
+
const files = readdirSync(PORT_FILE_DIR);
|
|
307
|
+
for (const file of files) {
|
|
308
|
+
if (file.startsWith(PORT_FILE_PREFIX) && file.endsWith('.json')) {
|
|
309
|
+
const filePath = join(PORT_FILE_DIR, file);
|
|
310
|
+
try {
|
|
311
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
312
|
+
const data = JSON.parse(raw);
|
|
313
|
+
if (!isProcessAlive(data.pid)) {
|
|
314
|
+
// Dead PID — just clean up the file
|
|
315
|
+
unlinkSync(filePath);
|
|
316
|
+
cleaned++;
|
|
317
|
+
logger.debug({ port: data.port, pid: data.pid }, 'Cleaned up stale port file (dead process)');
|
|
318
|
+
}
|
|
319
|
+
else if (data.pid !== process.pid && isStaleInstance(data)) {
|
|
320
|
+
// Live PID but stale — zombie process, terminate it to free the port
|
|
321
|
+
logger.info({ port: data.port, pid: data.pid, startedAt: data.startedAt, lastSeen: data.lastSeen }, 'Detected zombie MCP process — sending SIGTERM to free port');
|
|
322
|
+
terminateProcess(data.pid);
|
|
323
|
+
try {
|
|
324
|
+
unlinkSync(filePath);
|
|
325
|
+
}
|
|
326
|
+
catch { /* best-effort */ }
|
|
327
|
+
cleaned++;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Corrupt file — remove it
|
|
332
|
+
try {
|
|
333
|
+
unlinkSync(filePath);
|
|
334
|
+
cleaned++;
|
|
335
|
+
}
|
|
336
|
+
catch { /* ignore */ }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// Can't read /tmp — unusual but not fatal
|
|
343
|
+
}
|
|
344
|
+
return cleaned;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Deep scan for orphaned MCP server processes that hold ports but have no port files.
|
|
348
|
+
* These are processes left behind by Claude Desktop when tabs close without proper cleanup.
|
|
349
|
+
*
|
|
350
|
+
* Uses lsof (macOS/Linux) to find PIDs listening on each port in the range,
|
|
351
|
+
* then verifies they're figma-console-mcp before terminating.
|
|
352
|
+
*
|
|
353
|
+
* Call AFTER cleanupStalePortFiles() — that handles the port-file-based cleanup first,
|
|
354
|
+
* then this catches any remaining ghosts.
|
|
355
|
+
*/
|
|
356
|
+
export function cleanupOrphanedProcesses(preferredPort = DEFAULT_WS_PORT, options = {}) {
|
|
357
|
+
// Only supported on macOS/Linux (lsof)
|
|
358
|
+
if (process.platform === 'win32')
|
|
359
|
+
return 0;
|
|
360
|
+
const minAgeMs = options.minAgeMs ?? ORPHAN_MIN_AGE_MS;
|
|
361
|
+
let cleaned = 0;
|
|
362
|
+
const myPid = process.pid;
|
|
363
|
+
const ports = getPortRange(preferredPort);
|
|
364
|
+
// Collect PIDs that have valid port files (known-good servers)
|
|
365
|
+
const knownPids = new Set();
|
|
366
|
+
for (const port of ports) {
|
|
367
|
+
const data = readPortFile(port);
|
|
368
|
+
if (data)
|
|
369
|
+
knownPids.add(data.pid);
|
|
370
|
+
}
|
|
371
|
+
knownPids.add(myPid); // Never kill ourselves
|
|
372
|
+
for (const port of ports) {
|
|
373
|
+
try {
|
|
374
|
+
// Find PIDs listening on this port via lsof
|
|
375
|
+
const { execSync } = require('child_process');
|
|
376
|
+
const output = execSync(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null`, {
|
|
377
|
+
encoding: 'utf-8',
|
|
378
|
+
timeout: 3000,
|
|
379
|
+
}).trim();
|
|
380
|
+
if (!output)
|
|
381
|
+
continue;
|
|
382
|
+
const pids = output.split('\n').map(Number).filter(Boolean);
|
|
383
|
+
for (const pid of pids) {
|
|
384
|
+
if (knownPids.has(pid))
|
|
385
|
+
continue; // Skip known-good servers
|
|
386
|
+
// Verify this is actually a figma-console-mcp process before killing
|
|
387
|
+
try {
|
|
388
|
+
const cmdline = execSync(`ps -p ${pid} -o command= 2>/dev/null`, {
|
|
389
|
+
encoding: 'utf-8',
|
|
390
|
+
timeout: 2000,
|
|
391
|
+
}).trim();
|
|
392
|
+
if (cmdline.includes('figma-console-mcp') || cmdline.includes('figma_console_mcp') || cmdline.includes('local.js')) {
|
|
393
|
+
// Don't reap a sibling that is still starting up (bound a port but
|
|
394
|
+
// hasn't advertised yet). Real orphans are far older than this.
|
|
395
|
+
const ageMs = getProcessAgeMs(pid);
|
|
396
|
+
if (minAgeMs > 0 && ageMs !== null && ageMs < minAgeMs) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
logger.info({ port, pid, command: cmdline.substring(0, 120) }, 'Terminating orphaned MCP server (no port file, holding port)');
|
|
400
|
+
// terminateProcess escalates SIGTERM -> SIGKILL. Only count it as
|
|
401
|
+
// cleaned when the process is confirmed gone, so the log reflects
|
|
402
|
+
// reality (the old code counted attempts and reported success even
|
|
403
|
+
// when a SIGTERM-ignoring zombie survived).
|
|
404
|
+
if (terminateProcess(pid)) {
|
|
405
|
+
cleaned++;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
logger.warn({ port, pid }, 'Failed to terminate orphaned MCP server (survived SIGKILL)');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// Can't read process info — skip to be safe
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// lsof failed for this port — skip
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (cleaned > 0) {
|
|
422
|
+
// Give terminated processes a moment to release their ports
|
|
423
|
+
try {
|
|
424
|
+
const { execSync } = require('child_process');
|
|
425
|
+
execSync('sleep 0.5', { timeout: 2000 });
|
|
426
|
+
}
|
|
427
|
+
catch { /* non-critical */ }
|
|
428
|
+
logger.info({ cleaned }, `Cleaned up ${cleaned} orphaned MCP server process(es)`);
|
|
429
|
+
}
|
|
430
|
+
return cleaned;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Last-resort eviction: terminate the oldest MCP server instance to free a port.
|
|
434
|
+
*
|
|
435
|
+
* Called ONLY when all ports in the range are exhausted after both cleanup phases
|
|
436
|
+
* (cleanupStalePortFiles + cleanupOrphanedProcesses) have already run. This handles
|
|
437
|
+
* the case where old instances are still alive and heartbeating but no longer needed
|
|
438
|
+
* (e.g., from yesterday's Claude Desktop session that was closed without terminating
|
|
439
|
+
* the MCP server process).
|
|
440
|
+
*
|
|
441
|
+
* Safety guards:
|
|
442
|
+
* - Only evicts instances older than EVICTION_MIN_AGE_MS (2 min) to prevent cascade
|
|
443
|
+
* - Never evicts our own PID
|
|
444
|
+
* - Re-reads port file before kill to avoid TOCTOU race
|
|
445
|
+
* - Uses SIGTERM for graceful shutdown
|
|
446
|
+
* - Waits briefly for port release before returning
|
|
447
|
+
*
|
|
448
|
+
* @returns true if an instance was evicted (caller should retry port binding), false otherwise
|
|
449
|
+
*/
|
|
450
|
+
export function evictOldestInstance(preferredPort = DEFAULT_WS_PORT) {
|
|
451
|
+
const myPid = process.pid;
|
|
452
|
+
const ports = getPortRange(preferredPort);
|
|
453
|
+
const candidates = [];
|
|
454
|
+
// Collect all valid port file entries that aren't us
|
|
455
|
+
for (const port of ports) {
|
|
456
|
+
const filePath = getPortFilePath(port);
|
|
457
|
+
try {
|
|
458
|
+
if (!existsSync(filePath))
|
|
459
|
+
continue;
|
|
460
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
461
|
+
const data = JSON.parse(raw);
|
|
462
|
+
if (data.pid === myPid)
|
|
463
|
+
continue; // Never evict ourselves
|
|
464
|
+
if (!isProcessAlive(data.pid)) {
|
|
465
|
+
// Dead process — just clean up the file (port should already be free)
|
|
466
|
+
try {
|
|
467
|
+
unlinkSync(filePath);
|
|
468
|
+
}
|
|
469
|
+
catch { /* best-effort */ }
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
candidates.push({ ...data, filePath });
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// Corrupt or unreadable — skip
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (candidates.length === 0) {
|
|
479
|
+
logger.debug('No eviction candidates — ports may be held by non-MCP processes');
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
// Sort by startedAt ascending — oldest first
|
|
483
|
+
candidates.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime());
|
|
484
|
+
const oldest = candidates[0];
|
|
485
|
+
const ageMs = Date.now() - new Date(oldest.startedAt).getTime();
|
|
486
|
+
// Safety: don't evict instances that started recently (prevents cascade)
|
|
487
|
+
if (ageMs < EVICTION_MIN_AGE_MS) {
|
|
488
|
+
logger.info({ port: oldest.port, pid: oldest.pid, ageSeconds: Math.round(ageMs / 1000) }, 'Oldest instance is too recent to evict — skipping');
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
// Re-read the port file to avoid TOCTOU race
|
|
492
|
+
try {
|
|
493
|
+
const raw = readFileSync(oldest.filePath, 'utf-8');
|
|
494
|
+
const freshData = JSON.parse(raw);
|
|
495
|
+
if (freshData.pid !== oldest.pid) {
|
|
496
|
+
// PID changed between reads — another process took over, skip
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
// File disappeared — port may already be free
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
logger.info({ port: oldest.port, pid: oldest.pid, startedAt: oldest.startedAt, ageHours: Math.round(ageMs / 3600000 * 10) / 10 }, 'Evicting oldest MCP server instance to free port (all ports exhausted)');
|
|
505
|
+
terminateProcess(oldest.pid);
|
|
506
|
+
try {
|
|
507
|
+
unlinkSync(oldest.filePath);
|
|
508
|
+
}
|
|
509
|
+
catch { /* best-effort */ }
|
|
510
|
+
// Brief wait for the port to be released by the OS
|
|
511
|
+
try {
|
|
512
|
+
const { execSync } = require('child_process');
|
|
513
|
+
execSync('sleep 0.5', { timeout: 2000 });
|
|
514
|
+
}
|
|
515
|
+
catch { /* non-critical */ }
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Register process exit handlers to clean up port advertisement file.
|
|
520
|
+
* Should be called once after the port is successfully bound.
|
|
521
|
+
*/
|
|
522
|
+
export function registerPortCleanup(port) {
|
|
523
|
+
const cleanup = () => unadvertisePort(port);
|
|
524
|
+
process.on('exit', cleanup);
|
|
525
|
+
// Re-register SIGINT/SIGTERM to ensure cleanup runs before the
|
|
526
|
+
// existing handlers in local.ts main() call process.exit()
|
|
527
|
+
const originalSigintListeners = process.listeners('SIGINT');
|
|
528
|
+
const originalSigtermListeners = process.listeners('SIGTERM');
|
|
529
|
+
// Prepend our cleanup — it runs first, then existing handlers take over
|
|
530
|
+
process.prependListener('SIGINT', cleanup);
|
|
531
|
+
process.prependListener('SIGTERM', cleanup);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Start a periodic background reaper that re-runs the cleanup passes while this
|
|
535
|
+
* server is alive. Startup-only reaping leaves orphans to accumulate between
|
|
536
|
+
* launches (a sibling client that closes without its server exiting cleanly
|
|
537
|
+
* keeps holding a port until the *next* server starts). Periodic reaping keeps
|
|
538
|
+
* the range clean continuously.
|
|
539
|
+
*
|
|
540
|
+
* Safe against live siblings: they hold fresh advertisement files (heartbeat
|
|
541
|
+
* every 30s) so they are in the known-PID set and skipped, and the age guard in
|
|
542
|
+
* cleanupOrphanedProcesses protects mid-startup siblings.
|
|
543
|
+
*
|
|
544
|
+
* The interval is unref'd so it never keeps the process alive on its own.
|
|
545
|
+
*
|
|
546
|
+
* @returns a stop function that clears the interval.
|
|
547
|
+
*/
|
|
548
|
+
export function startPeriodicReaper(preferredPort = DEFAULT_WS_PORT) {
|
|
549
|
+
const tick = () => {
|
|
550
|
+
try {
|
|
551
|
+
cleanupStalePortFiles();
|
|
552
|
+
cleanupOrphanedProcesses(preferredPort);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
logger.warn({ error }, 'Periodic reaper tick failed');
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const interval = setInterval(tick, REAP_INTERVAL_MS);
|
|
559
|
+
if (typeof interval.unref === 'function')
|
|
560
|
+
interval.unref();
|
|
561
|
+
return () => clearInterval(interval);
|
|
562
|
+
}
|
|
563
|
+
//# sourceMappingURL=port-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-discovery.js","sourceRoot":"","sources":["../../src/core/port-discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-package-root.d.ts","sourceRoot":"","sources":["../../src/core/resolve-package-root.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,YAAY,KAA4D,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the package root directory.
|
|
3
|
+
*
|
|
4
|
+
* Uses import.meta.url (ESM) to compute an absolute path to the package root,
|
|
5
|
+
* independent of process.cwd(). This file is ESM-only; in CJS test contexts
|
|
6
|
+
* (Jest/ts-jest), it is replaced via moduleNameMapper — see jest.config.cjs.
|
|
7
|
+
*/
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
// dist/core/resolve-package-root.js → ../../ = package root
|
|
11
|
+
export const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
12
|
+
//# sourceMappingURL=resolve-package-root.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-package-root.js","sourceRoot":"","sources":["../../src/core/resolve-package-root.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Register Figma Slides tools.
|
|
4
|
+
* These tools only work when the connected file is a Figma Slides presentation (editorType === 'slides').
|
|
5
|
+
* Used by both local mode (src/local.ts) and cloud mode (src/index.ts).
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerSlidesTools(server: McpServer, getDesktopConnector: () => Promise<any>): void;
|
|
8
|
+
//# sourceMappingURL=slides-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slides-tools.d.ts","sourceRoot":"","sources":["../../src/core/slides-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA+DzE;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,SAAS,EACjB,mBAAmB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GACrC,IAAI,CAsuBN"}
|