index.ts
1 import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron' 2 import type { AsyncFunction, MCPAPI, DXTAPI, McpMetadataDxt, ClientProfile } from '@/types/mcp' 3 import type { LlmConfig } from '@/types/llm' 4 import type { PopupConfig } from '@/types/popup' 5 import type { StartupConfig } from '@/types/startup' 6 7 // Whitelist of valid channels used for IPC communication (Send message from Renderer to Main) 8 const mainAvailChannels: string[] = [ 9 'msgRequestAppInfo', 10 'msgRequestGetDxtUrl', 11 'msgOpenExternalLink', 12 'msgOpenDxtFilePath', 13 'msgOpenPath', 14 'msgOpenFile', 15 'msgFileTransferRequest', 16 'msgCommandSelectionResult', 17 'msgGetApiToken', 18 'msgMcpServersInit', 19 'msgMcpServersStop', 20 'msgWindowReload' 21 ] 22 23 const rendererAvailChannels: string[] = [ 24 'renderListenStdioProgress', 25 'msgSamplingTransferInvoke', 26 'msgElicitationTransferInvoke', 27 'msgCommandSelectionInvoke', 28 'msgMcpServersWatch', 29 'msgFileTransferResponse' 30 ] 31 32 const rendererDynamicChannels: string[] = [ 33 'msgSamplingTransferResult', 34 'msgElicitationTransferResult' 35 ] 36 37 contextBridge.exposeInMainWorld('mainApi', { 38 send: (channel: string, ...data: any[]): void => { 39 if ( 40 mainAvailChannels.includes(channel) || 41 rendererDynamicChannels.some((respChannel) => channel.startsWith(respChannel)) 42 ) { 43 ipcRenderer.send.apply(null, [channel, ...data]) 44 if (process.env.NODE_ENV === 'development') { 45 console.log({ type: 'send', channel, request: data }) 46 } 47 } else { 48 throw new Error(`Unknown ipc channel name: ${channel}`) 49 } 50 }, 51 on: (channel: string, listener: (_event: IpcRendererEvent, ..._args: any[]) => void): void => { 52 if (rendererAvailChannels.includes(channel)) { 53 ipcRenderer.on(channel, listener) 54 } else { 55 throw new Error(`Unknown ipc channel name: ${channel}`) 56 } 57 }, 58 removeListener: ( 59 channel: string, 60 listener: (_event: IpcRendererEvent, ..._args: any[]) => void 61 ): void => { 62 if (rendererAvailChannels.includes(channel)) { 63 ipcRenderer.removeListener(channel, listener) 64 } else { 65 throw new Error(`Unknown ipc channel name: ${channel}`) 66 } 67 }, 68 once: (channel: string, listener: (_event: IpcRendererEvent, ..._args: any[]) => void): void => { 69 if (rendererAvailChannels.includes(channel)) { 70 ipcRenderer.once(channel, listener) 71 } else { 72 throw new Error(`Unknown ipc channel name: ${channel}`) 73 } 74 }, 75 off: (channel: string, listener: (_event: IpcRendererEvent, ..._args: any[]) => void): void => { 76 if (rendererAvailChannels.includes(channel)) { 77 ipcRenderer.off(channel, listener) 78 } else { 79 throw new Error(`Unknown ipc channel name: ${channel}`) 80 } 81 }, 82 invoke: async (channel: string, ...data: any[]): Promise<any> => { 83 if (mainAvailChannels.includes(channel)) { 84 const result = await ipcRenderer.invoke.apply(null, [channel, ...data]) 85 if (process.env.NODE_ENV === 'development') { 86 console.log({ type: 'invoke', channel, request: data, result }) 87 } 88 return result 89 } 90 91 throw new Error(`Unknown ipc channel name: ${channel}`) 92 } 93 }) 94 95 /* ------------------------------ LLM Config ------------------------------ */ 96 97 const llm = { 98 _currentAPI: {} as LlmConfig, 99 get: () => { 100 return llm._currentAPI 101 } 102 } 103 104 async function initLLM() { 105 const llms: LlmConfig = await ipcRenderer.invoke('list-llms') 106 llm._currentAPI = llms 107 } 108 109 initLLM() 110 111 contextBridge.exposeInMainWorld('llmApis', llm) 112 113 /* ------------------------------ Popup Config ------------------------------ */ 114 115 const popup = { 116 _currentAPI: {} as PopupConfig, 117 get: () => { 118 return popup._currentAPI 119 } 120 } 121 122 async function initPopup() { 123 const popups: PopupConfig = await ipcRenderer.invoke('list-popups') 124 popup._currentAPI = popups 125 } 126 127 initPopup() 128 129 contextBridge.exposeInMainWorld('popupApis', popup) 130 131 /* ------------------------------ LLM Config ------------------------------ */ 132 133 const startup = { 134 _currentAPI: {} as StartupConfig, 135 get: () => { 136 return startup._currentAPI 137 } 138 } 139 140 async function initStartup() { 141 const startups: StartupConfig = await ipcRenderer.invoke('list-startups') 142 startup._currentAPI = startups 143 } 144 145 initStartup() 146 147 contextBridge.exposeInMainWorld('startupApis', startup) 148 149 /* ------------------------------ MCP Client Config ------------------------------ */ 150 151 async function listClients(): Promise<ClientProfile[]> { 152 return await ipcRenderer.invoke('list-clients') 153 } 154 155 function createAPIMethods(methods: Record<string, string>) { 156 const result: Record<string, AsyncFunction> = {} 157 Object.keys(methods).forEach((key) => { 158 const methodName = methods[key] 159 result[key] = (...args: any[]) => ipcRenderer.invoke(methodName, ...args) 160 }) 161 return result 162 } 163 164 const api = { 165 _currentAPI: {}, 166 get: () => { 167 // console.log('Preload currentAPI:', api._currentAPI) 168 return api._currentAPI 169 }, 170 refresh: async () => { 171 await refreshAPI() 172 return api._currentAPI 173 }, 174 update: async (name: string) => { 175 await updateAPI(name) 176 return api._currentAPI 177 } 178 } 179 180 function buildClientAPI(client: ClientProfile): MCPAPI[string] { 181 const { name, tools, prompts, resources, config, description } = client 182 const apiItem: MCPAPI[string] = {} 183 184 if (tools) apiItem.tools = createAPIMethods(tools) 185 if (prompts) apiItem.prompts = createAPIMethods(prompts) 186 if (resources) apiItem.resources = createAPIMethods(resources) 187 188 const metadata = { 189 name, 190 type: 'metadata__stdio_config' as const, 191 config, 192 description 193 } 194 195 apiItem.metadata = metadata 196 197 return apiItem 198 } 199 200 async function refreshAPI() { 201 const clients = await listClients() 202 const newAPI: MCPAPI = {} 203 204 clients.forEach((client) => { 205 newAPI[client.name] = buildClientAPI(client) 206 }) 207 208 const dxtManifests = await traverseManifest() 209 210 Object.keys(dxtManifests).forEach((key) => { 211 const manifest = dxtManifests[key] 212 const metadata: McpMetadataDxt = { 213 name: key, 214 type: 'metadata__mcpb_manifest', 215 config: manifest 216 } 217 if (newAPI[key]) { 218 // If key exists, only update/replace the metadata 219 newAPI[key].metadata = metadata 220 } else { 221 // If key doesn't exist, create new entry 222 newAPI[key] = { metadata } 223 } 224 }) 225 226 api._currentAPI = newAPI 227 } 228 229 async function updateAPI(name: string) { 230 const clients = await listClients() 231 const client = clients.find((c) => c.name === name) 232 if (!client) return 233 234 api._currentAPI[name] = buildClientAPI(client) 235 } 236 237 refreshAPI() 238 239 contextBridge.exposeInMainWorld('mcpServers', api) 240 241 const dxt = { 242 _currentAPI: {}, 243 get: () => { 244 console.log('List currentDXT:', dxt._currentAPI) 245 return dxt._currentAPI 246 }, 247 refresh: async () => { 248 await refreshDXT() 249 return dxt._currentAPI 250 }, 251 update: async (name: string) => { 252 void name 253 // await updateAPI(name) 254 return dxt._currentAPI 255 } 256 } 257 258 async function traverseManifest(): Promise<DXTAPI> { 259 const manifests = await ipcRenderer.invoke('list-manifests') 260 return manifests.result || {} 261 } 262 263 async function refreshDXT() { 264 const manifests = await traverseManifest() 265 dxt._currentAPI = manifests 266 } 267 268 refreshDXT() 269 270 contextBridge.exposeInMainWorld('dxtManifest', dxt)