@runsnative/mcp-server 0.3.0 → 0.5.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.
Files changed (97) hide show
  1. package/README.md +7 -1
  2. package/dist/components-inline.js +15478 -0
  3. package/dist/dev-host.js +68 -0
  4. package/dist/iframe-runtime.js +103 -0
  5. package/dist/index.js +2 -103
  6. package/dist/inferrer/nav-error.js +31 -0
  7. package/dist/inferrer/page-fetcher.js +6 -26
  8. package/dist/server.js +131 -0
  9. package/dist/surface/assets.js +89 -0
  10. package/dist/surface/gathering-card.js +162 -0
  11. package/dist/surface/iframe-runtime-entry.js +8 -0
  12. package/dist/surface/package-surface.js +59 -0
  13. package/dist/tokens/tokens-arctic-frost.css +1320 -0
  14. package/dist/tokens/tokens-botanical-garden.css +1320 -0
  15. package/dist/tokens/tokens-bridgesai.css +1269 -0
  16. package/dist/tokens/tokens-clay-studio.css +1320 -0
  17. package/dist/tokens/tokens-daisy-acid.css +1320 -0
  18. package/dist/tokens/tokens-daisy-aqua.css +1320 -0
  19. package/dist/tokens/tokens-daisy-autumn.css +1320 -0
  20. package/dist/tokens/tokens-daisy-black.css +1321 -0
  21. package/dist/tokens/tokens-daisy-bumblebee.css +1320 -0
  22. package/dist/tokens/tokens-daisy-business.css +1320 -0
  23. package/dist/tokens/tokens-daisy-cmyk.css +1321 -0
  24. package/dist/tokens/tokens-daisy-coffee.css +1320 -0
  25. package/dist/tokens/tokens-daisy-corporate.css +1320 -0
  26. package/dist/tokens/tokens-daisy-cupcake.css +1320 -0
  27. package/dist/tokens/tokens-daisy-cyberpunk.css +1320 -0
  28. package/dist/tokens/tokens-daisy-dark.css +1320 -0
  29. package/dist/tokens/tokens-daisy-dim.css +1320 -0
  30. package/dist/tokens/tokens-daisy-dracula.css +1320 -0
  31. package/dist/tokens/tokens-daisy-emerald.css +1320 -0
  32. package/dist/tokens/tokens-daisy-fantasy.css +1320 -0
  33. package/dist/tokens/tokens-daisy-forest.css +1320 -0
  34. package/dist/tokens/tokens-daisy-garden.css +1320 -0
  35. package/dist/tokens/tokens-daisy-halloween.css +1320 -0
  36. package/dist/tokens/tokens-daisy-lemonade.css +1320 -0
  37. package/dist/tokens/tokens-daisy-light.css +1320 -0
  38. package/dist/tokens/tokens-daisy-lofi.css +1321 -0
  39. package/dist/tokens/tokens-daisy-luxury.css +1320 -0
  40. package/dist/tokens/tokens-daisy-night.css +1320 -0
  41. package/dist/tokens/tokens-daisy-nord.css +1320 -0
  42. package/dist/tokens/tokens-daisy-pastel.css +1320 -0
  43. package/dist/tokens/tokens-daisy-retro.css +1320 -0
  44. package/dist/tokens/tokens-daisy-sunset.css +1320 -0
  45. package/dist/tokens/tokens-daisy-synthwave.css +1320 -0
  46. package/dist/tokens/tokens-daisy-valentine.css +1320 -0
  47. package/dist/tokens/tokens-daisy-winter.css +1320 -0
  48. package/dist/tokens/tokens-daisy-wireframe.css +1321 -0
  49. package/dist/tokens/tokens-daisyui-base.css +1269 -0
  50. package/dist/tokens/tokens-desert-rose.css +1320 -0
  51. package/dist/tokens/tokens-dotorg-site.css +1269 -0
  52. package/dist/tokens/tokens-forest-canopy.css +1320 -0
  53. package/dist/tokens/tokens-golden-hour.css +1320 -0
  54. package/dist/tokens/tokens-harlow-pets.css +1320 -0
  55. package/dist/tokens/tokens-kehribar.css +1320 -0
  56. package/dist/tokens/tokens-midnight-galaxy.css +1320 -0
  57. package/dist/tokens/tokens-modern-minimalist.css +1270 -0
  58. package/dist/tokens/tokens-ocean-depths.css +1320 -0
  59. package/dist/tokens/tokens-showcase-base.css +1269 -0
  60. package/dist/tokens/tokens-smoke-bold.css +1269 -0
  61. package/dist/tokens/tokens-smoke-friendly.css +1269 -0
  62. package/dist/tokens/tokens-smoke-minimal.css +1281 -0
  63. package/dist/tokens/tokens-smoke-premium.css +1280 -0
  64. package/dist/tokens/tokens-smoke-professional.css +1269 -0
  65. package/dist/tokens/tokens-smoke-retail.css +1321 -0
  66. package/dist/tokens/tokens-smoke-startup.css +1269 -0
  67. package/dist/tokens/tokens-solvlys.css +1320 -0
  68. package/dist/tokens/tokens-sunset-boulevard.css +1320 -0
  69. package/dist/tokens/tokens-tech-innovation.css +1320 -0
  70. package/dist/tokens/tokens-theme-atrium.css +1320 -0
  71. package/dist/tokens/tokens-theme-catalyst.css +1320 -0
  72. package/dist/tokens/tokens-theme-circuit.css +1320 -0
  73. package/dist/tokens/tokens-theme-dispatch.css +1320 -0
  74. package/dist/tokens/tokens-theme-dither.css +1320 -0
  75. package/dist/tokens/tokens-theme-ember.css +1320 -0
  76. package/dist/tokens/tokens-theme-fanfare.css +1320 -0
  77. package/dist/tokens/tokens-theme-ferro.css +1321 -0
  78. package/dist/tokens/tokens-theme-folio.css +1320 -0
  79. package/dist/tokens/tokens-theme-hamara.css +1270 -0
  80. package/dist/tokens/tokens-theme-komorebi.css +1321 -0
  81. package/dist/tokens/tokens-theme-kyo.css +1269 -0
  82. package/dist/tokens/tokens-theme-lattice.css +1320 -0
  83. package/dist/tokens/tokens-theme-ma.css +1321 -0
  84. package/dist/tokens/tokens-theme-marquee.css +1320 -0
  85. package/dist/tokens/tokens-theme-meridian.css +1320 -0
  86. package/dist/tokens/tokens-theme-nimbus.css +1320 -0
  87. package/dist/tokens/tokens-theme-plasma.css +1320 -0
  88. package/dist/tokens/tokens-theme-plinth.css +1320 -0
  89. package/dist/tokens/tokens-theme-quartz.css +1320 -0
  90. package/dist/tokens/tokens-theme-sus.css +1320 -0
  91. package/dist/tokens/tokens-theme-trellis.css +1269 -0
  92. package/dist/tokens/tokens-theme-vesper.css +1320 -0
  93. package/dist/tokens/tokens-theme-wabi.css +1320 -0
  94. package/dist/tools/apply-inferred-theme.js +89 -0
  95. package/dist/tools/render-gathering.js +88 -0
  96. package/package.json +5 -2
  97. package/dist/tools/surface-preview.js +0 -36
