Command Palette

Search for a command to run...

UnylyUnyly
Browse all

Kajet Turbo

FreeNot checked

Serena inspired MCP server for your obsidian-style workspaces

GitHubEmbed

About

Serena inspired MCP server for your obsidian-style workspaces

README

[!WARNING] Highly unstable and unsafe. This is a solo hobby project driven by a love of refactoring and design more than of shipping — the git history is full of bugs, some still lurking. No security audit, no stability guarantees. Do not point it at data you can't afford to lose or expose.

[!NOTE] That said, for personal use it's been reasonably solid operating on workspaces with 1k+ files.

MCP-first RAG for markdown notes — somewhere between Serena and Obsidian: from the MCP side you create a workspace (an Obsidian-style vault), then add and edit files on it. The MCP endpoint is OAuth-capable, so it can be exposed and connected to from an MCP client (Claude among others).

The initial prototype was kajet, a Rust-based predecessor to this project. The "turbo" rewrite happened for the same kind of reason the original did: more dog walks, wanting to look up notes from a phone on the go instead of a native macOS binary, and being tired of a 50GB target/ cache just to rebuild the thing.

Notes live as markdown files in a per-user git repo (workspace). The backend chunks them, computes embeddings, and exposes hybrid search (SQLite FTS5 + vectors) through:

  • MCP (/mcp) — tools to search and edit notes, with OAuth, for plugging into any MCP client;
  • REST API (/api) — for the SPA and integrations;
  • SPA (SvelteKit, frontend/) — a human-facing note browser.

Three process roles (KAJET_ROLE) share the same SQLite database and the same on-disk workspaces — see Environment variables below for topology details.

Stack

Backend:

Frontend:

  • SvelteKit 5 + Vite + TypeScript
  • Orval — generates the typed API client from the backend's OpenAPI schema
  • Vitest — unit tests
  • ESLint + Prettier + svelte-check — lint, format, type-check

Local development

Backend (Python 3.14t, uv):

uv sync
MCP_BASE_URL=http://localhost:8000 uv run kajet-turbo

Frontend (SvelteKit + Bun, proxies to localhost:8000):

cd frontend
bun install
bun run dev

Common commands:

uv run pytest                              # backend tests
uv run ruff check --fix . && uv run ruff format .
uv run ty check                            # type-checking
cd frontend && bun run check && bun run lint
bash scripts/generate-api.sh               # regenerate the API client after a backend change

Schema migrations use Alembic, never create_all; details in CLAUDE.md. More on repo structure, concurrency rules (free-threaded Python), and code conventions is also in CLAUDE.md.

Running (production / container)

uv sync
MCP_BASE_URL=http://localhost:8000 kajet-turbo

Environment variables

Required

