Skip to content
Merged
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
7 changes: 7 additions & 0 deletions common/app-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* App identity constants — single source of truth for build-configurable
* values shared across forge config, deep links, and scripts.
*/

export const DEEP_LINK_PROTOCOL = 'toolhive-gui'
export const FLATPAK_APP_ID = 'com.stacklok.ToolHive'
Comment thread
kantord marked this conversation as resolved.
2 changes: 0 additions & 2 deletions common/deep-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { z } from 'zod/v4'

const safeIdentifier = z.string().regex(/^[a-zA-Z0-9_.-]+$/)

export const DEEP_LINK_PROTOCOL = 'toolhive-gui'

const VERSION = 'v1'

export type NavigateTarget = {
Expand Down
14 changes: 11 additions & 3 deletions forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import 'dotenv/config'
import type { ForgeConfig } from '@electron-forge/shared-types'
import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm'
import { DEEP_LINK_PROTOCOL } from './common/app-info'
import MakerFlatpakBuilder from './utils/forge-makers/MakerFlatpakBuilder'
Comment thread
kantord marked this conversation as resolved.
import { VitePlugin } from '@electron-forge/plugin-vite'
import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses'
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'
import { ensureThv } from './utils/fetch-thv'
import { generateFlatpakAssets } from './utils/generate-flatpak-assets'
import MakerTarGz from './utils/forge-makers/MakerTarGz'
import MakerDMGWithArch from './utils/forge-makers/MakerDMGWithArch'
import { isPrerelease } from './utils/pre-release'
Expand Down Expand Up @@ -37,7 +39,7 @@ const config: ForgeConfig = {
protocols: [
{
name: 'ToolHive Studio',
schemes: ['toolhive-gui'],
schemes: [DEEP_LINK_PROTOCOL],
},
],
// Windows specific options
Expand Down Expand Up @@ -162,7 +164,7 @@ const config: ForgeConfig = {
requires: ['docker >= 20.10'],
license: 'Apache-2.0',
bin: 'ToolHive',
mimeType: ['x-scheme-handler/toolhive-gui'],
mimeType: [`x-scheme-handler/${DEEP_LINK_PROTOCOL}`],
},
}),
new MakerDeb({
Expand All @@ -176,7 +178,7 @@ const config: ForgeConfig = {
homepage: 'https://github.com/stacklok/toolhive-studio',
section: 'devel',
bin: 'ToolHive',
mimeType: ['x-scheme-handler/toolhive-gui'],
mimeType: [`x-scheme-handler/${DEEP_LINK_PROTOCOL}`],
},
}),
new MakerFlatpakBuilder({}, ['linux']),
Expand Down Expand Up @@ -244,6 +246,12 @@ const config: ForgeConfig = {

// Download/cache the exact binary needed for this build target
await ensureThv(platform, arch)

// Generate flatpak assets from app-info so protocol name and app ID
// stay in sync with constants in app-info.ts
if (platform === 'linux') {
await generateFlatpakAssets()
}
},
},
}
Expand Down
3 changes: 1 addition & 2 deletions main/src/cli/symlink-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ import { readMarkerFile } from './marker-file'
import { binPath } from '../toolhive-manager'
import type { SymlinkCheckResult, Platform } from './types'
import log from '../logger'

const FLATPAK_APP_ID = 'com.stacklok.ToolHive'
import { FLATPAK_APP_ID } from '@common/app-info'

