@100xprompt/chitta 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/assets/rules/claude-md.md +9 -0
  4. package/assets/skill/SKILL.md +47 -0
  5. package/package.json +48 -0
  6. package/src/README.md +124 -0
  7. package/src/arango-client.ts +67 -0
  8. package/src/arango-graph-provider.ts +364 -0
  9. package/src/bin.ts +27 -0
  10. package/src/config-env.ts +53 -0
  11. package/src/embedded/authorizer.ts +89 -0
  12. package/src/embedded/cli.ts +86 -0
  13. package/src/embedded/code-extractor.ts +9 -0
  14. package/src/embedded/demo.ts +36 -0
  15. package/src/embedded/extract.ts +12 -0
  16. package/src/embedded/extractors/code.ts +308 -0
  17. package/src/embedded/extractors/deterministic.ts +63 -0
  18. package/src/embedded/extractors/llm.ts +151 -0
  19. package/src/embedded/extractors/text-hygiene.ts +54 -0
  20. package/src/embedded/extractors/types.ts +34 -0
  21. package/src/embedded/graph/acl-paths.ts +96 -0
  22. package/src/embedded/graph/adjacency.ts +61 -0
  23. package/src/embedded/graph/centrality.ts +23 -0
  24. package/src/embedded/graph/communities.ts +46 -0
  25. package/src/embedded/graph/cypher.ts +17 -0
  26. package/src/embedded/graph/impact.ts +24 -0
  27. package/src/embedded/graph/knowledge-graph.ts +108 -0
  28. package/src/embedded/graph/pagerank.ts +57 -0
  29. package/src/embedded/graph/sql-access.ts +13 -0
  30. package/src/embedded/graph/traversal.ts +73 -0
  31. package/src/embedded/graph/types.ts +35 -0
  32. package/src/embedded/graph-query.ts +126 -0
  33. package/src/embedded/index.ts +171 -0
  34. package/src/embedded/ingest.ts +262 -0
  35. package/src/embedded/kgqa/answer-paths.ts +197 -0
  36. package/src/embedded/kgqa/entity-link.ts +13 -0
  37. package/src/embedded/kgqa/intent.ts +14 -0
  38. package/src/embedded/kgqa/predicates.ts +9 -0
  39. package/src/embedded/kgqa/preference.ts +20 -0
  40. package/src/embedded/kgqa/select.ts +99 -0
  41. package/src/embedded/kgqa/text.ts +16 -0
  42. package/src/embedded/kgqa/types.ts +6 -0
  43. package/src/embedded/kgqa-service.ts +122 -0
  44. package/src/embedded/llm-extractor.ts +10 -0
  45. package/src/embedded/local-embeddings.ts +36 -0
  46. package/src/embedded/personal.ts +100 -0
  47. package/src/embedded/reranker.ts +62 -0
  48. package/src/embedded/retrieval/decay-stage.ts +59 -0
  49. package/src/embedded/retrieval/diversity.ts +37 -0
  50. package/src/embedded/retrieval/fuse.ts +52 -0
  51. package/src/embedded/retrieval/graph-stage.ts +45 -0
  52. package/src/embedded/retrieval/hybrid-retriever.ts +80 -0
  53. package/src/embedded/retrieval/keyword-stage.ts +27 -0
  54. package/src/embedded/retrieval/passage.ts +44 -0
  55. package/src/embedded/retrieval/rerank-stage.ts +31 -0
  56. package/src/embedded/retrieval/trace.ts +31 -0
  57. package/src/embedded/retrieval/vector-stage.ts +15 -0
  58. package/src/embedded/sqlite-graph-provider.ts +119 -0
  59. package/src/embedded/sqlite-store.ts +95 -0
  60. package/src/embedded/sqlite-vec-service.ts +122 -0
  61. package/src/embedded/store/chunks.ts +61 -0
  62. package/src/embedded/store/fts.ts +50 -0
  63. package/src/embedded/store/nodes-edges.ts +112 -0
  64. package/src/embedded/store/salience.ts +37 -0
  65. package/src/embedded/store/schema.ts +109 -0
  66. package/src/embedded/transformers-embeddings.ts +100 -0
  67. package/src/embeddings.ts +51 -0
  68. package/src/eval/goldset.ts +46 -0
  69. package/src/eval/harness.ts +65 -0
  70. package/src/eval/metrics.ts +38 -0
  71. package/src/http/server.ts +93 -0
  72. package/src/index.ts +44 -0
  73. package/src/install/index.ts +139 -0
  74. package/src/install/platforms.ts +126 -0
  75. package/src/install/skill.ts +46 -0
  76. package/src/install/writers.ts +82 -0
  77. package/src/mcp/backend.ts +129 -0
  78. package/src/mcp/server.ts +83 -0
  79. package/src/mcp/tools/context-about.ts +69 -0
  80. package/src/mcp/tools/context-graph.ts +23 -0
  81. package/src/mcp/tools/context-ingest.ts +88 -0
  82. package/src/mcp/tools/context-rebuild.ts +22 -0
  83. package/src/mcp/tools/context-relate.ts +88 -0
  84. package/src/mcp/tools/get-context.ts +52 -0
  85. package/src/mcp/tools/index.ts +40 -0
  86. package/src/mcp/tools/types.ts +33 -0
  87. package/src/permission.ts +72 -0
  88. package/src/provider.ts +65 -0
  89. package/src/qdrant-vector.ts +76 -0
  90. package/src/retrieval.ts +218 -0
  91. package/src/service.ts +40 -0
  92. package/src/types.ts +91 -0
