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
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,38 @@ To remove the server from all clients:
npx @kunobi/mcp --uninstall
```

### Updating

`install` pins your clients to an exact version (e.g. `@kunobi/mcp@1.2.3`) so each
launch runs from the npm cache instead of resolving `latest` from the registry on
every spawn — startup stays fast and can't stall on the network. When a newer
version is published, the server tells you on connect; apply it with:

```bash
npx @kunobi/mcp upgrade
```

This re-pins every client where Kunobi is registered to the newest version and
warms the cache. **Restart your AI client** to load it (the new version takes
effect on the next start).

**Installed before pinning existed?** Pin your current version in place — no
upgrade, no registry call:

```bash
npx @kunobi/mcp pin
```

If you ran an older build, the server also hints this on connect when it detects
an unpinned config. Silence the hint with `MCP_KUNOBI_NO_PIN_HINT=1`.

**Prefer always-latest?** Opt back out — each launch will resolve the newest
version again (slower startup, and can stall on the registry):

```bash
npx @kunobi/mcp unpin
```

### Manual

If you prefer manual setup, add the following to your client's MCP config:
Expand Down Expand Up @@ -153,6 +185,7 @@ kunobi-mcp remove juan
| `MCP_KUNOBI_RECONNECT_INTERVAL_MS` | `5000` | Reconnect interval in ms |
| `MCP_KUNOBI_VARIANTS` | — | `name:port` pairs to merge (e.g., `juan:4200,test:5000`) |
| `MCP_KUNOBI_AUTO_CONNECT` | `true` | Set `false` to disable automatic background connections. `kunobi_refresh` still works for manual retries. |
| `MCP_KUNOBI_NO_PIN_HINT` | — | Set `1` to suppress the startup hint that suggests pinning an unpinned install. |

Priority: config file defaults → `MCP_KUNOBI_VARIANTS` env var (merges on top).

Expand All @@ -167,8 +200,11 @@ kunobi-mcp [command] [options]
| `list` | Show configured variants and connection status |
| `add <name> <port>` | Add or update a variant |
| `remove <name>` | Remove a variant |
| `install` | Register this MCP server with your AI clients |
| `install` | Register this MCP server with your AI clients (pinned to the current version) |
| `uninstall` | Remove this MCP server from your AI clients |
| `pin` | Pin your AI clients to the current version (faster, hang-proof startup) |
| `unpin` | Revert to always-latest (`npx -y @kunobi/mcp`; slower startup) |
| `upgrade` | Re-pin your AI clients to the latest published version (restart to apply) |
| `--help`, `-h` | Show help message |
| `--version`, `-v` | Show version number |
| *(no command, piped)* | Start the stdio MCP server (used by AI clients) |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"dependencies": {
"@kunobi/mcp-bundler": "^0.0.10",
"@kunobi/mcp-installer": "^0.0.7",
"@kunobi/mcp-installer": "^0.0.8",
"@modelcontextprotocol/sdk": "^1.27.1",
"zod": "^4.3.6"
},
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

152 changes: 137 additions & 15 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node

import { createRequire } from 'node:module';
import type { RepinResult } from '@kunobi/mcp-installer';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { buildDiscoveryCatalog } from './catalog.js';
Expand All @@ -18,6 +19,35 @@ import { registerStatusTool } from './tools/status.js';
const require = createRequire(import.meta.url);
const { version } = require('../package.json') as { version: string };

const PKG = '@kunobi/mcp';

// Re-pin every client config where kunobi is registered to a given spec, and
// report. `spec` is the npm spec used as the last npx arg: `@kunobi/mcp@<v>` to
// pin, or bare `@kunobi/mcp` to unpin (always-latest). Shared by pin/unpin/upgrade.
async function repinAll(
spec: string,
verb: string,
): Promise<{ updated: RepinResult[]; failed: RepinResult[] }> {
const { repin } = await import('@kunobi/mcp-installer');
const results = repin({ name: 'kunobi', command: 'npx', args: ['-y', spec] });
const updated = results.filter((r) => r.action === 'updated');
const failed = results.filter((r) => r.action === 'error');

if (updated.length === 0 && failed.length === 0) {
console.error(
"kunobi is not registered in any AI client config. Run 'kunobi-mcp install' first.",
);
process.exit(1);
}
for (const r of updated) {
console.log(`${verb} ${r.client} (${r.scope}) — ${r.path}`);
}
for (const r of failed) {
console.error(`Failed: ${r.client} (${r.scope}): ${r.error}`);
}
return { updated, failed };
}

const arg = process.argv[2];
const HELP = `Kunobi MCP server v${version} — connects AI assistants to the Kunobi desktop app.

Expand All @@ -26,8 +56,11 @@ Usage:
kunobi-mcp list Show configured variants and connection status
kunobi-mcp add <name> <port> Add or update a variant
kunobi-mcp remove <name> Remove a variant
kunobi-mcp install Register this MCP server with your AI clients
kunobi-mcp install Register this MCP server with your AI clients (pinned)
kunobi-mcp uninstall Remove this MCP server from your AI clients
kunobi-mcp pin Pin your AI clients to the current version (faster, hang-proof startup)
kunobi-mcp unpin Revert to always-latest (npx -y @kunobi/mcp; slower startup)
kunobi-mcp upgrade Re-pin your AI clients to the latest published version (restart to apply)