package/dist/index.js CHANGED
@@ -1,107 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from 'node:module';
3
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
6
- import { GET_COMPONENT_TOOL, handleGetComponent } from './tools/get-component.js';
7
- import { LIST_COMPONENTS_TOOL, handleListComponents } from './tools/list-components.js';
8
- import { GET_FOUNDATION_TOOL, handleGetFoundation } from './tools/get-foundation.js';
9
- import { SEARCH_DOCS_TOOL, handleSearchDocs } from './tools/search-docs.js';
10
- import { INFER_THEME_TOOL, handleInferTheme } from './tools/infer-theme.js';
11
- import { INFER_BRAND_THEME_TOOL, handleInferBrandTheme } from './tools/infer-brand-theme.js';
12
- import { LIST_EXERCISES_TOOL, handleListExercises } from './tools/list-exercises.js';
13
- import { START_EXERCISE_TOOL, handleStartExercise } from './tools/start-exercise.js';
14
- import { GET_STEP_TOOL, handleGetStep } from './tools/get-step.js';
15
- import { SURFACE_PREVIEW_TOOL, handleSurfacePreview } from './tools/surface-preview.js';
16
- import { LIST_COMPOSITION_PATTERNS_TOOL, handleListCompositionPatterns } from './tools/list-composition-patterns.js';
17
- import { GET_COMPOSITION_PATTERN_TOOL, handleGetCompositionPattern } from './tools/get-composition-pattern.js';
18
- import { GET_EMPHASIS_SCALE_TOOL, handleGetEmphasisScale } from './tools/get-emphasis-scale.js';
19
- import { GET_COMPLETENESS_MAP_TOOL, handleGetCompletenessMap } from './tools/get-completeness-map.js';
20
- import { LINK_SESSION_TOOL, handleLinkSession } from './tools/link-session.js';
21
- import { SET_THEME_TOOL, handleSetTheme } from './tools/set-theme.js';
22
- import { NAVIGATE_TOOL, handleNavigate } from './tools/navigate.js';
23
- import { SET_INSTANCE_VARIANT_TOOL, handleSetInstanceVariant } from './tools/set-instance-variant.js';
24
- import { GET_MARKER_CAPTURE_TOOL, handleGetMarkerCapture } from './tools/get-marker-capture.js';
25
- import { createProvider } from './provider.js';
26
- import { RemoteContentProvider } from './remote-provider.js';
27
- // Create the content provider — LocalContentProvider if content tree is present,
28
- // RemoteContentProvider otherwise. Initialize runs the manifest version check.
29
- const provider = await createProvider();
30
- if (provider instanceof RemoteContentProvider) {
31
- await provider.initialize();
32
- }
33
- // Advertised version reads package.json at runtime — a literal here drifts
34
- // the moment the next release forgets to update it (0.3.0 survived four
35
- // releases; 0.1.4 outlived its release within hours).
36
- const require = createRequire(import.meta.url);
37
- const { version: pkgVersion } = require('../package.json');
38
- const server = new Server({ name: 'runsnative-mcp', version: pkgVersion }, { capabilities: { tools: {} } });
39
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
40
- tools: [
41
- GET_COMPONENT_TOOL,
42
- LIST_COMPONENTS_TOOL,
43
- GET_FOUNDATION_TOOL,
44
- SEARCH_DOCS_TOOL,
45
- INFER_THEME_TOOL,
46
- INFER_BRAND_THEME_TOOL,
47
- LIST_EXERCISES_TOOL,
48
- START_EXERCISE_TOOL,
49
- GET_STEP_TOOL,
50
- SURFACE_PREVIEW_TOOL,
51
- LIST_COMPOSITION_PATTERNS_TOOL,
52
- GET_COMPOSITION_PATTERN_TOOL,
53
- GET_EMPHASIS_SCALE_TOOL,
54
- GET_COMPLETENESS_MAP_TOOL,
55
- LINK_SESSION_TOOL,
56
- SET_THEME_TOOL,
57
- NAVIGATE_TOOL,
58
- SET_INSTANCE_VARIANT_TOOL,
59
- GET_MARKER_CAPTURE_TOOL,
60
- ],
61
- }));
62
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
63
- switch (request.params.name) {
64
- case 'get_component':
65
- return handleGetComponent(provider, request.params.arguments ?? {});
66
- case 'list_components':
67
- return handleListComponents(provider, request.params.arguments ?? {});
68
- case 'get_foundation':
69
- return handleGetFoundation(provider, request.params.arguments ?? {});
70
- case 'search_docs':
71
- return handleSearchDocs(provider, request.params.arguments ?? {});
72
- case 'infer_theme':
73
- return handleInferTheme(request.params.arguments ?? {});
74
- case 'infer_brand_theme':
75
- return handleInferBrandTheme(request.params.arguments ?? {});
76
- case 'list_exercises':
77
- return handleListExercises(provider, request.params.arguments ?? {});
78
- case 'start_exercise':
79
- return handleStartExercise(provider, request.params.arguments ?? {});
80
- case 'get_step':
81
- return handleGetStep(provider, request.params.arguments ?? {});
82
- case 'surface_preview':
83
- return handleSurfacePreview();
84
- case 'list_composition_patterns':
85
- return handleListCompositionPatterns(provider, request.params.arguments ?? {});
86
- case 'get_composition_pattern':
87
- return handleGetCompositionPattern(provider, request.params.arguments ?? {});
88
- case 'get_emphasis_scale':
89
- return handleGetEmphasisScale();
90
- case 'get_completeness_map':
91
- return handleGetCompletenessMap();
92
- case 'link_session':
93
- return handleLinkSession(request.params.arguments ?? {});
94
- case 'set_theme':
95
- return handleSetTheme(request.params.arguments ?? {});
96
- case 'navigate':
97
- return handleNavigate(request.params.arguments ?? {});
98
- case 'set_instance_variant':
99
- return handleSetInstanceVariant(request.params.arguments ?? {});
100
- case 'get_marker_capture':
101
- return handleGetMarkerCapture(request.params.arguments ?? {});
102
- default:
103
- throw new Error(`Unknown tool: ${request.params.name}`);
104
- }
105
- });
3
+ import { createRunsnativeServer } from './server.js';
4
+ const server = await createRunsnativeServer();
106
5
  const transport = new StdioServerTransport();
