Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,37 @@ it as `VITE_API_URL` in the `.env` file (locally) or in the CI environment.
| `SENTRY_ORG` | `false` | `true` | `false` | Sentry organization. Used for sourcemap uploads at build-time to enable readable stacktraces. |
| `SENTRY_PROJECT` | `false` | `true` | `false` | Sentry project name. Used for sourcemap uploads at build-time to enable readable stacktraces. |

## Developer notes: Using a custom thv binary (dev only)

During development, you can test the UI with a custom `thv` binary by running it
manually:

1. Start your custom `thv` binary with the serve command:

```bash
thv serve \
--openapi \
--host=127.0.0.1 --port=50000 \
--experimental-mcp \
--experimental-mcp-host=127.0.0.1 \
--experimental-mcp-port=50001
```

2. Set the `THV_PORT` and `THV_MCP_PORT` environment variables and start the dev
server.

```bash
THV_PORT=50000 THV_MCP_PORT=50001 pnpm start
```

The UI displays a banner with the HTTP address when using a custom port. This
works in development mode only; packaged builds use the embedded binary.

> Note on MCP Optimizer If you plan to use the MCP Optimizer with an external
> `thv`, ensure `THV_PORT` is within the range `50000-50100`. The app starts its
> embedded server in this range, and the optimizer expects the ToolHive API to
> be reachable there.

## Code signing