function getFlatpakCliPath(): string {
if (process.arch !== 'x64' && process.arch !== 'arm64') {
Expand Down
7 changes: 2 additions & 5 deletions main/src/deep-links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import {
waitForMainWindowReady,
} from '../main-window'
import { parseDeepLinkUrl } from './parse'
import {
DEEP_LINK_PROTOCOL,
showNotFound,
resolveDeepLinkTarget,
} from '@common/deep-links'
import { showNotFound, resolveDeepLinkTarget } from '@common/deep-links'
import { DEEP_LINK_PROTOCOL } from '@common/app-info'

export { registerProtocolWithSquirrel } from './squirrel'

Expand Down
7 changes: 2 additions & 5 deletions main/src/deep-links/parse.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { z } from 'zod/v4'
import log from '../logger'
import {
DEEP_LINK_PROTOCOL,
deepLinkSchema,
type DeepLinkIntent,
} from '@common/deep-links'
import { deepLinkSchema, type DeepLinkIntent } from '@common/deep-links'
import { DEEP_LINK_PROTOCOL } from '@common/app-info'

type ParseResult =
| { ok: true; deepLink: DeepLinkIntent }
Expand Down
2 changes: 1 addition & 1 deletion main/src/deep-links/squirrel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { app } from 'electron'
import path from 'node:path'
import log from '../logger'
import { DEEP_LINK_PROTOCOL } from '@common/deep-links'
import { DEEP_LINK_PROTOCOL } from '@common/app-info'

/**
* Register the toolhive-gui:// protocol handler.
Expand Down
Empty file modified scripts/install-deep-link-handler.sh
100755 → 100644
Empty file.
4 changes: 3 additions & 1 deletion tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"main/src",
"preload/src",
"forge.env.d.ts",
"utils"
"utils",
"scripts",
"common"
],
"exclude": ["node_modules"]
}
3 changes: 1 addition & 2 deletions utils/forge-makers/MakerFlatpakBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import {
flatpakFilesystemEntries,
parseThvClients,
} from '../flatpak-client-paths'
import { FLATPAK_APP_ID as APP_ID } from '../../common/app-info'

const execFileAsync = promisify(execFile)

const APP_ID = 'com.stacklok.ToolHive'
const RUNTIME_VERSION = '24.08'

function runCommand(cmd: string, args: string[], cwd?: string): Promise<void> {
Expand Down
86 changes: 86 additions & 0 deletions utils/generate-flatpak-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { DEEP_LINK_PROTOCOL, FLATPAK_APP_ID } from '../common/app-info'

const FLATPAK_DIR = path.resolve(import.meta.dirname, '..', 'flatpak')

function desktopFileContent(): string {
return `[Desktop Entry]
Name=ToolHive
Comment=Install, manage and run MCP servers and connect them to AI agents and clients
GenericName=ToolHive
Exec=toolhive-wrapper %U
Icon=${FLATPAK_APP_ID}
Type=Application
StartupNotify=true
Categories=Development;Utility;
MimeType=x-scheme-handler/${DEEP_LINK_PROTOCOL};
`
}

function metainfoFileContent(): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>${FLATPAK_APP_ID}</id>
<name>ToolHive</name>
<summary>Install, manage and run MCP servers</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>Apache-2.0</project_license>

<developer id="com.stacklok">
<name>Stacklok</name>
</developer>

<description>
<p>ToolHive is an application that allows you to install, manage and run MCP servers and connect them to AI agents and clients.</p>
</description>

<launchable type="desktop-id">${FLATPAK_APP_ID}.desktop</launchable>

<url type="homepage">https://github.com/stacklok/toolhive-studio</url>
<url type="bugtracker">https://github.com/stacklok/toolhive-studio/issues</url>

<content_rating type="oars-1.1" />

<releases>
<release version="0.0.1" date="2026-02-18">
<description>
<p>Initial release.</p>
</description>
</release>
</releases>

<!-- TODO: Add real screenshots before Flathub submission -->
<screenshots>
<screenshot type="default">
<caption>ToolHive main window</caption>
<image>https://raw.githubusercontent.com/stacklok/toolhive-studio/main/assets/screenshot.png</image>
</screenshot>
</screenshots>
</component>
`
}

async function writeIfChanged(
filePath: string,
content: string
): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true })
const existing = await fs.readFile(filePath, 'utf8').catch(() => null)
if (existing !== content) {
await fs.writeFile(filePath, content, 'utf8')
}
}

export async function generateFlatpakAssets(): Promise<void> {
await Promise.all([
writeIfChanged(
path.join(FLATPAK_DIR, `${FLATPAK_APP_ID}.desktop`),
desktopFileContent()
),
writeIfChanged(
path.join(FLATPAK_DIR, `${FLATPAK_APP_ID}.metainfo.xml`),
metainfoFileContent()
),
])
}
Loading