107
6
  await server.connect(transport);
@@ -0,0 +1,31 @@
1
+ // Navigation-failure classification, extracted from page-fetcher.ts (JUNE-607) so the
2
+ // Cloudflare Worker port can import it without dragging in page-fetcher's lazy
3
+ // `import('playwright')` — esbuild cannot bundle a literal dynamic import of a package
4
+ // that is not installed. This module is pure (no imports, no I/O); page-fetcher.ts
5
+ // re-exports everything here, so existing consumers are unaffected.
6
+ /**
7
+ * Map a Chromium/Playwright navigation error string to a user-meaningful reason. Signatures
8
+ * verified against live failures: ecolab.com → ERR_HTTP2_PROTOCOL_ERROR (bot-block),
9
+ * a dead domain → ERR_NAME_NOT_RESOLVED, badssl.com → ERR_CERT_*, port 1 → ERR_UNSAFE_PORT.
10
+ */
11
+ export function classifyNavError(cause) {
12
+ const c = cause.toUpperCase();
13
+ if (/\bTIMEOUT\b|EXCEEDED/.test(c))
14
+ return 'timeout';
15
+ if (c.includes('ERR_NAME_NOT_RESOLVED') || c.includes('ERR_NAME_RESOLUTION_FAILED'))
16
+ return 'not_found';
17
+ if (c.includes('ERR_CERT'))
18
+ return 'tls';
19
+ // Connection reset / HTTP2 protocol errors on the initial navigation are the fingerprint of
20
+ // a WAF/bot wall fingerprinting headless Chromium (verified: ecolab.com). ERR_ACCESS_DENIED
21
+ // and ERR_BLOCKED_BY_* are the explicit forms.
22
+ if (c.includes('ERR_HTTP2_PROTOCOL_ERROR') || c.includes('ERR_CONNECTION_RESET') ||
23
+ c.includes('ERR_ACCESS_DENIED') || c.includes('ERR_BLOCKED'))
24
+ return 'blocked';
25
+ if (c.includes('ERR_CONNECTION_REFUSED') || c.includes('ERR_CONNECTION_TIMED_OUT') ||
26
+ c.includes('ERR_ADDRESS_UNREACHABLE') || c.includes('ERR_UNSAFE_PORT') ||
27
+ c.includes('ERR_CONNECTION_CLOSED') || c.includes('ERR_EMPTY_RESPONSE') ||
28
+ c.includes('ERR_SOCKET_NOT_CONNECTED'))
29
+ return 'unreachable';
30
+ return 'unknown';
31
+ }
@@ -1,4 +1,10 @@
1
1
  import { extractStylesFromPage } from './style-extractor.js';