Supports both macOS and Windows code signing. macOS uses Apple certificates,
Expand Down
2 changes: 2 additions & 0 deletions main/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
isToolhiveRunning,
binPath,
getToolhiveMcpPort,
isUsingCustomPort,
} from './toolhive-manager'
import log from './logger'
import { getInstanceId, isOfficialReleaseBuild } from './util'
Expand Down Expand Up @@ -451,6 +452,7 @@ ipcMain.handle('quit-app', (e) => {
ipcMain.handle('get-toolhive-port', () => getToolhivePort())
ipcMain.handle('get-toolhive-mcp-port', () => getToolhiveMcpPort())
ipcMain.handle('is-toolhive-running', () => isToolhiveRunning())
ipcMain.handle('is-using-custom-port', () => isUsingCustomPort())

// Window control handlers for custom title bar
ipcMain.handle('window-minimize', () => {
Expand Down
25 changes: 24 additions & 1 deletion main/src/toolhive-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export function isToolhiveRunning(): boolean {
return isRunning
}

/**
* Returns whether the app is using a custom ToolHive port (externally managed thv).
*/
export function isUsingCustomPort(): boolean {
return !app.isPackaged && !!process.env.THV_PORT
}

async function findFreePort(
minPort?: number,
maxPort?: number
Expand Down Expand Up @@ -103,13 +110,29 @@ async function findFreePort(

export async function startToolhive(): Promise<void> {
Sentry.withScope<Promise<void>>(async (scope) => {
if (isUsingCustomPort()) {
const customPort = parseInt(process.env.THV_PORT!, 10)
if (isNaN(customPort)) {
log.error(
`Invalid THV_PORT environment variable: ${process.env.THV_PORT}`
)
return
}
toolhivePort = customPort
toolhiveMcpPort = process.env.THV_MCP_PORT
? parseInt(process.env.THV_MCP_PORT!, 10)
: undefined
log.info(`Using external ToolHive on port ${toolhivePort}`)
return
}

if (!existsSync(binPath)) {
log.error(`ToolHive binary not found at: ${binPath}`)
return
}

toolhivePort = await findFreePort(50000, 50100)
toolhiveMcpPort = await findFreePort()
toolhivePort = await findFreePort(50000, 50100)
log.info(
`Starting ToolHive from: ${binPath} on port ${toolhivePort}, MCP on port ${toolhiveMcpPort}`
)
Expand Down
4 changes: 3 additions & 1 deletion preload/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
getToolhiveVersion: () => TOOLHIVE_VERSION,
// ToolHive is running
isToolhiveRunning: () => ipcRenderer.invoke('is-toolhive-running'),
isUsingCustomPort: () => ipcRenderer.invoke('is-using-custom-port'),

// Container engine check
checkContainerEngine: () => ipcRenderer.invoke('check-container-engine'),
Expand Down Expand Up @@ -233,8 +234,9 @@ export interface ElectronAPI {
quitApp: () => Promise<void>
getToolhivePort: () => Promise<number | undefined>
getToolhiveMcpPort: () => Promise<number | undefined>
getToolhiveVersion: () => Promise<string>
getToolhiveVersion: () => string
isToolhiveRunning: () => Promise<boolean>
isUsingCustomPort: () => Promise<boolean>
checkContainerEngine: () => Promise<{
docker: boolean
podman: boolean
Expand Down
55 changes: 55 additions & 0 deletions renderer/src/common/components/custom-port-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react'
import { AlertTriangle } from 'lucide-react'
import { Alert, AlertDescription } from './ui/alert'

/**
* Banner that displays a warning when using a custom ToolHive port in development mode.
* Only visible when THV_PORT environment variable is set.
*/
export function CustomPortBanner() {
const [isCustomPort, setIsCustomPort] = useState(false)
const [port, setPort] = useState<number | undefined>(undefined)

useEffect(() => {
Promise.all([
window.electronAPI.isUsingCustomPort(),
window.electronAPI.getToolhivePort(),
])
.then(([usingCustom, toolhivePort]) => {
setIsCustomPort(usingCustom)
setPort(toolhivePort)
})
.catch((error: unknown) => {
console.error('Failed to get custom port info:', error)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use log instead of console.log

})
}, [])

// Don't render if not using custom port or port is not available
if (!isCustomPort || !port) {
return null
}

const httpAddress = `http://127.0.0.1:${port}`

return (
<Alert
variant="warning"
className="fixed bottom-4 left-1/2 z-50 w-auto max-w-[95vw]
-translate-x-1/2"
>
<AlertTriangle />
<AlertDescription className="flex items-start gap-2">
<div className="whitespace-nowrap">
<span>Using external ToolHive at </span>
<span
className="rounded bg-yellow-100 px-1 py-0.5 font-mono text-xs
dark:bg-yellow-900"
title={httpAddress}
>
{httpAddress}
</span>
</div>
</AlertDescription>
</Alert>
)
}
2 changes: 2 additions & 0 deletions renderer/src/common/components/ui/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const alertVariants = cva(
default: 'bg-card text-card-foreground',
destructive:
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
warning:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove warning style from here and instead override in the component itself (because the warning is not an official design)

'border-yellow-200 bg-yellow-50 text-yellow-900 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-100',
},
},
defaultVariants: {
Expand Down
4 changes: 4 additions & 0 deletions renderer/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import '@fontsource-variable/inter/wght.css'
import log from 'electron-log/renderer'
import * as Sentry from '@sentry/electron/renderer'
import { StartingToolHive } from '@/common/components/starting-toolhive'
import { CustomPortBanner } from '@/common/components/custom-port-banner'

async function setupSecretProvider(queryClient: QueryClient) {
const createEncryptedProvider = async () =>
Expand Down Expand Up @@ -53,6 +54,9 @@ function RootComponent() {
return (
<>
{!isShutdownRoute && <TopNav />}
{!isShutdownRoute &&
import.meta.env.DEV &&
!!import.meta.env.THV_PORT && <CustomPortBanner />}
<Main>
<Outlet />
<Toaster
Expand Down
5 changes: 5 additions & 0 deletions renderer/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
interface ImportBaseApiEnv {
readonly VITE_BASE_API_URL: string
}

// Extend renderer env typings for custom development flag
interface ImportMetaEnv extends ImportBaseApiEnv {
readonly THV_PORT?: string
}
Loading