Skip to content

Commit 088cf92

Browse files
authored
chore: simplify running with different thv binary (#1206)
1 parent 148b57b commit 088cf92

File tree

7 files changed

+123
-2
lines changed

7 files changed

+123
-2
lines changed

docs/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,37 @@ it as `VITE_API_URL` in the `.env` file (locally) or in the CI environment.
102102
| `SENTRY_ORG` | `false` | `true` | `false` | Sentry organization. Used for sourcemap uploads at build-time to enable readable stacktraces. |
103103
| `SENTRY_PROJECT` | `false` | `true` | `false` | Sentry project name. Used for sourcemap uploads at build-time to enable readable stacktraces. |
104104

105+
## Developer notes: Using a custom thv binary (dev only)
106+
107+
During development, you can test the UI with a custom `thv` binary by running it
108+
manually:
109+
110+
1. Start your custom `thv` binary with the serve command:
111+
112+
```bash
113+
thv serve \
114+
--openapi \
115+
--host=127.0.0.1 --port=50000 \
116+
--experimental-mcp \
117+
--experimental-mcp-host=127.0.0.1 \
118+
--experimental-mcp-port=50001
119+
```
120+
121+
2. Set the `THV_PORT` and `THV_MCP_PORT` environment variables and start the dev
122+
server.
123+
124+
```bash
125+
THV_PORT=50000 THV_MCP_PORT=50001 pnpm start
126+
```
127+
128+
The UI displays a banner with the HTTP address when using a custom port. This
129+
works in development mode only; packaged builds use the embedded binary.
130+
131+
> Note on MCP Optimizer If you plan to use the MCP Optimizer with an external
132+
> `thv`, ensure `THV_PORT` is within the range `50000-50100`. The app starts its
133+
> embedded server in this range, and the optimizer expects the ToolHive API to
134+
> be reachable there.
135+
105136
## Code signing
106137

107138
Supports both macOS and Windows code signing. macOS uses Apple certificates,

main/src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
isToolhiveRunning,
4545
binPath,
4646
getToolhiveMcpPort,
47+
isUsingCustomPort,
4748
} from './toolhive-manager'
4849
import log from './logger'
4950
import { getInstanceId, isOfficialReleaseBuild } from './util'
@@ -455,6 +456,7 @@ ipcMain.handle('quit-app', (e) => {
455456
ipcMain.handle('get-toolhive-port', () => getToolhivePort())
456457
ipcMain.handle('get-toolhive-mcp-port', () => getToolhiveMcpPort())
457458
ipcMain.handle('is-toolhive-running', () => isToolhiveRunning())
459+
ipcMain.handle('is-using-custom-port', () => isUsingCustomPort())
458460

459461
// Window control handlers for custom title bar
460462
ipcMain.handle('window-minimize', () => {

main/src/toolhive-manager.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ export function isToolhiveRunning(): boolean {
4444
return isRunning
4545
}
4646

47+
/**
48+
* Returns whether the app is using a custom ToolHive port (externally managed thv).
49+
*/
50+
export function isUsingCustomPort(): boolean {
51+
return !app.isPackaged && !!process.env.THV_PORT
52+
}
53+
4754
async function findFreePort(
4855
minPort?: number,
4956
maxPort?: number
@@ -103,13 +110,29 @@ async function findFreePort(
103110

104111
export async function startToolhive(): Promise<void> {
105112
Sentry.withScope<Promise<void>>(async (scope) => {
113+
if (isUsingCustomPort()) {
114+
const customPort = parseInt(process.env.THV_PORT!, 10)
115+
if (isNaN(customPort)) {
116+
log.error(
117+
`Invalid THV_PORT environment variable: ${process.env.THV_PORT}`
118+
)
119+
return
120+
}
121+
toolhivePort = customPort
122+
toolhiveMcpPort = process.env.THV_MCP_PORT
123+
? parseInt(process.env.THV_MCP_PORT!, 10)
124+
: undefined
125+
log.info(`Using external ToolHive on port ${toolhivePort}`)
126+
return
127+
}
128+
106129
if (!existsSync(binPath)) {
107130
log.error(`ToolHive binary not found at: ${binPath}`)
108131
return
109132
}
110133

111-
toolhivePort = await findFreePort(50000, 50100)
112134
toolhiveMcpPort = await findFreePort()
135+
toolhivePort = await findFreePort(50000, 50100)
113136
log.info(
114137
`Starting ToolHive from: ${binPath} on port ${toolhivePort}, MCP on port ${toolhiveMcpPort}`
115138
)

preload/src/preload.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
4040
getToolhiveVersion: () => TOOLHIVE_VERSION,
4141
// ToolHive is running
4242
isToolhiveRunning: () => ipcRenderer.invoke('is-toolhive-running'),
43+
isUsingCustomPort: () => ipcRenderer.invoke('is-using-custom-port'),
4344

4445
// Container engine check
4546
checkContainerEngine: () => ipcRenderer.invoke('check-container-engine'),
@@ -240,8 +241,9 @@ export interface ElectronAPI {
240241
quitApp: () => Promise<void>
241242
getToolhivePort: () => Promise<number | undefined>
242243
getToolhiveMcpPort: () => Promise<number | undefined>
243-
getToolhiveVersion: () => Promise<string>
244+
getToolhiveVersion: () => string
244245
isToolhiveRunning: () => Promise<boolean>
246+
isUsingCustomPort: () => Promise<boolean>
245247
checkContainerEngine: () => Promise<{
246248
docker: boolean
247249
podman: boolean
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useEffect, useState } from 'react'
2+
import { AlertTriangle } from 'lucide-react'
3+
import { Alert, AlertDescription } from './ui/alert'
4+
import log from 'electron-log/renderer'
5+
6+
/**
7+
* Banner that displays a warning when using a custom ToolHive port in development mode.
8+
* Only visible when THV_PORT environment variable is set.
9+
*/
10+
export function CustomPortBanner() {
11+
const [isCustomPort, setIsCustomPort] = useState(false)
12+
const [port, setPort] = useState<number | undefined>(undefined)
13+
14+
useEffect(() => {
15+
Promise.all([
16+
window.electronAPI.isUsingCustomPort(),
17+
window.electronAPI.getToolhivePort(),
18+
])
19+
.then(([usingCustom, toolhivePort]) => {
20+
setIsCustomPort(usingCustom)
21+
setPort(toolhivePort)
22+
})
23+
.catch((error: unknown) => {
24+
log.error('Failed to get custom port info:', error)
25+
})
26+
}, [])
27+
28+
// Don't render if not using custom port or port is not available
29+
if (!isCustomPort || !port) {
30+
return null
31+
}
32+
33+
const httpAddress = `http://127.0.0.1:${port}`
34+
35+
return (
36+
<Alert
37+
className="fixed bottom-4 left-1/2 z-50 w-auto max-w-[95vw]
38+
-translate-x-1/2 border-yellow-200 bg-yellow-50 text-yellow-900
39+
dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-100"
40+
>
41+
<AlertTriangle />
42+
<AlertDescription className="flex items-start gap-2">
43+
<div className="whitespace-nowrap">
44+
<span>Using external ToolHive at </span>
45+
<span
46+
className="rounded bg-yellow-100 px-1 py-0.5 font-mono text-xs
47+
dark:bg-yellow-900"
48+
title={httpAddress}
49+
>
50+
{httpAddress}
51+
</span>
52+
</div>
53+
</AlertDescription>
54+
</Alert>
55+
)
56+
}

renderer/src/routes/__root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import '@fontsource-variable/inter/wght.css'
2222
import log from 'electron-log/renderer'
2323
import * as Sentry from '@sentry/electron/renderer'
2424
import { StartingToolHive } from '@/common/components/starting-toolhive'
25+
import { CustomPortBanner } from '@/common/components/custom-port-banner'
2526

2627
async function setupSecretProvider(queryClient: QueryClient) {
2728
const createEncryptedProvider = async () =>
@@ -53,6 +54,7 @@ function RootComponent() {
5354
return (
5455
<>
5556
{!isShutdownRoute && <TopNav />}
57+
{!isShutdownRoute && import.meta.env.DEV && <CustomPortBanner />}
5658
<Main>
5759
<Outlet />
5860
<Toaster

renderer/src/vite-env.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
interface ImportBaseApiEnv {
33
readonly VITE_BASE_API_URL: string
44
}
5+
6+
// Extend renderer env typings for custom development flag
7+
interface ImportMetaEnv extends ImportBaseApiEnv {
8+
readonly THV_PORT?: string
9+
}

0 commit comments

Comments
 (0)