@portel/photon 1.5.1 → 1.6.1

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 (69) hide show
  1. package/README.md +361 -339
  2. package/dist/auto-ui/beam.d.ts +5 -0
  3. package/dist/auto-ui/beam.d.ts.map +1 -1
  4. package/dist/auto-ui/beam.js +727 -51
  5. package/dist/auto-ui/beam.js.map +1 -1
  6. package/dist/auto-ui/bridge/index.d.ts +37 -0
  7. package/dist/auto-ui/bridge/index.d.ts.map +1 -0
  8. package/dist/auto-ui/bridge/index.js +555 -0
  9. package/dist/auto-ui/bridge/index.js.map +1 -0
  10. package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
  11. package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
  12. package/dist/auto-ui/bridge/openai-shim.js +231 -0
  13. package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
  14. package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
  15. package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
  16. package/dist/auto-ui/bridge/photon-app.js +460 -0
  17. package/dist/auto-ui/bridge/photon-app.js.map +1 -0
  18. package/dist/auto-ui/bridge/types.d.ts +128 -0
  19. package/dist/auto-ui/bridge/types.d.ts.map +1 -0
  20. package/dist/auto-ui/bridge/types.js +7 -0
  21. package/dist/auto-ui/bridge/types.js.map +1 -0
  22. package/dist/auto-ui/index.d.ts +3 -1
  23. package/dist/auto-ui/index.d.ts.map +1 -1
  24. package/dist/auto-ui/index.js +5 -2
  25. package/dist/auto-ui/index.js.map +1 -1
  26. package/dist/auto-ui/platform-compat.d.ts.map +1 -1
  27. package/dist/auto-ui/platform-compat.js +60 -6
  28. package/dist/auto-ui/platform-compat.js.map +1 -1
  29. package/dist/auto-ui/streamable-http-transport.d.ts +25 -1
  30. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  31. package/dist/auto-ui/streamable-http-transport.js +581 -20
  32. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  33. package/dist/auto-ui/types.d.ts +74 -0
  34. package/dist/auto-ui/types.d.ts.map +1 -1
  35. package/dist/auto-ui/types.js +21 -0
  36. package/dist/auto-ui/types.js.map +1 -1
  37. package/dist/beam.bundle.js +51377 -1778
  38. package/dist/beam.bundle.js.map +4 -4
  39. package/dist/cli.js +12 -2
  40. package/dist/cli.js.map +1 -1
  41. package/dist/daemon/client.d.ts +5 -3
  42. package/dist/daemon/client.d.ts.map +1 -1
  43. package/dist/daemon/client.js +30 -4
  44. package/dist/daemon/client.js.map +1 -1
  45. package/dist/daemon/manager.d.ts +5 -0
  46. package/dist/daemon/manager.d.ts.map +1 -1
  47. package/dist/daemon/manager.js +20 -0
  48. package/dist/daemon/manager.js.map +1 -1
  49. package/dist/loader.d.ts +23 -0
  50. package/dist/loader.d.ts.map +1 -1
  51. package/dist/loader.js +77 -12
  52. package/dist/loader.js.map +1 -1
  53. package/dist/photon-cli-runner.d.ts.map +1 -1
  54. package/dist/photon-cli-runner.js +2 -0
  55. package/dist/photon-cli-runner.js.map +1 -1
  56. package/dist/photon-doc-extractor.d.ts +1 -0
  57. package/dist/photon-doc-extractor.d.ts.map +1 -1
  58. package/dist/photon-doc-extractor.js +25 -6
  59. package/dist/photon-doc-extractor.js.map +1 -1
  60. package/dist/server.d.ts +12 -1
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +386 -13
  63. package/dist/server.js.map +1 -1
  64. package/dist/template-manager.js +2 -2
  65. package/dist/version.d.ts +8 -0
  66. package/dist/version.d.ts.map +1 -1
  67. package/dist/version.js +16 -0
  68. package/dist/version.js.map +1 -1
  69. package/package.json +18 -8
package/dist/server.js CHANGED
@@ -125,6 +125,12 @@ export class PhotonServer {
125
125
  // Before initialization or no capabilities - assume legacy Photon
126
126
  return 'photon';
127
127
  }
