tachy
A self-hosted knowledge engine that enables Claude Code to fetch, analyze, and store structured lessons learned from support tickets/issues via MCP tools, leveraging hybrid search for relevant prior cases.
README
tachý
A self-hosted, source-agnostic knowledge engine for engineering work items. It ingests support tickets / issues from pluggable sources (Freshdesk, GitHub... with more to come), lets an LLM agent turn them into structured, queryable "lessons learned", and retrieves relevant prior cases when a new item comes in.
The LLM agent is the reasoning layer. This service only persists and retrieves; it never calls an LLM itself. Each item is read and structured once, then reused.
The server speaks plain MCP, so any MCP-compatible client works (Codex CLI, other agents). This README assumes Claude Code as the client; nothing here is Claude-specific.
Architecture
PowerShell -> Claude Code --(MCP stdio)--> tachy MCP server -> core -> Postgres
\-> sources/* -> Freshdesk / GitHub / ...
teammates / cron / CI --(HTTP)--> Hono REST API -> core ----^
packages/core: DB, theWorkItemSourceinterface, services (source-agnostic)packages/sources/freshdesk: Freshdesk adapterpackages/sources/github: GitHub Issues adapterpackages/mcp: MCP server for Claude Code or any MCP client (the primary surface)packages/api: Hono REST API (cron, teammates, future UI)packages/cli:synccommanddb/schema.sql: canonical Postgres schema
Prerequisites
- Node 20+
- PostgreSQL 14+ with the
vector(pgvector),pg_trgm, andpgcryptoextensions available;schema.sqlcreates them. The easiest way to get pgvector locally is thepgvector/pgvectorDocker image, orapt install postgresql-16-pgvector.
Setup
npm install
# Create the database and apply the schema:
createdb tachy
psql "postgres://localhost:5432/tachy" -f db/schema.sql
cp .env.example .env # then fill in DATABASE_URL and your Freshdesk token
schema.sql seeds the teams/products you use (Track & Trace -> tpd, ftrace;
BPT -> csdr, eudr, pcf, medical-devices), the osapiens-freshdesk source, and
an example group mapping (Freshdesk group 48000641379 -> tpd). Adjust the
seed block for your other groups.
Freshdesk numeric
status(e.g. 6) is account-specific and stored raw.
Use it from Claude Code (PowerShell)
.mcp.json already registers the server (Claude Code's own config format;
other MCP clients register it differently). From the project folder:
claude
Then, in the session:
analyze ticket 58925 from osapiens-freshdesk
Claude will call fetch_work_item, clean + summarize, show you the summary,
and only call save_knowledge_entry after you approve. To consult:
what do we know that's relevant to ticket 61010?
Claude calls get_context (fetch + archive search) and answers. Optionally:
post that analysis as a private note on 61010
MCP tools
Core loop: fetch_work_item, search_knowledge, get_context,
save_knowledge_entry, post_private_note, add_knowledge_feedback,
record_analysis_run.
search_knowledge and get_context are hybrid: keyword (FTS + trigram) blended
with semantic similarity over a local embedding, so paraphrases surface even
with no shared keywords. save_knowledge_entry stamps created_by from
TACHY_USER_EMAIL, embeds the entry on save, and is customer-blind:
identity never enters the searchable text or the embedding (see "Customers
and versions" below).
Curated vocabulary, so Claude never invents categories from a ticket alone:
list_resolution_patterns / add_resolution_pattern,
list_components / add_component, list_customers / add_customer.
Correction: set_work_item_customer, set_observed_version.
REST API (optional)
npm run api # http://localhost:8787
GET /health, POST /work-items/:source/:id/fetch, GET /knowledge/search?q=,
POST /knowledge, GET /knowledge/:id/feedback, POST /knowledge/:id/feedback,
POST /analysis-runs, POST /work-items/:source/:id/notes,
PATCH /work-items/:id/customer, PATCH /work-items/:id/observed-version,
GET|POST /resolution-patterns, GET|POST /products/:slug/components,
GET|POST /customers.
Set TACHY_API_TOKEN to require a bearer token on every route except /health
(Authorization: Bearer <token>). If it is unset, the server binds to
127.0.0.1 only and warns. The MCP server (stdio) is unaffected.
Incremental sync (optional)
npm run sync sync osapiens-freshdesk --since=2026-06-01T00:00:00Z --group=48000641379
Stores/refreshes raw work items only. It never creates knowledge entries (those always require your approval). Schedule it with Windows Task Scheduler.
Sources
external_id |
Routing | Write-back | Token env var | |
|---|---|---|---|---|
| Freshdesk | ticket number | group_id maps to a product |
private notes | FRESHDESK_TOKEN_<SLUG>, falling back to FRESHDESK_TOKEN |
| GitHub (issues) | owner/repo#123 |
owner/repo maps to a product, via config.repos or --group; PRs are skipped |
not supported (GitHub comments are public, so post_private_note is refused) |
GITHUB_TOKEN_<SLUG>, falling back to GITHUB_TOKEN |
Adding another source
Implement WorkItemSource (see packages/core/src/source.ts), register it in
the entrypoints, and add a source_connections row. No schema change.
Customers, versions, and controlled vocabulary
knowledge_entries is customer-blind by design. Identity never enters search
or the embedding, so retrieval matches on the fault, not on who reported it.
Customer and version are properties of the ticket, not the lesson:
work_items.customer_idis auto-matched at ingest by the requester's email domain againstcustomers.aliases(handles distributors/resellers fronting for the same account, e.g. an alias list ofdavidoff.com+arvato.comon onecustomersrow). Wrong or missing matches are corrected withset_work_item_customer; corrections are never overwritten by a later re-sync.customersstarts empty: add real ones withadd_customer.work_items.observed_versionis set manually withset_observed_versionwhen a ticket states a version. It's never inferred.knowledge_entries.resolution_patternis a controlled vocabulary, not free text: it's a foreign key intoresolution_patterns, which starts empty. Claude must calllist_resolution_patternsand pick an existing slug (or leave it unset);add_resolution_patternis a separate, deliberate action, not something invented per-ticket. This is what makes cross-team pattern queries group correctly.knowledge_entries.signals(error codes, config filenames, component names) are promoted into a real, indexed field instead of being buried instructured, so they're searchable.- Components (
list_components/add_component) are a hierarchical, per-product architecture glossary (e.g.business-objectwith nested poolsconfiguration,id-issuer, ...), fed conversationally with no ticket required. Two valid ways to populate it: you describe the app directly (call immediately), or a ticket mentions something unrecognized (Claude proposes, you confirm, then it's added). Never silently invented from a ticket.
fetch_work_item / get_context return the resolved customer_id,
customer_name, and observed_version alongside the ticket, so Claude can
reason about staleness/relevance narratively ("this customer is on v1.4, the
matched lesson was fixed in v1.6...") without any of it touching the search
index.
Semantic search
Embeddings are produced by a local model (all-MiniLM-L6-v2, 384-dim, via
fastembed). Nothing is sent to an external API. The model (~90MB) is
downloaded on first use into .fastembed-cache/. Fresh databases get the
vector column from schema.sql; to upgrade an existing database run
db/0002_pgvector.sql (and db/0003_customer_signals_patterns_components.sql
if upgrading from before customers/signals/resolution_patterns/components)
then backfill embeddings for prior entries:
npm run sync embed-backfill
Backups
npm run sync backup # pg_dump -Fc into ./backups/
npm run sync restore --file=backups/tachy-….dump # pg_restore (overwrites!)
Both need the PostgreSQL client tools (pg_dump / pg_restore) on PATH.
backups/ is git-ignored. Dumps contain real ticket data, so keep them off
any shared/synced folder. Schedule backup with Windows Task Scheduler.
Privacy
Never commit real ticket data, tokens, customer names, or internal URLs.
.env is git-ignored; tokens are resolved from env by source slug, never stored
in the DB.
License
AGPL-3.0-or-later. If you run a modified version of tachy as a network
service, you must make the modified source available to its users (see
LICENSE).
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.