@@ -0,0 +1,126 @@
1
+ // The platform registry: one row per AI tool, with its EXACT (verified 2025-2026) MCP
2
+ // config location + format and Skill directory. Paths are resolved per-OS at call time.
3
+ // Adding a tool = adding a row here; the writers in writers.ts handle each `format`.
4
+ import { homedir } from "node:os"
5
+ import { join } from "node:path"
6
+
7
+ const HOME = homedir()
8
+ const PLAT = process.platform // "darwin" | "linux" | "win32"
9
+
10
+ // App-data root per OS (where Electron/VS Code apps store user config).
11
+ const appData = (): string =>
12
+ PLAT === "darwin" ? join(HOME, "Library", "Application Support")
13
+ : PLAT === "win32" ? (process.env.APPDATA ?? join(HOME, "AppData", "Roaming"))
14
+ : join(HOME, ".config")
15
+
16
+ // VS Code "User" dir (settings.json / mcp.json / globalStorage live here).
17
+ const vscodeUser = (): string => join(appData(), "Code", "User")
18
+
19
+ /** How the MCP server entry is shaped + where it nests. */
20
+ export type Format =
21
+ | "json" // JSON object: container[key][name] = entry
22
+ | "json-array" // JSON: container[key] is an ARRAY of entries (Trae)
23
+ | "toml" // Codex TOML
24
+ | "manual" // no stable on-disk path → print instructions
25
+
26
+ /** The per-tool server-entry dialect. */
27
+ export type Entry = "standard" | "vscode" | "zed" | "local" | "trae"
28
+
29
+ export interface Platform {
30
+ id: string
31
+ label: string
32
+ format: Format
33
+ /** top-level container key in the config (e.g. mcpServers / servers / context_servers / mcp / amp.mcpServers) */
34
+ key?: string
35
+ entry?: Entry
36
+ /** absolute global/user config path (null if tool has no global file) */
37
+ global: string | null
38
+ /** project-relative config path (undefined if not supported) */
39
+ project?: string
40
+ /** skill dirs (we append `/chitta/SKILL.md`); undefined if tool has no skills */
41
+ skillGlobal?: string
42
+ skillProject?: string
43
+ /** optional note shown to the user */
44
+ note?: string
45
+ }
46
+
47
+ export const PLATFORMS: Platform[] = [
48
+ {
49
+ id: "claude-code", label: "Claude Code", format: "json", key: "mcpServers", entry: "standard",
50
+ global: join(HOME, ".claude.json"), project: ".mcp.json",
51
+ skillGlobal: join(HOME, ".claude", "skills"), skillProject: ".claude/skills",
52
+ note: "global edits ~/.claude.json; `claude mcp add` is the official alternative.",
53
+ },
54
+ {
55
+ id: "claude-desktop", label: "Claude Desktop", format: "json", key: "mcpServers", entry: "standard",
56
+ global: join(appData(), "Claude", "claude_desktop_config.json"),
57
+ note: "restart Claude Desktop after install (config is read once at startup).",
58
+ },
59
+ {
60
+ id: "cursor", label: "Cursor", format: "json", key: "mcpServers", entry: "standard",
61
+ global: join(HOME, ".cursor", "mcp.json"), project: ".cursor/mcp.json",
62
+ skillGlobal: join(HOME, ".cursor", "skills"), skillProject: ".cursor/skills",
63
+ },
64
+ {
65
+ id: "vscode", label: "VS Code (Copilot)", format: "json", key: "servers", entry: "vscode",
66
+ global: join(vscodeUser(), "mcp.json"), project: ".vscode/mcp.json",
67
+ },
68
+ {
69
+ id: "windsurf", label: "Windsurf", format: "json", key: "mcpServers", entry: "standard",
70
+ global: join(HOME, ".codeium", "windsurf", "mcp_config.json"),
71
+ },
72
+ {
73
+ id: "zed", label: "Zed", format: "json", key: "context_servers", entry: "zed",
74
+ global: join(HOME, ".config", "zed", "settings.json"), project: ".zed/settings.json",
75
+ },
76
+ {
77
+ id: "cline", label: "Cline", format: "json", key: "mcpServers", entry: "standard",
78
+ global: join(vscodeUser(), "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
79
+ },
80
+ {
81
+ id: "roo", label: "Roo Code", format: "json", key: "mcpServers", entry: "standard",
82
+ global: join(vscodeUser(), "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json"),
83
+ project: ".roo/mcp.json",
84
+ },
85
+ {
86
+ id: "codex", label: "Codex CLI", format: "toml",
87
+ global: join(HOME, ".codex", "config.toml"), project: ".codex/config.toml",
88
+ },
89
+ {
90
+ id: "gemini", label: "Gemini CLI", format: "json", key: "mcpServers", entry: "standard",
91
+ global: join(HOME, ".gemini", "settings.json"), project: ".gemini/settings.json",
92
+ skillGlobal: join(HOME, ".gemini", "skills"), skillProject: ".gemini/skills",
93
+ },
94
+ {
95
+ id: "opencode", label: "opencode", format: "json", key: "mcp", entry: "local",
96
+ global: join(HOME, ".config", "opencode", "opencode.json"), project: "opencode.json",
97
+ skillGlobal: join(HOME, ".config", "opencode", "skills"), skillProject: ".opencode/skills",
98
+ },
99
+ {
100
+ id: "kiro", label: "Kiro", format: "json", key: "mcpServers", entry: "standard",
101
+ global: join(HOME, ".kiro", "settings", "mcp.json"), project: ".kiro/settings/mcp.json",
102
+ skillGlobal: join(HOME, ".kiro", "skills"), skillProject: ".kiro/skills",
103
+ },
104
+ {
105
+ id: "amp", label: "Amp", format: "json", key: "amp.mcpServers", entry: "standard",
106
+ global: join(appData(), "amp", "settings.json"), project: ".amp/settings.json",
107
+ skillGlobal: join(HOME, ".config", "agents", "skills"), skillProject: ".agents/skills",
108
+ },
109
+ {
110
+ id: "factory", label: "Factory Droid", format: "json", key: "mcpServers", entry: "standard",
111
+ global: join(HOME, ".factory", "mcp.json"), project: ".factory/mcp.json",
112
+ skillGlobal: join(HOME, ".factory", "skills"), skillProject: ".factory/skills",
113
+ },
114
+ {
115
+ id: "kilo", label: "Kilo Code", format: "json", key: "mcp", entry: "local",
116
+ global: join(HOME, ".config", "kilo", "kilo.json"), project: ".kilo/kilo.json",
117
+ skillGlobal: join(HOME, ".kilo", "skills"), skillProject: ".kilo/skills",
118
+ },
119
+ {
120
+ id: "trae", label: "Trae", format: "json-array", key: "mcpServers", entry: "trae",
121
+ global: null, skillProject: ".trae/skills",
122
+ note: "Trae's global MCP file path is undocumented; add via the in-app MCP panel (use --print), Skill installs to .trae/skills.",
123
+ },
124
+ ]
125
+
126
+ export const byId = (id: string): Platform | undefined => PLATFORMS.find((p) => p.id === id)
@@ -0,0 +1,46 @@
1
+ // Skill installer — copies the bundled SKILL.md into a tool's skills directory as
2
+ // <skillsDir>/chitta/SKILL.md. Tools that support Claude-style skills get guidance on
3
+ // WHEN to use Chitta's MCP tools (recall before answering, ingest durable facts, etc.).
4
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs"
5
+ import { join, dirname } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const HERE = dirname(fileURLToPath(import.meta.url))
9
+ // assets/skill/SKILL.md lives at repo root (../../assets from src/install). When compiled,
10
+ // the build embeds it next to the binary; fall back to an inline copy if not found.
11
+ const SKILL_SRC_CANDIDATES = [
12
+ join(HERE, "..", "..", "assets", "skill", "SKILL.md"),
13
+ join(HERE, "assets", "skill", "SKILL.md"),
14
+ ]
15
+
16
+ const INLINE_FALLBACK = `---
17
+ name: chitta
18
+ description: Permission-aware long-term memory for AI agents. Use to recall prior context before answering, store durable facts/decisions, and query how concepts relate. Backed by Chitta's MCP tools (context_ingest, get_context, context_graph).
19
+ ---
20
+
21
+ # Chitta — memory for this agent
22
+
23
+ Chitta gives you persistent, permission-aware memory via MCP tools. Use it proactively.
24
+
25
+ - **Before answering** anything that may depend on prior work, call **get_context** with the
26
+ user's question to retrieve ranked, cited, permission-filtered snippets.
27
+ - **After learning** a durable fact, decision, or preference, call **context_ingest** to store it.
28
+ - To understand **how things relate**, call **context_graph**.
29
+
30
+ If MCP tools are unavailable, use the CLI: \`bunx @100xprompt/chitta query "<q>"\` and
31
+ \`bunx @100xprompt/chitta ingest --text "<fact>"\`.
32
+ `
33
+
34
+ export function skillContent(): string {
35
+ for (const p of SKILL_SRC_CANDIDATES) if (existsSync(p)) return readFileSync(p, "utf8")
36
+ return INLINE_FALLBACK
37
+ }
38
+
39
+ /** Write chitta/SKILL.md under the given skills directory. Returns the file path. */
40
+ export function installSkill(skillsDir: string): string {
41
+ const dir = join(skillsDir, "chitta")
42
+ mkdirSync(dir, { recursive: true })
43
+ const dst = join(dir, "SKILL.md")
44
+ writeFileSync(dst, skillContent())
45
+ return dst
46
+ }
@@ -0,0 +1,82 @@
1
+ // Config writers — one per FORMAT, not per tool. Each MERGES into existing config so other
2
+ // MCP servers and unrelated settings are preserved, and re-running is idempotent.
3
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"
4
+ import { dirname } from "node:path"
5
+ import type { Entry } from "./platforms"
6
+
7
+ export const PKG = "@100xprompt/chitta"
8
+ // Chitta runs on the Bun runtime, so it launches via `bunx` (Bun's package runner).
9
+ // Users need Bun once: `curl -fsSL https://bun.sh/install | bash`.
10
+ const RUN = ["bunx", PKG] // [command, ...args]
11
+
12
+ /** Build the server-entry object in the dialect a given tool expects. */
13
+ export function serverEntry(entry: Entry, env: Record<string, string>): unknown {
14
+ const hasEnv = Object.keys(env).length > 0
15
+ const cmd = { command: "bunx", args: [PKG] }
16
+ switch (entry) {
17
+ case "standard":
18
+ return { ...cmd, ...(hasEnv ? { env } : {}) }
19
+ case "vscode":
20
+ return { type: "stdio", ...cmd, ...(hasEnv ? { env } : {}) }
21
+ case "zed":
22
+ return { source: "custom", ...cmd, ...(hasEnv ? { env } : {}) }
23
+ case "local": // opencode / kilo: combined command array + `environment` + enabled
24
+ return { type: "local", command: [...RUN], enabled: true, ...(hasEnv ? { environment: env } : {}) }
25
+ case "trae": // array entry carrying its own name + combined command array
26
+ return { name: "chitta", command: [...RUN], ...(hasEnv ? { env } : {}) }
27
+ }
28
+ }
29
+
30
+ function readJson(path: string): any {
31
+ if (!existsSync(path)) return {}
32
+ const raw = readFileSync(path, "utf8").trim()
33
+ if (!raw) return {}
34
+ try {
35
+ return JSON.parse(raw)
36
+ } catch {
37
+ // tolerate JSONC line comments (VS Code / opencode allow them)
38
+ return JSON.parse(raw.replace(/^\s*\/\/.*$/gm, ""))
39
+ }
40
+ }
41
+
42
+ /** Merge `chitta` into a JSON config under `key`. Object form (most tools) or array form (Trae). */
43
+ export function writeJsonConfig(
44
+ path: string,
45
+ key: string,
46
+ entry: unknown,
47
+ array: boolean,
48
+ ): void {
49
+ mkdirSync(dirname(path), { recursive: true })
50
+ const cfg = readJson(path)
51
+ if (array) {
52
+ const list = Array.isArray(cfg[key]) ? cfg[key] : []
53
+ const filtered = list.filter((e: any) => e?.name !== "chitta")
54
+ filtered.push(entry)
55
+ cfg[key] = filtered
56
+ } else {
57
+ if (typeof cfg[key] !== "object" || cfg[key] === null || Array.isArray(cfg[key])) cfg[key] = {}
58
+ cfg[key]["chitta"] = entry
59
+ }
60
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n")
61
+ }
62
+
63
+ /** Codex TOML: replace-or-append the [mcp_servers.chitta] block (+ optional env table). */
64
+ export function writeCodexToml(path: string, env: Record<string, string>): void {
65
+ mkdirSync(dirname(path), { recursive: true })
66
+ let text = existsSync(path) ? readFileSync(path, "utf8") : ""
67
+ // strip any existing chitta block (the table + its sub-tables up to the next top-level [table])
68
+ text = text.replace(/\n*\[mcp_servers\.chitta\][\s\S]*?(?=\n\[[^.\]]|\n\[mcp_servers\.(?!chitta)|\s*$)/g, "\n")
69
+ text = text.replace(/\n{3,}/g, "\n\n").trimEnd()
70
+ let block = `\n\n[mcp_servers.chitta]\ncommand = "bunx"\nargs = ["${PKG}"]\n`
71
+ const keys = Object.keys(env)
72
+ if (keys.length) {
73
+ block += `\n[mcp_servers.chitta.env]\n`
74
+ for (const k of keys) block += `${k} = ${JSON.stringify(env[k])}\n`
75
+ }
76
+ writeFileSync(path, (text + block).trimStart() + "\n")
77
+ }
78
+
79
+ /** The generic snippet for --print / unsupported clients. */
80
+ export function printSnippet(env: Record<string, string>): string {
81
+ return JSON.stringify({ mcpServers: { chitta: serverEntry("standard", env) } }, null, 2)
82
+ }
@@ -0,0 +1,129 @@
1
+ // Resolves which backend the MCP server talks to, from env:
2
+ // • Central office: if CONTEXT_ARANGO_URL/QDRANT_URL/EMBED_URL/COLLECTION are set,
3
+ // query the shared backend with this user's identity → org-wide graph + per-user ACL.
4
+ // • Local embedded (default): a single SQLite file - ingest + query, no servers.
5
+ // Identity (who is asking) drives ACL and comes from CONTEXT_USER_ID/CONTEXT_ORG_ID.
6
+
7
+ import { personalContext, personalContextPath } from "../embedded/personal"
8
+ import { buildContextService } from "../service"
9
+ import { loadContextConfigFromEnv } from "../config-env"
10
+ import type { IngestDoc } from "../embedded/ingest"
11
+ import type { RetrievalResponse } from "../types"
12
+
13
+ export interface KnowledgeGraph {
14
+ entities: Array<{ id: string; label: string; type: string }>
15
+ relations: Array<{ from: string; to: string }>
16
+ }
17
+
18
+ export interface BackendStats {
19
+ records: number
20
+ chunks: number
21
+ entities: number
22
+ relations: number
23
+ }
24
+
25
+ export interface ExactAnswer {
26
+ answer: string
27
+ facts: string[]
28
+ triple: { subject: string; predicate: string; object: string }
29
+ citations: string[]
30
+ confidence: number
31
+ }
32
+
33
+ export interface ContextBackend {
34
+ mode: "central" | "local"
35
+ userId: string
36
+ orgId: string
37
+ /** Where the data lives (db path for local, backend url for central). */
38
+ storage: string
39
+ /** Active vector search engine. */
40
+ vectorIndex: string
41
+ /** Configured embedding mode (auto/transformers/hash, or central). */
42
+ embeddings: string
43
+ /** Knowledge extraction mode - confirms whether the LLM is wired. */
44
+ extraction: string
45
+ query(q: string): Promise<RetrievalResponse>
46
+ /** KGQA: exact answer from the typed graph, or null to fall back to ranked. */
47
+ ask?: (q: string) => Promise<ExactAnswer | null>
48
+ ingest?: (doc: IngestDoc) => Promise<{ recordId: string; chunks: number; entities: number }>
49
+ /** The accessible knowledge graph (entities + relations). Local mode only. */
50
+ graph?: () => Promise<KnowledgeGraph>
51
+ /** Re-extract the knowledge graph over all records with the current extractor
52
+ * (typed triples when an LLM is configured). Local mode only. */
53
+ rebuild?: () => Promise<{ records: number; entities: number }>
54
+ /** Graph-query surface (ACL-filtered traversal over the entity graph). Local only. */
55
+ graphQuery?: {
56
+ neighbors: (name: string, relation?: string) => Promise<unknown>
57
+ path: (a: string, b: string) => Promise<unknown>
58
+ impact: (name: string) => Promise<unknown>
59
+ central: (limit?: number) => Promise<unknown>
60
+ communities: () => Promise<unknown>
61
+ cypher: () => Promise<string>
62
+ walk: (seeds: string[]) => Promise<unknown>
63
+ }
64
+ /** Live counts for the about/discovery endpoint. */
65
+ stats?: () => Promise<BackendStats>
66
+ }
67
+
68
+ export function resolveBackend(): ContextBackend {
69
+ const central = loadContextConfigFromEnv(process.env)
70
+
71
+ if (central) {
72
+ const userId = process.env.CONTEXT_USER_ID
73
+ const orgId = process.env.CONTEXT_ORG_ID
74
+ if (!userId || !orgId) {
75
+ throw new Error("central mode needs CONTEXT_USER_ID and CONTEXT_ORG_ID so results are ACL-filtered")
76
+ }
77
+ const svc = buildContextService(central)
78
+ return {
79
+ mode: "central",
80
+ userId,
81
+ orgId,
82
+ storage: central.qdrant.url,
83
+ vectorIndex: "Qdrant (hybrid + RRF)",
84
+ embeddings: "central embedding service",
85
+ extraction: "central ingestion pipeline",
86
+ // Ingestion in the central tier is normally via connectors - not exposed here.
87
+ query: (q) => svc.retrieval.searchWithFilters({ queries: [q], userId, orgId, limit: 10 }),
88
+ }
89
+ }
90
+
91
+ const ctx = personalContext()
92
+ const count = (sql: string) => (ctx.store.db.query(sql).get() as { c: number }).c
93
+ return {
94
+ mode: "local",
95
+ userId: ctx.userId,
96
+ orgId: ctx.orgId,
97
+ storage: personalContextPath(),
98
+ vectorIndex: ctx.store.vecEnabled ? "sqlite-vec ANN (in-process)" : "brute-force cosine",
99
+ embeddings: (process.env.CONTEXT_EMBEDDINGS ?? "auto").toLowerCase(),
100
+ extraction: process.env.CONTEXT_LLM_URL
101
+ ? `LLM typed-triples (${process.env.CONTEXT_LLM_MODEL || "default"} @ ${process.env.CONTEXT_LLM_URL})`
102
+ : "caller-supplied typed triples (the calling model passes entities+relations to context_ingest); " +
103
+ "deterministic fallback when none given",
104
+ query: (q) => ctx.searchWithGraph(q, ctx.userId, ctx.orgId), // vector + ACL + GraphRAG expansion
105
+ ask: (q) => ctx.ask(q, ctx.userId, ctx.orgId), // KGQA: exact answer from the typed graph
106
+ ingest: (doc) => ctx.authorizedIngest(ctx.userId, doc), // write-side authorization + ownership
107
+ graph: async () => {
108
+ const accessible = await ctx.graph.getAccessibleVirtualRecordIds({ userId: ctx.userId, orgId: ctx.orgId })
109
+ const recordIds = [...new Set(Object.values(accessible))]
110
+ return ctx.graph.getKnowledgeGraph(recordIds)
111
+ },
112
+ rebuild: () => ctx.rebuildGraph(),
113
+ graphQuery: {
114
+ neighbors: (name, relation) => ctx.graphQuery.neighbors(name, ctx.userId, ctx.orgId, relation),
115
+ path: (a, b) => ctx.graphQuery.pathBetween(a, b, ctx.userId, ctx.orgId),
116
+ impact: (name) => ctx.graphQuery.impactOf(name, ctx.userId, ctx.orgId),
117
+ central: (limit) => ctx.graphQuery.central(ctx.userId, ctx.orgId, limit),
118
+ communities: () => ctx.graphQuery.communities(ctx.userId, ctx.orgId),
119
+ cypher: () => ctx.graphQuery.toCypher(ctx.userId, ctx.orgId),
120
+ walk: (seeds) => ctx.graphQuery.walk(seeds, ctx.userId, ctx.orgId),
121
+ },
122
+ stats: async () => ({
123
+ records: count("SELECT count(*) c FROM nodes WHERE coll = 'records'"),
124
+ chunks: count("SELECT count(*) c FROM chunks"),
125
+ entities: count("SELECT count(*) c FROM nodes WHERE coll = 'entities'"),
126
+ relations: count("SELECT count(*) c FROM edges WHERE label = 'relates_to'"),
127
+ }),
128
+ }
129
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bun
2
+ // 100x Context - MCP server. Exposes the knowledge graph as standard MCP tools so
3
+ // ANY client (100xprompt, Claude Desktop, Cursor, IDEs) uses it via config alone,
4
+ // no code changes. Ships as one command; point it at the central office backend
5
+ // with env (see backend.ts) to share one org-wide graph with per-user ACL.
6
+ //
7
+ // Client config:
8
+ // "mcp": { "context": { "type": "local", "command": ["chitta"],
9
+ // "environment": { "CONTEXT_USER_ID": "alice", "CONTEXT_ORG_ID": "acme" } } }
10
+ //
11
+ // The 6 tools live one-per-module under ./tools; this file just wires the server:
12
+ // resolve the backend, build the (capability-gated) tool list, register ListTools
13
+ // from those schemas, dispatch CallTool through the name→handler map, and connect
14
+ // the stdio transport.
15
+
16
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js"
17
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
18
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
19
+ import { resolveBackend } from "./backend"
20
+ import { resolveTools } from "./tools"
21
+
22
+ const backend = resolveBackend()
23
+ const { schemas, dispatch } = resolveTools(backend)
24
+
25
+ const INSTRUCTIONS = [
26
+ "100x Context - a permission-aware knowledge graph + vector memory. It is the user's / organization's",
27
+ "long-term memory. Treat it as the source of truth for anything personal or organization-specific.",
28
+ "",
29
+ "WHEN TO USE EACH TOOL:",
30
+ "• context_ingest - Call WHENEVER the user states something durable worth remembering: facts, preferences,",
31
+ " people, projects, decisions, documents, or any 'remember that…/store…/note that…/I am/like/work on…'.",
32
+ " Don't ask permission for obvious saves - just store it. Not for transient chit-chat.",
33
+ " YOU ARE THE EXTRACTOR: you just read and understood this content, so ALWAYS pass the `entities` and",
34
+ " `relations` you identified alongside the text. Relations are subject→predicate→object with a SHORT",
35
+ " snake_case predicate (partners_with, acquired, works_at, deploys, located_in, ceo_of…). This stores precise",
36
+ " TYPED triples - no second model re-reads the text, and the graph can answer relational questions exactly.",
37
+ "• get_context - Call BEFORE answering ANY question that could touch the user's own notes, people, projects,",
38
+ " org knowledge, or prior statements ('who/what/when did I…', 'what do we know about…', 'remind me…').",
39
+ " Always check memory first instead of guessing. If it returns nothing, say so plainly.",
40
+ "• context_graph - Call when the user asks how things relate, for an overview, or 'what do you know about X' /",
41
+ " 'how are these connected' - it returns the concept map (entities + relationships).",
42
+ "• context_about - Call to discover this server's capabilities, current mode, storage, engines, and live stats",
43
+ " (e.g. when unsure what memory can do, or to report status).",
44
+ "",
45
+ "HOW IT WORKS (set expectations from this):",
46
+ "• Two stores behind one interface: a GRAPH (records, people, concepts + relationships + permissions) and a",
47
+ " VECTOR store (semantic search). get_context combines them: ACL-filter (what the user may see) → semantic",
48
+ " search over only those records → expand along the concept graph → return ranked, cited snippets.",
49
+ "• EVERYTHING IS PERMISSION-FILTERED. Results only ever contain what the asking user is authorized to see.",
50
+ " Never assume the user can access more than what comes back; never invent beyond the returned content.",
51
+ "• WRITES ARE AUTHORIZED TOO: roles (admin/editor/viewer) gate creation; only an owner or admin may delete/",
52
+ " modify a record; non-admins can't share outside their org/groups. If a write is denied, relay that plainly.",
53
+ "",
54
+ "TIER SYSTEM (set by config, not by you - tool behavior is identical either way):",
55
+ "• local (default): a single private SQLite store on this machine - personal memory, no servers.",
56
+ "• central-office: a shared organization-wide store (ArangoDB graph + Qdrant vectors). Everyone shares one",
57
+ " knowledge base, but each user sees only what their ACL permits. Identity comes from CONTEXT_USER_ID/ORG_ID.",
58
+ "",
59
+ "DEFAULT BEHAVIOR: prefer recall over guessing (get_context first); ingest durable facts proactively; cite",
60
+ "what you retrieve.",
61
+ ].join("\n")
62
+
63
+ const server = new Server(
64
+ { name: "100x-context", version: "0.1.0" },
65
+ { capabilities: { tools: {} }, instructions: INSTRUCTIONS },
66
+ )
67
+
68
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: schemas }))
69
+
70
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
71
+ const { name, arguments: args = {} } = req.params
72
+ try {
73
+ const handler = dispatch.get(name)
74
+ if (!handler) return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true }
75
+ return await handler(args as Record<string, unknown>, backend)
76
+ } catch (e) {
77
+ const msg = e instanceof Error && e.name === "AuthorizationError" ? `Not authorized: ${e.message}` : `error: ${String(e)}`
78
+ return { content: [{ type: "text", text: msg }], isError: true }
79
+ }
80
+ })
81
+
82
+ await server.connect(new StdioServerTransport())
83
+ console.error(`[100x-context] MCP server ready (mode=${backend.mode}, user=${backend.userId})`)
@@ -0,0 +1,69 @@
1
+ import { CodeExtractor } from "../../embedded/code-extractor"
2
+ import type { ContextBackend } from "../backend"
3
+ import type { ToolModule, ToolResult, ToolSchema } from "./types"
4
+
5
+ // The about/describe output lists every AVAILABLE tool with its description. The
6
+ // index injects the resolved tool list (after capability gating) so this stays in
7
+ // lockstep with what ListTools reports - identical to the old inline TOOLS loop.
8
+ let listedTools: ToolSchema[] = []
9
+ export function setAboutToolList(tools: ToolSchema[]): void {
10
+ listedTools = tools
11
+ }
12
+
13
+ const schema = {
14
+ name: "context_about",
15
+ description:
16
+ "Describe this context server: its purpose, current mode/identity/storage, the vector + knowledge-graph " +
17
+ "engines in use, live counts, and every tool with what it's for. Call this first to discover capabilities.",
18
+ inputSchema: { type: "object" as const, properties: {} },
19
+ }
20
+
21
+ // Self-describing capability report for discovery (the context_about tool).
22
+ async function describe(backend: ContextBackend): Promise<string> {
23
+ const lines: string[] = [
24
+ "# 100x Context - permission-aware knowledge graph + vector memory (MCP server)",
25
+ "",
26
+ "Stores PROSE or CODE as: a record node + permission edges (ACL) + vector chunks + an extracted graph.",
27
+ "Code files are parsed with tree-sitter into a real code graph (functions/classes + calls/imports/defines),",
28
+ "so the same graph queries (neighbors / path / impact / communities) work over code as well as notes.",
29
+ "Retrieval = ACL filter (who can see what) → semantic vector search (restricted to accessible records) →",
30
+ "GraphRAG expansion along concept links → ranked, cited snippets.",
31
+ "",
32
+ "The graph is BI-TEMPORAL and self-densifying: every relationship carries a weight that accumulates as it's",
33
+ "re-asserted (frequency≈confidence), provenance (which records asserted it), and validity intervals. New facts",
34
+ "MERGE into existing ones rather than duplicating; a newer single-valued fact (e.g. moved cities) SUPERSEDES the",
35
+ "old one non-destructively - the prior fact stays in history (marked expired) and never surfaces as current.",
36
+ "",
37
+ "## Current state",
38
+ `- mode: ${backend.mode} · user: ${backend.userId} · org: ${backend.orgId}`,
39
+ `- storage: ${backend.storage}`,
40
+ `- vector search: ${backend.vectorIndex}`,
41
+ `- embeddings: ${backend.embeddings}`,
42
+ `- knowledge extraction: ${backend.extraction}`,
43
+ `- code graph: tree-sitter AST over ${CodeExtractor.languages().length} languages (functions/classes + calls/imports/defines)`,
44
+ ]
45
+ if (backend.stats) {
46
+ const s = await backend.stats()
47
+ lines.push(`- contents: ${s.records} record(s), ${s.chunks} chunk(s), ${s.entities} concept(s), ${s.relations} relationship(s)`)
48
+ }
49
+ lines.push("", "## Tools")
50
+ for (const t of listedTools) lines.push(`- **${t.name}** - ${t.description}`)
51
+ lines.push(
52
+ "",
53
+ "## Modes",
54
+ "- personal (default): one private SQLite file, single owner - no ACL friction.",
55
+ "- TEAM / multi-user (embedded): point multiple clients at the SAME SQLite file (CONTEXT_DB) but give each",
56
+ " its own CONTEXT_USER_ID / CONTEXT_ORG_ID / CONTEXT_USER_ROLE / CONTEXT_USER_GROUPS. ONE shared graph",
57
+ " (entities are a common backbone), but every user sees ONLY their ACL slice - private to the owner, shared",
58
+ " with named groups (context_ingest `share`), or org-wide (`org_wide`). Edges are permission-filtered per",
59
+ " provenance, so a private relationship between two otherwise-visible entities never leaks.",
60
+ "- central-office: set CONTEXT_ARANGO_URL/QDRANT_URL/EMBED_URL/COLLECTION → server-backed org graph, same ACL.",
61
+ )
62
+ return lines.join("\n")
63
+ }
64
+
65
+ async function handler(_args: Record<string, unknown>, backend: ContextBackend): Promise<ToolResult> {
66
+ return { content: [{ type: "text", text: await describe(backend) }] }
67
+ }
68
+
69
+ export const contextAboutTool: ToolModule = { schema, handler }
@@ -0,0 +1,23 @@
1
+ import type { ContextBackend } from "../backend"
2
+ import type { ToolModule, ToolResult } from "./types"
3
+
4
+ const schema = {
5
+ name: "context_graph",
6
+ description:
7
+ "See how stored concepts connect. USE WHEN: the user asks for an overview, 'what do you know about X', " +
8
+ "'how are these related', or wants the map of their knowledge. Returns entities + relationships " +
9
+ "(permission-filtered). DON'T USE to fetch document text - that's get_context.",
10
+ inputSchema: { type: "object" as const, properties: {} },
11
+ }
12
+
13
+ async function handler(_args: Record<string, unknown>, backend: ContextBackend): Promise<ToolResult> {
14
+ const g = await backend.graph!()
15
+ const byId = new Map(g.entities.map((e) => [e.id, e.label]))
16
+ const lines = [
17
+ `Knowledge graph: ${g.entities.length} concept(s), ${g.relations.length} relationship(s).`,
18
+ ...g.relations.slice(0, 50).map((r) => ` ${byId.get(r.from) ?? r.from} ── ${byId.get(r.to) ?? r.to}`),
19
+ ]
20
+ return { content: [{ type: "text", text: lines.join("\n") }] }
21
+ }
22
+
23
+ export const contextGraphTool: ToolModule = { schema, handler, available: (b) => !!b.graph }
@@ -0,0 +1,88 @@
1
+ import type { ContextBackend } from "../backend"
2
+ import { slug, type ToolModule, type ToolResult } from "./types"
3
+
4
+ const schema = {
5
+ name: "context_ingest",
6
+ description:
7
+ "Remember new information durably. USE WHEN: the user states a fact, preference, person, project, " +
8
+ "decision, or document worth keeping, or says 'remember/store/note that…'. Don't ask permission for " +
9
+ "obvious saves - just store it. Writes a record + permission edges (ACL) + vector chunks + a concept " +
10
+ "graph. IMPORTANT: YOU already understood this content, so ALSO pass the `entities` and `relations` you " +
11
+ "extracted - they're stored as precise TYPED triples (no second model re-reads it). Relations should be " +
12
+ "subject→predicate→object with a SHORT snake_case predicate (e.g. partners_with, acquired, works_at, " +
13
+ "deploys, located_in). DON'T USE for transient chit-chat or one-off computations.",
14
+ inputSchema: {
15
+ type: "object" as const,
16
+ properties: {
17
+ content: { type: "string", description: "text to store" },
18
+ name: { type: "string", description: "short title" },
19
+ entities: {
20
+ type: "array",
21
+ description: "entities you identified in the content (people, orgs, products, concepts)",
22
+ items: {
23
+ type: "object",
24
+ properties: {
25
+ name: { type: "string", description: "entity name as written" },
26
+ type: { type: "string", description: "PERSON | ORG | PRODUCT | CONCEPT | PLACE | …" },
27
+ },
28
+ required: ["name"],
29
+ },
30
+ },
31
+ relations: {
32
+ type: "array",
33
+ description: "typed relationships between entities (subject → predicate → object)",
34
+ items: {
35
+ type: "object",
36
+ properties: {
37
+ from: { type: "string", description: "subject entity name" },
38
+ to: { type: "string", description: "object entity name" },
39
+ type: { type: "string", description: "SHORT snake_case predicate, e.g. partners_with / acquired / works_at" },
40
+ confidence: { type: "number", description: "0..1 how certain (optional)" },
41
+ },
42
+ required: ["from", "to", "type"],
43
+ },
44
+ },
45
+ share: {
46
+ type: "array",
47
+ description: "principal ids (users or groups) to ALSO grant read access - default is private to you",
48
+ items: { type: "string" },
49
+ },
50
+ org_wide: { type: "boolean", description: "share with everyone in your org (default false)" },
51
+ },
52
+ required: ["content", "name"],
53
+ },
54
+ }
55
+
56
+ async function handler(args: Record<string, unknown>, backend: ContextBackend): Promise<ToolResult> {
57
+ const a = args as {
58
+ content: string
59
+ name: string
60
+ entities?: Array<{ name: string; type?: string }>
61
+ relations?: Array<{ from: string; to: string; type: string; confidence?: number }>
62
+ share?: string[]
63
+ org_wide?: boolean
64
+ }
65
+ // owner is always added by authorizedIngest; `share` widens to named principals/
66
+ // groups; `org_wide` shares with everyone in the org. The authorizer rejects any
67
+ // grant outside the caller's scope (no over-sharing).
68
+ const principals = [...new Set([backend.userId, ...(a.share ?? [])])]
69
+ const out = await backend.ingest!({
70
+ recordId: `${slug(a.name)}-${Date.now().toString(36)}`,
71
+ orgId: backend.orgId,
72
+ recordName: a.name,
73
+ text: a.content,
74
+ permittedPrincipals: principals,
75
+ shareWithOrg: a.org_wide ? backend.orgId : undefined,
76
+ entities: a.entities,
77
+ relations: a.relations,
78
+ })
79
+ const typed = a.relations?.length ? `, ${a.relations.length} typed relation(s)` : ""
80
+ const vis = a.org_wide ? "org-wide" : a.share?.length ? `shared with ${a.share.join(", ")}` : "private"
81
+ return {
82
+ content: [
83
+ { type: "text", text: `Stored "${a.name}" (${out.chunks} chunk(s), ${out.entities} concept(s)${typed}; ${vis}) as ${out.recordId}.` },
84
+ ],
85
+ }
86
+ }
87
+
88
+ export const contextIngestTool: ToolModule = { schema, handler, available: (b) => !!b.ingest }