128
+ // Check for MCP Apps extension (io.modelcontextprotocol/ui)
129
+ // Claude Desktop and other MCP Apps clients use this format
130
+ const extensions = capabilities.extensions;
131
+ if (extensions?.['io.modelcontextprotocol/ui']) {
132
+ return 'sep-1865';
133
+ }
128
134
  // Check for SEP-1865 UI capability
129
135
  // SEP-1865 clients advertise: { experimental: { ui: {} } } or { ui: {} }
130
136
  const experimental = capabilities.experimental;
@@ -179,13 +185,22 @@ export class PhotonServer {
179
185
  }
180
186
  }
181
187
  /**
182
- * Get UI mimeType based on detected format
188
+ * Get UI mimeType based on detected format and client capabilities
183
189
  *
184
190
  * @param server - Optional server instance (for SSE sessions)
185
191
  */
186
192
  getUIMimeType(server) {
193
+ const targetServer = server || this.server;
194
+ const capabilities = targetServer.getClientCapabilities();
195
+ // Check for MCP Apps extension with declared mimeTypes
196
+ // Claude Desktop uses: { extensions: { "io.modelcontextprotocol/ui": { mimeTypes: ["text/html;profile=mcp-app"] } } }
197
+ const extensions = capabilities?.extensions;
198
+ const mcpUI = extensions?.['io.modelcontextprotocol/ui'];
199
+ if (mcpUI?.mimeTypes?.[0]) {
200
+ return mcpUI.mimeTypes[0];
201
+ }
187
202
  const format = this.getUIFormat(server);
188
- return format === 'sep-1865' ? 'text/html+mcp' : 'text/html';
203
+ return format === 'sep-1865' ? 'text/html;profile=mcp-app' : 'text/html';
189
204
  }
190
205
  /**
191
206
  * Check if client supports elicitation
@@ -210,7 +225,12 @@ export class PhotonServer {
210
225
  */
