@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.
- package/README.md +361 -339
- package/dist/auto-ui/beam.d.ts +5 -0
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +727 -51
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts +37 -0
- package/dist/auto-ui/bridge/index.d.ts.map +1 -0
- package/dist/auto-ui/bridge/index.js +555 -0
- package/dist/auto-ui/bridge/index.js.map +1 -0
- package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
- package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
- package/dist/auto-ui/bridge/openai-shim.js +231 -0
- package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
- package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
- package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
- package/dist/auto-ui/bridge/photon-app.js +460 -0
- package/dist/auto-ui/bridge/photon-app.js.map +1 -0
- package/dist/auto-ui/bridge/types.d.ts +128 -0
- package/dist/auto-ui/bridge/types.d.ts.map +1 -0
- package/dist/auto-ui/bridge/types.js +7 -0
- package/dist/auto-ui/bridge/types.js.map +1 -0
- package/dist/auto-ui/index.d.ts +3 -1
- package/dist/auto-ui/index.d.ts.map +1 -1
- package/dist/auto-ui/index.js +5 -2
- package/dist/auto-ui/index.js.map +1 -1
- package/dist/auto-ui/platform-compat.d.ts.map +1 -1
- package/dist/auto-ui/platform-compat.js +60 -6
- package/dist/auto-ui/platform-compat.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +25 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +581 -20
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +74 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js +21 -0
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +51377 -1778
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli.js +12 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon/client.d.ts +5 -3
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +30 -4
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +5 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +20 -0
- package/dist/daemon/manager.js.map +1 -1
- package/dist/loader.d.ts +23 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +77 -12
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +2 -0
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +1 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +25 -6
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/server.d.ts +12 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +386 -13
- package/dist/server.js.map +1 -1
- package/dist/template-manager.js +2 -2
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -1
- 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
|
|
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
|
-
//
|
|
1116
|
-
|
|
1117
|
-
|
|
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/
|
|
1122
|
-
params: {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
};
|