ToolHive developer guide

May 12, 2026 · View on GitHub

This is the front-end for ToolHive, an Electron application built with React, TypeScript, and Vite.

Getting started

This project uses pnpm as the package manager.

It is recommended to use a Node.js version manager like nvm or fnm. The required Node.js version is specified in the .nvmrc file (which points to the latest LTS version).

[!IMPORTANT] Make sure the Docker daemon is running before you start, as it is required by ToolHive.

To get started, follow these steps:

  1. Install dependencies:

    pnpm install
    
  2. Make sure thv is downloaded and runs properly:

    pnpm thv
    

    This command needs to be run once to build the application before starting the development server.

  3. Start the development server:

    pnpm run start
    

    This will start the Electron application with hot reload for the renderer process.

Available scripts

Here are the most common scripts you will use during development:

  • pnpm run start: Starts the development server with hot reload.
  • pnpm run lint: Lints the code using ESLint.
  • pnpm run format: Formats the code with Prettier.
  • pnpm run type-check: Runs TypeScript type checking.
  • pnpm run test: Runs tests using Vitest.
  • pnpm run test:coverage: Runs tests with coverage.
  • pnpm run thv: Run the same thv binary that the dev server uses

End-to-end tests

E2E tests use Playwright to test the packaged Electron application.

  • pnpm run e2e: Packages the app and runs e2e tests. Use this for a full test run from scratch.
  • pnpm run e2e:prebuilt: Runs e2e tests against an already packaged app in out/. Use this when iterating on tests without rebuilding.
  • Detailed E2E guide: docs/e2e-testing.md

See the deep link design document for background and rationale.

The app registers the toolhive-gui:// protocol for deep linking. Deep links follow the format:

toolhive-gui://<version>/<intent>?<params>

Example deep links:

toolhive-gui://v1/open-registry-server-detail?serverName=fetch
toolhive-gui://v1/open-registry-server-detail?serverName=time

Linux

Register the protocol handler (builds a .deb, extracts and installs the .desktop file system-wide):

pnpm run deeplink:registerTestLinuxProtocolHandler

Once registered, you can open deep links from the browser or with xdg-open:

xdg-open "toolhive-gui://v1/open-registry-server-detail?serverName=fetch"

You can check whether the protocol handler is registered with:

xdg-mime query default x-scheme-handler/toolhive-gui

Building and packaging

  • pnpm run package: Packages the application for the current platform.
  • pnpm run make: Creates distributable packages for the application.

API client generation

  • pnpm run generate-client: Fetches the latest OpenAPI specification and generates the API client.
  • pnpm run generate-client:nofetch: Generates the API client from the existing local OpenAPI specification.

Project structure

The project is structured as a typical Electron application:

  • main/: Contains the code for the Electron main process.
  • preload/: Contains the preload scripts for the Electron renderer process.
  • renderer/: Contains the React application for the renderer process. This is where the UI components live.

Environment variables

Important


Electron applications can be decompiled, so do not store sensitive information in runtime environment variables. Use secure methods to handle sensitive data.

The project uses environment variables for configuration.

You can set these in a .env file in the root directory when developing locally. The .env.example file provides a template for the required variables.

For building and deploying the application, these should be configured in Github actions secrets/variables (as appropriate).

To expose environment variables at run time, you need to prefix them with VITE_. This will make them available on import.meta.env (not process.env))

For example, if you want to expose a variable named API_URL, you should define it as VITE_API_URL in the .env file (locally) or in the CI environment.

VariableRequiredBuild-timeRun-timeDescription
VITE_SENTRY_DSNfalsetruetrueSentry DSN. The URL that events are posted to.
VITE_ENABLE_AUTO_DEVTOOLSfalsefalsetrueEnable automatic opening of DevTools in development mode. Set to true to enable.
SENTRY_AUTH_TOKENfalsetruefalseSentry authentication token. Used for sourcemap uploads at build-time to enable readable stacktraces.
SENTRY_ORGfalsetruefalseSentry organization. Used for sourcemap uploads at build-time to enable readable stacktraces.
SENTRY_PROJECTfalsetruefalseSentry project name. Used for sourcemap uploads at build-time to enable readable stacktraces.

Developer notes: Simulating OS design variants

The app renders different window chrome depending on the platform — macOS uses the system traffic-light buttons and extra left padding, while Windows/Linux renders custom min/max/close controls.

To test either layout without switching machines, use the OsDesign helper exposed on window in the renderer DevTools console:

OsDesign.setMac() // macOS layout (traffic-light padding, no custom controls)
OsDesign.setWindows() // Windows/Linux layout (custom min/max/close buttons)
OsDesign.reset() // restore the real platform detection
OsDesign.current() // log the currently active variant

Each set* call writes to sessionStorage and reloads the page. The override is scoped to the current session and does not affect any behavioural logic (file paths, networking flags, etc.) — only the visual layout.

Tip: Open DevTools with Ctrl+Shift+I (or Cmd+Option+I on macOS), then run the commands in the Console tab.

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

