/ src / preload / index.ts
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)