2
+ import { classifyNavError } from './nav-error.js';
3
+ // Failure classification lives in nav-error.ts (JUNE-607) so the Cloudflare Worker port can
4
+ // share it without importing this module's lazy `import('playwright')` (esbuild cannot bundle
5
+ // a literal dynamic import of an uninstalled package). Re-exported so existing consumers
6
+ // (pipeline, tests, the MCP tool) keep their import path.
7
+ export { classifyNavError };
2
8
  export async function loadChromium() {
3
9
  try {
4
10
  const playwright = await import('playwright');
@@ -8,32 +14,6 @@ export async function loadChromium() {
8
14
  return null;
9
15
  }
10
16
  }
11
- /**
12
- * Map a Chromium/Playwright navigation error string to a user-meaningful reason. Signatures
13
- * verified against live failures: ecolab.com → ERR_HTTP2_PROTOCOL_ERROR (bot-block),
14
- * a dead domain → ERR_NAME_NOT_RESOLVED, badssl.com → ERR_CERT_*, port 1 → ERR_UNSAFE_PORT.
15
- */
16
- export function classifyNavError(cause) {
17
- const c = cause.toUpperCase();
18
- if (/\bTIMEOUT\b|EXCEEDED/.test(c))
19
- return 'timeout';
20
- if (c.includes('ERR_NAME_NOT_RESOLVED') || c.includes('ERR_NAME_RESOLUTION_FAILED'))
21
- return 'not_found';
22
- if (c.includes('ERR_CERT'))
23
- return 'tls';
24
- // Connection reset / HTTP2 protocol errors on the initial navigation are the fingerprint of
25
- // a WAF/bot wall fingerprinting headless Chromium (verified: ecolab.com). ERR_ACCESS_DENIED
26
- // and ERR_BLOCKED_BY_* are the explicit forms.
27
- if (c.includes('ERR_HTTP2_PROTOCOL_ERROR') || c.includes('ERR_CONNECTION_RESET') ||
28
- c.includes('ERR_ACCESS_DENIED') || c.includes('ERR_BLOCKED'))
29
- return 'blocked';
30
- if (c.includes('ERR_CONNECTION_REFUSED') || c.includes('ERR_CONNECTION_TIMED_OUT') ||
31
- c.includes('ERR_ADDRESS_UNREACHABLE') || c.includes('ERR_UNSAFE_PORT') ||
32
- c.includes('ERR_CONNECTION_CLOSED') || c.includes('ERR_EMPTY_RESPONSE') ||
33
- c.includes('ERR_SOCKET_NOT_CONNECTED'))
34
- return 'unreachable';
35
- return 'unknown';
36
- }
37
17
  const VIEWPORT = { width: 1280, height: 800 };
38
18
  // Navigation timing. `domcontentloaded` is the hard gate; `networkidle` is a bounded bonus.
39
19
  const GOTO_TIMEOUT_MS = 25_000; // hard cap on the initial document navigation
package/dist/server.js ADDED
@@ -0,0 +1,131 @@
1
+ import { createRequire } from 'node:module';
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { GET_COMPONENT_TOOL, handleGetComponent } from './tools/get-component.js';
5
+ import { LIST_COMPONENTS_TOOL, handleListComponents } from './tools/list-components.js';
6
+ import { GET_FOUNDATION_TOOL, handleGetFoundation } from './tools/get-foundation.js';
7
+ import { SEARCH_DOCS_TOOL, handleSearchDocs } from './tools/search-docs.js';
8
+ import { INFER_THEME_TOOL, handleInferTheme } from './tools/infer-theme.js';
9
+ import { INFER_BRAND_THEME_TOOL, handleInferBrandTheme } from './tools/infer-brand-theme.js';
10
+ import { LIST_EXERCISES_TOOL, handleListExercises } from './tools/list-exercises.js';
11
+ import { START_EXERCISE_TOOL, handleStartExercise } from './tools/start-exercise.js';
12
+ import { GET_STEP_TOOL, handleGetStep } from './tools/get-step.js';
13
+ import { RENDER_GATHERING_TOOL, GATHERING_CARD_RESOURCE, handleRenderGathering, readGatheringCardResource, } from './tools/render-gathering.js';
14
+ import { GATHERING_CARD_URI } from './surface/gathering-card.js';
15
+ import { LIST_COMPOSITION_PATTERNS_TOOL, handleListCompositionPatterns } from './tools/list-composition-patterns.js';
16
+ import { GET_COMPOSITION_PATTERN_TOOL, handleGetCompositionPattern } from './tools/get-composition-pattern.js';
17
+ import { GET_EMPHASIS_SCALE_TOOL, handleGetEmphasisScale } from './tools/get-emphasis-scale.js';
18
+ import { GET_COMPLETENESS_MAP_TOOL, handleGetCompletenessMap } from './tools/get-completeness-map.js';
19
+ import { LINK_SESSION_TOOL, handleLinkSession } from './tools/link-session.js';
20
+ import { SET_THEME_TOOL, handleSetTheme } from './tools/set-theme.js';
21
+ import { APPLY_INFERRED_THEME_TOOL, handleApplyInferredTheme } from './tools/apply-inferred-theme.js';
22
+ import { NAVIGATE_TOOL, handleNavigate } from './tools/navigate.js';
23
+ import { SET_INSTANCE_VARIANT_TOOL, handleSetInstanceVariant } from './tools/set-instance-variant.js';
24
+ import { GET_MARKER_CAPTURE_TOOL, handleGetMarkerCapture } from './tools/get-marker-capture.js';
25
+ import { createProvider } from './provider.js';
26
+ import { RemoteContentProvider } from './remote-provider.js';
27
+ /**
28
+ * Build a fully-registered runsnative MCP Server. Transport-agnostic: the
29
+ * stdio entry (index.ts) and the dev/verification HTTP entry (dev-host.ts)
30
+ * both consume this factory, so tool/resource registration cannot drift
31
+ * between transports.
32
+ */
33
+ export async function createRunsnativeServer() {
34
+ // Content provider — LocalContentProvider if a content tree is present,
35
+ // RemoteContentProvider otherwise. Initialize runs the manifest version check.
36
+ const provider = await createProvider();
37
+ if (provider instanceof RemoteContentProvider) {
38
+ await provider.initialize();
39
+ }
40
+ // Advertised version reads package.json at runtime — a literal here drifts
41
+ // the moment the next release forgets to update it (0.3.0 survived four
42
+ // releases; 0.1.4 outlived its release within hours).
43
+ const require = createRequire(import.meta.url);
44
+ const { version: pkgVersion } = require('../package.json');
45
+ const server = new Server({ name: 'runsnative-mcp', version: pkgVersion }, { capabilities: { tools: {}, resources: {} } });
46
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
47
+ tools: [
48
+ GET_COMPONENT_TOOL,
49
+ LIST_COMPONENTS_TOOL,
50
+ GET_FOUNDATION_TOOL,
51
+ SEARCH_DOCS_TOOL,
52
+ INFER_THEME_TOOL,
53
+ INFER_BRAND_THEME_TOOL,
54
+ LIST_EXERCISES_TOOL,
55
+ START_EXERCISE_TOOL,
56
+ GET_STEP_TOOL,
57
+ RENDER_GATHERING_TOOL,
58
+ LIST_COMPOSITION_PATTERNS_TOOL,
59
+ GET_COMPOSITION_PATTERN_TOOL,
60
+ GET_EMPHASIS_SCALE_TOOL,
61
+ GET_COMPLETENESS_MAP_TOOL,
62
+ LINK_SESSION_TOOL,
63
+ SET_THEME_TOOL,
64
+ APPLY_INFERRED_THEME_TOOL,
65
+ NAVIGATE_TOOL,
66
+ SET_INSTANCE_VARIANT_TOOL,
67
+ GET_MARKER_CAPTURE_TOOL,
68
+ ],
69
+ }));
70
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
71
+ switch (request.params.name) {
72
+ case 'get_component':
73
+ return handleGetComponent(provider, request.params.arguments ?? {});
74
+ case 'list_components':
75
+ return handleListComponents(provider, request.params.arguments ?? {});
76
+ case 'get_foundation':
77
+ return handleGetFoundation(provider, request.params.arguments ?? {});
78
+ case 'search_docs':
79
+ return handleSearchDocs(provider, request.params.arguments ?? {});
80
+ case 'infer_theme':
81
+ return handleInferTheme(request.params.arguments ?? {});
82
+ case 'infer_brand_theme':
83
+ return handleInferBrandTheme(request.params.arguments ?? {});
84
+ case 'list_exercises':
85
+ return handleListExercises(provider, request.params.arguments ?? {});
86
+ case 'start_exercise':
87
+ return handleStartExercise(provider, request.params.arguments ?? {});
88
+ case 'get_step':
89
+ return handleGetStep(provider, request.params.arguments ?? {});
90
+ case 'render_gathering':
91
+ return handleRenderGathering();
92
+ case 'list_composition_patterns':
93
+ return handleListCompositionPatterns(provider, request.params.arguments ?? {});
94
+ case 'get_composition_pattern':
95
+ return handleGetCompositionPattern(provider, request.params.arguments ?? {});
96
+ case 'get_emphasis_scale':
97
+ return handleGetEmphasisScale();
98
+ case 'get_completeness_map':
99
+ return handleGetCompletenessMap();
100
+ case 'link_session':
101
+ return handleLinkSession(request.params.arguments ?? {});
102
+ case 'set_theme':
103
+ return handleSetTheme(request.params.arguments ?? {});
104
+ case 'apply_inferred_theme':
105
+ return handleApplyInferredTheme(request.params.arguments ?? {});
106
+ case 'navigate':
107
+ return handleNavigate(request.params.arguments ?? {});
108
+ case 'set_instance_variant':
109
+ return handleSetInstanceVariant(request.params.arguments ?? {});
110
+ case 'get_marker_capture':
111
+ return handleGetMarkerCapture(request.params.arguments ?? {});
112
+ default:
113
+ throw new Error(`Unknown tool: ${request.params.name}`);
114
+ }
115
+ });
116
+ // MCP App surfaces (JUNE-618): ui:// resources referenced by tool _meta. The
117
+ // host reads the resource and renders it in a sandboxed iframe; each document
118
+ // is fully self-contained (bundle + runtime + CSS inlined by the seam).
119
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
120
+ resources: [GATHERING_CARD_RESOURCE],
121
+ }));
122
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
123
+ switch (request.params.uri) {
124
+ case GATHERING_CARD_URI:
125
+ return readGatheringCardResource();
126
+ default:
127
+ throw new Error(`Unknown resource: ${request.params.uri}`);
128
+ }
129
+ });
130
+ return server;
131
+ }
@@ -0,0 +1,89 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = dirname(__filename);
6
+ // trim(): trailing whitespace from setx/cmd wrappers silently corrupts URLs
7
+ // and headers (KUKAMANGA-wide env-read rule).
8
+ const THEME_CSS_BASE = (process.env['RUNSNATIVE_THEME_CSS_BASE'] ?? 'https://runsnative.org').trim();
9
+ const FETCH_HEADERS = { 'User-Agent': 'runsnative-mcp-server (surface-assets)' };
10
+ let _bundleCache = null;
11
+ let _runtimeCache = null;
12
+ const _themeCssCache = new Map();
13
+ // In dist/, this module compiles to dist/surface/assets.js — both build-time
14
+ // artifacts sit one level up, in the shipped dist/ root.
15
+ function distPath(file) {
16
+ return resolve(__dirname, '..', file);
17
+ }
18
+ /**
19
+ * The run-* web-component bundle to inline into an MCP App surface.
20
+ *
21
+ * Read from dist/components-inline.js — a build-time flattening of the pinned
22
+ * @runsnative/components release into ONE self-contained ES module (the
23
+ * published runsnative.min.js statically imports ./chunks/*, so it cannot be
24
+ * inlined as-is). No network at tool-call time; the sandbox CSP never sees an
25
+ * external URL because the fetch problem is solved at build time.
26
+ */
27
+ export function componentsBundle() {
28
+ if (_bundleCache === null) {
29
+ _bundleCache = readFileSync(distPath('components-inline.js'), 'utf8');
30
+ }
31
+ return _bundleCache;
32
+ }
33
+ /**
34
+ * The iframe-side MCP Apps runtime (App class + PostMessageTransport), built
35
+ * to dist/iframe-runtime.js as a self-contained IIFE exposing
36
+ * globalThis.__MCP_APP_SDK__.
37
+ */
38
+ export function iframeRuntime() {
39
+ if (_runtimeCache === null) {
40
+ _runtimeCache = readFileSync(distPath('iframe-runtime.js'), 'utf8');
41
+ }
42
+ return _runtimeCache;
43
+ }
44
+ const BRAND_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
45
+ // Strip @import lines — webfont imports cannot load inside the sandboxed
46
+ // iframe and would only produce console noise (same transform compare.js
47
+ // applies for section-scoped theme loading, JUNE-95).
48
+ function stripImports(css) {
49
+ return css.replace(/^\s*@import[^;]*;\s*$/gm, '');
50
+ }
51
+ /**
52
+ * Token-engine theme CSS for a brand (tokens-<brand>.css, already scoped to
53
+ * :where([data-run-theme="X"])). Sourced from the pinned components package
54
+ * first (dist/tokens/, offline, version-consistent with the inlined bundle);
55
+ * falls back to the runsnative.org public root for brands the pinned release
56
+ * doesn't carry (e.g. themes landed after the pin).
57
+ * Returns null when the brand has no landed theme file — the card then renders
58
+ * with the accent-hex fallback.
59
+ */
60
+ export async function themeCss(brandId) {
61
+ if (!BRAND_ID_PATTERN.test(brandId))
62
+ return null;
63
+ if (_themeCssCache.has(brandId))
64
+ return _themeCssCache.get(brandId) ?? null;
65
+ let css = null;
66
+ // dist/tokens/ is populated at build time from the same pinned release as
67
+ // the inlined bundle — offline and version-consistent.
68
+ const packaged = distPath(`tokens/tokens-${brandId}.css`);
69
+ if (existsSync(packaged))
70
+ css = stripImports(readFileSync(packaged, 'utf8'));
71
+ if (css === null) {
72
+ try {
73
+ const res = await fetch(`${THEME_CSS_BASE}/tokens-${brandId}.css`, { headers: FETCH_HEADERS });
74
+ if (res.ok)
75
+ css = stripImports(await res.text());
76
+ }
77
+ catch {
78
+ css = null;
79
+ }
80
+ }
81
+ _themeCssCache.set(brandId, css);
82
+ return css;
83
+ }
84
+ /** Test seam — reset module-level caches between tests. */
85
+ export function _resetAssetCaches() {
86
+ _bundleCache = null;
87
+ _runtimeCache = null;
88
+ _themeCssCache.clear();
89
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The gathering-card MCP App shell (JUNE-618) — the Create-a-Gathering finale.
3
+ *
4
+ * Static shell: no per-call data is baked in. The host delivers the tool
5
+ * result (gathering + theme CSS) via the MCP Apps tool-result notification;
6
+ * the app script below renders the card client-side with DOM APIs
7
+ * (textContent only — user-authored fields can never parse as markup).
8
+ */
9
+ import { packageSurface } from './package-surface.js';
10
+ export const GATHERING_CARD_URI = 'ui://runsnative/gathering-card.html';
11
+ const CARD_CSS = `
12
+ /* Shell defaults live in the lowest layer so injected token-engine CSS
13
+ (@layer primitives/semantics/themes, or unlayered scoped bundles) always
14
+ wins the cascade. Order is fixed by this first declaration. */
15
+ @layer rn-shell, primitives, semantics, themes;
16
+ @layer rn-shell {
17
+ :root {
18
+ --run-color-text-primary: #1e293b;
19
+ --run-color-text-secondary: #64748b;
20
+ --run-color-surface-default: #ffffff;
21
+ --run-color-surface-subtle: #f8fafc;
22
+ --run-color-surface-canvas: transparent;
23
+ --run-color-border-default: #e2e8f0;
24
+ --run-color-focus-ring: #2563eb;
25
+ --run-font-size-s: 14px;
26
+ --run-font-size-m: 16px;
27
+ }
28
+ }
29
+ * { box-sizing: border-box; margin: 0; padding: 0; }
30
+ body {
31
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
32
+ color: var(--run-color-text-primary);
33
+ background: var(--run-color-surface-canvas);
34
+ padding: 12px;
35
+ }
36
+ .gathering-card {
37
+ max-width: 440px;
38
+ border: 1px solid var(--run-color-border-default);
39
+ border-radius: 12px;
40
+ background: var(--run-color-surface-default);
41
+ padding: 20px;
42
+ display: none;
43
+ flex-direction: column;
44
+ gap: 10px;
45
+ }
46
+ .gathering-card.is-ready { display: flex; }
47
+ .card-host-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
48
+ .card-meta { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
49
+ .card-avatar {
50
+ width: 28px; height: 28px; border-radius: 50%;
51
+ background: var(--run-color-brand-600, var(--card-accent, #2563eb)); color: #fff;
52
+ display: inline-flex; align-items: center; justify-content: center;
53
+ font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
54
+ }
55
+ .card-actions { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
56
+ .card-status { font-size: 12px; color: var(--run-color-text-secondary); min-height: 1.2em; }
57
+ #placeholder { font-size: 13px; color: var(--run-color-text-secondary); padding: 8px 2px; }
58
+ `;
59
+ const CARD_BODY = `
60
+ <div id="placeholder">Waiting for your gathering…</div>
61
+ <article class="gathering-card" id="card">
62
+ <div class="card-host-bar">
63
+ <run-text id="card-brand" variant="label" as="span" size="sm" color="theme"></run-text>
64
+ <run-badge variant="info" size="sm" pill>Yours</run-badge>
65
+ </div>
66
+ <run-text id="card-title" variant="title" as="h3" size="md"></run-text>
67
+ <run-text id="card-desc" variant="body" as="p" size="sm" color="secondary"></run-text>
68
+ <div class="card-meta">
69
+ <span class="card-avatar" id="card-avatar" aria-hidden="true"></span>
70
+ <run-text id="card-presenter" variant="caption" as="span"></run-text>
71
+ <run-text id="card-date" variant="caption" as="span" color="secondary"></run-text>
72
+ </div>
73
+ <div class="card-actions">
74
+ <run-button id="view-btn" variant="primary" size="sm">View on Convene</run-button>
75
+ </div>
76
+ <div class="card-status" id="card-status"></div>
77
+ </article>
78
+ `;
79
+ // Static, data-free by design: everything user-authored arrives at runtime via
80
+ // the tool-result notification and is assigned with textContent. Keep it that
81
+ // way — interpolating server-side values here would reopen the injection door
82
+ // the seam exists to close.
83
+ const CARD_APP_SCRIPT = `
84
+ const { App } = globalThis.__MCP_APP_SDK__;
85
+
86
+ const BRAND_ID = /^[a-z0-9][a-z0-9-]*$/;
87
+ const ACCENT_HEX = /^#[0-9a-fA-F]{3,8}$/;
88
+
89
+ function setText(id, value) {
90
+ document.getElementById(id).textContent = value == null ? '' : String(value);
91
+ }
92
+
93
+ function render(payload) {
94
+ const g = payload && payload.gathering;
95
+ if (!g) return;
96
+ const card = document.getElementById('card');
97
+
98
+ // Theme: landed brands get their token-engine CSS (scoped to
99
+ // :where([data-run-theme="X"])); everything else falls back to the accent.
100
+ if (typeof payload.theme_css === 'string' && payload.theme_css && BRAND_ID.test(g.brand_id || '')) {
101
+ const style = document.createElement('style');
102
+ style.textContent = payload.theme_css;
103
+ document.head.appendChild(style);
104
+ card.setAttribute('data-run-theme', g.brand_id);
105
+ document.documentElement.setAttribute('data-run-theme', g.brand_id);
106
+ } else if (typeof g.accent_hex === 'string' && ACCENT_HEX.test(g.accent_hex)) {
107
+ // No landed theme — carry the user's accent onto the tokens the card's
108
+ // components actually consume.
109
+ card.style.setProperty('--card-accent', g.accent_hex);
110
+ card.style.setProperty('--run-color-focus-ring', g.accent_hex);
111
+ card.style.setProperty('--run-button-background', g.accent_hex);
112
+ card.style.setProperty('--run-button-background-hover', g.accent_hex);
113
+ card.style.setProperty('--run-button-background-active', g.accent_hex);
114
+ card.style.setProperty('--run-button-color', '#ffffff');
115
+ card.style.setProperty('--run-button-focus-ring-color', g.accent_hex);
116
+ }
117
+
118
+ setText('card-brand', payload.brand_label || g.brand_id || 'RunsNative');
119
+ setText('card-title', g.title);
120
+ setText('card-desc', g.description);
121
+ setText('card-avatar', (g.presenter_initials || '').slice(0, 3).toUpperCase());
122
+ setText('card-presenter', g.presenter_name);
123
+ setText('card-date', g.date_string);
124
+
125
+ document.getElementById('placeholder').style.display = 'none';
126
+ card.classList.add('is-ready');
127
+ }
128
+
129
+ const app = new App({ name: 'runsnative-gathering-card', version: '1.0.0' }, {});
130
+
131
+ // Register before connect() so no notification is missed.
132
+ app.ontoolresult = (params) => {
133
+ if (params && params.isError) {
134
+ setText('placeholder', 'Could not load your gathering — see the conversation for details.');
135
+ return;
136
+ }
137
+ render(params && params.structuredContent);
138
+ };
139
+
140
+ document.getElementById('view-btn').addEventListener('click', async () => {
141
+ const status = document.getElementById('card-status');
142
+ status.textContent = 'Sending your tab to Convene…';
143
+ try {
144
+ const result = await app.callServerTool({ name: 'navigate', arguments: { path: '/compare/' } });
145
+ status.textContent = result && result.isError
146
+ ? 'No linked tab — open runsnative.org/compare/ to see it live.'
147
+ : 'Your linked tab is heading to Convene.';
148
+ } catch (e) {
149
+ status.textContent = 'No linked tab — open runsnative.org/compare/ to see it live.';
150
+ }
151
+ });
152
+
153
+ await app.connect();
154
+ `;
155
+ export function buildGatheringCardHtml(assets) {
156
+ return packageSurface({
157
+ title: 'Your Gathering — Convene',
158
+ bodyHtml: CARD_BODY,
159
+ css: CARD_CSS,
160
+ appScript: CARD_APP_SCRIPT,
161
+ }, assets);
162
+ }
@@ -0,0 +1,8 @@
1
+ // Entry for the iframe-side MCP Apps runtime bundle. Built by
2
+ // scripts/build-iframe-runtime.mjs into dist/iframe-runtime.js as a
3
+ // self-contained IIFE, then inlined into every packaged surface by the seam
4
+ // (package-surface.ts). Exposes a stable global so the (also inlined) app
5
+ // script can use the SDK without module imports — inline module scripts
6
+ // cannot import each other, and external URLs are blocked by the host CSP.
7
+ import { App } from '@modelcontextprotocol/ext-apps';
8
+ globalThis.__MCP_APP_SDK__ = { App };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The MCP App packaging seam (JUNE-618) — component tree + theme + app script
3
+ * into ONE self-contained `ui://` resource document.
4
+ *
5
+ * This is the mold-shaped boundary: every host-specific quirk of rendering
6
+ * RunsNative surfaces inside an agent host lives in this module (and the
7
+ * assets it inlines), NEVER in components. Contract:
8
+ * mukadra docs/contracts/agent-surface-packaging-contract-v1.md.
9
+ *
10
+ * Invariants:
11
+ * - The emitted document references ZERO external URLs. The component bundle,
12
+ * the MCP Apps iframe runtime, and all CSS ride inside the document —
13
+ * the host sandbox CSP blocks external loads (RUN-477 / in-host verify).
14
+ * - Data does NOT ride in the document. The shell is static; per-call data
15
+ * arrives through the MCP Apps tool-result notification and is rendered
16
+ * client-side with DOM APIs (textContent), never markup interpolation.
17
+ */
18
+ // MIME type for MCP App resources, per ext-apps (RESOURCE_MIME_TYPE). Kept as
19
+ // a literal so this module stays dependency-free and pure.
20
+ export const SURFACE_MIME_TYPE = 'text/html;profile=mcp-app';
21
+ export function escapeHtml(s) {
22
+ return String(s ?? '')
23
+ .replace(/&/g, '&amp;')
24
+ .replace(/</g, '&lt;')
25
+ .replace(/>/g, '&gt;')
26
+ .replace(/"/g, '&quot;')
27
+ .replace(/'/g, '&#39;');
28
+ }
29
+ // A closing </script> inside inlined JS text would terminate the surrounding
30
+ // script element early and let the remainder parse as markup. Break the
31
+ // sequence — safe inside JS strings and regexes alike.
32
+ function escapeInlineScript(js) {
33
+ return js.replace(/<\/script/gi, '<\\/script');
34
+ }
35
+ function escapeInlineStyle(css) {
36
+ return css.replace(/<\/style/gi, '<\\/style');
37
+ }
38
+ /**
39
+ * Package a surface into a single self-contained HTML document.
40
+ * Pure function: assets are injected so tests need no network or filesystem.
41
+ */
42
+ export function packageSurface(input, assets) {
43
+ const { title, bodyHtml, css, appScript } = input;
44
+ return `<!DOCTYPE html>
45
+ <html lang="en" data-run-mode="light">
46
+ <head>
47
+ <meta charset="UTF-8">
48
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
49
+ <title>${escapeHtml(title)}</title>
50
+ <style>${escapeInlineStyle(css ?? '')}</style>
51
+ </head>
52
+ <body>
53
+ ${bodyHtml}
54
+ <script type="module">${escapeInlineScript(assets.componentsBundle)}</script>
55
+ <script>${escapeInlineScript(assets.iframeRuntime)}</script>
56
+ <script type="module">${escapeInlineScript(appScript ?? '')}</script>
57
+ </body>
58
+ </html>`;
59
+ }