The studio talks to its managed thv over a UNIX domain socket on macOS/Linux and a Windows named pipe on Windows. To test the UI with a custom thv binary, run it manually with the same --socket flag the studio uses internally and point the studio at it via THV_SOCKET:

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

    macOS / Linux

    thv serve \
      --openapi \
      --socket=/tmp/thv-dev.sock \
      --experimental-mcp \
      --experimental-mcp-host=127.0.0.1 \
      --experimental-mcp-port=50001
    

    Windows (PowerShell)

    thv.exe serve `
      --openapi `
      --socket='\\.\pipe\thv-dev' `
      --experimental-mcp `
      --experimental-mcp-host=127.0.0.1 `
      --experimental-mcp-port=50001
    
  2. Set THV_SOCKET (and THV_MCP_PORT if you also need the experimental MCP backend) and start the dev server:

    THV_SOCKET=/tmp/thv-dev.sock THV_MCP_PORT=50001 pnpm start
    

    On Windows:

    $env:THV_SOCKET = '\\.\pipe\thv-dev'; $env:THV_MCP_PORT = '50001'; pnpm start
    

The UI displays a banner with the socket / pipe path when THV_SOCKET is set. This works in development mode only; packaged builds use the embedded binary and an auto-generated per-process socket path.

Code signing

Supports both macOS and Windows code signing. macOS uses Apple certificates. Windows uses Azure Trusted Signing (preferred) with a DigiCert KeyLocker fallback during the migration.

Local development

macOS

Optional: Set MAC_DEVELOPER_IDENTITY in .env to use a specific certificate:

MAC_DEVELOPER_IDENTITY="Developer ID Application: Your Name (TEAM123)"

Local signing is not required for development.

CI/CD

macOS Signing

Requires these GitHub secrets:

  • APPLE_CERTIFICATE - Base64 encoded .p12 certificate
  • APPLE_CERTIFICATE_PASSWORD - Certificate password
  • KEYCHAIN_PASSWORD - Temporary keychain password
  • APPLE_API_KEY - Base64 encoded .p8 API key
  • APPLE_ISSUER_ID - Apple API Issuer ID
  • APPLE_KEY_ID - Apple API Key ID

Windows Signing (Azure Trusted Signing)

Authentication uses OIDC workload identity federation — no client secret is stored in the repo. The signing job must run in the artifact-signing GitHub environment (its federated credential subject is repo:stacklok/toolhive-studio:environment:artifact-signing), and the six values below must be stored as environment secrets on that environment (Settings → Environments → artifact-signing):

  • AZURE_ARTIFACT_SIGNING_CLIENT_ID - Service principal client ID
  • AZURE_ARTIFACT_SIGNING_TENANT_ID - Azure AD tenant ID
  • AZURE_ARTIFACT_SIGNING_SUBSCRIPTION_ID - Azure subscription ID
  • AZURE_ARTIFACT_SIGNING_ENDPOINT - Trusted Signing account endpoint URL
  • AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME - Trusted Signing account name
  • AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME - Certificate profile name

The setup-azure-trusted-signing composite action handles Azure login (OIDC), installs the Azure Code Signing DLib, and writes the metadata.json consumed by signtool.exe. Electron Forge picks this up through utils/windows-sign-azure.ts and signs the app + installer during pnpm run make / pnpm run publish.

Where it's wired up today:

  • pr-build-test.yml — any PR that opts in via /build-test --sign-windows.
  • on-release.ymlprerelease tags only (*-alpha, *-beta, *-rc). Stable releases still sign via DigiCert until the Azure flow is validated end-to-end on prereleases.

The artifact-signing environment is activated only for the Windows matrix row of those jobs. Non-Windows rows and stable Windows releases stay outside the environment, so they don't pick up its secrets or gating.

Windows Signing (DigiCert KeyLocker — legacy fallback)

Kept for stable releases while Azure Trusted Signing is validated on prereleases. Used by on-release.yml when github.event.release.prerelease != true. Remove the step and the setup-windows-codesign action once stable releases are also migrated. Requires these GitHub secrets:

  • SM_HOST - DigiCert KeyLocker host URL
  • SM_API_KEY - DigiCert KeyLocker API key
  • SM_CLIENT_CERT_FILE_B64 - Base64 encoded client certificate (.p12)
  • SM_CLIENT_CERT_PASSWORD - Client certificate password
  • SM_CODE_SIGNING_CERT_SHA1_HASH - SHA1 fingerprint of the code signing certificate

forge.config.ts prefers Azure Trusted Signing when its env vars are present, and falls back to DigiCert when SM_HOST + SM_API_KEY are set. If neither is configured the build produces unsigned artifacts.

ESLint configuration

The project uses ESLint with typescript-eslint for linting TypeScript code. The configuration is in the eslint.config.mjs file. It includes rules for React hooks and React Refresh.

Code of conduct

This project adheres to the Contributor Covenant code of conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to code-of-conduct@stacklok.dev.


Contributing

We welcome contributions and feedback from the community!

If you have ideas, suggestions, or want to get involved, check out our contributing guide or open an issue. Join us in making ToolHive even better!


License

This project is licensed under the Apache 2.0 License.