211
226
  createMCPInputProvider(server) {
212
227
  const targetServer = server || this.server;
228
+ const capabilities = targetServer.getClientCapabilities();
213
229
  const supportsElicitation = this.clientSupportsElicitation(server);
230
+ this.log('debug', 'Creating MCP input provider', {
231
+ supportsElicitation,
232
+ capabilities: JSON.stringify(capabilities),
233
+ });
214
234
  return async (ask) => {
215
235
  // If client doesn't support elicitation, fall back to logging the ask
216
236
  // (MCP servers can't use readline - they communicate via protocol)
@@ -1107,19 +1127,31 @@ export class PhotonServer {
1107
1127
  }
1108
1128
  /**
1109
1129
  * Handle incoming channel messages and forward as MCP notifications
1130
+ * This enables cross-client real-time updates (e.g., Beam updates show in Claude Desktop)
1131
+ *
1132
+ * Uses standard MCP Apps notification with embedded _photon data:
1133
+ * - Claude Desktop forwards standard notifications (ui/notifications/host-context-changed)
1134
+ * - Photon bridge extracts _photon field and routes to event listeners
1135
+ * - This ensures cross-client sync works without requiring custom protocol support
1110
1136
  */
1111
1137
  async handleChannelMessage(message) {
1112
1138
  if (!message || typeof message !== 'object')
1113
1139
  return;
1114
1140
  const msg = message;
1115
- // Format message for display
1116
- const displayMessage = msg.event
1117
- ? `[${msg.event}] ${JSON.stringify(msg.data || {})}`
1118
- : JSON.stringify(msg);
1119
- // Forward as MCP status notification to all connected clients
1141
+ // Use STANDARD notification with embedded photon data
1142
+ // Claude Desktop will forward this (it's a standard notification)
1143
+ // Our bridge extracts _photon and routes to the appropriate event handler
1120
1144
  const payload = {
1121
- method: 'notifications/status',
1122
- params: { type: 'info', message: displayMessage },
1145
+ method: 'ui/notifications/host-context-changed',
1146
+ params: {
1147
+ // _photon field carries our custom event data
1148
+ _photon: {
1149
+ photon: this.daemonName,
1150
+ channel: msg.channel,
1151
+ event: msg.event,
1152
+ data: msg.data,
1153
+ },
1154
+ },
1123
1155
  };
1124
1156
  try {
1125
1157
  await this.server.notification(payload);
@@ -1727,10 +1759,346 @@ export class PhotonServer {
1727
1759
  if (!ui || !ui.resolvedPath) {
1728
1760
  throw new Error(`UI asset not found: ${uri}`);
1729
1761
  }
1730
- const content = await fs.readFile(ui.resolvedPath, 'utf-8');
1762
+ let content = await fs.readFile(ui.resolvedPath, 'utf-8');
1763
+ // Inject MCP Apps bridge script for Claude Desktop compatibility
1764
+ const bridgeScript = this.generateMcpAppsBridge();
1765
+ content = content.replace('<head>', `<head>\n${bridgeScript}`);
1731
1766
  return {
1732
- contents: [{ uri, mimeType: ui.mimeType || 'text/html+mcp', text: content }],
1767
+ contents: [{ uri, mimeType: ui.mimeType || 'text/html;profile=mcp-app', text: content }],
1768
+ };
1769
+ }
1770
+ /**
1771
+ * Generate minimal MCP Apps bridge script for Claude Desktop compatibility
1772
+ * This handles the ui/initialize handshake and tool result delivery
1773
+ */
1774
+ generateMcpAppsBridge() {
1775
+ const photonName = this.mcp?.name || 'photon-app';
1776
+ const injectedPhotons = this.mcp?.injectedPhotons || [];
1777
+ return `<script>
1778
+ (function() {
1779
+ 'use strict';
1780
+ var pendingCalls = {};
1781
+ var callIdCounter = 0;
1782
+ var toolResult = null;
1783
+ var resultListeners = [];
1784
+ var emitListeners = [];
1785
+ var themeListeners = [];
1786
+ var eventListeners = {}; // For specific event subscriptions (e.g., 'taskMove')
1787
+ var photonEventListeners = {}; // Namespaced by photon name for injected photons
1788
+ var currentTheme = 'dark';
1789
+ var injectedPhotons = ${JSON.stringify(injectedPhotons)};
1790
+
1791
+ function generateCallId() {
1792
+ return 'call_' + (++callIdCounter) + '_' + Math.random().toString(36).slice(2);
1793
+ }
1794
+
1795
+ function postToHost(msg) {
1796
+ window.parent.postMessage(msg, '*');
1797
+ }
1798
+
1799
+ // Listen for messages from host
1800
+ window.addEventListener('message', function(e) {
1801
+ var m = e.data;
1802
+ if (!m || typeof m !== 'object') return;
1803
+
1804
+ // Handle JSON-RPC messages
1805
+ if (m.jsonrpc === '2.0') {
1806
+ // Response to our request (has id, no method)
1807
+ if (m.id && !m.method && pendingCalls[m.id]) {
1808
+ var pending = pendingCalls[m.id];
1809
+ delete pendingCalls[m.id];
1810
+ if (m.error) {
1811
+ pending.reject(new Error(m.error.message));
1812
+ } else {
1813
+ // Extract clean data from MCP result format
1814
+ var result = m.result;
1815
+ var cleanData = result;
1816
+ if (result && result.structuredContent) {
1817
+ cleanData = result.structuredContent;
1818
+ } else if (result && result.content && Array.isArray(result.content)) {
1819
+ var textItem = result.content.find(function(i) { return i.type === 'text'; });
1820
+ if (textItem && textItem.text) {
1821
+ try { cleanData = JSON.parse(textItem.text); } catch(e) { cleanData = textItem.text; }
1822
+ }
1823
+ }
1824
+ pending.resolve(cleanData);
1825
+ }
1826
+ return;
1827
+ }
1828
+
1829
+ // Tool result notification
1830
+ if (m.method === 'ui/notifications/tool-result') {
1831
+ var result = m.params;
1832
+ // Extract data from MCP result format
1833
+ if (result.structuredContent) {
1834
+ toolResult = result.structuredContent;
1835
+ } else if (result.content && Array.isArray(result.content)) {
1836
+ var textItem = result.content.find(function(i) { return i.type === 'text'; });
1837
+ if (textItem && textItem.text) {
1838
+ try { toolResult = JSON.parse(textItem.text); } catch(e) { toolResult = textItem.text; }
1839
+ }
1840
+ } else {
1841
+ toolResult = result;
1842
+ }
1843
+ // Set __PHOTON_DATA__ for UIs that read it at init
1844
+ window.__PHOTON_DATA__ = toolResult;
1845
+ // Dispatch event for UIs to re-initialize with new data
1846
+ window.dispatchEvent(new CustomEvent('photon:data-ready', { detail: toolResult }));
1847
+ resultListeners.forEach(function(cb) { cb(toolResult); });
1848
+ }
1849
+
1850
+ // Host context changed (theme + embedded photon events)
1851
+ if (m.method === 'ui/notifications/host-context-changed') {
1852
+ // Standard theme handling
1853
+ if (m.params && m.params.theme) {
1854
+ currentTheme = m.params.theme;
1855
+ document.documentElement.classList.remove('light', 'dark');
1856
+ document.documentElement.classList.add(m.params.theme);
1857
+ document.documentElement.setAttribute('data-theme', m.params.theme);
1858
+ themeListeners.forEach(function(cb) { cb(currentTheme); });
1859
+ }
1860
+
1861
+ // Extract embedded photon event data
1862
+ // This enables real-time sync via standard MCP protocol
1863
+ if (m.params && m.params._photon) {
1864
+ var photonData = m.params._photon;
1865
+ // Route to generic emit listeners
1866
+ emitListeners.forEach(function(cb) { cb(photonData); });
1867
+
1868
+ var eventName = photonData.event;
1869
+ var sourcePhoton = photonData.data && photonData.data._source;
1870
+
1871
+ // Route to photon-specific listeners if _source is specified (injected photon events)
1872
+ if (sourcePhoton && photonEventListeners[sourcePhoton] && photonEventListeners[sourcePhoton][eventName]) {
1873
+ photonEventListeners[sourcePhoton][eventName].forEach(function(cb) {
1874
+ cb(photonData.data);
1875
+ });
1876
+ }
1877
+
1878
+ // Also route to global event listeners (main photon events, or fallback)
1879
+ if (eventName && eventListeners[eventName]) {
1880
+ eventListeners[eventName].forEach(function(cb) {
1881
+ cb(photonData.data);
1882
+ });
1883
+ }
1884
+ }
1885
+ }
1886
+ }
1887
+ });
1888
+
1889
+ // Mark that we're in MCP Apps context (not Beam)
1890
+ window.__MCP_APPS_CONTEXT__ = true;
1891
+
1892
+ // Expose photon bridge API
1893
+ window.photon = {
1894
+ get toolOutput() { return toolResult; },
1895
+ onResult: function(cb) {
1896
+ resultListeners.push(cb);
1897
+ if (toolResult) cb(toolResult);
1898
+ return function() {
1899
+ var i = resultListeners.indexOf(cb);
1900
+ if (i >= 0) resultListeners.splice(i, 1);
1901
+ };
1902
+ },
1903
+ callTool: function(name, args) {
1904
+ var callId = generateCallId();
1905
+ return new Promise(function(resolve, reject) {
1906
+ pendingCalls[callId] = { resolve: resolve, reject: reject };
1907
+ postToHost({
1908
+ jsonrpc: '2.0',
1909
+ id: callId,
1910
+ method: 'tools/call',
1911
+ params: { name: name, arguments: args || {} }
1912
+ });
1913
+ setTimeout(function() {
1914
+ if (pendingCalls[callId]) {
1915
+ delete pendingCalls[callId];
1916
+ reject(new Error('Tool call timeout'));
1917
+ }
1918
+ }, 30000);
1919
+ });
1920
+ },
1921
+ invoke: function(name, args) { return window.photon.callTool(name, args); },
1922
+ onEmit: function(cb) {
1923
+ emitListeners.push(cb);
1924
+ return function() {
1925
+ var i = emitListeners.indexOf(cb);
1926
+ if (i >= 0) emitListeners.splice(i, 1);
1927
+ };
1928
+ },
1929
+ onThemeChange: function(cb) {
1930
+ themeListeners.push(cb);
1931
+ // Call immediately with current theme
1932
+ cb(currentTheme);
1933
+ return function() {
1934
+ var i = themeListeners.indexOf(cb);
1935
+ if (i >= 0) themeListeners.splice(i, 1);
1936
+ };
1937
+ },
1938
+ get theme() { return currentTheme; },
1939
+
1940
+ // Generic event subscription for real-time sync
1941
+ // Usage: photon.on('taskMove', function(data) { ... })
1942
+ on: function(eventName, cb) {
1943
+ if (!eventListeners[eventName]) eventListeners[eventName] = [];
1944
+ eventListeners[eventName].push(cb);
1945
+ return function() {
1946
+ var i = eventListeners[eventName].indexOf(cb);
1947
+ if (i >= 0) eventListeners[eventName].splice(i, 1);
1948
+ };
1949
+ },
1950
+
1951
+ // Photon-specific event subscription (for injected photon events)
1952
+ // Usage: photon.onPhoton('notifications', 'alertCreated', function(data) { ... })
1953
+ onPhoton: function(photonName, eventName, cb) {
1954
+ if (!photonEventListeners[photonName]) photonEventListeners[photonName] = {};
1955
+ if (!photonEventListeners[photonName][eventName]) photonEventListeners[photonName][eventName] = [];
1956
+ photonEventListeners[photonName][eventName].push(cb);
1957
+ return function() {
1958
+ var i = photonEventListeners[photonName][eventName].indexOf(cb);
1959
+ if (i >= 0) photonEventListeners[photonName][eventName].splice(i, 1);
1960
+ };
1961
+ }
1962
+ };
1963
+
1964
+ // Create direct window object: window.{photonName}
1965
+ // This provides a clean class-like API that mirrors server methods:
1966
+ // Server: this.emit('taskMove', data)
1967
+ // Client: kanban.onTaskMove(cb) - subscribe to events
1968
+ // Client: kanban.taskMove(args) - call server method
1969
+ var photonName = '${photonName}';
1970
+ window[photonName] = new Proxy({}, {
1971
+ get: function(target, prop) {
1972
+ if (typeof prop !== 'string') return undefined;
1973
+
1974
+ // onEventName -> subscribe to 'eventName' event
1975
+ // e.g., onTaskMove -> subscribe to 'taskMove'
1976
+ if (prop.startsWith('on') && prop.length > 2) {
1977
+ var eventName = prop.charAt(2).toLowerCase() + prop.slice(3);
1978
+ return function(cb) {
1979
+ return window.photon.on(eventName, cb);
1733
1980
  };
1981
+ }
1982
+
1983
+ // methodName -> call server tool
1984
+ // e.g., taskMove(args) -> photon.callTool('taskMove', args)
1985
+ return function(args) {
1986
+ return window.photon.callTool(prop, args);
1987
+ };
1988
+ }
1989
+ });
1990
+
1991
+ // Create proxies for injected photons (for event subscriptions)
1992
+ // e.g., notifications.onAlertCreated(cb) subscribes to 'alertCreated' from 'notifications' photon
1993
+ injectedPhotons.forEach(function(injectedName) {
1994
+ window[injectedName] = new Proxy({}, {
1995
+ get: function(target, prop) {
1996
+ if (typeof prop !== 'string') return undefined;
1997
+
1998
+ // onEventName -> subscribe to photon-specific event
1999
+ if (prop.startsWith('on') && prop.length > 2) {
2000
+ var eventName = prop.charAt(2).toLowerCase() + prop.slice(3);
2001
+ return function(cb) {
2002
+ return window.photon.onPhoton(injectedName, eventName, cb);
2003
+ };
2004
+ }
2005
+
2006
+ // Method calls on injected photons are not supported from client
2007
+ // (injected photon methods are only available server-side)
2008
+ return undefined;
2009
+ }
2010
+ });
2011
+ });
2012
+
2013
+ // Size notification helper
2014
+ function sendSizeChanged() {
2015
+ var body = document.body;
2016
+ var root = document.documentElement;
2017
+
2018
+ // Calculate actual content dimensions
2019
+ var width = Math.max(
2020
+ body.scrollWidth,
2021
+ body.offsetWidth,
2022
+ root.clientWidth,
2023
+ root.scrollWidth,
2024
+ root.offsetWidth
2025
+ );
2026
+ var height = Math.max(
2027
+ body.scrollHeight,
2028
+ body.offsetHeight,
2029
+ root.clientHeight,
2030
+ root.scrollHeight,
2031
+ root.offsetHeight
2032
+ );
2033
+
2034
+ // Check for scrollable containers with overflow:hidden that hide true content size
2035
+ var containers = document.querySelectorAll('.board, [style*="overflow"]');
2036
+ containers.forEach(function(el) {
2037
+ if (el.scrollWidth > width) width = el.scrollWidth;
2038
+ if (el.scrollHeight > height) height = el.scrollHeight;
2039
+ });
2040
+
2041
+ // For kanban-style boards, calculate from column count
2042
+ var columns = document.querySelectorAll('.column');
2043
+ if (columns.length > 0) {
2044
+ var columnWidth = 220; // min-width + gap
2045
+ var boardPadding = 48;
2046
+ var neededWidth = (columns.length * columnWidth) + boardPadding;
2047
+ if (neededWidth > width) width = neededWidth;
2048
+ }
2049
+
2050
+ // Reasonable minimums, maximums, and padding
2051
+ width = Math.max(width, 600) + 32;
2052
+ // Force minimum height for kanban-style boards
2053
+ // header(120) + column headers(50) + 3-4 cards(450) = 620
2054
+ if (columns.length > 0) {
2055
+ height = Math.max(height, 620);
2056
+ } else {
2057
+ height = Math.max(height, 400);
2058
+ }
2059
+
2060
+ postToHost({
2061
+ jsonrpc: '2.0',
2062
+ method: 'ui/notifications/size-changed',
2063
+ params: { width: width, height: height }
2064
+ });
2065
+ }
2066
+
2067
+ // MCP Apps handshake: send ui/initialize and wait for response
2068
+ var initId = generateCallId();
2069
+ pendingCalls[initId] = {
2070
+ resolve: function(result) {
2071
+ // Apply theme from host context
2072
+ if (result.hostContext && result.hostContext.theme) {
2073
+ document.documentElement.classList.add(result.hostContext.theme);
2074
+ document.documentElement.setAttribute('data-theme', result.hostContext.theme);
2075
+ }
2076
+ // Complete handshake
2077
+ postToHost({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
2078
+
2079
+ // Set up size notifications after handshake
2080
+ setTimeout(sendSizeChanged, 100);
2081
+ var resizeObserver = new ResizeObserver(function() {
2082
+ sendSizeChanged();
2083
+ });
2084
+ resizeObserver.observe(document.documentElement);
2085
+ resizeObserver.observe(document.body);
2086
+ },
2087
+ reject: function(err) { console.error('MCP Apps init failed:', err); }
2088
+ };
2089
+
2090
+ postToHost({
2091
+ jsonrpc: '2.0',
2092
+ id: initId,
2093
+ method: 'ui/initialize',
2094
+ params: {
2095
+ appInfo: { name: '${photonName}', version: '1.0.0' },
2096
+ appCapabilities: {},
2097
+ protocolVersion: '2026-01-26'
2098
+ }
2099
+ });
2100
+ })();
2101
+ </script>`;
1734
2102
  }
1735
2103
  /**
1736
2104
  * Handle legacy photon:// asset read
@@ -1743,7 +2111,7 @@ export class PhotonServer {
1743
2111
  const ui = this.mcp.assets.ui.find((u) => u.id === assetId);
1744
2112
  if (ui) {
1745
2113
  resolvedPath = ui.resolvedPath;
1746
- mimeType = ui.mimeType || 'text/html';
2114
+ mimeType = ui.mimeType || 'text/html;profile=mcp-app';
1747
2115
  }
1748
2116
  }
1749
2117
  else if (assetType === 'prompts') {
@@ -1761,7 +2129,12 @@ export class PhotonServer {
1761
2129
  }
1762
2130
  }
1763
2131
  if (resolvedPath) {
1764
- const content = await fs.readFile(resolvedPath, 'utf-8');
2132
+ let content = await fs.readFile(resolvedPath, 'utf-8');
2133
+ // Inject MCP Apps bridge for UI assets
2134
+ if (assetType === 'ui') {
2135
+ const bridgeScript = this.generateMcpAppsBridge();
2136
+ content = content.replace('<head>', `<head>\n${bridgeScript}`);
2137
+ }
1765
2138
  return {
1766
2139
  contents: [{ uri, mimeType, text: content }],
1767
2140
  };