Variable Description
MCP_BASE_URL Public URL of the server (e.g. https://kajet.example.com). Some PaaS deployment targets set this automatically from their own env vars.

Server

Variable Default Description
MCP_HOST 0.0.0.0 Listen address
MCP_PORT 8000 Listen port
KAJET_ROLE all Process role: all (MCP+API+SPA in one — dev), mcp (/mcp + OAuth only, N workers), api (REST /api + SPA, N workers)
MCP_WORKERS 1 Worker count for roles mcp and all
API_WORKERS 2 Worker count for role api
KAJET_METRICS_SAMPLE_SHARED role default Whether this process owns shared-state sampling (SQLite files, queue snapshots). Default: worker on, api/mcp off, all on only with one process. See Metrics
KAJET_METRICS_PORT 9100 Metrics listener port of role worker (roles api/mcp/all serve /metrics on their normal port)
KAJET_WORKER_STALE_AFTER 300 Seconds since a claimed job's last lease renewal before another worker may reclaim it (role worker/all)
KAJET_GIT_SSH_CONNECT_TIMEOUT 15 Seconds ssh may spend connecting/handshaking with a git push remote before failing (ConnectTimeout)
KAJET_GIT_SSH_KEEPALIVE_INTERVAL 15 Seconds between ssh keepalive probes on an established git push connection (ServerAliveInterval)
KAJET_GIT_SSH_KEEPALIVE_COUNT_MAX 3 Missed keepalive probes tolerated before ssh gives up on a stalled git push (ServerAliveCountMax)

Metrics

Every role exposes Prometheus metrics, and none of them through the ingress:

Role Storage Endpoint
api, mcp multiprocess: one directory per container run (PROMETHEUS_MULTIPROC_DIR, default /tmp/kajet-prometheus, cleared once by the supervisor at start) GET /metrics on the role's port (kajet-api:8000, kajet-mcp:8000)
worker in-process registry own listener on KAJET_METRICS_PORT (kajet-worker:9100), started and stopped with the process
all in-process registry GET /metrics on the normal port

Scrape these targets directly on the compose network. /metrics is a root route of each app — never under /mcp or /api, which Caddy proxies — so it matches no ingress allowlist and is not reachable from outside. Do not publish port 9100 either.

Shared state (SQLite file sizes, WAL, queue snapshots) has exactly one owner per deployment, selected by KAJET_METRICS_SAMPLE_SHARED and set explicitly in docker-compose.yml: 1 on kajet-worker, 0 on kajet-api and kajet-mcp. If you scale kajet-worker past one replica, exactly one replica carries the flag; the others set it to 0, otherwise every replica reports the same database and the series duplicate. Worker replicas are separate scrape targets with their own instance label — ordinary Prometheus, not multiprocess mode.

Sampler health is exported per collector (kajet_sampler_success, kajet_sampler_last_success_timestamp_seconds, kajet_sampler_duration_seconds, kajet_sampler_errors_total, kajet_sampler_age_seconds). A reachable /metrics alone does not prove sampling is healthy: alert on kajet_sampler_age_seconds growing, and note that when a sample fails the last good snapshot stays visible rather than dropping to zero. Metric families are catalogued in docs/specs/metrics.md.

Production topology (docker-compose.yml): ingress (Caddy) + kajet-api (stateless, N workers) + kajet-mcp (stateless, N workers via MCP_WORKERS). Both roles share the /data (SQLite) and /workspaces (git) volumes on the same host. The host proxy only routes Host → ingress:8000; path splitting is done by the Caddyfile.

Development images

CI builds the final Dockerfile targets for linux/amd64 (linux/arm64 is disabled for now — not deployed anywhere yet):

  • ghcr.io/jpalczewski/kajet-turbo-app
  • ghcr.io/jpalczewski/kajet-turbo-ingress

Pull requests only build, validate, and scan the images. A push to main publishes images that passed the Trivy scan under the immutable sha-<commit> tag. After successfully building both targets, CI also moves the convenient, moving develop tag.

[!IMPORTANT] On first publish, set both packages to Public in their GitHub settings. The workflow checks anonymous pull access for both images before moving develop, so a private package will block the promotion; after fixing visibility you can safely re-run the same run.

# Latest known-good development build
docker pull ghcr.io/jpalczewski/kajet-turbo-app:develop
docker pull ghcr.io/jpalczewski/kajet-turbo-ingress:develop

# Reproducible pair of images from one commit
docker pull ghcr.io/jpalczewski/kajet-turbo-app:sha-<commit>
docker pull ghcr.io/jpalczewski/kajet-turbo-ingress:sha-<commit>

[!NOTE] For deployments or debugging, use the same sha-<commit> tag for both images. GHCR does not update two packages transactionally, so develop is purely a shortcut for day-to-day work.

Every platform variant has a CycloneDX SBOM and provenance signed by GitHub Actions. The index attestation can be verified like this:

gh attestation verify \
  oci://ghcr.io/jpalczewski/kajet-turbo-app:sha-<commit> \
  -R jpalczewski/kajet-turbo

The SBOM is pinned to a specific platform manifest (amd64):

gh attestation verify \
  oci://ghcr.io/jpalczewski/kajet-turbo-app:sha-<commit>-amd64 \
  -R jpalczewski/kajet-turbo \
  --predicate-type https://cyclonedx.org/bom

Once a week, CI re-scans the published develop images to catch vulnerabilities disclosed after publication. CI images are development artifacts: the workflow does not trigger a deployment. The production deployment builds both targets from the Dockerfile itself, independent of these CI-published images.

[!WARNING] Narrow exceptions to the Trivy gate live in .trivyignore.yaml. Each one is scoped to a specific package or path and has an expiry date; an exception without those constraints should not be added.

Data

Variable Default Description
DB_PATH /data/kajet.db Path to the SQLite database
WORKSPACES_DIR /workspaces Root directory for workspaces

Vector embeddings are not configured via env vars. Each user sets their own embedding profile — an OpenAI-compatible endpoint (base_url + model

  • dimension, optional API key, stored encrypted) — through the API/SPA. Vector tables are sharded per dimension and created on demand, so nothing needs to be pre-declared. Without an active profile, search still works — it just falls back to FTS5 full-text only.

Initialization

Variable Default Description
KAJET_ADMIN_EMAIL Admin account email (created on first start)
KAJET_ADMIN_PASSWORD Admin account password

Logging

Variable Default Description
LOG_LEVEL INFO Log level (DEBUG, INFO, WARNING, ERROR)
LOG_SQL Set to any value to log SQL queries (LOG_SQL=1)

Logs are emitted to stderr as JSONL. Examples:

# production — INFO only, no SQL
kajet-turbo

# debug — full application logs
LOG_LEVEL=DEBUG kajet-turbo

# trace SQL queries
LOG_SQL=1 kajet-turbo

# full debug with SQL
LOG_LEVEL=DEBUG LOG_SQL=1 kajet-turbo 2> debug.jsonl

from github.com/jpalczewski/kajet-turbo

Install Kajet Turbo in Claude Desktop, Claude Code & Cursor

Recommended · one command, every IDE
unyly install kajet-turbo

Installs into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.

First time? Get the CLI: curl -fsSL https://unyly.org/install | sh

Or configure manually

Run in your terminal:

claude mcp add kajet-turbo -- uvx --from git+https://github.com/jpalczewski/kajet-turbo kajet-turbo

Step-by-step: how to install Kajet Turbo

FAQ

Is Kajet Turbo MCP free?

Yes, Kajet Turbo MCP is free — one-click install via Unyly at no cost.

Does Kajet Turbo need an API key?

No, Kajet Turbo runs without API keys or environment variables.

Is Kajet Turbo hosted or self-hosted?

Self-hosted: the server runs locally on your machine via the install command above.

How do I install Kajet Turbo in Claude Desktop, Claude Code or Cursor?

Open Kajet Turbo on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.

Related MCPs

Compare Kajet Turbo with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All development MCPs