Options:
--help, -h Show this help message
Expand Down Expand Up @@ -82,10 +115,14 @@ if (arg === 'remove') {

if (arg === 'install' || arg === '--install' || arg === '-i') {
const { install } = await import('@kunobi/mcp-installer');
// Pin to this exact version so client spawns run from the npm cache instead of
// resolving the `latest` dist-tag on every launch (a registry round-trip that
// is the slow/hang-prone part — see openai/codex #14470). Upgrades are explicit
// via `kunobi-mcp upgrade`, which re-pins to the newest version.
await install({
name: 'kunobi',
command: 'npx',
args: ['-y', '@kunobi/mcp'],
args: ['-y', `@kunobi/mcp@${version}`],
});
process.exit(0);
}
Expand All @@ -96,6 +133,60 @@ if (arg === 'uninstall' || arg === '--uninstall' || arg === '-u') {
process.exit(0);
}

if (arg === 'pin') {
// Pin every registered config to the CURRENT version. Offline, instant — no
// registry check. For users who installed before pinning, or manually.
const { failed } = await repinAll(`${PKG}@${version}`, 'Pinned');
console.log(`\nPinned to ${version}. Restart your AI client to apply.`);
process.exit(failed.length > 0 ? 1 : 0);
}

if (arg === 'unpin') {
// Revert to bare `npx -y @kunobi/mcp` (always-latest). This re-introduces the
// per-spawn `latest` resolution — slower startup that can stall on the registry
// — by choice, for users who want the newest build on every launch.
const { failed } = await repinAll(PKG, 'Unpinned');
console.log(
'\nUnpinned — each launch now resolves the latest version (slower startup, can stall on the registry). Restart your AI client to apply.',
);
process.exit(failed.length > 0 ? 1 : 0);
}

if (arg === 'upgrade' || arg === '--upgrade') {
const { checkForUpdate } = await import('@kunobi/mcp-installer');

const info = await checkForUpdate('@kunobi/mcp', version);
if (!info) {
console.error(
'Could not check for updates (offline or npm registry unreachable). Try again later.',
);
process.exit(1);
}
if (!info.updateAvailable) {
console.log(`Already on the latest version (${version}).`);
process.exit(0);
}

const { failed } = await repinAll(`${PKG}@${info.latest}`, 'Re-pinned');

// Best-effort: warm the npm cache so the next client spawn is instant. Bounded
// and non-fatal — if it fails, the next `npx` run simply fetches on demand.
try {
const { spawnSync } = await import('node:child_process');
spawnSync('npm', ['cache', 'add', `${PKG}@${info.latest}`], {
stdio: 'ignore',
timeout: 60_000,
});
} catch {
// ignore — cache warming is an optimization, not a requirement
}

console.log(
`\nUpgraded ${version} → ${info.latest}. Restart your AI client to load the new version.`,
);
process.exit(failed.length > 0 ? 1 : 0);
}

const connectionConfig = getConnectionConfig();

const server = new McpServer(
Expand Down Expand Up @@ -354,18 +445,49 @@ async function shutdown() {
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);

// Non-blocking version check
import('@kunobi/mcp-installer')
.then(({ checkForUpdate }) => checkForUpdate('@kunobi/mcp', version))
.then((update) => {
// Non-blocking, best-effort startup advisories: notify if a newer version is
// available, and (unless silenced) hint that an unpinned setup is slower and can
// hang on the registry. Both run after the session is connected.
function notify(level: 'info' | 'warning', data: string): void {
server.server
.sendLoggingMessage({ level, logger: 'kunobi-mcp', data })
.catch(() => {});
}

void (async () => {
try {
const { checkForUpdate, list } = await import('@kunobi/mcp-installer');

const update = await checkForUpdate('@kunobi/mcp', version);
if (update?.updateAvailable) {
server.server
.sendLoggingMessage({
level: 'warning',
logger: 'kunobi-mcp',
data: `A newer version of @kunobi/mcp is available (${update.current} → ${update.latest}). Restart the MCP server to pick it up.`,
})
.catch(() => {});
notify(
'warning',
`A newer version of @kunobi/mcp is available (${update.current} → ${update.latest}). Run 'npx -y @kunobi/mcp upgrade' (or 'kunobi-mcp upgrade') to re-pin your AI clients, then restart to apply.`,
);
}

// Unpinned = a bare `@kunobi/mcp` arg (no `@version`). Suppressible for users
// who deliberately run always-latest.
if (process.env.MCP_KUNOBI_NO_PIN_HINT !== '1') {
const unpinned = list().filter((c) =>
c.servers.some(
(s) =>
s.name === 'kunobi' &&
Array.isArray(s.args) &&
s.args.includes('@kunobi/mcp'),
),
);
if (unpinned.length > 0) {
const where = unpinned
.map((c) => `${c.client} (${c.scope})`)
.join(', ');
notify(
'warning',
`Kunobi is running unpinned in: ${where}. Pinning speeds up startup and avoids registry hangs — run 'kunobi-mcp pin'. (Set MCP_KUNOBI_NO_PIN_HINT=1 to hide this, or 'kunobi-mcp unpin' to stay on latest deliberately.)`,
);
}
}
})
.catch(() => {});
} catch {
// advisories are best-effort — never block or crash startup
}
})();
Loading